Преглед изворни кода

wxNviz: simple animation implemented

git-svn-id: https://svn.osgeo.org/grass/grass/trunk@49274 15284696-431f-4ddb-bdfa-cd5b030d7da7
Anna Petrášová пре 13 година
родитељ
комит
8d2c6d33a1

+ 1 - 1
gui/wxpython/gui_modules/mapdisp.py

@@ -649,7 +649,7 @@ class MapFrame(MapFrameBase):
             # add Nviz notebookpage
             # add Nviz notebookpage
             self._layerManager.AddNvizTools()
             self._layerManager.AddNvizTools()
             self.MapWindow3D.ResetViewHistory()
             self.MapWindow3D.ResetViewHistory()
-            for page in ('view', 'light', 'fringe', 'constant', 'cplane'):
+            for page in ('view', 'light', 'fringe', 'constant', 'cplane', 'animation'):
                 self._layerManager.nviz.UpdatePage(page)
                 self._layerManager.nviz.UpdatePage(page)
                 
                 
         self.MapWindow3D.overlays = self.MapWindow2D.overlays
         self.MapWindow3D.overlays = self.MapWindow2D.overlays

+ 208 - 0
gui/wxpython/gui_modules/nviz_animation.py

@@ -0,0 +1,208 @@
+"""!
+@package nviz_animation.py
+
+@brief Nviz (3D view) animation
+
+Classes:
+ - Animation
+
+(C) 2008-2011 by the GRASS Development Team
+
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS
+for details.
+
+@author Anna Kratochvilova <kratochanna gmail.com> 
+"""
+import os
+import copy
+
+import wx
+from wx.lib.newevent import NewEvent
+
+wxAnimationFinished, EVT_ANIM_FIN = NewEvent()
+wxAnimationUpdateIndex, EVT_ANIM_UPDATE_IDX = NewEvent()
+
+
+class Animation:
+    """!Class represents animation as a sequence of states (views).
+    It enables to record, replay the sequence and finally generate
+    all image files. Recording and replaying is based on timer events.
+    There is no frame interpolation like in the Tcl/Tk based Nviz.
+    """
+    def __init__(self, mapWindow, timer):
+        """!Animation constructor
+        
+        @param mapWindow glWindow where rendering takes place
+        @param timer timer for recording and replaying
+        """
+        
+        self.animationList = []         # view states
+        self.timer = timer
+        self.mapWindow = mapWindow
+        self.actions = {'record': self.Record,
+                        'play': self.Play}
+        self.formats = ['ppm', 'tif']   # currently supported formats
+        self.mode = 'record'            # current mode (record, play, save)
+        self.paused = False             # recording/replaying paused
+        self.currentFrame = 0           # index of current frame
+        self.fps = 24 # user settings   # Frames per second
+        
+        self.stopSaving = False         # stop during saving images
+        self.animationSaved = False     # current animation saved or not
+        
+    def Start(self):
+        """!Start recording/playing"""
+        self.timer.Start(self.GetInterval())
+        
+    def Pause(self):
+        """!Pause recording/playing"""
+        self.timer.Stop()
+        
+    def Stop(self):
+        """!Stop recording/playing"""
+        self.timer.Stop()
+        self.PostFinishedEvent()
+        
+    def Update(self):
+        """!Record/play next view state (on timer event)"""
+        self.actions[self.mode]()
+    
+    def Record(self):
+        """!Record new view state"""
+        self.animationList.append({'view' : copy.deepcopy(self.mapWindow.view),
+                                   'iview': copy.deepcopy(self.mapWindow.iview)})
+        self.currentFrame += 1
+        self.PostUpdateIndexEvent(index = self.currentFrame)
+        self.animationSaved = False
+        
+    def Play(self):
+        """!Render next frame"""
+        if not self.animationList:
+            self.Stop()
+            return
+        try:
+            self.IterAnimation()
+        except IndexError:
+            # no more frames
+            self.Stop()
+            
+    def IterAnimation(self):
+        params = self.animationList[self.currentFrame]
+        self.UpdateView(params)
+        self.currentFrame += 1
+        
+        self.PostUpdateIndexEvent(index = self.currentFrame)
+        
+    def UpdateView(self, params):
+        """!Update view data in map window and render"""
+        toolWin = self.mapWindow.GetToolWin()
+        toolWin.UpdateState(view = params['view'], iview = params['iview'])
+        
+        self.mapWindow.UpdateView()
+        
+        self.mapWindow.render['quick'] = True
+        self.mapWindow.Refresh(False)
+        
+    def IsRunning(self):
+        """!Test if timer is running"""
+        return self.timer.IsRunning()
+        
+    def SetMode(self, mode):
+        """!Start animation mode
+        
+        @param mode animation mode (record, play, save)
+        """
+        self.mode = mode
+        
+    def GetMode(self):
+        """!Get animation mode (record, play, save)"""
+        return self.mode
+        
+    def IsPaused(self):
+        """!Test if animation is paused"""
+        return self.paused
+        
+    def SetPause(self, pause):
+        self.paused = pause
+        
+    def Exists(self):
+        """!Returns if an animation has been recorded"""
+        return bool(self.animationList)
+        
+    def GetFrameCount(self):
+        """!Return number of recorded frames"""
+        return len(self.animationList)
+        
+    def Clear(self):
+        """!Clear all records"""
+        self.animationList = []
+        self.currentFrame = 0
+        
+    def GoToFrame(self, index):
+        """!Render frame of given index"""
+        if index >= len(self.animationList):
+            return
+            
+        self.currentFrame = index
+        params = self.animationList[self.currentFrame]
+        self.UpdateView(params)
+        
+    def PostFinishedEvent(self):
+        """!Animation ends"""
+        toolWin = self.mapWindow.GetToolWin()
+        event = wxAnimationFinished(mode = self.mode)
+        wx.PostEvent(toolWin, event)
+        
+    def PostUpdateIndexEvent(self, index):
+        """!Frame index changed, update tool window"""
+        toolWin = self.mapWindow.GetToolWin()
+        event = wxAnimationUpdateIndex(index = index, mode = self.mode)
+        wx.PostEvent(toolWin, event)
+        
+    def StopSaving(self):
+        """!Abort image files generation"""
+        self.stopSaving = True
+        
+    def IsSaved(self):
+        """"!Test if animation has been saved (to images)"""
+        return self.animationSaved
+        
+    def SaveAnimationFile(self, path, prefix, format):
+        """!Generate image files
+        
+        @param path path to direcory
+        @param prefix file prefix
+        @param format index of image file format
+        """
+        w, h = self.mapWindow.GetClientSizeTuple()
+        toolWin = self.mapWindow.GetToolWin()
+        
+        self.currentFrame = 0
+        self.mode = 'save'
+        for params in self.animationList:
+            if not self.stopSaving:
+                self.UpdateView(params)
+                filename = prefix + "_" + str(self.currentFrame) + '.' + self.formats[format]
+                filepath = os.path.join(path, filename)
+                self.mapWindow.SaveToFile(FileName = filepath, FileType = self.formats[format],
+                                                  width = w, height = h)
+                self.currentFrame += 1
+                
+                wx.Yield()
+                toolWin.UpdateFrameIndex(index = self.currentFrame, goToFrame = False)
+            else:
+                self.stopSaving = False
+                break
+        self.animationSaved = True
+        self.PostFinishedEvent()
+
+    def SetFPS(self, fps):
+        """!Set Frames Per Second value
+        @param fps frames per second
+        """
+        self.fps = fps
+    
+    def GetInterval(self):
+        """!Return timer interval in ms"""
+        return 1000. / self.fps

