Пример #1
0
class OverlapTests(unittest.TestCase):
    def setUp(self):
        class FakeGui:
            def stateofthenation(self,
                                 recalibrate=False,
                                 auto_resize_canvas=True):
                pass

        self.g = Graph()
        self.overlap_remover = OverlapRemoval(self.g, margin=5, gui=FakeGui())

    def tearDown(self):
        pass

    def testStress1(self):

        for i in range(10):
            self.g.LoadGraphFromStrings(TEST_GRAPH5_STRESS)
            print i,
            were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
            self.assertTrue(were_all_overlaps_removed)

            self.g.Clear()

    def testStress2_InitialBoot(self):
        """
        This is the slowest stress test because it runs the spring layout several times.
        """

        from layout.layout_spring import GraphLayoutSpring
        from layout.coordinate_mapper import CoordinateMapper

        self.g.LoadGraphFromStrings(
            GRAPH_INITIALBOOT)  # load the scenario ourselves

        layouter = GraphLayoutSpring(self.g)
        coordmapper = CoordinateMapper(self.g, (800, 800))

        def AllToLayoutCoords():
            coordmapper.AllToLayoutCoords()

        def AllToWorldCoords():
            coordmapper.AllToWorldCoords()

        for i in range(8):
            print i,

            AllToLayoutCoords()
            layouter.layout(keep_current_positions=False)
            AllToWorldCoords()

            were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
            self.assertTrue(were_all_overlaps_removed)
