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