+ 87 - 33
gui/wxpython/gui_modules/nviz_mapdisp.py

@@ -42,6 +42,7 @@ from mapdisp_window import MapWindow
 from goutput        import wxCmdOutput
 from goutput        import wxCmdOutput
 from preferences    import globalSettings as UserSettings
 from preferences    import globalSettings as UserSettings
 from workspace      import Nviz as NvizDefault
 from workspace      import Nviz as NvizDefault
+from nviz_animation import Animation
 
 
 import wxnviz
 import wxnviz
 
 
@@ -119,6 +120,8 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
         self.textdict = {}
         self.textdict = {}
         self.dragid = -1
         self.dragid = -1
         self.hitradius = 5
         self.hitradius = 5
+        # layer manager toolwindow
+        self.toolWin = None        
     
     
         if self.lmgr:
         if self.lmgr:
             self.log = self.lmgr.goutput
             self.log = self.lmgr.goutput
@@ -158,6 +161,9 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
         
         
         # timer for flythrough
         # timer for flythrough
         self.timerFly = wx.Timer(self, id = wx.NewId())
         self.timerFly = wx.Timer(self, id = wx.NewId())
+        # timer for animations
+        self.timerAnim = wx.Timer(self, id = wx.NewId())
+        self.animation = Animation(mapWindow = self, timer = self.timerAnim)        
         
         
         self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
         self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
         self.Bind(wx.EVT_SIZE,             self.OnSize)
         self.Bind(wx.EVT_SIZE,             self.OnSize)
@@ -165,10 +171,11 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
         self._bindMouseEvents()
         self._bindMouseEvents()
         
         
         self.Bind(EVT_UPDATE_PROP,   self.UpdateMapObjProperties)
         self.Bind(EVT_UPDATE_PROP,   self.UpdateMapObjProperties)
-        self.Bind(EVT_UPDATE_VIEW,   self.UpdateView)
+        self.Bind(EVT_UPDATE_VIEW,   self.OnUpdateView)
         self.Bind(EVT_UPDATE_LIGHT,  self.UpdateLight)
         self.Bind(EVT_UPDATE_LIGHT,  self.UpdateLight)
         self.Bind(EVT_UPDATE_CPLANE, self.UpdateCPlane)
         self.Bind(EVT_UPDATE_CPLANE, self.UpdateCPlane)
         
         
+        self.Bind(wx.EVT_TIMER, self.OnTimerAnim, self.timerAnim)
         self.Bind(wx.EVT_TIMER, self.OnTimerFly, self.timerFly)
         self.Bind(wx.EVT_TIMER, self.OnTimerFly, self.timerFly)
         self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
         self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
         self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
         self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
@@ -207,6 +214,7 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
         self.ComputeFlyValues(mx = mx, my = my)
         self.ComputeFlyValues(mx = mx, my = my)
         self._display.FlyThrough(flyInfo = self.fly['value'], mode = self.fly['mode'],
         self._display.FlyThrough(flyInfo = self.fly['value'], mode = self.fly['mode'],
                                  exagInfo = self.fly['exag'])
                                  exagInfo = self.fly['exag'])
+        self.ChangeInnerView()                                 
         self.render['quick'] = True
         self.render['quick'] = True
         self.Refresh(False)
         self.Refresh(False)
         
         
@@ -262,6 +270,7 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
     
     
     def __del__(self):
     def __del__(self):
         """!Stop timers if running, unload data"""
         """!Stop timers if running, unload data"""
+        self.StopTimer(self.timerAnim)
         self.StopTimer(self.timerFly)
         self.StopTimer(self.timerFly)
         self.UnloadDataLayers(force = True)
         self.UnloadDataLayers(force = True)
     
     
@@ -280,8 +289,17 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
             cplane = copy.deepcopy(UserSettings.Get(group = 'nviz', key = 'cplane'))
             cplane = copy.deepcopy(UserSettings.Get(group = 'nviz', key = 'cplane'))
             cplane['on'] = False
             cplane['on'] = False
             self.cplanes.append(cplane)
             self.cplanes.append(cplane)
+        
+    def SetToolWin(self, toolWin):
+        """!Sets reference to nviz toolwindow in layer manager"""
+        self.toolWin = toolWin
+        
+    def GetToolWin(self):
+        """!Returns reference to nviz toolwindow in layer manager"""
+        return self.toolWin
             
             
     def OnClose(self, event):
     def OnClose(self, event):
+        self.StopTimer(self.timerAnim)
         self.StopTimer(self.timerFly)
         self.StopTimer(self.timerFly)
         # cleanup when window actually closes (on quit) and not just is hidden
         # cleanup when window actually closes (on quit) and not just is hidden
         self.UnloadDataLayers(force = True)
         self.UnloadDataLayers(force = True)
@@ -298,13 +316,14 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
             self.SetCurrent()
             self.SetCurrent()
             self._display.ResizeWindow(size.width,
             self._display.ResizeWindow(size.width,
                                        size.height)
                                        size.height)
-        self.size = size
-        
-        # reposition checkbox in statusbar
-        self.parent.StatusbarReposition()
         
         
-        # update statusbar
-        self.parent.StatusbarUpdate()
+            # reposition checkbox in statusbar
+            self.parent.StatusbarReposition()
+            
+            # update statusbar
+            self.parent.StatusbarUpdate()
+            
+        self.size = size
         
         
         event.Skip()
         event.Skip()
        
        
@@ -334,6 +353,7 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
                 self.lmgr.nviz.UpdatePage('light')
                 self.lmgr.nviz.UpdatePage('light')
                 self.lmgr.nviz.UpdatePage('cplane')
                 self.lmgr.nviz.UpdatePage('cplane')
                 self.lmgr.nviz.UpdatePage('decoration')
                 self.lmgr.nviz.UpdatePage('decoration')
