Skip to content

Commit

Permalink
Merge pull request #26 from sci-visus/feature-syncview-boxselect
Browse files Browse the repository at this point in the history
changed main file for sync view and changed show_details file for box…
  • Loading branch information
scrgiorgio authored Nov 13, 2024
2 parents 80c5ce9 + 4caafa2 commit fef5b06
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 102 deletions.
216 changes: 134 additions & 82 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,90 +4,142 @@
import base64
import json
import panel as pn
from openvisuspy import SetupLogger, Slice, ProbeTool, cbool
import time

class SliceSynchronizer:
def __init__(self, slice1, slice2, throttle_delay=0.05):
self.slice1 = slice1
self.slice2 = slice2
self.sync_in_progress = False
self.last_sync_time = 0
self.throttle_delay = throttle_delay

def throttle(self, func, *args, **kwargs):
"""Throttle function calls to limit frequency."""
current_time = time.time()
if current_time - self.last_sync_time >= self.throttle_delay:
self.last_sync_time = current_time
func(*args, **kwargs)

def sync_slices(self, box1=None, box2=None, is_reverse=False):
"""Synchronize viewports between two slices, with an option to reverse the direction."""
if self.sync_in_progress:
return
self.sync_in_progress = True
try:
# Set default boxes if not provided
if box1 is None:
box1 = self.slice1.db.getPhysicBox()
if box2 is None:
box2 = self.slice2.db.getPhysicBox()

# Determine source and target based on is_reverse flag
if is_reverse:
(src_box, tgt_box) = (box2, box1)
(src_slice, tgt_slice) = (self.slice2, self.slice1)
else:
(src_box, tgt_box) = (box1, box2)
(src_slice, tgt_slice) = (self.slice1, self.slice2)

# Unpack source and target boxes
(a, b), (c, d) = src_box
(A, B), (C, D) = tgt_box

# Get viewport from the source slice
x, y, w, h = src_slice.canvas.getViewport()

# Map coordinates from source to target
x1, x2 = [A + ((value - a) / (b - a)) * (B - A) for value in [x, x + w]]
y1, y2 = [C + ((value - c) / (d - c)) * (D - C) for value in [y, y + h]]

# Set the target slice's viewport and refresh
tgt_slice.canvas.setViewport([x1, y1, x2 - x1, y2 - y1])
tgt_slice.refresh("SyncSlices")
finally:
self.sync_in_progress = False

def update_slice2(self, attr, old, new):
"""Throttle and synchronize slice1 -> slice2."""
self.throttle(self.sync_slices, is_reverse=False)

def update_slice1(self, attr, old, new):
"""Throttle and synchronize slice2 -> slice1 (reverse direction)."""
self.throttle(self.sync_slices, is_reverse=True)

from openvisuspy import SetupLogger, Slice, ProbeTool,cbool

# ////////////////////////////////////////////////////////////////////////////
def SyncSlices(slice1, slice2, box1=None, box2=None):

if box1 is None:
box1=slice1.db.getPhysicBox()

if box2 is None:
box2=slice2.db.getPhysicBox()

# map box1 to box2
(a, b), (c, d)=box1
(A, B), (C, D)=box2

x, y, w, h = slice1.canvas.getViewport()
x1, x2 = [A + ((value - a) / (b - a)) * (B - A) for value in [x, x + w]]
y1, y2 = [C + ((value - c) / (d - c)) * (D - C) for value in [y, y + h]]
slice2.canvas.setViewport([x1, y1, x2 - x1, y2 - y1])
slice2.refresh("SyncSlices")

# ////////////////////////////////////////////////////////////////////////////
if __name__.startswith('bokeh'):

pn.extension(
"ipywidgets",
"floatpanel",
"codeeditor",
log_level="DEBUG",
notifications=True,
sizing_mode="stretch_width"
)

query_params = {k: v for k,v in pn.state.location.query_params.items()}

