diff --git a/app/main.py b/app/main.py index 004dfcf..b79f346 100644 --- a/app/main.py +++ b/app/main.py @@ -11,6 +11,45 @@ from sync_link import SliceSynchronizer +##################################################################### + +''' +Syncronized zooming and updated zomm level +''' +class ZoomSync: + def __init__(self, slice1, slice2, scale_factor, slice1_caption, slice2_caption): + self.synchronizer = SliceSynchronizer(slice1, slice2, scale_factor) # Synchronizer for slices + self.synchronizer.register_callback(self.on_zoom_update) + + # Store reference to captions for updates + self.slice1_caption = slice1_caption + self.slice2_caption = slice2_caption + + def update_caption(self, caption, label, zoom_level): + """ + Helper method to update captions dynamically. + """ + caption.object = f""" +
+

{label}     Zoom Level: {round(zoom_level, 2)} %

+
+ """ + + def on_zoom_update(self, zoom_level_fig1, zoom_level_fig2): + """ + Callback function to update zoom levels in captions. + """ + print(f"Updated Zoom Levels - Fig1: {zoom_level_fig1}, Fig2: {zoom_level_fig2}") + self.update_caption(self.slice1_caption, slice1.image_type.value, zoom_level_fig1) + self.update_caption(self.slice2_caption, slice2.image_type.value, zoom_level_fig2) + + def run(self): + """ + Run the application. + """ + print("Application is running...") + + ##################################################################### def get_file_path(): @@ -34,6 +73,9 @@ def get_file_path(): logger.info(f"Command Line Args: {cmd_args}") return cmd_args + +##################################################################### + if __name__.startswith('bokeh'): pn.extension( "ipywidgets", @@ -52,10 +94,21 @@ def get_file_path(): # Show options for both slices # "resolution" max , "view_dependent" is yes; set Default - show_options = { - "top": [["scene", "resolution","view_dependent"]], # color_mapper_type, Pallete, field, range mode max min are removed - "bottom": [["request", "response"]], - } + ''' + show_options={ + "top": [ + [ "menu_button","scene", "timestep", "timestep_delta", "play_sec","play_button","palette", "color_mapper_type","view_dependent", "resolution", "num_refinements", "show_probe"], + ["field","direction", "offset", "range_mode", "range_min", "range_max"] + + ], + "bottom": [ + ["request","response", "zoom_level", "image_type"] + ] + } + + ''' + + show_options = {} args = get_file_path() logger.info(f"Final Args Being Used: {args}") @@ -64,10 +117,10 @@ def get_file_path(): logger.info(f"Using two-file sync view mode with files: {args[0]} and {args[1]}") # Load for Sync Slices view slice1 = Slice(ViewChoice="SYNC_VIEW") - slice1.load(args[0]) + slice1.load(sys.argv[1]) slice2 = Slice(ViewChoice="SYNC_VIEW") - slice2.load(args[1]) + slice2.load(sys.argv[2]) # Compute scale factor @@ -75,6 +128,15 @@ def get_file_path(): box2 = slice2.db.getPhysicBox() scale_factor = box2[0][1] / box1[0][1] # Scaling based on x-axis + + if scale_factor < 1: # reverse both images + slice1.image_type.value = "Super-Resolution Image" + slice2.image_type.value = "Original Image" + else: + slice1.image_type.value = "Original Image" + slice2.image_type.value = "Super-Resolution Image" + + slice1.setShowOptions(show_options) slice2.setShowOptions(show_options) @@ -86,12 +148,11 @@ def get_file_path(): f"""
-

Original Image  ({int(box1[0][1])} x {int(box1[1][1])})

+ justify-content: top; + align-items: top; + text-align: top; + height: 5;"> +

Original Image

""", sizing_mode="stretch_width", # Ensures full width for centering @@ -101,34 +162,38 @@ def get_file_path(): f"""
-

Super-Resolution Image  ({int(box2[0][1])} x {int(box2[1][1])})    Scale Factor: {scale_factor:.2f}

+ justify-content: top; + align-items: top; + text-align: top; + height: 5;"> +

Super-Resolution Image

