diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fb653b29..0de11f3b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@
exclude: '^(versioneer\.py|.*/_version\.py)$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-ast
@@ -25,7 +25,7 @@ repos:
- id: fix-byte-order-marker
- id: trailing-whitespace
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 24.3.0
+ rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
@@ -33,7 +33,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.9.0
+ rev: v1.10.0
hooks:
- id: mypy
- repo: https://github.com/Lucas-C/pre-commit-hooks
@@ -67,6 +67,6 @@ repos:
- --comment-style
- "/*| *| */"
- repo: https://github.com/fsfe/reuse-tool
- rev: v3.0.1
+ rev: v3.0.2
hooks:
- id: reuse
diff --git a/README.md b/README.md
index 5f0d394a..9e67fc51 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ When the extension is installed you get additional method `.context_diagram` ava
### Interface context
-![Interface context diagram of **Left to right**](https://raw.githubusercontent.com/DSD-DBS/capellambse-context-diagrams/main/docs/assets/images/Interface%20Context%20of%20Left%20to%20right.svg "Interface context diagram of **Left to right**")
+![Interface context diagram of **Interface**](https://raw.githubusercontent.com/DSD-DBS/capellambse-context-diagrams/main/docs/assets/images/Interface%20Context%20of%20Interface.svg "Interface context diagram of **Interface**")
Have a look at our [documentation](https://dsd-dbs.github.io/capellambse-context-diagrams/) to get started and see the capabilities of this extension.
diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py
index 706b15dd..47cfd7e7 100644
--- a/capellambse_context_diagrams/__init__.py
+++ b/capellambse_context_diagrams/__init__.py
@@ -28,8 +28,9 @@
from capellambse.model.crosslayer import fa, information
from capellambse.model.layers import ctx, la, oa, pa
from capellambse.model.modeltypes import DiagramType
+from capellambse.svg import decorations
-from . import context, styling
+from . import _elkjs, context, styling
try:
__version__ = metadata.version("capellambse-context-diagrams")
@@ -46,6 +47,18 @@
ATTR_NAME = "context_diagram"
+def install_elk() -> None:
+ """Install elk.js and its dependencies into the local cache directory.
+
+ When rendering a context diagram, elk.js will be installed
+ automatically into a persistent local cache directory. This function
+ may be called while building a container, starting a server or
+ similar tasks in order to prepare the elk.js execution environment
+ ahead of time.
+ """
+ _elkjs._install_required_npm_pkg_versions()
+
+
def init() -> None:
"""Initialize the extension."""
register_classes()
@@ -60,7 +73,11 @@ def register_classes() -> None:
"""Add the `context_diagram` property to the relevant model objects."""
supported_classes: list[SupportedClass] = [
(oa.Entity, DiagramType.OAB, {}),
- (oa.OperationalActivity, DiagramType.OAIB, {}),
+ (
+ oa.OperationalActivity,
+ DiagramType.OAB,
+ {"display_parent_relation": True},
+ ),
(oa.OperationalCapability, DiagramType.OCB, {}),
(ctx.Mission, DiagramType.MCB, {}),
(ctx.Capability, DiagramType.MCB, {"display_symbols_as_boxes": False}),
@@ -69,33 +86,52 @@ def register_classes() -> None:
DiagramType.SAB,
{
"display_symbols_as_boxes": True,
+ "display_parent_relation": True,
"render_styles": styling.BLUE_ACTOR_FNCS,
},
),
(
ctx.SystemFunction,
- DiagramType.SDFB,
- {"render_styles": styling.BLUE_ACTOR_FNCS},
+ DiagramType.SAB,
+ {
+ "display_symbols_as_boxes": True,
+ "display_parent_relation": True,
+ "render_styles": styling.BLUE_ACTOR_FNCS,
+ },
),
(
la.LogicalComponent,
DiagramType.LAB,
- {"render_styles": styling.BLUE_ACTOR_FNCS},
+ {
+ "display_symbols_as_boxes": True,
+ "display_parent_relation": True,
+ "render_styles": styling.BLUE_ACTOR_FNCS,
+ },
),
(
la.LogicalFunction,
- DiagramType.LDFB,
- {"render_styles": styling.BLUE_ACTOR_FNCS},
+ DiagramType.LAB,
+ {
+ "display_symbols_as_boxes": True,
+ "display_parent_relation": True,
+ "render_styles": styling.BLUE_ACTOR_FNCS,
+ },
),
(
pa.PhysicalComponent,
DiagramType.PAB,
- {"render_styles": styling.BLUE_ACTOR_FNCS},
+ {
+ "display_parent_relation": True,
+ "render_styles": styling.BLUE_ACTOR_FNCS,
+ },
),
(
pa.PhysicalFunction,
- DiagramType.PDFB,
- {"render_styles": styling.BLUE_ACTOR_FNCS},
+ DiagramType.PAB,
+ {
+ "display_parent_relation": True,
+ "render_styles": styling.BLUE_ACTOR_FNCS,
+ },
),
]
patch_styles(supported_classes)
@@ -128,43 +164,6 @@ def patch_styles(classes: cabc.Iterable[SupportedClass]) -> None:
for _, dt, _ in classes:
capstyle.STYLES[dt.value]["Circle.FunctionalExchange"] = circle_style
- for dt in (DiagramType.SAB, DiagramType.LAB, DiagramType.PAB):
- text_fill = COLORS["black"]
- if dt == DiagramType.SAB:
- fill = [
- COLORS["_CAP_Component_Blue_min"],
- COLORS["_CAP_Component_Blue"],
- ]
- stroke = COLORS["_CAP_Component_Border_Blue"]
- elif dt == DiagramType.LAB:
- fill = [
- COLORS["_CAP_Component_Blue_min"],
- COLORS["_CAP_Component_Blue"],
- ]
- text_fill = COLORS["_CAP_Component_Label_Blue"]
- elif dt == DiagramType.PAB:
- fill = [
- COLORS["_CAP_Unset_Gray_min"],
- COLORS["_CAP_Unset_Gray"],
- ]
- stroke = COLORS["_CAP_Lifeline_Gray"]
-
- capstyle.STYLES[dt.value].update(
- {
- "Box.DerivedBox": {
- "fill": fill,
- "stroke": stroke,
- "stroke-dasharray": "4",
- "text_fill": text_fill,
- },
- "Edge.DerivedComponentExchange": {
- "stroke": COLORS["_CAP_Component_Border_Blue"],
- "stroke-width": 2,
- "stroke-dasharray": "4",
- },
- }
- )
-
def register_interface_context() -> None:
"""Add the `context_diagram` property to interface model objects."""
diff --git a/capellambse_context_diagrams/_elkjs.py b/capellambse_context_diagrams/_elkjs.py
index 7a7bc2c1..337588b1 100644
--- a/capellambse_context_diagrams/_elkjs.py
+++ b/capellambse_context_diagrams/_elkjs.py
@@ -56,6 +56,7 @@
"hierarchyHandling": "INCLUDE_CHILDREN",
"layered.edgeLabels.sideSelection": "ALWAYS_DOWN",
"layered.nodePlacement.strategy": "BRANDES_KOEPF",
+ "layered.considerModelOrder.strategy": "NODES_AND_EDGES",
"spacing.labelNode": "0.0",
}
"""
diff --git a/capellambse_context_diagrams/collectors/dataflow_view.py b/capellambse_context_diagrams/collectors/dataflow_view.py
index e706f2cb..906b217f 100644
--- a/capellambse_context_diagrams/collectors/dataflow_view.py
+++ b/capellambse_context_diagrams/collectors/dataflow_view.py
@@ -8,6 +8,7 @@
import functools
import operator
import typing as t
+from itertools import chain
from capellambse.model import modeltypes
from capellambse.model.crosslayer import fa
@@ -128,7 +129,7 @@ def collector_default(
connections = default.port_exchange_collector(_ports, filter=filter)
in_ports: dict[str, fa.FunctionPort] = {}
out_ports: dict[str, fa.FunctionPort] = {}
- for edge in connections:
+ for edge in (edges := list(chain.from_iterable(connections.values()))):
if edge.source.owner == fnc:
out_ports.setdefault(edge.source.uuid, edge.source)
else:
@@ -142,7 +143,7 @@ def collector_default(
) * max(len(in_ports), len(out_ports))
ex_datas: list[generic.ExchangeData] = []
- for ex in connections:
+ for ex in edges:
if ex.uuid in made_edges:
continue
diff --git a/capellambse_context_diagrams/collectors/default.py b/capellambse_context_diagrams/collectors/default.py
index b134dfe1..ade84e02 100644
--- a/capellambse_context_diagrams/collectors/default.py
+++ b/capellambse_context_diagrams/collectors/default.py
@@ -8,32 +8,46 @@
import collections.abc as cabc
import typing as t
+from itertools import chain
from capellambse import helpers
from capellambse.model import common
from capellambse.model.crosslayer import cs, fa
-from capellambse.model.layers import ctx, la
+from capellambse.model.layers import ctx as sa
+from capellambse.model.layers import la
from capellambse.model.modeltypes import DiagramType as DT
from .. import _elkjs, context
from . import exchanges, generic, makers
+STYLECLASS_PREFIX = "__Derived"
+
def collector(
diagram: context.ContextDiagram, params: dict[str, t.Any] | None = None
) -> _elkjs.ELKInputData:
"""Collect context data from ports of centric box."""
+ diagram.display_derived_interfaces = (params or {}).pop(
+ "display_derived_interfaces", diagram.display_derived_interfaces
+ )
data = generic.collector(diagram, no_symbol=True)
ports = port_collector(diagram.target, diagram.type)
centerbox = data["children"][0]
- centerbox["ports"] = [makers.make_port(i.uuid) for i in ports]
connections = port_exchange_collector(ports)
+ centerbox["ports"] = [
+ makers.make_port(uuid) for uuid, edges in connections.items() if edges
+ ]
ex_datas: list[generic.ExchangeData] = []
- for ex in connections:
+ edges: common.ElementList[fa.AbstractExchange] = list(
+ chain.from_iterable(connections.values())
+ )
+ for ex in edges:
if is_hierarchical := exchanges.is_hierarchical(ex, centerbox):
- if not diagram.include_inner_objects:
+ if not diagram.display_parent_relation:
continue
-
+ centerbox["labels"][0][
+ "layoutOptions"
+ ] = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
elkdata: _elkjs.ELKInputData = centerbox
else:
elkdata = data
@@ -45,23 +59,57 @@ def collector(
ex_datas.append(ex_data)
except AttributeError:
continue
-
global_boxes = {centerbox["id"]: centerbox}
- if diagram.display_parent_relation:
+ made_boxes = {centerbox["id"]: centerbox}
+ boxes_to_delete = {centerbox["id"]}
+
+ def _make_box_and_update_globals(
+ obj: t.Any,
+ **kwargs: t.Any,
+ ) -> _elkjs.ELKInputChild:
box = makers.make_box(
- diagram.target.parent,
- no_symbol=diagram.display_symbols_as_boxes,
- layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
+ obj,
+ **kwargs,
)
- box["children"] = [centerbox]
- del data["children"][0]
- global_boxes[diagram.target.parent.uuid] = box
+ global_boxes[obj.uuid] = box
+ made_boxes[obj.uuid] = box
+ return box
+
+ def _make_owner_box(current: t.Any) -> t.Any:
+ if not (parent_box := global_boxes.get(current.owner.uuid)):
+ parent_box = _make_box_and_update_globals(
+ current.owner,
+ no_symbol=diagram.display_symbols_as_boxes,
+ layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
+ )
+ for box in (children := parent_box.setdefault("children", [])):
+ if box["id"] == current.uuid:
+ box = global_boxes.get(current.uuid, current)
+ break
+ else:
+ children.append(global_boxes.get(current.uuid, current))
+ boxes_to_delete.add(current.uuid)
+ return current.owner
+
+ if diagram.display_parent_relation:
+ try:
+ if not isinstance(diagram.target.owner, generic.PackageTypes):
+ box = _make_box_and_update_globals(
+ diagram.target.owner,
+ no_symbol=diagram.display_symbols_as_boxes,
+ layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
+ )
+ box["children"] = [centerbox]
+ del data["children"][0]
+ except AttributeError:
+ pass
+ diagram_target_owners = generic.get_all_owners(diagram.target)
+ common_owners = set()
stack_heights: dict[str, float | int] = {
"input": -makers.NEIGHBOR_VMARGIN,
"output": -makers.NEIGHBOR_VMARGIN,
}
- child_boxes: list[_elkjs.ELKInputChild] = []
for child, local_ports, side in port_context_collector(ex_datas, ports):
_, label_height = helpers.get_text_extent(child.name)
height = max(
@@ -77,47 +125,52 @@ def collector(
)
box["height"] += height
else:
- box = makers.make_box(
+ box = _make_box_and_update_globals(
child,
height=height,
no_symbol=diagram.display_symbols_as_boxes,
)
box["ports"] = [makers.make_port(j.uuid) for j in local_ports]
- if child.parent.uuid == centerbox["id"]:
- child_boxes.append(box)
- else:
- global_boxes[child.uuid] = box
if diagram.display_parent_relation:
- if child == diagram.target.parent:
- _move_edge_to_local_edges(
- box, connections, local_ports, diagram, data
- )
- elif child.parent == diagram.target.parent:
- parent_box = global_boxes[child.parent.uuid]
- parent_box.setdefault("children", []).append(
- global_boxes.pop(child.uuid)
- )
- for label in parent_box["labels"]:
- label["layoutOptions"] = (
- makers.CENTRIC_LABEL_LAYOUT_OPTIONS
- )
-
- _move_edge_to_local_edges(
- parent_box, connections, local_ports, diagram, data
- )
+ current = child
+ while current and current.uuid not in diagram_target_owners:
+ try:
+ if isinstance(current.owner, generic.PackageTypes):
+ break
+ current = _make_owner_box(current)
+ except AttributeError:
+ break
+ common_owners.add(current.uuid)
stack_heights[side] += makers.NEIGHBOR_VMARGIN + height
- del global_boxes[centerbox["id"]]
+ if diagram.display_parent_relation and diagram.target.owner:
+ current = diagram.target.owner
+ common_owner_uuid = current.uuid
+ for owner in diagram_target_owners[::-1]:
+ if owner in common_owners:
+ common_owner_uuid = owner
+ break
+ while current and current.uuid != common_owner_uuid:
+ try:
+ if isinstance(current.owner, generic.PackageTypes):
+ break
+ current = _make_owner_box(current)
+ except AttributeError:
+ break
+
+ for uuid in boxes_to_delete:
+ del global_boxes[uuid]
data["children"].extend(global_boxes.values())
- if child_boxes:
- centerbox["children"] = child_boxes
- centerbox["width"] = makers.EOI_WIDTH
- for label in centerbox.get("labels", []):
- label.setdefault("layoutOptions", {}).update(
- makers.DEFAULT_LABEL_LAYOUT_OPTIONS
- )
+ if diagram.display_parent_relation:
+ owner_boxes: dict[str, _elkjs.ELKInputChild] = {
+ uuid: box
+ for uuid, box in made_boxes.items()
+ if box.get("children")
+ }
+ generic.move_parent_boxes_to_owner(owner_boxes, diagram.target, data)
+ generic.move_edges(owner_boxes, edges, data)
centerbox["height"] = max(centerbox["height"], *stack_heights.values())
if diagram.display_derived_interfaces:
@@ -126,31 +179,6 @@ def collector(
return data
-def _move_edge_to_local_edges(
- box: _elkjs.ELKInputChild,
- connections: list[common.GenericElement],
- local_ports: list[common.GenericElement],
- diagram: context.ContextDiagram,
- data: _elkjs.ELKInputData,
-) -> None:
- edges_to_remove: list[str] = []
- for c in connections:
- if (
- c.target in local_ports
- and c.source in diagram.target.ports
- or c.source in local_ports
- and c.target in diagram.target.ports
- ):
- for edge in data["edges"]:
- if edge["id"] == c.uuid:
- box.setdefault("edges", []).append(edge)
- edges_to_remove.append(edge["id"])
-
- data["edges"] = [
- e for e in data["edges"] if e["id"] not in edges_to_remove
- ]
-
-
def port_collector(
target: common.GenericElement | common.ElementList, diagram_type: DT
) -> list[common.GenericElement]:
@@ -186,13 +214,12 @@ def port_exchange_collector(
[cabc.Iterable[common.GenericElement]],
cabc.Iterable[common.GenericElement],
] = lambda i: i,
-) -> list[common.GenericElement]:
+) -> dict[str, common.ElementList[fa.AbstractExchange]]:
"""Collect exchanges from `ports` savely."""
- edges: list[common.GenericElement] = []
+ edges: dict[str, common.ElementList[fa.AbstractExchange]] = {}
for i in ports:
try:
- filtered = filter(getattr(i, "exchanges"))
- edges.extend(filtered)
+ edges[i.uuid] = filter(getattr(i, "exchanges"))
except AttributeError:
pass
return edges
@@ -272,20 +299,25 @@ def add_derived_components_and_interfaces(
The derived exchanges are displayed with a dashed line.
"""
- if not (derivator := DERIVATORS.get(type(diagram.target))):
- return
+ if derivator := DERIVATORS.get(type(diagram.target)):
+ derivator(diagram, data)
- derivator(diagram, data)
-
-def _derive_from_functions(
+def derive_from_functions(
diagram: context.ContextDiagram, data: _elkjs.ELKInputData
-):
+) -> None:
+ """Derive Components from allocated functions of the context target.
+
+ A Component, a ComponentExchange and two ComponentPorts are added
+ to ``data``. These elements are prefixed with ``Derived-`` to
+ receive special styling in the serialization step.
+ """
assert isinstance(diagram.target, cs.Component)
ports = []
for fnc in diagram.target.allocated_functions:
ports.extend(port_collector(fnc, diagram.type))
+ context_box_ids = {child["id"] for child in data["children"]}
components: dict[str, cs.Component] = {}
for port in ports:
for fex in port.exchanges:
@@ -296,30 +328,40 @@ def _derive_from_functions(
try:
derived_comp = getattr(fex, attr).owner.owner
+ if (
+ derived_comp == diagram.target
+ or derived_comp.uuid in context_box_ids
+ ):
+ continue
+
if derived_comp.uuid not in components:
components[derived_comp.uuid] = derived_comp
- except AttributeError:
- ...
+ except AttributeError: # No owner of owner.
+ pass
- # TODO: Even out derived interfaces on each side
+ # Idea: Include flow direction of derived interfaces from all functional
+ # exchanges. Mixed means bidirectional. Just even out bidirectional
+ # interfaces and keep flow direction of others.
- for i, (uuid, derived_component) in enumerate(components.items()):
+ centerbox = data["children"][0]
+ for i, (uuid, derived_component) in enumerate(components.items(), 1):
box = makers.make_box(
derived_component,
no_symbol=diagram.display_symbols_as_boxes,
)
- box["id"] = comp_uuid = f"__DerivedBox_{uuid}"
+ class_ = type(derived_comp).__name__
+ box["id"] = f"{STYLECLASS_PREFIX}-{class_}:{uuid}"
data["children"].append(box)
+ source_id = f"{STYLECLASS_PREFIX}-CP_INOUT:{i}"
+ target_id = f"{STYLECLASS_PREFIX}-CP_INOUT:{-i}"
+ box.setdefault("ports", []).append(makers.make_port(source_id))
+ centerbox.setdefault("ports", []).append(makers.make_port(target_id))
if i % 2 == 0:
- source_id = comp_uuid
- target_id = diagram.target.uuid
- else:
- source_id = diagram.target.uuid
- target_id = comp_uuid
+ source_id, target_id = target_id, source_id
data["edges"].append(
{
- "id": f"__DerivedComponentExchange_{i}",
+ "id": f"{STYLECLASS_PREFIX}-ComponentExchange:{i}",
"sources": [source_id],
"targets": [target_id],
}
@@ -332,6 +374,7 @@ def _derive_from_functions(
DERIVATORS = {
- la.LogicalComponent: _derive_from_functions,
- ctx.SystemComponent: _derive_from_functions,
+ la.LogicalComponent: derive_from_functions,
+ sa.SystemComponent: derive_from_functions,
}
+"""Supported objects to build derived contexts for."""
diff --git a/capellambse_context_diagrams/collectors/exchanges.py b/capellambse_context_diagrams/collectors/exchanges.py
index f4eaab46..83f64933 100644
--- a/capellambse_context_diagrams/collectors/exchanges.py
+++ b/capellambse_context_diagrams/collectors/exchanges.py
@@ -9,6 +9,7 @@
import typing as t
from capellambse.model import common
+from capellambse.model.crosslayer import cs
from capellambse.model.modeltypes import DiagramType as DT
from .. import _elkjs, context
@@ -65,31 +66,60 @@ def get_functions_and_exchanges(
self, comp: common.GenericElement, interface: common.GenericElement
) -> tuple[
list[common.GenericElement],
- list[common.GenericElement],
- list[common.GenericElement],
+ dict[str, common.GenericElement],
+ dict[str, common.GenericElement],
]:
"""Return `Function`s, incoming and outgoing
`FunctionalExchange`s for given `Component` and `interface`.
"""
- functions, outgoings, incomings = [], [], []
+ functions, incomings, outgoings = [], {}, {}
alloc_functions = self.get_alloc_functions(comp)
for fex in self.get_alloc_fex(interface):
source = self.get_source(fex)
if source in alloc_functions:
- if fex not in outgoings:
- outgoings.append(fex)
+ if fex.uuid not in outgoings:
+ outgoings[fex.uuid] = fex
if source not in functions:
functions.append(source)
target = self.get_target(fex)
if target in alloc_functions:
- if fex not in incomings:
- incomings.append(fex)
+ if fex.uuid not in incomings:
+ incomings[fex.uuid] = fex
if target not in functions:
functions.append(target)
return functions, incomings, outgoings
+ def collect_context(
+ self, comp: common.GenericElement, interface: common.GenericElement
+ ) -> tuple[
+ dict[str, t.Any],
+ dict[str, common.GenericElement],
+ dict[str, common.GenericElement],
+ ]:
+ functions, incomings, outgoings = self.get_functions_and_exchanges(
+ comp, interface
+ )
+ components = []
+ for cmp in comp.components:
+ fncs, _, _ = self.get_functions_and_exchanges(cmp, interface)
+ functions.extend(fncs)
+ if fncs:
+ c, incs, outs = self.collect_context(cmp, interface)
+ components.append(c)
+ incomings |= incs
+ outgoings |= outs
+ return (
+ {
+ "element": comp,
+ "functions": functions,
+ "components": components,
+ },
+ incomings,
+ outgoings,
+ )
+
def make_ports_and_update_children_size(
self,
data: _elkjs.ELKInputChild,
@@ -100,6 +130,9 @@ def make_ports_and_update_children_size(
for child in data["children"]:
inputs, outputs = [], []
obj = self.obj._model.by_uuid(child["id"])
+ if isinstance(obj, cs.Component):
+ self.make_ports_and_update_children_size(child, exchanges)
+ return
port_ids = {p.uuid for p in obj.inputs + obj.outputs}
for ex in exchanges:
source, target = ex["sources"][0], ex["targets"][0]
@@ -153,12 +186,12 @@ class InterfaceContextCollector(ExchangeCollector):
for building the interface context.
"""
- left: common.GenericElement
- """Source or target Component of the interface."""
- right: common.GenericElement
- """Source or target Component of the interface."""
- outgoing_edges: list[common.GenericElement]
- incoming_edges: list[common.GenericElement]
+ left: _elkjs.ELKInputChild | None
+ """Left (source) Component Box of the interface."""
+ right: _elkjs.ELKInputChild | None
+ """Right (target) Component Box of the interface."""
+ outgoing_edges: dict[str, common.GenericElement]
+ incoming_edges: dict[str, common.GenericElement]
def __init__(
self,
@@ -166,8 +199,16 @@ def __init__(
data: _elkjs.ELKInputData,
params: dict[str, t.Any],
) -> None:
+ self.left = None
+ self.right = None
+ self.incoming_edges = {}
+ self.outgoing_edges = {}
+
super().__init__(diagram, data, params)
+
self.get_left_and_right()
+ if diagram.include_interface:
+ self.add_interface()
def get_left_and_right(self) -> None:
made_children: set[str] = set()
@@ -178,15 +219,19 @@ def get_capella_order(
alloc_functions = self.get_alloc_functions(comp)
return [fnc for fnc in alloc_functions if fnc in functions]
- def make_boxes(
- comp: common.GenericElement, functions: list[common.GenericElement]
- ) -> None:
+ def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None:
+ comp = cntxt["element"]
+ functions = cntxt["functions"]
+ components = cntxt["components"]
if comp.uuid not in made_children:
children = [
- makers.make_box(c)
- for c in functions
- if c in self.get_alloc_functions(comp)
+ makers.make_box(fnc)
+ for fnc in functions
+ if fnc in self.get_alloc_functions(comp)
]
+ for cmp in components:
+ if child := make_boxes(cmp):
+ children.append(child)
if children:
layout_options = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
else:
@@ -196,52 +241,68 @@ def make_boxes(
comp, no_symbol=True, layout_options=layout_options
)
box["children"] = children
- self.data["children"].append(box)
made_children.add(comp.uuid)
+ return box
+ return None
try:
comp = self.get_source(self.obj)
- functions, incs, outs = self.get_functions_and_exchanges(
- comp, self.obj
- )
- inc_port_ids = set(ex.target.uuid for ex in incs)
- out_port_ids = set(ex.source.uuid for ex in outs)
+ left_context, incs, outs = self.collect_context(comp, self.obj)
+ inc_port_ids = set(ex.target.uuid for ex in incs.values())
+ out_port_ids = set(ex.source.uuid for ex in outs.values())
port_spread = len(out_port_ids) - len(inc_port_ids)
_comp = self.get_target(self.obj)
- _functions, _, _ = self.get_functions_and_exchanges(
- _comp, self.obj
- )
- _inc_port_ids = set(ex.target.uuid for ex in outs)
- _out_port_ids = set(ex.source.uuid for ex in incs)
+ right_context, _, _ = self.collect_context(_comp, self.obj)
+ _inc_port_ids = set(ex.target.uuid for ex in outs.values())
+ _out_port_ids = set(ex.source.uuid for ex in incs.values())
_port_spread = len(_out_port_ids) - len(_inc_port_ids)
- functions = get_capella_order(comp, functions)
- _functions = get_capella_order(_comp, _functions)
+ left_context["functions"] = get_capella_order(
+ comp, left_context["functions"]
+ )
+ right_context["functions"] = get_capella_order(
+ _comp, right_context["functions"]
+ )
if port_spread >= _port_spread:
- self.left = comp
- self.right = _comp
- self.outgoing_edges = outs
self.incoming_edges = incs
- left_functions = functions
- right_functions = _functions
+ self.outgoing_edges = outs
else:
- self.left = _comp
- self.right = comp
- self.outgoing_edges = incs
self.incoming_edges = outs
- left_functions = _functions
- right_functions = functions
-
- make_boxes(self.left, left_functions)
- make_boxes(self.right, right_functions)
+ self.outgoing_edges = incs
+ left_context, right_context = right_context, left_context
+
+ if left_child := make_boxes(left_context):
+ self.data["children"].append(left_child)
+ self.left = left_child
+ if right_child := make_boxes(right_context):
+ self.data["children"].append(right_child)
+ self.right = right_child
except AttributeError:
pass
+ def add_interface(self) -> None:
+ ex_data = generic.ExchangeData(
+ self.obj,
+ self.data,
+ self.diagram.filters,
+ self.params,
+ is_hierarchical=False,
+ )
+ src, tgt = generic.exchange_data_collector(ex_data)
+ assert self.right is not None
+ if self.get_source(self.obj).uuid == self.right["id"]:
+ self.data["edges"][-1]["sources"] = [tgt.uuid]
+ self.data["edges"][-1]["targets"] = [src.uuid]
+
+ assert self.left is not None
+ self.left.setdefault("ports", []).append(makers.make_port(src.uuid))
+ self.right.setdefault("ports", []).append(makers.make_port(tgt.uuid))
+
def collect(self) -> None:
- """Return all allocated `FunctionalExchange`s in the context."""
+ """Collect all allocated `FunctionalExchange`s in the context."""
try:
- for ex in self.incoming_edges + self.outgoing_edges:
+ for ex in (self.incoming_edges | self.outgoing_edges).values():
ex_data = generic.ExchangeData(
ex,
self.data,
@@ -251,7 +312,7 @@ def collect(self) -> None:
)
src, tgt = generic.exchange_data_collector(ex_data)
- if ex in self.incoming_edges:
+ if ex in self.incoming_edges.values():
self.data["edges"][-1]["sources"] = [tgt.uuid]
self.data["edges"][-1]["targets"] = [src.uuid]
@@ -300,7 +361,7 @@ def collect(self) -> None:
made_children.add(comp.uuid)
all_functions.extend(functions)
- functional_exchanges.extend(inc + outs)
+ functional_exchanges.extend(inc | outs)
self.data["children"][0]["children"] = [
makers.make_box(c)
diff --git a/capellambse_context_diagrams/collectors/generic.py b/capellambse_context_diagrams/collectors/generic.py
index 9f350be2..51e9f3c3 100644
--- a/capellambse_context_diagrams/collectors/generic.py
+++ b/capellambse_context_diagrams/collectors/generic.py
@@ -11,7 +11,7 @@
import logging
import typing as t
-from capellambse.model import common
+from capellambse.model import common, layers
from capellambse.model.crosslayer import interaction
from capellambse.model.modeltypes import DiagramType as DT
@@ -43,6 +43,12 @@
"""Default size of marker-ends in pixels."""
MARKER_PADDING = makers.PORT_PADDING
"""Default padding of markers in pixels."""
+PackageTypes: tuple[type[common.GenericElement], ...] = (
+ layers.oa.EntityPkg,
+ layers.la.LogicalComponentPkg,
+ layers.ctx.SystemComponentPkg,
+ layers.pa.PhysicalComponentPkg,
+)
def collector(
@@ -185,3 +191,75 @@ def collect_label(obj: common.GenericElement) -> str | None:
elif isinstance(obj, interaction.AbstractCapabilityInclude):
return "« i »"
return "" if obj.name.startswith("(Unnamed") else obj.name
+
+
+def move_parent_boxes_to_owner(
+ boxes: dict[str, _elkjs.ELKInputChild],
+ obj: common.GenericElement,
+ data: _elkjs.ELKInputData,
+ filter_types: tuple[type, ...] = PackageTypes,
+) -> None:
+ """Move boxes to their owner box."""
+ boxes_to_remove: list[str] = []
+ for child in data["children"]:
+ if not child.get("children"):
+ continue
+
+ owner = obj._model.by_uuid(child["id"])
+ if (
+ isinstance(owner, filter_types)
+ or not (oowner := owner.owner)
+ or isinstance(oowner, filter_types)
+ or not (oowner_box := boxes.get(oowner.uuid))
+ ):
+ continue
+
+ oowner_box.setdefault("children", []).append(child)
+ boxes_to_remove.append(child["id"])
+
+ data["children"] = [
+ b for b in data["children"] if b["id"] not in boxes_to_remove
+ ]
+
+
+def move_edges(
+ boxes: dict[str, _elkjs.ELKInputChild],
+ connections: list[common.GenericElement],
+ data: _elkjs.ELKInputData,
+) -> None:
+ """Move edges to boxes."""
+ edges_to_remove: list[str] = []
+ for c in connections:
+ source_owner_uuids = get_all_owners(c.source)
+ target_owner_uuids = get_all_owners(c.target)
+ common_owner_uuid = None
+ for owner in source_owner_uuids:
+ if owner in target_owner_uuids:
+ common_owner_uuid = owner
+ break
+
+ if not common_owner_uuid or not (
+ owner_box := boxes.get(common_owner_uuid)
+ ):
+ continue
+
+ for edge in data["edges"]:
+ if edge["id"] == c.uuid:
+ owner_box.setdefault("edges", []).append(edge)
+ edges_to_remove.append(edge["id"])
+ data["edges"] = [
+ e for e in data["edges"] if e["id"] not in edges_to_remove
+ ]
+
+
+def get_all_owners(obj: common.GenericElement) -> list[str]:
+ """Return the UUIDs from all owners of ``obj``."""
+ owners: list[str] = []
+ current = obj
+ while current is not None:
+ owners.append(current.uuid)
+ try:
+ current = current.owner
+ except AttributeError:
+ break
+ return owners
diff --git a/capellambse_context_diagrams/collectors/makers.py b/capellambse_context_diagrams/collectors/makers.py
index 672c3869..fdea3a12 100644
--- a/capellambse_context_diagrams/collectors/makers.py
+++ b/capellambse_context_diagrams/collectors/makers.py
@@ -42,10 +42,12 @@
FAULT_PAD = 10
"""Height adjustment for labels."""
BOX_TO_SYMBOL = (
- layers.ctx.Capability,
- layers.oa.OperationalCapability,
- layers.ctx.Mission,
- layers.ctx.SystemComponent,
+ layers.ctx.Capability.__name__,
+ layers.oa.OperationalCapability.__name__,
+ layers.ctx.Mission.__name__,
+ layers.ctx.SystemComponent.__name__,
+ "SystemHumanActor",
+ "SystemActor",
)
"""
Types that need to be converted to symbols during serialization if
@@ -189,9 +191,13 @@ def calculate_height_and_width(
return width, max(height, _height)
-def is_symbol(obj: common.GenericElement) -> bool:
+def is_symbol(obj: str | common.GenericElement | None) -> bool:
"""Check if given `obj` is rendered as a Symbol instead of a Box."""
- return isinstance(obj, BOX_TO_SYMBOL)
+ if obj is None:
+ return False
+ elif isinstance(obj, str):
+ return obj in BOX_TO_SYMBOL
+ return type(obj).__name__ in BOX_TO_SYMBOL
def make_port(uuid: str) -> _elkjs.ELKInputPort:
diff --git a/capellambse_context_diagrams/collectors/portless.py b/capellambse_context_diagrams/collectors/portless.py
index a2f4c688..3d3b1c45 100644
--- a/capellambse_context_diagrams/collectors/portless.py
+++ b/capellambse_context_diagrams/collectors/portless.py
@@ -41,12 +41,24 @@ def collector(
except AttributeError:
continue
+ contexts = context_collector(connections, diagram.target)
+ global_boxes = {centerbox["id"]: centerbox}
+ made_boxes = {centerbox["id"]: centerbox}
+ if diagram.display_parent_relation and diagram.target.owner is not None:
+ box = makers.make_box(
+ diagram.target.owner,
+ no_symbol=diagram.display_symbols_as_boxes,
+ layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
+ )
+ box["children"] = [centerbox]
+ del data["children"][0]
+ global_boxes[diagram.target.owner.uuid] = box
+ made_boxes[diagram.target.owner.uuid] = box
+
stack_heights: dict[str, float | int] = {
"input": -makers.NEIGHBOR_VMARGIN,
"output": -makers.NEIGHBOR_VMARGIN,
}
- contexts = context_collector(connections, diagram.target)
- made_boxes = {centerbox["id"]: centerbox}
for i, exchanges, side in contexts:
var_height = generic.MARKER_PADDING + (
generic.MARKER_SIZE + generic.MARKER_PADDING
@@ -58,7 +70,7 @@ def collector(
else:
height = var_height
- if box := made_boxes.get(i.uuid):
+ if box := global_boxes.get(i.uuid): # type: ignore[assignment]
if box is centerbox:
continue
box["height"] = height
@@ -68,12 +80,38 @@ def collector(
height=height,
no_symbol=diagram.display_symbols_as_boxes,
)
+ global_boxes[i.uuid] = box
made_boxes[i.uuid] = box
+ if diagram.display_parent_relation and i.owner is not None:
+ if not (parent_box := global_boxes.get(i.owner.uuid)):
+ parent_box = makers.make_box(
+ i.owner,
+ no_symbol=diagram.display_symbols_as_boxes,
+ )
+ global_boxes[i.owner.uuid] = parent_box
+ made_boxes[i.owner.uuid] = parent_box
+
+ parent_box.setdefault("children", []).append(
+ global_boxes.pop(i.uuid)
+ )
+ for label in parent_box["labels"]:
+ label["layoutOptions"] = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
+
stack_heights[side] += makers.NEIGHBOR_VMARGIN + height
- del made_boxes[centerbox["id"]]
- data["children"].extend(made_boxes.values())
+ del global_boxes[centerbox["id"]]
+ data["children"].extend(global_boxes.values())
+
+ if diagram.display_parent_relation:
+ owner_boxes: dict[str, _elkjs.ELKInputChild] = {
+ uuid: box
+ for uuid, box in made_boxes.items()
+ if box.get("children")
+ }
+ generic.move_parent_boxes_to_owner(owner_boxes, diagram.target, data)
+ generic.move_edges(owner_boxes, connections, data)
+
centerbox["height"] = max(centerbox["height"], *stack_heights.values())
if not diagram.display_symbols_as_boxes and makers.is_symbol(
diagram.target
@@ -140,7 +178,6 @@ def context_collector(
info = ctx.setdefault(obj.uuid, info)
if exchange not in info.connections:
info.connections.append(exchange)
-
return iter(ctx.values())
diff --git a/capellambse_context_diagrams/collectors/tree_view.py b/capellambse_context_diagrams/collectors/tree_view.py
index 081d1eec..d9d1698c 100644
--- a/capellambse_context_diagrams/collectors/tree_view.py
+++ b/capellambse_context_diagrams/collectors/tree_view.py
@@ -16,6 +16,7 @@
from . import generic, makers
logger = logging.getLogger(__name__)
+
DATA_TYPE_LABEL_LAYOUT_OPTIONS: _elkjs.LayoutOptions = {
"nodeLabels.placement": "INSIDE, V_CENTER, H_CENTER"
}
@@ -25,6 +26,7 @@
"elk.direction": "DOWN",
"edgeRouting": "ORTHOGONAL",
}
+ASSOC_STYLECLASS = "__Association"
class ClassProcessor:
@@ -58,7 +60,7 @@ def process_class(self, cls: ClassInfo, params: dict[str, t.Any]):
if len(edges) == 1:
edge_id = edges[0].uuid
else:
- edge_id = f"__Association_{self.edge_counter}"
+ edge_id = f"{ASSOC_STYLECLASS}_{self.edge_counter}"
self.edge_counter += 1
if edge_id not in self.made_edges:
self.made_edges.add(edge_id)
diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py
index 419574cb..a5072bdc 100644
--- a/capellambse_context_diagrams/context.py
+++ b/capellambse_context_diagrams/context.py
@@ -259,7 +259,6 @@ def __init__(
display_symbols_as_boxes: bool = False,
display_parent_relation: bool = False,
display_derived_interfaces: bool = False,
- include_inner_objects: bool = False,
slim_center_box: bool = True,
) -> None:
super().__init__(obj._model)
@@ -272,7 +271,6 @@ def __init__(
self.display_symbols_as_boxes = display_symbols_as_boxes
self.display_parent_relation = display_parent_relation
self.display_derived_interfaces = display_derived_interfaces
- self.include_inner_objects = include_inner_objects
self.slim_center_box = slim_center_box
if standard_filter := STANDARD_FILTERS.get(class_):
@@ -345,6 +343,18 @@ def render(self, fmt: str | None, /, **params) -> t.Any:
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
transparent_background = params.pop("transparent_background", False)
+ self.display_parent_relation = params.pop(
+ "display_parent_relation", self.display_parent_relation
+ )
+ self.display_derived_interfaces = params.pop(
+ "display_derived_interfaces", self.display_derived_interfaces
+ )
+ self.display_symbols_as_boxes = params.pop(
+ "display_symbols_as_boxes", self.display_symbols_as_boxes
+ )
+ self.slim_center_box = params.pop(
+ "slim_center_box", self.slim_center_box
+ )
data = params.get("elkdata") or get_elkdata(self, params)
layout = try_to_layout(data)
add_context(layout, params.get("is_legend", False))
@@ -368,7 +378,14 @@ class InterfaceContextDiagram(ContextDiagram):
``ComponentExchange``s.
"""
- def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
+ def __init__(
+ self,
+ class_: str,
+ obj: common.GenericElement,
+ include_interface: bool = False,
+ **kw,
+ ) -> None:
+ self.include_interface = include_interface
super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)
@property
diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py
index 8f37d2e8..b0176bd5 100644
--- a/capellambse_context_diagrams/serializers.py
+++ b/capellambse_context_diagrams/serializers.py
@@ -17,7 +17,8 @@
from capellambse import diagram
from capellambse.svg import decorations
-from . import _elkjs, collectors, context
+from . import _elkjs, context
+from .collectors import makers
logger = logging.getLogger(__name__)
@@ -31,6 +32,11 @@
* `edge`
* `junction`.
"""
+EdgeContext = tuple[
+ _elkjs.ELKOutputEdge,
+ diagram.Vector2D,
+ diagram.Box | diagram.Edge | None,
+]
REMAP_STYLECLASS: dict[str, str] = {"Unset": "Association"}
@@ -54,6 +60,7 @@ def __init__(self, elk_diagram: context.ContextDiagram) -> None:
self.model = elk_diagram.target._model
self._diagram = elk_diagram
self._cache: dict[str, diagram.Box | diagram.Edge] = {}
+ self._edges: dict[str, EdgeContext] = {}
def make_diagram(
self,
@@ -81,6 +88,9 @@ def make_diagram(
for child in data["children"]:
self.deserialize_child(child, diagram.Vector2D(), None)
+ for edge, ref, parent in self._edges.values():
+ self.deserialize_child(edge, ref, parent)
+
self.diagram.calculate_viewport()
self.order_children()
return self.diagram
@@ -114,27 +124,26 @@ class type that stores all previously named classes.
"""
uuid: str
styleclass: str | None
+ derived = False
if child["id"].startswith("__"):
- styleclass, uuid = child["id"][2:].split("_", 1)
+ styleclass, uuid = child["id"][2:].split(":", 1)
+ if styleclass.startswith("Derived-"):
+ styleclass = styleclass.removeprefix("Derived-")
+ derived = True
else:
styleclass = self.get_styleclass(child["id"])
uuid = child["id"]
+ styleoverrides = self.get_styleoverrides(uuid, child, derived=derived)
element: diagram.Box | diagram.Edge | diagram.Circle
if child["type"] in {"node", "port"}:
assert parent is None or isinstance(parent, diagram.Box)
- has_symbol_cls = False
- try:
- obj = self.model.by_uuid(uuid)
- has_symbol_cls = collectors.makers.is_symbol(obj)
- except KeyError:
- logger.error("ModelObject could not be found: '%s'", uuid)
-
+ has_symbol_cls = makers.is_symbol(styleclass)
is_port = child["type"] == "port"
box_type = ("box", "symbol")[
is_port
or has_symbol_cls
- and not self._diagram.target == obj
+ and not self._diagram.target.uuid == uuid
and not self._diagram.display_symbols_as_boxes
]
@@ -151,7 +160,7 @@ class type that stores all previously named classes.
parent=parent,
port=is_port,
styleclass=styleclass,
- styleoverrides=self.get_styleoverrides(uuid, child),
+ styleoverrides=styleoverrides,
features=features,
context=child.get("context"),
)
@@ -165,11 +174,11 @@ class type that stores all previously named classes.
source_id = child["sourceId"]
if source_id.startswith("__"):
- source_id = source_id[2:].split("_", 1)[-1]
+ source_id = source_id[2:].split(":", 1)[-1]
target_id = child["targetId"]
if target_id.startswith("__"):
- target_id = target_id[2:].split("_", 1)[-1]
+ target_id = target_id[2:].split(":", 1)[-1]
if child["routingPoints"]:
refpoints = [
@@ -183,11 +192,11 @@ class type that stores all previously named classes.
element = diagram.Edge(
refpoints,
- uuid=uuid,
+ uuid=child["id"],
source=self.diagram[source_id],
target=self.diagram[target_id],
styleclass=styleclass,
- styleoverrides=self.get_styleoverrides(uuid, child),
+ styleoverrides=styleoverrides,
context=child.get("context"),
)
self.diagram.add_element(element)
@@ -196,9 +205,7 @@ class type that stores all previously named classes.
assert parent is not None
if not parent.port:
if parent.JSON_TYPE != "symbol":
- parent.styleoverrides |= self.get_styleoverrides(
- uuid, child
- )
+ parent.styleoverrides |= styleoverrides
if isinstance(parent, diagram.Box):
attr_name = "floating_labels"
@@ -223,9 +230,7 @@ class type that stores all previously named classes.
+ (child["position"]["x"], child["position"]["y"]),
(child["size"]["width"], child["size"]["height"]),
label=child["text"],
- styleoverrides=self.get_styleoverrides(
- uuid, child
- ),
+ styleoverrides=styleoverrides,
)
)
@@ -242,7 +247,7 @@ class type that stores all previously named classes.
5,
uuid=child["id"],
styleclass=self.get_styleclass(uuid),
- styleoverrides=self.get_styleoverrides(uuid, child),
+ styleoverrides=styleoverrides,
context=child.get("context"),
)
self.diagram.add_element(element)
@@ -251,7 +256,10 @@ class type that stores all previously named classes.
return
for i in child.get("children", []): # type: ignore
- self.deserialize_child(i, ref, element)
+ if i["type"] == "edge":
+ self._edges.setdefault(i["id"], (i, ref, parent))
+ else:
+ self.deserialize_child(i, ref, element)
def _is_hierarchical(self, uuid: str) -> bool:
def is_contained(obj: diagram.Box) -> bool:
@@ -274,12 +282,12 @@ def get_styleclass(self, uuid: str) -> str | None:
except KeyError:
if not uuid.startswith("__"):
return None
- return uuid[2:].split("_", 1)[0]
+ return uuid[2:].split(":", 1)[0]
else:
return diagram.get_styleclass(melodyobj)
def get_styleoverrides(
- self, uuid: str, child: _elkjs.ELKOutputChild
+ self, uuid: str, child: _elkjs.ELKOutputChild, *, derived: bool = False
) -> diagram.StyleOverrides:
"""Return
[`styling.CSSStyles`][capellambse_context_diagrams.styling.CSSStyles]
@@ -296,6 +304,12 @@ def get_styleoverrides(
styleoverrides = style_condition(obj, self) or {}
+ if uuid == self._diagram.target.uuid:
+ styleoverrides["stroke-width"] = "4"
+
+ if derived:
+ styleoverrides["stroke-dasharray"] = "4"
+
style: dict[str, t.Any]
if style := child.get("style", {}):
styleoverrides |= style
diff --git a/docs/assets/images/Context of Left.svg b/docs/assets/images/Context of Left.svg
index ad286d03..1e6ec85c 100644
--- a/docs/assets/images/Context of Left.svg
+++ b/docs/assets/images/Context of Left.svg
@@ -3,730 +3,4 @@
~ SPDX-License-Identifier: Apache-2.0
-->
-
+
diff --git a/docs/assets/images/Interface Context of Interface.svg b/docs/assets/images/Interface Context of Interface.svg
new file mode 100644
index 00000000..20d877c6
--- /dev/null
+++ b/docs/assets/images/Interface Context of Interface.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/docs/assets/images/Interface Context of Left to right.svg b/docs/assets/images/Interface Context of Left to right.svg
deleted file mode 100644
index f39d3d27..00000000
--- a/docs/assets/images/Interface Context of Left to right.svg
+++ /dev/null
@@ -1,799 +0,0 @@
-
-
-
diff --git a/docs/data_flow_view.md b/docs/data_flow_view.md
index fa101353..9c9127e2 100644
--- a/docs/data_flow_view.md
+++ b/docs/data_flow_view.md
@@ -24,7 +24,7 @@ The diagram elements are collected from the
diag.as_svgdiagram.save(pretty=True)
```
diff --git a/docs/extras/derived.md b/docs/extras/derived.md
new file mode 100644
index 00000000..796833f4
--- /dev/null
+++ b/docs/extras/derived.md
@@ -0,0 +1,37 @@
+
+
+# Derived diagram elements
+
+With capellambse-context-diagrams
+[`v0.2.36`](https://github.com/DSD-DBS/capellambse-context-diagrams/releases/tag/v0.2.36)
+a separate context is built. The elements are derived from the diagram target,
+i.e. the system of interest on which `context_diagram` was called on. The
+render parameter to enable this feature is called `display_derived_interfaces`
+and is available on:
+
+- `LogicalComponent`s and
+- `SystemComponent`s
+
+!!! example "Context Diagram with derived elements"
+
+ ```py
+ from capellambse import MelodyModel
+
+ lost = model.by_uuid("0d18f31b-9a13-4c54-9e63-a13dbf619a69")
+ diag = obj.context_diagram
+ diag.render(
+ "svgdiagram", display_derived_interfaces=True
+ ).save(pretty=True)
+ ```
+
+
+See [`the derivator
+functions`][capellambse_context_diagrams.collectors.default.DERIVATORS] to gain
+an overview over all supported capellambse types and the logic to derive
+elements.
diff --git a/docs/gen_images.py b/docs/gen_images.py
index c09bfe8a..3f5d5c6a 100644
--- a/docs/gen_images.py
+++ b/docs/gen_images.py
@@ -5,6 +5,7 @@
import logging
import pathlib
+import typing as t
import mkdocs_gen_files
from capellambse import MelodyModel, diagram
@@ -16,19 +17,23 @@
dest = pathlib.Path("assets") / "images"
model_path = pathlib.Path(__file__).parent.parent / "tests" / "data"
model = MelodyModel(path=model_path, entrypoint="ContextDiagram.aird")
-general_context_diagram_uuids = {
- "Environment": "e37510b9-3166-4f80-a919-dfaac9b696c7",
- "Eat": "8bcb11e6-443b-4b92-bec2-ff1d87a224e7",
- "Middle": "da08ddb6-92ba-4c3b-956a-017424dbfe85",
- "Capability": "9390b7d5-598a-42db-bef8-23677e45ba06",
- "Lost": "a5642060-c9cc-4d49-af09-defaa3024bae",
- "Left": "f632888e-51bc-4c9f-8e81-73e9404de784",
- "educate Wizards": "957c5799-1d4a-4ac0-b5de-33a65bf1519c",
- "Weird guy": "098810d9-0325-4ae8-a111-82202c0d2016",
- "Top secret": "5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6",
+general_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = {
+ "Environment": ("e37510b9-3166-4f80-a919-dfaac9b696c7", {}),
+ "Eat": ("8bcb11e6-443b-4b92-bec2-ff1d87a224e7", {}),
+ "Middle": ("da08ddb6-92ba-4c3b-956a-017424dbfe85", {}),
+ "Capability": ("9390b7d5-598a-42db-bef8-23677e45ba06", {}),
+ "Lost": ("a5642060-c9cc-4d49-af09-defaa3024bae", {}),
+ "Left": ("f632888e-51bc-4c9f-8e81-73e9404de784", {}),
+ "educate Wizards": ("957c5799-1d4a-4ac0-b5de-33a65bf1519c", {}),
+ "Weird guy": ("098810d9-0325-4ae8-a111-82202c0d2016", {}),
+ "Top secret": ("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6", {}),
}
-interface_context_diagram_uuids = {
- "Left to right": "3ef23099-ce9a-4f7d-812f-935f47e7938d",
+interface_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = {
+ "Left to right": ("3ef23099-ce9a-4f7d-812f-935f47e7938d", {}),
+ "Interface": (
+ "2f8ed849-fbda-4902-82ec-cbf8104ae686",
+ {"include_interface": True},
+ ),
}
hierarchy_context = "16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb"
diagram_uuids = general_context_diagram_uuids | interface_context_diagram_uuids
@@ -36,18 +41,20 @@
realization_fnc_uuid = "beaf5ba4-8fa9-4342-911f-0266bb29be45"
realization_comp_uuid = "b9f9a83c-fb02-44f7-9123-9d86326de5f1"
data_flow_uuid = "3b83b4ba-671a-4de8-9c07-a5c6b1d3c422"
+derived_uuid = "47c3130b-ec39-4365-a77a-5ab6365d1e2e"
def generate_index_images() -> None:
- for uuid in diagram_uuids.values():
+ for uuid, render_params in diagram_uuids.values():
diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd:
- print(diag.render("svg", transparent_background=False), file=fd)
+ render_params["transparent_background"] = False # type: ignore[index]
+ print(diag.render("svg", **render_params), file=fd) # type: ignore[arg-type]
def generate_no_symbol_images() -> None:
for name in ("Capability", "Middle"):
- uuid = general_context_diagram_uuids[name]
+ uuid, _ = general_context_diagram_uuids[name]
diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
diag.display_symbols_as_boxes = True
diag.invalidate_cache()
@@ -99,7 +106,9 @@ def generate_hierarchy_image() -> None:
with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd:
print(
diag.render(
- "svg", include_inner_objects=True, transparent_background=False
+ "svg",
+ display_parent_relation=True,
+ transparent_background=False,
),
file=fd,
)
@@ -153,27 +162,40 @@ def generate_data_flow_image() -> None:
print(diag.render("svg", transparent_background=False), file=fd)
+def generate_derived_image() -> None:
+ diag: context.ContextDiagram = model.by_uuid(derived_uuid).context_diagram
+ params = {
+ "display_derived_interfaces": True,
+ "transparent_background": False,
+ }
+ with mkdocs_gen_files.open(
+ f"{str(dest / diag.name)}-derived.svg", "w"
+ ) as fd:
+ print(diag.render("svg", **params), file=fd)
+
+
generate_index_images()
generate_hierarchy_image()
generate_no_symbol_images()
-wizard = general_context_diagram_uuids["educate Wizards"]
-generate_no_edgelabel_image(wizard)
+wizard_uuid = general_context_diagram_uuids["educate Wizards"][0]
+generate_no_edgelabel_image(wizard_uuid)
-lost = general_context_diagram_uuids["Lost"]
-generate_filter_image(lost, filters.EX_ITEMS, "ex")
-generate_filter_image(lost, filters.FEX_EX_ITEMS, "fex and ex")
-generate_filter_image(lost, filters.FEX_OR_EX_ITEMS, "fex or ex")
+lost_uuid = general_context_diagram_uuids["Lost"][0]
+generate_filter_image(lost_uuid, filters.EX_ITEMS, "ex")
+generate_filter_image(lost_uuid, filters.FEX_EX_ITEMS, "fex and ex")
+generate_filter_image(lost_uuid, filters.FEX_OR_EX_ITEMS, "fex or ex")
generate_styling_image(
- lost,
+ lost_uuid,
dict(
styling.BLUE_ACTOR_FNCS,
junction=lambda o, s: {"stroke": diagram.RGB(220, 20, 60)},
),
"red junction",
)
-generate_styling_image(wizard, {}, "no_styles")
+generate_styling_image(wizard_uuid, {}, "no_styles")
generate_class_tree_images()
generate_realization_view_images()
generate_data_flow_image()
+generate_derived_image()
diff --git a/docs/index.md b/docs/index.md
index 93ab4d46..ce5ed877 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -17,8 +17,8 @@ base class with [`ContextDiagram`s][capellambse_context_diagrams.context.Context
Generate **Context Diagrams** from your model data!
## Features
@@ -27,131 +27,131 @@ Generate **Context Diagrams** from your model data!
The data is collected by either
-- [portless_collector][capellambse_context_diagrams.collectors.portless.collector] for [`ModelObject`s][capellambse.model.common.element.ModelObject] from the Operational Architecture Layer
-- [with_port_collector][capellambse_context_diagrams.collectors.default.collector] for all other Architecture Layers that use ports as connectors of exchanges.
+- [portless_collector][capellambse_context_diagrams.collectors.portless.collector] for [`ModelObject`s][capellambse.model.common.element.ModelObject] from the Operational Architecture Layer
+- [with_port_collector][capellambse_context_diagrams.collectors.default.collector] for all other Architecture Layers that use ports as connectors of exchanges.
It is served conveniently by [get_elkdata][capellambse_context_diagrams.collectors.get_elkdata].
Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.element.ModelObject] with (diagram-class):
-- ??? example "[`oa.Entity`][capellambse.model.layers.oa.Entity] (OAB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("e37510b9-3166-4f80-a919-dfaac9b696c7").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- ??? example "[`oa.OperationalActivity`][capellambse.model.layers.oa.OperationalActivity] (OAIB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("8bcb11e6-443b-4b92-bec2-ff1d87a224e7").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- ??? example "[`oa.OperationalCapability`][capellambse.model.layers.oa.OperationalCapability] (OCB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("da08ddb6-92ba-4c3b-956a-017424dbfe85").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- ??? example "[`ctx.Mission`][capellambse.model.layers.ctx.Mission] (MCB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- ??? example "[`ctx.Capability`][capellambse.model.layers.ctx.Capability] (MCB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("9390b7d5-598a-42db-bef8-23677e45ba06").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- [`ctx.SystemComponent`][capellambse.model.layers.ctx.SystemComponent] (SAB)
-
-- ??? example "[`ctx.SystemFunction`][capellambse.model.layers.ctx.SystemFunction] (SDFB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("a5642060-c9cc-4d49-af09-defaa3024bae").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- ??? example "[`la.LogicalComponent`][capellambse.model.layers.la.LogicalComponent] (LAB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("f632888e-51bc-4c9f-8e81-73e9404de784").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-- ??? example "[`la.LogicalFunction`][capellambse.model.layers.la.LogicalFunction] (LDFB)"
-
- ``` py
- import capellambse
-
- model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
- diag = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c").context_diagram
- diag.render("svgdiagram").save(pretty=True)
- ```
-
-
-* [`pa.PhysicalComponent`][capellambse.model.layers.pa.PhysicalComponent] (PAB)
-* [`pa.PhysicalFunction`][capellambse.model.layers.pa.PhysicalFunction] (PDFB)
-* [`pa.PhysicalComponent`][capellambse.model.layers.pa.PhysicalComponent] (PAB)
-* [`pa.PhysicalFunction`][capellambse.model.layers.pa.PhysicalFunction] (PDFB)
+- ??? example "[`oa.Entity`][capellambse.model.layers.oa.Entity] (OAB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("e37510b9-3166-4f80-a919-dfaac9b696c7").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- ??? example "[`oa.OperationalActivity`][capellambse.model.layers.oa.OperationalActivity] (OAIB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("8bcb11e6-443b-4b92-bec2-ff1d87a224e7").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- ??? example "[`oa.OperationalCapability`][capellambse.model.layers.oa.OperationalCapability] (OCB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("da08ddb6-92ba-4c3b-956a-017424dbfe85").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- ??? example "[`ctx.Mission`][capellambse.model.layers.ctx.Mission] (MCB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- ??? example "[`ctx.Capability`][capellambse.model.layers.ctx.Capability] (MCB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("9390b7d5-598a-42db-bef8-23677e45ba06").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- [`ctx.SystemComponent`][capellambse.model.layers.ctx.SystemComponent] (SAB)
+
+- ??? example "[`ctx.SystemFunction`][capellambse.model.layers.ctx.SystemFunction] (SDFB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("a5642060-c9cc-4d49-af09-defaa3024bae").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- ??? example "[`la.LogicalComponent`][capellambse.model.layers.la.LogicalComponent] (LAB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("f632888e-51bc-4c9f-8e81-73e9404de784").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+- ??? example "[`la.LogicalFunction`][capellambse.model.layers.la.LogicalFunction] (LDFB)"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c").context_diagram
+ diag.render("svgdiagram").save(pretty=True)
+ ```
+
+
+* [`pa.PhysicalComponent`][capellambse.model.layers.pa.PhysicalComponent] (PAB)
+* [`pa.PhysicalFunction`][capellambse.model.layers.pa.PhysicalFunction] (PDFB)
+* [`pa.PhysicalComponent`][capellambse.model.layers.pa.PhysicalComponent] (PAB)
+* [`pa.PhysicalFunction`][capellambse.model.layers.pa.PhysicalFunction] (PDFB)
#### Hierarchy in diagrams
@@ -166,7 +166,7 @@ Hierarchy is identified and supported:
model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
obj = model.by_uuid("16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb")
- diagram = obj.context_diagram.render("svgdiagram", include_inner_objects=True)
+ diagram = obj.context_diagram.render("svgdiagram", display_parent_relation=True)
diagram.save(pretty=True)
```
+??? example "Include the interface the ([`fa.ComponentExchange`][capellambse.model.crosslayer.fa.ComponentExchange])"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("fbb7f735-3c1f-48de-9791-179d35ca7b98").context_diagram
+ diag.render("svgdiagram", include_interface=True).save(pretty=True)
+ ```
+
+
+
!!! warning "Interface context only supported for the LogicalComponentExchanges"
### Customized edge routing
!!! note "Custom routing"
- The routing differs from [ELK's Layered Algorithm](https://www.eclipse.org/elk/reference/algorithms/org-eclipse-elk-layered.html): The flow display is disrupted!
- We configure exchanges such that they appear in between the context
- participants. This decision breaks the display of data flow which is one
- of the main aims of ELK's Layered algorithm. However this lets counter
- flow exchanges routes lengths and bendpoints increase.
+The routing differs from [ELK's Layered Algorithm](https://www.eclipse.org/elk/reference/algorithms/org-eclipse-elk-layered.html): The flow display is disrupted!
+We configure exchanges such that they appear in between the context
+participants. This decision breaks the display of data flow which is one
+of the main aims of ELK's Layered algorithm. However this lets counter
+flow exchanges routes lengths and bendpoints increase.