log_filename = os.environ.get("OPENVISUSPY_DASHBOARDS_LOG_FILENAME", "/tmp/openvisuspy-dashboards.log")
logger = SetupLogger(log_filename=log_filename, logging_level=logging.DEBUG)

# sync view
if len(sys.argv[1:])==2:

from openvisuspy.utils import SafeCallback
from panel import Column, Row

slice1 = Slice();slice1.load(sys.argv[1])
slice2 = Slice();slice2.load(sys.argv[2])

show_options = {
"top": [
[
"palette",
"color_mapper_type",
"resolution",
"num_refinements",
"field",
"range_mode",
"range_min",
"range_max"
],
],
# "bottom": [["request", "response"]],
}
slice1.setShowOptions(show_options)
slice2.setShowOptions(show_options)
slice1.scene_body.param.watch( SafeCallback(lambda evt: SyncSlices(slice1,slice2)), "value", onlychanged=True, queued=True)
main_layout = pn.Row(slice1.getMainLayout(), slice2.getMainLayout())
main_layout.servable()

else:

slice = Slice()
slice.load(sys.argv[1])

# load a whole scene
if "load" in query_params:
body = json.loads(base64.b64decode(query_params['load']).decode("utf-8"))
slice.setBody(body)

# select from list of choices
elif "dataset" in query_params:
scene_name = query_params["dataset"]
slice.scene.value = scene_name

main_layout = slice.getMainLayout()
main_layout.servable()

pn.extension(
"ipywidgets",
"floatpanel",
"codeeditor",
log_level="DEBUG",
notifications=True,
)

query_params = {k: v for k, v in pn.state.location.query_params.items()}

log_filename = os.environ.get("OPENVISUSPY_DASHBOARDS_LOG_FILENAME", "/tmp/openvisuspy-dashboards.log")
logger = SetupLogger(log_filename=log_filename, logging_level=logging.DEBUG)

# Sync view
if len(sys.argv[1:]) == 2:

slice1 = Slice()
slice1.load(sys.argv[1])
slice2 = Slice()
slice2.load(sys.argv[2])

show_options = {
"top": [
[
"palette",
"color_mapper_type",
"resolution",
"num_refinements",
"field",
"range_mode",
"range_min",
"range_max"
],
],
}
slice1.setShowOptions(show_options)
slice2.setShowOptions(show_options)

# Initialize the synchronizer
synchronizer = SliceSynchronizer(slice1, slice2, throttle_delay = 0.05)

# Watch for viewport changes in slice1 and slice2 using on_change
slice1.canvas.fig.x_range.on_change('start', synchronizer.update_slice2)
slice1.canvas.fig.x_range.on_change('end', synchronizer.update_slice2)
slice1.canvas.fig.y_range.on_change('start', synchronizer.update_slice2)
slice1.canvas.fig.y_range.on_change('end', synchronizer.update_slice2)

slice2.canvas.fig.x_range.on_change('start', synchronizer.update_slice1)
slice2.canvas.fig.x_range.on_change('end', synchronizer.update_slice1)
slice2.canvas.fig.y_range.on_change('start', synchronizer.update_slice1)
slice2.canvas.fig.y_range.on_change('end', synchronizer.update_slice1)

# Layout for both slices
main_layout = pn.Row(slice1.getMainLayout(), slice2.getMainLayout())
main_layout.servable()

else:
slice = Slice()
slice.load(sys.argv[1])

# Load a whole scene
if "load" in query_params:
body = json.loads(base64.b64decode(query_params['load']).decode("utf-8"))
slice.setBody(body)

# Select from list of choices
elif "dataset" in query_params:
scene_name = query_params["dataset"]
slice.scene.value = scene_name

main_layout = slice.getMainLayout()
main_layout.servable()
64 changes: 44 additions & 20 deletions src/openvisuspy/show_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from bokeh.models import LinearColorMapper
import bokeh.models
import logging
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.pyplot as plt
from bokeh.models import ColumnDataSource, ColorBar, LinearColorMapper
from .utils import *

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -106,7 +110,7 @@ def ShowDetails(self,x,y,w,h):
pdim=self.getPointDim()

