histogram.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. """
  2. @package modules.histogram
  3. Plotting histogram based on d.histogram
  4. Classes:
  5. - histogram::BufferedWindow
  6. - histogram::HistogramFrame
  7. - histogram::HistogramToolbar
  8. (C) 2007, 2010-2011 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Michael Barton
  12. @author Various updates by Martin Landa <landa.martin gmail.com>
  13. """
  14. import os
  15. import sys
  16. import wx
  17. from core import globalvar
  18. from core.render import Map
  19. from gui_core.forms import GUI
  20. from mapdisp.gprint import PrintOptions
  21. from core.utils import GetLayerNameFromCmd, _
  22. from gui_core.dialogs import GetImageHandlers, ImageSizeDialog
  23. from gui_core.preferences import DefaultFontDialog
  24. from core.debug import Debug
  25. from core.gcmd import GError
  26. from gui_core.toolbars import BaseToolbar, BaseIcons
  27. class BufferedWindow(wx.Window):
  28. """A Buffered window class.
  29. When the drawing needs to change, you app needs to call the
  30. UpdateHist() method. Since the drawing is stored in a bitmap, you
  31. can also save the drawing to file by calling the
  32. SaveToFile(self,file_name,file_type) method.
  33. """
  34. def __init__(self, parent, id=wx.ID_ANY,
  35. style=wx.NO_FULL_REPAINT_ON_RESIZE,
  36. Map=None, **kwargs):
  37. wx.Window.__init__(self, parent, id=id, style=style, **kwargs)
  38. self.parent = parent
  39. self.Map = Map
  40. self.mapname = self.parent.mapname
  41. #
  42. # Flags
  43. #
  44. self.render = True # re-render the map from GRASS or just redraw image
  45. self.resize = False # indicates whether or not a resize event has taken place
  46. self.dragimg = None # initialize variable for map panning
  47. self.pen = None # pen for drawing zoom boxes, etc.
  48. self._oldfont = self._oldencoding = None
  49. #
  50. # Event bindings
  51. #
  52. self.Bind(wx.EVT_PAINT, self.OnPaint)
  53. self.Bind(wx.EVT_SIZE, self.OnSize)
  54. self.Bind(wx.EVT_IDLE, self.OnIdle)
  55. #
  56. # Render output objects
  57. #
  58. self.mapfile = None # image file to be rendered
  59. self.img = None # wx.Image object (self.mapfile)
  60. self.imagedict = {} # images and their PseudoDC ID's for painting and dragging
  61. self.pdc = wx.PseudoDC()
  62. # will store an off screen empty bitmap for saving to file
  63. self._buffer = wx.EmptyBitmap(
  64. max(1, self.Map.width),
  65. max(1, self.Map.height))
  66. # make sure that extents are updated at init
  67. self.Map.region = self.Map.GetRegion()
  68. self.Map.SetRegion()
  69. self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
  70. def Draw(self, pdc, img=None, drawid=None,
  71. pdctype='image', coords=[0, 0, 0, 0]):
  72. """Draws histogram or clears window
  73. """
  74. if drawid is None:
  75. if pdctype == 'image':
  76. drawid = imagedict[img]
  77. elif pdctype == 'clear':
  78. drawid is None
  79. else:
  80. drawid = wx.NewId()
  81. else:
  82. pdc.SetId(drawid)
  83. pdc.BeginDrawing()
  84. Debug.msg(
  85. 3, "BufferedWindow.Draw(): id=%s, pdctype=%s, coord=%s" %
  86. (drawid, pdctype, coords))
  87. if pdctype == 'clear': # erase the display
  88. bg = wx.WHITE_BRUSH
  89. pdc.SetBackground(bg)
  90. pdc.Clear()
  91. self.Refresh()
  92. pdc.EndDrawing()
  93. return
  94. if pdctype == 'image':
  95. bg = wx.TRANSPARENT_BRUSH
  96. pdc.SetBackground(bg)
  97. bitmap = wx.BitmapFromImage(img)
  98. w, h = bitmap.GetSize()
  99. pdc.DrawBitmap(
  100. bitmap, coords[0],
  101. coords[1],
  102. True) # draw the composite map
  103. pdc.SetIdBounds(drawid, (coords[0], coords[1], w, h))
  104. pdc.EndDrawing()
  105. self.Refresh()
  106. def OnPaint(self, event):
  107. """Draw psuedo DC to buffer
  108. """
  109. dc = wx.BufferedPaintDC(self, self._buffer)
  110. # use PrepareDC to set position correctly
  111. # probably does nothing, removed from wxPython 2.9
  112. # self.PrepareDC(dc)
  113. # we need to clear the dc BEFORE calling PrepareDC
  114. bg = wx.Brush(self.GetBackgroundColour())
  115. dc.SetBackground(bg)
  116. dc.Clear()
  117. # create a clipping rect from our position and size
  118. # and the Update Region
  119. rgn = self.GetUpdateRegion()
  120. r = rgn.GetBox()
  121. # draw to the dc using the calculated clipping rect
  122. self.pdc.DrawToDCClipped(dc, r)
  123. def OnSize(self, event):
  124. """Init image size to match window size
  125. """
  126. # set size of the input image
  127. self.Map.width, self.Map.height = self.GetClientSize()
  128. # Make new off screen bitmap: this bitmap will always have the
  129. # current drawing in it, so it can be used to save the image to
  130. # a file, or whatever.
  131. self._buffer = wx.EmptyBitmap(self.Map.width, self.Map.height)
  132. # get the image to be rendered
  133. self.img = self.GetImage()
  134. # update map display
  135. if self.img and self.Map.width + self.Map.height > 0: # scale image during resize
  136. self.img = self.img.Scale(self.Map.width, self.Map.height)
  137. self.render = False
  138. self.UpdateHist()
  139. # re-render image on idle
  140. self.resize = True
  141. def OnIdle(self, event):
  142. """Only re-render a histogram image from GRASS during idle
  143. time instead of multiple times during resizing.
  144. """
  145. if self.resize:
  146. self.render = True
  147. self.UpdateHist()
  148. event.Skip()
  149. def SaveToFile(self, FileName, FileType, width, height):
  150. """This will save the contents of the buffer to the specified
  151. file. See the wx.Windows docs for wx.Bitmap::SaveFile for the
  152. details
  153. """
  154. busy = wx.BusyInfo(message=_("Please wait, exporting image..."),
  155. parent=self)
  156. wx.Yield()
  157. self.Map.ChangeMapSize((width, height))
  158. ibuffer = wx.EmptyBitmap(max(1, width), max(1, height))
  159. self.Map.Render(force=True, windres=True)
  160. img = self.GetImage()
  161. self.Draw(self.pdc, img, drawid=99)
  162. dc = wx.BufferedDC(None, ibuffer)
  163. dc.Clear()
  164. # probably does nothing, removed from wxPython 2.9
  165. # self.PrepareDC(dc)
  166. self.pdc.DrawToDC(dc)
  167. ibuffer.SaveFile(FileName, FileType)
  168. busy.Destroy()
  169. def GetImage(self):
  170. """Converts files to wx.Image
  171. """
  172. if self.Map.mapfile and os.path.isfile(self.Map.mapfile) and \
  173. os.path.getsize(self.Map.mapfile):
  174. img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY)
  175. else:
  176. img = None
  177. self.imagedict[img] = 99 # set image PeudoDC ID
  178. return img
  179. def UpdateHist(self, img=None):
  180. """Update canvas if histogram options changes or window
  181. changes geometry
  182. """
  183. Debug.msg(
  184. 2, "BufferedWindow.UpdateHist(%s): render=%s" %
  185. (img, self.render))
  186. if not self.render:
  187. return
  188. # render new map images
  189. # set default font and encoding environmental variables
  190. if "GRASS_FONT" in os.environ:
  191. self._oldfont = os.environ["GRASS_FONT"]
  192. if self.parent.font:
  193. os.environ["GRASS_FONT"] = self.parent.font
  194. if "GRASS_ENCODING" in os.environ:
  195. self._oldencoding = os.environ["GRASS_ENCODING"]
  196. if self.parent.encoding is not None and self.parent.encoding != "ISO-8859-1":
  197. os.environ[GRASS_ENCODING] = self.parent.encoding
  198. # using active comp region
  199. self.Map.GetRegion(update=True)
  200. self.Map.width, self.Map.height = self.GetClientSize()
  201. self.mapfile = self.Map.Render(force=self.render)
  202. self.Map.GetRenderMgr().renderDone.connect(self.UpdateHistDone)
  203. def UpdateHistDone(self):
  204. """Histogram image generated, finish rendering."""
  205. self.img = self.GetImage()
  206. self.resize = False
  207. if not self.img:
  208. return
  209. try:
  210. id = self.imagedict[self.img]
  211. except:
  212. return
  213. # paint images to PseudoDC
  214. self.pdc.Clear()
  215. self.pdc.RemoveAll()
  216. self.Draw(self.pdc, self.img, drawid=id) # draw map image background
  217. self.resize = False
  218. # update statusbar
  219. self.Map.SetRegion()
  220. self.parent.statusbar.SetStatusText(
  221. "Image/Raster map <%s>" %
  222. self.parent.mapname)
  223. # set default font and encoding environmental variables
  224. if self._oldfont:
  225. os.environ["GRASS_FONT"] = self._oldfont
  226. if self._oldencoding:
  227. os.environ["GRASS_ENCODING"] = self._oldencoding
  228. def EraseMap(self):
  229. """Erase the map display
  230. """
  231. self.Draw(self.pdc, pdctype='clear')
  232. class HistogramFrame(wx.Frame):
  233. """Main frame for hisgram display window. Uses d.histogram
  234. rendered onto canvas
  235. """
  236. def __init__(self, parent, giface, id=wx.ID_ANY,
  237. title=_("GRASS GIS Histogramming Tool (d.histogram)"),
  238. size=wx.Size(500, 350),
  239. style=wx.DEFAULT_FRAME_STYLE, **kwargs):
  240. wx.Frame.__init__(
  241. self,
  242. parent,
  243. id,
  244. title,
  245. size=size,
  246. style=style,
  247. **kwargs)
  248. self.SetIcon(
  249. wx.Icon(
  250. os.path.join(
  251. globalvar.ICONDIR,
  252. 'grass.ico'),
  253. wx.BITMAP_TYPE_ICO))
  254. self._giface = giface
  255. self.Map = Map() # instance of render.Map to be associated with display
  256. self.layer = None # reference to layer with histogram
  257. # Init variables
  258. self.params = {} # previously set histogram parameters
  259. self.propwin = '' # ID of properties dialog
  260. self.font = ""
  261. self.encoding = 'ISO-8859-1' # default encoding for display fonts
  262. self.toolbar = HistogramToolbar(parent=self)
  263. # workaround for http://trac.wxwidgets.org/ticket/13888
  264. if sys.platform != 'darwin':
  265. self.SetToolBar(self.toolbar)
  266. # find selected map
  267. # might by moved outside this class
  268. # setting to None but honestly we do not handle no map case
  269. # TODO: when self.mapname is None content of map window is showed
  270. self.mapname = None
  271. layers = self._giface.GetLayerList().GetSelectedLayers(checkedOnly=False)
  272. if len(layers) > 0:
  273. self.mapname = layers[0].maplayer.name
  274. # Add statusbar
  275. self.statusbar = self.CreateStatusBar(number=1, style=0)
  276. # self.statusbar.SetStatusWidths([-2, -1])
  277. hist_frame_statusbar_fields = ["Histogramming %s" % self.mapname]
  278. for i in range(len(hist_frame_statusbar_fields)):
  279. self.statusbar.SetStatusText(hist_frame_statusbar_fields[i], i)
  280. # Init map display
  281. self.InitDisplay() # initialize region values
  282. # initialize buffered DC
  283. self.HistWindow = BufferedWindow(
  284. self, id=wx.ID_ANY, Map=self.Map) # initialize buffered DC
  285. # Bind various events
  286. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  287. # Init print module and classes
  288. self.printopt = PrintOptions(self, self.HistWindow)
  289. # Add layer to the map
  290. self.layer = self.Map.AddLayer(
  291. ltype="command",
  292. name='histogram',
  293. command=[
  294. ['d.histogram']],
  295. active=False,
  296. hidden=False,
  297. opacity=1,
  298. render=False)
  299. if self.mapname:
  300. self.SetHistLayer(self.mapname, None)
  301. else:
  302. self.OnErase(None)
  303. wx.CallAfter(self.OnOptions, None)
  304. def InitDisplay(self):
  305. """Initialize histogram display, set dimensions and region
  306. """
  307. self.width, self.height = self.GetClientSize()
  308. self.Map.geom = self.width, self.height
  309. def OnOptions(self, event):
  310. """Change histogram settings"""
  311. cmd = ['d.histogram']
  312. if self.mapname != '':
  313. cmd.append('map=%s' % self.mapname)
  314. module = GUI(parent=self)
  315. module.ParseCommand(
  316. cmd,
  317. completed=(
  318. self.GetOptData,
  319. None,
  320. self.params))
  321. def GetOptData(self, dcmd, layer, params, propwin):
  322. """Callback method for histogram command generated by dialog
  323. created in menuform.py
  324. """
  325. if dcmd:
  326. name, found = GetLayerNameFromCmd(dcmd, fullyQualified=True,
  327. layerType='raster')
  328. if not found:
  329. GError(parent=propwin,
  330. message=_("Raster map <%s> not found") % name)
  331. return
  332. self.SetHistLayer(name, dcmd)
  333. self.params = params
  334. self.propwin = propwin
  335. self.HistWindow.UpdateHist()
  336. def SetHistLayer(self, name, cmd=None):
  337. """Set histogram layer
  338. """
  339. self.mapname = name
  340. if not cmd:
  341. cmd = ['d.histogram', ('map=%s' % self.mapname)]
  342. self.layer = self.Map.ChangeLayer(layer=self.layer,
  343. command=[cmd],
  344. active=True)
  345. return self.layer
  346. def SetHistFont(self, event):
  347. """Set font for histogram. If not set, font will be default
  348. display font.
  349. """
  350. dlg = DefaultFontDialog(parent=self, id=wx.ID_ANY,
  351. title=_('Select font for histogram text'))
  352. dlg.fontlb.SetStringSelection(self.font, True)
  353. if dlg.ShowModal() == wx.ID_CANCEL:
  354. dlg.Destroy()
  355. return
  356. # set default font type, font, and encoding to whatever selected in
  357. # dialog
  358. if dlg.font is not None:
  359. self.font = dlg.font
  360. if dlg.encoding is not None:
  361. self.encoding = dlg.encoding
  362. dlg.Destroy()
  363. self.HistWindow.UpdateHist()
  364. def OnErase(self, event):
  365. """Erase the histogram display
  366. """
  367. self.HistWindow.Draw(self.HistWindow.pdc, pdctype='clear')
  368. def OnRender(self, event):
  369. """Re-render histogram
  370. """
  371. self.HistWindow.UpdateHist()
  372. def GetWindow(self):
  373. """Get buffered window"""
  374. return self.HistWindow
  375. def SaveToFile(self, event):
  376. """Save to file
  377. """
  378. filetype, ltype = GetImageHandlers(self.HistWindow.img)
  379. # get size
  380. dlg = ImageSizeDialog(self)
  381. dlg.CentreOnParent()
  382. if dlg.ShowModal() != wx.ID_OK:
  383. dlg.Destroy()
  384. return
  385. width, height = dlg.GetValues()
  386. dlg.Destroy()
  387. # get filename
  388. dlg = wx.FileDialog(parent=self,
  389. message=_("Choose a file name to save the image "
  390. "(no need to add extension)"),
  391. wildcard=filetype,
  392. style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
  393. if dlg.ShowModal() == wx.ID_OK:
  394. path = dlg.GetPath()
  395. if not path:
  396. dlg.Destroy()
  397. return
  398. base, ext = os.path.splitext(path)
  399. fileType = ltype[dlg.GetFilterIndex()]['type']
  400. extType = ltype[dlg.GetFilterIndex()]['ext']
  401. if ext != extType:
  402. path = base + '.' + extType
  403. self.HistWindow.SaveToFile(path, fileType,
  404. width, height)
  405. self.HistWindow.UpdateHist()
  406. dlg.Destroy()
  407. def PrintMenu(self, event):
  408. """Print options and output menu
  409. """
  410. point = wx.GetMousePosition()
  411. printmenu = wx.Menu()
  412. # Add items to the menu
  413. setup = wx.MenuItem(printmenu, id=wx.ID_ANY, text=_('Page setup'))
  414. printmenu.AppendItem(setup)
  415. self.Bind(wx.EVT_MENU, self.printopt.OnPageSetup, setup)
  416. preview = wx.MenuItem(printmenu, id=wx.ID_ANY, text=_('Print preview'))
  417. printmenu.AppendItem(preview)
  418. self.Bind(wx.EVT_MENU, self.printopt.OnPrintPreview, preview)
  419. doprint = wx.MenuItem(printmenu, id=wx.ID_ANY, text=_('Print display'))
  420. printmenu.AppendItem(doprint)
  421. self.Bind(wx.EVT_MENU, self.printopt.OnDoPrint, doprint)
  422. # Popup the menu. If an item is selected then its handler
  423. # will be called before PopupMenu returns.
  424. self.PopupMenu(printmenu)
  425. printmenu.Destroy()
  426. def OnQuit(self, event):
  427. self.Close(True)
  428. def OnCloseWindow(self, event):
  429. """Window closed
  430. Also remove associated rendered images
  431. """
  432. try:
  433. self.propwin.Close(True)
  434. except:
  435. pass
  436. self.Map.Clean()
  437. self.Destroy()
  438. class HistogramToolbar(BaseToolbar):
  439. """Histogram toolbar (see histogram.py)
  440. """
  441. def __init__(self, parent):
  442. BaseToolbar.__init__(self, parent)
  443. # workaround for http://trac.wxwidgets.org/ticket/13888
  444. if sys.platform == 'darwin':
  445. parent.SetToolBar(self)
  446. self.InitToolbar(self._toolbarData())
  447. # realize the toolbar
  448. self.Realize()
  449. def _toolbarData(self):
  450. """Toolbar data"""
  451. return self._getToolbarData((('histogram', BaseIcons["histogramD"],
  452. self.parent.OnOptions),
  453. ('render', BaseIcons["display"],
  454. self.parent.OnRender),
  455. ('erase', BaseIcons["erase"],
  456. self.parent.OnErase),
  457. ('font', BaseIcons["font"],
  458. self.parent.SetHistFont),
  459. (None, ),
  460. ('save', BaseIcons["saveFile"],
  461. self.parent.SaveToFile),
  462. ('hprint', BaseIcons["print"],
  463. self.parent.PrintMenu),
  464. (None, ),
  465. ('quit', BaseIcons["quit"],
  466. self.parent.OnQuit))
  467. )