Index: /anuga_core/source/anuga/examples/sww_file_visualiser_example.py
===================================================================
--- /anuga_core/source/anuga/examples/sww_file_visualiser_example.py	(revision 3872)
+++ /anuga_core/source/anuga/examples/sww_file_visualiser_example.py	(revision 3873)
@@ -1,2 +1,3 @@
+#!/usr/bin/env python
 ##########
 # Demonstration of the VTK sww Visualiser
@@ -7,4 +8,5 @@
 # Import the offline visualiser
 from anuga.visualiser import OfflineVisualiser
+from vtk import vtkCubeAxesActor2D
 
 # The argument to OfflineVisualiser is the path to a sww file
@@ -31,5 +33,8 @@
 o.render_axes()
 
-# Draw a polygon (here, a triangle) at height 10
+# Increase the number of labels on the axes
+o.alter_axes(vtkCubeAxesActor2D.SetNumberOfLabels, (5,))
+
+# Draw a yellow polygon at height 10
 o.overlay_polygon([(20, 50), (40, 40), (50, 10), (30, 20), (10, 30)], 10, colour=(1.0, 1.0, 0.0))
 
@@ -39,3 +44,6 @@
 
 # Start the visualiser (in its own thread).
-o.run()
+o.start()
+
+# Wait for the visualiser to terminate before shutting down.
+o.join()
Index: /anuga_core/source/anuga/examples/visualise_rectangle.py
===================================================================
--- /anuga_core/source/anuga/examples/visualise_rectangle.py	(revision 3873)
+++ /anuga_core/source/anuga/examples/visualise_rectangle.py	(revision 3873)
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+##########
+# Demonstration of the VTK realtime Visualiser.
+# Based on run_sw_rectangle.py
+# Jack Kelly and Stephen Roberts
+# October 2006
+##########
+
+# Import the offline visualiser
+from anuga.visualiser import RealtimeVisualiser
+from vtk import vtkCubeAxesActor2D
+
+import time
+from Numeric import array
+from anuga.shallow_water import Domain
+from anuga.shallow_water import Reflective_boundary
+from anuga.abstract_2d_finite_volumes.mesh_factory import rectangular
+
+
+class Set_Stage:
+    """Set an initial condition with constant water height, for x<x0
+    """
+
+    def __init__(self, x0=0.25, x1=0.75, y0=0.0, y1=1.0, h=5.0, h0=0.0):
+        self.x0 = x0
+        self.x1 = x1
+        self.y0 = y0
+        self.y1 = y1
+        self.h  = h
+        self.h0 = h0
+
+    def __call__(self, x, y):
+        return self.h0 + self.h*((x>self.x0)&(x<self.x1)&(y>self.y0)&(y<self.y1))
+
+M = 30
+points, vertices, boundary = rectangular(M, M, len1 = 1.0, len2 = 1.0)
+
+yieldstep = 0.002
+finaltime = 1.0
+rect = [0.0, 0.0, 1.0, 1.0]
+
+domain = Domain(points, vertices, boundary)
+
+# Turn on the visualisation. The argument to the realtime visualier
+# is a domain object.
+v = RealtimeVisualiser(domain)
+
+# Specify the height-based-quantities to render.
+# Remember to set dynamic=True for time-varying quantities
+v.render_quantity_height("elevation", dynamic=False)
+v.render_quantity_height("stage", dynamic=True)
+
+# Colour the stage:
+# Either with an RGB value as a 3-tuple of Floats,
+#v.colour_height_quantity('stage', (0.0, 0.0, 0.8))
+# Or with a function of the quantities at that point, such as the stage height:
+# 0 and 1 are the minimum and maximum values of the stage.
+v.colour_height_quantity('stage', (lambda q:q['stage'], 0, 1))
+# Or with the magnitude of the momentum at that point:
+# Needs the sqrt function from Numeric. Again, 0 and 10 define the colour range.
+#v.colour_height_quantity('stage', (lambda q:sqrt((q['xmomentum'] ** 2) +
+#                                                 (q['ymomentum'] ** 2)), 0, 10))
+
+# Draw some axes on the visualiser so we can see how big the wave is
+v.render_axes()
+
+# Increase the number of labels on the axes
+v.alter_axes(vtkCubeAxesActor2D.SetNumberOfLabels, (5,))
+
+# Draw a yellow polygon at height 10
+v.overlay_polygon([(20, 50), (40, 40), (50, 10), (30, 20), (10, 30)], 10, colour=(1.0, 1.0, 0.0))
+
+# Start the visualiser (in its own thread).
+v.start()
+
+#-----------------------------------------------------------------
+# Boundaries and Initial conditions
+#-----------------------------------------------------------------
+R = Reflective_boundary(domain)
+domain.set_boundary( {'left': R, 'right': R, 'bottom': R, 'top': R} )
+domain.set_quantity('stage', Set_Stage(0.2, 0.4, 0.25, 0.75, 1.0, 0.00))
+
+#-----------------------------------------------------------------
+# Evolve
+#-----------------------------------------------------------------
+t0 = time.time()
+for t in domain.evolve(yieldstep = yieldstep, finaltime = finaltime):
+    v.update()
+    domain.write_time()
+# Unhook the visualiser from the evolve loop.
+# It won't shutdown cleanly unless you do this.
+v.evolveFinished()
+
+print 'That took %.2f seconds' %(time.time()-t0)
+
+# Wait for the visualiser to be closed
+v.join()
Index: /anuga_core/source/anuga/visualiser/offline.py
===================================================================
--- /anuga_core/source/anuga/visualiser/offline.py	(revision 3872)
+++ /anuga_core/source/anuga/visualiser/offline.py	(revision 3873)
@@ -1,5 +1,5 @@
 from Numeric import array, Float, ravel, zeros
 from Scientific.IO.NetCDF import NetCDFFile
