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