# todo for 2D dataset
assert(pdim==3)
# assert(pdim==3)

z=int(self.offset.value)
logic_box=self.toLogic([x,y,w,h])
Expand Down Expand Up @@ -148,29 +152,49 @@ def ShowDetails(self,x,y,w,h):
self.range_min.value = min(self.range_min.value, self.vmin)
self.range_max.value = max(self.range_max.value, self.vmax)
logger.info(f"Updating range with selected area vmin={self.vmin} vmax={self.vmax}")

p = figure(x_range=(self.selected_physic_box[0][0], self.selected_physic_box[0][1]), y_range=(self.selected_physic_box[1][0], self.selected_physic_box[1][1]))
fig, ax = plt.subplots()

p1 = figure(x_range=(0,100), y_range=(0,100))
palette_name = self.palette.value_name if self.palette.value_name.endswith("256") else "Turbo256"

mapper = LinearColorMapper(palette=palette_name, low=np.min(self.detailed_data), high=np.max(self.detailed_data))

# Flip data to match imshow orientation
data_flipped = data
source = bokeh.models.ColumnDataSource(data=dict(image=[data_flipped]))
data_flipped = data

print(type(self.selected_physic_box[0][1]))
dw = abs(self.selected_physic_box[0][1] -self.selected_physic_box[0][0])

dh = abs(self.selected_physic_box[1][1] - self.selected_physic_box[1][0])
p.image(image='image', x=self.selected_physic_box[0][0], y=self.selected_physic_box[1][0], dw=dw, dh=dh, color_mapper=mapper, source=source)
color_bar = bokeh.models.ColorBar(color_mapper=mapper, label_standoff=12, location=(0,0))
p.add_layout(color_bar, 'right')
p.xaxis.axis_label = "X"
p.yaxis.axis_label = "Y"

self.showDialog(
pn.Column(
self.file_name_input,
pn.Row(save_numpy_button,download_script_button),
pn.Row(apply_avg_min_colormap_button,apply_avg_max_colormap_button,add_range_button,apply_colormap_button),
pn.Row(pn.pane.Bokeh(p,sizing_mode="stretch_both")),
sizing_mode="stretch_both"
)
, width=900, height=800, name=f"Palette: {palette_name} Min: {self.vmin}, Max: {self.vmax}")
x_min, x_max = int(self.selected_physic_box[0][0]), int(self.selected_physic_box[0][1])
y_min, y_max = int(self.selected_physic_box[1][0]), int(self.selected_physic_box[1][1])

#fig, ax = plt.subplots(figsize=(14, 20))
fig, ax = plt.subplots(figsize=(4, 4))
im = ax.imshow(data_flipped, cmap='turbo',extent=[x_min, x_max, y_min, y_max], aspect='auto')
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1)
cbar = plt.colorbar(im, cax=cax)

ax.set_xlim(self.selected_physic_box[0][0], self.selected_physic_box[0][1])
ax.set_ylim(self.selected_physic_box[1][1], self.selected_physic_box[1][0])

ax.set_xlabel("X")
ax.set_ylabel("Y")
plt.tight_layout()

dialog_layout = pn.Column(
self.file_name_input,
pn.Row(save_numpy_button, download_script_button),
pn.Row(pn.pane.Matplotlib(fig), pn.Column(
pn.pane.Markdown(f"#### Palette Used: {palette_name}"),
pn.pane.Markdown(f"#### New Min/Max Found.."),
pn.pane.Markdown(f"#### Min: {self.vmin}, Max: {self.vmax}"),
pn.Row(apply_avg_min_colormap_button, apply_avg_max_colormap_button),
add_range_button,
apply_colormap_button
)),
sizing_mode="stretch_both"
)

self.showDialog(dialog_layout, width=500, height=600, name="Details")

0 comments on commit fef5b06

Please sign in to comment.