Пример #2
0
class UmlCanvas(ogl.ShapeCanvas):
    def __init__(self, parent, log, frame):
        ogl.ShapeCanvas.__init__(self, parent)

        self.observers = multicast()
        self.app = None  # assigned later by app boot

        self.log = log
        self.frame = frame
        self.SetBackgroundColour("LIGHT BLUE")

        self.SetDiagram(ogl.Diagram())
        self.GetDiagram().SetCanvas(self)

        wx.EVT_WINDOW_DESTROY(self, self.OnDestroy)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheelZoom)
        self.Bind(wx.EVT_KEY_DOWN, self.onKeyPress)
        self.Bind(wx.EVT_CHAR, self.onKeyChar)

        self.font1 = wx.Font(14, wx.MODERN, wx.NORMAL, wx.NORMAL, False)
        self.font2 = wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL, False)

        self.working = False
        self._kill_layout = False  # flag to communicate with layout engine.  aborting keypress in gui should set this to true

        @property
        def kill_layout(self):
            return self._kill_layout

        @kill_layout.setter
        def kill_layout(self, value):
            self._kill_layout = value

    def InitSizeAndObjs(self):
        # Only call this once enclosing frame has been set up, so that get correct world coord dimensions

        self.canvas_resizer = CanvasResizer(canvas=self)

        # Don't assert canvas size sanity anymore as wxpython3 (phoenix) doesn't set canvas size
        # as quickly as wxpython2.8 does, even though frame has been sized and shown
        # with frame.SetSize(WINDOW_SIZE) and frame.Show(True)
        # In wxpython3 (phoenix) canvas stays at (20,20) despite the frame increasing in size to (1024,768)
        # but good ole wxpython2.8 does indeed change canvas size immediately to (1024,768)
        #
        # assert not self.canvas_resizer.canvas_too_small(), "InitSizeAndObjs being called too early - please set up enclosing frame size first"

        self.umlworkspace = UmlWorkspace()
        self.layout = LayoutBasic(leftmargin=5,
                                  topmargin=5,
                                  verticalwhitespace=50,
                                  horizontalwhitespace=50,
                                  maxclassesperline=7)
        self.snapshot_mgr = GraphSnapshotMgr(graph=self.umlworkspace.graph,
                                             umlcanvas=self)
        self.coordmapper = CoordinateMapper(self.umlworkspace.graph,
                                            self.GetSize())
        self.layouter = GraphLayoutSpring(self.umlworkspace.graph, gui=self)
        self.overlap_remover = OverlapRemoval(self.umlworkspace.graph,
                                              margin=50,
                                              gui=self)

    def AllToLayoutCoords(self):
        self.coordmapper.AllToLayoutCoords()

    def AllToWorldCoords(self):
        self.coordmapper.AllToWorldCoords()

    def onKeyPress(self, event):
        keycode = event.GetKeyCode(
        )  # http://www.wxpython.org/docs/api/wx.KeyEvent-class.html

        if self.working:
            event.Skip()
            return
        self.working = True

        if keycode == wx.WXK_ESCAPE:
            print "ESC key detected: Abort Layout"
            self.kill_layout = True

        if keycode == wx.WXK_RIGHT:
            self.app.run.CmdLayoutExpand(remove_overlaps=not event.ShiftDown())

        elif keycode == wx.WXK_LEFT:
            self.app.run.CmdLayoutContract(
                remove_overlaps=not event.ShiftDown())

        self.working = False
        event.Skip()

    def onKeyChar(self, event):
        """
        These are secret keycodes not exposed on the menu
        Normally shortcuts added on the menu work fine.
        """
        if event.GetKeyCode() >= 256:
            event.Skip()
            return
        if self.working:
            event.Skip()
            return
        self.working = True

        keycode = chr(event.GetKeyCode())

        if keycode in ['q', 'Q']:
            self.NewEdgeMarkFrom()

        elif keycode in ['w', 'W']:
            self.NewEdgeMarkTo(edge_type='composition')

        elif keycode in ['e', 'E']:
            self.NewEdgeMarkTo(edge_type='generalisation')

        elif keycode in ['1', '2', '3', '4', '5', '6', '7', '8']:
            todisplay = ord(keycode) - ord('1')
            self.snapshot_mgr.Restore(todisplay)

        elif keycode == 'P':
            self.Refresh()

        elif keycode in ['d', 'D']:
            self.app.run.CmdDumpUmlWorkspace()

        elif keycode == 's':
            self.CmdTrimScrollbars()

        elif keycode == 'G':  # and event.ShiftDown() and event.ControlDown():
            self.app.run.CmdBuildColourChartWorkspace()

        elif keycode in ['h', 'H']:
            self.app.run.CmdColourSequential(
                color_range_offset=(keycode == 'H'))

        self.working = False
        event.Skip()

    def CmdTrimScrollbars(self):
        self.canvas_resizer.resize_virtual_canvas_tofit_bounds(
            shrinkage_leeway=0, bounds_dirty=True)

    def CmdRememberLayout1(self):
        self.snapshot_mgr.QuickSave(slot=1)

    def CmdRememberLayout2(self):
        self.snapshot_mgr.QuickSave(slot=2)

    def CmdRestoreLayout1(self):
        self.snapshot_mgr.QuickRestore(slot=1)

    def CmdRestoreLayout2(self):
        self.snapshot_mgr.QuickRestore(slot=2)

    def SelectNodeNow(self, shape):
        canvas = shape.GetCanvas()

        self.app.run.CmdDeselectAllShapes()

        dc = wx.ClientDC(canvas)
        canvas.PrepareDC(dc)
        shape.Select(
            True, dc
        )  # could pass None as dc if you don't want to trigger the OnDrawControlPoints(dc) handler immediately - e.g. if you want to do a complete redraw of everything later anyway

        # change colour when select
        #shape.SetBrush(wx.WHITE_BRUSH) #wx.Brush("WHEAT", wx.SOLID))
        #canvas.Refresh(False) # works
        #canvas.Redraw(dc) # works too
        #shape.Draw(dc) # works too, most efficient

        #canvas.Refresh(False)   # t/f or don't use - doesn't seem to make a difference

        #self.UpdateStatusBar(shape)  # only available in the shape evt handler (this method used to live there...)

    def delete_shape_view(self, shape):
        # View
        self.app.run.CmdDeselectAllShapes()
        for line in shape.GetLines()[:]:
            line.Delete()
        shape.Delete()

    def Clear(self):
        print "Draw: Clear"
        self.GetDiagram().DeleteAllShapes()

        dc = wx.ClientDC(self)
        self.GetDiagram().Clear(
            dc
        )  # Clears screen - don't prepare the dc or it will only clear the top scrolled bit (see my mailing list discussion)

        self.umlworkspace.Clear()

        if "wxMac" in wx.PlatformInfo:  # Hack on Mac so that onKeyChar bindings take hold properly.
            wx.CallAfter(self.SetFocus)
        elif 'wxGTK' in wx.PlatformInfo:  # Hack on Linux so that onKeyChar bindings take hold properly.
            wx.CallLater(1500, self.app.context.wxapp.multiText.SetFocus)
            wx.CallLater(1500, self.SetFocus)

    def NewEdgeMarkFrom(self):
        selected = [
            s for s in self.GetDiagram().GetShapeList() if s.Selected()
        ]
        if not selected:
            print "Please select a node"
            return

        self.new_edge_from = selected[0].node
        print "From", self.new_edge_from.id

    def NewEdgeMarkTo(self, edge_type='composition'):
        selected = [
            s for s in self.GetDiagram().GetShapeList() if s.Selected()
        ]
        if not selected:
            print "Please select a node"
            return

        tonode = selected[0].node
        print "To", tonode.id

        if self.new_edge_from == None:
            print "Please set from node first"
            return

        if self.new_edge_from.id == tonode.id:
            print "Can't link to self"
            return

        if not self.umlworkspace.graph.FindNodeById(self.new_edge_from.id):
            print "From node %s doesn't seem to be in graph anymore!" % self.new_edge_from.id
            return

        edge = self.umlworkspace.graph.AddEdge(
            tonode, self.new_edge_from,
            weight=None)  # swap direction as is a directional composition.
        # TODO should also arguably add to umlworkspace's associations_composition or associations_generalisation list (or create a new one for unlabelled associations like the one we are creating here)
        #edge['uml_edge_type'] = ''
        edge['uml_edge_type'] = edge_type
        self.CreateUmlEdge(edge)
        self.stateofthenation()

    def CreateImageShape(self, F):
        #shape = ogl.BitmapShape()
        shape = BitmapShapeResizable()
        img = wx.Image(F, wx.BITMAP_TYPE_ANY)

        #adjusted_img = img.AdjustChannels(factor_red = 1., factor_green = 1., factor_blue = 1., factor_alpha = 0.5)
        #adjusted_img = img.Rescale(10,10)
        adjusted_img = img

        bmp = wx.BitmapFromImage(adjusted_img)
        shape.SetBitmap(bmp)

        self.GetDiagram().AddShape(shape)
        shape.Show(True)

        evthandler = UmlShapeHandler(
            self.log, self.frame, self
        )  # just init the handler with whatever will be convenient for it to know.
        evthandler.SetShape(shape)
        evthandler.SetPreviousHandler(shape.GetEventHandler())
        shape.SetEventHandler(evthandler)
        self.new_evthandler_housekeeping(evthandler)

        setpos(shape, 0, 0)
        #setpos(shape, node.left, node.top)
        #node.width, node.height = shape.GetBoundingBoxMax()
        #node.shape = shape
        #shape.node = node
        return shape

    def CreateUmlShape(self, node):
        def newRegion(font, name, textLst, maxWidth, totHeight=10):
            # Taken from Boa, but put into the canvas class instead of the scrolled window class.
            region = ogl.ShapeRegion()

            if len(textLst) == 0:
                return region, maxWidth, 0

            dc = wx.ClientDC(self)  # self is the canvas
            dc.SetFont(font)

            for text in textLst:
                w, h = dc.GetTextExtent(text)
                if w > maxWidth: maxWidth = w
                totHeight = totHeight + h + 0  # interline padding

            region.SetFont(font)
            region.SetText('\n'.join(textLst))
            region.SetName(name)

            return region, maxWidth, totHeight

        shape = DividedShape(width=99, height=98, canvas=self)
        maxWidth = 10  # min node width, grows as we update it with text widths
        """
        Future: Perhaps be able to show regions or not. Might need to totally
        reconstruct the shape.
        """
        #if not self.showAttributes: classAttrs = [' ']
        #if not self.showMethods: classMeths = [' ']

        # Create each region.  If height returned is 0 this means don't create this region.
        regionName, maxWidth, nameHeight = newRegion(self.font1, 'class_name',
                                                     [node.classname],
                                                     maxWidth)
        regionAttribs, maxWidth, attribsHeight = newRegion(
            self.font2, 'attributes', node.attrs, maxWidth)
        regionMeths, maxWidth, methsHeight = newRegion(self.font2, 'methods',
                                                       node.meths, maxWidth)

        # Work out total height of shape
        totHeight = nameHeight + attribsHeight + methsHeight

        # Set regions to be a proportion of the total height of shape
        regionName.SetProportions(0.0, 1.0 * (nameHeight / float(totHeight)))
        regionAttribs.SetProportions(0.0,
                                     1.0 * (attribsHeight / float(totHeight)))
        regionMeths.SetProportions(0.0, 1.0 * (methsHeight / float(totHeight)))

        # Add regions to the shape
        shape.AddRegion(regionName)
        if attribsHeight:  # Dont' make a region unless we have to
            shape.AddRegion(regionAttribs)
        if methsHeight:  # Dont' make a region unless we have to
            shape.AddRegion(regionMeths)

        shape.SetSize(maxWidth + 10, totHeight + 10)
        shape.SetCentreResize(
            False
        )  # Specify whether the shape is to be resized from the centre (the centre stands still) or from the corner or side being dragged (the other corner or side stands still).

        regionName.SetFormatMode(ogl.FORMAT_CENTRE_HORIZ)
        shape.region1 = regionName  # Andy added, for later external reference to classname from just having the shape instance.

        shape.SetDraggable(True, True)
        shape.SetCanvas(self)
        shape.SetPen(
            wx.BLACK_PEN)  # Controls the color of the border of the shape
        shape.SetBrush(wx.Brush("WHEAT", wx.SOLID))
        setpos(shape, node.left, node.top)
        self.GetDiagram().AddShape(
            shape
        )  # self.AddShape is ok too, ShapeCanvas's AddShape is delegated back to Diagram's AddShape.  ShapeCanvas-->Diagram
        shape.Show(True)

        evthandler = UmlShapeHandler(
            self.log, self.frame, self
        )  # just init the handler with whatever will be convenient for it to know.
        evthandler.SetShape(shape)
        evthandler.SetPreviousHandler(shape.GetEventHandler())
        shape.SetEventHandler(evthandler)
        self.new_evthandler_housekeeping(evthandler)

        shape.FlushText()

        # Don't set the node left,top here as the shape needs to conform to the node.
        # On the other hand the node needs to conform to the shape's width,height.
        # ACTUALLY I now do set the pos of the shape, see above,
        # just before the AddShape() call.
        #
        node.width, node.height = shape.GetBoundingBoxMax(
        )  # TODO: Shouldn't this be in node coords not world coords?
        node.shape = shape
        shape.node = node
        return shape

    def createNodeShape(self, node):  # FROM SPRING LAYOUT
        shape = ogl.RectangleShape(node.width, node.height)
        shape.AddText(node.id)
        setpos(shape, node.left, node.top)
        #shape.SetDraggable(True, True)
        self.AddShape(shape)
        node.shape = shape
        shape.node = node

        # wire in the event handler for the new shape
        evthandler = UmlShapeHandler(
            None, self.frame, self
        )  # just init the handler with whatever will be convenient for it to know.
        evthandler.SetShape(shape)
        evthandler.SetPreviousHandler(shape.GetEventHandler())
        shape.SetEventHandler(evthandler)
        self.new_evthandler_housekeeping(evthandler)

    def createCommentShape(self, node):
        shape = ogl.TextShape(node.width, node.height)

        shape.SetCanvas(self)
        shape.SetPen(wx.BLACK_PEN)
        shape.SetBrush(wx.LIGHT_GREY_BRUSH)
        shape.SetBrush(wx.RED_BRUSH)
        for line in node.comment.split('\n'):
            shape.AddText(line)

        setpos(shape, node.left, node.top)
        #shape.SetDraggable(True, True)
        self.AddShape(shape)
        node.shape = shape
        shape.node = node

        # wire in the event handler for the new shape
        evthandler = UmlShapeHandler(
            None, self.frame, self
        )  # just init the handler with whatever will be convenient for it to know.
        evthandler.SetShape(shape)
        evthandler.SetPreviousHandler(shape.GetEventHandler())
        shape.SetEventHandler(evthandler)
        self.new_evthandler_housekeeping(evthandler)

    def new_evthandler_housekeeping(self, evthandler):
        # notify app of this new evthandler so app can
        # assign the evthandler's .app attribute.
        # Or could have just done:
        #   evthandler.app = self.app
        # here.  But we may need observer for other things later.
        self.observers.NOTIFY_EVT_HANDLER_CREATED(evthandler)

    def CreateUmlEdge(self, edge):
        """
        @startuml

        class ArrowHead {
            _arrowType
            __init__(   type, end, size, xOffset, name, mf, arrowId)
        }
        class Shape {
            SetCanvas()
            SetPen()
            SetBrush()
        }
        class LineShape <<lib.ogl._lines.py>> {
            _from
            _to
            _arcArrows
            AddArrow(arrowtype)
            DrawArrow(self, dc, arrow, XOffset, proportionalOffset)
            MakeLineControlPoints(2)
        }
        class LineShapeCustom <<src.gui.uml_lines.py>> {
            DrawArrow(self, dc, arrow, XOffset, proportionalOffset)
        }

        Shape <|-- LineShape
        LineShape <|-- LineShapeCustom
        LineShape --> "0..*" ArrowHead : _arcArrows

        note as N1
            custom line drawing introduced
                ARROW_UML_GENERALISATION
                ARROW_UML_COMPOSITION
        end note

        N1 .. LineShapeCustom
        @enduml

        """
        from gui.uml_lines import LineShapeCustom, ARROW_UML_GENERALISATION, ARROW_UML_COMPOSITION

        fromShape = edge['source'].shape
        toShape = edge['target'].shape

        edge_label = edge.get('uml_edge_type', '')
        if edge_label == 'generalisation':
            arrowtype = ARROW_UML_GENERALISATION  # used to be ogl.ARROW_ARROW
        elif edge_label == 'composition':
            arrowtype = ARROW_UML_COMPOSITION  # used to be ogl.ARROW_FILLED_CIRCLE
        else:
            arrowtype = None

        line = LineShapeCustom()  # used to be ogl.LineShape()

        line.SetCanvas(self)
        line.SetPen(wx.BLACK_PEN)
        line.SetBrush(wx.BLACK_BRUSH)
        if arrowtype:
            line.AddArrow(arrowtype)
        line.MakeLineControlPoints(2)

        fromShape.AddLine(line, toShape)
        self.GetDiagram().AddShape(line)
        line.Show(True)

    def OnWheelZoom(self, event):
        #print "OnWheelZoom"
        if self.working: return
        self.working = True

        SCROLL_AMOUNT = 40
        if not event.ControlDown():
            oldscrollx = self.GetScrollPos(wx.HORIZONTAL)
            oldscrolly = self.GetScrollPos(wx.VERTICAL)
            if event.GetWheelRotation() < 0:
                self.Scroll(oldscrollx, oldscrolly + SCROLL_AMOUNT)
            else:
                self.Scroll(oldscrollx, max(0, oldscrolly - SCROLL_AMOUNT))
        else:
            if event.GetWheelRotation() < 0:
                self.app.run.CmdLayoutContract(
                    remove_overlaps=not event.ShiftDown())
            else:
                self.app.run.CmdLayoutExpand(
                    remove_overlaps=not event.ShiftDown())

        self.working = False

    # UTILITY - called by CmdFileImportSource, CmdFileLoadWorkspaceBase.LoadGraph
    def build_view(self, translatecoords=True):
        if translatecoords:
            self.AllToWorldCoords()

        # Clear existing visualisation
        for node in self.umlworkspace.graph.nodes:
            if node.shape:
                self.delete_shape_view(node.shape)
                node.shape = None

        # Create fresh visualisation
        for node in self.umlworkspace.graph.nodes:
            assert not node.shape
            shape = self.CreateUmlShape(node)
            self.umlworkspace.classnametoshape[
                node.
                id] = shape  # Record the name to shape map so that we can wire up the links later.

        for edge in self.umlworkspace.graph.edges:
            self.CreateUmlEdge(edge)

    # UTILITY - called by
    #
    #CmdInsertNewNodeClass, CmdInsertImage, CmdLayoutExpandContractBase,
    #umlwin.OnWheelZoom_OverlapRemoval_Defunct,
    #umlwin.layout_and_position_shapes,
    #UmlShapeHandler.OnEndDragLeft
    #UmlShapeHandler.OnSizingEndDragLeft
    #LayoutBlackboard.LayoutLoopTillNoChange
    #
    def remove_overlaps(self, watch_removals=True):
        """
        Returns T/F if any overlaps found, so caller can decide whether to
        redraw the screen
        """
        self.overlap_remover.RemoveOverlaps(watch_removals=watch_removals)
        return self.overlap_remover.GetStats()['total_overlaps_found'] > 0

    # UTILITY - called by everyone!!??
    #
    #CmdFileLoadWorkspaceBase, CmdInsertComment, CmdEditClass
    #CmdLayoutExpandContractBase,
    #CmdInsertNewNodeClass
    #umlwin.NewEdgeMarkTo
    #umlwin.OnWheelZoom_OverlapRemoval_Defunct
    #LayoutBlackboard.LayoutThenPickBestScale
    #LayoutBlackboard.Experiment1
    #LayoutBlackboard.LayoutLoopTillNoChange
    #LayoutBlackboard.ScaleUpMadly
    #LayoutBlackboard.GetVitalStats  (only if animate is true)
    #OverlapRemoval.RemoveOverlaps   ( refresh gui if self.gui and watch_removals)
    #GraphSnapshotMgr.RestoreGraph
    #
    # these do an overlap removal first before calling here
    #
    #CmdInsertImage
    #umlwin.layout_and_position_shapes,
    #UmlShapeHandler.OnEndDragLeft
    #UmlShapeHandler.OnSizingEndDragLeft
    #
    # recalibrate = True - called by core spring layout self.gui.stateofthenation()
    #
    # RENAME?: dc_DiagramClearAndRedraw
    #
    def stateofthenation(self, recalibrate=False, auto_resize_canvas=True):
        if recalibrate:  # was stateofthespring
            self.coordmapper.Recalibrate()
            self.AllToWorldCoords()

        dc = wx.ClientDC(self)
        self.PrepareDC(dc)

        for node in self.umlworkspace.graph.nodes:
            node.shape.Move2(dc, node.left, node.top, display=False)
        self.Refresh()

        self.Update(
        )  # or wx.SafeYield()  # Without this the nodes don't paint during a "L" layout (edges do!?)
        # You need to be yielding or updating on a regular basis, so that when your OS/window manager sends repaint messages to your app, it can handle them. See http://stackoverflow.com/questions/10825128/wxpython-how-to-force-ui-refresh
        if auto_resize_canvas:
            self.canvas_resizer.resize_virtual_canvas_tofit_bounds(
                bounds_dirty=True)

    # UTILITY - used by CmdLayout and CmdFileImportBase
    def layout_and_position_shapes(self):
        self.canvas_resizer.frame_calibration(
            auto_resize_virtualcanvas=False
        )  # going to do a stateofthenation later so no point changing virt canvas now
        self.AllToLayoutCoords()
        self.layouter.layout(keep_current_positions=False, optimise=True)
        self.AllToWorldCoords()
        if self.remove_overlaps():
            self.stateofthenation()

    def get_umlboxshapes(self):
        #return [s for s in self.GetDiagram().GetShapeList() if not isinstance(s, ogl.LineShape)]
        return [
            s for s in self.GetDiagram().GetShapeList()
            if isinstance(s, DividedShape)
        ]  # TODO take into account images and other shapes

    umlboxshapes = property(get_umlboxshapes)

    def OnDestroy(self, evt):
        for shape in self.GetDiagram().GetShapeList():
            if shape.GetParent() == None:
                shape.SetCanvas(None)

    def OnLeftClick(self, x, y, keys):  # Override of ShapeCanvas method
        # keys is a bit list of the following: KEY_SHIFT  KEY_CTRL
        self.app.run.CmdDeselectAllShapes()