-from Tkinter import Button, E, W
+from Tkinter import Button, E, Tk, W
 from visualiser import Visualiser
 from vtk import vtkCellArray, vtkPoints, vtkPolyData
@@ -13,6 +13,7 @@
     structures for any dynamic height based quantities to render.
     """
-    def __init__(self, source):
+    def __init__(self, source, frameDelay=100):
         """The source parameter is assumed to be a NetCDF sww file.
+        The frameDelay parameter is the number of milliseconds waited between frames.
         """
         Visualiser.__init__(self, source)
@@ -22,4 +23,6 @@
         self.maxFrameNumber = fin.variables['time'].shape[0] - 1
         fin.close()
+
+        self.frameDelay = frameDelay
 
         self.xmin = None
@@ -140,10 +143,10 @@
 
     def run(self):
-        self.tk_root.after(100, self.animateForward)
+        self.alter_tkroot(Tk.after, (self.frameDelay, self.animateForward))
         Visualiser.run(self)
 
     def restart(self):
         self.frameNumber = 0
-        self.redraw_quantities(True)
+        self.redraw_quantities()
         self.pause()
 
@@ -153,5 +156,5 @@
         else:
             self.frameNumber = 0
-        self.redraw_quantities(True)
+        self.redraw_quantities()
         self.pause()
 
@@ -159,5 +162,5 @@
         if self.frameNumber > 0:
             self.frameNumber -= 1
-            self.redraw_quantities(True)
+            self.redraw_quantities()
             self.pause()
 
@@ -175,5 +178,5 @@
         self.paused = False
         self.tk_pauseResume.config(text="Pause")
-        self.tk_root.after(100, self.animateForward)
+        self.tk_root.after(self.frameDelay, self.animateForward)
 
     def forward(self):
@@ -184,5 +187,5 @@
         if self.frameNumber < self.maxFrameNumber:
             self.frameNumber += 1
-            self.redraw_quantities(True)
+            self.redraw_quantities()
         else:
             self.pause()
@@ -193,5 +196,5 @@
         else:
             self.frameNumber = self.maxFrameNumber
-        self.redraw_quantities(True)
+        self.redraw_quantities()
         self.pause()
 
@@ -199,3 +202,3 @@
         if self.paused is not True:
             self.forward_step()
-            self.tk_root.after(100, self.animateForward)
+            self.tk_root.after(self.frameDelay, self.animateForward)
Index: /anuga_core/source/anuga/visualiser/realtime.py
===================================================================
--- /anuga_core/source/anuga/visualiser/realtime.py	(revision 3872)
+++ /anuga_core/source/anuga/visualiser/realtime.py	(revision 3873)
@@ -1,4 +1,4 @@
 from Numeric import Float, zeros
-from Tkinter import Button, E, W
+from Tkinter import Button, E, Tk, W
 from threading import Event
 from visualiser import Visualiser
@@ -7,4 +7,13 @@
 class RealtimeVisualiser(Visualiser):
     """A VTK-powered realtime visualiser which runs in its own thread.