+                self.lmgr.nviz.UpdatePage('animation')
                 layer = self.GetSelectedLayer()
                 layer = self.GetSelectedLayer()
                 if layer:
                 if layer:
                     if layer.type ==  'raster':
                     if layer.type ==  'raster':
@@ -492,6 +512,12 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
                 return texture.id
                 return texture.id
         return -1
         return -1
         
         
+    def OnTimerAnim(self, event):
+         self.animation.Update()
+         
+    def GetAnimation(self):
+         return self.animation
+         
     def OnKeyDown(self, event):
     def OnKeyDown(self, event):
         """!Key was pressed.
         """!Key was pressed.
         
         
@@ -752,6 +778,12 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
             if self.fly['mouseControl']:
             if self.fly['mouseControl']:
                 self.StopTimer(self.timerFly)
                 self.StopTimer(self.timerFly)
                 self.fly['mouseControl'] = None
                 self.fly['mouseControl'] = None
+                #for key in self.iview['dir'].keys():
+                    #self.iview[''][key] = -1
+                # this causes sudden change, but it should be there
+                #if hasattr(self.lmgr, "nviz"):
+                    #self.lmgr.nviz.UpdateSettings()
+                    
                 self.render['quick'] = False
                 self.render['quick'] = False
                 self.Refresh(False)
                 self.Refresh(False)
             
             
@@ -788,8 +820,7 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
             focus['z'] -= dz
             focus['z'] -= dz
             
             
             #update properties
             #update properties
-            evt = wxUpdateView(zExag = False)
-            wx.PostEvent(self, evt)
+            self.PostViewEvent()
             
             
             self.mouse['tmp'] = event.GetPositionTuple()
             self.mouse['tmp'] = event.GetPositionTuple()
             self.render['quick'] = True
             self.render['quick'] = True
@@ -928,8 +959,7 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
         focus['x'], focus['y'] = e, n
         focus['x'], focus['y'] = e, n
         self.saveHistory = True
         self.saveHistory = True
         #update properties
         #update properties
-        evt = wxUpdateView(zExag = False)
-        wx.PostEvent(self, evt)
+        self.PostViewEvent()
         
         
         self.render['quick'] = False
         self.render['quick'] = False
         self.Refresh(False)
         self.Refresh(False)
@@ -982,37 +1012,62 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
             self.log.WriteLog(_("No point on surface"))
             self.log.WriteLog(_("No point on surface"))
             self.log.WriteCmdLog('-' * 80)
             self.log.WriteCmdLog('-' * 80)
     
     
+    def PostViewEvent(self, zExag = False):
+        """!Change view settings"""
+        event = wxUpdateView(zExag = zExag)
+        wx.PostEvent(self, event)
+        
     def OnQueryVector(self, event):
     def OnQueryVector(self, event):
         """!Query vector on given position"""
         """!Query vector on given position"""
         self.parent.QueryVector(*event.GetPosition())
         self.parent.QueryVector(*event.GetPosition())
-        
-    def UpdateView(self, event):
+
+    def ChangeInnerView(self):
+        """!Get current viewdir and viewpoint and set view"""
+        view = self.view
+        iview = self.iview
+        (view['position']['x'], view['position']['y'],
+        iview['height']['value']) = self._display.GetViewpointPosition()
+        for key, val in zip(('x', 'y', 'z'), self._display.GetViewdir()):
+            iview['dir'][key] = val
+        
+        iview['dir']['use'] = True
+        
+    def OnUpdateView(self, event):
         """!Change view settings"""
         """!Change view settings"""
-        data = self.view
-        
-        ## multiple z-exag value from slider by original value computed in ogsf so it scales
-        ## correctly with maps of different height ranges        
-        if event and event.zExag and 'value' in data['z-exag']:
-            self._display.SetZExag(self.iview['z-exag']['original'] * data['z-exag']['value'])
+        if event:
+                self.UpdateView(zexag = event.zExag)
+                
+        self.saveHistory = True
+        if event:
+            event.Skip()
+            
             
             
-        self._display.SetView(data['position']['x'], data['position']['y'],
-                              self.iview['height']['value'],
-                              data['persp']['value'],
-                              data['twist']['value'])
+    def UpdateView(self, zexag = False):
+        """!Change view settings"""
+        view = self.view
+        iview = self.iview
+        if zexag and 'value' in view['z-exag']:
+            self._display.SetZExag(self.iview['z-exag']['original'] * view['z-exag']['value'])
+        
         
         
-        if self.iview['focus']['x'] != -1:
+        self._display.SetView(view['position']['x'], view['position']['y'],
+                              iview['height']['value'],
+                              view['persp']['value'],
+                              view['twist']['value'])
+        
+        if iview['dir']['use']:
+            self._display.SetViewdir(iview['dir']['x'], iview['dir']['y'], iview['dir']['z'])
+        
+        elif iview['focus']['x'] != -1:
             self._display.SetFocus(self.iview['focus']['x'], self.iview['focus']['y'],
             self._display.SetFocus(self.iview['focus']['x'], self.iview['focus']['y'],
                                    self.iview['focus']['z'])
                                    self.iview['focus']['z'])
-        if 'rotation' in self.iview:
-            if self.iview['rotation']:
-                self._display.SetRotationMatrix(self.iview['rotation'])
+                                       
+        if 'rotation' in iview:
+            if iview['rotation']:
+                self._display.SetRotationMatrix(iview['rotation'])
             else:
             else:
                 self._display.ResetRotation()
                 self._display.ResetRotation()
         
         
-        self.saveHistory = True
-        if event:
-            event.Skip()
-
     def UpdateLight(self, event):
     def UpdateLight(self, event):
         """!Change light settings"""
         """!Change light settings"""
         data = self.light
         data = self.light
@@ -1694,8 +1749,7 @@ class GLWindow(MapWindow, glcanvas.GLCanvas):
         focus = self.iview['focus']
         focus = self.iview['focus']
         focus['x'], focus['y'], focus['z'] = self._display.GetFocus()
         focus['x'], focus['y'], focus['z'] = self._display.GetFocus()
         
         
-        event = wxUpdateView(zExag = False)
-        wx.PostEvent(self, event)
+        self.PostViewEvent()
         
         
     def UpdateMapObjProperties(self, event):
     def UpdateMapObjProperties(self, event):
         """!Generic method to update data layer properties"""
         """!Generic method to update data layer properties"""

+ 446 - 13
gui/wxpython/gui_modules/nviz_tools.py

@@ -4,6 +4,11 @@
 @brief Nviz (3D view) tools window
 @brief Nviz (3D view) tools window
 
 
 Classes:
 Classes:
+ - ScrolledPanel
+ - NTCValidator
+ - NumTextCtrl
+ - FloatSlider
+ - SymbolButton
  - NvizToolWindow
  - NvizToolWindow
  - PositionWindow
  - PositionWindow
  - ViewPositionWindow
  - ViewPositionWindow
