base.py 22 KB

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