Skip to content

Commit

Permalink
Merge branch 'main' into remove-unstyled-ports
Browse files Browse the repository at this point in the history
  • Loading branch information
ewuerger authored Oct 20, 2023
2 parents 6dddcf1 + ff7fca8 commit ef85787
Show file tree
Hide file tree
Showing 15 changed files with 611 additions and 30 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
exclude: '^(versioneer\.py|.*/_version\.py)$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-ast
Expand All @@ -24,16 +24,16 @@ repos:
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.3.0
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
rev: v1.6.1
hooks:
- id: mypy
- repo: https://github.com/Lucas-C/pre-commit-hooks
Expand Down Expand Up @@ -67,6 +67,6 @@ repos:
- --comment-style
- '/*| *| */'
- repo: https://github.com/fsfe/reuse-tool
rev: v1.1.2
rev: v2.1.0
hooks:
- id: reuse
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Example:
>
> model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
> diag = model.by_uuid("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6").context_diagram
> diag.render("svgdiagram").save_drawing(True)
> diag.render("svgdiagram").save_drawing(pretty=True)
> ```
> <figure markdown>
> <img src="assets/images/Context of Top secret.svg" width="1000000">
Expand Down
12 changes: 11 additions & 1 deletion capellambse_context_diagrams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from capellambse.diagram import COLORS, CSSdef, capstyle
from capellambse.model import common
from capellambse.model.crosslayer import fa
from capellambse.model.crosslayer import fa, information
from capellambse.model.layers import ctx, la, oa, pa
from capellambse.model.modeltypes import DiagramType

Expand All @@ -47,6 +47,7 @@ def init() -> None:
"""Initialize the extension."""
register_classes()
register_interface_context()
register_class_tree()
# register_functional_context() XXX: Future


Expand Down Expand Up @@ -148,3 +149,12 @@ def register_functional_context() -> None:
attr_name,
context.FunctionalContextAccessor(dgcls.value),
)


def register_class_tree() -> None:
"""Add the `tree_diagram` attribute to ``Class``es."""
common.set_accessor(
information.Class,
"tree_diagram",
context.ClassTreeAccessor(DiagramType.CDB.value),
)
85 changes: 85 additions & 0 deletions capellambse_context_diagrams/collectors/class_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 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 Class-Tree diagram."""
from __future__ import annotations

import collections.abc as cabc
import typing as t

from capellambse import helpers
from capellambse.model.crosslayer import information

from .. import _elkjs, context
from . import generic, makers


def collector(
diagram: context.ContextDiagram, params: dict[str, t.Any]
) -> _elkjs.ELKInputData:
"""Return the class tree data for ELK."""
assert isinstance(diagram.target, information.Class)
data = generic.collector(diagram, no_symbol=True)
if params.get("partitioning", False):
data["layoutOptions"]["partitioning.activate"] = True
data["children"][0]["layoutOptions"] = {}
data["children"][0]["layoutOptions"]["elk.partitioning.partition"] = 0

data["layoutOptions"]["edgeLabels.sideSelection"] = params.get(
"edgeLabelsSide", "ALWAYS_DOWN"
)
data["layoutOptions"]["algorithm"] = params.get("algorithm", "layered")
data["layoutOptions"]["elk.direction"] = params.get("direction", "DOWN")
data["layoutOptions"]["edgeRouting"] = params.get(
"edgeRouting", "ORTHOGONAL"
)

made_boxes: set[str] = set()
for _, (source, prop, target, partition) in get_all_classes(
diagram.target
):
if target.uuid not in made_boxes:
made_boxes.add(target.uuid)
box = makers.make_box(target)
if params.get("partitioning", False):
box["layoutOptions"] = {}
box["layoutOptions"]["elk.partitioning.partition"] = int(
partition
)
data["children"].append(box)

width, height = helpers.extent_func(prop.name)
label: _elkjs.ELKInputLabel = {
"text": prop.name,
"width": width + 2 * makers.LABEL_HPAD,
"height": height + 2 * makers.LABEL_VPAD,
}
data["edges"].append(
{
"id": prop.uuid,
"sources": [source.uuid],
"targets": [target.uuid],
"labels": [label],
}
)
return data


ClassContext = tuple[
information.Class, information.Property, information.Class, int
]


def get_all_classes(
root: information.Class, partition: int = 0
) -> cabc.Iterator[tuple[str, ClassContext]]:
"""Yield all classes of the class tree."""
partition += 1
classes: dict[str, ClassContext] = {}
for prop in root.properties:
if prop.type.xtype.endswith("Class"):
edge_id = f"{root.uuid} {prop.uuid} {prop.type.uuid}"
if edge_id not in classes:
classes[edge_id] = (root, prop, prop.type, partition)
classes.update(dict(get_all_classes(prop.type, partition)))

yield from classes.items()
51 changes: 50 additions & 1 deletion capellambse_context_diagrams/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from capellambse.model import common, diagram, modeltypes

from . import _elkjs, filters, serializers, styling
from .collectors import exchanges, get_elkdata
from .collectors import class_tree, exchanges, get_elkdata

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -129,6 +129,26 @@ def __get__( # type: ignore
)


class ClassTreeAccessor(ContextAccessor):
"""Provides access to the class tree diagrams."""

# pylint: disable=super-init-not-called
def __init__(self, diagclass: str) -> None:
self._dgcls = diagclass

def __get__( # type: ignore
self,
obj: common.T | None,
objtype: type | None = None,
) -> common.Accessor | ContextDiagram:
"""Make a ContextDiagram for the given model object."""
del objtype
if obj is None: # pragma: no cover
return self
assert isinstance(obj, common.GenericElement)
return self._get(obj, ClassTreeDiagram, "{}_class_tree")


class ContextDiagram(diagram.AbstractDiagram):
"""An automatically generated context diagram.
Expand Down Expand Up @@ -305,3 +325,32 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
self, exchanges.FunctionalContextCollector, params
)
return super()._create_diagram(params)


class ClassTreeDiagram(ContextDiagram):
"""An automatically generated ClassTree Diagram.
This diagram is exclusively for ``Class``es.
"""

def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)

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

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

def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
params.setdefault("algorithm", "layered")
params.setdefault("direction", "DOWN")
params.setdefault("edgeRouting", "POLYLINE")
params.setdefault("partitioning", True)
params.setdefault("edgeLabelsSide", "SMART_DOWN")
params["elkdata"] = class_tree.collector(self, params)
return super()._create_diagram(params)
5 changes: 4 additions & 1 deletion capellambse_context_diagrams/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
* `junction`.
"""

REMAP_STYLECLASS: dict[str, str] = {"Unset": "Association"}


class DiagramSerializer:
"""Serialize an ``elk_diagram`` into an
Expand All @@ -47,7 +49,7 @@ class DiagramSerializer:
def __init__(self, elk_diagram: context.ContextDiagram) -> None:
self.model = elk_diagram.target._model
self._diagram = elk_diagram
self._cache: dict[str, diagram.Box] = {}
self._cache: dict[str, diagram.Box | diagram.Edge] = {}

def make_diagram(self, data: _elkjs.ELKOutputData) -> diagram.Diagram:
"""Transform a layouted diagram into an `diagram.Diagram`.
Expand Down Expand Up @@ -136,6 +138,7 @@ class type that stores all previously named classes.
self.diagram.add_element(element)
self._cache[child["id"]] = element
elif child["type"] == "edge":
styleclass = REMAP_STYLECLASS.get(styleclass, styleclass) # type: ignore[arg-type]
element = diagram.Edge(
[
ref + (point["x"], point["y"])
Expand Down
83 changes: 83 additions & 0 deletions docs/class-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!--
~ SPDX-FileCopyrightText: 2022 Copyright DB Netz AG and the capellambse-context-diagrams contributors
~ SPDX-License-Identifier: Apache-2.0
-->

# Class Tree Diagram

With release [`v0.5.35`](https://github.com/DSD-DBS/py-capellambse/releases/tag/v0.5.35) of [py-capellambse](https://github.com/DSD-DBS/py-capellambse) you can access the
`.tree_diagram` on [`Class`][capellambse.model.crosslayer.information.Class]
objects. A class tree diagram shows a tree made from all properties of the
parent class.

??? example "Class Tree of Root"

``` py
import capellambse

