clone

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

main
diagramzip / renderers / python / blockdiag-family / src / seqdiag / metrics.py
metrics.py 18.7K
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 division1718from collections import defaultdict, namedtuple1920import blockdiag.metrics21from blockdiag.utils import XY, Box22from blockdiag.utils.logging import warning2324from seqdiag import elements252627class DiagramMetrics(blockdiag.metrics.DiagramMetrics):28 edge_height = 102930 def __init__(self, diagram, **kwargs):31 super(DiagramMetrics, self).__init__(diagram, **kwargs)3233 self.node_count = len(diagram.nodes)34 self.edges = diagram.edges35 self.separators = diagram.separators36 self.page_padding = [0, 0, self.cellsize * 3, 0]3738 if diagram.edge_length:39 span_width = diagram.edge_length - self.node_width40 if span_width < 0:41 warning("edge_length is too short: %d", diagram.edge_length)42 span_width = 04344 self.spreadsheet.set_span_width(0, self.span_width)45 self.spreadsheet.set_span_width(self.node_count, self.span_width)46 self.span_width = span_width4748 for edge in diagram.edges:49 edge.textwidth, edge.textheight = self.edge_textsize(edge)5051 height = self.edge_height + edge.textheight52 if edge.diagonal:53 height += self.node_height * 3 // 454 elif edge.direction == 'self':55 height += self.cellsize * 25657 font = self.font_for(edge)58 if edge.leftnote:59 edge.leftnotesize = self.textsize(edge.leftnote, font=font)60 if height < edge.leftnotesize.height:61 height = edge.leftnotesize.height6263 if edge.rightnote:64 edge.rightnotesize = self.textsize(edge.rightnote, font=font)65 if height < edge.rightnotesize.height:66 height = edge.rightnotesize.height6768 self.spreadsheet.set_node_height(edge.order + 1, height)69 self.expand_pagesize_for_note(edge)7071 span_width = defaultdict(int)72 span_height = defaultdict(int)73 for block in diagram.altblocks:74 x1, y1 = block.xy75 x2 = x1 + block.colwidth76 y2 = y1 + block.colheight7778 for y in range(y1, y2):79 span_width[(x1, y)] += 180 span_width[(x2, y)] += 18182 for x in range(x1, x2):83 if block.type != 'else':84 span_height[(x, y1)] += 185 span_height[(x, y2)] += 18687 for x in range(self.node_count + 1):88 widths = [span_width[xy] for xy in span_width if xy[0] == x]89 if widths:90 width = self.span_width + max(widths) * self.cellsize91 self.spreadsheet.set_span_width(x, width)9293 for y in range(0, len(self.edges) + 1):94 blocks = [b for b in diagram.altblocks if b.edges[0].order == y]95 span_height = self.spreadsheet.span_height[y]96 span_height = 09798 if blocks:99 max_ylevel_top = max(b.ylevel_top for b in blocks)100 span_height = (self.spreadsheet.span_height[y + 1] +101 self.cellsize * 5 // 2 * (max_ylevel_top - 1) +102 self.cellsize)103 self.spreadsheet.set_span_height(y + 1, span_height)104105 blocks = [b for b in diagram.altblocks if b.edges[-1].order == y]106 if blocks:107 max_ylevel_bottom = max(b.ylevel_bottom for b in blocks)108 span_height = (self.spreadsheet.span_height[y + 2] +109 self.cellsize // 2 * (max_ylevel_bottom - 1))110111 self.spreadsheet.set_span_height(y + 2, span_height)112113 def pagesize(self, width=None, height=None):114 width = self.node_count115 height = len(self.edges) + len(self.separators) + 1116 return super(DiagramMetrics, self).pagesize(width, height)117118 @property119 def bottomheight(self):120 height = len(self.edges) + len(self.separators)121 dummy = elements.DiagramNode(None)122 dummy.xy = XY(1, height)123 _, y = self.spreadsheet._node_bottomright(dummy, use_padding=False)124 y += self.spreadsheet.span_height[len(self.edges) + 1] // 2125 return y126127 @property128 def edge_length(self):129 return self.node_width + self.span_width130131 def lifeline(self, node):132 delayed = []133 for sep in self.separators:134 if sep.type == 'delay':135 delayed.append(sep)136137 lines = []138 d = self.cellsize139 pt = self.node(node).bottom140 for sep in delayed:141 m = self.cell(sep)142 y1 = m.top.y143 y2 = m.bottom.y144 lines.append(((pt, XY(pt.x, y1)), '8,4'))145 lines.append(((XY(pt.x, y1 + d), XY(pt.x, y2 - d)), '2,8'))146147 pt = XY(pt.x, y2)148149 y = self.bottomheight + self.cellsize * 4150 lines.append(((pt, XY(pt.x, y)), '8,4'))151152 return lines153154 def activity_box(self, node, activity):155 edges = dict([e.order, e] for e in self.edges + self.separators)156157 # y coodinates for top of activity box158 starts = activity['lifetime'][0]159 edge = edges[starts]160 y1 = self.edge(edge).baseheight161 if isinstance(edge, elements.DiagramEdge):162 if edge.diagonal and edge.node2 == node:163 y1 += self.edge_height * 3 // 4164165 # y coodinates for bottom of activity box166 ends = activity['lifetime'][-1] + 1167 if ends < len(edges):168 y2 = self.edge(edges[ends]).baseheight169 else:170 y2 = self.bottomheight + self.cellsize * 2171172 index = activity['level']173 base_x = self.cell(node).bottom.x174 box = Box(base_x + (index - 1) * self.cellsize // 2, y1,175 base_x + (index + 1) * self.cellsize // 2, y2)176177 return box178179 def activity_shadow(self, node, activity):180 box = self.activity_box(node, activity)181182 return Box(box[0] + self.shadow_offset.x,183 box[1] + self.shadow_offset.y,184 box[2] + self.shadow_offset.x,185 box[3] + self.shadow_offset.y)186187 def edge_textsize(self, edge):188 width = 0189 height = 0190 if edge.label:191 if edge.direction == 'self':192 cell = self.cell(edge.node1)193 width = self.edge(edge).right - cell.center.x194 else:195 width = (self.cell(edge.right_node).center.x -196 self.cell(edge.left_node).center.x -197 edge.label_margin * 2 -198 self.cellsize * 4) # 4: width of activity and padding199 width, height = self.textsize(edge.label, width=width,200 font=self.font_for(edge))201202 return XY(width, height)203204 def expand_pagesize_for_note(self, edge):205 if edge.leftnote:206 cell = self.cell(edge.left_node)207 width = cell.center.x - self.cellsize * 6208209 if width < edge.leftnotesize.width:210 span_width = edge.leftnotesize.width - width211 self.spreadsheet.span_width[0] += span_width212213 if edge.rightnote:214 cell = self.cell(edge.right_node)215 if edge.direction == 'self':216 right = self.edge(edge).right217 width = self.pagesize().width - right - self.cellsize * 3218 else:219 width = (self.pagesize().width -220 cell.center.x - self.cellsize * 3)221222 if edge.right_node.xy.x + 1 == self.node_count:223 width -= self.cellsize * 2224225 if width < edge.rightnotesize.width:226 span_width = edge.rightnotesize.width - width227 self.spreadsheet.span_width[self.node_count] += span_width228229 def cell(self, obj, use_padding=True):230 if isinstance(obj, (elements.DiagramEdge, elements.EdgeSeparator)):231 klass = namedtuple('_', 'xy width height colwidth colheight')232 edge = klass(XY(1, obj.order + 1), None, None, 1, 1)233 return super(DiagramMetrics, self).cell(edge, use_padding=False)234 elif isinstance(obj, elements.AltBlock):235 box = super(DiagramMetrics, self).cell(obj, use_padding=False)236 return AltBlockMetrics(self, obj, box)237 else:238 return super(DiagramMetrics, self).cell(obj, use_padding)239240 def edge(self, edge):241 if isinstance(edge, elements.EdgeSeparator):242 return self.separator(edge)243 else:244 return EdgeMetrics(edge, self)245246 def separator(self, separator):247 return SeparatorMetrics(separator, self)248249250class AltBlockMetrics(blockdiag.metrics.NodeMetrics):251 def __init__(self, metrics, block, node):252 self.block = block253 sheet = metrics.spreadsheet254 cellsize = metrics.cellsize255256 box = node.box257 box[0] -= (sheet.span_width[block.xy.x + 1] // 2 -258 cellsize * (block.xlevel - 1))259 box[1] -= cellsize * 5 // 2 * (block.ylevel_top - 1) + cellsize * 3260 box[2] += (sheet.span_width[block.xy.x + block.colwidth + 2] // 2 -261 cellsize * (block.xlevel - 1))262 box[3] += cellsize * (block.ylevel_bottom - 1) + cellsize263264 super(AltBlockMetrics, self).__init__(metrics, *box)265266 @property267 def textbox(self):268 size = self.metrics.textsize(self.block.type,269 font=self.metrics.font_for(self.block))270 return Box(self.x1, self.y1,271 self.x1 + size.width, self.y1 + size.height)272273274class EdgeMetrics(object):275 def __init__(self, edge, metrics):276 self.metrics = metrics277 self.edge = edge278279 @property280 def baseheight(self):281 cell = self.metrics.cell(self.edge)282 if (cell.height == self.edge.leftnotesize.height or283 (cell.height == self.edge.rightnotesize.height)):284 return cell.center.y285 else:286 return cell.top.y + self.edge.textheight287288 @property289 def right(self):290 m = self.metrics291 cell = m.cell(self.edge.right_node)292293 if self.edge.direction == 'self':294295 if self.edge.node1.xy.x + 1 == m.node_count:296 width = cell.width // 2 + m.cellsize * 3297 else:298 span_width = m.spreadsheet.span_width[self.edge.node1.xy.x]299 width = cell.width // 2 + span_width // 2300301 x = cell.bottom.x + width302 else:303 x = cell.bottom.x - m.cellsize304305 if self.edge.failed:306 x -= self.metrics.edge_length // 2307308 return x309310 @property311 def shaft(self):312 m = self.metrics313 baseheight = self.baseheight314315 if self.edge.direction == 'self':316 cell = m.cell(self.edge.node1)317 fold_height = m.cellsize * 2318319 # adjust textbox to right on activity-lines320 base_x = cell.bottom.x321 x1 = base_x + self.activity_line_width(self.edge.node1)322 x2 = self.right323324 line = [XY(x1 + m.cellsize, baseheight),325 XY(x2, baseheight),326 XY(x2, baseheight + fold_height),327 XY(x1 + m.cellsize, baseheight + fold_height)]328 else:329 x1 = (self.metrics.node(self.edge.left_node).bottom.x +330 self.activity_line_width(self.edge.left_node))331 x2 = self.metrics.node(self.edge.right_node).bottom.x332333 margin = m.cellsize334 if self.edge.diagonal:335 height = m.node_height * 3 // 4336 if self.edge.direction == 'right':337 line = [XY(x1 + margin, baseheight),338 XY(x2 - margin, baseheight + height)]339 else:340 line = [XY(x1 + margin, baseheight + height),341 XY(x2 - margin, baseheight)]342 else:343 line = [XY(x1 + margin, baseheight),344 XY(x2 - margin, baseheight)]345346 if self.edge.failed:347 edge_length = self.metrics.edge_length348 pt1, pt2 = line349 if self.edge.direction == 'right':350 pt2 = XY(pt2.x - edge_length // 2, (pt1.y + pt2.y) // 2)351 else:352 pt1 = XY(pt1.x + edge_length // 2, (pt1.y + pt2.y) // 2)353 line = [pt1, pt2]354355 return line356357 @property358 def failedmark(self):359 lines = []360 if self.edge.failed:361 r = self.metrics.cellsize362 if self.edge.direction == 'right':363 pt = self.shaft[-1]364 lines.append((XY(pt.x + r, pt.y - r),365 XY(pt.x + r * 3, pt.y + r)))366 lines.append((XY(pt.x + r, pt.y + r),367 XY(pt.x + r * 3, pt.y - r)))368 else:369 pt = self.shaft[0]370 lines.append((XY(pt.x - r * 3, pt.y - r),371 XY(pt.x - r, pt.y + r)))372 lines.append((XY(pt.x - r * 3, pt.y + r),373 XY(pt.x - r, pt.y - r)))374375 return lines376377 @property378 def head(self):379 cell = self.metrics.cellsize380381 head = []382 if self.edge.direction == 'right':383 xy = self.shaft[-1]384 head.append(XY(xy.x - cell, xy.y - cell // 2))385 head.append(xy)386 head.append(XY(xy.x - cell, xy.y + cell // 2))387 elif self.edge.direction == 'left':388 xy = self.shaft[0]389 head.append(XY(xy.x + cell, xy.y - cell // 2))390 head.append(xy)391 head.append(XY(xy.x + cell, xy.y + cell // 2))392 else: # self393 xy = self.shaft[-1]394 head.append(XY(xy.x + cell, xy.y - cell // 2))395 head.append(xy)396 head.append(XY(xy.x + cell, xy.y + cell // 2))397398 return head399400 @property401 def textbox(self):402 m = self.metrics403404 if self.edge.direction == 'self':405 x = m.node(self.edge.node1).bottom.x + \406 self.activity_line_width(self.edge.node1) + \407 self.edge.label_margin * 2408 elif self.edge.direction == 'right':409 x = m.node(self.edge.left_node).bottom.x + \410 self.activity_line_width(self.edge.left_node) + \411 self.edge.label_margin * 2 + \412 self.metrics.cellsize // 2413 else: # left414 x = m.node(self.edge.right_node).bottom.x - \415 self.edge.textwidth - \416 self.edge.label_margin * 2417418 y1 = self.baseheight - self.edge.textheight - self.edge.label_margin419 return Box(x, y1, x + self.edge.textwidth, y1 + self.edge.textheight)420421 def activity_line_width(self, node):422 m = self.metrics423424 index = self.edge.order425 activities = [a for a in node.activities if index in a['lifetime']]426 if activities:427 level = max(a['level'] for a in activities)428 else:429 level = 0430431 return m.cellsize // 2 * level432433 @property434 def leftnotebox(self):435 if not self.edge.leftnote:436 return Box(0, 0, 0, 0)437438 m = self.metrics439 cell = m.cell(self.edge.left_node)440 notesize = self.edge.leftnotesize441442 x = cell.center.x - m.cellsize * 3 - notesize.width443 y = self.baseheight - notesize.height // 2444445 if self.edge.failed and self.edge.direction == 'left':446 x += self.metrics.edge_length // 2 - m.cellsize447448 return Box(x, y, x + notesize.width, y + notesize.height)449450 @property451 def leftnoteshape(self):452 if not self.edge.leftnote:453 return []454455 r = self.metrics.cellsize456 box = self.leftnotebox457 return [XY(box[0], box[1]), XY(box[2], box[1]),458 XY(box[2] + r, box[1] + r), XY(box[2] + r, box[3]),459 XY(box[0], box[3]), XY(box[0], box[1])]460461 @property462 def rightnotebox(self):463 if not self.edge.rightnote:464 return Box(0, 0, 0, 0)465466 m = self.metrics467 cell = m.cell(self.edge.right_node)468 if self.edge.direction == 'self':469 x = self.right + m.cellsize * 2470 elif self.edge.failed and self.edge.direction == 'right':471 x = self.right + m.cellsize * 4472 else:473 x = cell.center.x + m.cellsize * 2474475 notesize = self.edge.rightnotesize476 y = self.baseheight - notesize.height // 2477 return Box(x, y, x + notesize.width, y + notesize.height)478479 @property480 def rightnoteshape(self):481 if not self.edge.rightnote:482 return []483484 r = self.metrics.cellsize485 box = self.rightnotebox486 return [XY(box[0], box[1]), XY(box[2], box[1]),487 XY(box[2] + r, box[1] + r), XY(box[2] + r, box[3]),488 XY(box[0], box[3]), XY(box[0], box[1])]489490491class SeparatorMetrics(object):492 def __init__(self, separator, metrics):493 self.metrics = metrics494 self.separator = separator495496 x1, x2 = self.baseline497 y1 = self.baseheight498 y2 = y1 + self.metrics.node_height499 d = self.metrics.cellsize // 4500501 font = metrics.font_for(self.separator)502 size = metrics.textsize(separator.label, font, x2 - x1)503 dx = (x2 - x1 - size.width) // 2504 dy = (y2 - y1 - size.height) // 2505 self.labelbox = Box(x1 + dx - d,506 y1 + dy - d,507 x1 + dx + size.width + d,508 y1 + dy + size.height + d)509510 @property511 def baseheight(self):512 return self.metrics.cell(self.separator).top.y513514 @property515 def baseline(self):516 dummy = elements.DiagramNode(None)517 dummy.xy = XY(0, 1)518 dummy.colwidth = self.metrics.node_count519 m = self.metrics.cell(dummy)520 r = self.metrics.cellsize * 3521 return (m.x1 - r, m.x2 + r)522523 @property524 def lines(self):525 lines = []526 if self.separator.type == 'divider':527 y = (self.labelbox[1] + self.labelbox[3]) // 2528 x1, x2 = self.baseline529 d = self.metrics.cellsize // 4530531 lines.append((XY(x1, y - d), XY(self.labelbox[0], y - d)))532 lines.append((XY(x1, y + d), XY(self.labelbox[0], y + d)))533 lines.append((XY(self.labelbox[2], y - d), XY(x2, y - d)))534 lines.append((XY(self.labelbox[2], y + d), XY(x2, y + d)))535536 return lines