analysis.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. AnalysisControllerBase.Stop(self, restore=restore)
  157. self._giface.WriteCmdLog(_('Measuring finished'))
  158. def Start(self):
  159. """!Init measurement routine that calculates map distance
  160. along transect drawn on map display
  161. """
  162. AnalysisControllerBase.Start(self)
  163. self._totaldist = 0.0 # total measured distance
  164. # initiating output (and write a message)
  165. # e.g., in Layer Manager switch to output console
  166. # TODO: this should be something like: write important message or write tip
  167. # TODO: mixed 'switching' and message? no, measuring handles 'swithing' on its own
  168. self._giface.WriteWarning(_('Click and drag with left mouse button '
  169. 'to measure.%s'
  170. 'Double click with left button to clear.') % \
  171. (os.linesep))
  172. if self._projInfo['proj'] != 'xy':
  173. mapunits = self._projInfo['units']
  174. self._giface.WriteCmdLog(_('Measuring distance') + ' ('
  175. + mapunits + '):')
  176. else:
  177. self._giface.WriteCmdLog(_('Measuring distance:'))
  178. if self._projInfo['proj'] == 'll':
  179. try:
  180. import grass.lib.gis as gislib
  181. gislib.G_begin_distance_calculations()
  182. self._useCtypes = True
  183. except ImportError, e:
  184. self._giface.WriteWarning(_('Geodesic distance calculation '
  185. 'is not available.\n'
  186. 'Reason: %s' % e))
  187. def MeasureDist(self, beginpt, endpt):
  188. """!Calculate distance and print to output window.
  189. @param beginpt,endpt EN coordinates
  190. """
  191. # move also Distance method?
  192. dist, (north, east) = self._mapWindow.Distance(beginpt, endpt, screen=False)
  193. dist = round(dist, 3)
  194. mapunits = self._projInfo['units']
  195. if mapunits == 'degrees' and self._useCtypes:
  196. mapunits = 'meters'
  197. d, dunits = units.formatDist(dist, mapunits)
  198. self._totaldist += dist
  199. td, tdunits = units.formatDist(self._totaldist,
  200. mapunits)
  201. strdist = str(d)
  202. strtotdist = str(td)
  203. if self._projInfo['proj'] == 'xy' or 'degree' not in self._projInfo['unit']:
  204. angle = int(math.degrees(math.atan2(north, east)) + 0.5)
  205. # uncomment below (or flip order of atan2(y,x) above) to use
  206. # the mathematical theta convention (CCW from +x axis)
  207. #angle = 90 - angle
  208. if angle < 0:
  209. angle = 360 + angle
  210. mstring = '%s = %s %s\n%s = %s %s\n%s = %d %s\n%s' \
  211. % (_('segment'), strdist, dunits,
  212. _('total distance'), strtotdist, tdunits,
  213. _('bearing'), angle, _('degrees (clockwise from grid-north)'),
  214. '-' * 60)
  215. else:
  216. mstring = '%s = %s %s\n%s = %s %s\n%s' \
  217. % (_('segment'), strdist, dunits,
  218. _('total distance'), strtotdist, tdunits,
  219. '-' * 60)
  220. self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE)
  221. return dist