From 801735f3ef4296ffc292f1c3c5ba2ac7ec380b94 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Fri, 26 Apr 2024 19:11:23 +0000 Subject: [PATCH 01/23] fix: slice by index instead of value to allow slicing time coord --- pan3d/dataset_builder.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pan3d/dataset_builder.py b/pan3d/dataset_builder.py index a7e459d8..7bdb1b62 100644 --- a/pan3d/dataset_builder.py +++ b/pan3d/dataset_builder.py @@ -1,5 +1,6 @@ import os import json +import pandas import pyvista import xarray @@ -335,17 +336,41 @@ def slicing(self, slicing: Dict[str, List]) -> None: raise ValueError( "Values in slicing must be lists of length 3 ([start, stop, step])." ) - acceptable_coords = self.dataset[self.data_array_name].coords + da = self.dataset[self.data_array_name] + acceptable_coords = da.dims if key not in acceptable_coords: raise ValueError( f"Key {key} not found in data array. Must be one of {list(acceptable_coords.keys())}." ) - key_coord = acceptable_coords[key] + key_coord = da[key] - if value[2] > key_coord.size: + step = value[2] + if step > key_coord.size: raise ValueError( f"Value {value} not applicable for Key {key}. Step value must be <= {key_coord.size}." ) + + start_value = value[0] + end_value = value[1] + if key_coord.dtype.kind in ['O', 'M']: + start_value = pandas.to_datetime(start_value) + end_value = pandas.to_datetime(end_value) + elif key_coord.dtype.kind in ['m']: + start_value = pandas.to_timedelta(start_value).total_seconds() + end_value = pandas.to_timedelta(end_value).total_seconds() + start_index = None + end_index = None + for i, c in enumerate(key_coord.to_series()): + if key_coord.dtype.kind in ['O', 'M']: + c = pandas.to_datetime(str(c)) + elif key_coord.dtype.kind in ['m']: + c = pandas.to_timedelta(c).total_seconds() + if c >= start_value and start_index is None: + start_index = i + if c <= end_value: + end_index = i + slicing[key] = [start_index, end_index, step] + self._algorithm.slicing = slicing if self._viewer: self._viewer._data_slicing_changed() From 73653e9c3a0c5d9d185e0b103b54e88c9bc6f330 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Wed, 1 May 2024 13:36:13 +0000 Subject: [PATCH 02/23] feat: Add BoundsConfigure component within render area --- pan3d/dataset_builder.py | 47 +++++---- pan3d/dataset_viewer.py | 158 ++++++++++++------------------- pan3d/ui/__init__.py | 2 + pan3d/ui/bounds_configure.py | 51 ++++++++++ pan3d/ui/coordinate_configure.py | 47 +++++---- 5 files changed, 161 insertions(+), 144 deletions(-) create mode 100644 pan3d/ui/bounds_configure.py diff --git a/pan3d/dataset_builder.py b/pan3d/dataset_builder.py index 7bdb1b62..7a3b36b4 100644 --- a/pan3d/dataset_builder.py +++ b/pan3d/dataset_builder.py @@ -1,4 +1,5 @@ import os +import math import json import pandas import pyvista @@ -20,6 +21,7 @@ def __init__( server: Any = None, viewer: bool = False, catalogs: List[str] = [], + resolution: int = 2 ** 7, ) -> None: """Create an instance of the DatasetBuilder class. @@ -33,6 +35,7 @@ def __init__( self._dataset = None self._dataset_info = None self._da_name = None + self._resolution = resolution self._server = server self._catalogs = catalogs @@ -172,6 +175,7 @@ def data_array_name(self, data_array_name: Optional[str]) -> None: self._viewer._data_array_changed() self._viewer._mesh_changed() self._auto_select_coordinates() + self._auto_select_slicing() @property def data_array(self) -> Optional[xarray.DataArray]: @@ -350,27 +354,6 @@ def slicing(self, slicing: Dict[str, List]) -> None: f"Value {value} not applicable for Key {key}. Step value must be <= {key_coord.size}." ) - start_value = value[0] - end_value = value[1] - if key_coord.dtype.kind in ['O', 'M']: - start_value = pandas.to_datetime(start_value) - end_value = pandas.to_datetime(end_value) - elif key_coord.dtype.kind in ['m']: - start_value = pandas.to_timedelta(start_value).total_seconds() - end_value = pandas.to_timedelta(end_value).total_seconds() - start_index = None - end_index = None - for i, c in enumerate(key_coord.to_series()): - if key_coord.dtype.kind in ['O', 'M']: - c = pandas.to_datetime(str(c)) - elif key_coord.dtype.kind in ['m']: - c = pandas.to_timedelta(c).total_seconds() - if c >= start_value and start_index is None: - start_index = i - if c <= end_value: - end_index = i - slicing[key] = [start_index, end_index, step] - self._algorithm.slicing = slicing if self._viewer: self._viewer._data_slicing_changed() @@ -436,6 +419,9 @@ def _auto_select_coordinates(self) -> None: if self.dataset is not None and self.data_array_name is not None: da = self.dataset[self.data_array_name] assigned_coords = [] + unassigned_axes = [ + a for a in ["x", "y", "z", "t"] if getattr(self, a) is None + ] # Prioritize assignment by known names for coord_name in da.dims: name = coord_name.lower() @@ -447,18 +433,29 @@ def _auto_select_coordinates(self) -> None: if (len(accepted) == 1 and accepted == name) or (len(accepted) > 1 and accepted in name) ] - if len(name_match) > 0: + if len(name_match) > 0 and coord_name in unassigned_axes: setattr(self, axis, coord_name) assigned_coords.append(coord_name) # Then assign any remaining by index - unassigned_axes = [ - a for a in ["x", "y", "z", "t"] if getattr(self, a) is None - ] unassigned_coords = [d for d in da.dims if d not in assigned_coords] for i, d in enumerate(unassigned_coords): if i < len(unassigned_axes): setattr(self, unassigned_axes[i], d) + def _auto_select_slicing(self, bounds: Optional[Dict] = None) -> None: + """Automatically select slicing for selected data array.""" + if not bounds: + da = self.dataset[self.data_array_name] + bounds = { + k: [0, da[k].size] + for k in da.dims + } + self.slicing = { + k: [v[0], v[1], math.ceil((v[1] - v[0]) / self._resolution)] + for k, v in bounds.items() + } + + # ----------------------------------------------------- # Config logic # ----------------------------------------------------- diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index a72ab704..82f337dc 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -18,7 +18,7 @@ from pan3d import catalogs as pan3d_catalogs from pan3d.dataset_builder import DatasetBuilder -from pan3d.ui import AxisDrawer, MainDrawer, Toolbar, RenderOptions +from pan3d.ui import AxisDrawer, MainDrawer, Toolbar, RenderOptions, BoundsConfigure from pan3d.utils import ( initial_state, has_gpu_rendering, @@ -58,6 +58,7 @@ def __init__( self.plotter = geovista.GeoPlotter(off_screen=True, notebook=False) self.plotter.set_background("lightgrey") + self.reset_camera = True self.plot_view = None self.actor = None self.ctrl.get_plotter = lambda: self.plotter @@ -137,6 +138,9 @@ def ui(self) -> VAppLayout: with html.Div( v_if=("da_active",), style="height: 100%; position: relative" ): + BoundsConfigure( + coordinate_change_bounds_function=self._coordinate_change_bounds, + ) RenderOptions() with pyvista.trame.ui.plotter_ui( self.ctrl.get_plotter(), @@ -232,31 +236,28 @@ def _coordinate_select_axis( self.state[current_axis] = None if new_axis and new_axis != "undefined": self.state[new_axis] = coordinate_name + self.reset_camera = True def _coordinate_change_slice(self, coordinate_name, slice_attribute_name, value): - try: - value = float(value) - coordinate_matches = [ - (index, coordinate) - for index, coordinate in enumerate(self.state.da_coordinates) - if coordinate["name"] == coordinate_name - ] - if len(coordinate_matches) > 0: - coord_i, coordinate = coordinate_matches[0] - if slice_attribute_name == "step": - if value > 0 and value < coordinate["size"]: - coordinate[slice_attribute_name] = value - else: - if ( - value >= coordinate["range"][0] - and value <= coordinate["range"][1] - ): - coordinate[slice_attribute_name] = value + value = float(value) + for coord in self.state.da_coordinates: + if coord['name'] == coordinate_name: + bounds = coord.get('bounds') + if slice_attribute_name == 'start': + bounds[0] = value + elif slice_attribute_name == 'stop': + bounds[1] = value + elif slice_attribute_name == 'step': + coord.update(dict(step=value)) + coord.update(dict(bounds=bounds)) + self.state.dirty("da_coordinates") + - self.state.da_coordinates[coord_i] = coordinate + def _coordinate_change_bounds(self, coordinate_name, bounds): + for coord in self.state.da_coordinates: + if coord['name'] == coordinate_name: + coord.update(dict(bounds=bounds)) self.state.dirty("da_coordinates") - except Exception: - pass def _coordinate_toggle_expansion(self, coordinate_name): if coordinate_name in self.state.ui_expanded_coordinates: @@ -275,6 +276,7 @@ def submit(): self.builder.import_config(json.loads(file_content.decode())) self._mesh_changed() + self.reset_camera = True self.run_as_async( submit, loading_state="ui_import_loading", unapplied_changes_state=None @@ -379,10 +381,12 @@ def plot_mesh(self) -> None: mesh, **args, ) - if len(self.builder.data_array.shape) > 2: - self.plotter.view_isometric() - elif not self.state.render_cartographic: - self.plotter.view_xy() + if self.reset_camera: + if len(self.builder.data_array.shape) > 2: + self.plotter.view_vector([1, 1, -1], [0, 1, 0]) + elif not self.state.render_cartographic: + self.plotter.view_xy() + self.reset_camera = False if self.plot_view: self.ctrl.push_camera() @@ -475,72 +479,42 @@ def _data_array_changed(self) -> None: da_name = self.builder.data_array_name self.state.da_coordinates = [] self.state.ui_expanded_coordinates = [] + self.reset_camera = True if dataset is None or da_name is None: return da = dataset[da_name] - if len(da.dims) > 0 and self._ui is not None: - self.state.ui_axis_drawer = True for key in da.dims: - current_coord = da.coords[key] - d = current_coord.dtype - numeric = True - array_min = current_coord.values.min() - array_max = current_coord.values.max() - - # make content serializable by its type - if d.kind in ["O", "M"]: # is datetime - if not hasattr(array_min, "strftime"): - array_min = pandas.to_datetime(array_min) - if not hasattr(array_max, "strftime"): - array_max = pandas.to_datetime(array_max) - array_min = array_min.strftime("%b %d %Y %H:%M") - array_max = array_max.strftime("%b %d %Y %H:%M") - numeric = False - elif d.kind in ["m"]: # is timedelta - if not hasattr(array_min, "total_seconds"): - array_min = pandas.to_timedelta(array_min) - if not hasattr(array_max, "total_seconds"): - array_max = pandas.to_timedelta(array_max) - array_min = array_min.total_seconds() - array_max = array_max.total_seconds() - elif d.kind in ["i", "u"]: - array_min = int(array_min) - array_max = int(array_max) - elif d.kind in ["f", "c"]: - array_min = round(float(array_min), 2) - array_max = round(float(array_max), 2) - - coord_attrs = [ - {"key": str(k), "value": str(v)} - for k, v in da.coords[key].attrs.items() - ] - coord_attrs.append({"key": "dtype", "value": str(da.coords[key].dtype)}) - coord_attrs.append({"key": "length", "value": int(da.coords[key].size)}) - coord_attrs.append( - { - "key": "range", - "value": [array_min, array_max], - } - ) if key not in [c["name"] for c in self.state.da_coordinates]: - coord_info = { + current_coord = da.coords[key] + values = current_coord.values + size = current_coord.size + coord_range = [ + str(round(v)) if isinstance(v, float) else str(v) + for v in [values.item(0), values.item(size-1)] + ] + dtype = current_coord.dtype + + coord_attrs = [ + {"key": str(k), "value": str(v)} + for k, v in da.coords[key].attrs.items() + ] + coord_attrs.append({"key": "dtype", "value": str(dtype)}) + coord_attrs.append({"key": "length", "value": int(size)}) + coord_attrs.append({"key": "range", "value": coord_range}) + bounds = [0, size - 1] + if self.builder.slicing: + slicing = self.builder.slicing.get(key) + if slicing: + bounds = [values.item(slicing[0]), values.item(slicing[1])] + self.state.da_coordinates.append({ "name": key, - "numeric": numeric, "attrs": coord_attrs, - "size": da.coords[key].size, - "range": [array_min, array_max], - } - coord_slicing = { - "start": array_min, - "stop": array_max, + "labels": [str(round(v)) if isinstance(v, float) else str(v) for v in values], + "full_bounds": bounds, + "bounds": bounds, "step": 1, - } - if self.builder.slicing and self.builder.slicing.get(key): - coord_slicing = dict( - zip(["start", "stop", "step"], self.builder.slicing.get(key)) - ) - self.state.da_coordinates.append(dict(**coord_info, **coord_slicing)) + }) self.state.dirty("da_coordinates") self.plotter.clear() @@ -552,7 +526,10 @@ def _data_slicing_changed(self) -> None: for coord in self.state.da_coordinates: slicing = self.builder.slicing.get(coord["name"]) if slicing: - coord.update(dict(zip(["start", "stop", "step"], slicing))) + bounds = [slicing[0], slicing[1]] + if bounds != coord.get('bounds'): + coord.update(dict(bounds=bounds)) + self.state.dirty("da_coordinates") def _time_index_changed(self) -> None: dataset = self.builder.dataset @@ -671,17 +648,8 @@ def _on_change_da_t_index(self, da_t_index, **kwargs): @change("da_coordinates") def _on_change_da_coordinates(self, da_coordinates, **kwargs): - if len(da_coordinates) == 0: - self.builder.slicing = None - else: - self.builder.slicing = { - coord["name"]: [ - coord["start"], - coord["stop"], - coord["step"], - ] - for coord in da_coordinates - } + bounds = {c.get('name'): c.get('bounds') for c in da_coordinates} + self.builder._auto_select_slicing(bounds) @change("ui_action_name") def _on_change_action_name(self, ui_action_name, **kwargs): diff --git a/pan3d/ui/__init__.py b/pan3d/ui/__init__.py index e30946d8..a560aa85 100644 --- a/pan3d/ui/__init__.py +++ b/pan3d/ui/__init__.py @@ -2,10 +2,12 @@ from .main_drawer import MainDrawer from .toolbar import Toolbar from .render_options import RenderOptions +from .bounds_configure import BoundsConfigure __all__ = [ AxisDrawer, MainDrawer, Toolbar, RenderOptions, + BoundsConfigure, ] diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py new file mode 100644 index 00000000..c6a48f43 --- /dev/null +++ b/pan3d/ui/bounds_configure.py @@ -0,0 +1,51 @@ +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + + +class BoundsConfigure(vuetify.VMenu): + def __init__( + self, + coordinate_change_bounds_function, + da_coordinates="da_coordinates", + ): + super().__init__( + location="start", + transition="slide-y-transition", + close_on_content_click=False, + ) + with self: + with vuetify.Template( + activator="{ props }", + __properties=[ + ("activator", "v-slot:activator"), + ], + ): + vuetify.VBtn( + v_bind=("props",), + size="small", + icon="mdi-tune-variant", + style="position: absolute; left: 20px; top: 60px; z-index:2", + ) + with vuetify.VCard(classes="pa-6", style="width: 320px"): + with vuetify.VRangeSlider( + v_for=(f"coord in {da_coordinates}",), + model_value=("coord.bounds",), + label=("coord.name",), + strict=True, + hide_details=True, + step=1, + min=("coord.full_bounds[0]",), + max=("coord.full_bounds[1]",), + thumb_label=True, + style="width: 250px", + end=( + coordinate_change_bounds_function, + f"[coord.name, $event]" + ), + __events=[("end", "end")], + ): + with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): + html.Span( + ("{{ coord.labels[modelValue] }}",), + style="white-space: nowrap" + ) diff --git a/pan3d/ui/coordinate_configure.py b/pan3d/ui/coordinate_configure.py index e46332ac..473d68ff 100644 --- a/pan3d/ui/coordinate_configure.py +++ b/pan3d/ui/coordinate_configure.py @@ -49,7 +49,7 @@ def __init__( with vuetify.VSlider( v_model=(axis_info["index_var"],), min=0, - max=(f"{coordinate_info}?.size - 1",), + max=(f"{coordinate_info}?.length - 1",), step=(f"{coordinate_info}?.step",), classes="mx-5", ): @@ -59,7 +59,7 @@ def __init__( vuetify.VTextField( v_model=(axis_info["index_var"],), min=0, - max=(f"{coordinate_info}?.size - 1",), + max=(f"{coordinate_info}?.length - 1",), step=(f"{coordinate_info}?.step",), hide_details=True, density="compact", @@ -70,47 +70,45 @@ def __init__( else: vuetify.VCardSubtitle( - "Select values", - v_if=(f"{coordinate_info}?.numeric",), + "Select slicing", classes="mt-3", ) with vuetify.VContainer( classes="d-flex pa-0", style="column-gap: 3px", - v_if=(f"{coordinate_info}?.numeric",), ): vuetify.VTextField( - model_value=(f"{coordinate_info}?.start",), + model_value=(f"{coordinate_info}?.bounds[0]",), label="Start", hide_details=True, density="compact", type="number", - min=(f"{coordinate_info}?.range[0]",), - max=(f"{coordinate_info}?.range[1]",), - step="0.01", + min=(f"{coordinate_info}.full_bounds[0]",), + max=(f"{coordinate_info}.full_bounds[1]",), + step="1", __properties=["min", "max", "step"], - change_prevent=( + update=( coordinate_change_slice_function, - f"[{coordinate_info}.name, 'start', $event.target.value]", + f"[{coordinate_info}.name, 'start', $event]", ), - __events=[("change_prevent", "change.prevent")], + __events=[("update", "update:modelValue")], style="flex-grow: 1", ) vuetify.VTextField( - model_value=(f"{coordinate_info}?.stop",), + model_value=(f"{coordinate_info}?.bounds[1]",), label="Stop", hide_details=True, density="compact", type="number", - min=(f"{coordinate_info}?.range[0]",), - max=(f"{coordinate_info}?.range[1]",), - step="0.01", + min=(f"{coordinate_info}.full_bounds[0]",), + max=(f"{coordinate_info}.full_bounds[1]",), + step="1", __properties=["min", "max", "step"], - change_prevent=( + update=( coordinate_change_slice_function, - f"[{coordinate_info}.name, 'stop', $event.target.value]", + f"[{coordinate_info}.name, 'stop', $event]", ), - __events=[("change_prevent", "change.prevent")], + __events=[("update", "update:modelValue")], style="flex-grow: 1", ) vuetify.VTextField( @@ -120,13 +118,14 @@ def __init__( density="compact", type="number", min="1", - max=(f"{coordinate_info}?.size",), - __properties=["min", "max"], - change_prevent=( + max=(f"{coordinate_info}?.length",), + step="1", + __properties=["min", "max", "step"], + update=( coordinate_change_slice_function, - f"[{coordinate_info}.name, 'step', $event.target.value]", + f"[{coordinate_info}.name, 'step', $event]", ), - __events=[("change_prevent", "change.prevent")], + __events=[("update", "update:modelValue")], style="flex-grow: 1", ) From 24c97adc8af68f8ed3f9b6be6b381bb98ec48977 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Thu, 2 May 2024 14:02:39 +0000 Subject: [PATCH 03/23] feat: Add `resolution` CLI arg and disable auto slicing when <= 1 --- pan3d/dataset_builder.py | 6 +++++- pan3d/serve_viewer.py | 2 ++ pan3d/ui/axis_drawer.py | 3 +++ pan3d/ui/bounds_configure.py | 2 ++ pan3d/ui/coordinate_configure.py | 3 +++ pan3d/utils.py | 1 + 6 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pan3d/dataset_builder.py b/pan3d/dataset_builder.py index 7a3b36b4..deab4d5e 100644 --- a/pan3d/dataset_builder.py +++ b/pan3d/dataset_builder.py @@ -74,6 +74,7 @@ def viewer(self): da_z=self.z, da_t=self.t, da_t_index=self.t_index, + da_auto_slicing=self._resolution > 1 ), ) return self._viewer @@ -451,7 +452,10 @@ def _auto_select_slicing(self, bounds: Optional[Dict] = None) -> None: for k in da.dims } self.slicing = { - k: [v[0], v[1], math.ceil((v[1] - v[0]) / self._resolution)] + k: [ + v[0], v[1], + math.ceil((v[1] - v[0]) / self._resolution) if self._resolution > 1 else 1 + ] for k, v in bounds.items() } diff --git a/pan3d/serve_viewer.py b/pan3d/serve_viewer.py index fc178b17..f55f3ac9 100644 --- a/pan3d/serve_viewer.py +++ b/pan3d/serve_viewer.py @@ -10,6 +10,7 @@ def serve(): parser.add_argument("--config_path") parser.add_argument("--dataset") + parser.add_argument("--resolution", type=int) parser.add_argument("--catalogs", nargs="+") parser.add_argument("--server", action=BooleanOptionalAction) parser.add_argument("--debug", action=BooleanOptionalAction) @@ -19,6 +20,7 @@ def serve(): builder = DatasetBuilder( dataset=args.dataset, catalogs=args.catalogs, + resolution=args.resolution, ) if args.config_path: builder.import_config(args.config_path) diff --git a/pan3d/ui/axis_drawer.py b/pan3d/ui/axis_drawer.py index 12cfadb0..cc8271c4 100644 --- a/pan3d/ui/axis_drawer.py +++ b/pan3d/ui/axis_drawer.py @@ -14,6 +14,7 @@ def __init__( ui_current_time_string="ui_current_time_string", da_active="da_active", da_coordinates="da_coordinates", + da_auto_slicing="da_auto_slicing", da_x="da_x", da_y="da_y", da_z="da_z", @@ -68,6 +69,7 @@ def __init__( CoordinateConfigure( axes, da_coordinates, + da_auto_slicing, f"{da_coordinates}.find((c) => c.name === {axis['name_var']})", ui_expanded_coordinates, ui_current_time_string, @@ -98,6 +100,7 @@ def __init__( CoordinateConfigure( axes, da_coordinates, + da_auto_slicing, "coord", ui_expanded_coordinates, ui_current_time_string, diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index c6a48f43..9f30b266 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -7,8 +7,10 @@ def __init__( self, coordinate_change_bounds_function, da_coordinates="da_coordinates", + da_auto_slicing="da_auto_slicing" ): super().__init__( + v_if=(da_auto_slicing,), location="start", transition="slide-y-transition", close_on_content_click=False, diff --git a/pan3d/ui/coordinate_configure.py b/pan3d/ui/coordinate_configure.py index 473d68ff..ec5a9355 100644 --- a/pan3d/ui/coordinate_configure.py +++ b/pan3d/ui/coordinate_configure.py @@ -6,6 +6,7 @@ def __init__( self, axes, da_coordinates, + da_auto_slicing, coordinate_info, ui_expanded_coordinates, ui_current_time_string, @@ -71,9 +72,11 @@ def __init__( else: vuetify.VCardSubtitle( "Select slicing", + v_if=(f"!{da_auto_slicing}",), classes="mt-3", ) with vuetify.VContainer( + v_if=(f"!{da_auto_slicing}",), classes="d-flex pa-0", style="column-gap: 3px", ): diff --git a/pan3d/utils.py b/pan3d/utils.py index 9e2b937c..02843375 100644 --- a/pan3d/utils.py +++ b/pan3d/utils.py @@ -64,6 +64,7 @@ def has_gpu_rendering(): "da_t": None, "da_t_index": 0, "da_size": None, + "da_auto_slicing": True, "ui_loading": False, "ui_import_loading": False, "ui_main_drawer": False, From cfa631b8296dbcd019945a956ea10f513c3a9410 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Thu, 2 May 2024 14:08:44 +0000 Subject: [PATCH 04/23] style: reformat with black --- pan3d/dataset_builder.py | 18 ++++++++-------- pan3d/dataset_viewer.py | 40 ++++++++++++++++++++---------------- pan3d/ui/bounds_configure.py | 9 +++----- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/pan3d/dataset_builder.py b/pan3d/dataset_builder.py index deab4d5e..ef79a85e 100644 --- a/pan3d/dataset_builder.py +++ b/pan3d/dataset_builder.py @@ -1,7 +1,6 @@ import os import math import json -import pandas import pyvista import xarray @@ -21,7 +20,7 @@ def __init__( server: Any = None, viewer: bool = False, catalogs: List[str] = [], - resolution: int = 2 ** 7, + resolution: int = 2**7, ) -> None: """Create an instance of the DatasetBuilder class. @@ -74,7 +73,7 @@ def viewer(self): da_z=self.z, da_t=self.t, da_t_index=self.t_index, - da_auto_slicing=self._resolution > 1 + da_auto_slicing=self._resolution > 1, ), ) return self._viewer @@ -447,19 +446,18 @@ def _auto_select_slicing(self, bounds: Optional[Dict] = None) -> None: """Automatically select slicing for selected data array.""" if not bounds: da = self.dataset[self.data_array_name] - bounds = { - k: [0, da[k].size] - for k in da.dims - } + bounds = {k: [0, da[k].size] for k in da.dims} self.slicing = { k: [ - v[0], v[1], - math.ceil((v[1] - v[0]) / self._resolution) if self._resolution > 1 else 1 + v[0], + v[1], + math.ceil((v[1] - v[0]) / self._resolution) + if self._resolution > 1 + else 1, ] for k, v in bounds.items() } - # ----------------------------------------------------- # Config logic # ----------------------------------------------------- diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index 82f337dc..7cb4a5b7 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -241,21 +241,20 @@ def _coordinate_select_axis( def _coordinate_change_slice(self, coordinate_name, slice_attribute_name, value): value = float(value) for coord in self.state.da_coordinates: - if coord['name'] == coordinate_name: - bounds = coord.get('bounds') - if slice_attribute_name == 'start': + if coord["name"] == coordinate_name: + bounds = coord.get("bounds") + if slice_attribute_name == "start": bounds[0] = value - elif slice_attribute_name == 'stop': + elif slice_attribute_name == "stop": bounds[1] = value - elif slice_attribute_name == 'step': + elif slice_attribute_name == "step": coord.update(dict(step=value)) coord.update(dict(bounds=bounds)) self.state.dirty("da_coordinates") - def _coordinate_change_bounds(self, coordinate_name, bounds): for coord in self.state.da_coordinates: - if coord['name'] == coordinate_name: + if coord["name"] == coordinate_name: coord.update(dict(bounds=bounds)) self.state.dirty("da_coordinates") @@ -491,7 +490,7 @@ def _data_array_changed(self) -> None: size = current_coord.size coord_range = [ str(round(v)) if isinstance(v, float) else str(v) - for v in [values.item(0), values.item(size-1)] + for v in [values.item(0), values.item(size - 1)] ] dtype = current_coord.dtype @@ -507,14 +506,19 @@ def _data_array_changed(self) -> None: slicing = self.builder.slicing.get(key) if slicing: bounds = [values.item(slicing[0]), values.item(slicing[1])] - self.state.da_coordinates.append({ - "name": key, - "attrs": coord_attrs, - "labels": [str(round(v)) if isinstance(v, float) else str(v) for v in values], - "full_bounds": bounds, - "bounds": bounds, - "step": 1, - }) + self.state.da_coordinates.append( + { + "name": key, + "attrs": coord_attrs, + "labels": [ + str(round(v)) if isinstance(v, float) else str(v) + for v in values + ], + "full_bounds": bounds, + "bounds": bounds, + "step": 1, + } + ) self.state.dirty("da_coordinates") self.plotter.clear() @@ -527,7 +531,7 @@ def _data_slicing_changed(self) -> None: slicing = self.builder.slicing.get(coord["name"]) if slicing: bounds = [slicing[0], slicing[1]] - if bounds != coord.get('bounds'): + if bounds != coord.get("bounds"): coord.update(dict(bounds=bounds)) self.state.dirty("da_coordinates") @@ -648,7 +652,7 @@ def _on_change_da_t_index(self, da_t_index, **kwargs): @change("da_coordinates") def _on_change_da_coordinates(self, da_coordinates, **kwargs): - bounds = {c.get('name'): c.get('bounds') for c in da_coordinates} + bounds = {c.get("name"): c.get("bounds") for c in da_coordinates} self.builder._auto_select_slicing(bounds) @change("ui_action_name") diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index 9f30b266..7a7f7328 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -7,7 +7,7 @@ def __init__( self, coordinate_change_bounds_function, da_coordinates="da_coordinates", - da_auto_slicing="da_auto_slicing" + da_auto_slicing="da_auto_slicing", ): super().__init__( v_if=(da_auto_slicing,), @@ -40,14 +40,11 @@ def __init__( max=("coord.full_bounds[1]",), thumb_label=True, style="width: 250px", - end=( - coordinate_change_bounds_function, - f"[coord.name, $event]" - ), + end=(coordinate_change_bounds_function, "[coord.name, $event]"), __events=[("end", "end")], ): with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): html.Span( ("{{ coord.labels[modelValue] }}",), - style="white-space: nowrap" + style="white-space: nowrap", ) From 5cac70ea660dda2cc61fb6ecaa137ae3aec19b2f Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Fri, 3 May 2024 16:33:43 +0000 Subject: [PATCH 05/23] fix: Update example config files --- examples/example_config_cmip.json | 17 ++--------------- examples/example_config_esgf.json | 20 +------------------- examples/example_config_noaa.json | 12 ++++++------ examples/example_config_pangeo.json | 22 ---------------------- examples/example_config_xarray.json | 17 ----------------- 5 files changed, 9 insertions(+), 79 deletions(-) diff --git a/examples/example_config_cmip.json b/examples/example_config_cmip.json index 1e71a61b..74907dcc 100644 --- a/examples/example_config_cmip.json +++ b/examples/example_config_cmip.json @@ -4,24 +4,11 @@ "name": "zos", "x": "i", "y": "j", - "t": "time", - "t_index": 0 - }, - "data_slices": { - "j": [ - 0, - 290, - 1 - ], - "i": [ - 0, - 359, - 1 - ] + "t": "time" }, "ui": { "main_drawer": true, "axis_drawer": true, "expanded_coordinates": [] } -} \ No newline at end of file +} diff --git a/examples/example_config_esgf.json b/examples/example_config_esgf.json index 8efa2d69..752e6ef6 100644 --- a/examples/example_config_esgf.json +++ b/examples/example_config_esgf.json @@ -7,25 +7,7 @@ "name": "sfcWind", "x": "lon", "y": "lat", - "t": "time", - "t_index": 0 - }, - "data_slices": { - "time": [ - "Apr 01 2000 12:00", - "May 30 2001 12:00", - 1 - ], - "lat": [ - -89.72, - 89.72, - 1 - ], - "lon": [ - 0.42, - 359.58, - 1 - ] + "z": "time" }, "ui": { "main_drawer": false, diff --git a/examples/example_config_noaa.json b/examples/example_config_noaa.json index ca2345e8..1c64177f 100644 --- a/examples/example_config_noaa.json +++ b/examples/example_config_noaa.json @@ -9,14 +9,14 @@ }, "data_slices": { "lat": [ - -90, - 90, - 20 + 0, + 500, + 5 ], "lon": [ - -180, - 180, - 20 + 0, + 500, + 5 ] }, "ui": { diff --git a/examples/example_config_pangeo.json b/examples/example_config_pangeo.json index 117055df..867892c8 100644 --- a/examples/example_config_pangeo.json +++ b/examples/example_config_pangeo.json @@ -11,28 +11,6 @@ "t": "time", "t_index": 0 }, - "data_slices": { - "time": [ - 142560000, - 171072000, - 1 - ], - "Z": [ - -2912.67, - -0.5, - 1 - ], - "YC": [ - 1000, - 1999000, - 1 - ], - "XC": [ - 1000, - 999000, - 1 - ] - }, "ui": { "main_drawer": false, "axis_drawer": false, diff --git a/examples/example_config_xarray.json b/examples/example_config_xarray.json index 298be75d..2d0c2a56 100644 --- a/examples/example_config_xarray.json +++ b/examples/example_config_xarray.json @@ -11,23 +11,6 @@ "t": "month", "t_index": 0 }, - "data_slices": { - "level": [ - 200, - 850, - 1 - ], - "latitude": [ - -90, - 0, - 10 - ], - "longitude": [ - -180, - 0, - 10 - ] - }, "ui": { "main_drawer": true, "axis_drawer": true, From 9c9ebf7862dfd93369bf7ea4b4ab46fef51cf902 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Fri, 3 May 2024 16:35:29 +0000 Subject: [PATCH 06/23] fix: Correct various bugs and unexpected behavior --- pan3d/dataset_builder.py | 54 +++++++++++++++++++++++++----------- pan3d/dataset_viewer.py | 21 +++++++------- pan3d/ui/bounds_configure.py | 40 ++++++++++++++------------ 3 files changed, 71 insertions(+), 44 deletions(-) diff --git a/pan3d/dataset_builder.py b/pan3d/dataset_builder.py index ef79a85e..92aee6ff 100644 --- a/pan3d/dataset_builder.py +++ b/pan3d/dataset_builder.py @@ -35,6 +35,8 @@ def __init__( self._dataset_info = None self._da_name = None self._resolution = resolution + self._import_mode = False + self._import_viewer_state = {} self._server = server self._catalogs = catalogs @@ -74,6 +76,7 @@ def viewer(self): da_t=self.t, da_t_index=self.t_index, da_auto_slicing=self._resolution > 1, + **self._import_viewer_state, ), ) return self._viewer @@ -174,8 +177,9 @@ def data_array_name(self, data_array_name: Optional[str]) -> None: if self._viewer: self._viewer._data_array_changed() self._viewer._mesh_changed() - self._auto_select_coordinates() - self._auto_select_slicing() + if not self._import_mode: + self._auto_select_coordinates() + self._auto_select_slicing() @property def data_array(self) -> Optional[xarray.DataArray]: @@ -205,7 +209,7 @@ def x(self, x: Optional[str]) -> None: acceptable_values = self.dataset[self.data_array_name].dims if x not in acceptable_values: raise ValueError( - f"{x} does not exist on data array. Must be one of {acceptable_values}." + f"{x} does not exist on data array. Must be one of {sorted(acceptable_values)}." ) if self._algorithm.x != x: self._algorithm.x = x @@ -229,7 +233,7 @@ def y(self, y: Optional[str]) -> None: acceptable_values = self.dataset[self.data_array_name].dims if y not in acceptable_values: raise ValueError( - f"{y} does not exist on data array. Must be one of {acceptable_values}." + f"{y} does not exist on data array. Must be one of {sorted(acceptable_values)}." ) if self._algorithm.y != y: self._algorithm.y = y @@ -253,7 +257,7 @@ def z(self, z: Optional[str]) -> None: acceptable_values = self.dataset[self.data_array_name].dims if z not in acceptable_values: raise ValueError( - f"{z} does not exist on data array. Must be one of {acceptable_values}." + f"{z} does not exist on data array. Must be one of {sorted(acceptable_values)}." ) if self._algorithm.z != z: self._algorithm.z = z @@ -278,7 +282,7 @@ def t(self, t: Optional[str]) -> None: acceptable_values = self.dataset[self.data_array_name].dims if t not in acceptable_values: raise ValueError( - f"{t} does not exist on data array. Must be one of {acceptable_values}." + f"{t} does not exist on data array. Must be one of {sorted(acceptable_values)}." ) if self._algorithm.time != t: self._algorithm.time = t @@ -336,15 +340,19 @@ def slicing(self, slicing: Dict[str, List]) -> None: for key, value in slicing.items(): if not isinstance(key, str): raise ValueError("Keys in slicing must be strings.") - if not isinstance(value, list) or len(value) != 3: + if ( + not isinstance(value, list) + or len(value) != 3 + or any(not isinstance(v, int) for v in value) + ): raise ValueError( - "Values in slicing must be lists of length 3 ([start, stop, step])." + "Values in slicing must be lists of 3 integers ([start, stop, step])." ) da = self.dataset[self.data_array_name] acceptable_coords = da.dims if key not in acceptable_coords: raise ValueError( - f"Key {key} not found in data array. Must be one of {list(acceptable_coords.keys())}." + f"Key {key} not found in data array. Must be one of {sorted(acceptable_coords)}." ) key_coord = da[key] @@ -433,7 +441,7 @@ def _auto_select_coordinates(self) -> None: if (len(accepted) == 1 and accepted == name) or (len(accepted) > 1 and accepted in name) ] - if len(name_match) > 0 and coord_name in unassigned_axes: + if len(name_match) > 0 and axis in unassigned_axes: setattr(self, axis, coord_name) assigned_coords.append(coord_name) # Then assign any remaining by index @@ -442,8 +450,14 @@ def _auto_select_coordinates(self) -> None: if i < len(unassigned_axes): setattr(self, unassigned_axes[i], d) - def _auto_select_slicing(self, bounds: Optional[Dict] = None) -> None: + def _auto_select_slicing( + self, + bounds: Optional[Dict] = None, + steps: Optional[Dict] = None, + ) -> None: """Automatically select slicing for selected data array.""" + if not self.dataset or not self.data_array_name: + return if not bounds: da = self.dataset[self.data_array_name] bounds = {k: [0, da[k].size] for k in da.dims} @@ -453,6 +467,8 @@ def _auto_select_slicing(self, bounds: Optional[Dict] = None) -> None: v[1], math.ceil((v[1] - v[0]) / self._resolution) if self._resolution > 1 + else steps.get(k, 1) + if steps is not None else 1, ] for k, v in bounds.items() @@ -489,22 +505,27 @@ def import_config(self, config_file: Union[str, Path, None]) -> None: "source": "default", "id": origin_config, } + self._import_mode = True self.dataset_info = origin_config self.data_array_name = array_config.pop("name") for key, value in array_config.items(): setattr(self, key, value) self.slicing = config.get("data_slices") + self._import_mode = False + ui_config = {f"ui_{k}": v for k, v in config.get("ui", {}).items()} + render_config = {f"render_{k}": v for k, v in config.get("render", {}).items()} if self._viewer: - ui_config = {f"ui_{k}": v for k, v in config.get("ui", {}).items()} - render_config = { - f"render_{k}": v for k, v in config.get("render", {}).items() - } self._set_state_values( **ui_config, **render_config, ui_action_name=None, ) + else: + self._import_viewer_state = dict( + **ui_config, + **render_config, + ) def export_config(self, config_file: Union[str, Path, None] = None) -> None: """Export the current state to a JSON configuration file. @@ -527,8 +548,9 @@ def export_config(self, config_file: Union[str, Path, None] = None) -> None: if getattr(self, key) is not None }, }, - "data_slices": self.slicing, } + if self._algorithm.slicing: + config["data_slices"] = self._algorithm.slicing if self._viewer: state_items = list(self._viewer.state.to_dict().items()) config["ui"] = { diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index 7cb4a5b7..c387b970 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -85,6 +85,7 @@ def __init__( def _on_ready(self, **kwargs): self.state.render_auto = True + self.reset_camera = True self._mesh_changed() def start(self, **kwargs): @@ -239,7 +240,7 @@ def _coordinate_select_axis( self.reset_camera = True def _coordinate_change_slice(self, coordinate_name, slice_attribute_name, value): - value = float(value) + value = int(value) for coord in self.state.da_coordinates: if coord["name"] == coordinate_name: bounds = coord.get("bounds") @@ -501,11 +502,7 @@ def _data_array_changed(self) -> None: coord_attrs.append({"key": "dtype", "value": str(dtype)}) coord_attrs.append({"key": "length", "value": int(size)}) coord_attrs.append({"key": "range", "value": coord_range}) - bounds = [0, size - 1] - if self.builder.slicing: - slicing = self.builder.slicing.get(key) - if slicing: - bounds = [values.item(slicing[0]), values.item(slicing[1])] + bounds = [0, size] self.state.da_coordinates.append( { "name": key, @@ -519,10 +516,9 @@ def _data_array_changed(self) -> None: "step": 1, } ) - - self.state.dirty("da_coordinates") - self.plotter.clear() - self.plotter.view_isometric() + self.state.dirty("da_coordinates") + self.plotter.clear() + self.plotter.view_isometric() def _data_slicing_changed(self) -> None: if self.builder.slicing is None: @@ -568,6 +564,8 @@ def _mesh_changed(self) -> None: self.state.ui_unapplied_changes = False return total_bytes = da.size * da.dtype.itemsize + if total_bytes == 0: + self.state.da_size = "0 bytes" exponents_map = {0: "bytes", 1: "KB", 2: "MB", 3: "GB"} for exponent in sorted(exponents_map.keys(), reverse=True): divisor = 1024**exponent @@ -653,7 +651,8 @@ def _on_change_da_t_index(self, da_t_index, **kwargs): @change("da_coordinates") def _on_change_da_coordinates(self, da_coordinates, **kwargs): bounds = {c.get("name"): c.get("bounds") for c in da_coordinates} - self.builder._auto_select_slicing(bounds) + steps = {c.get("name"): c.get("step") for c in da_coordinates} + self.builder._auto_select_slicing(bounds, steps) @change("ui_action_name") def _on_change_action_name(self, ui_action_name, **kwargs): diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index 7a7f7328..091af73e 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -8,6 +8,9 @@ def __init__( coordinate_change_bounds_function, da_coordinates="da_coordinates", da_auto_slicing="da_auto_slicing", + da_x="da_x", + da_y="da_y", + da_z="da_z", ): super().__init__( v_if=(da_auto_slicing,), @@ -29,22 +32,25 @@ def __init__( style="position: absolute; left: 20px; top: 60px; z-index:2", ) with vuetify.VCard(classes="pa-6", style="width: 320px"): - with vuetify.VRangeSlider( + with html.Div( v_for=(f"coord in {da_coordinates}",), - model_value=("coord.bounds",), - label=("coord.name",), - strict=True, - hide_details=True, - step=1, - min=("coord.full_bounds[0]",), - max=("coord.full_bounds[1]",), - thumb_label=True, - style="width: 250px", - end=(coordinate_change_bounds_function, "[coord.name, $event]"), - __events=[("end", "end")], ): - with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): - html.Span( - ("{{ coord.labels[modelValue] }}",), - style="white-space: nowrap", - ) + with vuetify.VRangeSlider( + v_if=(f"[{da_x}, {da_y}, {da_z}].includes(coord.name)",), + model_value=("coord.bounds",), + label=("coord.name",), + strict=True, + hide_details=True, + step=1, + min=("coord.full_bounds[0]",), + max=("coord.full_bounds[1]",), + thumb_label=True, + style="width: 250px", + end=(coordinate_change_bounds_function, "[coord.name, $event]"), + __events=[("end", "end")], + ): + with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): + html.Span( + ("{{ coord.labels[modelValue] }}",), + style="white-space: nowrap", + ) From e08af6b40fb2a17f689071a8b484e69b9157c97b Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Fri, 3 May 2024 16:36:19 +0000 Subject: [PATCH 07/23] test: Update expected values in tests --- tests/test_builder.py | 17 +++++++++-------- tests/test_viewer.py | 14 +++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/test_builder.py b/tests/test_builder.py index 79565c0f..7cca0919 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -9,8 +9,8 @@ def test_import_config(): builder = DatasetBuilder() builder.import_config("examples/example_config_noaa.json") - # With slicing, data_array has shape (180, 360) - assert builder.data_array.size == 64800 + # With slicing, data_array has shape (100, 100) + assert builder.data_array.size == 10000 def test_export_config(): @@ -135,30 +135,31 @@ def test_setters_invalid_values(): # Set a valid value to proceed builder.data_array_name = "v" + acceptable_coord_names = ["latitude", "level", "longitude", "month"] # Setting wrong values for x, y, z, t with pytest.raises(ValueError) as e: builder.x = "foo" assert ( str(e.value) - == "foo does not exist on data array. Must be one of ('month', 'level', 'latitude', 'longitude')." + == f"foo does not exist on data array. Must be one of {acceptable_coord_names}." ) with pytest.raises(ValueError) as e: builder.y = "foo" assert ( str(e.value) - == "foo does not exist on data array. Must be one of ('month', 'level', 'latitude', 'longitude')." + == f"foo does not exist on data array. Must be one of {acceptable_coord_names}." ) with pytest.raises(ValueError) as e: builder.z = "foo" assert ( str(e.value) - == "foo does not exist on data array. Must be one of ('month', 'level', 'latitude', 'longitude')." + == f"foo does not exist on data array. Must be one of {acceptable_coord_names}." ) with pytest.raises(ValueError) as e: builder.t = "foo" assert ( str(e.value) - == "foo does not exist on data array. Must be one of ('month', 'level', 'latitude', 'longitude')." + == f"foo does not exist on data array. Must be one of {acceptable_coord_names}." ) # Set valid values to proceed @@ -183,13 +184,13 @@ def test_setters_invalid_values(): builder.slicing = {"foo": []} assert ( str(e.value) - == "Values in slicing must be lists of length 3 ([start, stop, step])." + == "Values in slicing must be lists of 3 integers ([start, stop, step])." ) with pytest.raises(ValueError) as e: builder.slicing = {"foo": [0, 1, 1]} assert ( str(e.value) - == "Key foo not found in data array. Must be one of ['longitude', 'latitude', 'level', 'month']." + == f"Key foo not found in data array. Must be one of {acceptable_coord_names}." ) with pytest.raises(ValueError) as e: builder.slicing = {"month": [-1, 10, 10]} diff --git a/tests/test_viewer.py b/tests/test_viewer.py index 303b5a71..83b77ebc 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -7,7 +7,7 @@ def push_builder_state(builder): builder.z = "level" builder.t = "month" builder.t_index = 1 - builder.slicing = {"longitude": [0, 90, 2]} + builder.slicing = {"longitude": [0, 90, 1], "latitude": [0, 100, 1]} def push_viewer_state(viewer): @@ -51,7 +51,7 @@ def assert_viewer_state(viewer): assert viewer.state.da_z == "level" assert viewer.state.da_t == "month" assert viewer.state.da_t_index == 1 - assert viewer.state.da_size == "345 KB" + assert viewer.state.da_size == "211 KB" assert viewer.state.da_vars == [ {"name": "z", "id": 0}, {"name": "u", "id": 1}, @@ -128,10 +128,10 @@ def test_viewer_export(): assert viewer.state.state_export["data_array"]["z"] == "level" assert viewer.state.state_export["data_array"]["t"] == "month" assert viewer.state.state_export["data_array"]["t_index"] == 1 - assert viewer.state.state_export["data_slices"]["longitude"] == [-180.0, 179.25, 1] - assert viewer.state.state_export["data_slices"]["latitude"] == [-90.0, 90.0, 1] - assert viewer.state.state_export["data_slices"]["level"] == [200, 850, 1] - assert viewer.state.state_export["data_slices"]["month"] == [1, 7, 1] + assert viewer.state.state_export["data_slices"]["longitude"] == [0, 480, 4] + assert viewer.state.state_export["data_slices"]["latitude"] == [0, 241, 2] + assert viewer.state.state_export["data_slices"]["level"] == [0, 3, 1] + assert viewer.state.state_export["data_slices"]["month"] == [0, 2, 1] assert not viewer.state.state_export["ui"]["main_drawer"] assert not viewer.state.state_export["ui"]["axis_drawer"] assert viewer.state.state_export["ui"]["unapplied_changes"] @@ -179,7 +179,7 @@ def test_sync_from_viewer_ui_functions(): viewer._coordinate_select_axis("month", None, "da_t") viewer._coordinate_change_slice("longitude", "start", 0) viewer._coordinate_change_slice("longitude", "stop", 90) - viewer._coordinate_change_slice("longitude", "step", 2) + viewer._coordinate_change_bounds("latitude", [0, 100]) viewer.state.flush() assert_builder_state(builder) From faabc39df0ef5edbdb5caa69efb34dfcaa260876 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Thu, 16 May 2024 16:14:44 +0000 Subject: [PATCH 08/23] fix: Move default resolution value (cmd arg is None if not specified) --- pan3d/dataset_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pan3d/dataset_builder.py b/pan3d/dataset_builder.py index 92aee6ff..960d2109 100644 --- a/pan3d/dataset_builder.py +++ b/pan3d/dataset_builder.py @@ -20,7 +20,7 @@ def __init__( server: Any = None, viewer: bool = False, catalogs: List[str] = [], - resolution: int = 2**7, + resolution: int = None, ) -> None: """Create an instance of the DatasetBuilder class. @@ -34,7 +34,7 @@ def __init__( self._dataset = None self._dataset_info = None self._da_name = None - self._resolution = resolution + self._resolution = resolution or 2**7 self._import_mode = False self._import_viewer_state = {} From ad2fcdc14b202ba548433db69705827af90a8a10 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Mon, 20 May 2024 23:45:29 +0000 Subject: [PATCH 09/23] feat: Generate cube face preview images and set up cube mode state vars --- pan3d/dataset_viewer.py | 81 ++++++++++++++++++++++++++++++++++++ pan3d/ui/bounds_configure.py | 49 +++++++++++++++++++++- pan3d/ui/render_options.py | 2 + pan3d/utils.py | 5 +++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index c387b970..058491c6 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -4,6 +4,10 @@ import pandas import pyvista import geovista +import numpy +import base64 +from io import BytesIO +from PIL import Image from pathlib import Path from typing import Dict, List, Optional, Union @@ -152,6 +156,9 @@ def ui(self) -> VAppLayout: self.ctrl.reset_camera = plot_view.reset_camera self.ctrl.push_camera = plot_view.push_camera self.plot_view = plot_view + # turn on axis orientation widget by default with state var from pyvista + # (typo in visibility is intentional, done to match pyvista) + self.state[f"{self.ctrl.get_plotter()._id_name}_axis_visiblity"] = True return self._ui # ----------------------------------------------------- @@ -530,6 +537,7 @@ def _data_slicing_changed(self) -> None: if bounds != coord.get("bounds"): coord.update(dict(bounds=bounds)) self.state.dirty("da_coordinates") + self._generate_preview() def _time_index_changed(self) -> None: dataset = self.builder.dataset @@ -556,6 +564,75 @@ def _time_index_changed(self) -> None: current_time = pandas.to_timedelta(current_time) current_time = f"{current_time.total_seconds()} seconds" self.state.ui_current_time_string = str(current_time) + self._generate_preview() + + def _generate_preview(self) -> None: + if ( + self.builder.dataset is None + or self.builder.data_array_name is None + or self.builder.slicing is None + ): + return + preview_slicing = {} + if self.builder.t is not None and self.builder.t_index is not None: + preview_slicing[self.builder.t] = self.builder.t_index + + face_options = [] + if self.builder.z is not None: + face_options += ["+Z", "-Z"] + if self.builder.y is not None: + face_options += ["+Y", "-Y"] + if self.builder.x is not None: + face_options += ["+X", "-X"] + self.state.cube_preview_face_options = face_options + if self.state.cube_preview_face not in face_options and len(face_options): + self.state.cube_preview_face = face_options[0] + + axis_name = None + if "X" in self.state.cube_preview_face: + self.state.cube_preview_axes = dict( + x=self.builder.y, + y=self.builder.z, + ) + axis_name = self.builder.x + elif "Y" in self.state.cube_preview_face: + self.state.cube_preview_axes = dict( + x=self.builder.x, + y=self.builder.z, + ) + axis_name = self.builder.y + elif "Z" in self.state.cube_preview_face: + self.state.cube_preview_axes = dict( + x=self.builder.x, + y=self.builder.y, + ) + axis_name = self.builder.z + + if axis_name is not None: + axis_slicing = self.builder.slicing.get(axis_name) + if axis_slicing is not None: + preview_slicing[axis_name] = ( + axis_slicing[0] + if "+" in self.state.cube_preview_face + else axis_slicing[1] - 1 + ) + + data = ( + self.builder.dataset[self.builder.data_array_name] + .isel(preview_slicing) + .to_numpy() + ) + normalized_data = numpy.vectorize( + lambda x, x_min, x_max: (x - x_min) / (x_max - x_min) * 255 + )(data, numpy.min(data), numpy.max(data)).astype(numpy.uint8) + img = Image.fromarray(normalized_data) + img = img.rotate(180) # match default axis orientation + buffer = BytesIO() + img.save(buffer, format="PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("ascii") + + # save encoded to state + self.state.cube_preview = f"data:image/png;base64,{encoded}" def _mesh_changed(self) -> None: da = self.builder.data_array @@ -692,3 +769,7 @@ def _on_change_render_options( scalar_warp=render_scalar_warp, cartographic=render_cartographic, ) + + @change("cube_preview_face") + def _on_change_cube_preview_face(self, cube_preview_face, **kwargs): + self._generate_preview() diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index 091af73e..ad9b4ef6 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -11,12 +11,22 @@ def __init__( da_x="da_x", da_y="da_y", da_z="da_z", + da_t="da_t", + da_t_index="da_t_index", + current_time="ui_current_time_string", + cube_view_mode="cube_view_mode", + cube_preview="cube_preview", + cube_preview_face="cube_preview_face", + cube_preview_face_options="cube_preview_face_options", + cube_preview_axes="cube_preview_axes", ): super().__init__( v_if=(da_auto_slicing,), location="start", transition="slide-y-transition", close_on_content_click=False, + persistent=True, + no_click_animation=True, ) with self: with vuetify.Template( @@ -31,12 +41,30 @@ def __init__( icon="mdi-tune-variant", style="position: absolute; left: 20px; top: 60px; z-index:2", ) - with vuetify.VCard(classes="pa-6", style="width: 320px"): + with vuetify.VCard(classes="pa-3", style="width: 325px"): + vuetify.VCardTitle("Configure Bounds") + vuetify.VSelect( + v_if=(cube_view_mode,), + v_model=(cube_preview_face,), + items=(cube_preview_face_options, []), + label="Face", + hide_details=True, + style="float:right", + ) + vuetify.VCheckbox( + v_model=(cube_view_mode,), label="Cube View", hide_details=True + ) with html.Div( v_for=(f"coord in {da_coordinates}",), ): with vuetify.VRangeSlider( - v_if=(f"[{da_x}, {da_y}, {da_z}].includes(coord.name)",), + v_if=( + f""" + ({da_x} === coord.name && (!{cube_view_mode} || {cube_preview_face}.includes('X'))) || + ({da_y} === coord.name && (!{cube_view_mode} || {cube_preview_face}.includes('Y'))) || + ({da_z} === coord.name && (!{cube_view_mode} || {cube_preview_face}.includes('Z'))) + """, + ), model_value=("coord.bounds",), label=("coord.name",), strict=True, @@ -54,3 +82,20 @@ def __init__( ("{{ coord.labels[modelValue] }}",), style="white-space: nowrap", ) + with html.Div( + v_for=(f"coord in {da_coordinates}",), + ): + with vuetify.VSlider( + v_model=(da_t_index), + v_if=(f"{da_t} === coord.name",), + label=("coord.name",), + min=("coord.full_bounds[0]",), + max=("coord.full_bounds[1] - 1",), + step=1, + thumb_label=True, + ): + with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): + html.Span( + ("{{ coord.labels[modelValue] }}",), + style="white-space: nowrap", + ) diff --git a/pan3d/ui/render_options.py b/pan3d/ui/render_options.py index 2a61a7d6..26ffb49e 100644 --- a/pan3d/ui/render_options.py +++ b/pan3d/ui/render_options.py @@ -19,6 +19,8 @@ def __init__( location="start", transition="slide-y-transition", close_on_content_click=False, + persistent=True, + no_click_animation=True, ) with self: with vuetify.Template( diff --git a/pan3d/utils.py b/pan3d/utils.py index 02843375..85660702 100644 --- a/pan3d/utils.py +++ b/pan3d/utils.py @@ -65,6 +65,11 @@ def has_gpu_rendering(): "da_t_index": 0, "da_size": None, "da_auto_slicing": True, + "cube_view_mode": True, + "cube_preview": None, + "cube_preview_face": "+Z", + "cube_preview_face_options": [], + "cube_preview_axes": None, "ui_loading": False, "ui_import_loading": False, "ui_main_drawer": False, From 1d9c89892df76ee0d505218c563c52a1e36423ec Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Mon, 20 May 2024 23:49:05 +0000 Subject: [PATCH 10/23] feat: Add PreviewBounds component, written with Vue --- pan3d/dataset_viewer.py | 2 + pan3d/ui/bounds_configure.py | 12 + pan3d/ui/preview_bounds.py | 14 + pan3d/ui/vue/.eslintrc.cjs | 12 + pan3d/ui/vue/.gitignore | 7 + pan3d/ui/vue/module/__init__.py | 13 + pan3d/ui/vue/package.json | 42 +++ pan3d/ui/vue/src/components/PreviewBounds.js | 266 +++++++++++++++++++ pan3d/ui/vue/src/components/index.js | 5 + pan3d/ui/vue/src/main.js | 7 + pan3d/ui/vue/vite.config.js | 21 ++ 11 files changed, 401 insertions(+) create mode 100644 pan3d/ui/preview_bounds.py create mode 100644 pan3d/ui/vue/.eslintrc.cjs create mode 100644 pan3d/ui/vue/.gitignore create mode 100644 pan3d/ui/vue/module/__init__.py create mode 100644 pan3d/ui/vue/package.json create mode 100644 pan3d/ui/vue/src/components/PreviewBounds.js create mode 100644 pan3d/ui/vue/src/components/index.js create mode 100644 pan3d/ui/vue/src/main.js create mode 100644 pan3d/ui/vue/vite.config.js diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index 058491c6..dcd5588a 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -23,6 +23,7 @@ from pan3d import catalogs as pan3d_catalogs from pan3d.dataset_builder import DatasetBuilder from pan3d.ui import AxisDrawer, MainDrawer, Toolbar, RenderOptions, BoundsConfigure +from pan3d.ui.vue import module from pan3d.utils import ( initial_state, has_gpu_rendering, @@ -56,6 +57,7 @@ def __init__( builder._viewer = self self.builder = builder self.server = get_server(server, client_type="vue3") + self.server.enable_module(module) self.current_event_loop = asyncio.get_event_loop() self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) self._ui = None diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index ad9b4ef6..7163a8cc 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -1,6 +1,8 @@ from trame.widgets import html from trame.widgets import vuetify3 as vuetify +from .preview_bounds import PreviewBounds + class BoundsConfigure(vuetify.VMenu): def __init__( @@ -54,6 +56,16 @@ def __init__( vuetify.VCheckbox( v_model=(cube_view_mode,), label="Cube View", hide_details=True ) + with html.Div(v_if=(cube_view_mode,)): + PreviewBounds( + preview=(cube_preview,), + axes=(cube_preview_axes,), + coordinates=(da_coordinates,), + update_bounds=( + coordinate_change_bounds_function, + "[$event.name, $event.bounds]", + ), + ) with html.Div( v_for=(f"coord in {da_coordinates}",), ): diff --git a/pan3d/ui/preview_bounds.py b/pan3d/ui/preview_bounds.py new file mode 100644 index 00000000..b99f9d6a --- /dev/null +++ b/pan3d/ui/preview_bounds.py @@ -0,0 +1,14 @@ +from trame_client.widgets.core import HtmlElement + + +class PreviewBounds(HtmlElement): + def __init__(self, children=None, **kwargs): + super().__init__("preview-bounds", children, **kwargs) + self._attr_names += [ + "preview", + "axes", + "coordinates", + ] + self._event_names += [ + ("update_bounds", "update-bounds"), + ] diff --git a/pan3d/ui/vue/.eslintrc.cjs b/pan3d/ui/vue/.eslintrc.cjs new file mode 100644 index 00000000..425c253e --- /dev/null +++ b/pan3d/ui/vue/.eslintrc.cjs @@ -0,0 +1,12 @@ +/* eslint-env node */ +module.exports = { + root: true, + extends: [ + "plugin:vue/vue3-essential", + "eslint:recommended", + "@vue/eslint-config-prettier", + ], + parserOptions: { + ecmaVersion: "latest", + }, +}; diff --git a/pan3d/ui/vue/.gitignore b/pan3d/ui/vue/.gitignore new file mode 100644 index 00000000..7dff8cea --- /dev/null +++ b/pan3d/ui/vue/.gitignore @@ -0,0 +1,7 @@ +serve +serve/*.map +serve/*.html +serve/*.common.* +serve/*-light.* +serve/*.umd.js +package-lock.json diff --git a/pan3d/ui/vue/module/__init__.py b/pan3d/ui/vue/module/__init__.py new file mode 100644 index 00000000..1c3fdd17 --- /dev/null +++ b/pan3d/ui/vue/module/__init__.py @@ -0,0 +1,13 @@ +from pathlib import Path + +# Compute local path to serve +serve_path = str(Path(__file__).parent.with_name("serve").resolve()) + +# Serve directory for JS/CSS files +serve = {"__pan3d_components": serve_path} + +# List of JS files to load (usually from the serve path above) +scripts = ["__pan3d_components/pan3d-components.umd.js"] + +# List of Vue plugins to install/load +vue_use = ["pan3d_components"] diff --git a/pan3d/ui/vue/package.json b/pan3d/ui/vue/package.json new file mode 100644 index 00000000..76ced23b --- /dev/null +++ b/pan3d/ui/vue/package.json @@ -0,0 +1,42 @@ +{ + "name": "pan3d-components", + "version": "0.0.0", + "description": "Vue component for pan3d", + "main": "./dist/pan3d-components.umd.js", + "unpkg": "./dist/pan3d-components.umd.js", + "exports": { + ".": { + "require": "./dist/pan3d-components.umd.js" + } + }, + "scripts": { + "dev": "vite", + "build": "vite build --emptyOutDir", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore --ignore-pattern public", + "semantic-release": "semantic-release" + }, + "dependencies": {}, + "peerDependencies": { + "vue": "^2.7.0 || >=3.0.0" + }, + "devDependencies": { + "@vue/eslint-config-prettier": "^7.0.0", + "eslint": "^8.33.0", + "eslint-plugin-vue": "^9.3.0", + "prettier": "^2.7.1", + "vite": "^4.1.0", + "vue": "^3.0.0" + }, + "author": "Kitware Inc", + "license": "MIT", + "keywords": [ + "Kitware", + "pan3d" + ], + "files": [ + "dist/*", + "src/*", + "*.json", + "*.js" + ] +} diff --git a/pan3d/ui/vue/src/components/PreviewBounds.js b/pan3d/ui/vue/src/components/PreviewBounds.js new file mode 100644 index 00000000..6e5e397b --- /dev/null +++ b/pan3d/ui/vue/src/components/PreviewBounds.js @@ -0,0 +1,266 @@ +const { ref, toRefs, watch, onMounted } = window.Vue; + +export default { + props: { + preview: { + default: null, + }, + axes: { + default: null, + }, + coordinates: { + default: null, + }, + }, + emits: ["update-bounds"], + setup(props, { emit }) { + const svg = ref(); + const { preview, coordinates } = toRefs(props); + const previewImage = ref(); + const previewShape = ref(); + const viewBox = ref(); + const boundsBox = ref([0, 0, 0, 0]); + let dragging = null; + + function updateViewBox() { + const i = new Image(); + i.onload = () => { + const factor = 300 / i.width; + previewShape.value = [300, Math.round(i.height * factor)]; + viewBox.value = `0 0 ${previewShape.value.join(" ")}`; + }; + i.src = preview.value; + } + + function updateBoundsBox() { + if (!previewShape.value) return; + let xMin; + let xMax; + let yMin; + let yMax; + props.coordinates.forEach((coord) => { + if (coord.name === props.axes.x) { + xMin = + previewShape.value[0] - + ((coord.bounds[1] - coord.full_bounds[0]) / + (coord.full_bounds[1] - coord.full_bounds[0])) * + previewShape.value[0]; + xMax = + previewShape.value[0] - + ((coord.bounds[0] - coord.full_bounds[0]) / + (coord.full_bounds[1] - coord.full_bounds[0])) * + previewShape.value[0]; + } else if (coord.name === props.axes.y) { + yMin = + previewShape.value[1] - + ((coord.bounds[1] - coord.full_bounds[0]) / + (coord.full_bounds[1] - coord.full_bounds[0])) * + previewShape.value[1]; + yMax = + previewShape.value[1] - + ((coord.bounds[0] - coord.full_bounds[0]) / + (coord.full_bounds[1] - coord.full_bounds[0])) * + previewShape.value[1]; + } + }); + boundsBox.value = [xMin, yMin, xMax, yMax]; + } + + function onMousePress(e) { + const allowance = 5; + const svgBounds = svg.value.getBoundingClientRect(); + const location = [e.clientX - svgBounds.x, e.clientY - svgBounds.y]; + if ( + location[0] >= boundsBox.value[0] - allowance && + location[0] <= boundsBox.value[2] + allowance && + location[1] >= boundsBox.value[1] - allowance && + location[1] <= boundsBox.value[3] + allowance + ) { + dragging = { + from: location, + xMin: false, + xMax: false, + yMin: false, + yMax: false, + wholeBox: true, + }; + if (Math.abs(location[0] - boundsBox.value[0]) <= allowance * 2) { + dragging.xMin = true; + dragging.wholeBox = false; + } + if (Math.abs(location[0] - boundsBox.value[2]) <= allowance * 2) { + dragging.xMax = true; + dragging.wholeBox = false; + } + if (Math.abs(location[1] - boundsBox.value[1]) <= allowance * 2) { + dragging.yMin = true; + dragging.wholeBox = false; + } + if (Math.abs(location[1] - boundsBox.value[3]) <= allowance * 2) { + dragging.yMax = true; + dragging.wholeBox = false; + } + } + } + + function onMouseMove(e) { + if (dragging) { + const svgBounds = svg.value.getBoundingClientRect(); + const location = [e.clientX - svgBounds.x, e.clientY - svgBounds.y]; + const [dx, dy] = [ + location[0] - dragging.from[0], + location[1] - dragging.from[1], + ]; + dragging.from = location; + let xMin = boundsBox.value[0]; + let yMin = boundsBox.value[1]; + let xMax = boundsBox.value[2]; + let yMax = boundsBox.value[3]; + if (dragging.xMin || dragging.xMax || dragging.wholeBox) { + if (dragging.xMin || dragging.wholeBox) xMin += dx; + if (dragging.xMax || dragging.wholeBox) xMax += dx; + if (xMin >= 0 && xMax <= previewShape.value[0]) { + boundsBox.value[0] = xMin; + boundsBox.value[2] = xMax; + } + } + if (dragging.yMin || dragging.yMax || dragging.wholeBox) { + if (dragging.yMin || dragging.wholeBox) yMin += dy; + if (dragging.yMax || dragging.wholeBox) yMax += dy; + if (yMin >= 0 && yMax <= previewShape.value[1]) { + boundsBox.value[1] = yMin; + boundsBox.value[3] = yMax; + } + } + } + } + + function onMouseRelease() { + if (dragging) { + dragging = null; + let xRange; + let yRange; + props.coordinates.forEach((coord) => { + if (coord.name === props.axes.x) { + xRange = [ + ((previewShape.value[0] - boundsBox.value[2]) / + previewShape.value[0]) * + (coord.full_bounds[1] - coord.full_bounds[0]) + + coord.full_bounds[0], + ((previewShape.value[0] - boundsBox.value[0]) / + previewShape.value[0]) * + (coord.full_bounds[1] - coord.full_bounds[0]) + + coord.full_bounds[0], + ]; + } else if (coord.name === props.axes.y) { + yRange = [ + ((previewShape.value[1] - boundsBox.value[3]) / + previewShape.value[1]) * + (coord.full_bounds[1] - coord.full_bounds[0]) + + coord.full_bounds[0], + ((previewShape.value[1] - boundsBox.value[1]) / + previewShape.value[1]) * + (coord.full_bounds[1] - coord.full_bounds[0]) + + coord.full_bounds[0], + ]; + } + }); + emit("update-bounds", { + name: props.axes.x, + bounds: xRange.map((v) => Math.round(v)), + }); + emit("update-bounds", { + name: props.axes.y, + bounds: yRange.map((v) => Math.round(v)), + }); + } + } + + onMounted(updateViewBox); + watch(preview, updateViewBox); + watch(previewShape, updateBoundsBox, { deep: true }); + watch(coordinates, updateBoundsBox, { deep: true }); + + return { + svg, + preview, + previewImage, + viewBox, + boundsBox, + onMouseMove, + onMousePress, + onMouseRelease, + color: "rgb(255,0,0)", + radius: 7, + }; + }, + template: ` + <svg + ref="svg" + :viewBox="viewBox" + @mousedown.prevent="onMousePress" + @mousemove="onMouseMove" + @mouseup="onMouseRelease" + @mouseleave="onMouseRelease" + style="cursor: pointer;" + > + <image + ref="previewImage" + :href="preview" + style="width: 300px" + /> + <circle + :cx="boundsBox[0]" + :cy="boundsBox[1]" + :r="radius" + :fill="color" + /> + <circle + :cx="boundsBox[2]" + :cy="boundsBox[1]" + :r="radius" + :fill="color" + /> + <circle + :cx="boundsBox[0]" + :cy="boundsBox[3]" + :r="radius" + :fill="color" + /> + <circle + :cx="boundsBox[2]" + :cy="boundsBox[3]" + :r="radius" + :fill="color" + /> + <line + :x1="boundsBox[0]" + :y1="boundsBox[1]" + :x2="boundsBox[2]" + :y2="boundsBox[1]" + :style="'stroke-width:5;stroke:'+color" + /> + <line + :x1="boundsBox[0]" + :y1="boundsBox[1]" + :x2="boundsBox[0]" + :y2="boundsBox[3]" + :style="'stroke-width:5;stroke:'+color" + /> + <line + :x1="boundsBox[2]" + :y1="boundsBox[1]" + :x2="boundsBox[2]" + :y2="boundsBox[3]" + :style="'stroke-width:5;stroke:'+color" + /> + <line + :x1="boundsBox[0]" + :y1="boundsBox[3]" + :x2="boundsBox[2]" + :y2="boundsBox[3]" + :style="'stroke-width:5;stroke:'+color" + /> + </svg> +`, +}; diff --git a/pan3d/ui/vue/src/components/index.js b/pan3d/ui/vue/src/components/index.js new file mode 100644 index 00000000..4bd00c06 --- /dev/null +++ b/pan3d/ui/vue/src/components/index.js @@ -0,0 +1,5 @@ +import PreviewBounds from "./PreviewBounds"; + +export default { + PreviewBounds, +}; diff --git a/pan3d/ui/vue/src/main.js b/pan3d/ui/vue/src/main.js new file mode 100644 index 00000000..55d953f7 --- /dev/null +++ b/pan3d/ui/vue/src/main.js @@ -0,0 +1,7 @@ +import components from "./components"; + +export function install(Vue) { + Object.keys(components).forEach((name) => { + Vue.component(name, components[name]); + }); +} diff --git a/pan3d/ui/vue/vite.config.js b/pan3d/ui/vue/vite.config.js new file mode 100644 index 00000000..fe2f1211 --- /dev/null +++ b/pan3d/ui/vue/vite.config.js @@ -0,0 +1,21 @@ +export default { + base: "./", + build: { + lib: { + entry: "./src/main.js", + name: "pan3d_components", + formats: ["umd"], + fileName: "pan3d-components", + }, + rollupOptions: { + external: ["vue"], + output: { + globals: { + vue: "Vue", + }, + }, + }, + outDir: "serve", + assetsDir: ".", + }, +}; From ce688ed14488a793cbf1827f7db79ec84fd881fc Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Thu, 23 May 2024 17:41:57 +0000 Subject: [PATCH 11/23] fix: Add npm installation steps to CI tests --- .github/workflows/test.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 197cce4c..60456256 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,6 +55,20 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Set up NPM + uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install Vue component dependencies + run: npm install + working-directory: pan3d/ui/vue + - name: Lint Vue components + run: npm run lint + working-directory: pan3d/ui/vue + - name: Build Vue components + run: npm run build + working-directory: pan3d/ui/vue + - name: Install and Run Tests run: | pip install . From 1532b5ae25084aa207ddd28c6b973344d596be07 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Fri, 24 May 2024 13:15:42 +0000 Subject: [PATCH 12/23] fix: apply suggested usability changes --- pan3d/dataset_viewer.py | 44 ++++++++-- pan3d/ui/bounds_configure.py | 89 +++++++++++++------- pan3d/ui/vue/src/components/PreviewBounds.js | 5 +- pan3d/utils.py | 4 +- 4 files changed, 99 insertions(+), 43 deletions(-) diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index dcd5588a..f60ad3fe 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -61,6 +61,7 @@ def __init__( self.current_event_loop = asyncio.get_event_loop() self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) self._ui = None + self._default_style = CSS_FILE.read_text() self.plotter = geovista.GeoPlotter(off_screen=True, notebook=False) self.plotter.set_background("lightgrey") @@ -120,7 +121,8 @@ def ui(self) -> VAppLayout: # Build UI self._ui = VAppLayout(self.server) with self._ui: - client.Style(CSS_FILE.read_text()) + with client.Style(self._default_style) as style: + self.ctrl.update_style = style.update Toolbar( self.apply_and_render, self._submit_import, @@ -147,6 +149,7 @@ def ui(self) -> VAppLayout: ): BoundsConfigure( coordinate_change_bounds_function=self._coordinate_change_bounds, + snap_camera_function=self._snap_camera_view_face, ) RenderOptions() with pyvista.trame.ui.plotter_ui( @@ -291,6 +294,21 @@ def submit(): submit, loading_state="ui_import_loading", unapplied_changes_state=None ) + def _snap_camera_view_face(self): + face = self.state.cube_preview_face + if "X" in face: + viewUp = [0, 0, 1] + vector = [-1, -1, 1] if "+" in face else [1, 1, 1] + self.plotter.view_vector(vector, viewUp) + if "Y" in face: + viewUp = [0, 0, 1] + vector = [1, -1, 1] if "+" in face else [-1, 1, 1] + self.plotter.view_vector(vector, viewUp) + if "Z" in face: + viewUp = [0, 1, 0] + vector = [-1, 1, -1] if "+" in face else [-1, 1, 1] + self.plotter.view_vector(vector, viewUp) + # ----------------------------------------------------- # Rendering methods # ----------------------------------------------------- @@ -392,7 +410,7 @@ def plot_mesh(self) -> None: ) if self.reset_camera: if len(self.builder.data_array.shape) > 2: - self.plotter.view_vector([1, 1, -1], [0, 1, 0]) + self.plotter.view_vector([1, 1, 1], [0, 1, 0]) elif not self.state.render_cartographic: self.plotter.view_xy() self.reset_camera = False @@ -511,7 +529,7 @@ def _data_array_changed(self) -> None: coord_attrs.append({"key": "dtype", "value": str(dtype)}) coord_attrs.append({"key": "length", "value": int(size)}) coord_attrs.append({"key": "range", "value": coord_range}) - bounds = [0, size] + bounds = [0, size - 1] self.state.da_coordinates.append( { "name": key, @@ -573,8 +591,12 @@ def _generate_preview(self) -> None: self.builder.dataset is None or self.builder.data_array_name is None or self.builder.slicing is None + or self._ui is None ): return + if not self.state.cube_view_mode: + self.ctrl.update_style(self._default_style) + return preview_slicing = {} if self.builder.t is not None and self.builder.t_index is not None: preview_slicing[self.builder.t] = self.builder.t_index @@ -616,9 +638,14 @@ def _generate_preview(self) -> None: preview_slicing[axis_name] = ( axis_slicing[0] if "+" in self.state.cube_preview_face - else axis_slicing[1] - 1 + else axis_slicing[1] ) + # update CSS to make blue slider thumb match preview outline + thumb_selector = f'.{axis_name}-slider .v-slider-thumb[aria-valuenow="{preview_slicing[axis_name]}"]' + thumb_style = thumb_selector + " { color: rgb(0, 100, 255) }" + self.ctrl.update_style(self._default_style + thumb_style) + data = ( self.builder.dataset[self.builder.data_array_name] .isel(preview_slicing) @@ -628,7 +655,10 @@ def _generate_preview(self) -> None: lambda x, x_min, x_max: (x - x_min) / (x_max - x_min) * 255 )(data, numpy.min(data), numpy.max(data)).astype(numpy.uint8) img = Image.fromarray(normalized_data) - img = img.rotate(180) # match default axis orientation + # apply transposes to match rendering orientation + img = img.transpose(Image.FLIP_TOP_BOTTOM) + if "+" in self.state.cube_preview_face: + img = img.transpose(Image.FLIP_LEFT_RIGHT) buffer = BytesIO() img.save(buffer, format="PNG") encoded = base64.b64encode(buffer.getvalue()).decode("ascii") @@ -772,6 +802,6 @@ def _on_change_render_options( cartographic=render_cartographic, ) - @change("cube_preview_face") - def _on_change_cube_preview_face(self, cube_preview_face, **kwargs): + @change("cube_view_mode", "cube_preview_face") + def _on_change_cube_view(self, cube_view_mode, cube_preview_face, **kwargs): self._generate_preview() diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index 7163a8cc..0631a386 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -8,6 +8,7 @@ class BoundsConfigure(vuetify.VMenu): def __init__( self, coordinate_change_bounds_function, + snap_camera_function, da_coordinates="da_coordinates", da_auto_slicing="da_auto_slicing", da_x="da_x", @@ -45,19 +46,29 @@ def __init__( ) with vuetify.VCard(classes="pa-3", style="width: 325px"): vuetify.VCardTitle("Configure Bounds") + vuetify.VCheckbox( + v_model=(cube_view_mode,), + label="Interactive Preview", + hide_details=True, + ) vuetify.VSelect( v_if=(cube_view_mode,), v_model=(cube_preview_face,), items=(cube_preview_face_options, []), label="Face", hide_details=True, - style="float:right", + style="float:left", ) - vuetify.VCheckbox( - v_model=(cube_view_mode,), label="Cube View", hide_details=True + vuetify.VBtn( + v_if=(cube_view_mode,), + size="small", + icon="mdi-video-marker", + click=snap_camera_function, + style="float:right", ) with html.Div(v_if=(cube_view_mode,)): PreviewBounds( + v_if=(cube_preview,), preview=(cube_preview,), axes=(cube_preview_axes,), coordinates=(da_coordinates,), @@ -69,7 +80,7 @@ def __init__( with html.Div( v_for=(f"coord in {da_coordinates}",), ): - with vuetify.VRangeSlider( + with html.Div( v_if=( f""" ({da_x} === coord.name && (!{cube_view_mode} || {cube_preview_face}.includes('X'))) || @@ -77,37 +88,51 @@ def __init__( ({da_z} === coord.name && (!{cube_view_mode} || {cube_preview_face}.includes('Z'))) """, ), - model_value=("coord.bounds",), - label=("coord.name",), - strict=True, - hide_details=True, - step=1, - min=("coord.full_bounds[0]",), - max=("coord.full_bounds[1]",), - thumb_label=True, - style="width: 250px", - end=(coordinate_change_bounds_function, "[coord.name, $event]"), - __events=[("end", "end")], + style="text-transform: capitalize", ): - with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): - html.Span( - ("{{ coord.labels[modelValue] }}",), - style="white-space: nowrap", - ) + html.Span("{{ coord.name.replaceAll('_', ' ') }}") + with vuetify.VRangeSlider( + model_value=("coord.bounds",), + strict=True, + hide_details=True, + step=1, + min=("coord.full_bounds[0]",), + max=("coord.full_bounds[1]",), + thumb_label=True, + classes=("coord.name +'-slider px-3'",), + end=( + coordinate_change_bounds_function, + "[coord.name, $event]", + ), + __events=[("end", "end")], + ): + with vuetify.Template( + v_slot_thumb_label=("{ modelValue }",) + ): + html.Span( + ("{{ coord.labels[modelValue] }}",), + style="white-space: nowrap", + ) with html.Div( v_for=(f"coord in {da_coordinates}",), ): - with vuetify.VSlider( - v_model=(da_t_index), + with html.Div( v_if=(f"{da_t} === coord.name",), - label=("coord.name",), - min=("coord.full_bounds[0]",), - max=("coord.full_bounds[1] - 1",), - step=1, - thumb_label=True, + style="text-transform: capitalize", ): - with vuetify.Template(v_slot_thumb_label=("{ modelValue }",)): - html.Span( - ("{{ coord.labels[modelValue] }}",), - style="white-space: nowrap", - ) + html.Span("{{ coord.name }}") + with vuetify.VSlider( + v_model=(da_t_index), + min=("coord.full_bounds[0]",), + max=("coord.full_bounds[1] - 1",), + step=1, + thumb_label=True, + classes="px-3", + ): + with vuetify.Template( + v_slot_thumb_label=("{ modelValue }",) + ): + html.Span( + ("{{ coord.labels[modelValue] }}",), + style="white-space: nowrap", + ) diff --git a/pan3d/ui/vue/src/components/PreviewBounds.js b/pan3d/ui/vue/src/components/PreviewBounds.js index 6e5e397b..42d65de4 100644 --- a/pan3d/ui/vue/src/components/PreviewBounds.js +++ b/pan3d/ui/vue/src/components/PreviewBounds.js @@ -190,7 +190,8 @@ export default { onMouseMove, onMousePress, onMouseRelease, - color: "rgb(255,0,0)", + color: "rgb(255, 0, 0)", + outline: "4px solid rgb(0, 100, 255)", radius: 7, }; }, @@ -202,7 +203,7 @@ export default { @mousemove="onMouseMove" @mouseup="onMouseRelease" @mouseleave="onMouseRelease" - style="cursor: pointer;" + :style="'cursor: pointer; outline:'+outline" > <image ref="previewImage" diff --git a/pan3d/utils.py b/pan3d/utils.py index 85660702..80e8daeb 100644 --- a/pan3d/utils.py +++ b/pan3d/utils.py @@ -65,9 +65,9 @@ def has_gpu_rendering(): "da_t_index": 0, "da_size": None, "da_auto_slicing": True, - "cube_view_mode": True, + "cube_view_mode": False, "cube_preview": None, - "cube_preview_face": "+Z", + "cube_preview_face": "-Z", "cube_preview_face_options": [], "cube_preview_axes": None, "ui_loading": False, From 32476117eebe3de18a9e7daecaac58632af1b374 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Fri, 7 Jun 2024 14:58:56 +0000 Subject: [PATCH 13/23] fix: add new folders to packages list in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 05c189d8..cbf0db3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["pan3d", "pan3d.ui", "pan3d.catalogs"] +packages = ["pan3d", "pan3d.ui", "pan3d.ui.vue", "pan3d.ui.vue.module", "pan3d.catalogs"] [tool.semantic_release] version_variables = ["setup.py:__version__"] From 7aa8ea9977829c1df43fce06c586b67cdb4e7ad3 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 16:46:32 +0000 Subject: [PATCH 14/23] refactor: move javascript code to its own top-level directory --- {pan3d/ui/vue => pan3d-js}/.eslintrc.cjs | 0 {pan3d/ui/vue => pan3d-js}/.gitignore | 0 {pan3d/ui/vue => pan3d-js}/module/__init__.py | 0 {pan3d/ui/vue => pan3d-js}/package.json | 0 {pan3d/ui/vue => pan3d-js}/src/components/PreviewBounds.js | 0 {pan3d/ui/vue => pan3d-js}/src/components/index.js | 0 {pan3d/ui/vue => pan3d-js}/src/main.js | 0 {pan3d/ui/vue => pan3d-js}/vite.config.js | 0 pyproject.toml | 2 +- 9 files changed, 1 insertion(+), 1 deletion(-) rename {pan3d/ui/vue => pan3d-js}/.eslintrc.cjs (100%) rename {pan3d/ui/vue => pan3d-js}/.gitignore (100%) rename {pan3d/ui/vue => pan3d-js}/module/__init__.py (100%) rename {pan3d/ui/vue => pan3d-js}/package.json (100%) rename {pan3d/ui/vue => pan3d-js}/src/components/PreviewBounds.js (100%) rename {pan3d/ui/vue => pan3d-js}/src/components/index.js (100%) rename {pan3d/ui/vue => pan3d-js}/src/main.js (100%) rename {pan3d/ui/vue => pan3d-js}/vite.config.js (100%) diff --git a/pan3d/ui/vue/.eslintrc.cjs b/pan3d-js/.eslintrc.cjs similarity index 100% rename from pan3d/ui/vue/.eslintrc.cjs rename to pan3d-js/.eslintrc.cjs diff --git a/pan3d/ui/vue/.gitignore b/pan3d-js/.gitignore similarity index 100% rename from pan3d/ui/vue/.gitignore rename to pan3d-js/.gitignore diff --git a/pan3d/ui/vue/module/__init__.py b/pan3d-js/module/__init__.py similarity index 100% rename from pan3d/ui/vue/module/__init__.py rename to pan3d-js/module/__init__.py diff --git a/pan3d/ui/vue/package.json b/pan3d-js/package.json similarity index 100% rename from pan3d/ui/vue/package.json rename to pan3d-js/package.json diff --git a/pan3d/ui/vue/src/components/PreviewBounds.js b/pan3d-js/src/components/PreviewBounds.js similarity index 100% rename from pan3d/ui/vue/src/components/PreviewBounds.js rename to pan3d-js/src/components/PreviewBounds.js diff --git a/pan3d/ui/vue/src/components/index.js b/pan3d-js/src/components/index.js similarity index 100% rename from pan3d/ui/vue/src/components/index.js rename to pan3d-js/src/components/index.js diff --git a/pan3d/ui/vue/src/main.js b/pan3d-js/src/main.js similarity index 100% rename from pan3d/ui/vue/src/main.js rename to pan3d-js/src/main.js diff --git a/pan3d/ui/vue/vite.config.js b/pan3d-js/vite.config.js similarity index 100% rename from pan3d/ui/vue/vite.config.js rename to pan3d-js/vite.config.js diff --git a/pyproject.toml b/pyproject.toml index cbf0db3d..64b0ed1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["pan3d", "pan3d.ui", "pan3d.ui.vue", "pan3d.ui.vue.module", "pan3d.catalogs"] +packages = ["pan3d", "pan3d.ui", "pan3d.ui.vue", "pan3d-js.serve", "pan3d.catalogs"] [tool.semantic_release] version_variables = ["setup.py:__version__"] From 743232879e4f4dab9fa9ffa4b19d2031c0d78889 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 16:49:03 +0000 Subject: [PATCH 15/23] fix: update javscript path in CI for build step --- .github/workflows/test.yml | 69 +++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60456256..1adb03dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,51 +29,42 @@ jobs: matrix: python-version: [3.9] config: - - { - name: "Linux", - os: ubuntu-latest - } - - { - name: "MacOSX", - os: macos-latest - } - - { - name: "Windows", - os: windows-latest - } + - { name: "Linux", os: ubuntu-latest } + - { name: "MacOSX", os: macos-latest } + - { name: "Windows", os: windows-latest } defaults: run: shell: bash steps: - - name: Checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} - - name: Set up NPM - uses: actions/setup-node@v4 - with: - node-version: 18 - - name: Install Vue component dependencies - run: npm install - working-directory: pan3d/ui/vue - - name: Lint Vue components - run: npm run lint - working-directory: pan3d/ui/vue - - name: Build Vue components - run: npm run build - working-directory: pan3d/ui/vue + - name: Set up NPM + uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install Vue component dependencies + run: npm install + working-directory: pan3d-js + - name: Lint Vue components + run: npm run lint + working-directory: pan3d-js + - name: Build Vue components + run: npm run build + working-directory: pan3d-js - - name: Install and Run Tests - run: | - pip install . - pip install -r tests/requirements.txt - pytest -s ./tests/test_builder.py - pip install .[viewer] - pytest -s ./tests/test_viewer.py - working-directory: . + - name: Install and Run Tests + run: | + pip install . + pip install -r tests/requirements.txt + pytest -s ./tests/test_builder.py + pip install .[viewer] + pytest -s ./tests/test_viewer.py + working-directory: . From 73d24a45b0485502819862ba8a2f779595ef2f7f Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 17:08:41 +0000 Subject: [PATCH 16/23] refactor: move module and serve dirs back into python package --- .gitignore | 1 + pan3d-js/vite.config.js | 2 +- pan3d/dataset_viewer.py | 2 +- {pan3d-js => pan3d/ui/pan3d_components}/module/__init__.py | 0 pyproject.toml | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) rename {pan3d-js => pan3d/ui/pan3d_components}/module/__init__.py (100%) diff --git a/.gitignore b/.gitignore index bbee3317..28a57cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules examples/jupyter/*.gif site/* +pan3d/ui/pan3d_components/serve # local env files .env.local diff --git a/pan3d-js/vite.config.js b/pan3d-js/vite.config.js index fe2f1211..b7df32ba 100644 --- a/pan3d-js/vite.config.js +++ b/pan3d-js/vite.config.js @@ -15,7 +15,7 @@ export default { }, }, }, - outDir: "serve", + outDir: "../pan3d/ui/pan3d_components/serve", assetsDir: ".", }, }; diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index f60ad3fe..59d1e19d 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -23,7 +23,7 @@ from pan3d import catalogs as pan3d_catalogs from pan3d.dataset_builder import DatasetBuilder from pan3d.ui import AxisDrawer, MainDrawer, Toolbar, RenderOptions, BoundsConfigure -from pan3d.ui.vue import module +from pan3d.ui.pan3d_components import module from pan3d.utils import ( initial_state, has_gpu_rendering, diff --git a/pan3d-js/module/__init__.py b/pan3d/ui/pan3d_components/module/__init__.py similarity index 100% rename from pan3d-js/module/__init__.py rename to pan3d/ui/pan3d_components/module/__init__.py diff --git a/pyproject.toml b/pyproject.toml index 64b0ed1a..99d740dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["pan3d", "pan3d.ui", "pan3d.ui.vue", "pan3d-js.serve", "pan3d.catalogs"] +packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components.serve", "pan3d.catalogs"] [tool.semantic_release] version_variables = ["setup.py:__version__"] From c9b449ff6a54a8f50818b49d1ea15e0bab2ba22b Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 17:20:42 +0000 Subject: [PATCH 17/23] fix: include module and serve packages individusaly --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99d740dc..d3eedccf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components.serve", "pan3d.catalogs"] +packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components.module", "pan3d.ui.pan3d_components.serve", "pan3d.catalogs"] [tool.semantic_release] version_variables = ["setup.py:__version__"] From c891b387ed64989eaf1dda385f5cb3f26991d3c2 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 17:58:36 +0000 Subject: [PATCH 18/23] refactor: apply suggestions --- pan3d/ui/bounds_configure.py | 2 +- pan3d/ui/pan3d_components/module/__init__.py | 2 +- pan3d/ui/{preview_bounds.py => widgets.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename pan3d/ui/{preview_bounds.py => widgets.py} (100%) diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index 0631a386..0925c0ad 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -1,7 +1,7 @@ from trame.widgets import html from trame.widgets import vuetify3 as vuetify -from .preview_bounds import PreviewBounds +from .widgets import PreviewBounds class BoundsConfigure(vuetify.VMenu): diff --git a/pan3d/ui/pan3d_components/module/__init__.py b/pan3d/ui/pan3d_components/module/__init__.py index 1c3fdd17..c8c44572 100644 --- a/pan3d/ui/pan3d_components/module/__init__.py +++ b/pan3d/ui/pan3d_components/module/__init__.py @@ -1,7 +1,7 @@ from pathlib import Path # Compute local path to serve -serve_path = str(Path(__file__).parent.with_name("serve").resolve()) +serve_path = str(Path('pan3d/ui/pan3d_components/module')) # Serve directory for JS/CSS files serve = {"__pan3d_components": serve_path} diff --git a/pan3d/ui/preview_bounds.py b/pan3d/ui/widgets.py similarity index 100% rename from pan3d/ui/preview_bounds.py rename to pan3d/ui/widgets.py From bf128ba380135ed2b2a90b34fea5288843b799a4 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 17:59:42 +0000 Subject: [PATCH 19/23] style: Use double quotes --- pan3d/ui/pan3d_components/module/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pan3d/ui/pan3d_components/module/__init__.py b/pan3d/ui/pan3d_components/module/__init__.py index c8c44572..8370ddeb 100644 --- a/pan3d/ui/pan3d_components/module/__init__.py +++ b/pan3d/ui/pan3d_components/module/__init__.py @@ -1,7 +1,7 @@ from pathlib import Path # Compute local path to serve -serve_path = str(Path('pan3d/ui/pan3d_components/module')) +serve_path = str(Path("pan3d/ui/pan3d_components/module")) # Serve directory for JS/CSS files serve = {"__pan3d_components": serve_path} From 06bc8bd0840bba4f662dbd1d16ac0d9180d69190 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 18:07:50 +0000 Subject: [PATCH 20/23] refactor: move `serve` directory within `module` directory --- .gitignore | 2 +- pan3d-js/vite.config.js | 2 +- pan3d/ui/pan3d_components/module/__init__.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 28a57cc5..20159639 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ node_modules examples/jupyter/*.gif site/* -pan3d/ui/pan3d_components/serve +pan3d/ui/pan3d_components/module/serve # local env files .env.local diff --git a/pan3d-js/vite.config.js b/pan3d-js/vite.config.js index b7df32ba..b3aa06b9 100644 --- a/pan3d-js/vite.config.js +++ b/pan3d-js/vite.config.js @@ -15,7 +15,7 @@ export default { }, }, }, - outDir: "../pan3d/ui/pan3d_components/serve", + outDir: "../pan3d/ui/pan3d_components/module/serve", assetsDir: ".", }, }; diff --git a/pan3d/ui/pan3d_components/module/__init__.py b/pan3d/ui/pan3d_components/module/__init__.py index 8370ddeb..01ea56bc 100644 --- a/pan3d/ui/pan3d_components/module/__init__.py +++ b/pan3d/ui/pan3d_components/module/__init__.py @@ -1,7 +1,7 @@ from pathlib import Path # Compute local path to serve -serve_path = str(Path("pan3d/ui/pan3d_components/module")) +serve_path = str(Path(__file__).with_name("serve").resolve()) # Serve directory for JS/CSS files serve = {"__pan3d_components": serve_path} diff --git a/pyproject.toml b/pyproject.toml index d3eedccf..2cb141c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components.module", "pan3d.ui.pan3d_components.serve", "pan3d.catalogs"] +packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components.module", "pan3d.catalogs"] [tool.semantic_release] version_variables = ["setup.py:__version__"] From a28e3083212c6c2dd0556228951f85d9f62a15d9 Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 18:51:35 +0000 Subject: [PATCH 21/23] refactor: move widgets.py into pan3d_components --- pan3d/ui/bounds_configure.py | 2 +- pan3d/ui/{ => pan3d_components}/widgets.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pan3d/ui/{ => pan3d_components}/widgets.py (100%) diff --git a/pan3d/ui/bounds_configure.py b/pan3d/ui/bounds_configure.py index 0925c0ad..238d748b 100644 --- a/pan3d/ui/bounds_configure.py +++ b/pan3d/ui/bounds_configure.py @@ -1,7 +1,7 @@ from trame.widgets import html from trame.widgets import vuetify3 as vuetify -from .widgets import PreviewBounds +from .pan3d_components.widgets import PreviewBounds class BoundsConfigure(vuetify.VMenu): diff --git a/pan3d/ui/widgets.py b/pan3d/ui/pan3d_components/widgets.py similarity index 100% rename from pan3d/ui/widgets.py rename to pan3d/ui/pan3d_components/widgets.py From 8389496ac09e0b4988c4d733fd6ac0c91f34d3bf Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 18:54:27 +0000 Subject: [PATCH 22/23] fix: Add "pan3d.ui.pan3d_components" to packages list --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2cb141c9..909d9396 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components.module", "pan3d.catalogs"] +packages = ["pan3d", "pan3d.ui", "pan3d.ui.pan3d_components", "pan3d.ui.pan3d_components.module", "pan3d.catalogs"] [tool.semantic_release] version_variables = ["setup.py:__version__"] From 1c4d30cbab102623c7c4be0aaf73ef4fe768da1f Mon Sep 17 00:00:00 2001 From: Anne Haley <anne.haley@kitware.com> Date: Tue, 18 Jun 2024 20:35:09 +0000 Subject: [PATCH 23/23] refactor: Enable module in widgets, not dataset_viewer --- pan3d/dataset_viewer.py | 2 -- pan3d/ui/pan3d_components/widgets.py | 14 +++++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pan3d/dataset_viewer.py b/pan3d/dataset_viewer.py index 59d1e19d..092bdc44 100644 --- a/pan3d/dataset_viewer.py +++ b/pan3d/dataset_viewer.py @@ -23,7 +23,6 @@ from pan3d import catalogs as pan3d_catalogs from pan3d.dataset_builder import DatasetBuilder from pan3d.ui import AxisDrawer, MainDrawer, Toolbar, RenderOptions, BoundsConfigure -from pan3d.ui.pan3d_components import module from pan3d.utils import ( initial_state, has_gpu_rendering, @@ -57,7 +56,6 @@ def __init__( builder._viewer = self self.builder = builder self.server = get_server(server, client_type="vue3") - self.server.enable_module(module) self.current_event_loop = asyncio.get_event_loop() self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) self._ui = None diff --git a/pan3d/ui/pan3d_components/widgets.py b/pan3d/ui/pan3d_components/widgets.py index b99f9d6a..a95a0740 100644 --- a/pan3d/ui/pan3d_components/widgets.py +++ b/pan3d/ui/pan3d_components/widgets.py @@ -1,4 +1,16 @@ -from trame_client.widgets.core import HtmlElement +from trame_client.widgets.core import AbstractElement +from . import module + +__all__ = [ + "PreviewBounds", +] + + +class HtmlElement(AbstractElement): + def __init__(self, _elem_name, children=None, **kwargs): + super().__init__(_elem_name, children, **kwargs) + if self.server: + self.server.enable_module(module) class PreviewBounds(HtmlElement):