1# -*- coding: utf-8 -*-
2#----------------------------------------------------------------------------
3# Name:         lines.py
4# Purpose:      LineShape class
5#
6# Author:       Pierre Hjälm (from C++ original by Julian Smart)
7#
8# Created:      2004-05-08
9# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
10# Licence:      wxWindows license
11# Tags:         phoenix-port, unittest, py3-port, documented
12#----------------------------------------------------------------------------
13"""
14The :class:`~lib.ogl.lines.LineShape` class may have arrowheads at the
15beginning and end.
16"""
17import sys
18import math
19
20from .basic import Shape, ShapeRegion, ShapeTextLine, ControlPoint, RectangleShape
21from .oglmisc import *
22
23# Line alignment flags
24# Vertical by default
25LINE_ALIGNMENT_HORIZ =              1
26LINE_ALIGNMENT_VERT =               0
27LINE_ALIGNMENT_TO_NEXT_HANDLE =     2
28LINE_ALIGNMENT_NONE =               0
29
30
31
32class LineControlPoint(ControlPoint):
33    """
34    The :class:`LineControlPoint` class.
35    """
36    def __init__(self, theCanvas = None, object = None, size = 0.0, x = 0.0, y = 0.0, the_type = 0):
37        """
38        Default class constructor.
39
40        :param `theCanvas`: a :class:`~lib.ogl.Canvas`
41        :param `object`: not used ???
42        :param float `size`: the size
43        :param float `x`: the x position
44        :param float `y`: the y position
45        :param int `the_type`: one of the following types
46
47         ======================================== ==================================
48         Control point type                       Description
49         ======================================== ==================================
50         `CONTROL_POINT_VERTICAL`                 Vertical
51         `CONTROL_POINT_HORIZONTAL`               Horizontal
52         `CONTROL_POINT_DIAGONAL`                 Diagonal
53         ======================================== ==================================
54
55        """
56        ControlPoint.__init__(self, theCanvas, object, size, x, y, the_type)
57        self._xpos = x
58        self._ypos = y
59        self._type = the_type
60        self._point = None
61        self._originalPos = None
62
63    def OnDraw(self, dc):
64        """The draw handler."""
65        RectangleShape.OnDraw(self, dc)
66
67    # Implement movement of Line point
68    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
69        """The drag left handler."""
70        self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment)
71
72    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
73        """The begin drag left handler."""
74        self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment)
75
76    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
77        """The end drag left handler."""
78        self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment)
79
80
81
82class ArrowHead(object):
83    """
84    The arrow head class.
85    """
86    def __init__(self, type = 0, end = 0, size = 0.0, dist = 0.0, name = "", mf = None, arrowId = -1):
87        """
88        Default class constructor.
89
90        :param int `type`: the type
91
92         ======================================== ==================================
93         Arrow head type                          Description
94         ======================================== ==================================
95         `ARROW_HOLLOW_CIRCLE`                    a hollow circle
96         `ARROW_FILLED_CIRCLE`                    a filled circle
97         `ARROW_ARROW`                            an arrow
98         `ARROW_SINGLE_OBLIQUE`                   a single oblique
99         `ARROW_DOUBLE_OBLIQUE`                   a double oblique
100         `ARROW_METAFILE`                         custom, define in metafile
101         ======================================== ==================================
102
103        :param int `end`: end of arrow head ???
104        :param float `size`: size of arrow head
105        :param float `dist`: dist ???
106        :param string `name`: name of arrow head
107        :param `mf`: mf ???
108        :param `arrowId`: id of arrow head
109
110        """
111        if isinstance(type, ArrowHead):
112            pass
113        else:
114            self._arrowType = type
115            self._arrowEnd = end
116            self._arrowSize = size
117            self._xOffset = dist
118            self._yOffset = 0.0
119            self._spacing = 5.0
120
121            self._arrowName = name
122            self._metaFile = mf
123            self._id = arrowId
124            if self._id == -1:
125                self._id = wx.NewIdRef()
126
127    def _GetType(self):
128        return self._arrowType
129
130    def GetPosition(self):
131        """Get end position."""
132        return self._arrowEnd
133
134    def SetPosition(self, pos):
135        """Set end position.
136
137        :param `pos`: position to set it to
138        """
139        self._arrowEnd = pos
140
141    def GetXOffset(self):
142        """Get the X offset."""
143        return self._xOffset
144
145    def GetYOffset(self):
146        """Get the Y offset."""
147        return self._yOffset
148
149    def GetSpacing(self):
150        """Get the spacing."""
151        return self._spacing
152
153    def GetSize(self):
154        """Get the arrow size."""
155        return self._arrowSize
156
157    def SetSize(self, size):
158        """Set the arrow size.
159
160        :param `size`: size in points???
161
162        :note: if a custom arrow is used size is used to scale the arrow???
163        """
164        self._arrowSize = size
165        if self._arrowType == ARROW_METAFILE and self._metaFile:
166            oldWidth = self._metaFile._width
167            if oldWidth == 0:
168                return
169
170            scale = float(size) / oldWidth
171            if scale != 1:
172                self._metaFile.Scale(scale, scale)
173
174    def GetName(self):
175        """Get the arrow name."""
176        return self._arrowName
177
178    def SetXOffset(self, x):
179        """Set the X offset.
180
181        :param `x`: value to set the offset to???
182        """
183        self._xOffset = x
184
185    def SetYOffset(self, y):
186        """Set the Y offset.
187
188        :param `y`: value to set the offset to???
189        """
190        self._yOffset = y
191
192    def GetMetaFile(self):
193        """Get the metafile."""
194        return self._metaFile
195
196    def GetId(self):
197        """Get the id."""
198        return self._id
199
200    def GetArrowEnd(self):
201        """Get the arrow end position."""
202        return self._arrowEnd
203
204    def GetArrowSize(self):
205        """Get the arrow size."""
206        return self._arrowSize
207
208    def SetSpacing(self, sp):
209        """Set the spacing.
210
211        :param `sp`: the new spacing value
212        """
213        self._spacing = sp
214
215
216
217class LabelShape(RectangleShape):
218    """
219    The label shape class.
220    """
221    def __init__(self, parent, region, w, h):
222        """
223        Default class constructor.
224
225        :param `parent`: the parent an instance of :class:`Line`
226        :param `region`: the shape region an instance of :class:`~lib.ogl.ShapeRegion`
227        :param `w`: width in points
228        :param `h`: heigth in points
229
230        """
231        RectangleShape.__init__(self, w, h)
232        self._lineShape = parent
233        self._shapeRegion = region
234        self.SetPen(wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_DOT))
235
236    def OnDraw(self, dc):
237        """The draw handler."""
238        if self._lineShape and not self._lineShape.GetDrawHandles():
239            return
240
241        x1 = self._xpos - self._width / 2.0
242        y1 = self._ypos - self._height / 2.0
243
244        if self._pen:
245            if self._pen.GetWidth() == 0:
246                dc.SetPen(wx.Pen(wx.WHITE, 1, wx.TRANSPARENT))
247            else:
248                dc.SetPen(self._pen)
249        dc.SetBrush(wx.TRANSPARENT_BRUSH)
250
251        if self._cornerRadius > 0:
252            dc.DrawRoundedRectangle(x1, y1, self._width, self._height, self._cornerRadius)
253        else:
254            dc.DrawRectangle(x1, y1, self._width, self._height)
255
256    def OnDrawContents(self, dc):
257        """not implemented???"""
258        pass
259
260    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
261        """The drag left handler."""
262        RectangleShape.OnDragLeft(self, draw, x, y, keys, attachment)
263
264    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
265        """The begin drag left handler."""
266        RectangleShape.OnBeginDragLeft(self, x, y, keys, attachment)
267
268    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
269        """The end drag left handler."""
270        RectangleShape.OnEndDragLeft(self, x, y, keys, attachment)
271
272    def OnMovePre(self, dc, x, y, old_x, old_y, display):
273        """The move 'pre' handler.???"""
274        return self._lineShape.OnLabelMovePre(dc, self, x, y, old_x, old_y, display)
275
276    # Divert left and right clicks to line object
277    def OnLeftClick(self, x, y, keys = 0, attachment = 0):
278        """The left click handler."""
279        self._lineShape.GetEventHandler().OnLeftClick(x, y, keys, attachment)
280
281    def OnRightClick(self, x, y, keys = 0, attachment = 0):
282        """The right click handler."""
283        self._lineShape.GetEventHandler().OnRightClick(x, y, keys, attachment)
284
285
286
287class LineShape(Shape):
288    """
289    The LineShape class may be attached to two nodes, it may be segmented,
290    in which case a control point is drawn for each joint.
291
292    A :class:`LineShape` may have arrows at the beginning, end and centre.
293    """
294    def __init__(self):
295        """
296        Default class constructor.
297
298        """
299        Shape.__init__(self)
300
301        self._sensitivity = OP_CLICK_LEFT | OP_CLICK_RIGHT
302        self._draggable = False
303        self._attachmentTo = 0
304        self._attachmentFrom = 0
305        self._from = None
306        self._to = None
307        self._erasing = False
308        self._arrowSpacing = 5.0
309        self._ignoreArrowOffsets = False
310        self._isSpline = False
311        self._maintainStraightLines = False
312        self._alignmentStart = 0
313        self._alignmentEnd = 0
314
315        self._lineControlPoints = None
316
317        # Clear any existing regions (created in an earlier constructor)
318        # and make the three line regions.
319        self.ClearRegions()
320        for name in ["Middle","Start","End"]:
321            newRegion = ShapeRegion()
322            newRegion.SetName(name)
323            newRegion.SetSize(150, 50)
324            self._regions.append(newRegion)
325
326        self._labelObjects = [None, None, None]
327        self._lineOrientations = []
328        self._lineControlPoints = []
329        self._arcArrows = []
330
331    def GetFrom(self):
332        """Get the 'from' object."""
333        return self._from
334
335    def GetTo(self):
336        """Get the 'to' object."""
337        return self._to
338
339    def GetAttachmentFrom(self):
340        """Get the attachment point on the 'from' node."""
341        return self._attachmentFrom
342
343    def GetAttachmentTo(self):
344        """Get the attachment point on the 'to' node."""
345        return self._attachmentTo
346
347    def GetLineControlPoints(self):
348        """Get the line control points."""
349        return self._lineControlPoints
350
351    def SetSpline(self, spline):
352        """Specifies whether a spline is to be drawn through the control points.
353
354        :param boolean `spline`: True draw a spline through control points.
355
356        """
357        self._isSpline = spline
358
359    def IsSpline(self):
360        """If `True` a spline is drawn through the control points."""
361        return self._isSpline
362
363    def SetAttachmentFrom(self, attach):
364        """Set the 'from' shape attachment."""
365        self._attachmentFrom = attach
366
367    def SetAttachmentTo(self, attach):
368        """Set the 'to' shape attachment."""
369        self._attachmentTo = attach
370
371    def Draggable(self):
372        """
373        Line is not draggable.
374
375        :note: This is really to distinguish between lines and other images.
376         For lines we want to pass drag to canvas, since lines tend to prevent
377         dragging on a canvas (they get in the way.)
378
379        """
380        return False
381
382    def SetIgnoreOffsets(self, ignore):
383        """Set whether to ignore offsets from the end of the line when drawing."""
384        self._ignoreArrowOffsets = ignore
385
386    def GetArrows(self):
387        """Get the defined arrows."""
388        return self._arcArrows
389
390    def GetAlignmentStart(self):
391        """Get alignment start"""
392        return self._alignmentStart
393
394    def GetAlignmentEnd(self):
395        """Get alignment end."""
396        return self._alignmentEnd
397
398    def IsEnd(self, nodeObject):
399        """`True` if shape is at the end of the line."""
400        return self._to == nodeObject
401
402    def MakeLineControlPoints(self, n):
403        """
404        Make a given number of control points.
405
406        :param int `n`: number of control points, minimum of two
407
408        """
409        self._lineControlPoints = []
410
411        for _ in range(n):
412            point = wx.RealPoint(-999, -999)
413            self._lineControlPoints.append(point)
414
415        # pi: added _initialised to keep track of when we have set
416        # the middle points to something other than (-999, -999)
417        self._initialised = False
418
419    def InsertLineControlPoint(self, dc = None, point = None):
420        """
421        Insert a control point at an optional given position.
422
423        :param `dc`: an instance of :class:`wx.MemoryDC`
424        :param `point`: an optional point, otherwise will use _lineControlPoints
425
426        """
427        if dc:
428            self.Erase(dc)
429
430        if point:
431            line_x, line_y = point
432        else:
433            last_point = self._lineControlPoints[-1]
434            second_last_point = self._lineControlPoints[-2]
435
436            line_x = (last_point[0] + second_last_point[0]) / 2.0
437            line_y = (last_point[1] + second_last_point[1]) / 2.0
438
439        point = wx.RealPoint(line_x, line_y)
440        self._lineControlPoints.insert(len(self._lineControlPoints)-1, point)
441
442    def DeleteLineControlPoint(self):
443        """Delete an arbitary point on the line."""
444        if len(self._lineControlPoints) < 3:
445            return False
446
447        del self._lineControlPoints[-2]
448        return True
449
450    def Initialise(self):
451        """Initialise the line object."""
452        if self._lineControlPoints:
453            # Just move the first and last control points
454            first_point = self._lineControlPoints[0]
455            last_point = self._lineControlPoints[-1]
456
457            # If any of the line points are at -999, we must
458            # initialize them by placing them half way between the first
459            # and the last.
460
461            for i in range(1,len(self._lineControlPoints)):
462                point = self._lineControlPoints[i]
463                if point[0] == -999:
464                    if first_point[0] < last_point[0]:
465                        x1 = first_point[0]
466                        x2 = last_point[0]
467                    else:
468                        x2 = first_point[0]
469                        x1 = last_point[0]
470                    if first_point[1] < last_point[1]:
471                        y1 = first_point[1]
472                        y2 = last_point[1]
473                    else:
474                        y2 = first_point[1]
475                        y1 = last_point[1]
476                    self._lineControlPoints[i] = wx.RealPoint((x2 - x1) / 2.0 + x1, (y2 - y1) / 2.0 + y1)
477            self._initialised = True
478
479    def FormatText(self, dc, s, i):
480        """
481        Format a text string according to the region size, adding
482        strings with positions to region text list.
483
484        :param `dc`: an instance of :class:`wx.MemoryDC`
485        :param str `s`: the text string
486        :param int `i`: index to the region to be used???
487
488        """
489        self.ClearText(i)
490
491        if len(self._regions) == 0 or i >= len(self._regions):
492            return
493
494        region = self._regions[i]
495        region.SetText(s)
496        dc.SetFont(region.GetFont())
497
498        w, h = region.GetSize()
499        # Initialize the size if zero
500        if (w == 0 or h == 0) and s:
501            w, h = 100, 50
502            region.SetSize(w, h)
503
504        string_list = FormatText(dc, s, w - 5, h - 5, region.GetFormatMode())
505        for s in string_list:
506            line = ShapeTextLine(0.0, 0.0, s)
507            region.GetFormattedText().append(line)
508
509        actualW = w
510        actualH = h
511        if region.GetFormatMode() & FORMAT_SIZE_TO_CONTENTS:
512            actualW, actualH = GetCentredTextExtent(dc, region.GetFormattedText(), self._xpos, self._ypos, w, h)
513            if actualW != w or actualH != h:
514                xx, yy = self.GetLabelPosition(i)
515                self.EraseRegion(dc, region, xx, yy)
516                if len(self._labelObjects) < i:
517                    self._labelObjects[i].Select(False, dc)
518                    self._labelObjects[i].Erase(dc)
519                    self._labelObjects[i].SetSize(actualW, actualH)
520
521                region.SetSize(actualW, actualH)
522
523                if len(self._labelObjects) < i:
524                    self._labelObjects[i].Select(True, dc)
525                    self._labelObjects[i].Draw(dc)
526
527        CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, actualW, actualH, region.GetFormatMode())
528        self._formatted = True
529
530    def DrawRegion(self, dc, region, x, y):
531        """
532        Format one region at this position.
533
534        :param `dc`: an instance of :class:`wx.MemoryDC`
535        :param `dc`: an instance of :class:`~lib.ogl.ShapeRegion`
536        :param `x`: the x position
537        :param `y`: the y position
538
539        """
540        if self.GetDisableLabel():
541            return
542
543        w, h = region.GetSize()
544
545        # Get offset from x, y
546        xx, yy = region.GetPosition()
547
548        xp = xx + x
549        yp = yy + y
550
551        # First, clear a rectangle for the text IF there is any
552        if len(region.GetFormattedText()):
553            dc.SetPen(self.GetBackgroundPen())
554            dc.SetBrush(self.GetBackgroundBrush())
555
556            # Now draw the text
557            if region.GetFont():
558                dc.SetFont(region.GetFont())
559                dc.DrawRectangle(xp - w / 2.0, yp - h / 2.0, w, h)
560
561                if self._pen:
562                    dc.SetPen(self._pen)
563                dc.SetTextForeground(region.GetActualColourObject())
564
565                DrawFormattedText(dc, region.GetFormattedText(), xp, yp, w, h, region.GetFormatMode())
566
567    def EraseRegion(self, dc, region, x, y):
568        """
569        Erase one region at this position.
570
571        :param `dc`: an instance of :class:`wx.MemoryDC`
572        :param `dc`: an instance of :class:`~lib.ogl.ShapeRegion`
573        :param `x`: the x position
574        :param `y`: the y position
575
576        """
577        if self.GetDisableLabel():
578            return
579
580        w, h = region.GetSize()
581
582        # Get offset from x, y
583        xx, yy = region.GetPosition()
584
585        xp = xx + x
586        yp = yy + y
587
588        if region.GetFormattedText():
589            dc.SetPen(self.GetBackgroundPen())
590            dc.SetBrush(self.GetBackgroundBrush())
591
592            dc.DrawRectangle(xp - w / 2.0, yp - h / 2.0, w, h)
593
594    def GetLabelPosition(self, position):
595        """
596        Get the reference point for a label.
597
598        :param `position`: 0 = middle, 1 = start, 2 = end, Region x and y are offsets from this.
599
600        """
601        if position == 0:
602            # Want to take the middle section for the label
603            half_way = int(len(self._lineControlPoints) / 2.0)
604
605            # Find middle of this line
606            point = self._lineControlPoints[half_way - 1]
607            next_point = self._lineControlPoints[half_way]
608
609            dx = next_point[0] - point[0]
610            dy = next_point[1] - point[1]
611
612            return point[0] + dx / 2.0, point[1] + dy / 2.0
613        elif position == 1:
614            return self._lineControlPoints[0][0], self._lineControlPoints[0][1]
615        elif position == 2:
616            return self._lineControlPoints[-1][0], self._lineControlPoints[-1][1]
617
618    def Straighten(self, dc = None):
619        """
620        Straighten verticals and horizontals.
621
622        :param `dc`: an instance of :class:`wx.MemoryDC` or None
623
624        """
625        if len(self._lineControlPoints) < 3:
626            return
627
628        if dc:
629            self.Erase(dc)
630
631        GraphicsStraightenLine(self._lineControlPoints[-1], self._lineControlPoints[-2])
632
633        for i in range(len(self._lineControlPoints) - 2):
634            GraphicsStraightenLine(self._lineControlPoints[i], self._lineControlPoints[i + 1])
635
636        if dc:
637            self.Draw(dc)
638
639    def Unlink(self):
640        """Unlink the line from the nodes at either end."""
641        if self._to:
642            self._to.GetLines().remove(self)
643        if self._from:
644            self._from.GetLines().remove(self)
645        self._to = None
646        self._from = None
647        for i in range(3):
648            if self._labelObjects[i]:
649                self._labelObjects[i].Select(False)
650                self._labelObjects[i].RemoveFromCanvas(self._canvas)
651        self.ClearArrowsAtPosition(-1)
652
653    def Delete(self):
654        """Delete the line, unlink it first."""
655        self.Unlink()
656        Shape.Delete(self)
657
658    def SetEnds(self, x1, y1, x2, y2):
659        """
660        Set the end positions of the line.
661
662        :param: `x1`: x1 position
663        :param: `y1`: y1 position
664        :param: `x2`: x2 position
665        :param: `y2`: y2 position
666
667
668        """
669        self._lineControlPoints[0] = wx.RealPoint(x1, y1)
670        self._lineControlPoints[-1] = wx.RealPoint(x2, y2)
671
672        # Find centre point
673        self._xpos = (x1 + x2) / 2.0
674        self._ypos = (y1 + y2) / 2.0
675
676    # Get absolute positions of ends
677    def GetEnds(self):
678        """Get the visible endpoints of the lines for drawing between two objects."""
679        first_point = self._lineControlPoints[0]
680        last_point = self._lineControlPoints[-1]
681
682        return first_point[0], first_point[1], last_point[0], last_point[1]
683
684    def SetAttachments(self, from_attach, to_attach):
685        """
686        Specify which object attachment points should be used at each end of the line.
687
688        :param `from_attach`: the from points ???
689        :param `to_attach`: the to points ???
690
691        """
692        self._attachmentFrom = from_attach
693        self._attachmentTo = to_attach
694
695    def HitTest(self, x, y):
696        """
697        Line hit test.
698
699        :param `x`: x position
700        :param `y`: y position
701
702        """
703        if not self._lineControlPoints:
704            return False
705
706        # Look at label regions in case mouse is over a label
707        inLabelRegion = False
708        for i in range(3):
709            if self._regions[i]:
710                region = self._regions[i]
711                if len(region._formattedText):
712                    xp, yp = self.GetLabelPosition(i)
713                    # Offset region from default label position
714                    cx, cy = region.GetPosition()
715                    cw, ch = region.GetSize()
716                    cx += xp
717                    cy += yp
718
719                    rLeft = cx - cw / 2.0
720                    rTop = cy - ch / 2.0
721                    rRight = cx + cw / 2.0
722                    rBottom = cy + ch / 2.0
723                    if x > rLeft and x < rRight and y > rTop and y < rBottom:
724                        inLabelRegion = True
725                        break
726
727        for i in range(len(self._lineControlPoints) - 1):
728            point1 = self._lineControlPoints[i]
729            point2 = self._lineControlPoints[i + 1]
730
731            # For inaccurate mousing allow 8 pixel corridor
732            extra = 4
733
734            dx = point2[0] - point1[0]
735            dy = point2[1] - point1[1]
736
737            seg_len = math.sqrt(dx * dx + dy * dy)
738            if dy == 0 and dx == 0:
739                continue
740            distance_from_seg = seg_len * float((x - point1[0]) * dy - (y - point1[1]) * dx) / (dy * dy + dx * dx)
741            distance_from_prev = seg_len * float((y - point1[1]) * dy + (x - point1[0]) * dx) / (dy * dy + dx * dx)
742
743            if abs(distance_from_seg) < extra and distance_from_prev >= 0 and distance_from_prev <= seg_len or inLabelRegion:
744                return 0, distance_from_seg
745
746        return False
747
748    def DrawArrows(self, dc):
749        """Draw all arrows."""
750        # Distance along line of each arrow: space them out evenly
751        startArrowPos = 0.0
752        endArrowPos = 0.0
753        middleArrowPos = 0.0
754
755        for arrow in self._arcArrows:
756            ah = arrow.GetArrowEnd()
757            if ah == ARROW_POSITION_START:
758                if arrow.GetXOffset() and not self._ignoreArrowOffsets:
759                    # If specified, x offset is proportional to line length
760                    self.DrawArrow(dc, arrow, arrow.GetXOffset(), True)
761                else:
762                    self.DrawArrow(dc, arrow, startArrowPos, False)
763                    startArrowPos += arrow.GetSize() + arrow.GetSpacing()
764            elif ah == ARROW_POSITION_END:
765                if arrow.GetXOffset() and not self._ignoreArrowOffsets:
766                    self.DrawArrow(dc, arrow, arrow.GetXOffset(), True)
767                else:
768                    self.DrawArrow(dc, arrow, endArrowPos, False)
769                    endArrowPos += arrow.GetSize() + arrow.GetSpacing()
770            elif ah == ARROW_POSITION_MIDDLE:
771                arrow.SetXOffset(middleArrowPos)
772                if arrow.GetXOffset() and not self._ignoreArrowOffsets:
773                    self.DrawArrow(dc, arrow, arrow.GetXOffset(), True)
774                else:
775                    self.DrawArrow(dc, arrow, middleArrowPos, False)
776                    middleArrowPos += arrow.GetSize() + arrow.GetSpacing()
777
778    def DrawArrow(self, dc, arrow, XOffset, proportionalOffset):
779        """
780        Draw the given arrowhead (or annotation).
781
782        :param `dc`: an instance of :class:`wx.MemoryDC`
783        :param `arrow`: an instannce of :class:`ArrowHead`
784        :param `XOffset`: the x offset ???
785        :param `proportionalOffset`: ???
786
787        """
788        first_line_point = self._lineControlPoints[0]
789        second_line_point = self._lineControlPoints[1]
790
791        last_line_point = self._lineControlPoints[-1]
792        second_last_line_point = self._lineControlPoints[-2]
793
794        # Position of start point of line, at the end of which we draw the arrow
795        startPositionX, startPositionY = 0.0, 0.0
796
797        ap = arrow.GetPosition()
798        if ap == ARROW_POSITION_START:
799            # If we're using a proportional offset, calculate just where this
800            # will be on the line.
801            realOffset = XOffset
802            if proportionalOffset:
803                totalLength = math.sqrt((second_line_point[0] - first_line_point[0]) * (second_line_point[0] - first_line_point[0]) + (second_line_point[1] - first_line_point[1]) * (second_line_point[1] - first_line_point[1]))
804                realOffset = XOffset * totalLength
805
806            positionOnLineX, positionOnLineY = GetPointOnLine(second_line_point[0], second_line_point[1], first_line_point[0], first_line_point[1], realOffset)
807
808            startPositionX = second_line_point[0]
809            startPositionY = second_line_point[1]
810        elif ap == ARROW_POSITION_END:
811            # If we're using a proportional offset, calculate just where this
812            # will be on the line.
813            realOffset = XOffset
814            if proportionalOffset:
815                totalLength = math.sqrt((second_last_line_point[0] - last_line_point[0]) * (second_last_line_point[0] - last_line_point[0]) + (second_last_line_point[1] - last_line_point[1]) * (second_last_line_point[1] - last_line_point[1]));
816                realOffset = XOffset * totalLength
817
818            positionOnLineX, positionOnLineY = GetPointOnLine(second_last_line_point[0], second_last_line_point[1], last_line_point[0], last_line_point[1], realOffset)
819
820            startPositionX = second_last_line_point[0]
821            startPositionY = second_last_line_point[1]
822        elif ap == ARROW_POSITION_MIDDLE:
823            # Choose a point half way between the last and penultimate points
824            x = (last_line_point[0] + second_last_line_point[0]) / 2.0
825            y = (last_line_point[1] + second_last_line_point[1]) / 2.0
826
827            # If we're using a proportional offset, calculate just where this
828            # will be on the line.
829            realOffset = XOffset
830            if proportionalOffset:
831                totalLength = math.sqrt((second_last_line_point[0] - x) * (second_last_line_point[0] - x) + (second_last_line_point[1] - y) * (second_last_line_point[1] - y));
832                realOffset = XOffset * totalLength
833
834            positionOnLineX, positionOnLineY = GetPointOnLine(second_last_line_point[0], second_last_line_point[1], x, y, realOffset)
835            startPositionX = second_last_line_point[0]
836            startPositionY = second_last_line_point[1]
837
838        # Add yOffset to arrow, if any
839
840        # The translation that the y offset may give
841        deltaX = 0.0
842        deltaY = 0.0
843        if arrow.GetYOffset and not self._ignoreArrowOffsets:
844            #                             |(x4, y4)
845            #                             |d
846            #                             |
847            #   (x1, y1)--------------(x3, y3)------------------(x2, y2)
848            #   x4 = x3 - d * math.sin(theta)
849            #   y4 = y3 + d * math.cos(theta)
850            #
851            #   Where theta = math.tan(-1) of (y3-y1) / (x3-x1)
852            x1 = startPositionX
853            y1 = startPositionY
854            x3 = float(positionOnLineX)
855            y3 = float(positionOnLineY)
856            d = -arrow.GetYOffset() # Negate so +offset is above line
857
858            if x3 == x1:
859                theta = math.pi / 2.0
860            else:
861                theta = math.atan((y3 - y1) / (x3 - x1))
862
863            x4 = x3 - d * math.sin(theta)
864            y4 = y3 + d * math.cos(theta)
865
866            deltaX = x4 - positionOnLineX
867            deltaY = y4 - positionOnLineY
868
869        at = arrow._GetType()
870        if at == ARROW_ARROW:
871            arrowLength = arrow.GetSize()
872            arrowWidth = arrowLength / 3.0
873
874            tip_x, tip_y, side1_x, side1_y, side2_x, side2_y = GetArrowPoints(startPositionX + deltaX, startPositionY + deltaY, positionOnLineX + deltaX, positionOnLineY + deltaY, arrowLength, arrowWidth)
875
876            points = [[tip_x, tip_y],
877                    [side1_x, side1_y],
878                    [side2_x, side2_y],
879                    [tip_x, tip_y]]
880
881            dc.SetPen(self._pen)
882            dc.SetBrush(self._brush)
883            dc.DrawPolygon(points)
884        elif at in [ARROW_HOLLOW_CIRCLE, ARROW_FILLED_CIRCLE]:
885            # Find point on line of centre of circle, which is a radius away
886            # from the end position
887            diameter = arrow.GetSize()
888            x, y = GetPointOnLine(startPositionX + deltaX, startPositionY + deltaY,
889                               positionOnLineX + deltaX, positionOnLineY + deltaY,
890                               diameter / 2.0)
891            x1 = x - diameter / 2.0
892            y1 = y - diameter / 2.0
893            dc.SetPen(self._pen)
894            if arrow._GetType() == ARROW_HOLLOW_CIRCLE:
895                dc.SetBrush(self.GetBackgroundBrush())
896            else:
897                dc.SetBrush(self._brush)
898
899            dc.DrawEllipse(x1, y1, diameter, diameter)
900        elif at == ARROW_SINGLE_OBLIQUE:
901            pass
902        elif at == ARROW_METAFILE:
903            if arrow.GetMetaFile():
904                # Find point on line of centre of object, which is a half-width away
905                # from the end position
906                #
907                #                 width
908                #  <-- start pos  <-----><-- positionOnLineX
909                #                 _____
910                #  --------------|  x  | <-- e.g. rectangular arrowhead
911                #                 -----
912                #
913                x, y = GetPointOnLine(startPositionX, startPositionY,
914                                   positionOnLineX, positionOnLineY,
915                                   arrow.GetMetaFile()._width / 2.0)
916                # Calculate theta for rotating the metafile.
917                #
918                # |
919                # |     o(x2, y2)   'o' represents the arrowhead.
920                # |    /
921                # |   /
922                # |  /theta
923                # | /(x1, y1)
924                # |______________________
925                #
926                x1 = startPositionX
927                y1 = startPositionY
928                x2 = float(positionOnLineX)
929                y2 = float(positionOnLineY)
930
931                theta = math.atan2(y2 - y1, x2 - x1) % (2 * math.pi)
932
933                # Rotate about the centre of the object, then place
934                # the object on the line.
935                if arrow.GetMetaFile().GetRotateable():
936                    arrow.GetMetaFile().Rotate(0.0, 0.0, theta)
937
938                if self._erasing:
939                    # If erasing, just draw a rectangle
940                    minX, minY, maxX, maxY = arrow.GetMetaFile().GetBounds()
941                    # Make erasing rectangle slightly bigger or you get droppings
942                    extraPixels = 4
943                    dc.DrawRectangle(deltaX + x + minX - extraPixels / 2.0, deltaY + y + minY - extraPixels / 2.0, maxX - minX + extraPixels, maxY - minY + extraPixels)
944                else:
945                    arrow.GetMetaFile().Draw(dc, x + deltaX, y + deltaY)
946
947    def OnErase(self, dc):
948        """The erase handler."""
949        old_pen = self._pen
950        old_brush = self._brush
951
952        bg_pen = self.GetBackgroundPen()
953        bg_brush = self.GetBackgroundBrush()
954        self.SetPen(bg_pen)
955        self.SetBrush(bg_brush)
956
957        bound_x, bound_y = self.GetBoundingBoxMax()
958        if self._font:
959            dc.SetFont(self._font)
960
961        # Undraw text regions
962        for i in range(3):
963            if self._regions[i]:
964                x, y = self.GetLabelPosition(i)
965                self.EraseRegion(dc, self._regions[i], x, y)
966
967        # Undraw line
968        dc.SetPen(self.GetBackgroundPen())
969        dc.SetBrush(self.GetBackgroundBrush())
970
971        # Drawing over the line only seems to work if the line has a thickness
972        # of 1.
973        if old_pen and old_pen.GetWidth() > 1:
974            dc.DrawRectangle(self._xpos - bound_x / 2.0 - 2, self._ypos - bound_y / 2.0 - 2,
975                             bound_x + 4, bound_y + 4)
976        else:
977            self._erasing = True
978            self.GetEventHandler().OnDraw(dc)
979            self.GetEventHandler().OnEraseControlPoints(dc)
980            self._erasing = False
981
982        if old_pen:
983            self.SetPen(old_pen)
984        if old_brush:
985            self.SetBrush(old_brush)
986
987    def GetBoundingBoxMin(self):
988        """Get the minimum bounding box."""
989        x1, y1 = 10000, 10000
990        x2, y2 = -10000, -10000
991
992        for point in self._lineControlPoints:
993            if point[0] < x1:
994                x1 = point[0]
995            if point[1] < y1:
996                y1 = point[1]
997            if point[0] > x2:
998                x2 = point[0]
999            if point[1] > y2:
1000                y2 = point[1]
1001
1002        return x2 - x1, y2 - y1
1003
1004    # For a node image of interest, finds the position of this arc
1005    # amongst all the arcs which are attached to THIS SIDE of the node image,
1006    # and the number of same.
1007    def FindNth(self, image, incoming):
1008        """
1009        Find the position of the line on the given object.
1010
1011        Specify whether incoming or outgoing lines are being considered
1012        with incoming.
1013
1014        :param `image`: a node image
1015        :param `incoming`: True to get incoming lines ???
1016
1017        :returns: nth line, number of lines ???
1018
1019        """
1020        n = -1
1021        num = 0
1022
1023        if image == self._to:
1024            this_attachment = self._attachmentTo
1025        else:
1026            this_attachment = self._attachmentFrom
1027
1028        # Find number of lines going into / out of this particular attachment point
1029        for line in image.GetLines():
1030            if line._from == image:
1031                # This is the nth line attached to 'image'
1032                if line == self and not incoming:
1033                    n = num
1034
1035                # Increment num count if this is the same side (attachment number)
1036                if line._attachmentFrom == this_attachment:
1037                    num += 1
1038
1039            if line._to == image:
1040                # This is the nth line attached to 'image'
1041                if line == self and incoming:
1042                    n = num
1043
1044                # Increment num count if this is the same side (attachment number)
1045                if line._attachmentTo == this_attachment:
1046                    num += 1
1047
1048        return n, num
1049
1050    def OnDrawOutline(self, dc, x, y, w, h):
1051        """The draw outline handler."""
1052        old_pen = self._pen
1053        old_brush = self._brush
1054
1055        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_DOT)
1056        self.SetPen(dottedPen)
1057        self.SetBrush(wx.TRANSPARENT_BRUSH)
1058
1059        self.GetEventHandler().OnDraw(dc)
1060
1061        if old_pen:
1062            self.SetPen(old_pen)
1063        else:
1064            self.SetPen(None)
1065        if old_brush:
1066            self.SetBrush(old_brush)
1067        else:
1068            self.SetBrush(None)
1069
1070    def OnMovePre(self, dc, x, y, old_x, old_y, display = True):
1071        """The move 'pre' handler. ???"""
1072        x_offset = x - old_x
1073        y_offset = y - old_y
1074
1075        if self._lineControlPoints and not (x_offset == 0 and y_offset == 0):
1076            for point in self._lineControlPoints:
1077                point[0] += x_offset
1078                point[1] += y_offset
1079
1080        # Move temporary label rectangles if necessary
1081        for i in range(3):
1082            if self._labelObjects[i]:
1083                self._labelObjects[i].Erase(dc)
1084                xp, yp = self.GetLabelPosition(i)
1085                if i < len(self._regions):
1086                    xr, yr = self._regions[i].GetPosition()
1087                else:
1088                    xr, yr = 0, 0
1089                self._labelObjects[i].Move(dc, xp + xr, yp + yr)
1090        return True
1091
1092    def OnMoveLink(self, dc, moveControlPoints = True):
1093        """The move linke handler, called when a connected object has moved, to move the link to
1094        correct position.
1095        """
1096        if not self._from or not self._to:
1097            return
1098
1099        # Do each end - nothing in the middle. User has to move other points
1100        # manually if necessary
1101        end_x, end_y, other_end_x, other_end_y = self.FindLineEndPoints()
1102
1103        oldX, oldY = self._xpos, self._ypos
1104
1105        # pi: The first time we go through FindLineEndPoints we can't
1106        # use the middle points (since they don't have sane values),
1107        # so we just do what we do for a normal line. Then we call
1108        # Initialise to set the middle points, and then FindLineEndPoints
1109        # again, but this time (and from now on) we use the middle
1110        # points to calculate the end points.
1111        # This was buggy in the C++ version too.
1112
1113        self.SetEnds(end_x, end_y, other_end_x, other_end_y)
1114
1115        if len(self._lineControlPoints) > 2:
1116            self.Initialise()
1117
1118        # Do a second time, because one may depend on the other
1119        end_x, end_y, other_end_x, other_end_y = self.FindLineEndPoints()
1120        self.SetEnds(end_x, end_y, other_end_x, other_end_y)
1121
1122        # Try to move control points with the arc
1123        x_offset = self._xpos - oldX
1124        y_offset = self._ypos - oldY
1125
1126        # Only move control points if it's a self link. And only works
1127        # if attachment mode is ON
1128        if self._from == self._to and self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE and moveControlPoints and self._lineControlPoints and not (x_offset == 0 and y_offset == 0):
1129            for point in self._lineControlPoints[1:-1]:
1130                point[0] += x_offset
1131                point[1] += y_offset
1132
1133        self.Move(dc, self._xpos, self._ypos)
1134
1135    def FindLineEndPoints(self):
1136        """
1137        Finds the x, y points at the two ends of the line.
1138
1139        This function can be used by e.g. line-routing routines to
1140        get the actual points on the two node images where the lines will be
1141        drawn to / from.
1142        """
1143        if not self._from or not self._to:
1144            return
1145
1146        # Do each end - nothing in the middle. User has to move other points
1147        # manually if necessary.
1148        second_point = self._lineControlPoints[1]
1149        second_last_point = self._lineControlPoints[-2]
1150
1151        # pi: If we have a segmented line and this is the first time,
1152        # do this as a straight line.
1153        if len(self._lineControlPoints) > 2 and self._initialised:
1154            if self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
1155                nth, no_arcs = self.FindNth(self._from, False) # Not incoming
1156                end_x, end_y = self._from.GetAttachmentPosition(self._attachmentFrom, nth, no_arcs, self)
1157            else:
1158                end_x, end_y = self._from.GetPerimeterPoint(self._from.GetX(), self._from.GetY(), second_point[0], second_point[1])
1159
1160            if self._to.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
1161                nth, no_arch = self.FindNth(self._to, True) # Incoming
1162                other_end_x, other_end_y = self._to.GetAttachmentPosition(self._attachmentTo, nth, no_arch, self)
1163            else:
1164                other_end_x, other_end_y = self._to.GetPerimeterPoint(self._to.GetX(), self._to.GetY(), second_last_point[0], second_last_point[1])
1165        else:
1166            fromX = self._from.GetX()
1167            fromY = self._from.GetY()
1168            toX = self._to.GetX()
1169            toY = self._to.GetY()
1170
1171            if self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
1172                nth, no_arcs = self.FindNth(self._from, False)
1173                end_x, end_y = self._from.GetAttachmentPosition(self._attachmentFrom, nth, no_arcs, self)
1174                fromX = end_x
1175                fromY = end_y
1176
1177            if self._to.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
1178                nth, no_arcs = self.FindNth(self._to, True)
1179                other_end_x, other_end_y = self._to.GetAttachmentPosition(self._attachmentTo, nth, no_arcs, self)
1180                toX = other_end_x
1181                toY = other_end_y
1182
1183            if self._from.GetAttachmentMode() == ATTACHMENT_MODE_NONE:
1184                end_x, end_y = self._from.GetPerimeterPoint(self._from.GetX(), self._from.GetY(), toX, toY)
1185
1186            if self._to.GetAttachmentMode() == ATTACHMENT_MODE_NONE:
1187                other_end_x, other_end_y = self._to.GetPerimeterPoint(self._to.GetX(), self._to.GetY(), fromX, fromY)
1188
1189        return end_x, end_y, other_end_x, other_end_y
1190
1191    def OnDraw(self, dc):
1192        """The draw handler."""
1193        if not self._lineControlPoints:
1194            return
1195
1196        if self._pen:
1197            dc.SetPen(self._pen)
1198        if self._brush:
1199            dc.SetBrush(self._brush)
1200
1201        points = []
1202        for point in self._lineControlPoints:
1203            points.append(wx.Point(point[0], point[1]))
1204
1205        if self._isSpline:
1206            dc.DrawSpline(points)
1207        else:
1208            dc.DrawLines(points)
1209
1210        if sys.platform[:3] == "win":
1211            # For some reason, last point isn't drawn under Windows
1212            pt = points[-1]
1213            dc.DrawPoint(pt[0], pt[1])
1214
1215        # Problem with pen - if not a solid pen, does strange things
1216        # to the arrowhead. So make (get) a new pen that's solid.
1217        if self._pen and self._pen.GetStyle() != wx.PENSTYLE_SOLID:
1218            solid_pen = wx.Pen(self._pen.GetColour(), 1, wx.PENSTYLE_SOLID)
1219            if solid_pen:
1220                dc.SetPen(solid_pen)
1221
1222        self.DrawArrows(dc)
1223
1224    def OnDrawControlPoints(self, dc):
1225        """The draw control points handler."""
1226        if not self._drawHandles:
1227            return
1228
1229        # Draw temporary label rectangles if necessary
1230        for i in range(3):
1231            if self._labelObjects[i]:
1232                self._labelObjects[i].Draw(dc)
1233
1234        Shape.OnDrawControlPoints(self, dc)
1235
1236    def OnEraseControlPoints(self, dc):
1237        """The erase control points handler."""
1238        # Erase temporary label rectangles if necessary
1239
1240        for i in range(3):
1241            if self._labelObjects[i]:
1242                self._labelObjects[i].Erase(dc)
1243
1244        Shape.OnEraseControlPoints(self, dc)
1245
1246    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
1247        """not implemented???"""
1248        pass
1249
1250    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
1251        """not implemented???"""
1252        pass
1253
1254    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
1255        """not implemented???"""
1256        pass
1257
1258    def OnDrawContents(self, dc):
1259        """The draw contents handler."""
1260        if self.GetDisableLabel():
1261            return
1262
1263        for i in range(3):
1264            if self._regions[i]:
1265                x, y = self.GetLabelPosition(i)
1266                self.DrawRegion(dc, self._regions[i], x, y)
1267
1268    def SetTo(self, object):
1269        """Set the 'to' object for the line."""
1270        self._to = object
1271
1272    def SetFrom(self, object):
1273        """Set the 'from' object for the line."""
1274        self._from = object
1275
1276    def MakeControlPoints(self):
1277        """Make handle control points."""
1278        if self._canvas and self._lineControlPoints:
1279            first = self._lineControlPoints[0]
1280            last = self._lineControlPoints[-1]
1281
1282            control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, first[0], first[1], CONTROL_POINT_ENDPOINT_FROM)
1283            control._point = first
1284            self._canvas.AddShape(control)
1285            self._controlPoints.append(control)
1286
1287            for point in self._lineControlPoints[1:-1]:
1288                control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, point[0], point[1], CONTROL_POINT_LINE)
1289                control._point = point
1290                self._canvas.AddShape(control)
1291                self._controlPoints.append(control)
1292
1293            control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, last[0], last[1], CONTROL_POINT_ENDPOINT_TO)
1294            control._point = last
1295            self._canvas.AddShape(control)
1296            self._controlPoints.append(control)
1297
1298    def ResetControlPoints(self):
1299        """Reset the control points."""
1300        if self._canvas and self._lineControlPoints and self._controlPoints:
1301            for i in range(min(len(self._controlPoints), len(self._lineControlPoints))):
1302                point = self._lineControlPoints[i]
1303                control = self._controlPoints[i]
1304                control.SetX(point[0])
1305                control.SetY(point[1])
1306
1307    def Select(self, select, dc = None):
1308        """Overridden select, to create / delete temporary label-moving objects."""
1309        Shape.Select(self, select, dc)
1310        if select:
1311            for i in range(3):
1312                if self._regions[i]:
1313                    region = self._regions[i]
1314                    if region._formattedText:
1315                        w, h = region.GetSize()
1316                        x, y = region.GetPosition()
1317                        xx, yy = self.GetLabelPosition(i)
1318
1319                        if self._labelObjects[i]:
1320                            self._labelObjects[i].Select(False)
1321                            self._labelObjects[i].RemoveFromCanvas(self._canvas)
1322
1323                        self._labelObjects[i] = self.OnCreateLabelShape(self, region, w, h)
1324                        self._labelObjects[i].AddToCanvas(self._canvas)
1325                        self._labelObjects[i].Show(True)
1326                        if dc:
1327                            self._labelObjects[i].Move(dc, x + xx, y + yy)
1328                        self._labelObjects[i].Select(True, dc)
1329        else:
1330            for i in range(3):
1331                if self._labelObjects[i]:
1332                    self._labelObjects[i].Select(False, dc)
1333                    self._labelObjects[i].Erase(dc)
1334                    self._labelObjects[i].RemoveFromCanvas(self._canvas)
1335                    self._labelObjects[i] = None
1336
1337    # Control points ('handles') redirect control to the actual shape, to
1338    # make it easier to override sizing behaviour.
1339    def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0):
1340        """The sizing drag left handler."""
1341        dc = wx.MemoryDC()
1342        dc.SelectObject(self.GetCanvas().GetBuffer())
1343        self.GetCanvas().PrepareDC(dc)
1344        dc.SetLogicalFunction(OGLRBLF)
1345
1346        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_DOT)
1347        dc.SetPen(dottedPen)
1348        dc.SetBrush(wx.TRANSPARENT_BRUSH)
1349
1350        if pt._type == CONTROL_POINT_LINE:
1351            x, y = self._canvas.Snap(x, y)
1352
1353            pt.SetX(x)
1354            pt.SetY(y)
1355            pt._point[0] = x
1356            pt._point[1] = y
1357
1358            old_pen = self.GetPen()
1359            old_brush = self.GetBrush()
1360
1361            self.SetPen(dottedPen)
1362            self.SetBrush(wx.TRANSPARENT_BRUSH)
1363
1364            self.GetEventHandler().OnMoveLink(dc, False)
1365
1366            self.SetPen(old_pen)
1367            self.SetBrush(old_brush)
1368
1369    def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0):
1370        """The sizing begin drag left handler."""
1371        dc = wx.MemoryDC()
1372        dc.SelectObject(self.GetCanvas().GetBuffer())
1373        self.GetCanvas().PrepareDC(dc)
1374
1375        if pt._type == CONTROL_POINT_LINE:
1376            pt._originalPos = pt._point
1377            x, y = self._canvas.Snap(x, y)
1378
1379            self.Erase(dc)
1380
1381            # Redraw start and end objects because we've left holes
1382            # when erasing the line
1383            self.GetFrom().OnDraw(dc)
1384            self.GetFrom().OnDrawContents(dc)
1385            self.GetTo().OnDraw(dc)
1386            self.GetTo().OnDrawContents(dc)
1387
1388            self.SetDisableLabel(True)
1389            dc.SetLogicalFunction(OGLRBLF)
1390
1391            pt._xpos = x
1392            pt._ypos = y
1393            pt._point[0] = x
1394            pt._point[1] = y
1395
1396            old_pen = self.GetPen()
1397            old_brush = self.GetBrush()
1398
1399            dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_DOT)
1400            self.SetPen(dottedPen)
1401            self.SetBrush(wx.TRANSPARENT_BRUSH)
1402
1403            self.GetEventHandler().OnMoveLink(dc, False)
1404
1405            self.SetPen(old_pen)
1406            self.SetBrush(old_brush)
1407
1408        if pt._type == CONTROL_POINT_ENDPOINT_FROM or pt._type == CONTROL_POINT_ENDPOINT_TO:
1409            self._canvas.SetCursor(wx.Cursor(wx.CURSOR_BULLSEYE))
1410            pt._oldCursor = wx.STANDARD_CURSOR
1411
1412    def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0):
1413        """The sizing end drag left handler."""
1414        dc = wx.MemoryDC()
1415        dc.SelectObject(self.GetCanvas().GetBuffer())
1416        self.GetCanvas().PrepareDC(dc)
1417
1418        self.SetDisableLabel(False)
1419
1420        if pt._type == CONTROL_POINT_LINE:
1421            x, y = self._canvas.Snap(x, y)
1422
1423            rpt = wx.RealPoint(x, y)
1424
1425            # Move the control point back to where it was;
1426            # MoveControlPoint will move it to the new position
1427            # if it decides it wants. We only moved the position
1428            # during user feedback so we could redraw the line
1429            # as it changed shape.
1430            pt._xpos = pt._originalPos[0]
1431            pt._ypos = pt._originalPos[1]
1432            pt._point[0] = pt._originalPos[0]
1433            pt._point[1] = pt._originalPos[1]
1434
1435            self.OnMoveMiddleControlPoint(dc, pt, rpt)
1436
1437        if pt._type == CONTROL_POINT_ENDPOINT_FROM:
1438            if pt._oldCursor:
1439                self._canvas.SetCursor(pt._oldCursor)
1440
1441            if self.GetFrom():
1442                self.GetFrom().MoveLineToNewAttachment(dc, self, x, y)
1443
1444        if pt._type == CONTROL_POINT_ENDPOINT_TO:
1445            if pt._oldCursor:
1446                self._canvas.SetCursor(pt._oldCursor)
1447
1448            if self.GetTo():
1449                self.GetTo().MoveLineToNewAttachment(dc, self, x, y)
1450
1451    # This is called only when a non-end control point is moved
1452    def OnMoveMiddleControlPoint(self, dc, lpt, pt):
1453        """The move middle control point handler."""
1454        lpt._xpos = pt[0]
1455        lpt._ypos = pt[1]
1456
1457        lpt._point[0] = pt[0]
1458        lpt._point[1] = pt[1]
1459
1460        self.GetEventHandler().OnMoveLink(dc)
1461
1462        return True
1463
1464    def AddArrow(self, type, end = ARROW_POSITION_END, size = 10.0, xOffset = 0.0, name = "", mf = None, arrowId = -1):
1465        """
1466        Add an arrow (or annotation) to the line.
1467
1468        :param `type`: an arrow head type, one of the following
1469
1470         ======================================== ==================================
1471         Arrow head type                          Description
1472         ======================================== ==================================
1473         `ARROW_HOLLOW_CIRCLE`                    a hollow circle
1474         `ARROW_FILLED_CIRCLE`                    a filled circle
1475         `ARROW_ARROW`                            an arrow
1476         `ARROW_SINGLE_OBLIQUE`                   a single oblique
1477         `ARROW_DOUBLE_OBLIQUE`                   a double oblique
1478         `ARROW_METAFILE`                         custom, define in metafile
1479         ======================================== ==================================
1480
1481        :param `end`: may be one of the following
1482
1483         ======================================== ==================================
1484         Arrow head type                          Description
1485         ======================================== ==================================
1486         `ARROW_POSITION_END`                     arrow appears at the end
1487         `ARROW_POSITION_START`                   arrow appears at the start
1488         ======================================== ==================================
1489
1490        :param `size`: specifies the lenght of the arrow
1491        :param `xOffset`: specifies the offset from the end of the line
1492        :param `name`: specifies a name
1493        :param `mf`: mf can be a wxPseduoMetaFile, perhaps loaded from a simple Windows
1494          metafile.
1495        :param `arrowId`: the id for the arrow
1496
1497        """
1498        arrow = ArrowHead(type, end, size, xOffset, name, mf, arrowId)
1499        self._arcArrows.append(arrow)
1500        return arrow
1501
1502    # Add arrowhead at a particular position in the arrowhead list
1503    def AddArrowOrdered(self, arrow, referenceList, end):
1504        """
1505        Add an arrowhead in the position indicated by the reference list
1506        of arrowheads, which contains all legal arrowheads for this line, in
1507        the correct order. E.g.
1508
1509        Reference list:      a b c d e
1510        Current line list:   a d
1511
1512        Add c, then line list is: a c d.
1513
1514        If no legal arrowhead position, return FALSE. Assume reference list
1515        is for one end only, since it potentially defines the ordering for
1516        any one of the 3 positions. So we don't check the reference list for
1517        arrowhead position.
1518
1519        :param `arrow`: an instance of :class:`ArrowHead`
1520        :param `referenceList`: ???
1521        :param `end`: ???
1522
1523        """
1524        if not referenceList:
1525            return False
1526
1527        targetName = arrow.GetName()
1528
1529        # First check whether we need to insert in front of list,
1530        # because this arrowhead is the first in the reference
1531        # list and should therefore be first in the current list.
1532        refArrow = referenceList[0]
1533        if refArrow.GetName() == targetName:
1534            self._arcArrows.insert(0, arrow)
1535            return True
1536
1537        i1 = i2 = 0
1538        while i1 < len(referenceList) and i2 < len(self._arcArrows):
1539            refArrow = referenceList[i1]
1540            currArrow = self._arcArrows[i2]
1541
1542            # Matching: advance current arrow pointer
1543            if currArrow.GetArrowEnd() == end and currArrow.GetName() == refArrow.GetName():
1544                i2 += 1
1545
1546            # Check if we're at the correct position in the
1547            # reference list
1548            if targetName == refArrow.GetName():
1549                if i2 < len(self._arcArrows):
1550                    self._arcArrows.insert(i2, arrow)
1551                else:
1552                    self._arcArrows.append(arrow)
1553                return True
1554            i1 += 1
1555
1556        self._arcArrows.append(arrow)
1557        return True
1558
1559    def ClearArrowsAtPosition(self, end):
1560        """
1561        Delete the arrows at the specified position, or at any position if position is -1.
1562
1563        :param `end`: position to clear arrow from
1564
1565        """
1566        if end == -1:
1567            self._arcArrows = []
1568            return
1569
1570        for arrow in self._arcArrows:
1571            if arrow.GetArrowEnd() == end:
1572                self._arcArrows.remove(arrow)
1573
1574    def ClearArrow(self, name):
1575        """
1576        Delete the arrow with the given name.
1577
1578        :param `name`: name of arrow to delete
1579
1580        """
1581        for arrow in self._arcArrows:
1582            if arrow.GetName() == name:
1583                self._arcArrows.remove(arrow)
1584                return True
1585        return False
1586
1587    def FindArrowHead(self, position, name):
1588        """
1589        Find arrowhead by position and name.
1590
1591        if position is -1, matches any position.
1592
1593        :param `position`: position of arrow to find or -1
1594        :param `name`: name of arrow to find
1595
1596        """
1597        for arrow in self._arcArrows:
1598            if (position == -1 or position == arrow.GetArrowEnd()) and arrow.GetName() == name:
1599                return arrow
1600
1601        return None
1602
1603    def FindArrowHeadId(self, arrowId):
1604        """
1605        Find arrowhead by id.
1606
1607        :param `arrowId`: id of arrow to find
1608
1609        """
1610        for arrow in self._arcArrows:
1611            if arrowId == arrow.GetId():
1612                return arrow
1613
1614        return None
1615
1616    def DeleteArrowHead(self, position, name):
1617        """
1618        Delete arrowhead by position and name.
1619
1620        if position is -1, matches any position.
1621
1622        :param `position`: position of arrow to delete or -1
1623        :param `name`: name of arrow to delete
1624
1625        """
1626        for arrow in self._arcArrows:
1627            if (position == -1 or position == arrow.GetArrowEnd()) and arrow.GetName() == name:
1628                self._arcArrows.remove(arrow)
1629                return True
1630        return False
1631
1632    def DeleteArrowHeadId(self, arrowId):
1633        """
1634        Delete arrowhead by id.
1635
1636        :param `arrowId`: id of arrow to delete
1637
1638        """
1639        for arrow in self._arcArrows:
1640            if arrowId == arrow.GetId():
1641                self._arcArrows.remove(arrow)
1642                return True
1643        return False
1644
1645    # Calculate the minimum width a line
1646    # occupies, for the purposes of drawing lines in tools.
1647    def FindMinimumWidth(self):
1648        """
1649        Find the horizontal width for drawing a line with arrows in
1650        minimum space. Assume arrows at end only.
1651        """
1652        minWidth = 0.0
1653        for arrowHead in self._arcArrows:
1654            minWidth += arrowHead.GetSize()
1655            if arrowHead != self._arcArrows[-1]:
1656                minWidth += arrowHead + arrowHead.GetSpacing()
1657
1658        # We have ABSOLUTE minimum now. So
1659        # scale it to give it reasonable aesthetics
1660        # when drawing with line.
1661        if minWidth > 0:
1662            minWidth = minWidth * 1.4
1663        else:
1664            minWidth = 20.0
1665
1666        self.SetEnds(0.0, 0.0, minWidth, 0.0)
1667        self.Initialise()
1668
1669        return minWidth
1670
1671    def FindLinePosition(self, x, y):
1672        """
1673        Find which position we're talking about.
1674
1675        :param `x`: x position
1676        :param `y`: y position
1677
1678        :returns: ARROW_POSITION_START or ARROW_POSITION_MIDDLE or ARROW_POSITION_END.
1679        """
1680        startX, startY, endX, endY = self.GetEnds()
1681
1682        # Find distances from centre, start and end. The smallest wins
1683        centreDistance = math.sqrt((x - self._xpos) * (x - self._xpos) + (y - self._ypos) * (y - self._ypos))
1684        startDistance = math.sqrt((x - startX) * (x - startX) + (y - startY) * (y - startY))
1685        endDistance = math.sqrt((x - endX) * (x - endX) + (y - endY) * (y - endY))
1686
1687        if centreDistance < startDistance and centreDistance < endDistance:
1688            return ARROW_POSITION_MIDDLE
1689        elif startDistance < endDistance:
1690            return ARROW_POSITION_START
1691        else:
1692            return ARROW_POSITION_END
1693
1694    def SetAlignmentOrientation(self, isEnd, isHoriz):
1695        """
1696        Set the alignment orientation.
1697
1698        :param `isEnd`: True or False ???
1699        :param `isHoriz`: True of False ???
1700
1701        """
1702        if isEnd:
1703            if isHoriz and self._alignmentEnd & LINE_ALIGNMENT_HORIZ != LINE_ALIGNMENT_HORIZ:
1704                self._alignmentEnd != LINE_ALIGNMENT_HORIZ
1705            elif not isHoriz and self._alignmentEnd & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ:
1706                self._alignmentEnd -= LINE_ALIGNMENT_HORIZ
1707        else:
1708            if isHoriz and self._alignmentStart & LINE_ALIGNMENT_HORIZ != LINE_ALIGNMENT_HORIZ:
1709                self._alignmentStart != LINE_ALIGNMENT_HORIZ
1710            elif not isHoriz and self._alignmentStart & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ:
1711                self._alignmentStart -= LINE_ALIGNMENT_HORIZ
1712
1713    def SetAlignmentType(self, isEnd, alignType):
1714        """
1715        Set the alignment type.
1716
1717        :param `isEnd`: if True set the type for the begin, else for the end ???
1718        :param `alignType`: one of the following
1719
1720         ======================================== ==================================
1721         Arrow head type                          Description
1722         ======================================== ==================================
1723         `LINE_ALIGNMENT_HORIZ`                   Align horizontally
1724         `LINE_ALIGNMENT_VERT`                    Align vertically
1725         `LINE_ALIGNMENT_TO_NEXT_HANDLE`          Align to next handle
1726         `LINE_ALIGNMENT_NONE`                    vertical by default
1727         ======================================== ==================================
1728
1729        """
1730        if isEnd:
1731            if alignType == LINE_ALIGNMENT_TO_NEXT_HANDLE:
1732                if self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE != LINE_ALIGNMENT_TO_NEXT_HANDLE:
1733                    self._alignmentEnd |= LINE_ALIGNMENT_TO_NEXT_HANDLE
1734            elif self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE == LINE_ALIGNMENT_TO_NEXT_HANDLE:
1735                self._alignmentEnd -= LINE_ALIGNMENT_TO_NEXT_HANDLE
1736        else:
1737            if alignType == LINE_ALIGNMENT_TO_NEXT_HANDLE:
1738                if self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE != LINE_ALIGNMENT_TO_NEXT_HANDLE:
1739                    self._alignmentStart |= LINE_ALIGNMENT_TO_NEXT_HANDLE
1740            elif self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE == LINE_ALIGNMENT_TO_NEXT_HANDLE:
1741                self._alignmentStart -= LINE_ALIGNMENT_TO_NEXT_HANDLE
1742
1743    def GetAlignmentOrientation(self, isEnd):
1744        """
1745        Get the alignment orientation.
1746
1747        :param `isEnd`: if True get the type for the begin, else for the end ???
1748
1749        """
1750        if isEnd:
1751            return self._alignmentEnd & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ
1752        else:
1753            return self._alignmentStart & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ
1754
1755    def GetAlignmentType(self, isEnd):
1756        """
1757        Get the alignment type.
1758
1759        :param `isEnd`: if True get the type for the begin, else for the end ???
1760
1761        """
1762        if isEnd:
1763            return self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE
1764        else:
1765            return self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE
1766
1767    def GetNextControlPoint(self, shape):
1768        """Find the next control point in the line after the start / end point,
1769        depending on whether the shape is at the start or end.
1770
1771        :param `shape`: an instance of :class:`~lib.ogl.Shape` ???
1772        """
1773        n = len(self._lineControlPoints)
1774        if self._to == shape:
1775            # Must be END of line, so we want (n - 1)th control point.
1776            # But indexing ends at n-1, so subtract 2.
1777            nn = n - 2
1778        else:
1779            nn = 1
1780        if nn < len(self._lineControlPoints):
1781            return self._lineControlPoints[nn]
1782        return None
1783
1784    def OnCreateLabelShape(self, parent, region, w, h):
1785        """Create label shape handler."""
1786        return LabelShape(parent, region, w, h)
1787
1788
1789    def OnLabelMovePre(self, dc, labelShape, x, y, old_x, old_y, display):
1790        """Label move 'pre' handler. ???"""
1791        labelShape._shapeRegion.SetSize(labelShape.GetWidth(), labelShape.GetHeight())
1792
1793        # Find position in line's region list
1794        i = self._regions.index(labelShape._shapeRegion)
1795
1796        xx, yy = self.GetLabelPosition(i)
1797        # Set the region's offset, relative to the default position for
1798        # each region.
1799        labelShape._shapeRegion.SetPosition(x - xx, y - yy)
1800        labelShape.SetX(x)
1801        labelShape.SetY(y)
1802
1803        # Need to reformat to fit region
1804        if labelShape._shapeRegion.GetText():
1805            s = labelShape._shapeRegion.GetText()
1806            labelShape.FormatText(dc, s, i)
1807            self.DrawRegion(dc, labelShape._shapeRegion, xx, yy)
1808        return True
1809
1810