analysis.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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. import core.units as units
  18. from core.gcmd import RunCommand
  19. from core.giface import Notification
  20. from grass.pydispatch.signal import Signal
  21. from grass.script.utils import parse_key_val
  22. class AnalysisControllerBase:
  23. """Base class for analysis which require drawing line in map display."""
  24. def __init__(self, giface, mapWindow):
  25. """
  26. :param giface: grass interface
  27. :param mapWindow: instance of BufferedMapWindow
  28. """
  29. self._giface = giface
  30. self._mapWindow = mapWindow
  31. self._registeredGraphics = None
  32. self._graphicsType = None
  33. self._oldMouseUse = None
  34. self._oldCursor = None
  35. def IsActive(self):
  36. """Returns True if analysis mode is activated."""
  37. return bool(self._registeredGraphics)
  38. def _start(self, x, y):
  39. """Handles the actual start of drawing line
  40. and adding each new point.
  41. :param x,y: east north coordinates
  42. """
  43. if not self._registeredGraphics.GetAllItems():
  44. item = self._registeredGraphics.AddItem(coords=[[x, y]])
  45. item.SetPropertyVal('penName', 'analysisPen')
  46. else:
  47. # needed to switch mouse begin and end to draw intermediate line
  48. # 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.SafeYield()
  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
  95. # starts
  96. self._mapWindow.SetNamedCursor(self._oldCursor)
  97. self._mapWindow.mouse['use'] = self._oldMouseUse
  98. def Start(self):
  99. """Init analysis: register graphics to map window,
  100. connect required mouse signals.
  101. """
  102. self._oldMouseUse = self._mapWindow.mouse['use']
  103. self._oldCursor = self._mapWindow.GetNamedCursor()
  104. self._registeredGraphics = self._mapWindow.RegisterGraphicsToDraw(
  105. graphicsType=self._graphicsType, mapCoords=True)
  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(
  112. colour='red', width=2, style=wx.SHORT_DASH)
  113. self._registeredGraphics.AddPen('analysisPen', self._getPen())
  114. # change the cursor
  115. self._mapWindow.SetNamedCursor('pencil')
  116. class ProfileController(AnalysisControllerBase):
  117. """Class controls profiling in map display.
  118. It should be used inside ProfileFrame
  119. """
  120. def __init__(self, giface, mapWindow):
  121. AnalysisControllerBase.__init__(
  122. self, giface=giface, mapWindow=mapWindow)
  123. self.transectChanged = Signal('ProfileController.transectChanged')
  124. self._graphicsType = 'line'
  125. def _doAnalysis(self, coords):
  126. """Informs profile dialog that profile changed.
  127. :param coords: EN coordinates
  128. """
  129. self.transectChanged.emit(coords=coords)
  130. def _disconnectAll(self):
  131. self._mapWindow.mouseLeftDown.disconnect(self._start)
  132. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  133. def _connectAll(self):
  134. self._mapWindow.mouseLeftDown.connect(self._start)
  135. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  136. def _getPen(self):
  137. return wx.Pen(
  138. colour=wx.Colour(0, 100, 0),
  139. width=2, style=wx.SHORT_DASH)
  140. def Stop(self, restore=True):
  141. AnalysisControllerBase.Stop(self, restore=restore)
  142. self.transectChanged.emit(coords=[])
  143. class MeasureDistanceController(AnalysisControllerBase):
  144. """Class controls measuring distance in map display."""
  145. def __init__(self, giface, mapWindow):
  146. AnalysisControllerBase.__init__(
  147. self, giface=giface, mapWindow=mapWindow)
  148. self._projInfo = self._mapWindow.Map.projinfo
  149. self._totaldist = 0.0 # total measured distance
  150. self._useCtypes = False
  151. self._graphicsType = 'line'
  152. def _doAnalysis(self, coords):
  153. """New point added.
  154. :param x,y: east north coordinates
  155. """
  156. self.MeasureDist(coords[-2], coords[-1])
  157. def _disconnectAll(self):
  158. self._mapWindow.mouseLeftDown.disconnect(self._start)
  159. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  160. self._mapWindow.mouseDClick.disconnect(self.Stop)
  161. def _connectAll(self):
  162. self._mapWindow.mouseLeftDown.connect(self._start)
  163. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  164. self._mapWindow.mouseDClick.connect(self.Stop)
  165. def _getPen(self):
  166. return wx.Pen(colour='green', width=2, style=wx.SHORT_DASH)
  167. def Stop(self, restore=True):
  168. if not self.IsActive():
  169. return
  170. AnalysisControllerBase.Stop(self, restore=restore)
  171. self._giface.WriteCmdLog(_('Measuring finished'))
  172. def Start(self):
  173. """Init measurement routine that calculates map distance
  174. along transect drawn on map display
  175. """
  176. if self.IsActive():
  177. return
  178. AnalysisControllerBase.Start(self)
  179. self._totaldist = 0.0 # total measured distance
  180. # initiating output (and write a message)
  181. # e.g., in Layer Manager switch to output console
  182. # TODO: this should be something like: write important message or write tip
  183. # TODO: mixed 'switching' and message? no, measuring handles 'swithing'
  184. # on its own
  185. self._giface.WriteWarning(
  186. _(
  187. 'Click and drag with left mouse button '
  188. 'to measure.%s'
  189. 'Double click with left button to clear.') %
  190. (os.linesep))
  191. if self._projInfo['proj'] != 'xy':
  192. mapunits = self._projInfo['units']
  193. self._giface.WriteCmdLog(_('Measuring distance') + ' ('
  194. + mapunits + '):')
  195. else:
  196. self._giface.WriteCmdLog(_('Measuring distance:'))
  197. if self._projInfo['proj'] == 'll':
  198. try:
  199. import grass.lib.gis as gislib
  200. gislib.G_begin_distance_calculations()
  201. self._useCtypes = True
  202. except ImportError as e:
  203. self._giface.WriteWarning(_('Geodesic distance calculation '
  204. 'is not available.\n'
  205. 'Reason: %s' % e))
  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(
  212. beginpt, endpt, screen=False)
  213. dist = round(dist, 3)
  214. mapunits = self._projInfo['units']
  215. if mapunits == 'degrees' and self._useCtypes:
  216. mapunits = 'meters'
  217. d, dunits = units.formatDist(dist, mapunits)
  218. self._totaldist += dist
  219. td, tdunits = units.formatDist(self._totaldist,
  220. mapunits)
  221. if dunits == 'units' and mapunits:
  222. dunits = tdunits = mapunits
  223. strdist = str(d)
  224. strtotdist = str(td)
  225. if self._projInfo[
  226. 'proj'] == 'xy' or 'degree' not in self._projInfo['unit']:
  227. angle = int(math.degrees(math.atan2(north, east)) + 0.5)
  228. # uncomment below (or flip order of atan2(y,x) above) to use
  229. # the mathematical theta convention (CCW from +x axis)
  230. #angle = 90 - angle
  231. if angle < 0:
  232. angle = 360 + angle
  233. mstring = '%s = %s %s\n%s = %s %s\n%s = %d %s\n%s' % (
  234. _('segment'),
  235. strdist, dunits, _('total distance'),
  236. strtotdist, tdunits, _('bearing'),
  237. angle, _('degrees (clockwise from grid-north)'),
  238. '-' * 60)
  239. else:
  240. mstring = '%s = %s %s\n%s = %s %s\n%s' \
  241. % (_('segment'), strdist, dunits,
  242. _('total distance'), strtotdist, tdunits,
  243. '-' * 60)
  244. self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE)
  245. return dist
  246. class MeasureAreaController(AnalysisControllerBase):
  247. """Class controls measuring area in map display."""
  248. def __init__(self, giface, mapWindow):
  249. AnalysisControllerBase.__init__(
  250. self, giface=giface, mapWindow=mapWindow)
  251. self._graphicsType = 'polygon'
  252. def _doAnalysis(self, coords):
  253. """New point added.
  254. :param coords: east north coordinates as a list
  255. """
  256. self.MeasureArea(coords)
  257. def _disconnectAll(self):
  258. self._mapWindow.mouseLeftDown.disconnect(self._start)
  259. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  260. self._mapWindow.mouseDClick.disconnect(self.Stop)
  261. def _connectAll(self):
  262. self._mapWindow.mouseLeftDown.connect(self._start)
  263. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  264. self._mapWindow.mouseDClick.connect(self.Stop)
  265. def _getPen(self):
  266. return wx.Pen(colour='green', width=2, style=wx.SOLID)
  267. def Stop(self, restore=True):
  268. if not self.IsActive():
  269. return
  270. AnalysisControllerBase.Stop(self, restore=restore)
  271. self._giface.WriteCmdLog(_('Measuring finished'))
  272. def Start(self):
  273. """Init measurement routine that calculates area of polygon
  274. drawn on map display.
  275. """
  276. if self.IsActive():
  277. return
  278. AnalysisControllerBase.Start(self)
  279. self._giface.WriteWarning(
  280. _(
  281. 'Click and drag with left mouse button '
  282. 'to measure.%s'
  283. 'Double click with left button to clear.') %
  284. (os.linesep))
  285. self._giface.WriteCmdLog(_('Measuring area:'))
  286. def MeasureArea(self, coords):
  287. """Calculate area and print to output window.
  288. :param coords: list of E, N coordinates
  289. """
  290. # TODO: make sure appending first point is needed for m.measure
  291. coordinates = coords + [coords[0]]
  292. coordinates = ','.join([str(item)
  293. for sublist in coordinates for item in
  294. sublist])
  295. result = RunCommand(
  296. 'm.measure',
  297. flags='g',
  298. coordinates=coordinates,
  299. read=True).strip()
  300. result = parse_key_val(result)
  301. if 'units' not in result:
  302. self._giface.WriteWarning(
  303. _("Units not recognized, measurement failed."))
  304. unit = ''
  305. else:
  306. unit = result['units'].split(',')[1]
  307. if 'area' not in result:
  308. text = _("Area: {area} {unit}\n").format(area=0, unit=unit)
  309. else:
  310. text = _("Area: {area} {unit}\n").format(
  311. area=result['area'], unit=unit)
  312. self._giface.WriteLog(text, notification=Notification.MAKE_VISIBLE)