From 363931379c176f165c3c9f87f1cdb5b753e658da Mon Sep 17 00:00:00 2001 From: Maarten Pronk <8655030+evetion@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:06:24 +0200 Subject: [PATCH] Add name as column and use as visualisation in QGIS. (#658) Fixes #567 - [x] Some warnings still need updating Screenshot 2023-10-09 at 13 46 40 --- core/src/create.jl | 30 ++++++++++--------- core/src/io.jl | 9 ++++++ core/src/validation.jl | 2 ++ core/test/validation.jl | 4 +-- docs/schema/Edge.schema.json | 5 ++++ docs/schema/Node.schema.json | 5 ++++ python/ribasim/ribasim/geometry/edge.py | 4 +++ python/ribasim/ribasim/geometry/node.py | 4 +++ python/ribasim/ribasim/models.py | 2 ++ python/ribasim/ribasim/utils.py | 7 +++++ .../ribasim_testmodels/basic.py | 6 +++- qgis/core/nodes.py | 19 ++++++++++-- 12 files changed, 78 insertions(+), 19 deletions(-) diff --git a/core/src/create.jl b/core/src/create.jl index 7c38e58fb..3afd22730 100644 --- a/core/src/create.jl +++ b/core/src/create.jl @@ -33,6 +33,7 @@ function parse_static_and_time( vals_out = [] node_ids = get_ids(db, nodetype) + node_names = get_names(db, nodetype) n_nodes = length(node_ids) # Initialize the vectors for the output @@ -91,7 +92,7 @@ function parse_static_and_time( t_end = seconds_since(config.endtime, config.starttime) trivial_timespan = [nextfloat(-Inf), prevfloat(Inf)] - for (node_idx, node_id) in enumerate(node_ids) + for (node_idx, (node_id, node_name)) in enumerate(zip(node_ids, node_names)) if node_id in static_node_ids # The interval of rows of the static table that have the current node_id rows = searchsorted(static.node_id, node_id) @@ -153,7 +154,7 @@ function parse_static_and_time( ) if !is_valid errors = true - @error "A $parameter_name time series for $nodetype node #$node_id has repeated times, this can not be interpolated." + @error "A $parameter_name time series for $nodetype node $(repr(node_name)) (#$node_id) has repeated times, this can not be interpolated." end else # Activity of transient nodes is assumed to be true @@ -167,7 +168,7 @@ function parse_static_and_time( getfield(out, parameter_name)[node_idx] = val end else - @error "$nodetype node #$node_id data not in any table." + @error "$nodetype node $(repr(node_name)) (#$node_id) data not in any table." errors = true end end @@ -179,10 +180,11 @@ function static_and_time_node_ids( static::StructVector, time::StructVector, node_type::String, -)::Tuple{Set{Int}, Set{Int}, Vector{Int}, Bool} +)::Tuple{Set{Int}, Set{Int}, Vector{Int}, Vector{String}, Bool} static_node_ids = Set(static.node_id) time_node_ids = Set(time.node_id) node_ids = get_ids(db, node_type) + node_names = get_names(db, node_type) doubles = intersect(static_node_ids, time_node_ids) errors = false if !isempty(doubles) @@ -193,7 +195,7 @@ function static_and_time_node_ids( errors = true @error "$node_type node IDs don't match." end - return static_node_ids, time_node_ids, node_ids, !errors + return static_node_ids, time_node_ids, node_ids, node_names, !errors end function Connectivity(db::DB, config::Config, chunk_size::Int)::Connectivity @@ -249,7 +251,7 @@ function TabulatedRatingCurve(db::DB, config::Config)::TabulatedRatingCurve static = load_structvector(db, config, TabulatedRatingCurveStaticV1) time = load_structvector(db, config, TabulatedRatingCurveTimeV1) - static_node_ids, time_node_ids, node_ids, valid = + static_node_ids, time_node_ids, node_ids, node_names, valid = static_and_time_node_ids(db, static, time, "TabulatedRatingCurve") if !valid @@ -263,7 +265,7 @@ function TabulatedRatingCurve(db::DB, config::Config)::TabulatedRatingCurve active = BitVector() errors = false - for node_id in node_ids + for (node_id, node_name) in zip(node_ids, node_names) if node_id in static_node_ids # Loop over all static rating curves (groups) with this node_id. # If it has a control_state add it to control_mapping. @@ -294,11 +296,11 @@ function TabulatedRatingCurve(db::DB, config::Config)::TabulatedRatingCurve push!(interpolations, interpolation) push!(active, true) else - @error "TabulatedRatingCurve node #$node_id data not in any table." + @error "TabulatedRatingCurve node $(repr(node_name)) (#$node_id) data not in any table." errors = true end if !is_valid - @error "A Q(h) relationship for TabulatedRatingCurve #$node_id from the $source table has repeated levels, this can not be interpolated." + @error "A Q(h) relationship for TabulatedRatingCurve $(repr(node_name)) (#$node_id) from the $source table has repeated levels, this can not be interpolated." errors = true end end @@ -349,7 +351,7 @@ function LevelBoundary(db::DB, config::Config)::LevelBoundary static = load_structvector(db, config, LevelBoundaryStaticV1) time = load_structvector(db, config, LevelBoundaryTimeV1) - static_node_ids, time_node_ids, node_ids, valid = + static_node_ids, time_node_ids, node_ids, node_names, valid = static_and_time_node_ids(db, static, time, "LevelBoundary") if !valid @@ -377,7 +379,7 @@ function FlowBoundary(db::DB, config::Config)::FlowBoundary static = load_structvector(db, config, FlowBoundaryStaticV1) time = load_structvector(db, config, FlowBoundaryTimeV1) - static_node_ids, time_node_ids, node_ids, valid = + static_node_ids, time_node_ids, node_ids, node_names, valid = static_and_time_node_ids(db, static, time, "FlowBoundary") if !valid @@ -397,7 +399,7 @@ function FlowBoundary(db::DB, config::Config)::FlowBoundary for itp in parsed_parameters.flow_rate if any(itp.u .< 0.0) @error( - "Currently negative flow rates are not supported, found some for dynamic flow boundary #$node_id." + "Currently negative flow rates are not supported, found some in dynamic flow boundary." ) valid = false end @@ -564,7 +566,7 @@ function PidControl(db::DB, config::Config, chunk_size::Int)::PidControl static = load_structvector(db, config, PidControlStaticV1) time = load_structvector(db, config, PidControlTimeV1) - static_node_ids, time_node_ids, node_ids, valid = + static_node_ids, time_node_ids, node_ids, node_names, valid = static_and_time_node_ids(db, static, time, "PidControl") if !valid @@ -626,7 +628,7 @@ function User(db::DB, config::Config)::User static = load_structvector(db, config, UserStaticV1) time = load_structvector(db, config, UserTimeV1) - static_node_ids, time_node_ids, node_ids, valid = + static_node_ids, time_node_ids, node_ids, node_names, valid = static_and_time_node_ids(db, static, time, "User") if !valid diff --git a/core/src/io.jl b/core/src/io.jl index afe50db34..5628e3a85 100644 --- a/core/src/io.jl +++ b/core/src/io.jl @@ -7,6 +7,15 @@ function get_ids(db::DB, nodetype)::Vector{Int} return only(execute(columntable, db, sql)) end +function get_names(db::DB)::Vector{String} + return only(execute(columntable, db, "SELECT name FROM Node ORDER BY fid")) +end + +function get_names(db::DB, nodetype)::Vector{String} + sql = "SELECT name FROM Node where type = $(esc_id(nodetype)) ORDER BY fid" + return only(execute(columntable, db, sql)) +end + function exists(db::DB, tablename::String) query = execute( db, diff --git a/core/src/validation.jl b/core/src/validation.jl index 1dc79af3f..1cc358867 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -127,11 +127,13 @@ n_neighbor_bounds_control(nodetype) = # TODO NodeV1 and EdgeV1 are not yet used @version NodeV1 begin fid::Int + name::String = isnothing(s) ? "" : String(s) type::String = in(Symbol(type), nodetypes) ? type : error("Unknown node type $type") end @version EdgeV1 begin fid::Int + name::String = isnothing(s) ? "" : String(s) from_node_id::Int to_node_id::Int edge_type::String diff --git a/core/test/validation.jl b/core/test/validation.jl index 2eacb4963..1f8fcb42e 100644 --- a/core/test/validation.jl +++ b/core/test/validation.jl @@ -43,10 +43,10 @@ end @test length(logger.logs) == 2 @test logger.logs[1].level == Error @test logger.logs[1].message == - "A Q(h) relationship for TabulatedRatingCurve #1 from the static table has repeated levels, this can not be interpolated." + "A Q(h) relationship for TabulatedRatingCurve \"\" (#1) from the static table has repeated levels, this can not be interpolated." @test logger.logs[2].level == Error @test logger.logs[2].message == - "A Q(h) relationship for TabulatedRatingCurve #2 from the time table has repeated levels, this can not be interpolated." + "A Q(h) relationship for TabulatedRatingCurve \"\" (#2) from the time table has repeated levels, this can not be interpolated." end @testset "Neighbor count validation" begin diff --git a/docs/schema/Edge.schema.json b/docs/schema/Edge.schema.json index 319a947ba..7cea96291 100644 --- a/docs/schema/Edge.schema.json +++ b/docs/schema/Edge.schema.json @@ -9,6 +9,10 @@ "format": "default", "type": "integer" }, + "name": { + "format": "default", + "type": "string" + }, "from_node_id": { "format": "default", "type": "integer" @@ -30,6 +34,7 @@ }, "required": [ "fid", + "name", "from_node_id", "to_node_id", "edge_type" diff --git a/docs/schema/Node.schema.json b/docs/schema/Node.schema.json index 81a47a013..18a7dc643 100644 --- a/docs/schema/Node.schema.json +++ b/docs/schema/Node.schema.json @@ -9,6 +9,10 @@ "format": "default", "type": "integer" }, + "name": { + "format": "default", + "type": "string" + }, "type": { "format": "default", "type": "string" @@ -22,6 +26,7 @@ }, "required": [ "fid", + "name", "type" ] } diff --git a/python/ribasim/ribasim/geometry/edge.py b/python/ribasim/ribasim/geometry/edge.py index 6a07ac313..d5075d331 100644 --- a/python/ribasim/ribasim/geometry/edge.py +++ b/python/ribasim/ribasim/geometry/edge.py @@ -18,10 +18,14 @@ class StaticSchema(pa.SchemaModel): + name: Series[str] = pa.Field(default="") from_node_id: Series[int] = pa.Field(coerce=True) to_node_id: Series[int] = pa.Field(coerce=True) geometry: GeoSeries[Any] + class Config: + add_missing_columns = True + class Edge(TableModel): """ diff --git a/python/ribasim/ribasim/geometry/node.py b/python/ribasim/ribasim/geometry/node.py index c604b781d..37145be82 100644 --- a/python/ribasim/ribasim/geometry/node.py +++ b/python/ribasim/ribasim/geometry/node.py @@ -16,9 +16,13 @@ class StaticSchema(pa.SchemaModel): + name: Series[str] = pa.Field(default="") type: Series[str] geometry: GeoSeries[Any] + class Config: + add_missing_columns = True + class Node(TableModel): """ diff --git a/python/ribasim/ribasim/models.py b/python/ribasim/ribasim/models.py index e68def42d..eba64fd86 100644 --- a/python/ribasim/ribasim/models.py +++ b/python/ribasim/ribasim/models.py @@ -61,6 +61,7 @@ class DiscreteControlLogic(BaseModel): class Edge(BaseModel): fid: int + name: str from_node_id: int to_node_id: int edge_type: str @@ -123,6 +124,7 @@ class ManningResistanceStatic(BaseModel): class Node(BaseModel): fid: int + name: str type: str remarks: str = Field("", description="a hack for pandera") diff --git a/python/ribasim/ribasim/utils.py b/python/ribasim/ribasim/utils.py index 6a3db761e..259bfab87 100644 --- a/python/ribasim/ribasim/utils.py +++ b/python/ribasim/ribasim/utils.py @@ -1,3 +1,5 @@ +import random +import string from typing import Any, Sequence, Tuple import numpy as np @@ -76,3 +78,8 @@ def connectivity_from_geometry( from_id = node_index[edge_node_id[:, 0]].to_numpy() to_id = node_index[edge_node_id[:, 1]].to_numpy() return from_id, to_id + + +def random_string(length=3): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for i in range(length)) diff --git a/python/ribasim_testmodels/ribasim_testmodels/basic.py b/python/ribasim_testmodels/ribasim_testmodels/basic.py index fbbc9967f..1ff43127f 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/basic.py +++ b/python/ribasim_testmodels/ribasim_testmodels/basic.py @@ -159,7 +159,10 @@ def basic_model() -> ribasim.Model: # Make sure the feature id starts at 1: explicitly give an index. node = ribasim.Node( static=gpd.GeoDataFrame( - data={"type": node_type}, + data={ + "type": node_type, + "name": [ribasim.utils.random_string() for _ in range(len(node_id))], + }, index=pd.Index(node_id, name="fid"), geometry=node_xy, crs="EPSG:28992", @@ -177,6 +180,7 @@ def basic_model() -> ribasim.Model: edge = ribasim.Edge( static=gpd.GeoDataFrame( data={ + "name": [ribasim.utils.random_string() for _ in range(len(from_id))], "from_node_id": from_id, "to_node_id": to_id, "edge_type": len(from_id) * ["flow"], diff --git a/qgis/core/nodes.py b/qgis/core/nodes.py index d02115dbc..93f82a0a9 100644 --- a/qgis/core/nodes.py +++ b/qgis/core/nodes.py @@ -28,6 +28,7 @@ from ribasim_qgis.core import geopackage from qgis.core import ( + Qgis, QgsCategorizedSymbolRenderer, QgsEditorWidgetSetup, QgsField, @@ -128,7 +129,10 @@ def set_editor_widget(self) -> None: class Node(Input): input_type = "Node" geometry_type = "Point" - attributes = (QgsField("type", QVariant.String),) + attributes = ( + QgsField("name", QVariant.String), + QgsField("type", QVariant.String), + ) @classmethod def is_spatial(cls): @@ -206,7 +210,8 @@ def renderer(self) -> QgsCategorizedSymbolRenderer: @property def labels(self) -> Any: pal_layer = QgsPalLayerSettings() - pal_layer.fieldName = "fid" + pal_layer.fieldName = """concat("name", ' (#', "fid", ')')""" + pal_layer.isExpression = True pal_layer.enabled = True pal_layer.dist = 2.0 labels = QgsVectorLayerSimpleLabeling(pal_layer) @@ -217,6 +222,7 @@ class Edge(Input): input_type = "Edge" geometry_type = "Linestring" attributes = [ + QgsField("name", QVariant.String), QgsField("from_node_id", QVariant.Int), QgsField("to_node_id", QVariant.Int), QgsField("edge_type", QVariant.String), @@ -281,6 +287,15 @@ def renderer(self) -> QgsCategorizedSymbolRenderer: ) return renderer + @property + def labels(self) -> Any: + pal_layer = QgsPalLayerSettings() + pal_layer.fieldName = "name" + pal_layer.enabled = True + pal_layer.placement = Qgis.LabelPlacement.Line + labels = QgsVectorLayerSimpleLabeling(pal_layer) + return labels + class BasinProfile(Input): input_type = "Basin / profile"