1# -*- coding: utf-8 -*- 2# Copyright (C) 2012, Almar Klein 3# 4# Visvis is distributed under the terms of the (new) BSD License. 5# The full license can be found in 'license.txt'. 6 7""" Module axises 8 9Defines the Axis wobject class to draw tickmarks and lines for each 10dimension. 11 12I chose to name this module using an awkward plural to avoid a name clash 13with the axis() function. 14 15 16""" 17 18# todo: split in multiple modules axis_base axis_2d, axis_3d, axis_polar 19 20import OpenGL.GL as gl 21import OpenGL.GLU as glu 22 23import numpy as np 24import math 25 26from visvis.utils.pypoints import Pointset, Point 27# 28from visvis.core import base 29from visvis.core.misc import Range, getColor, basestring 30from visvis.core.misc import Property, PropWithDraw, DrawAfter 31# 32from visvis.text import Text 33from visvis.core.line import lineStyles, PolarLine 34from visvis.core.cameras import depthToZ, TwoDCamera, FlyCamera 35 36 37# A note about tick labels. We format these such that the width of the ticks 38# never becomes larger than 10 characters (including sign bit). 39# With a fontsize of 9, this needs little less than 70 pixels. The 40# correction applied when visualizing axis (and ticks) is 60, because 41# the default offset is 10 pixels for the axes. 42# See the docstring of GetTickTexts() for more info. 43 44# create tick units 45_tickUnits = [] 46for e in range(-10, 98): 47 for i in [10, 20, 25, 50]: 48 _tickUnits.append( i*10**e) 49 50 51class AxisText(Text): 52 """ Text with a disabled Draw() method. """ 53 54 def Draw(self): 55 pass 56 57 @Property 58 def x(): 59 """Get/Set the x position of the text.""" 60 def fget(self): 61 return self._x 62 def fset(self, value): 63 self._x = value 64 return locals() 65 66 @Property 67 def y(): 68 """Get/Set the y position of the text.""" 69 def fget(self): 70 return self._y 71 def fset(self, value): 72 self._y = value 73 return locals() 74 75 @Property 76 def z(): 77 """Get/Set the z position of the text.""" 78 def fget(self): 79 return self._z 80 def fset(self, value): 81 self._z = value 82 return locals() 83 84 85class AxisLabel(AxisText): 86 """ AxisLabel(parent, text) 87 88 A special label that moves itself just past the tickmarks. 89 The _textDict attribute should contain the Text objects of the tickmarks. 90 91 This is a helper class for the axis classes, and has a disabled Draw() 92 method. 93 94 """ 95 96 def __init__(self, *args, **kwargs): 97 Text.__init__(self, *args, **kwargs) 98 self._textDict = {} 99 self._move = 0 100 101 # upon creation, one typically needs a second draw; only after all 102 # ticks are drawn can this label be positioned properly. 103 104 def OnDrawScreen(self): 105 106 # get current position 107 pos = Point(self._screenx, self._screeny) 108 109 # get normal vector eminating from that position 110 if int(self.textAngle) == 90: 111 a = (self.textAngle + 90) * np.pi/180 112 self.valign = 1 113 distance = 8 114 else: 115 a = (self.textAngle - 90) * np.pi/180 116 self.valign = -1 117 distance = 3 118 normal = Point(np.cos(a), np.sin(a)).normalize() 119 120 # project the corner points of all text objects to the normal vector. 121 def project(p,normal): 122 p = p-pos 123 phi = abs(normal.angle(p)) 124 return float( p.norm()*np.cos(phi) ) 125 # apply 126 alpha = [] 127 for text in self._textDict.values(): 128 if text is self: 129 continue 130 if not text.isPositioned: 131 continue # Only consider drawn text objects 132 x,y = text._screenx, text._screeny 133 deltax, deltay = text.GetVertexLimits() 134 xmin, xmax = deltax 135 ymin, ymax = deltay 136 alpha.append( project(Point(x+xmin, y+ymin), normal) ) 137 alpha.append( project(Point(x+xmin, y+ymax), normal) ) 138 alpha.append( project(Point(x+xmax, y+ymin), normal) ) 139 alpha.append( project(Point(x+xmax, y+ymax), normal) ) 140 141 # establish the amount of pixels that we should move along the normal. 142 if alpha: 143 self._move = distance+max(alpha) 144 145 # move in the direction of the normal 146 tmp = pos + normal * self._move 147 self._screenx, self._screeny = int(tmp.x+0.5), int(tmp.y+0.5) 148 149 # draw and reset position 150 Text.OnDrawScreen(self) 151 self._screenx, self._screeny = pos.x, pos.y 152 153 154def GetTickTexts(ticks): 155 """ GetTickTexts(ticks) 156 157 Get tick labels of maximally 9 characters (plus sign char). 158 159 All ticks will be formatted in the same manner, and with the same number 160 of decimals. In exponential notation, the exponent is written with as 161 less characters as possible, leaving more chars for the decimals. 162 163 The algorithm is to first test for each tick the number of characters 164 before the dot, the number of decimals, and the number of chars for 165 the exponent. Then the ticks are formatted only without exponent if 166 the first two chars (plus one for the dot) are less than 9. 167 168 Examples are: 169 xx.yyyyyy 170 xxxxxxx.y 171 x.yyyye+z 172 x.yye+zzz 173 174 """ 175 176 # For padding/unpadding exponent notation 177 def exp_pad(s, i=1): 178 return s.lstrip('0').rjust(i,'0') 179 180 181 # Round 1: determine amount of chars before dot, after dot, in exp 182 minChars1, maxChars1 = 99999, 0 183 maxChars2 = 0 184 maxChars3 = 0 185 for tick in ticks: 186 187 # Make abs, our goal is to format the ticks such that without 188 # the sign char, the string is smaller than 9 chars. 189 tick = abs(tick) 190 191 # Format with exponential notation and get exponent 192 t = '%1.0e' % tick 193 i = t.find('e') 194 expPart = t[i+2:] 195 196 # Get number of chars before dot 197 chars1 = int(expPart)+1 198 maxChars1 = max(maxChars1, chars1) 199 minChars1 = min(minChars1, chars1) 200 201 # Get number of chars in exponent 202 maxChars3 = max(maxChars3, len(exp_pad(expPart))) 203 204 # Get number of chars after the dot 205 t = '%1.7f' % tick 206 i = t.find('.') 207 decPart = t[i+1:] 208 maxChars2 = max(maxChars2, len(decPart.rstrip('0'))) 209 210 # Round 2: Create actual texts 211 ticks2 = [] 212 if maxChars1 + maxChars2 + 1 <= 9: 213 # This one is easy 214 215 chars2 = maxChars2 216 f = '%%1.%if' % chars2 217 for tick in ticks: 218 # Format tick and store 219 if tick == -0: tick = 0 220 ticks2.append( f % tick ) 221 222 elif maxChars1 < 9: 223 # Do the best we can 224 225 chars2 = 9 - (maxChars1+1) 226 f = '%%1.%if' % chars2 227 for tick in ticks: 228 # Format tick and store 229 if tick == -0: tick = 0 230 ticks2.append( f % tick ) 231 232 else: 233 # Exponential notation 234 chars2 = 9 - (4+maxChars3) # 0.xxxe+yy 235 f = '%%1.%ie' % chars2 236 for tick in ticks: 237 # Format tick 238 if tick == -0: tick = 0 239 t = f % tick 240 # Remove zeros in exp 241 i = t.find('e') 242 t = t[:i+2] + exp_pad(t[i+2:], maxChars3) 243 # Store 244 ticks2.append(t) 245 246 # Done 247 return ticks2 248 249def GetTickText_deprecated(tick): 250 """ GetTickText(tick) 251 252 Obtain text from a tick. Convert to exponential notation 253 if necessary. 254 255 """ 256 257 # Correct -0: 0 has on some systems been reported to be shown as -0 258 if tick == -0: 259 tick = 0 260 # Get text 261 text = '%1.4g' % tick 262 iExp = text.find('e') 263 if iExp>0: 264 front = text[:iExp+2] 265 text = front + text[iExp+2:].lstrip('0') 266 return text 267 268 269def GetTicks(p0, p1, lim, minTickDist=40, givenTicks=None): 270 """ GetTicks(p0, p1, lim, minTickDist=40, ticks=None) 271 272 Get the tick values, position and texts. 273 These are calculated from a start end end position and the range 274 of values to map on a straight line between these two points 275 (which can be 2d or 3d). If givenTicks is given, use these values instead. 276 277 """ 278 279 # Vector from start to end point 280 vec = p1-p0 281 282 # Init tick stuff 283 tickValues = [] 284 tickTexts = [] 285 tickPositions = [] 286 287 if givenTicks is None: 288 # Calculate all ticks if not given 289 290 # Get pixels per unit 291 if lim.range == 0: 292 return [],[],[] 293 294 # Pixels per unit (use float64 to prevent inf for large numbers) 295 pixelsPerUnit = float( vec.norm() / lim.range ) 296 297 # Try all tickunits, starting from the smallest, until we find 298 # one which results in a distance between ticks more than 299 # X pixels. 300 try: 301 for tickUnit in _tickUnits: 302 if tickUnit * pixelsPerUnit >= minTickDist: 303 break 304 # if the numbers are VERY VERY large (which is very unlikely) 305 # We use smaller-equal and a multiplication, so the error 306 # is also raised when pixelsPerUnit and minTickDist are inf. 307 # Thanks to Torquil Macdonald Sorensen for this bug report. 308 if tickUnit*pixelsPerUnit <= 0.99*minTickDist: 309 raise ValueError 310 except (ValueError, TypeError): 311 # too small 312 return [],[],[] 313 314 # Calculate the ticks (the values) themselves 315 firstTick = np.ceil( lim.min/tickUnit ) * tickUnit 316 lastTick = np.floor( lim.max/tickUnit ) * tickUnit 317 count = 0 318 tickValues.append(firstTick) 319 while tickValues[-1] < lastTick-tickUnit/2: 320 count += 1 321 t = firstTick + count*tickUnit 322 tickValues.append(t) 323 if count > 1000: 324 break # Safety 325 # Get tick texts 326 tickTexts = GetTickTexts(tickValues) 327 328 elif isinstance(givenTicks, dict): 329 # Use given ticks in dict 330 331 for tickValue in givenTicks: 332 if tickValue >= lim.min and tickValue <= lim.max: 333 tickText = givenTicks[tickValue] 334 tickValues.append(tickValue) 335 if isinstance(tickText, basestring): 336 tickTexts.append(tickText) 337 else: 338 tickTexts.append(str(tickText)) 339 340 elif isinstance(givenTicks, (tuple,list)): 341 # Use given ticks as list 342 343 # Init temp tick texts list 344 tickTexts2 = [] 345 346 for i in range(len(givenTicks)): 347 348 # Get tick 349 t = givenTicks[i] 350 if isinstance(t, basestring): 351 tickValue = i 352 tickText = t 353 else: 354 tickValue = float(t) 355 tickText = None 356 357 # Store 358 if tickValue >= lim.min and tickValue <= lim.max: 359 tickValues.append(tickValue) 360 tickTexts2.append(tickText) 361 362 # Get tick text that we normally would have used 363 tickTexts = GetTickTexts(tickValues) 364 365 # Replace with any given strings 366 for i in range(len(tickTexts)): 367 tmp = tickTexts2[i] 368 if tmp is not None: 369 tickTexts[i] = tmp 370 371 372 # Calculate tick positions 373 for t in tickValues: 374 pos = p0 + vec * ( (t-lim.min) / lim.range ) 375 tickPositions.append( pos ) 376 377 # Done 378 return tickValues, tickPositions, tickTexts 379 380 381class BaseAxis(base.Wobject): 382 """ BaseAxis(parent) 383 384 This is the (abstract) base class for all axis classes, such 385 as the CartesianAxis and PolarAxis. 386 387 An Axis object represents the lines, ticks and grid that make 388 up an axis. Not to be confused with an Axes, which represents 389 a scene and is a Wibject. 390 391 """ 392 # This documentation holds for the 3D axis, the 2D axis is a bit 393 # simpeler in some aspects. 394 # 395 # The scene is limits by the camera limits, thus forming a cube 396 # The axis is drawn on this square. 397 # The ASCI-art image below illustrates how the corners of this cube 398 # are numbered. 399 # 400 # The thicks are drawn along three ridges of the cube. A reference 401 # corner is selected first, which has a corresponding ridge vector. 402 # 403 # In orthogonal view, all ridges are parellel, but this is not the 404 # case in projective view. For each dimension there are 4 ridges to 405 # consider. Any grid lines are drawn between two ridges. The amount 406 # of ticks to draw (or minTickDist to be precise) should be determined 407 # based on the shortest ridge. 408 # 409 # 6 O---------------O 7 410 # /| /| 411 # / / | 412 # / | / | 413 # 3 O---------------O 5 | 414 # | | | | 415 # | 2 o- - - - - -|- -O 4 416 # | / | / 417 # | | / 418 # |/ |/ 419 # 0 O---------------O 1 420 # 421 # / \ _ 422 # | /| 423 # | z / x 424 # | / y -----> 425 # 426 427 428 def __init__(self, parent): 429 base.Wobject.__init__(self, parent) 430 431 # Make the axis the first wobject in the list. This somehow seems 432 # right and makes the Axes.axis property faster. 433 if hasattr(parent, '_wobjects') and self in parent._wobjects: 434 parent._wobjects.remove(self) 435 parent._wobjects.insert(0, self) 436 437 # Init property variables 438 self._showBox = True 439 self._axisColor = (0,0,0) 440 self._tickFontSize = 9 441 self._gridLineStyle = ':' 442 self._xgrid, self._ygrid, self._zgrid = False, False, False 443 self._xminorgrid, self._yminorgrid, self._zminorgrid =False,False,False 444 self._xticks, self._yticks, self._zticks = None, None, None 445 self._xlabel, self._ylabel, self._zlabel = '','','' 446 447 # For the cartesian 2D axis, xticks can be rotated 448 self._xTicksAngle = 0 449 450 # Define parameters 451 self._lineWidth = 1 # 0.8 452 self._minTickDist = 40 453 454 # Corners of a cube in relative coordinates 455 self._corners = tmp = Pointset(3) 456 tmp.append(0,0,0); tmp.append(1,0,0); tmp.append(0,1,0) 457 tmp.append(0,0,1); tmp.append(1,1,0); tmp.append(1,0,1) 458 tmp.append(0,1,1); tmp.append(1,1,1) 459 460 # Indices of the base corners for each dimension. 461 # The order is very important, don't mess it up... 462 self._cornerIndicesPerDirection = [ [0,2,6,3], [3,5,1,0], [0,1,4,2] ] 463 # And the indices of the corresponding pair corners 464 self._cornerPairIndicesPerDirection = [ [1,4,7,5], [6,7,4,2], [3,5,7,6] ] 465 466 # Dicts to be able to optimally reuse text objects; creating new 467 # text objects or changing the text takes a relatively large amount 468 # of time (if done every draw). 469 self._textDicts = [{},{},{}] 470 471 472 ## Properties 473 474 475 @PropWithDraw 476 def showBox(): 477 """ Get/Set whether to show the box of the axis. """ 478 def fget(self): 479 return self._showBox 480 def fset(self, value): 481 self._showBox = bool(value) 482 return locals() 483 484 485 @PropWithDraw 486 def axisColor(): 487 """ Get/Set the color of the box, ticklines and tick marks. """ 488 def fget(self): 489 return self._axisColor 490 def fset(self, value): 491 self._axisColor = getColor(value, 'setting axis color') 492 return locals() 493 494 495 @PropWithDraw 496 def tickFontSize(): 497 """ Get/Set the font size of the tick marks. """ 498 def fget(self): 499 return self._tickFontSize 500 def fset(self, value): 501 self._tickFontSize = value 502 return locals() 503 504 505 @PropWithDraw 506 def gridLineStyle(): 507 """ Get/Set the style of the gridlines as a single char similar 508 to the lineStyle (ls) property of the line wobject (or in plot). """ 509 def fget(self): 510 return self._gridLineStyle 511 def fset(self, value): 512 if value not in lineStyles: 513 raise ValueError("Invalid lineStyle for grid lines") 514 self._gridLineStyle = value 515 return locals() 516 517 518 @PropWithDraw 519 def showGridX(): 520 """ Get/Set whether to show a grid for the x dimension. """ 521 def fget(self): 522 return self._xgrid 523 def fset(self, value): 524 self._xgrid = bool(value) 525 return locals() 526 527 @PropWithDraw 528 def showGridY(): 529 """ Get/Set whether to show a grid for the y dimension. """ 530 def fget(self): 531 return self._ygrid 532 def fset(self, value): 533 self._ygrid = bool(value) 534 return locals() 535 536 @PropWithDraw 537 def showGridZ(): 538 """ Get/Set whether to show a grid for the z dimension. """ 539 def fget(self): 540 return self._zgrid 541 def fset(self, value): 542 self._zgrid = bool(value) 543 return locals() 544 545 @PropWithDraw 546 def showGrid(): 547 """ Show/hide the grid for the x,y and z dimension. """ 548 def fget(self): 549 return self._xgrid, self._ygrid, self._zgrid 550 def fset(self, value): 551 if isinstance(value, tuple): 552 value = tuple([bool(v) for v in value]) 553 self._xgrid, self._ygrid, self._zgrid = value 554 else: 555 self._xgrid = self._ygrid = self._zgrid = bool(value) 556 return locals() 557 558 559 @PropWithDraw 560 def showMinorGridX(): 561 """ Get/Set whether to show a minor grid for the x dimension. """ 562 def fget(self): 563 return self._xminorgrid 564 def fset(self, value): 565 self._xminorgrid = bool(value) 566 return locals() 567 568 @PropWithDraw 569 def showMinorGridY(): 570 """ Get/Set whether to show a minor grid for the y dimension. """ 571 def fget(self): 572 return self._yminorgrid 573 def fset(self, value): 574 self._yminorgrid = bool(value) 575 return locals() 576 577 @PropWithDraw 578 def showMinorGridZ(): 579 """ Get/Set whether to show a minor grid for the z dimension. """ 580 def fget(self): 581 return self._zminorgrid 582 def fset(self, value): 583 self._zminorgrid = bool(value) 584 return locals() 585 586 @PropWithDraw 587 def showMinorGrid(): 588 """ Show/hide the minor grid for the x, y and z dimension. """ 589 def fget(self): 590 return self._xminorgrid, self._yminorgrid, self._zminorgrid 591 def fset(self, value): 592 if isinstance(value, tuple): 593 tmp = tuple([bool(v) for v in value]) 594 self._xminorgrid, self._yminorgrid, self._zminorgridd = tmp 595 else: 596 tmp = bool(value) 597 self._xminorgrid = self._yminorgrid = self._zminorgrid = tmp 598 return locals() 599 600 601 @PropWithDraw 602 def xTicks(): 603 """ Get/Set the ticks for the x dimension. 604 605 The value can be: 606 * None: the ticks are determined automatically. 607 * A tuple/list/numpy_array with float or string values: Floats 608 specify at which location tickmarks should be drawn. Strings are 609 drawn at integer positions corresponding to the index in the 610 given list. 611 * A dict with numbers or strings as values. The values are drawn at 612 the positions specified by the keys (which should be numbers). 613 """ 614 def fget(self): 615 return self._xticks 616 def fset(self, value): 617 m = 'Ticks must be a dict/list/tuple/numpy array of numbers or strings.' 618 if value is None: 619 self._xticks = None 620 elif isinstance(value, dict): 621 try: 622 ticks = {} 623 for key in value: 624 ticks[key] = str(value[key]) 625 self._xticks = ticks 626 except Exception: 627 raise ValueError(m) 628 elif isinstance(value, (list, tuple, np.ndarray)): 629 try: 630 ticks = [] 631 for val in value: 632 if isinstance(val, basestring): 633 ticks.append(val) 634 else: 635 ticks.append(float(val)) 636 self._xticks = ticks 637 except Exception: 638 raise ValueError(m) 639 else: 640 raise ValueError(m) 641 return locals() 642 643 644 @PropWithDraw 645 def yTicks(): 646 """ Get/Set the ticks for the y dimension. 647 648 The value can be: 649 * None: the ticks are determined automatically. 650 * A tuple/list/numpy_array with float or string values: Floats 651 specify at which location tickmarks should be drawn. Strings are 652 drawn at integer positions corresponding to the index in the 653 given list. 654 * A dict with numbers or strings as values. The values are drawn at 655 the positions specified by the keys (which should be numbers). 656 """ 657 def fget(self): 658 return self._yticks 659 def fset(self, value): 660 m = 'Ticks must be a dict/list/tuple/numpy array of numbers or strings.' 661 if value is None: 662 self._yticks = None 663 elif isinstance(value, dict): 664 try: 665 ticks = {} 666 for key in value: 667 ticks[key] = str(value[key]) 668 self._yticks = ticks 669 except Exception: 670 raise ValueError(m) 671 elif isinstance(value, (list, tuple, np.ndarray)): 672 try: 673 ticks = [] 674 for val in value: 675 if isinstance(val, basestring): 676 ticks.append(val) 677 else: 678 ticks.append(float(val)) 679 self._yticks = ticks 680 except Exception: 681 raise ValueError(m) 682 else: 683 raise ValueError(m) 684 return locals() 685 686 687 @PropWithDraw 688 def zTicks(): 689 """ Get/Set the ticks for the z dimension. 690 691 The value can be: 692 * None: the ticks are determined automatically. 693 * A tuple/list/numpy_array with float or string values: Floats 694 specify at which location tickmarks should be drawn. Strings are 695 drawn at integer positions corresponding to the index in the 696 given list. 697 * A dict with numbers or strings as values. The values are drawn at 698 the positions specified by the keys (which should be numbers). 699 """ 700 def fget(self): 701 return self._zticks 702 def fset(self, value): 703 m = 'Ticks must be a dict/list/tuple/numpy array of numbers or strings.' 704 if value is None: 705 self._zticks = None 706 elif isinstance(value, dict): 707 try: 708 ticks = {} 709 for key in value: 710 ticks[key] = str(value[key]) 711 self._zticks = ticks 712 except Exception: 713 raise ValueError(m) 714 elif isinstance(value, (list, tuple, np.ndarray)): 715 try: 716 ticks = [] 717 for val in value: 718 if isinstance(val, basestring): 719 ticks.append(val) 720 else: 721 ticks.append(float(val)) 722 self._zticks = ticks 723 except Exception: 724 raise ValueError(m) 725 else: 726 raise ValueError(m) 727 return locals() 728 729 730 @PropWithDraw 731 def xLabel(): 732 """ Get/Set the label for the x dimension. 733 """ 734 def fget(self): 735 return self._xlabel 736 def fset(self, value): 737 self._xlabel = value 738 return locals() 739 740 @PropWithDraw 741 def yLabel(): 742 """ Get/Set the label for the y dimension. 743 """ 744 def fget(self): 745 return self._ylabel 746 def fset(self, value): 747 self._ylabel = value 748 return locals() 749 750 @PropWithDraw 751 def zLabel(): 752 """ Get/Set the label for the z dimension. 753 """ 754 def fget(self): 755 return self._zlabel 756 def fset(self, value): 757 self._zlabel = value 758 return locals() 759 760 761 ## Methods for drawing 762 763 def OnDraw(self, ppc_pps_ppg=None): 764 765 # Get axes and return if there is none, 766 # or if it doesn't want to show an axis. 767 axes = self.GetAxes() 768 if not axes: 769 return 770 771 # Calculate lines and labels (or get from argument) 772 if ppc_pps_ppg: 773 ppc, pps, ppg = ppc_pps_ppg 774 else: 775 try: 776 ppc, pps, ppg = self._CreateLinesAndLabels(axes) 777 except Exception: 778 self.Destroy() # So the error message does not repeat itself 779 raise 780 781 # Store lines to be drawn in screen coordinates 782 self._pps = pps 783 784 # Prepare for drawing lines 785 gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 786 clr = self._axisColor 787 gl.glColor(clr[0], clr[1], clr[2]) 788 gl.glLineWidth(self._lineWidth) 789 790 # Draw lines 791 if len(ppc): 792 gl.glVertexPointerf(ppc.data) 793 gl.glDrawArrays(gl.GL_LINES, 0, len(ppc)) 794 795 # Draw gridlines 796 if len(ppg): 797 # Set stipple pattern 798 if not self.gridLineStyle in lineStyles: 799 stipple = False 800 else: 801 stipple = lineStyles[self.gridLineStyle] 802 if stipple: 803 gl.glEnable(gl.GL_LINE_STIPPLE) 804 gl.glLineStipple(1, stipple) 805 # Draw using array 806 gl.glVertexPointerf(ppg.data) 807 gl.glDrawArrays(gl.GL_LINES, 0, len(ppg)) 808 809 # Clean up 810 gl.glDisableClientState(gl.GL_VERTEX_ARRAY) 811 gl.glDisable(gl.GL_LINE_STIPPLE) 812 813 814 def OnDrawScreen(self): 815 # Actually draw the axis 816 817 axes = self.GetAxes() 818 if not axes: 819 return 820 821 # get pointset 822 if not hasattr(self, '_pps') or not self._pps: 823 return 824 pps = self._pps.copy() 825 pps[:,2] = depthToZ( pps[:,2] ) 826 827 # Prepare for drawing lines 828 gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 829 gl.glVertexPointerf(pps.data) 830 if isinstance(axes.camera, TwoDCamera): 831 gl.glDisable(gl.GL_LINE_SMOOTH) 832 833 # Draw lines 834 clr = self._axisColor 835 gl.glColor(clr[0], clr[1], clr[2]) 836 gl.glLineWidth(self._lineWidth) 837 if len(pps): 838 gl.glDrawArrays(gl.GL_LINES, 0, len(pps)) 839 840 # Clean up 841 gl.glDisableClientState(gl.GL_VERTEX_ARRAY) 842 gl.glEnable(gl.GL_LINE_SMOOTH) 843 844 845 ## Help methods 846 847 def _DestroyChildren(self): 848 """ Method to clean up the children (text objects). 849 """ 850 if self._children: 851 for child in self.children: 852 child.Destroy() 853 854 855 def _CalculateCornerPositions(self, xlim, ylim, zlim): 856 """ Calculate the corner positions in world coorinates 857 and screen coordinates, given the limits for each dimension. 858 """ 859 860 # To translate to real coordinates 861 pmin = Point(xlim.min, ylim.min, zlim.min) 862 pmax = Point(xlim.max, ylim.max, zlim.max) 863 def relativeToCoord(p): 864 pi = Point(1,1,1) - p 865 return pmin*pi + pmax*p 866 867 # Get the 8 corners of the cube in real coords and screen pixels 868 # Note that in perspective mode the screen coords for points behind 869 # the near clipping plane are undefined. This results in odd values, 870 # which should be accounted for. This is mostly only a problem for 871 # the fly camera though. 872 proj = glu.gluProject 873 874 corners8_c = [relativeToCoord(p) for p in self._corners] 875 corners8_s = [Point(proj(p.x,p.y,p.z)) for p in corners8_c] 876 877 878 # Return 879 return corners8_c, corners8_s 880 881 882 def _GetTicks(self, tickUnit, lim): 883 """ Given tickUnit (the distance in world units between the ticks) 884 and the range to cover (lim), calculate the actual tick values. 885 """ 886 887 # Get position of first and last tick 888 firstTick = np.ceil( lim.min/tickUnit ) * tickUnit 889 lastTick = np.floor( lim.max/tickUnit ) * tickUnit 890 891 # Valid range? 892 if firstTick > lim.max or lastTick < lim.min: 893 return [] 894 895 # Create ticks 896 count = 0 897 ticks = [firstTick] 898 while ticks[-1] < lastTick-tickUnit: 899 count += 1 900# tmp = firstTick + count*tickUnit 901# if abs(tmp/tickUnit) < 10**-10: 902# tmp = 0 # due round-off err, 0 can otherwise be 0.5e-17 or so 903# ticks.append(tmp) 904 ticks.append( firstTick + count*tickUnit ) 905 return ticks 906 907 908 def _NextCornerIndex(self, i, d, vector_s): 909 """ Calculate the next corner index. 910 """ 911 912 if d<2 and vector_s.x >= 0: 913 i+=self._delta 914 elif d==2 and vector_s.y < 0: 915 i+=self._delta 916 else: 917 i-=self._delta 918 if i>3: i=0 919 if i<0: i=3 920 return i 921 922 923 def _CreateLinesAndLabels(self, axes): 924 """ This is the method that calculates where lines should be 925 drawn and where labels should be placed. 926 927 It returns three point sets in which the pairs of points 928 represent the lines to be drawn (using GL_LINES): 929 * ppc: lines in real coords 930 * pps: lines in screen pixels 931 * ppg: dotted lines in real coords 932 """ 933 raise NotImplementedError('This is the abstract base class.') 934 935 936class CartesianAxis2D(BaseAxis): 937 """ CartesianAxis2D(parent) 938 939 An Axis object represents the lines, ticks and grid that make 940 up an axis. Not to be confused with an Axes, which represents 941 a scene and is a Wibject. 942 943 The CartesianAxis2D is a straightforward axis, drawing straight 944 lines for cartesian coordinates in 2D. 945 """ 946 947 @PropWithDraw 948 def xTicksAngle(): 949 """ Get/Set the angle of the tick marks for te x-dimension. 950 This can be used when the tick labels are long, to prevent 951 them from overlapping. Note that if this value is non-zero, 952 the horizontal alignment is changed to left (instead of center). 953 """ 954 def fget(self): 955 return self._xTicksAngle 956 def fset(self, value): 957 self._xTicksAngle = value 958 return locals() 959 960 961 def _CreateLinesAndLabels(self, axes): 962 """ This is the method that calculates where lines should be 963 drawn and where labels should be placed. 964 965 It returns three point sets in which the pairs of points 966 represent the lines to be drawn (using GL_LINES): 967 * ppc: lines in real coords 968 * pps: lines in screen pixels 969 * ppg: dotted lines in real coords 970 """ 971 972 # Get camera instance 973 cam = axes.camera 974 975 # Get parameters 976 drawGrid = [v for v in self.showGrid] 977 drawMinorGrid = [v for v in self.showMinorGrid] 978 ticksPerDim = [self.xTicks, self.yTicks] 979 980 # Get limits 981 lims = axes.GetLimits() 982 lims = [lims[0], lims[1], cam._zlim] 983 984 # Get labels 985 labels = [self.xLabel, self.yLabel] 986 987 988 # Init the new text object dictionaries 989 newTextDicts = [{},{},{}] 990 991 # Init pointsets for drawing lines and gridlines 992 ppc = Pointset(3) # lines in real coords 993 pps = Pointset(3) # lines in screen pixels 994 ppg = Pointset(3) # dotted lines in real coords 995 996 997 # Calculate cornerpositions of the cube 998 corners8_c, corners8_s = self._CalculateCornerPositions(*lims) 999 1000 # We use this later to determine the order of the corners 1001 self._delta = 1 1002 for i in axes.daspect: 1003 if i<0: self._delta*=-1 1004 1005 # For each dimension ... 1006 for d in range(2): # d for dimension/direction 1007 lim = lims[d] 1008 1009 # Get the four corners that are of interest for this dimension 1010 # In 2D, the first two are the same as the last two 1011 tmp = self._cornerIndicesPerDirection[d] 1012 tmp = [tmp[i] for i in [0,1,0,1]] 1013 corners4_c = [corners8_c[i] for i in tmp] 1014 corners4_s = [corners8_s[i] for i in tmp] 1015 1016 # Get directional vectors in real coords and screen pixels. 1017 # Easily calculated since the first _corner elements are 1018 # 000,100,010,001 1019 vector_c = corners8_c[d+1] - corners8_c[0] 1020 vector_s = corners8_s[d+1] - corners8_s[0] 1021 1022 # Correct the tickdist for the x-axis if the numbers are large 1023 minTickDist = self._minTickDist 1024 if d==0: 1025 mm = max(abs(lim.min),abs(lim.max)) 1026 if mm >= 10000: 1027 minTickDist = 80 1028 1029 # Calculate tick distance in world units 1030 minTickDist *= vector_c.norm() / vector_s.norm() 1031 1032 # Get index of corner to put ticks at 1033 i0 = 0; bestVal = 999999999999999999999999 1034 for i in range(2): 1035 val = corners4_s[i].y 1036 if val < bestVal: 1037 i0 = i 1038 bestVal = val 1039 1040 # Get indices of the two next corners on which 1041 # ridges we may draw grid lines 1042 i1 = self._NextCornerIndex(i0, d, vector_s) 1043 # i2 = self._NextCornerIndex(i1, d, vector_s) 1044 1045 # Get first corner and grid vectors 1046 firstCorner = corners4_c[i0] 1047 gv1 = corners4_c[i1] - corners4_c[i0] 1048 # gv2 = corners4_c[i2] - corners4_c[i1] 1049 1050 # Get tick vector to indicate tick 1051 gv1s = corners4_s[i1] - corners4_s[i0] 1052 #tv = gv1 * (5 / gv1s.norm() ) 1053 npixels = ( gv1s.x**2 + gv1s.y**2 ) ** 0.5 + 0.000001 1054 tv = gv1 * (5.0 / npixels ) 1055 1056 # Always draw these corners 1057 pps.append(corners4_s[i0]) 1058 pps.append(corners4_s[i0]+vector_s) 1059 1060 # Add line pieces to draw box 1061 if self._showBox: 1062 for i in range(2): 1063 if i != i0: 1064 corner = corners4_s[i] 1065 pps.append(corner) 1066 pps.append(corner+vector_s) 1067 1068 # Get ticks stuff 1069 tickValues = ticksPerDim[d] # can be None 1070 p1, p2 = firstCorner.copy(), firstCorner+vector_c 1071 tmp = GetTicks(p1,p2, lim, minTickDist, tickValues) 1072 ticks, ticksPos, ticksText = tmp 1073 tickUnit = lim.range 1074 if len(ticks)>=2: 1075 tickUnit = ticks[1] - ticks[0] 1076 1077 # Apply Ticks 1078 for tick, pos, text in zip(ticks, ticksPos, ticksText): 1079 1080 # Get little tail to indicate tick 1081 p1 = pos 1082 p2 = pos - tv 1083 1084 # Add tick lines 1085 factor = ( tick-firstCorner[d] ) / vector_c[d] 1086 p1s = corners4_s[i0] + vector_s * factor 1087 tmp = Point(0,0,0) 1088 tmp[int(not d)] = 4 1089 pps.append(p1s) 1090 pps.append(p1s-tmp) 1091 1092 # Put a textlabel at tick 1093 textDict = self._textDicts[d] 1094 if tick in textDict and textDict[tick] in self._children: 1095 t = textDict.pop(tick) 1096 t.text = text 1097 t.x, t.y, t.z = p2.x, p2.y, p2.z 1098 else: 1099 t = AxisText(self,text, p2.x,p2.y,p2.z) 1100 # Add to dict 1101 newTextDicts[d][tick] = t 1102 # Set other properties right 1103 t._visible = True 1104 t.fontSize = self._tickFontSize 1105 t._color = self._axisColor # Use private attr for performance 1106 if d==1: 1107 t.halign = 1 1108 t.valign = 0 1109 else: 1110 t.textAngle = self._xTicksAngle 1111 if self._xTicksAngle > 0: 1112 t.halign = 1 1113 elif self._xTicksAngle < 0: 1114 t.halign = -1 1115 else: 1116 t.halign = 0 1117 if abs(self._xTicksAngle) > 45: 1118 t.valign = 0 1119 else: 1120 t.valign = -1 1121 1122 # We should hide this last tick if it sticks out 1123 if d==0 and len(ticks): 1124 # Get positions 1125 fig = axes.GetFigure() 1126 if fig: 1127 tmp1 = fig.position.width 1128 tmp2 = glu.gluProject(t.x, t.y, t.z)[0] 1129 tmp2 += t.GetVertexLimits()[0][1] # Max of x 1130 # Apply 1131 if tmp1 < tmp2: 1132 t._visible = False 1133 1134 # Get gridlines 1135 if drawGrid[d] or drawMinorGrid[d]: 1136 # Get more gridlines if required 1137 if drawMinorGrid[d]: 1138 ticks = self._GetTicks(tickUnit/5, lim) 1139 # Get positions 1140 for tick in ticks: 1141 # Get tick location 1142 p1 = firstCorner.copy() 1143 p1[d] = tick 1144 # Add gridlines 1145 p3 = p1+gv1 1146 #p4 = p3+gv2 1147 ppg.append(p1); ppg.append(p3) 1148 1149 # Apply label 1150 textDict = self._textDicts[d] 1151 p1 = corners4_c[i0] + vector_c * 0.5 1152 key = '_label_' 1153 if key in textDict and textDict[key] in self._children: 1154 t = textDict.pop(key) 1155 t.text = labels[d] 1156 t.x, t.y, t.z = p1.x, p1.y, p1.z 1157 else: 1158 #t = AxisText(self,labels[d], p1.x,p1.y,p1.z) 1159 t = AxisLabel(self,labels[d], p1.x,p1.y,p1.z) 1160 t.fontSize=10 1161 newTextDicts[d][key] = t 1162 t.halign = 0 1163 t._color = self._axisColor 1164 # Move label to back, so the repositioning works right 1165 if not t in self._children[-3:]: 1166 self._children.remove(t) 1167 self._children.append(t) 1168 # Get vec to calc angle 1169 vec = Point(vector_s.x, vector_s.y) 1170 if vec.x < 0: 1171 vec = vec * -1 1172 t.textAngle = float(vec.angle() * 180/np.pi) 1173 # Keep up to date (so label can move itself just beyond ticks) 1174 t._textDict = newTextDicts[d] 1175 1176 # Correct gridlines so they are all at z=0. 1177 # The grid is always exactly at 0. Images are at -0.1 or less. 1178 # lines and poins are at +0.1 1179 ppg.data[:,2] = 0.0 1180 1181 # Clean up the text objects that are left 1182 for tmp in self._textDicts: 1183 for t in list(tmp.values()): 1184 t.Destroy() 1185 1186 # Store text object dictionaries for next time ... 1187 self._textDicts = newTextDicts 1188 1189 # Return 1190 return ppc, pps, ppg 1191 1192 1193class CartesianAxis3D(BaseAxis): 1194 """ CartesianAxis3D(parent) 1195 1196 An Axis object represents the lines, ticks and grid that make 1197 up an axis. Not to be confused with an Axes, which represents 1198 a scene and is a Wibject. 1199 1200 The CartesianAxis3D is a straightforward axis, drawing straight 1201 lines for cartesian coordinates in 3D. 1202 1203 """ 1204 1205 def _GetRidgeVector(self, d, corners8_c, corners8_s): 1206 """ _GetRidgeVector(d, corners8_c, corners8_s) 1207 1208 Get the four vectors for the four ridges coming from the 1209 corners that correspond to the given direction. 1210 1211 Also returns the lengths of the smallest vectors, for the 1212 calculation of the minimum tick distance. 1213 1214 """ 1215 1216 # Get the vectors 1217 vectors_c = [] 1218 vectors_s = [] 1219 for i in range(4): 1220 i1 = self._cornerIndicesPerDirection[d][i] 1221 i2 = self._cornerPairIndicesPerDirection[d][i] 1222 vectors_c.append( corners8_c[i2] - corners8_c[i1]) 1223 vectors_s.append( corners8_s[i2] - corners8_s[i1]) 1224 1225 # Select the smallest vector (in screen coords) 1226 smallest_i, smallest_L = 0, 9999999999999999999999999.0 1227 for i in range(4): 1228 L = vectors_s[i].x**2 + vectors_s[i].y**2 1229 if L < smallest_L: 1230 smallest_i = i 1231 smallest_L = L 1232 1233 # Return smallest and the vectors 1234 norm_c = vectors_c[smallest_i].norm() 1235 norm_s = smallest_L**0.5 1236 return norm_c, norm_s, vectors_c, vectors_s 1237 1238 1239 def _CreateLinesAndLabels(self, axes): 1240 """ This is the method that calculates where lines should be 1241 drawn and where labels should be placed. 1242 1243 It returns three point sets in which the pairs of points 1244 represent the lines to be drawn (using GL_LINES): 1245 * ppc: lines in real coords 1246 * pps: lines in screen pixels 1247 * ppg: dotted lines in real coords 1248 """ 1249 1250 # Get camera instance 1251 cam = axes.camera 1252 1253 # Get parameters 1254 drawGrid = [v for v in self.showGrid] 1255 drawMinorGrid = [v for v in self.showMinorGrid] 1256 ticksPerDim = [self.xTicks, self.yTicks, self.zTicks] 1257 1258 # Get limits 1259 lims = [cam._xlim, cam._ylim, cam._zlim] 1260 1261 # Get labels 1262 labels = [self.xLabel, self.yLabel, self.zLabel] 1263 1264 1265 # Init the new text object dictionaries 1266 newTextDicts = [{},{},{}] 1267 1268 # Init pointsets for drawing lines and gridlines 1269 ppc = Pointset(3) # lines in real coords 1270 pps = Pointset(3) # lines in screen pixels 1271 ppg = Pointset(3) # dotted lines in real coords 1272 1273 1274 # Calculate cornerpositions of the cube 1275 corners8_c, corners8_s = self._CalculateCornerPositions(*lims) 1276 1277 # we use this later to determine the order of the corners 1278 self._delta = 1 1279 for i in axes.daspect: 1280 if i<0: self._delta*=-1 1281 1282 1283 # For each dimension ... 1284 for d in range(3): # d for dimension/direction 1285 lim = lims[d] 1286 1287 # Get the four corners that are of interest for this dimension 1288 # They represent one of the faces that we might draw in. 1289 tmp = self._cornerIndicesPerDirection[d] 1290 corners4_c = [corners8_c[i] for i in tmp] 1291 corners4_s = [corners8_s[i] for i in tmp] 1292 1293 # Get directional vectors (i.e. ridges) corresponding to 1294 # (emanating from) the four corners. Also returns the length 1295 # of the shortest ridges (in screen coords) 1296 _vectors = self._GetRidgeVector(d, corners8_c, corners8_s) 1297 norm_c, norm_s, vectors4_c, vectors4_s = _vectors 1298 1299 # Due to cords not being defined behind the near clip plane, 1300 # the vectors4_s migt be inaccurate. This means the size and 1301 # angle of the tickmarks may be calculated wrong. It also 1302 # means the norm_s might be wrong. Since this is mostly a problem 1303 # for the fly camera, we use a fixed norm_s in that case. This 1304 # also prevents grid line flicker due to the constant motion 1305 # of the camera. 1306 if isinstance(axes.camera, FlyCamera): 1307 norm_s = axes.position.width 1308 1309 # Calculate tick distance in units (using shortest ridge vector) 1310 minTickDist = self._minTickDist 1311 if norm_s > 0: 1312 minTickDist *= norm_c / norm_s 1313 1314 # Get index of corner to put ticks at. 1315 # This is determined by chosing the corner which is the lowest 1316 # on screen (for x and y), or the most to the left (for z). 1317 i0 = 0; bestVal = 999999999999999999999999 1318 for i in range(4): 1319 if d==2: val = corners4_s[i].x # chose leftmost corner 1320 else: val = corners4_s[i].y # chose bottommost corner 1321 if val < bestVal: 1322 i0 = i 1323 bestVal = val 1324 1325 # Get indices of next corners corresponding to the ridges 1326 # between which we may draw grid lines 1327 # i0, i1, i2 are all in [0,1,2,3] 1328 i1 = self._NextCornerIndex(i0, d, vectors4_s[i0]) 1329 i2 = self._NextCornerIndex(i1, d, vectors4_s[i0]) 1330 1331 # Get first corner and grid vectors 1332 firstCorner = corners4_c[i0] 1333 gv1 = corners4_c[i1] - corners4_c[i0] 1334 gv2 = corners4_c[i2] - corners4_c[i1] 1335 1336 # Get tick vector to indicate tick 1337 gv1s = corners4_s[i1] - corners4_s[i0] 1338 #tv = gv1 * (5 / gv1s.norm() ) 1339 npixels = ( gv1s.x**2 + gv1s.y**2 ) ** 0.5 + 0.000001 1340 tv = gv1 * (5.0 / npixels ) 1341 1342 # Draw edge lines (optionally to create a full box) 1343 for i in range(4): 1344 if self._showBox or i in [i0, i1, i2]: 1345 #if self._showBox or i ==i0: # for a real minimalistic axis 1346 # Note that we use world coordinates, rather than screen 1347 # as the 2D axis does. 1348 ppc.append(corners4_c[i]) 1349 j = self._cornerPairIndicesPerDirection[d][i] 1350 ppc.append(corners8_c[j]) 1351 1352 # Get ticks stuff 1353 tickValues = ticksPerDim[d] # can be None 1354 p1, p2 = firstCorner.copy(), firstCorner+vectors4_c[i0] 1355 tmp = GetTicks(p1,p2, lim, minTickDist, tickValues) 1356 ticks, ticksPos, ticksText = tmp 1357 tickUnit = lim.range 1358 if len(ticks)>=2: 1359 tickUnit = ticks[1] - ticks[0] 1360 1361 # Apply Ticks 1362 for tick, pos, text in zip(ticks, ticksPos, ticksText): 1363 1364 # Get little tail to indicate tick 1365 p1 = pos 1366 p2 = pos - tv 1367 1368 # Add tick lines 1369 ppc.append(p1) 1370 ppc.append(p2) 1371 1372 # z-axis has valign=0, thus needs extra space 1373 if d==2: 1374 text+=' ' 1375 1376 # Put textlabel at tick 1377 textDict = self._textDicts[d] 1378 if tick in textDict and textDict[tick] in self._children: 1379 t = textDict.pop(tick) 1380 t.x, t.y, t.z = p2.x, p2.y, p2.z 1381 else: 1382 t = AxisText(self,text, p2.x,p2.y,p2.z) 1383 # Add to dict 1384 newTextDicts[d][tick] = t 1385 # Set other properties right 1386 t._visible = True 1387 if t.fontSize != self._tickFontSize: 1388 t.fontSize = self._tickFontSize 1389 t._color = self._axisColor # Use private attr for performance 1390 if d==2: 1391 t.valign = 0 1392 t.halign = 1 1393 else: 1394 if vectors4_s[i0].y*vectors4_s[i0].x >= 0: 1395 t.halign = -1 1396 t.valign = -1 1397 else: 1398 t.halign = 1 1399 t.valign = -1 1400 1401 # Get gridlines 1402 draw4 = self._showBox and isinstance(axes.camera, FlyCamera) 1403 if drawGrid[d] or drawMinorGrid[d]: 1404 # get more gridlines if required 1405 if drawMinorGrid[d]: 1406 ticks = self._GetTicks(tickUnit/5, lim) 1407 # get positions 1408 for tick in ticks: 1409 # get tick location 1410 p1 = firstCorner.copy() 1411 p1[d] = tick 1412 if tick not in [lim.min, lim.max]: # not ON the box 1413 # add gridlines (back and front) 1414 if True: 1415 p3 = p1+gv1 1416 p4 = p3+gv2 1417 ppg.append(p1); ppg.append(p3) 1418 ppg.append(p3); ppg.append(p4) 1419 if draw4: 1420 p5 = p1+gv2 1421 p6 = p5+gv1 1422 ppg.append(p1); ppg.append(p5) 1423 ppg.append(p5); ppg.append(p6) 1424 1425 # Apply label 1426 textDict = self._textDicts[d] 1427 p1 = corners4_c[i0] + vectors4_c[i0] * 0.5 1428 key = '_label_' 1429 if key in textDict and textDict[key] in self._children: 1430 t = textDict.pop(key) 1431 t.text = labels[d] 1432 t.x, t.y, t.z = p1.x, p1.y, p1.z 1433 else: 1434 #t = AxisText(self,labels[d], p1.x,p1.y,p1.z) 1435 t = AxisLabel(self,labels[d], p1.x,p1.y,p1.z) 1436 t.fontSize=10 1437 newTextDicts[d][key] = t 1438 t.halign = 0 1439 t._color = self._axisColor # Use private attr for performance 1440 # Move to back such that they can position themselves right 1441 if not t in self._children[-3:]: 1442 self._children.remove(t) 1443 self._children.append(t) 1444 # Get vec to calc angle 1445 vec = Point(vectors4_s[i0].x, vectors4_s[i0].y) 1446 if vec.x < 0: 1447 vec = vec * -1 1448 t.textAngle = float(vec.angle() * 180/np.pi) 1449 # Keep up to date (so label can move itself just beyond ticks) 1450 t._textDict = newTextDicts[d] 1451 1452 1453 # Clean up the text objects that are left 1454 for tmp in self._textDicts: 1455 for t in list(tmp.values()): 1456 t.Destroy() 1457 1458 # Store text object dictionaries for next time ... 1459 self._textDicts = newTextDicts 1460 1461 # Return 1462 return ppc, pps, ppg 1463 1464 1465class CartesianAxis(CartesianAxis2D, CartesianAxis3D): 1466 """ CartesianAxis(parent) 1467 1468 An Axis object represents the lines, ticks and grid that make 1469 up an axis. Not to be confused with an Axes, which represents 1470 a scene and is a Wibject. 1471 1472 The CartesianAxis combines the 2D and 3D axis versions; it uses 1473 the 2D version when the 2d camera is used, and the 3D axis 1474 otherwise. 1475 1476 """ 1477 # A bit ugly inheritance going on here, but otherwise the code below 1478 # would not work ... 1479 1480 def _CreateLinesAndLabels(self, axes): 1481 """ Choose depending on what camera is used. """ 1482 1483 if isinstance(axes.camera, TwoDCamera): 1484 return CartesianAxis2D._CreateLinesAndLabels(self,axes) 1485 else: 1486 return CartesianAxis3D._CreateLinesAndLabels(self,axes) 1487 1488 1489 1490def GetPolarTicks(p0, radius, lim, angularRefPos, sense , minTickDist=100, ticks=None): 1491 """ GetPolarTicks(p0, radius, lim, angularRefPos, sense , minTickDist=100, 1492 ticks=None) 1493 1494 Get the tick values, position and texts. 1495 These are calculated from the polar center, radius and the range 1496 of values to map on a straight line between these two points 1497 (which can be 2d or 3d). If ticks is given, use these values instead. 1498 1499 """ 1500 1501 pTickUnits = [1,2,3,5,6,9,18,30,45] # 90 = 3*3*2*5*1 1502 #circumference of circle 1503 circum = 2*np.pi*radius 1504 1505 # Calculate all ticks if not given 1506 if ticks is None: 1507 # Get pixels per unit 1508 if lim.range == 0: 1509 return [],[],[] 1510 pixelsPerUnit = circum / 360 #lim.range 1511 # Try all tickunits, starting from the smallest, until we find 1512 # one which results in a distance between ticks more than 1513 # X pixels. 1514 try: 1515 for tickUnit in pTickUnits : 1516 if tickUnit * pixelsPerUnit >= minTickDist: 1517 break 1518 # if the numbers are VERY VERY large (which is very unlikely) 1519 if tickUnit*pixelsPerUnit < minTickDist: 1520 raise ValueError 1521 except (ValueError, TypeError): 1522 # too small 1523 return [],[],[] 1524 1525 # Calculate the ticks (the values) themselves 1526 ticks = [] 1527 firstTick = np.ceil( lim.min/tickUnit ) * tickUnit 1528 lastTick = np.floor( lim.max/tickUnit ) * tickUnit 1529 count = 0 1530 ticks = [firstTick] 1531 while ticks[-1] < lastTick-tickUnit/2: 1532 count += 1 1533 ticks.append( firstTick + count*tickUnit ) 1534 1535 # Calculate tick positions and text 1536 ticksPos, ticksText = [], [] 1537 for tick in ticks: 1538 theta = angularRefPos + sense*tick*np.pi/180.0 1539 x = radius*np.cos(theta) 1540 y = radius*np.sin(theta) 1541 pos = p0 + Point(x,y,0) 1542 if tick == -0: 1543 tick = 0 1544 text = '%1.4g' % tick 1545 iExp = text.find('e') 1546 if iExp>0: 1547 front = text[:iExp+2] 1548 text = front + text[iExp+2:].lstrip('0') 1549 # Store 1550 ticksPos.append( pos ) 1551 ticksText.append( text ) 1552 1553 # Done 1554 return ticks, ticksPos, ticksText 1555 1556 1557class PolarAxis2D(BaseAxis): 1558 """ PolarAxis2D(parent) 1559 1560 An Axis object represents the lines, ticks and grid that make 1561 up an axis. Not to be confused with an Axes, which represents 1562 a scene and is a Wibject. 1563 1564 PolarAxis2D draws a polar grid, and modifies PolarLine objects 1565 to properly plot onto the polar grid. PolarAxis2D has some 1566 specialized methods uniques to it for adjusting the polar plot. 1567 These include: 1568 SetLimits(thetaRange, radialRange): 1569 thetaRange, radialRange = GetLimits(): 1570 1571 angularRefPos: Get and Set methods for the relative screen 1572 angle of the 0 degree polar reference. Default is 0 degs 1573 which corresponds to the positive x-axis (y =0) 1574 1575 isCW: Get and Set methods for the sense of rotation CCW or 1576 CW. This method takes/returns a bool (True if the default CW). 1577 1578 Drag mouse up/down to translate radial axis 1579 Drag mouse left/right to rotate angular ref position 1580 Drag mouse + shift key up/down to rescale radial axis (min R fixed) 1581 1582 """ 1583 1584 def __init__(self, parent): 1585 BaseAxis.__init__(self, parent) 1586 self.ppb = None 1587 axes = self.GetAxes() 1588 axes.daspectAuto = False 1589 self.bgcolor = axes.bgcolor 1590 axes.bgcolor = None # disables the default background 1591 # Size of the boarder where circular tick labels are drawn 1592 self.labelPix = 5 1593 1594 self._radialRange = Range(-1, 1) # default 1595 self._angularRange = Range(-179, 180) # always 360 deg 1596 self._angularRefPos = 0 1597 self._sense = 1.0 1598 1599 # Need to overrride this because the PolarAxis has 1600 # four sets of radial ticks (with same dict key!) 1601 self._textDicts = [{}, {}, {}, {}, {}] 1602 1603 # reference stuff for interaction 1604 self.ref_loc = 0, 0, 0 # view_loc when clicked 1605 self.ref_mloc = 0, 0 # mouse location when clicked 1606 self.ref_but = 0 # mouse button when clicked 1607 1608 self.controlIsDown = False 1609 self.shiftIsDown = False 1610 1611 # bind special event for translating lower radial limit 1612 axes.eventKeyDown.Bind(self.OnKeyDown) 1613 axes.eventKeyUp.Bind(self.OnKeyUp) 1614 1615 # Mouse events 1616 axes.eventMouseDown.Bind(self.OnMouseDown) 1617 axes.eventMouseUp.Bind(self.OnMouseUp) 1618 axes.eventMotion.Bind(self.OnMotion) 1619 1620 1621 @DrawAfter 1622 def RescalePolarData(self): 1623 """ RescalePolarData() 1624 1625 This method finds and transforms all polar line data 1626 by the current polar radial axis limits so that data below 1627 the center of the polar plot is set to 0,0,0 and data beyond 1628 the maximum (outter radius) is clipped. 1629 1630 """ 1631 1632 axes = self.GetAxes() 1633 drawObjs = axes.FindObjects(PolarLine) 1634 # Now set the transform for the PolarLine data 1635 for anObj in drawObjs: 1636 anObj.TransformPolar(self._radialRange, self._angularRefPos, self._sense) 1637 1638 1639 def _CreateLinesAndLabels(self, axes): 1640 """ This is the method that calculates where polar axis lines 1641 should be drawn and where labels should be placed. 1642 1643 It returns three point sets in which the pairs of points 1644 represent the lines to be drawn (using GL_LINES): 1645 * ppc: lines in real coords 1646 * pps: lines in screen pixels 1647 * ppg: dotted lines in real coords 1648 """ 1649 1650 # Get camera 1651 # This camera has key bindings which are used to 1652 # rescale the lower radial limits. Thus for polar plots the 1653 # user can slide the radial range up 1654 # and down and rotate the plot 1655 cam = axes.camera 1656 1657 # Get axis grid and tick parameters 1658 drawGrid = [v for v in self.showGrid] 1659 drawMinorGrid = [v for v in self.showMinorGrid] 1660 # these are equivalent to axes.thetaTicks and axes.RadialTicks 1661 ticksPerDim = [self.xTicks, self.yTicks] 1662 1663 # Get x-y limits in world coordinates 1664 lims = axes.GetLimits() 1665 lims = [lims[0], lims[1], cam._zlim] 1666 1667 # From current lims calculate the radial axis min and max 1668 1669 # Get labels. These are equivalent to Theta and radial labels 1670 labels = [self.xLabel, self.yLabel] 1671 1672 # Init the new text object dictionaries 1673 # (theta, R(0),R(90),R(180),R(270)) 1674 newTextDicts = [{}, {}, {}, {}, {}] 1675 1676 # Init pointsets for drawing lines and gridlines 1677 ppc = Pointset(3) # lines in real coords 1678 pps = Pointset(3) # lines in screen pixels, not used by PolarAxis 1679 ppg = Pointset(3) # dotted lines in real coords (for grids) 1680 # circular background poly for polar ( rectangular bkgd is 1681 # turned off and a circular one drawn instead ) 1682 self.ppb = Pointset(3) 1683 1684 # outter circle at max radius 1685 self.ppr = Pointset(3) 1686 1687 # Calculate corner positions of the x-y-z world and screen cube 1688 # Note: Its not clear why you want, or what the meaning 1689 # of x-y-z screen coordinates is (corners8_s) since the 1690 # screen is only 2D 1691 corners8_c, corners8_s = self._CalculateCornerPositions(*lims) 1692 # We use this later to determine the order of the corners 1693 self._delta = 1 1694 for i in axes.daspect: 1695 if i < 0: 1696 self._delta *= -1 1697 1698 # Since in polar coordinates screen and data x and y values 1699 # need to be mapped to theta and R 1700 # PolarAxis calculates things differently from Cartesian2D. 1701 # Also, polar coordinates need to be 1702 # fixed to world coordinates, not screen coordinates 1703 vector_cx = corners8_c[1] - corners8_c[0] 1704 vector_sx = corners8_s[1] - corners8_s[0] 1705 vector_cy = corners8_c[2] - corners8_c[0] 1706 vector_sy = corners8_s[2] - corners8_s[0] 1707 1708 # The screen window may be any rectangular shape and 1709 # for PolarAxis, axes.daspectAuto = False so 1710 # that circles always look like circle 1711 # (x & y are always scaled together). 1712 # The first step is to find the radial extent of the PolarAxis. 1713 # For the axis to fit this will simply be the smallest window size in 1714 # x or y. We also need to reduce it further so 1715 # that tick labels can be drawn 1716 if vector_cx.norm() < vector_cy.norm(): 1717 dimMax_c = (vector_cx.norm() / 2) 1718 dimMax_s = (vector_sx.norm() / 2) 1719 else: 1720 dimMax_c = (vector_cy.norm() / 2) 1721 dimMax_s = (vector_sy.norm() / 2) 1722 1723 pix2c = dimMax_c / dimMax_s # for screen to world conversion 1724 txtSize = self.labelPix * pix2c 1725 radiusMax_c = dimMax_c - 3.0 * txtSize # Max radial scale extent 1726 center_c = Point(0.0, 0.0, 0.0) 1727 #self._radialRange = radiusMax_c 1728 radiusMax_c = self._radialRange.range 1729 1730 1731 #========================================================== 1732 # Apply labels 1733 #========================================================== 1734 for d in range(2): 1735 # Get the four corners that are of interest for this dimension 1736 # In 2D, the first two are the same as the last two 1737 tmp = self._cornerIndicesPerDirection[d] 1738 tmp = [tmp[i] for i in [0, 1, 0, 1]] 1739 corners4_c = [corners8_c[i] for i in tmp] 1740 corners4_s = [corners8_s[i] for i in tmp] 1741 # Get index of corner to put ticks at 1742 i0 = 0 1743 bestVal = 999999999999999999999999 1744 for i in range(4): 1745 val = corners4_s[i].y 1746 if val < bestVal: 1747 i0 = i 1748 bestVal = val 1749 1750 # Get directional vectors in real coords and screen pixels. 1751 # Easily calculated since the first _corner elements are 1752 # 000,100,010,001 1753 vector_c = corners8_c[d + 1] - corners8_c[0] 1754 vector_s = corners8_s[d + 1] - corners8_s[0] 1755 textDict = self._textDicts[d] 1756 p1 = corners4_c[i0] + vector_c * 0.5 1757 key = '_label_' 1758 if key in textDict and textDict[key] in self._children: 1759 t = textDict.pop(key) 1760 t.text = labels[d] 1761 t.x, t.y, t.z = p1.x, p1.y, p1.z 1762 else: 1763 #t = AxisText(self,labels[d], p1.x,p1.y,p1.z) 1764 t = AxisLabel(self, labels[d], p1.x, p1.y, p1.z) 1765 t.fontSize = 10 1766 newTextDicts[d][key] = t 1767 t.halign = 0 1768 t._color = self._axisColor # Use private attr for performance 1769 # Move to back 1770 if not t in self._children[-3:]: 1771 self._children.remove(t) 1772 self._children.append(t) 1773 # Get vec to calc angle 1774 vec = Point(vector_s.x, vector_s.y) 1775 if vec.x < 0: 1776 vec = vec * -1 1777 1778 # This was causing weird behaviour, so I commented it out 1779 # t.textAngle = float(vec.angle() * 180/np.pi) 1780 # Keep up to date (so label can move itself just beyond ticks) 1781 t._textDict = newTextDicts[d] 1782 1783 # To make things easier to program I just pulled out 1784 # the Polar angular and radial calulations since they 1785 # are disimilar anyway (i.e. a 'for range(2)' doesn't really help here) 1786 1787 #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1788 # Angular Axis lines, tick and circular background calculations 1789 #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1790 # theta axis is circle at the outer radius 1791 # with a line segment every 6 degrees to form circle 1792 theta = self._angularRefPos + \ 1793 self._sense * np.linspace(0, 2 * np.pi, 61) 1794 1795 # x,y for background 1796 xb = radiusMax_c * np.cos(theta) 1797 yb = radiusMax_c * np.sin(theta) 1798 1799 #x,y for maximum scale radius 1800 xc = radiusMax_c * np.cos(theta) 1801 yc = radiusMax_c * np.sin(theta) 1802 # ppb is the largest circle that will fit 1803 # and is used to draw the polar background poly 1804 for x, y in np.column_stack((xb, yb)): 1805 self.ppb.append(x, y, -10.0) 1806 1807 for x, y in np.column_stack((xc, yc)): 1808 self.ppr.append(x, y, -1.0) 1809 1810 # polar ticks 1811 # Correct the tickdist for the x-axis if the numbers are large 1812 minTickDist = self._minTickDist 1813 minTickDist = 40 # This should be set by the font size 1814 1815 # Calculate tick distance in world units 1816 minTickDist *= pix2c 1817 tickValues = ticksPerDim[0] # can be None 1818 1819 tmp = GetPolarTicks(center_c, radiusMax_c, self._angularRange, 1820 self._angularRefPos, self._sense, 1821 minTickDist, tickValues) 1822 ticks, ticksPos, ticksText = tmp 1823 textRadius = (2.2 * txtSize) + radiusMax_c 1824 # Get tick unit 1825 tickUnit = self._angularRange.range 1826 if len(ticks)>=2: 1827 tickUnit = ticks[1] - ticks[0] 1828 1829 for tick, pos, text in zip(ticks, ticksPos, ticksText): 1830 # Get little tail to indicate tick, current hard coded to 4 1831 p1 = pos 1832 tv = 0.05 * radiusMax_c * p1 / p1.norm() 1833 # polar ticks are inline with vector to tick position 1834 p2s = pos - tv 1835 1836 # Add tick lines 1837 ppc.append(pos) 1838 ppc.append(p2s) 1839 1840 # Text is in word coordinates so need to create them based on ticks 1841 theta = self._angularRefPos + (self._sense * tick * np.pi / 180.0) 1842 p2 = Point((textRadius * np.cos(theta))[0], (textRadius * np.sin(theta))[0], 0) 1843 # Put a textlabel at tick 1844 textDict = self._textDicts[0] 1845 if tick in textDict and textDict[tick] in self._children: 1846 t = textDict.pop(tick) 1847 t.x, t.y, t.z = p2.x, p2.y, p2.z 1848 else: 1849 t = AxisText(self, text, p2.x, p2.y, p2.z) 1850 # Add to dict 1851 newTextDicts[0][tick] = t 1852 # Set other properties right 1853 t._visible = True 1854 if t.fontSize != self._tickFontSize: 1855 t.fontSize = self._tickFontSize 1856 t._color = self._axisColor # Use private attr for performance 1857 t.halign = 0 1858 t.valign = 0 1859 #=================================================================== 1860 # Get gridlines 1861 if drawGrid[0] or drawMinorGrid[0]: 1862 # Get more gridlines if required 1863 if drawMinorGrid[0]: 1864 ticks = self._GetPolarTicks(tickUnit / 5, self._angularRange) 1865 # Get positions 1866 for tick, p in zip(ticks, ticksPos): 1867 ppg.append(center_c) 1868 ppg.append(p) 1869 1870 #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1871 # radial Axis lines, tick calculations 1872 #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1873 1874 # the radial axis is vertical and horizontal lines through the center 1875 # radial lines every 90 deg 1876 theta = self._angularRefPos + \ 1877 self._sense * np.arange(0, 2 * np.pi, np.pi / 2) 1878 xc = radiusMax_c * np.cos(theta) 1879 yc = radiusMax_c * np.sin(theta) 1880 1881 for x, y in np.column_stack((xc, yc)): 1882 ppc.append(0.0, 0.0, 0.0) 1883 ppc.append(x, y, 0.0) 1884 1885 # radial ticks 1886 # Correct the tickdist for the x-axis if the numbers are large 1887 minTickDist = self._minTickDist 1888 # Calculate tick distance in world units 1889 minTickDist *= pix2c 1890 tickValues = ticksPerDim[1] # can be None 1891 1892 ticks, ticksPos, ticksText, quadIndex = [], [], [], [] 1893 for index, theta in enumerate(self._angularRefPos + 1894 self._sense * np.array([0, np.pi / 2, np.pi, np.pi * 3 / 2])): 1895 xc = radiusMax_c * np.cos(theta) 1896 yc = radiusMax_c * np.sin(theta) 1897 p2 = Point(xc, yc, 0) 1898 tmp = GetTicks(center_c, p2, Range(0, radiusMax_c), minTickDist, tickValues) 1899 if index == 0: 1900 ticks = ticks + tmp[0] 1901 ticksPos = ticksPos + tmp[1] 1902 quadIndex = quadIndex + [index + 1] * len(tmp[0]) 1903 else: 1904 ticks = ticks + tmp[0][1:] 1905 ticksPos = ticksPos + tmp[1][1:] 1906 quadIndex = quadIndex + [index + 1] * len(tmp[1][1:]) 1907 1908 for tick, pos, qIndx in zip(ticks, ticksPos, quadIndex): 1909 # Get little tail to indicate tick 1910 tickXformed = tick + self._radialRange.min 1911 text = '%1.4g' % (tickXformed) 1912 iExp = text.find('e') 1913 if iExp > 0: 1914 front = text[:iExp + 2] 1915 text = front + text[iExp + 2:].lstrip('0') 1916 1917 p1 = pos 1918 if (p1.norm() != 0): 1919 tv = (4 * pix2c[0]) * p1 / p1.norm() 1920 tvTxt = ((4 * pix2c[0]) + txtSize[0].view(float)) * p1 / p1.norm() 1921 else: 1922 tv = Point(0, 0, 0) 1923 tvTxt = Point(-txtSize[0], 0, 0) 1924 # radial ticks are orthogonal to tick position 1925 tv = Point(tv.y, tv.x, 0) 1926 tvTxt = Point(tvTxt.y, tvTxt.x, 0) 1927 ptic = pos - tv 1928 ptxt = pos - tvTxt 1929 1930 # Add tick lines 1931 ppc = ppc + pos 1932 ppc = ppc + ptic 1933 1934 textDict = self._textDicts[qIndx] 1935 1936 if tickXformed in textDict and \ 1937 textDict[tickXformed] in self._children: 1938 t = textDict.pop(tickXformed) 1939 t.x, t.y, t.z = ptxt.x, ptxt.y, ptxt.z 1940 else: 1941 t = AxisText(self, text, ptxt.x, ptxt.y, ptxt.z) 1942 # Add to dict 1943 #print(tick, '=>',text, 'but', t.text) 1944 newTextDicts[qIndx][tickXformed] = t 1945 # Set other properties right 1946 t._visible = True 1947 if t.fontSize != self._tickFontSize: 1948 t.fontSize = self._tickFontSize 1949 t._color = self._axisColor # Use private attr for performance 1950 t.halign = 1 1951 t.valign = 0 1952 1953 #==================================================================== 1954 # Get gridlines 1955 if drawGrid[1] or drawMinorGrid[1]: 1956 # Get more gridlines if required 1957 # line segment every 6 degrees to form circle 1958 theta = self._angularRefPos + \ 1959 self._sense * np.linspace(0, 2 * np.pi, 61) 1960 if drawMinorGrid[1]: 1961 ticks = self._GetTicks(tickUnit / 5, self._angularRange) 1962 # Get positions 1963 for tick in ticks: 1964 xc = tick * np.cos(theta) 1965 yc = tick * np.sin(theta) 1966 xlast = xc[:-1][0] 1967 ylast = yc[:-1][0] 1968 for x, y in np.column_stack((xc, yc)): 1969 ppg.append(Point(xlast, ylast, 0.0)) 1970 ppg.append(Point(x, y, 0.0)) 1971 xlast = x 1972 ylast = y 1973 1974 # Clean up the text objects that are left 1975 for tmp in self._textDicts: 1976 for t in list(tmp.values()): 1977 t.Destroy() 1978 1979 # Store text object dictionaries for next time ... 1980 self._textDicts = newTextDicts 1981 1982 # Return points (note: Special PolarAxis points are set as class 1983 # variables since this method was overrridden) 1984 return ppc, pps, ppg 1985 1986 1987 def OnDraw(self): 1988 1989 # Get axes 1990 axes = self.GetAxes() 1991 if not axes: 1992 return 1993 1994 # Calculate lines and labels 1995 try: 1996 ppc, pps, ppg = self._CreateLinesAndLabels(axes) 1997 except Exception: 1998 self.Destroy() # So the error message does not repeat itself 1999 raise 2000 2001 # Draw background and lines 2002 if self.ppb and self.ppr: 2003 2004 # Set view params 2005 s = axes.camera.GetViewParams() 2006 if s['loc'][0] != s['loc'][1] != 0: 2007 axes.camera.SetViewParams(loc=(0,0,0)) 2008 2009 # Prepare data for polar coordinates 2010 self.RescalePolarData() 2011 2012 # Prepare for drawing lines and background 2013 gl.glEnableClientState(gl.GL_VERTEX_ARRAY) 2014 gl.glDisable(gl.GL_DEPTH_TEST) 2015 2016 # Draw polygon background 2017 clr = 1, 1, 1 2018 gl.glColor3f(clr[0], clr[1], clr[2]) 2019 gl.glVertexPointerf(self.ppb.data) 2020 gl.glDrawArrays(gl.GL_POLYGON, 0, len(self.ppb)) 2021 2022 # Draw lines 2023 clr = self._axisColor 2024 gl.glColor(clr[0], clr[1], clr[2]) 2025 gl.glLineWidth(self._lineWidth) 2026 gl.glVertexPointerf(self.ppr.data) 2027 gl.glDrawArrays(gl.GL_LINE_LOOP, 0, len(self.ppr)) 2028 2029 # Clean up 2030 gl.glFlush() 2031 gl.glEnable(gl.GL_DEPTH_TEST) 2032 gl.glDisableClientState(gl.GL_VERTEX_ARRAY) 2033 2034 2035 # Draw axes lines and text etc. 2036 BaseAxis.OnDraw(self, (ppc, pps, ppg)) 2037 2038 2039 def OnKeyDown(self, event): 2040 if event.key == 17 and self.ref_but == 1: 2041 self.shiftIsDown = True 2042 elif event.key == 19 and self.ref_but == 0: 2043 self.controlIsDown = True 2044 return True 2045 2046 2047 def OnKeyUp(self, event): 2048 self.shiftIsDown = False 2049 self.controlIsDown = False 2050 self.ref_but = 0 # in case the mouse was also down 2051 return True 2052 2053 2054 def OnMouseDown(self, event): 2055 # store mouse position and button 2056 self.ref_mloc = event.x, event.y 2057 self.ref_but = event.button 2058 self.ref_lowerRadius = self._radialRange.min 2059 self.ref_angularRefPos = self.angularRefPos 2060 2061 2062 def OnMouseUp(self, event): 2063 self.ref_but = 0 2064 self.Draw() 2065 2066 2067 def OnMotion(self, event): 2068 if not self.ref_but: 2069 return 2070 2071 axes = event.owner 2072 mloc = axes.mousepos 2073 Rrange = self._radialRange.range 2074 if self.ref_but == 1: 2075 # get distance and convert to world coordinates 2076 refloc = axes.camera.ScreenToWorld(self.ref_mloc) 2077 loc = axes.camera.ScreenToWorld(mloc) 2078 # calculate radial and circular ref position translations 2079 dx = loc[0] - refloc[0] 2080 dy = loc[1] - refloc[1] 2081 2082 if self.shiftIsDown: 2083 minRadius = self.ref_lowerRadius - dy 2084 self.SetLimits(rangeR=Range(minRadius, minRadius + Rrange)) 2085 else: 2086 self.angularRefPos = self.ref_angularRefPos - (50 * dx / Rrange) 2087 2088 elif self.ref_but == 2: 2089 # zoom 2090 2091 # Don't care about x zooming for polar plot 2092 # get movement in x (in pixels) and normalize 2093 #factor_x = float(self.ref_mloc[0] - mloc[0]) 2094 #factor_x /= axes.position.width 2095 2096 # get movement in y (in pixels) and normalize 2097 factor_y = float(self.ref_mloc[1] - mloc[1]) 2098 # normalize by axes height 2099 factor_y /= axes.position.height 2100 2101 # apply (use only y-factor ). 2102 Rrange = Rrange * math.exp(-factor_y) 2103 self.SetLimits(rangeR=Range(self._radialRange.min, self._radialRange.min + Rrange)) 2104 self.ref_mloc = mloc 2105 self.Draw() 2106 return True 2107 2108 2109 @DrawAfter 2110 def SetLimits(self, rangeTheta=None, rangeR=None, margin=0.04): 2111 """ SetLimits(rangeTheta=None, rangeR=None, margin=0.02) 2112 2113 Set the Polar limits of the scene. These are taken as hints to set 2114 the camera view, and determine where the axis is drawn for the 2115 3D camera. 2116 2117 Either range can be None, rangeTheta can be a scalar since only the 2118 starting position is used. RangeTheta is always 360 degrees 2119 Both rangeTheta dn rangeR can be a 2 element iterable, or a 2120 visvis.Range object. If a range is None, the range is obtained from 2121 the wobjects currently in the scene. To set the range that will fit 2122 all wobjects, simply use "SetLimits()" 2123 2124 The margin represents the fraction of the range to add (default 2%). 2125 2126 """ 2127 2128 if rangeTheta is None or isinstance(rangeTheta, Range): 2129 pass # ok 2130 elif hasattr(rangeTheta, '__len__') and len(rangeTheta) >= 1: 2131 rangeTheta = Range(rangeTheta[0], rangeTheta[0] + 359) 2132 else: 2133 rangeTheta = Range(float(rangeTheta), float(rangeTheta) + 359) 2134 2135 if rangeR is None or isinstance(rangeR, Range): 2136 pass # ok 2137 elif hasattr(rangeR, '__len__') and len(rangeR) == 2: 2138 rangeR = Range(rangeR[0], rangeR[1]) 2139 else: 2140 raise ValueError("radial limits should be Range \ 2141 or two-element iterables.") 2142 2143 if rangeTheta is not None: 2144 self._angularRange = rangeTheta 2145 2146 2147 rR = rangeR 2148 rZ = rangeZ = None 2149 2150 axes = self.GetAxes() 2151 2152 # find outmost range 2153 drawObjs = axes.FindObjects(PolarLine) 2154 # Now set the transform for the PolarLine data 2155 for ob in drawObjs: 2156 2157 # Ask object what it's polar limits are 2158 tmp = ob._GetPolarLimits() 2159 if not tmp: 2160 continue 2161 tmpTheta, tmpR = tmp # in the future may use theta limits 2162 if not tmp: 2163 continue 2164 tmp = ob._GetLimits() 2165 tmpX, tmpY, tmpZ = tmp 2166 2167 # update min/max 2168 if rangeR: 2169 pass 2170 elif tmpR and rR: 2171 rR = Range(min(rR.min, tmpR.min), max(rR.max, tmpR.max)) 2172 elif tmpR: 2173 rR = tmpR 2174 2175 if rangeZ: 2176 pass 2177 elif tmpZ and rZ: 2178 rZ = Range(min(rZ.min, tmpZ.min), max(rZ.max, tmpZ.max)) 2179 elif tmpX: 2180 rZ = tmpZ 2181 2182 # default values 2183 if rR is None: 2184 rR = Range(-1, 1) 2185 2186 if rZ is None: 2187 rZ = Range(0, 1) 2188 2189 self._radialRange = rR 2190 # apply margins 2191 if margin: 2192 # x 2193 tmp = rR.range * margin 2194 if tmp == 0: 2195 tmp = margin 2196 adjDim = rR.range + tmp 2197 rX = Range(-adjDim, adjDim) 2198 rY = Range(-adjDim, adjDim) 2199 # z 2200 tmp = rZ.range * margin 2201 if tmp == 0: 2202 tmp = margin 2203 rZ = Range(rZ.min - tmp, rZ.max + tmp) 2204 2205 # apply to each camera 2206 for cam in axes._cameras.values(): 2207 cam.SetLimits(rX, rY, rZ) 2208 2209 2210 def GetLimits(self): 2211 """ GetLimits() 2212 2213 Get the limits of the polar axis as displayed now. 2214 Returns a tuple of limits for theta and r, respectively. 2215 2216 """ 2217 return self._angularRange, self._radialRange 2218 2219 2220 @PropWithDraw 2221 def angularRefPos(): 2222 """ Get/Set the angular reference position in 2223 degrees wrt +x screen axis. 2224 """ 2225 # internal store in radians to avoid constant conversions 2226 def fget(self): 2227 return 180.0 * self._angularRefPos / np.pi 2228 2229 def fset(self, value): 2230 self._angularRefPos = np.pi * int(value) / 180 2231 self.Draw() 2232 return locals() 2233 2234 2235 @PropWithDraw 2236 def isCW(): 2237 """ Get/Set the sense of rotation. 2238 """ 2239 def fget(self): 2240 return (self._sense == 1) 2241 2242 def fset(self, value): 2243 if isinstance(value, bool): 2244 if value: 2245 self._sense = 1.0 2246 else: 2247 self._sense = -1.0 2248 self.Draw() 2249 else: 2250 raise Exception("isCW can only be assigned " + 2251 "by a bool (True or False)") 2252 return locals() 2253