Skip to content

Commit

Permalink
added graphviz layout engine
Browse files Browse the repository at this point in the history
  • Loading branch information
christophevg committed Jan 24, 2024
1 parent f965e6d commit 393677b
Show file tree
Hide file tree
Showing 16 changed files with 786 additions and 191 deletions.
1 change: 1 addition & 0 deletions .pypi-template
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ requires:
- fire
- xmltodict
- colorama
- graphviz
scripts: []
skip:
- docs
Expand Down
2 changes: 1 addition & 1 deletion bpmn_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
BPMN Tools is a collection of tools to work with BPMN ;-)
"""

__version__ = "0.9.4"
__version__ = "0.10.0"
216 changes: 30 additions & 186 deletions bpmn_tools/diagrams.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from . import xml

from bpmn_tools.collaboration import Participant
from bpmn_tools.flow import Element, Flow, MessageFlow, Gateway
from bpmn_tools.flow import Element, Flow, MessageFlow
from bpmn_tools.flow import Annotation, Association

from bpmn_tools.layout import routing

class Bounds(xml.Element):
__tag__ = "dc:Bounds"

Expand Down Expand Up @@ -95,20 +97,13 @@ def children(self):
]
return children

class WayPoint(xml.Element):
__tag__ = "di:waypoint"

def __init__(self, x=0, y=0):
super().__init__()
self["x"] = str(int(x))
self["y"] = str(int(y))

class Edge(xml.Element):
__tag__ = "bpmndi:BPMNEdge"

def __init__(self, flow=None, id=None):
def __init__(self, flow=None, id=None, strategy=None):
super().__init__()
self._flow = flow
self.strategy = routing.Default() if strategy is None else strategy

@property
def flow(self):
Expand Down Expand Up @@ -136,186 +131,28 @@ def attributes(self):
@property
def children(self):
children = super().children.copy()

if not self.flow or not self.flow.source or not self.flow.target:
return children
if isinstance(self.flow.source, Gateway) or isinstance(self.flow.target, Gateway):
children = self._route_gateway()
else:
children = self._route_default()

children = self.strategy.route(self.flow)

if self.flow["name"]:
x = self.flow.target.x
y = self.flow.target.y + self.flow.target.height/2
children.append(Label(x, y))
return children

def _route_default(self):
"""
Default routing routes directly from the right/middel exit of the source
to the left/middle of the target.
Unless the target lies completely above or below the source, then routing
starts from the bottom/top to the top/bottom.
T (above)
0,0 ---> ^
| X S --> T +---+
| Y |
v S
"""
# determine position of target vs source
below = self.flow.target.y > (self.flow.source.y + self.flow.source.height)
above = (self.flow.target.y + self.flow.target.height) < self.flow.source.y

if not (above or below):
children = [
WayPoint(
x=self.flow.source.x + self.flow.source.width,
y=self.flow.source.y + int(self.flow.source.height/2)
),
WayPoint(
x=self.flow.target.x,
y=self.flow.target.y + int(self.flow.target.height/2)
)
]
else:
if below:
top = self.flow.source
bottom = self.flow.target
reverse = False
else:
top = self.flow.target
bottom = self.flow.source
reverse = True
children = [
WayPoint( # bottom middle exit
x=top.x + int(top.width/2),
y=top.y + top.height
),
WayPoint(
x=top.x + int(top.width/2), # straight down to 17px
y=bottom.y - 17
),
WayPoint( # left/right to bottom
x=bottom.x + int(bottom.width/2),
y=bottom.y - 17
),
WayPoint(
x=bottom.x + int(bottom.width/2), # top middle entry
y=bottom.y
)
]
if reverse:
children.reverse()
return children

def _route_gateway(self):
"""
Gateway routing routes directly from the right/middel exit of the source
to the left/middle of the target.
Unless the target's middle lies above or below the gateway, then routing
starts from the bottom/top to the left/middle.
0,0 --->
| X G --> T +---> T
| Y |
v G
"""
# determine position of target vs source
below = self.flow.target.y > (self.flow.source.y + int(self.flow.source.height/2))
above = (self.flow.target.y + int(self.flow.target.height/2)) < self.flow.source.y
if not (above or below):
children = [
WayPoint(
x=self.flow.source.x + self.flow.source.width,
y=self.flow.source.y + int(self.flow.source.height/2)
),
WayPoint(
x=self.flow.target.x,
y=self.flow.target.y + int(self.flow.target.height/2)
)
]
else:
if below:
top = self.flow.source
bottom = self.flow.target
if isinstance(top, Gateway): # starting from GW?
children = [
WayPoint( # bottom middle exit
x=top.x + int(top.width/2),
y=top.y + top.height
),
WayPoint(
x=top.x + int(top.width/2), # straight down middle
y=bottom.y + int(bottom.height/2)
),
WayPoint( # left/right to left
x=bottom.x,
y=bottom.y + int(bottom.height/2)
)
]
else:
children = [
WayPoint( # middle right exit
x=top.x + top.width,
y=top.y + int(top.height/2)
),
WayPoint(
x=bottom.x + int(bottom.width/2), # right to middle
y=top.y + int(top.height/2)
),
WayPoint( # down to top
x=bottom.x + int(bottom.width/2),
y=bottom.y
)
]
else: # target is above
top = self.flow.target
bottom = self.flow.source
# gateways route from top/bottom to side of tasks, but
# from side to top/bottom of other gws
if isinstance(bottom, Gateway) and not isinstance(top, Gateway):
children = [
WayPoint( # top middle exit
x=bottom.x + int(bottom.width/2),
y=bottom.y
),
WayPoint(
x=bottom.x + int(bottom.width/2), # straight up to middle
y=top.y + int(top.height/2)
),
WayPoint( # left/right to left
x=top.x,
y=top.y + int(top.height/2)
)
]
else:
children = [
WayPoint( # middle right exit
x=bottom.x + bottom.width,
y=bottom.y + int(bottom.height/2)
),
WayPoint( # right to middle
x=top.x + int(top.width/2),
y=bottom.y + int(bottom.height/2)
),
WayPoint( # up to bottom
x=top.x + int(top.width/2),
y=top.y + top.height
)
]
return children

class Plane(xml.Element):
__tag__ = "bpmndi:BPMNPlane"

def __init__(self, id="plane", element=None):
def __init__(self, id="plane", element=None, strategy=None):
super().__init__()
self._element = element
self["id"] = id
self._shapes = []
self._edges = []
self["id"] = id
self._shapes = []
self._edges = []
self.strategy = strategy

@property
def element(self):
Expand Down Expand Up @@ -360,28 +197,35 @@ def children(self):
for element in participant.process.children_oftype(Annotation):
children.append(Shape(element))
for element in participant.process.children_oftype(Association):
children.append(Edge(element))
children.append(self.edge(element))
for flow in participant.process.children_oftype(Flow):
children.append(Edge(flow))
children.append(self.edge(flow))
for flow in self.element.children_oftype(MessageFlow):
children.append(Edge(flow))
children.append(self.edge(flow))
return sorted(children, key=lambda k: k.id)

def edge(self, flow):
return Edge(flow, strategy=self.strategy)

class Diagram(xml.Element):
__tag__ = "bpmndi:BPMNDiagram"

def __init__(self, id="diagram", plane=None):
super().__init__()
self._plane = plane
self["id"] = id
self._plane = plane
self["id"] = id
self.strategy = routing.Default()

@property
def plane(self):
if self._plane:
return self._plane
if self._children:
return self._children[0]
return Plane()
plane = self._plane
elif self._children:
plane = self._children[0]
else:
plane = Plane()
plane.strategy = self.strategy
return plane

@property
def children(self):
Expand Down
1 change: 1 addition & 0 deletions bpmn_tools/layout/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = [ "simple", "graphviz" ]
Loading

0 comments on commit 393677b

Please sign in to comment.