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