clone

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

main
diagramzip / renderers / python / blockdiag-family / src / blockdiag / elements.py
elements.py 19.4K
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.1516import copy17import os18import re1920from blockdiag import noderenderer, plugins21from blockdiag.utils import XY, images, unquote, urlutil, uuid22from blockdiag.utils.logging import warning232425class Base(object):26 basecolor = (255, 255, 255)27 textcolor = (0, 0, 0)28 fontfamily = None29 fontsize = None30 style = None31 int_attrs = ['colwidth', 'colheight', 'fontsize']3233 @classmethod34 def set_default_color(cls, color):35 cls.basecolor = images.color_to_rgb(color)3637 @classmethod38 def set_default_style(cls, style):39 cls.style = style4041 @classmethod42 def set_default_text_color(cls, color):43 cls.textcolor = images.color_to_rgb(color)4445 @classmethod46 def set_default_fontfamily(cls, fontfamily):47 cls.fontfamily = fontfamily4849 @classmethod50 def set_default_fontsize(cls, fontsize):51 cls.fontsize = int(fontsize)5253 @classmethod54 def clear(cls):55 cls.basecolor = (255, 255, 255)56 cls.textcolor = (0, 0, 0)57 cls.fontfamily = None58 cls.fontsize = None59 cls.style = None6061 def duplicate(self):62 return copy.copy(self)6364 def set_attribute(self, attr):65 name = attr.name66 value = unquote(attr.value)6768 if name == 'class':69 if value in Diagram.classes:70 klass = Diagram.classes[value]71 self.set_attributes(klass.attrs)72 else:73 msg = "Unknown class: %s" % value74 raise AttributeError(msg)75 elif hasattr(self, "set_%s" % name):76 getattr(self, "set_%s" % name)(value)77 elif name in self.int_attrs:78 setattr(self, name, int(value))79 elif hasattr(self, name) and not callable(getattr(self, name)):80 setattr(self, name, value)81 else:82 class_name = self.__class__.__name__83 msg = "Unknown attribute: %s.%s" % (class_name, attr.name)84 raise AttributeError(msg)8586 def set_attributes(self, attrs):87 for attr in attrs:88 self.set_attribute(attr)8990 def set_style(self, value):91 if re.search(r'^(?:none|solid|dotted|dashed|\d+(,\d+)*)$', value, re.I):92 self.style = value.lower()93 else:94 class_name = self.__class__.__name__95 msg = "unknown %s style: %s" % (class_name, value)96 raise AttributeError(msg)979899class Element(Base):100 namespace = {}101 int_attrs = Base.int_attrs + ['width', 'height']102103 @classmethod104 def get(cls, elemid):105 if not elemid:106 elemid = uuid.generate()107108 unquote_id = unquote(elemid)109 if unquote_id not in cls.namespace:110 obj = cls(elemid)111 cls.namespace[unquote_id] = obj112113 return cls.namespace[unquote_id]114115 @classmethod116 def clear(cls):117 super(Element, cls).clear()118 cls.namespace = {}119 cls.basecolor = (255, 255, 255)120 cls.textcolor = (0, 0, 0)121122 def __init__(self, elemid):123 self.id = unquote(elemid)124 self.label = ''125 self.xy = XY(0, 0)126 self.group = None127 self.drawable = False128 self.order = 0129 self.color = self.basecolor130 self.width = None131 self.height = None132 self.colwidth = 1133 self.colheight = 1134 self.stacked = False135136 def __repr__(self):137 _format = "<%s '%s' %s %dx%d at 0x%08x>"138 params = (self.__class__.__name__, self.id, str(self.xy),139 self.colwidth, self.colheight, id(self))140 return _format % params141142 def set_color(self, color):143 self.color = images.color_to_rgb(color)144145 def set_textcolor(self, color):146 self.textcolor = images.color_to_rgb(color)147148149class DiagramNode(Element):150 shape = 'box'151 int_attrs = Element.int_attrs + ['rotate']152 linecolor = (0, 0, 0)153 label_orientation = 'horizontal'154 desctable = []155 attrname = {}156157 @classmethod158 def set_default_shape(cls, shape):159 cls.shape = shape160161 @classmethod162 def set_default_linecolor(cls, color):163 cls.linecolor = images.color_to_rgb(color)164165 @classmethod166 def clear(cls):167 super(DiagramNode, cls).clear()168 cls.shape = 'box'169 cls.linecolor = (0, 0, 0)170 cls.label_orientation = 'horizontal'171 cls.desctable = ['numbered', 'label', 'description']172 cls.attrname = dict(numbered='No', label='Name',173 description='Description')174175 def __init__(self, elemid):176 super(DiagramNode, self).__init__(elemid)177178 self.label = unquote(elemid) or ''179 self.numbered = None180 self.icon = None181 self.background = None182 self.description = None183 self.rotate = 0184 self.drawable = True185 self.href = None186187 plugins.fire_node_event(self, 'created')188189 def set_attribute(self, attr):190 if plugins.fire_node_event(self, 'attr_changing', attr):191 super(DiagramNode, self).set_attribute(attr)192 plugins.fire_node_event(self, 'attr_changed', attr)193194 def set_linecolor(self, color):195 self.linecolor = images.color_to_rgb(color)196197 def set_shape(self, value):198 if noderenderer.get(value):199 self.shape = value200 else:201 msg = "unknown node shape: %s" % value202 raise AttributeError(msg)203204 def set_icon(self, value):205 if urlutil.isurl(value) or os.path.isfile(value):206 self.icon = value207 else:208 warning("icon image not found: %s", value)209210 def set_background(self, value):211 if urlutil.isurl(value) or os.path.isfile(value):212 self.background = value213 else:214 warning("background image not found: %s", value)215216 def set_stacked(self, _):217 self.stacked = True218219 def set_label_orientation(self, value):220 value = value.lower()221 if value in ('horizontal', 'vertical'):222 self.label_orientation = value223 else:224 msg = "unknown label orientation: %s" % value225 raise AttributeError(msg)226227 def to_desctable(self):228 attrs = []229 for name in self.desctable:230 value = getattr(self, name)231 if value is None:232 attrs.append("")233 else:234 attrs.append(value)235236 return attrs237238239class NodeGroup(Element):240 basecolor = (243, 152, 0)241242 @classmethod243 def clear(cls):244 super(NodeGroup, cls).clear()245 cls.basecolor = (243, 152, 0)246247 def __init__(self, elemid):248 super(NodeGroup, self).__init__(elemid)249250 self.level = 0251 self.separated = False252 self.shape = 'box'253 self.thick = 3254 self.nodes = []255 self.edges = []256 self.icon = None257 self.orientation = 'landscape'258 self.href = None259260 def duplicate(self):261 copied = super(NodeGroup, self).duplicate()262 copied.nodes = []263 copied.edges = []264265 return copied266267 def parent(self, level):268 if self.level < level:269 return None270271 group = self272 while group.level != level:273 group = group.group274275 return group276277 def is_parent(self, other):278 parent = self.parent(other.level)279 return parent == other280281 def traverse_nodes(self, preorder=False):282 for node in self.nodes:283 if isinstance(node, NodeGroup):284 if preorder:285 yield node286287 for subnode in node.traverse_nodes(preorder=preorder):288 yield subnode289290 if not preorder:291 yield node292 else:293 yield node294295 def traverse_edges(self, preorder=False):296 if preorder:297 for edge in self.edges:298 yield edge299300 for group in self.traverse_groups(preorder):301 for edge in group.traverse_edges(preorder):302 yield edge303304 if not preorder:305 for edge in self.edges:306 yield edge307308 def traverse_groups(self, preorder=False):309 for node in self.traverse_nodes(preorder=preorder):310 if isinstance(node, NodeGroup):311 yield node312313 def fixiate(self, fixiate_nodes=False):314 if self.separated:315 self.colwidth = 1316 self.colheight = 1317318 return319 elif len(self.nodes) > 0:320 self.colwidth = max(x.xy.x + x.colwidth for x in self.nodes)321 self.colheight = max(x.xy.y + x.colheight for x in self.nodes)322323 for node in self.nodes:324 if fixiate_nodes:325 node.xy = XY(self.xy.x + node.xy.x,326 self.xy.y + node.xy.y)327328 if isinstance(node, NodeGroup):329 node.fixiate(fixiate_nodes)330331 def update_order(self):332 for i, node in enumerate(self.nodes):333 node.order = i334335 def set_orientation(self, value):336 value = value.lower()337 if value in ('landscape', 'portrait'):338 self.orientation = value339 else:340 msg = "unknown diagram orientation: %s" % value341 raise AttributeError(msg)342343 def set_shape(self, value):344 value = value.lower()345 if value in ('box', 'line'):346 self.shape = value347 else:348 msg = "unknown group shape: %s" % value349 raise AttributeError(msg)350351352class DiagramEdge(Base):353 basecolor = (0, 0, 0)354 namespace = {}355356 @classmethod357 def get(cls, node1, node2):358 if node1 not in cls.namespace:359 cls.namespace[node1] = {}360361 if node2 not in cls.namespace[node1]:362 obj = cls(node1, node2)363 cls.namespace[node1][node2] = obj364365 return cls.namespace[node1][node2]366367 @classmethod368 def find(cls, node1, node2=None):369 if node1 is None and node2 is None:370 return cls.find_all()371 elif isinstance(node1, NodeGroup):372 edges = cls.find(None, node2)373 edges = (e for e in edges if e.node1.group.is_parent(node1))374 return [e for e in edges if not e.node2.group.is_parent(node1)]375 elif isinstance(node2, NodeGroup):376 edges = cls.find(node1, None)377 edges = (e for e in edges if e.node2.group.is_parent(node2))378 return [e for e in edges if not e.node1.group.is_parent(node2)]379 elif node1 is None:380 return [e for e in cls.find_all() if e.node2 == node2]381 else:382 if node1 not in cls.namespace:383 return []384385 if node2 is None:386 return cls.namespace[node1].values()387388 if node2 not in cls.namespace[node1]:389 return []390391 return cls.namespace[node1][node2]392393 @classmethod394 def find_all(cls):395 for v1 in cls.namespace.values():396 for v2 in v1.values():397 yield v2398399 @classmethod400 def find_by_level(cls, level):401 edges = []402 for e in cls.find_all():403 edge = e.duplicate()404 skips = 0405406 if edge.node1.group.level < level:407 skips += 1408 else:409 while edge.node1.group.level != level:410 edge.node1 = edge.node1.group411412 if edge.node2.group.level < level:413 skips += 1414 else:415 while edge.node2.group.level != level:416 edge.node2 = edge.node2.group417418 if skips == 2:419 continue420421 edges.append(edge)422423 return edges424425 @classmethod426 def clear(cls):427 super(DiagramEdge, cls).clear()428 cls.namespace = {}429 cls.basecolor = (0, 0, 0)430431 def __init__(self, node1, node2):432 self.node1 = node1433 self.node2 = node2434 self.crosspoints = []435 self.skipped = 0436437 self.label = None438 self.description = None439 self.dir = 'forward'440 self.color = self.basecolor441 self.hstyle = None442 self.folded = None443 self.thick = None444445 def __repr__(self):446 _format = "<%s '%s' %s - '%s' %s at 0x%08x>"447 params = (self.__class__.__name__, self.node1.id, self.node1.xy,448 self.node2.id, self.node2.xy, id(self))449 return _format % params450451 def set_dir(self, value):452 value = value.lower()453 if value in ('back', 'both', 'none', 'forward'):454 self.dir = value455 elif value == '-<':456 self.dir = 'forward'457 self.hstyle = 'onemany'458 elif value == '>-':459 self.dir = 'back'460 self.hstyle = 'manyone'461 elif value == '>-<':462 self.dir = 'both'463 self.hstyle = 'manymany'464 elif value == '->':465 self.dir = 'forward'466 elif value == '<-':467 self.dir = 'back'468 elif value == '<->':469 self.dir = 'both'470 elif value == '--':471 self.dir = 'none'472 else:473 msg = "unknown edge dir: %s" % value474 raise AttributeError(msg)475476 def set_color(self, color):477 self.color = images.color_to_rgb(color)478479 def set_hstyle(self, value):480 value = value.lower()481 if value in ('generalization', 'composition', 'aggregation'):482 self.hstyle = value483 elif value == 'oneone':484 self.dir = 'none'485 self.hstyle = value486 elif value == 'onemany':487 self.dir = 'forward'488 self.hstyle = value489 elif value == 'manyone':490 self.dir = 'back'491 self.hstyle = value492 elif value == 'manymany':493 self.dir = 'both'494 self.hstyle = value495 else:496 msg = "unknown edge hstyle: %s" % value497 raise AttributeError(msg)498499 def set_folded(self, _):500 self.folded = True501502 def set_nofolded(self, _):503 self.folded = False504505 def set_thick(self, _):506 self.thick = 3507508 @property509 def direction(self):510 node1 = self.node1.xy511 node2 = self.node2.xy512513 if node1.x > node2.x:514 if node1.y > node2.y:515 _dir = 'left-up'516 elif node1.y == node2.y:517 _dir = 'left'518 else:519 _dir = 'left-down'520 elif node1.x == node2.x:521 if node1.y > node2.y:522 _dir = 'up'523 elif node1.y == node2.y:524 _dir = 'same'525 else:526 _dir = 'down'527 else:528 if node1.y > node2.y:529 _dir = 'right-up'530 elif node1.y == node2.y:531 _dir = 'right'532 else:533 _dir = 'right-down'534535 return _dir536537 def to_desctable(self):538 label = "%s -> %s" % (self.node1.label, self.node2.label)539 return [label, self.description]540541542class Diagram(NodeGroup):543 _DiagramNode = DiagramNode544 _DiagramEdge = DiagramEdge545 _NodeGroup = NodeGroup546547 classes = {}548 shadow_style = 'blur'549 linecolor = (0, 0, 0)550 int_attrs = (NodeGroup.int_attrs +551 ['node_width', 'node_height', 'span_width', 'span_height'])552553 @classmethod554 def clear(cls):555 super(Diagram, cls).clear()556 cls.shadow_style = 'blur'557 cls.linecolor = (0, 0, 0)558 cls.classes = {}559560 def __init__(self):561 super(Diagram, self).__init__(None)562563 self.config = None564 self.node_width = None565 self.node_height = None566 self.span_width = None567 self.span_height = None568 self.page_padding = None569 self.edge_layout = None570571 def set_plugin(self, name, attrs, **kwargs):572 kwargs.update(dict([str(unquote(attr.name)), unquote(attr.value)]573 for attr in attrs))574 plugins.load([name], diagram=self, **kwargs)575576 def set_plugins(self, value, **kwargs):577 modules = [name.strip() for name in value.split(',')]578 plugins.load(modules, diagram=self, **kwargs)579580 def set_default_shape(self, value):581 if noderenderer.get(value):582 DiagramNode.set_default_shape(value)583 else:584 msg = "unknown node shape: %s" % value585 raise AttributeError(msg)586587 def set_default_label_orientation(self, value):588 value = value.lower()589 if value in ('horizontal', 'vertical'):590 DiagramNode.label_orientation = value591 else:592 msg = "unknown label orientation: %s" % value593 raise AttributeError(msg)594595 def set_default_text_color(self, color):596 warning("default_text_color is obsoleted; use default_textcolor")597 self.set_default_textcolor(color)598599 def set_default_textcolor(self, color):600 self.textcolor = images.color_to_rgb(color)601 self._DiagramNode.set_default_text_color(self.textcolor)602 self._NodeGroup.set_default_text_color(self.textcolor)603 self._DiagramEdge.set_default_text_color(self.textcolor)604605 def set_default_node_color(self, color):606 color = images.color_to_rgb(color)607 self._DiagramNode.set_default_color(color)608609 def set_default_node_style(self, value):610 if re.search(r'^(?:none|solid|dotted|dashed|\d+(,\d+)*)$', value, re.I):611 self._DiagramNode.set_default_style(value)612 else:613 msg = "unknown node style: %s" % value614 raise AttributeError(msg)615616 def set_default_line_color(self, color):617 warning("default_line_color is obsoleted; use default_linecolor")618 self.set_default_linecolor(color)619620 def set_default_linecolor(self, color):621 self.linecolor = images.color_to_rgb(color)622 self._DiagramNode.set_default_linecolor(self.linecolor)623 self._DiagramEdge.set_default_color(self.linecolor)624625 def set_default_group_color(self, color):626 color = images.color_to_rgb(color)627 self._NodeGroup.set_default_color(color)628629 def set_shape_namespace(self, value):630 noderenderer.set_default_namespace(value)631632 def set_default_fontfamily(self, fontfamily):633 self._DiagramNode.set_default_fontfamily(fontfamily)634 self._NodeGroup.set_default_fontfamily(fontfamily)635 self._DiagramEdge.set_default_fontfamily(fontfamily)636637 def set_default_fontsize(self, fontsize):638 self._DiagramNode.set_default_fontsize(fontsize)639 self._NodeGroup.set_default_fontsize(fontsize)640 self._DiagramEdge.set_default_fontsize(fontsize)641642 def set_shadow_style(self, value):643 value = value.lower()644 if value in ('solid', 'blur', 'none'):645 self.shadow_style = value646 else:647 msg = "unknown shadow style: %s" % value648 raise AttributeError(msg)649650 def set_edge_layout(self, value):651 value = value.lower()652 if value in ('normal', 'flowchart'):653 warning("edge_layout is very experimental feature!")654655 self.edge_layout = value656 else:657 msg = "unknown edge layout: %s" % value658 raise AttributeError(msg)659660 def set_fontsize(self, value):661 warning("fontsize is obsoleted; use default_fontsize")662 self.set_default_fontsize(int(value))