Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat element relation view #128

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
32 changes: 32 additions & 0 deletions capellambse_context_diagrams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def init() -> None:
register_tree_view()
register_realization_view()
register_data_flow_view()
register_exchange_item_relation_view()
# register_functional_context() XXX: Future


Expand Down Expand Up @@ -271,3 +272,34 @@ def register_data_flow_view() -> None:
for class_, dgcls, default_render_params in supported_classes:
accessor = context.DataFlowAccessor(dgcls.value, default_render_params)
common.set_accessor(class_, "data_flow_view", accessor)


def register_exchange_item_relation_view() -> None:

supported_classes: list[
tuple[type[common.GenericElement], set[DiagramType], dict[str, t.Any]]
] = [
(oa.CommunicationMean, {DiagramType.OAB}, {}),
(
fa.ComponentExchange,
{DiagramType.SAB, DiagramType.LAB, DiagramType.PAB},
{},
),
]

styles: dict[str, dict[str, capstyle.CSSdef]] = {}
for _class, _diagclasses, render_params in supported_classes:
common.set_accessor(
_class,
"exchange_item_relation_view",
context.ExchangeItemRelationAccessor(
"ExchangeItemRelationView Diagram", render_params
),
)
for dgcls in _diagclasses:
styles.update(capstyle.STYLES.get(dgcls.value, {}))

capstyle.STYLES["ExchangeItemRelationView Diagram"] = styles
capstyle.STYLES["ExchangeItemRelationView Diagram"].update(
capstyle.STYLES["Class Diagram Blank"]
)
1 change: 0 additions & 1 deletion capellambse_context_diagrams/_elkjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ class ELKInputEdge(BaseELKModel):

id: str
layoutOptions: LayoutOptions = pydantic.Field(default_factory=dict)

