# -*- coding: utf-8 -*- """ @package mapwin.analysis @brief Map display controllers for analyses (profiling, measuring) Classes: - analysis::AnalysisControllerBase - analysis::ProfileController - analysis::MeasureDistanceController (C) 2013 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 """ import os import math import wx from core.utils import _ import core.units as units from core.gcmd import RunCommand from core.giface import Notification from grass.pydispatch.signal import Signal from grass.script.utils import parse_key_val class AnalysisControllerBase: """Base class for analysis which require drawing line in map display.""" def __init__(self, giface, mapWindow): """ :param giface: grass interface :param mapWindow: instance of BufferedMapWindow """ self._giface = giface self._mapWindow = mapWindow self._registeredGraphics = None self._graphicsType = None self._oldMouseUse = None self._oldCursor = None def IsActive(self): """Returns True if analysis mode is activated.""" return bool(self._registeredGraphics) def _start(self, x, y): """Handles the actual start of drawing line and adding each new point. :param x,y: east north coordinates """ if not self._registeredGraphics.GetAllItems(): item = self._registeredGraphics.AddItem(coords=[[x, y]]) item.SetPropertyVal('penName', 'analysisPen') else: # needed to switch mouse begin and end to draw intermediate line properly coords = self._registeredGraphics.GetItem(0).GetCoords()[-1] self._mapWindow.mouse['begin'] = self._mapWindow.Cell2Pixel(coords) def _addPoint(self, x, y): """New point added. :param x,y: east north coordinates """ # add new point and calculate distance item = self._registeredGraphics.GetItem(0) coords = item.GetCoords() + [[x, y]] item.SetCoords(coords) # draw self._mapWindow.ClearLines() self._registeredGraphics.Draw() self._mapWindow.Refresh() wx.Yield() self._doAnalysis(coords) def _doAnalysis(self, coords): """Perform the required analysis (compute distnace, update profile) :param coords: EN coordinates """ raise NotImplementedError() def _disconnectAll(self): """Disconnect all mouse signals to stop drawing.""" raise NotImplementedError() def _connectAll(self): """Connect all mouse signals to draw.""" raise NotImplementedError() def _getPen(self): """Returns wx.Pen instance.""" raise NotImplementedError() def Stop(self, restore=True): """Analysis mode is stopped. :param restore: if restore previous cursor, mouse['use'] """ self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp) self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin'] # disconnect mouse events self._disconnectAll() # unregister self._mapWindow.UnregisterGraphicsToDraw(self._registeredGraphics) 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 Start(self): """Init analysis: register graphics to map window, connect required mouse signals. """ self._oldMouseUse = self._mapWindow.mouse['use'] self._oldCursor = self._mapWindow.GetNamedCursor() self._registeredGraphics = self._mapWindow.RegisterGraphicsToDraw(graphicsType=self._graphicsType, mapCoords=True) 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) self._registeredGraphics.AddPen('analysisPen', self._getPen()) # change the cursor self._mapWindow.SetNamedCursor('pencil') class ProfileController(AnalysisControllerBase): """Class controls profiling in map display. It should be used inside ProfileFrame """ def __init__(self, giface, mapWindow): AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow) self.transectChanged = Signal('ProfileController.transectChanged') self._graphicsType = 'line' def _doAnalysis(self, coords): """Informs profile dialog that profile changed. :param coords: EN coordinates """ self.transectChanged.emit(coords=coords) def _disconnectAll(self): self._mapWindow.mouseLeftDown.disconnect(self._start) self._mapWindow.mouseLeftUp.disconnect(self._addPoint) def _connectAll(self): self._mapWindow.mouseLeftDown.connect(self._start) self._mapWindow.mouseLeftUp.connect(self._addPoint) def _getPen(self): return wx.Pen(colour=wx.Colour(0, 100, 0), width=2, style=wx.SHORT_DASH) def Stop(self, restore=True): AnalysisControllerBase.Stop(self, restore=restore) self.transectChanged.emit(coords=[]) class MeasureDistanceController(AnalysisControllerBase): """Class controls measuring distance in map display.""" def __init__(self, giface, mapWindow): AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow) self._projInfo = self._mapWindow.Map.projinfo self._totaldist = 0.0 # total measured distance self._useCtypes = False self._graphicsType = 'line' def _doAnalysis(self, coords): """New point added. :param x,y: east north coordinates """ self.MeasureDist(coords[-2], coords[-1]) def _disconnectAll(self): self._mapWindow.mouseLeftDown.disconnect(self._start) self._mapWindow.mouseLeftUp.disconnect(self._addPoint) self._mapWindow.mouseDClick.disconnect(self.Stop) def _connectAll(self): self._mapWindow.mouseLeftDown.connect(self._start) self._mapWindow.mouseLeftUp.connect(self._addPoint) self._mapWindow.mouseDClick.connect(self.Stop) def _getPen(self): return wx.Pen(colour='green', width=2, style=wx.SHORT_DASH) def Stop(self, restore=True): if not self.IsActive(): return AnalysisControllerBase.Stop(self, restore=restore) self._giface.WriteCmdLog(_('Measuring finished')) def Start(self): """Init measurement routine that calculates map distance along transect drawn on map display """ if self.IsActive(): return AnalysisControllerBase.Start(self) self._totaldist = 0.0 # total measured distance # initiating output (and write a message) # e.g., in Layer Manager switch to output console # TODO: this should be something like: write important message or write tip # TODO: mixed 'switching' and message? no, measuring handles 'swithing' on its own self._giface.WriteWarning(_('Click and drag with left mouse button ' 'to measure.%s' 'Double click with left button to clear.') % (os.linesep)) if self._projInfo['proj'] != 'xy': mapunits = self._projInfo['units'] self._giface.WriteCmdLog(_('Measuring distance') + ' (' + mapunits + '):') else: self._giface.WriteCmdLog(_('Measuring distance:')) if self._projInfo['proj'] == 'll': try: import grass.lib.gis as gislib gislib.G_begin_distance_calculations() self._useCtypes = True except ImportError as e: self._giface.WriteWarning(_('Geodesic distance calculation ' 'is not available.\n' 'Reason: %s' % e)) def MeasureDist(self, beginpt, endpt): """Calculate distance and print to output window. :param beginpt,endpt: EN coordinates """ # move also Distance method? dist, (north, east) = self._mapWindow.Distance(beginpt, endpt, screen=False) dist = round(dist, 3) mapunits = self._projInfo['units'] if mapunits == 'degrees' and self._useCtypes: mapunits = 'meters' d, dunits = units.formatDist(dist, mapunits) self._totaldist += dist td, tdunits = units.formatDist(self._totaldist, mapunits) if dunits == 'units' and mapunits: dunits = tdunits = mapunits strdist = str(d) strtotdist = str(td) if self._projInfo['proj'] == 'xy' or 'degree' not in self._projInfo['unit']: angle = int(math.degrees(math.atan2(north, east)) + 0.5) # uncomment below (or flip order of atan2(y,x) above) to use # the mathematical theta convention (CCW from +x axis) #angle = 90 - angle if angle < 0: angle = 360 + angle mstring = '%s = %s %s\n%s = %s %s\n%s = %d %s\n%s' \ % (_('segment'), strdist, dunits, _('total distance'), strtotdist, tdunits, _('bearing'), angle, _('degrees (clockwise from grid-north)'), '-' * 60) else: mstring = '%s = %s %s\n%s = %s %s\n%s' \ % (_('segment'), strdist, dunits, _('total distance'), strtotdist, tdunits, '-' * 60) self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE) return dist class MeasureAreaController(AnalysisControllerBase): """Class controls measuring area in map display.""" def __init__(self, giface, mapWindow): AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow) self._graphicsType = 'polygon' def _doAnalysis(self, coords): """New point added. :param coords: east north coordinates as a list """ self.MeasureArea(coords) def _disconnectAll(self): self._mapWindow.mouseLeftDown.disconnect(self._start) self._mapWindow.mouseLeftUp.disconnect(self._addPoint) self._mapWindow.mouseDClick.disconnect(self.Stop) def _connectAll(self): self._mapWindow.mouseLeftDown.connect(self._start) self._mapWindow.mouseLeftUp.connect(self._addPoint) self._mapWindow.mouseDClick.connect(self.Stop) def _getPen(self): return wx.Pen(colour='green', width=2, style=wx.SOLID) def Stop(self, restore=True): if not self.IsActive(): return AnalysisControllerBase.Stop(self, restore=restore) self._giface.WriteCmdLog(_('Measuring finished')) def Start(self): """Init measurement routine that calculates area of polygon drawn on map display. """ if self.IsActive(): return AnalysisControllerBase.Start(self) self._giface.WriteWarning(_('Click and drag with left mouse button ' 'to measure.%s' 'Double click with left button to clear.') % (os.linesep)) self._giface.WriteCmdLog(_('Measuring area:')) def MeasureArea(self, coords): """Calculate area and print to output window. :param coords: list of E, N coordinates """ # TODO: make sure appending first point is needed for m.measure coordinates = coords + [coords[0]] coordinates = ','.join([str(item) for sublist in coordinates for item in sublist]) result = RunCommand('m.measure', flags='g', coordinates=coordinates, read=True).strip() result = parse_key_val(result) if 'units' not in result: self._giface.WriteWarning(_("Units not recognized, measurement failed.")) unit = '' else: unit = result['units'].split(',')[1] if 'area' not in result: text = _("Area: {area} {unit}\n").format(area=0, unit=unit) else: text = _("Area: {area} {unit}\n").format(area=result['area'], unit=unit) self._giface.WriteLog(text, notification=Notification.MAKE_VISIBLE)