clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / python / blockdiag-family / src / actdiag / builder.py
builder.py 12.6K
1# -*- coding: utf-8 -*-2# Copyright 2011 Takeshi KOMIYA3#4# Licensed under the Apache License, Version 2.0 (the "License");5# you may not use this file except in compliance with the License.6# You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing, software11# distributed under the License is distributed on an "AS IS" BASIS,12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13# See the License for the specific language governing permissions and14# limitations under the License.1516from __future__ import print_function1718from blockdiag.utils import XY, unquote19from blockdiag.utils.compat import cmp_to_key2021from actdiag import parser22from actdiag.elements import Diagram, DiagramEdge, DiagramNode, NodeGroup232425class DiagramTreeBuilder(object):26 def build(self, tree):27 self.diagram = Diagram()28 diagram = self.instantiate(self.diagram, tree)2930 self.bind_edges(diagram)3132 if len(self.diagram.lanes) == 0:33 self.diagram.lanes.append(NodeGroup.get(None))3435 for node in self.diagram.nodes:36 if node.lane is None:37 edges = DiagramEdge.find(None, node)38 parents = [e.node1 for e in edges if e.node1.lane]39 parents.sort(key=lambda x: x.order)4041 if parents:42 node.lane = parents[0].lane43 node.lane.nodes.append(node)44 else:45 node.lane = self.diagram.lanes[0]46 node.lane.nodes.append(node)4748 for lane in diagram.lanes:49 if len(lane.nodes) == 0:50 diagram.lanes.remove(lane)5152 return diagram5354 def belong_to(self, node, lane):55 if lane and node.lane and node.lane != lane:56 print(node, node.lane, lane)57 msg = "DiagramNode could not belong to two lanes"58 raise RuntimeError(msg)5960 node.group = self.diagram61 if lane:62 node.lane = lane63 lane.nodes.append(node)6465 if node not in self.diagram.nodes:66 self.diagram.nodes.append(node)6768 def instantiate(self, group, tree, lane=None):69 for stmt in tree.stmts:70 if isinstance(stmt, parser.Node):71 node = DiagramNode.get(stmt.id)72 node.set_attributes(stmt.attrs)73 self.belong_to(node, lane)7475 elif isinstance(stmt, parser.Edge):76 from_nodes = [DiagramNode.get(n) for n in stmt.from_nodes]77 to_nodes = [DiagramNode.get(n) for n in stmt.to_nodes]7879 for node in from_nodes + to_nodes:80 self.belong_to(node, lane)8182 for node1 in from_nodes:83 for node2 in to_nodes:84 edge = DiagramEdge.get(node1, node2)85 edge.set_dir(stmt.edge_type)86 edge.set_attributes(stmt.attrs)8788 elif isinstance(stmt, parser.Lane):89 _lane = NodeGroup.get(stmt.id)90 if _lane not in self.diagram.lanes:91 self.diagram.lanes.append(_lane)9293 self.instantiate(group, stmt, _lane)9495 elif isinstance(stmt, parser.Attr):96 if lane:97 lane.set_attribute(stmt)98 else:99 self.diagram.set_attribute(stmt)100101 elif isinstance(stmt, parser.Extension):102 if stmt.type == 'class':103 name = unquote(stmt.name)104 Diagram.classes[name] = stmt105 if stmt.type == 'plugin':106 self.diagram.set_plugin(stmt.name, stmt.attrs)107108 elif isinstance(stmt, parser.Statements):109 self.instantiate(group, stmt, lane)110111 group.update_order()112 return group113114 def bind_edges(self, group):115 for node in group.nodes:116 if isinstance(node, DiagramNode):117 group.edges += DiagramEdge.find(node)118 else:119 self.bind_edges(node)120121122class DiagramLayoutManager:123 def __init__(self, diagram):124 self.diagram = diagram125126 self.circulars = []127 self.heightRefs = []128129 def run(self):130 self.edges = [e for e in DiagramEdge.find_all()]131 self.do_layout()132 self.diagram.fixiate()133 self.fixiate_lanes()134135 def fixiate_lanes(self):136 height = 0137 for lane in self.diagram.lanes:138 if self.coordinates[lane]:139 for node in self.diagram.nodes:140 if node.lane == lane:141 node.xy = XY(node.xy.x, node.xy.y + height)142143 height += max(xy.y for xy in self.coordinates[lane]) + 1144145 nodes = [n for n in self.diagram.nodes if n.lane == lane]146 x = min(n.xy.x for n in nodes)147 y = min(n.xy.y for n in nodes)148 lane.xy = XY(x, y)149 lane.colwidth = max(n.xy.x + n.colwidth for n in nodes) - x150 lane.colheight = max(n.xy.y + n.colheight for n in nodes) - y151152 def do_layout(self):153 self.detect_circulars()154155 self.set_node_width()156 self.adjust_node_order()157158 height = 0159 self.initialize_markers()160 for node in self.diagram.traverse_nodes():161 if node.xy.x == 0:162 lane = node.lane163 if self.coordinates[lane]:164 height = max(xy.y for xy in self.coordinates[lane]) + 1165 else:166 height = 0167 self.set_node_height(node, height)168169 def get_related_nodes(self, node, parent=False, child=False):170 uniq = {}171 for edge in self.edges:172 if edge.folded:173 continue174175 if parent and edge.node2 == node:176 uniq[edge.node1] = 1177 elif child and edge.node1 == node:178 uniq[edge.node2] = 1179180 related = []181 for uniq_node in uniq.keys():182 if uniq_node == node:183 pass184 else:185 related.append(uniq_node)186187 related.sort(key=lambda x: x.order)188 return related189190 def get_parent_nodes(self, node):191 return self.get_related_nodes(node, parent=True)192193 def get_child_nodes(self, node):194 return self.get_related_nodes(node, child=True)195196 def detect_circulars(self):197 for node in self.diagram.nodes:198 if not [x for x in self.circulars if node in x]:199 self.detect_circulars_sub(node, [node])200201 # remove part of other circular202 for c1 in self.circulars:203 for c2 in self.circulars:204 intersect = set(c1) & set(c2)205206 if c1 != c2 and set(c1) == intersect:207 self.circulars.remove(c1)208 break209210 def detect_circulars_sub(self, node, parents):211 for child in self.get_child_nodes(node):212 if child in parents:213 i = parents.index(child)214 self.circulars.append(parents[i:])215 else:216 self.detect_circulars_sub(child, parents + [child])217218 def is_circular_ref(self, node1, node2):219 for circular in self.circulars:220 if node1 in circular and node2 in circular:221 parents = []222 for node in circular:223 for parent in self.get_parent_nodes(node):224 if parent not in circular:225 parents.append(parent)226227 parents.sort(key=lambda x: x.order)228229 for parent in parents:230 children = self.get_child_nodes(parent)231 if node1 in children and node2 in children:232 if circular.index(node1) > circular.index(node2):233 return True234 elif node2 in children:235 return True236 elif node1 in children:237 return False238 else:239 if circular.index(node1) > circular.index(node2):240 return True241242 return False243244 def set_node_width(self, depth=0):245 for node in self.diagram.traverse_nodes():246 if node.xy.x != depth:247 continue248249 for child in self.get_child_nodes(node):250 if self.is_circular_ref(node, child):251 pass252 elif node == child:253 pass254 elif child.xy.x > node.xy.x + node.colwidth:255 pass256 else:257 child.xy = XY(node.xy.x + node.colwidth, 0)258259 nodes_iter = self.diagram.traverse_nodes()260 depther_node = [x for x in nodes_iter if x.xy.x > depth]261 if len(depther_node) > 0:262 self.set_node_width(depth + 1)263264 def adjust_node_order(self):265 for node in self.diagram.traverse_nodes():266 parents = self.get_parent_nodes(node)267 if len(set(parents)) > 1:268 for i in range(1, len(parents)):269 idx1 = self.diagram.nodes.index(parents[i - 1])270 idx2 = self.diagram.nodes.index(parents[i])271 if idx1 < idx2:272 self.diagram.nodes.remove(parents[i])273 self.diagram.nodes.insert(idx1 + 1, parents[i])274 else:275 self.diagram.nodes.remove(parents[i - 1])276 self.diagram.nodes.insert(idx2 + 1, parents[i - 1])277278 if isinstance(node, NodeGroup):279 nodes = [n for n in node.nodes if n in self.diagram.nodes]280 if nodes:281 idx = min(self.diagram.nodes.index(n) for n in nodes)282 if idx < self.diagram.nodes.index(node):283 self.diagram.nodes.remove(node)284 self.diagram.nodes.insert(idx + 1, node)285286 self.diagram.update_order()287288 def initialize_markers(self):289 self.coordinates = {}290 for lane in self.diagram.lanes:291 self.coordinates[lane] = []292293 def mark_xy(self, node):294 xy = node.xy295 for w in range(node.colwidth):296 for h in range(node.colheight):297 self.coordinates[node.lane].append(XY(xy.x + w, xy.y + h))298299 def is_marked(self, lane, xy):300 return xy in self.coordinates[lane]301302 def set_node_height(self, node, height=0):303 xy = XY(node.xy.x, height)304 if self.is_marked(node.lane, xy):305 return False306 node.xy = xy307 self.mark_xy(node)308309 def cmp(x, y):310 if x.xy.x < y.xy.y:311 return -1312 elif x.xy.x == y.xy.y:313 return 0314 else:315 return 1316317 count = 0318 children = self.get_child_nodes(node)319 children.sort(key=cmp_to_key(cmp))320 for child in children:321 if child.id in self.heightRefs:322 pass323 elif node is not None and node.xy.x >= child.xy.x:324 pass325 else:326 if node.lane == child.lane:327 h = height328 else:329 h = 0330331 while True:332 if self.set_node_height(child, h):333 child.xy = XY(child.xy.x, h)334 self.mark_xy(child)335 self.heightRefs.append(child.id)336337 count += 1338 break339 elif node.lane != child.lane:340 h += 1341 else:342 if count == 0:343 return False344345 h += 1346347 if node.lane == child.lane:348 height = h + 1349350 return True351352353class ScreenNodeBuilder(object):354 @classmethod355 def build(cls, tree, separate=False):356 DiagramNode.clear()357 DiagramEdge.clear()358 NodeGroup.clear()359360 diagram = DiagramTreeBuilder().build(tree)361 DiagramLayoutManager(diagram).run()362 diagram.fixiate(True)363364 if diagram.orientation == 'portrait':365 cls.rotate_diagram(diagram)366367 return diagram368369 @classmethod370 def rotate_diagram(cls, diagram):371 for node in diagram.traverse_nodes():372 node.xy = XY(node.xy.y, node.xy.x)373 node.colwidth, node.colheight = (node.colheight, node.colwidth)374375 for lane in diagram.lanes:376 lane.xy = XY(lane.xy.y, lane.xy.x)377 lane.colwidth, lane.colheight = (lane.colheight, lane.colwidth)378379 size = (diagram.colheight, diagram.colwidth)380 diagram.colwidth, diagram.colheight = size