sources: cabc.MutableSequence[str]
targets: cabc.MutableSequence[str]
labels: cabc.MutableSequence[ELKInputLabel] = pydantic.Field(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import logging
import math
import typing as t

from capellambse.model.crosslayer import information

from .. import _elkjs, context
from . import makers, tree_view

logger = logging.getLogger(__name__)

DEFAULT_LAYOUT_OPTIONS: _elkjs.LayoutOptions = {
"algorithm": "layered",
"elk.direction": "RIGHT",
"layered.nodePlacement.strategy": "LINEAR_SEGMENTS",
"edgeStraightening": "NONE",
"spacing.labelNode": "0.0",
"layered.edgeLabels.sideSelection": "ALWAYS_DOWN",
"edgeRouting": "POLYLINE",
"nodeSize.constraints": "NODE_LABELS",
"hierarchyHandling": "INCLUDE_CHILDREN",
"layered.spacing.edgeEdgeBetweenLayers": "2.0",
}


class ExchangeItemRelationCollector:
def __init__(
self,
diagram: context.ExchangeItemRelationViewDiagram,
*,
params: dict[str, t.Any] | None = None,
) -> None:
self.diagram = diagram
self.data = makers.make_diagram(diagram)
self.data.layoutOptions = DEFAULT_LAYOUT_OPTIONS
self.params = params or {}
self.global_boxes: dict[str, _elkjs.ELKInputChild] = {}
self.classes: dict[str, information.Class] = {}
self.edges: dict[str, list[_elkjs.ELKInputEdge]] = {}

def __call__(self) -> _elkjs.ELKInputData:
if not self.diagram.target.exchange_items:
logger.warning("No exchange items to display")
return self.data
for item in self.diagram.target.exchange_items:
if not (parent_box := self.global_boxes.get(item.parent.uuid)):
parent_box = self.global_boxes.setdefault(
item.parent.uuid,
makers.make_box(
item.parent,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
),
)

if item.uuid in (i.id for i in parent_box.children):
continue
box = makers.make_box(item)
parent_box.children.append(box)

for elem in item.elements:
if elem.abstract_type is None:
continue
self.classes.setdefault(
elem.abstract_type.uuid, elem.abstract_type
)
eid = f"__ExchangeItemElement:{item.uuid}_{elem.abstract_type.uuid}"
edge = _elkjs.ELKInputEdge(
id=eid,
sources=[item.uuid],
targets=[elem.abstract_type.uuid],
labels=makers.make_label(
elem.name,
),
)
self.data.edges.append(edge)
self.edges.setdefault(item.parent.uuid, []).append(edge)

for cls in self.classes.values():
box = self._make_class_box(cls)
self.data.children.append(box)
if cls.super:
eid = f"__Generalization:{cls.super.uuid}_{cls.uuid}"
self.data.edges.append(
_elkjs.ELKInputEdge(
id=eid,
sources=[cls.super.uuid],
targets=[cls.uuid],
)
)

top = sum(len(box.children) for box in self.global_boxes.values())
mid = top / 2
ordered = list(self.global_boxes.keys())
for uuid in ordered:
if top < mid:
break
self.data.edges.append(
_elkjs.ELKInputEdge(
id=f"__Hide{uuid}",
sources=[uuid],
targets=[ordered[-1]],
)
)
for edge in self.edges.get(uuid, []):
edge.sources, edge.targets = edge.targets, edge.sources
edge.id = f"__Reverse-{edge.id[2:]}"
top -= len(self.global_boxes[uuid].children)

self.data.children.extend(self.global_boxes.values())
return self.data

def _make_class_box(self, cls: information.Class) -> _elkjs.ELKInputChild:
box = makers.make_box(
cls, layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS
)
properties = [
_elkjs.ELKInputLabel(
text="",
layoutOptions=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
width=0,
height=0,
)
]
for prop in cls.properties:
if prop.type is None or (
isinstance(prop.type, information.Class)
and not prop.type.is_primitive
):
continue

text = tree_view._get_property_text(prop)
labels = makers.make_label(
text,
icon=(makers.ICON_WIDTH, 0),
layout_options=tree_view.DATA_TYPE_LABEL_LAYOUT_OPTIONS,
max_width=math.inf,
)
properties.extend(labels)

box.labels.extend(properties)
box.width, box.height = makers.calculate_height_and_width(properties)
return box


def collector(
diagram: context.ExchangeItemRelationViewDiagram, params: dict[str, t.Any]
) -> _elkjs.ELKInputData:
"""Return ExchangeElement data for ELK."""
return ExchangeItemRelationCollector(diagram, params=params)()
36 changes: 36 additions & 0 deletions capellambse_context_diagrams/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from . import _elkjs, filters, serializers, styling
from .collectors import (
dataflow_view,
exchange_item_relation_view,
exchanges,
get_elkdata,
realization_view,
Expand Down Expand Up @@ -188,6 +189,20 @@ def __get__( # type: ignore
return self._get(obj, DataFlowViewDiagram)


class ExchangeItemRelationAccessor(ContextAccessor):
def __get__( # type: ignore
self,
obj: common.T | None,
objtype: type | None = None,
) -> common.Accessor | ContextDiagram:
"""Make a ExchangeItemRelationViewDiagram 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, ExchangeItemRelationViewDiagram)


class ContextDiagram(diagram.AbstractDiagram):
"""An automatically generated context diagram.

Expand Down Expand Up @@ -675,6 +690,27 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
return super()._create_diagram(params)


class ExchangeItemRelationViewDiagram(ContextDiagram):
"""An automatically generated ExchangeItemRelationViewDiagram."""

@property
def uuid(self) -> str: # type: ignore
"""Returns the UUID of the diagram."""
return f"{self.target.uuid}_exchange_item_relation_view"

@property
def name(self) -> str: # type: ignore
return f"Exchange Item Relation View of {self.target.name}"

def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
data = exchange_item_relation_view.collector(self, params)
layout = try_to_layout(data)
return self.serializer.make_diagram(
layout,
transparent_background=params.get("transparent_background", False),
)


def try_to_layout(data: _elkjs.ELKInputData) -> _elkjs.ELKOutputData:
"""Try calling elkjs, raise a JSONDecodeError if it fails."""
try:
Expand Down
15 changes: 15 additions & 0 deletions capellambse_context_diagrams/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ class type that stores all previously named classes.
uuid: str
styleclass: str | None
derived = False
reverse = False

if child.id.startswith("__Hide"):
if hasattr(child, "position"):
ref = ref + (child.position.x, child.position.y)
for i in getattr(child, "children", []):
self.deserialize_child(i, ref, parent)
return

if child.id.startswith("__"):
if ":" in child.id:
styleclass, uuid = child.id[2:].split(":", 1)
Expand All @@ -139,6 +148,9 @@ class type that stores all previously named classes.
if styleclass.startswith("Derived-"):
styleclass = styleclass.removeprefix("Derived-")
derived = True
elif styleclass.startswith("Reverse-"):
styleclass = styleclass.removeprefix("Reverse-")
reverse = True
else:
styleclass = self.get_styleclass(child.id)
uuid = child.id
Expand Down Expand Up @@ -193,6 +205,9 @@ class type that stores all previously named classes.
if target_id.startswith("__"):
target_id = target_id[2:].split(":", 1)[-1]

if reverse:
source_id, target_id = target_id, source_id

if child.routingPoints:
refpoints = [
ref + (point.x, point.y) for point in child.routingPoints
Expand Down
34 changes: 34 additions & 0 deletions docs/exchange_item_relation_view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--
~ SPDX-FileCopyrightText: 2022 Copyright DB InfraGO AG and the capellambse-context-diagrams contributors
~ SPDX-License-Identifier: Apache-2.0
-->

# Exchange Item Relation View

The `ExchangeItemRelationView` visualizes the hierarchical structure of exchange items and the relationships between their associated classes in a tree view. You can access `.exchange_item_relation_view` on any `fa.ComponentExchange`. Data collection starts on the allocated exchange items and collects the associated classes through their exchange item elements.

??? example "Exchange Item Relation View of C 28"

``` py
import capellambse

model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
diag = model.by_uuid("0ab202d7-6497-4b78-9d13-fd7c9a75486c").exchange_item_relation_view
diag.render("svgdiagram").save(pretty=True)
```
<figure markdown>
<img src="../assets/images/Exchange Item Relation View of C 28.svg">
<figcaption>[LAB] Exchange Item Relation View of C 28</figcaption>
</figure>

## Known Issues

One known issue with the current implementation is related to the routing of edges for ExchangeItemElements. The edges might not be routed optimally in certain cases due to the limitations of ELK'S edge routing algorithms.

This issue could potentially be resolved when Libavoid for ELK becomes publicly available. Libavoid is an advanced edge routing library that offers object-avoiding orthogonal and polyline connector routing, which could improve the layout of the edges in the diagram. At that point the exchange item element labels will be added to the diagram as well.

## Check out the code

To understand the collection have a look into the
[`exchange_item_relation_view`][capellambse_context_diagrams.collectors.exchange_item_relation_view]
module.
9 changes: 9 additions & 0 deletions docs/gen_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
realization_comp_uuid = "b9f9a83c-fb02-44f7-9123-9d86326de5f1"
data_flow_uuid = "3b83b4ba-671a-4de8-9c07-a5c6b1d3c422"
derived_uuid = "47c3130b-ec39-4365-a77a-5ab6365d1e2e"
exchange_item_class_tree_uuid = "0ab202d7-6497-4b78-9d13-fd7c9a75486c"


def generate_index_images() -> None:
Expand Down Expand Up @@ -192,6 +193,13 @@ def generate_interface_with_hide_interface_image():
print(diag.render("svg", **params), file=fd)


def generate_exchange_item_class_tree_images() -> None:
obj = model.by_uuid(exchange_item_class_tree_uuid)
diag = obj.exchange_item_relation_view
with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd:
print(diag.render("svg", transparent_background=False), file=fd)


generate_index_images()
generate_hierarchy_image()
generate_symbol_images()
Expand Down Expand Up @@ -219,3 +227,4 @@ def generate_interface_with_hide_interface_image():
generate_derived_image()
generate_interface_with_hide_functions_image()
generate_interface_with_hide_interface_image()
generate_exchange_item_class_tree_images()
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ nav:
- Overview: realization_view.md
- DataFlow View:
- Overview: data_flow_view.md
- Exchange Item Relation View:
- Overview: exchange_item_relation_view.md
- Extras:
- Filters: extras/filters.md
- Styling: extras/styling.md
Expand Down
Loading
Loading