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 line
8
9Defines the Line class, which represents a line connected (optionally)
10with markers. It is the object created with the plot() function.
11
12The lines are drawn simply with OpenGL lines. The markers are drawn
13with OpenGl points if possible, and using sprites otherwise.
14
15Since this is such a fundamental part of visvis, and it's uses by
16for example the Legend class, this module is part of the core.
17
18"""
19
20import OpenGL.GL as gl
21import numpy as np
22import math
23
24from visvis.utils.pypoints import Pointset, is_Pointset
25from visvis.core.misc import PropWithDraw, DrawAfter, basestring
26from visvis.core.misc import Range, getColor, getOpenGlCapable
27from visvis.core.base import Wobject
28
29
30# int('1010101010101010',2)  int('1100110011001100',2)
31lineStyles = {  ':':int('1010101010101010',2),  '--':int('1111000011110000',2),
32                '-.':int('1110010011100100',2), '.-':int('1110010011100100',2),
33                '-':False, '+':False}
34
35
36class Sprite:
37    """ Sprite(data, width)
38
39    Represents an OpenGL sprite object.
40
41    """
42
43    def __init__(self, data, width):
44        """ Supply the data, which must be uint8 alpha data,
45        preferably shaped with a power of two. """
46        self._texId = 0
47        self._data = data
48        self._width = width
49        self._canUse = None # set to True/False if OpenGl version high enough
50
51    def Create(self):
52        """ Create an OpenGL texture from the data. """
53
54        # detemine now if we can use point sprites
55        self._canUse = getOpenGlCapable('2.0',
56            'point sprites (for advanced markers)')
57        if not self._canUse:
58            return
59
60        # gl.glEnable(gl.GL_TEXTURE_2D)
61
62        # make texture
63        self._texId = gl.glGenTextures(1)
64        gl.glBindTexture(gl.GL_TEXTURE_2D, self._texId)
65
66        # set interpolation and extrapolation parameters
67        tmp = gl.GL_NEAREST # gl.GL_NEAREST | gl.GL_LINEAR
68        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, tmp)
69        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, tmp)
70
71        # upload data
72        shape = self._data.shape
73        gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_ALPHA, shape[0], shape[1],
74            0, gl.GL_ALPHA, gl.GL_UNSIGNED_BYTE, self._data)
75
76
77    @property
78    def usable(self):
79        return self._canUse
80
81
82    def Enable(self):
83        """ Enable the sprite, drawing points after calling this
84        draws this sprite at each point. """
85
86        if not self._texId and self._canUse in [None, True]:
87            self.Create()
88
89        if not self._canUse:
90            gl.glEnable(gl.GL_POINT_SMOOTH)
91            gl.glPointSize(self._width)
92            return # proceed if None; canUse is assigned in Create()
93
94        # bind to texture
95        gl.glEnable(gl.GL_TEXTURE_2D)
96        gl.glBindTexture(gl.GL_TEXTURE_2D, self._texId)
97
98        # get allowed point size
99        sizeRange = gl.glGetFloatv(gl.GL_ALIASED_POINT_SIZE_RANGE)
100        gl.glPointParameterf( gl.GL_POINT_SIZE_MIN, sizeRange[0] )
101        gl.glPointParameterf( gl.GL_POINT_SIZE_MAX, sizeRange[1] )
102
103        # enable sprites and set params
104        gl.glEnable(gl.GL_POINT_SPRITE)
105
106        # tell opengl to iterate over the texture
107        gl.glTexEnvf( gl.GL_POINT_SPRITE, gl.GL_COORD_REPLACE, gl.GL_TRUE )
108
109
110    def Disable(self):
111        """ Return to normal points. """
112        if self._canUse:
113            gl.glDisable(gl.GL_TEXTURE_2D)
114            gl.glDisable(gl.GL_POINT_SPRITE)
115        else:
116            gl.glDisable(gl.GL_POINT_SMOOTH)
117
118
119    def Destroy(self):
120        """ Destroy the sprite, removing the texture from opengl. """
121        if self._texId > 0:
122            try:
123                gl.glDeleteTextures([self._texId])
124                self._texId = 0
125            except Exception:
126                pass
127
128    def __del__(self):
129        """ Delete when GC cleans up. """
130        self.Destroy()
131
132
133class MarkerManager:
134    """ MarkerManager()
135
136    The markermanager manages sprites to draw the markers. It
137    creates the sprite textures on the fly when they are needed.
138    Already created sprites are reused.
139
140    Given the markerStyle, markerWidth and markerEdgeWidth a marker
141    can be requested.
142
143    """
144
145    def __init__(self):
146        # a dict of 3 element tuples (size, sprite1, sprite2)
147        # where the first is for the face, the second for the edge.
148        self.sprites = {}
149
150    def GetSprites(self, ms, mw, mew):
151        """ GetSprites(mw, mw, mew)
152
153        Get the sprites for drawing the edge and the face, given
154        the ms, mw and mew.
155
156        This will create the appropriate sprites or reuse a previously
157        created set of sprites if available.
158
159        Returns a tuple (size, faceSprite, edgeSprite)
160        """
161        # find the diameter which best fits, but which is a multiple of 4
162        # such that 32 bits are always filled.
163        mw, mew = int(mw), int(mew)
164
165        # create key of these settings
166        key = "%s_%i_%i" % (ms, mw, mew)
167
168        # if it does not exist, create it!
169        if key not in self.sprites:
170            self.sprites[key] = self._CreateSprites(ms, mw, mew)
171        else:
172            # check if it is a valid texture ...
173            id = self.sprites[key][1]._texId
174            if not gl.glIsTexture(id):
175                self.sprites[key] = self._CreateSprites(ms, mw, mew)
176
177        # return
178        return self.sprites[key]
179
180
181    def _CreateSprites(self, ms, mw, mew):
182        """ Create the sprites from scratch. """
183
184        ## init
185        # We'll make a 2D array of size d, which fits the marker completely.
186        # Then we make a second array with the original eroded as many
187        # times as the edge is wide.
188
189        # find nearest multiple of four
190        d = 4
191        while d < mw+2*mew:
192            d += 4
193        # what is the offset for the face
194        #dd = ( d-(mw+2*mew) ) / 2
195        dd = (d-mw)//2
196
197        # calc center
198        c = mw/2.0
199
200        # create patch
201        data1 = np.zeros((d,d),dtype=np.uint8)
202
203        # create subarray for face
204        data2 = data1[dd:,dd:][:mw,:mw]
205
206        ## define marker functions
207        def square(xy):
208            x, y = xy
209            data2[y,x]=255
210        def diamond(xy):
211            x, y = xy
212            if y > x-mw/2 and y<x+mw/2 and y > (mw-x)-c and y<(mw-x)+c:
213                data2[y,x]=255
214        def plus(xy):
215            x, y = xy
216            if y > mw/3 and y < 2*mw/3:
217                data2[y,x]=255
218            if x > mw/3 and x < 2*mw/3:
219                data2[y,x]=255
220        def cross(xy):
221            x, y = xy
222            if y > x-mw/4 and y < x+mw/4:
223                data2[y,x]=255
224            if y > (mw-x)-mw/4 and y < (mw-x)+mw/4:
225                data2[y,x]=255
226        def flower(xy):
227            x, y = xy
228            a = math.atan2(y-c,x-c)
229            r = (x-c)**2 + (y-c)**2
230            relAng = 5 * a / (2*math.pi)  # whole circle from 1 to 5
231            subAng = (relAng % 1)       # get the non-integer bit
232            if subAng>0.5: subAng = 1-subAng
233            refRad1, refRad2 = c/4, c
234            a = math.sin(subAng*math.pi)
235            refRad = (1-a)*refRad1 + a*refRad2
236            if r < refRad**2:
237                data2[y,x]=255
238        def star5(xy):
239            x, y = xy
240            a = math.atan2(y-c,x-c) - 0.5*math.pi
241            r = (x-c)**2 + (y-c)**2
242            relAng = 5 * a / (2*math.pi)  # whole circle from 1 to 5
243            subAng = (relAng % 1)       # get the non-integer bit
244            if subAng>0.5: subAng = 1-subAng
245            refRad1, refRad2 = c/4, c
246            a = math.asin(subAng*2) / (math.pi/2)
247            refRad = (1-a)*refRad1 + a*refRad2
248            if r < refRad**2:
249                data2[y,x]=255
250        def star6(xy):
251            x, y = xy
252            a = math.atan2(y-c,x-c)
253            r = (x-c)**2 + (y-c)**2
254            relAng = 6 * a / (2*math.pi)  # whole circle from 1 to 5
255            subAng = (relAng % 1)       # get the non-integer bit
256            if subAng>0.5: subAng = 1-subAng
257            refRad1, refRad2 = c/3, c
258            a = math.asin(subAng*2) / (math.pi/2)
259            refRad = (1-a)*refRad1 + a*refRad2
260            if r < refRad**2:
261                data2[y,x]=255
262        def circle(xy):
263            x,y = xy
264            r = (x-c)**2 + (y-c)**2
265            if r < c**2:
266                data2[y,x] = 255
267        def triangleDown(xy):
268            x,y = xy
269            if x >= 0.5*y and x <= mw-0.5*(y+1):
270                data2[y,x] = 255
271        def triangleUp(xy):
272            x,y = xy
273            if x >= c-0.5*y and x <= c+0.5*y:
274                data2[y,x] = 255
275        def triangleLeft(xy):
276            x,y = xy
277            if y >= c-0.5*x and y <= c+0.5*x:
278                data2[y,x] = 255
279        def triangleRight(xy):
280            x,y = xy
281            print('oef', x, y)
282            if y >= 0.5*x and y <= mw-0.5*(x+1):
283                data2[y,x] = 255
284
285        # a dict ms to function
286        funcs = {   's':square, 'd':diamond, '+':plus, 'x':cross,
287                    '*':star5, 'p':star5, 'h':star6, 'f':flower,
288                    '.':circle, 'o':circle, 'v':triangleDown,
289                    '^':triangleUp, '<':triangleLeft, '>':triangleRight}
290
291        # select function
292        try:
293            func = funcs[ms]
294        except KeyError:
295            func = circle
296
297        ## Create face
298        I,J = np.where(data2==0)
299        for xy in zip(I,J):
300            func(xy)
301
302        ## dilate x times to create edge
303        # we add a border to the array to make the dilation possible
304        data3 = np.zeros((d+4,d+4), dtype=np.uint8)
305        data3[2:-2,2:-2] = 1
306        # retrieve indices.
307        I,J = np.where(data3==1)
308        # copy face
309        data3[2:-2,2:-2] = data1
310        tmp = data3.copy()
311        # apply
312        def dilatePixel(xy):
313            x,y = xy
314            if tmp[y-1:y+2,x-1:x+2].max():
315                data3[y,x] = 255
316        for i in range(int(mew)):
317            for xy in zip(I,J):
318                dilatePixel(xy)
319            tmp = data3.copy()
320        # remove border
321        data3 = data3[2:-2,2:-2]
322
323        ## create sprites and return
324
325        sprite1 = Sprite(data1, mw)
326        sprite2 = Sprite(data3-data1, mw+2*mew)
327
328        return d, sprite1, sprite2
329
330
331class Line(Wobject):
332    """ Line(parent, points)
333
334    The line class represents a set of points (locations) in world coordinates.
335    This class can draw lines between the points, markers at the point
336    coordinates, or both.
337
338    Line objects can be created with the function vv.plot().
339
340    Performance tips
341    ----------------
342    The s, o (and .) styles can be drawn using standard
343    OpenGL points if alpha is 1 or if no markeredge is drawn.
344
345    Otherwise point sprites are used, which can be slower
346    on some (older) cards (like ATI, Nvidia performs quite ok with with
347    sprites).
348
349    Some video cards simply do not support sprites (seen on ATI).
350
351    """
352
353    def __init__(self, parent, points):
354        Wobject.__init__(self, parent)
355
356        # Store points
357        self.SetPoints(points)
358
359        # init line properties
360        self._lw, self._ls, self._lc = 1, '-', 'b'
361        # init marker properties
362        self._mw, self._ms, self._mc = 7, '', 'b'
363        # init marker edge properties
364        self._mew, self._mec = 1, 'k'
365
366        # alpha values
367        self._alpha1 = 1
368
369
370    def _AsFloat(self, value, descr):
371        """ Make sure a value is a float. """
372        try:
373            value = float(value)
374            if value<0:
375                raise ValueError()
376        except ValueError:
377            tmp = "the value must be a number equal or larger than zero!"
378            raise Exception("Error in %s: %s" % (descr, tmp) )
379        return value
380
381
382    def _GetLimits(self):
383        """ Get the limits in world coordinates between which the object exists.
384        """
385
386        # Obtain untransformed coords (if not an empty set)
387        if not self._points:
388            return None
389        p = self._points.data
390        valid = np.isfinite(p[:,0]) * np.isfinite(p[:,1]) * np.isfinite(p[:,2])
391        validpoints = p[valid, :]
392        x1, y1, z1 = validpoints.min(axis=0)
393        x2, y2, z2 = validpoints.max(axis=0)
394
395        # There we are
396        return Wobject._GetLimits(self, x1, x2, y1, y2, z1, z2)
397
398
399    ## Create properties
400
401
402    @PropWithDraw
403    def lw():
404        """ Get/Set the lineWidth: the width of the line in pixels.
405        If zero, the line is not drawn.
406        """
407        def fget(self):
408            return self._lw
409        def fset(self, value):
410            self._lw = self._AsFloat(value, 'lineWidth')
411        return locals()
412
413    @PropWithDraw
414    def ls():
415        """ Get/Set the lineStyle: the style of the line.
416          * Solid line: '-'
417          * Dotted line: ':'
418          * Dashed line: '--'
419          * Dash-dot line: '-.' or '.-'
420          * A line that is drawn between each pair of points: '+'
421          * No line: '' or None.
422        """
423        def fget(self):
424            return self._ls
425        def fset(self, value):
426            if not value:
427                value = None
428            elif not isinstance(value, basestring):
429                raise Exception("Error in lineStyle: style must be a string!")
430            elif value not in ['-', '--', ':', '-.', '.-', '+']:
431                raise Exception("Error in lineStyle: unknown line style!")
432            self._ls = value
433        return locals()
434
435    @PropWithDraw
436    def lc():
437        """ Get/Set the lineColor: the color of the line, as a 3-element
438        tuple or as a single character string (shown in uppercase):
439        Red, Green, Blue, Yellow, Cyan, Magenta, blacK, White.
440        """
441        def fget(self):
442            return self._lc
443        def fset(self, value):
444            value = getColor(value, 'lineColor')
445            self._lc = value
446        return locals()
447
448
449    @PropWithDraw
450    def mw():
451        """ Get/Set the markerWidth: the width (bounding box) of the marker
452        in (screen) pixels. If zero, no marker is drawn.
453        """
454        def fget(self):
455            return self._mw
456        def fset(self, value):
457            self._mw = self._AsFloat(value, 'markerWidth')
458        return locals()
459
460    @PropWithDraw
461    def ms():
462        """ Get/Set the markerStyle: the style of the marker.
463          * Plus: '+'
464          * Cross: 'x'
465          * Square: 's'
466          * Diamond: 'd'
467          * Triangle (pointing up, down, left, right): '^', 'v', '<', '>'
468          * Pentagram star: 'p' or '*'
469          * Hexgram: 'h'
470          * Point/cirle: 'o' or '.'
471          * No marker: '' or None
472        """
473        def fget(self):
474            return self._ms
475        def fset(self, value):
476            if not value:
477                value = None
478            elif not isinstance(value, basestring):
479                raise Exception("markerstyle (ms) should be a string!")
480            elif value not in 'sd+x*phfv^><.o':
481                raise Exception("Error in markerStyle: unknown line style!")
482            self._ms = value
483        return locals()
484
485    @PropWithDraw
486    def mc():
487        """ Get/Set the markerColor: The color of the face of the marker
488        If None, '', or False, the marker face is not drawn (but the edge is).
489        """
490        def fget(self):
491            return self._mc
492        def fset(self, value):
493            self._mc = getColor(value, 'markerColor')
494        return locals()
495
496    @PropWithDraw
497    def mew():
498        """ Get/Set the markerEdgeWidth: the width of the edge of the marker.
499        If zero, no edge is drawn.
500        """
501        def fget(self):
502            return self._mew
503        def fset(self, value):
504            self._mew = self._AsFloat(value, 'markerEdgeWidth')
505        return locals()
506
507    @PropWithDraw
508    def mec():
509        """ Get/Set the markerEdgeColor: the color of the edge of the marker.
510        """
511        def fget(self):
512            return self._mec
513        def fset(self, value):
514            self._mec = getColor(value, 'markerEdgeColor')
515        return locals()
516
517#     # create aliases
518#     lineWidth = lw
519#     lineStyle = ls
520#     lineColor = lc
521#     markerWidth = mw
522#     markerStyle = ms
523#     markerColor = mc
524#     markerEdgeWidth = mew
525#     markerEdgeColor = mec
526
527    @PropWithDraw
528    def alpha():
529        """ Get/Set the alpha (transparancy) of the line and markers.
530        When this is < 1, the line cannot be anti-aliased, and it
531        is drawn on top of any other wobjects.
532        """
533        def fget(self):
534            return self._alpha1
535        def fset(self, value):
536            self._alpha1 = self._AsFloat(value, 'alpha')
537        return locals()
538
539    ## Set methods
540
541    @DrawAfter
542    def SetXdata(self, data):
543        """ SetXdata(data)
544
545        Set the x coordinates of the points of the line.
546
547        """
548        self._points[:,0] = handleInvalidValues(data)
549
550    @DrawAfter
551    def SetYdata(self, data):
552        """ SetYdata(data)
553
554        Set the y coordinates of the points of the line.
555
556        """
557        self._points[:,1] = handleInvalidValues(data)
558
559    @DrawAfter
560    def SetZdata(self, data):
561        """ SetZdata(data)
562
563        Set the z coordinates of the points of the line.
564
565        """
566        self._points[:,2] = handleInvalidValues(data)
567
568    @DrawAfter
569    def SetPoints(self, points):
570        """ SetPoints(points)
571
572        Set x,y (and optionally z) data. The given argument can be anything
573        that can be converted to a pointset. (From version 1.7 this method
574        also works with 2D pointsets.)
575
576        The data is copied, so changes to original data will not affect
577        the visualized points. If you do want this, use the points property.
578
579        """
580
581        # Try make it a (copied) pointset (handle masked array)
582        if is_Pointset(points):
583            points = Pointset(handleInvalidValues(points.data))
584        else:
585            points = Pointset(handleInvalidValues(points))
586
587        # Add z dimension to points if not available
588        if points.ndim == 3:
589            pass
590        elif points.ndim == 2:
591            zz = 0.1*np.ones((len(points._data),1), dtype='float32')
592            points._data = np.concatenate((points._data, zz),1)
593        elif points.ndim == 1:
594            N = len(points._data)
595            xx = np.arange(N, dtype='float32').reshape(N, 1)
596            zz = 0.1*np.ones((N,1), dtype='float32')
597            points._data = np.concatenate((xx, points._data, zz), 1)
598
599        # Store
600        self._points = points
601
602    @property
603    def points(self):
604        """ Get a reference to the internal Pointset used to draw the line
605        object. Note that this pointset is always 3D. One can modify
606        this pointset in place, but note that a call to Draw() may be
607        required to update the screen. (New in version 1.7.)
608        """
609        return self._points
610
611
612    ## Draw methods
613
614    def OnDrawFast(self):
615        self.OnDraw(True)
616
617    def OnDraw(self, fast=False):
618
619        # add z dimension to points if not available
620        pp = self._points
621        if pp.ndim == 2:
622            # a bit dirty this
623            tmp = pp._data, 0.1*np.ones((len(pp._data),1),dtype='float32')
624            pp._data = np.concatenate(tmp,1)
625
626        # can I draw this data?
627        if pp.ndim != 3:
628            raise Exception("Can only draw 3D data!")
629
630        # no need to draw if no points
631        if len(self._points) == 0:
632            return
633
634        # enable anti aliasing and blending
635        gl.glEnable(gl.GL_LINE_SMOOTH)
636        gl.glEnable(gl.GL_BLEND)
637
638        # lines
639        if self.lw and self.ls:
640            self._DrawLines()
641
642        # points
643        if self.mw and self.ms:
644            self._DrawPoints()
645
646        # clean up
647        #gl.glDisable(gl.GL_BLEND)
648
649
650    def _DrawLines(self):
651
652        # set stipple style
653        if not self.ls in lineStyles:
654            stipple = False
655        else:
656            stipple = lineStyles[self.ls]
657        #
658        if stipple and self.lw:
659            gl.glEnable(gl.GL_LINE_STIPPLE)
660            gl.glLineStipple(int(round(self.lw)), stipple)
661        else:
662            gl.glDisable(gl.GL_LINE_STIPPLE)
663
664        # init vertex array
665        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
666        gl.glVertexPointerf(self._points.data)
667
668        # linepieces drawn on top of other should draw just fine. See issue #95
669        gl.glDepthFunc(gl.GL_LEQUAL)
670
671        # init blending. Only use constant blendfactor when alpha<1
672        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
673        if self._alpha1<1:
674            #if len(self._points) < 1000:
675            if getOpenGlCapable('1.4','transparant points and lines'):
676                gl.glBlendFunc(gl.GL_CONSTANT_ALPHA, gl.GL_ONE_MINUS_CONSTANT_ALPHA)
677                gl.glBlendColor(0.0,0.0,0.0, self._alpha1)
678            gl.glDisable(gl.GL_DEPTH_TEST)
679
680        # get color
681        clr = getColor( self.lc )
682
683        if clr and self._alpha1>0:
684
685            # set width and color
686            gl.glLineWidth(self.lw)
687            gl.glColor3f(clr[0], clr[1], clr[2])
688
689            # draw
690            method = gl.GL_LINE_STRIP
691            if self.ls == '+':
692                method = gl.GL_LINES
693            gl.glDrawArrays(method, 0, len(self._points))
694            # flush!
695            gl.glFlush()
696
697        # clean up
698        gl.glDisable(gl.GL_LINE_STIPPLE)
699        gl.glLineStipple(int(round(self.lw)), int('1111111111111111',2))
700        gl.glEnable(gl.GL_DEPTH_TEST)
701        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
702        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
703        gl.glDepthFunc(gl.GL_LESS)
704
705
706    def _DrawPoints(self):
707
708        # get colors (use color from edge or face if not present)
709        clr1 = getColor(self.mc)
710        clr2 = getColor(self.mec)
711
712        # draw face or edge?
713        drawFace = bool(self.mc) # if not ms or mw we would not get here
714        drawEdge = self.mec and self.mew
715        if not drawFace and not drawEdge:
716            return
717
718        # get figure
719        f = self.GetFigure()
720        if not f:
721            return
722
723        # init blending. Only use constant blendfactor when alpha<1
724        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
725        if self._alpha1<1:
726            if getOpenGlCapable('1.4','transparant points and lines'):
727                gl.glBlendFunc(gl.GL_CONSTANT_ALPHA,
728                    gl.GL_ONE_MINUS_CONSTANT_ALPHA)
729                gl.glBlendColor(0.0,0.0,0.0, self._alpha1)
730            gl.glDisable(gl.GL_DEPTH_TEST)
731
732        # init vertex array
733        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
734        gl.glVertexPointerf(self._points.data)
735
736        # points drawn on top of points should draw (because we draw
737        # the face and edge seperately)
738        gl.glDepthFunc(gl.GL_LEQUAL)
739
740        # Enable alpha test, such that fragments with 0 alpha
741        # will not update the z-buffer.
742        gl.glEnable(gl.GL_ALPHA_TEST)
743        gl.glAlphaFunc(gl.GL_GREATER, 0.01)
744
745        if self.ms in ['o','.','s'] and not drawEdge:
746            # Use standard OpenGL points, faster and anti-aliased
747            # Pure filled points or squares always work.
748
749            # choose style
750            if self.ms == 's':
751                gl.glDisable(gl.GL_POINT_SMOOTH)
752            else:
753                gl.glEnable(gl.GL_POINT_SMOOTH)
754
755            # draw faces only
756            if drawFace:
757                gl.glColor3f(clr1[0],clr1[1],clr1[2])
758                gl.glPointSize(self.mw)
759                gl.glDrawArrays(gl.GL_POINTS, 0, len(self._points))
760
761        elif self.ms in ['o','.','s'] and drawFace and self.alpha==1:
762            # Use standard OpenGL points, faster and anti-aliased
763            # If alpha=1 and we have a filled marker, we can draw in two steps.
764
765            # choose style
766            if self.ms == 's':
767                gl.glDisable(gl.GL_POINT_SMOOTH)
768            else:
769                gl.glEnable(gl.GL_POINT_SMOOTH)
770
771            # draw edges
772            if drawEdge:
773                gl.glColor3f(clr2[0],clr2[1],clr2[2])
774                gl.glPointSize(self.mw+self.mew*2)
775                gl.glDrawArrays(gl.GL_POINTS, 0, len(self._points))
776            # draw faces
777            if drawFace:
778                gl.glColor3f(clr1[0],clr1[1],clr1[2])
779                gl.glPointSize(self.mw)
780                gl.glDrawArrays(gl.GL_POINTS, 0, len(self._points))
781
782        #elif self.alpha>0:
783        else:
784            # Use sprites
785
786            # get sprites
787            tmp = f._markerManager.GetSprites(self.ms, self.mw, self.mew)
788            pSize, sprite1, sprite2 = tmp
789            gl.glPointSize(pSize)
790
791            # draw points for the edges
792            if drawEdge:
793                sprite2.Enable()
794                gl.glColor3f(clr2[0],clr2[1],clr2[2])
795                gl.glDrawArrays(gl.GL_POINTS, 0, len(self._points))
796            # draw points for the faces
797            if drawFace:
798                sprite1.Enable()
799                gl.glColor3f(clr1[0],clr1[1],clr1[2])
800                gl.glDrawArrays(gl.GL_POINTS, 0, len(self._points))
801
802            # disable sprites
803            sprite1.Disable() # Could as well have used sprite2
804
805        # clean up
806        gl.glDisable(gl.GL_ALPHA_TEST)
807        gl.glEnable(gl.GL_DEPTH_TEST)
808        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
809        gl.glDepthFunc(gl.GL_LESS)
810        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
811
812
813    def OnDrawShape(self, clr):
814        # Draw the shape of the line so we can detect mouse actions
815
816        # disable anti aliasing and blending
817        gl.glDisable(gl.GL_LINE_SMOOTH)
818        gl.glDisable(gl.GL_BLEND)
819
820        # no stippling, square points
821        gl.glDisable(gl.GL_LINE_STIPPLE)
822        gl.glDisable(gl.GL_POINT_SMOOTH)
823
824        # init vertex array
825        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
826        gl.glVertexPointerf(self._points.data)
827
828        # detect which parts to draw
829        drawLine, drawMarker = False, False
830        if self.lw and self.ls and getColor(self.lc):
831            drawLine = True
832        if self.mw and self.ms:
833            drawMarker = True
834
835        if drawLine:
836            # set width and color
837            gl.glLineWidth(self.lw)
838            gl.glColor3f(clr[0], clr[1], clr[2])
839            # draw
840            gl.glDrawArrays(gl.GL_LINE_STRIP, 0, len(self._points))
841            gl.glFlush()
842
843        if drawMarker:
844            w = self.mw
845            if self.mec:
846                w += self.mew
847            # set width and color
848            gl.glColor3f(clr[0],clr[1],clr[2])
849            gl.glPointSize(w)
850            # draw
851            gl.glDrawArrays(gl.GL_POINTS, 0, len(self._points))
852            gl.glFlush()
853
854        # clean up
855        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
856
857
858    def OnDestroy(self):
859        # clean up some memory
860        self._points.clear()
861
862
863# This is a new type of wobject called PolarLine which encapsulates
864# polar plot data.
865class PolarLine(Line):
866    """ PolarLine(parent, angle(radians), mag)
867
868    The Polarline class represents a set of points (locations)
869    in world coordinates. This class can draw lines between the points,
870    markers at the point coordinates, or both.
871
872    There are several linestyles that can be used:
873      * -  a solid line
874      * :   a dotted line
875      * --  a dashed line
876      * -.  a dashdot line
877      * .-  dito
878      * +   draws a line between each pair of points (handy for visualizing
879            for example vectore fields)
880    If None, '' or False is given no line is drawn.
881
882    There are several marker styles that can be used:
883      * `+`  a plus
884      * `x`  a cross
885      * `s`  a square
886      * `d`  a diamond
887      * `^v<>` an up-, down-, left- or rightpointing triangle
888      * `*` or `p`  a (pentagram star)
889      * `h`  a hexagram
890      * `o` or `.`  a point/circle
891    If None, '', or False is given, no marker is drawn.
892
893    Performance tip
894    ---------------
895    The s, o (and .) styles can be drawn using standard
896    OpenGL points if alpha is 1 or if no markeredge is drawn.
897    Otherwise point sprites are used, which can be slower
898    on some cards (like ATI, Nvidia performs quite ok with with
899    sprites)
900
901    """
902    def __init__(self, parent, angs, mags):
903        self._angs = angs
904        self._mags = mags
905        x = mags * np.cos(angs)
906        y = mags * np.sin(angs)
907        z = np.zeros((np.size(x), 1))
908        tmp = x, y, z
909        pp = Pointset(np.concatenate(tmp, 1))
910        Line.__init__(self, parent, pp)
911
912    def TransformPolar(self, radialRange, angRefPos, sense):
913        offsetMags = self._mags - radialRange.min
914        rangeMags = radialRange.range
915        offsetMags[offsetMags > rangeMags] = rangeMags
916        tmpang = angRefPos + sense * self._angs
917        x = offsetMags * np.cos(tmpang)
918        y = offsetMags * np.sin(tmpang)
919        z = np.zeros((np.size(x), 1)) + 0.2
920        x[offsetMags < 0] = 0
921        y[offsetMags < 0] = 0
922        tmp = x, y, z
923        self._points = Pointset(np.concatenate(tmp, 1))
924
925    def _GetPolarLimits(self):
926        if not self._points:
927            return None
928        else:
929            return Range(self._angs.min(), self._angs.max()), \
930                   Range(self._mags.min(), self._mags.max())
931
932
933
934def handleInvalidValues(values):
935    """ handleInvalidValues(values)
936
937    Modifies any invalid values (NaN, Inf, -Inf) to Inf,
938    and turn masked values of masked arrays to Inf.
939    Returns a copy if correction if needed.
940    """
941    if isinstance(values, np.ma.MaskedArray):
942        values = values.filled(np.inf)
943        _inplace = True  # values is already a copy, so we can modify it
944    else:
945        _inplace = False
946    if not isinstance(values, np.ndarray):
947        values = np.array(values)
948
949    invalid = ~np.isfinite(values)
950    # Determine if we should make a copy
951    if invalid.sum() and not _inplace:
952        values = values.copy()
953    # Convert values and return
954    try:
955        values[invalid] = np.inf
956    except OverflowError:
957        pass # Cannot do this with integer types
958    return values
959