base.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. """!
  2. @package wxplot.base
  3. @brief Base classes for iinteractive plotting using PyPlot
  4. Classes:
  5. - base::PlotIcons
  6. - base::BasePlotFrame
  7. (C) 2011 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 wx
  15. import wx.lib.plot as plot
  16. from core.globalvar import ETCICONDIR
  17. from core.settings import UserSettings
  18. from wxplot.dialogs import TextDialog, OptDialog
  19. from core.render import Map
  20. from icons.icon import MetaIcon
  21. from gui_core.toolbars import BaseIcons
  22. from core.utils import _
  23. import grass.script as grass
  24. PlotIcons = {
  25. 'draw' : MetaIcon(img = 'show',
  26. label = _('Draw/re-draw plot')),
  27. 'transect' : MetaIcon(img = 'layer-raster-profile',
  28. label = _('Draw transect in map display window to profile')),
  29. 'options' : MetaIcon(img = 'settings',
  30. label = _('Plot options')),
  31. 'statistics' : MetaIcon(img = 'check',
  32. label = _('Plot statistics')),
  33. 'save' : MetaIcon(img = 'save',
  34. label = _('Save profile data to CSV file')),
  35. 'quit' : BaseIcons['quit'].SetLabel(_('Quit plot tool')),
  36. }
  37. class BasePlotFrame(wx.Frame):
  38. """!Abstract PyPlot display frame class"""
  39. def __init__(self, parent = None, mapwindow = None, id = wx.ID_ANY, size = wx.Size(700, 400),
  40. style = wx.DEFAULT_FRAME_STYLE, rasterList = [], **kwargs):
  41. wx.Frame.__init__(self, parent, id, size = size, style = style, **kwargs)
  42. self.parent = parent # MapFrame for a plot type
  43. self.mapwin = mapwindow
  44. self.Map = Map() # instance of render.Map to be associated with display
  45. self.rasterList = rasterList #list of rasters to plot
  46. self.raster = {} # dictionary of raster maps and their plotting parameters
  47. self.plottype = ''
  48. self.linestyledict = { 'solid' : wx.SOLID,
  49. 'dot' : wx.DOT,
  50. 'long-dash' : wx.LONG_DASH,
  51. 'short-dash' : wx.SHORT_DASH,
  52. 'dot-dash' : wx.DOT_DASH }
  53. self.ptfilldict = { 'transparent' : wx.TRANSPARENT,
  54. 'solid' : wx.SOLID }
  55. #
  56. # Icon
  57. #
  58. self.SetIcon(wx.Icon(os.path.join(ETCICONDIR, 'grass.ico'), wx.BITMAP_TYPE_ICO))
  59. #
  60. # Add statusbar
  61. #
  62. self.statusbar = self.CreateStatusBar(number = 2, style = 0)
  63. self.statusbar.SetStatusWidths([-2, -1])
  64. #
  65. # Define canvas and settings
  66. #
  67. #
  68. self.client = plot.PlotCanvas(self)
  69. #define the function for drawing pointLabels
  70. self.client.SetPointLabelFunc(self.DrawPointLabel)
  71. # Create mouse event for showing cursor coords in status bar
  72. self.client.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
  73. # Show closest point when enabled
  74. self.client.canvas.Bind(wx.EVT_MOTION, self.OnMotion)
  75. self.plotlist = [] # list of things to plot
  76. self.plot = None # plot draw object
  77. self.ptitle = "" # title of window
  78. self.xlabel = "" # default X-axis label
  79. self.ylabel = "" # default Y-axis label
  80. #
  81. # Bind various events
  82. #
  83. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  84. self.CentreOnScreen()
  85. self._createColorDict()
  86. def _createColorDict(self):
  87. """!Create color dictionary to return wx.Colour tuples
  88. for assigning colors to images in imagery groups"""
  89. self.colorDict = {}
  90. for clr in grass.named_colors.iterkeys():
  91. if clr == 'white': continue
  92. r = grass.named_colors[clr][0] * 255
  93. g = grass.named_colors[clr][1] * 255
  94. b = grass.named_colors[clr][2] * 255
  95. self.colorDict[clr] = (r,g,b,255)
  96. def InitPlotOpts(self, plottype):
  97. """!Initialize options for entire plot
  98. """
  99. self.plottype = plottype # histogram, profile, or scatter
  100. self.properties = {} # plot properties
  101. self.properties['font'] = {}
  102. self.properties['font']['prop'] = UserSettings.Get(group = self.plottype, key = 'font')
  103. self.properties['font']['wxfont'] = wx.Font(11, wx.FONTFAMILY_SWISS,
  104. wx.FONTSTYLE_NORMAL,
  105. wx.FONTWEIGHT_NORMAL)
  106. self.properties['raster'] = {}
  107. self.properties['raster'] = UserSettings.Get(group = self.plottype, key = 'raster')
  108. colstr = str(self.properties['raster']['pcolor'])
  109. self.properties['raster']['pcolor'] = tuple(int(colval) for colval in colstr.strip('()').split(','))
  110. if self.plottype == 'profile':
  111. self.properties['marker'] = UserSettings.Get(group = self.plottype, key = 'marker')
  112. # changing color string to tuple for markers/points
  113. colstr = str(self.properties['marker']['color'])
  114. self.properties['marker']['color'] = tuple(int(colval) for colval in colstr.strip('()').split(','))
  115. self.properties['grid'] = UserSettings.Get(group = self.plottype, key = 'grid')
  116. colstr = str(self.properties['grid']['color']) # changing color string to tuple
  117. self.properties['grid']['color'] = tuple(int(colval) for colval in colstr.strip('()').split(','))
  118. self.properties['x-axis'] = {}
  119. self.properties['x-axis']['prop'] = UserSettings.Get(group = self.plottype, key = 'x-axis')
  120. self.properties['x-axis']['axis'] = None
  121. self.properties['y-axis'] = {}
  122. self.properties['y-axis']['prop'] = UserSettings.Get(group = self.plottype, key = 'y-axis')
  123. self.properties['y-axis']['axis'] = None
  124. self.properties['legend'] = UserSettings.Get(group = self.plottype, key = 'legend')
  125. self.zoom = False # zooming disabled
  126. self.drag = False # draging disabled
  127. self.client.SetShowScrollbars(True) # vertical and horizontal scrollbars
  128. # x and y axis set to normal (non-log)
  129. self.client.setLogScale((False, False))
  130. if self.properties['x-axis']['prop']['type']:
  131. self.client.SetXSpec(self.properties['x-axis']['prop']['type'])
  132. else:
  133. self.client.SetXSpec('auto')
  134. if self.properties['y-axis']['prop']['type']:
  135. self.client.SetYSpec(self.properties['y-axis']['prop']['type'])
  136. else:
  137. self.client.SetYSpec('auto')
  138. def InitRasterOpts(self, rasterList, plottype):
  139. """!Initialize or update raster dictionary for plotting
  140. """
  141. rdict = {} # initialize a dictionary
  142. self.properties['raster'] = UserSettings.Get(group = self.plottype, key = 'raster')
  143. for r in rasterList:
  144. idx = rasterList.index(r)
  145. try:
  146. ret = grass.raster_info(r)
  147. except:
  148. continue
  149. # if r.info cannot parse map, skip it
  150. self.raster[r] = self.properties['raster'] # some default settings
  151. rdict[r] = {} # initialize sub-dictionaries for each raster in the list
  152. rdict[r]['units'] = ''
  153. if ret['units'] not in ('(none)', '"none"', '', None):
  154. rdict[r]['units'] = ret['units']
  155. rdict[r]['plegend'] = r.split('@')[0]
  156. rdict[r]['datalist'] = [] # list of cell value,frequency pairs for plotting histogram
  157. rdict[r]['pline'] = None
  158. rdict[r]['datatype'] = ret['datatype']
  159. #
  160. #initialize with saved values
  161. #
  162. if self.properties['raster']['pwidth'] != None:
  163. rdict[r]['pwidth'] = self.properties['raster']['pwidth']
  164. else:
  165. rdict[r]['pwidth'] = 1
  166. if self.properties['raster']['pstyle'] != None and \
  167. self.properties['raster']['pstyle'] != '':
  168. rdict[r]['pstyle'] = self.properties['raster']['pstyle']
  169. else:
  170. rdict[r]['pstyle'] = 'solid'
  171. if idx < len(self.colorList):
  172. if idx == 0:
  173. # use saved color for first plot
  174. if self.properties['raster']['pcolor'] != None:
  175. rdict[r]['pcolor'] = self.properties['raster']['pcolor']
  176. else:
  177. rdict[r]['pcolor'] = self.colorDict[self.colorList[idx]]
  178. else:
  179. rdict[r]['pcolor'] = self.colorDict[self.colorList[idx]]
  180. else:
  181. r = randint(0, 255)
  182. b = randint(0, 255)
  183. g = randint(0, 255)
  184. rdict[r]['pcolor'] = ((r,g,b,255))
  185. return rdict
  186. def InitRasterPairs(self, rasterList, plottype):
  187. """!Initialize or update raster dictionary with raster pairs for
  188. bivariate scatterplots
  189. """
  190. if len(rasterList) == 0: return
  191. rdict = {} # initialize a dictionary
  192. for rpair in rasterList:
  193. idx = rasterList.index(rpair)
  194. try:
  195. ret0 = grass.raster_info(rpair[0])
  196. ret1 = grass.raster_info(rpair[1])
  197. except:
  198. continue
  199. # if r.info cannot parse map, skip it
  200. self.raster[rpair] = UserSettings.Get(group = plottype, key = 'rasters') # some default settings
  201. rdict[rpair] = {} # initialize sub-dictionaries for each raster in the list
  202. rdict[rpair][0] = {}
  203. rdict[rpair][1] = {}
  204. rdict[rpair][0]['units'] = ''
  205. rdict[rpair][1]['units'] = ''
  206. if ret0['units'] not in ('(none)', '"none"', '', None):
  207. rdict[rpair][0]['units'] = ret0['units']
  208. if ret1['units'] not in ('(none)', '"none"', '', None):
  209. rdict[rpair][1]['units'] = ret1['units']
  210. rdict[rpair]['plegend'] = rpair[0].split('@')[0] + ' vs ' + rpair[1].split('@')[0]
  211. rdict[rpair]['datalist'] = [] # list of cell value,frequency pairs for plotting histogram
  212. rdict[rpair][0]['datatype'] = ret0['datatype']
  213. rdict[rpair][1]['datatype'] = ret1['datatype']
  214. #
  215. #initialize with saved values
  216. #
  217. if self.properties['raster']['ptype'] != None and \
  218. self.properties['raster']['ptype'] != '':
  219. rdict[rpair]['ptype'] = self.properties['raster']['ptype']
  220. else:
  221. rdict[rpair]['ptype'] = 'dot'
  222. if self.properties['raster']['psize'] != None:
  223. rdict[rpair]['psize'] = self.properties['raster']['psize']
  224. else:
  225. rdict[rpair]['psize'] = 1
  226. if self.properties['raster']['pfill'] != None and \
  227. self.properties['raster']['pfill'] != '':
  228. rdict[rpair]['pfill'] = self.properties['raster']['pfill']
  229. else:
  230. rdict[rpair]['pfill'] = 'solid'
  231. if idx <= len(self.colorList):
  232. rdict[rpair]['pcolor'] = self.colorDict[self.colorList[idx]]
  233. else:
  234. r = randint(0, 255)
  235. b = randint(0, 255)
  236. g = randint(0, 255)
  237. rdict[rpair]['pcolor'] = ((r,g,b,255))
  238. return rdict
  239. def SetGraphStyle(self):
  240. """!Set plot and text options
  241. """
  242. self.client.SetFont(self.properties['font']['wxfont'])
  243. self.client.SetFontSizeTitle(self.properties['font']['prop']['titleSize'])
  244. self.client.SetFontSizeAxis(self.properties['font']['prop']['axisSize'])
  245. self.client.SetEnableZoom(self.zoom)
  246. self.client.SetEnableDrag(self.drag)
  247. #
  248. # axis settings
  249. #
  250. if self.properties['x-axis']['prop']['type'] == 'custom':
  251. self.client.SetXSpec('min')
  252. else:
  253. self.client.SetXSpec(self.properties['x-axis']['prop']['type'])
  254. if self.properties['y-axis']['prop']['type'] == 'custom':
  255. self.client.SetYSpec('min')
  256. else:
  257. self.client.SetYSpec(self.properties['y-axis']['prop'])
  258. if self.properties['x-axis']['prop']['type'] == 'custom' and \
  259. self.properties['x-axis']['prop']['min'] < self.properties['x-axis']['prop']['max']:
  260. self.properties['x-axis']['axis'] = (self.properties['x-axis']['prop']['min'],
  261. self.properties['x-axis']['prop']['max'])
  262. else:
  263. self.properties['x-axis']['axis'] = None
  264. if self.properties['y-axis']['prop']['type'] == 'custom' and \
  265. self.properties['y-axis']['prop']['min'] < self.properties['y-axis']['prop']['max']:
  266. self.properties['y-axis']['axis'] = (self.properties['y-axis']['prop']['min'],
  267. self.properties['y-axis']['prop']['max'])
  268. else:
  269. self.properties['y-axis']['axis'] = None
  270. if self.properties['x-axis']['prop']['log'] == True:
  271. self.properties['x-axis']['axis'] = None
  272. self.client.SetXSpec('min')
  273. if self.properties['y-axis']['prop']['log'] == True:
  274. self.properties['y-axis']['axis'] = None
  275. self.client.SetYSpec('min')
  276. self.client.setLogScale((self.properties['x-axis']['prop']['log'],
  277. self.properties['y-axis']['prop']['log']))
  278. #
  279. # grid settings
  280. #
  281. self.client.SetEnableGrid(self.properties['grid']['enabled'])
  282. self.client.SetGridColour(wx.Colour(self.properties['grid']['color'][0],
  283. self.properties['grid']['color'][1],
  284. self.properties['grid']['color'][2],
  285. 255))
  286. #
  287. # legend settings
  288. #
  289. self.client.SetFontSizeLegend(self.properties['font']['prop']['legendSize'])
  290. self.client.SetEnableLegend(self.properties['legend']['enabled'])
  291. def DrawPlot(self, plotlist):
  292. """!Draw line and point plot from list plot elements.
  293. """
  294. self.plot = plot.PlotGraphics(plotlist,
  295. self.ptitle,
  296. self.xlabel,
  297. self.ylabel)
  298. if self.properties['x-axis']['prop']['type'] == 'custom':
  299. self.client.SetXSpec('min')
  300. else:
  301. self.client.SetXSpec(self.properties['x-axis']['prop']['type'])
  302. if self.properties['y-axis']['prop']['type'] == 'custom':
  303. self.client.SetYSpec('min')
  304. else:
  305. self.client.SetYSpec(self.properties['y-axis']['prop']['type'])
  306. self.client.Draw(self.plot, self.properties['x-axis']['axis'],
  307. self.properties['y-axis']['axis'])
  308. def DrawPointLabel(self, dc, mDataDict):
  309. """!This is the fuction that defines how the pointLabels are
  310. plotted dc - DC that will be passed mDataDict - Dictionary
  311. of data that you want to use for the pointLabel
  312. As an example I have decided I want a box at the curve
  313. point with some text information about the curve plotted
  314. below. Any wxDC method can be used.
  315. """
  316. dc.SetPen(wx.Pen(wx.BLACK))
  317. dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
  318. sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
  319. dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
  320. px,py = mDataDict["pointXY"]
  321. cNum = mDataDict["curveNum"]
  322. pntIn = mDataDict["pIndex"]
  323. legend = mDataDict["legend"]
  324. #make a string to display
  325. s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
  326. dc.DrawText(s, sx , sy+1)
  327. def OnZoom(self, event):
  328. """!Enable zooming and disable dragging
  329. """
  330. self.zoom = True
  331. self.drag = False
  332. self.client.SetEnableZoom(self.zoom)
  333. self.client.SetEnableDrag(self.drag)
  334. def OnDrag(self, event):
  335. """!Enable dragging and disable zooming
  336. """
  337. self.zoom = False
  338. self.drag = True
  339. self.client.SetEnableDrag(self.drag)
  340. self.client.SetEnableZoom(self.zoom)
  341. def OnRedraw(self, event):
  342. """!Redraw the plot window. Unzoom to original size
  343. """
  344. self.client.Reset()
  345. self.client.Redraw()
  346. def OnErase(self, event):
  347. """!Erase the plot window
  348. """
  349. self.client.Clear()
  350. self.mapwin.ClearLines(self.mapwin.pdc)
  351. self.mapwin.ClearLines(self.mapwin.pdcTmp)
  352. self.mapwin.polycoords = []
  353. self.mapwin.Refresh()
  354. def SaveToFile(self, event):
  355. """!Save plot to graphics file
  356. """
  357. self.client.SaveFile()
  358. def OnMouseLeftDown(self,event):
  359. self.SetStatusText(_("Left Mouse Down at Point:") + \
  360. " (%.4f, %.4f)" % self.client._getXY(event))
  361. event.Skip() # allows plotCanvas OnMouseLeftDown to be called
  362. def OnMotion(self, event):
  363. """!Indicate when mouse is outside the plot area
  364. """
  365. if self.client.OnLeave(event): print 'out of area'
  366. #show closest point (when enbled)
  367. if self.client.GetEnablePointLabel() == True:
  368. #make up dict with info for the pointLabel
  369. #I've decided to mark the closest point on the closest curve
  370. dlst = self.client.GetClosetPoint( self.client._getXY(event), pointScaled = True)
  371. if dlst != []: #returns [] if none
  372. curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
  373. #make up dictionary to pass to my user function (see DrawPointLabel)
  374. mDataDict = {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
  375. "pointXY":pointXY, "scaledXY":scaledXY}
  376. #pass dict to update the pointLabel
  377. self.client.UpdatePointLabel(mDataDict)
  378. event.Skip() #go to next handler
  379. def PlotOptionsMenu(self, event):
  380. """!Popup menu for plot and text options
  381. """
  382. point = wx.GetMousePosition()
  383. popt = wx.Menu()
  384. # Add items to the menu
  385. settext = wx.MenuItem(popt, wx.ID_ANY, _('Text settings'))
  386. popt.AppendItem(settext)
  387. self.Bind(wx.EVT_MENU, self.PlotText, settext)
  388. setgrid = wx.MenuItem(popt, wx.ID_ANY, _('Plot settings'))
  389. popt.AppendItem(setgrid)
  390. self.Bind(wx.EVT_MENU, self.PlotOptions, setgrid)
  391. # Popup the menu. If an item is selected then its handler
  392. # will be called before PopupMenu returns.
  393. self.PopupMenu(popt)
  394. popt.Destroy()
  395. def NotFunctional(self):
  396. """!Creates a 'not functional' message dialog
  397. """
  398. dlg = wx.MessageDialog(parent = self,
  399. message = _('This feature is not yet functional'),
  400. caption = _('Under Construction'),
  401. style = wx.OK | wx.ICON_INFORMATION)
  402. dlg.ShowModal()
  403. dlg.Destroy()
  404. def OnPlotText(self, dlg):
  405. """!Custom text settings for histogram plot.
  406. """
  407. self.ptitle = dlg.ptitle
  408. self.xlabel = dlg.xlabel
  409. self.ylabel = dlg.ylabel
  410. self.client.SetFont(self.properties['font']['wxfont'])
  411. self.client.SetFontSizeTitle(self.properties['font']['prop']['titleSize'])
  412. self.client.SetFontSizeAxis(self.properties['font']['prop']['axisSize'])
  413. if self.plot:
  414. self.plot.setTitle(dlg.ptitle)
  415. self.plot.setXLabel(dlg.xlabel)
  416. self.plot.setYLabel(dlg.ylabel)
  417. self.OnRedraw(event = None)
  418. def PlotText(self, event):
  419. """!Set custom text values for profile title and axis labels.
  420. """
  421. dlg = TextDialog(parent = self, id = wx.ID_ANY,
  422. plottype = self.plottype,
  423. title = _('Text settings'))
  424. btnval = dlg.ShowModal()
  425. if btnval == wx.ID_SAVE or btnval == wx.ID_OK or btnval == wx.ID_CANCEL:
  426. dlg.Destroy()
  427. def PlotOptions(self, event):
  428. """!Set various profile options, including: line width, color,
  429. style; marker size, color, fill, and style; grid and legend
  430. options. Calls OptDialog class.
  431. """
  432. dlg = OptDialog(parent = self, id = wx.ID_ANY,
  433. plottype = self.plottype,
  434. title = _('Plot settings'))
  435. btnval = dlg.ShowModal()
  436. if btnval == wx.ID_SAVE or btnval == wx.ID_OK or btnval == wx.ID_CANCEL:
  437. dlg.Destroy()
  438. def PrintMenu(self, event):
  439. """!Print options and output menu
  440. """
  441. point = wx.GetMousePosition()
  442. printmenu = wx.Menu()
  443. for title, handler in ((_("Page setup"), self.OnPageSetup),
  444. (_("Print preview"), self.OnPrintPreview),
  445. (_("Print display"), self.OnDoPrint)):
  446. item = wx.MenuItem(printmenu, wx.ID_ANY, title)
  447. printmenu.AppendItem(item)
  448. self.Bind(wx.EVT_MENU, handler, item)
  449. # Popup the menu. If an item is selected then its handler
  450. # will be called before PopupMenu returns.
  451. self.PopupMenu(printmenu)
  452. printmenu.Destroy()
  453. def OnPageSetup(self, event):
  454. self.client.PageSetup()
  455. def OnPrintPreview(self, event):
  456. self.client.PrintPreview()
  457. def OnDoPrint(self, event):
  458. self.client.Printout()
  459. def OnQuit(self, event):
  460. self.Close(True)
  461. def OnCloseWindow(self, event):
  462. """!Close plot window and clean up
  463. """
  464. try:
  465. self.mapwin.ClearLines()
  466. self.mapwin.mouse['begin'] = self.mapwin.mouse['end'] = (0.0, 0.0)
  467. self.mapwin.mouse['use'] = 'pointer'
  468. self.mapwin.mouse['box'] = 'point'
  469. self.mapwin.polycoords = []
  470. self.mapwin.UpdateMap(render = False, renderVector = False)
  471. except:
  472. pass
  473. if self.mapwin:
  474. self.mapwin.SetCursor(self.Parent.cursors["default"])
  475. self.Destroy()