analysis.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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. self._connectAll()
  101. # change mouse['box'] and pen to draw line during dragging
  102. # TODO: better solution for drawing this line
  103. self._mapWindow.mouse['use'] = None
  104. self._mapWindow.mouse['box'] = "line"
  105. self._mapWindow.pen = wx.Pen(colour='red', width=2, style=wx.SHORT_DASH)
  106. self._registeredGraphics.AddPen('analysisPen', self._getPen())
  107. # change the cursor
  108. self._mapWindow.SetNamedCursor('pencil')
  109. class ProfileController(AnalysisControllerBase):
  110. """!Class controls profiling in map display.
  111. It should be used inside ProfileFrame
  112. """
  113. def __init__(self, giface, mapWindow):
  114. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  115. self.transectChanged = Signal('ProfileController.transectChanged')
  116. def _doAnalysis(self, coords):
  117. """!Informs profile dialog that profile changed.
  118. @param coords EN coordinates
  119. """
  120. self.transectChanged.emit(coords=coords)
  121. def _disconnectAll(self):
  122. self._mapWindow.mouseLeftDown.disconnect(self._start)
  123. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  124. def _connectAll(self):
  125. self._mapWindow.mouseLeftDown.connect(self._start)
  126. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  127. def _getPen(self):
  128. return wx.Pen(colour=wx.Colour(0, 100, 0), width=2, style=wx.SHORT_DASH)
  129. def Stop(self, restore=True):
  130. AnalysisControllerBase.Stop(self, restore=restore)
  131. self.transectChanged.emit(coords=[])
  132. class MeasureDistanceController(AnalysisControllerBase):
  133. """!Class controls measuring distance in map display."""
  134. def __init__(self, giface, mapWindow):
  135. AnalysisControllerBase.__init__(self, giface=giface, mapWindow=mapWindow)
  136. self._projInfo = self._mapWindow.Map.projinfo
  137. self._totaldist = 0.0 # total measured distance
  138. self._useCtypes = False
  139. def _doAnalysis(self, coords):
  140. """!New point added.
  141. @param x,y east north coordinates
  142. """
  143. self.MeasureDist(coords[-2], coords[-1])
  144. def _disconnectAll(self):
  145. self._mapWindow.mouseLeftDown.disconnect(self._start)
  146. self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
  147. self._mapWindow.mouseDClick.disconnect(self.Stop)
  148. def _connectAll(self):
  149. self._mapWindow.mouseLeftDown.connect(self._start)
  150. self._mapWindow.mouseLeftUp.connect(self._addPoint)
  151. self._mapWindow.mouseDClick.connect(self.Stop)
  152. def _getPen(self):
  153. return wx.Pen(colour='green', width=2, style=wx.SHORT_DASH)
  154. def Stop(self, restore=True):
  155. AnalysisControllerBase.Stop(self, restore=restore)
  156. self._giface.WriteCmdLog(_('Measuring finished'))
  157. def Start(self):
  158. """!Init measurement routine that calculates map distance
  159. along transect drawn on map display
  160. """
  161. AnalysisControllerBase.Start(self)
  162. self._totaldist = 0.0 # total measured distance
  163. # initiating output (and write a message)
  164. # e.g., in Layer Manager switch to output console
  165. # TODO: this should be something like: write important message or write tip
  166. # TODO: mixed 'switching' and message? no, measuring handles 'swithing' on its own
  167. self._giface.WriteWarning(_('Click and drag with left mouse button '
  168. 'to measure.%s'
  169. 'Double click with left button to clear.') % \
  170. (os.linesep))
  171. if self._projInfo['proj'] != 'xy':
  172. mapunits = self._projInfo['units']
  173. self._giface.WriteCmdLog(_('Measuring distance') + ' ('
  174. + mapunits + '):')
  175. else:
  176. self._giface.WriteCmdLog(_('Measuring distance:'))
  177. if self._projInfo['proj'] == 'll':
  178. try:
  179. import grass.lib.gis as gislib
  180. gislib.G_begin_distance_calculations()
  181. self._useCtypes = True
  182. except ImportError, e:
  183. self._giface.WriteWarning(_('Geodesic distance calculation '
  184. 'is not available.\n'
  185. 'Reason: %s' % e))
  186. def MeasureDist(self, beginpt, endpt):
  187. """!Calculate distance and print to output window.
  188. @param beginpt,endpt EN coordinates
  189. """
  190. # move also Distance method?
  191. dist, (north, east) = self._mapWindow.Distance(beginpt, endpt, screen=False)
  192. dist = round(dist, 3)
  193. mapunits = self._projInfo['units']
  194. if mapunits == 'degrees' and self._useCtypes:
  195. mapunits = 'meters'
  196. d, dunits = units.formatDist(dist, mapunits)
  197. self._totaldist += dist
  198. td, tdunits = units.formatDist(self._totaldist,
  199. mapunits)
  200. strdist = str(d)
  201. strtotdist = str(td)
  202. if self._projInfo['proj'] == 'xy' or 'degree' not in self._projInfo['unit']:
  203. angle = int(math.degrees(math.atan2(north, east)) + 0.5)
  204. # uncomment below (or flip order of atan2(y,x) above) to use
  205. # the mathematical theta convention (CCW from +x axis)
  206. #angle = 90 - angle
  207. if angle < 0:
  208. angle = 360 + angle
  209. mstring = '%s = %s %s\n%s = %s %s\n%s = %d %s\n%s' \
  210. % (_('segment'), strdist, dunits,
  211. _('total distance'), strtotdist, tdunits,
  212. _('bearing'), angle, _('degrees (clockwise from grid-north)'),
  213. '-' * 60)
  214. else:
  215. mstring = '%s = %s %s\n%s = %s %s\n%s' \
  216. % (_('segment'), strdist, dunits,
  217. _('total distance'), strtotdist, tdunits,
  218. '-' * 60)
  219. self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE)
  220. return dist