vselect.py 14 KB

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