[hdf-compass] 255/295: Extend the compass model with GeoArray and GeoSurface
Ghislain Vaillant
ghisvail-guest at moszumanska.debian.org
Sun May 8 10:35:51 UTC 2016
This is an automated email from the git hooks/post-receive script.
ghisvail-guest pushed a commit to branch debian/master
in repository hdf-compass.
commit 928f522414ae6d6e9f60629d2112b793052ab3a3
Author: giumas <giumas at yahoo.it>
Date: Fri Nov 6 23:49:34 2015 -0500
Extend the compass model with GeoArray and GeoSurface
---
HDFCompass.1file.spec | 4 +-
HDFCompass.1folder.spec | 2 +-
README.rst | 1 +
docs/conf.py | 2 +-
hdf_compass/bag_model/model.py | 189 +++++++++-
hdf_compass/compass_model/__init__.py | 3 +-
hdf_compass/compass_model/model.py | 63 +++-
.../geo_array}/__init__.py | 4 +-
hdf_compass/compass_viewer/geo_array/frame.py | 416 +++++++++++++++++++++
hdf_compass/compass_viewer/geo_array/plot.py | 207 ++++++++++
.../geo_surface}/__init__.py | 4 +-
hdf_compass/compass_viewer/geo_surface/frame.py | 416 +++++++++++++++++++++
hdf_compass/compass_viewer/geo_surface/plot.py | 213 +++++++++++
hdf_compass/compass_viewer/viewer.py | 10 +-
hdf_compass/utils/__init__.py | 2 +-
setup.cfg | 15 +-
setup.py | 5 +-
17 files changed, 1524 insertions(+), 32 deletions(-)
diff --git a/HDFCompass.1file.spec b/HDFCompass.1file.spec
index f4c62d4..0595c9c 100644
--- a/HDFCompass.1file.spec
+++ b/HDFCompass.1file.spec
@@ -59,12 +59,12 @@ else:
if not os.path.exists(icon_file):
raise RuntimeError("invalid path to icon: %s" % icon_file)
-version = '0.6.0.dev1'
+version = '0.6.0b2'
app_name = 'HDFCompass_' + version
a = Analysis(['HDFCompass.py'],
pathex=[],
- hiddenimports=[],
+ hiddenimports=['scipy.linalg.cython_blas', 'scipy.linalg.cython_lapack'], # for cartopy
excludes=["PySide"], # exclude libraries from being bundled (in case that are installed)
hookspath=None,
runtime_hooks=None)
diff --git a/HDFCompass.1folder.spec b/HDFCompass.1folder.spec
index e492c48..c9ea9ba 100644
--- a/HDFCompass.1folder.spec
+++ b/HDFCompass.1folder.spec
@@ -61,7 +61,7 @@ if not os.path.exists(icon_file):
a = Analysis(['HDFCompass.py'],
pathex=[],
- hiddenimports=[],
+ hiddenimports=['scipy.linalg.cython_blas', 'scipy.linalg.cython_lapack'], # for cartopy
excludes=["PySide"], # exclude libraries from being bundled (in case that are installed)
hookspath=None,
runtime_hooks=None)
diff --git a/README.rst b/README.rst
index 9a59acc..c8b6900 100644
--- a/README.rst
+++ b/README.rst
@@ -42,6 +42,7 @@ You will need:
* `NumPy <https://github.com/numpy/numpy>`_
* `Matplotlib <https://github.com/matplotlib/matplotlib>`_
* `wxPython Phoenix 3.0.0 <https://github.com/wxWidgets/Phoenix>`_ *(later releases have not been tested)*
+* `Cartopy <https://github.com/SciTools/cartopy>`_
* `h5py <https://github.com/h5py/h5py>`_ *[HDF plugin]*
* `hydroffice.bag <https://bitbucket.org/ccomjhc/hyo_bag>`_ *[BAG plugin]*
* `Pydap <https://github.com/robertodealmeida/pydap>`_ *[OPeNDAP plugin]*
diff --git a/docs/conf.py b/docs/conf.py
index f127e90..a9378fa 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -57,7 +57,7 @@ author = u'The HDF Group'
# The short X.Y version.
version = '0.6'
# The full version, including alpha/beta/rc tags.
-release = '0.6.0.dev1'
+release = '0.6.0b2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/hdf_compass/bag_model/model.py b/hdf_compass/bag_model/model.py
index 26a35e7..cb345fe 100644
--- a/hdf_compass/bag_model/model.py
+++ b/hdf_compass/bag_model/model.py
@@ -282,7 +282,95 @@ class BAGDataset(compass_model.Array):
return True
-class BAGElevation(compass_model.Array):
+class BAGElevationArray(compass_model.Array):
+ """ Represents a BAG elevation. """
+ class_kind = "BAG Elevation [array]"
+
+ @staticmethod
+ def can_handle(store, key):
+ return (key == "/BAG_root/elevation") and (key in store) and (isinstance(store.f[key], h5py.Dataset))
+
+ def __init__(self, store, key):
+ self._store = store
+ self._key = key
+ self._dset = store.f.elevation(mask_nan=True)
+
+ @property
+ def key(self):
+ return self._key
+
+ @property
+ def store(self):
+ return self._store
+
+ @property
+ def display_name(self):
+ return pp.basename(self.key)
+
+ @property
+ def description(self):
+ return 'Dataset "%s"' % (self.display_name,)
+
+ @property
+ def shape(self):
+ return self._dset.shape
+
+ @property
+ def dtype(self):
+ return self._dset.dtype
+
+ def __getitem__(self, args):
+ return self._dset[args]
+
+
+class BAGElevationGeoArray(compass_model.GeoArray):
+ """ Represents a BAG elevation. """
+ class_kind = "BAG Elevation [geo array]"
+
+ @staticmethod
+ def can_handle(store, key):
+ return (key == "/BAG_root/elevation") and (key in store) and (isinstance(store.f[key], h5py.Dataset))
+
+ def __init__(self, store, key):
+ self._store = store
+ self._key = key
+ self._dset = store.f.elevation(mask_nan=True)
+ self._meta = store.f.populate_metadata()
+
+ @property
+ def key(self):
+ return self._key
+
+ @property
+ def store(self):
+ return self._store
+
+ @property
+ def display_name(self):
+ return pp.basename(self.key)
+
+ @property
+ def description(self):
+ return 'Dataset "%s"' % (self.display_name,)
+
+ @property
+ def shape(self):
+ return self._dset.shape
+
+ @property
+ def dtype(self):
+ return self._dset.dtype
+
+ @property
+ def extent(self):
+ """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """
+ return self._meta.geo_extent()
+
+ def __getitem__(self, args):
+ return self._dset[args]
+
+
+class BAGElevation(compass_model.GeoSurface):
""" Represents a BAG elevation. """
class_kind = "BAG Elevation"
@@ -294,6 +382,53 @@ class BAGElevation(compass_model.Array):
self._store = store
self._key = key
self._dset = store.f.elevation(mask_nan=True)
+ self._meta = store.f.populate_metadata()
+
+ @property
+ def key(self):
+ return self._key
+
+ @property
+ def store(self):
+ return self._store
+
+ @property
+ def display_name(self):
+ return pp.basename(self.key)
+
+ @property
+ def description(self):
+ return 'Dataset "%s"' % (self.display_name,)
+
+ @property
+ def shape(self):
+ return self._dset.shape
+
+ @property
+ def dtype(self):
+ return self._dset.dtype
+
+ @property
+ def extent(self):
+ """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """
+ return self._meta.geo_extent()
+
+ def __getitem__(self, args):
+ return self._dset[args]
+
+
+class BAGUncertaintyArray(compass_model.Array):
+ """ Represents an uncertainty array. """
+ class_kind = "BAG Uncertainty [array]"
+
+ @staticmethod
+ def can_handle(store, key):
+ return (key == "/BAG_root/uncertainty") and (key in store) and (isinstance(store.f[key], h5py.Dataset))
+
+ def __init__(self, store, key):
+ self._store = store
+ self._key = key
+ self._dset = store.f.uncertainty(mask_nan=True)
@property
def key(self):
@@ -501,7 +636,48 @@ class BAGMetadataXml(compass_model.Xml):
return self.store.f.validation_info()
-class BAGUncertainty(compass_model.Array):
+class BAGUncertaintyArray(compass_model.Array):
+ """ Represents an uncertainty array. """
+ class_kind = "BAG Uncertainty [array]"
+
+ @staticmethod
+ def can_handle(store, key):
+ return (key == "/BAG_root/uncertainty") and (key in store) and (isinstance(store.f[key], h5py.Dataset))
+
+ def __init__(self, store, key):
+ self._store = store
+ self._key = key
+ self._dset = store.f.uncertainty(mask_nan=True)
+
+ @property
+ def key(self):
+ return self._key
+
+ @property
+ def store(self):
+ return self._store
+
+ @property
+ def display_name(self):
+ return pp.basename(self.key)
+
+ @property
+ def description(self):
+ return 'Dataset "%s"' % (self.display_name,)
+
+ @property
+ def shape(self):
+ return self._dset.shape
+
+ @property
+ def dtype(self):
+ return self._dset.dtype
+
+ def __getitem__(self, args):
+ return self._dset[args]
+
+
+class BAGUncertainty(compass_model.GeoArray):
""" Represents a BAG uncertainty. """
class_kind = "BAG Uncertainty"
@@ -513,6 +689,7 @@ class BAGUncertainty(compass_model.Array):
self._store = store
self._key = key
self._dset = store.f.uncertainty(mask_nan=True)
+ self._meta = store.f.populate_metadata()
@property
def key(self):
@@ -538,6 +715,11 @@ class BAGUncertainty(compass_model.Array):
def dtype(self):
return self._dset.dtype
+ @property
+ def extent(self):
+ """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """
+ return self._meta.geo_extent()
+
def __getitem__(self, args):
return self._dset[args]
@@ -642,7 +824,10 @@ class BAGImage(compass_model.Image):
# Register handlers
BAGStore.push(BAGKV)
BAGStore.push(BAGDataset)
+BAGStore.push(BAGElevationArray)
+BAGStore.push(BAGElevationGeoArray)
BAGStore.push(BAGElevation)
+BAGStore.push(BAGUncertaintyArray)
BAGStore.push(BAGUncertainty)
BAGStore.push(BAGTrackinList)
BAGStore.push(BAGMetadataRaw)
diff --git a/hdf_compass/compass_model/__init__.py b/hdf_compass/compass_model/__init__.py
index 0853f0a..c18b5b0 100644
--- a/hdf_compass/compass_model/__init__.py
+++ b/hdf_compass/compass_model/__init__.py
@@ -11,7 +11,8 @@
##############################################################################
from __future__ import absolute_import, division, print_function, unicode_literals
-from .model import get_stores, push, Store, Node, Container, KeyValue, Array, Text, Xml, Image, Unknown
+from .model import get_stores, push, Store, Node, Container, KeyValue, \
+ GeoArray, GeoSurface, Array, Text, Xml, Image, Unknown
import logging
log = logging.getLogger(__name__)
diff --git a/hdf_compass/compass_model/model.py b/hdf_compass/compass_model/model.py
index 82d49a7..4e32c88 100644
--- a/hdf_compass/compass_model/model.py
+++ b/hdf_compass/compass_model/model.py
@@ -392,10 +392,67 @@ class Array(Node):
return True
+class GeoArray(Node):
+ """ Represents a NumPy-style regular, rectangular array with a known geographic extent. """
+
+ __metaclass__ = ABCMeta
+
+ icons = {16: os.path.join(icon_folder, "array_16.png"),
+ 64: os.path.join(icon_folder, "array_64.png")}
+
+ @property
+ def shape(self):
+ """ Shape tuple """
+ raise NotImplementedError
+
+ @property
+ def dtype(self):
+ """ Data type """
+ raise NotImplementedError
+
+ @property
+ def extent(self):
+ """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """
+ raise NotImplementedError
+
+ def __getitem__(self, args):
+ """ Retrieve data elements """
+ raise NotImplementedError
+
+ def is_plottable(self):
+ """ To be overriden in case that there are cases in which the array is not plottable """
+ return True
+
+
+class GeoSurface(Node):
+ """ Represents a NumPy-style regular, rectangular surface with a known geographic extent. """
+
+ __metaclass__ = ABCMeta
+
+ icons = {16: os.path.join(icon_folder, "array_16.png"),
+ 64: os.path.join(icon_folder, "array_64.png")}
+
+ @property
+ def shape(self):
+ """ Shape tuple """
+ raise NotImplementedError
+
+ @property
+ def dtype(self):
+ """ Data type """
+ raise NotImplementedError
+
+ def __getitem__(self, args):
+ """ Retrieve data elements """
+ raise NotImplementedError
+
+ def is_plottable(self):
+ """ To be overriden in case that there are cases in which the array is not plottable """
+ return True
+
+
class Image(Node):
- """
- A single raster image.
- """
+ """ A single raster image. """
__metaclass__ = ABCMeta
diff --git a/hdf_compass/compass_model/__init__.py b/hdf_compass/compass_viewer/geo_array/__init__.py
similarity index 87%
copy from hdf_compass/compass_model/__init__.py
copy to hdf_compass/compass_viewer/geo_array/__init__.py
index 0853f0a..aa3d0ec 100644
--- a/hdf_compass/compass_model/__init__.py
+++ b/hdf_compass/compass_viewer/geo_array/__init__.py
@@ -11,8 +11,8 @@
##############################################################################
from __future__ import absolute_import, division, print_function, unicode_literals
-from .model import get_stores, push, Store, Node, Container, KeyValue, Array, Text, Xml, Image, Unknown
+from .frame import GeoArrayFrame
import logging
log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+log.addHandler(logging.NullHandler())
\ No newline at end of file
diff --git a/hdf_compass/compass_viewer/geo_array/frame.py b/hdf_compass/compass_viewer/geo_array/frame.py
new file mode 100644
index 0000000..ccefe99
--- /dev/null
+++ b/hdf_compass/compass_viewer/geo_array/frame.py
@@ -0,0 +1,416 @@
+##############################################################################
+# Copyright by The HDF Group. #
+# All rights reserved. #
+# #
+# This file is part of the HDF Compass Viewer. The full HDF Compass #
+# copyright notice, including terms governing use, modification, and #
+# terms governing use, modification, and redistribution, is contained in #
+# the file COPYING, which can be found at the root of the source code #
+# distribution tree. If you do not have access to this file, you may #
+# request a copy from help at hdfgroup.org. #
+##############################################################################
+"""
+Implements a viewer frame for compass_model.Array.
+"""
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import wx
+import wx.grid
+from wx.lib.newevent import NewCommandEvent
+
+import os
+import logging
+
+log = logging.getLogger(__name__)
+
+from ..frame import NodeFrame
+from .plot import LinePlotFrame, ContourPlotFrame
+
+
+# Indicates that the slicing selection may have changed.
+# These events are emitted by the SlicerPanel.
+ArraySlicedEvent, EVT_ARRAY_SLICED = NewCommandEvent()
+
+# Menu and button IDs
+ID_VIS_MENU_PLOT = wx.NewId()
+
+
+class GeoArrayFrame(NodeFrame):
+ """
+ Top-level frame displaying objects of type compass_model.Array.
+
+ From top to bottom, has:
+
+ 1. Toolbar (see ArrayFrame.init_toolbar)
+ 2. SlicerPanel, with controls for changing what's displayed.
+ 3. An ArrayGrid, which displays the data in a spreadsheet-like view.
+ """
+
+ def __init__(self, node, pos=None):
+ """ Create a new array viewer to display the node. """
+ NodeFrame.__init__(self, node, size=(800, 400), title=node.display_name, pos=pos)
+
+ self.node = node
+
+ # Update the menu
+ vis_menu = wx.Menu()
+ if self.node.is_plottable():
+ vis_menu.Append(ID_VIS_MENU_PLOT, "Map Data\tCtrl-D")
+ self.add_menu(vis_menu, "Visualize")
+ # Initialize the toolbar
+ self.init_toolbar()
+
+ # The Slicer is the panel with indexing controls
+ self.slicer = SlicerPanel(self, node.shape, node.dtype.fields is not None)
+ # Create the grid array
+ self.grid = ArrayGrid(self, node, self.slicer)
+ # Sizer for slicer and grid
+ gridsizer = wx.BoxSizer(wx.VERTICAL)
+ gridsizer.Add(self.slicer, 0, wx.EXPAND)
+ gridsizer.Add(self.grid, 1, wx.EXPAND)
+ self.view = gridsizer
+
+ self.Bind(EVT_ARRAY_SLICED, self.on_sliced)
+ if self.node.is_plottable():
+ self.Bind(wx.EVT_MENU, self.on_plot, id=ID_VIS_MENU_PLOT)
+
+ # Workaround for wxPython bug (see SlicerPanel.enable_spinctrls)
+ ID_WORKAROUND_TIMER = wx.NewId()
+ self.Bind(wx.EVT_TIMER, self.on_workaround_timer, id=ID_WORKAROUND_TIMER)
+ self.timer = wx.Timer(self, ID_WORKAROUND_TIMER)
+ self.timer.Start(100)
+
+ def init_toolbar(self):
+ """ Set up the toolbar at the top of the window. """
+ t_size = (24, 24)
+ plot_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_plot_24.png"), wx.BITMAP_TYPE_ANY)
+
+ self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT)
+
+ self.toolbar.SetToolBitmapSize(t_size)
+ self.toolbar.AddStretchableSpace()
+ if self.node.is_plottable():
+ self.toolbar.AddLabelTool(ID_VIS_MENU_PLOT, "Map Data", plot_bmp,
+ shortHelp="Map geographic data in a popup window",
+ longHelp="Map the geographic array data in a popup window")
+ self.toolbar.Realize()
+
+ def on_sliced(self, evt):
+ """ User has chosen to display a different part of the dataset. """
+ self.grid.Refresh()
+
+ def on_plot(self, evt):
+ """ User has chosen to plot the current selection """
+ cols = self.grid.GetSelectedCols()
+ rows = self.grid.GetSelectedRows()
+
+ # Scalar data can't be line-plotted.
+ if len(self.node.shape) == 0:
+ return
+
+ # Columns in the view are selected
+ if len(cols) != 0:
+
+ # The data is compound
+ if self.node.dtype.names is not None:
+ names = [self.grid.GetColLabelValue(x) for x in cols]
+ data = self.node[self.slicer.indices] # -> 1D compound array
+ data = [data[n] for n in names]
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+ # Plot multiple columns independently
+ else:
+ if len(self.node.shape) == 1:
+ data = [self.node[self.slicer.indices]]
+ else:
+ data = [self.node[self.slicer.indices + (c,)] for c in cols]
+
+ names = ["Col %d" % c for c in cols] if len(data) > 1 else None
+
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+
+ # Rows in view are selected
+ elif len(rows) != 0:
+
+ data = [self.node[self.slicer.indices + (slice(None, None, None), r)] for r in rows]
+ names = ["Row %d" % r for r in rows] if len(data) > 1 else None
+
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+
+ # No row or column selection. Plot everything
+ else:
+
+ data = self.node[self.slicer.indices]
+
+ # The data is compound
+ if self.node.dtype.names is not None:
+ names = [self.grid.GetColLabelValue(x) for x in xrange(self.grid.GetNumberCols())]
+ data = [data[n] for n in names]
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+ # Plot 1D
+ elif len(self.node.shape) == 1:
+ f = LinePlotFrame([data])
+ f.Show()
+
+ # Plot 2D
+ else:
+ f = ContourPlotFrame(data, extent=self.node.extent)
+ f.Show()
+
+ def on_workaround_timer(self, evt):
+ """ See slicer.enable_spinctrls docs """
+ self.timer.Destroy()
+ self.slicer.enable_spinctrls()
+
+
+class SlicerPanel(wx.Panel):
+ """
+ Holds controls for data access.
+
+ Consult the "indices" property, which returns a tuple of indices that
+ prefix the array. This will be RANK-2 elements long, unless hasfields
+ is true, in which case it will be RANK-1 elements long.
+ """
+
+ @property
+ def indices(self):
+ """ A tuple of integer indices appropriate for slicing.
+
+ Will be RANK-2 elements long, RANK-1 if compound data is in use
+ (hasfields == True).
+ """
+ return tuple([x.GetValue() for x in self.spincontrols])
+
+ def __init__(self, parent, shape, hasfields):
+ """ Create a new slicer panel.
+
+ parent: The wxPython parent window
+ shape: Shape of the data to visualize
+ hasfields: If True, the data is compound and the grid can only
+ display one axis. So, we should display an extra spinbox.
+ """
+ wx.Panel.__init__(self, parent)
+
+ self.shape = shape
+ self.hasfields = hasfields
+ self.spincontrols = []
+
+ # Rank of the underlying array
+ rank = len(shape)
+
+ # Rank displayable in the grid. If fields are present, they occupy
+ # the columns, so the data displayed is actually 1-D.
+ visible_rank = 1 if hasfields else 2
+
+ sizer = wx.BoxSizer(wx.HORIZONTAL) # Will arrange the SpinCtrls
+
+ if rank > visible_rank:
+ infotext = wx.StaticText(self, wx.ID_ANY, "Array Indexing: ")
+ sizer.Add(infotext, 0, flag=wx.EXPAND | wx.ALL, border=10)
+
+ for idx in xrange(rank - visible_rank):
+ sc = wx.SpinCtrl(self, max=shape[idx] - 1, value="0", min=0)
+ sizer.Add(sc, 0, flag=wx.EXPAND | wx.ALL, border=10)
+ sc.Disable()
+ self.spincontrols.append(sc)
+
+ self.SetSizer(sizer)
+
+ self.Bind(wx.EVT_SPINCTRL, self.on_spin)
+
+ def enable_spinctrls(self):
+ """ Unlock the spin controls.
+
+ Because of a bug in wxPython on Mac, by default the first spin control
+ has bizarre contents (and control focus) when the panel starts up.
+ Call this after a short delay (e.g. 100 ms) to enable indexing.
+ """
+ for sc in self.spincontrols:
+ sc.Enable()
+
+ def on_spin(self, evt):
+ """ Spinbox value changed; notify parent to refresh the grid. """
+ wx.PostEvent(self, ArraySlicedEvent(self.GetId()))
+
+
+class ArrayGrid(wx.grid.Grid):
+ """
+ Grid class to display the Array.
+
+ Cell contents and appearance are handled by the table model in ArrayTable.
+ """
+
+ def __init__(self, parent, node, slicer):
+ wx.grid.Grid.__init__(self, parent)
+ table = ArrayTable(node, slicer)
+ self.SetTable(table, True)
+
+ # Column selection is always allowed
+ selmode = wx.grid.Grid.wxGridSelectColumns
+
+ # Row selection is forbidden for compound types, and for
+ # scalar/1-D datasets
+ if node.dtype.names is None and len(node.shape) > 1:
+ selmode |= wx.grid.Grid.wxGridSelectRows
+
+ self.SetSelectionMode(selmode)
+
+
+class LRUTileCache(object):
+ """
+ Simple tile-based LRU cache which goes between the Grid and
+ the Array object. Caches tiles along the last 1 or 2 dimensions
+ of a dataset.
+
+ Access is via __getitem__. Because this class exists specifically
+ to support point-based callbacks for the Grid, arguments may
+ only be indices, not slices.
+ """
+
+ TILESIZE = 100 # Tiles will have shape (100,) or (100, 100)
+ MAXTILES = 50 # Max number of tiles to retain in the cache
+
+ def __init__(self, arr):
+ """ *arr* is anything implementing compass_model.Array """
+ import collections
+ self.cache = collections.OrderedDict()
+ self.arr = arr
+
+ def __getitem__(self, args):
+ """ Restricted to an index or tuple of indices. """
+
+ if not isinstance(args, tuple):
+ args = (args,)
+
+ # Split off the last 1 or 2 dimensions
+ coarse_position, fine_position = args[0:-2], args[-2:]
+
+ def clip(x):
+ """ Round down to nearest TILESIZE; takes e.g. 181 -> 100 """
+ return (x // self.TILESIZE) * self.TILESIZE
+
+ # Tuple with index of tile corner
+ tile_key = coarse_position + tuple(clip(x) for x in fine_position)
+
+ # Slice which will be applied to dataset to retrieve tile
+ tile_slice = coarse_position + tuple(slice(clip(x), clip(x) + self.TILESIZE) for x in fine_position)
+
+ # Index applied to tile to retrieve the desired data point
+ tile_data_index = tuple(x % self.TILESIZE for x in fine_position)
+
+ # Case 1: Add tile to cache, ejecting oldest tile if needed
+ if not tile_key in self.cache:
+
+ if len(self.cache) >= self.MAXTILES:
+ self.cache.popitem(last=False)
+
+ tile = self.arr[tile_slice]
+ self.cache[tile_key] = tile
+
+ # Case 2: Mark the tile as recently accessed
+ else:
+ tile = self.cache.pop(tile_key)
+ self.cache[tile_key] = tile
+
+ return tile[tile_data_index]
+
+
+class ArrayTable(wx.grid.PyGridTableBase):
+ """
+ "Table" class which provides data and metadata for the grid to display.
+
+ The methods defined here define the contents of the table, as well as
+ the number of rows, columns and their values.
+ """
+
+ def __init__(self, node, slicer):
+ """ Create a new Table instance for use with a grid control.
+
+ node: An compass_model.Array implementation instance.
+ slicer: An instance of SlicerPanel, so we can see what indices the
+ user has requested.
+ """
+ wx.grid.PyGridTableBase.__init__(self)
+
+ self.node = node
+ self.slicer = slicer
+
+ self.rank = len(node.shape)
+ self.names = node.dtype.names
+
+ self.cache = LRUTileCache(self.node)
+
+ def GetNumberRows(self):
+ """ Callback for number of rows displayed by the grid control """
+ if self.rank == 0:
+ return 1
+ return self.node.shape[-1]
+
+ def GetNumberCols(self):
+ """ Callback for number of columns displayed by the grid control.
+
+ Note that if compound data is in use, columns display the field names.
+ """
+ if self.names is not None:
+ return len(self.names)
+ if self.rank < 2:
+ return 1
+ return self.node.shape[-2]
+
+ def GetValue(self, row, col):
+ """ Callback which provides data to the Grid.
+
+ row, col: Integers giving row and column position (0-based).
+ """
+ # Scalar case
+ if self.rank == 0:
+ data = self.node[()]
+ if self.names is None:
+ return data
+ return data[col]
+
+ # 1D case
+ if self.rank == 1:
+ data = self.cache[row]
+ if self.names is None:
+ return data
+ return data[self.names[col]]
+
+ # ND case. Watch out for compound mode!
+ if self.names is None:
+ args = self.slicer.indices + (col, row)
+ else:
+ args = self.slicer.indices + (row,)
+
+ data = self.cache[args]
+ if self.names is None:
+ return data
+ return data[self.names[col]]
+
+ def GetRowLabelValue(self, row):
+ """ Callback for row labels.
+
+ Row number is used unless the data is scalar.
+ """
+ if self.rank == 0:
+ return "Value"
+ return str(row)
+
+ def GetColLabelValue(self, col):
+ """ Callback for column labels.
+
+ Column number is used, except for scalar or 1D data, or if we're
+ displaying field names in the columns.
+ """
+ if self.names is not None:
+ return self.names[col]
+ if self.rank == 0 or self.rank == 1:
+ return "Value"
+ return str(col)
diff --git a/hdf_compass/compass_viewer/geo_array/plot.py b/hdf_compass/compass_viewer/geo_array/plot.py
new file mode 100644
index 0000000..29ca013
--- /dev/null
+++ b/hdf_compass/compass_viewer/geo_array/plot.py
@@ -0,0 +1,207 @@
+##############################################################################
+# Copyright by The HDF Group. #
+# All rights reserved. #
+# #
+# This file is part of the HDF Compass Viewer. The full HDF Compass #
+# copyright notice, including terms governing use, modification, and #
+# terms governing use, modification, and redistribution, is contained in #
+# the file COPYING, which can be found at the root of the source code #
+# distribution tree. If you do not have access to this file, you may #
+# request a copy from help at hdfgroup.org. #
+##############################################################################
+
+"""
+Matplotlib window with toolbar.
+"""
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import numpy as np
+import wx
+import cartopy.crs as ccrs
+from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
+import matplotlib.pyplot as plt
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigCanvas
+from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar
+
+import logging
+log = logging.getLogger(__name__)
+
+from ..frame import BaseFrame
+
+ID_VIEW_CMAP_JET = wx.NewId() # default
+ID_VIEW_CMAP_BONE = wx.NewId()
+ID_VIEW_CMAP_GIST_EARTH = wx.NewId()
+ID_VIEW_CMAP_OCEAN = wx.NewId()
+ID_VIEW_CMAP_RAINBOW = wx.NewId()
+ID_VIEW_CMAP_RDYLGN = wx.NewId()
+ID_VIEW_CMAP_WINTER = wx.NewId()
+
+
+class PlotFrame(BaseFrame):
+ """ Base class for Matplotlib plot windows.
+
+ Override draw_figure() to plot your figure on the provided axes.
+ """
+
+ def __init__(self, data, title="a title"):
+ """ Create a new Matplotlib plotting window for a 1D line plot """
+
+ log.debug(self.__class__.__name__)
+ BaseFrame.__init__(self, id=wx.ID_ANY, title=title, size=(800, 400))
+
+ self.data = data
+
+ self.panel = wx.Panel(self)
+
+ self.dpi = 100
+ self.fig = Figure((6.0, 4.0), dpi=self.dpi)
+ self.canvas = FigCanvas(self.panel, -1, self.fig)
+
+ self.axes = self.fig.add_subplot(111, projection=ccrs.PlateCarree())
+ self.toolbar = NavigationToolbar(self.canvas)
+
+ self.vbox = wx.BoxSizer(wx.VERTICAL)
+ self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
+ self.vbox.Add(self.toolbar, 0, wx.EXPAND)
+
+ self.panel.SetSizer(self.vbox)
+ self.vbox.Fit(self)
+
+ self.draw_figure()
+
+ def draw_figure(self):
+ raise NotImplementedError
+
+
+class LinePlotFrame(PlotFrame):
+ def __init__(self, data, names=None, title="Line Plot"):
+ self.names = names
+ PlotFrame.__init__(self, data, title)
+
+ def draw_figure(self):
+
+ lines = [self.axes.plot(d)[0] for d in self.data]
+ if self.names is not None:
+ for n in self.names:
+ self.axes.legend(tuple(lines), tuple(self.names))
+
+
+class ContourPlotFrame(PlotFrame):
+ def __init__(self, data, extent, names=None, title="Contour Map"):
+ self.geo_extent = extent
+ log.debug("Extent: %f, %f, %f, %f" % self.geo_extent)
+ # need to be set before calling the parent (need for plotting)
+ self.colormap = "jet"
+ self.cb = None # matplotlib color-bar
+ self.xx = None
+ self.yy = None
+
+ PlotFrame.__init__(self, data, title)
+
+ self.cmap_menu = wx.Menu()
+ self.cmap_menu.Append(ID_VIEW_CMAP_JET, "Jet", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_BONE, "Bone", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_GIST_EARTH, "Gist Earth", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_OCEAN, "Ocean", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_RAINBOW, "Rainbow", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_RDYLGN, "Red-Yellow-Green", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_WINTER, "Winter", kind=wx.ITEM_RADIO)
+ self.add_menu(self.cmap_menu, "Colormap")
+
+ self.Bind(wx.EVT_MENU, self.on_cmap_jet, id=ID_VIEW_CMAP_JET)
+ self.Bind(wx.EVT_MENU, self.on_cmap_bone, id=ID_VIEW_CMAP_BONE)
+ self.Bind(wx.EVT_MENU, self.on_cmap_gist_earth, id=ID_VIEW_CMAP_GIST_EARTH)
+ self.Bind(wx.EVT_MENU, self.on_cmap_ocean, id=ID_VIEW_CMAP_OCEAN)
+ self.Bind(wx.EVT_MENU, self.on_cmap_rainbow, id=ID_VIEW_CMAP_RAINBOW)
+ self.Bind(wx.EVT_MENU, self.on_cmap_rdylgn, id=ID_VIEW_CMAP_RDYLGN)
+ self.Bind(wx.EVT_MENU, self.on_cmap_winter, id=ID_VIEW_CMAP_WINTER)
+
+ self.status_bar = wx.StatusBar(self, -1)
+ self.status_bar.SetFieldsCount(2)
+ self.SetStatusBar(self.status_bar)
+
+ self.canvas.mpl_connect('motion_notify_event', self.update_status_bar)
+ self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.change_cursor)
+
+ def on_cmap_jet(self, evt):
+ log.debug("cmap: jet")
+ self.colormap = "jet"
+ self._refresh_plot()
+
+ def on_cmap_bone(self, evt):
+ log.debug("cmap: bone")
+ self.colormap = "bone"
+ self._refresh_plot()
+
+ def on_cmap_gist_earth(self, evt):
+ log.debug("cmap: gist_earth")
+ self.colormap = "gist_earth"
+ self._refresh_plot()
+
+ def on_cmap_ocean(self, evt):
+ log.debug("cmap: ocean")
+ self.colormap = "ocean"
+ self._refresh_plot()
+
+ def on_cmap_rainbow(self, evt):
+ log.debug("cmap: rainbow")
+ self.colormap = "rainbow"
+ self._refresh_plot()
+
+ def on_cmap_rdylgn(self, evt):
+ log.debug("cmap: RdYlGn")
+ self.colormap = "RdYlGn"
+ self._refresh_plot()
+
+ def on_cmap_winter(self, evt):
+ log.debug("cmap: winter")
+ self.colormap = "winter"
+ self._refresh_plot()
+
+ def _refresh_plot(self):
+ self.draw_figure()
+ self.canvas.draw()
+
+ def draw_figure(self):
+ max_elements = 500 # don't attempt plot more than 500x500 elements
+ rows = self.data.shape[0]
+ cols = self.data.shape[1]
+ row_stride = rows // max_elements + 1
+ col_stride = cols // max_elements + 1
+ data = self.data[::row_stride, ::col_stride]
+ self.xx = np.linspace(self.geo_extent[0], self.geo_extent[1], data.shape[1])
+ self.yy = np.linspace(self.geo_extent[2], self.geo_extent[3], data.shape[0])
+ img = self.axes.contourf(self.xx, self.yy, data, 25, cmap=plt.cm.get_cmap(self.colormap),
+ transform=ccrs.PlateCarree())
+ self.axes.coastlines(resolution='50m', color='gray', linewidth=1)
+ # add gridlines with labels only on the left and on the bottom
+ grl = self.axes.gridlines(crs=ccrs.PlateCarree(), color='gray', draw_labels=True)
+ grl.xformatter = LONGITUDE_FORMATTER
+ grl.yformatter = LATITUDE_FORMATTER
+ grl.xlabel_style = {'size': 8}
+ grl.ylabel_style = {'size': 8}
+ grl.ylabels_right = False
+ grl.xlabels_top = False
+
+ if self.cb:
+ self.cb.on_mappable_changed(img)
+ else:
+ self.cb = plt.colorbar(img, ax=self.axes)
+ self.cb.ax.tick_params(labelsize=8)
+
+ def change_cursor(self, event):
+ self.canvas.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
+
+ @staticmethod
+ def _find_nearest(arr, value):
+ """ Helper function to find the nearest value in an array """
+ return (np.abs(arr - value)).argmin()
+
+ def update_status_bar(self, event):
+ msg = str()
+ if event.inaxes:
+ x, y = event.xdata, event.ydata
+ z = self.data[self._find_nearest(self.yy, y), self._find_nearest(self.xx, x)]
+ msg = "x= %f, y= %f, z= %f" % (x, y, z)
+ self.status_bar.SetStatusText(msg, 1)
diff --git a/hdf_compass/compass_model/__init__.py b/hdf_compass/compass_viewer/geo_surface/__init__.py
similarity index 87%
copy from hdf_compass/compass_model/__init__.py
copy to hdf_compass/compass_viewer/geo_surface/__init__.py
index 0853f0a..0898d00 100644
--- a/hdf_compass/compass_model/__init__.py
+++ b/hdf_compass/compass_viewer/geo_surface/__init__.py
@@ -11,8 +11,8 @@
##############################################################################
from __future__ import absolute_import, division, print_function, unicode_literals
-from .model import get_stores, push, Store, Node, Container, KeyValue, Array, Text, Xml, Image, Unknown
+from .frame import GeoSurfaceFrame
import logging
log = logging.getLogger(__name__)
-log.addHandler(logging.NullHandler())
+log.addHandler(logging.NullHandler())
\ No newline at end of file
diff --git a/hdf_compass/compass_viewer/geo_surface/frame.py b/hdf_compass/compass_viewer/geo_surface/frame.py
new file mode 100644
index 0000000..02eabe5
--- /dev/null
+++ b/hdf_compass/compass_viewer/geo_surface/frame.py
@@ -0,0 +1,416 @@
+##############################################################################
+# Copyright by The HDF Group. #
+# All rights reserved. #
+# #
+# This file is part of the HDF Compass Viewer. The full HDF Compass #
+# copyright notice, including terms governing use, modification, and #
+# terms governing use, modification, and redistribution, is contained in #
+# the file COPYING, which can be found at the root of the source code #
+# distribution tree. If you do not have access to this file, you may #
+# request a copy from help at hdfgroup.org. #
+##############################################################################
+"""
+Implements a viewer frame for compass_model.Array.
+"""
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import wx
+import wx.grid
+from wx.lib.newevent import NewCommandEvent
+
+import os
+import logging
+
+log = logging.getLogger(__name__)
+
+from ..frame import NodeFrame
+from .plot import LinePlotFrame, ContourPlotFrame
+
+
+# Indicates that the slicing selection may have changed.
+# These events are emitted by the SlicerPanel.
+ArraySlicedEvent, EVT_ARRAY_SLICED = NewCommandEvent()
+
+# Menu and button IDs
+ID_VIS_MENU_PLOT = wx.NewId()
+
+
+class GeoSurfaceFrame(NodeFrame):
+ """
+ Top-level frame displaying objects of type compass_model.Array.
+
+ From top to bottom, has:
+
+ 1. Toolbar (see ArrayFrame.init_toolbar)
+ 2. SlicerPanel, with controls for changing what's displayed.
+ 3. An ArrayGrid, which displays the data in a spreadsheet-like view.
+ """
+
+ def __init__(self, node, pos=None):
+ """ Create a new array viewer to display the node. """
+ NodeFrame.__init__(self, node, size=(800, 400), title=node.display_name, pos=pos)
+
+ self.node = node
+
+ # Update the menu
+ vis_menu = wx.Menu()
+ if self.node.is_plottable():
+ vis_menu.Append(ID_VIS_MENU_PLOT, "Map Surface\tCtrl-D")
+ self.add_menu(vis_menu, "Visualize")
+ # Initialize the toolbar
+ self.init_toolbar()
+
+ # The Slicer is the panel with indexing controls
+ self.slicer = SlicerPanel(self, node.shape, node.dtype.fields is not None)
+ # Create the grid array
+ self.grid = ArrayGrid(self, node, self.slicer)
+ # Sizer for slicer and grid
+ gridsizer = wx.BoxSizer(wx.VERTICAL)
+ gridsizer.Add(self.slicer, 0, wx.EXPAND)
+ gridsizer.Add(self.grid, 1, wx.EXPAND)
+ self.view = gridsizer
+
+ self.Bind(EVT_ARRAY_SLICED, self.on_sliced)
+ if self.node.is_plottable():
+ self.Bind(wx.EVT_MENU, self.on_plot, id=ID_VIS_MENU_PLOT)
+
+ # Workaround for wxPython bug (see SlicerPanel.enable_spinctrls)
+ ID_WORKAROUND_TIMER = wx.NewId()
+ self.Bind(wx.EVT_TIMER, self.on_workaround_timer, id=ID_WORKAROUND_TIMER)
+ self.timer = wx.Timer(self, ID_WORKAROUND_TIMER)
+ self.timer.Start(100)
+
+ def init_toolbar(self):
+ """ Set up the toolbar at the top of the window. """
+ t_size = (24, 24)
+ plot_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_plot_24.png"), wx.BITMAP_TYPE_ANY)
+
+ self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT)
+
+ self.toolbar.SetToolBitmapSize(t_size)
+ self.toolbar.AddStretchableSpace()
+ if self.node.is_plottable():
+ self.toolbar.AddLabelTool(ID_VIS_MENU_PLOT, "Map Surface", plot_bmp,
+ shortHelp="Map geographic surface in a popup window",
+ longHelp="Map the geographic surface array in a popup window")
+ self.toolbar.Realize()
+
+ def on_sliced(self, evt):
+ """ User has chosen to display a different part of the dataset. """
+ self.grid.Refresh()
+
+ def on_plot(self, evt):
+ """ User has chosen to plot the current selection """
+ cols = self.grid.GetSelectedCols()
+ rows = self.grid.GetSelectedRows()
+
+ # Scalar data can't be line-plotted.
+ if len(self.node.shape) == 0:
+ return
+
+ # Columns in the view are selected
+ if len(cols) != 0:
+
+ # The data is compound
+ if self.node.dtype.names is not None:
+ names = [self.grid.GetColLabelValue(x) for x in cols]
+ data = self.node[self.slicer.indices] # -> 1D compound array
+ data = [data[n] for n in names]
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+ # Plot multiple columns independently
+ else:
+ if len(self.node.shape) == 1:
+ data = [self.node[self.slicer.indices]]
+ else:
+ data = [self.node[self.slicer.indices + (c,)] for c in cols]
+
+ names = ["Col %d" % c for c in cols] if len(data) > 1 else None
+
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+
+ # Rows in view are selected
+ elif len(rows) != 0:
+
+ data = [self.node[self.slicer.indices + (slice(None, None, None), r)] for r in rows]
+ names = ["Row %d" % r for r in rows] if len(data) > 1 else None
+
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+
+ # No row or column selection. Plot everything
+ else:
+
+ data = self.node[self.slicer.indices]
+
+ # The data is compound
+ if self.node.dtype.names is not None:
+ names = [self.grid.GetColLabelValue(x) for x in xrange(self.grid.GetNumberCols())]
+ data = [data[n] for n in names]
+ f = LinePlotFrame(data, names)
+ f.Show()
+
+ # Plot 1D
+ elif len(self.node.shape) == 1:
+ f = LinePlotFrame([data])
+ f.Show()
+
+ # Plot 2D
+ else:
+ f = ContourPlotFrame(data, extent=self.node.extent)
+ f.Show()
+
+ def on_workaround_timer(self, evt):
+ """ See slicer.enable_spinctrls docs """
+ self.timer.Destroy()
+ self.slicer.enable_spinctrls()
+
+
+class SlicerPanel(wx.Panel):
+ """
+ Holds controls for data access.
+
+ Consult the "indices" property, which returns a tuple of indices that
+ prefix the array. This will be RANK-2 elements long, unless hasfields
+ is true, in which case it will be RANK-1 elements long.
+ """
+
+ @property
+ def indices(self):
+ """ A tuple of integer indices appropriate for slicing.
+
+ Will be RANK-2 elements long, RANK-1 if compound data is in use
+ (hasfields == True).
+ """
+ return tuple([x.GetValue() for x in self.spincontrols])
+
+ def __init__(self, parent, shape, hasfields):
+ """ Create a new slicer panel.
+
+ parent: The wxPython parent window
+ shape: Shape of the data to visualize
+ hasfields: If True, the data is compound and the grid can only
+ display one axis. So, we should display an extra spinbox.
+ """
+ wx.Panel.__init__(self, parent)
+
+ self.shape = shape
+ self.hasfields = hasfields
+ self.spincontrols = []
+
+ # Rank of the underlying array
+ rank = len(shape)
+
+ # Rank displayable in the grid. If fields are present, they occupy
+ # the columns, so the data displayed is actually 1-D.
+ visible_rank = 1 if hasfields else 2
+
+ sizer = wx.BoxSizer(wx.HORIZONTAL) # Will arrange the SpinCtrls
+
+ if rank > visible_rank:
+ infotext = wx.StaticText(self, wx.ID_ANY, "Array Indexing: ")
+ sizer.Add(infotext, 0, flag=wx.EXPAND | wx.ALL, border=10)
+
+ for idx in xrange(rank - visible_rank):
+ sc = wx.SpinCtrl(self, max=shape[idx] - 1, value="0", min=0)
+ sizer.Add(sc, 0, flag=wx.EXPAND | wx.ALL, border=10)
+ sc.Disable()
+ self.spincontrols.append(sc)
+
+ self.SetSizer(sizer)
+
+ self.Bind(wx.EVT_SPINCTRL, self.on_spin)
+
+ def enable_spinctrls(self):
+ """ Unlock the spin controls.
+
+ Because of a bug in wxPython on Mac, by default the first spin control
+ has bizarre contents (and control focus) when the panel starts up.
+ Call this after a short delay (e.g. 100 ms) to enable indexing.
+ """
+ for sc in self.spincontrols:
+ sc.Enable()
+
+ def on_spin(self, evt):
+ """ Spinbox value changed; notify parent to refresh the grid. """
+ wx.PostEvent(self, ArraySlicedEvent(self.GetId()))
+
+
+class ArrayGrid(wx.grid.Grid):
+ """
+ Grid class to display the Array.
+
+ Cell contents and appearance are handled by the table model in ArrayTable.
+ """
+
+ def __init__(self, parent, node, slicer):
+ wx.grid.Grid.__init__(self, parent)
+ table = ArrayTable(node, slicer)
+ self.SetTable(table, True)
+
+ # Column selection is always allowed
+ selmode = wx.grid.Grid.wxGridSelectColumns
+
+ # Row selection is forbidden for compound types, and for
+ # scalar/1-D datasets
+ if node.dtype.names is None and len(node.shape) > 1:
+ selmode |= wx.grid.Grid.wxGridSelectRows
+
+ self.SetSelectionMode(selmode)
+
+
+class LRUTileCache(object):
+ """
+ Simple tile-based LRU cache which goes between the Grid and
+ the Array object. Caches tiles along the last 1 or 2 dimensions
+ of a dataset.
+
+ Access is via __getitem__. Because this class exists specifically
+ to support point-based callbacks for the Grid, arguments may
+ only be indices, not slices.
+ """
+
+ TILESIZE = 100 # Tiles will have shape (100,) or (100, 100)
+ MAXTILES = 50 # Max number of tiles to retain in the cache
+
+ def __init__(self, arr):
+ """ *arr* is anything implementing compass_model.Array """
+ import collections
+ self.cache = collections.OrderedDict()
+ self.arr = arr
+
+ def __getitem__(self, args):
+ """ Restricted to an index or tuple of indices. """
+
+ if not isinstance(args, tuple):
+ args = (args,)
+
+ # Split off the last 1 or 2 dimensions
+ coarse_position, fine_position = args[0:-2], args[-2:]
+
+ def clip(x):
+ """ Round down to nearest TILESIZE; takes e.g. 181 -> 100 """
+ return (x // self.TILESIZE) * self.TILESIZE
+
+ # Tuple with index of tile corner
+ tile_key = coarse_position + tuple(clip(x) for x in fine_position)
+
+ # Slice which will be applied to dataset to retrieve tile
+ tile_slice = coarse_position + tuple(slice(clip(x), clip(x) + self.TILESIZE) for x in fine_position)
+
+ # Index applied to tile to retrieve the desired data point
+ tile_data_index = tuple(x % self.TILESIZE for x in fine_position)
+
+ # Case 1: Add tile to cache, ejecting oldest tile if needed
+ if not tile_key in self.cache:
+
+ if len(self.cache) >= self.MAXTILES:
+ self.cache.popitem(last=False)
+
+ tile = self.arr[tile_slice]
+ self.cache[tile_key] = tile
+
+ # Case 2: Mark the tile as recently accessed
+ else:
+ tile = self.cache.pop(tile_key)
+ self.cache[tile_key] = tile
+
+ return tile[tile_data_index]
+
+
+class ArrayTable(wx.grid.PyGridTableBase):
+ """
+ "Table" class which provides data and metadata for the grid to display.
+
+ The methods defined here define the contents of the table, as well as
+ the number of rows, columns and their values.
+ """
+
+ def __init__(self, node, slicer):
+ """ Create a new Table instance for use with a grid control.
+
+ node: An compass_model.Array implementation instance.
+ slicer: An instance of SlicerPanel, so we can see what indices the
+ user has requested.
+ """
+ wx.grid.PyGridTableBase.__init__(self)
+
+ self.node = node
+ self.slicer = slicer
+
+ self.rank = len(node.shape)
+ self.names = node.dtype.names
+
+ self.cache = LRUTileCache(self.node)
+
+ def GetNumberRows(self):
+ """ Callback for number of rows displayed by the grid control """
+ if self.rank == 0:
+ return 1
+ return self.node.shape[-1]
+
+ def GetNumberCols(self):
+ """ Callback for number of columns displayed by the grid control.
+
+ Note that if compound data is in use, columns display the field names.
+ """
+ if self.names is not None:
+ return len(self.names)
+ if self.rank < 2:
+ return 1
+ return self.node.shape[-2]
+
+ def GetValue(self, row, col):
+ """ Callback which provides data to the Grid.
+
+ row, col: Integers giving row and column position (0-based).
+ """
+ # Scalar case
+ if self.rank == 0:
+ data = self.node[()]
+ if self.names is None:
+ return data
+ return data[col]
+
+ # 1D case
+ if self.rank == 1:
+ data = self.cache[row]
+ if self.names is None:
+ return data
+ return data[self.names[col]]
+
+ # ND case. Watch out for compound mode!
+ if self.names is None:
+ args = self.slicer.indices + (col, row)
+ else:
+ args = self.slicer.indices + (row,)
+
+ data = self.cache[args]
+ if self.names is None:
+ return data
+ return data[self.names[col]]
+
+ def GetRowLabelValue(self, row):
+ """ Callback for row labels.
+
+ Row number is used unless the data is scalar.
+ """
+ if self.rank == 0:
+ return "Value"
+ return str(row)
+
+ def GetColLabelValue(self, col):
+ """ Callback for column labels.
+
+ Column number is used, except for scalar or 1D data, or if we're
+ displaying field names in the columns.
+ """
+ if self.names is not None:
+ return self.names[col]
+ if self.rank == 0 or self.rank == 1:
+ return "Value"
+ return str(col)
diff --git a/hdf_compass/compass_viewer/geo_surface/plot.py b/hdf_compass/compass_viewer/geo_surface/plot.py
new file mode 100644
index 0000000..17e7a91
--- /dev/null
+++ b/hdf_compass/compass_viewer/geo_surface/plot.py
@@ -0,0 +1,213 @@
+##############################################################################
+# Copyright by The HDF Group. #
+# All rights reserved. #
+# #
+# This file is part of the HDF Compass Viewer. The full HDF Compass #
+# copyright notice, including terms governing use, modification, and #
+# terms governing use, modification, and redistribution, is contained in #
+# the file COPYING, which can be found at the root of the source code #
+# distribution tree. If you do not have access to this file, you may #
+# request a copy from help at hdfgroup.org. #
+##############################################################################
+
+"""
+Matplotlib window with toolbar.
+"""
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import numpy as np
+import wx
+import cartopy.crs as ccrs
+from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
+import matplotlib.pyplot as plt
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigCanvas
+from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar
+from matplotlib.colors import LinearSegmentedColormap
+from matplotlib import cm
+from matplotlib.colors import LightSource
+
+import logging
+log = logging.getLogger(__name__)
+
+from ..frame import BaseFrame
+
+np.seterr(divide='ignore', invalid='ignore')
+
+ID_VIEW_CMAP_BAG = wx.NewId() # default
+ID_VIEW_CMAP_JET = wx.NewId()
+ID_VIEW_CMAP_BONE = wx.NewId()
+ID_VIEW_CMAP_GIST_EARTH = wx.NewId()
+ID_VIEW_CMAP_OCEAN = wx.NewId()
+ID_VIEW_CMAP_RAINBOW = wx.NewId()
+ID_VIEW_CMAP_RDYLGN = wx.NewId()
+ID_VIEW_CMAP_WINTER = wx.NewId()
+
+
+class PlotFrame(BaseFrame):
+ """ Base class for Matplotlib plot windows.
+
+ Override draw_figure() to plot your figure on the provided axes.
+ """
+
+ def __init__(self, data, title="a title"):
+ """ Create a new Matplotlib plotting window for a 1D line plot """
+
+ log.debug(self.__class__.__name__)
+ BaseFrame.__init__(self, id=wx.ID_ANY, title=title, size=(800, 400))
+
+ self.data = data
+
+ self.panel = wx.Panel(self)
+
+ self.dpi = 100
+ self.fig = Figure((6.0, 4.0), dpi=self.dpi)
+ self.canvas = FigCanvas(self.panel, -1, self.fig)
+
+ self.axes = self.fig.add_subplot(111, projection=ccrs.PlateCarree())
+ self.toolbar = NavigationToolbar(self.canvas)
+
+ self.vbox = wx.BoxSizer(wx.VERTICAL)
+ self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
+ self.vbox.Add(self.toolbar, 0, wx.EXPAND)
+
+ self.panel.SetSizer(self.vbox)
+ self.vbox.Fit(self)
+
+ self.draw_figure()
+
+ def draw_figure(self):
+ raise NotImplementedError
+
+
+class LinePlotFrame(PlotFrame):
+ def __init__(self, data, names=None, title="Line Plot"):
+ self.names = names
+ PlotFrame.__init__(self, data, title)
+
+ def draw_figure(self):
+
+ lines = [self.axes.plot(d)[0] for d in self.data]
+ if self.names is not None:
+ for n in self.names:
+ self.axes.legend(tuple(lines), tuple(self.names))
+
+
+class ContourPlotFrame(PlotFrame):
+ def __init__(self, data, extent, names=None, title="Surface Map"):
+ self.geo_extent = extent
+ log.debug("Extent: %f, %f, %f, %f" % self.geo_extent)
+ # need to be set before calling the parent (need for plotting)
+ self.colormap = LinearSegmentedColormap.from_list("BAG",
+ ["#63006c", "#2b4ef4", "#2f73ff", "#4b8af4", "#bee2bf"])
+ # register a new colormap
+ cm.register_cmap(cmap=self.colormap)
+ self.cb = None # matplotlib color-bar
+ self.xx = None
+ self.yy = None
+
+ PlotFrame.__init__(self, data, title)
+
+ self.cmap_menu = wx.Menu()
+ self.cmap_menu.Append(ID_VIEW_CMAP_BAG, "BAG", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_JET, "Jet", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_BONE, "Bone", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_GIST_EARTH, "Gist Earth", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_OCEAN, "Ocean", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_RAINBOW, "Rainbow", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_RDYLGN, "Red-Yellow-Green", kind=wx.ITEM_RADIO)
+ self.cmap_menu.Append(ID_VIEW_CMAP_WINTER, "Winter", kind=wx.ITEM_RADIO)
+ self.add_menu(self.cmap_menu, "Colormap")
+
+ self.Bind(wx.EVT_MENU, self.on_cmap_bag, id=ID_VIEW_CMAP_BAG)
+ self.Bind(wx.EVT_MENU, self.on_cmap_jet, id=ID_VIEW_CMAP_JET)
+ self.Bind(wx.EVT_MENU, self.on_cmap_bone, id=ID_VIEW_CMAP_BONE)
+ self.Bind(wx.EVT_MENU, self.on_cmap_gist_earth, id=ID_VIEW_CMAP_GIST_EARTH)
+ self.Bind(wx.EVT_MENU, self.on_cmap_ocean, id=ID_VIEW_CMAP_OCEAN)
+ self.Bind(wx.EVT_MENU, self.on_cmap_rainbow, id=ID_VIEW_CMAP_RAINBOW)
+ self.Bind(wx.EVT_MENU, self.on_cmap_rdylgn, id=ID_VIEW_CMAP_RDYLGN)
+ self.Bind(wx.EVT_MENU, self.on_cmap_winter, id=ID_VIEW_CMAP_WINTER)
+
+ self.status_bar = wx.StatusBar(self, -1)
+ self.status_bar.SetFieldsCount(2)
+ self.SetStatusBar(self.status_bar)
+
+ self.canvas.mpl_connect('motion_notify_event', self.update_status_bar)
+ self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.change_cursor)
+
+ def on_cmap_bag(self, evt):
+ log.debug("cmap: bag")
+ self.colormap = cm.get_cmap("BAG")
+ self._refresh_plot()
+
+ def on_cmap_jet(self, evt):
+ log.debug("cmap: jet")
+ self.colormap = cm.get_cmap("jet")
+ self._refresh_plot()
+
+ def on_cmap_bone(self, evt):
+ log.debug("cmap: bone")
+ self.colormap = cm.get_cmap("bone")
+ self._refresh_plot()
+
+ def on_cmap_gist_earth(self, evt):
+ log.debug("cmap: gist_earth")
+ self.colormap = cm.get_cmap("gist_earth")
+ self._refresh_plot()
+
+ def on_cmap_ocean(self, evt):
+ log.debug("cmap: ocean")
+ self.colormap = cm.get_cmap("ocean")
+ self._refresh_plot()
+
+ def on_cmap_rainbow(self, evt):
+ log.debug("cmap: rainbow")
+ self.colormap = cm.get_cmap("rainbow")
+ self._refresh_plot()
+
+ def on_cmap_rdylgn(self, evt):
+ log.debug("cmap: RdYlGn")
+ self.colormap = cm.get_cmap("RdYlGn")
+ self._refresh_plot()
+
+ def on_cmap_winter(self, evt):
+ log.debug("cmap: winter")
+ self.colormap = cm.get_cmap("winter")
+ self._refresh_plot()
+
+ def _refresh_plot(self):
+ self.draw_figure()
+ self.canvas.draw()
+
+ def draw_figure(self):
+ ls = LightSource(azdeg=315, altdeg=45)
+ blended_surface = ls.shade(self.data, cmap=self.colormap, vert_exag=5, blend_mode="overlay",
+ vmin=np.nanmin(self.data), vmax=np.nanmax(self.data))
+ self.axes.coastlines(resolution='50m', color='gray', linewidth=1)
+ img = self.axes.imshow(blended_surface, origin='lower', cmap=self.colormap,
+ extent=self.geo_extent, transform=ccrs.PlateCarree())
+ img.set_clim(vmin=np.nanmin(self.data), vmax=np.nanmax(self.data))
+ # add gridlines with labels only on the left and on the bottom
+ grl = self.axes.gridlines(crs=ccrs.PlateCarree(), color='gray', draw_labels=True)
+ grl.xformatter = LONGITUDE_FORMATTER
+ grl.yformatter = LATITUDE_FORMATTER
+ grl.xlabel_style = {'size': 8}
+ grl.ylabel_style = {'size': 8}
+ grl.ylabels_right = False
+ grl.xlabels_top = False
+
+ if self.cb:
+ self.cb.on_mappable_changed(img)
+ else:
+ self.cb = plt.colorbar(img, ax=self.axes)
+ self.cb.ax.tick_params(labelsize=8)
+
+ def change_cursor(self, event):
+ self.canvas.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
+
+ def update_status_bar(self, event):
+ msg = str()
+ if event.inaxes:
+ x, y = event.xdata, event.ydata
+ msg = self.axes.format_coord(x, y)
+ self.status_bar.SetStatusText(msg, 1)
diff --git a/hdf_compass/compass_viewer/viewer.py b/hdf_compass/compass_viewer/viewer.py
index e521419..d0876b8 100644
--- a/hdf_compass/compass_viewer/viewer.py
+++ b/hdf_compass/compass_viewer/viewer.py
@@ -31,7 +31,7 @@ from hdf_compass import compass_model
from hdf_compass import utils
from .events import ID_COMPASS_OPEN
-from . import container, array, keyvalue, image, frame, text
+from . import container, array, geo_surface, geo_array, keyvalue, image, frame, text
__version__ = utils.__version__
@@ -121,6 +121,14 @@ def open_node(node, pos=None):
f = container.ContainerFrame(node, pos=new_pos)
f.Show()
+ elif isinstance(node, compass_model.GeoSurface):
+ f = geo_surface.GeoSurfaceFrame(node, pos=new_pos)
+ f.Show()
+
+ elif isinstance(node, compass_model.GeoArray):
+ f = geo_array.GeoArrayFrame(node, pos=new_pos)
+ f.Show()
+
elif isinstance(node, compass_model.Array):
f = array.ArrayFrame(node, pos=new_pos)
f.Show()
diff --git a/hdf_compass/utils/__init__.py b/hdf_compass/utils/__init__.py
index 20802d5..26f8107 100644
--- a/hdf_compass/utils/__init__.py
+++ b/hdf_compass/utils/__init__.py
@@ -19,4 +19,4 @@ log.addHandler(logging.NullHandler())
from .utils import is_darwin, is_win, is_linux, url2path, path2url, data_url
-__version__ = "0.6.0.dev1"
+__version__ = "0.6.0"
diff --git a/setup.cfg b/setup.cfg
index b70f841..1a58bba 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,19 +1,6 @@
[bumpversion]
-current_version = 0.6.0.dev1
+current_version = 0.6.0b2
files = setup.py hdf_compass/utils/__init__.py docs/conf.py HDFCompass.1file.spec
-parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:\.dev(?P<dev>\d+))?
-serialize =
- {major}.{minor}.{patch}.dev{dev}
- {major}.{minor}.{patch}
-
-[bumpversion:part:dev]
-values =
- 0
- 1
- 2
- 3
- 4
-optional_value = 4
[bdist_wheel]
universal = 0
diff --git a/setup.py b/setup.py
index 1f1863d..17a19ed 100644
--- a/setup.py
+++ b/setup.py
@@ -55,7 +55,7 @@ setup_args = dict()
setup_args['name'] = 'hdf_compass'
# The adopted versioning scheme follow PEP40
-setup_args['version'] = '0.6.0.dev1'
+setup_args['version'] = '0.6.0b2'
setup_args['url'] = 'https://github.com/HDFGroup/hdf-compass/'
setup_args['license'] = 'BSD-like license'
setup_args['author'] = 'HDFGroup'
@@ -104,9 +104,10 @@ setup_args['install_requires'] =\
[
"numpy",
"matplotlib",
+ "cartopy",
"h5py",
"wxPython",
- "hydroffice.bag",
+ "hydroffice.bag>=0.2.5",
"pydap",
"requests"
]
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/hdf-compass.git
More information about the debian-science-commits
mailing list