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