Skip to content

Commit

Permalink
wip: Holy moly
Browse files Browse the repository at this point in the history
  • Loading branch information
ewuerger committed Dec 19, 2023
1 parent 308175f commit fbd908b
Show file tree
Hide file tree
Showing 9 changed files with 702 additions and 108 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ repos:
- --license-filepath
- license_header.txt
- --comment-style
- '#'
- "#"
- id: insert-license
name: Insert Licence for HTML/XML/SVG files
files: '\.html$|\.md$|\.svg$'
Expand All @@ -56,7 +56,7 @@ repos:
- --license-filepath
- license_header.txt
- --comment-style
- '<!--| ~| -->'
- "<!--| ~| -->"
- id: insert-license
name: Insert Licence for CSS files
files: '\.css$'
Expand All @@ -65,7 +65,7 @@ repos:
- --license-filepath
- license_header.txt
- --comment-style
- '/*| *| */'
- "/*| *| */"
- repo: https://github.com/fsfe/reuse-tool
rev: v2.1.0
hooks:
Expand Down
27 changes: 26 additions & 1 deletion capellambse_context_diagrams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def init() -> None:
register_classes()
register_interface_context()
register_tree_view()
register_realization_view()
# register_functional_context() XXX: Future


Expand Down Expand Up @@ -152,9 +153,33 @@ def register_functional_context() -> None:


def register_tree_view() -> None:
"""Add the `tree_view` attribute to ``Class``es."""
"""Add the ``tree_view`` attribute to ``Class``es."""
common.set_accessor(
information.Class,
"tree_view",
context.ClassTreeAccessor(DiagramType.CDB.value),
)


def register_realization_view() -> None:
"""Add the ``realization_view`` attribute to various objects.
Adds ``realization_view`` to Activities, Functions and Components
of all layers.
"""
supported_classes: list[ClassPair] = [
(oa.Entity, DiagramType.OAB),
(oa.OperationalActivity, DiagramType.OAIB),
(ctx.SystemComponent, DiagramType.SAB),
(ctx.SystemFunction, DiagramType.SDFB),
(la.LogicalComponent, DiagramType.LAB),
(la.LogicalFunction, DiagramType.LDFB),
(pa.PhysicalComponent, DiagramType.PAB),
(pa.PhysicalFunction, DiagramType.PDFB),
]
for class_, dgcls in supported_classes:
common.set_accessor(
class_,
"realization_view",
context.RealizationViewContextAccessor(dgcls.value),
)
30 changes: 17 additions & 13 deletions capellambse_context_diagrams/_elkjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
NODE_HOME = Path(capellambse.dirs.user_cache_dir, "elkjs", "node_modules")
PATH_TO_ELK_JS = Path(__file__).parent / "elk.js"
REQUIRED_NPM_PKG_VERSIONS: t.Dict[str, str] = {
"elkjs": "0.8.2",
# "elkjs": "0.8.2",
"elkjs": "file:~/elk-master/git/elkjs",
}
"""npm package names and versions required by this Python module."""

Expand Down Expand Up @@ -149,62 +150,65 @@ class ELKSize(t.TypedDict):
height: t.Union[int, float]


class ELKOutputData(t.TypedDict):
"""Data that comes from ELK."""
class ELKOutputElement(t.TypedDict):
"""Base class for all elements that comes out of ELK."""

id: str

style: dict[str, t.Any]


class ELKOutputData(ELKOutputElement):
"""Data that comes from ELK."""

type: t.Literal["graph"]
children: cabc.MutableSequence[ELKOutputChild] # type: ignore


class ELKOutputNode(t.TypedDict):
class ELKOutputNode(ELKOutputElement):
"""Node that comes out of ELK."""

id: str
type: t.Literal["node"]
children: cabc.MutableSequence[ELKOutputChild] # type: ignore

position: ELKPoint
size: ELKSize


