123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- """
- @package rdigit.controller
- @brief rdigit controller for drawing and rasterizing
- Classes:
- - controller::RDigitController
- (C) 2014 by 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 Anna Petrasova <kratochanna gmail.com>
- """
- import os
- import tempfile
- import wx
- import uuid
- from wx.lib.newevent import NewEvent
- from grass.script import core as gcore
- from grass.script import raster as grast
- from grass.exceptions import CalledModuleError, ScriptError
- from grass.pydispatch.signal import Signal
- from core.gcmd import GError, GMessage
- from core.settings import UserSettings
- from core.gthread import gThread
- from rdigit.dialogs import NewRasterDialog
- updateProgress, EVT_UPDATE_PROGRESS = NewEvent()
- class RDigitController(wx.EvtHandler):
- """Controller object for raster digitizer.
- Inherits from EvtHandler to be able to send wx events from thraed.
- """
- def __init__(self, giface, mapWindow):
- """Constructs controller
- :param giface: grass interface object
- :param mapWindow: instance of BufferedMapWindow
- """
- wx.EvtHandler.__init__(self)
- self._giface = giface
- self._mapWindow = mapWindow
- # thread for running rasterization process
- self._thread = gThread()
- # name of raster map which is edited (also new one)
- self._editedRaster = None
- # name of optional background raster
- self._backgroundRaster = None
- # name of temporary raster used to backup original state
- self._backupRasterName = None
- # if we edit an old raster or a new one (important for setting color
- # table)
- self._editOldRaster = False
- # type of output raster map (CELL, FCELL, DCELL)
- self._mapType = None
- # GraphicsSet for drawing areas, lines, points
- self._areas = None
- self._lines = None
- self._points = None
- # list of all GraphicsItems in the order of drawing
- self._all = []
- # if in state of drawing lin or area
- self._drawing = False
- # if running digitizing process in thread (to block drawing)
- self._running = False
- # color used to draw (should be moved to settings)
- self._drawColor = wx.GREEN
- # transparency used to draw (should be moved to settings)
- self._drawTransparency = 100
- # current selected drawing method
- self._graphicsType = 'area'
- # last edited cell value
- self._currentCellValue = None
- # last edited buffer value
- self._currentWidthValue = None
- # digit env
- self._env = os.environ.copy()
- self._oldMouseUse = None
- self._oldCursor = None
- # signal to add new raster to toolbar items
- self.newRasterCreated = Signal('RDigitController:newRasterCreated')
- # signal to add just used cell value in toolbar combo
- self.newFeatureCreated = Signal('RDigitController:newFeatureCreated')
- # signal to upload unique categories of background map into toolbar
- # combo
- self.uploadMapCategories = Signal(
- 'RDigitController:uploadMapCategories')
- self.quitDigitizer = Signal('RDigitController:quitDigitizer')
- self.showNotification = Signal('RDigitController:showNotification')
- def _connectAll(self):
- self._mapWindow.mouseLeftDown.connect(self._start)
- self._mapWindow.mouseLeftUp.connect(self._addPoint)
- self._mapWindow.mouseRightUp.connect(self._finish)
- self._mapWindow.Unbind(wx.EVT_CONTEXT_MENU)
- def _disconnectAll(self):
- self._mapWindow.mouseLeftDown.disconnect(self._start)
- self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
- self._mapWindow.mouseRightUp.disconnect(self._finish)
- self._mapWindow.Bind(
- wx.EVT_CONTEXT_MENU,
- self._mapWindow.OnContextMenu)
- def _start(self, x, y):
- """Start digitizing a new object.
- :param x: x coordinate in map units
- :param y: y coordinate in map units
- """
- if self._running:
- return
- if not self._editedRaster:
- GMessage(parent=self._mapWindow, message=_(
- "Please select first the raster map"))
- return
- if not self._drawing:
- if self._graphicsType == 'area':
- item = self._areas.AddItem(coords=[])
- item.SetPropertyVal('penName', 'pen1')
- self._all.append(item)
- elif self._graphicsType == 'line':
- item = self._lines.AddItem(coords=[])
- item.SetPropertyVal('penName', 'pen1')
- self._all.append(item)
- elif self._graphicsType == 'point':
- item = self._points.AddItem(coords=[])
- item.SetPropertyVal('penName', 'pen1')
- self._all.append(item)
- self._drawing = True
- def _addPoint(self, x, y):
- """Add point to an object.
- :param x: x coordinate in map units
- :param y: y coordinate in map units
- """
- if self._running:
- return
- if not self._drawing:
- return
- if self._graphicsType == 'area':
- area = self._areas.GetItem(-1)
- coords = area.GetCoords() + [[x, y]]
- area.SetCoords(coords)
- self.showNotification.emit(text=_("Right click to finish area"))
- elif self._graphicsType == 'line':
- line = self._lines.GetItem(-1)
- coords = line.GetCoords() + [[x, y]]
- line.SetCoords(coords)
- self.showNotification.emit(text=_("Right click to finish line"))
- elif self._graphicsType == 'point':
- point = self._points.GetItem(-1)
- point.SetCoords([x, y])
- self._finish()
- # draw
- self._mapWindow.ClearLines()
- self._lines.Draw()
- self._areas.Draw()
- self._points.Draw()
- self._mapWindow.Refresh()
- def _finish(self):
- """Finish digitizing a new object and redraws.
- Saves current cell value and buffer width for that object.
- :param x: x coordinate in map units
- :param y: y coordinate in map units
- """
- if self._running:
- return
- if self._graphicsType == 'point':
- item = self._points.GetItem(-1)
- elif self._graphicsType == 'area':
- item = self._areas.GetItem(-1)
- elif self._graphicsType == 'line':
- item = self._lines.GetItem(-1)
- else:
- return
- self._drawing = False
- item.SetPropertyVal('brushName', 'done')
- item.AddProperty('cellValue')
- item.AddProperty('widthValue')
- item.SetPropertyVal('cellValue', self._currentCellValue)
- item.SetPropertyVal('widthValue', self._currentWidthValue)
- self.newFeatureCreated.emit()
- self._mapWindow.ClearLines()
- self._points.Draw()
- self._areas.Draw()
- self._lines.Draw()
- self._mapWindow.Refresh()
- def SelectType(self, drawingType):
- """Selects method (area/line/point) for drawing.
- Connects and disconnects signal to allow other tools
- in map toolbar to work.
- """
- if self._graphicsType and drawingType and self._graphicsType != drawingType \
- and self._drawing:
- # if we select different drawing tool, finish the feature
- self._finish()
- if self._graphicsType and not drawingType:
- self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
- self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin']
- # disconnect mouse events
- self._disconnectAll()
- self._mapWindow.SetNamedCursor(self._oldCursor)
- self._mapWindow.mouse['use'] = self._oldMouseUse
- elif self._graphicsType is None and drawingType:
- self._connectAll()
- # change mouse['box'] and pen to draw line during dragging
- # TODO: better solution for drawing this line
- self._mapWindow.mouse['use'] = None
- self._mapWindow.mouse['box'] = "line"
- self._mapWindow.pen = wx.Pen(
- colour='red', width=2, style=wx.SHORT_DASH)
- # change the cursor
- self._mapWindow.SetNamedCursor('pencil')
- self._graphicsType = drawingType
- def SetCellValue(self, value):
- self._currentCellValue = value
- def SetWidthValue(self, value):
- self._currentWidthValue = value
- def ChangeDrawColor(self, color):
- self._drawColor = color[:3] + (self._drawTransparency,)
- for each in (self._areas, self._lines, self._points):
- each.GetPen('pen1').SetColour(self._drawColor)
- each.GetBrush('done').SetColour(self._drawColor)
- self._mapWindow.UpdateMap(render=False)
- def Start(self):
- """Registers graphics to map window,
- connect required mouse signals.
- """
- self._oldMouseUse = self._mapWindow.mouse['use']
- self._oldCursor = self._mapWindow.GetNamedCursor()
- self._connectAll()
- # change mouse['box'] and pen to draw line during dragging
- # TODO: better solution for drawing this line
- self._mapWindow.mouse['use'] = None
- self._mapWindow.mouse['box'] = "line"
- self._mapWindow.pen = wx.Pen(
- colour='red', width=2, style=wx.SHORT_DASH)
- color = self._drawColor[:3] + (self._drawTransparency,)
- self._areas = self._mapWindow.RegisterGraphicsToDraw(
- graphicsType='polygon', pdc=self._mapWindow.pdcTransparent, mapCoords=True)
- self._areas.AddPen(
- 'pen1',
- wx.Pen(
- colour=color,
- width=2,
- style=wx.SOLID))
- self._areas.AddBrush('done', wx.Brush(colour=color, style=wx.SOLID))
- self._lines = self._mapWindow.RegisterGraphicsToDraw(
- graphicsType='line', pdc=self._mapWindow.pdcTransparent, mapCoords=True)
- self._lines.AddPen(
- 'pen1',
- wx.Pen(
- colour=color,
- width=2,
- style=wx.SOLID))
- self._lines.AddBrush('done', wx.Brush(colour=color, style=wx.SOLID))
- self._points = self._mapWindow.RegisterGraphicsToDraw(
- graphicsType='point', pdc=self._mapWindow.pdcTransparent, mapCoords=True)
- self._points.AddPen(
- 'pen1',
- wx.Pen(
- colour=color,
- width=2,
- style=wx.SOLID))
- self._points.AddBrush('done', wx.Brush(colour=color, style=wx.SOLID))
- # change the cursor
- self._mapWindow.SetNamedCursor('pencil')
- def Stop(self):
- """Before stopping digitizer, asks to save edits"""
- if self._editedRaster:
- dlg = wx.MessageDialog(
- self._mapWindow,
- _("Do you want to save changes?"),
- _("Save raster map changes"),
- wx.YES_NO)
- if dlg.ShowModal() == wx.ID_YES:
- if self._drawing:
- self._finish()
- self._thread.Run(callable=self._exportRaster,
- ondone=lambda event: self._updateAndQuit())
- else:
- self.quitDigitizer.emit()
- else:
- self.quitDigitizer.emit()
- def Save(self):
- """Saves current edits to a raster map"""
- if self._drawing:
- self._finish()
- self._thread.Run(callable=self._exportRaster,
- ondone=lambda event: self._update())
- def Undo(self):
- """Undo a change, goes object back (finished or not finished)"""
- if len(self._all):
- removed = self._all.pop(-1)
- # try to remove from each, it fails quietly when theitem is not
- # there
- self._areas.DeleteItem(removed)
- self._lines.DeleteItem(removed)
- self._points.DeleteItem(removed)
- self._drawing = False
- self._mapWindow.UpdateMap(render=False)
- def CleanUp(self, restore=True):
- """Cleans up drawing, temporary maps.
- :param restore: if restore previous cursor, mouse['use']
- """
- try:
- if self._backupRasterName:
- gcore.run_command(
- 'g.remove',
- type='raster',
- flags='f',
- name=self._backupRasterName,
- quiet=True)
- except CalledModuleError:
- pass
- self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
- self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin']
- # disconnect mouse events
- if self._graphicsType:
- self._disconnectAll()
- # unregister
- self._mapWindow.UnregisterGraphicsToDraw(self._areas)
- self._mapWindow.UnregisterGraphicsToDraw(self._lines)
- self._mapWindow.UnregisterGraphicsToDraw(self._points)
- #self._registeredGraphics = None
- self._mapWindow.UpdateMap(render=False)
- if restore:
- # restore mouse['use'] and cursor to the state before measuring
- # starts
- self._mapWindow.SetNamedCursor(self._oldCursor)
- self._mapWindow.mouse['use'] = self._oldMouseUse
- def _updateAndQuit(self):
- """Called when thread is done. Updates map and calls to quits digitizer."""
- self._running = False
- self._mapWindow.UpdateMap(render=True)
- self.quitDigitizer.emit()
- def _update(self):
- """Called when thread is done. Updates map."""
- self._running = False
- self._mapWindow.UpdateMap(render=True)
- def SelectOldMap(self, name):
- """After selecting old raster, creates a backup copy for editing."""
- try:
- self._backupRaster(name)
- except ScriptError:
- GError(parent=self._mapWindow, message=_(
- "Failed to create backup copy of edited raster map."))
- return False
- self._editedRaster = name
- self._mapType = grast.raster_info(map=name)['datatype']
- self._editOldRaster = True
- return True
- def SelectNewMap(
- self, standalone=False, mapName=None, bgMap=None,
- mapType=None,
- ):
- """After selecting new raster, shows dialog to choose name,
- background map and type of the new map.
- :params standalone, mapName, bgMap, mapType: if digitizer is
- launched as standalone module
- :param bool standalone: if digitizer is launched as standalone
- module
- :param str mapName: edited raster map name
- :param str bgMap: background raster map name
- :param str mapType: raster map type CELL, FCELL, DCELL
- """
- if standalone:
- try:
- self._createNewMap(
- mapName=mapName, backgroundMap=bgMap,
- mapType=mapType,
- )
- except ScriptError:
- GError(
- parent=self._mapWindow, message=_(
- "Failed to create new raster map."
- ),
- )
- return False
- return True
- else:
- dlg = NewRasterDialog(parent=self._mapWindow)
- dlg.CenterOnParent()
- if dlg.ShowModal() == wx.ID_OK:
- try:
- self._createNewMap(
- mapName=dlg.GetMapName(),
- backgroundMap=dlg.GetBackgroundMapName(),
- mapType=dlg.GetMapType(),
- )
- except ScriptError:
- GError(
- parent=self._mapWindow, message=_(
- "Failed to create new raster map."
- ),
- )
- return False
- finally:
- dlg.Destroy()
- return True
- else:
- dlg.Destroy()
- return False
- def _createNewMap(self, mapName, backgroundMap, mapType):
- """Creates a new raster map based on specified background and type."""
- name = mapName.split('@')[0]
- background = backgroundMap.split('@')[0]
- types = {'CELL': 'int', 'FCELL': 'float', 'DCELL': 'double'}
- if background:
- back = background
- else:
- back = 'null()'
- try:
- grast.mapcalc(
- exp="{name} = {mtype}({back})".format(
- name=name,
- mtype=types[mapType],
- back=back),
- overwrite=True,
- quiet=True)
- if background:
- self._backgroundRaster = backgroundMap
- gcore.run_command(
- 'r.colors',
- map=name,
- raster=self._backgroundRaster,
- quiet=True)
- if mapType == 'CELL':
- values = gcore.read_command('r.describe', flags='1n',
- map=name, quiet=True).strip()
- if values:
- self.uploadMapCategories.emit(
- values=values.split('\n'))
- except CalledModuleError:
- raise ScriptError
- self._backupRaster(name)
- name = name + '@' + gcore.gisenv()['MAPSET']
- self._editedRaster = name
- self._mapType = mapType
- self.newRasterCreated.emit(name=name)
- gisenv = gcore.gisenv()
- self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
- location=gisenv['LOCATION_NAME'],
- mapset=gisenv['MAPSET'],
- action='new',
- map=name.split('@')[0],
- element='raster')
- def _backupRaster(self, name):
- """Creates a temporary backup raster necessary for undo behavior.
- :param str name: name of raster map for which we create backup
- """
- name = name.split('@')[0]
- backup = name + '_backupcopy_' + str(os.getpid())
- try:
- gcore.run_command('g.copy', raster=[name, backup], quiet=True)
- except CalledModuleError:
- raise ScriptError
- self._backupRasterName = backup
- def _exportRaster(self):
- """Rasterizes digitized features.
- Uses r.in.poly and r.grow for buffering features. Creates separate raster
- maps depending on common cell values and buffering width necessary to
- keep the order of editing. These rasters are then patched together.
- Sets default color table for the newly digitized raster.
- """
- self._setRegion()
- if not self._editedRaster or self._running:
- return
- self._running = True
- if len(self._all) < 1:
- new = self._editedRaster
- if '@' in self._editedRaster:
- new = self._editedRaster.split('@')[0]
- gcore.run_command('g.copy', raster=[self._backupRasterName, new],
- overwrite=True, quiet=True)
- else:
- tempRaster = 'tmp_rdigit_rast_' + str(os.getpid())
- text = []
- rastersToPatch = []
- i = 0
- lastCellValue = lastWidthValue = None
- evt = updateProgress(
- range=len(self._all),
- value=0, text=_("Rasterizing..."))
- wx.PostEvent(self, evt)
- lastCellValue = self._all[0].GetPropertyVal('cellValue')
- lastWidthValue = self._all[0].GetPropertyVal('widthValue')
- for item in self._all:
- if item.GetPropertyVal('widthValue') and \
- (lastCellValue != item.GetPropertyVal('cellValue') or
- lastWidthValue != item.GetPropertyVal('widthValue')):
- if text:
- out = self._rasterize(
- text, lastWidthValue, self._mapType, tempRaster)
- rastersToPatch.append(out)
- text = []
- self._writeItem(item, text)
- out = self._rasterize(
- text, item.GetPropertyVal('widthValue'),
- self._mapType, tempRaster)
- rastersToPatch.append(out)
- text = []
- else:
- self._writeItem(item, text)
- lastCellValue = item.GetPropertyVal('cellValue')
- lastWidthValue = item.GetPropertyVal('widthValue')
- i += 1
- evt = updateProgress(
- range=len(self._all),
- value=i, text=_("Rasterizing..."))
- wx.PostEvent(self, evt)
- if text:
- out = self._rasterize(text, item.GetPropertyVal('widthValue'),
- self._mapType, tempRaster)
- rastersToPatch.append(out)
- gcore.run_command(
- 'r.patch', input=rastersToPatch[:: -1] +
- [self._backupRasterName],
- output=self._editedRaster, overwrite=True, quiet=True,
- env=self._env)
- gcore.run_command(
- 'g.remove',
- type='raster',
- flags='f',
- name=rastersToPatch +
- [tempRaster],
- quiet=True)
- try:
- # setting the right color table
- if self._editOldRaster:
- return
- if not self._backgroundRaster:
- table = UserSettings.Get(
- group='rasterLayer',
- key='colorTable',
- subkey='selection')
- if not table:
- table = 'rainbow'
- gcore.run_command(
- 'r.colors',
- color=table,
- map=self._editedRaster,
- quiet=True)
- else:
- gcore.run_command('r.colors', map=self._editedRaster,
- raster=self._backgroundRaster, quiet=True)
- except CalledModuleError:
- self._running = False
- GError(parent=self._mapWindow, message=_(
- "Failed to set default color table for edited raster map"))
- def _writeFeature(self, item, vtype, text):
- """Writes digitized features in r.in.poly format."""
- coords = item.GetCoords()
- if vtype == 'P':
- coords = [coords]
- cellValue = item.GetPropertyVal('cellValue')
- record = '{vtype}\n'.format(vtype=vtype)
- for coord in coords:
- record += ' '.join([str(c) for c in coord])
- record += '\n'
- record += '= {cellValue}\n'.format(cellValue=cellValue)
- text.append(record)
- def _writeItem(self, item, text):
- if item in self._areas.GetAllItems():
- self._writeFeature(item, vtype='A', text=text)
- elif item in self._lines.GetAllItems():
- self._writeFeature(item, vtype='L', text=text)
- elif item in self._points.GetAllItems():
- self._writeFeature(item, vtype='P', text=text)
- def _rasterize(self, text, bufferDist, mapType, tempRaster):
- """Performs the actual rasterization using r.in.poly
- and buffering with r.grow if required.
- :param str text: string in r.in.poly format
- :param float bufferDist: buffer distance in map units
- :param str mapType: CELL, FCELL, DCELL
- :param str tempRaster: name of temporary raster used in computation
- :return: output raster map name as a result of digitization
- """
- output = 'x' + str(uuid.uuid4())[:8]
- asciiFile = tempfile.NamedTemporaryFile(mode='w', delete=False)
- asciiFile.write('\n'.join(text))
- asciiFile.close()
- if bufferDist:
- bufferDist /= 2.
- gcore.run_command(
- 'r.in.poly',
- input=asciiFile.name,
- output=tempRaster,
- type_=mapType,
- overwrite=True,
- quiet=True)
- gcore.run_command('r.grow', input=tempRaster, output=output,
- flags='m', radius=bufferDist, quiet=True,
- env=self._env)
- else:
- gcore.run_command('r.in.poly', input=asciiFile.name, output=output,
- type_=mapType, quiet=True, env=self._env)
- os.unlink(asciiFile.name)
- return output
- def _setRegion(self):
- """Set region according input raster map"""
- self._env['GRASS_REGION'] = gcore.region_env(
- raster=self._backupRasterName)
|