histogram.py 18 KB

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