~ubuntu-branches/ubuntu/karmic/eric/karmic

« back to all changes in this revision

Viewing changes to eric/Graphics/AssociationItem.py

  • Committer: Bazaar Package Importer
  • Author(s): Scott Kitterman
  • Date: 2008-01-28 18:02:25 UTC
  • mfrom: (1.1.4 upstream)
  • Revision ID: james.westby@ubuntu.com-20080128180225-6nrox6yrworh2c4v
Tags: 4.0.4-1ubuntu1
* Add python-qt3 to build-depends becuase that's where Ubuntu puts 
  pyqtconfig
* Change maintainer to MOTU

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
 
 
3
# Copyright (c) 2004 - 2007 Detlev Offenbach <detlev@die-offenbachs.de>
 
4
#
 
5
 
 
6
"""
 
7
Module implementing a graphics item for an association between two items.
 
8
"""
 
9
 
 
10
from PyQt4.QtCore import *
 
11
from PyQt4.QtGui import *
 
12
 
 
13
from E4Graphics.E4ArrowItem import E4ArrowItem, NormalArrow, WideArrow
 
14
 
 
15
Normal = 0
 
16
Generalisation = 1
 
17
Imports = 2
 
18
 
 
19
NoRegion = 0
 
20
West = 1
 
21
North = 2
 
22
East = 3
 
23
South = 4
 
24
NorthWest = 5
 
25
NorthEast = 6
 
26
SouthEast = 7
 
27
SouthWest = 8
 
28
Center = 9
 
29
 
 
30
class AssociationItem(E4ArrowItem):
 
31
    """
 
32
    Class implementing a graphics item for an association between two items.
 
33
    
 
34
    The association is drawn as an arrow starting at the first items and
 
35
    ending at the second.
 
36
    """
 
37
    def __init__(self, itemA, itemB, type = Normal, parent = None, scene = None):
 
38
        """
 
39
        Constructor
 
40
        
 
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
 
44
            <ul>
 
45
            <li>Normal (default)</li>
 
46
            <li>Generalisation</li>
 
47
            <li>Imports</li>
 
48
            </ul>
 
49
        @keyparam parent reference to the parent object (QGraphicsItem)
 
50
        @keyparam scene reference to the scene object (QGraphicsScene)
 
51
        """
 
52
        if type == Normal:
 
53
            arrowType = NormalArrow
 
54
            arrowFilled = True
 
55
        elif type == Imports:
 
56
            arrowType = NormalArrow
 
57
            arrowFilled = True
 
58
        elif type == Generalisation:
 
59
            arrowType = WideArrow
 
60
            arrowFilled = False
 
61
        
 
62
        E4ArrowItem.__init__(self, QPointF(0, 0), QPointF(100, 100), 
 
63
            arrowFilled, arrowType, parent, scene)
 
64
        
 
65
        self.setFlag(QGraphicsItem.ItemIsMovable, False)
 
66
        self.setFlag(QGraphicsItem.ItemIsSelectable, False)
 
67
        
 
68
##        self.calculateEndingPoints = self.__calculateEndingPoints_center
 
69
        self.calculateEndingPoints = self.__calculateEndingPoints_rectangle
 
70
        
 
71
        self.itemA = itemA
 
72
        self.itemB = itemB
 
73
        self.assocType = type
 
74
        
 
75
        self.regionA = NoRegion
 
76
        self.regionB = NoRegion
 
77
        
 
78
        self.calculateEndingPoints()
 
79
        
 
80
        self.itemA.addAssociation(self)
 
81
        self.itemB.addAssociation(self)
 
82
        
 
83
    def __mapRectFromItem(self, item):
 
84
        """
 
85
        Private method to map item's rectangle to this item's coordinate system.
 
86
        
 
87
        @param item reference to the item to be mapped (QGraphicsRectItem)
 
88
        @return item's rectangle in local coordinates (QRectF)
 
89
        """
 
