analysis.py 13 KB

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