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 --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Left - - - - - - - - - - - - Right - - - - - - - - - Upper - - - - - - - - - - Left to right - - - - - - - Upper to Left - - - - +HogwartsLeftRightUpperLeft to rightUpper to Left 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 @@ + + +LFLC 1LFNC 1LA 1LFNC 2LC 20RFNC 1LC 1RFNC 2LC 1RFNC 3InterfaceFE 2FE 3FE 4FE 1 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Right - - - - - - Circle - - - - - - - - - - - - - - - - - - - - - Left - - - - - - First - - - - - - - - - - - - Second - - - - - - - - - - - - Third - - - - - - - - - - the - - - - - - - labelsize - - - - - - - The - - - - - - - determines - - - - - - - edgelength - - - - 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) ```
- +
[OAIB] DataFlow View Diagram of Eat food
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) + ``` +
+ +
Context diagram of Center with derived context
+
+ +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!
- -
Interface context diagram of Left to right
+ +
Interface context diagram of Interface
## 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) - ``` -
- -
Context diagram of Environment Entity with type [OAB]
-
- -- ??? 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) - ``` -
- -
Context diagram of Activity Eat with type [OAIB]
-
- -- ??? 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) - ``` -
- -
Context diagram of Middle OperationalCapability with type [OCB]
-
- -- ??? 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) - ``` -
- -
Context diagram of Mission Top secret with type [MCB]
-
- -- ??? 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) - ``` -
- -
Context diagram of Capability Capability with type [MCB]
-
- -- [`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) - ``` -
- -
Context diagram of Lost SystemFunction with type [SDFB]
-
- -- ??? 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) - ``` -
- -
Context diagram of Left LogicalComponent with type [LAB]
-
- -- ??? 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) - ``` -
- -
Context diagram of educate Wizards LogicalFunction with type [LDFB]
-
- -* [`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) + ``` +
+ +
Context diagram of Environment Entity with type [OAB]
+
+ +- ??? 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) + ``` +
+ +
Context diagram of Activity Eat with type [OAIB]
+
+ +- ??? 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) + ``` +
+ +
Context diagram of Middle OperationalCapability with type [OCB]
+
+ +- ??? 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) + ``` +
+ +
Context diagram of Mission Top secret with type [MCB]
+
+ +- ??? 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) + ``` +
+ +
Context diagram of Capability Capability with type [MCB]
+
+ +- [`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) + ``` +
+ +
Context diagram of Lost SystemFunction with type [SDFB]
+
+ +- ??? 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) + ``` +
+ +
Context diagram of Left LogicalComponent with type [LAB]
+
+ +- ??? 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) + ``` +
+ +
Context diagram of educate Wizards LogicalFunction with type [LDFB]
+
+ +* [`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) ```
@@ -192,16 +192,31 @@ The data is collected by [get_elkdata_for_exchanges][capellambse_context_diagram
Interface context diagram of Left to right LogicalComponentExchange with type [LAB]
+??? 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) + ``` +
+ +
Interface context diagram of Interface LogicalComponentExchange with type [LAB]
+
+ + !!! 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.
diff --git a/mkdocs.yml b/mkdocs.yml index 11b56c02..7ff20180 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -89,11 +89,12 @@ nav: - Extras: - Filters: extras/filters.md - Styling: extras/styling.md + - 🔥 Derived 🔥: extras/derived.md - Tree View: - Overview: tree_view.md - Realization View: - Overview: realization_view.md - - 🔥 DataFlow View 🔥: + - DataFlow View: - Overview: data_flow_view.md - Code Reference: reference/ diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird index 61b5b7b3..f5b807cc 100644 --- a/tests/data/ContextDiagram.aird +++ b/tests/data/ContextDiagram.aird @@ -51,6 +51,14 @@ + + + + + + + + @@ -78,7 +86,7 @@ - +
@@ -94,10 +102,26 @@ - - -
-
+ + +
+
+ + + + + + +
+
+ + + + + + +
+
@@ -7498,6 +7522,17 @@ + + + + + + + + + + + @@ -7557,16 +7592,16 @@ - - - + + + - - - + + + - - + + @@ -7700,22 +7735,6 @@ - - - - - - - - - - - - - - - - @@ -7732,28 +7751,60 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - - - - + + + + + - + @@ -7787,14 +7838,11 @@ - - - - - KEEP_LOCATION - KEEP_SIZE - KEEP_RATIO - + + + + + @@ -7819,6 +7867,15 @@ + + + + + + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO @@ -7831,7 +7888,7 @@ - + @@ -7862,8 +7919,8 @@ - - + + @@ -7904,8 +7961,8 @@ KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + @@ -7913,7 +7970,7 @@ - + @@ -7957,15 +8014,6 @@ - - - - - - - - - @@ -7975,15 +8023,33 @@ - + + + + + + + + + + - + - + + + + + + + + + + @@ -8944,509 +9010,3617 @@ - - - - - - - - - + + + + + + + + + - - - + + + - - - - - - - - - - + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + - - - + + + + + + + + + + - - + + - - + + - - - - - - + + + + + + + + + + - - - + + + + + + + + + + - - + + - - + + - - - - - - - - - - + + + + + + - - - - - - - - - - + + + - - + + - - + + - - + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + - - + + - - - - - - - - - - - - + + + - - - + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - - - - + + + + - - - - - - - - - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + - - - + + + - - + + - - + + - - - - - + + + + + - - - + + + - - + + - - + + - - - - - + + + + + - - - + + + - - + + - - + + - - - - - + + + + + - - + + - - - - - - - - - - - - - + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - - - - - - - - - - - - - - - - - - - - - - + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - + - - - - - - - - - - - - + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - + - - - - + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - - - - - - - - - KEEP_LOCATION - KEEP_SIZE - KEEP_RATIO - - - - - + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - + - - - - - - KEEP_LOCATION - KEEP_SIZE - KEEP_RATIO - - - - - - - - - - - - - - - - + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_RATIO + KEEP_SIZE + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + - - - - + + + KEEP_LOCATION - KEEP_SIZE KEEP_RATIO - - + KEEP_SIZE + + - - - - - - - - - - - - - + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + - + - - - - - - - - - - - - - - KEEP_LOCATION - KEEP_SIZE - KEEP_RATIO - - + + + + + + + + - + - - - - - - + + + + + + + + - + - - - - - - - - + + + + + + + + - + - - - - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_RATIO + KEEP_SIZE + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_RATIO + KEEP_SIZE + + + + + + + + + KEEP_LOCATION + KEEP_RATIO + KEEP_SIZE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella index a20b61b3..7182daa5 100644 --- a/tests/data/ContextDiagram.capella +++ b/tests/data/ContextDiagram.capella @@ -253,6 +253,40 @@ id="14ed11c2-da65-4721-9258-180467c2c376" name="Pooring rain"/> + + + + + + + + + + + + + + + + + @@ -289,6 +323,57 @@ + + + + + + + + + + + + + + + + + name="RightStack" abstractType="#e9a6fd43-88d2-4832-91d5-595b6fbf613d"/> + + + + name="RightStack" actor="true" human="true"/> + + + + + + + + + + + + + + + + + + + + + + + + + + id="fa1b9f3b-0770-4aab-b5a9-2843af69706f" name="FIP 1"/> + id="77c9db12-29c7-4ced-822d-3a154726f2be" name="Hierarchy Function"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id="f6447eb1-a4d3-4af6-84ad-22fdd29c05c8" name="FIP 1"/> + id="05fe69c7-d4c3-43a3-aaa4-0ce0fb27ce7e" name="FOP 1"/> + id="b839937e-8cc4-49cd-87e7-e1aee020ee05" name="LFNC 1"> + + id="c9b687d0-6134-4de8-99e6-929f2810ca58" name="FOP 2"/> + id="a1283c40-6fd4-4c2d-aec1-114155811684" name="RFNC 3"> + id="7c38a08d-c70e-477f-a8d7-47263c47df94" name="FIP 1"/> + id="a305ab7e-d3ac-4152-8ee3-995102c556da" name="RFNC 2"> + + id="3d03fde7-3c12-409f-9a4a-5ccbc5cb9ee1" name="FOP 1"/> + id="fbfb2b20-b711-4211-9b75-25e38390cdbc" name="RFNC 1"> + id="3f88a495-cc76-4a01-8aae-c79bdb6b22e2" name="FIP 1"/> id="fe86c5d6-7666-4537-af07-5817b2f12d1c" name="woa" target="#fa1b9f3b-0770-4aab-b5a9-2843af69706f" source="#9c2fab55-c507-4250-aaa1-9a3d5111ef43"/> + id="d3b412f4-91cc-4f1c-a32d-3a56459a108d" name="IF1 to IF2" target="#aed02601-7d03-4ed8-bd19-83faa0df3d71" + source="#9fefbe1e-3f74-4544-833c-bb040bcfeac7"/> + + + + id="209d39e3-d6d9-4d87-8e48-a85b3a4565ab" name="IF2 to RF2" target="#da3e108d-c8b1-4beb-896a-bbb6618acf22" + source="#ad194d39-1467-490f-9825-a7bafcd3e952"/> + id="0b97879b-08f5-4fc2-bc3b-66e73e7c2158" name="IF2 to RF1" target="#ba26d020-30cc-4d79-ac9a-b4729663a9e5" + source="#09ad0a10-a06b-401e-a31b-3ca0e33ede8e"/> + + + + + + + + + name="Multiport" abstractType="#b3888dad-a870-4b8b-97d4-0ddb83ef9251"/> - - + id="5286d854-84cc-4514-88ab-d0d67c6ddd37" name="o2" source="#3e0a3791-cc99-4af5-b789-5b33451ca743" target="#0b558f0b-dd4a-4f35-ad88-f4abe44df415" kind="FLOW"/> + id="dbd99773-efb6-4476-bf5c-270a61f18b09" name="Interface" source="#33f06d77-84ec-46eb-ae0d-f2694a35556b" + target="#5d562aa6-738a-48ce-aeb9-eb9d656edc7b" kind="FLOW"> + + id="1c050208-ea5e-403e-8987-5090dbcb0a02" name="o3" source="#5442bfe8-79d8-45b5-bf49-29f7f0c6f635" target="#5ccac19c-76d3-405d-8fc3-85c8958dab45" kind="FLOW"/> + + + + + + + name="Left 1" abstractType="#1c416771-64bc-4d0c-99eb-296ce27d0a35"/> - - - - + + + + + + + @@ -3760,16 +4062,20 @@ The predator is far away + + - @@ -3779,63 +4085,178 @@ The predator is far away + + + + id="99a1d711-74af-4db7-af08-4dbd91c281ce" name="Inner owner"> + + + + + + + id="1c416771-64bc-4d0c-99eb-296ce27d0a35" name="Left 1" actor="true" + human="true"> + + id="c999f0f0-49d0-4b01-b260-f69dc63abbcb" name="Right 1" actor="true"> + - + id="53558f58-270e-4206-8fc7-3cf9e788fac9" name="Right owner"> + + + + + + id="47c3130b-ec39-4365-a77a-5ab6365d1e2e" name="Center"> + id="42b9684c-634b-451a-bb40-646f5039adee" targetElement="#20f75169-da3d-43d1-a1ae-3f0426bf948f" + sourceElement="#47c3130b-ec39-4365-a77a-5ab6365d1e2e"/> + id="4ff0564f-2e95-46db-809f-094f66988a6c" targetElement="#8fca36e2-40c2-4805-bc6d-ee3b2caece99" + sourceElement="#47c3130b-ec39-4365-a77a-5ab6365d1e2e"/> + id="33f06d77-84ec-46eb-ae0d-f2694a35556b" name="CP 1" orientation="OUT" + kind="FLOW"> + + + + + + id="eb976b50-2f61-446a-b524-68790af06a57" name="LC 17" actor="true"> + id="6d632960-6583-42cb-be6a-05d8b4624a0b" targetElement="#24e89910-0f30-4744-8f69-368c70627e60" + sourceElement="#eb976b50-2f61-446a-b524-68790af06a57"/> + + + + + + + + + + + + + + + + + id="7f375127-f08a-4deb-8fb6-97c955840f89" name="LC 20"> + id="ceeb2266-e3e4-4ce8-a6f0-4a6163b2db66" targetElement="#fbfb2b20-b711-4211-9b75-25e38390cdbc" + sourceElement="#7f375127-f08a-4deb-8fb6-97c955840f89"/> + + + + + + + + + + + + + + @@ -3995,16 +4416,17 @@ The predator is far away kind="FLOW"/> + id="b3d9d50e-7592-4689-8ba7-e50f082cd71c" name="LA 9" actor="true" human="true"> - - + id="fa38a4f1-b17d-486a-aee0-644e03d7c3f0" targetElement="#b68b57a3-5e07-4854-8209-b84a13c24e44" + sourceElement="#b3d9d50e-7592-4689-8ba7-e50f082cd71c"/> + id="5d562aa6-738a-48ce-aeb9-eb9d656edc7b" name="CP 1" orientation="IN" + kind="FLOW"> + + = 37 -def test_hierarchy_in_context_diagram(model: capellambse.MelodyModel) -> None: +def test_parent_relation_in_context_diagram( + model: capellambse.MelodyModel, +) -> None: obj = model.by_uuid(TEST_HIERARCHY_UUID) - expected_children = TEST_HIERARCHY_CHILDREN_UUIDS - adiag = obj.context_diagram.render(None, include_inner_objects=True) + diag = obj.context_diagram + hide_relation = diag.render(None, display_parent_relation=False) + display_relation = diag.render(None, display_parent_relation=True) - children = {obj.uuid for obj in adiag[TEST_HIERARCHY_UUID].children} + for uuid in TEST_HIERARCHY_PARENTS_UUIDS: + assert display_relation[uuid] - for uuid in expected_children: - assert uuid in children + with pytest.raises(KeyError): + hide_relation[uuid] # pylint: disable=pointless-statement -def test_context_diagram_pass_params_to_render( - model: capellambse.MelodyModel, +@pytest.mark.parametrize("uuid", TEST_ACTIVITY_UUIDS) +def test_context_diagram_of_allocated_activities( + model: capellambse.MelodyModel, uuid: str ) -> None: - obj = model.by_uuid(TEST_HIERARCHY_UUID) + obj = model.by_uuid(uuid) diag = obj.context_diagram - without_hierarchy = diag.render(None, include_inner_objects=False) - with_hierarchy = diag.render(None, include_inner_objects=True) + diag.display_parent_relation = True - for uuid in TEST_HIERARCHY_CHILDREN_UUIDS: - assert with_hierarchy[uuid] + assert len(diag.nodes) > 1 - with pytest.raises(KeyError): - without_hierarchy[uuid] # pylint: disable=pointless-statement + +@pytest.mark.parametrize("uuid", TEST_FUNCTION_UUIDS) +def test_context_diagram_of_allocated_functions( + model: capellambse.MelodyModel, uuid: str +) -> None: + obj = model.by_uuid(uuid) + + diag = obj.context_diagram + diag.display_parent_relation = True + + assert len(diag.nodes) > 1 def test_context_diagram_with_derived_interfaces( @@ -156,9 +178,9 @@ def test_context_diagram_with_derived_interfaces( ) -> None: obj = model.by_uuid(TEST_DERIVED_UUID) - diag = obj.context_diagram - diag.display_derived_interfaces = True - - diag.render("svgdiagram").save(pretty=True) + context_diagram = obj.context_diagram + derived_diagram = context_diagram.render( + None, display_derived_interfaces=True + ) - assert len(diag.nodes) > 5 + assert len(derived_diagram) > 5 diff --git a/tests/test_interface_diagrams.py b/tests/test_interface_diagrams.py index ed0918ac..00fce9b0 100644 --- a/tests/test_interface_diagrams.py +++ b/tests/test_interface_diagrams.py @@ -4,6 +4,8 @@ import capellambse import pytest +TEST_INTERFACE_UUID = "2f8ed849-fbda-4902-82ec-cbf8104ae686" + @pytest.mark.parametrize( "uuid", @@ -21,3 +23,23 @@ def test_interface_diagrams_get_rendered( diag = obj.context_diagram assert diag.nodes + + +def test_interface_diagrams_with_nested_components( + model: capellambse.MelodyModel, +) -> None: + obj = model.by_uuid(TEST_INTERFACE_UUID) + + diag = obj.context_diagram + + assert diag.nodes + + +def test_interface_diagram_with_included_interface( + model: capellambse.MelodyModel, +) -> None: + obj = model.by_uuid(TEST_INTERFACE_UUID) + + diag = obj.context_diagram.render(None, include_interface=True) + + assert diag[TEST_INTERFACE_UUID]