vselect.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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. category = self.slist.GetItemText(index)
  113. for item in self.selectedFeatures:
  114. if int(item['Category']) == int(category):
  115. self.selectedFeatures.remove(item)
  116. break
  117. self.slist.DeleteItem(index)
  118. self._draw()
  119. def OnDelete(self, event):
  120. """Delete row in widget by press key(delete)
  121. """
  122. keycode = event.GetKeyCode()
  123. if keycode == wx.WXK_DELETE:
  124. self.OnDeleteRow()
  125. def RegisterMapEvtHandler(self):
  126. if not self.register:
  127. self.mapWin.RegisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  128. self._onMapClickHandler,
  129. 'cross')
  130. self.register = True
  131. def UnregisterMapEvtHandler(self):
  132. """Unregistrates _onMapClickHandler from mapWin"""
  133. if self.register:
  134. self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  135. self._onMapClickHandler)
  136. self.register = False
  137. def OnClose(self):
  138. self.selectedFeatures = []
  139. self._draw()
  140. self.UnregisterMapEvtHandler()
  141. def OnCloseDialog(self, evt=None):
  142. if not self.onCloseDialog:
  143. return
  144. self.onCloseDialog.emit()
  145. self.selectedFeatures = []
  146. self.painter.Clear()
  147. self._dialog.Destroy()
  148. self.UnregisterMapEvtHandler()
  149. def Reset(self):
  150. """Remove items from dialog list"""
  151. self.selectedFeatures = []
  152. if self._dialog:
  153. self.slist.DeleteAllItems()
  154. self._dialog.Raise()
  155. self.RegisterMapEvtHandler()
  156. def _onMapClickHandler(self, event):
  157. """Registred handler for clicking on grass disp
  158. """
  159. if event == "unregistered":
  160. return
  161. vWhatDic = self.QuerySelectedMap()
  162. if 'Category' in vWhatDic:
  163. self.AddVecInfo(vWhatDic)
  164. self._draw()
  165. if self._dialog:
  166. self._dialog.Raise()
  167. def AddVecInfo(self, vInfoDictTMP):
  168. """Update vector in list
  169. Note: click on features add category
  170. second click on the same vector remove category from list
  171. """
  172. if len(self.selectedFeatures) > 0:
  173. for sel in self.selectedFeatures:
  174. if sel['Category'] == vInfoDictTMP[
  175. 'Category']: # features is selected=> remove features
  176. self.selectedFeatures.remove(sel)
  177. if self._dialog: # if dialog initilized->update dialog
  178. self.slist.RemoveItem(vInfoDictTMP)
  179. return True
  180. self.selectedFeatures.append(vInfoDictTMP)
  181. if self._dialog:
  182. self.slist.AddItem(vInfoDictTMP)
  183. else: # only one is selected
  184. self.selectedFeatures.append(vInfoDictTMP)
  185. if self._dialog:
  186. self.slist.AddItem(vInfoDictTMP)
  187. if len(self.selectedFeatures) == 0:
  188. return False
  189. return True
  190. def _draw(self):
  191. """Call class 'VectorSelectHighlighter' to draw selected features"""
  192. self.updateLayer.emit()
  193. if len(self.selectedFeatures) > 0:
  194. self.painter.SetLayer(self.selectedFeatures[0]['Layer'])
  195. self.painter.SetMap(
  196. self.selectedFeatures[0]['Map'] + '@' + self.selectedFeatures[0]['Mapset']
  197. )
  198. tmp = list()
  199. for i in self.selectedFeatures:
  200. tmp.append(i['Category'])
  201. self.painter.SetCats(tmp)
  202. self.painter.DrawSelected()
  203. else:
  204. self.painter.Clear()
  205. def GetSelectedMap(self):
  206. """Return name of selected map in layer tree"""
  207. layerList = self._giface.GetLayerList()
  208. layerSelected = layerList.GetSelectedLayer()
  209. if layerSelected is None:
  210. return None
  211. if not layerSelected.maplayer.IsActive():
  212. GWarning(_("Selected map <%s> has been disabled for rendering. "
  213. "Operation canceled.") % str(layerSelected), parent=self.mapWin)
  214. return None
  215. if layerSelected:
  216. mapName = str(layerSelected)
  217. if self.mapName is not None:
  218. if self.mapName != mapName:
  219. self.Reset()
  220. else:
  221. mapName = None
  222. self.UnregisterMapEvtHandler()
  223. GError(_("No map layer selected. Operation canceled."))
  224. return mapName
  225. def QuerySelectedMap(self):
  226. """Return w.what info from last clicked coords on display
  227. """
  228. self.mapName = self.GetSelectedMap()
  229. if not self.mapName:
  230. return {}
  231. mapInfo = self.mapWin.GetMap()
  232. threshold = 10.0 * (
  233. (mapInfo.region['e'] - mapInfo.region['w']) / mapInfo.width)
  234. try:
  235. query = grass.vector_what(map=[self.mapName],
  236. coord=self.mapWin.GetLastEN(),
  237. distance=threshold, skip_attributes=True)
  238. except grass.ScriptError:
  239. GError(parent=self,
  240. message=_("Failed to query vector map(s) <%s>.") % self.map)
  241. return None
  242. return query[0]
  243. def GetLineStringSelectedCats(self):
  244. """Return line of categories separated by comma"""
  245. strTMP = ''
  246. for cat in self.selectedFeatures:
  247. strTMP += str(cat['Category']) + ','
  248. return strTMP[:-1]
  249. def _id_generator(self, size=6,
  250. chars=string.ascii_uppercase + string.digits):
  251. return ''.join(random.choice(chars) for _ in range(size))
  252. def OnExportMap(self, event):
  253. """Export selected features to a new map
  254. Add new map layer to layer tree and checked it
  255. @todo: set color of map to higlight color
  256. """
  257. if len(self.selectedFeatures) == 0:
  258. GMessage(_('No features selected'))
  259. return
  260. lst = ''
  261. for cat in self.selectedFeatures: # build text string of categories for v.extract input
  262. lst += str(cat['Category']) + ','
  263. lst = lst[:-1]
  264. outMap = str(self.selectedFeatures[0][
  265. 'Map']) + '_selection' + str(self._id_generator(3))
  266. ret, err = RunCommand('v.extract',
  267. input=self.selectedFeatures[0]['Map'],
  268. layer=self.selectedFeatures[0]['Layer'],
  269. output=outMap,
  270. cats=lst,
  271. getErrorMsg=True)
  272. if ret == 0:
  273. tree = self._giface.GetLayerTree()
  274. if tree:
  275. tree.AddLayer(ltype='vector', lname=outMap,
  276. lcmd=['d.vect', 'map=%s' % outMap],
  277. lchecked=True)
  278. # TODO colorize new map
  279. self.Reset()
  280. else:
  281. GMessage(_('Vector map <%s> was created') % outMap)
  282. self.Reset()
  283. else:
  284. GError(_("Unable to create a new vector map.\n\nReason: %s") % err)
  285. """
  286. def SetSelectedCat(self, cats):
  287. # allows setting selected vector categories by list of cats (per line)
  288. info = self.QuerySelectedMap()
  289. if 'Category' not in info:
  290. return
  291. for cat in cats.splitlines():
  292. tmpDict = {}
  293. tmpDict['Category'] = cat
  294. tmpDict['Map'] = info['Map']
  295. tmpDict['Layer'] = info['Layer']
  296. tmpDict['Type'] = '-'
  297. self.AddVecInfo(tmpDict)
  298. self._draw()
  299. """
  300. class VectorSelectHighlighter():
  301. """Class for highlighting selected features on display
  302. :param mapdisp: Map display frame
  303. """
  304. def __init__(self, mapdisp, giface):
  305. self.qlayer = None
  306. self.mapdisp = mapdisp
  307. self.giface = giface
  308. self.layerCat = {}
  309. self.data = {}
  310. self.data['Category'] = list()
  311. self.data['Map'] = None
  312. self.data['Layer'] = None
  313. def SetMap(self, map):
  314. self.data['Map'] = map
  315. def SetLayer(self, layer):
  316. self.data['Layer'] = layer
  317. def SetCats(self, cats):
  318. self.data['Category'] = cats
  319. def Clear(self):
  320. self.data['Category'] = list()
  321. self.data['Map'] = None
  322. self.data['Layer'] = None
  323. self.mapdisp.RemoveQueryLayer()
  324. self.giface.GetMapWindow().UpdateMap(render=False)
  325. def DrawSelected(self):
  326. """Highlight selected features"""
  327. self.layerCat[int(self.data['Layer'])] = self.data['Category']
  328. # add map layer with higlighted vector features
  329. self.AddQueryMapLayer() # -> self.qlayer
  330. self.qlayer.SetOpacity(0.7)
  331. self.giface.updateMap.emit(render=True, renderVector=True)
  332. def AddQueryMapLayer(self):
  333. """Redraw a map
  334. :return: True if map has been redrawn, False if no map is given
  335. """
  336. if self.mapdisp.GetMap().GetLayerIndex(self.qlayer) < 0:
  337. self.qlayer = None
  338. if self.qlayer:
  339. self.qlayer.SetCmd(
  340. self.mapdisp.AddTmpVectorMapLayer(
  341. self.data['Map'],
  342. self.layerCat,
  343. addLayer=False))
  344. else:
  345. self.qlayer = self.mapdisp.AddTmpVectorMapLayer(
  346. self.data['Map'], self.layerCat)
  347. return self.qlayer