Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat derived interfaces #94

Merged
merged 8 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions capellambse_context_diagrams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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

Expand Down
98 changes: 98 additions & 0 deletions capellambse_context_diagrams/collectors/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@
from capellambse import helpers
from capellambse.model import common
from capellambse.model.crosslayer import cs, fa
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]
Expand Down Expand Up @@ -119,6 +126,9 @@ def collector(
)

centerbox["height"] = max(centerbox["height"], *stack_heights.values())
if diagram.display_derived_interfaces:
add_derived_components_and_interfaces(diagram, data)

return data


Expand Down Expand Up @@ -259,3 +269,91 @@ def port_context_collector(
info.ports.append(port)

return iter(ctx.values())


def add_derived_components_and_interfaces(
diagram: context.ContextDiagram, data: _elkjs.ELKInputData
) -> None:
"""Add hidden Boxes and Exchanges to ``obj``'s context.

The derived exchanges are displayed with a dashed line.
"""
if derivator := DERIVATORS.get(type(diagram.target)):
derivator(diagram, data)


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:
if isinstance(port, fa.FunctionOutputPort):
attr = "target"
else:
attr = "source"

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: # No owner of owner.
pass

# 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.

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,
)
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, target_id = target_id, source_id
ewuerger marked this conversation as resolved.
Show resolved Hide resolved

data["edges"].append(
{
"id": f"{STYLECLASS_PREFIX}-ComponentExchange:{i}",
"sources": [source_id],
"targets": [target_id],
}
)

data["children"][0]["height"] += (
makers.PORT_PADDING
+ (makers.PORT_SIZE + makers.PORT_PADDING) * len(components) // 2
)


DERIVATORS = {
la.LogicalComponent: derive_from_functions,
sa.SystemComponent: derive_from_functions,
}
"""Supported objects to build derived contexts for."""
18 changes: 12 additions & 6 deletions capellambse_context_diagrams/collectors/makers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion capellambse_context_diagrams/collectors/tree_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -25,6 +26,7 @@
"elk.direction": "DOWN",
"edgeRouting": "ORTHOGONAL",
}
ASSOC_STYLECLASS = "__Association"


class ClassProcessor:
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions capellambse_context_diagrams/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ class ContextDiagram(diagram.AbstractDiagram):
display_parent_relation
Display objects with a parent relationship to the object of
interest as the parent box.
display_derived_interfaces
Display derived objects collected from additional collectors
beside the main collector for building the context.
slim_center_box
Minimal width for the center box, containing just the icon and
the label. This is False if hierarchy was identified.
Expand All @@ -255,6 +258,7 @@ def __init__(
render_styles: dict[str, styling.Styler] | None = None,
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:
Expand All @@ -267,6 +271,7 @@ def __init__(
self.__filters: cabc.MutableSet[str] = self.FilterSet(self)
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

Expand Down
67 changes: 40 additions & 27 deletions capellambse_context_diagrams/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -112,27 +113,28 @@ def deserialize_child(
[`diagram.Diagram`][capellambse.diagram.Diagram] : Diagram
class type that stores all previously named classes.
"""
uuid: str
styleclass: str | None
derived = False
if child["id"].startswith("__"):
styleclass: str | None = child["id"][2:].split("_", 1)[0]
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(child["id"])
has_symbol_cls = collectors.makers.is_symbol(obj)
except KeyError:
logger.error(
"ModelObject could not be found: '%s'", child["id"]
)

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
]

Expand All @@ -145,48 +147,56 @@ class type that stores all previously named classes.
element = diagram.Box(
ref,
size,
uuid=child["id"],
uuid=uuid,
parent=parent,
port=is_port,
styleclass=styleclass,
styleoverrides=self.get_styleoverrides(child),
styleoverrides=styleoverrides,
features=features,
context=child.get("context"),
)
element.JSON_TYPE = box_type
self.diagram.add_element(element)
self._cache[child["id"]] = element
self._cache[uuid] = element
elif child["type"] == "edge":
styleclass = child.get("styleclass", styleclass) # type: ignore[assignment]
styleclass = REMAP_STYLECLASS.get(styleclass, styleclass) # type: ignore[arg-type]
EDGE_HANDLER.get(styleclass, lambda c: c)(child)

source_id = child["sourceId"]
if source_id.startswith("__"):
source_id = source_id[2:].split(":", 1)[-1]

target_id = child["targetId"]
if target_id.startswith("__"):
target_id = target_id[2:].split(":", 1)[-1]

if child["routingPoints"]:
refpoints = [
ref + (point["x"], point["y"])
for point in child["routingPoints"]
]
else:
source = self._cache[child["sourceId"]]
target = self._cache[child["targetId"]]
source = self._cache[source_id]
target = self._cache[target_id]
refpoints = route_shortest_connection(source, target)

element = diagram.Edge(
refpoints,
uuid=child["id"],
source=self.diagram[child["sourceId"]],
target=self.diagram[child["targetId"]],
source=self.diagram[source_id],
target=self.diagram[target_id],
styleclass=styleclass,
styleoverrides=self.get_styleoverrides(child),
styleoverrides=styleoverrides,
context=child.get("context"),
)
self.diagram.add_element(element)
self._cache[child["id"]] = element
self._cache[uuid] = element
elif child["type"] == "label":
assert parent is not None
if not parent.port:
if parent.JSON_TYPE != "symbol":
parent.styleoverrides |= self.get_styleoverrides(child)
parent.styleoverrides |= styleoverrides

if isinstance(parent, diagram.Box):
attr_name = "floating_labels"
Expand All @@ -211,7 +221,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(child),
styleoverrides=styleoverrides,
)
)

Expand All @@ -228,7 +238,7 @@ class type that stores all previously named classes.
5,
uuid=child["id"],
styleclass=self.get_styleclass(uuid),
styleoverrides=self.get_styleoverrides(child),
styleoverrides=styleoverrides,
context=child.get("context"),
)
self.diagram.add_element(element)
Expand Down Expand Up @@ -260,12 +270,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, child: _elkjs.ELKOutputChild
self, uuid: str, child: _elkjs.ELKOutputChild, *, derived: bool = False
) -> diagram.StyleOverrides:
"""Return
[`styling.CSSStyles`][capellambse_context_diagrams.styling.CSSStyles]
Expand All @@ -276,12 +286,15 @@ def get_styleoverrides(
styleoverrides: dict[str, t.Any] = {}
if style_condition is not None:
if child["type"] != "junction":
obj = self._diagram._model.by_uuid(child["id"])
obj = self._diagram._model.by_uuid(uuid)
else:
obj = None

styleoverrides = style_condition(obj, self) or {}

if derived:
styleoverrides["stroke-dasharray"] = "4"

style: dict[str, t.Any]
if style := child.get("style", {}):
styleoverrides |= style
Expand Down
Loading
Loading