@@ -27,8 +32,15 @@ import types
 import string
 import string
 
 
 import wx
 import wx
-import wx.lib.colourselect as csel
+import wx.lib.colourselect  as csel
 import wx.lib.scrolledpanel as SP
 import wx.lib.scrolledpanel as SP
+import wx.lib.filebrowsebutton as filebrowse
+
+try:
+    from wx.lib.buttons import ThemedGenBitmapTextButton as BitmapTextButton
+except ImportError: # not sure about TGBTButton version
+    from wx.lib.buttons import GenBitmapTextButton as BitmapTextButton
+    
 try:
 try:
     import wx.lib.agw.flatnotebook as FN
     import wx.lib.agw.flatnotebook as FN
 except ImportError:
 except ImportError:
@@ -50,12 +62,14 @@ import colorrules
 from preferences import globalSettings as UserSettings
 from preferences import globalSettings as UserSettings
 from gselect import VectorDBInfo
 from gselect import VectorDBInfo
 
 
+from nviz_animation import EVT_ANIM_FIN, EVT_ANIM_UPDATE_IDX
 try:
 try:
     from nviz_mapdisp import wxUpdateView, wxUpdateLight, wxUpdateProperties,\
     from nviz_mapdisp import wxUpdateView, wxUpdateLight, wxUpdateProperties,\
                             wxUpdateCPlane
                             wxUpdateCPlane
     import wxnviz
     import wxnviz
 except ImportError:
 except ImportError:
     pass
     pass
+
 from debug import Debug
 from debug import Debug
 
 
 
 
@@ -154,11 +168,67 @@ class FloatSlider(wx.Slider):
         Debug.msg(4, "FloatSlider.GetValue(): value = %f" % (val/self.coef))
         Debug.msg(4, "FloatSlider.GetValue(): value = %f" % (val/self.coef))
         return val/self.coef
         return val/self.coef
         
         
+        
+class SymbolButton(BitmapTextButton):
+    """!Button with symbol and label."""
+    def __init__(self, parent, usage, label, **kwargs):
+        """!Constructor
+        
+        @param parent parent (usually wx.Panel)
+        @param usage determines usage and picture
+        @param label displayed label
+        """
+        BitmapTextButton.__init__(self, parent = parent, label = " " + label, **kwargs)
+        
+        size = (15, 15)
+        buffer = wx.EmptyBitmap(*size)
+        dc = wx.MemoryDC()
+        dc.SelectObject(buffer)
+        maskColor = wx.Color(255, 255, 255)
+        dc.SetBrush(wx.Brush(maskColor))
+        dc.Clear()
+        
+        if usage == 'record':
+            self.DrawRecord(dc, size)
+        elif usage == 'stop':
+            self.DrawStop(dc, size)
+        elif usage == 'play':
+            self.DrawPlay(dc, size)
+        elif usage == 'pause':
+            self.DrawPause(dc, size)
+
+        buffer.SetMaskColour(maskColor)
+        self.SetBitmapLabel(buffer)
+        dc.SelectObject(wx.NullBitmap)
+        
+    def DrawRecord(self, dc, size):
+        """!Draw record symbol"""
+        dc.SetBrush(wx.Brush(wx.Color(255, 0, 0)))
+        dc.DrawCircle(size[0]/2, size[1] / 2, size[0] / 2)
+        
+    def DrawStop(self, dc, size):
+        """!Draw stop symbol"""
+        dc.SetBrush(wx.Brush(wx.Color(50, 50, 50)))
+        dc.DrawRectangle(0, 0, size[0], size[1])
+        
+    def DrawPlay(self, dc, size):
+        """!Draw play symbol"""
+        dc.SetBrush(wx.Brush(wx.Color(0, 255, 0)))
+        points = (wx.Point(0, 0), wx.Point(0, size[1]), wx.Point(size[0], size[1] / 2))
+        dc.DrawPolygon(points)
+        
+    def DrawPause(self, dc, size):
+        """!Draw pause symbol"""
+        dc.SetBrush(wx.Brush(wx.Color(50, 50, 50)))
+        dc.DrawRectangle(0, 0, 2 * size[0] / 5, size[1])
+        dc.DrawRectangle(3 * size[0] / 5, 0, 2 * size[0] / 5, size[1])
+        
+        
 class NvizToolWindow(FN.FlatNotebook):
 class NvizToolWindow(FN.FlatNotebook):
     """!Nviz (3D view) tools panel
     """!Nviz (3D view) tools panel
     """
     """
     def __init__(self, parent, display, id = wx.ID_ANY,
     def __init__(self, parent, display, id = wx.ID_ANY,
-                 style = globalvar.FNPageStyle|FN.FNB_NO_X_BUTTON|FN.FNB_NO_NAV_BUTTONS,
+                 style = globalvar.FNPageStyle|FN.FNB_NO_X_BUTTON,
                  **kwargs):
                  **kwargs):
         Debug.msg(5, "NvizToolWindow.__init__()")
         Debug.msg(5, "NvizToolWindow.__init__()")
         self.parent     = parent # GMFrame
         self.parent     = parent # GMFrame
@@ -191,8 +261,14 @@ class NvizToolWindow(FN.FlatNotebook):
         # analysis page
         # analysis page
         self.AddPage(page = self._createAnalysisPage(),
         self.AddPage(page = self._createAnalysisPage(),
                      text = " %s " % _("Analysis"))
                      text = " %s " % _("Analysis"))
+        # view page
+        self.AddPage(page = self._createAnimationPage(),
+                     text = " %s " % _("Animation"))
         
         
         self.UpdateSettings()
         self.UpdateSettings()
+        
+        self.mapWindow.SetToolWin(self)
+        
         self.pageChanging = False
         self.pageChanging = False
         self.vetoGSelectEvt = False #when setting map, event is invoked
         self.vetoGSelectEvt = False #when setting map, event is invoked
         self.mapWindow.render['quick'] = False
         self.mapWindow.render['quick'] = False
@@ -202,6 +278,9 @@ class NvizToolWindow(FN.FlatNotebook):
         self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
         self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
         self.Bind(wx.EVT_SIZE, self.OnSize)
         self.Bind(wx.EVT_SIZE, self.OnSize)
         
         
+        self.Bind(EVT_ANIM_FIN, self.OnAnimationFinished)
+        self.Bind(EVT_ANIM_UPDATE_IDX, self.OnAnimationUpdateIndex)
+        
         Debug.msg(3, "NvizToolWindow.__init__()")
         Debug.msg(3, "NvizToolWindow.__init__()")
         
         
         self.Update()
         self.Update()
@@ -444,7 +523,154 @@ class NvizToolWindow(FN.FlatNotebook):
         panel.SetSizer(pageSizer)
         panel.SetSizer(pageSizer)
         
         
         return panel
         return panel