Пример #3
0
class GraphRendererOgl:
    def __init__(self, graph, oglcanvas):
        self.graph = graph
        self.oglcanvas = oglcanvas
        self.oglcanvas.graphrendererogl = self
        self.coordmapper = CoordinateMapper(self.graph, self.oglcanvas.GetSize())

        self.oglcanvas.Bind(wx.EVT_MOUSEWHEEL, self.OnWheelZoom)
        self.oglcanvas.Bind(wx.EVT_RIGHT_DOWN, self.OnRightButtonMenu)
        self.oglcanvas.Bind(wx.EVT_KEY_DOWN, self.onKeyPress)
        self.oglcanvas.Bind(wx.EVT_CHAR, self.onKeyChar)
        self.oglcanvas.Bind(wx.EVT_SIZE, self.OnResizeFrame)
        
        self.popupmenu = None
        self.need_abort = False
        self.new_edge_from = None
        self.working = False
        self.snapshot_mgr = GraphSnapshotMgr(graph=self.graph, umlcanvas=self)

        if UNIT_TESTING_MODE:
            self.overlap_remover = OverlapRemoval(self.graph, margin=5, gui=self)
        else:
            self.overlap_remover = OverlapRemoval(self.graph, margin=50, gui=self)

    def AllToLayoutCoords(self):
            self.coordmapper.AllToLayoutCoords()

    def AllToWorldCoords(self):
            self.coordmapper.AllToWorldCoords()

    def OnResizeFrame (self, event):   # ANDY  interesting - GetVirtualSize grows when resize frame
        frame = self.oglcanvas.GetTopLevelParent()
        print "frame resize", frame.GetClientSize()
        self.coordmapper.Recalibrate(frame.GetClientSize()) # may need to call self.CalcVirtSize() if scrolled window
   
    def DeselectAllShapes(self):
        selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()]
        if selected:
            s = selected[0]
            canvas = s.GetCanvas()
            dc = wx.ClientDC(canvas)
            canvas.PrepareDC(dc)
            s.Select(False, dc)
            canvas.Refresh(False)   # Need this or else Control points ('handles') leave blank holes      

    def InsertNewNode(self):
        id = 'D' + str(random.randint(1,99))
        dialog = wx.TextEntryDialog ( None, 'Enter an id string:', 'Create a new node', id )
        if dialog.ShowModal() == wx.ID_OK:
            id = dialog.GetValue()
            if self.graph.FindNodeById(id):
                id += str(random.randint(1,9999))
            node = GraphNode(id, random.randint(0, 100),random.randint(0,100),random.randint(60, 160),random.randint(60,160))
            node = self.graph.AddNode(node)
            self.createNodeShape(node)
            node.shape.Show(True)
            self.stateofthenation()
        dialog.Destroy()

    def DeleteSelectedNode(self):
        selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()]
        if selected:
            shape = selected[0]
            print 'delete', shape.node.id

            # model
            self.graph.DeleteNodeById(shape.node.id)

            # view
            self.DeselectAllShapes()
            for line in shape.GetLines()[:]:
                line.Delete()
            shape.Delete()

    def NewEdgeMarkFrom(self):
        selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()]
        if not selected:
            print "Please select a node"
            return
        
        self.new_edge_from = selected[0].node
        print "From", self.new_edge_from.id

    def NewEdgeMarkTo(self):
        selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()]
        if not selected:
            print "Please select a node"
            return
        
        tonode = selected[0].node
        print "To", tonode.id
        
        if self.new_edge_from == None:
            print "Please set from node first"
            return
        
        if self.new_edge_from.id == tonode.id:
            print "Can't link to self"
            return
        
        if not self.graph.FindNodeById(self.new_edge_from.id):
            print "From node %s doesn't seem to be in graph anymore!" % self.new_edge_from.id
            return
        
        edge = self.graph.AddEdge(self.new_edge_from, tonode, weight=None)
        self.createEdgeShape(edge)
        self.stateofthenation()

    def OnWheelZoom(self, event):
        if self.working: return
        self.working = True

        if event.GetWheelRotation() < 0:
            self.stage2()
            print self.overlap_remover.GetStats()
        else:
            self.stateofthenation()

        self.working = False

    def onKeyPress(self, event):
        keycode = event.GetKeyCode()  # http://www.wxpython.org/docs/api/wx.KeyEvent-class.html

        if self.working:
            event.Skip()
            return
        self.working = True

        if keycode == wx.WXK_DOWN:
            optimise = not event.ShiftDown()
            self.ReLayout(keep_current_positions=True, gui=self, optimise=optimise)

        elif keycode == wx.WXK_UP:
            optimise = not event.ShiftDown()
            self.ReLayout(keep_current_positions=False, gui=self, optimise=optimise)
            
        elif keycode == wx.WXK_RIGHT:
            if self.coordmapper.scale > 0.8:
                self.ChangeScale(-0.2, remap_world_to_layout=event.ShiftDown(), removeoverlaps=not event.ControlDown())
                print "expansion ", self.coordmapper.scale
            else:
                print "Max expansion prevented.", self.coordmapper.scale
            print "LL/raw %d/%d" % (len(self.graph.CountLineOverLineIntersections(ignore_nodes=False)), \
                                                len(self.graph.CountLineOverLineIntersections(ignore_nodes=True)))
            
        elif keycode == wx.WXK_LEFT:
            if self.coordmapper.scale < 3:
                self.ChangeScale(0.2, remap_world_to_layout=event.ShiftDown(), removeoverlaps=not event.ControlDown())
                print "contraction ", self.coordmapper.scale
            else:
                print "Min expansion thwarted.", self.coordmapper.scale
            print "LL/raw %d/%d" % (len(self.graph.CountLineOverLineIntersections(ignore_nodes=False)), \
                                                len(self.graph.CountLineOverLineIntersections(ignore_nodes=True)))
            
        elif keycode == wx.WXK_DELETE:
            self.DeleteSelectedNode()

        elif keycode == wx.WXK_INSERT:
            self.InsertNewNode()

        self.working = False
        event.Skip()

    def onKeyChar(self, event):
        if event.GetKeyCode() >= 256:
            event.Skip()
            return
        if self.working:
            event.Skip()
            return
        self.working = True
        
        keycode = chr(event.GetKeyCode())

        if keycode == 'q':
            self.NewEdgeMarkFrom()

        elif keycode == 'w':
            self.NewEdgeMarkTo()
            
        elif keycode == '(':
            self.snapshot_mgr.QuickSave(slot=1)
            
        elif keycode == ')':
            self.snapshot_mgr.QuickSave(slot=2)

        elif keycode == '9':
            self.snapshot_mgr.QuickRestore(slot=1)

        elif keycode == '0':
            self.snapshot_mgr.QuickRestore(slot=2)

        elif keycode in ['1','2','3','4','5','6','7','8']:
            todisplay = ord(keycode) - ord('1')
            self.snapshot_mgr.Restore(todisplay)

        elif keycode in ['x', 'X', 'z', 'Z', 'c', 'C']:
            if keycode in ['Z','z']:
                strategy = ":reduce pre overlap removal NN overlaps"
            elif keycode in ['X','x']:
                strategy = ":reduce post overlap removal LN crossings"
            elif keycode in ['C','c']:
                strategy = ":reduce post overlap removal LN and LL crossings"
            b = LayoutBlackboard(graph=self.graph, umlwin=self)
            b.LayoutThenPickBestScale(scramble=keycode in ['Z','X','C'], strategy=strategy)
            
        elif keycode in ['e',]:
            b = LayoutBlackboard(graph=self.graph, umlwin=self)
            b.Experiment1()

        elif keycode in ['r', 'R']:
            b = LayoutBlackboard(graph=self.graph, umlwin=self)
            b.LayoutLoopTillNoChange(scramble=keycode == 'R')

        elif keycode in ['b', 'B']:
            b = LayoutBlackboard(graph=self.graph, umlwin=self)
            b.LayoutMultipleChooseBest(4)
            
        elif keycode in ['?',]:
            self.DumpStatus()
            
        self.working = False
        event.Skip()

    def DumpStatus(self):
        #print "-"*50
        print "scale", self.coordmapper.scale
        print "line-line intersections", len(self.graph.CountLineOverLineIntersections())
        print "node-node overlaps", self.overlap_remover.CountOverlaps()
        print "line-node crossings", self.graph.CountLineOverNodeCrossings()['ALL']/2 #, self.graph.CountLineOverNodeCrossings()
        print "bounds", self.graph.GetBounds()
        
    def draw(self, translatecoords=True):
        self.stage1(translatecoords=translatecoords)
        #thread.start_new_thread(self.DoSomeLongTask, ())

    def stage1(self, translatecoords=True):
        import time
        
        if translatecoords:
            self.AllToWorldCoords()

        for node in self.graph.nodes:
            self.createNodeShape(node)
        for edge in self.graph.edges:
            self.createEdgeShape(edge)

        self.Redraw()

    def stage2(self, force_stateofthenation=False, watch_removals=True):
        ANIMATION = False
        
        if ANIMATION:
            self.graph.SaveOldPositionsForAnimationPurposes()
            watch_removals = False  # added this when I turned animation on.
        
        self.overlap_remover.RemoveOverlaps(watch_removals=watch_removals)
        if self.overlap_remover.GetStats()['total_overlaps_found'] > 0 or force_stateofthenation:
            self.stateofthenation(animate=ANIMATION)
        
    def stateofthenation(self, animate=False, recalibrate=False):
        if recalibrate:  # was stateofthespring
            self.coordmapper.Recalibrate()
            self.AllToWorldCoords()
        if animate:
            from animation import GeneratePoints
            
            for node in self.graph.nodes:
                node.anilist = GeneratePoints((node.previous_left, node.previous_top), (node.left, node.top), steps=5)

            dc = wx.ClientDC(self.oglcanvas)
            self.oglcanvas.PrepareDC(dc)
            for i in range(len(self.graph.nodes[0].anilist)):
                for node in self.graph.nodes:
                    point = node.anilist.pop(0)
                    x, y = point
                    #node.shape.Move(dc, x, y, True) # don't do this or it will flicker
                    setpos(node.shape, x, y)
                    node.shape.MoveLinks(dc)
                    self.Redraw(clear=False)
                self.Redraw()
            
        else:
            for node in self.graph.nodes:
                self.AdjustShapePosition(node)
            self.Redraw()
            wx.SafeYield()
        
    #def stateofthespring(self):
    #    self.coordmapper.Recalibrate()
    #    self.AllToWorldCoords()
    #    self.stateofthenation() # DON'T do overlap removal or it will get mad!

    def ChangeScale(self, delta, remap_world_to_layout=False, removeoverlaps=True):
        if remap_world_to_layout:
            self.AllToLayoutCoords()    # Experimental - only needed when you've done world coord changes 
        self.coordmapper.Recalibrate(scale=self.coordmapper.scale+delta)
        self.AllToWorldCoords()
        numoverlaps = self.overlap_remover.CountOverlaps()
        if removeoverlaps:
            self.stage2(force_stateofthenation=True, watch_removals=False) # does overlap removal and stateofthenation
        else:
            self.stateofthenation()
        
    def ReLayout(self, keep_current_positions=False, gui=None, optimise=True):
        self.AllToLayoutCoords()

        layouter = GraphLayoutSpring(self.graph, gui)    # should keep this around
        layouter.layout(keep_current_positions, optimise=optimise)
        
        self.AllToWorldCoords()
        self.stage2() # does overlap removal and stateofthenation
        
    def AdjustShapePosition(self, node, point=None):
        assert node.shape
        
        if point:
            x, y = point
        else:
            x, y = node.left, node.top
            
        # Don't need to use node.shape.Move(dc, x, y, False)
        setpos(node.shape, x, y)

        # But you DO need to use a dc to adjust the links
        dc = wx.ClientDC(self.oglcanvas)
        self.oglcanvas.PrepareDC(dc)
        node.shape.MoveLinks(dc)
        
    def Redraw(self, clear=True):
        diagram = self.oglcanvas.GetDiagram()
        canvas = self.oglcanvas
        assert canvas == diagram.GetCanvas()

        dc = wx.ClientDC(canvas)
        canvas.PrepareDC(dc)
        
        #for node in self.graph.nodes:    # TODO am still moving nodes in the pynsourcegui version?
        #    shape = node.shape
        #    shape.Move(dc, shape.GetX(), shape.GetY())
        
        if clear:
            diagram.Clear(dc)
        diagram.Redraw(dc)
     
    def createNodeShape(self, node):
        shape = ogl.RectangleShape( node.width, node.height )
        shape.AddText(node.id)
        setpos(shape, node.left, node.top)
        #shape.SetDraggable(True, True)
        self.oglcanvas.AddShape( shape )
        node.shape = shape
        shape.node = node
        
        # wire in the event handler for the new shape
        evthandler = MyEvtHandler(None, self.oglcanvas)
        evthandler.SetShape(shape)
        evthandler.SetPreviousHandler(shape.GetEventHandler())
        shape.SetEventHandler(evthandler)
       
    def createEdgeShape(self, edge):
        line = ogl.LineShape()
        line.SetCanvas(self.oglcanvas)
        line.SetPen(wx.BLACK_PEN)
        line.SetBrush(wx.BLACK_BRUSH)
        line.MakeLineControlPoints(2)
       
        fromShape = edge['source'].shape
        toShape = edge['target'].shape
        fromShape.AddLine(line, toShape)
        self.oglcanvas.GetDiagram().AddShape(line)
        line.Show(True)
        
    def OnRightButtonMenu(self, event):   # Menu
        x, y = event.GetPosition()
        frame = self.oglcanvas.GetTopLevelParent()
        
        if self.popupmenu:
            self.popupmenu.Destroy()    # wx.Menu objects need to be explicitly destroyed (e.g. menu.Destroy()) in this situation. Otherwise, they will rack up the USER Objects count on Windows; eventually crashing a program when USER Objects is maxed out. -- U. Artie Eoff  http://wiki.wxpython.org/index.cgi/PopupMenuOnRightClick
        self.popupmenu = wx.Menu()     # Create a menu
        
        item = self.popupmenu.Append(2015, "Load Graph from text...")
        frame.Bind(wx.EVT_MENU, self.OnLoadGraphFromText, item)
        
        item = self.popupmenu.Append(2017, "Dump Graph to console")
        frame.Bind(wx.EVT_MENU, self.OnSaveGraphToConsole, item)

        self.popupmenu.AppendSeparator()

        item = self.popupmenu.Append(2011, "Load Graph...")
        frame.Bind(wx.EVT_MENU, self.OnLoadGraph, item)
        
        item = self.popupmenu.Append(2012, "Save Graph...")
        frame.Bind(wx.EVT_MENU, self.OnSaveGraph, item)

        self.popupmenu.AppendSeparator()

        imp = wx.Menu()
        item = imp.Append(2021, "Load Test Graph 1"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph1, item)
        item = imp.Append(2022, "Load Test Graph 2"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph2, item)
        item = imp.Append(2023, "Load Test Graph 3"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph3, item)
        item = imp.Append(2043, "Load Test Graph 3a"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph3a, item)
        item = imp.Append(2024, "Load Test Graph 4"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph4, item)
        item = imp.Append(2025, "Load Test Graph 6 (line overlaps)"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph6, item)
        item = imp.Append(2026, "Load Test Graph 7"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph7, item)
        item = imp.Append(2046, "Load Test Graph 8 (up snug)"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph8, item)
        self.popupmenu.AppendMenu(-1, 'Unit Test Graphs', imp)

        imp = wx.Menu()
        item = imp.Append(2031, "Spring 2"); frame.Bind(wx.EVT_MENU, self.OnLoadSpring2, item)
        item = imp.Append(2032, "Spring 3"); frame.Bind(wx.EVT_MENU, self.OnLoadSpring3, item)
        imp.AppendSeparator()
        item = imp.Append(2033, "Initial Boot"); frame.Bind(wx.EVT_MENU, self.OnLoadInitialBoot, item)
        self.popupmenu.AppendMenu(-1, 'Other Test Graphs', imp)

        self.popupmenu.AppendSeparator()

        item = self.popupmenu.Append(2014, "Clear")
        frame.Bind(wx.EVT_MENU, self.OnClear, item)  # must pass item

        item = self.popupmenu.Append(2013, "Cancel")
        #frame.Bind(wx.EVT_MENU, self.OnPopupItemSelected, item)
        
        frame.PopupMenu(self.popupmenu, wx.Point(x,y))

    def OnLoadTestGraph1(self, event):
        self.LoadGraph(TEST_GRAPH1)
        
    def OnLoadTestGraph2(self, event):
        self.LoadGraph(TEST_GRAPH2)
        
    def OnLoadTestGraph3(self, event):
        self.LoadGraph(TEST_GRAPH3)

    def OnLoadTestGraph3a(self, event):
        self.LoadGraph(TEST_GRAPH3A)

    def OnLoadTestGraph4(self, event):
        self.LoadGraph(TEST_GRAPH4)

    def OnLoadTestGraph6(self, event):
        self.LoadGraph(TEST_GRAPH6)

    def OnLoadTestGraph7(self, event):
        self.LoadGraph(TEST_GRAPH7)

    def OnLoadTestGraph8(self, event):
        self.LoadGraph(TEST_GRAPH8)

    def OnLoadSpring2(self, event):
        self.LoadGraph(GRAPH_SPRING2)

    def OnLoadSpring3(self, event):
        self.LoadGraph(GRAPH_SPRING3)

    def OnLoadInitialBoot(self, event):
        self.LoadGraph(GRAPH_INITIALBOOT)
 
    def OnSaveGraphToConsole(self, event):
        print self.graph.GraphToString()

    def OnSaveGraph(self, event):
        frame = self.oglcanvas.GetTopLevelParent()
        dlg = wx.FileDialog(parent=frame, message="choose", defaultDir='.',
            defaultFile="", wildcard="*.txt", style=wx.FD_SAVE, pos=wx.DefaultPosition)
        if dlg.ShowModal() == wx.ID_OK:
            filename = dlg.GetPath()
            
            fp = open(filename, "w")
            fp.write(self.graph.GraphToString())
            fp.close()
        dlg.Destroy()
        
    def OnLoadGraphFromText(self, event):
        eg = "{'type':'node', 'id':'A', 'x':142, 'y':129, 'width':250, 'height':250}"
        dialog = wx.TextEntryDialog ( None, 'Enter an node/edge persistence strings:', 'Create a new node', eg,  style=wx.OK|wx.CANCEL|wx.TE_MULTILINE )
        if dialog.ShowModal() == wx.ID_OK:
            txt = dialog.GetValue()
            self.LoadGraph(txt)
            
    def OnLoadGraph(self, event):
        frame = self.oglcanvas.GetTopLevelParent()
        dlg = wx.FileDialog(parent=frame, message="choose", defaultDir='.',
            defaultFile="", wildcard="*.txt", style=wx.OPEN, pos=wx.DefaultPosition)
        if dlg.ShowModal() == wx.ID_OK:
            filename = dlg.GetPath()

            fp = open(filename, "r")
            s = fp.read()
            fp.close()

            self.LoadGraph(s)
        dlg.Destroy()

    def LoadGraph(self, filedata=""):
        self.Clear()
        
        self.graph.LoadGraphFromStrings(filedata)
                
        # build view from model
        self.draw(translatecoords=False)

        # set layout coords to be in sync with world, so that if expand scale things will work
        self.coordmapper.Recalibrate()
        self.AllToLayoutCoords()
        
        # refresh view
        self.oglcanvas.GetDiagram().ShowAll(1) # need this, yes
        self.stateofthenation()

    def OnClear(self, event):
        self.Clear()
        
    def Clear(self):
        # Clear view        
        self.oglcanvas.GetDiagram().DeleteAllShapes()
        dc = wx.ClientDC(self.oglcanvas)
        self.oglcanvas.GetDiagram().Clear(dc)   # only ends up calling dc.Clear() - I wonder if this clears the screen?
        
        # clear model
        self.graph.Clear()
        
    def DoSomeLongTask(self):
        
        for i in range(1,50):
            if self.need_abort:
                print "aborted."
                return
            wx.CallAfter(self.stage2)
            #print '*',
            #wx.CallAfter(self.DoStuff)
            time.sleep(2)   # lets events through to the main wx thread and paints/messages get through ok
        print "Done."
        
        """
Пример #4
0
class OverlapTests(unittest.TestCase):
    def setUp(self):
        class FakeGui:
            def stateofthenation(self,
                                 recalibrate=False,
                                 auto_resize_canvas=True):
                pass

        self.g = Graph()
        self.overlap_remover = OverlapRemoval(self.g, margin=5, gui=FakeGui())

    def tearDown(self):
        #pprint.pprint(self.overlap_remover.GetStats())
        pass

    def test0_1OneNode(self):
        g = self.g
        g.AddNode(GraphNode('A', 0, 0, 250, 250))

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertEqual(
            0,
            self.overlap_remover.GetStats()['total_overlaps_found'])

    def test0_2TwoNode_notoverlapping(self):
        g = self.g
        g.AddNode(GraphNode('A', 0, 0, 250, 250))
        g.AddNode(GraphNode('B', 260, 0, 250, 250))

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertEqual(
            0,
            self.overlap_remover.GetStats()['total_overlaps_found'])

    def test0_3TwoNode_overlapping(self):
        g = self.g
        g.AddNode(GraphNode('A', 0, 0, 250, 250))
        g.AddNode(GraphNode('B', 200, 0, 250, 250))

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

    def test0_4OverlapRemoverCreation(self):
        g = self.g
        a = GraphNode('A', 0, 0, 250, 250)
        a1 = GraphNode('A1', 0, 0)
        a2 = GraphNode('A2', 0, 0)
        g.AddEdge(a, a1)
        g.AddEdge(a, a2)

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)

    """
    Smarter tests.
    Load scenarios from persistence and use special box comparison utility methods
    """

    def _ensureXorder(self, *args):
        nodes = [self.g.FindNodeById(id) for id in args]
        assert len(nodes) >= 2
        for i in range(0, len(nodes) - 1):
            if not nodes[i].right < nodes[i + 1].left:
                return False
        return True

    def _ensureXorderLefts(self, *args):
        nodes = [self.g.FindNodeById(id) for id in args]
        assert len(nodes) >= 2
        for i in range(0, len(nodes) - 1):
            if not nodes[i].left < nodes[i + 1].left:
                return False
        return True

    def _ensureYorder(self, *args):
        nodes = [self.g.FindNodeById(id) for id in args]
        assert len(nodes) >= 2
        for i in range(0, len(nodes) - 1):
            if not nodes[i].bottom < nodes[i + 1].top:
                return False
        return True

    def _ensureYorderBottoms(self, *args):
        nodes = [self.g.FindNodeById(id) for id in args]
        assert len(nodes) >= 2
        for i in range(0, len(nodes) - 1):
            if not nodes[i].bottom < nodes[i + 1].bottom:
                return False
        return True

    def _LoadScenario1(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH1)

    def test1_1MoveLeftPushedBackHorizontally01(self):
        self._LoadScenario1()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (150, 9)

        # assert m1 has been pushed back to the right
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])
        self.assertTrue(node.left > self.g.FindNodeById('D25').right)
        self.assertTrue(node.top < self.g.FindNodeById('D25').bottom)

    def test1_2MoveLeftPushedBackDownRight02(self):
        self._LoadScenario1()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (106, 79)

        # assert m1 has been pushed back to the right but also down
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(node.left > self.g.FindNodeById('D13').right)
        self.assertTrue(node.top > self.g.FindNodeById('D25').bottom)

    def test1_3MoveInsertedVertically1(self):
        self._LoadScenario1()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (16, 74)

        # assert m1 has been squeezed in between the two existing
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            2,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        #print self.g.GraphToString()
        self.assertTrue(node.top > self.g.FindNodeById('D25').bottom)
        self.assertTrue(node.bottom < self.g.FindNodeById('D13').top)
        self.assertTrue(
            self.g.FindNodeById('D13').top > self.g.FindNodeById('D25').bottom)
        self.assertTrue(node.left < self.g.FindNodeById('D25').right)
        self.assertTrue(node.left < self.g.FindNodeById('D13').right)

    def test1_4MovePushedVertically2(self):
        self._LoadScenario1()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (6, 154)

        # assert m1 has been pushed vertically underneath the other two nodes
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureYorder('D25', 'D13', 'm1'))

        self.assertTrue(node.left < self.g.FindNodeById('D25').right)
        self.assertTrue(node.left < self.g.FindNodeById('D13').right)

    def _LoadScenario2(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH2)

    def test2_1InsertAndPushedRightHorizontally(self):
        self._LoadScenario2()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (121, 14)

        # assert m1 has been inserted and node to the right pushed right
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            2,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'm1', 'D97'))
        self.assertTrue(self._ensureYorderBottoms(
            'm1', 'D97'))  # m1 bottom above D97 bottom

    def test2_2PushedRightAndDownNicely(self):
        self._LoadScenario2()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (96, 114)

        # assert m1 has been pushed down and right nicely and snugly
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D13', 'm1'))
        self.assertTrue(self._ensureXorderLefts('D13', 'm1', 'D97'))
        self.assertTrue(self._ensureXorderLefts('D25', 'm1', 'D97'))
        self.assertTrue(self._ensureYorderBottoms('D25', 'D97', 'm1'))
        self.assertTrue(self._ensureYorder('D25', 'm1'))
        self.assertTrue(self._ensureYorderBottoms('D25', 'D13', 'm1'))
        self.assertTrue(self._ensureYorderBottoms('D25', 'D97', 'm1'))

    def _LoadScenario3(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH3)

    def test3_1PushedBetweenLeftAndRight(self):
        self._LoadScenario3()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (266, 9)

        # assert m1 has been pushed between two nodes, horizontally.  Both left and right nodes moved left and right respectively.
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            2,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        GatherSnugProposals_ON = True  # see method ProposeRemovalsAndApply() in overlap_removal.py

        if GatherSnugProposals_ON:
            # Newer snug behaviour
            self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
            self.assertTrue(self._ensureYorder('D98', 'm1'))
            self.assertTrue(self._ensureYorder('D97', 'm1'))
            self.assertTrue(self._ensureYorder('D25', 'm1'))
        else:
            # Older squeeze behaviour
            self.assertTrue(self._ensureXorder('D25', 'D97', 'm1', 'D98'))
            self.assertTrue(self._ensureYorder('D25', 'D13'))
            self.assertTrue(self._ensureYorderBottoms('D25', 'D97', 'D13'))

    def test3_2PushedBetweenLeftAndRightRefused(self):
        self._LoadScenario3()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (226, 14)

        # assert m1 has been not been inserted - refused and snuggled instead
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D25', 'D97', 'm1'))
        self.assertTrue(self._ensureYorder('D98', 'm1'))
        self.assertTrue(self._ensureYorderBottoms('D98', 'D97', 'm1'))

    def test3_2aPushedRefusedButLeftMovedAnyway(self):
        self._LoadScenario3()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (281, 64)

        d97 = self.g.FindNodeById('D97')
        oldD97pos = (d97.left, d97.top)

        # assert m1 has been refused insertion, but left (D97) moved leftwards cos there is room.  m1 snuggled below and to the right.
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            2,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D13', 'D97', 'm1'))
        self.assertTrue(self._ensureYorder('D25', 'D13'))
        self.assertTrue(self._ensureYorder('D98', 'm1'))
        self.assertTrue(self._ensureYorderBottoms('D98', 'D97', 'm1'))
        self.assertNotEqual(
            oldD97pos, (d97.left, d97.top))  # ensure D97 HAS been pushed left

    def test3_3InsertedAndTwoPushedRight(self):
        self._LoadScenario3()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (146, 9)

        # assert m1 has been inserted - and two nodes pushed right
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            3,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'm1', 'D97', 'D98'))

    def test3_4InsertedVerticallyNothingPushedRight(self):
        self._LoadScenario3()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (91, 64)

        d97 = self.g.FindNodeById('D97')
        oldD97pos = (d97.left, d97.top)

        # assert m1 has been inserted vertically - one node pushed down, NO nodes pushed right
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            5,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('m1', 'D97', 'D98'))
        self.assertTrue(self._ensureYorder('D25', 'm1', 'D13'))
        self.assertTrue(self._ensureYorder('D25', 'm1', 'D13'))
        self.assertEqual(oldD97pos,
                         (d97.left, d97.top))  # ensure D97 hasn't been pushed

    def test3_5InsertedVerticallyTwoPushedDown(self):
        self._LoadScenario3()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (6, 4)

        d97 = self.g.FindNodeById('D97')
        oldD97pos = (d97.left, d97.top)

        # assert m1 has been inserted vertically - two pushed down
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            2,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureYorder('m1', 'D25', 'D13'))
        self.assertTrue(self._ensureXorder('m1', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D13', 'D97', 'D98'))
        self.assertEqual(oldD97pos,
                         (d97.left, d97.top))  # ensure D97 hasn't been pushed

    def _LoadScenario3a(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH3A)

    def test3A_1PushedLeft(self):
        self._LoadScenario3a()

        node = self.g.FindNodeById('m1')
        node.left, node.top = (246, 9)

        # move m1 to the right, should be pushed back to the left
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'm1', 'D97'))
        self.assertTrue(self._ensureXorder('D13', 'm1', 'D97'))
        self.assertTrue(self._ensureYorder('D25', 'D13'))
        self.assertTrue(self._ensureYorder('m1', 'D13'))
        self.assertTrue(self._ensureYorder('m1', 'D98'))
        self.assertTrue(self._ensureYorder('D97', 'D98'))

        self.assertFalse(self._ensureYorder('D25', 'm1'))  # don't want this
        self.assertFalse(self._ensureYorder('D97', 'm1'))  # don't want this

    def test3A_2PushedLeftD97DoesntMove(self):
        self._LoadScenario3a()

        d97 = self.g.FindNodeById('D97')
        oldD97pos = (d97.left, d97.top)

        node = self.g.FindNodeById('m1')
        node.left, node.top = (261, 104)

        # move m1 to the right, should be pushed back to the left.  D97 shouldn't move!
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)

        self.assertTrue(self._ensureXorder('D25', 'm1', 'D97'))
        self.assertTrue(self._ensureXorder('D25', 'm1', 'D98'))
        self.assertTrue(self._ensureXorderLefts('D25', 'm1', 'D98', 'D97'))

        self.assertEqual(oldD97pos,
                         (d97.left, d97.top))  # ensure D97 hasn't been pushed

    def test3A_3PushedLeftNotFlyingUpY(self):
        self._LoadScenario3a()

        node = self.g.FindNodeById('m1')
        node.left, node.top = (216, 164)

        # move m1 to the right, should be pushed back to the left.  Not flown up Y.
        # note that the top of m1 is > top of D98 to trigger this test.
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D25', 'm1', 'D97'))
        self.assertTrue(self._ensureXorder('D25', 'm1', 'D98'))
        self.assertTrue(self._ensureYorder('D25', 'm1'))
        self.assertFalse(self._ensureYorder('m1', 'D98'))  # don't want this

    def _LoadScenario4(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH4)

    def test4_1InsertedTwoPushedRightTwoPushedDown(self):
        self._LoadScenario4()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (136, 99)

        d97 = self.g.FindNodeById('D97')
        d98 = self.g.FindNodeById('D98')
        d50 = self.g.FindNodeById('D50')
        d51 = self.g.FindNodeById('D51')
        oldD97pos = (d97.left, d97.top)
        oldD98pos = (d98.left, d98.top)
        oldD50pos = (d50.left, d50.top)
        oldD51pos = (d51.left, d51.top)

        # assert m1 has been inserted - two pushed right, two pushed down
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            5,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('D13', 'm1', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
        self.assertTrue(self._ensureYorder('D25', 'D13', 'D50', 'D51'))
        self.assertTrue(self._ensureYorder('D25', 'm1', 'D50', 'D51'))
        self.assertTrue(self._ensureYorder('D97', 'D50', 'D51'))
        self.assertTrue(self._ensureYorder('D98', 'D50', 'D51'))
        self.assertTrue(self._ensureXorder('D13', 'D50'))
        self.assertTrue(self._ensureXorder('D13', 'D51'))
        self.assertNotEqual(oldD97pos,
                            (d97.left, d97.top))  # ensure D97 HAS been pushed
        self.assertNotEqual(oldD98pos,
                            (d98.left, d98.top))  # ensure D98 HAS been pushed
        self.assertNotEqual(oldD50pos,
                            (d50.left, d50.top))  # ensure D50 HAS been pushed
        self.assertNotEqual(oldD51pos,
                            (d51.left, d51.top))  # ensure D51 HAS been pushed

    def test4_2InsertedTwoNotPushedRightThreePushedDown(self):
        self._LoadScenario4()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (101, 99)

        d97 = self.g.FindNodeById('D97')
        d98 = self.g.FindNodeById('D98')
        d50 = self.g.FindNodeById('D50')
        d51 = self.g.FindNodeById('D51')
        d13 = self.g.FindNodeById('D13')
        oldD97pos = (d97.left, d97.top)
        oldD98pos = (d98.left, d98.top)
        oldD50pos = (d50.left, d50.top)
        oldD51pos = (d51.left, d51.top)
        oldD13pos = (d13.left, d13.top)

        # assert m1 has been inserted - two NOT pushed right, two pushed down, and extra D13 pushed down
        # because m1 overlaps/attacks D13 (and there is room for D13 to move downwards I guess)
        #
        # An earlier version of this test allowed D97 and D98 to be pushed to the right.  I changed this.
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)

        self.assertFalse(self._ensureXorder(
            'D13', 'm1', 'D97',
            'D98'))  # not true anymore, m1 not pushed to the right so much
        self.assertFalse(self._ensureYorder('D25', 'D13', 'D50',
                                            'D51'))  # not true anymore,

        self.assertTrue(self._ensureXorder('D25', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D13', 'D97', 'D98'))
        self.assertTrue(self._ensureXorder('D13', 'D50'))
        self.assertTrue(self._ensureXorder('D13', 'D51'))

        self.assertTrue(self._ensureYorder('D25', 'm1', 'D13', 'D51'))
        self.assertTrue(self._ensureYorder('D25', 'm1', 'D50', 'D51'))
        self.assertTrue(self._ensureYorder('D97', 'D50', 'D51'))
        self.assertTrue(self._ensureYorder('D98', 'D50', 'D51'))
        self.assertTrue(
            self._ensureYorderBottoms('D25', 'D97', 'm1', 'D13', 'D50', 'D51'))

        self.assertEqual(oldD97pos,
                         (d97.left, d97.top))  # ensure D97 HAS NOT been pushed
        self.assertEqual(oldD98pos,
                         (d98.left, d98.top))  # ensure D98 HAS NOT been pushed
        self.assertNotEqual(oldD50pos,
                            (d50.left, d50.top))  # ensure D50 HAS been pushed
        self.assertNotEqual(oldD51pos,
                            (d51.left, d51.top))  # ensure D51 HAS been pushed
        self.assertNotEqual(oldD13pos,
                            (d13.left, d13.top))  # ensure D13 HAS been pushed

    def _LoadScenario6_linecrossing(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH6)

    def test6_1LineCrossingNotNeeded(self):

        if not LINE_NODE_OVERLAP_REMOVAL_ENABLED:
            print "Test passed (disabled)",
            return

        self._LoadScenario6_linecrossing()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (49, 48)

        # assert m1 has been repulsed and snuggeled
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('A', 'B'))
        self.assertTrue(self._ensureXorder('A', 'm1'))
        self.assertTrue(self._ensureYorder('A', 'C'))
        self.assertTrue(self._ensureYorder('B', 'm1', 'C'))
        self.assertFalse(
            self._ensureYorder('A', 'm1', 'C')
        )  # don't want this otherwise the line from A to C would be crossed

    def test6_2LineCrossingAvoided(self):

        if not LINE_NODE_OVERLAP_REMOVAL_ENABLED:
            print "Test passed (disabled)",
            return

        self._LoadScenario6_linecrossing()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        #node.left, node.top = (9, 103) # A is to the left of A
        node.left, node.top = (
            24, 98)  # m1 is to the left of A - a bit easier to solve

        # assert m1 has been repulsed and snuggled, and line not crossed - same results as ABOVE
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('A', 'm1'))

        # ensure m1 not crossing any lines
        line_start_point = self.g.FindNodeById('A').centre_point
        line_end_point = self.g.FindNodeById('C').centre_point
        crossings = node.CalcLineIntersectionPoints(line_start_point,
                                                    line_end_point)
        self.assertEqual(0, len(crossings))

    def test6_3LineCrossingAvoidedGoSnug(self):

        if not LINE_NODE_OVERLAP_REMOVAL_ENABLED:
            print "Test passed (disabled)",
            return

        self._LoadScenario6_linecrossing()

        # move m1 to down and left, crossing the line
        node = self.g.FindNodeById('m1')
        node.left, node.top = (79, 163)

        # assert m1 has been repulsed up and to the right, snuggled, and line not crossed
        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('A', 'B'))
        self.assertTrue(self._ensureXorder('A', 'm1'))
        self.assertFalse(self._ensureXorder('C', 'm1'))  # don't want this
        self.assertTrue(self._ensureYorder('m1', 'C'))
        self.assertTrue(self._ensureYorder('A', 'C'))

    def _LoadScenario7(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH7)

    def test7_1DontJumpTooFarY(self):

        self._LoadScenario7()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (240, 15)

        # assert m1 has been pushed to the right.  don't see why edges should make any difference
        # initially found m1 was being pushed way too far down in the Y direction!

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('A', 'm1'))
        self.assertTrue(self._ensureXorder('A', 'c'))
        self.assertTrue(self._ensureYorder('m1', 'c'))

        self.assertFalse(self._ensureYorder(
            'A', 'm1'))  # don't want this huge Y jump

    def test7_2DontJumpTooFarY(self):

        self._LoadScenario7()

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (235, 120)

        # assert m1 has been pushed to the right and down snugly
        # not pushed way too far down in the Y direction!

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)
        self.assertEqual(
            1,
            self.overlap_remover.GetStats()['total_overlaps_found'])

        self.assertTrue(self._ensureXorder('A', 'm1'))
        self.assertTrue(self._ensureXorder('A', 'c'))
        self.assertTrue(self._ensureYorder(
            'c', 'm1'))  # ensure m1 is snuggled below c

        self.assertFalse(self._ensureYorder(
            'A', 'm1'))  # don't want this huge Y jump

    def _LoadScenario8(self):
        self.g.LoadGraphFromStrings(TEST_GRAPH8)

    def test8_1JumpUpAndSnuggleB1PushedOk(self):

        self._LoadScenario8()

        b1 = self.g.FindNodeById('B1')
        a = self.g.FindNodeById('A')
        oldB1pos = (b1.left, b1.top)
        oldApos = (a.left, a.top)

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (64, 75)

        # assert m1 has been pushed up and to the right. Ok for B1 to be pushed a little left

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)

        self.assertTrue(self._ensureXorder('B1', 'm1', 'B2'))
        self.assertTrue(self._ensureYorder('B1', 'A'))
        self.assertTrue(self._ensureYorder('B2', 'A'))

        self.assertEqual(oldApos,
                         (a.left, a.top))  # ensure A HAS NOT been pushed
        self.assertNotEqual(oldB1pos,
                            (b1.left, b1.top))  # ok if B1 HAS been pushed

    def test8_2JumpUpAndSnuggle(self):

        self._LoadScenario8()

        b1 = self.g.FindNodeById('B1')
        a = self.g.FindNodeById('A')
        oldB1pos = (b1.left, b1.top)
        oldApos = (a.left, a.top)

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (34, 75)

        # assert m1 has been pushed up and to the right. We used to have it so
        # moving y up was not an option for m1 so A got pushed down instead.

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)

        self.assertTrue(self._ensureXorder('B1', 'm1', 'B2'))
        self.assertTrue(self._ensureYorder('B1', 'A'))
        self.assertTrue(self._ensureYorder('B2', 'A'))

        self.assertEqual(oldApos,
                         (a.left, a.top))  # ensure A HAS NOT been pushed
        self.assertEqual(oldB1pos,
                         (b1.left, b1.top))  # ensure B1 HAS NOT been pushed

    def test8_3JumpUpAndSnuggle(self):

        self._LoadScenario8()

        b2 = self.g.FindNodeById('B2')
        a = self.g.FindNodeById('A')
        oldB2pos = (b2.left, b2.top)
        oldApos = (a.left, a.top)

        # move m1 to the left
        node = self.g.FindNodeById('m1')
        node.left, node.top = (114, 75)

        # assert m1 has been pushed up and to the left. We used to have it so
        # moving y up was not an option for m1 so A got pushed down instead.

        were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps()
        self.assertTrue(were_all_overlaps_removed)

        self.assertTrue(self._ensureXorder('B1', 'm1', 'B2'))
        self.assertTrue(self._ensureYorder('B1', 'A'))
        self.assertTrue(self._ensureYorder('B2', 'A'))

        self.assertEqual(oldApos,
                         (a.left, a.top))  # ensure A HAS NOT been pushed
        self.assertEqual(oldB2pos,
                         (b2.left, b2.top))  # ensure B2 HAS NOT been pushed