analysis.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. """
  2. @package mapwin.analysis
  3. @brief Map display controllers for analyses (profiling, measuring)
  4. Classes:
  5. - analysis::AnalysisControllerBase
  6. - analysis::ProfileController
  7. - analysis::MeasureDistanceController
  8. (C) 2013 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Anna Petrasova <kratochanna gmail.com>
  12. """
  13. import os
  14. import math
  15. import wx
  16. import core.units as units
  17. from core.gcmd import RunCommand
  18. from core.giface import Notification
  19. from grass.pydispatch.signal import Signal
  20. from grass.script.utils import parse_key_val
  21. class AnalysisControllerBase:
  22. """Base class for analysis which require drawing line in map display."""
  23. def __init__(self, giface, mapWindow):
  24. """
  25. :param giface: grass interface
  26. :param mapWindow: instance of BufferedMapWindow
  27. """
  28. self._giface = giface
  29. self._mapWindow = mapWindow
  30. self._registeredGraphics = None
  31. self._graphicsType = None
  32. self._oldMouseUse = None
  33. self._oldCursor = None
  34. def IsActive(self):
  35. """Returns True if analysis mode is activated."""
  36. return bool(self._registeredGraphics)
  37. def _start(self, x, y):
  38. """Handles the actual start of drawing line
  39. and adding each new point.
  40. :param x,y: east north coordinates
  41. """
  42. if not self._registeredGraphics.GetAllItems():
  43. item = self._registeredGraphics.AddItem(coords=[[x, y]])
  44. item.SetPropertyVal("penName", "analysisPen")
  45. else:
  46. # needed to switch mouse begin and end to draw intermediate line
  47. # properly
  48. coords = self._registeredGraphics.GetItem(0).GetCoords()[-1]
  49. self._mapWindow.mouse["begin"] = self._mapWindow.Cell2Pixel(coords)
  50. def _addPoint(self, x, y):
  51. """New point added.
  52. :param x,y: east north coordinates
  53. """
  54. # add new point and calculate distance
  55. item = self._registeredGraphics.GetItem(0)
  56. coords = item.GetCoords() + [[x, y]]
  57. item.SetCoords(coords)
  58. # draw
  59. self._mapWindow.ClearLines()
  60. self._registeredGraphics.Draw()
  61. self._mapWindow.Refresh()
  62. wx.SafeYield()
  63. self._doAnalysis(coords)
  64. def _doAnalysis(self, coords):
  65. """Perform the required analysis
  66. (compute distnace, update profile)
  67. :param coords: EN coordinates
  68. """
  69. raise NotImplementedError()
  70. def _disconnectAll(self):
  71. """Disconnect all mouse signals
  72. to stop drawing."""
  73. raise NotImplementedError()
  74. def _connectAll(self):
  75. """Connect all mouse signals to draw."""
  76. raise NotImplementedError()
  77. def _getPen(self):
  78. """Returns wx.Pen instance."""
  79. raise NotImplementedError()
  80. def Stop(self, restore=True):
  81. """Analysis mode is stopped.
  82. :param restore: if restore previous cursor, mouse['use']
  83. """
  84. self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
  85. self._mapWindow.mouse["end"] = self._mapWindow.mouse["begin"]
  86. # disconnect mouse events
  87. self._disconnectAll()
  88. # unregister
  89. self._mapWindow.UnregisterGraphicsToDraw(self._registeredGraphics)
  90. self._registeredGraphics = None
  91. self._mapWindow.UpdateMap(render=False)
  92. if restore:
  93. # restore mouse['use'] and cursor to the state before measuring
  94. # starts
  95. self._mapWindow.SetNamedCursor(self._oldCursor)
  96. self._mapWindow.mouse["use"] = self._oldMouseUse
  97. def Start(self):
  98. """Init analysis: register graphics to map window,
  99. connect required mouse signals.
  100. """
  101. self._oldMouseUse = self._mapWindow.mouse["use"]
  102. self._oldCursor = self._mapWindow.GetNamedCursor()
  103. self._registeredGraphics = self._mapWindow.RegisterGraphicsToDraw(
  104. graphicsType=self._graphicsType, mapCoords=True
  105. )
  106. self._connectAll()
  107. # change mouse['box'] and pen to draw line during dragging
  108. # TODO: better solution for drawing this line
  109. self._mapWindow.mouse["use"] = None
  110. self._mapWindow.mouse["box"] = "line"
  111. self._mapWindow.pen = wx.Pen(colour="red", width=2, style=wx.SHORT_DASH)
  112. self._registeredGraphics.AddPen("analysisPen", self._getPen())
  113. # change the cursor
  114. self._mapWindow.SetNamedCursor("pencil")
  115. class ProfileController(AnalysisControllerBase):
  116. """Class controls profiling in map display.
  117. It should be used inside ProfileFrame
  118. """
  119. def __init__(self, giface, mapWindow):
  120. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  121. self.transectChanged = Signal("ProfileController.transectChanged")
  122. self._graphicsType = "line"
  123. def _doAnalysis(self, coords):
  124. """Informs profile dialog that profile changed.
  125. :param coords: EN coordinates
  126. """
  127. self.transectChanged.emit(coords=coords)
  128. def _disconnectAll(self):
  129. self._mapWindow.mouseLeftDown.disconnect(self._start)
  130. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  131. def _connectAll(self):
  132. self._mapWindow.mouseLeftDown.connect(self._start)
  133. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  134. def _getPen(self):
  135. return wx.Pen(colour=wx.Colour(0, 100, 0), width=2, style=wx.SHORT_DASH)
  136. def Stop(self, restore=True):
  137. AnalysisControllerBase.Stop(self, restore=restore)
  138. self.transectChanged.emit(coords=[])
  139. class MeasureDistanceController(AnalysisControllerBase):
  140. """Class controls measuring distance in map display."""
  141. def __init__(self, giface, mapWindow):
  142. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  143. self._projInfo = self._mapWindow.Map.projinfo
  144. self._totaldist = 0.0 # total measured distance
  145. self._useCtypes = False
  146. self._graphicsType = "line"
  147. def _doAnalysis(self, coords):
  148. """New point added.
  149. :param x,y: east north coordinates
  150. """
  151. self.MeasureDist(coords[-2], coords[-1])
  152. def _disconnectAll(self):
  153. self._mapWindow.mouseLeftDown.disconnect(self._start)
  154. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  155. self._mapWindow.mouseDClick.disconnect(self.Stop)
  156. def _connectAll(self):
  157. self._mapWindow.mouseLeftDown.connect(self._start)
  158. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  159. self._mapWindow.mouseDClick.connect(self.Stop)
  160. def _getPen(self):
  161. return wx.Pen(colour="green", width=2, style=wx.SHORT_DASH)
  162. def Stop(self, restore=True):
  163. if not self.IsActive():
  164. return
  165. AnalysisControllerBase.Stop(self, restore=restore)
  166. self._giface.WriteCmdLog(_("Measuring finished"))
  167. def Start(self):
  168. """Init measurement routine that calculates map distance
  169. along transect drawn on map display
  170. """
  171. if self.IsActive():
  172. return
  173. AnalysisControllerBase.Start(self)
  174. self._totaldist = 0.0 # total measured distance
  175. # initiating output (and write a message)
  176. # e.g., in Layer Manager switch to output console
  177. # TODO: this should be something like: write important message or write tip
  178. # TODO: mixed 'switching' and message? no, measuring handles 'swithing'
  179. # on its own
  180. self._giface.WriteWarning(
  181. _(
  182. "Click and drag with left mouse button "
  183. "to measure.%s"
  184. "Double click with left button to clear."
  185. )
  186. % (os.linesep)
  187. )
  188. if self._projInfo["proj"] != "xy":
  189. mapunits = self._projInfo["units"]
  190. self._giface.WriteCmdLog(_("Measuring distance") + " (" + mapunits + "):")
  191. else:
  192. self._giface.WriteCmdLog(_("Measuring distance:"))
  193. if self._projInfo["proj"] == "ll":
  194. try:
  195. import grass.lib.gis as gislib
  196. gislib.G_begin_distance_calculations()
  197. self._useCtypes = True
  198. except ImportError as e:
  199. self._giface.WriteWarning(
  200. _(
  201. "Geodesic distance calculation "
  202. "is not available.\n"
  203. "Reason: %s" % e
  204. )
  205. )
  206. def MeasureDist(self, beginpt, endpt):
  207. """Calculate distance and print to output window.
  208. :param beginpt,endpt: EN coordinates
  209. """
  210. # move also Distance method?
  211. dist, (north, east) = self._mapWindow.Distance(beginpt, endpt, screen=False)
  212. dist = round(dist, 3)
  213. mapunits = self._projInfo["units"]
  214. if mapunits == "degrees" and self._useCtypes:
  215. mapunits = "meters"
  216. d, dunits = units.formatDist(dist, mapunits)
  217. self._totaldist += dist
  218. td, tdunits = units.formatDist(self._totaldist, mapunits)
  219. if dunits == "units" and mapunits:
  220. dunits = tdunits = mapunits
  221. strdist = str(d)
  222. strtotdist = str(td)
  223. if self._projInfo["proj"] == "xy" or "degree" not in self._projInfo["unit"]:
  224. angle = int(math.degrees(math.atan2(north, east)) + 0.5)
  225. # uncomment below (or flip order of atan2(y,x) above) to use
  226. # the mathematical theta convention (CCW from +x axis)
  227. # angle = 90 - angle
  228. if angle < 0:
  229. angle = 360 + angle
  230. mstring = "%s = %s %s\n%s = %s %s\n%s = %d %s\n%s" % (
  231. _("segment"),
  232. strdist,
  233. dunits,
  234. _("total distance"),
  235. strtotdist,
  236. tdunits,
  237. _("bearing"),
  238. angle,
  239. _("degrees (clockwise from grid-north)"),
  240. "-" * 60,
  241. )
  242. else:
  243. mstring = "%s = %s %s\n%s = %s %s\n%s" % (
  244. _("segment"),
  245. strdist,
  246. dunits,
  247. _("total distance"),
  248. strtotdist,
  249. tdunits,
  250. "-" * 60,
  251. )
  252. self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE)
  253. return dist
  254. class MeasureAreaController(AnalysisControllerBase):
  255. """Class controls measuring area in map display."""
  256. def __init__(self, giface, mapWindow):
  257. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  258. self._graphicsType = "polygon"
  259. def _doAnalysis(self, coords):
  260. """New point added.
  261. :param coords: east north coordinates as a list
  262. """
  263. self.MeasureArea(coords)
  264. def _disconnectAll(self):
  265. self._mapWindow.mouseLeftDown.disconnect(self._start)
  266. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  267. self._mapWindow.mouseDClick.disconnect(self.Stop)
  268. def _connectAll(self):
  269. self._mapWindow.mouseLeftDown.connect(self._start)
  270. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  271. self._mapWindow.mouseDClick.connect(self.Stop)
  272. def _getPen(self):
  273. return wx.Pen(colour="green", width=2, style=wx.SOLID)
  274. def Stop(self, restore=True):
  275. if not self.IsActive():
  276. return
  277. AnalysisControllerBase.Stop(self, restore=restore)
  278. self._giface.WriteCmdLog(_("Measuring finished"))
  279. def Start(self):
  280. """Init measurement routine that calculates area of polygon
  281. drawn on map display.
  282. """
  283. if self.IsActive():
  284. return
  285. AnalysisControllerBase.Start(self)
  286. self._giface.WriteWarning(
  287. _(
  288. "Click and drag with left mouse button "
  289. "to measure.%s"
  290. "Double click with left button to clear."
  291. )
  292. % (os.linesep)
  293. )
  294. self._giface.WriteCmdLog(_("Measuring area:"))
  295. def MeasureArea(self, coords):
  296. """Calculate area and print to output window.
  297. :param coords: list of E, N coordinates
  298. """
  299. # TODO: make sure appending first point is needed for m.measure
  300. coordinates = coords + [coords[0]]
  301. coordinates = ",".join(
  302. [str(item) for sublist in coordinates for item in sublist]
  303. )
  304. result = RunCommand(
  305. "m.measure", flags="g", coordinates=coordinates, read=True
  306. ).strip()
  307. result = parse_key_val(result)
  308. if "units" not in result:
  309. self._giface.WriteWarning(_("Units not recognized, measurement failed."))
  310. unit = ""
  311. else:
  312. unit = result["units"].split(",")[1]
  313. if "area" not in result:
  314. text = _("Area: {area} {unit}\n").format(area=0, unit=unit)
  315. else:
  316. text = _("Area: {area} {unit}\n").format(area=result["area"], unit=unit)
  317. self._giface.WriteLog(text, notification=Notification.MAKE_VISIBLE)