From 9004fb7d533b80a840c929357651803beb74d66f Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Aug 2026 15:35:14 +0200 Subject: [PATCH 01/12] feat(ruler): Add new ruler tool RPC and pipeline to render points, line and value dynamically --- opengeodeweb_viewer_schemas.json | 28 +++++ package.json | 2 +- .../rpc/viewer/schemas/__init__.py | 1 + .../rpc/viewer/schemas/ruler.json | 27 ++++ .../rpc/viewer/schemas/ruler.py | 13 ++ .../rpc/viewer/viewer_protocols.py | 34 +++++ src/opengeodeweb_viewer/vtk_pipeline.py | 119 ++++++++++++++++++ src/opengeodeweb_viewer/vtk_protocol.py | 7 ++ tests/data/images/viewer/ruler.jpeg | Bin 0 -> 6701 bytes tests/test_viewer_protocols.py | 21 ++++ 10 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json create mode 100644 src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py create mode 100644 tests/data/images/viewer/ruler.jpeg diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index 9dc7d248..7d2efe77 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -2196,6 +2196,34 @@ ], "additionalProperties": false }, + "ruler": { + "$id": "opengeodeweb_viewer.viewer.ruler", + "rpc": "ruler", + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "point1": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "point2": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": [], + "additionalProperties": false + }, "reset_visualization": { "$id": "opengeodeweb_viewer.viewer.reset_visualization", "rpc": "reset_visualization", diff --git a/package.json b/package.json index 5da3d5f8..3dca6c79 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "description": "", "scripts": { - "json": "npx opengeodeweb-microservice-generate opengeodeweb_viewer rpc rpc .", + "json": "npx opengeodeweb-microservice-generate --startDir src/opengeodeweb_viewer/rpc --key rpc --separator .", "test": "npm run json", "build": "npm run json" }, diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py index b3cfc453..e46aa937 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py @@ -4,6 +4,7 @@ from .shrink import * from .set_z_scaling import * from .set_background_color import * +from .ruler import * from .reset_visualization import * from .reset_camera import * from .render import * diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json new file mode 100644 index 00000000..64ca76a1 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json @@ -0,0 +1,27 @@ +{ + "rpc": "ruler", + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "point1": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "point2": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": [], + "additionalProperties": false +} diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py new file mode 100644 index 00000000..ec9ed841 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py @@ -0,0 +1,13 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass +from typing import Optional, List + + +@dataclass +class Ruler(DataClassJsonMixin): + def __post_init__(self) -> None: + print(self, flush=True) + + enabled: Optional[bool] = None + point1: Optional[List[float]] = None + point2: Optional[List[float]] = None diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 2bb02c5f..d4227725 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -30,6 +30,7 @@ validate_schema, RpcParams, ) +from opengeodeweb_viewer.vtk_pipeline import RulerPipeline from opengeodeweb_viewer.vtk_protocol import VtkView from opengeodeweb_viewer.rpc.viewer import schemas @@ -98,6 +99,9 @@ def resetVisualization(self, rpc_params: RpcParams) -> None: self.set_axes(axes) self.set_widget(widget) + ruler = RulerPipeline() + ruler.add_to_renderer(renderer) + self.set_ruler(ruler) renderer.SetBackground([180 / 255, 180 / 255, 180 / 255]) @@ -319,6 +323,9 @@ def updateCamera(self, rpc_params: RpcParams) -> None: camera.SetPosition(camera_options.position) camera.SetViewAngle(camera_options.view_angle) camera.SetClippingRange(camera_options.clipping_range) + ruler = self.get_ruler() + if ruler is not None: + ruler.update_scale(self.get_renderer()) @exportRpc(viewer_prefix + viewer_schemas_dict["render"]["rpc"]) def renderNow(self, rpc_params: RpcParams) -> None: @@ -465,3 +472,30 @@ def previewPoints(self, rpc_params: RpcParams) -> None: self._preview_polydata.SetPolys(polys) self._preview_polydata.Modified() self.render(-1) + + @exportRpc(viewer_prefix + viewer_schemas_dict["ruler"]["rpc"]) + def setRuler( + self, rpc_params: RpcParams + ) -> dict[str, float | list[float] | None]: + validate_schema( + rpc_params, self.viewer_schemas_dict["ruler"], self.viewer_prefix + ) + params = schemas.Ruler.from_dict(rpc_params) + ruler = self.get_ruler() + assert ruler is not None + point1: tuple[float, float, float] | None = ( + (params.point1[0], params.point1[1], params.point1[2]) + if params.enabled and params.point1 + else None + ) + point2: tuple[float, float, float] | None = ( + (params.point2[0], params.point2[1], params.point2[2]) + if params.enabled and params.point2 + else None + ) + distance = ruler.set_endpoints(point1, point2, renderer=self.get_renderer()) + return { + "distance": distance, + "point1": list(point1) if point1 else None, + "point2": list(point2) if point2 else None, + } diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index af9a4a23..fe9b6f58 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -1,12 +1,15 @@ # Standard library imports from dataclasses import dataclass, field +import math from typing import cast, Literal, TypedDict # Third party imports from vtkmodules.vtkRenderingCore import ( vtkActor, vtkDataSetMapper, + vtkFollower, vtkMapper, + vtkPolyDataMapper, vtkCompositePolyDataMapper, vtkCompositeDataDisplayAttributes, vtkColorTransferFunction, @@ -14,6 +17,8 @@ from vtkmodules.vtkRenderingAnnotation import ( vtkScalarBarActor, ) +from vtkmodules.vtkRenderingFreeType import vtkVectorText +from vtkmodules.vtkFiltersSources import vtkLineSource, vtkSphereSource from vtkmodules.vtkCommonDataModel import ( vtkDataObject, vtkDataSet, @@ -54,6 +59,120 @@ class HighlightPipeline: selection: vtkSelection = field(default_factory=vtkSelection) extractSelection: vtkExtractSelection = field(default_factory=vtkExtractSelection) +@dataclass +class RulerPipeline: + _SPHERE_RESOLUTION = 32 + _PRIMARY_COLOR = (60 / 255, 153 / 255, 131 / 255) + _TEXT_COLOR = (0.05, 0.05, 0.05) + + _point1: tuple[float, float, float] | None = field(default=None, init=False) + _point2: tuple[float, float, float] | None = field(default=None, init=False) + _line_source: vtkLineSource = field(default_factory=vtkLineSource) + line_actor: vtkActor = field(init=False) + _point1_source: vtkSphereSource = field(init=False) + point1_actor: vtkActor = field(init=False) + _point2_source: vtkSphereSource = field(init=False) + point2_actor: vtkActor = field(init=False) + _text_source: vtkVectorText = field(default_factory=vtkVectorText) + text_follower: vtkFollower = field(init=False) + + def __post_init__(self) -> None: + self.line_actor = self._setup_actor( + vtkActor(), self._line_source, self._PRIMARY_COLOR, line_width=3.0 + ) + self._point1_source, self.point1_actor = self._make_sphere() + self._point2_source, self.point2_actor = self._make_sphere() + self.text_follower = self._setup_actor( + vtkFollower(), self._text_source, self._TEXT_COLOR, offset=-50000.0 + ) + + def add_to_renderer(self, renderer) -> None: + self.text_follower.SetCamera(renderer.GetActiveCamera()) + for actor in ( + self.line_actor, + self.point1_actor, + self.point2_actor, + self.text_follower, + ): + actor.VisibilityOff() + renderer.AddActor(actor) + + def _setup_actor( + self, + actor, + source, + color: tuple[float, float, float], + line_width: float | None = None, + offset: float = -10000.0, + ): + mapper = vtkPolyDataMapper() + mapper.SetInputConnection(source.GetOutputPort()) + mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters(offset, offset) + mapper.SetRelativeCoincidentTopologyLineOffsetParameters(offset, offset) + mapper.SetRelativeCoincidentTopologyPointOffsetParameter(offset) + actor.SetMapper(mapper) + actor.SetPickable(False) + actor_property = actor.GetProperty() + actor_property.SetColor(*color) + actor_property.SetAmbient(1.0) + actor_property.SetDiffuse(0.0) + if line_width is not None: + actor_property.SetLineWidth(line_width) + return actor + + def _make_sphere(self) -> tuple[vtkSphereSource, vtkActor]: + source = vtkSphereSource() + source.SetPhiResolution(self._SPHERE_RESOLUTION) + source.SetThetaResolution(self._SPHERE_RESOLUTION) + return source, self._setup_actor(vtkActor(), source, self._PRIMARY_COLOR) + + def update_scale(self, renderer=None) -> None: + if self._point1 is None or renderer is None: + return + camera_position = renderer.GetActiveCamera().GetPosition() + self._point1_source.SetRadius( + max(math.dist(self._point1, camera_position) * 0.003, 0.0001) + ) + if self._point2 is None: + return + self._point2_source.SetRadius( + max(math.dist(self._point2, camera_position) * 0.003, 0.0001) + ) + midpoint = tuple( + (coord1 + coord2) / 2 + for coord1, coord2 in zip(self._point1, self._point2) + ) + text_scale = max(math.dist(midpoint, camera_position) * 0.008, 0.001) + self.text_follower.SetPosition( + midpoint[0], midpoint[1] + text_scale * 1.2, midpoint[2] + ) + self.text_follower.SetScale(text_scale, text_scale, text_scale) + + def set_endpoints( + self, + point1: tuple[float, float, float] | None, + point2: tuple[float, float, float] | None, + renderer=None, + ) -> float: + self._point1 = point1 + self._point2 = point2 + self.point1_actor.SetVisibility(point1 is not None) + self.point2_actor.SetVisibility(point2 is not None) + self.line_actor.SetVisibility(point2 is not None) + self.text_follower.SetVisibility(point2 is not None) + if point1 is not None: + self._point1_source.SetCenter(*point1) + if point2 is None or point1 is None: + self.update_scale(renderer) + return 0.0 + self._line_source.SetPoint1(*point1) + self._line_source.SetPoint2(*point2) + self._point2_source.SetCenter(*point2) + distance = math.dist(point1, point2) + self._text_source.SetText(f"{distance:.2f}") + self.update_scale(renderer) + return distance + class BlockStyle(TypedDict): name: str diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 183d30b5..5831734c 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -38,6 +38,7 @@ from opengeodeweb_microservice.database.data import Data from opengeodeweb_viewer.rpc.viewer.schemas.clipping_planes import Plane from opengeodeweb_viewer.vtk_pipeline import ( + RulerPipeline, ViewerData, VtkPipeline, ) @@ -83,6 +84,12 @@ def get_widget(self) -> vtkOrientationMarkerWidget | None: def set_widget(self, widget: vtkOrientationMarkerWidget) -> None: self.coreServer.setSharedObject("widget", widget) + def get_ruler(self) -> RulerPipeline | None: + return cast(RulerPipeline | None, self.getSharedObject("ruler")) + + def set_ruler(self, ruler: RulerPipeline) -> None: + self.coreServer.setSharedObject("ruler", ruler) + def get_viewer_data(self, data_id: str) -> ViewerData: if Data is None: raise Exception("Data model not available") diff --git a/tests/data/images/viewer/ruler.jpeg b/tests/data/images/viewer/ruler.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b922a2cfa1150be6e94069cbcbf691f1d51b721c GIT binary patch literal 6701 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 literal 0 HcmV?d00001 diff --git a/tests/test_viewer_protocols.py b/tests/test_viewer_protocols.py index f99f2d23..42803e46 100644 --- a/tests/test_viewer_protocols.py +++ b/tests/test_viewer_protocols.py @@ -464,3 +464,24 @@ def test_shrink_then_clipping( ], ) assert server.compare_image("viewer/clipping_then_shrink.jpeg") == True + + +def test_ruler( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["reset_visualization"]["rpc"] + ) + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["ruler"]["rpc"], + [ + { + "enabled": True, + "point1": [-0.2, -0.2, 0.0], + "point2": [0.2, 0.2, 0.0], + } + ], + ) + assert server.compare_image("viewer/ruler.jpeg") == True From 713b198f95fa9defaf4683f65242895f86145f2e Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Aug 2026 15:36:20 +0200 Subject: [PATCH 02/12] fix(highlight): highlight right object when data is skrinked --- src/opengeodeweb_viewer/vtk_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 5831734c..af4e142a 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -316,7 +316,7 @@ def extract_picked_attributes( field_type: str, dataset: vtkDataObject | None, ) -> dict[str, list[float] | float]: - data_object = dataset or pipeline.reader.GetOutputDataObject(0) + data_object = dataset or pipeline.mapper.GetInputDataObject(0, 0) if not isinstance(data_object, vtkDataSet): return {} field_data = ( From f9eeb9a8d7594c189bec8eee6a3963de5534783b Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 13 Aug 2026 16:52:10 +0200 Subject: [PATCH 03/12] rm enabled --- opengeodeweb_viewer_schemas.json | 3 --- src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json | 3 --- src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py | 3 +-- src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py | 4 ++-- tests/test_viewer_protocols.py | 1 - 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index 7d2efe77..c98d6e68 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -2201,9 +2201,6 @@ "rpc": "ruler", "type": "object", "properties": { - "enabled": { - "type": "boolean" - }, "point1": { "type": "array", "items": { diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json index 64ca76a1..5ee8433f 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json @@ -2,9 +2,6 @@ "rpc": "ruler", "type": "object", "properties": { - "enabled": { - "type": "boolean" - }, "point1": { "type": "array", "items": { diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py index ec9ed841..3899291e 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py @@ -1,6 +1,6 @@ from dataclasses_json import DataClassJsonMixin from dataclasses import dataclass -from typing import Optional, List +from typing import List, Optional @dataclass @@ -8,6 +8,5 @@ class Ruler(DataClassJsonMixin): def __post_init__(self) -> None: print(self, flush=True) - enabled: Optional[bool] = None point1: Optional[List[float]] = None point2: Optional[List[float]] = None diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index d4227725..acc0d28f 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -485,12 +485,12 @@ def setRuler( assert ruler is not None point1: tuple[float, float, float] | None = ( (params.point1[0], params.point1[1], params.point1[2]) - if params.enabled and params.point1 + if params.point1 else None ) point2: tuple[float, float, float] | None = ( (params.point2[0], params.point2[1], params.point2[2]) - if params.enabled and params.point2 + if params.point2 else None ) distance = ruler.set_endpoints(point1, point2, renderer=self.get_renderer()) diff --git a/tests/test_viewer_protocols.py b/tests/test_viewer_protocols.py index 42803e46..b151cf99 100644 --- a/tests/test_viewer_protocols.py +++ b/tests/test_viewer_protocols.py @@ -478,7 +478,6 @@ def test_ruler( + VtkViewerView.viewer_schemas_dict["ruler"]["rpc"], [ { - "enabled": True, "point1": [-0.2, -0.2, 0.0], "point2": [0.2, 0.2, 0.0], } From dee60f7e23e0c213539d3e59f1ca37a7b2c691b1 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:59:09 +0000 Subject: [PATCH 04/12] Apply prepare changes --- requirements.txt | 1 - src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py | 4 +--- src/opengeodeweb_viewer/vtk_pipeline.py | 4 ++-- tests/test_viewer_protocols.py | 7 ++----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index dc966e83..86373ac9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,4 +63,3 @@ wslink==1.12.4 yarl>=1 # via aiohttp -opengeodeweb-microservice==1.*,>=1.2.1rc1 diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index acc0d28f..3cfa1c30 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -474,9 +474,7 @@ def previewPoints(self, rpc_params: RpcParams) -> None: self.render(-1) @exportRpc(viewer_prefix + viewer_schemas_dict["ruler"]["rpc"]) - def setRuler( - self, rpc_params: RpcParams - ) -> dict[str, float | list[float] | None]: + def setRuler(self, rpc_params: RpcParams) -> dict[str, float | list[float] | None]: validate_schema( rpc_params, self.viewer_schemas_dict["ruler"], self.viewer_prefix ) diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index fe9b6f58..6bee2e30 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -59,6 +59,7 @@ class HighlightPipeline: selection: vtkSelection = field(default_factory=vtkSelection) extractSelection: vtkExtractSelection = field(default_factory=vtkExtractSelection) + @dataclass class RulerPipeline: _SPHERE_RESOLUTION = 32 @@ -139,8 +140,7 @@ def update_scale(self, renderer=None) -> None: max(math.dist(self._point2, camera_position) * 0.003, 0.0001) ) midpoint = tuple( - (coord1 + coord2) / 2 - for coord1, coord2 in zip(self._point1, self._point2) + (coord1 + coord2) / 2 for coord1, coord2 in zip(self._point1, self._point2) ) text_scale = max(math.dist(midpoint, camera_position) * 0.008, 0.001) self.text_follower.SetPosition( diff --git a/tests/test_viewer_protocols.py b/tests/test_viewer_protocols.py index b151cf99..169f8c79 100644 --- a/tests/test_viewer_protocols.py +++ b/tests/test_viewer_protocols.py @@ -466,16 +466,13 @@ def test_shrink_then_clipping( assert server.compare_image("viewer/clipping_then_shrink.jpeg") == True -def test_ruler( - server: ServerMonitor, dataset_factory: Callable[..., str] -) -> None: +def test_ruler(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: server.call( VtkViewerView.viewer_prefix + VtkViewerView.viewer_schemas_dict["reset_visualization"]["rpc"] ) server.call( - VtkViewerView.viewer_prefix - + VtkViewerView.viewer_schemas_dict["ruler"]["rpc"], + VtkViewerView.viewer_prefix + VtkViewerView.viewer_schemas_dict["ruler"]["rpc"], [ { "point1": [-0.2, -0.2, 0.0], From 555742be0111295301b09347a10deaca564288f1 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 15:27:10 +0200 Subject: [PATCH 05/12] reset_ruler RPC --- opengeodeweb_viewer_schemas.json | 12 ++++++++- .../rpc/viewer/schemas/__init__.py | 1 + .../rpc/viewer/schemas/reset_ruler.json | 7 +++++ .../rpc/viewer/schemas/reset_ruler.py | 10 +++++++ .../rpc/viewer/schemas/ruler.json | 4 ++- .../rpc/viewer/schemas/ruler.py | 2 +- .../rpc/viewer/viewer_protocols.py | 26 ++++++++++++------- src/opengeodeweb_viewer/vtk_pipeline.py | 26 +++++++++++++------ 8 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.json create mode 100644 src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.py diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index 430e722a..e5a564c7 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -2143,7 +2143,9 @@ "maxItems": 3 } }, - "required": [], + "required": [ + "point1" + ], "additionalProperties": false }, "reset_visualization": { @@ -2154,6 +2156,14 @@ "required": [], "additionalProperties": false }, + "reset_ruler": { + "$id": "opengeodeweb_viewer.viewer.reset_ruler", + "rpc": "reset_ruler", + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }, "reset_camera": { "$id": "opengeodeweb_viewer.viewer.reset_camera", "rpc": "reset_camera", diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py index e46aa937..9571601a 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py @@ -6,6 +6,7 @@ from .set_background_color import * from .ruler import * from .reset_visualization import * +from .reset_ruler import * from .reset_camera import * from .render import * from .preview_points import * diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.json new file mode 100644 index 00000000..63c66eae --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.json @@ -0,0 +1,7 @@ +{ + "rpc": "reset_ruler", + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false +} diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.py new file mode 100644 index 00000000..f672bbfb --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.py @@ -0,0 +1,10 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass + + +@dataclass +class ResetRuler(DataClassJsonMixin): + def __post_init__(self) -> None: + print(self, flush=True) + + pass diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json index 5ee8433f..56a77b40 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json @@ -19,6 +19,8 @@ "maxItems": 3 } }, - "required": [], + "required": [ + "point1" + ], "additionalProperties": false } diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py index 3899291e..f24fe77f 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py @@ -8,5 +8,5 @@ class Ruler(DataClassJsonMixin): def __post_init__(self) -> None: print(self, flush=True) - point1: Optional[List[float]] = None + point1: List[float] point2: Optional[List[float]] = None diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 3cfa1c30..39762073 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -474,19 +474,17 @@ def previewPoints(self, rpc_params: RpcParams) -> None: self.render(-1) @exportRpc(viewer_prefix + viewer_schemas_dict["ruler"]["rpc"]) - def setRuler(self, rpc_params: RpcParams) -> dict[str, float | list[float] | None]: + def setRuler( + self, rpc_params: RpcParams + ) -> dict[str, float | list[float] | None]: validate_schema( rpc_params, self.viewer_schemas_dict["ruler"], self.viewer_prefix ) params = schemas.Ruler.from_dict(rpc_params) ruler = self.get_ruler() assert ruler is not None - point1: tuple[float, float, float] | None = ( - (params.point1[0], params.point1[1], params.point1[2]) - if params.point1 - else None - ) - point2: tuple[float, float, float] | None = ( + point1 = (params.point1[0], params.point1[1], params.point1[2]) + point2 = ( (params.point2[0], params.point2[1], params.point2[2]) if params.point2 else None @@ -494,6 +492,16 @@ def setRuler(self, rpc_params: RpcParams) -> dict[str, float | list[float] | Non distance = ruler.set_endpoints(point1, point2, renderer=self.get_renderer()) return { "distance": distance, - "point1": list(point1) if point1 else None, - "point2": list(point2) if point2 else None, + "point1": params.point1, + "point2": params.point2, } + + @exportRpc(viewer_prefix + viewer_schemas_dict["reset_ruler"]["rpc"]) + def resetRuler(self, rpc_params: RpcParams) -> None: + validate_schema( + rpc_params, self.viewer_schemas_dict["reset_ruler"], self.viewer_prefix + ) + ruler = self.get_ruler() + assert ruler is not None + ruler.reset() + diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index 6bee2e30..28cf6f4b 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -148,23 +148,33 @@ def update_scale(self, renderer=None) -> None: ) self.text_follower.SetScale(text_scale, text_scale, text_scale) + def reset(self) -> None: + self._point1 = None + self._point2 = None + self.point1_actor.VisibilityOff() + self.point2_actor.VisibilityOff() + self.line_actor.VisibilityOff() + self.text_follower.VisibilityOff() + def set_endpoints( self, - point1: tuple[float, float, float] | None, + point1: tuple[float, float, float], point2: tuple[float, float, float] | None, renderer=None, ) -> float: self._point1 = point1 self._point2 = point2 - self.point1_actor.SetVisibility(point1 is not None) - self.point2_actor.SetVisibility(point2 is not None) - self.line_actor.SetVisibility(point2 is not None) - self.text_follower.SetVisibility(point2 is not None) - if point1 is not None: - self._point1_source.SetCenter(*point1) - if point2 is None or point1 is None: + self.point1_actor.VisibilityOn() + self._point1_source.SetCenter(*point1) + if point2 is None: + self.point2_actor.VisibilityOff() + self.line_actor.VisibilityOff() + self.text_follower.VisibilityOff() self.update_scale(renderer) return 0.0 + self.point2_actor.VisibilityOn() + self.line_actor.VisibilityOn() + self.text_follower.VisibilityOn() self._line_source.SetPoint1(*point1) self._line_source.SetPoint2(*point2) self._point2_source.SetCenter(*point2) From 2dceb61b0339f29e3daf35b8e08d6ebec20a8bfd Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:27:53 +0000 Subject: [PATCH 06/12] Apply prepare changes --- src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 39762073..10a7ea77 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -474,9 +474,7 @@ def previewPoints(self, rpc_params: RpcParams) -> None: self.render(-1) @exportRpc(viewer_prefix + viewer_schemas_dict["ruler"]["rpc"]) - def setRuler( - self, rpc_params: RpcParams - ) -> dict[str, float | list[float] | None]: + def setRuler(self, rpc_params: RpcParams) -> dict[str, float | list[float] | None]: validate_schema( rpc_params, self.viewer_schemas_dict["ruler"], self.viewer_prefix ) @@ -504,4 +502,3 @@ def resetRuler(self, rpc_params: RpcParams) -> None: ruler = self.get_ruler() assert ruler is not None ruler.reset() - From 141413a47e67b49f3dc2b174a77ae62a975c6c21 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 15:37:19 +0200 Subject: [PATCH 07/12] mypy --- src/opengeodeweb_viewer/vtk_pipeline.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index 28cf6f4b..4daea76d 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -13,6 +13,7 @@ vtkCompositePolyDataMapper, vtkCompositeDataDisplayAttributes, vtkColorTransferFunction, + vtkRenderer, ) from vtkmodules.vtkRenderingAnnotation import ( vtkScalarBarActor, @@ -87,7 +88,7 @@ def __post_init__(self) -> None: vtkFollower(), self._text_source, self._TEXT_COLOR, offset=-50000.0 ) - def add_to_renderer(self, renderer) -> None: + def add_to_renderer(self, renderer: vtkRenderer) -> None: self.text_follower.SetCamera(renderer.GetActiveCamera()) for actor in ( self.line_actor, @@ -100,8 +101,8 @@ def add_to_renderer(self, renderer) -> None: def _setup_actor( self, - actor, - source, + actor: vtkActor, + source: vtkAlgorithm, color: tuple[float, float, float], line_width: float | None = None, offset: float = -10000.0, @@ -127,7 +128,7 @@ def _make_sphere(self) -> tuple[vtkSphereSource, vtkActor]: source.SetThetaResolution(self._SPHERE_RESOLUTION) return source, self._setup_actor(vtkActor(), source, self._PRIMARY_COLOR) - def update_scale(self, renderer=None) -> None: + def update_scale(self, renderer: vtkRenderer | None = None) -> None: if self._point1 is None or renderer is None: return camera_position = renderer.GetActiveCamera().GetPosition() @@ -160,7 +161,7 @@ def set_endpoints( self, point1: tuple[float, float, float], point2: tuple[float, float, float] | None, - renderer=None, + renderer: vtkRenderer | None = None, ) -> float: self._point1 = point1 self._point2 = point2 From c251e24ef158603ab5a4c1ed9c06c73093af4fa6 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 15:40:57 +0200 Subject: [PATCH 08/12] mypy --- src/opengeodeweb_viewer/vtk_pipeline.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index 4daea76d..5b06eb18 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -79,13 +79,15 @@ class RulerPipeline: text_follower: vtkFollower = field(init=False) def __post_init__(self) -> None: - self.line_actor = self._setup_actor( - vtkActor(), self._line_source, self._PRIMARY_COLOR, line_width=3.0 + self.line_actor = vtkActor() + self._setup_actor( + self.line_actor, self._line_source, self._PRIMARY_COLOR, line_width=3.0 ) self._point1_source, self.point1_actor = self._make_sphere() self._point2_source, self.point2_actor = self._make_sphere() - self.text_follower = self._setup_actor( - vtkFollower(), self._text_source, self._TEXT_COLOR, offset=-50000.0 + self.text_follower = vtkFollower() + self._setup_actor( + self.text_follower, self._text_source, self._TEXT_COLOR, offset=-50000.0 ) def add_to_renderer(self, renderer: vtkRenderer) -> None: @@ -106,7 +108,7 @@ def _setup_actor( color: tuple[float, float, float], line_width: float | None = None, offset: float = -10000.0, - ): + ) -> None: mapper = vtkPolyDataMapper() mapper.SetInputConnection(source.GetOutputPort()) mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters(offset, offset) @@ -120,13 +122,14 @@ def _setup_actor( actor_property.SetDiffuse(0.0) if line_width is not None: actor_property.SetLineWidth(line_width) - return actor def _make_sphere(self) -> tuple[vtkSphereSource, vtkActor]: source = vtkSphereSource() source.SetPhiResolution(self._SPHERE_RESOLUTION) source.SetThetaResolution(self._SPHERE_RESOLUTION) - return source, self._setup_actor(vtkActor(), source, self._PRIMARY_COLOR) + actor = vtkActor() + self._setup_actor(actor, source, self._PRIMARY_COLOR) + return source, actor def update_scale(self, renderer: vtkRenderer | None = None) -> None: if self._point1 is None or renderer is None: From 5c531ce6a0ae071e8715cbbbe5a8e12c0dbd2218 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 16:50:19 +0200 Subject: [PATCH 09/12] package json --- package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/package.json b/package.json index 3bea0838..210fe01f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,16 @@ "name": "@geode/opengeodeweb-viewer", "version": "0.0.0", "description": "", + "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Viewer", + "bugs": { + "url": "https://github.com/Geode-solutions/OpenGeodeWeb-Viewer/issues" + }, + "license": "MIT", + "author": { + "name": "Geode-solutions", + "email": "contact@geode-solutions.com", + "url": "https://geode-solutions.com/" + }, "repository": { "type": "git", "url": "git+https://github.com/Geode-solutions/OpenGeodeWeb-Viewer.git" From 4b5135a02a8da8511fc8ae3a6a33fa51d9621434 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 17:04:06 +0200 Subject: [PATCH 10/12] update schema --- opengeodeweb_viewer_schemas.json | 23 ++++++++----------- .../rpc/viewer/schemas/ruler.json | 23 ++++++++----------- .../rpc/viewer/schemas/ruler.py | 5 ++-- .../rpc/viewer/viewer_protocols.py | 10 ++++---- 4 files changed, 27 insertions(+), 34 deletions(-) diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index e5a564c7..1c17f132 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -2126,25 +2126,22 @@ "rpc": "ruler", "type": "object", "properties": { - "point1": { - "type": "array", - "items": { - "type": "number" - }, - "minItems": 3, - "maxItems": 3 - }, - "point2": { + "points": { "type": "array", "items": { - "type": "number" + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 }, - "minItems": 3, - "maxItems": 3 + "minItems": 1, + "maxItems": 2 } }, "required": [ - "point1" + "points" ], "additionalProperties": false }, diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json index 56a77b40..17c609fb 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json @@ -2,25 +2,22 @@ "rpc": "ruler", "type": "object", "properties": { - "point1": { + "points": { "type": "array", "items": { - "type": "number" + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 }, - "minItems": 3, - "maxItems": 3 - }, - "point2": { - "type": "array", - "items": { - "type": "number" - }, - "minItems": 3, - "maxItems": 3 + "minItems": 1, + "maxItems": 2 } }, "required": [ - "point1" + "points" ], "additionalProperties": false } diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py index f24fe77f..54784b1c 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py @@ -1,6 +1,6 @@ from dataclasses_json import DataClassJsonMixin from dataclasses import dataclass -from typing import List, Optional +from typing import List @dataclass @@ -8,5 +8,4 @@ class Ruler(DataClassJsonMixin): def __post_init__(self) -> None: print(self, flush=True) - point1: List[float] - point2: Optional[List[float]] = None + points: List[List[float]] diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 10a7ea77..6491befd 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -481,17 +481,17 @@ def setRuler(self, rpc_params: RpcParams) -> dict[str, float | list[float] | Non params = schemas.Ruler.from_dict(rpc_params) ruler = self.get_ruler() assert ruler is not None - point1 = (params.point1[0], params.point1[1], params.point1[2]) + point1 = (params.points[0][0], params.points[0][1], params.points[0][2]) point2 = ( - (params.point2[0], params.point2[1], params.point2[2]) - if params.point2 + (params.points[1][0], params.points[1][1], params.points[1][2]) + if len(params.points) > 1 else None ) distance = ruler.set_endpoints(point1, point2, renderer=self.get_renderer()) return { "distance": distance, - "point1": params.point1, - "point2": params.point2, + "point1": list(point1), + "point2": list(point2) if point2 is not None else None, } @exportRpc(viewer_prefix + viewer_schemas_dict["reset_ruler"]["rpc"]) From 43f55ba3d705c250fd0b210a59dffd5e3445cf8a Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 17:12:04 +0200 Subject: [PATCH 11/12] list --- .../rpc/viewer/viewer_protocols.py | 12 ++++-------- src/opengeodeweb_viewer/vtk_pipeline.py | 8 ++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 6491befd..d74bc7fe 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -481,17 +481,13 @@ def setRuler(self, rpc_params: RpcParams) -> dict[str, float | list[float] | Non params = schemas.Ruler.from_dict(rpc_params) ruler = self.get_ruler() assert ruler is not None - point1 = (params.points[0][0], params.points[0][1], params.points[0][2]) - point2 = ( - (params.points[1][0], params.points[1][1], params.points[1][2]) - if len(params.points) > 1 - else None - ) + point1 = params.points[0] + point2 = params.points[1] if len(params.points) > 1 else None distance = ruler.set_endpoints(point1, point2, renderer=self.get_renderer()) return { "distance": distance, - "point1": list(point1), - "point2": list(point2) if point2 is not None else None, + "point1": point1, + "point2": point2, } @exportRpc(viewer_prefix + viewer_schemas_dict["reset_ruler"]["rpc"]) diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index 5b06eb18..5fad5e1f 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -67,8 +67,8 @@ class RulerPipeline: _PRIMARY_COLOR = (60 / 255, 153 / 255, 131 / 255) _TEXT_COLOR = (0.05, 0.05, 0.05) - _point1: tuple[float, float, float] | None = field(default=None, init=False) - _point2: tuple[float, float, float] | None = field(default=None, init=False) + _point1: list[float] | None = field(default=None, init=False) + _point2: list[float] | None = field(default=None, init=False) _line_source: vtkLineSource = field(default_factory=vtkLineSource) line_actor: vtkActor = field(init=False) _point1_source: vtkSphereSource = field(init=False) @@ -162,8 +162,8 @@ def reset(self) -> None: def set_endpoints( self, - point1: tuple[float, float, float], - point2: tuple[float, float, float] | None, + point1: list[float], + point2: list[float] | None, renderer: vtkRenderer | None = None, ) -> float: self._point1 = point1 From 7f9324a87bc1a1e8d108c1bd71e70c884e8a2429 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 14 Aug 2026 17:18:04 +0200 Subject: [PATCH 12/12] update test --- tests/test_viewer_protocols.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_viewer_protocols.py b/tests/test_viewer_protocols.py index 169f8c79..fcf2fedc 100644 --- a/tests/test_viewer_protocols.py +++ b/tests/test_viewer_protocols.py @@ -475,8 +475,7 @@ def test_ruler(server: ServerMonitor, dataset_factory: Callable[..., str]) -> No VtkViewerView.viewer_prefix + VtkViewerView.viewer_schemas_dict["ruler"]["rpc"], [ { - "point1": [-0.2, -0.2, 0.0], - "point2": [0.2, 0.2, 0.0], + "points": [[-0.2, -0.2, 0.0], [0.2, 0.2, 0.0]], } ], )