vselect.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. """
  2. @package gui_core.vselect
  3. @brief wxGUI classes for interactive selection of vector
  4. features. Allows creating a new vector map from selected vector
  5. features or return their categories
  6. Classes:
  7. - vselect::VectorSelectList
  8. - vselect::VectorSelectDialog
  9. - vselect::VectorSelectBase
  10. - vselect::VectorSelectHighlighter
  11. (C) 2014-2015 by Matej Krejci, and the GRASS Development Team
  12. This program is free software under the GNU General Public License
  13. (>=v2). Read the file COPYING that comes with GRASS for details.
  14. @author Matej Krejci <matejkrejci gmail.com> (mentor: Martin Landa)
  15. """
  16. import string
  17. import random
  18. import wx
  19. import wx.lib.mixins.listctrl as listmix
  20. from core.utils import _
  21. from core.gcmd import GMessage, GError, GWarning
  22. from core.gcmd import RunCommand
  23. from gui_core.wrap import Button
  24. import grass.script as grass
  25. from grass.pydispatch.signal import Signal
  26. class VectorSelectList(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
  27. """Widget for managing vector features selected from map display
  28. """
  29. def __init__(self, parent):
  30. wx.ListCtrl.__init__(
  31. self,
  32. parent=parent,
  33. id=wx.ID_ANY,
  34. style=wx.LC_REPORT | wx.BORDER_SUNKEN)
  35. listmix.ListCtrlAutoWidthMixin.__init__(self)
  36. self.InsertColumn(col=0, heading=_('category'))
  37. self.InsertColumn(col=1, heading=_('type'))
  38. self.SetColumnWidth(0, 100)
  39. self.SetColumnWidth(1, 100)
  40. self.index = 0
  41. self.dictIndex = {}
  42. def AddItem(self, item):
  43. if 'Category' not in item:
  44. return
  45. pos = self.InsertStringItem(0, str(item['Category']))
  46. self.SetStringItem(pos, 1, str(item['Type']))
  47. self.dictIndex[str(item['Category'])] = pos
  48. def RemoveItem(self, item):
  49. index = self.dictIndex.get(str(item['Category']), -1)
  50. if index > -1:
  51. self.DeleteItem(index)
  52. class VectorSelectDialog(wx.Dialog):
  53. """Dialog for managing vector features selected from map display"""
  54. def __init__(self, parent, title=_("Select features"), size=(200, 300)):
  55. wx.Dialog.__init__(
  56. self,
  57. parent=parent,
  58. id=wx.ID_ANY,
  59. title=title,
  60. size=size,
  61. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
  62. self._layout()
  63. def AddWidget(self, widget, proportion=1, flag=wx.EXPAND):
  64. self.mainSizer.Add(widget, proportion=proportion, flag=flag)
  65. self.Layout()
  66. def _layout(self):
  67. self.mainSizer = wx.BoxSizer(wx.VERTICAL)
  68. self.SetSizer(self.mainSizer)
  69. self.Show()
  70. class VectorSelectBase():
  71. """@brief Main class of vector selection function
  72. It allows selecting vector features from map display and to export
  73. them as a new vector map. Current version allows selecting
  74. features one-by-one by single click in map display.
  75. This class can be initialized with (see CreateDialog()) or without
  76. (see gselect) dialog (see VectorSelectDialog).
  77. """
  78. def __init__(self, parent, giface):
  79. self.parent = parent
  80. self._giface = giface
  81. self.register = False
  82. self.mapWin = self._giface.GetMapWindow()
  83. self.mapDisp = giface.GetMapDisplay()
  84. self.RegisterMapEvtHandler()
  85. self.selectedFeatures = []
  86. self.mapName = None # chosen map for selecting features
  87. self._dialog = None
  88. self.onCloseDialog = None
  89. self.updateLayer = Signal('VectorSelectBase.updateLayer')
  90. self.painter = VectorSelectHighlighter(self.mapDisp, giface)
  91. def CreateDialog(self, createButton=True):
  92. """Create dialog
  93. :param createButton: True to add 'create new map' button
  94. """
  95. if self._dialog:
  96. return
  97. self._dialog = VectorSelectDialog(parent=self.parent)
  98. self._dialog.Bind(wx.EVT_CLOSE, self.OnCloseDialog)
  99. if createButton:
  100. createMap = Button(
  101. self._dialog, wx.ID_ANY, _("Create a new map"))
  102. createMap.Bind(wx.EVT_BUTTON, self.OnExportMap)
  103. self._dialog.AddWidget(createMap, proportion=0.1)
  104. self.slist = VectorSelectList(self._dialog)
  105. self.slist.Bind(wx.EVT_LIST_KEY_DOWN, self.OnDelete)
  106. self.slist.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnDeleteRow)
  107. self._dialog.AddWidget(self.slist)
  108. self.onCloseDialog = Signal('VectorSelectBase.onCloseDialog')
  109. def OnDeleteRow(self, event=None):
  110. """Delete row in widget
  111. """
  112. index = self.slist.GetFocusedItem()
  113. category = self.slist.GetItemText(index)
  114. for item in self.selectedFeatures:
  115. if int(item['Category']) == int(category):
  116. self.selectedFeatures.remove(item)
  117. break
  118. self.slist.DeleteItem(index)
  119. self._draw()
  120. def OnDelete(self, event):
  121. """Delete row in widget by press key(delete)
  122. """
  123. keycode = event.GetKeyCode()
  124. if keycode == wx.WXK_DELETE:
  125. self.OnDeleteRow()
  126. def RegisterMapEvtHandler(self):
  127. if not self.register:
  128. self.mapWin.RegisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  129. self._onMapClickHandler,
  130. 'cross')
  131. self.register = True
  132. def UnregisterMapEvtHandler(self):
  133. """Unregistrates _onMapClickHandler from mapWin"""
  134. if self.register:
  135. self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  136. self._onMapClickHandler)
  137. self.register = False
  138. def OnClose(self):
  139. self.selectedFeatures = []
  140. self._draw()
  141. self.UnregisterMapEvtHandler()
  142. def OnCloseDialog(self, evt=None):
  143. if not self.onCloseDialog:
  144. return
  145. self.onCloseDialog.emit()
  146. self.selectedFeatures = []
  147. self.painter.Clear()
  148. self._dialog.Destroy()
  149. self.UnregisterMapEvtHandler()
  150. def Reset(self):
  151. """Remove items from dialog list"""
  152. self.selectedFeatures = []
  153. if self._dialog:
  154. self.slist.DeleteAllItems()
  155. self._dialog.Raise()
  156. self.RegisterMapEvtHandler()
  157. def _onMapClickHandler(self, event):
  158. """Registred handler for clicking on grass disp
  159. """
  160. if event == "unregistered":
  161. return
  162. vWhatDic = self.QuerySelectedMap()
  163. if 'Category' in vWhatDic:
  164. self.AddVecInfo(vWhatDic)
  165. self._draw()
  166. if self._dialog:
  167. self._dialog.Raise()
  168. def AddVecInfo(self, vInfoDictTMP):
  169. """Update vector in list
  170. Note: click on features add category
  171. second click on the same vector remove category from list
  172. """
  173. if len(self.selectedFeatures) > 0:
  174. for sel in self.selectedFeatures:
  175. if sel['Category'] == vInfoDictTMP[
  176. 'Category']: # features is selected=> remove features
  177. self.selectedFeatures.remove(sel)
  178. if self._dialog: # if dialog initilized->update dialog
  179. self.slist.RemoveItem(vInfoDictTMP)
  180. return True
  181. self.selectedFeatures.append(vInfoDictTMP)
  182. if self._dialog:
  183. self.slist.AddItem(vInfoDictTMP)
  184. else: # only one is selected
  185. self.selectedFeatures.append(vInfoDictTMP)
  186. if self._dialog:
  187. self.slist.AddItem(vInfoDictTMP)
  188. if len(self.selectedFeatures) == 0:
  189. return False
  190. return True
  191. def _draw(self):
  192. """Call class 'VectorSelectHighlighter' to draw selected features"""
  193. self.updateLayer.emit()
  194. if len(self.selectedFeatures) > 0:
  195. self.painter.SetLayer(self.selectedFeatures[0]['Layer'])
  196. self.painter.SetMap(self.selectedFeatures[0]['Map'])
  197. tmp = list()
  198. for i in self.selectedFeatures:
  199. tmp.append(i['Category'])
  200. self.painter.SetCats(tmp)
  201. self.painter.DrawSelected()
  202. else:
  203. self.painter.Clear()
  204. def GetSelectedMap(self):
  205. """Return name of selected map in layer tree"""
  206. layerList = self._giface.GetLayerList()
  207. layerSelected = layerList.GetSelectedLayer()
  208. if layerSelected is None:
  209. return None
  210. if not layerSelected.maplayer.IsActive():
  211. GWarning(_("Selected map <%s> has been disabled for rendering. "
  212. "Operation canceled.") % str(layerSelected), parent=self.mapWin)
  213. return None
  214. if layerSelected:
  215. mapName = str(layerSelected)
  216. if self.mapName is not None:
  217. if self.mapName != mapName:
  218. self.Reset()
  219. else:
  220. mapName = None
  221. self.UnregisterMapEvtHandler()
  222. GError(_("No map layer selected. Operation canceled."))
  223. return mapName
  224. def QuerySelectedMap(self):
  225. """Return w.what info from last clicked coords on display
  226. """
  227. self.mapName = self.GetSelectedMap()
  228. if not self.mapName:
  229. return {}
  230. mapInfo = self.mapWin.GetMap()
  231. threshold = 10.0 * (
  232. (mapInfo.region['e'] - mapInfo.region['w']) / mapInfo.width)
  233. try:
  234. query = grass.vector_what(map=[self.mapName],
  235. coord=self.mapWin.GetLastEN(),
  236. distance=threshold, skip_attributes=True)
  237. except grass.ScriptError:
  238. GError(parent=self,
  239. message=_("Failed to query vector map(s) <%s>.") % self.map)
  240. return None
  241. return query[0]
  242. def GetLineStringSelectedCats(self):
  243. """Return line of categories separated by comma"""
  244. strTMP = ''
  245. for cat in self.selectedFeatures:
  246. strTMP += str(cat['Category']) + ','
  247. return strTMP[:-1]
  248. def _id_generator(self, size=6,
  249. chars=string.ascii_uppercase + string.digits):
  250. return ''.join(random.choice(chars) for _ in range(size))
  251. def OnExportMap(self, event):
  252. """Export selected features to a new map
  253. Add new map layer to layer tree and checked it
  254. @todo: set color of map to higlight color
  255. """
  256. if len(self.selectedFeatures) == 0:
  257. GMessage(_('No features selected'))
  258. return
  259. lst = ''
  260. for cat in self.selectedFeatures: # build text string of categories for v.extract input
  261. lst += str(cat['Category']) + ','
  262. lst = lst[:-1]
  263. outMap = str(self.selectedFeatures[0][
  264. 'Map']) + '_selection' + str(self._id_generator(3))
  265. ret, err = RunCommand('v.extract',
  266. input=self.selectedFeatures[0]['Map'],
  267. layer=self.selectedFeatures[0]['Layer'],
  268. output=outMap,
  269. cats=lst,
  270. getErrorMsg=True)
  271. if ret == 0:
  272. tree = self._giface.GetLayerTree()
  273. if tree:
  274. tree.AddLayer(ltype='vector', lname=outMap,
  275. lcmd=['d.vect', 'map=%s' % outMap],
  276. lchecked=True)
  277. # TODO colorize new map
  278. self.Reset()
  279. else:
  280. GMessage(_('Vector map <%s> was created') % outMap)
  281. self.Reset()
  282. else:
  283. GError(_("Unable to create a new vector map.\n\nReason: %s") % err)
  284. """
  285. def SetSelectedCat(self, cats):
  286. # allows setting selected vector categories by list of cats (per line)
  287. info = self.QuerySelectedMap()
  288. if 'Category' not in info:
  289. return
  290. for cat in cats.splitlines():
  291. tmpDict = {}
  292. tmpDict['Category'] = cat
  293. tmpDict['Map'] = info['Map']
  294. tmpDict['Layer'] = info['Layer']
  295. tmpDict['Type'] = '-'
  296. self.AddVecInfo(tmpDict)
  297. self._draw()
  298. """
  299. class VectorSelectHighlighter():
  300. """Class for highlighting selected features on display
  301. :param mapdisp: Map display frame
  302. """
  303. def __init__(self, mapdisp, giface):
  304. self.qlayer = None
  305. self.mapdisp = mapdisp
  306. self.giface = giface
  307. self.layerCat = {}
  308. self.data = {}
  309. self.data['Category'] = list()
  310. self.data['Map'] = None
  311. self.data['Layer'] = None
  312. def SetMap(self, map):
  313. self.data['Map'] = map
  314. def SetLayer(self, layer):
  315. self.data['Layer'] = layer
  316. def SetCats(self, cats):
  317. self.data['Category'] = cats
  318. def Clear(self):
  319. self.data['Category'] = list()
  320. self.data['Map'] = None
  321. self.data['Layer'] = None
  322. self.mapdisp.RemoveQueryLayer()
  323. self.giface.GetMapWindow().UpdateMap(render=False)
  324. def DrawSelected(self):
  325. """Highlight selected features"""
  326. self.layerCat[int(self.data['Layer'])] = self.data['Category']
  327. # add map layer with higlighted vector features
  328. self.AddQueryMapLayer() # -> self.qlayer
  329. self.qlayer.SetOpacity(0.7)
  330. self.giface.updateMap.emit(render=True, renderVector=True)
  331. def AddQueryMapLayer(self):
  332. """Redraw a map
  333. :return: True if map has been redrawn, False if no map is given
  334. """
  335. if self.mapdisp.GetMap().GetLayerIndex(self.qlayer) < 0:
  336. self.qlayer = None
  337. if self.qlayer:
  338. self.qlayer.SetCmd(
  339. self.mapdisp.AddTmpVectorMapLayer(
  340. self.data['Map'],
  341. self.layerCat,
  342. addLayer=False))
  343. else:
  344. self.qlayer = self.mapdisp.AddTmpVectorMapLayer(
  345. self.data['Map'], self.layerCat)
  346. return self.qlayer