Graphics/AssociationItem.py

changeset 0
de9c2efb9d02
child 13
1af94a91f439
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Graphics/AssociationItem.py	Mon Dec 28 16:03:33 2009 +0000
@@ -0,0 +1,473 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2004 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a graphics item for an association between two items.
+"""
+
+from PyQt4.QtCore import *
+from PyQt4.QtGui import *
+
+from E4Graphics.E4ArrowItem import E4ArrowItem, NormalArrow, WideArrow
+
+Normal = 0
+Generalisation = 1
+Imports = 2
+
+NoRegion = 0
+West = 1
+North = 2
+East = 3
+South = 4
+NorthWest = 5
+NorthEast = 6
+SouthEast = 7
+SouthWest = 8
+Center = 9
+
+class AssociationItem(E4ArrowItem):
+    """
+    Class implementing a graphics item for an association between two items.
+    
+    The association is drawn as an arrow starting at the first items and
+    ending at the second.
+    """
+    def __init__(self, itemA, itemB, type = Normal, parent = None):
+        """
+        Constructor
+        
+        @param itemA first widget of the association
+        @param itemB second widget of the association
+        @param type type of the association. This must be one of
+            <ul>
+            <li>Normal (default)</li>
+            <li>Generalisation</li>
+            <li>Imports</li>
+            </ul>
+        @keyparam parent reference to the parent object (QGraphicsItem)
+        """
+        if type == Normal:
+            arrowType = NormalArrow
+            arrowFilled = True
+        elif type == Imports:
+            arrowType = NormalArrow
+            arrowFilled = True
+        elif type == Generalisation:
+            arrowType = WideArrow
+            arrowFilled = False
+        
+        E4ArrowItem.__init__(self, QPointF(0, 0), QPointF(100, 100), 
+            arrowFilled, arrowType, parent)
+        
+        self.setFlag(QGraphicsItem.ItemIsMovable, False)
+        self.setFlag(QGraphicsItem.ItemIsSelectable, False)
+        
+##        self.calculateEndingPoints = self.__calculateEndingPoints_center
+        self.calculateEndingPoints = self.__calculateEndingPoints_rectangle
+        
+        self.itemA = itemA
+        self.itemB = itemB
+        self.assocType = type
+        
+        self.regionA = NoRegion
+        self.regionB = NoRegion
+        
+        self.calculateEndingPoints()
+        
+        self.itemA.addAssociation(self)
+        self.itemB.addAssociation(self)
+        
+    def __mapRectFromItem(self, item):
+        """
+        Private method to map item's rectangle to this item's coordinate system.
+        
+        @param item reference to the item to be mapped (QGraphicsRectItem)
+        @return item's rectangle in local coordinates (QRectF)
+        """
+        rect = item.rect()
+        tl = self.mapFromItem(item, rect.topLeft())
+        return QRectF(tl.x(), tl.y(), rect.width(), rect.height())
+        
+    def __calculateEndingPoints_center(self):
+        """
+        Private method to calculate the ending points of the association item.
+        
+        The ending points are calculated from the centers of the
+        two associated items.
+        """
+        if self.itemA is None or self.itemB is None:
+            return
+        
+        self.prepareGeometryChange()
+        
+        rectA = self.__mapRectFromItem(self.itemA)
+        rectB = self.__mapRectFromItem(self.itemB)
+        midA = QPointF(rectA.x() + rectA.width() / 2.0,
+                       rectA.y() + rectA.height() / 2.0)
+        midB = QPointF(rectB.x() + rectB.width() / 2.0,
+                       rectB.y() + rectB.height() / 2.0)
+        startP = self.__findRectIntersectionPoint(self.itemA, midA, midB)
+        endP = self.__findRectIntersectionPoint(self.itemB, midB, midA)
+        
+        if startP.x() != -1 and startP.y() != -1 and \
+           endP.x() != -1 and endP.y() != -1:
+            self.setPoints(startP.x(), startP.y(), endP.x(), endP.y())
+        
+    def __calculateEndingPoints_rectangle(self):
+        """
+        Private method to calculate the ending points of the association item.
+        
+        The ending points are calculated by the following method.
+        
+        For each item the diagram is divided in four Regions by its diagonals
+        as indicated below
+        <pre>
+                   \  Region 2  /
+                    \          /
+                     |--------|
+                     | \    / |
+                     |  \  /  |
+                     |   \/   |
+            Region 1 |   /\   | Region 3
+                     |  /  \  |
+                     | /    \ |
+                     |--------|
+                    /          \
+                   /  Region 4  \
+        </pre>
+        
+        Each diagonal is defined by two corners of the bounding rectangle
+        
+        To calculate the start point  we have to find out in which
+        region (defined by itemA's diagonals) is itemB's TopLeft corner 
+        (lets call it region M). After that the start point will be
+        the middle point of rectangle's side contained in region M.
+        
+        To calculate the end point we repeat the above but in the opposite direction
+        (from itemB to itemA)
+        """
+        if self.itemA is None or self.itemB is None:
+            return
+        
+        self.prepareGeometryChange()
+        
+        rectA = self.__mapRectFromItem(self.itemA)
+        rectB = self.__mapRectFromItem(self.itemB)
+        
+        xA = rectA.x() + rectA.width() / 2.0
+        yA = rectA.y() + rectA.height() / 2.0
+        xB = rectB.x() + rectB.width() / 2.0
+        yB = rectB.y() + rectB.height() / 2.0
+        
+        # find itemA region
+        rc = QRectF(xA, yA, rectA.width(), rectA.height())
+        oldRegionA = self.regionA
+        self.regionA = self.__findPointRegion(rc, xB, yB)
+        # move some regions to the standard ones
+        if self.regionA == NorthWest:
+            self.regionA = North
+        elif self.regionA == NorthEast:
+            self.regionA = East
+        elif self.regionA == SouthEast:
+            self.regionA = South
+        elif self.regionA == SouthWest:
+            self.regionA = West
+        elif self.regionA == Center:
+            self.regionA = West
+        
+        self.__updateEndPoint(self.regionA, True)
+        
+        # now do the same for itemB
+        rc = QRectF(xB, yB, rectB.width(), rectB.height())
+        oldRegionB = self.regionB
+        self.regionB = self.__findPointRegion(rc, xA, yA)
+        # move some regions to the standard ones
+        if self.regionB == NorthWest:
+            self.regionB = North
+        elif self.regionB == NorthEast:
+            self.regionB = East
+        elif self.regionB == SouthEast:
+            self.regionB = South
+        elif self.regionB == SouthWest:
+            self.regionB = West
+        elif self.regionB == Center:
+            self.regionB = West
+        
+        self.__updateEndPoint(self.regionB, False)
+        
+    def __findPointRegion(self, rect, posX, posY):
+        """
+        Private method to find out, which region of rectangle rect contains the point 
+        (PosX, PosY) and returns the region number.
+        
+        @param rect rectangle to calculate the region for (QRectF)
+        @param posX x position of point (float)
+        @param posY y position of point (float)
+        @return the calculated region number<br />
+            West = Region 1<br />
+            North = Region 2<br />
+            East = Region 3<br />
+            South = Region 4<br />
+            NorthWest = On diagonal 2 between Region 1 and 2<br />
+            NorthEast = On diagonal 1 between Region 2 and 3<br />
+            SouthEast = On diagonal 2 between Region 3 and 4<br />
+            SouthWest = On diagonal 1 between Region4 and 1<br />
+            Center = On diagonal 1 and On diagonal 2 (the center)<br />
+        """
+        w = rect.width()
+        h = rect.height()
+        x = rect.x()
+        y = rect.y()
+        slope2 = w / h
+        slope1 = -slope2
+        b1 = x + w / 2.0 - y * slope1
+        b2 = x + w / 2.0 - y * slope2
+        
+        eval1 = slope1 * posY + b1
+        eval2 = slope2 * posY + b2
+        
+        result = NoRegion
+        
+        # inside region 1
+        if eval1 > posX and eval2 > posX:
+            result = West
+        
+        #inside region 2
+        elif eval1 > posX and eval2 < posX:
+            result = North
+        
+        # inside region 3
+        elif eval1 < posX and eval2 < posX:
+            result = East
+        
+        # inside region 4
+        elif eval1 < posX and eval2 > posX:
+            result = South
+        
+        # inside region 5
+        elif eval1 == posX and eval2 < posX:
+            result = NorthWest
+        
+        # inside region 6
+        elif eval1 < posX and eval2 == posX:
+            result = NorthEast
+        
+        # inside region 7
+        elif eval1 == posX and eval2 > posX:
+            result = SouthEast
+        
+        # inside region 8
+        elif eval1 > posX and eval2 == posX:
+            result = SouthWest
+        
+        # inside region 9
+        elif eval1 == posX and eval2 == posX:
+            result = Center
+        
+        return result
+        
+    def __updateEndPoint(self, region, isWidgetA):
+        """
+        Private method to update an endpoint.
+        
+        @param region the region for the endpoint (integer)
+        @param isWidgetA flag indicating update for itemA is done (boolean)
+        """
+        if region == NoRegion:
+            return
+        
+        if isWidgetA:
+            rect = self.__mapRectFromItem(self.itemA)
+        else:
+            rect = self.__mapRectFromItem(self.itemB)
+        x = rect.x()
+        y = rect.y()
+        ww = rect.width()
+        wh = rect.height()
+        ch = wh / 2.0
+        cw = ww / 2.0
+        
+        if region == West:
+            px = x
+            py = y + ch
+        elif region == North:
+            px = x + cw
+            py = y
+        elif region == East:
+            px = x + ww
+            py = y + ch
+        elif region == South:
+            px = x + cw
+            py = y + wh
+        elif region == Center:
+            px = x + cw
+            py = y + wh
+        
+        if isWidgetA:
+            self.setStartPoint(px, py)
+        else:
+            self.setEndPoint(px, py)
+        
+    def __findRectIntersectionPoint(self, item, p1, p2):
+        """
+        Private method to find the intersetion point of a line with a rectangle.
+        
+        @param item item to check against
+        @param p1 first point of the line (QPointF)
+        @param p2 second point of the line (QPointF)
+        @return the intersection point (QPointF)
+        """
+        rect = self.__mapRectFromItem(item)
+        lines = [
+            QLineF(rect.topLeft(), rect.topRight()),
+            QLineF(rect.topLeft(), rect.bottomLeft()),
+            QLineF(rect.bottomRight(), rect.bottomLeft()),
+            QLineF(rect.bottomRight(), rect.topRight())
+        ]
+        intersectLine = QLineF(p1, p2)
+        intersectPoint = QPointF(0, 0)
+        for line in lines:
+            if intersectLine.intersect(line, intersectPoint) == \
+               QLineF.BoundedIntersection:
+                return intersectPoint
+        return QPointF(-1.0, -1.0)
+        
+    def __findIntersection(self, p1, p2, p3, p4):
+        """
+        Method to calculate the intersection point of two lines.
+        
+        The first line is determined by the points p1 and p2, the second
+        line by p3 and p4. If the intersection point is not contained in
+        the segment p1p2, then it returns (-1.0, -1.0).
+        
+        For the function's internal calculations remember:<br />
+        QT coordinates start with the point (0,0) as the topleft corner
+        and x-values increase from left to right and y-values increase
+        from top to bottom; it means the visible area is quadrant I in
+        the regular XY coordinate system
+        
+        <pre>
+            Quadrant II     |   Quadrant I
+           -----------------|-----------------
+            Quadrant III    |   Quadrant IV
+        </pre>
+        
+        In order for the linear function calculations to work in this method
+        we must switch x and y values (x values become y values and viceversa)
+        
+        @param p1 first point of first line (QPointF)
+        @param p2 second point of first line (QPointF)
+        @param p3 first point of second line (QPointF)
+        @param p4 second point of second line (QPointF)
+        @return the intersection point (QPointF)
+        """
+        x1 = p1.y()
+        y1 = p1.x()
+        x2 = p2.y()
+        y2 = p2.x()
+        x3 = p3.y()
+        y3 = p3.x()
+        x4 = p4.y()
+        y4 = p4.x()
+        
+        # line 1 is the line between (x1, y1) and (x2, y2)
+        # line 2 is the line between (x3, y3) and (x4, y4)
+        no_line1 = True    # it is false, if line 1 is a linear function
+        no_line2 = True    # it is false, if line 2 is a linear function
+        slope1 = 0.0
+        slope2 = 0.0
+        b1 = 0.0
+        b2 = 0.0
+        
+        if x2 != x1:
+            slope1 = (y2 - y1) / (x2 - x1)
+            b1 = y1 - slope1 * x1
+            no_line1 = False
+        if x4 != x3:
+            slope2 = (y4 - y3) / (x4 - x3)
+            b2 = y3 - slope2 * x3
+            no_line2 = False
+        
+        pt = QPointF()
+        # if either line is not a function
+        if no_line1 and no_line2:
+            # if the lines are not the same one
+            if x1 != x3:
+                return QPointF(-1.0, -1.0)
+            # if the lines are the same ones
+            if y3 <= y4:
+                if y3 <= y1 and y1 <= y4:
+                    return QPointF(y1, x1)
+                else:
+                    return QPointF(y2, x2)
+            else:
+                if y4 <= y1 and y1 <= y3:
+                    return QPointF(y1, x1)
+                else:
+                    return QPointF(y2, x2)
+        elif no_line1:
+            pt.setX(slope2 * x1 + b2)
+            pt.setY(x1)
+            if y1 >= y2:
+                if not (y2 <= pt.x() and pt.x() <= y1):
+                    pt.setX(-1.0)
+                    pt.setY(-1.0)
+            else:
+                if not (y1 <= pt.x() and pt.x() <= y2):
+                    pt.setX(-1.0)
+                    pt.setY(-1.0)
+            return pt
+        elif no_line2:
+            pt.setX(slope1 * x3 + b1)
+            pt.setY(x3)
+            if y3 >= y4:
+                if not (y4 <= pt.x() and pt.x() <= y3):
+                    pt.setX(-1.0)
+                    pt.setY(-1.0)
+            else:
+                if not (y3 <= pt.x() and pt.x() <= y4):
+                    pt.setX(-1.0)
+                    pt.setY(-1.0)
+            return pt
+        
+        if slope1 == slope2:
+            pt.setX(-1.0)
+            pt.setY(-1.0)
+            return pt
+        
+        pt.setY((b2 - b1) / (slope1 - slope2))
+        pt.setX(slope1 * pt.y() + b1)
+        # the intersection point must be inside the segment (x1, y1) (x2, y2)
+        if x2 >= x1 and y2 >= y1:
+            if not ((x1 <= pt.y() and pt.y() <= x2) and (y1 <= pt.x() and pt.x() <= y2)):
+                pt.setX(-1.0)
+                pt.setY(-1.0)
+        elif x2 < x1 and y2 >= y1:
+            if not ((x2 <= pt.y() and pt.y() <= x1) and (y1 <= pt.x() and pt.x() <= y2)):
+                pt.setX(-1.0)
+                pt.setY(-1.0)
+        elif x2 >= x1 and y2 < y1:
+            if not ((x1 <= pt.y() and pt.y() <= x2) and (y2 <= pt.x() and pt.x() <= y1)):
+                pt.setX(-1.0)
+                pt.setY(-1.0)
+        else:
+            if not ((x2 <= pt.y() and pt.y() <= x1) and (y2 <= pt.x() and pt.x() <= y1)):
+                pt.setX(-1.0)
+                pt.setY(-1.0)
+        
+        return pt
+        
+    def widgetMoved(self):
+        """
+        Public method to recalculate the association after a widget was moved.
+        """
+        self.calculateEndingPoints()
+        
+    def unassociate(self):
+        """
+        Public method to unassociate from the widgets.
+        """
+        self.itemA.removeAssociation(self)
+        self.itemB.removeAssociation(self)

eric ide

mercurial