Forráskód Böngészése

g.gui.tplot: backported by grass trunk

git-svn-id: https://svn.osgeo.org/grass/grass/branches/releasebranch_7_0@66934 15284696-431f-4ddb-bdfa-cd5b030d7da7
Luca Delucchi 9 éve
szülő
commit
65107d5559

+ 3 - 3
gui/wxpython/Makefile

@@ -1,6 +1,6 @@
 MODULE_TOPDIR = ../..
 
-SUBDIRS = docs animation mapswipe gmodeler rlisetup psmap dbmgr vdigit iclass gcp timeline
+SUBDIRS = docs animation mapswipe gmodeler rlisetup psmap dbmgr vdigit iclass gcp timeline tplot
 EXTRA_CLEAN_FILES = menustrings.py build_ext.pyc xml/menudata.xml xml/module_tree_menudata.xml */*.pyc
 
 include $(MODULE_TOPDIR)/include/Make/Dir.make
@@ -12,7 +12,7 @@ SRCFILES := $(wildcard icons/*.py scripts/*.py xml/*) \
 	$(wildcard animation/* core/*.py dbmgr/* gcp/*.py gmodeler/* \
 	gui_core/*.py iclass/* lmgr/*.py location_wizard/*.py mapwin/*.py mapdisp/*.py \
 	mapswipe/* modules/*.py nviz/*.py psmap/* rlisetup/* timeline/* vdigit/* \
-	vnet/*.py web_services/*.py wxplot/*.py iscatt/*.py) \
+	vnet/*.py web_services/*.py wxplot/*.py iscatt/*.py tplot/*) \
 	gis_set.py gis_set_error.py wxgui.py README
 
 DSTFILES := $(patsubst %,$(DSTDIR)/%,$(SRCFILES)) \
@@ -20,7 +20,7 @@ DSTFILES := $(patsubst %,$(DSTDIR)/%,$(SRCFILES)) \
 
 PYDSTDIRS := $(patsubst %,$(DSTDIR)/%,animation core dbmgr gcp gmodeler \
 	gui_core iclass lmgr location_wizard mapwin mapdisp modules nviz psmap \
-	mapswipe vdigit wxplot web_services rlisetup vnet timeline iscatt)
+	mapswipe vdigit wxplot web_services rlisetup vnet timeline iscatt tplot)
 
 
 DSTDIRS := $(patsubst %,$(DSTDIR)/%,icons scripts xml)

+ 10 - 0
gui/wxpython/lmgr/frame.py

@@ -1616,6 +1616,16 @@ class GMFrame(wx.Frame):
         frame = TimelineFrame(None)
         frame.Show()
 
+    def OnTplotTool(self, event=None, cmd=None):
+        """Launch Temporal Plot Tool"""
+        try:
+            from tplot.frame import TplotFrame
+        except ImportError:
+            GError(parent=self, message=_("Unable to start Temporal Plot Tool."))
+            return
+        frame = TplotFrame(parent=self, giface=self._giface)
+        frame.Show()
+          
     def OnHistogram(self, event):
         """Init histogram display canvas and tools
         """

+ 5 - 0
gui/wxpython/tplot/Makefile

@@ -0,0 +1,5 @@
+MODULE_TOPDIR = ../../..
+
+include $(MODULE_TOPDIR)/include/Make/GuiScript.make
+
+default: guiscript

+ 4 - 0
gui/wxpython/tplot/__init__.py

@@ -0,0 +1,4 @@
+all = [
+    'g.gui.tplot',
+    'frame',
+    ]

+ 968 - 0
gui/wxpython/tplot/frame.py

@@ -0,0 +1,968 @@
+#!/usr/bin/env python
+
+"""
+@package frame
+
+@brief Temporal Plot Tool
+
+Classes:
+ - frame::DataCursor
+ - frame::TplotFrame
+ - frame::LookUp
+
+(C) 2012-2014 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 Luca Delucchi
+@author start stvds support Matej Krejci
+"""
+from itertools import cycle
+import numpy as np
+
+import wx
+from grass.pygrass.modules import Module
+
+import grass.script as grass
+from core.utils import _
+
+try:
+    import matplotlib
+    # The recommended way to use wx with mpl is with the WXAgg
+    # backend.
+    matplotlib.use('WXAgg')
+    from matplotlib.figure import Figure
+    from matplotlib.backends.backend_wxagg import \
+        FigureCanvasWxAgg as FigCanvas, \
+        NavigationToolbar2WxAgg as NavigationToolbar
+    import matplotlib.dates as mdates
+    from matplotlib import cbook
+except ImportError:
+    raise ImportError(_('The Temporal Plot Tool needs the "matplotlib" '
+                        '(python-matplotlib) package to be installed.'))
+
+from core.utils import _
+
+import grass.temporal as tgis
+from core.gcmd import GMessage, GError, GException, RunCommand
+from gui_core.widgets import CoordinatesValidator
+from gui_core import gselect
+from core import globalvar
+from grass.pygrass.vector.geometry import Point
+from grass.pygrass.raster import RasterRow
+from collections import OrderedDict
+from subprocess import PIPE
+try:
+    import wx.lib.agw.flatnotebook as FN
+except ImportError:
+    import wx.lib.flatnotebook as FN
+from gui_core.widgets import GNotebook
+
+ALPHA = 0.5
+COLORS = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
+
+
+def check_version(*version):
+    """Checks if given version or newer is installed"""
+    versionInstalled = []
+    for i in matplotlib.__version__.split('.'):
+        try:
+            v = int(i)
+            versionInstalled.append(v)
+        except ValueError:
+            versionInstalled.append(0)
+    if versionInstalled < list(version):
+        return False
+    else:
+        return True
+
+
+def findBetween(s, first, last):
+    try:
+        start = s.rindex(first) + len(first)
+        end = s.rindex(last, start)
+        return s[start:end]
+    except ValueError:
+        return ""
+
+
+class TplotFrame(wx.Frame):
+    """The main frame of the application"""
+
+    def __init__(self, parent, giface):
+        wx.Frame.__init__(self, parent, id=wx.ID_ANY,
+                          title=_("GRASS GIS Temporal Plot Tool"))
+
+        tgis.init(True)
+        self._giface = giface
+        self.datasetsV = None
+        self.datasetsR = None
+        # self.vectorDraw=False
+        # self.rasterDraw=False
+        self.init()
+        self._layout()
+
+        # We create a database interface here to speedup the GUI
+        self.dbif = tgis.SQLDatabaseInterfaceConnection()
+        self.dbif.connect()
+        self.Bind(wx.EVT_CLOSE, self.onClose)
+
+    def init(self):
+        self.timeDataR = OrderedDict()
+        self.timeDataV = OrderedDict()
+        self.temporalType = None
+        self.unit = None
+        self.listWhereConditions = []
+        self.plotNameListR = []
+        self.plotNameListV = []
+        self.poi = None
+
+    def __del__(self):
+        """Close the database interface and stop the messenger and C-interface
+           subprocesses.
+        """
+        if self.dbif.connected is True:
+            self.dbif.close()
+        tgis.stop_subprocesses()
+
+    def onClose(self,evt):
+        self.coorval.OnClose()
+        self.cats.OnClose()
+        self.Destroy()
+
+    def _layout(self):
+        """Creates the main panel with all the controls on it:
+             * mpl canvas
+             * mpl navigation toolbar
+             * Control panel for interaction
+        """
+        self.mainPanel = wx.Panel(self)
+        # Create the mpl Figure and FigCanvas objects.
+        # 5x4 inches, 100 dots-per-inch
+        #
+        # color =  wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
+        # ------------CANVAS AND TOOLBAR------------
+        self.fig = Figure((5.0, 4.0), facecolor=(1, 1, 1))
+        self.canvas = FigCanvas(self.mainPanel, wx.ID_ANY, self.fig)
+        # axes are initialized later
+        self.axes2d = None
+        self.axes3d = None
+
+        # Create the navigation toolbar, tied to the canvas
+        #
+        self.toolbar = NavigationToolbar(self.canvas)
+
+        #
+        # Layout
+        #
+        # ------------MAIN VERTICAL SIZER------------
+        self.vbox = wx.BoxSizer(wx.VERTICAL)
+        self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.EXPAND)
+        self.vbox.Add(self.toolbar, 0, wx.EXPAND)
+        # self.vbox.AddSpacer(10)
+
+        # ------------ADD NOTEBOOK------------
+        self.ntb = GNotebook(parent=self.mainPanel, style=FN.FNB_FANCY_TABS)
+
+        # ------------ITEMS IN NOTEBOOK PAGE (RASTER)------------------------
+
+        self.controlPanelRaster = wx.Panel(parent=self.ntb, id=wx.ID_ANY)
+        self.datasetSelectLabelR = wx.StaticText(parent=self.controlPanelRaster,
+                                                 id=wx.ID_ANY,
+                                                 label=_('Raster temporal '
+                                                         'dataset (strds)'))
+
+        self.datasetSelectR = gselect.Select(parent=self.controlPanelRaster,
+                                             id=wx.ID_ANY,
+                                             size=globalvar.DIALOG_GSELECT_SIZE,
+                                             type='strds', multiple=True)
+        self.coor = wx.StaticText(parent=self.controlPanelRaster, id=wx.ID_ANY,
+                                  label=_('X and Y coordinates separated by '
+                                          'comma:'))
+        try:
+            self._giface.GetMapWindow()
+            self.coorval = gselect.CoordinatesSelect(parent=self.controlPanelRaster,
+                                                     giface=self._giface)
+        except:
+            self.coorval = wx.TextCtrl(parent=self.controlPanelRaster,
+                                       id=wx.ID_ANY,
+                                       size=globalvar.DIALOG_TEXTCTRL_SIZE,
+                                       validator=CoordinatesValidator())
+
+        self.coorval.SetToolTipString(_("Coordinates can be obtained for example"
+                                        " by right-clicking on Map Display."))
+        self.controlPanelSizerRaster = wx.BoxSizer(wx.VERTICAL)
+        # self.controlPanelSizer.Add(wx.StaticText(self.panel, id=wx.ID_ANY,
+        # label=_("Select space time raster dataset(s):")),
+        # pos=(0, 0), flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
+        self.controlPanelSizerRaster.Add(self.datasetSelectLabelR,
+                                         flag=wx.EXPAND)
+        self.controlPanelSizerRaster.Add(self.datasetSelectR, flag=wx.EXPAND)
+
+        self.controlPanelSizerRaster.Add(self.coor, flag=wx.EXPAND)
+        self.controlPanelSizerRaster.Add(self.coorval, flag=wx.EXPAND)
+
+        self.controlPanelRaster.SetSizer(self.controlPanelSizerRaster)
+        self.controlPanelSizerRaster.Fit(self)
+        self.ntb.AddPage(page=self.controlPanelRaster, text=_('STRDS'),
+                         name='STRDS')
+
+        # ------------ITEMS IN NOTEBOOK PAGE (VECTOR)------------------------
+        self.controlPanelVector = wx.Panel(parent=self.ntb, id=wx.ID_ANY)
+        self.datasetSelectLabelV = wx.StaticText(parent=self.controlPanelVector,
+                                                 id=wx.ID_ANY,
+                                                 label=_('Vector temporal '
+                                                         'dataset (strds)'))
+        self.datasetSelectV = gselect.Select(parent=self.controlPanelVector,
+                                             id=wx.ID_ANY,
+                                             size=globalvar.DIALOG_GSELECT_SIZE,
+                                             type='stvds', multiple=True)
+        self.datasetSelectV.Bind(wx.EVT_TEXT, self.OnVectorSelected)
+
+        self.attribute = gselect.ColumnSelect(parent=self.controlPanelVector)
+        self.attributeLabel = wx.StaticText(parent=self.controlPanelVector,
+                                            id=wx.ID_ANY,
+                                            label=_('Select attribute column'))
+        # TODO fix the category selection as done for coordinates
+        try:
+            self._giface.GetMapWindow()
+            self.cats = gselect.VectorCategorySelect(parent=self.controlPanelVector,
+                                                     giface=self._giface)
+        except:
+            self.cats = wx.TextCtrl(parent=self.controlPanelVector, id=wx.ID_ANY,
+                                    size=globalvar.DIALOG_TEXTCTRL_SIZE)
+        self.catsLabel = wx.StaticText(parent=self.controlPanelVector,
+                                       id=wx.ID_ANY,
+                                       label=_('Select category of vector(s)'))
+
+        self.controlPanelSizerVector = wx.BoxSizer(wx.VERTICAL)
+        #self.controlPanelSizer.Add(wx.StaticText(self.panel, id=wx.ID_ANY,
+        #label=_("Select space time raster dataset(s):")),
+        #pos=(0, 0), flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
+        self.controlPanelSizerVector.Add(self.datasetSelectLabelV,
+                                         flag=wx.EXPAND)
+        self.controlPanelSizerVector.Add(self.datasetSelectV, flag=wx.EXPAND)
+
+        self.controlPanelSizerVector.Add(self.attributeLabel, flag=wx.EXPAND)
+        self.controlPanelSizerVector.Add(self.attribute, flag=wx.EXPAND)
+
+        self.controlPanelSizerVector.Add(self.catsLabel, flag=wx.EXPAND)
+        self.controlPanelSizerVector.Add(self.cats, flag=wx.EXPAND)
+
+        self.controlPanelVector.SetSizer(self.controlPanelSizerVector)
+        self.controlPanelSizerVector.Fit(self)
+        self.ntb.AddPage(page=self.controlPanelVector, text=_('STVDS'),
+                         name='STVDS')
+
+
+        # ------------Buttons on the bottom(draw,help)------------
+        self.vButtPanel = wx.Panel(self.mainPanel, id=wx.ID_ANY)
+        self.vButtSizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        self.drawButton = wx.Button(self.vButtPanel, id=wx.ID_ANY,
+                                    label=_("Draw"))
+        self.drawButton.Bind(wx.EVT_BUTTON, self.OnRedraw)
+        self.helpButton = wx.Button(self.vButtPanel, id=wx.ID_ANY,
+                                    label=_("Help"))
+        self.helpButton.Bind(wx.EVT_BUTTON, self.OnHelp)
+        self.vButtSizer.Add(self.drawButton)
+        self.vButtSizer.Add(self.helpButton)
+        self.vButtPanel.SetSizer(self.vButtSizer)
+
+        self.mainPanel.SetSizer(self.vbox)
+        self.vbox.Add(self.ntb, flag=wx.EXPAND)
+        self.vbox.Add(self.vButtPanel, flag=wx.EXPAND)
+        self.vbox.Fit(self)
+        self.mainPanel.Fit()
+
+    def _getSTRDdata(self, timeseries):
+        """Load data and read properties
+        :param list timeseries: a list of timeseries
+        """
+        mode = None
+        unit = None
+        columns = ','.join(['name', 'start_time', 'end_time'])
+        for series in timeseries:
+            name = series[0]
+            fullname = name + '@' + series[1]
+            etype = series[2]
+            sp = tgis.dataset_factory(etype, fullname)
+            if not sp.is_in_db(dbif=self.dbif):
+                GError(message=_("Dataset <%s> not found in temporal "
+                                 "database") % (fullname), parent=self)
+                return
+            sp.select(dbif=self.dbif)
+
+            minmin = sp.metadata.get_min_min()
+            self.plotNameListR.append(name)
+            self.timeDataR[name] = OrderedDict()
+
+
+            self.timeDataR[name]['temporalDataType'] = etype
+            self.timeDataR[name]['temporalType'] = sp.get_temporal_type()
+            self.timeDataR[name]['granularity'] = sp.get_granularity()
+
+            if mode is None:
+                mode = self.timeDataR[name]['temporalType']
+            elif self.timeDataR[name]['temporalType'] != mode:
+                GError(parent=self, message=_("Datasets have different temporal"
+                                              " type (absolute x relative), "
+                                              "which is not allowed."))
+                return
+
+            # check topology
+            maps = sp.get_registered_maps_as_objects(dbif=self.dbif)
+            self.timeDataR[name]['validTopology'] = sp.check_temporal_topology(maps=maps, dbif=self.dbif)
+
+            self.timeDataR[name]['unit'] = None  # only with relative
+            if self.timeDataR[name]['temporalType'] == 'relative':
+                start, end, self.timeDataR[name]['unit'] = sp.get_relative_time()
+                if unit is None:
+                    unit = self.timeDataR[name]['unit']
+                elif self.timeDataR[name]['unit'] != unit:
+                    GError(parent=self, message=_("Datasets have different "
+                                                  "time unit which is not "
+                                                  "allowed."))
+                    return
+
+            rows = sp.get_registered_maps(columns=columns, where=None,
+                                          order='start_time', dbif=self.dbif)
+            for row in rows:
+                self.timeDataR[name][row[0]] = {}
+                self.timeDataR[name][row[0]]['start_datetime'] = row[1]
+                self.timeDataR[name][row[0]]['end_datetime'] = row[2]
+                r = RasterRow(row[0])
+                r.open()
+                val = r.get_value(self.poi)
+                r.close()
+                if val == -2147483648 and val < minmin:
+                    self.timeDataR[name][row[0]]['value'] = None
+                else:
+                    self.timeDataR[name][row[0]]['value'] = val
+
+        self.unit = unit
+        self.temporalType = mode
+        return
+
+    def _parseVDbConn(self, mapp, layerInp):
+        '''find attribute key according to layer of input map'''
+        vdb = Module('v.db.connect', map=mapp, flags='g', stdout_=PIPE)
+
+        vdb = vdb.outputs.stdout
+        for line in vdb.splitlines():
+            lsplit = line.split('|')
+            layer = lsplit[0].split('/')[0]
+            if str(layer) == str(layerInp):
+                return lsplit[2]
+        return None
+
+    def _getExistingCategories(self, mapp, cats):
+        """Get a list of categories for a vector map"""
+        vdb = grass.read_command('v.category', input=mapp, option='print')
+        categories = vdb.splitlines()
+        for cat in cats:
+            if str(cat) not in categories:
+                GMessage(message=_("Category {ca} is not on vector map"
+                                   " {ma} and it will be used").format(ma=mapp,
+                                                                       ca=cat),
+                         parent=self)
+                cats.remove(cat)
+        return cats
+
+    def _getSTVDData(self, timeseries):
+        """Load data and read properties
+        :param list timeseries: a list of timeseries
+        """
+
+        mode = None
+        unit = None
+        cats = None
+        attribute = self.attribute.GetValue()
+        if self.cats.GetValue() != '':
+            cats = self.cats.GetValue().split(',')
+        if cats and self.poi:
+            GMessage(message=_("Both coordinates and categories are set, "
+                               "coordinates will be used. The use categories "
+                               "remove text from coordinate form"))
+        if not attribute or attribute == '':
+            GError(parent=self, showTraceback=False,
+                   message=_("With Vector temporal dataset you have to select"
+                             " an attribute column"))
+            return
+        columns = ','.join(['name', 'start_time', 'end_time', 'id', 'layer'])
+        for series in timeseries:
+            name = series[0]
+            fullname = name + '@' + series[1]
+            etype = series[2]
+            sp = tgis.dataset_factory(etype, fullname)
+            if not sp.is_in_db(dbif=self.dbif):
+                GError(message=_("Dataset <%s> not found in temporal "
+                                 "database") % (fullname), parent=self,
+                                 showTraceback=False)
+                return
+            sp.select(dbif=self.dbif)
+
+            rows = sp.get_registered_maps(dbif=self.dbif, order="start_time",
+                                          columns=columns, where=None)
+
+            self.timeDataV[name] = OrderedDict()
+            self.timeDataV[name]['temporalDataType'] = etype
+            self.timeDataV[name]['temporalType'] = sp.get_temporal_type()
+            self.timeDataV[name]['granularity'] = sp.get_granularity()
+
+            if mode is None:
+                mode = self.timeDataV[name]['temporalType']
+            elif self.timeDataV[name]['temporalType'] != mode:
+                GError(parent=self, showTraceback=False,
+                       message=_("Datasets have different temporal type ("
+                                 "absolute x relative), which is not allowed."))
+                return
+            self.timeDataV[name]['unit'] = None  # only with relative
+            if self.timeDataV[name]['temporalType'] == 'relative':
+                start, end, self.timeDataV[name]['unit'] = sp.get_relative_time()
+                if unit is None:
+                    unit = self.timeDataV[name]['unit']
+                elif self.timeDataV[name]['unit'] != unit:
+                    GError(message=_("Datasets have different time unit which"
+                                     " is not allowed."), parent=self,
+                           showTraceback=False)
+                    return
+            if self.poi:
+                self.plotNameListV.append(name)
+                # TODO set an appropriate distance, right now a big one is set
+                # to return the closer point to the selected one
+                out = grass.vector_what(map='pois_srvds',
+                                        coord=self.poi.coords(),
+                                        distance=10000000000000000)
+                if len(out) != len(rows):
+                    GError(parent=self, showTraceback=False,
+                           message=_("Difference number of vector layers and "
+                                     "maps in the vector temporal dataset"))
+                    return
+                for i in range(len(rows)):
+                    row = rows[i]
+                    values = out[i]
+                    if str(row['layer']) == str(values['Layer']):
+                        lay = "{map}_{layer}".format(map=row['name'],
+                                                     layer=values['Layer'])
+                        self.timeDataV[name][lay] = {}
+                        self.timeDataV[name][lay]['start_datetime'] = row['start_time']
+                        self.timeDataV[name][lay]['end_datetime'] = row['start_time']
+                        self.timeDataV[name][lay]['value'] = values['Attributes'][attribute]
+            else:
+                wherequery = ''
+                cats = self._getExistingCategories(rows[0]['name'], cats)
+                totcat = len(cats)
+                ncat = 1
+                for cat in cats:
+                    if ncat == 1 and totcat != 1:
+                        wherequery += '{k}={c} or'.format(c=cat, k="{key}")
+                    elif ncat == 1 and totcat == 1:
+                        wherequery += '{k}={c}'.format(c=cat, k="{key}")
+                    elif ncat == totcat:
+                        wherequery += ' {k}={c}'.format(c=cat, k="{key}")
+                    else:
+                        wherequery += ' {k}={c} or'.format(c=cat, k="{key}")
+
+                    catn = "cat{num}".format(num=cat)
+                    self.plotNameListV.append("{na}+{cat}".format(na=name,
+                                                                  cat=catn))
+                    self.timeDataV[name][catn] = OrderedDict()
+                    ncat += 1
+                for row in rows:
+                    lay = int(row['layer'])
+                    catkey = self._parseVDbConn(row['name'], lay)
+                    if not catkey:
+                        GError(parent=self, showTraceback=False,
+                           message=_("No connection between vector map {vmap} "
+                                     "and layer {la}".format(vmap=row['name'],
+                                                              la=lay)))
+                        return
+                    vals = grass.vector_db_select(map=row['name'], layer=lay,
+                                                  where=wherequery.format(key=catkey),
+                                                  columns=attribute)
+                    layn = "lay{num}".format(num=lay)
+                    for cat in cats:
+                        catn = "cat{num}".format(num=cat)
+                        if layn not in self.timeDataV[name][catn].keys():
+                            self.timeDataV[name][catn][layn] = {}
+                        self.timeDataV[name][catn][layn]['start_datetime'] = row['start_time']
+                        self.timeDataV[name][catn][layn]['end_datetime'] = row['end_time']
+                        self.timeDataV[name][catn][layn]['value'] = vals['values'][int(cat)][0]
+        self.unit = unit
+        self.temporalType = mode
+        return
+
+    def _drawFigure(self):
+        """Draws or print 2D plot (temporal extents)"""
+        self.axes2d.clear()
+        self.axes2d.grid(False)
+        if self.temporalType == 'absolute':
+            self.axes2d.xaxis_date()
+            self.fig.autofmt_xdate()
+            self.convert = mdates.date2num
+            self.invconvert = mdates.num2date
+        else:
+            self.convert = lambda x: x
+            self.invconvert = self.convert
+
+        self.colors = cycle(COLORS)
+
+        self.yticksNames = []
+        self.yticksPos = []
+        self.plots = []
+
+        if self.datasetsR:
+            self.lookUp = LookUp(self.timeDataR, self.invconvert)
+        else:
+            self.lookUp = LookUp(self.timeDataV, self.invconvert)
+
+        if self.datasetsR:
+            self.drawR()
+        if self.datasetsV:
+            if self.poi:
+                self.drawV()
+            elif self.cats:
+                self.drawVCats()
+
+        self.canvas.draw()
+        DataCursor(self.plots, self.lookUp, InfoFormat, self.convert)
+
+    def drawR(self):
+        for i, name in enumerate(self.datasetsR):
+            name = name[0]
+            # just name; with mapset it would be long
+            self.yticksNames.append(name)
+            self.yticksPos.append(1)  # TODO
+            xdata = []
+            ydata = []
+            for keys, values in self.timeDataR[name].iteritems():
+                if keys in ['temporalType', 'granularity', 'validTopology',
+                            'unit', 'temporalDataType']:
+                    continue
+                xdata.append(self.convert(values['start_datetime']))
+                ydata.append(values['value'])
+
+            self.lookUp.AddDataset(yranges=ydata, xranges=xdata,
+                                   datasetName=name)
+            color = self.colors.next()
+            self.plots.append(self.axes2d.plot(xdata, ydata, marker='o',
+                                               color=color,
+                                               label=self.plotNameListR[i])[0])
+
+        if self.temporalType == 'absolute':
+            self.axes2d.set_xlabel(_("Temporal resolution: %s" % self.timeDataR[name]['granularity']))
+        else:
+            self.axes2d.set_xlabel(_("Time [%s]") % self.unit)
+        self.axes2d.set_ylabel(', '.join(self.yticksNames))
+
+        # legend
+        handles, labels = self.axes2d.get_legend_handles_labels()
+        self.axes2d.legend(loc=0)
+
+    def drawVCats(self):
+        for i, name in enumerate(self.plotNameListV):
+            # just name; with mapset it would be long
+            labelname = name.replace('+', ' ')
+            self.yticksNames.append(labelname)
+            name_cat = name.split('+')
+            name = name_cat[0]
+            self.yticksPos.append(1)  # TODO
+            xdata = []
+            ydata = []
+            for keys, values in self.timeDataV[name_cat[0]][name_cat[1]].iteritems():
+                if keys in ['temporalType', 'granularity', 'validTopology',
+                            'unit', 'temporalDataType']:
+                    continue
+                xdata.append(self.convert(values['start_datetime']))
+                ydata.append(values['value'])
+
+            self.lookUp.AddDataset(yranges=ydata, xranges=xdata,
+                                   datasetName=name)
+            color = self.colors.next()
+
+            self.plots.append(self.axes2d.plot(xdata, ydata, marker='o',
+                                               color=color, label=labelname)[0])
+        # ============================
+        if self.temporalType == 'absolute':
+            self.axes2d.set_xlabel(_("Temporal resolution: %s" % self.timeDataV[name]['granularity']))
+        else:
+            self.axes2d.set_xlabel(_("Time [%s]") % self.unit)
+        self.axes2d.set_ylabel(', '.join(self.yticksNames))
+
+        # legend
+        handles, labels = self.axes2d.get_legend_handles_labels()
+        self.axes2d.legend(loc=0)
+        self.listWhereConditions = []
+
+    def drawV(self):
+        for i, name in enumerate(self.plotNameListV):
+            # just name; with mapset it would be long
+            self.yticksNames.append(self.attribute.GetValue())
+            self.yticksPos.append(0)  # TODO
+            xdata = []
+            ydata = []
+            for keys, values in self.timeDataV[name].iteritems():
+                if keys in ['temporalType', 'granularity', 'validTopology',
+                            'unit', 'temporalDataType']:
+                    continue
+                xdata.append(self.convert(values['start_datetime']))
+                ydata.append(values['value'])
+
+            self.lookUp.AddDataset(yranges=ydata, xranges=xdata,
+                                   datasetName=name)
+            color = self.colors.next()
+
+            self.plots.append(self.axes2d.plot(xdata, ydata, marker='o',
+                                               color=color, label=name)[0])
+        # ============================
+        if self.temporalType == 'absolute':
+            self.axes2d.set_xlabel(_("Temporal resolution: %s" % self.timeDataV[name]['granularity']))
+        else:
+            self.axes2d.set_xlabel(_("Time [%s]") % self.unit)
+        self.axes2d.set_ylabel(', '.join(self.yticksNames))
+
+        # legend
+        handles, labels = self.axes2d.get_legend_handles_labels()
+        self.axes2d.legend(loc=0)
+        self.listWhereConditions = []
+
+    def OnRedraw(self, event=None):
+        """Required redrawing."""
+        self.init()
+        datasetsR = self.datasetSelectR.GetValue().strip()
+        datasetsV = self.datasetSelectV.GetValue().strip()
+
+        if not datasetsR and not datasetsV:
+            return
+
+        try:
+            getcoors = self.coorval.coordsField.GetValue()
+        except:
+            try:
+                getcoors = self.coorval.GetValue()
+            except:
+                getcoors = None
+        if getcoors and getcoors != '':
+            try:
+                coordx, coordy = getcoors.split(',')
+                coordx, coordy = float(coordx), float(coordy)
+            except (ValueError, AttributeError):
+                try:
+                    coordx, coordy = self.coorval.GetValue().split(',')
+                    coordx, coordy = float(coordx), float(coordy)
+                except (ValueError, AttributeError):
+                    GMessage(message=_("Incorrect coordinates format, should "
+                                       "be: x,y"), parent=self)
+            coors = [coordx, coordy]
+            if coors:
+                try:
+                    self.poi = Point(float(coors[0]), float(coors[1]))
+                except GException:
+                    GError(parent=self, message=_("Invalid input coordinates"),
+                           showTraceback=False)
+                    return
+        # check raster dataset
+        if datasetsR:
+            datasetsR = datasetsR.split(',')
+            try:
+                datasetsR = self._checkDatasets(datasetsR, 'strds')
+                if not datasetsR:
+                    return
+            except GException:
+                GError(parent=self, message=_("Invalid input raster dataset"),
+                       showTraceback=False)
+                return
+
+            self.datasetsR = datasetsR
+
+        # check vector dataset
+        if datasetsV:
+            datasetsV = datasetsV.split(',')
+            try:
+                datasetsV = self._checkDatasets(datasetsV, 'stvds')
+                if not datasetsV:
+                    return
+            except GException:
+                GError(parent=self, message=_("Invalid input vector dataset"),
+                       showTraceback=False)
+                return
+            self.datasetsV = datasetsV
+        self._redraw()
+
+    def _redraw(self):
+        """Readraw data.
+
+        Decides if to draw also 3D and adjusts layout if needed.
+        """
+        if self.datasetsR:
+            self._getSTRDdata(self.datasetsR)
+
+        if self.datasetsV:
+            self._getSTVDData(self.datasetsV)
+
+        # axes3d are physically removed
+        if not self.axes2d:
+            self.axes2d = self.fig.add_subplot(1, 1, 1)
+        self._drawFigure()
+
+    def _checkDatasets(self, datasets, typ):
+        """Checks and validates datasets.
+
+        Reports also type of dataset (e.g. 'strds').
+
+        :param list datasets: list of temporal dataset's name
+        :return: (mapName, mapset, type)
+        """
+        validated = []
+        tDict = tgis.tlist_grouped(type=typ, group_type=True, dbif=self.dbif)
+        # nested list with '(map, mapset, etype)' items
+        allDatasets = [[[(map, mapset, etype) for map in maps]
+                        for etype, maps in etypesDict.iteritems()]
+                       for mapset, etypesDict in tDict.iteritems()]
+        # flatten this list
+        if allDatasets:
+            allDatasets = reduce(lambda x, y: x + y, reduce(lambda x, y: x + y,
+                                                            allDatasets))
+            mapsets = tgis.get_tgis_c_library_interface().available_mapsets()
+            allDatasets = [i for i in sorted(allDatasets,
+                                             key=lambda l: mapsets.index(l[1]))]
+
+        for dataset in datasets:
+            errorMsg = _("Space time dataset <%s> not found.") % dataset
+            if dataset.find("@") >= 0:
+                nameShort, mapset = dataset.split('@', 1)
+                indices = [n for n, (mapName, mapsetName, etype) in enumerate(allDatasets)
+                           if nameShort == mapName and mapsetName == mapset]
+            else:
+                indices = [n for n, (mapName, mapset, etype) in enumerate(allDatasets)
+                           if dataset == mapName]
+
+            if len(indices) == 0:
+                raise GException(errorMsg)
+            elif len(indices) >= 2:
+                dlg = wx.SingleChoiceDialog(self,
+                                            message=_("Please specify the "
+                                                      "space time dataset "
+                                                      "<%s>." % dataset),
+                                            caption=_("Ambiguous dataset name"),
+                                            choices=[("%(map)s@%(mapset)s:"
+                                                      " %(etype)s" % {'map': allDatasets[i][0],
+                                                                      'mapset': allDatasets[i][1],
+                                                                      'etype': allDatasets[i][2]})
+                                                     for i in indices],
+                                            style=wx.CHOICEDLG_STYLE | wx.OK)
+                if dlg.ShowModal() == wx.ID_OK:
+                    index = dlg.GetSelection()
+                    validated.append(allDatasets[indices[index]])
+                else:
+                    continue
+            else:
+                validated.append(allDatasets[indices[0]])
+
+        return validated
+
+    def OnHelp(self, event):
+        """Function to show help"""
+        RunCommand(prog='g.manual', quiet=True, entry='g.gui.tplot')
+
+    def SetDatasets(self, rasters, vectors, coors, cats, attr):
+        """Set the data
+        #TODO
+        :param list rasters: a list of temporal raster dataset's name
+        :param list vectors: a list of temporal vector dataset's name
+        :param list coors: a list with x/y coordinates
+        :param list cats: a list with incld. categories of vector
+        :param str attr:  name of atribute of vectror data
+        """
+        if not (rasters or vectors) or not (coors or cats):
+            return
+        try:
+            if rasters:
+                self.datasetsR = self._checkDatasets(rasters, 'strds')
+            if vectors:
+                self.datasetsV = self._checkDatasets(vectors, 'stvds')
+            if not (self.datasetsR or self.datasetsV):
+                return
+        except GException:
+            GError(parent=self, message=_("Invalid input temporal dataset"),
+                   showTraceback=False)
+            return
+        if coors:
+            try:
+                self.poi = Point(float(coors[0]), float(coors[1]))
+            except GException:
+                GError(parent=self, message=_("Invalid input coordinates"),
+                       showTraceback=False)
+                return
+            try:
+                self.coorval.coordsField.SetValue(','.join(coors))
+            except:
+                self.coorval.SetValue(','.join(coors))
+        if self.datasetsV:
+            vdatas = ','.join(map(lambda x: x[0] + '@' + x[1], self.datasetsV))
+            self.datasetSelectV.SetValue(vdatas)
+            if attr:
+                self.attribute.SetValue(attr)
+            if cats:
+                self.cats.SetValue(cats)
+        if self.datasetsR:
+            self.datasetSelectR.SetValue(','.join(map(lambda x: x[0] + '@' + x[1],
+                                                      self.datasetsR)))
+        self._redraw()
+
+    def OnVectorSelected(self, event):
+        """Update the controlbox related to stvds"""
+        dataset = self.datasetSelectV.GetValue().strip()
+        vect_list = grass.read_command('t.vect.list', flags='s', input=dataset,
+                                       col='name')
+        vect_list = list(set(sorted(vect_list.split())))
+        for vec in vect_list:
+            self.attribute.InsertColumns(vec, 1)
+
+
+class LookUp:
+    """Helper class for searching info by coordinates"""
+
+    def __init__(self, timeData, convert):
+        self.data = {}
+        self.timeData = timeData
+        self.convert = convert
+
+    def AddDataset(self, yranges, xranges, datasetName):
+        if len(yranges) != len(xranges):
+            GError(parent=self, showTraceback=False,
+                   message=_("Datasets have different number of values"))
+            return
+        self.data[datasetName] = {}
+        for i in range(len(xranges)):
+            self.data[datasetName][xranges[i]] = yranges[i]
+
+    def GetInformation(self, x):
+        values = {}
+        for key, value in self.data.iteritems():
+            if value[x]:
+                values[key] = [self.convert(x), value[x]]
+
+        if len(values) == 0:
+            return None
+
+        return self.timeData, values
+
+
+def InfoFormat(timeData, values):
+    """Formats information about dataset"""
+    text = []
+    for key, val in values.iteritems():
+        etype = timeData[key]['temporalDataType']
+        if etype == 'strds':
+            text.append(_("Space time raster dataset: %s") % key)
+        elif etype == 'stvds':
+            text.append(_("Space time vector dataset: %s") % key)
+        elif etype == 'str3ds':
+            text.append(_("Space time 3D raster dataset: %s") % key)
+
+        text.append(_("Value for {date} is {val}".format(date=val[0],
+                      val=val[1])))
+        text.append('\n')
+    text.append(_("Press Del to dismiss."))
+
+    return '\n'.join(text)
+
+
+class DataCursor(object):
+    """A simple data cursor widget that displays the x,y location of a
+    matplotlib artist when it is selected.
+
+
+    Source: http://stackoverflow.com/questions/4652439/
+            is-there-a-matplotlib-equivalent-of-matlabs-datacursormode/4674445
+    """
+
+    def __init__(self, artists, lookUp, formatFunction, convert,
+                 tolerance=5, offsets=(-30, 20), display_all=False):
+        """Create the data cursor and connect it to the relevant figure.
+        "artists" is the matplotlib artist or sequence of artists that will be
+            selected.
+        "tolerance" is the radius (in points) that the mouse click must be
+            within to select the artist.
+        "offsets" is a tuple of (x,y) offsets in points from the selected
+            point to the displayed annotation box
+        "display_all" controls whether more than one annotation box will
+            be shown if there are multiple axes.  Only one will be shown
+            per-axis, regardless.
+        """
+        self.lookUp = lookUp
+        self.formatFunction = formatFunction
+        self.offsets = offsets
+        self.display_all = display_all
+        if not cbook.iterable(artists):
+            artists = [artists]
+        self.artists = artists
+        self.convert = convert
+        self.axes = tuple(set(art.axes for art in self.artists))
+        self.figures = tuple(set(ax.figure for ax in self.axes))
+
+        self.annotations = {}
+        for ax in self.axes:
+            self.annotations[ax] = self.annotate(ax)
+        for artist in self.artists:
+            artist.set_picker(tolerance)
+        for fig in self.figures:
+            fig.canvas.mpl_connect('pick_event', self)
+            fig.canvas.mpl_connect('key_press_event', self.keyPressed)
+
+    def keyPressed(self, event):
+        """Key pressed - hide annotation if Delete was pressed"""
+        if event.key != 'delete':
+            return
+        for ax in self.axes:
+            self.annotations[ax].set_visible(False)
+            event.canvas.draw()
+
+    def annotate(self, ax):
+        """Draws and hides the annotation box for the given axis "ax"."""
+        annotation = ax.annotate(self.formatFunction, xy=(0, 0), ha='center',
+                                 xytext=self.offsets, va='bottom',
+                                 textcoords='offset points',
+                                 bbox=dict(boxstyle='round,pad=0.5',
+                                           fc='yellow', alpha=0.7),
+                                 arrowprops=dict(arrowstyle='->',
+                                                 connectionstyle='arc3,rad=0'),
+                                 annotation_clip=False, multialignment='left')
+        annotation.set_visible(False)
+
+        return annotation
+
+    def __call__(self, event):
+        """Intended to be called through "mpl_connect"."""
+        # Rather than trying to interpolate, just display the clicked coords
+        # This will only be called if it's within "tolerance", anyway.
+        x, y = event.mouseevent.xdata, event.mouseevent.ydata
+        annotation = self.annotations[event.artist.axes]
+        if x is not None:
+            if not self.display_all:
+                # Hide any other annotation boxes...
+                for ann in self.annotations.values():
+                    ann.set_visible(False)
+            if 'Line2D' in str(type(event.artist)):
+                xData = []
+                for a in event.artist.get_xdata():
+                    try:
+                        d = self.convert(a)
+                    except:
+                        d = a
+                    xData.append(d)
+                x = xData[np.argmin(abs(xData - x))]
+
+            info = self.lookUp.GetInformation(x)
+            ys = zip(*info[1].values())[1]
+            if not info:
+                return
+            # Update the annotation in the current axis..
+            annotation.xy = x, max(ys)
+            text = self.formatFunction(*info)
+            annotation.set_text(text)
+            annotation.set_visible(True)
+            event.canvas.draw()

+ 41 - 0
gui/wxpython/tplot/g.gui.tplot.html

@@ -0,0 +1,41 @@
+<!-- meta page description: wxGUI Temporal Plot Tool -->
+<!-- meta page index: topic_gui|GUI -->
+<h2>DESCRIPTION</h2>
+
+The <b>Temporal Plot Tool</b> is a <em><a href="wxGUI.html">wxGUI</a></em> component
+which allows the user to see in a plot the values of one or more temporal datasets (strds, stvds,
+str3ds) for a queried point defined by a coordinate pair.
+<p>
+Supported features:
+<ul>
+  <li>temporal datasets with interval/point and absolute/relative time,</li>
+  <li>2D plots,</li>
+  <!-- <li>3D plots - spatio-temporal extent (matplotlib &gt;= 1.0.0)</li> -->
+  <li>pop-up annotations with values information,</li>
+  <li>automatic output to query several point.</li>
+</ul>
+
+<center>
+<img src="tplot.png" border="1" alt="Temporal Plot Tool">
+</center>
+
+<h2>NOTES</h2>
+
+<em>g.gui.tplot</em> requires the Python plotting library 
+<a href="http://matplotlib.org/">Matplotlib</a>.
+
+<h2>SEE ALSO</h2>
+
+<em>
+  <a href="temporal.html">Temporal data processing</a><br>
+  <a href="wxGUI.html">wxGUI</a><br>
+  <a href="wxGUI.components.html">wxGUI components</a>
+</em>
+
+<h2>AUTHOR</h2>
+
+Luca Delucchi,
+<a href="http://www.gis.cri.fmach.it">Fondazione Edmund Mach</a>, Italy
+
+<p>
+<i>$Date$</i>

+ 131 - 0
gui/wxpython/tplot/g.gui.tplot.py

@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+############################################################################
+#
+# MODULE:    g.gui.tplot.py
+# AUTHOR(S): Luca Delucchi
+# PURPOSE:   Temporal Plot Tool is a wxGUI component (based on matplotlib)
+#            the user to see in a plot the values of one or more temporal
+#            datasets for a queried point defined by a coordinate pair.
+# COPYRIGHT: (C) 2014 by Luca Delucchi, and the GRASS Development Team
+#
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; either version 2 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+############################################################################
+
+#%module
+#% description: Allows the user to see in a plot the values of one or more temporal raser datasets for a queried point defined by a coordinate pair. Also allows plotting data of vector dataset for a defined categories and attribute.
+#% keywords: general
+#% keywords: GUI
+#% keywords: temporal
+#%end
+
+#%option G_OPT_STVDS_INPUTS
+#% key: stvds
+#% required: no
+#%end
+
+#%option G_OPT_STRDS_INPUTS
+#% key: strds
+#% required: no
+#%end
+
+#%option G_OPT_M_COORDS
+#% required: no
+#%end
+
+#TODO use option G_OPT_V_CATS
+#%option
+#% key: cats
+#% label: Categories of vectores features
+#% description: To use only with stvds
+#% required: no
+#%end
+
+
+#%option
+#% key: attr
+#% label: Name of attribute
+#% description: Name of attribute which represent data for plotting
+#% required: no
+#%end
+
+#%option G_OPT_F_OUTPUT
+#% required: no
+#% label: Name for output file
+#% description: Add extension to specify format (.png, .pdf, .svg)
+#%end
+
+#%option
+#% key: size
+#% type: string
+#% label: The size for output image
+#% description: It works only with output parameter
+#% required: no
+#%end
+
+import grass.script as gscript
+
+
+def main():
+    options, flags = gscript.parser()
+
+    import wx
+    from core.utils import _
+    from core.giface import StandaloneGrassInterface
+    try:
+        from tplot.frame import TplotFrame
+    except ImportError as e:
+        gscript.fatal(e.message)
+    rasters = None
+    if options['strds']:
+        rasters = options['strds'].strip().split(',')
+    coords = None
+    if options['coordinates']:
+        coords = options['coordinates'].strip().split(',')
+    cats = None
+    if options['cats']:
+        cats = options['cats']
+    output = options['output']
+    vectors = None
+    attr = None
+    if options['stvds']:
+        vectors = options['stvds'].strip().split(',')
+        if not options['attr']:
+            gscript.fatal(_("With stvds you have to set 'attr' option"))
+        else:
+            attr = options['attr']
+        if coords and cats:
+            gscript.fatal(_("With stvds it is not possible use 'coordinates' "
+                            "and 'cats' options together"))
+        elif not coords and not cats:
+            gscript.warning(_("With stvds you have to use 'coordinates' or "
+                              "'cats' option"))
+    app = wx.App()
+    frame = TplotFrame(parent=None, giface=StandaloneGrassInterface())
+    frame.SetDatasets(rasters, vectors, coords, cats, attr)
+    if output:
+        frame.OnRedraw()
+        if options['size']:
+            sizes = options['size'].strip().split(',')
+            sizes = [int(s) for s in sizes]
+            frame.canvas.SetSize(sizes)
+        if output.split('.')[-1].lower() == 'png':
+            frame.canvas.print_png(output)
+        if output.split('.')[-1].lower() in ['jpg', 'jpeg']:
+            frame.canvas.print_jpg(output)
+        if output.split('.')[-1].lower() in ['tif', 'tiff']:
+            frame.canvas.print_tif(output)
+    else:
+        frame.Show()
+        app.MainLoop()
+
+if __name__ == '__main__':
+    main()

BIN
gui/wxpython/tplot/tplot.png


+ 1 - 0
gui/wxpython/xml/toolboxes.xml

@@ -1952,6 +1952,7 @@
     <items>
       <wxgui-item name="AnimationTool"/>
       <wxgui-item name="TimelineTool"/>
+      <wxgui-item name="TplotTool"/>
     </items>
   </toolbox>
   <toolbox name="GuiTools">

+ 7 - 0
gui/wxpython/xml/wxgui_items.xml

@@ -35,6 +35,13 @@
     <description>Plot temporal extents.</description>
     <keywords>general,gui,temporal</keywords>
   </wxgui-item>
+  <wxgui-item name="TplotTool">
+    <label>Temporal plot tool</label>
+    <handler>OnTplotTool</handler>
+    <related-module>g.gui.tplot</related-module>
+    <description>Plot temporal values.</description>
+    <keywords>general,gui,temporal</keywords>
+  </wxgui-item>
   <wxgui-item name="CartographicComposer">
     <label>Cartographic Composer</label>
     <handler>OnPsMap</handler>