123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- """
- @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 <kratochanna gmail.com>
- """
- import os
- import math
- import wx
- 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.SafeYield()
- 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)
|