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