class ELKOutputJunction(t.TypedDict):
class ELKOutputJunction(ELKOutputElement):
"""Exchange-Junction that comes out of ELK."""

id: str
type: t.Literal["junction"]

position: ELKPoint
size: ELKSize


class ELKOutputPort(t.TypedDict):
class ELKOutputPort(ELKOutputElement):
"""Port that comes out of ELK."""

id: str
type: t.Literal["port"]
children: cabc.MutableSequence[ELKOutputLabel]

position: ELKPoint
size: ELKSize


class ELKOutputLabel(t.TypedDict):
class ELKOutputLabel(ELKOutputElement):
"""Label that comes out of ELK."""

id: str
type: t.Literal["label"]
text: str

position: ELKPoint
size: ELKSize


class ELKOutputEdge(t.TypedDict):
class ELKOutputEdge(ELKOutputElement):
"""Edge that comes out of ELK."""

id: str
type: t.Literal["edge"]

sourceId: str
targetId: str
routingPoints: cabc.MutableSequence[ELKPoint]
Expand Down
2 changes: 1 addition & 1 deletion capellambse_context_diagrams/collectors/makers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import typing_extensions as te
from capellambse import helpers
from capellambse.model import common, layers
from capellambse.model import common, crosslayer, layers
from capellambse.svg.decorations import icon_padding, icon_size

from .. import _elkjs, context
Expand Down
188 changes: 188 additions & 0 deletions capellambse_context_diagrams/collectors/realization_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# SPDX-FileCopyrightText: 2022 Copyright DB Netz AG and the capellambse-context-diagrams contributors
# SPDX-License-Identifier: Apache-2.0
"""This submodule defines the collector for the RealizationView diagram."""
from __future__ import annotations

import collections.abc as cabc
import copy
import re
import typing as t

from capellambse.model import common, crosslayer
from capellambse.model.crosslayer import cs, fa

from .. import _elkjs, context
from . import makers

RE_LAYER_PTRN = re.compile(r"([A-Z]?[a-z]+)")


def collector(
diagram: context.ContextDiagram, params: dict[str, t.Any]
) -> tuple[_elkjs.ELKInputData, list[_elkjs.ELKInputEdge]]:
"""Return the class tree data for ELK."""
data = makers.make_diagram(diagram)
layout_options: _elkjs.LayoutOptions = copy.deepcopy(
_elkjs.RECT_PACKING_LAYOUT_OPTIONS # type:ignore[arg-type]
)
layout_options["elk.contentAlignment"] = "V_CENTER H_CENTER"
del layout_options["widthApproximation.targetWidth"]
data["layoutOptions"] = layout_options
_collector = COLLECTORS[params.get("search_direction", "ALL")]
lay_to_els = _collector(diagram.target, params.get("depth", 1))
layer_layout_options: _elkjs.LayoutOptions = layout_options | { # type: ignore[operator]
"nodeSize.constraints": "[NODE_LABELS,MINIMUM_SIZE]",
} # type: ignore[assignment]
edges: list[_elkjs.ELKInputEdge] = []
for layer, elements in lay_to_els.items():
labels = [makers.make_label(layer)]
width, height = makers.calculate_height_and_width(labels)
layer_box: _elkjs.ELKInputChild = {
"id": elements[0]["layer"].uuid,
"children": [],
"height": width,
"width": height,
"layoutOptions": layer_layout_options,
}
children: dict[str, _elkjs.ELKInputChild] = {}
for elt in elements:
if elt["origin"] is not None:
edges.append(
{
"id": f'{elt["origin"].uuid}_{elt["element"].uuid}',
"sources": [elt["origin"].uuid],
"targets": [elt["element"].uuid],
}
)

if not (element_box := children.get(elt["element"].uuid)):
element_box = makers.make_box(elt["element"], no_symbol=True)
children[elt["element"].uuid] = element_box
layer_box["children"].append(element_box)
index = len(layer_box["children"]) - 1