""", sizing_mode="stretch_width", # Ensures full width for centering ) - # Synchronizer for slices - synchronizer = SliceSynchronizer(slice1, slice2, scale_factor) + # Initialize the ZoomSync class + zoom_sync = ZoomSync(slice1, slice2, scale_factor, slice1_caption,slice2_caption) + zoom_sync.run() # Add the buttons to the layout - layout = pn.Row( - pn.Column( - slice1_caption, - slice1.getMainLayout(), + layout = pn.Column( + pn.Row( + pn.Column( + slice1.getMainLayout(), + sizing_mode="stretch_both", + ), + pn.Column( + slice2.getMainLayout(), + sizing_mode="stretch_both", + ), sizing_mode="stretch_both", ), - pn.Column( - slice2_caption, - slice2.getMainLayout(), + pn.Row( + slice1_caption, slice2_caption, sizing_mode="stretch_both", - ), - sizing_mode="stretch_both", + ) ) layout.servable() else: diff --git a/app/sync_link.py b/app/sync_link.py index 15cb797..9e83b9c 100644 --- a/app/sync_link.py +++ b/app/sync_link.py @@ -1,5 +1,6 @@ from bokeh.models import CustomJS from threading import Timer +import math class SliceSynchronizer: @@ -7,8 +8,14 @@ def __init__(self, slice1, slice2, scale_factor=4): self.slice1 = slice1 self.slice2 = slice2 self.scale_factor = scale_factor + self.full_width = None self.debounce_timer = None # Timer to debounce refresh calls self.debounce_delay = 0.1 # Delay in seconds for debounce + self.update_callbacks = [] # Callback list for external + + # Store zoom levels + self.zoom_level_1 = 1.0 #intial value + self.zoom_level_2 = 1.0 # Link ranges with JavaScript callbacks self.link_ranges() @@ -64,6 +71,11 @@ def trigger_refresh(): self.slice1.refresh("DebouncedSyncFromSlice2") self.slice2.refresh("DebouncedSyncFromSlice1") + self.zoom_level_1= self.update_zoom_level(fig1) + self.zoom_level_2= self.update_zoom_level(fig2) + + self.notify_subscribers() + def debounce_refresh(attr, old, new): """Debounce refresh calls.""" if self.debounce_timer: @@ -82,4 +94,40 @@ def debounce_refresh(attr, old, new): fig2.x_range.on_change('start', debounce_refresh) fig2.x_range.on_change('end', debounce_refresh) fig2.y_range.on_change('start', debounce_refresh) - fig2.y_range.on_change('end', debounce_refresh) \ No newline at end of file + fig2.y_range.on_change('end', debounce_refresh) + + + #Zoom level Calculation + def update_zoom_level(self, fig): + """ + Calculate and update the zoom level dynamically. + """ + # Get current viewport size + image_width = fig.x_range.end - fig.x_range.start + + # Prevent division by zero + image_width = max(1, image_width) + + viewport_width = fig.inner_width + + zoomLevel = (viewport_width / math.ceil(image_width)) * 100 + + print(f"zoom: viewport width={viewport_width}, image width={math.ceil(image_width)}, zoomLevel={zoomLevel}%") + + + return (zoomLevel) # Return the zoom level + + + def register_callback(self, callback): + """ + Register a callback function to be notified when zoom levels are updated. + The callback function should accept two arguments: (zoom_level_fig1, zoom_level_fig2). + """ + self.update_callbacks.append(callback) + + def notify_subscribers(self): + """ + Notify all registered subscribers with the updated zoom levels. + """ + for callback in self.update_callbacks: + callback(self.zoom_level_1, self.zoom_level_2) \ No newline at end of file diff --git a/src/openvisuspy/slice.py b/src/openvisuspy/slice.py index 28e784f..559f7ab 100644 --- a/src/openvisuspy/slice.py +++ b/src/openvisuspy/slice.py @@ -271,7 +271,7 @@ class Slice(param.Parameterized): ], "bottom": [ - ["request","response"] + ["request","response","image_type"] ] } @@ -577,6 +577,8 @@ def onColorMapperTypeChange(evt): self.request = pn.widgets.TextInput(name="", sizing_mode='stretch_width', disabled=False) self.response = pn.widgets.TextInput(name="", sizing_mode='stretch_width', disabled=False) + self.image_type = pn.widgets.TextInput(name="", sizing_mode='stretch_width', disabled=False) # Sync-View Image Title + self.file_name_input = pn.widgets.TextInput(name="Numpy_File", value='test', placeholder='Numpy File Name to save') self.canvas = Canvas(self.id, self.view_choice)