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 misc
8
9Various things are defined here that did not fit nicely in any
10other module.
11
12This module is also meant to be imported by many
13other visvis modules, and therefore should not depend on other
14visvis modules.
15
16"""
17
18import os
19import sys
20import zipfile
21
22import numpy as np
23import OpenGL.GL as gl
24
25from visvis.utils import ssdf
26from visvis.utils.pypoints import getExceptionInstance  # noqa
27
28
29V2 = sys.version_info[0] == 2
30if V2:
31    unichr = unichr  # noqa
32    basestring = basestring  # noqa
33else:
34    basestring = str
35    unichr = chr
36
37
38# Used to load resources when in a zipfile
39def splitPathInZip(path):
40    """ splitPathInZip(path)
41    Split a filename in two parts: the path to a zipfile, and the path
42    within the zipfile. If the given path is a native file or direcory,
43    returns ('', path). Raises an error if no valid zipfile can be found.
44    """
45    ori_path = path
46    if os.path.exists(path):
47        return '', path
48    # Split path in parts
49    zipPath = []
50    while True:
51        path, sub = os.path.split(path)
52        if not sub:
53            raise RuntimeError('Not a real path nor in a zipfile: "%s"' % ori_path)
54        zipPath.insert(0, sub)
55        if os.path.isfile(path):
56            if zipfile.is_zipfile(path):
57                return path, os.path.join(*zipPath)
58            else:
59                raise RuntimeError('Not a zipfile: "%s"' % ori_path)
60
61
62
63## For info about OpenGL version
64
65
66# To store openGl info
67_glInfo = [None]*4
68
69
70def ensureString(s):
71    if isinstance(s, str):
72        return s
73    else:
74        return s.decode('ascii')
75
76def getOpenGlInfo():
77    """ getOpenGlInfo()
78
79    Get information about the OpenGl version on this system.
80    Returned is a tuple (version, vendor, renderer, extensions)
81    Note that this function will return 4 Nones if the openGl
82    context is not set.
83
84    """
85
86    if gl.glGetString(gl.GL_VERSION) is None:
87        raise RuntimeError('There is currently no OpenGL context')
88
89    if not _glInfo[0]:
90        _glInfo[0] = ensureString(gl.glGetString(gl.GL_VERSION))
91        _glInfo[1] = ensureString(gl.glGetString(gl.GL_VENDOR))
92        _glInfo[2] = ensureString(gl.glGetString(gl.GL_RENDERER))
93        _glInfo[3] = ensureString(gl.glGetString(gl.GL_EXTENSIONS))
94    return tuple(_glInfo)
95
96_glLimitations = {}
97def getOpenGlCapable(version, what=None):
98    """ getOpenGlCapable(version, what)
99
100    Returns True if the OpenGl version on this system is equal or higher
101    than the one specified and False otherwise.
102
103    If False, will display a message to inform the user, but only the first
104    time that this limitation occurs (identified by the second argument).
105
106    """
107
108    # obtain version of system
109    curVersion = _glInfo[0]
110    if not curVersion:
111        curVersion, dum1, dum2, dum3 = getOpenGlInfo()
112        if not curVersion:
113            return False # OpenGl context not set, better safe than sory
114
115    # make sure version is a string
116    if isinstance(version, (int,float)):
117        version = str(version)
118
119    # test
120    if curVersion >= version :
121        return True
122    else:
123        # show message?
124        if what and (what not in _glLimitations):
125            _glLimitations[what] = True
126            tmp = "Warning: the OpenGl version on this system is too low "
127            tmp += "to support " + what + ". "
128            tmp += "Try updating your drivers or buy a new video card."
129            print(tmp)
130        return False
131
132
133## Decorators
134
135
136def Property(function):
137    """ Property(function)
138
139    A property decorator which allows to define fget, fset and fdel
140    inside the function.
141
142    Note that the class to which this is applied must inherit from object!
143    Code based on an example posted by Walker Hale:
144    http://code.activestate.com/recipes/410698/#c6
145
146    Example
147    -------
148
149    class Example(object):
150        @Property
151        def myattr():
152            ''' This is the doc string.
153            '''
154            def fget(self):
155                return self._value
156            def fset(self, value):
157                self._value = value
158            def fdel(self):
159                del self._value
160            return locals()
161
162    """
163
164    # Define known keys
165    known_keys = 'fget', 'fset', 'fdel', 'doc'
166
167    # Get elements for defining the property. This should return a dict
168    func_locals = function()
169    if not isinstance(func_locals, dict):
170        raise RuntimeError('Property function should "return locals()".')
171
172    # Create dict with kwargs for property(). Init doc with docstring.
173    D = {'doc': function.__doc__}
174
175    # Copy known keys. Check if there are invalid keys
176    for key in list(func_locals.keys()):
177        if key in known_keys:
178            D[key] = func_locals[key]
179        else:
180            raise RuntimeError('Invalid Property element: %s' % key)
181
182    # Done
183    return property(**D)
184
185
186def PropWithDraw(function):
187    """ PropWithDraw(function)
188
189    A property decorator which allows to define fget, fset and fdel
190    inside the function.
191
192    Same as Property, but calls self.Draw() when using fset.
193
194    """
195
196    # Define known keys
197    known_keys = 'fget', 'fset', 'fdel', 'doc'
198
199    # Get elements for defining the property. This should return a dict
200    func_locals = function()
201    if not isinstance(func_locals, dict):
202        raise RuntimeError('Property function should "return locals()".')
203
204    # Create dict with kwargs for property(). Init doc with docstring.
205    D = {'doc': function.__doc__}
206
207    # Copy known keys. Check if there are invalid keys
208    for key in list(func_locals.keys()):
209        if key in known_keys:
210            D[key] = func_locals[key]
211        else:
212            raise RuntimeError('Invalid Property element: %s' % key)
213
214    # Replace fset
215    fset = D.get('fset', None)
216    def fsetWithDraw(self, *args):
217        fset(self, *args)
218        if hasattr(self, 'Draw'):
219            self.Draw()
220        #print(fset._propname, self, time.time())
221    if fset:
222        fset._propname = function.__name__
223        D['fset'] = fsetWithDraw
224
225    # Done
226    return property(**D)
227
228
229def DrawAfter(function):
230    """ DrawAfter(function)
231
232    Decorator for methods that make self.Draw() be called right after
233    the function is called.
234
235    """
236    def newFunc(self, *args, **kwargs):
237        retval = function(self, *args, **kwargs)
238        if hasattr(self, 'Draw'):
239            self.Draw()
240        return retval
241    newFunc.__doc__ = function.__doc__
242    return newFunc
243
244
245def PropertyForSettings(function):
246    """ PropertyForSettings(function)
247
248    A property decorator that also supplies the function name to the
249    fget and fset function. The fset method also calls _Save()
250
251    """
252
253    # Define known keys
254    known_keys = 'fget', 'fset', 'fdel', 'doc'
255
256    # Get elements for defining the property. This should return a dict
257    func_locals = function()
258    if not isinstance(func_locals, dict):
259        raise RuntimeError('From visvis version 1.6, Property function should "return locals()".')
260
261    # Create dict with kwargs for property(). Init doc with docstring.
262    D = {'doc': function.__doc__}
263
264    # Copy known keys. Check if there are invalid keys
265    for key in list(func_locals.keys()):
266        if key in known_keys:
267            D[key] = func_locals[key]
268        else:
269            raise RuntimeError('Invalid Property element: %s' % key)
270
271    # Replace fset and fget
272    fset = D.get('fset', None)
273    fget = D.get('fget', None)
274    def fsetWithKey(self, *args):
275        fset(self, function.__name__, *args)
276        self._Save()
277    def fgetWithKey(self, *args):
278        return fget(self, function.__name__)
279    if fset:
280        D['fset'] = fsetWithKey
281    if fget:
282        D['fget'] = fgetWithKey
283
284    # Done
285    return property(**D)
286
287
288## The range class
289
290
291class Range(object):
292    """ Range(min=0, max=0)
293
294    Represents a range (a minimum and a maximum ). Can also be instantiated
295    using a tuple.
296
297    If max is set smaller than min, the min and max are flipped.
298
299    """
300    def __init__(self, min=0, max=1):
301        self.Set(min,max)
302
303    def Set(self, min=0, max=1):
304        """ Set the values of min and max with one call.
305        Same signature as constructor.
306        """
307        if isinstance(min, Range):
308            min, max = min.min, min.max
309        elif isinstance(min, (tuple,list)):
310            min, max = min[0], min[1]
311        self._min = float(min)
312        self._max = float(max)
313        self._Check()
314
315    @property
316    def range(self):
317        return self._max - self._min
318
319    @Property # visvis.Property
320    def min():
321        """ Get/Set the minimum value of the range. """
322        def fget(self):
323            return self._min
324        def fset(self,value):
325            self._min = float(value)
326            self._Check()
327        return locals()
328
329    @Property # visvis.Property
330    def max():
331        """ Get/Set the maximum value of the range. """
332        def fget(self):
333            return self._max
334        def fset(self,value):
335            self._max = float(value)
336            self._Check()
337        return locals()
338
339    def _Check(self):
340        """ Flip min and max if order is wrong. """
341        if self._min > self._max:
342            self._max, self._min = self._min, self._max
343
344    def Copy(self):
345        return Range(self.min, self.max)
346
347    def __repr__(self):
348        return "<Range %1.2f to %1.2f>" % (self.min, self.max)
349
350
351## Transform classes for wobjects
352
353
354class Transform_Base(object):
355    """ Transform_Base
356
357    Base transform object.
358    Inherited by classes for translation, scale and rotation.
359
360    """
361    pass
362
363class Transform_Translate(Transform_Base):
364    """ Transform_Translate(dx=0.0, dy=0.0, dz=0.0)
365
366    Translates the wobject.
367
368    """
369    def __init__(self, dx=0.0, dy=0.0, dz=0.0):
370        self.dx = dx
371        self.dy = dy
372        self.dz = dz
373
374class Transform_Scale(Transform_Base):
375    """ Transform_Scale(sx=1.0, sy=1.0, sz=1.0)
376
377    Scales the wobject.
378
379    """
380    def __init__(self, sx=1.0, sy=1.0, sz=1.0):
381        self.sx = sx
382        self.sy = sy
383        self.sz = sz
384
385class Transform_Rotate(Transform_Base):
386    """ Transform_Rotate( angle=0.0, ax=0, ay=0, az=1, angleInRadians=None)
387
388    Rotates the wobject. Angle is in degrees.
389    Use angleInRadians to specify the angle in radians,
390    which is then converted in degrees.
391    """
392    def __init__(self, angle=0.0, ax=0, ay=0, az=1, angleInRadians=None):
393        if angleInRadians is not None:
394            angle = angleInRadians * 180 / np.pi
395        self.angle = angle
396        self.ax = ax
397        self.ay = ay
398        self.az = az
399
400## Colour stuff
401
402# Define named colors
403colorDict = {}
404colorDict['black']  = colorDict['k'] = (0,0,0)
405colorDict['white']  = colorDict['w'] = (1,1,1)
406colorDict['red']    = colorDict['r'] = (1,0,0)
407colorDict['green']  = colorDict['g'] = (0,1,0)
408colorDict['blue']   = colorDict['b'] = (0,0,1)
409colorDict['cyan']   = colorDict['c'] = (0,1,1)
410colorDict['yellow'] = colorDict['y'] = (1,1,0)
411colorDict['magenta']= colorDict['m'] = (1,0,1)
412
413def getColor(value, descr='getColor'):
414    """ getColor(value, descr='getColor')
415
416    Make sure a value is a color. If a character is given, returns the color
417    as a tuple.
418
419    """
420    tmp = ""
421    if not value:
422        value = None
423    elif isinstance(value, basestring):
424        if value not in colorDict:
425            tmp = "string color must be one of 'rgbycmkw' !"
426        else:
427            value = colorDict[value]
428    elif isinstance(value, (list, tuple)):
429        if len(value) != 3:
430            tmp = "tuple color must be length 3!"
431        value = tuple(value)
432    else:
433        tmp = "color must be a three element tuple or a character!"
434    # error or ok?
435    if tmp:
436        raise ValueError("Error in %s: %s" % (descr, tmp) )
437    return value
438
439
440## More functions ...
441
442def isFrozen():
443    """ isFrozen
444
445    Returns whether this is a frozen application
446    (using bbfreeze or py2exe). From pyzolib.paths.py
447
448    """
449    return bool( getattr(sys, 'frozen', None) )
450
451
452# todo: cx_Freeze and friends should provide a mechanism to store
453# resources automatically ...
454def getResourceDir():
455    """ getResourceDir()
456
457    Get the directory to the visvis resources.
458
459    """
460    if isFrozen():
461        # See application_dir() in pyzolib/paths.py
462        path =  os.path.abspath(os.path.dirname(sys.path[0]))
463    else:
464        path = os.path.abspath( os.path.dirname(__file__) )
465        path = os.path.split(path)[0]
466    return os.path.join(path, 'visvisResources')
467
468
469# From pyzolib/paths.py
470import os, sys  # noqa
471def appdata_dir(appname=None, roaming=False, macAsLinux=False):
472    """ appdata_dir(appname=None, roaming=False,  macAsLinux=False)
473    Get the path to the application directory, where applications are allowed
474    to write user specific files (e.g. configurations). For non-user specific
475    data, consider using common_appdata_dir().
476    If appname is given, a subdir is appended (and created if necessary).
477    If roaming is True, will prefer a roaming directory (Windows Vista/7).
478    If macAsLinux is True, will return the Linux-like location on Mac.
479    """
480
481    # Define default user directory
482    userDir = os.path.expanduser('~')
483
484    # Get system app data dir
485    path = None
486    if sys.platform.startswith('win'):
487        path1, path2 = os.getenv('LOCALAPPDATA'), os.getenv('APPDATA')
488        path = (path2 or path1) if roaming else (path1 or path2)
489    elif sys.platform.startswith('darwin') and not macAsLinux:
490        path = os.path.join(userDir, 'Library', 'Application Support')
491    # On Linux and as fallback
492    if not (path and os.path.isdir(path)):
493        path = userDir
494
495    # Maybe we should store things local to the executable (in case of a
496    # portable distro or a frozen application that wants to be portable)
497    prefix = sys.prefix
498    if getattr(sys, 'frozen', None): # See application_dir() function
499        prefix = os.path.abspath(os.path.dirname(sys.path[0]))
500    for reldir in ('settings', '../settings'):
501        localpath = os.path.abspath(os.path.join(prefix, reldir))
502        if os.path.isdir(localpath):
503            try:
504                open(os.path.join(localpath, 'test.write'), 'wb').close()
505                os.remove(os.path.join(localpath, 'test.write'))
506            except IOError:
507                pass # We cannot write in this directory
508            else:
509                path = localpath
510                break
511
512    # Get path specific for this app
513    if appname:
514        if path == userDir:
515            appname = '.' + appname.lstrip('.') # Make it a hidden directory
516        path = os.path.join(path, appname)
517        if not os.path.isdir(path):
518            os.mkdir(path)
519
520    # Done
521    return path
522
523
524class Settings(object):
525    """ Global settings object.
526
527    This object can be used to set the visvis settings in an easy way
528    from the Python interpreter.
529
530    The settings are stored in a file in the user directory (the filename
531    can be obtained using the _fname attribute).
532
533    Note that some settings require visvis to restart.
534
535    """
536    def __init__(self):
537
538        # Define settings file name
539        self._fname = os.path.join(appdata_dir('visvis'), 'config.ssdf')
540
541        # Init settings
542        self._s = ssdf.new()
543
544        # Load settings if we can
545        if os.path.exists(self._fname):
546            try:
547                self._s = ssdf.load(self._fname)
548            except Exception:
549                pass
550
551        # Update any missing settings to their defaults
552        for key in dir(self):
553            if key.startswith('_'):
554                continue
555            self._s[key] = getattr(self, key)
556
557        # Save now so the config file contains all settings
558        self._Save()
559
560    def _Save(self):
561        try:
562            ssdf.save(self._fname, self._s)
563        except IOError:
564            pass # Maybe an installed frozen application (no write-rights)
565
566    @PropertyForSettings
567    def preferredBackend():
568        """ The preferred backend GUI toolkit to use
569        ('pyside', 'pyqt4', 'wx', 'gtk', 'fltk').
570          * If the selected backend is not available, another one is selected.
571          * If preferAlreadyLoadedBackend is True, will prefer a backend that
572            is already imported.
573          * Requires a restart.
574        """
575        def fget(self, key):
576            if key in self._s:
577                return self._s[key]
578            else:
579                return 'pyside'  # Default value
580        def fset(self, key, value):
581            # Note that 'qt4' in valid for backward compatibility
582            value = value.lower()
583            if value in ['pyside', 'qt4', 'pyqt4', 'wx', 'gtk', 'fltk', 'foo']:
584                self._s[key] = value
585            else:
586                raise ValueError('Invalid backend specified.')
587        return locals()
588
589    @PropertyForSettings
590    def preferAlreadyLoadedBackend():
591        """ Bool that indicates whether visvis should prefer an already
592        imported backend (even if it's not the preferredBackend). This is
593        usefull in interactive session in for example IEP, Spyder or IPython.
594        Requires a restart.
595        """
596        def fget(self, key):
597            if key in self._s:
598                return bool(self._s[key])
599            else:
600                return True  # Default value
601        def fset(self, key, value):
602            self._s[key] = bool(value)
603        return locals()
604
605#     @PropertyForSettings
606#     def defaultInterpolation2D():
607#         """ The default interpolation mode for 2D textures (bool). If False
608#         the pixels are well visible, if True the image looks smoother.
609#         Default is False.
610#         """
611#         def fget(self, key):
612#             if key in self._s:
613#                 return bool(self._s[key])
614#             else:
615#                 return False  # Default value
616#         def fset(self, key, value):
617#             self._s[key] = bool(value)
618#         return locals()
619
620    @PropertyForSettings
621    def figureSize():
622        """ The initial size for figure windows. Should be a 2-element
623        tuple or list. Default is (560, 420).
624        """
625        def fget(self, key):
626            if key in self._s:
627                return tuple(self._s[key])
628            else:
629                return (560, 420)  # Default value
630        def fset(self, key, value):
631            if not isinstance(value, (list,tuple)) or len(value) != 2:
632                raise ValueError('Figure size must be a 2-element list or tuple.')
633            value = [int(i) for i in value]
634            self._s[key] = tuple(value)
635        return locals()
636
637    @PropertyForSettings
638    def volshowPreference():
639        """ Whether the volshow() function prefers the volshow2() or volshow3()
640        function. By default visvis prefers volshow3(), but falls back to
641        volshow2() when the OpenGl version is not high enough. Some OpenGl
642        drivers, however, support volume rendering only in ultra-slow software
643        mode (seen on ATI). In this case, or if you simply prefer volshow2()
644        you can set this setting to '2'.
645        """
646        def fget(self, key):
647            if key in self._s:
648                return self._s[key]
649            else:
650                return 3  # Default value
651        def fset(self, key, value):
652            if value not in [2,3]:
653                raise ValueError('volshowPreference must be 2 or 3.')
654            self._s[key] = int(value)
655        return locals()
656
657    @PropertyForSettings
658    def defaultFontName():
659        """ The default font to use. Can be 'mono', 'sans' or 'serif', with
660        'sans' being the default.
661        """
662        def fget(self, key):
663            if key in self._s:
664                return self._s[key]
665            else:
666                return 'sans'  # Default value
667        def fset(self, key, value):
668            value = value.lower()
669            if value not in ['mono', 'sans', 'serif', 'humor']:
670                raise ValueError("defaultFontName must be 'mono', 'sans', 'serif' or 'humor'.")
671            self._s[key] = value
672        return locals()
673
674    @PropertyForSettings
675    def defaultRelativeFontSize():
676        """ The default relativeFontSize of new figures. The relativeFontSize
677        property can be used to scale all text simultenously, as well as
678        increase/decrease the margins availavle for the text. The default is
679        1.0.
680        """
681        def fget(self, key):
682            if key in self._s:
683                return self._s[key]
684            else:
685                return 1.0
686        def fset(self, key, value):
687            self._s[key] = float(value)
688        return locals()
689
690    # todo: more? maybe axes bgcolor and axisColor?
691
692# Create settings instance, this is what gets inserted in the visvis namespace
693settings = Settings()
694
695# Set __file__ absolute when loading
696__file__ = os.path.abspath(__file__)
697