if params.get("show_owners"):
owner = elt["element"].owner
if not isinstance(owner, (fa.Function, cs.Component)):
continue

if not (owner_box := children.get(owner.uuid)):
owner_box = makers.make_box(owner, no_symbol=True)
owner_box["height"] += element_box["height"]
children[owner.uuid] = owner_box
layer_box["children"].append(owner_box)

del layer_box["children"][index]
owner_box.setdefault("children", []).append(element_box)
owner_box["width"] += element_box["width"]
if (
elt["origin"] is not None
and elt["origin"].owner.uuid in children
and owner.uuid in children
):
eid = f'{elt["origin"].owner.uuid}_{owner.uuid}'
edges.append(
{
"id": eid,
"sources": [elt["origin"].owner.uuid],
"targets": [owner.uuid],
}
)

data["children"].append(layer_box)
data["children"] = data["children"][::-1]
return data, edges


def collect_realized(
start: common.GenericElement, depth: int
) -> dict[LayerLiteral, list[dict[str, t.Any]]]:
"""Collect all elements from ``realized_`` attributes up to depth."""
return collect_elements(start, depth, "ABOVE", "realized")


def collect_realizing(
start: common.GenericElement, depth: int
) -> dict[LayerLiteral, list[dict[str, t.Any]]]:
"""Collect all elements from ``realizing_`` attributes down to depth."""
return collect_elements(start, depth, "BELOW", "realizing")


def collect_all(
start: common.GenericElement, depth: int
) -> dict[LayerLiteral, list[common.GenericElement]]:
"""Collect all elements in both ABOVE and BELOW directions."""
above = collect_realized(start, depth)
below = collect_realizing(start, depth)
return above | below


def collect_elements(
start: common.GenericElement,
depth: int,
direction: str,
attribute_prefix: str,
origin: common.GenericElement = None,
) -> dict[LayerLiteral, list[dict[str, t.Any]]]:
"""Collect elements based on the specified direction and attribute name."""
layer_obj, layer = find_layer(start)
collected_elements: dict[LayerLiteral, list[dict[str, t.Any]]] = {
layer: [{"element": start, "origin": origin, "layer": layer_obj}]
}
if (
(direction == "ABOVE" and layer == "Operational")
or (direction == "BELOW" and layer == "Physical")
or depth == 0
):
return collected_elements

if isinstance(start, fa.Function):
attribute_name = f"{attribute_prefix}_functions"
else:
assert isinstance(start, cs.Component)
attribute_name = f"{attribute_prefix}_components"

for element in getattr(start, attribute_name, []):
sub_collected = collect_elements(
element, depth - 1, direction, attribute_prefix, origin=start
)
for sub_layer, sub_elements in sub_collected.items():
collected_elements.setdefault(sub_layer, []).extend(sub_elements)
return collected_elements


LayerLiteral = t.Union[
t.Literal["Operational"],
t.Literal["System"],
t.Literal["Logical"],
t.Literal["Physical"],
]


def find_layer(
obj: common.GenericElement,
) -> tuple[crosslayer.BaseArchitectureLayer, LayerLiteral]:
"""Return the layer object and its literal.
Return either one of the following:
* ``Operational``
* ``System``
* ``Logical``
* ``Physical``
"""
parent = obj
while not isinstance(parent, crosslayer.BaseArchitectureLayer):
parent = parent.parent
if not (match := RE_LAYER_PTRN.match(type(parent).__name__)):
raise ValueError("No layer was found.")
return parent, match.group(1) # type:ignore[return-value]


Collector = cabc.Callable[
[common.GenericElement, int], dict[LayerLiteral, list[dict[str, t.Any]]]
]
COLLECTORS: dict[str, Collector] = {
"ALL": collect_all,
"ABOVE": collect_realized,
"BELOW": collect_realizing,
}
Loading

0 comments on commit fbd908b

Please sign in to comment.