controller.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. # -*- coding: utf-8 -*-
  2. """
  3. @package rdigit.controller
  4. @brief rdigit controller for drawing and rasterizing
  5. Classes:
  6. - controller::RDigitController
  7. (C) 2014 by the GRASS Development Team
  8. This program is free software under the GNU General Public
  9. License (>=v2). Read the file COPYING that comes with GRASS
  10. for details.
  11. @author Anna Petrasova <kratochanna gmail.com>
  12. """
  13. import os
  14. import tempfile
  15. import wx
  16. import uuid
  17. from wx.lib.newevent import NewEvent
  18. from grass.script import core as gcore
  19. from grass.script import raster as grast
  20. from grass.exceptions import CalledModuleError, ScriptError
  21. from grass.pydispatch.signal import Signal
  22. from core.gcmd import GError, GMessage
  23. from core.settings import UserSettings
  24. from core.gthread import gThread
  25. from rdigit.dialogs import NewRasterDialog
  26. updateProgress, EVT_UPDATE_PROGRESS = NewEvent()
  27. class RDigitController(wx.EvtHandler):
  28. """Controller object for raster digitizer.
  29. Inherits from EvtHandler to be able to send wx events from thraed.
  30. """
  31. def __init__(self, giface, mapWindow):
  32. """Constructs controller
  33. :param giface: grass interface object
  34. :param mapWindow: instance of BufferedMapWindow
  35. """
  36. wx.EvtHandler.__init__(self)
  37. self._giface = giface
  38. self._mapWindow = mapWindow
  39. # thread for running rasterization process
  40. self._thread = gThread()
  41. # name of raster map which is edited (also new one)
  42. self._editedRaster = None
  43. # name of optional background raster
  44. self._backgroundRaster = None
  45. # name of temporary raster used to backup original state
  46. self._backupRasterName = None
  47. # if we edit an old raster or a new one (important for setting color
  48. # table)
  49. self._editOldRaster = False
  50. # type of output raster map (CELL, FCELL, DCELL)
  51. self._mapType = None
  52. # GraphicsSet for drawing areas, lines, points
  53. self._areas = None
  54. self._lines = None
  55. self._points = None
  56. # list of all GraphicsItems in the order of drawing
  57. self._all = []
  58. # if in state of drawing lin or area
  59. self._drawing = False
  60. # if running digitizing process in thread (to block drawing)
  61. self._running = False
  62. # color used to draw (should be moved to settings)
  63. self._drawColor = wx.GREEN
  64. # transparency used to draw (should be moved to settings)
  65. self._drawTransparency = 100
  66. # current selected drawing method
  67. self._graphicsType = 'area'
  68. # last edited cell value
  69. self._currentCellValue = None
  70. # last edited buffer value
  71. self._currentWidthValue = None
  72. # digit env
  73. self._env = os.environ.copy()
  74. self._oldMouseUse = None
  75. self._oldCursor = None
  76. # signal to add new raster to toolbar items
  77. self.newRasterCreated = Signal('RDigitController:newRasterCreated')
  78. # signal to add just used cell value in toolbar combo
  79. self.newFeatureCreated = Signal('RDigitController:newFeatureCreated')
  80. # signal to upload unique categories of background map into toolbar
  81. # combo
  82. self.uploadMapCategories = Signal(
  83. 'RDigitController:uploadMapCategories')
  84. self.quitDigitizer = Signal('RDigitController:quitDigitizer')
  85. self.showNotification = Signal('RDigitController:showNotification')
  86. def _connectAll(self):
  87. self._mapWindow.mouseLeftDown.connect(self._start)
  88. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  89. self._mapWindow.mouseRightUp.connect(self._finish)
  90. self._mapWindow.Unbind(wx.EVT_CONTEXT_MENU)
  91. def _disconnectAll(self):
  92. self._mapWindow.mouseLeftDown.disconnect(self._start)
  93. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  94. self._mapWindow.mouseRightUp.disconnect(self._finish)
  95. self._mapWindow.Bind(
  96. wx.EVT_CONTEXT_MENU,
  97. self._mapWindow.OnContextMenu)
  98. def _start(self, x, y):
  99. """Start digitizing a new object.
  100. :param x: x coordinate in map units
  101. :param y: y coordinate in map units
  102. """
  103. if self._running:
  104. return
  105. if not self._editedRaster:
  106. GMessage(parent=self._mapWindow, message=_(
  107. "Please select first the raster map"))
  108. return
  109. if not self._drawing:
  110. if self._graphicsType == 'area':
  111. item = self._areas.AddItem(coords=[])
  112. item.SetPropertyVal('penName', 'pen1')
  113. self._all.append(item)
  114. elif self._graphicsType == 'line':
  115. item = self._lines.AddItem(coords=[])
  116. item.SetPropertyVal('penName', 'pen1')
  117. self._all.append(item)
  118. elif self._graphicsType == 'point':
  119. item = self._points.AddItem(coords=[])
  120. item.SetPropertyVal('penName', 'pen1')
  121. self._all.append(item)
  122. self._drawing = True
  123. def _addPoint(self, x, y):
  124. """Add point to an object.
  125. :param x: x coordinate in map units
  126. :param y: y coordinate in map units
  127. """
  128. if self._running:
  129. return
  130. if not self._drawing:
  131. return
  132. if self._graphicsType == 'area':
  133. area = self._areas.GetItem(-1)
  134. coords = area.GetCoords() + [[x, y]]
  135. area.SetCoords(coords)
  136. self.showNotification.emit(text=_("Right click to finish area"))
  137. elif self._graphicsType == 'line':
  138. line = self._lines.GetItem(-1)
  139. coords = line.GetCoords() + [[x, y]]
  140. line.SetCoords(coords)
  141. self.showNotification.emit(text=_("Right click to finish line"))
  142. elif self._graphicsType == 'point':
  143. point = self._points.GetItem(-1)
  144. point.SetCoords([x, y])
  145. self._finish()
  146. # draw
  147. self._mapWindow.ClearLines()
  148. self._lines.Draw()
  149. self._areas.Draw()
  150. self._points.Draw()
  151. self._mapWindow.Refresh()
  152. def _finish(self):
  153. """Finish digitizing a new object and redraws.
  154. Saves current cell value and buffer width for that object.
  155. :param x: x coordinate in map units
  156. :param y: y coordinate in map units
  157. """
  158. if self._running:
  159. return
  160. if self._graphicsType == 'point':
  161. item = self._points.GetItem(-1)
  162. elif self._graphicsType == 'area':
  163. item = self._areas.GetItem(-1)
  164. elif self._graphicsType == 'line':
  165. item = self._lines.GetItem(-1)
  166. else:
  167. return
  168. self._drawing = False
  169. item.SetPropertyVal('brushName', 'done')
  170. item.AddProperty('cellValue')
  171. item.AddProperty('widthValue')
  172. item.SetPropertyVal('cellValue', self._currentCellValue)
  173. item.SetPropertyVal('widthValue', self._currentWidthValue)
  174. self.newFeatureCreated.emit()
  175. self._mapWindow.ClearLines()
  176. self._points.Draw()
  177. self._areas.Draw()
  178. self._lines.Draw()
  179. self._mapWindow.Refresh()
  180. def SelectType(self, drawingType):
  181. """Selects method (area/line/point) for drawing.
  182. Connects and disconnects signal to allow other tools
  183. in map toolbar to work.
  184. """
  185. if self._graphicsType and drawingType and self._graphicsType != drawingType \
  186. and self._drawing:
  187. # if we select different drawing tool, finish the feature
  188. self._finish()
  189. if self._graphicsType and not drawingType:
  190. self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
  191. self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin']
  192. # disconnect mouse events
  193. self._disconnectAll()
  194. self._mapWindow.SetNamedCursor(self._oldCursor)
  195. self._mapWindow.mouse['use'] = self._oldMouseUse
  196. elif self._graphicsType is None and drawingType:
  197. self._connectAll()
  198. # change mouse['box'] and pen to draw line during dragging
  199. # TODO: better solution for drawing this line
  200. self._mapWindow.mouse['use'] = None
  201. self._mapWindow.mouse['box'] = "line"
  202. self._mapWindow.pen = wx.Pen(
  203. colour='red', width=2, style=wx.SHORT_DASH)
  204. # change the cursor
  205. self._mapWindow.SetNamedCursor('pencil')
  206. self._graphicsType = drawingType
  207. def SetCellValue(self, value):
  208. self._currentCellValue = value
  209. def SetWidthValue(self, value):
  210. self._currentWidthValue = value
  211. def ChangeDrawColor(self, color):
  212. self._drawColor = color[:3] + (self._drawTransparency,)
  213. for each in (self._areas, self._lines, self._points):
  214. each.GetPen('pen1').SetColour(self._drawColor)
  215. each.GetBrush('done').SetColour(self._drawColor)
  216. self._mapWindow.UpdateMap(render=False)
  217. def Start(self):
  218. """Registers graphics to map window,
  219. connect required mouse signals.
  220. """
  221. self._oldMouseUse = self._mapWindow.mouse['use']
  222. self._oldCursor = self._mapWindow.GetNamedCursor()
  223. self._connectAll()
  224. # change mouse['box'] and pen to draw line during dragging
  225. # TODO: better solution for drawing this line
  226. self._mapWindow.mouse['use'] = None
  227. self._mapWindow.mouse['box'] = "line"
  228. self._mapWindow.pen = wx.Pen(
  229. colour='red', width=2, style=wx.SHORT_DASH)
  230. color = self._drawColor[:3] + (self._drawTransparency,)
  231. self._areas = self._mapWindow.RegisterGraphicsToDraw(
  232. graphicsType='polygon', pdc=self._mapWindow.pdcTransparent, mapCoords=True)
  233. self._areas.AddPen(
  234. 'pen1',
  235. wx.Pen(
  236. colour=color,
  237. width=2,
  238. style=wx.SOLID))
  239. self._areas.AddBrush('done', wx.Brush(colour=color, style=wx.SOLID))
  240. self._lines = self._mapWindow.RegisterGraphicsToDraw(
  241. graphicsType='line', pdc=self._mapWindow.pdcTransparent, mapCoords=True)
  242. self._lines.AddPen(
  243. 'pen1',
  244. wx.Pen(
  245. colour=color,
  246. width=2,
  247. style=wx.SOLID))
  248. self._lines.AddBrush('done', wx.Brush(colour=color, style=wx.SOLID))
  249. self._points = self._mapWindow.RegisterGraphicsToDraw(
  250. graphicsType='point', pdc=self._mapWindow.pdcTransparent, mapCoords=True)
  251. self._points.AddPen(
  252. 'pen1',
  253. wx.Pen(
  254. colour=color,
  255. width=2,
  256. style=wx.SOLID))
  257. self._points.AddBrush('done', wx.Brush(colour=color, style=wx.SOLID))
  258. # change the cursor
  259. self._mapWindow.SetNamedCursor('pencil')
  260. def Stop(self):
  261. """Before stopping digitizer, asks to save edits"""
  262. if self._editedRaster:
  263. dlg = wx.MessageDialog(
  264. self._mapWindow,
  265. _("Do you want to save changes?"),
  266. _("Save raster map changes"),
  267. wx.YES_NO)
  268. if dlg.ShowModal() == wx.ID_YES:
  269. if self._drawing:
  270. self._finish()
  271. self._thread.Run(callable=self._exportRaster,
  272. ondone=lambda event: self._updateAndQuit())
  273. else:
  274. self.quitDigitizer.emit()
  275. else:
  276. self.quitDigitizer.emit()
  277. def Save(self):
  278. """Saves current edits to a raster map"""
  279. if self._drawing:
  280. self._finish()
  281. self._thread.Run(callable=self._exportRaster,
  282. ondone=lambda event: self._update())
  283. def Undo(self):
  284. """Undo a change, goes object back (finished or not finished)"""
  285. if len(self._all):
  286. removed = self._all.pop(-1)
  287. # try to remove from each, it fails quietly when theitem is not
  288. # there
  289. self._areas.DeleteItem(removed)
  290. self._lines.DeleteItem(removed)
  291. self._points.DeleteItem(removed)
  292. self._drawing = False
  293. self._mapWindow.UpdateMap(render=False)
  294. def CleanUp(self, restore=True):
  295. """Cleans up drawing, temporary maps.
  296. :param restore: if restore previous cursor, mouse['use']
  297. """
  298. try:
  299. if self._backupRasterName:
  300. gcore.run_command(
  301. 'g.remove',
  302. type='raster',
  303. flags='f',
  304. name=self._backupRasterName,
  305. quiet=True)
  306. except CalledModuleError:
  307. pass
  308. self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
  309. self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin']
  310. # disconnect mouse events
  311. if self._graphicsType:
  312. self._disconnectAll()
  313. # unregister
  314. self._mapWindow.UnregisterGraphicsToDraw(self._areas)
  315. self._mapWindow.UnregisterGraphicsToDraw(self._lines)
  316. self._mapWindow.UnregisterGraphicsToDraw(self._points)
  317. #self._registeredGraphics = None
  318. self._mapWindow.UpdateMap(render=False)
  319. if restore:
  320. # restore mouse['use'] and cursor to the state before measuring
  321. # starts
  322. self._mapWindow.SetNamedCursor(self._oldCursor)
  323. self._mapWindow.mouse['use'] = self._oldMouseUse
  324. def _updateAndQuit(self):
  325. """Called when thread is done. Updates map and calls to quits digitizer."""
  326. self._running = False
  327. self._mapWindow.UpdateMap(render=True)
  328. self.quitDigitizer.emit()
  329. def _update(self):
  330. """Called when thread is done. Updates map."""
  331. self._running = False
  332. self._mapWindow.UpdateMap(render=True)
  333. def SelectOldMap(self, name):
  334. """After selecting old raster, creates a backup copy for editing."""
  335. try:
  336. self._backupRaster(name)
  337. except ScriptError:
  338. GError(parent=self._mapWindow, message=_(
  339. "Failed to create backup copy of edited raster map."))
  340. return False
  341. self._editedRaster = name
  342. self._mapType = grast.raster_info(map=name)['datatype']
  343. self._editOldRaster = True
  344. return True
  345. def SelectNewMap(self):
  346. """After selecting new raster, shows dialog to choose name,
  347. background map and type of the new map."""
  348. dlg = NewRasterDialog(parent=self._mapWindow)
  349. dlg.CenterOnParent()
  350. if dlg.ShowModal() == wx.ID_OK:
  351. try:
  352. self._createNewMap(mapName=dlg.GetMapName(),
  353. backgroundMap=dlg.GetBackgroundMapName(),
  354. mapType=dlg.GetMapType())
  355. except ScriptError:
  356. GError(parent=self._mapWindow, message=_(
  357. "Failed to create new raster map."))
  358. return False
  359. finally:
  360. dlg.Destroy()
  361. return True
  362. else:
  363. dlg.Destroy()
  364. return False
  365. def _createNewMap(self, mapName, backgroundMap, mapType):
  366. """Creates a new raster map based on specified background and type."""
  367. name = mapName.split('@')[0]
  368. background = backgroundMap.split('@')[0]
  369. types = {'CELL': 'int', 'FCELL': 'float', 'DCELL': 'double'}
  370. if background:
  371. back = background
  372. else:
  373. back = 'null()'
  374. try:
  375. grast.mapcalc(
  376. exp="{name} = {mtype}({back})".format(
  377. name=name,
  378. mtype=types[mapType],
  379. back=back),
  380. overwrite=True,
  381. quiet=True)
  382. if background:
  383. self._backgroundRaster = backgroundMap
  384. gcore.run_command(
  385. 'r.colors',
  386. map=name,
  387. raster=self._backgroundRaster,
  388. quiet=True)
  389. if mapType == 'CELL':
  390. values = gcore.read_command('r.describe', flags='1n',
  391. map=name, quiet=True).strip()
  392. if values:
  393. self.uploadMapCategories.emit(
  394. values=values.split('\n'))
  395. except CalledModuleError:
  396. raise ScriptError
  397. self._backupRaster(name)
  398. name = name + '@' + gcore.gisenv()['MAPSET']
  399. self._editedRaster = name
  400. self._mapType = mapType
  401. self.newRasterCreated.emit(name=name)
  402. gisenv = gcore.gisenv()
  403. self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
  404. location=gisenv['LOCATION_NAME'],
  405. mapset=gisenv['MAPSET'],
  406. action='new',
  407. map=name.split('@')[0],
  408. element='raster')
  409. def _backupRaster(self, name):
  410. """Creates a temporary backup raster necessary for undo behavior.
  411. :param str name: name of raster map for which we create backup
  412. """
  413. name = name.split('@')[0]
  414. backup = name + '_backupcopy_' + str(os.getpid())
  415. try:
  416. gcore.run_command('g.copy', raster=[name, backup], quiet=True)
  417. except CalledModuleError:
  418. raise ScriptError
  419. self._backupRasterName = backup
  420. def _exportRaster(self):
  421. """Rasterizes digitized features.
  422. Uses r.in.poly and r.grow for buffering features. Creates separate raster
  423. maps depending on common cell values and buffering width necessary to
  424. keep the order of editing. These rasters are then patched together.
  425. Sets default color table for the newly digitized raster.
  426. """
  427. self._setRegion()
  428. if not self._editedRaster or self._running:
  429. return
  430. self._running = True
  431. if len(self._all) < 1:
  432. new = self._editedRaster
  433. if '@' in self._editedRaster:
  434. new = self._editedRaster.split('@')[0]
  435. gcore.run_command('g.copy', raster=[self._backupRasterName, new],
  436. overwrite=True, quiet=True)
  437. else:
  438. tempRaster = 'tmp_rdigit_rast_' + str(os.getpid())
  439. text = []
  440. rastersToPatch = []
  441. i = 0
  442. lastCellValue = lastWidthValue = None
  443. evt = updateProgress(
  444. range=len(self._all),
  445. value=0, text=_("Rasterizing..."))
  446. wx.PostEvent(self, evt)
  447. lastCellValue = self._all[0].GetPropertyVal('cellValue')
  448. lastWidthValue = self._all[0].GetPropertyVal('widthValue')
  449. for item in self._all:
  450. if item.GetPropertyVal('widthValue') and \
  451. (lastCellValue != item.GetPropertyVal('cellValue') or
  452. lastWidthValue != item.GetPropertyVal('widthValue')):
  453. if text:
  454. out = self._rasterize(
  455. text, lastWidthValue, self._mapType, tempRaster)
  456. rastersToPatch.append(out)
  457. text = []
  458. self._writeItem(item, text)
  459. out = self._rasterize(
  460. text, item.GetPropertyVal('widthValue'),
  461. self._mapType, tempRaster)
  462. rastersToPatch.append(out)
  463. text = []
  464. else:
  465. self._writeItem(item, text)
  466. lastCellValue = item.GetPropertyVal('cellValue')
  467. lastWidthValue = item.GetPropertyVal('widthValue')
  468. i += 1
  469. evt = updateProgress(
  470. range=len(self._all),
  471. value=i, text=_("Rasterizing..."))
  472. wx.PostEvent(self, evt)
  473. if text:
  474. out = self._rasterize(text, item.GetPropertyVal('widthValue'),
  475. self._mapType, tempRaster)
  476. rastersToPatch.append(out)
  477. gcore.run_command(
  478. 'r.patch', input=rastersToPatch[:: -1] +
  479. [self._backupRasterName],
  480. output=self._editedRaster, overwrite=True, quiet=True,
  481. env=self._env)
  482. gcore.run_command(
  483. 'g.remove',
  484. type='raster',
  485. flags='f',
  486. name=rastersToPatch +
  487. [tempRaster],
  488. quiet=True)
  489. try:
  490. # setting the right color table
  491. if self._editOldRaster:
  492. return
  493. if not self._backgroundRaster:
  494. table = UserSettings.Get(
  495. group='rasterLayer',
  496. key='colorTable',
  497. subkey='selection')
  498. if not table:
  499. table = 'rainbow'
  500. gcore.run_command(
  501. 'r.colors',
  502. color=table,
  503. map=self._editedRaster,
  504. quiet=True)
  505. else:
  506. gcore.run_command('r.colors', map=self._editedRaster,
  507. raster=self._backgroundRaster, quiet=True)
  508. except CalledModuleError:
  509. self._running = False
  510. GError(parent=self._mapWindow, message=_(
  511. "Failed to set default color table for edited raster map"))
  512. def _writeFeature(self, item, vtype, text):
  513. """Writes digitized features in r.in.poly format."""
  514. coords = item.GetCoords()
  515. if vtype == 'P':
  516. coords = [coords]
  517. cellValue = item.GetPropertyVal('cellValue')
  518. record = '{vtype}\n'.format(vtype=vtype)
  519. for coord in coords:
  520. record += ' '.join([str(c) for c in coord])
  521. record += '\n'
  522. record += '= {cellValue}\n'.format(cellValue=cellValue)
  523. text.append(record)
  524. def _writeItem(self, item, text):
  525. if item in self._areas.GetAllItems():
  526. self._writeFeature(item, vtype='A', text=text)
  527. elif item in self._lines.GetAllItems():
  528. self._writeFeature(item, vtype='L', text=text)
  529. elif item in self._points.GetAllItems():
  530. self._writeFeature(item, vtype='P', text=text)
  531. def _rasterize(self, text, bufferDist, mapType, tempRaster):
  532. """Performs the actual rasterization using r.in.poly
  533. and buffering with r.grow if required.
  534. :param str text: string in r.in.poly format
  535. :param float bufferDist: buffer distance in map units
  536. :param str mapType: CELL, FCELL, DCELL
  537. :param str tempRaster: name of temporary raster used in computation
  538. :return: output raster map name as a result of digitization
  539. """
  540. output = 'x' + str(uuid.uuid4())[:8]
  541. asciiFile = tempfile.NamedTemporaryFile(mode='w', delete=False)
  542. asciiFile.write('\n'.join(text))
  543. asciiFile.close()
  544. if bufferDist:
  545. bufferDist /= 2.
  546. gcore.run_command(
  547. 'r.in.poly',
  548. input=asciiFile.name,
  549. output=tempRaster,
  550. type_=mapType,
  551. overwrite=True,
  552. quiet=True)
  553. gcore.run_command('r.grow', input=tempRaster, output=output,
  554. flags='m', radius=bufferDist, quiet=True,
  555. env=self._env)
  556. else:
  557. gcore.run_command('r.in.poly', input=asciiFile.name, output=output,
  558. type_=mapType, quiet=True, env=self._env)
  559. os.unlink(asciiFile.name)
  560. return output
  561. def _setRegion(self):
  562. """Set region according input raster map"""
  563. self._env['GRASS_REGION'] = gcore.region_env(
  564. raster=self._backupRasterName)