analysis.py 13 KB

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