-
+        
+    def _createAnimationPage(self):
+        """!Create view settings page"""
+        panel = SP.ScrolledPanel(parent = self, id = wx.ID_ANY)
+        panel.SetupScrolling(scroll_x = False)
+        self.page['animation'] = { 'id' : 0,
+                                   'notebook' : self.GetId()}
+        
+        pageSizer = wx.BoxSizer(wx.VERTICAL)
+        box = wx.StaticBox (parent = panel, id = wx.ID_ANY,
+                            label = " %s " % (_("Animation")))
+        boxSizer = wx.StaticBoxSizer(box, wx.VERTICAL)
+        hSizer = wx.BoxSizer(wx.HORIZONTAL)
+        
+        self.win['anim'] = {}
+        # animation help text
+        help = wx.StaticText(parent = panel, id = wx.ID_ANY,
+                             label = _("Press 'Record' button and start changing the view. "
+                                       "It is recommended to use fly-through mode "
+                                       "(Map Display toolbar) to achieve smooth motion."))
+        self.win['anim']['help'] = help.GetId()
+        hSizer.Add(item = help, proportion = 0)
+        boxSizer.Add(item = hSizer, proportion = 1,
+                     flag = wx.ALL | wx.EXPAND, border = 5)
+                     
+        # animation controls
+        hSizer = wx.BoxSizer(wx.HORIZONTAL)
+        record = SymbolButton(parent = panel, id = wx.ID_ANY,
+                                    usage = "record", label = _("Record"))
+        play = SymbolButton(parent = panel, id = wx.ID_ANY,
+                                  usage = "play", label = _("Play"))
+        pause = SymbolButton(parent = panel, id = wx.ID_ANY,
+                                   usage = "pause", label = _("Pause"))
+        stop = SymbolButton(parent = panel, id = wx.ID_ANY,
+                                  usage = "stop", label = _("Stop"))
+        
+        self.win['anim']['record'] = record.GetId()
+        self.win['anim']['play'] = play.GetId()
+        self.win['anim']['pause'] = pause.GetId()
+        self.win['anim']['stop'] = stop.GetId()
+                            
+        self._createControl(panel, data = self.win['anim'], name = 'frameIndex',
+                            range = (0, 1), floatSlider = False,
+                            bind = (self.OnFrameIndex, None, self.OnFrameIndexText))
+        frameSlider = self.FindWindowById(self.win['anim']['frameIndex']['slider'])
+        frameText = self.FindWindowById(self.win['anim']['frameIndex']['text'])
+        infoLabel = wx.StaticText(parent = panel, id = wx.ID_ANY, label = _("Total number of frames :"))
+        info = wx.StaticText(parent = panel, id = wx.ID_ANY)
+        self.win['anim']['info'] = info.GetId()
+        
+        fpsLabel = wx.StaticText(parent = panel, id = wx.ID_ANY, label = _("Frame rate (FPS):"))
+        fps = wx.SpinCtrl(parent = panel, id = wx.ID_ANY, size = (65, -1),
+                           initial = UserSettings.Get(group = 'nviz', key = 'animation', subkey = 'fps'),
+                           min = 1,
+                           max = 50)
+        self.win['anim']['fps'] = fps.GetId()
+        fps.SetToolTipString(_("Frames are recorded with given frequency (FPS). "))
+                            
+        record.Bind(wx.EVT_BUTTON, self.OnRecord)
+        play.Bind(wx.EVT_BUTTON, self.OnPlay)
+        stop.Bind(wx.EVT_BUTTON, self.OnStop)
+        pause.Bind(wx.EVT_BUTTON, self.OnPause)
+        fps.Bind(wx.EVT_SPINCTRL, self.OnFPS)
+        
+        hSizer.Add(item = record, proportion = 0)
+        hSizer.Add(item = play, proportion = 0)
+        hSizer.Add(item = pause, proportion = 0)
+        hSizer.Add(item = stop, proportion = 0)
+        boxSizer.Add(item = hSizer, proportion = 0,
+                     flag = wx.ALL | wx.EXPAND, border = 3)
+        
+        sliderBox = wx.BoxSizer(wx.HORIZONTAL)
+        sliderBox.Add(item = frameSlider, proportion = 1, border = 5, flag = wx.EXPAND | wx.RIGHT)
+        sliderBox.Add(item = frameText, proportion = 0, border = 5, flag = wx.EXPAND| wx.RIGHT | wx.LEFT)
+        boxSizer.Add(item = sliderBox, proportion = 0, flag = wx.EXPAND)
+        
+        # total number of frames
+        hSizer = wx.BoxSizer(wx.HORIZONTAL)
+        hSizer.Add(item = infoLabel, proportion = 0, flag = wx.RIGHT, border = 5)
+        hSizer.Add(item = info, proportion = 0, flag = wx.LEFT, border = 5)
+        
+        boxSizer.Add(item = hSizer, proportion = 0,
+                     flag = wx.ALL | wx.EXPAND, border = 5)
+                     
+        # frames per second
+        hSizer = wx.BoxSizer(wx.HORIZONTAL)
+        hSizer.Add(item = fpsLabel, proportion = 0, flag = wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, border = 5)
+        hSizer.Add(item = fps, proportion = 0, flag = wx.LEFT | wx.ALIGN_CENTER_VERTICAL, border = 5)
+        
+        boxSizer.Add(item = hSizer, proportion = 0,
+                     flag = wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, border = 5)
+        pageSizer.Add(item = boxSizer, proportion = 0,
+                      flag = wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM,
+                      border = 3)
+                      
+        # save animation
+        self.win['anim']['save'] = {}
+        self.win['anim']['save']['image'] = {}
+        box = wx.StaticBox (parent = panel, id = wx.ID_ANY,
+                            label = " %s " % (_("Save image sequence")))
+        boxSizer = wx.StaticBoxSizer(box, wx.VERTICAL)
+        vSizer = wx.BoxSizer(wx.VERTICAL)
+        gridSizer = wx.GridBagSizer(vgap = 5, hgap = 10)
+        
+        pwd = os.getcwd()
+        dir = filebrowse.DirBrowseButton(parent = panel, id = wx.ID_ANY,
+                                         labelText = _("Choose a directory:"),
+                                         dialogTitle = _("Choose a directory for images"),
+                                         buttonText = _('Browse'),
+                                         startDirectory = pwd)
+        dir.SetValue(pwd)
+        prefixLabel = wx.StaticText(parent = panel, id = wx.ID_ANY, label = _("File prefix:"))
+        prefixCtrl = wx.TextCtrl(parent = panel, id = wx.ID_ANY, size = (100, -1),
+                                 value = UserSettings.Get(group = 'nviz',
+                                                          key = 'animation', subkey = 'prefix'))
+        prefixCtrl.SetToolTipString(_("Generated files names will look like this: prefix_1.ppm, prefix_2.ppm, ..."))
+        fileTypeLabel = wx.StaticText(parent = panel, id = wx.ID_ANY, label = _("File format:"))
+        fileTypeCtrl = wx.Choice(parent = panel, id = wx.ID_ANY, choices = ["PPM", "TIF"])
+        
+        save = wx.Button(parent = panel, id = wx.ID_ANY,
+                         label = "Save")
+                         
+        self.win['anim']['save']['image']['dir'] = dir.GetId()
+        self.win['anim']['save']['image']['prefix'] = prefixCtrl.GetId()
+        self.win['anim']['save']['image']['format'] = fileTypeCtrl.GetId()
+        self.win['anim']['save']['image']['confirm'] = save.GetId()
+        
+        boxSizer.Add(item = dir, proportion = 0, flag = wx.ALL | wx.EXPAND, border = 3)
+        
+        gridSizer.Add(item = prefixLabel, pos = (0, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT)
+        gridSizer.Add(item = prefixCtrl, pos = (0, 1), flag = wx.EXPAND )
+        gridSizer.Add(item = fileTypeLabel, pos = (1, 0), flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT)
+        gridSizer.Add(item = fileTypeCtrl, pos = (1, 1), flag = wx.EXPAND )
+        
+        boxSizer.Add(item = gridSizer, proportion = 1,
+                     flag = wx.ALL | wx.EXPAND, border = 5)
+        boxSizer.Add(item = save, proportion = 0, flag = wx.ALL | wx.ALIGN_RIGHT, border = 5)
+        
+        save.Bind(wx.EVT_BUTTON, self.OnSaveAnimation)
+        
+        pageSizer.Add(item = boxSizer, proportion = 0,
+                      flag = wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 
+                      border = 3)
+        
+        panel.SetSizer(pageSizer)
+        
+        return panel
+        
     def _createDataPage(self):
     def _createDataPage(self):
         """!Create data (surface, vector, volume) settings page"""
         """!Create data (surface, vector, volume) settings page"""
 
 
@@ -1944,7 +2170,195 @@ class NvizToolWindow(FN.FlatNotebook):
             return self.mapWindow.GetLayerByName(name, mapType = '3d-raster', dataType = 'nviz')
             return self.mapWindow.GetLayerByName(name, mapType = '3d-raster', dataType = 'nviz')
         
         
         return None
         return None
-    
+        
+    def OnRecord(self, event):
+        """!Animation: start recording"""
+        anim = self.mapWindow.GetAnimation()
+        if not anim.IsPaused():
+            if anim.Exists() and not anim.IsSaved():
+                msg = _("Do you want to record new animation without saving the previous one?")
+                dlg = wx.MessageDialog(parent = self,
+                                       message = msg,
+                                       caption =_("Animation already axists"),
+                                       style = wx.YES_NO | wx.CENTRE)
+                if dlg.ShowModal() == wx.ID_NO:
+                    dlg.Destroy()
+                    return
+                
+        
+            anim.Clear()
+            self.UpdateFrameIndex(0)
+            self.UpdateFrameCount()
+            
+        anim.SetPause(False)
+        anim.SetMode(mode = 'record')
+        anim.Start()
+        
+        self.FindWindowById(self.win['anim']['play']).Disable()
+        self.FindWindowById(self.win['anim']['record']).Disable()
+        self.FindWindowById(self.win['anim']['pause']).Enable()
+        self.FindWindowById(self.win['anim']['stop']).Enable()
+        self.FindWindowById(self.win['anim']['frameIndex']['slider']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['text']).Disable()
+        
+    def OnPlay(self, event):
+        """!Animation: replay"""
+        anim = self.mapWindow.GetAnimation()
+        anim.SetPause(False)
+        anim.SetMode(mode = 'play')
+        anim.Start()
+        
+        self.FindWindowById(self.win['anim']['play']).Disable()
+        self.FindWindowById(self.win['anim']['record']).Disable()
+        self.FindWindowById(self.win['anim']['pause']).Enable()
+        self.FindWindowById(self.win['anim']['stop']).Enable()
+        self.FindWindowById(self.win['anim']['frameIndex']['slider']).Enable()
+        self.FindWindowById(self.win['anim']['frameIndex']['text']).Enable()
+        
+    def OnStop(self, event):
+        """!Animation: stop recording/replaying"""
+        anim = self.mapWindow.GetAnimation()
+        anim.SetPause(False)
+        if anim.GetMode() == 'save':
+            anim.StopSaving()
+        if anim.IsRunning():
+            anim.Stop()
+        
+        self.UpdateFrameIndex(0)
+        
+        self.FindWindowById(self.win['anim']['play']).Enable()
+        self.FindWindowById(self.win['anim']['record']).Enable()
+        self.FindWindowById(self.win['anim']['pause']).Disable()
+        self.FindWindowById(self.win['anim']['stop']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['slider']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['text']).Disable()
+        
+    def OnPause(self, event):
+        """!Pause animation"""
+        anim = self.mapWindow.GetAnimation()
+        
+        anim.SetPause(True)
+        mode = anim.GetMode()
+        if anim.IsRunning():
+            anim.Pause()
+            
+        if mode == "record":
+            self.FindWindowById(self.win['anim']['play']).Disable()
+            self.FindWindowById(self.win['anim']['record']).Enable()
+            self.FindWindowById(self.win['anim']['frameIndex']['slider']).Disable()
+            self.FindWindowById(self.win['anim']['frameIndex']['text']).Disable()
+        elif mode == 'play':
+            self.FindWindowById(self.win['anim']['record']).Disable()
+            self.FindWindowById(self.win['anim']['play']).Enable()
+            self.FindWindowById(self.win['anim']['frameIndex']['slider']).Enable()
+            self.FindWindowById(self.win['anim']['frameIndex']['text']).Enable()
+        
+        self.FindWindowById(self.win['anim']['pause']).Disable()
+        self.FindWindowById(self.win['anim']['stop']).Enable()
+
+        
+    def OnFrameIndex(self, event):
+        """!Frame index changed (by slider)"""
+        index = event.GetInt()
+        self.UpdateFrameIndex(index = index, sliderWidget = False)
+        
+    def OnFrameIndexText(self, event):
+        """!Frame index changed by (textCtrl)"""
+        index = event.GetValue()
+        self.UpdateFrameIndex(index = index, textWidget = False)
+        
+    def OnFPS(self, event):
+        """!Frames per second changed"""
+        anim = self.mapWindow.GetAnimation()
+        anim.SetFPS(event.GetInt())
+        
+    def UpdateFrameIndex(self, index, sliderWidget = True, textWidget = True, goToFrame = True):
+        """!Update frame index"""
+        anim = self.mapWindow.GetAnimation()
+        
+        # check index
+        frameCount = anim.GetFrameCount()
+        if index >= frameCount:
+            index = frameCount - 1
+        if index < 0:
+            index = 0
+            
+        if sliderWidget:
+            slider = self.FindWindowById(self.win['anim']['frameIndex']['slider'])
+            slider.SetValue(index)
+        if textWidget:
+            text = self.FindWindowById(self.win['anim']['frameIndex']['text'])
+            text.SetValue(int(index))
+        
+        # if called from tool window, update frame
+        if goToFrame:
+            anim.GoToFrame(int(index))
+            
+    def UpdateFrameCount(self):
+        """!Update frame count label"""
+        anim = self.mapWindow.GetAnimation()
+        count = anim.GetFrameCount()
+        self.FindWindowById(self.win['anim']['info']).SetLabel(str(count))
+        
+    def OnAnimationFinished(self, event):
+        """!Animation finished"""
+        anim = self.mapWindow.GetAnimation()
+        self.UpdateFrameIndex(index = 0)
+        
+        slider = self.FindWindowById(self.win['anim']['frameIndex']['slider'])
+        text = self.FindWindowById(self.win['anim']['frameIndex']['text'])
+        
+        if event.mode == 'record':
+            count = anim.GetFrameCount()
+            slider.SetMax(count)
+            self.UpdateFrameCount()
+            
+        self.FindWindowById(self.win['anim']['pause']).Disable()
+        self.FindWindowById(self.win['anim']['stop']).Disable()
+        self.FindWindowById(self.win['anim']['record']).Enable()
+        self.FindWindowById(self.win['anim']['play']).Enable()
+        self.FindWindowById(self.win['anim']['frameIndex']['slider']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['text']).Disable()
+        self.FindWindowById(self.win['anim']['save']['image']['confirm']).Enable()
+        
+        self.mapWindow.render['quick'] = False
+        self.mapWindow.Refresh(False)
+        
+    def OnAnimationUpdateIndex(self, event):
+        """!Animation: frame index changed"""
+        if event.mode == 'record':
+            self.UpdateFrameCount()
+        elif event.mode == 'play':
+            self.UpdateFrameIndex(index = event.index, goToFrame = False)
+        
+    def OnSaveAnimation(self, event):
+        """!Save animation as a sequence of images"""
+        anim = self.mapWindow.GetAnimation()
+        
+        prefix = self.FindWindowById(self.win['anim']['save']['image']['prefix']).GetValue()
+        format = self.FindWindowById(self.win['anim']['save']['image']['format']).GetSelection()
+        dir = self.FindWindowById(self.win['anim']['save']['image']['dir']).GetValue()
+        
+        if not prefix:
+            gcmd.GMessage(parent = self,
+                          message = _("No file prefix given."))
+            return
+        elif not os.path.exists(dir):
+            gcmd.GMessage(parent = self,
+                          message = _("Directory %s does not exist.") % dir)
+            return
+            
+        self.FindWindowById(self.win['anim']['pause']).Disable()
+        self.FindWindowById(self.win['anim']['stop']).Enable()
+        self.FindWindowById(self.win['anim']['record']).Disable()
+        self.FindWindowById(self.win['anim']['play']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['slider']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['text']).Disable()
+        
+        self.FindWindowById(self.win['anim']['save']['image']['confirm']).Disable()
+        
+        anim.SaveAnimationFile(path = dir, prefix = prefix, format = format)
+        
     def OnNewConstant(self, event):
     def OnNewConstant(self, event):
         """!Create new surface with constant value"""
         """!Create new surface with constant value"""
         #TODO settings
         #TODO settings
@@ -2336,6 +2750,8 @@ class NvizToolWindow(FN.FlatNotebook):
         sizer.Add(item = sw, pos = (2, 0), flag = wx.ALIGN_CENTER)
         sizer.Add(item = sw, pos = (2, 0), flag = wx.ALIGN_CENTER)
         sizer.Add(item = w, pos = (1, 0), flag = wx.ALIGN_CENTER)
         sizer.Add(item = w, pos = (1, 0), flag = wx.ALIGN_CENTER)
         
         
+        
+        
     def __GetWindowName(self, data, id):
     def __GetWindowName(self, data, id):
         for name in data.iterkeys():
         for name in data.iterkeys():
             if type(data[name]) is type({}):
             if type(data[name]) is type({}):
@@ -2513,7 +2929,7 @@ class NvizToolWindow(FN.FlatNotebook):
         for win in self.win['view'][winName].itervalues():
         for win in self.win['view'][winName].itervalues():
             self.FindWindowById(win).SetValue(value)
             self.FindWindowById(win).SetValue(value)
 
 
-                
+        self.mapWindow.iview['dir']['use'] = False
         self.mapWindow.render['quick'] = True
         self.mapWindow.render['quick'] = True
         if self.mapDisplay.IsAutoRendered():
         if self.mapDisplay.IsAutoRendered():
             self.mapWindow.Refresh(False)
             self.mapWindow.Refresh(False)
@@ -4191,10 +4607,33 @@ class NvizToolWindow(FN.FlatNotebook):
             self.FindWindowById(self.win['cplane']['position']['z']['text']).SetValue(zRange[0])
             self.FindWindowById(self.win['cplane']['position']['z']['text']).SetValue(zRange[0])
             self.OnCPlaneSelection(None)
             self.OnCPlaneSelection(None)
             
             
+        elif pageId == 'animation':
+            self.UpdateAnimationPage()
             
             
         self.Update()
         self.Update()
         self.pageChanging = False
         self.pageChanging = False
         
         
+    def UpdateAnimationPage(self):
+        """!Update animation page"""
+        # wrap help text according to tool window
+        help = self.FindWindowById(self.win['anim']['help'])
+        width = help.GetGrandParent().GetSizeTuple()[0]
+        help.Wrap(width - 15)
+        anim = self.mapWindow.GetAnimation()
+        if anim.Exists():
+            self.FindWindowById(self.win['anim']['play']).Enable()
+        else:
+            self.UpdateFrameIndex(index = 0)
+            
+        self.UpdateFrameCount()
+        
+        self.FindWindowById(self.win['anim']['play']).Disable()
+        self.FindWindowById(self.win['anim']['record']).Enable()
+        self.FindWindowById(self.win['anim']['pause']).Disable()
+        self.FindWindowById(self.win['anim']['stop']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['slider']).Disable()
+        self.FindWindowById(self.win['anim']['frameIndex']['text']).Disable()
+        
     def UpdateCPlanePage(self, index):
     def UpdateCPlanePage(self, index):
         """!Update widgets according to selected clip plane"""
         """!Update widgets according to selected clip plane"""
         if index == -1:   
         if index == -1:   
@@ -4209,14 +4648,7 @@ class NvizToolWindow(FN.FlatNotebook):
                 
                 
     def UpdateSurfacePage(self, layer, data, updateName = True):
     def UpdateSurfacePage(self, layer, data, updateName = True):
         """!Update surface page"""
         """!Update surface page"""
-        ret = gcmd.RunCommand('r.info',
-                              read = True,
-                              flags = 'm',
-                              map = layer.name)
-        if ret:
-            desc = ret.split('=')[1].rstrip('\n')
-        else:
-            desc = None
+        desc = grass.raster_info(layer.name)['title']
         if updateName:
         if updateName:
             self.FindWindowById(self.win['surface']['map']).SetValue(layer.name)
             self.FindWindowById(self.win['surface']['map']).SetValue(layer.name)
         self.FindWindowById(self.win['surface']['desc']).SetLabel(desc)
         self.FindWindowById(self.win['surface']['desc']).SetLabel(desc)
@@ -4730,6 +5162,7 @@ class ViewPositionWindow(PositionWindow):
         return x, y
         return x, y
         
         
     def OnMouse(self, event):
     def OnMouse(self, event):
+        self.mapWindow.iview['dir']['use'] = False # use focus instead of viewdir
         PositionWindow.OnMouse(self, event)
         PositionWindow.OnMouse(self, event)
         if event.LeftIsDown():
         if event.LeftIsDown():
             self.mapWindow.render['quick'] = self.quick
             self.mapWindow.render['quick'] = self.quick

+ 10 - 0
gui/wxpython/gui_modules/preferences.py

@@ -549,6 +549,10 @@ class Settings:
                         'turn' : 1,
                         'turn' : 1,
                         }
                         }
                     },
                     },