model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").tree_diagram
diag.render("svgdiagram").save_drawing(pretty=True)
```
<figure markdown>
<img src="../assets/images/Class Tree of Root.svg">
<figcaption>[CDB] Class Tree Diagram of Root</figcaption>
</figure>

Additional rendering parameters enable the control over the layout computed by
ELK. The available options are:

1. edgeRouting - Controls the style of the edges.
- POLYLINE (default)
- ORTHOGONAL
- SPLINE
2. algorithm - Controls the algorithm for the diagram layout.
- layered (default)
- mr.tree
- ... Have a look for [all available ELK algorithms](https://eclipse.dev/elk/reference/algorithms.html).
3. direction - The flow direction for the ELK Layered algortihm.
- DOWN (DEFAULT)
- UP
- RIGHT
- LEFT
4. partitioning - Enable partitioning. Each recursion level for collecting the
classes is its own partition.
- True (default)
- False
5. edgeLabelSide - Controls edge label placement.
- SMART_DOWN (default)
- SMART_UP
- ALWAYS_UP
- ALWAYS_DOWN
- DIRECTION_UP
- DIRECTION_DOWN

Here is an example that shows how convenient these parameters can be passed
before rendering:

??? example "Class Tree of Root"

``` py
import capellambse

model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").tree_diagram
diag.render(
"svgdiagram",
edgeRouting="ORTHOGONAL",
direction="Right",
# partitioning=False,
# edgeLabelsSide="ALWAYS_DOWN",
).save_drawing(pretty=True)
```
<figure markdown>
<img src="../assets/images/Class Tree of Root-params.svg">
<figcaption>[CDB] Class Tree Diagram of Root</figcaption>
</figure>

They are optional and don't need to be set all together.

## Check out the code

To understand the collection have a look into the
[`class_tree`][capellambse_context_diagrams.collectors.class_tree] module.
6 changes: 3 additions & 3 deletions docs/extras/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Currently the supported filters are:
diag = obj.context_diagram
assert filters.EX_ITEMS == "show.exchange.items.filter"
diag.filters.add(filters.EX_ITEMS)
diag.render("svgdiagram").save_drawing(True)
diag.render("svgdiagram").save_drawing(pretty=True)
```
<figure markdown>
<img src="../../assets/images/Context of Lost ex.svg" width="1000000">
Expand All @@ -43,7 +43,7 @@ Currently the supported filters are:
diag = obj.context_diagram
assert filters.FEX_EX_ITEMS == "show.functional.exchanges.exchange.items.filter"
filters.filters = {filters.FEX_EX_ITEMS}
diag.render("svgdiagram").save_drawing(True)
diag.render("svgdiagram").save_drawing(pretty=True)
```
<figure markdown>
<img src="../../assets/images/Context of Lost fex and ex.svg" width="1000000">
Expand All @@ -62,7 +62,7 @@ Currently the supported filters are:
diag = obj.context_diagram
assert filters.FEX_OR_EX_ITEMS == "capellambse_context_diagrams-show.functional.exchanges.or.exchange.items.filter"
filters.filters.add(filters.FEX_OR_EX_ITEMS)
diag.render("svgdiagram").save_drawing(True)
diag.render("svgdiagram").save_drawing(pretty=True)
```
<figure markdown>
<img src="../../assets/images/Context of Lost fex or ex.svg" width="1000000">
Expand Down
Loading

0 comments on commit ef85787

Please sign in to comment.