-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathSpanLayout.py
263 lines (219 loc) · 9.09 KB
/
SpanLayout.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env python3
# -*- coding: utf-8, vim: expandtab:ts=4 -*-
from AbstractEdgeLayout import AbstractEdgeLayout
from utils.Counter import Counter
from utils.HashMultiMapArrayList import HashMultiMapArrayList
from SVGWriter import *
"""
* A SpanLayouy lays out edges as rectangular blocks under or above the tokens that the edge covers. The label is
* written into these blocks. If there are multiple edge types then all spans of the same type appear in the same
* contiguous vertical area.
*
* @author Sebastian Riedel
"""
class SpanLayout(AbstractEdgeLayout):
"""
* Should the graph be upside-down reverted.
"""
@property
def revert(self):
return self._revert
@revert.setter
def revert(self, value):
self._revert = value
"""
* Should we draw separation lines between the areas for different span types.
"""
@property
def separationLines(self):
return self._separationLines
@separationLines.setter
def separationLines(self, value):
self._separationLines = value
"""
* The order/vertical layer in which the area of a certain type should be drawn.
"""
@property
def orders(self):
return self._orders
@orders.setter
def orders(self, value):
self._orders = value
"""
* How much space should at least be between the label of a span and the right and left edges of the span.
"""
@property
def totalTextMargin(self):
return self._totalTextMargin
@totalTextMargin.setter
def totalTextMargin(self, value):
self._totalTextMargin = value
"""
* Creates a new SpanLayout.
"""
def __init__(self):
super().__init__()
self._baseline = 1
self._revert = True
self._separationLines = True
self._orders = {}
self._totalTextMargin = 6
"""
* Sets the order/vertical layer in which the area of a certain type should be drawn.
*
* @param type the type we want to change the order for.
* @param order the order/vertical layer in which the area of the given type should be drawn.
"""
def setTypeOrder(self, type, order):
self._orders[type] = order
"""
* Returns the order/vertical layer in which the area of a certain type should be drawn.
*
* @param type the type we want to get the order for.
* @return the order/vertical layer in which the area of the given type should be drawn.
"""
def getOrder(self, type):
if type in self._orders:
order = self._orders[type]
else:
order = None
return order
"""
* Should we draw separation lines between the areas for different span types.
*
* @return true iff separation lines should be drawn.
"""
def isSeparationLines(self):
return self._separationLines
"""
* Should we draw separation lines between the areas for different span types.
*
* @param separationLines true iff separation lines should be drawn.
"""
# See the setter above...
"""
* For each token that has a self-loop we need the token to be wide enough. This method calculates the needed token
* width for a given set of edges. That is, for all self-loops in the set of edges we calculate how wide the
* corresponding token need to be.
*
* @param edges the set of edges that can contain self-loops.
* @param g2d the graphics object needed to find out the actual width of text.
* @return A mapping from tokens with self-loops to pixel widths.
"""
def estimateRequiredTokenWidths(self, edges, scene):
result = {}
for edge in edges:
if edge.From == edge.To:
labelwith = Text(scene, (0, 0), edge.label, 12, scene.color).getWidth() # Original fontsize is 8
if edge.From in result:
width = max(labelwith, result[edge.From]) # oldWith is result[...]
else:
width = labelwith
result[edge.From] = width + self._totalTextMargin
return result
"""
* Lays out the edges as spans (blocks) under or above the tokens they contain.
*
* @param edges the edges to layout.
* @param bounds the bounds of the tokens the spans connect.
* @param g2d the graphics object to draw on.
* @return the dimensions of the drawn graph.
"""
def layoutEdges(self, edges, bounds, scene):
if len(self.visible) > 0:
edges = set(edges)
edges &= self._visible # Intersection
# find out height of each edge
self._shapes.clear()
depth = Counter()
offset = Counter()
dominates = HashMultiMapArrayList()
for over in edges:
for under in edges:
orderOver = self.getOrder(over.getTypePrefix())
orderUnder = self.getOrder(under.getTypePrefix())
if orderOver > orderUnder or orderOver == orderUnder and (
over.covers(under) or over.coversSemi(under) or
over.coversExactly(under) and
over.lexicographicOrder(under) > 0 or
over.overlaps(under) and over.getMinIndex() < under.getMinIndex()):
dominates.add(over, under)
for edge in edges:
self.calculateDepth(dominates, depth, edge)
# calculate maxHeight and maxWidth
maxDepth = depth.getMaximum()
if len(edges) > 0:
maxHeight = (maxDepth + 1) * self._heightPerLevel + 3
else:
maxHeight = 1
# in case there are no edges that cover other edges (depth == 0) we need
# to increase the height slightly because loops on the same token
# have height of 1.5 levels
# build map from vertex to incoming/outgoing edges
vertex2edges = HashMultiMapArrayList() # XXX NOT NOT LINKED LIST!
for edge in edges:
vertex2edges.add(edge.From, edge)
vertex2edges.add(edge.To, edge)
# assign starting and end points of edges by sorting the edges per vertex
maxWidth = 0
# draw each edge
for edge in edges:
# set Color and remember old color
old = scene.color
scene.color = self.getColor(edge.type)
# prepare label (will be needed for spacing)
labelwith = Text(scene, (0, 0), edge.label, 12, scene.color).getWidth() * 0
# draw lines
if self._revert:
spanLevel = maxDepth - depth[edge]
else:
spanLevel = depth[edge]
height = self._baseline + maxHeight - (spanLevel + 1) * self._heightPerLevel + offset[edge]
# scene.setStroke(self.getStroke(edge)) # TODO: Ez rossz
buffer = 2
fromBounds = bounds[edge.From]
toBounds = bounds[edge.To]
minX = min(fromBounds.From, toBounds.From)
maxX = max(fromBounds.To, toBounds.To)
if maxX > maxWidth:
maxWidth = maxX + 1
if maxX - minX < labelwith + self._totalTextMargin:
middle = minX + (maxX - minX) // 2
textWidth = labelwith + self._totalTextMargin
minX = middle - textWidth // 2
maxX = middle + textWidth // 2
# connection
if self.curve:
scene.add(Rectangle(scene, (minX, height-buffer), maxX-minX, self._heightPerLevel - 2 * buffer,
(255, 255, 255), (0, 0, 0), 1))
else:
scene.add(Rectangle(scene, (minX, height-buffer), maxX-minX, self._heightPerLevel - 2 * buffer,
(255, 255, 255), (0, 0, 0), 1))
# write label in the middle under
labelx = minX + (maxX - minX) // 2 - labelwith // 2
labely = height + self._heightPerLevel // 2
scene.add(Text(scene, (labelx, labely), edge.getLabelWithNote(), 12, scene.color))
scene.color = old
self._shapes[(minX, height-buffer, maxX-minX, self._heightPerLevel - 2 * buffer)] = edge
# int maxWidth = 0;
for bound in bounds.values():
if bound.To > maxWidth:
maxWidth = bound.To
if self._separationLines:
# find largest depth for each prefix type
minDepths = {}
for edge in edges:
edgeDepth = depth[edge]
typeDepth = minDepths.get(edge.getTypePrefix())
if typeDepth is None or typeDepth > edgeDepth:
typeDepth = edgeDepth
minDepths[edge.getTypePrefix()] = typeDepth
height = self._baseline - 1
for d in minDepths.values():
if not self._revert:
height += (maxDepth - d) * self._heightPerLevel
else:
height += d * self._heightPerLevel
scene.color = (211, 211, 211) # Color.LIGHT_GRAY
scene.add(Line(scene, (0, height), (maxWidth, height), color=scene.color))
return maxWidth+scene.offsetx, maxHeight+scene.offsety