analysis.py 9.6 KB

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