+                'animation' : {
+                    'fps' : 24,
+                    'prefix' : _("animation")
+                    },
                 'surface' : {
                 'surface' : {
                     'shine': {
                     'shine': {
                         'map' : False,
                         'map' : False,
@@ -781,6 +785,12 @@ class Settings:
         self.internalSettings['nviz']['view']['focus']['x'] = -1
         self.internalSettings['nviz']['view']['focus']['x'] = -1
         self.internalSettings['nviz']['view']['focus']['y'] = -1
         self.internalSettings['nviz']['view']['focus']['y'] = -1
         self.internalSettings['nviz']['view']['focus']['z'] = -1
         self.internalSettings['nviz']['view']['focus']['z'] = -1
+        self.internalSettings['nviz']['view']['dir'] = {}
+        self.internalSettings['nviz']['view']['dir']['x'] = -1
+        self.internalSettings['nviz']['view']['dir']['y'] = -1
+        self.internalSettings['nviz']['view']['dir']['z'] = -1
+        self.internalSettings['nviz']['view']['dir']['use'] = False
+        
         for decor in ('arrow', 'scalebar'):
         for decor in ('arrow', 'scalebar'):
             self.internalSettings['nviz'][decor] = {}
             self.internalSettings['nviz'][decor] = {}
             self.internalSettings['nviz'][decor]['position'] = {}
             self.internalSettings['nviz'][decor]['position'] = {}

+ 13 - 0
gui/wxpython/gui_modules/workspace.py

@@ -514,6 +514,19 @@ class ProcessWorkspaceFile:
         iview['focus']['x'] = self.__processLayerNvizNode(node_focus, 'x', int)
         iview['focus']['x'] = self.__processLayerNvizNode(node_focus, 'x', int)
         iview['focus']['y'] = self.__processLayerNvizNode(node_focus, 'y', int)
         iview['focus']['y'] = self.__processLayerNvizNode(node_focus, 'y', int)
         iview['focus']['z'] = self.__processLayerNvizNode(node_focus, 'z', int)
         iview['focus']['z'] = self.__processLayerNvizNode(node_focus, 'z', int)
+        node_dir = node_view.find('dir')
+        if node_dir:
+            iview['dir'] = {}
+            iview['dir']['x'] = self.__processLayerNvizNode(node_dir, 'x', int)
+            iview['dir']['y'] = self.__processLayerNvizNode(node_dir, 'y', int)
+            iview['dir']['z'] = self.__processLayerNvizNode(node_dir, 'z', int)
+            iview['dir']['use'] = True
+        else:
+            iview['dir'] = {}
+            iview['dir']['x'] = -1
+            iview['dir']['y'] = -1
+            iview['dir']['z'] = -1
+            iview['dir']['use'] = False
         
         
         view['background'] = {}
         view['background'] = {}
         color = self.__processLayerNvizNode(node_view, 'background_color', str)
         color = self.__processLayerNvizNode(node_view, 'background_color', str)

+ 25 - 0
gui/wxpython/gui_modules/wxnviz.py

@@ -170,6 +170,15 @@ class Nviz(object):
         Debug.msg(3, "Nviz::SetView(): x=%f, y=%f, height=%f, persp=%f, twist=%f",
         Debug.msg(3, "Nviz::SetView(): x=%f, y=%f, height=%f, persp=%f, twist=%f",
                   x, y, height, persp, twist)
                   x, y, height, persp, twist)
                 
                 
+    def GetViewpointPosition(self):
+        x = c_double()
+        y = c_double()
+        h = c_double()
+        Nviz_get_viewpoint_height(byref(h))
+        Nviz_get_viewpoint_position(byref(x), byref(y))
+        
+        return (x.value, y.value, h.value)
+        
     def LookHere(self, x, y):
     def LookHere(self, x, y):
         """!Look here feature 
         """!Look here feature 
         @param x,y screen coordinates
         @param x,y screen coordinates
@@ -200,6 +209,22 @@ class Nviz(object):
         Debug.msg(3, "Nviz::SetFocus()")
         Debug.msg(3, "Nviz::SetFocus()")
         Nviz_set_focus(self.data, x, y, z)
         Nviz_set_focus(self.data, x, y, z)
         
         
+    def GetViewdir(self):
+        """!Get viewdir"""
+        Debug.msg(3, "Nviz::GetViewdir()")
+        dir = (c_float * 3)()
+        GS_get_viewdir(byref(dir))
+        
+        return dir[0], dir[1], dir[2]
+        
+    def SetViewdir(self, x, y, z):
+        """!Set viewdir"""
+        Debug.msg(3, "Nviz::SetViewdir(): x=%f, y=%f, z=%f" % (x, y, z))
+        dir = (c_float * 3)()
+        for i, coord in enumerate((x, y, z)):
+            dir[i] = coord
+        GS_set_viewdir(byref(dir))
+                
     def SetZExag(self, z_exag):
     def SetZExag(self, z_exag):
         """!Set z-exag value
         """!Set z-exag value