"""Built-in node templates: SVG symbol + port configuration + default props. A :class:`NodeTemplate` is a reusable definition dragged from the node panel to create :class:`NodeModel` instances on the canvas. """ from __future__ import annotations from typing import Callable, Iterable, Optional from .node import NodeModel from .port import Port, PortDirection, PortSide from .properties import Property, PropertyBag, PropertyGroup, PropertyType def _presentation(title: str, color: str = "#37474f") -> PropertyGroup: return PropertyGroup("Presentation", [ Property("title", "Title", PropertyType.STRING, title), Property("color", "Color", PropertyType.COLOR, color), Property("notes", "Notes", PropertyType.STRING, ""), ]) class NodeTemplate: """Definition of a draggable node type.""" def __init__( self, key: str, label: str, group: str, svg_name: str, ports: Iterable[Port], *, width: float = 80.0, height: float = 80.0, default_color: str = "#37474f", physics: Optional[Callable[[], PropertyGroup]] = None, ) -> None: self.key = key self.label = label self.group = group self.svg_name = svg_name self.width = width self.height = height self.default_color = default_color self._ports = list(ports) self._physics = physics def make_properties(self) -> PropertyBag: bag = PropertyBag([_presentation(self.label, self.default_color)]) if self._physics is not None: bag.add_group(self._physics()) return bag def instantiate(self, node_id: str, x: float, y: float) -> NodeModel: return NodeModel( node_id, self.key, svg_name=self.svg_name, x=x, y=y, width=self.width, height=self.height, ports=[p.clone() for p in self._ports], properties=self.make_properties(), ) class NodeLibrary: """Ordered collection of templates, grouped for the node panel.""" def __init__(self, templates: Optional[Iterable[NodeTemplate]] = None) -> None: self._templates: dict[str, NodeTemplate] = {} for t in templates or []: self.add(t) def add(self, template: NodeTemplate) -> NodeTemplate: self._templates[template.key] = template return template def get(self, key: str) -> Optional[NodeTemplate]: return self._templates.get(key) def groups(self) -> dict[str, list[NodeTemplate]]: out: dict[str, list[NodeTemplate]] = {} for t in self._templates.values(): out.setdefault(t.group, []).append(t) return out def __iter__(self): return iter(self._templates.values()) # -- physics group builders ---------------------------------------------- def _pump_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("head", "Rated Head", PropertyType.FLOAT, 30.0, unit="m", minimum=0), Property("power", "Power", PropertyType.FLOAT, 5.5, unit="kW", minimum=0), Property("status", "Status", PropertyType.ENUM, "on", options=["on", "off"]), ]) def _tank_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("volume", "Volume", PropertyType.FLOAT, 100.0, unit="m3", minimum=0), Property("level", "Level", PropertyType.FLOAT, 50.0, unit="%", minimum=0, maximum=100), Property("elevation", "Elevation", PropertyType.FLOAT, 0.0, unit="m"), ]) def _valve_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("diameter", "Diameter", PropertyType.CATALOGUE_ITEM, "DN100", catalogue="pipe_classes"), Property("state", "State", PropertyType.ENUM, "open", options=["open", "closed", "throttled"]), Property("opening", "Opening", PropertyType.FLOAT, 100.0, unit="%", minimum=0, maximum=100), ]) def _source_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("supply", "Supply Rate", PropertyType.FLOAT, 120.0, unit="m3/h", minimum=0), Property("pressure", "Pressure", PropertyType.FLOAT, 4.0, unit="bar", minimum=0), ]) def _sink_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("demand", "Demand", PropertyType.FLOAT, 40.0, unit="m3/h", minimum=0), Property("flow_type", "Flow Type", PropertyType.ENUM, "steady", options=["steady", "peak", "intermittent"]), ]) def _junction_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("elevation", "Elevation", PropertyType.FLOAT, 0.0, unit="m"), ]) def _transformer_physics() -> PropertyGroup: return PropertyGroup("Physics", [ Property("rating", "Rating", PropertyType.FLOAT, 250.0, unit="kVA", minimum=0), Property("primary_v", "Primary", PropertyType.FLOAT, 11.0, unit="kV", minimum=0), Property("secondary_v", "Secondary", PropertyType.FLOAT, 0.4, unit="kV", minimum=0), ]) def default_library() -> NodeLibrary: """A starter library covering water and power network symbols.""" W = "water" P = "power" lib = NodeLibrary() lib.add(NodeTemplate( "source", "Water Source", "Sources", "source.svg", [Port("out", W, PortSide.RIGHT, 0.5, name="Outlet", direction=PortDirection.OUTPUT)], default_color="#1565c0", physics=_source_physics, )) lib.add(NodeTemplate( "tank", "Storage Tank", "Storage", "tank.svg", [Port("in", W, PortSide.TOP, 0.35, name="Inlet", direction=PortDirection.INPUT), Port("out", W, PortSide.BOTTOM, 0.65, name="Outlet", direction=PortDirection.OUTPUT)], default_color="#0277bd", physics=_tank_physics, )) lib.add(NodeTemplate( "pump", "Pump", "Equipment", "pump.svg", [Port("in", W, PortSide.LEFT, 0.5, name="Suction", direction=PortDirection.INPUT), Port("out", W, PortSide.RIGHT, 0.5, name="Discharge", direction=PortDirection.OUTPUT)], default_color="#00838f", physics=_pump_physics, )) lib.add(NodeTemplate( "valve", "Valve", "Equipment", "valve.svg", [Port("in", W, PortSide.LEFT, 0.5, name="In", direction=PortDirection.INPUT), Port("out", W, PortSide.RIGHT, 0.5, name="Out", direction=PortDirection.OUTPUT)], width=80, height=50, default_color="#455a64", physics=_valve_physics, )) lib.add(NodeTemplate( "junction", "Junction", "Junctions", "junction.svg", [Port("n", W, PortSide.TOP, 0.5, name="N"), Port("e", W, PortSide.RIGHT, 0.5, name="E"), Port("s", W, PortSide.BOTTOM, 0.5, name="S"), Port("w", W, PortSide.LEFT, 0.5, name="W")], width=50, height=50, default_color="#546e7a", physics=_junction_physics, )) lib.add(NodeTemplate( "consumer", "Consumer", "Sinks", "consumer.svg", [Port("in", W, PortSide.LEFT, 0.5, name="Inlet", direction=PortDirection.INPUT)], default_color="#5d4037", physics=_sink_physics, )) lib.add(NodeTemplate( "transformer", "Transformer", "Power", "transformer.svg", [Port("hv", P, PortSide.TOP, 0.5, name="HV", direction=PortDirection.INPUT), Port("lv", P, PortSide.BOTTOM, 0.5, name="LV", direction=PortDirection.OUTPUT)], default_color="#2e7d32", physics=_transformer_physics, )) lib.add(NodeTemplate( "power_source", "Power Source", "Power", "power_source.svg", [Port("out", P, PortSide.RIGHT, 0.5, name="Feed", direction=PortDirection.OUTPUT)], default_color="#388e3c", physics=_source_physics, )) return lib