analysis.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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.giface import Notification
  20. from grass.pydispatch.signal import Signal
  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._oldMouseUse = None
  32. self._oldCursor = None
  33. def IsActive(self):
  34. """!Returns True if analysis mode is activated."""
  35. return bool(self._registeredGraphics)
  36. def _start(self, x, y):
  37. """!Handles the actual start of drawing line
  38. and adding each new point.
  39. @param x,y east north coordinates
  40. """
  41. if not self._registeredGraphics.GetAllItems():
  42. item = self._registeredGraphics.AddItem(coords=[[x, y]])
  43. item.SetPropertyVal('penName', 'analysisPen')
  44. else:
  45. # needed to switch mouse begin and end to draw intermediate line properly
  46. coords = self._registeredGraphics.GetItem(0).GetCoords()[-1]
  47. self._mapWindow.mouse['begin'] = self._mapWindow.Cell2Pixel(coords)
  48. def _addPoint(self, x, y):
  49. """!New point added.
  50. @param x,y east north coordinates
  51. """
  52. # add new point and calculate distance
  53. item = self._registeredGraphics.GetItem(0)
  54. coords = item.GetCoords() + [[x, y]]
  55. item.SetCoords(coords)
  56. # draw
  57. self._mapWindow.ClearLines()
  58. self._registeredGraphics.Draw(pdc=self._mapWindow.pdcTmp)
  59. wx.Yield()
  60. self._doAnalysis(coords)
  61. def _doAnalysis(self, coords):
  62. """!Perform the required analysis
  63. (compute distnace, update profile)
  64. @param coords EN coordinates
  65. """
  66. raise NotImplementedError()
  67. def _disconnectAll(self):
  68. """!Disconnect all mouse signals
  69. to stop drawing."""
  70. raise NotImplementedError()
  71. def _connectAll(self):
  72. """!Connect all mouse signals to draw."""
  73. raise NotImplementedError()
  74. def _getPen(self):
  75. """!Returns wx.Pen instance."""
  76. raise NotImplementedError()
  77. def Stop(self, restore=True):
  78. """!Analysis mode is stopped.
  79. @param restore if restore previous cursor, mouse['use']
  80. """
  81. self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
  82. self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin']
  83. # disconnect mouse events
  84. self._disconnectAll()
  85. # unregister
  86. self._mapWindow.UnregisterGraphicsToDraw(self._registeredGraphics)
  87. self._registeredGraphics = None
  88. self._mapWindow.Refresh()
  89. if restore:
  90. # restore mouse['use'] and cursor to the state before measuring starts
  91. self._mapWindow.SetNamedCursor(self._oldCursor)
  92. self._mapWindow.mouse['use'] = self._oldMouseUse
  93. def Start(self):
  94. """!Init analysis: register graphics to map window,
  95. connect required mouse signals.
  96. """
  97. self._oldMouseUse = self._mapWindow.mouse['use']
  98. self._oldCursor = self._mapWindow.GetNamedCursor()
  99. self._registeredGraphics = self._mapWindow.RegisterGraphicsToDraw(graphicsType='line',
  100. mapCoords=False)
  101. self._connectAll()
  102. # change mouse['box'] and pen to draw line during dragging
  103. # TODO: better solution for drawing this line
  104. self._mapWindow.mouse['use'] = None
  105. self._mapWindow.mouse['box'] = "line"
  106. self._mapWindow.pen = wx.Pen(colour='red', width=2, style=wx.SHORT_DASH)
  107. self._registeredGraphics.AddPen('analysisPen', self._getPen())
  108. # change the cursor
  109. self._mapWindow.SetNamedCursor('pencil')
  110. class ProfileController(AnalysisControllerBase):
  111. """!Class controls profiling in map display.
  112. It should be used inside ProfileFrame
  113. """
  114. def __init__(self, giface, mapWindow):
  115. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  116. self.transectChanged = Signal('ProfileController.transectChanged')
  117. def _doAnalysis(self, coords):
  118. """!Informs profile dialog that profile changed.
  119. @param coords EN coordinates
  120. """
  121. self.transectChanged.emit(coords=coords)
  122. def _disconnectAll(self):
  123. self._mapWindow.mouseLeftDown.disconnect(self._start)
  124. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  125. def _connectAll(self):
  126. self._mapWindow.mouseLeftDown.connect(self._start)
  127. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  128. def _getPen(self):
  129. return wx.Pen(colour=wx.Colour(0, 100, 0), width=2, style=wx.SHORT_DASH)
  130. def Stop(self, restore=True):
  131. AnalysisControllerBase.Stop(self, restore=restore)
  132. self.transectChanged.emit(coords=[])
  133. class MeasureDistanceController(AnalysisControllerBase):
  134. """!Class controls measuring distance in map display."""
  135. def __init__(self, giface, mapWindow):
  136. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  137. self._projInfo = self._mapWindow.Map.projinfo
  138. self._totaldist = 0.0 # total measured distance
  139. self._useCtypes = False
  140. def _doAnalysis(self, coords):
  141. """!New point added.
  142. @param x,y east north coordinates
  143. """
  144. self.MeasureDist(coords[-2], coords[-1])
  145. def _disconnectAll(self):
  146. self._mapWindow.mouseLeftDown.disconnect(self._start)
  147. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  148. self._mapWindow.mouseDClick.disconnect(self.Stop)
  149. def _connectAll(self):
  150. self._mapWindow.mouseLeftDown.connect(self._start)
  151. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  152. self._mapWindow.mouseDClick.connect(self.Stop)
  153. def _getPen(self):
  154. return wx.Pen(colour='green', width=2, style=wx.SHORT_DASH)
  155. def Stop(self, restore=True):
  156. if not self.IsActive():
  157. return
  158. AnalysisControllerBase.Stop(self, restore=restore)
  159. self._giface.WriteCmdLog(_('Measuring finished'))
  160. def Start(self):
  161. """!Init measurement routine that calculates map distance
  162. along transect drawn on map display
  163. """
  164. if self.IsActive():
  165. return
  166. AnalysisControllerBase.Start(self)
  167. self._totaldist = 0.0 # total measured distance
  168. # initiating output (and write a message)
  169. # e.g., in Layer Manager switch to output console
  170. # TODO: this should be something like: write important message or write tip
  171. # TODO: mixed 'switching' and message? no, measuring handles 'swithing' on its own
  172. self._giface.WriteWarning(_('Click and drag with left mouse button '
  173. 'to measure.%s'
  174. 'Double click with left button to clear.') % \
  175. (os.linesep))
  176. if self._projInfo['proj'] != 'xy':
  177. mapunits = self._projInfo['units']
  178. self._giface.WriteCmdLog(_('Measuring distance') + ' ('
  179. + mapunits + '):')
  180. else:
  181. self._giface.WriteCmdLog(_('Measuring distance:'))
  182. if self._projInfo['proj'] == 'll':
  183. try:
  184. import grass.lib.gis as gislib
  185. gislib.G_begin_distance_calculations()
  186. self._useCtypes = True
  187. except ImportError, e:
  188. self._giface.WriteWarning(_('Geodesic distance calculation '
  189. 'is not available.\n'
  190. 'Reason: %s' % e))
  191. def MeasureDist(self, beginpt, endpt):
  192. """!Calculate distance and print to output window.
  193. @param beginpt,endpt EN coordinates
  194. """
  195. # move also Distance method?
  196. dist, (north, east) = self._mapWindow.Distance(beginpt, endpt, screen=False)
  197. dist = round(dist, 3)
  198. mapunits = self._projInfo['units']
  199. if mapunits == 'degrees' and self._useCtypes:
  200. mapunits = 'meters'
  201. d, dunits = units.formatDist(dist, mapunits)
  202. self._totaldist += dist
  203. td, tdunits = units.formatDist(self._totaldist,
  204. mapunits)
  205. strdist = str(d)
  206. strtotdist = str(td)
  207. if self._projInfo['proj'] == 'xy' or 'degree' not in self._projInfo['unit']:
  208. angle = int(math.degrees(math.atan2(north, east)) + 0.5)
  209. # uncomment below (or flip order of atan2(y,x) above) to use
  210. # the mathematical theta convention (CCW from +x axis)
  211. #angle = 90 - angle
  212. if angle < 0:
  213. angle = 360 + angle
  214. mstring = '%s = %s %s\n%s = %s %s\n%s = %d %s\n%s' \
  215. % (_('segment'), strdist, dunits,
  216. _('total distance'), strtotdist, tdunits,
  217. _('bearing'), angle, _('degrees (clockwise from grid-north)'),
  218. '-' * 60)
  219. else:
  220. mstring = '%s = %s %s\n%s = %s %s\n%s' \
  221. % (_('segment'), strdist, dunits,
  222. _('total distance'), strtotdist, tdunits,
  223. '-' * 60)
  224. self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE)
  225. return dist