diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index 595aac21..fe135e29 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -1673,6 +1673,232 @@ } } }, + "edges": { + "attribute": { + "vertex": { + "attribute": { + "$id": "opengeodeweb_viewer.model.blocks.edges.attribute.vertex.attribute", + "rpc": "attribute", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "item": { + "type": "integer", + "minimum": 0 + }, + "points": { + "type": "array", + "description": "Flat array of [value, r, g, b, ...]", + "items": { + "type": "number" + }, + "minItems": 8 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "no_data_color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "name", + "item", + "points", + "minimum", + "maximum" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + } + } + }, + "width": { + "$id": "opengeodeweb_viewer.model.blocks.edges.width", + "rpc": "width", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "width": { + "type": "number" + } + }, + "required": [ + "id", + "block_ids", + "width" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + }, + "visibility": { + "$id": "opengeodeweb_viewer.model.blocks.edges.visibility", + "rpc": "visibility", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "visibility": { + "type": "boolean" + } + }, + "required": [ + "id", + "block_ids", + "visibility" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + }, + "color": { + "$id": "opengeodeweb_viewer.model.blocks.edges.color", + "rpc": "color", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "color" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + } + }, "points": { "attribute": { "vertex": { @@ -2312,6 +2538,32 @@ } }, "edges": { + "width": { + "$id": "opengeodeweb_viewer.model.edges.width", + "rpc": "width", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "width": { + "type": "number" + } + }, + "required": [ + "id", + "width" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + }, "visibility": { "$id": "opengeodeweb_viewer.model.edges.visibility", "rpc": "visibility", @@ -3295,6 +3547,232 @@ } } }, + "edges": { + "attribute": { + "vertex": { + "attribute": { + "$id": "opengeodeweb_viewer.model.surfaces.edges.attribute.vertex.attribute", + "rpc": "attribute", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "item": { + "type": "integer", + "minimum": 0 + }, + "points": { + "type": "array", + "description": "Flat array of [value, r, g, b, ...]", + "items": { + "type": "number" + }, + "minItems": 8 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "no_data_color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "name", + "item", + "points", + "minimum", + "maximum" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + } + } + }, + "width": { + "$id": "opengeodeweb_viewer.model.surfaces.edges.width", + "rpc": "width", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "width": { + "type": "number" + } + }, + "required": [ + "id", + "block_ids", + "width" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + }, + "visibility": { + "$id": "opengeodeweb_viewer.model.surfaces.edges.visibility", + "rpc": "visibility", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "visibility": { + "type": "boolean" + } + }, + "required": [ + "id", + "block_ids", + "visibility" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + }, + "color": { + "$id": "opengeodeweb_viewer.model.surfaces.edges.color", + "rpc": "color", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "color" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + } + }, "points": { "attribute": { "vertex": { diff --git a/opengeodeweb_viewer_typed_schemas.d.ts b/opengeodeweb_viewer_typed_schemas.d.ts index ad909995..f646a1fd 100644 --- a/opengeodeweb_viewer_typed_schemas.d.ts +++ b/opengeodeweb_viewer_typed_schemas.d.ts @@ -463,6 +463,64 @@ export interface ModelBlocksAttributeVertexAttributeParamsNoDataColor { export interface ModelBlocksAttributeVertexAttributeResponse { } +export interface ModelBlocksEdgesAttributeVertexAttributeParams { + block_ids: number[]; + id: string; + item: number; + maximum: number; + minimum: number; + name: string; + no_data_color?: ModelBlocksEdgesAttributeVertexAttributeParamsNoDataColor; + /** + * Flat array of [value, r, g, b, ...] + */ + points: number[]; +} + +export interface ModelBlocksEdgesAttributeVertexAttributeParamsNoDataColor { + alpha: number; + blue: number; + green: number; + red: number; +} + +export interface ModelBlocksEdgesAttributeVertexAttributeResponse { +} + +export interface ModelBlocksEdgesWidthParams { + block_ids: number[]; + id: string; + width: number; +} + +export interface ModelBlocksEdgesWidthResponse { +} + +export interface ModelBlocksEdgesVisibilityParams { + block_ids: number[]; + id: string; + visibility: boolean; +} + +export interface ModelBlocksEdgesVisibilityResponse { +} + +export interface ModelBlocksEdgesColorParams { + block_ids: number[]; + color: ModelBlocksEdgesColorParamsColor; + id: string; +} + +export interface ModelBlocksEdgesColorParamsColor { + alpha: number; + blue: number; + green: number; + red: number; +} + +export interface ModelBlocksEdgesColorResponse { +} + export interface ModelBlocksPointsAttributeVertexAttributeParams { block_ids: number[]; id: string; @@ -629,6 +687,14 @@ export interface FluffyColorRGBA { red: number; } +export interface ModelEdgesWidthParams { + id: string; + width: number; +} + +export interface ModelEdgesWidthResponse { +} + export interface ModelEdgesVisibilityParams { id: string; visibility: boolean; @@ -889,6 +955,64 @@ export interface ModelSurfacesAttributeVertexAttributeParamsNoDataColor { export interface ModelSurfacesAttributeVertexAttributeResponse { } +export interface ModelSurfacesEdgesAttributeVertexAttributeParams { + block_ids: number[]; + id: string; + item: number; + maximum: number; + minimum: number; + name: string; + no_data_color?: ModelSurfacesEdgesAttributeVertexAttributeParamsNoDataColor; + /** + * Flat array of [value, r, g, b, ...] + */ + points: number[]; +} + +export interface ModelSurfacesEdgesAttributeVertexAttributeParamsNoDataColor { + alpha: number; + blue: number; + green: number; + red: number; +} + +export interface ModelSurfacesEdgesAttributeVertexAttributeResponse { +} + +export interface ModelSurfacesEdgesWidthParams { + block_ids: number[]; + id: string; + width: number; +} + +export interface ModelSurfacesEdgesWidthResponse { +} + +export interface ModelSurfacesEdgesVisibilityParams { + block_ids: number[]; + id: string; + visibility: boolean; +} + +export interface ModelSurfacesEdgesVisibilityResponse { +} + +export interface ModelSurfacesEdgesColorParams { + block_ids: number[]; + color: ModelSurfacesEdgesColorParamsColor; + id: string; +} + +export interface ModelSurfacesEdgesColorParamsColor { + alpha: number; + blue: number; + green: number; + red: number; +} + +export interface ModelSurfacesEdgesColorResponse { +} + export interface ModelSurfacesPointsAttributeVertexAttributeParams { block_ids: number[]; id: string; @@ -1350,6 +1474,16 @@ export interface Schemas { readonly attribute: (typeof json)["opengeodeweb_viewer"]["model"]["blocks"]["attribute"]["vertex"]["attribute"] & TypedSchema; }; }; + readonly edges: { + readonly attribute: { + readonly vertex: { + readonly attribute: (typeof json)["opengeodeweb_viewer"]["model"]["blocks"]["edges"]["attribute"]["vertex"]["attribute"] & TypedSchema; + }; + }; + readonly width: (typeof json)["opengeodeweb_viewer"]["model"]["blocks"]["edges"]["width"] & TypedSchema; + readonly visibility: (typeof json)["opengeodeweb_viewer"]["model"]["blocks"]["edges"]["visibility"] & TypedSchema; + readonly color: (typeof json)["opengeodeweb_viewer"]["model"]["blocks"]["edges"]["color"] & TypedSchema; + }; readonly points: { readonly attribute: { readonly vertex: { @@ -1373,6 +1507,7 @@ export interface Schemas { readonly color: (typeof json)["opengeodeweb_viewer"]["model"]["corners"]["color"] & TypedSchema; }; readonly edges: { + readonly width: (typeof json)["opengeodeweb_viewer"]["model"]["edges"]["width"] & TypedSchema; readonly visibility: (typeof json)["opengeodeweb_viewer"]["model"]["edges"]["visibility"] & TypedSchema; }; readonly lines: { @@ -1415,6 +1550,16 @@ export interface Schemas { readonly attribute: (typeof json)["opengeodeweb_viewer"]["model"]["surfaces"]["attribute"]["vertex"]["attribute"] & TypedSchema; }; }; + readonly edges: { + readonly attribute: { + readonly vertex: { + readonly attribute: (typeof json)["opengeodeweb_viewer"]["model"]["surfaces"]["edges"]["attribute"]["vertex"]["attribute"] & TypedSchema; + }; + }; + readonly width: (typeof json)["opengeodeweb_viewer"]["model"]["surfaces"]["edges"]["width"] & TypedSchema; + readonly visibility: (typeof json)["opengeodeweb_viewer"]["model"]["surfaces"]["edges"]["visibility"] & TypedSchema; + readonly color: (typeof json)["opengeodeweb_viewer"]["model"]["surfaces"]["edges"]["color"] & TypedSchema; + }; readonly points: { readonly attribute: { readonly vertex: { diff --git a/pyproject.toml b/pyproject.toml index f3cb8e58..95b9d21c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,11 +61,15 @@ where = ["src"] "opengeodeweb_viewer.rpc.model.surfaces.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.surfaces.points.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.surfaces.points.attribute.vertex.schemas" = ["*.json"] +"opengeodeweb_viewer.rpc.model.surfaces.edges.schemas" = ["*.json"] +"opengeodeweb_viewer.rpc.model.surfaces.edges.attribute.vertex.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.surfaces.attribute.polygon.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.surfaces.attribute.vertex.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.blocks.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.blocks.points.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.blocks.points.attribute.vertex.schemas" = ["*.json"] +"opengeodeweb_viewer.rpc.model.blocks.edges.schemas" = ["*.json"] +"opengeodeweb_viewer.rpc.model.blocks.edges.attribute.vertex.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.blocks.attribute.polyhedron.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.model.blocks.attribute.vertex.schemas" = ["*.json"] "opengeodeweb_viewer.rpc.viewer.schemas" = ["*.json"] diff --git a/requirements.txt b/requirements.txt index cf63d0c6..8d21d552 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,4 +63,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 30693dd8..faec84bb 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from opengeodeweb_viewer.utils_functions import ColorClassProtocol - from opengeodeweb_viewer.vtk_pipeline import VtkPipeline + from opengeodeweb_viewer.vtk_pipeline import ColorMapKey, VtkPipeline logger = logging.getLogger(__name__) @@ -62,6 +62,12 @@ def remove_object(self, data_id: str) -> None: renderer.RemoveViewProp(pipeline.points_scalar_bar) for size_actor in pipeline.points_size_actors.values(): renderer.RemoveActor(size_actor.actor) + if pipeline.edges_actor is not None: + renderer.RemoveActor(pipeline.edges_actor) + for width_actor in pipeline.edges_width_actors.values(): + renderer.RemoveActor(width_actor.actor) + if pipeline.edges_scalar_bar is not None: + renderer.RemoveViewProp(pipeline.edges_scalar_bar) renderer.RemoveViewProp(pipeline.scalar_bar) for scalar_bar in pipeline.scalar_bars.values(): renderer.RemoveViewProp(scalar_bar.actor) @@ -71,65 +77,90 @@ def remove_object(self, data_id: str) -> None: def set_visibility(self, data_id: str, *, visibility: bool) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.actor.SetVisibility(visibility) - self.set_points_actors_visibility(pipeline, visibility=visibility) + self.set_style_actors_visibility(pipeline, visibility=visibility) if not visibility: pipeline.scalar_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() + if pipeline.edges_scalar_bar is not None: + pipeline.edges_scalar_bar.VisibilityOff() else: if ( pipeline.mapper.GetScalarVisibility() and pipeline.mapper.GetLookupTable() is not None ): pipeline.scalar_bar.VisibilityOn() - self.update_points_scalar_bar_visibility(pipeline) - for style in pipeline.block_styles.values(): - if style.get("name"): - for scalar_bar in pipeline.scalar_bars.values(): - if scalar_bar.actor.GetLookupTable() is not None: - scalar_bar.actor.VisibilityOn() - break + self.update_style_scalar_bars_visibility(pipeline) self.update_scalar_bars_layout() - def set_points_actors_visibility(self, pipeline: VtkPipeline, *, visibility: bool) -> None: + def set_style_actors_visibility(self, pipeline: VtkPipeline, *, visibility: bool) -> None: if pipeline.points_actor is not None: pipeline.points_actor.SetVisibility(visibility and pipeline.points_visibility) for size_actor in pipeline.points_size_actors.values(): size_actor.actor.SetVisibility(visibility) + if pipeline.edges_actor is not None: + pipeline.edges_actor.SetVisibility(visibility and pipeline.edges_visibility) + for width_actor in pipeline.edges_width_actors.values(): + width_actor.actor.SetVisibility(visibility) def set_opacity(self, data_id: str, opacity: float) -> None: actor = self.get_vtk_pipeline(data_id).actor actor.GetProperty().SetOpacity(opacity) def set_color(self, data_id: str, color: ColorClassProtocol) -> None: - mapper = self.get_vtk_pipeline(data_id).mapper - mapper.ScalarVisibilityOff() - actor = self.get_vtk_pipeline(data_id).actor + pipeline = self.get_vtk_pipeline(data_id) + pipeline.mapper.ScalarVisibilityOff() + actor = pipeline.actor actor.GetProperty().SetColor([color.red / 255, color.green / 255, color.blue / 255]) actor.GetProperty().SetOpacity(color.alpha) + # Points and edges bars that reused the polygons one now show their own + pipeline.scalar_bar.VisibilityOff() + self.update_style_scalar_bars_visibility(pipeline) + self.update_scalar_bars_layout() def set_edges_visibility(self, data_id: str, *, visibility: bool) -> None: if self.get_viewer_data(data_id).viewer_elements_type == "edges": self.set_visibility(data_id, visibility=visibility) - else: - actor = self.get_vtk_pipeline(data_id).actor - actor.GetProperty().SetEdgeVisibility(visibility) + return + pipeline = self.get_vtk_pipeline(data_id) + if pipeline.edges_actor is None: + pipeline.actor.GetProperty().SetEdgeVisibility(visibility) + return + pipeline.edges_visibility = visibility + pipeline.edges_actor.SetVisibility(pipeline.actor.GetVisibility() and visibility) + self.update_style_scalar_bars_visibility(pipeline) + self.update_scalar_bars_layout() def set_edges_width(self, data_id: str, width: float) -> None: - actor = self.get_vtk_pipeline(data_id).actor + pipeline = self.get_vtk_pipeline(data_id) if self.get_viewer_data(data_id).viewer_elements_type == "edges": - actor.GetProperty().SetLineWidth(width) - else: - actor.GetProperty().SetEdgeWidth(width) + pipeline.actor.GetProperty().SetLineWidth(width) + return + # VTK draws polygon edges with the line width, the edge width property is not rendered + pipeline.actor.GetProperty().SetLineWidth(width) + if pipeline.edges_actor is not None: + pipeline.edges_actor.GetProperty().SetLineWidth(width) def set_edges_color(self, data_id: str, color: ColorClassProtocol) -> None: if self.get_viewer_data(data_id).viewer_elements_type == "edges": self.set_color(data_id, color) - else: - actor = self.get_vtk_pipeline(data_id).actor - actor.GetProperty().SetEdgeColor([color.red / 255, color.green / 255, color.blue / 255]) + return + pipeline = self.get_vtk_pipeline(data_id) + rgb = [color.red / 255, color.green / 255, color.blue / 255] + pipeline.actor.GetProperty().SetEdgeColor(rgb) + if pipeline.edges_actor is None or pipeline.edges_mapper is None: + return + # A constant color replaces the edges attribute and its scalar bar + pipeline.edges_mapper.ScalarVisibilityOff() + pipeline.edges_actor.GetProperty().SetColor(rgb) + pipeline.edges_actor.GetProperty().SetOpacity(color.alpha) + if pipeline.edges_scalar_bar is not None: + pipeline.edges_scalar_bar.SetLookupTable(None) + pipeline.edges_color_map_key = None + self.update_style_scalar_bars_visibility(pipeline) + self.update_scalar_bars_layout() def set_points_visibility(self, data_id: str, *, visibility: bool) -> None: if self.get_viewer_data(data_id).viewer_elements_type == "points": @@ -139,7 +170,7 @@ def set_points_visibility(self, data_id: str, *, visibility: bool) -> None: if pipeline.points_actor is not None: pipeline.points_visibility = visibility pipeline.points_actor.SetVisibility(visibility) - self.update_points_scalar_bar_visibility(pipeline) + self.update_style_scalar_bars_visibility(pipeline) self.update_scalar_bars_layout() else: pipeline.actor.GetProperty().SetVertexVisibility(visibility) @@ -178,25 +209,92 @@ def ensure_points_actor(self, pipeline: VtkPipeline) -> vtkActor: pipeline.points_scalar_bar = scalar_bar return actor - def update_points_scalar_bar_visibility(self, pipeline: VtkPipeline) -> None: - # A points scalar bar only holds a lookup table when it differs from the polygons one - bar = pipeline.points_scalar_bar - if bar is None: - return - bar.SetVisibility( - pipeline.actor.GetVisibility() + @staticmethod + def polygons_color_map_key(pipeline: VtkPipeline) -> ColorMapKey | None: + mapper = pipeline.mapper + if not mapper.GetScalarVisibility() or mapper.GetLookupTable() is None: + return None + minimum, maximum = mapper.GetScalarRange() + return ( + mapper.GetArrayName(), + mapper.GetArrayComponent(), + minimum, + maximum, + tuple(pipeline.color_map_points), + ) + + def update_style_scalar_bars_visibility(self, pipeline: VtkPipeline) -> None: + # Points and edges bars show only when no visible bar already holds the same attribute + visible = bool(pipeline.actor.GetVisibility()) + polygons_key = self.polygons_color_map_key(pipeline) + points_bar_visible = ( + visible and pipeline.points_visibility - and bar.GetLookupTable() is not None + and pipeline.points_color_map_key is not None + and pipeline.points_color_map_key != polygons_key ) + if pipeline.points_scalar_bar is not None: + pipeline.points_scalar_bar.SetVisibility(points_bar_visible) + if pipeline.edges_scalar_bar is not None: + pipeline.edges_scalar_bar.SetVisibility( + visible + and pipeline.edges_visibility + and pipeline.edges_color_map_key is not None + and pipeline.edges_color_map_key != polygons_key + and not ( + points_bar_visible + and pipeline.edges_color_map_key == pipeline.points_color_map_key + ) + ) + + def ensure_edges_actor(self, pipeline: VtkPipeline) -> vtkActor: + # Edges get their own actor so their scalars stay independent from the polygons + if pipeline.edges_actor is not None: + return pipeline.edges_actor + mapper = vtkDataSetMapper() + mapper.SetInputConnection(pipeline.filter.GetOutputPort()) + mapper.ScalarVisibilityOff() + mapper.SetResolveCoincidentTopologyLineOffsetParameters(0, -4) + actor = vtkActor() + actor.SetMapper(mapper) + actor.PickableOff() + main_property = pipeline.actor.GetProperty() + edges_property = actor.GetProperty() + edges_property.SetRepresentationToWireframe() + edges_property.SetLineWidth(main_property.GetLineWidth()) + edges_property.SetColor(*main_property.GetEdgeColor()) + pipeline.edges_visibility = bool(main_property.GetEdgeVisibility()) + actor.SetVisibility(pipeline.actor.GetVisibility() and pipeline.edges_visibility) + main_property.EdgeVisibilityOff() + scalar_bar = vtkScalarBarActor() + scalar_bar.VisibilityOff() + render_window = self.getView("-1") + renderer = render_window.GetRenderers().GetFirstRenderer() + renderer.AddActor(actor) + renderer.AddViewProp(scalar_bar) + pipeline.edges_mapper = mapper + pipeline.edges_actor = actor + pipeline.edges_scalar_bar = scalar_bar + return actor def set_points_color(self, data_id: str, color: ColorClassProtocol) -> None: if self.get_viewer_data(data_id).viewer_elements_type == "points": self.set_color(data_id, color) - else: - actor = self.get_vtk_pipeline(data_id).actor - actor.GetProperty().SetVertexColor( - [color.red / 255, color.green / 255, color.blue / 255] - ) + return + pipeline = self.get_vtk_pipeline(data_id) + rgb = [color.red / 255, color.green / 255, color.blue / 255] + pipeline.actor.GetProperty().SetVertexColor(rgb) + if pipeline.points_actor is None or pipeline.points_mapper is None: + return + # A constant color replaces the points attribute and its scalar bar + pipeline.points_mapper.ScalarVisibilityOff() + pipeline.points_actor.GetProperty().SetColor(rgb) + pipeline.points_actor.GetProperty().SetOpacity(color.alpha) + if pipeline.points_scalar_bar is not None: + pipeline.points_scalar_bar.SetLookupTable(None) + pipeline.points_color_map_key = None + self.update_style_scalar_bars_visibility(pipeline) + self.update_scalar_bars_layout() def set_blocks_visibility( self, data_id: str, block_ids: list[int], *, visibility: bool @@ -212,6 +310,7 @@ def set_blocks_visibility( for block_id in block_ids: visibility_attributes.SetBlockVisibility(blocks[block_id], visibility) pipeline.update_points_size_actors() + pipeline.update_edges_width_actors() dataset = mapper.GetInputDataObject(0, 0) if not isinstance(dataset, vtkMultiBlockDataSet): return diff --git a/src/opengeodeweb_viewer/rpc/mesh/edges/attribute/vertex/edges_attribute_vertex_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/edges/attribute/vertex/edges_attribute_vertex_protocols.py index 5e48b17f..74c11aef 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/edges/attribute/vertex/edges_attribute_vertex_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/edges/attribute/vertex/edges_attribute_vertex_protocols.py @@ -22,5 +22,8 @@ def __init__(self) -> None: def set_mesh_edges_vertex_attribute( self, params: schemas.Attribute ) -> schemas.AttributeResponse: - self.display_attribute_on_vertices(params.id, params) + if self.get_viewer_data(params.id).viewer_elements_type == "edges": + self.display_attribute_on_vertices(params.id, params) + else: + self.display_attribute_on_edges(params.id, params) return schemas.AttributeResponse() diff --git a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py index 238e0239..f3377229 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py @@ -2,6 +2,7 @@ # Standard library imports from pathlib import Path +from typing import TYPE_CHECKING from opengeodeweb_microservice.database.data import Data @@ -20,10 +21,13 @@ AttributeProtocol, create_color_transfer_function, ) -from opengeodeweb_viewer.vtk_pipeline import VtkPipeline +from opengeodeweb_viewer.vtk_pipeline import ColorMapKey, VtkPipeline from . import schemas +if TYPE_CHECKING: + from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor + logger = logging.getLogger(__name__) @@ -114,46 +118,56 @@ def display_attribute_on_vertices(self, data_id: str, attribute: AttributeProtoc def display_attribute_on_points(self, data_id: str, attribute: AttributeProtocol) -> None: pipeline = self.get_vtk_pipeline(data_id) self.ensure_points_actor(pipeline) - mapper = pipeline.points_mapper - if mapper is None: + if pipeline.points_mapper is None: return + pipeline.points_color_map_key = self.display_attribute_on_style_mapper( + pipeline.points_mapper, pipeline.points_scalar_bar, attribute + ) + self.update_style_scalar_bars_visibility(pipeline) + self.update_scalar_bars_layout() + + def display_attribute_on_edges(self, data_id: str, attribute: AttributeProtocol) -> None: + pipeline = self.get_vtk_pipeline(data_id) + self.ensure_edges_actor(pipeline) + if pipeline.edges_mapper is None: + return + pipeline.edges_color_map_key = self.display_attribute_on_style_mapper( + pipeline.edges_mapper, pipeline.edges_scalar_bar, attribute + ) + self.update_style_scalar_bars_visibility(pipeline) + self.update_scalar_bars_layout() + + def display_attribute_on_style_mapper( + self, + mapper: vtkDataSetMapper, + scalar_bar: vtkScalarBarActor | None, + attribute: AttributeProtocol, + ) -> ColorMapKey: # Field data mode avoids touching the active scalars shared with the polygons mapper.ScalarVisibilityOn() mapper.SetScalarModeToUsePointFieldData() mapper.ColorByArrayComponent(attribute.name, attribute.item) - if self.shares_polygons_color_map(data_id, attribute): - # Same color map as the polygons: reuse their scalar bar instead of adding one - lut = pipeline.mapper.GetLookupTable() - points_bar_lut = None - else: - lut = create_color_transfer_function( - attribute.points, - attribute.minimum, - attribute.maximum, - attribute.item, - attribute.no_data_color, - ) - lut.SetRange(attribute.minimum, attribute.maximum) - points_bar_lut = lut + lut = create_color_transfer_function( + attribute.points, + attribute.minimum, + attribute.maximum, + attribute.item, + attribute.no_data_color, + ) + lut.SetRange(attribute.minimum, attribute.maximum) mapper.SetLookupTable(lut) mapper.SetScalarRange(attribute.minimum, attribute.maximum) mapper.UseLookupTableScalarRangeOff() mapper.InterpolateScalarsBeforeMappingOn() - if pipeline.points_scalar_bar is not None: - pipeline.points_scalar_bar.SetLookupTable(points_bar_lut) - self.update_points_scalar_bar_visibility(pipeline) - self.update_scalar_bars_layout() - - def shares_polygons_color_map(self, data_id: str, attribute: AttributeProtocol) -> bool: - pipeline = self.get_vtk_pipeline(data_id) - mapper = pipeline.mapper - return bool( - mapper.GetScalarVisibility() - and mapper.GetLookupTable() is not None - and mapper.GetArrayName() == attribute.name - and mapper.GetArrayComponent() == attribute.item - and tuple(mapper.GetScalarRange()) == (attribute.minimum, attribute.maximum) - and pipeline.color_map_points == list(attribute.points) + # Whether this bar shows or reuses an identical one is decided on each visibility update + if scalar_bar is not None: + scalar_bar.SetLookupTable(lut) + return ( + attribute.name, + attribute.item, + attribute.minimum, + attribute.maximum, + tuple(attribute.points), ) def display_attribute_on_cells(self, data_id: str, attribute: AttributeProtocol) -> None: @@ -174,6 +188,8 @@ def display_scalar_range(self, data_id: str, minimum: float, maximum: float) -> data.mapper.SetScalarRange(minimum, maximum) data.mapper.GetLookupTable().SetRange(minimum, maximum) data.mapper.UseLookupTableScalarRangeOff() + self.update_style_scalar_bars_visibility(data) + self.update_scalar_bars_layout() def setup_color_map(self, data_id: str, attribute: AttributeProtocol) -> None: data = self.get_vtk_pipeline(data_id) @@ -192,6 +208,7 @@ def setup_color_map(self, data_id: str, attribute: AttributeProtocol) -> None: data.scalar_bar.SetLookupTable(lut) data.scalar_bar.VisibilityOn() + self.update_style_scalar_bars_visibility(data) self.update_scalar_bars_layout() @typed_rpc(mesh_prefix, schemas.highlight_route) diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/__init__.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/__init__.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/__init__.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/blocks_edges_attribute_vertex_protocols.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/blocks_edges_attribute_vertex_protocols.py new file mode 100644 index 00000000..bbee371e --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/blocks_edges_attribute_vertex_protocols.py @@ -0,0 +1,30 @@ +# Standard library imports +from pathlib import Path + +# Third party imports +from opengeodeweb_microservice.schemas import get_schemas_dict + +# Local application imports +from opengeodeweb_viewer.rpc.model.model_protocols import VtkModelView +from opengeodeweb_viewer.typed_rpc import typed_rpc + +from . import schemas + + +class VtkModelBlocksEdgesAttributeVertexView(VtkModelView): + model_blocks_edges_attribute_vertex_prefix = ( + "opengeodeweb_viewer.model.blocks.edges.attribute.vertex." + ) + model_blocks_edges_attribute_vertex_schemas_dict = get_schemas_dict( + Path(__file__).parent / "schemas" + ) + + def __init__(self) -> None: + super().__init__() + + @typed_rpc(model_blocks_edges_attribute_vertex_prefix, schemas.attribute_route) + def set_model_blocks_edges_vertex_attribute( + self, params: schemas.Attribute + ) -> schemas.AttributeResponse: + self.set_component_edges_attribute(params.id, "blocks", params.block_ids, params) + return schemas.AttributeResponse() diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/__init__.py new file mode 100644 index 00000000..a563b254 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/__init__.py @@ -0,0 +1 @@ +from .attribute import * diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/attribute.json b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/attribute.json new file mode 100644 index 00000000..bf36e3b9 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/attribute.json @@ -0,0 +1,88 @@ +{ + "rpc": "attribute", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "item": { + "type": "integer", + "minimum": 0 + }, + "points": { + "type": "array", + "description": "Flat array of [value, r, g, b, ...]", + "items": { + "type": "number" + }, + "minItems": 8 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "no_data_color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "name", + "item", + "points", + "minimum", + "maximum" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/attribute.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/attribute.py new file mode 100644 index 00000000..3c07a38b --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/attribute/vertex/schemas/attribute.py @@ -0,0 +1,51 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class NoDataColor(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + alpha: float + blue: int + green: int + red: int + + +@dataclass +class Attribute(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + id: str + item: int + maximum: float + minimum: float + name: str + points: List[float] + """Flat array of [value, r, g, b, ...]""" + + no_data_color: Optional[NoDataColor] = None + + +@dataclass +class AttributeResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +attribute_route = Route( + schema=load_schema(Path(__file__)), + params=Attribute, + response=AttributeResponse, +) + +__all__ = ["NoDataColor", "Attribute", "AttributeResponse", "attribute_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/model_blocks_edges_protocols.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/model_blocks_edges_protocols.py new file mode 100644 index 00000000..26595563 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/model_blocks_edges_protocols.py @@ -0,0 +1,38 @@ +# Standard library imports +from pathlib import Path + +# Third party imports +from opengeodeweb_microservice.schemas import get_schemas_dict + +# Local application imports +from opengeodeweb_viewer.rpc.model.model_protocols import VtkModelView +from opengeodeweb_viewer.typed_rpc import typed_rpc + +from . import schemas + + +class VtkModelBlocksEdgesView(VtkModelView): + model_blocks_edges_prefix = "opengeodeweb_viewer.model.blocks.edges." + model_blocks_edges_schemas_dict = get_schemas_dict(Path(__file__).parent / "schemas") + + def __init__(self) -> None: + super().__init__() + + @typed_rpc(model_blocks_edges_prefix, schemas.visibility_route) + def set_model_blocks_edges_visibility( + self, params: schemas.Visibility + ) -> schemas.VisibilityResponse: + self.set_component_edges_visibility( + params.id, "blocks", params.block_ids, visibility=params.visibility + ) + return schemas.VisibilityResponse() + + @typed_rpc(model_blocks_edges_prefix, schemas.color_route) + def set_model_blocks_edges_color(self, params: schemas.Color) -> schemas.ColorResponse: + self.set_component_edges_color(params.id, "blocks", params.block_ids, params.color) + return schemas.ColorResponse() + + @typed_rpc(model_blocks_edges_prefix, schemas.width_route) + def set_model_blocks_edges_width(self, params: schemas.Width) -> schemas.WidthResponse: + self.set_component_edges_width(params.id, "blocks", params.block_ids, params.width) + return schemas.WidthResponse() diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/__init__.py new file mode 100644 index 00000000..cfe6b7ea --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/__init__.py @@ -0,0 +1,3 @@ +from .width import * +from .visibility import * +from .color import * diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/color.json b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/color.json new file mode 100644 index 00000000..54fcb685 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/color.json @@ -0,0 +1,62 @@ +{ + "rpc": "color", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "color" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/color.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/color.py new file mode 100644 index 00000000..68d42901 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/color.py @@ -0,0 +1,44 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List + + +@dataclass +class ColorClass(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + alpha: float + blue: int + green: int + red: int + + +@dataclass +class Color(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + color: ColorClass + id: str + + +@dataclass +class ColorResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +color_route = Route( + schema=load_schema(Path(__file__)), + params=Color, + response=ColorResponse, +) + +__all__ = ["ColorClass", "Color", "ColorResponse", "color_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/visibility.json b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/visibility.json new file mode 100644 index 00000000..77b18063 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/visibility.json @@ -0,0 +1,33 @@ +{ + "rpc": "visibility", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "visibility": { + "type": "boolean" + } + }, + "required": [ + "id", + "block_ids", + "visibility" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/visibility.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/visibility.py new file mode 100644 index 00000000..1b9616b0 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/visibility.py @@ -0,0 +1,33 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List + + +@dataclass +class Visibility(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + id: str + visibility: bool + + +@dataclass +class VisibilityResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +visibility_route = Route( + schema=load_schema(Path(__file__)), + params=Visibility, + response=VisibilityResponse, +) + +__all__ = ["Visibility", "VisibilityResponse", "visibility_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/width.json b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/width.json new file mode 100644 index 00000000..d98585fe --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/width.json @@ -0,0 +1,33 @@ +{ + "rpc": "width", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "width": { + "type": "number" + } + }, + "required": [ + "id", + "block_ids", + "width" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/width.py b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/width.py new file mode 100644 index 00000000..c1231a48 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/blocks/edges/schemas/width.py @@ -0,0 +1,33 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List + + +@dataclass +class Width(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + id: str + width: float + + +@dataclass +class WidthResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +width_route = Route( + schema=load_schema(Path(__file__)), + params=Width, + response=WidthResponse, +) + +__all__ = ["Width", "WidthResponse", "width_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/edges/model_edges_protocols.py b/src/opengeodeweb_viewer/rpc/model/edges/model_edges_protocols.py index fedec8d5..314ced91 100644 --- a/src/opengeodeweb_viewer/rpc/model/edges/model_edges_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/edges/model_edges_protocols.py @@ -20,5 +20,10 @@ def __init__(self) -> None: @typed_rpc(model_edges_prefix, schemas.visibility_route) def set_model_edges_visibility(self, params: schemas.Visibility) -> schemas.VisibilityResponse: - self.set_edges_visibility(params.id, visibility=params.visibility) + self.set_model_edges_visibility_on_all(params.id, visibility=params.visibility) return schemas.VisibilityResponse() + + @typed_rpc(model_edges_prefix, schemas.width_route) + def set_model_edges_width(self, params: schemas.Width) -> schemas.WidthResponse: + self.set_model_edges_width_on_all(params.id, params.width) + return schemas.WidthResponse() diff --git a/src/opengeodeweb_viewer/rpc/model/edges/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/model/edges/schemas/__init__.py index cbc73c09..e667a494 100644 --- a/src/opengeodeweb_viewer/rpc/model/edges/schemas/__init__.py +++ b/src/opengeodeweb_viewer/rpc/model/edges/schemas/__init__.py @@ -1 +1,2 @@ +from .width import * from .visibility import * diff --git a/src/opengeodeweb_viewer/rpc/model/edges/schemas/width.json b/src/opengeodeweb_viewer/rpc/model/edges/schemas/width.json new file mode 100644 index 00000000..7fd55c44 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/edges/schemas/width.json @@ -0,0 +1,25 @@ +{ + "rpc": "width", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "width": { + "type": "number" + } + }, + "required": [ + "id", + "width" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/edges/schemas/width.py b/src/opengeodeweb_viewer/rpc/model/edges/schemas/width.py new file mode 100644 index 00000000..b2776647 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/edges/schemas/width.py @@ -0,0 +1,31 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass + + +@dataclass +class Width(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + id: str + width: float + + +@dataclass +class WidthResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +width_route = Route( + schema=load_schema(Path(__file__)), + params=Width, + response=WidthResponse, +) + +__all__ = ["Width", "WidthResponse", "width_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index ea7dd55c..1dd8d562 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -30,6 +30,8 @@ ) from opengeodeweb_viewer.vtk_pipeline import ( BlockStyle, + EdgesWidthActor, + ModelEdgesStyle, ModelPointsStyle, ModelScalarBar, PointsSizeActor, @@ -62,6 +64,8 @@ class ColorResult(TypedDict): POINTS_COMPONENT_TYPES = ("lines", "surfaces", "blocks") +# Lines are edges themselves and corners have none, so only these components get an edges style +EDGES_COMPONENT_TYPES = ("surfaces", "blocks") class VtkModelView(VtkObjectView): @@ -151,13 +155,19 @@ def scalar_bar_key(active_attrs: dict[str, BlockStyle], style: BlockStyle, label 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 = [ - (block_id, str(block_id), style) for block_id, style in pipeline.block_styles.items() - ] + [ - (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 - ] + displayed_styles = ( + [(block_id, str(block_id), style) for block_id, style in pipeline.block_styles.items()] + + [ + (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 + ] + + [ + (block_id, f"{block_id} edges", edges_style.attribute) + for block_id, edges_style in pipeline.edges_block_styles.items() + if edges_style.attribute is not None and edges_style.visibility + ] + ) active_attrs: dict[str, BlockStyle] = {} active_block_ids: dict[str, list[int]] = {} for block_id, label, style in displayed_styles: @@ -180,7 +190,7 @@ def setup_model_color_map(self, pipeline: VtkPipeline) -> None: no_data_color = style["no_data_color"] lut = create_color_transfer_function(points, minimum, maximum, item, no_data_color) bar.SetLookupTable(lut) - bar.VisibilityOn() + bar.SetVisibility(pipeline.actor.GetVisibility()) for name, scalar_bar in pipeline.scalar_bars.items(): if name not in active_attrs: scalar_bar.actor.VisibilityOff() @@ -354,6 +364,123 @@ def set_model_points_size_on_all(self, data_id: str, size: float) -> None: style.size = size self.sync_points_size_actors(pipeline) + def ensure_model_edges_styles(self, pipeline: VtkPipeline) -> dict[int, ModelEdgesStyle]: + # Components edges leave the main actor once styled individually + if pipeline.edges_block_styles: + return pipeline.edges_block_styles + main_property = pipeline.actor.GetProperty() + red, green, blue = main_property.GetEdgeColor() + for block_id, component_type in pipeline.block_component_types().items(): + if component_type in EDGES_COMPONENT_TYPES: + pipeline.edges_block_styles[block_id] = ModelEdgesStyle( + width=main_property.GetEdgeWidth(), + visibility=bool(main_property.GetEdgeVisibility()), + color=(red, green, blue), + ) + main_property.EdgeVisibilityOff() + return pipeline.edges_block_styles + + def sync_edges_width_actors(self, pipeline: VtkPipeline) -> None: + renderer = self.get_renderer() + widths = {style.width for style in pipeline.edges_block_styles.values()} + for width in list(pipeline.edges_width_actors): + if width not in widths: + renderer.RemoveActor(pipeline.edges_width_actors.pop(width).actor) + for width in widths - pipeline.edges_width_actors.keys(): + mapper = vtkCompositePolyDataMapper() + mapper.SetInputDataObject(pipeline.mapper.GetInputDataObject(0, 0)) + mapper.ScalarVisibilityOn() + mapper.SetColorModeToMapScalars() + mapper.SetResolveCoincidentTopologyLineOffsetParameters(0, -4) + actor = vtkActor() + actor.SetMapper(mapper) + actor.PickableOff() + actor.SetVisibility(pipeline.actor.GetVisibility()) + edges_property = actor.GetProperty() + edges_property.SetRepresentationToWireframe() + edges_property.SetLineWidth(width) + pipeline.edges_width_actors[width] = EdgesWidthActor(mapper=mapper, actor=actor) + renderer.AddActor(actor) + pipeline.update_edges_width_actors() + + def component_edges_styles( + self, data_id: str, component_type: str, block_ids: list[int] + ) -> list[ModelEdgesStyle]: + pipeline = self.get_vtk_pipeline(data_id) + styles = self.ensure_model_edges_styles(pipeline) + component_types = pipeline.block_component_types() + return [ + styles[block_id] + for block_id in block_ids + if block_id in styles and component_types.get(block_id) == component_type + ] + + def set_component_edges_visibility( + self, data_id: str, component_type: str, block_ids: list[int], *, visibility: bool + ) -> None: + for style in self.component_edges_styles(data_id, component_type, block_ids): + style.visibility = visibility + pipeline = self.get_vtk_pipeline(data_id) + self.sync_edges_width_actors(pipeline) + self.setup_model_color_map(pipeline) + + def set_component_edges_color( + self, data_id: str, component_type: str, block_ids: list[int], color: ColorProtocol + ) -> None: + for style in self.component_edges_styles(data_id, component_type, block_ids): + style.color = (color.red / 255, color.green / 255, color.blue / 255) + style.opacity = color.alpha + style.attribute = None + pipeline = self.get_vtk_pipeline(data_id) + self.sync_edges_width_actors(pipeline) + self.setup_model_color_map(pipeline) + + def set_component_edges_attribute( + self, + data_id: str, + component_type: str, + block_ids: list[int], + attribute: AttributeProtocol, + ) -> None: + for style in self.component_edges_styles(data_id, component_type, block_ids): + style.attribute = BlockStyle( + name=attribute.name, + attribute_location="point", + points=attribute.points, + minimum=attribute.minimum, + maximum=attribute.maximum, + item=attribute.item, + no_data_color=attribute.no_data_color, + ) + pipeline = self.get_vtk_pipeline(data_id) + self.sync_edges_width_actors(pipeline) + self.setup_model_color_map(pipeline) + + def set_component_edges_width( + self, data_id: str, component_type: str, block_ids: list[int], width: float + ) -> None: + for style in self.component_edges_styles(data_id, component_type, block_ids): + style.width = width + self.sync_edges_width_actors(self.get_vtk_pipeline(data_id)) + + def set_model_edges_visibility_on_all(self, data_id: str, *, visibility: bool) -> None: + pipeline = self.get_vtk_pipeline(data_id) + if not pipeline.edges_block_styles: + self.set_edges_visibility(data_id, visibility=visibility) + return + for style in pipeline.edges_block_styles.values(): + style.visibility = visibility + self.sync_edges_width_actors(pipeline) + self.setup_model_color_map(pipeline) + + def set_model_edges_width_on_all(self, data_id: str, width: float) -> None: + # The main actor line width would also resize the lines, so edges always use their styles + pipeline = self.get_vtk_pipeline(data_id) + pipeline.actor.GetProperty().SetEdgeWidth(width) + for style in self.ensure_model_edges_styles(pipeline).values(): + style.width = width + self.sync_edges_width_actors(pipeline) + @typed_rpc(model_prefix, schemas.register_route) def register_model(self, params: schemas.Register) -> schemas.RegisterResponse: data_id = params.id @@ -398,6 +525,9 @@ def deregister_model(self, params: schemas.Deregister) -> schemas.DeregisterResp @typed_rpc(model_prefix, schemas.visibility_route) def set_model_visibility(self, params: schemas.Visibility) -> schemas.VisibilityResponse: self.set_visibility(params.id, visibility=params.visibility) + if params.visibility: + # Bars come back from the displayed attributes, hidden components' ones stay hidden + self.setup_model_color_map(self.get_vtk_pipeline(params.id)) return schemas.VisibilityResponse() @typed_rpc(model_prefix, schemas.highlight_route) diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/__init__.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/__init__.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/__init__.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/__init__.py new file mode 100644 index 00000000..a563b254 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/__init__.py @@ -0,0 +1 @@ +from .attribute import * diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/attribute.json b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/attribute.json new file mode 100644 index 00000000..bf36e3b9 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/attribute.json @@ -0,0 +1,88 @@ +{ + "rpc": "attribute", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "item": { + "type": "integer", + "minimum": 0 + }, + "points": { + "type": "array", + "description": "Flat array of [value, r, g, b, ...]", + "items": { + "type": "number" + }, + "minItems": 8 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "no_data_color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "name", + "item", + "points", + "minimum", + "maximum" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/attribute.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/attribute.py new file mode 100644 index 00000000..3c07a38b --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/schemas/attribute.py @@ -0,0 +1,51 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class NoDataColor(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + alpha: float + blue: int + green: int + red: int + + +@dataclass +class Attribute(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + id: str + item: int + maximum: float + minimum: float + name: str + points: List[float] + """Flat array of [value, r, g, b, ...]""" + + no_data_color: Optional[NoDataColor] = None + + +@dataclass +class AttributeResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +attribute_route = Route( + schema=load_schema(Path(__file__)), + params=Attribute, + response=AttributeResponse, +) + +__all__ = ["NoDataColor", "Attribute", "AttributeResponse", "attribute_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/surfaces_edges_attribute_vertex_protocols.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/surfaces_edges_attribute_vertex_protocols.py new file mode 100644 index 00000000..0854aa3b --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/attribute/vertex/surfaces_edges_attribute_vertex_protocols.py @@ -0,0 +1,30 @@ +# Standard library imports +from pathlib import Path + +# Third party imports +from opengeodeweb_microservice.schemas import get_schemas_dict + +# Local application imports +from opengeodeweb_viewer.rpc.model.model_protocols import VtkModelView +from opengeodeweb_viewer.typed_rpc import typed_rpc + +from . import schemas + + +class VtkModelSurfacesEdgesAttributeVertexView(VtkModelView): + model_surfaces_edges_attribute_vertex_prefix = ( + "opengeodeweb_viewer.model.surfaces.edges.attribute.vertex." + ) + model_surfaces_edges_attribute_vertex_schemas_dict = get_schemas_dict( + Path(__file__).parent / "schemas" + ) + + def __init__(self) -> None: + super().__init__() + + @typed_rpc(model_surfaces_edges_attribute_vertex_prefix, schemas.attribute_route) + def set_model_surfaces_edges_vertex_attribute( + self, params: schemas.Attribute + ) -> schemas.AttributeResponse: + self.set_component_edges_attribute(params.id, "surfaces", params.block_ids, params) + return schemas.AttributeResponse() diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/model_surfaces_edges_protocols.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/model_surfaces_edges_protocols.py new file mode 100644 index 00000000..6aa01870 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/model_surfaces_edges_protocols.py @@ -0,0 +1,38 @@ +# Standard library imports +from pathlib import Path + +# Third party imports +from opengeodeweb_microservice.schemas import get_schemas_dict + +# Local application imports +from opengeodeweb_viewer.rpc.model.model_protocols import VtkModelView +from opengeodeweb_viewer.typed_rpc import typed_rpc + +from . import schemas + + +class VtkModelSurfacesEdgesView(VtkModelView): + model_surfaces_edges_prefix = "opengeodeweb_viewer.model.surfaces.edges." + model_surfaces_edges_schemas_dict = get_schemas_dict(Path(__file__).parent / "schemas") + + def __init__(self) -> None: + super().__init__() + + @typed_rpc(model_surfaces_edges_prefix, schemas.visibility_route) + def set_model_surfaces_edges_visibility( + self, params: schemas.Visibility + ) -> schemas.VisibilityResponse: + self.set_component_edges_visibility( + params.id, "surfaces", params.block_ids, visibility=params.visibility + ) + return schemas.VisibilityResponse() + + @typed_rpc(model_surfaces_edges_prefix, schemas.color_route) + def set_model_surfaces_edges_color(self, params: schemas.Color) -> schemas.ColorResponse: + self.set_component_edges_color(params.id, "surfaces", params.block_ids, params.color) + return schemas.ColorResponse() + + @typed_rpc(model_surfaces_edges_prefix, schemas.width_route) + def set_model_surfaces_edges_width(self, params: schemas.Width) -> schemas.WidthResponse: + self.set_component_edges_width(params.id, "surfaces", params.block_ids, params.width) + return schemas.WidthResponse() diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/__init__.py new file mode 100644 index 00000000..cfe6b7ea --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/__init__.py @@ -0,0 +1,3 @@ +from .width import * +from .visibility import * +from .color import * diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/color.json b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/color.json new file mode 100644 index 00000000..54fcb685 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/color.json @@ -0,0 +1,62 @@ +{ + "rpc": "color", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "color": { + "type": "object", + "properties": { + "red": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "green": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "blue": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "alpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "required": [ + "red", + "green", + "blue", + "alpha" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "block_ids", + "color" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/color.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/color.py new file mode 100644 index 00000000..68d42901 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/color.py @@ -0,0 +1,44 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List + + +@dataclass +class ColorClass(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + alpha: float + blue: int + green: int + red: int + + +@dataclass +class Color(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + color: ColorClass + id: str + + +@dataclass +class ColorResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +color_route = Route( + schema=load_schema(Path(__file__)), + params=Color, + response=ColorResponse, +) + +__all__ = ["ColorClass", "Color", "ColorResponse", "color_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/visibility.json b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/visibility.json new file mode 100644 index 00000000..77b18063 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/visibility.json @@ -0,0 +1,33 @@ +{ + "rpc": "visibility", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "visibility": { + "type": "boolean" + } + }, + "required": [ + "id", + "block_ids", + "visibility" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/visibility.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/visibility.py new file mode 100644 index 00000000..1b9616b0 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/visibility.py @@ -0,0 +1,33 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List + + +@dataclass +class Visibility(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + id: str + visibility: bool + + +@dataclass +class VisibilityResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +visibility_route = Route( + schema=load_schema(Path(__file__)), + params=Visibility, + response=VisibilityResponse, +) + +__all__ = ["Visibility", "VisibilityResponse", "visibility_route"] diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/width.json b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/width.json new file mode 100644 index 00000000..d98585fe --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/width.json @@ -0,0 +1,33 @@ +{ + "rpc": "width", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "block_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "width": { + "type": "number" + } + }, + "required": [ + "id", + "block_ids", + "width" + ], + "additionalProperties": false, + "response": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } +} diff --git a/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/width.py b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/width.py new file mode 100644 index 00000000..c1231a48 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/model/surfaces/edges/schemas/width.py @@ -0,0 +1,33 @@ +from pathlib import Path +from opengeodeweb_microservice.schemas import Route, load_schema +from dataclasses_json import DataClassJsonMixin +from opengeodeweb_microservice.schemas import print_dataclass +from dataclasses import dataclass +from typing import List + + +@dataclass +class Width(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + block_ids: List[int] + id: str + width: float + + +@dataclass +class WidthResponse(DataClassJsonMixin): + def __post_init__(self) -> None: + print_dataclass(self) + + pass + + +width_route = Route( + schema=load_schema(Path(__file__)), + params=Width, + response=WidthResponse, +) + +__all__ = ["Width", "WidthResponse", "width_route"] diff --git a/src/opengeodeweb_viewer/rpc/protocols.py b/src/opengeodeweb_viewer/rpc/protocols.py index 91a45265..9ae575e3 100644 --- a/src/opengeodeweb_viewer/rpc/protocols.py +++ b/src/opengeodeweb_viewer/rpc/protocols.py @@ -36,6 +36,10 @@ from .model.blocks.attribute.vertex.blocks_attribute_vertex_protocols import ( VtkModelBlocksAttributeVertexView, ) +from .model.blocks.edges.attribute.vertex.blocks_edges_attribute_vertex_protocols import ( + VtkModelBlocksEdgesAttributeVertexView, +) +from .model.blocks.edges.model_blocks_edges_protocols import VtkModelBlocksEdgesView from .model.blocks.model_blocks_protocols import VtkModelBlocksView from .model.blocks.points.attribute.vertex.blocks_points_attribute_vertex_protocols import ( VtkModelBlocksPointsAttributeVertexView, @@ -64,6 +68,10 @@ from .model.surfaces.attribute.vertex.surfaces_attribute_vertex_protocols import ( VtkModelSurfacesAttributeVertexView, ) +from .model.surfaces.edges.attribute.vertex.surfaces_edges_attribute_vertex_protocols import ( + VtkModelSurfacesEdgesAttributeVertexView, +) +from .model.surfaces.edges.model_surfaces_edges_protocols import VtkModelSurfacesEdgesView from .model.surfaces.model_surfaces_protocols import VtkModelSurfacesView from .model.surfaces.points.attribute.vertex.surfaces_points_attribute_vertex_protocols import ( VtkModelSurfacesPointsAttributeVertexView, @@ -110,4 +118,8 @@ VtkModelLinesPointsAttributeVertexView, VtkModelSurfacesPointsAttributeVertexView, VtkModelBlocksPointsAttributeVertexView, + VtkModelSurfacesEdgesView, + VtkModelBlocksEdgesView, + VtkModelSurfacesEdgesAttributeVertexView, + VtkModelBlocksEdgesAttributeVertexView, ) diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index e5c7c921..94a0d877 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -51,6 +51,8 @@ ) if TYPE_CHECKING: + from collections.abc import Mapping + from opengeodeweb_microservice.database.data_types import ( ViewerElementsType, ViewerType, @@ -209,6 +211,10 @@ class BlockStyle(TypedDict): no_data_color: ColorClassProtocol | None +# Attribute name, item, range and color map points: equal keys mean identical scalar bars +ColorMapKey = tuple[str, int, float, float, tuple[float, ...]] + + @dataclass class ModelScalarBar: actor: vtkScalarBarActor = field(default_factory=vtkScalarBarActor) @@ -231,6 +237,26 @@ class ModelPointsStyle: attribute: BlockStyle | None = None +@dataclass +class EdgesWidthActor: + mapper: vtkCompositePolyDataMapper + actor: vtkActor + + +@dataclass +class ModelEdgesStyle: + width: float + visibility: bool + color: tuple[float, float, float] | None = None + opacity: float = 1.0 + attribute: BlockStyle | None = None + + +def style_actor_key(style: ModelPointsStyle | ModelEdgesStyle) -> float: + # Points actors are split by size, edges actors by width + return style.size if isinstance(style, ModelPointsStyle) else style.width + + @dataclass class VtkPipeline: reader: vtkXMLReader @@ -257,11 +283,23 @@ class VtkPipeline: # One points actor per distinct size, since point size is an actor property points_size_actors: dict[float, PointsSizeActor] = field(default_factory=dict) points_block_styles: dict[int, ModelPointsStyle] = field(default_factory=dict) + edges_mapper: vtkDataSetMapper | None = None + edges_actor: vtkActor | None = None + edges_scalar_bar: vtkScalarBarActor | None = None + edges_visibility: bool = False + # Attribute shown by the points and edges dedicated scalar bars, None when they have none + points_color_map_key: ColorMapKey | None = None + edges_color_map_key: ColorMapKey | None = None + # One edges actor per distinct width, since line width is an actor property + edges_width_actors: dict[float, EdgesWidthActor] = field(default_factory=dict) + edges_block_styles: dict[int, ModelEdgesStyle] = field(default_factory=dict) def connect_mappers(self) -> None: self.mapper.SetInputConnection(self.filter.GetOutputPort()) if self.points_mapper is not None: self.points_mapper.SetInputConnection(self.filter.GetOutputPort()) + if self.edges_mapper is not None: + self.edges_mapper.SetInputConnection(self.filter.GetOutputPort()) def extract_blocks(self, multiblock: vtkDataObject | None) -> list[vtkDataObject | None]: if not isinstance(multiblock, vtkMultiBlockDataSet): @@ -509,37 +547,49 @@ def block_component_types(self) -> dict[int, str]: iterator.GoToNextItem() return component_types + def update_style_actor_attributes( + self, + mapper: vtkCompositePolyDataMapper, + styles: Mapping[int, ModelPointsStyle | ModelEdgesStyle], + actor_key: float, + ) -> None: + # A block shows in a style actor only when it is visible and its style matches the actor + main_mapper = cast("vtkCompositePolyDataMapper", self.mapper) + attributes = main_mapper.GetCompositeDataDisplayAttributes() + actor_attributes = vtkCompositeDataDisplayAttributes() + for block_id, block in enumerate(self.block_data_sets): + if not isinstance(block, vtkDataSet): + continue + style = styles.get(block_id) + block_visibility = not attributes.HasBlockVisibility( + block + ) or attributes.GetBlockVisibility(block) + visible = ( + style is not None + and style_actor_key(style) == actor_key + and style.visibility + and block_visibility + ) + actor_attributes.SetBlockVisibility(block, visible) + if not visible or style is None: + continue + if style.color is not None: + actor_attributes.SetBlockColor(block, list(style.color)) + actor_attributes.SetBlockOpacity(block, style.opacity) + self.set_block_point_attribute(actor_attributes, block, style.attribute) + mapper.SetCompositeDataDisplayAttributes(actor_attributes) + mapper.Modified() + def update_points_size_actors(self) -> None: - # A block shows its points in the actor of its size, only when the block is visible - mapper = cast("vtkCompositePolyDataMapper", self.mapper) - attributes = mapper.GetCompositeDataDisplayAttributes() for size, size_actor in self.points_size_actors.items(): - size_actor_attributes = vtkCompositeDataDisplayAttributes() - for block_id, block in enumerate(self.block_data_sets): - if not isinstance(block, vtkDataSet): - continue - style = self.points_block_styles.get(block_id) - block_visibility = not attributes.HasBlockVisibility( - block - ) or attributes.GetBlockVisibility(block) - visible = ( - style is not None - and style.size == size - and style.visibility - and block_visibility - ) - size_actor_attributes.SetBlockVisibility(block, visible) - if not visible or style is None: - continue - if style.color is not None: - size_actor_attributes.SetBlockColor(block, list(style.color)) - size_actor_attributes.SetBlockOpacity(block, style.opacity) - self.set_points_block_attribute(size_actor_attributes, block, style.attribute) - size_actor.mapper.SetCompositeDataDisplayAttributes(size_actor_attributes) - size_actor.mapper.Modified() + self.update_style_actor_attributes(size_actor.mapper, self.points_block_styles, size) + + def update_edges_width_actors(self) -> None: + for width, width_actor in self.edges_width_actors.items(): + self.update_style_actor_attributes(width_actor.mapper, self.edges_block_styles, width) @staticmethod - def set_points_block_attribute( + def set_block_point_attribute( attributes: vtkCompositeDataDisplayAttributes, block: vtkDataSet, attribute: BlockStyle | None, @@ -595,6 +645,7 @@ def sync_block_display_attributes(self, new_blocks: list[vtkDataObject | None]) self.sync_mapper_display_attributes(mapper, self.block_data_sets, new_blocks) self.block_data_sets = new_blocks self.update_points_size_actors() + self.update_edges_width_actors() for block_id in self.block_styles: self.update_block_colors(block_id) @@ -611,6 +662,8 @@ def sync_composite_pipeline(self, dataset: vtkDataObject) -> None: self.mapper.SetInputDataObject(dataset) for size_actor in self.points_size_actors.values(): size_actor.mapper.SetInputDataObject(dataset) + for width_actor in self.edges_width_actors.values(): + width_actor.mapper.SetInputDataObject(dataset) if self.pick_mapper and isinstance(dataset, vtkMultiBlockDataSet): mapper = cast("vtkCompositePolyDataMapper", self.mapper) attributes = mapper.GetCompositeDataDisplayAttributes() diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 110251d1..5ba8af6b 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -530,6 +530,16 @@ def visible_scalar_bars( points_attr_name = pipeline.points_mapper.GetArrayName() visible_bars.append((points_bar, points_attr_name, data_id, pipeline)) + edges_bar = pipeline.edges_scalar_bar + if ( + edges_bar is not None + and pipeline.edges_mapper is not None + and edges_bar.GetVisibility() + and edges_bar.GetLookupTable() is not None + ): + edges_attr_name = pipeline.edges_mapper.GetArrayName() + visible_bars.append((edges_bar, edges_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)) diff --git a/tests/data/images/mesh/edges/polygons_color_after_shared_scalar_bar.jpeg b/tests/data/images/mesh/edges/polygons_color_after_shared_scalar_bar.jpeg new file mode 100644 index 00000000..36214120 Binary files /dev/null and b/tests/data/images/mesh/edges/polygons_color_after_shared_scalar_bar.jpeg differ diff --git a/tests/data/images/mesh/edges/polygons_edges_attribute_then_color.jpeg b/tests/data/images/mesh/edges/polygons_edges_attribute_then_color.jpeg new file mode 100644 index 00000000..d8825dbe Binary files /dev/null and b/tests/data/images/mesh/edges/polygons_edges_attribute_then_color.jpeg differ diff --git a/tests/data/images/mesh/edges/polygons_edges_hidden_with_object.jpeg b/tests/data/images/mesh/edges/polygons_edges_hidden_with_object.jpeg new file mode 100644 index 00000000..35b6a3d4 Binary files /dev/null and b/tests/data/images/mesh/edges/polygons_edges_hidden_with_object.jpeg differ diff --git a/tests/data/images/mesh/edges/polygons_edges_shared_scalar_bar.jpeg b/tests/data/images/mesh/edges/polygons_edges_shared_scalar_bar.jpeg new file mode 100644 index 00000000..8812def6 Binary files /dev/null and b/tests/data/images/mesh/edges/polygons_edges_shared_scalar_bar.jpeg differ diff --git a/tests/data/images/mesh/edges/polygons_edges_vertex_attribute.jpeg b/tests/data/images/mesh/edges/polygons_edges_vertex_attribute.jpeg new file mode 100644 index 00000000..36214120 Binary files /dev/null and b/tests/data/images/mesh/edges/polygons_edges_vertex_attribute.jpeg differ diff --git a/tests/data/images/mesh/edges/polygons_points_and_edges_shared_scalar_bar.jpeg b/tests/data/images/mesh/edges/polygons_points_and_edges_shared_scalar_bar.jpeg new file mode 100644 index 00000000..474429f9 Binary files /dev/null and b/tests/data/images/mesh/edges/polygons_points_and_edges_shared_scalar_bar.jpeg differ diff --git a/tests/data/images/mesh/points/polygons_points_attribute_then_color.jpeg b/tests/data/images/mesh/points/polygons_points_attribute_then_color.jpeg new file mode 100644 index 00000000..6839d6fa Binary files /dev/null and b/tests/data/images/mesh/points/polygons_points_attribute_then_color.jpeg differ diff --git a/tests/data/images/model/edges/blocks_edges_color.jpeg b/tests/data/images/model/edges/blocks_edges_color.jpeg new file mode 100644 index 00000000..3c409077 Binary files /dev/null and b/tests/data/images/model/edges/blocks_edges_color.jpeg differ diff --git a/tests/data/images/model/edges/surface_edges_width.jpeg b/tests/data/images/model/edges/surface_edges_width.jpeg new file mode 100644 index 00000000..bcc53b5f Binary files /dev/null and b/tests/data/images/model/edges/surface_edges_width.jpeg differ diff --git a/tests/data/images/model/edges/surfaces_edges_attribute.jpeg b/tests/data/images/model/edges/surfaces_edges_attribute.jpeg new file mode 100644 index 00000000..b4de0b78 Binary files /dev/null and b/tests/data/images/model/edges/surfaces_edges_attribute.jpeg differ diff --git a/tests/data/images/model/edges/surfaces_edges_color.jpeg b/tests/data/images/model/edges/surfaces_edges_color.jpeg new file mode 100644 index 00000000..2b61086d Binary files /dev/null and b/tests/data/images/model/edges/surfaces_edges_color.jpeg differ diff --git a/tests/data/images/model/edges/surfaces_edges_explode.jpeg b/tests/data/images/model/edges/surfaces_edges_explode.jpeg new file mode 100644 index 00000000..096f8943 Binary files /dev/null and b/tests/data/images/model/edges/surfaces_edges_explode.jpeg differ diff --git a/tests/data/images/model/edges/surfaces_edges_hidden_blocks.jpeg b/tests/data/images/model/edges/surfaces_edges_hidden_blocks.jpeg new file mode 100644 index 00000000..c23af39f Binary files /dev/null and b/tests/data/images/model/edges/surfaces_edges_hidden_blocks.jpeg differ diff --git a/tests/data/images/model/edges/surfaces_edges_opacity.jpeg b/tests/data/images/model/edges/surfaces_edges_opacity.jpeg new file mode 100644 index 00000000..6fe3ff82 Binary files /dev/null and b/tests/data/images/model/edges/surfaces_edges_opacity.jpeg differ diff --git a/tests/data/images/model/edges/width.jpeg b/tests/data/images/model/edges/width.jpeg new file mode 100644 index 00000000..86d9bdb3 Binary files /dev/null and b/tests/data/images/model/edges/width.jpeg differ diff --git a/tests/data/images/model/edges/width_after_component_width.jpeg b/tests/data/images/model/edges/width_after_component_width.jpeg new file mode 100644 index 00000000..b195971f Binary files /dev/null and b/tests/data/images/model/edges/width_after_component_width.jpeg differ diff --git a/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py b/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py index 5ba3154d..8fe72473 100644 --- a/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py +++ b/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py @@ -5,9 +5,18 @@ from opengeodeweb_viewer.rpc.mesh.edges.attribute.vertex.edges_attribute_vertex_protocols import ( VtkMeshEdgesAttributeVertexView, ) +from opengeodeweb_viewer.rpc.mesh.edges.edges_protocols import VtkMeshEdgesView # Third party imports from opengeodeweb_viewer.rpc.mesh.mesh_protocols import VtkMeshView +from opengeodeweb_viewer.rpc.mesh.points.attribute.vertex.points_attribute_vertex_protocols import ( + VtkMeshPointsAttributeVertexView, +) +from opengeodeweb_viewer.rpc.mesh.points.points_protocols import VtkMeshPointsView +from opengeodeweb_viewer.rpc.mesh.polygons.attribute.vertex import ( + polygons_attribute_vertex_protocols, +) +from opengeodeweb_viewer.rpc.mesh.polygons.polygons_protocols import VtkMeshPolygonsView # Local application imports @@ -16,6 +25,10 @@ from tests.conftest import ServerMonitor +VtkMeshPolygonsAttributeVertexView = ( + polygons_attribute_vertex_protocols.VtkMeshPolygonsAttributeVertexView +) + # Local constants mesh_id = "12345678901234567890123456789012" @@ -352,3 +365,153 @@ def test_edges_vertex_attribute_item( ) assert server.compare_image("mesh/edges/vertex_attribute_item.jpeg") + + +HAT_ATTRIBUTE = { + "name": "lambert2SG", + "item": 0, + "points": [0.0, 0, 0, 1.0, 1.0, 1.0, 0, 0], + "minimum": 0.0, + "maximum": 1.0, +} + + +def register_hat_with_edges(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + dataset_factory(data_id=mesh_id, viewable_file="hat.vtp", viewer_elements_type="polygons") + server.call( + VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["register"]["rpc"], + [{"id": mesh_id, "name": "hat.vtp"}], + ) + server.call( + VtkMeshEdgesView.mesh_edges_prefix + + VtkMeshEdgesView.mesh_edges_schemas_dict["visibility"]["rpc"], + [{"id": mesh_id, "visibility": True}], + ) + server.call( + VtkMeshEdgesView.mesh_edges_prefix + + VtkMeshEdgesView.mesh_edges_schemas_dict["width"]["rpc"], + [{"id": mesh_id, "width": 4}], + ) + + +def set_edges_vertex_attribute(server: ServerMonitor, attribute: dict[str, object]) -> None: + server.call( + VtkMeshEdgesAttributeVertexView.mesh_edges_attribute_vertex_prefix + + VtkMeshEdgesAttributeVertexView.mesh_edges_attribute_vertex_schemas_dict["attribute"][ + "rpc" + ], + [{"id": mesh_id, **attribute}], + ) + + +def test_polygons_edges_vertex_attribute( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + register_hat_with_edges(server, dataset_factory) + set_edges_vertex_attribute(server, HAT_ATTRIBUTE) + # Polygons keep their constant color, only the edges are colored + assert server.compare_image("mesh/edges/polygons_edges_vertex_attribute.jpeg") + + +def test_polygons_edges_shared_scalar_bar( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + register_hat_with_edges(server, dataset_factory) + server.call( + VtkMeshPolygonsAttributeVertexView.mesh_polygons_attribute_vertex_prefix + + VtkMeshPolygonsAttributeVertexView.mesh_polygons_attribute_vertex_schemas_dict[ + "attribute" + ]["rpc"], + [{"id": mesh_id, **HAT_ATTRIBUTE}], + ) + set_edges_vertex_attribute(server, HAT_ATTRIBUTE) + # Same attribute and color map: one scalar bar only + assert server.compare_image("mesh/edges/polygons_edges_shared_scalar_bar.jpeg") + + +def test_polygons_edges_attribute_then_color( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + register_hat_with_edges(server, dataset_factory) + set_edges_vertex_attribute(server, HAT_ATTRIBUTE) + server.call( + VtkMeshEdgesView.mesh_edges_prefix + + VtkMeshEdgesView.mesh_edges_schemas_dict["color"]["rpc"], + [{"id": mesh_id, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}}], + ) + # Red edges, no edges scalar bar + assert server.compare_image("mesh/edges/polygons_edges_attribute_then_color.jpeg") + + +def test_polygons_edges_hidden_with_object( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + register_hat_with_edges(server, dataset_factory) + set_edges_vertex_attribute(server, HAT_ATTRIBUTE) + server.call( + VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["visibility"]["rpc"], + [{"id": mesh_id, "visibility": False}], + ) + # Empty scene: no edges, no scalar bar + assert server.compare_image("mesh/edges/polygons_edges_hidden_with_object.jpeg") + + +def test_polygons_points_and_edges_shared_scalar_bar( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + register_hat_with_edges(server, dataset_factory) + server.call( + VtkMeshPointsView.mesh_points_prefix + + VtkMeshPointsView.mesh_points_schemas_dict["visibility"]["rpc"], + [{"id": mesh_id, "visibility": True}], + ) + server.call( + VtkMeshPointsAttributeVertexView.mesh_points_attribute_vertex_prefix + + VtkMeshPointsAttributeVertexView.mesh_points_attribute_vertex_schemas_dict["attribute"][ + "rpc" + ], + [{"id": mesh_id, **HAT_ATTRIBUTE}], + ) + set_edges_vertex_attribute(server, HAT_ATTRIBUTE) + # Same attribute and color map on points and edges: one scalar bar only + assert server.compare_image("mesh/edges/polygons_points_and_edges_shared_scalar_bar.jpeg") + + +def set_polygons_vertex_attribute(server: ServerMonitor) -> None: + server.call( + VtkMeshPolygonsAttributeVertexView.mesh_polygons_attribute_vertex_prefix + + VtkMeshPolygonsAttributeVertexView.mesh_polygons_attribute_vertex_schemas_dict[ + "attribute" + ]["rpc"], + [{"id": mesh_id, **HAT_ATTRIBUTE}], + ) + + +def test_polygons_edges_shared_scalar_bar_edges_first( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + register_hat_with_edges(server, dataset_factory) + set_edges_vertex_attribute(server, HAT_ATTRIBUTE) + set_polygons_vertex_attribute(server) + # Same picture as when the polygons attribute comes first: one scalar bar only + assert server.compare_image("mesh/edges/polygons_edges_shared_scalar_bar.jpeg") + + +def test_polygons_color_after_shared_scalar_bar( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_polygons_edges_shared_scalar_bar(server, dataset_factory) + server.call( + VtkMeshPolygonsView.mesh_polygons_prefix + + VtkMeshPolygonsView.mesh_polygons_schemas_dict["color"]["rpc"], + [{"id": mesh_id, "color": {"red": 255, "green": 255, "blue": 255, "alpha": 1.0}}], + ) + # Polygons back to a constant color: the edges keep their attribute and get their own bar + assert server.compare_image("mesh/edges/polygons_color_after_shared_scalar_bar.jpeg") diff --git a/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py b/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py index 3c55301c..cec2a704 100644 --- a/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py +++ b/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py @@ -495,3 +495,17 @@ def test_polygons_points_shared_scalar_bar( [attribute], ) assert server.compare_image("mesh/points/polygons_points_shared_scalar_bar.jpeg") + + +def test_polygons_points_attribute_then_color( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_polygons_points_vertex_attribute(server, dataset_factory) + server.call( + VtkMeshPointsView.mesh_points_prefix + + VtkMeshPointsView.mesh_points_schemas_dict["color"]["rpc"], + [{"id": mesh_id, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}}], + ) + # Red points, no points scalar bar + assert server.compare_image("mesh/points/polygons_points_attribute_then_color.jpeg") diff --git a/tests/model/edges/test_model_edges_protocols.py b/tests/model/edges/test_model_edges_protocols.py index 691b986c..86ae3172 100644 --- a/tests/model/edges/test_model_edges_protocols.py +++ b/tests/model/edges/test_model_edges_protocols.py @@ -3,20 +3,40 @@ # Third party imports from typing import TYPE_CHECKING +from opengeodeweb_viewer.rpc.model.blocks.edges.model_blocks_edges_protocols import ( + VtkModelBlocksEdgesView, +) from opengeodeweb_viewer.rpc.model.edges.model_edges_protocols import ( VtkModelEdgesView, ) +from opengeodeweb_viewer.rpc.model.model_protocols import VtkModelView +from opengeodeweb_viewer.rpc.model.surfaces.edges.attribute.vertex import ( + surfaces_edges_attribute_vertex_protocols, +) +from opengeodeweb_viewer.rpc.model.surfaces.edges.model_surfaces_edges_protocols import ( + VtkModelSurfacesEdgesView, +) +from opengeodeweb_viewer.rpc.model.surfaces.model_surfaces_protocols import ( + VtkModelSurfacesView, +) +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports -from tests.model.test_model_protocols import test_register_model +from tests.model.test_model_protocols import test_model_cube_side_view, test_register_model if TYPE_CHECKING: from collections.abc import Callable from tests.conftest import ServerMonitor +VtkModelSurfacesEdgesAttributeVertexView = ( + surfaces_edges_attribute_vertex_protocols.VtkModelSurfacesEdgesAttributeVertexView +) + # Local constants model_id = "12345678901234567890123456789012" +surfaces_ids = list(range(36, 47)) +blocks_ids = list(range(48, 50)) def test_edges_visibility(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: @@ -29,3 +49,145 @@ def test_edges_visibility(server: ServerMonitor, dataset_factory: Callable[..., [{"id": model_id, "visibility": True}], ) assert server.compare_image("model/edges/visibility.jpeg") + + +def test_edges_width(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + + test_edges_visibility(server, dataset_factory) + + server.call( + VtkModelEdgesView.model_edges_prefix + + VtkModelEdgesView.model_edges_schemas_dict["width"]["rpc"], + [{"id": model_id, "width": 6}], + ) + assert server.compare_image("model/edges/width.jpeg") + + +def call_surfaces_edges(server: ServerMonitor, route: str, params: dict[str, object]) -> None: + view = VtkModelSurfacesEdgesView + server.call( + view.model_surfaces_edges_prefix + view.model_surfaces_edges_schemas_dict[route]["rpc"], + [{"id": model_id, **params}], + ) + + +def test_surfaces_edges_color(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + + test_model_cube_side_view(server, dataset_factory) + call_surfaces_edges(server, "visibility", {"block_ids": surfaces_ids, "visibility": True}) + call_surfaces_edges(server, "width", {"block_ids": surfaces_ids, "width": 3}) + call_surfaces_edges( + server, + "color", + {"block_ids": surfaces_ids, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}}, + ) + assert server.compare_image("model/edges/surfaces_edges_color.jpeg") + + +def test_surface_edges_width(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + + test_surfaces_edges_color(server, dataset_factory) + call_surfaces_edges(server, "width", {"block_ids": [45], "width": 8}) + assert server.compare_image("model/edges/surface_edges_width.jpeg") + + +def test_surfaces_edges_attribute( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_surfaces_edges_color(server, dataset_factory) + view = VtkModelSurfacesEdgesAttributeVertexView + server.call( + view.model_surfaces_edges_attribute_vertex_prefix + + view.model_surfaces_edges_attribute_vertex_schemas_dict["attribute"]["rpc"], + [ + { + "id": model_id, + "block_ids": surfaces_ids, + "name": "unique vertices", + "item": 0, + "points": [0.0, 0.0, 0.0, 1.0, 663.0, 1.0, 0.0, 0.0], + "minimum": 0.0, + "maximum": 663.0, + } + ], + ) + assert server.compare_image("model/edges/surfaces_edges_attribute.jpeg") + + +def test_surfaces_edges_attribute_model_hidden_then_shown( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_surfaces_edges_attribute(server, dataset_factory) + for visibility in (False, True): + server.call( + VtkModelView.model_prefix + VtkModelView.model_schemas_dict["visibility"]["rpc"], + [{"id": model_id, "visibility": visibility}], + ) + # The edges scalar bar comes back with the model + assert server.compare_image("model/edges/surfaces_edges_attribute.jpeg") + + +def test_surfaces_edges_opacity(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + + test_surfaces_edges_color(server, dataset_factory) + call_surfaces_edges( + server, + "color", + {"block_ids": surfaces_ids, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 0.2}}, + ) + assert server.compare_image("model/edges/surfaces_edges_opacity.jpeg") + + +def test_surfaces_edges_hidden_blocks( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_surfaces_edges_color(server, dataset_factory) + server.call( + VtkModelSurfacesView.model_surfaces_prefix + + VtkModelSurfacesView.model_surfaces_schemas_dict["visibility"]["rpc"], + [{"id": model_id, "block_ids": surfaces_ids, "visibility": False}], + ) + # Hidden surfaces take their edges with them + assert server.compare_image("model/edges/surfaces_edges_hidden_blocks.jpeg") + + +def test_surfaces_edges_explode(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + + test_surfaces_edges_color(server, dataset_factory) + server.call( + VtkViewerView.viewer_prefix + VtkViewerView.viewer_schemas_dict["explode"]["rpc"], + [{"ids": [model_id], "explode_factor": 1.0}], + ) + assert server.compare_image("model/edges/surfaces_edges_explode.jpeg") + + +def test_model_edges_width_after_component_width( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_surface_edges_width(server, dataset_factory) + server.call( + VtkModelEdgesView.model_edges_prefix + + VtkModelEdgesView.model_edges_schemas_dict["width"]["rpc"], + [{"id": model_id, "width": 1}], + ) + # Model width overrides every component width, block 45 included + assert server.compare_image("model/edges/width_after_component_width.jpeg") + + +def test_blocks_edges_color(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: + + test_model_cube_side_view(server, dataset_factory) + view = VtkModelBlocksEdgesView + for route, params in ( + ("visibility", {"visibility": True}), + ("color", {"color": {"red": 0, "green": 0, "blue": 255, "alpha": 1.0}}), + ): + server.call( + view.model_blocks_edges_prefix + view.model_blocks_edges_schemas_dict[route]["rpc"], + [{"id": model_id, "block_ids": blocks_ids, **params}], + ) + assert server.compare_image("model/edges/blocks_edges_color.jpeg")