90
        rect = item.rect()
 
91
        tl = self.mapFromItem(item, rect.topLeft())
 
92
        return QRectF(tl.x(), tl.y(), rect.width(), rect.height())
 
93
        
 
94
    def __calculateEndingPoints_center(self):
 
95
        """
 
96
        Private method to calculate the ending points of the association item.
 
97
        
 
98
        The ending points are calculated from the centers of the
 
99
        two associated items.
 
100
        """
 
101
        if self.itemA is None or self.itemB is None:
 
102
            return
 
103
        
 
104
        self.prepareGeometryChange()
 
105
        
 
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)
 
114
        
 
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())
 
118
        
 
119
    def __calculateEndingPoints_rectangle(self):
 
120
        """
 
121
        Private method to calculate the ending points of the association item.
 
122
        
 
123
        The ending points are calculated by the following method.
 
124
        
 
125
        For each item the diagram is divided in four Regions by its diagonals
 
126
        as indicated below
 
127
        <pre>
 
128
                   \  Region 2  /
 
129
                    \          /
 
130
                     |--------|
 
131
                     | \    / |
 
132
                     |  \  /  |
 
133
                     |   \/   |
 
134
            Region 1 |   /\   | Region 3
 
135
                     |  /  \  |
 
136
                     | /    \ |
 
137
                     |--------|
 
138
                    /          \
 
139
                   /  Region 4  \
 
140
        </pre>
 
141
        
 
142
        Each diagonal is defined by two corners of the bounding rectangle
 
143
        
 
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.
 
148
        
 
149
        To calculate the end point we repeat the above but in the opposite direction
 
150
        (from itemB to itemA)
 
151
        """
 
152
        if self.itemA is None or self.itemB is None:
 
153
            return
 
154
        
 
155
        self.prepareGeometryChange()
 
156
        
 
157
        rectA = self.__mapRectFromItem(self.itemA)
 
158
        rectB = self.__mapRectFromItem(self.itemB)
 
159
        
 
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
 
164
        
 
165
        # find itemA region
 
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:
 
171
            self.regionA = North
 
172
        elif self.regionA == NorthEast:
 
173
            self.regionA = East
 
174
        elif self.regionA == SouthEast:
 
175
            self.regionA = South
 
176
        elif self.regionA == SouthWest:
 
177
            self.regionA = West
 
178
        elif self.regionA == Center:
 
179
            self.regionA = West
 
180
        
 
181
        self.__updateEndPoint(self.regionA, True)
 
182
        
 
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:
 
189
            self.regionB = North
 
190
        elif self.regionB == NorthEast:
 
191
            self.regionB = East
 
192
        elif self.regionB == SouthEast:
 
193
            self.regionB = South
 
194
        elif self.regionB == SouthWest:
 
195
            self.regionB = West
 
196
        elif self.regionB == Center:
 
197
            self.regionB = West
 
198
        
 
199
        self.__updateEndPoint(self.regionB, False)
 
200
        
 
201
    def __findPointRegion(self, rect, posX, posY):
 
202
        """
 
203
        Private method to find out, which region of rectangle rect contains the point 
 
204
        (PosX, PosY) and returns the region number.
 
205
        
 
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 />
 
219
        """
 
220
        w = rect.width()
 
221
        h = rect.height()
 
222
        x = rect.x()
 
223
        y = rect.y()
 
224
        slope2 = w / h
 
225
        slope1 = -slope2
 
226
        b1 = x + w / 2.0 - y * slope1
 
227
        b2 = x + w / 2.0 - y * slope2
 
228
        
 
229
        eval1 = slope1 * posY + b1
 
230
        eval2 = slope2 * posY + b2
 
231
        
 
232
        result = NoRegion
 
233
        
 
234
        # inside region 1
 
235
        if eval1 > posX and eval2 > posX:
 
236
            result = West
 
237
        
 
238
        #inside region 2
 