+    In addition to the functions provided by the standard visualiser,
+    the following additional functions are provided:
+
+    update() - Sync the visualiser to the current state of the model.
+    Should be called inside the evolve loop.
+
+    evolveFinished() - Clean up synchronisation constructs that tie the
+    visualiser to the evolve loop. Call this after the evolve loop finishes
+    to ensure a clean shutdown.
     """
     def __init__(self, source):
@@ -14,4 +23,11 @@
 
         self.running = True
+
+        self.xmin = None
+        self.xmax = None
+        self.ymin = None
+        self.ymax = None
+        self.zmin = None
+        self.zmax = None
 
         # Synchronisation Constructs
@@ -24,5 +40,5 @@
 
     def run(self):
-        self.tk_root.after(100, self.sync_idle.set)
+        self.alter_tkroot(Tk.after, (100, self.sync_idle.set))
         Visualiser.run(self)
 
@@ -35,5 +51,5 @@
             self.vtk_cells.InsertNextCell(3)
             for v in range(3):
-                self.vert_index[self.source.triangles[n][v]] = self.source.vertex_coordinates[n][i*2:i*2+2]
+                self.vert_index[self.source.triangles[n][v]] = self.source.vertex_coordinates[n][v*2:v*2+2]
                 self.vtk_cells.InsertCellPoint(self.source.triangles[n][v])
 
@@ -52,8 +68,23 @@
                                    qty_index[v] * self.height_zScales[quantityName]
                                    + self.height_offset[quantityName])
+            if self.xmin == None or self.xmin > self.vert_index[v][0]:
+                self.xmin = self.vert_index[v][0]
+            if self.xmax == None or self.xmax < self.vert_index[v][0]:
+                self.xmax = self.vert_index[v][0]
+            if self.ymin == None or self.ymin > self.vert_index[v][1]:
+                self.ymin = self.vert_index[v][1]
+            if self.ymax == None or self.ymax < self.vert_index[v][1]:
+                self.ymax = self.vert_index[v][1]
+            if self.zmin == None or self.zmin > qty_index[v] * self.height_zScales[quantityName] + self.height_offset[quantityName]:
+                self.zmin = qty_index[v] * self.height_zScales[quantityName] + self.height_offset[quantityName]
+            if self.zmax == None or self.zmax < qty_index[v] * self.height_zScales[quantityName] + self.height_offset[quantityName]:
+                self.zmax = qty_index[v] * self.height_zScales[quantityName] + self.height_offset[quantityName]
 
         polydata = self.vtk_polyData[quantityName] = vtkPolyData()
         polydata.SetPoints(points)
         polydata.SetPolys(self.vtk_cells)
+
+    def get_3d_bounds(self):
+        return [self.xmin, self.xmax, self.ymin, self.ymax, self.zmin, self.zmax]
         
     def build_quantity_dict(self):
@@ -86,2 +117,23 @@
         self.sync_unpaused.set()
 
+    def redraw(self):
+        if self.running:
+            self.sync_redrawReady.wait()
+            self.sync_redrawReady.clear()
+            self.redraw_quantities()
+            self.sync_idle.set()
+        Visualiser.redraw(self)
+
+    def update(self):
+        """Sync the visualiser to the domain. Call this in the evolve loop."""
+        if self.running:
+            self.sync_redrawReady.set()
+            self.sync_idle.wait()
+            self.sync_idle.clear()
+            self.sync_unpaused.wait()
+
+    def evolveFinished(self):
+        """Stop the visualiser from waiting on signals from the evolve loop.
+        Call this just after the evolve loop to ensure a clean shutdown."""
+        self.running = False
+        self.sync_redrawReady.set()
Index: /anuga_core/source/anuga/visualiser/visualiser.py
===================================================================
--- /anuga_core/source/anuga/visualiser/visualiser.py	(revision 3872)
+++ /anuga_core/source/anuga/visualiser/visualiser.py	(revision 3873)
@@ -1,3 +1,4 @@
 from threading import Thread
+from Queue import Queue
 from Tkinter import Tk, Button, Frame, N, E, S, W
 from types import FunctionType, TupleType
@@ -28,45 +29,85 @@
         self.vtk_mappers = {}
         self.vtk_polyData = {}
+
+        # A function queue to ensure all the visualiser configuration is done in the visualiser thread.
+        # The queue will contain zero or more functions, with the arguments to that function immediately following
+        # as a tuple.
+        self.sync_configFuncs = Queue(-1)
+
+    def run(self):
         self.vtk_renderer = vtkRenderer()
-
         self.setup_gui()
-
-    def run(self):
         self.setup_grid()
+
+        # Normally, empty() is not reliable, however all configuration is done
+        # before the visualiser is started and nothing is added to the queue after that.
+        while not self.sync_configFuncs.empty():
+            func = self.sync_configFuncs.get(True)
+            args = self.sync_configFuncs.get(True)
+            func(*args)
+
         # Draw Height Quantities
         for q in self.height_quantities:
             self.update_height_quantity(q, self.height_dynamic[q])
             self.draw_height_quantity(q)
+
+        if self.vtk_drawAxes is True:
+            self.tk_root.after(1, self.draw_axes)
+            
         self.tk_root.mainloop()
 
     def redraw_quantities(self, dynamic_only=False):
-        """Redraw all dynamic quantities, unless dynamic_only is True.
+        """Redraw all dynamic quantities.
         """
         # Height quantities
         for q in self.height_quantities:
