1
# -*- coding: utf-8 -*-
3
# Copyright (c) 2004 - 2007 Detlev Offenbach <detlev@die-offenbachs.de>
7
Module implementing a graphics item for an association between two items.
10
from PyQt4.QtCore import *
11
from PyQt4.QtGui import *
13
from E4Graphics.E4ArrowItem import E4ArrowItem, NormalArrow, WideArrow
30
class AssociationItem(E4ArrowItem):
32
Class implementing a graphics item for an association between two items.
34
The association is drawn as an arrow starting at the first items and
37
def __init__(self, itemA, itemB, type = Normal, parent = None, scene = None):
41
@param itemA first widget of the association
42
@param itemB second widget of the association
43
@param type type of the association. This must be one of
45
<li>Normal (default)</li>
46
<li>Generalisation</li>
49
@keyparam parent reference to the parent object (QGraphicsItem)
50
@keyparam scene reference to the scene object (QGraphicsScene)
53
arrowType = NormalArrow
56
arrowType = NormalArrow
58
elif type == Generalisation:
62
E4ArrowItem.__init__(self, QPointF(0, 0), QPointF(100, 100),
63
arrowFilled, arrowType, parent, scene)
65
self.setFlag(QGraphicsItem.ItemIsMovable, False)
66
self.setFlag(QGraphicsItem.ItemIsSelectable, False)
68
## self.calculateEndingPoints = self.__calculateEndingPoints_center
69
self.calculateEndingPoints = self.__calculateEndingPoints_rectangle
75
self.regionA = NoRegion
76
self.regionB = NoRegion
78
self.calculateEndingPoints()
80
self.itemA.addAssociation(self)
81
self.itemB.addAssociation(self)
83
def __mapRectFromItem(self, item):
85
Private method to map item's rectangle to this item's coordinate system.
87
@param item reference to the item to be mapped (QGraphicsRectItem)
88
@return item's rectangle in local coordinates (QRectF)
91
tl = self.mapFromItem(item, rect.topLeft())
92
return QRectF(tl.x(), tl.y(), rect.width(), rect.height())
94
def __calculateEndingPoints_center(self):
96
Private method to calculate the ending points of the association item.
98
The ending points are calculated from the centers of the
101
if self.itemA is None or self.itemB is None:
104
self.prepareGeometryChange()
106
rectA = self.__mapRectFromItem(self.itemA)
107
rectB = self.__mapRectFromItem(self.itemB)
108
midA = QPointF(rectA.x() + rectA.width() / 2.0,
109
rectA.y() + rectA.height() / 2.0)
110
midB = QPointF(rectB.x() + rectB.width() / 2.0,
111
rectB.y() + rectB.height() / 2.0)
112
startP = self.__findRectIntersectionPoint(self.itemA, midA, midB)
113
endP = self.__findRectIntersectionPoint(self.itemB, midB, midA)
115
if startP.x() != -1 and startP.y() != -1 and \
116
endP.x() != -1 and endP.y() != -1:
117
self.setPoints(startP.x(), startP.y(), endP.x(), endP.y())
119
def __calculateEndingPoints_rectangle(self):
121
Private method to calculate the ending points of the association item.
123
The ending points are calculated by the following method.
125
For each item the diagram is divided in four Regions by its diagonals
134
Region 1 | /\ | Region 3
142
Each diagonal is defined by two corners of the bounding rectangle
144
To calculate the start point we have to find out in which
145
region (defined by itemA's diagonals) is itemB's TopLeft corner
146
(lets call it region M). After that the start point will be
147
the middle point of rectangle's side contained in region M.
149
To calculate the end point we repeat the above but in the opposite direction
150
(from itemB to itemA)
152
if self.itemA is None or self.itemB is None:
155
self.prepareGeometryChange()
157
rectA = self.__mapRectFromItem(self.itemA)
158
rectB = self.__mapRectFromItem(self.itemB)
160
xA = rectA.x() + rectA.width() / 2.0
161
yA = rectA.y() + rectA.height() / 2.0
162
xB = rectB.x() + rectB.width() / 2.0
163
yB = rectB.y() + rectB.height() / 2.0
166
rc = QRectF(xA, yA, rectA.width(), rectA.height())
167
oldRegionA = self.regionA
168
self.regionA = self.__findPointRegion(rc, xB, yB)
169
# move some regions to the standard ones
170
if self.regionA == NorthWest:
172
elif self.regionA == NorthEast:
174
elif self.regionA == SouthEast:
176
elif self.regionA == SouthWest:
178
elif self.regionA == Center:
181
self.__updateEndPoint(self.regionA, True)
183
# now do the same for itemB
184
rc = QRectF(xB, yB, rectB.width(), rectB.height())
185
oldRegionB = self.regionB
186
self.regionB = self.__findPointRegion(rc, xA, yA)
187
# move some regions to the standard ones
188
if self.regionB == NorthWest:
190
elif self.regionB == NorthEast:
192
elif self.regionB == SouthEast:
194
elif self.regionB == SouthWest:
196
elif self.regionB == Center:
199
self.__updateEndPoint(self.regionB, False)
201
def __findPointRegion(self, rect, posX, posY):
203
Private method to find out, which region of rectangle rect contains the point
204
(PosX, PosY) and returns the region number.
206
@param rect rectangle to calculate the region for (QRectF)
207
@param posX x position of point (float)
208
@param posY y position of point (float)
209
@return the calculated region number<br />
210
West = Region 1<br />
211
North = Region 2<br />
212
East = Region 3<br />
213
South = Region 4<br />
214
NorthWest = On diagonal 2 between Region 1 and 2<br />
215
NorthEast = On diagonal 1 between Region 2 and 3<br />
216
SouthEast = On diagonal 2 between Region 3 and 4<br />
217
SouthWest = On diagonal 1 between Region4 and 1<br />
218
Center = On diagonal 1 and On diagonal 2 (the center)<br />
226
b1 = x + w / 2.0 - y * slope1
227
b2 = x + w / 2.0 - y * slope2
229
eval1 = slope1 * posY + b1
230
eval2 = slope2 * posY + b2
235
if eval1 > posX and eval2 > posX:
239
elif eval1 > posX and eval2 < posX:
243
elif eval1 < posX and eval2 < posX:
247
elif eval1 < posX and eval2 > posX:
251
elif eval1 == posX and eval2 < posX:
255
elif eval1 < posX and eval2 == posX:
259
elif eval1 == posX and eval2 > posX:
263
elif eval1 > posX and eval2 == posX:
267
elif eval1 == posX and eval2 == posX:
272
def __updateEndPoint(self, region, isWidgetA):
274
Private method to update an endpoint.
276
@param region the region for the endpoint (integer)
277
@param isWidgetA flag indicating update for itemA is done (boolean)
279
if region == NoRegion:
283
rect = self.__mapRectFromItem(self.itemA)
285
rect = self.__mapRectFromItem(self.itemB)
296
elif region == North:
302
elif region == South:
305
elif region == Center:
310
self.setStartPoint(px, py)
312
self.setEndPoint(px, py)
314
def __findRectIntersectionPoint(self, item, p1, p2):
316
Private method to find the intersetion point of a line with a rectangle.
318
@param item item to check against
319
@param p1 first point of the line (QPointF)
320
@param p2 second point of the line (QPointF)
321
@return the intersection point (QPointF)
323
rect = self.__mapRectFromItem(item)
325
QLineF(rect.topLeft(), rect.topRight()),
326
QLineF(rect.topLeft(), rect.bottomLeft()),
327
QLineF(rect.bottomRight(), rect.bottomLeft()),
328
QLineF(rect.bottomRight(), rect.topRight())
330
intersectLine = QLineF(p1, p2)
331
intersectPoint = QPointF(0, 0)
333
if intersectLine.intersect(line, intersectPoint) == \
334
QLineF.BoundedIntersection:
335
return intersectPoint
336
return QPointF(-1.0, -1.0)
338
def __findIntersection(self, p1, p2, p3, p4):
340
Method to calculate the intersection point of two lines.
342
The first line is determined by the points p1 and p2, the second
343
line by p3 and p4. If the intersection point is not contained in
344
the segment p1p2, then it returns (-1.0, -1.0).
346
For the function's internal calculations remember:<br />
347
QT coordinates start with the point (0,0) as the topleft corner
348
and x-values increase from left to right and y-values increase
349
from top to bottom; it means the visible area is quadrant I in
350
the regular XY coordinate system
353
Quadrant II | Quadrant I
354
-----------------|-----------------
355
Quadrant III | Quadrant IV
358
In order for the linear function calculations to work in this method
359
we must switch x and y values (x values become y values and viceversa)
361
@param p1 first point of first line (QPointF)
362
@param p2 second point of first line (QPointF)
363
@param p3 first point of second line (QPointF)
364
@param p4 second point of second line (QPointF)
365
@return the intersection point (QPointF)
376
# line 1 is the line between (x1, y1) and (x2, y2)
377
# line 2 is the line between (x3, y3) and (x4, y4)
378
no_line1 = True # it is false, if line 1 is a linear function
379
no_line2 = True # it is false, if line 2 is a linear function
386
slope1 = (y2 - y1) / (x2 - x1)
387
b1 = y1 - slope1 * x1
390
slope2 = (y4 - y3) / (x4 - x3)
391
b2 = y3 - slope2 * x3
395
# if either line is not a function
396
if no_line1 and no_line2:
397
# if the lines are not the same one
399
return QPointF(-1.0, -1.0)
400
# if the lines are the same ones
402
if y3 <= y1 and y1 <= y4:
403
return QPointF(y1, x1)
405
return QPointF(y2, x2)
407
if y4 <= y1 and y1 <= y3:
408
return QPointF(y1, x1)
410
return QPointF(y2, x2)
412
pt.setX(slope2 * x1 + b2)
415
if not (y2 <= pt.x() and pt.x() <= y1):
419
if not (y1 <= pt.x() and pt.x() <= y2):
424
pt.setX(slope1 * x3 + b1)
427
if not (y4 <= pt.x() and pt.x() <= y3):
431
if not (y3 <= pt.x() and pt.x() <= y4):
441
pt.setY((b2 - b1) / (slope1 - slope2))
442
pt.setX(slope1 * pt.y() + b1)
443
# the intersection point must be inside the segment (x1, y1) (x2, y2)
444
if x2 >= x1 and y2 >= y1:
445
if not ((x1 <= pt.y() and pt.y() <= x2) and (y1 <= pt.x() and pt.x() <= y2)):
448
elif x2 < x1 and y2 >= y1:
449
if not ((x2 <= pt.y() and pt.y() <= x1) and (y1 <= pt.x() and pt.x() <= y2)):
452
elif x2 >= x1 and y2 < y1:
453
if not ((x1 <= pt.y() and pt.y() <= x2) and (y2 <= pt.x() and pt.x() <= y1)):
457
if not ((x2 <= pt.y() and pt.y() <= x1) and (y2 <= pt.x() and pt.x() <= y1)):
463
def widgetMoved(self):
465
Public method to recalculate the association after a widget was moved.
467
self.calculateEndingPoints()
469
def unassociate(self):
471
Public method to unassociate from the widgets.
473
self.itemA.removeAssociation(self)
474
self.itemB.removeAssociation(self)