base.py 22 KB

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