base.py 22 KB

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