239
        elif eval1 > posX and eval2 < posX:
 
240
            result = North
 
241
        
 
242
        # inside region 3
 
243
        elif eval1 < posX and eval2 < posX:
 
244
            result = East
 
245
        
 
246
        # inside region 4
 
247
        elif eval1 < posX and eval2 > posX:
 
248
            result = South
 
249
        
 
250
        # inside region 5
 
251
        elif eval1 == posX and eval2 < posX:
 
252
            result = NorthWest
 
253
        
 
254
        # inside region 6
 
255
        elif eval1 < posX and eval2 == posX:
 
256
            result = NorthEast
 
257
        
 
258
        # inside region 7
 
259
        elif eval1 == posX and eval2 > posX:
 
260
            result = SouthEast
 
261
        
 
262
        # inside region 8
 
263
        elif eval1 > posX and eval2 == posX:
 
264
            result = SouthWest
 
265
        
 
266
        # inside region 9
 
267
        elif eval1 == posX and eval2 == posX:
 
268
            result = Center
 
269
        
 
270
        return result
 
271
        
 
272
    def __updateEndPoint(self, region, isWidgetA):
 
273
        """
 
274
        Private method to update an endpoint.
 
275
        
 
276
        @param region the region for the endpoint (integer)
 
277
        @param isWidgetA flag indicating update for itemA is done (boolean)
 
278
        """
 
279
        if region == NoRegion:
 
280
            return
 
281
        
 
282
        if isWidgetA:
 
283
            rect = self.__mapRectFromItem(self.itemA)
 
284
        else:
 
285
            rect = self.__mapRectFromItem(self.itemB)
 
286
        x = rect.x()
 
287
        y = rect.y()
 
288
        ww = rect.width()
 
289
        wh = rect.height()
 
290
        ch = wh / 2.0
 
291
        cw = ww / 2.0
 
292
        
 
293
        if region == West:
 
294
            px = x
 
295
            py = y + ch
 
296
        elif region == North:
 
297
            px = x + cw
 
298
            py = y
 
299
        elif region == East:
 
300
            px = x + ww
 
301
            py = y + ch
 
302
        elif region == South:
 
303
            px = x + cw
 
304
            py = y + wh
 
305
        elif region == Center:
 
306
            px = x + cw
 
307
            py = y + wh
 
308
        
 
309
        if isWidgetA:
 
310
            self.setStartPoint(px, py)
 
311
        else:
 
312
            self.setEndPoint(px, py)
 
313
        
 
314
    def __findRectIntersectionPoint(self, item, p1, p2):
 
315
        """
 
316
        Private method to find the intersetion point of a line with a rectangle.
 
317
        
 
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)
 
322
        """
 
323
        rect = self.__mapRectFromItem(item)
 
324
        lines = [
 
325
            QLineF(rect.topLeft(), rect.topRight()),
 
326
            QLineF(rect.topLeft(), rect.bottomLeft()),
 
327
            QLineF(rect.bottomRight(), rect.bottomLeft()),
 
328
            QLineF(rect.bottomRight(), rect.topRight())
 
329
        ]
 
330
        intersectLine = QLineF(p1, p2)
 
331
        intersectPoint = QPointF(0, 0)
 
332
        for line in lines:
 
333
            if intersectLine.intersect(line, intersectPoint) == \
 
334
               QLineF.BoundedIntersection:
 
335
                return intersectPoint
 
336
        return QPointF(-1.0, -1.0)
 
337
        
 
338
    def __findIntersection(self, p1, p2, p3, p4):
 
339
        """
 
340
        Method to calculate the intersection point of two lines.
 
341
        
 
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).
 
345
        
 
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
 
351
        
 
352
        <pre>
 
353
            Quadrant II     |   Quadrant I
 
354
           -----------------|-----------------
 
355
            Quadrant III    |   Quadrant IV
 
356
        </pre>
 
357
        
 
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)
 
360
        
 
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)
 
366
        """
 
