diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index f7453617..595aac21 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -4330,6 +4330,12 @@ "properties": { "data_id": { "type": "string" + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + } } }, "required": [], diff --git a/opengeodeweb_viewer_typed_schemas.d.ts b/opengeodeweb_viewer_typed_schemas.d.ts index 22d42bcc..ad909995 100644 --- a/opengeodeweb_viewer_typed_schemas.d.ts +++ b/opengeodeweb_viewer_typed_schemas.d.ts @@ -1187,7 +1187,8 @@ export interface ViewerPickColormapParams { } export interface ViewerPickColormapResponse { - data_id?: string; + block_ids?: number[]; + data_id?: string; } export interface ViewerHighlightParams { diff --git a/requirements.txt b/requirements.txt index 8928c17f..92c87b23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,4 +61,3 @@ wslink==2.6.0 yarl==1.* # via aiohttp -opengeodeweb-microservice==1.*,>=1.3.2 diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index a2c48010..30693dd8 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -63,8 +63,8 @@ def remove_object(self, data_id: str) -> None: for size_actor in pipeline.points_size_actors.values(): renderer.RemoveActor(size_actor.actor) renderer.RemoveViewProp(pipeline.scalar_bar) - for bar in pipeline.scalar_bars.values(): - renderer.RemoveViewProp(bar) + for scalar_bar in pipeline.scalar_bars.values(): + renderer.RemoveViewProp(scalar_bar.actor) self.deregister_object(data_id) self.update_scalar_bars_layout() @@ -74,8 +74,8 @@ def set_visibility(self, data_id: str, *, visibility: bool) -> None: self.set_points_actors_visibility(pipeline, visibility=visibility) if not visibility: pipeline.scalar_bar.VisibilityOff() - for bar in pipeline.scalar_bars.values(): - bar.VisibilityOff() + for scalar_bar in pipeline.scalar_bars.values(): + scalar_bar.actor.VisibilityOff() if pipeline.points_scalar_bar is not None: pipeline.points_scalar_bar.VisibilityOff() else: @@ -87,9 +87,9 @@ def set_visibility(self, data_id: str, *, visibility: bool) -> None: self.update_points_scalar_bar_visibility(pipeline) for style in pipeline.block_styles.values(): if style.get("name"): - for bar in pipeline.scalar_bars.values(): - if bar.GetLookupTable() is not None: - bar.VisibilityOn() + for scalar_bar in pipeline.scalar_bars.values(): + if scalar_bar.actor.GetLookupTable() is not None: + scalar_bar.actor.VisibilityOn() break self.update_scalar_bars_layout() @@ -234,8 +234,8 @@ def clear_colors(self, data_id: str) -> None: pipeline.clear_blocks_scalars() mapper.ScalarVisibilityOff() pipeline.scalar_bar.VisibilityOff() - for bar in pipeline.scalar_bars.values(): - bar.VisibilityOff() + for scalar_bar in pipeline.scalar_bars.values(): + scalar_bar.actor.VisibilityOff() self.update_scalar_bars_layout() def _apply_highlight_style(self, actor: vtkActor, mapper: vtkDataSetMapper) -> None: diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 3f4d5731..ea7dd55c 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -14,7 +14,6 @@ ) from vtkmodules.vtkFiltersCore import vtkAppendDataSets from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader -from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor from vtkmodules.vtkRenderingCore import ( vtkActor, vtkCompositeDataDisplayAttributes, @@ -32,6 +31,7 @@ from opengeodeweb_viewer.vtk_pipeline import ( BlockStyle, ModelPointsStyle, + ModelScalarBar, PointsSizeActor, VtkPipeline, ) @@ -122,46 +122,57 @@ def apply_color( self.setup_model_color_map(pipeline) return colors + @staticmethod + def scalar_bar_key(active_attrs: dict[str, BlockStyle], style: BlockStyle, label: str) -> str: + # Styles sharing the same color map share a scalar bar, + # the key only grows to tell them apart + name = style["name"] + item = style["item"] + minimum = style["minimum"] + maximum = style["maximum"] + points = style["points"] + attr_key = name + if attr_key in active_attrs and ( + active_attrs[attr_key]["item"] != item + or active_attrs[attr_key]["minimum"] != minimum + or active_attrs[attr_key]["maximum"] != maximum + or active_attrs[attr_key]["points"] != points + ): + attr_key = f"{name} (Item {item + 1})" if item > 0 else name + if attr_key in active_attrs and ( + active_attrs[attr_key]["minimum"] != minimum + or active_attrs[attr_key]["maximum"] != maximum + or active_attrs[attr_key]["points"] != points + ): + attr_key = f"{attr_key} [{minimum:g}, {maximum:g}]" + if attr_key in active_attrs and active_attrs[attr_key]["points"] != points: + attr_key = f"{attr_key} (Block {label})" + return attr_key + def setup_model_color_map(self, pipeline: VtkPipeline) -> None: # Points attributes join the polygons ones, so identical color maps share one scalar bar displayed_styles = [ - (str(block_id), style) for block_id, style in pipeline.block_styles.items() + (block_id, str(block_id), style) for block_id, style in pipeline.block_styles.items() ] + [ - (f"{block_id} points", points_style.attribute) + (block_id, f"{block_id} points", points_style.attribute) for block_id, points_style in pipeline.points_block_styles.items() if points_style.attribute is not None and points_style.visibility ] active_attrs: dict[str, BlockStyle] = {} - for block_id, style in displayed_styles: + active_block_ids: dict[str, list[int]] = {} + for block_id, label, style in displayed_styles: if style["name"]: - name = style["name"] - item = style["item"] - minimum = style["minimum"] - maximum = style["maximum"] - points = style["points"] - attr_key = name - if attr_key in active_attrs and ( - active_attrs[attr_key]["item"] != item - or active_attrs[attr_key]["minimum"] != minimum - or active_attrs[attr_key]["maximum"] != maximum - or active_attrs[attr_key]["points"] != points - ): - attr_key = f"{name} (Item {item + 1})" if item > 0 else name - if attr_key in active_attrs and ( - active_attrs[attr_key]["minimum"] != minimum - or active_attrs[attr_key]["maximum"] != maximum - or active_attrs[attr_key]["points"] != points - ): - attr_key = f"{attr_key} [{minimum:g}, {maximum:g}]" - if attr_key in active_attrs and active_attrs[attr_key]["points"] != points: - attr_key = f"{attr_key} (Block {block_id})" + attr_key = self.scalar_bar_key(active_attrs, style, label) active_attrs[attr_key] = style + bar_block_ids = active_block_ids.setdefault(attr_key, []) + if block_id not in bar_block_ids: + bar_block_ids.append(block_id) for name, style in active_attrs.items(): if name not in pipeline.scalar_bars: - bar = vtkScalarBarActor() - self.get_renderer().AddViewProp(bar) - pipeline.scalar_bars[name] = bar - bar = pipeline.scalar_bars[name] + pipeline.scalar_bars[name] = ModelScalarBar() + self.get_renderer().AddViewProp(pipeline.scalar_bars[name].actor) + pipeline.scalar_bars[name].block_ids = active_block_ids[name] + bar = pipeline.scalar_bars[name].actor item = style["item"] minimum = style["minimum"] maximum = style["maximum"] @@ -170,9 +181,9 @@ def setup_model_color_map(self, pipeline: VtkPipeline) -> None: lut = create_color_transfer_function(points, minimum, maximum, item, no_data_color) bar.SetLookupTable(lut) bar.VisibilityOn() - for name, bar in pipeline.scalar_bars.items(): + for name, scalar_bar in pipeline.scalar_bars.items(): if name not in active_attrs: - bar.VisibilityOff() + scalar_bar.actor.VisibilityOff() self.update_scalar_bars_layout() diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.json index 2f41e6b1..497e0836 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.json +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.json @@ -19,6 +19,12 @@ "properties": { "data_id": { "type": "string" + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + } } }, "required": [], diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.py index fd64e4cb..6b764a23 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/pick_colormap.py @@ -1,6 +1,6 @@ from pathlib import Path from opengeodeweb_microservice.schemas import Route, load_schema -from typing import Optional +from typing import List, Optional from dataclasses_json import DataClassJsonMixin from opengeodeweb_microservice.schemas import print_dataclass from dataclasses import dataclass @@ -20,6 +20,7 @@ class PickColormapResponse(DataClassJsonMixin): def __post_init__(self) -> None: print_dataclass(self) + block_ids: Optional[List[int]] = None data_id: Optional[str] = None diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index e34d84ab..89a01856 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -12,11 +12,12 @@ # Third party imports from vtkmodules.vtkIOImage import vtkJPEGWriter, vtkPNGWriter -from vtkmodules.vtkRenderingAnnotation import vtkAxesActor, vtkCubeAxesActor +from vtkmodules.vtkRenderingAnnotation import vtkAxesActor, vtkCubeAxesActor, vtkScalarBarActor from vtkmodules.vtkRenderingCore import ( vtkAbstractMapper, vtkActor, vtkCellPicker, + vtkCompositePolyDataMapper, vtkDataSetMapper, vtkPropPicker, vtkRenderer, @@ -210,18 +211,25 @@ def pick_colormap(self, params: schemas.PickColormap) -> schemas.PickColormapRes ny = 1.0 - (params.y / size[1]) for data_id, pipeline in self.get_data_base().items(): - bar = pipeline.scalar_bar - if bar.GetVisibility() and bar.GetLookupTable() is not None: - pos = bar.GetPositionCoordinate() - bx, by = pos.GetValue()[0], pos.GetValue()[1] - w, h = bar.GetWidth(), bar.GetHeight() - - # Check if click falls within the scalar bar bounding box - if bx <= nx <= bx + w and by <= ny <= by + h: - return schemas.PickColormapResponse(data_id=data_id) + # A model has one scalar bar per attribute, each coloring some of its blocks + if isinstance(pipeline.mapper, vtkCompositePolyDataMapper): + for scalar_bar in pipeline.scalar_bars.values(): + if self.is_scalar_bar_clicked(scalar_bar.actor, nx, ny): + return schemas.PickColormapResponse( + data_id=data_id, block_ids=scalar_bar.block_ids + ) + elif self.is_scalar_bar_clicked(pipeline.scalar_bar, nx, ny): + return schemas.PickColormapResponse(data_id=data_id) return schemas.PickColormapResponse() + @staticmethod + def is_scalar_bar_clicked(bar: vtkScalarBarActor, nx: float, ny: float) -> bool: + if not bar.GetVisibility() or bar.GetLookupTable() is None: + return False + bx, by = bar.GetPositionCoordinate().GetValue()[:2] + return bool(bx <= nx <= bx + bar.GetWidth() and by <= ny <= by + bar.GetHeight()) + def compute_epsilon(self, renderer: vtkRenderer, z: float) -> float: renderer.SetDisplayPoint(0, 0, z) renderer.DisplayToWorld() diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index d6b3f264..e5c7c921 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -209,6 +209,13 @@ class BlockStyle(TypedDict): no_data_color: ColorClassProtocol | None +@dataclass +class ModelScalarBar: + actor: vtkScalarBarActor = field(default_factory=vtkScalarBarActor) + # Blocks colored by this scalar bar + block_ids: list[int] = field(default_factory=list) + + @dataclass class PointsSizeActor: mapper: vtkCompositePolyDataMapper @@ -239,7 +246,7 @@ class VtkPipeline: block_data_sets: list[vtkDataObject | None] = field(default_factory=list) block_geode_ids: list[str] = field(default_factory=list) scalar_bar: vtkScalarBarActor = field(default_factory=vtkScalarBarActor) - scalar_bars: dict[str, vtkScalarBarActor] = field(default_factory=dict) + scalar_bars: dict[str, ModelScalarBar] = field(default_factory=dict) block_styles: dict[int, BlockStyle] = field(default_factory=dict) pick_mapper: vtkMapper | None = None points_mapper: vtkDataSetMapper | None = None diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index d35477fc..110251d1 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -530,9 +530,9 @@ def visible_scalar_bars( points_attr_name = pipeline.points_mapper.GetArrayName() visible_bars.append((points_bar, points_attr_name, data_id, pipeline)) - for attr_name, bar in pipeline.scalar_bars.items(): - if bar.GetVisibility() and bar.GetLookupTable() is not None: - visible_bars.append((bar, attr_name, data_id, pipeline)) + for attr_name, scalar_bar in pipeline.scalar_bars.items(): + if scalar_bar.actor.GetVisibility() and scalar_bar.actor.GetLookupTable() is not None: + visible_bars.append((scalar_bar.actor, attr_name, data_id, pipeline)) return visible_bars def update_scalar_bars_layout(self) -> None: diff --git a/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py b/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py index 3a6b217a..80038e19 100644 --- a/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py +++ b/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py @@ -12,6 +12,8 @@ ) # Local application imports +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView +from tests.conftest import ServerMonitor from tests.model.test_model_protocols import test_register_model_cube if TYPE_CHECKING: @@ -419,3 +421,35 @@ def test_blocks_polyhedron_color_map_rainbow( ) assert server.compare_image("model/blocks/color_map_rainbow.jpeg") + + +def test_pick_blocks_polyhedron_color_map( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_register_model_cube(server, dataset_factory) + + server.call( + VtkModelBlocksAttributePolyhedronView.model_blocks_attribute_polyhedron_prefix + + VtkModelBlocksAttributePolyhedronView.model_blocks_attribute_polyhedron_schemas_dict[ + "attribute" + ]["rpc"], + [ + { + "id": model_id, + "block_ids": list(range(48, 50)), + "name": "tetrahedron_vertices", + "item": 0, + "points": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0], + "minimum": 0.0, + "maximum": 1.0, + } + ], + ) + + # The first scalar bar is at the bottom left of the 300x300 view + pick_rpc = ( + VtkViewerView.viewer_prefix + VtkViewerView.viewer_schemas_dict["pick_colormap"]["rpc"] + ) + server.call(pick_rpc, [{"x": 81, "y": 276}]) + assert server.get_result(pick_rpc) == {"data_id": model_id, "block_ids": [48, 49]}