profile.py 18 KB

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