Skip to content
Merged
32 changes: 32 additions & 0 deletions opengeodeweb_viewer_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -2121,6 +2121,30 @@
],
"additionalProperties": false
},
"ruler": {
"$id": "opengeodeweb_viewer.viewer.ruler",
"rpc": "ruler",
"type": "object",
"properties": {
"points": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 3,
"maxItems": 3
},
"minItems": 1,
"maxItems": 2
}
},
"required": [
"points"
],
"additionalProperties": false
},
"reset_visualization": {
"$id": "opengeodeweb_viewer.viewer.reset_visualization",
"rpc": "reset_visualization",
Expand All @@ -2129,6 +2153,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",
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,3 @@ wslink==1.12.4
yarl>=1
# via aiohttp

opengeodeweb-microservice==1.*,>=1.2.1rc1
2 changes: 2 additions & 0 deletions src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from .shrink import *
from .set_z_scaling import *
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 *
Expand Down
7 changes: 7 additions & 0 deletions src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"rpc": "reset_ruler",
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}
10 changes: 10 additions & 0 deletions src/opengeodeweb_viewer/rpc/viewer/schemas/reset_ruler.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"rpc": "ruler",
"type": "object",
"properties": {
"points": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 3,
"maxItems": 3
},
"minItems": 1,
"maxItems": 2
}
},
"required": [
"points"
],
"additionalProperties": false
}
11 changes: 11 additions & 0 deletions src/opengeodeweb_viewer/rpc/viewer/schemas/ruler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses_json import DataClassJsonMixin
from dataclasses import dataclass
from typing import List


@dataclass
class Ruler(DataClassJsonMixin):
def __post_init__(self) -> None:
print(self, flush=True)

points: List[List[float]]
33 changes: 33 additions & 0 deletions src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -465,3 +472,29 @@ 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 = 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": point1,
"point2": 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()
133 changes: 133 additions & 0 deletions src/opengeodeweb_viewer/vtk_pipeline.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
# 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,
vtkRenderer,
)
from vtkmodules.vtkRenderingAnnotation import (
vtkScalarBarActor,
)
from vtkmodules.vtkRenderingFreeType import vtkVectorText
from vtkmodules.vtkFiltersSources import vtkLineSource, vtkSphereSource
from vtkmodules.vtkCommonDataModel import (
vtkDataObject,
vtkDataSet,
Expand Down Expand Up @@ -55,6 +61,133 @@ class HighlightPipeline:
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: 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)
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 = 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 = vtkFollower()
self._setup_actor(
self.text_follower, self._text_source, self._TEXT_COLOR, offset=-50000.0
)

def add_to_renderer(self, renderer: vtkRenderer) -> 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: vtkActor,
source: vtkAlgorithm,
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)
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)

def _make_sphere(self) -> tuple[vtkSphereSource, vtkActor]:
source = vtkSphereSource()
source.SetPhiResolution(self._SPHERE_RESOLUTION)
source.SetThetaResolution(self._SPHERE_RESOLUTION)
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:
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 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: list[float],
point2: list[float] | None,
renderer: vtkRenderer | None = None,
) -> float:
self._point1 = point1
self._point2 = point2
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)
distance = math.dist(point1, point2)
self._text_source.SetText(f"{distance:.2f}")
self.update_scale(renderer)
return distance


class BlockStyle(TypedDict):
name: str
attribute_location: Literal["point", "cell"]
Expand Down
9 changes: 8 additions & 1 deletion src/opengeodeweb_viewer/vtk_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -309,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 = (
Expand Down
Binary file added tests/data/images/viewer/ruler.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions tests/test_viewer_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,19 @@ 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"],
[
{
"points": [[-0.2, -0.2, 0.0], [0.2, 0.2, 0.0]],
}
],
)
assert server.compare_image("viewer/ruler.jpeg") == True
Loading