|
@@ -0,0 +1,403 @@
|
|
|
+"""
|
|
|
+@package gui_core.vselect
|
|
|
+
|
|
|
+@brief wxGUI classes for interactive selection of vector
|
|
|
+features. Allows to create a new vector map from selected vector
|
|
|
+features or return their categories
|
|
|
+
|
|
|
+Classes:
|
|
|
+- vselect::VectorSelectList
|
|
|
+- vselect::VectorSelectDialog
|
|
|
+- vselect::VectorSelectBase
|
|
|
+- vselect::VectorSelectHighlighter
|
|
|
+
|
|
|
+(C) 2014-2015 by Matej Krejci, and 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 Matej Krejci <matejkrejci gmail.com> (mentor: Martin Landa)
|
|
|
+"""
|
|
|
+
|
|
|
+import string
|
|
|
+import random
|
|
|
+
|
|
|
+import wx
|
|
|
+import wx.lib.mixins.listctrl as listmix
|
|
|
+
|
|
|
+from core.utils import _
|
|
|
+from core.gcmd import GMessage, GError, GWarning
|
|
|
+from core.gcmd import RunCommand
|
|
|
+
|
|
|
+import grass.script as grass
|
|
|
+from grass.pydispatch.signal import Signal
|
|
|
+
|
|
|
+class VectorSelectList(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
|
|
|
+ """Widget for managing vector features selected from map display
|
|
|
+ """
|
|
|
+ def __init__(self, parent):
|
|
|
+ wx.ListCtrl.__init__(self, parent=parent, id=wx.ID_ANY, style=wx.LC_REPORT | wx.BORDER_SUNKEN)
|
|
|
+ listmix.ListCtrlAutoWidthMixin.__init__(self)
|
|
|
+
|
|
|
+ self.InsertColumn(col=0, heading=_('category'))
|
|
|
+ self.InsertColumn(col=1, heading=_('type'))
|
|
|
+ self.SetColumnWidth(0, 100)
|
|
|
+ self.SetColumnWidth(1, 100)
|
|
|
+
|
|
|
+ self.index = 0
|
|
|
+ self.dictIndex = {}
|
|
|
+
|
|
|
+ def AddItem(self, item):
|
|
|
+ if 'Category' not in item:
|
|
|
+ return
|
|
|
+
|
|
|
+ pos = self.InsertStringItem(0, str(item['Category']))
|
|
|
+ self.SetStringItem(pos, 1, str(item['Type']))
|
|
|
+ self.dictIndex[str(item['Category'])] = pos
|
|
|
+
|
|
|
+ def RemoveItem(self, item):
|
|
|
+ index = self.dictIndex.get(str(item['Category']), -1)
|
|
|
+ if index > -1:
|
|
|
+ self.DeleteItem(index)
|
|
|
+
|
|
|
+class VectorSelectDialog(wx.Dialog):
|
|
|
+ """Dialog for managing vector features selected from map display"""
|
|
|
+ def __init__(self, parent, title=_("Select features"), size=(200, 300)):
|
|
|
+ wx.Dialog.__init__(self, parent=parent, id=wx.ID_ANY,
|
|
|
+ title=title, size=size, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
|
|
+
|
|
|
+ self._layout()
|
|
|
+
|
|
|
+ def AddWidget(self, widget, proportion=1, flag=wx.EXPAND):
|
|
|
+ self.mainSizer.Add(widget, proportion=proportion, flag=flag)
|
|
|
+ self.Layout()
|
|
|
+
|
|
|
+ def _layout(self):
|
|
|
+ self.mainSizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
+ self.SetSizer(self.mainSizer)
|
|
|
+
|
|
|
+ self.Show()
|
|
|
+
|
|
|
+class VectorSelectBase():
|
|
|
+ """@brief Main class of vector selection function
|
|
|
+
|
|
|
+ It allows to select vector features from map display and to export
|
|
|
+ them as a new vector map. Current version allows to select
|
|
|
+ features one-by-one by single click in map display.
|
|
|
+
|
|
|
+ This class can be initialized with (see CreateDialog()) or without
|
|
|
+ (see gselect) dialog (see VectorSelectDialog).
|
|
|
+ """
|
|
|
+ def __init__(self, parent, giface):
|
|
|
+ self.parent = parent
|
|
|
+ self._giface = giface
|
|
|
+ self.register = False
|
|
|
+ self.mapWin = self._giface.GetMapWindow()
|
|
|
+ self.mapDisp = giface.GetMapDisplay()
|
|
|
+ self.RegisterMapEvtHandler()
|
|
|
+
|
|
|
+ self.selectedFeatures = []
|
|
|
+ self.mapName = None # chosen map for selecting features
|
|
|
+
|
|
|
+ self._dialog = None
|
|
|
+ self.onCloseDialog = None
|
|
|
+
|
|
|
+ self.updateLayer = Signal('VectorSelectBase.updateLayer')
|
|
|
+
|
|
|
+ self.painter = VectorSelectHighlighter(self.mapDisp, giface)
|
|
|
+
|
|
|
+ def CreateDialog(self, createButton=True):
|
|
|
+ """Create dialog
|
|
|
+
|
|
|
+ :param createButton: True to add 'create new map' button
|
|
|
+ """
|
|
|
+ if self._dialog:
|
|
|
+ return
|
|
|
+
|
|
|
+ self._dialog = VectorSelectDialog(parent=self.parent)
|
|
|
+ self._dialog.Bind(wx.EVT_CLOSE,self.OnCloseDialog)
|
|
|
+ if createButton:
|
|
|
+ createMap = wx.Button(self._dialog, wx.ID_ANY, _("Create a new map"))
|
|
|
+ createMap.Bind(wx.EVT_BUTTON, self.OnExportMap)
|
|
|
+ self._dialog.AddWidget(createMap, proportion=0.1)
|
|
|
+ self.slist = VectorSelectList(self._dialog)
|
|
|
+ self.slist.Bind(wx.EVT_LIST_KEY_DOWN, self.OnDelete)
|
|
|
+ self.slist.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnDeleteRow)
|
|
|
+ self._dialog.AddWidget(self.slist)
|
|
|
+
|
|
|
+ self.onCloseDialog = Signal('VectorSelectBase.onCloseDialog')
|
|
|
+
|
|
|
+ def OnDeleteRow(self, event=None):
|
|
|
+ """Delete row in widget
|
|
|
+ """
|
|
|
+ index = self.slist.GetFocusedItem()
|
|
|
+ category = self.slist.GetItemText(index)
|
|
|
+ for item in self.selectedFeatures:
|
|
|
+ if int(item['Category']) == int(category):
|
|
|
+ self.selectedFeatures.remove(item)
|
|
|
+ break
|
|
|
+ self.slist.DeleteItem(index)
|
|
|
+ self._draw()
|
|
|
+
|
|
|
+ def OnDelete(self, event):
|
|
|
+ """Delete row in widget by press key(delete)
|
|
|
+ """
|
|
|
+ keycode = event.GetKeyCode()
|
|
|
+ if keycode == wx.WXK_DELETE:
|
|
|
+ self.OnDeleteRow()
|
|
|
+
|
|
|
+ def RegisterMapEvtHandler(self):
|
|
|
+ if not self.register:
|
|
|
+ self.mapWin.RegisterMouseEventHandler(wx.EVT_LEFT_DOWN,
|
|
|
+ self._onMapClickHandler,
|
|
|
+ 'cross')
|
|
|
+ self.register=True
|
|
|
+
|
|
|
+ def UnregisterMapEvtHandler(self):
|
|
|
+ """Unregistrates _onMapClickHandler from mapWin"""
|
|
|
+ if self.register:
|
|
|
+ self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
|
|
|
+ self._onMapClickHandler)
|
|
|
+ self.register=False
|
|
|
+
|
|
|
+ def OnCloseDialog(self,evt=None):
|
|
|
+ if not self.onCloseDialog:
|
|
|
+ return
|
|
|
+
|
|
|
+ self.onCloseDialog.emit()
|
|
|
+ self.selectedFeatures=[]
|
|
|
+ self.painter.Clear()
|
|
|
+ self._dialog.Destroy()
|
|
|
+ self.UnregisterMapEvtHandler()
|
|
|
+
|
|
|
+ def Reset(self):
|
|
|
+ """Remove items from dialog list"""
|
|
|
+ self.selectedFeatures = []
|
|
|
+ if self._dialog:
|
|
|
+ self.slist.DeleteAllItems()
|
|
|
+ self._dialog.Raise()
|
|
|
+ self.RegisterMapEvtHandler()
|
|
|
+ self._draw()
|
|
|
+
|
|
|
+ def _onMapClickHandler(self, event):
|
|
|
+ """Registred handler for clicking on grass disp
|
|
|
+ """
|
|
|
+ if event == "unregistered":
|
|
|
+ return
|
|
|
+ vWhatDic = self.QuerySelectedMap()
|
|
|
+ if 'Category' in vWhatDic:
|
|
|
+ self.AddVecInfo(vWhatDic)
|
|
|
+ self._draw()
|
|
|
+ self._dialog.Raise()
|
|
|
+
|
|
|
+ def AddVecInfo(self, vInfoDictTMP):
|
|
|
+ """Update vector in list
|
|
|
+
|
|
|
+ Note: click on features add category
|
|
|
+ second click on the same vector remove category from list
|
|
|
+ """
|
|
|
+ if len(self.selectedFeatures) > 0:
|
|
|
+ for sel in self.selectedFeatures:
|
|
|
+ if sel['Category'] == vInfoDictTMP['Category']: #features is selected=> remove features
|
|
|
+ self.selectedFeatures.remove(sel)
|
|
|
+ if self._dialog:#if dialog initilized->update dialog
|
|
|
+ self.slist.RemoveItem(vInfoDictTMP)
|
|
|
+ return True
|
|
|
+
|
|
|
+ self.selectedFeatures.append(vInfoDictTMP)
|
|
|
+ if self._dialog:
|
|
|
+ self.slist.AddItem(vInfoDictTMP)
|
|
|
+ else: # only one is selected
|
|
|
+ self.selectedFeatures.append(vInfoDictTMP)
|
|
|
+ if self._dialog:
|
|
|
+ self.slist.AddItem(vInfoDictTMP)
|
|
|
+
|
|
|
+ if len(self.selectedFeatures) == 0:
|
|
|
+ return False
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+ def _draw(self):
|
|
|
+ """Call class 'VectorSelectHighlighter' to draw selected features"""
|
|
|
+ self.updateLayer.emit()
|
|
|
+ if len(self.selectedFeatures) > 0:
|
|
|
+ self.painter.SetLayer(self.selectedFeatures[0]['Layer'])
|
|
|
+ self.painter.SetMap(self.selectedFeatures[0]['Map'])
|
|
|
+ tmp = list()
|
|
|
+ for i in self.selectedFeatures:
|
|
|
+ tmp.append(i['Category'])
|
|
|
+
|
|
|
+ self.painter.SetCats(tmp)
|
|
|
+ self.painter.DrawSelected()
|
|
|
+ else:
|
|
|
+ self.painter.Clear()
|
|
|
+
|
|
|
+ def GetSelectedMap(self):
|
|
|
+ """Return name of selected map in layer tree"""
|
|
|
+ layerList = self._giface.GetLayerList()
|
|
|
+ layerSelected = layerList.GetSelectedLayer()
|
|
|
+ if not layerSelected.maplayer.IsActive():
|
|
|
+ GWarning(_("Selected map <%s> has been disabled for rendering. "
|
|
|
+ "Operation canceled.") % str(layerSelected), parent=self.mapWin)
|
|
|
+ return None
|
|
|
+
|
|
|
+ if layerSelected:
|
|
|
+ mapName = str(layerSelected)
|
|
|
+ if self.mapName is not None:
|
|
|
+ if self.mapName!=mapName:
|
|
|
+ self.Reset()
|
|
|
+ else:
|
|
|
+ mapName = None
|
|
|
+ self.UnregisterMapEvtHandler()
|
|
|
+ GError(_("No map layer selected. Operation canceled."))
|
|
|
+ return mapName
|
|
|
+
|
|
|
+ def QuerySelectedMap(self):
|
|
|
+ """Return w.what info from last clicked coords on display
|
|
|
+
|
|
|
+ """
|
|
|
+ self.mapName = self.GetSelectedMap()
|
|
|
+ if not self.mapName:
|
|
|
+ return {}
|
|
|
+
|
|
|
+ mapInfo = self.mapWin.GetMap()
|
|
|
+ threshold = 10.0 * ((mapInfo.region['e'] - mapInfo.region['w']) / mapInfo.width)
|
|
|
+ try:
|
|
|
+ query = grass.vector_what(map=[self.mapName],
|
|
|
+ coord=self.mapWin.GetLastEN(),
|
|
|
+ distance=threshold)
|
|
|
+ except grass.ScriptError:
|
|
|
+ GError(parent=self,
|
|
|
+ message=_("Failed to query vector map(s) <%s>.") % self.map)
|
|
|
+ return None
|
|
|
+
|
|
|
+ return query[0]
|
|
|
+
|
|
|
+ def getSelectedFeatures(self):
|
|
|
+ return self.selectedFeatures
|
|
|
+
|
|
|
+ def GetLineStringSelectedCats(self):
|
|
|
+ """Return line of categories separated by comma"""
|
|
|
+ strTMP = ''
|
|
|
+ for cat in self.selectedFeatures:
|
|
|
+ strTMP += str(cat['Category']) + ','
|
|
|
+ return strTMP[:-1]
|
|
|
+
|
|
|
+ def _id_generator(self, size=6, chars=string.ascii_uppercase + string.digits):
|
|
|
+ return ''.join(random.choice(chars) for _ in range(size))
|
|
|
+
|
|
|
+ def OnExportMap(self, event):
|
|
|
+ """Export selected features to a new map
|
|
|
+
|
|
|
+ Add new map layer to layer tree and checked it
|
|
|
+
|
|
|
+ @todo: set color of map to higlight color
|
|
|
+ """
|
|
|
+
|
|
|
+ if len(self.selectedFeatures)==0:
|
|
|
+ GMessage(_('No features selected'))
|
|
|
+ return
|
|
|
+ lst = ''
|
|
|
+ for cat in self.selectedFeatures: # build text string of categories for v.extract input
|
|
|
+ lst += str(cat['Category']) + ','
|
|
|
+ lst = lst[:-1]
|
|
|
+ outMap = str(self.selectedFeatures[0]['Map']) + '_selection' + str(self._id_generator(3))
|
|
|
+ ret, err = RunCommand('v.extract',
|
|
|
+ input=self.selectedFeatures[0]['Map'],
|
|
|
+ layer=self.selectedFeatures[0]['Layer'],
|
|
|
+ output=outMap,
|
|
|
+ cats=lst,
|
|
|
+ getErrorMsg=True)
|
|
|
+ if ret == 0:
|
|
|
+ tree = self._giface.GetLayerTree()
|
|
|
+ if tree:
|
|
|
+ tree.AddLayer(ltype='vector', lname=outMap,
|
|
|
+ lcmd=['d.vect', 'map=%s' % outMap],
|
|
|
+ lchecked=True)
|
|
|
+ #colorize new map
|
|
|
+ ret, err =RunCommand('d.vect',
|
|
|
+ map=outMap,
|
|
|
+ color='red',getErrorMsg=True)
|
|
|
+ self.Reset()
|
|
|
+ else:
|
|
|
+ GMessage(_('Vector map <%s> was created') % outMap)
|
|
|
+ self.Reset()
|
|
|
+ else:
|
|
|
+ GError(_("Unable to create a new vector map.\n\nReason: %s") % err)
|
|
|
+
|
|
|
+ """
|
|
|
+ def SetSelectedCat(self, cats):
|
|
|
+ # allows to set selected vector categories by list of cats (per line)
|
|
|
+ info = self.QuerySelectedMap()
|
|
|
+ if 'Category' not in info:
|
|
|
+ return
|
|
|
+
|
|
|
+ for cat in cats.splitlines():
|
|
|
+ tmpDict = {}
|
|
|
+ tmpDict['Category'] = cat
|
|
|
+ tmpDict['Map'] = info['Map']
|
|
|
+ tmpDict['Layer'] = info['Layer']
|
|
|
+ tmpDict['Type'] = '-'
|
|
|
+ self.AddVecInfo(tmpDict)
|
|
|
+
|
|
|
+ self._draw()
|
|
|
+ """
|
|
|
+
|
|
|
+class VectorSelectHighlighter():
|
|
|
+ """Class for highlighting selected features on display
|
|
|
+
|
|
|
+ :param mapdisp: Map display frame
|
|
|
+ """
|
|
|
+ def __init__(self, mapdisp, giface):
|
|
|
+ self.qlayer = None
|
|
|
+ self.mapdisp = mapdisp
|
|
|
+ self.giface = giface
|
|
|
+ self.layerCat = {}
|
|
|
+ self.data = {}
|
|
|
+ self.data['Category'] = list()
|
|
|
+ self.data['Map'] = None
|
|
|
+ self.data['Layer']= None
|
|
|
+
|
|
|
+ def SetMap(self, map):
|
|
|
+ self.data['Map'] = map
|
|
|
+
|
|
|
+ def SetLayer(self, layer):
|
|
|
+ self.data['Layer'] = layer
|
|
|
+
|
|
|
+ def SetCats(self, cats):
|
|
|
+ self.data['Category'] = cats
|
|
|
+
|
|
|
+ def Clear(self):
|
|
|
+ self.data['Category']=list()
|
|
|
+ self.data['Layer']=1
|
|
|
+ self.data['Map'] = None
|
|
|
+ self.giface.updateMap.emit(render=True, renderVector=True)
|
|
|
+
|
|
|
+ def DrawSelected(self):
|
|
|
+ """Highlight selected features"""
|
|
|
+ self.layerCat[int(self.data['Layer'])] = self.data['Category']
|
|
|
+
|
|
|
+ # add map layer with higlighted vector features
|
|
|
+ self.AddQueryMapLayer() # -> self.qlayer
|
|
|
+ self.qlayer.SetOpacity(0.7)
|
|
|
+ self.giface.updateMap.emit(render=True, renderVector=True)
|
|
|
+
|
|
|
+ def AddQueryMapLayer(self):
|
|
|
+ """Redraw a map
|
|
|
+
|
|
|
+ :return: True if map has been redrawn, False if no map is given
|
|
|
+ """
|
|
|
+ if self.mapdisp.GetMap().GetLayerIndex(self.qlayer) < 0:
|
|
|
+ self.qlayer = None
|
|
|
+
|
|
|
+ if self.qlayer:
|
|
|
+ self.qlayer.SetCmd(self.mapdisp.AddTmpVectorMapLayer(self.data['Map'], self.layerCat, addLayer=False))
|
|
|
+ else:
|
|
|
+ self.qlayer = self.mapdisp.AddTmpVectorMapLayer(self.data['Map'], self.layerCat)
|
|
|
+
|
|
|
+ return self.qlayer
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|