-
Notifications
You must be signed in to change notification settings - Fork 1
/
DependencyLayout.py
262 lines (227 loc) · 9.59 KB
/
DependencyLayout.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
#!/usr/bin/env python3
# -*- coding: utf-8, vim: expandtab:ts=4 -*-
import functools
import itertools
import operator
from AbstractEdgeLayout import AbstractEdgeLayout
from utils.Counter import Counter
from utils.HashMultiMapArrayList import HashMultiMapArrayList
from SVGWriter import *
"""
* A DependencyLayout lays out edges in a dependency parse layout. Here the edge from head to modifier is represented as
* a directed edge that starts at the head, first goes up and then down to the modifier. The height depends on the
* number of other edges between the head and the modifier.
* <p/>
* <p>Note that all incoming and outgoing edges of a token are placed along the upper edge of the token bounding box in
* an order that depends on the distance of the other token of the edge. The further away the other token is, the closer
* the edge start or end point is to the middle of the token bounding box. There is one exception to this rule: self
* loops always start at the leftmost position and end at the rightmost position.
*
* @author Sebastian Riedel
"""
class DependencyLayout(AbstractEdgeLayout):
"""
* The size of the arrow
"""
@property
def arrowsize(self):
return self._arrowsize
@arrowsize.setter
def arrowsize(self, value):
self._arrowsize = value
def __init__(self):
super().__init__()
self._arrowsize = 2
"""
* Lays out the edges as directed labelled dependency links between tokens.
*
* @param edges the edges to layout.
* @param bounds the bounds of the tokens the edges connect.
* @param g2d the graphics object to draw on.
* @return the dimensions of the drawn graph.
"""
def layoutEdges(self, edges, bounds, scene):
edges_ = set(edges)
if len(self._visible) > 0:
edges_ &= self._visible # Intersection
# find out height of each edge
self._shapes.clear()
loops = HashMultiMapArrayList() # XXX THIS IS LINKED LIST NOT ARRAY LIST!
allLoops = set()
tokens = set()
for edge in edges_:
tokens.add(edge.From)
tokens.add(edge.To)
if edge.From == edge.To:
loops[edge.From] = edge
allLoops.add(edge)
edges_ -= allLoops
depth = Counter()
offset = Counter()
dominates = HashMultiMapArrayList() # XXX THIS IS LINKED LIST NOT ARRAY LIST!
for over in edges_:
for under in edges_:
if over != under and (over.covers(under) or over.coversSemi(under) or
over.coversExactly(under) and over.lexicographicOrder(under) > 0):
dominates[over] = under
for edge in edges_:
self.calculateDepth(dominates, depth, edge)
for left in edges_:
for right in edges_:
if left != right and left.crosses(right) and depth[left] == depth[right]:
if offset[left] == 0 and offset[right] == 0:
offset.increment(left, self._heightPerLevel // 2)
elif offset[left] == offset[right]:
offset[left] = self._heightPerLevel // 3
offset[right] = self._heightPerLevel * 2 // 3
# calculate maxHeight and maxWidth
maxHeight = (depth.getMaximum() + 1) * self._heightPerLevel + 3
# 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
if depth.getMaximum() == 0 and len(allLoops) > 0:
maxHeight += self._heightPerLevel // 2
# build map from vertex to incoming/outgoing edges
vertex2edges = HashMultiMapArrayList() # XXX THIS IS LINKED LIST NOT ARRAY LIST!
for edge in edges_:
vertex2edges[edge.From] = edge
vertex2edges[edge.To] = edge
# assign starting and end points of edges by sorting the edges per vertex
From = {}
To = {}
for token in tokens:
connections = vertex2edges[token]
"""
* Compare to edges to see which one should be drawn higher
*
* @param edge1 of type Edge
* @param edge2 of type Edge
* @return int < 0 if edge1 < edge2 else >0.
"""
def compare(edge1, edge2):
# if they point in different directions order is defined by left to right
if edge1.leftOf(token) and edge2.rightOf(token):
return -1
if edge2.leftOf(token) and edge1.rightOf(token):
return 1
# otherwise we order by length
diff = edge2.getLength() - edge1.getLength()
if edge1.leftOf(token) and edge2.leftOf(token):
if diff != 0:
return -diff
else:
return edge1.lexicographicOrder(edge2)
else:
if diff != 0:
return diff
else:
return edge2.lexicographicOrder(edge1)
connections = sorted(connections, key=functools.cmp_to_key(compare))
# now put points along the token vertex wrt to ordering
loopsOnVertex = loops[token]
width = (bounds[token].getWidth() + self._vertexExtraSpace) //\
(len(connections) + 1 + len(loopsOnVertex) * 2)
x = (bounds[token].From - (self._vertexExtraSpace // 2)) + width
for loop in loopsOnVertex:
point = (x, self._baseline + maxHeight)
From[loop] = point
x += width
for edge in connections:
point = (x, self._baseline + maxHeight)
if edge.From == token:
From[edge] = point
else:
To[edge] = point
x += width
for loop in loopsOnVertex:
point = (x, self._baseline + maxHeight)
To[loop] = point
x += width
# draw each edge
edges_ |= allLoops
for edge in edges_:
# set Color and remember old color
old = scene.color
scene.color = self.getColor(edge.type)
# draw lines
height = self._baseline + maxHeight - (depth[edge] + 1) * self._heightPerLevel + offset[edge]
if edge.From == edge.To:
height -= self._heightPerLevel // 2
p1 = From[edge]
if p1 is None:
print(edge)
p2 = (p1[0], height)
p4 = To[edge]
if p4 is None:
print(edges)
p3 = (p4[0], height)
# connection
if self._curve:
shape = self.createCurveArrow(scene, p1, p2, p3, p4)
else:
shape = self.createRectArrow(scene, p1, p2, p3, p4)
x = (p4[0] - self._arrowsize, p4[1] - self._arrowsize)
y = (p4[0], p4[1])
z = (p4[0] + self._arrowsize, p4[1] - self._arrowsize)
scene.add(Line(scene, x, y, scene.color))
scene.add(Line(scene, z, y, scene.color))
# write label in the middle under
# XXX Original fontsize is 8
labelwith = round(Text(scene, (0, 0), edge.getLabelWithNote(), 12, scene.color).getWidth() * 0.9)
labelx = min(p1[0], p3[0]) + abs(p1[0]-p3[0]) // 2# - labelwith // 2
# labely = height + 1
labely = height + 10 + 1 # XXX layout.getAscent()
# XXX Original fontsize is 8
scene.add(Text(scene, (labelx, labely), edge.getLabelWithNote(), 12, scene.color))
scene.color = old
self._shapes[shape] = edge
maxWidth = max(itertools.chain(From.values(), To.values()), key=operator.itemgetter(0), default=(0,))[0]
return maxWidth + self._arrowsize + 2, maxHeight
"""
* Create an rectangular path that starts at p1 the goes to p2, p3 and finally p4.
*
* @param p1 the first point
* @param p2 the second point
* @param p3 the third point
* @param p4 the last point
* @return an a path over the given points.
"""
@staticmethod
def createRectArrow(scene, p1, p2, p3, p4):
scene.add(Line(scene, p1, p2, scene.color))
scene.add(Line(scene, p2, p3, scene.color))
scene.add(Line(scene, p3, p4, scene.color))
return p1, p2, p3, p4
"""
* Create an curved path that starts at p1 and ends at p4. Points p2 and p3 are used as bezier control points.
*
* @param p1 the first point
* @param p2 the second point
* @param p3 the third point
* @param p4 the last point
* @return an a path over the given points.
"""
@staticmethod
def createCurveArrow(scene, p1, p2, p3, p4):
start = (p1[0], p1[1])
# y = (p2[0] - p1.x(), p2[1]-p1.y())
# z = (p2[0]+(p3[0]-p2[0]) / 2 -p1.x(), p2[1]-p1.y())
c1 = (p2[0], p2[1])
c2 = (p3[0], p3[1])
end = (p4[0], p4[1])
middle = (c1[0] + (c2[0]-c1[0]) // 2, c1[1])
scene.add(QuadraticBezierCurve(scene, start, c1, c1, middle, scene.color))
scene.add(QuadraticBezierCurve(scene, middle, c2, c2, end, scene.color))
return p1, p2, p3, p4
"""
* The size of the arrow.
*
* @param arrowSize the size of the arrow.
"""
# See the setter above...
"""
* Return the arrow size.
*
* @return the size of the arrow.
"""
# See the getter above...