367
        x1 = p1.y()
 
368
        y1 = p1.x()
 
369
        x2 = p2.y()
 
370
        y2 = p2.x()
 
371
        x3 = p3.y()
 
372
        y3 = p3.x()
 
373
        x4 = p4.y()
 
374
        y4 = p4.x()
 
375
        
 
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
 
380
        slope1 = 0.0
 
381
        slope2 = 0.0
 
382
        b1 = 0.0
 
383
        b2 = 0.0
 
384
        
 
385
        if x2 != x1:
 
386
            slope1 = (y2 - y1) / (x2 - x1)
 
387
            b1 = y1 - slope1 * x1
 
388
            no_line1 = False
 
389
        if x4 != x3:
 
390
            slope2 = (y4 - y3) / (x4 - x3)
 
391
            b2 = y3 - slope2 * x3
 
392
            no_line2 = False
 
393
        
 
394
        pt = QPointF()
 
395
        # if either line is not a function
 
396
        if no_line1 and no_line2:
 
397
            # if the lines are not the same one
 
398
            if x1 != x3:
 
399
                return QPointF(-1.0, -1.0)
 
400
            # if the lines are the same ones
 
401
            if y3 <= y4:
 
402
                if y3 <= y1 and y1 <= y4:
 
403
                    return QPointF(y1, x1)
 
404
                else:
 
405
                    return QPointF(y2, x2)
 
406
            else:
 
407
                if y4 <= y1 and y1 <= y3:
 
408
                    return QPointF(y1, x1)
 
409
                else:
 
410
                    return QPointF(y2, x2)
 
411
        elif no_line1:
 
412
            pt.setX(slope2 * x1 + b2)
 
413
            pt.setY(x1)
 
414
            if y1 >= y2:
 
415
                if not (y2 <= pt.x() and pt.x() <= y1):
 
416
                    pt.setX(-1.0)
 
417
                    pt.setY(-1.0)
 
418
            else:
 
419
                if not (y1 <= pt.x() and pt.x() <= y2):
 
420
                    pt.setX(-1.0)
 
421
                    pt.setY(-1.0)
 
422
            return pt
 
423
        elif no_line2:
 
424
            pt.setX(slope1 * x3 + b1)
 
425
            pt.setY(x3)
 
426
            if y3 >= y4:
 
427
                if not (y4 <= pt.x() and pt.x() <= y3):
 
428
                    pt.setX(-1.0)
 
429
                    pt.setY(-1.0)
 
430
            else:
 
431
                if not (y3 <= pt.x() and pt.x() <= y4):
 
432
                    pt.setX(-1.0)
 
433
                    pt.setY(-1.0)
 
434
            return pt
 
435
        
 
436
        if slope1 == slope2:
 
437
            pt.setX(-1.0)
 
438
            pt.setY(-1.0)
 
439
            return pt
 
440
        
 
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)):
 
446
                pt.setX(-1.0)
 
447
                pt.setY(-1.0)
 
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)):
 
450
                pt.setX(-1.0)
 
451
                pt.setY(-1.0)
 
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)):
 
454
                pt.setX(-1.0)
 
455
                pt.setY(-1.0)
 
456
        else:
 
457
            if not ((x2 <= pt.y() and pt.y() <= x1) and (y2 <= pt.x() and pt.x() <= y1)):
 
458
                pt.setX(-1.0)
 
459
                pt.setY(-1.0)
 
460
        
 
461
        return pt
 
462
        
 
463
    def widgetMoved(self):
 
464
        """
 
465
        Public method to recalculate the association after a widget was moved.
 
466
        """
 
467
        self.calculateEndingPoints()
 
468
        
 
469
    def unassociate(self):
 
470
        """
 
471
        Public method to unassociate from the widgets.
 
472
        """
 
473
        self.itemA.removeAssociation(self)
 
474
        self.itemB.removeAssociation(self)