diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2fda50e5..1d3d5abd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -47,7 +47,7 @@ repos:
- --license-filepath
- license_header.txt
- --comment-style
- - '#'
+ - "#"
- id: insert-license
name: Insert Licence for HTML/XML/SVG files
files: '\.html$|\.md$|\.svg$'
@@ -56,7 +56,7 @@ repos:
- --license-filepath
- license_header.txt
- --comment-style
- - ''
+ - ""
- id: insert-license
name: Insert Licence for CSS files
files: '\.css$'
@@ -65,7 +65,7 @@ repos:
- --license-filepath
- license_header.txt
- --comment-style
- - '/*| *| */'
+ - "/*| *| */"
- repo: https://github.com/fsfe/reuse-tool
rev: v2.1.0
hooks:
diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py
index 376ba2d2..48ddd949 100644
--- a/capellambse_context_diagrams/__init__.py
+++ b/capellambse_context_diagrams/__init__.py
@@ -48,6 +48,7 @@ def init() -> None:
register_classes()
register_interface_context()
register_tree_view()
+ register_realization_view()
# register_functional_context() XXX: Future
@@ -152,9 +153,45 @@ def register_functional_context() -> None:
def register_tree_view() -> None:
- """Add the `tree_view` attribute to ``Class``es."""
+ """Add the ``tree_view`` attribute to ``Class``es."""
common.set_accessor(
information.Class,
"tree_view",
context.ClassTreeAccessor(DiagramType.CDB.value),
)
+
+
+def register_realization_view() -> None:
+ """Add the ``realization_view`` attribute to various objects.
+
+ Adds ``realization_view`` to Activities, Functions and Components
+ of all layers.
+ """
+ supported_classes: list[ClassPair] = [
+ (oa.Entity, DiagramType.OAB),
+ (oa.OperationalActivity, DiagramType.OAIB),
+ (ctx.SystemComponent, DiagramType.SAB),
+ (ctx.SystemFunction, DiagramType.SDFB),
+ (la.LogicalComponent, DiagramType.LAB),
+ (la.LogicalFunction, DiagramType.LDFB),
+ (pa.PhysicalComponent, DiagramType.PAB),
+ (pa.PhysicalFunction, DiagramType.PDFB),
+ ]
+ styles: dict[str, dict[str, capstyle.CSSdef]] = {}
+ for class_, dgcls in supported_classes:
+ common.set_accessor(
+ class_,
+ "realization_view",
+ context.RealizationViewContextAccessor("RealizationView Diagram"),
+ )
+ styles.update(capstyle.STYLES.get(dgcls.value, {}))
+
+ capstyle.STYLES["RealizationView Diagram"] = styles
+ capstyle.STYLES["RealizationView Diagram"].update(
+ capstyle.STYLES["__GLOBAL__"]
+ )
+ capstyle.STYLES["RealizationView Diagram"]["Edge.Realization"] = {
+ "stroke": capstyle.COLORS["dark_gray"],
+ "marker-end": "FineArrowMark",
+ "stroke-dasharray": "5",
+ }
diff --git a/capellambse_context_diagrams/_elkjs.py b/capellambse_context_diagrams/_elkjs.py
index 8ed87aad..b84053e0 100644
--- a/capellambse_context_diagrams/_elkjs.py
+++ b/capellambse_context_diagrams/_elkjs.py
@@ -43,7 +43,7 @@
NODE_HOME = Path(capellambse.dirs.user_cache_dir, "elkjs", "node_modules")
PATH_TO_ELK_JS = Path(__file__).parent / "elk.js"
REQUIRED_NPM_PKG_VERSIONS: t.Dict[str, str] = {
- "elkjs": "0.8.2",
+ "elkjs": "0.9.0",
}
"""npm package names and versions required by this Python module."""
@@ -149,18 +149,24 @@ class ELKSize(t.TypedDict):
height: t.Union[int, float]
-class ELKOutputData(t.TypedDict):
- """Data that comes from ELK."""
+class ELKOutputElement(t.TypedDict):
+ """Base class for all elements that comes out of ELK."""
id: str
+
+ style: dict[str, t.Any]
+
+
+class ELKOutputData(ELKOutputElement):
+ """Data that comes from ELK."""
+
type: t.Literal["graph"]
children: cabc.MutableSequence[ELKOutputChild] # type: ignore
-class ELKOutputNode(t.TypedDict):
+class ELKOutputNode(ELKOutputElement):
"""Node that comes out of ELK."""
- id: str
type: t.Literal["node"]
children: cabc.MutableSequence[ELKOutputChild] # type: ignore
@@ -168,20 +174,18 @@ class ELKOutputNode(t.TypedDict):
size: ELKSize
-class ELKOutputJunction(t.TypedDict):
+class ELKOutputJunction(ELKOutputElement):
"""Exchange-Junction that comes out of ELK."""
- id: str
type: t.Literal["junction"]
position: ELKPoint
size: ELKSize
-class ELKOutputPort(t.TypedDict):
+class ELKOutputPort(ELKOutputElement):
"""Port that comes out of ELK."""
- id: str
type: t.Literal["port"]
children: cabc.MutableSequence[ELKOutputLabel]
@@ -189,10 +193,9 @@ class ELKOutputPort(t.TypedDict):
size: ELKSize
-class ELKOutputLabel(t.TypedDict):
+class ELKOutputLabel(ELKOutputElement):
"""Label that comes out of ELK."""
- id: str
type: t.Literal["label"]
text: str
@@ -200,11 +203,11 @@ class ELKOutputLabel(t.TypedDict):
size: ELKSize
-class ELKOutputEdge(t.TypedDict):
+class ELKOutputEdge(ELKOutputElement):
"""Edge that comes out of ELK."""
- id: str
type: t.Literal["edge"]
+
sourceId: str
targetId: str
routingPoints: cabc.MutableSequence[ELKPoint]
diff --git a/capellambse_context_diagrams/collectors/realization_view.py b/capellambse_context_diagrams/collectors/realization_view.py
new file mode 100644
index 00000000..eef6ab06
--- /dev/null
+++ b/capellambse_context_diagrams/collectors/realization_view.py
@@ -0,0 +1,213 @@
+# SPDX-FileCopyrightText: 2022 Copyright DB Netz AG and the capellambse-context-diagrams contributors
+# SPDX-License-Identifier: Apache-2.0
+"""This submodule defines the collector for the RealizationView diagram."""
+from __future__ import annotations
+
+import collections.abc as cabc
+import copy
+import re
+import typing as t
+
+from capellambse.model import common, crosslayer
+from capellambse.model.crosslayer import cs, fa
+
+from .. import _elkjs, context
+from . import makers
+
+RE_LAYER_PTRN = re.compile(r"([A-Z]?[a-z]+)")
+
+
+def collector(
+ diagram: context.ContextDiagram, params: dict[str, t.Any]
+) -> tuple[_elkjs.ELKInputData, list[_elkjs.ELKInputEdge]]:
+ """Return the class tree data for ELK."""
+ data = makers.make_diagram(diagram)
+ layout_options: _elkjs.LayoutOptions = copy.deepcopy(
+ _elkjs.RECT_PACKING_LAYOUT_OPTIONS # type:ignore[arg-type]
+ )
+ layout_options["elk.contentAlignment"] = "V_CENTER H_CENTER"
+ del layout_options["widthApproximation.targetWidth"]
+ data["layoutOptions"] = layout_options
+ _collector = COLLECTORS[params.get("search_direction", "ALL")]
+ lay_to_els = _collector(diagram.target, params.get("depth", 1))
+ layer_layout_options: _elkjs.LayoutOptions = layout_options | { # type: ignore[operator]
+ "nodeSize.constraints": "[NODE_LABELS,MINIMUM_SIZE]",
+ } # type: ignore[assignment]
+ edges: list[_elkjs.ELKInputEdge] = []
+ for layer in ("Operational", "System", "Logical", "Physical"):
+ if not (elements := lay_to_els.get(layer)): # type: ignore[call-overload]
+ continue
+
+ labels = [makers.make_label(layer)]
+ width, height = makers.calculate_height_and_width(labels)
+ layer_box: _elkjs.ELKInputChild = {
+ "id": elements[0]["layer"].uuid,
+ "children": [],
+ "height": width,
+ "width": height,
+ "layoutOptions": layer_layout_options,
+ }
+ children: dict[str, _elkjs.ELKInputChild] = {}
+ for elt in elements:
+ assert elt["element"] is not None
+ if elt["origin"] is not None:
+ edges.append(
+ {
+ "id": f'{elt["origin"].uuid}_{elt["element"].uuid}',
+ "sources": [elt["origin"].uuid],
+ "targets": [elt["element"].uuid],
+ }
+ )
+
+ if elt.get("reverse", False):
+ source = elt["element"]
+ target = elt["origin"]
+ else:
+ source = elt["origin"]
+ target = elt["element"]
+
+ if not (element_box := children.get(target.uuid)):
+ element_box = makers.make_box(target, no_symbol=True)
+ children[target.uuid] = element_box
+ layer_box["children"].append(element_box)
+ index = len(layer_box["children"]) - 1
+
+ if params.get("show_owners"):
+ owner = target.owner
+ if not isinstance(owner, (fa.Function, cs.Component)):
+ continue
+
+ if not (owner_box := children.get(owner.uuid)):
+ owner_box = makers.make_box(owner, no_symbol=True)
+ owner_box["height"] += element_box["height"]
+ children[owner.uuid] = owner_box
+ layer_box["children"].append(owner_box)
+
+ del layer_box["children"][index]
+ owner_box.setdefault("children", []).append(element_box)
+ owner_box["width"] += element_box["width"]
+ if (
+ source is not None
+ and source.owner.uuid in children
+ and owner.uuid in children
+ ):
+ eid = f"{source.owner.uuid}_{owner.uuid}"
+ edges.append(
+ {
+ "id": eid,
+ "sources": [source.owner.uuid],
+ "targets": [owner.uuid],
+ }
+ )
+
+ data["children"].append(layer_box)
+ return data, edges
+
+
+def collect_realized(
+ start: common.GenericElement, depth: int
+) -> dict[LayerLiteral, list[dict[str, t.Any]]]:
+ """Collect all elements from ``realized_`` attributes up to depth."""
+ return collect_elements(start, depth, "ABOVE", "realized")
+
+
+def collect_realizing(
+ start: common.GenericElement, depth: int
+) -> dict[LayerLiteral, list[dict[str, t.Any]]]:
+ """Collect all elements from ``realizing_`` attributes down to depth."""
+ return collect_elements(start, depth, "BELOW", "realizing")
+
+
+def collect_all(
+ start: common.GenericElement, depth: int
+) -> dict[LayerLiteral, list[common.GenericElement]]:
+ """Collect all elements in both ABOVE and BELOW directions."""
+ above = collect_realized(start, depth)
+ below = collect_realizing(start, depth)
+ return above | below
+
+
+def collect_elements(
+ start: common.GenericElement,
+ depth: int,
+ direction: str,
+ attribute_prefix: str,
+ origin: common.GenericElement = None,
+) -> dict[LayerLiteral, list[dict[str, t.Any]]]:
+ """Collect elements based on the specified direction and attribute name."""
+ layer_obj, layer = find_layer(start)
+ collected_elements: dict[LayerLiteral, list[dict[str, t.Any]]] = {}
+ if direction == "ABOVE" or origin is None:
+ collected_elements = {
+ layer: [{"element": start, "origin": origin, "layer": layer_obj}]
+ }
+ elif direction == "BELOW" and origin is not None:
+ collected_elements = {
+ layer: [
+ {
+ "element": origin,
+ "origin": start,
+ "layer": layer_obj,
+ "reverse": True,
+ }
+ ]
+ }
+
+ if (
+ (direction == "ABOVE" and layer == "Operational")
+ or (direction == "BELOW" and layer == "Physical")
+ or depth == 0
+ ):
+ return collected_elements
+
+ if isinstance(start, fa.Function):
+ attribute_name = f"{attribute_prefix}_functions"
+ else:
+ assert isinstance(start, cs.Component)
+ attribute_name = f"{attribute_prefix}_components"
+
+ for element in getattr(start, attribute_name, []):
+ sub_collected = collect_elements(
+ element, depth - 1, direction, attribute_prefix, origin=start
+ )
+ for sub_layer, sub_elements in sub_collected.items():
+ collected_elements.setdefault(sub_layer, []).extend(sub_elements)
+ return collected_elements
+
+
+LayerLiteral = t.Union[
+ t.Literal["Operational"],
+ t.Literal["System"],
+ t.Literal["Logical"],
+ t.Literal["Physical"],
+]
+
+
+def find_layer(
+ obj: common.GenericElement,
+) -> tuple[crosslayer.BaseArchitectureLayer, LayerLiteral]:
+ """Return the layer object and its literal.
+
+ Return either one of the following:
+ * ``Operational``
+ * ``System``
+ * ``Logical``
+ * ``Physical``
+ """
+ parent = obj
+ while not isinstance(parent, crosslayer.BaseArchitectureLayer):
+ parent = parent.parent
+ if not (match := RE_LAYER_PTRN.match(type(parent).__name__)):
+ raise ValueError("No layer was found.")
+ return parent, match.group(1) # type:ignore[return-value]
+
+
+Collector = cabc.Callable[
+ [common.GenericElement, int], dict[LayerLiteral, list[dict[str, t.Any]]]
+]
+COLLECTORS: dict[str, Collector] = {
+ "ALL": collect_all,
+ "ABOVE": collect_realized,
+ "BELOW": collect_realizing,
+}
+"""The functions to receive the diagram elements for every layer."""
diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py
index b9970fe7..6833115a 100644
--- a/capellambse_context_diagrams/context.py
+++ b/capellambse_context_diagrams/context.py
@@ -13,10 +13,11 @@
import typing as t
from capellambse import diagram as cdiagram
+from capellambse import helpers
from capellambse.model import common, diagram, modeltypes
from . import _elkjs, filters, serializers, styling
-from .collectors import exchanges, get_elkdata, tree_view
+from .collectors import exchanges, get_elkdata, realization_view, tree_view
logger = logging.getLogger(__name__)
@@ -142,7 +143,7 @@ def __get__( # type: ignore
obj: common.T | None,
objtype: type | None = None,
) -> common.Accessor | ContextDiagram:
- """Make a ContextDiagram for the given model object."""
+ """Make a ClassTreeDiagram for the given model object."""
del objtype
if obj is None: # pragma: no cover
return self
@@ -150,6 +151,26 @@ def __get__( # type: ignore
return self._get(obj, ClassTreeDiagram, "{}_class_tree")
+class RealizationViewContextAccessor(ContextAccessor):
+ """Provides access to the realization view diagrams."""
+
+ # pylint: disable=super-init-not-called
+ def __init__(self, diagclass: str) -> None:
+ self._dgcls = diagclass
+
+ def __get__( # type: ignore
+ self,
+ obj: common.T | None,
+ objtype: type | None = None,
+ ) -> common.Accessor | ContextDiagram:
+ """Make a RealizationViewDiagram for the given model object."""
+ del objtype
+ if obj is None: # pragma: no cover
+ return self
+ assert isinstance(obj, common.GenericElement)
+ return self._get(obj, RealizationViewDiagram, "{}_realization_view")
+
+
class ContextDiagram(diagram.AbstractDiagram):
"""An automatically generated context diagram.
@@ -275,12 +296,8 @@ def render(self, fmt: str | None, /, **params) -> t.Any:
return super().render(fmt, **rparams)
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
- try:
- data = params.get("elkdata") or get_elkdata(self, params)
- layout = _elkjs.call_elkjs(data)
- except json.JSONDecodeError as error:
- logger.error(json.dumps(data, indent=4))
- raise error
+ data = params.get("elkdata") or get_elkdata(self, params)
+ layout = try_to_layout(data)
return self.serializer.make_diagram(layout)
@property # type: ignore
@@ -379,6 +396,100 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
return class_diagram
+class RealizationViewDiagram(ContextDiagram):
+ """An automatically generated RealizationViewDiagram Diagram.
+
+ This diagram is exclusively for ``Activity``, ``Function``s,
+ ``Entity`` and ``Components`` of all layers.
+ """
+
+ def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
+ super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)
+
+ @property
+ def uuid(self) -> str: # type: ignore
+ """Returns the UUID of the diagram."""
+ return f"{self.target.uuid}_realization_view"
+
+ @property
+ def name(self) -> str: # type: ignore
+ """Returns the name of the diagram."""
+ return f"Realization view of {self.target.name}"
+
+ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
+ params.setdefault("depth", params.get("depth", 1))
+ params.setdefault(
+ "search_direction", params.get("search_direction", "ALL")
+ )
+ params.setdefault("show_owners", params.get("show_owners", True))
+ data, edges = realization_view.collector(self, params)
+ layout = try_to_layout(data)
+ min_width = max(child["size"]["width"] for child in layout["children"]) # type: ignore[typeddict-item]
+ min_width += 15.0
+ for layer in data["children"]:
+ min_height: int | float = 0
+ for layout_layer in layout["children"]:
+ if layer["id"] != layout_layer["id"]:
+ continue
+ assert layout_layer["type"] != "edge"
+ min_height = layout_layer["size"]["height"]
+
+ assert min_height > 0
+ layer["width"] = min_width
+ layer["layoutOptions"][
+ "nodeSize.minimum"
+ ] = f"({min_width},{min_height})"
+
+ layout = try_to_layout(data)
+ for edge in edges:
+ layout["children"].append(
+ {
+ "id": edge["id"],
+ "type": "edge",
+ "sourceId": edge["sources"][0],
+ "targetId": edge["targets"][0],
+ "routingPoints": [],
+ "styleclass": "Realization",
+ } # type: ignore[arg-type]
+ )
+ self._add_layer_labels(layout)
+ return self.serializer.make_diagram(layout)
+
+ def _add_layer_labels(self, layout: _elkjs.ELKOutputData) -> None:
+ for layer in layout["children"]:
+ if layer["type"] != "node":
+ continue
+
+ layer_obj = self.serializer.model.by_uuid(layer["id"])
+ _, layer_name = realization_view.find_layer(layer_obj)
+ pos = layer["position"]["x"], layer["position"]["y"]
+ size = layer["size"]["width"], layer["size"]["height"]
+ width, height = helpers.get_text_extent(layer_name)
+ x, y, tspan_y = calculate_label_position(*pos, *size)
+ label_box: _elkjs.ELKOutputChild = {
+ "type": "label",
+ "id": "None",
+ "text": layer_name,
+ "position": {"x": x, "y": y},
+ "size": {"width": width, "height": height},
+ "style": {
+ "text_transform": f"rotate(-90, {x}, {y}) {tspan_y}",
+ "text_fill": "grey",
+ },
+ }
+ layer["children"].insert(0, label_box)
+ layer["style"] = {"stroke": "grey", "rx": 5, "ry": 5}
+
+
+def try_to_layout(data: _elkjs.ELKInputData) -> _elkjs.ELKOutputData:
+ """Try calling elkjs, raise a JSONDecodeError if it fails."""
+ try:
+ return _elkjs.call_elkjs(data)
+ except json.JSONDecodeError as error:
+ logger.error(json.dumps(data, indent=4))
+ raise error
+
+
def stack_diagrams(
first: cdiagram.Diagram,
second: cdiagram.Diagram,
@@ -391,3 +502,32 @@ def stack_diagrams(
new = copy.deepcopy(element)
new.move(offset)
first += new
+
+
+def calculate_label_position(
+ x: float,
+ y: float,
+ width: float,
+ height: float,
+ padding: float = 10,
+) -> tuple[float, float, float]:
+ """Calculate the position of the label and tspan.
+
+ The function calculates the center of the rectangle and uses the
+ rectangle's width and height to adjust its position within it. The
+ text is assumed to be horizontally and vertically centered within
+ the rectangle. The tspan y coordinate is for positioning the label
+ right under the left side of the rectangle.
+
+ Parameters
+ ----------
+
+ Returns
+ -------
+ tuple
+ A tuple containing the x- and y-coordinate for the text element
+ and the adjusted y-coordinate for the tspan element.
+ """
+ center_y = y + height / 2
+ tspan_y = center_y - width / 2 + padding
+ return (x + width / 2, center_y, tspan_y)
diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py
index cc6593a9..cbfb7eb3 100644
--- a/capellambse_context_diagrams/serializers.py
+++ b/capellambse_context_diagrams/serializers.py
@@ -10,6 +10,7 @@
from __future__ import annotations
import logging
+import typing as t
from capellambse import diagram
from capellambse.svg import decorations
@@ -145,12 +146,20 @@ class type that stores all previously named classes.
self.diagram.add_element(element)
self._cache[child["id"]] = element
elif child["type"] == "edge":
+ styleclass = child.get("styleclass", styleclass) # type: ignore[assignment]
styleclass = REMAP_STYLECLASS.get(styleclass, styleclass) # type: ignore[arg-type]
- element = diagram.Edge(
- [
+ if child["routingPoints"]:
+ refpoints = [
ref + (point["x"], point["y"])
for point in child["routingPoints"]
- ],
+ ]
+ else:
+ source = self._cache[child["sourceId"]]
+ target = self._cache[child["targetId"]]
+ refpoints = route_shortest_connection(source, target)
+
+ element = diagram.Edge(
+ refpoints,
uuid=child["id"],
source=self.diagram[child["sourceId"]],
target=self.diagram[child["targetId"]],
@@ -164,12 +173,13 @@ class type that stores all previously named classes.
if isinstance(parent, diagram.Box) and not parent.port:
if parent.JSON_TYPE != "symbol":
parent.label = child["text"]
+ parent.styleoverrides |= self.get_styleoverrides(child)
else:
parent.label = diagram.Box(
ref + (child["position"]["x"], child["position"]["y"]),
(child["size"]["width"], child["size"]["height"]),
label=child["text"],
- # parent=parent,
+ styleoverrides=self.get_styleoverrides(child),
)
else:
assert isinstance(parent, diagram.Edge)
@@ -230,21 +240,25 @@ def get_styleclass(self, uuid: str) -> str | None:
def get_styleoverrides(
self, child: _elkjs.ELKOutputChild
- ) -> diagram.StyleOverrides | None:
+ ) -> diagram.StyleOverrides:
"""Return
[`styling.CSSStyles`][capellambse_context_diagrams.styling.CSSStyles]
from a given
[`_elkjs.ELKOutputChild`][capellambse_context_diagrams._elkjs.ELKOutputChild].
"""
style_condition = self._diagram.render_styles.get(child["type"])
- styleoverrides = None
+ styleoverrides: dict[str, t.Any] = {}
if style_condition is not None:
if child["type"] != "junction":
obj = self._diagram._model.by_uuid(child["id"])
else:
obj = None
- styleoverrides = style_condition(obj, self)
+ styleoverrides = style_condition(obj, self) or {}
+
+ style: dict[str, t.Any]
+ if style := child.get("style", {}):
+ styleoverrides |= style
return styleoverrides
def order_children(self) -> None:
@@ -277,3 +291,24 @@ def handle_features(child: _elkjs.ELKOutputNode) -> list[str]:
features.append(c["text"])
child["children"] = child["children"][:1]
return features
+
+
+def route_shortest_connection(
+ source: diagram.Box,
+ target: diagram.Box,
+) -> list[diagram.Vector2D]:
+ """Calculate shortest path between boxes with 'Oblique' style.
+
+ Calculate the intersection points of the line from source.center to
+ target.center with the bounding boxes of the source and target.
+ """
+ line_start = source.center
+ line_end = target.center
+
+ source_intersection = source.vector_snap(
+ line_start, source=line_end, style=diagram.RoutingStyle.OBLIQUE
+ )
+ target_intersection = target.vector_snap(
+ line_end, source=line_start, style=diagram.RoutingStyle.OBLIQUE
+ )
+ return [source_intersection, target_intersection]
diff --git a/docs/gen_images.py b/docs/gen_images.py
index d7873d72..2dae034f 100644
--- a/docs/gen_images.py
+++ b/docs/gen_images.py
@@ -33,6 +33,8 @@
hierarchy_context = "16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb"
diagram_uuids = general_context_diagram_uuids | interface_context_diagram_uuids
class_tree_uuid = "b7c7f442-377f-492c-90bf-331e66988bda"
+realization_fnc_uuid = "beaf5ba4-8fa9-4342-911f-0266bb29be45"
+realization_comp_uuid = "b9f9a83c-fb02-44f7-9123-9d86326de5f1"
def generate_index_images() -> None:
@@ -112,6 +114,22 @@ def generate_class_tree_images() -> None:
)
+def generate_realization_view_images() -> None:
+ for uuid in (realization_fnc_uuid, realization_comp_uuid):
+ obj = model.by_uuid(uuid)
+ diag = obj.realization_view
+ with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd:
+ print(
+ diag.render(
+ "svg",
+ depth=3,
+ search_direction="ALL",
+ show_owners=True,
+ ),
+ file=fd,
+ )
+
+
generate_index_images()
generate_hierarchy_image()
generate_no_symbol_images()
@@ -134,3 +152,4 @@ def generate_class_tree_images() -> None:
)
generate_styling_image(wizard, {}, "no_styles")
generate_class_tree_images()
+generate_realization_view_images()
diff --git a/docs/realization_view.md b/docs/realization_view.md
new file mode 100644
index 00000000..8ba377a1
--- /dev/null
+++ b/docs/realization_view.md
@@ -0,0 +1,70 @@
+
+
+# Tree View Diagram
+
+With release
+[`v0.5.42`](https://github.com/DSD-DBS/py-capellambse/releases/tag/v0.5.42) of
+[py-capellambse](https://github.com/DSD-DBS/py-capellambse) you can access the
+`.realization_view` on a Component or Function from any layer. A realization
+view diagram reveals the realization map that the layers of your model
+implement currently. The diagram elements are collected from the
+`.realized_components` or `.realized_functions` attribute for the direction
+`ABOVE` and `.realizing_components` or `.realizing_functions` for direction
+`BELOW`.
+
+??? example "Realization View Diagram of `LogicalFunction` `advise Harry`"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("beaf5ba4-8fa9-4342-911f-0266bb29be45").realization_view
+ diag.render(
+ "svgdiagram",
+ depth=3, # 1-3
+ search_direction="ALL", # BELOW; ABOVE and ALL
+ show_owners=True,
+ ).save_drawing(pretty=True)
+ ```
+
+
+??? example "Realization View Diagram of `PhysicalComponent` `Physical System`"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("b9f9a83c-fb02-44f7-9123-9d86326de5f1").realization_view
+ diag.render(
+ "svgdiagram",
+ depth=3,
+ search_direction="ALL",
+ show_owners=True,
+ ).save_drawing(pretty=True)
+ ```
+
+
+Additional rendering parameters enable showing owning functions or components,
+as well as the depth of traversion (i.e. `1`-`3`). They are put to display the
+maximum amount of diagram elements per default.
+
+??? bug "Alignment of diagram elements"
+
+ As of [elkjs@0.9.0](https://eclipse.dev/elk/downloads/releasenotes/release-0.9.0.html) ELK's rectpacking algorithm isn't correctly using the
+ content alignment enumeration. While developing the Realization View
+ [a fix for the horizontal alignment was proposed](https://github.com/eclipse/elk/issues/989).
+
+## Check out the code
+
+To understand the collection have a look into the
+[`realization_view`][capellambse_context_diagrams.collectors.realization_view]
+module.
diff --git a/mkdocs.yml b/mkdocs.yml
index b0c0201e..88d9ead7 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -91,6 +91,8 @@ nav:
- Styling: extras/styling.md
- Tree View:
- Overview: tree_view.md
+ - Realization View:
+ - Overview: realization_view.md
- Code Reference: reference/
extra_css:
diff --git a/tests/data/ContextDiagram.afm b/tests/data/ContextDiagram.afm
index dc32e25b..02f4486c 100644
--- a/tests/data/ContextDiagram.afm
+++ b/tests/data/ContextDiagram.afm
@@ -1,6 +1,6 @@
-
-
-
+
+
+
diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella
index abcda219..7a48804d 100644
--- a/tests/data/ContextDiagram.capella
+++ b/tests/data/ContextDiagram.capella
@@ -1536,6 +1536,21 @@ The predator is far away
source="#00e7b925-cf4c-4cb0-929e-5409a1cd872b" target="#85d41db2-9e17-438b-95cf-49342452ddf3"/>
+
+
+
+
+
@@ -2211,6 +2226,9 @@ The predator is far away
+
+
id="e7ea5a90-3b07-499d-b17f-4e61917c850a" name="CP 2" orientation="OUT"
kind="FLOW"/>
+
id="beaf5ba4-8fa9-4342-911f-0266bb29be45" name="advise Harry">
+
@@ -3258,10 +3285,6 @@ The predator is far away
name="Right" abstractType="#37dfa5e6-a121-4ce9-8aa4-09a0c73dc2e9"/>
-
-
@@ -3296,7 +3319,7 @@ The predator is far away
name="School" abstractType="#a58821df-c5b4-4958-9455-0d30755be6b1"/>
+ actor="true">
@@ -3317,6 +3340,12 @@ The predator is far away
+
+
@@ -3481,6 +3510,73 @@ The predator is far away
id="f0e39876-6665-4d6c-9eec-b0c0f1dd8399" targetElement="#b7f2d2ef-3aef-43a9-9309-cf172e50ebb7"
sourceElement="#58f89f0a-7647-4077-b29b-8fe2bf270f02"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3495,69 +3591,6 @@ The predator is far away
sourceElement="#1286b0fe-dd57-46ba-8303-bd57eb04178d"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id="d254c2aa-3fc4-4deb-a684-6191ace32c42" name="Child Function">
+
@@ -3869,6 +3905,9 @@ The predator is far away
+
@@ -3886,6 +3925,9 @@ The predator is far away
id="da53c21f-4fb8-4da7-8d58-12d7a600bfa6" name="Apping around">
+
+ id="b9f9a83c-fb02-44f7-9123-9d86326de5f1" name="Physical System" nature="NODE">
+
@@ -4024,6 +4069,24 @@ The predator is far away
+
+
+
+
+
+
None:
+ obj = model.by_uuid(uuid)
+
+ diag = obj.realization_view
+
+ assert diag.render(fmt)
+
+
+@pytest.mark.parametrize("uuid", [TEST_FNC_UUID, TEST_CMP_UUID])
+@pytest.mark.parametrize("depth", list(range(1, 4)))
+@pytest.mark.parametrize("search_direction", ["ABOVE", "BELOW"])
+@pytest.mark.parametrize("show_owners", [True, False])
+def test_tree_view_renders_with_additional_params(
+ model: capellambse.MelodyModel,
+ depth: int,
+ search_direction: str,
+ show_owners: bool,
+ uuid: str,
+) -> None:
+ obj = model.by_uuid(uuid)
+
+ diag = obj.realization_view
+
+ assert diag.render(
+ "svgdiagram",
+ depth=depth,
+ search_direction=search_direction,
+ show_owners=show_owners,
+ )