-            if (dynamic_only is False) or (self.height_dynamic[q]):
+            if (self.height_dynamic[q]):
                 self.update_height_quantity(q, self.height_dynamic[q])
                 self.draw_height_quantity(q)
         if self.vtk_drawAxes is True:
-            self.vtk_axes.SetBounds(self.get_3d_bounds())
-            if not self.vtk_axesSet:
-                self.vtk_axesSet = True
-                self.vtk_axes.SetCamera(self.vtk_renderer.GetActiveCamera())
-                self.vtk_renderer.AddActor(self.vtk_axes)
+            self.draw_axes()
+
+    # --- Axes --- #
         
-    # --- Height Based Rendering --- #
-
     def render_axes(self):
         """Intstruct the visualiser to render cube axes around the render.
         """
+        self.sync_configFuncs.put(self._render_axes, True)
+        self.sync_configFuncs.put(())
+
+    def _render_axes(self):
+        """Add the axes to the visualiser.
+        """
         self.vtk_drawAxes = True
         self.vtk_axes = vtkCubeAxesActor2D()
+
+    def draw_axes(self):
+        """Update the 3D bounds on the axes and add them to the pipeline if not yet connected.
+        """
+        self.vtk_axes.SetBounds(self.get_3d_bounds())
+        if not self.vtk_axesSet:
+            self.vtk_axesSet = True
+            self.vtk_axes.SetCamera(self.vtk_renderer.GetActiveCamera())
+            self.vtk_renderer.AddActor(self.vtk_axes)
         
-    def get_axes(self):
-        """Return the vtkCubeAxesActor2D object used to render the axes.
-        This is to allow simple manipulation of the axes such as
-        get_axes().SetNumberOfLabels(5) or similar.
-        """
-        return self.vtk_axes
+    def alter_axes(self, func, args):
+        """Attempt to apply the function 'func' with args tuple 'args' to the
+        vtkCubeAxesActor2D instance set up by render_axes. This is done this way to ensure
+        the axes setup is handled in the visualiser thread.
+
+        Example call:
+        from vtk import vtkCubeAxesActor2D
+        alter_axes(vtkCubeAxesActor2D.SetNumberOfPoints, (5,))
+        """
+        self.sync_configFuncs.put(self._alter_axes, True)
+        self.sync_configFuncs.put((func,)+args, True)
+
+    def _alter_axes(self, func, *args):
+        """Apply func, with args *args to the axes actor. This function should be called from
+        within the visualiser thread after the axes have been initialised.
+        """
+        func(*((self.vtk_axes,) + args))
+            
+    # --- Height Based Rendering --- #
 
     def setup_grid(self):
@@ -120,4 +161,6 @@
             actor.SetMapper(mapper)
             self.vtk_renderer.AddActor(actor)
+        else:
+            actor = self.vtk_actors[quantityName]
 
         if self.colours_height.has_key(quantityName):
@@ -175,4 +218,22 @@
         colour is the colour of the polygon, as a 3-tuple representing
         r, g, b values between 0 and 1."""
+        self.sync_configFuncs.put(self._overlay_polygon, True)
+        self.sync_configFuncs.put((coords, height, colour), True)
+
+    def _overlay_polygon(self, coords, height, colour):
+        """Add a polygon to the output of the visualiser.
+
+        coords is a list of 2-tuples representing x and y coordinates.
+        These are triangulated by vtkDelaunay2D.
+
+        height is the z-value given to all points.
+
+        colour is the colour of the polygon, as a 3-tuple representing
+        r, g, b values between 0 and 1.
+
+        This function should not be called from outside the visualiser thread.
+        Use overlay_polygon instead.
+    
+        """
         points = vtkPoints()
         for coord in coords:
@@ -212,4 +273,17 @@
         self.tk_renderWidget.GetRenderWindow().AddRenderer(self.vtk_renderer)
 
+    def alter_tkroot(self, func, args):
+        """Apply func, with arguments tuple args to the root tk window for this visualiser.
+        """
+        self.sync_configFuncs.put(self._alter_tkroot, True)
+        self.sync_configFuncs.put((func,)+args, True)
+
+    def _alter_tkroot(self, func, *args):
+        """Apply func, with arguments tuple args to the root tk window for this visualiser.
+        This function should only be called from within the visualiser after the tk root window
+        has been created.
+        """
+        func(*((self.tk_root,) + args))
+
     # --- GUI Events --- #
 
