profile.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. """!
  2. @package wxplot.profile
  3. @brief Profiling using PyPlot
  4. Classes:
  5. - profile::ProfileFrame
  6. - profile::ProfileToolbar
  7. (C) 2011-2012 by the GRASS Development Team
  8. This program is free software under the GNU General Public License
  9. (>=v2). Read the file COPYING that comes with GRASS for details.
  10. @author Michael Barton, Arizona State University
  11. """
  12. import os
  13. import sys
  14. import math
  15. import wx
  16. import wx.lib.plot as plot
  17. import grass.script as grass
  18. try:
  19. import numpy
  20. except ImportError:
  21. msg = _("This module requires the NumPy module, which could not be "
  22. "imported. It probably is not installed (it's not part of the "
  23. "standard Python distribution). See the Numeric Python site "
  24. "(http://numpy.scipy.org) for information on downloading source or "
  25. "binaries.")
  26. print >> sys.stderr, "wxplot.py: " + msg
  27. from wxplot.base import BasePlotFrame, PlotIcons
  28. from gui_core.toolbars import BaseToolbar, BaseIcons
  29. from wxplot.dialogs import ProfileRasterDialog, PlotStatsFrame
  30. from core.gcmd import RunCommand, GWarning, GError, GMessage
  31. class ProfileFrame(BasePlotFrame):
  32. """!Mainframe for displaying profile of one or more raster maps. Uses wx.lib.plot.
  33. """
  34. def __init__(self, parent, id = wx.ID_ANY, style = wx.DEFAULT_FRAME_STYLE,
  35. size = wx.Size(700, 400),
  36. rasterList = [], **kwargs):
  37. BasePlotFrame.__init__(self, parent, size = size, **kwargs)
  38. self.toolbar = ProfileToolbar(parent = self)
  39. self.SetToolBar(self.toolbar)
  40. self.SetTitle(_("GRASS Profile Analysis Tool"))
  41. #
  42. # Init variables
  43. #
  44. self.rasterList = rasterList
  45. self.plottype = 'profile'
  46. self.coordstr = '' # string of coordinates for r.profile
  47. self.seglist = [] # segment endpoint list
  48. self.ppoints = '' # segment endpoints data
  49. self.transect_length = 0.0 # total transect length
  50. self.ptitle = _('Profile of') # title of window
  51. self.colorList = ["blue", "red", "green", "yellow", "magenta", "cyan",
  52. "aqua", "black", "grey", "orange", "brown", "purple", "violet",
  53. "indigo"]
  54. self._initOpts()
  55. if len(self.rasterList) > 0: # set raster name(s) from layer manager if a map is selected
  56. self.raster = self.InitRasterOpts(self.rasterList, self.plottype)
  57. else:
  58. self.raster = {}
  59. # determine units (axis labels)
  60. if self.parent.Map.projinfo['units'] != '':
  61. self.xlabel = _('Distance (%s)') % self.parent.Map.projinfo['units']
  62. else:
  63. self.xlabel = _("Distance along transect")
  64. self.ylabel = _("Cell values")
  65. def _initOpts(self):
  66. """!Initialize plot options
  67. """
  68. self.InitPlotOpts('profile')
  69. def OnDrawTransect(self, event):
  70. """!Draws transect to profile in map display
  71. """
  72. self.parent.SwitchTool(self.parent.toolbars['map'], event)
  73. self.mapwin.polycoords = []
  74. self.seglist = []
  75. self.mapwin.ClearLines(self.mapwin.pdc)
  76. self.ppoints = ''
  77. self.parent.SetFocus()
  78. self.parent.Raise()
  79. self.mapwin.mouse['use'] = 'profile'
  80. self.mapwin.mouse['box'] = 'line'
  81. self.mapwin.pen = wx.Pen(colour = 'Red', width = 2, style = wx.SHORT_DASH)
  82. self.mapwin.polypen = wx.Pen(colour = 'dark green', width = 2, style = wx.SHORT_DASH)
  83. self.mapwin.SetCursor(self.Parent.cursors["cross"])
  84. def OnSelectRaster(self, event):
  85. """!Select raster map(s) to profile
  86. """
  87. dlg = ProfileRasterDialog(parent = self)
  88. if dlg.ShowModal() == wx.ID_OK:
  89. self.rasterList = dlg.rasterList
  90. self.raster = self.InitRasterOpts(self.rasterList, self.plottype)
  91. # plot profile
  92. if len(self.mapwin.polycoords) > 0 and len(self.rasterList) > 0:
  93. self.OnCreateProfile(event = None)
  94. dlg.Destroy()
  95. def SetupProfile(self):
  96. """!Create coordinate string for profiling. Create segment
  97. list for transect segment markers.
  98. """
  99. # create list of coordinate points for r.profile
  100. dist = 0
  101. cumdist = 0
  102. self.coordstr = ''
  103. lasteast = lastnorth = None
  104. region = grass.region()
  105. insideRegion = True
  106. if len(self.mapwin.polycoords) > 0:
  107. for point in self.mapwin.polycoords:
  108. if not (region['w'] <= point[0] <= region['e'] and region['s'] <= point[1] <= region['n']):
  109. insideRegion = False
  110. # build string of coordinate points for r.profile
  111. if self.coordstr == '':
  112. self.coordstr = '%d,%d' % (point[0], point[1])
  113. else:
  114. self.coordstr = '%s,%d,%d' % (self.coordstr, point[0], point[1])
  115. if not insideRegion:
  116. GWarning(message = _("Not all points of profile lie inside computational region."),
  117. parent = self)
  118. if len(self.rasterList) == 0:
  119. return
  120. # title of window
  121. self.ptitle = _('Profile of')
  122. # create list of coordinates for transect segment markers
  123. if len(self.mapwin.polycoords) > 0:
  124. self.seglist = []
  125. for point in self.mapwin.polycoords:
  126. # get value of raster cell at coordinate point
  127. ret = RunCommand('r.what',
  128. parent = self,
  129. read = True,
  130. map = self.rasterList[0],
  131. coordinates = '%d,%d' % (point[0],point[1]))
  132. val = ret.splitlines()[0].split('|')[3]
  133. if val == None or val == '*':
  134. continue
  135. val = float(val)
  136. # calculate distance between coordinate points
  137. if lasteast and lastnorth:
  138. dist = math.sqrt(math.pow((lasteast-point[0]),2) + math.pow((lastnorth-point[1]),2))
  139. cumdist += dist
  140. # store total transect length
  141. self.transect_length = cumdist
  142. # build a list of distance,value pairs for each segment of transect
  143. self.seglist.append((cumdist,val))
  144. lasteast = point[0]
  145. lastnorth = point[1]
  146. # delete extra first segment point
  147. try:
  148. self.seglist.pop(0)
  149. except:
  150. pass
  151. #
  152. # create datalist of dist/value pairs and y labels for each raster map
  153. #
  154. self.ylabel = ''
  155. i = 0
  156. for r in self.raster.iterkeys():
  157. self.raster[r]['datalist'] = []
  158. datalist = self.CreateDatalist(r, self.coordstr)
  159. if len(datalist) > 0:
  160. self.raster[r]['datalist'] = datalist
  161. # update ylabel to match units if they exist
  162. if self.raster[r]['units'] != '':
  163. self.ylabel += '%s (%d),' % (r['units'], i)
  164. i += 1
  165. # update title
  166. self.ptitle += ' %s ,' % r.split('@')[0]
  167. self.ptitle = self.ptitle.rstrip(',')
  168. if self.ylabel == '':
  169. self.ylabel = _('Raster values')
  170. else:
  171. self.ylabel = self.ylabel.rstrip(',')
  172. def CreateDatalist(self, raster, coords):
  173. """!Build a list of distance, value pairs for points along transect using r.profile
  174. """
  175. datalist = []
  176. # keep total number of transect points to 500 or less to avoid
  177. # freezing with large, high resolution maps
  178. region = grass.region()
  179. curr_res = min(float(region['nsres']),float(region['ewres']))
  180. transect_rec = 0
  181. if self.transect_length / curr_res > 500:
  182. transect_res = self.transect_length / 500
  183. else: transect_res = curr_res
  184. ret = RunCommand("r.profile",
  185. parent = self,
  186. input = raster,
  187. profile = coords,
  188. res = transect_res,
  189. null = "nan",
  190. quiet = True,
  191. read = True)
  192. if not ret:
  193. return []
  194. for line in ret.splitlines():
  195. dist, elev = line.strip().split(' ')
  196. if dist == None or dist == '' or dist == 'nan' or \
  197. elev == None or elev == '' or elev == 'nan':
  198. continue
  199. dist = float(dist)
  200. elev = float(elev)
  201. datalist.append((dist,elev))
  202. return datalist
  203. def OnCreateProfile(self, event):
  204. """!Main routine for creating a profile. Uses r.profile to
  205. create a list of distance,cell value pairs. This is passed to
  206. plot to create a line graph of the profile. If the profile
  207. transect is in multiple segments, these are drawn as
  208. points. Profile transect is drawn, using methods in mapdisp.py
  209. """
  210. if len(self.mapwin.polycoords) == 0 or len(self.rasterList) == 0:
  211. dlg = wx.MessageDialog(parent = self,
  212. message = _('You must draw a transect to profile in the map display window.'),
  213. caption = _('Nothing to profile'),
  214. style = wx.OK | wx.ICON_INFORMATION | wx.CENTRE)
  215. dlg.ShowModal()
  216. dlg.Destroy()
  217. return
  218. self.mapwin.SetCursor(self.parent.cursors["default"])
  219. self.SetCursor(self.parent.cursors["default"])
  220. self.SetGraphStyle()
  221. self.SetupProfile()
  222. p = self.CreatePlotList()
  223. self.DrawPlot(p)
  224. # reset transect
  225. self.mapwin.mouse['begin'] = self.mapwin.mouse['end'] = (0.0,0.0)
  226. self.mapwin.mouse['use'] = 'pointer'
  227. self.mapwin.mouse['box'] = 'point'
  228. def CreatePlotList(self):
  229. """!Create a plot data list from transect datalist and
  230. transect segment endpoint coordinates.
  231. """
  232. # graph the distance, value pairs for the transect
  233. self.plotlist = []
  234. # Add segment marker points to plot data list
  235. if len(self.seglist) > 0 :
  236. self.ppoints = plot.PolyMarker(self.seglist,
  237. legend = ' ' + self.properties['marker']['legend'],
  238. colour = wx.Colour(self.properties['marker']['color'][0],
  239. self.properties['marker']['color'][1],
  240. self.properties['marker']['color'][2],
  241. 255),
  242. size = self.properties['marker']['size'],
  243. fillstyle = self.ptfilldict[self.properties['marker']['fill']],
  244. marker = self.properties['marker']['type'])
  245. self.plotlist.append(self.ppoints)
  246. # Add profile distance/elevation pairs to plot data list for each raster profiled
  247. for r in self.rasterList:
  248. col = wx.Colour(self.raster[r]['pcolor'][0],
  249. self.raster[r]['pcolor'][1],
  250. self.raster[r]['pcolor'][2],
  251. 255)
  252. self.raster[r]['pline'] = plot.PolyLine(self.raster[r]['datalist'],
  253. colour = col,
  254. width = self.raster[r]['pwidth'],
  255. style = self.linestyledict[self.raster[r]['pstyle']],
  256. legend = self.raster[r]['plegend'])
  257. self.plotlist.append(self.raster[r]['pline'])
  258. if len(self.plotlist) > 0:
  259. return self.plotlist
  260. else:
  261. return None
  262. def Update(self):
  263. """!Update profile after changing options
  264. """
  265. self.SetGraphStyle()
  266. p = self.CreatePlotList()
  267. self.DrawPlot(p)
  268. def SaveProfileToFile(self, event):
  269. """!Save r.profile data to a csv file
  270. """
  271. dlg = wx.FileDialog(parent = self,
  272. message = _("Choose prefix for file(s) where to save profile values..."),
  273. defaultDir = os.getcwd(),
  274. wildcard = _("Comma separated value (*.csv)|*.csv"), style = wx.SAVE)
  275. pfile = []
  276. if dlg.ShowModal() == wx.ID_OK:
  277. path = dlg.GetPath()
  278. for r in self.rasterList:
  279. pfile.append(path + '_' + str(r.replace('@', '_')) + '.csv')
  280. if os.path.exists(pfile[-1]):
  281. dlgOv = wx.MessageDialog(self,
  282. message = _("File <%s> already exists. "
  283. "Do you want to overwrite this file?") % pfile[-1],
  284. caption = _("Overwrite file?"),
  285. style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
  286. if dlgOv.ShowModal() != wx.ID_YES:
  287. pfile.pop()
  288. dlgOv.Destroy()
  289. continue
  290. try:
  291. fd = open(pfile[-1], "w")
  292. except IOError, e:
  293. GError(parent = self,
  294. message = _("Unable to open file <%s> for writing.\n"
  295. "Reason: %s") % (pfile[-1], e))
  296. dlg.Destroy()
  297. return
  298. for datapair in self.raster[r]['datalist']:
  299. fd.write('%d,%d\n' % (float(datapair[0]),float(datapair[1])))
  300. fd.close()
  301. dlg.Destroy()
  302. if pfile:
  303. message = _("%d files created:\n%s") % (len(pfile), '\n'.join(pfile))
  304. else:
  305. message = _("No files generated.")
  306. GMessage(parent = self, message = message)
  307. def OnStats(self, event):
  308. """!Displays regression information in messagebox
  309. """
  310. message = []
  311. title = _('Statistics for Profile(s)')
  312. for r in self.raster.iterkeys():
  313. try:
  314. rast = r.split('@')[0]
  315. statstr = 'Profile of %s\n\n' % rast
  316. iterable = (i[1] for i in self.raster[r]['datalist'])
  317. a = numpy.fromiter(iterable, numpy.float)
  318. statstr += 'n: %f\n' % a.size
  319. statstr += 'minimum: %f\n' % numpy.amin(a)
  320. statstr += 'maximum: %f\n' % numpy.amax(a)
  321. statstr += 'range: %f\n' % numpy.ptp(a)
  322. statstr += 'mean: %f\n' % numpy.mean(a)
  323. statstr += 'standard deviation: %f\n' % numpy.std(a)
  324. statstr += 'variance: %f\n' % numpy.var(a)
  325. cv = numpy.std(a)/numpy.mean(a)
  326. statstr += 'coefficient of variation: %f\n' % cv
  327. statstr += 'sum: %f\n' % numpy.sum(a)
  328. statstr += 'median: %f\n' % numpy.median(a)
  329. statstr += 'distance along transect: %f\n\n' % self.transect_length
  330. message.append(statstr)
  331. except:
  332. pass
  333. stats = PlotStatsFrame(self, id = wx.ID_ANY, message = message,
  334. title = title)
  335. if stats.Show() == wx.ID_CLOSE:
  336. stats.Destroy()
  337. class ProfileToolbar(BaseToolbar):
  338. """!Toolbar for profiling raster map
  339. """
  340. def __init__(self, parent):
  341. BaseToolbar.__init__(self, parent)
  342. self.InitToolbar(self._toolbarData())
  343. # realize the toolbar
  344. self.Realize()
  345. def _toolbarData(self):
  346. """!Toolbar data"""
  347. return self._getToolbarData((('addraster', BaseIcons["addRast"],
  348. self.parent.OnSelectRaster),
  349. ('transect', PlotIcons["transect"],
  350. self.parent.OnDrawTransect),
  351. (None, ),
  352. ('draw', PlotIcons["draw"],
  353. self.parent.OnCreateProfile),
  354. ('erase', BaseIcons["erase"],
  355. self.parent.OnErase),
  356. ('drag', BaseIcons['pan'],
  357. self.parent.OnDrag),
  358. ('zoom', BaseIcons['zoomIn'],
  359. self.parent.OnZoom),
  360. ('unzoom', BaseIcons['zoomBack'],
  361. self.parent.OnRedraw),
  362. (None, ),
  363. ('statistics', PlotIcons['statistics'],
  364. self.parent.OnStats),
  365. ('datasave', PlotIcons["save"],
  366. self.parent.SaveProfileToFile),
  367. ('image', BaseIcons["saveFile"],
  368. self.parent.SaveToFile),
  369. ('print', BaseIcons["print"],
  370. self.parent.PrintMenu),
  371. (None, ),
  372. ('settings', PlotIcons["options"],
  373. self.parent.PlotOptionsMenu),
  374. ('quit', PlotIcons["quit"],
  375. self.parent.OnQuit),
  376. ))