profile.py 19 KB

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