1"""
2@package module.colorrules
3
4@brief Dialog for interactive management of raster/vector color tables
5and color rules.
6
7Classes:
8 - colorrules::RulesPanel
9 - colorrules::ColorTable
10 - colorrules::RasterColorTable
11 - colorrules::VectorColorTable
12 - colorrules::ThematicVectorTable
13 - colorrules::BufferedWindow
14
15(C) 2008-2015 by the GRASS Development Team
16
17This program is free software under the GNU General Public License
18(>=v2). Read the file COPYING that comes with GRASS for details.
19
20@author Michael Barton (Arizona State University)
21@author Martin Landa <landa.martin gmail.com> (various updates, pre-defined color table)
22@author Anna Kratochvilova <kratochanna gmail.com> (split to base and derived classes)
23"""
24
25import os
26import shutil
27import copy
28import tempfile
29import six
30
31import wx
32import wx.lib.colourselect as csel
33import wx.lib.scrolledpanel as scrolled
34import wx.lib.filebrowsebutton as filebrowse
35
36import grass.script as grass
37from grass.script.task import cmdlist_to_tuple
38
39from core import globalvar
40from core import utils
41from core.gcmd import GMessage, RunCommand, GError
42from gui_core.gselect import Select, LayerSelect, ColumnSelect, VectorDBInfo
43from core.render import Map
44from gui_core.forms import GUI
45from core.debug import Debug as Debug
46from core.settings import UserSettings
47from gui_core.widgets import ColorTablesComboBox
48from gui_core.wrap import SpinCtrl, PseudoDC, TextCtrl, Button, CancelButton, \
49     StaticText, StaticBox, EmptyBitmap, BitmapFromImage
50
51
52class RulesPanel:
53
54    def __init__(self, parent, mapType, attributeType,
55                 properties, panelWidth=180):
56        """Create rules panel
57
58        :param mapType: raster/vector
59        :param attributeType: color/size for choosing widget type
60        :param properties: properties of classes derived from ColorTable
61        :param panelWidth: width of scroll panel"""
62
63        self.ruleslines = {}
64        self.mapType = mapType
65        self.attributeType = attributeType
66        self.properties = properties
67        self.parent = parent
68        self.panelWidth = panelWidth
69
70        self.mainSizer = wx.FlexGridSizer(cols=3, vgap=6, hgap=4)
71        # put small border at the top of panel
72        for i in range(3):
73            self.mainSizer.Add(wx.Size(3, 3))
74
75        self.mainPanel = scrolled.ScrolledPanel(
76            parent, id=wx.ID_ANY, size=(self.panelWidth, 300),
77            style=wx.TAB_TRAVERSAL | wx.SUNKEN_BORDER)
78
79        # (un)check all
80        self.checkAll = wx.CheckBox(parent, id=wx.ID_ANY, label=_("Check all"))
81        self.checkAll.SetValue(True)
82        # clear button
83        self.clearAll = Button(parent, id=wx.ID_ANY, label=_("Clear all"))
84        #  determines how many rules should be added
85        self.numRules = SpinCtrl(parent, id=wx.ID_ANY, min=1, max=1e6,
86                                 initial=1, size=(150, -1))
87        # add rules
88        self.btnAdd = Button(parent, id=wx.ID_ADD)
89
90        self.btnAdd.Bind(wx.EVT_BUTTON, self.OnAddRules)
91        self.checkAll.Bind(wx.EVT_CHECKBOX, self.OnCheckAll)
92        self.clearAll.Bind(wx.EVT_BUTTON, self.OnClearAll)
93
94        self.mainPanel.SetSizer(self.mainSizer)
95        self.mainPanel.SetAutoLayout(True)
96        self.mainPanel.SetupScrolling()
97
98    def Clear(self):
99        """Clear and widgets and delete information"""
100        self.ruleslines.clear()
101        self.mainSizer.Clear(True)
102
103    def OnCheckAll(self, event):
104        """(Un)check all rules"""
105        check = event.GetInt()
106        for child in self.mainPanel.GetChildren():
107            if child.GetName() == 'enable':
108                child.SetValue(check)
109            else:
110                child.Enable(check)
111
112    def OnClearAll(self, event):
113        """Delete all widgets in panel"""
114        self.Clear()
115
116    def OnAddRules(self, event):
117        """Add rules button pressed"""
118        nrules = self.numRules.GetValue()
119        self.AddRules(nrules)
120
121    def AddRules(self, nrules, start=False):
122        """Add rules
123
124        :param start: set widgets (not append)
125        """
126
127        snum = len(self.ruleslines.keys())
128        if start:
129            snum = 0
130        for num in range(snum, snum + nrules):
131            # enable
132            enable = wx.CheckBox(parent=self.mainPanel, id=num)
133            enable.SetValue(True)
134            enable.SetName('enable')
135            enable.Bind(wx.EVT_CHECKBOX, self.OnRuleEnable)
136            # value
137            txt_ctrl = TextCtrl(parent=self.mainPanel, id=1000 + num,
138                                   size=(80, -1),
139                                   style=wx.TE_NOHIDESEL)
140            if self.mapType == 'vector':
141                txt_ctrl.SetToolTip(_("Enter vector attribute values"))
142            txt_ctrl.Bind(wx.EVT_TEXT, self.OnRuleValue)
143            txt_ctrl.SetName('source')
144            if self.attributeType == 'color':
145                # color
146                columnCtrl = csel.ColourSelect(
147                    self.mainPanel, id=2000 + num,
148                    size=globalvar.DIALOG_COLOR_SIZE)
149                columnCtrl.Bind(csel.EVT_COLOURSELECT, self.OnRuleColor)
150                columnCtrl.SetName('target')
151                if not start:
152                    self.ruleslines[enable.GetId()] = {'value': '',
153                                                       'color': "0:0:0"}
154            else:
155                # size or width
156                init = 2
157                if self.attributeType == 'size':
158                    init = 100
159                columnCtrl = SpinCtrl(self.mainPanel, id=2000 + num,
160                                      size=(50, -1), min=1, max=1e4,
161                                      initial=init)
162                columnCtrl.Bind(wx.EVT_SPINCTRL, self.OnRuleSize)
163                columnCtrl.Bind(wx.EVT_TEXT, self.OnRuleSize)
164                columnCtrl.SetName('target')
165                if not start:
166                    self.ruleslines[
167                        enable.GetId()] = {
168                        'value': '',
169                        self.attributeType: init}
170
171            self.mainSizer.Add(enable, proportion=0,
172                               flag=wx.ALIGN_CENTER_VERTICAL)
173            self.mainSizer.Add(txt_ctrl, proportion=0,
174                               flag=wx.ALIGN_CENTER | wx.RIGHT, border=5)
175            self.mainSizer.Add(columnCtrl, proportion=0,
176                               flag=wx.ALIGN_CENTER | wx.RIGHT, border=10)
177
178        self.mainPanel.Layout()
179        self.mainPanel.SetupScrolling(scroll_x=False)
180
181    def OnRuleEnable(self, event):
182        """Rule enabled/disabled"""
183        id = event.GetId()
184
185        if event.IsChecked():
186            self.mainPanel.FindWindowById(id + 1000).Enable()
187            self.mainPanel.FindWindowById(id + 2000).Enable()
188            if self.mapType == 'vector' and not self.parent.GetParent().colorTable:
189                vals = []
190                vals.append(
191                    self.mainPanel.FindWindowById(
192                        id + 1000).GetValue())
193                try:
194                    vals.append(
195                        self.mainPanel.FindWindowById(
196                            id + 1 + 1000).GetValue())
197                except AttributeError:
198                    vals.append(None)
199                value = self.SQLConvert(vals)
200            else:
201                value = self.mainPanel.FindWindowById(id + 1000).GetValue()
202            color = self.mainPanel.FindWindowById(id + 2000).GetValue()
203
204            if self.attributeType == 'color':
205                # color
206                color_str = str(color[0]) + ':' \
207                    + str(color[1]) + ':' \
208                    + str(color[2])
209                self.ruleslines[id] = {'value': value,
210                                       'color': color_str}
211
212            else:
213                # size or width
214                self.ruleslines[id] = {'value': value,
215                                       self.attributeType: float(color)}
216
217        else:
218            self.mainPanel.FindWindowById(id + 1000).Disable()
219            self.mainPanel.FindWindowById(id + 2000).Disable()
220            del self.ruleslines[id]
221
222    def OnRuleColor(self, event):
223        """Rule color changed"""
224        num = event.GetId()
225
226        rgba_color = event.GetValue()
227
228        rgb_string = str(rgba_color[0]) + ':' \
229            + str(rgba_color[1]) + ':' \
230            + str(rgba_color[2])
231
232        self.ruleslines[num - 2000]['color'] = rgb_string
233
234    def OnRuleSize(self, event):
235        """Rule size changed"""
236        num = event.GetId()
237        size = event.GetInt()
238
239        self.ruleslines[num - 2000][self.attributeType] = size
240
241    def OnRuleValue(self, event):
242        """Rule value changed"""
243        num = event.GetId()
244        val = event.GetString().strip()
245
246        if val == '':
247            return
248        try:
249            table = self.parent.colorTable
250        except AttributeError:
251            # due to panel/scrollpanel in vector dialog
252            if isinstance(self.parent.GetParent(), RasterColorTable):
253                table = self.parent.GetParent().colorTable
254            else:
255                table = self.parent.GetParent().GetParent().colorTable
256        if table:
257            self.SetRasterRule(num, val)
258        else:
259            self.SetVectorRule(num, val)
260
261    def SetRasterRule(self, num, val):
262        """Set raster rule"""
263        self.ruleslines[num - 1000]['value'] = val
264
265    def SetVectorRule(self, num, val):
266        """Set vector rule"""
267        vals = []
268        vals.append(val)
269        try:
270            vals.append(self.mainPanel.FindWindowById(num + 1).GetValue())
271        except AttributeError:
272            vals.append(None)
273        self.ruleslines[num - 1000]['value'] = self.SQLConvert(vals)
274
275    def Enable(self, enable=True):
276        """Enable/Disable all widgets"""
277        for child in self.mainPanel.GetChildren():
278            child.Enable(enable)
279        sql = True
280        self.LoadRulesline(sql)  # todo
281        self.btnAdd.Enable(enable)
282        self.numRules.Enable(enable)
283        self.checkAll.Enable(enable)
284        self.clearAll.Enable(enable)
285
286    def LoadRules(self):
287        message = ""
288        for item in range(len(self.ruleslines)):
289            try:
290                self.mainPanel.FindWindowById(
291                    item +
292                    1000).SetValue(
293                    self.ruleslines[item]['value'])
294                r, g, b = (0, 0, 0)  # default
295                if not self.ruleslines[item][self.attributeType]:
296                    if self.attributeType == 'color':
297                        self.ruleslines[item][
298                            self.attributeType] = '%d:%d:%d' % (
299                            r, g, b)
300                    elif self.attributeType == 'size':
301                        self.ruleslines[item][self.attributeType] = 100
302                    elif self.attributeType == 'width':
303                        self.ruleslines[item][self.attributeType] = 2
304
305                if self.attributeType == 'color':
306                    try:
307                        r, g, b = map(
308                            int, self.ruleslines[item][
309                                self.attributeType].split(':'))
310                    except ValueError as e:
311                        message = _(
312                            "Bad color format. Use color format '0:0:0'")
313                    self.mainPanel.FindWindowById(
314                        item + 2000).SetValue((r, g, b))
315                else:
316                    value = float(self.ruleslines[item][self.attributeType])
317                    self.mainPanel.FindWindowById(item + 2000).SetValue(value)
318            except:
319                continue
320
321        if message:
322            GMessage(parent=self.parent, message=message)
323            return False
324
325        return True
326
327    def SQLConvert(self, vals):
328        """Prepare value for SQL query"""
329        if vals[0].isdigit():
330            sqlrule = '%s=%s' % (self.properties['sourceColumn'], vals[0])
331            if vals[1]:
332                sqlrule += ' AND %s<%s' % (
333                    self.properties['sourceColumn'], vals[1])
334        else:
335            sqlrule = '%s=%s' % (self.properties['sourceColumn'], vals[0])
336
337        return sqlrule
338
339
340class ColorTable(wx.Frame):
341
342    def __init__(self, parent, title, layerTree=None, id=wx.ID_ANY,
343                 style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER,
344                 **kwargs):
345        """Dialog for interactively entering rules for map management
346        commands
347
348        :param raster: True to raster otherwise vector
349        :param nviz: True if ColorTable is called from nviz thematic mapping
350        """
351        self.parent = parent        # GMFrame ?
352        self.layerTree = layerTree  # LayerTree or None
353
354        wx.Frame.__init__(self, parent, id, title, style=style, **kwargs)
355
356        self.SetIcon(
357            wx.Icon(
358                os.path.join(
359                    globalvar.ICONDIR,
360                    'grass.ico'),
361                wx.BITMAP_TYPE_ICO))
362
363        self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
364
365        # instance of render.Map to be associated with display
366        self.Map = Map()
367
368        # input map to change
369        self.inmap = ''
370
371        # reference to layer with preview
372        self.layer = None
373
374        # layout
375        self._doLayout()
376
377        # bindings
378        self.Bind(wx.EVT_BUTTON, self.OnHelp, self.btnHelp)
379        self.selectionInput.Bind(wx.EVT_TEXT, self.OnSelectionInput)
380        self.Bind(wx.EVT_BUTTON, self.OnCancel, self.btnCancel)
381        self.Bind(wx.EVT_BUTTON, self.OnApply, self.btnApply)
382        self.Bind(wx.EVT_BUTTON, self.OnOK, self.btnOK)
383        self.Bind(wx.EVT_BUTTON, self.OnLoadDefaultTable, self.btnDefault)
384
385        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
386
387        self.Bind(wx.EVT_BUTTON, self.OnPreview, self.btnPreview)
388
389    def _initLayer(self):
390        """Set initial layer when opening dialog"""
391        # set map layer from layer tree, first selected,
392        # if not the right type, than select another
393        try:
394            sel = self.layerTree.layer_selected
395            if sel and self.layerTree.GetLayerInfo(
396                    sel, key='type') == self.mapType:
397                layer = sel
398            else:
399                layer = self.layerTree.FindItemByData(
400                    key='type', value=self.mapType)
401        except:
402            layer = None
403        if layer:
404            mapLayer = self.layerTree.GetLayerInfo(layer, key='maplayer')
405            name = mapLayer.GetName()
406            type = mapLayer.GetType()
407            self.selectionInput.SetValue(name)
408            self.inmap = name
409
410    def _createMapSelection(self, parent):
411        """Create map selection part of dialog"""
412        # top controls
413        if self.mapType == 'raster':
414            maplabel = _('Select raster map:')
415        else:
416            maplabel = _('Select vector map:')
417        inputBox = StaticBox(parent, id=wx.ID_ANY,
418                             label=" %s " % maplabel)
419        inputSizer = wx.StaticBoxSizer(inputBox, wx.VERTICAL)
420
421        self.selectionInput = Select(parent=parent, id=wx.ID_ANY,
422                                     size=globalvar.DIALOG_GSELECT_SIZE,
423                                     type=self.mapType)
424        # layout
425        inputSizer.Add(
426            self.selectionInput,
427            flag=wx.ALL | wx.EXPAND,
428            border=5)
429
430        return inputSizer
431
432    def _createFileSelection(self, parent):
433        """Create file (open/save rules) selection part of dialog"""
434        inputBox = StaticBox(
435            parent, id=wx.ID_ANY, label=" %s " %
436            _("Import or export color table:"))
437        inputSizer = wx.StaticBoxSizer(inputBox, wx.HORIZONTAL)
438
439        self.loadRules = filebrowse.FileBrowseButton(
440            parent=parent, id=wx.ID_ANY, fileMask='*', labelText='',
441            dialogTitle=_('Choose file to load color table'),
442            buttonText=_('Load'),
443            toolTip=_(
444                "Type filename or click to choose "
445                "file and load color table"),
446            startDirectory=os.getcwd(),
447            fileMode=wx.FD_OPEN, changeCallback=self.OnLoadRulesFile)
448        self.saveRules = filebrowse.FileBrowseButton(
449            parent=parent, id=wx.ID_ANY, fileMask='*', labelText='',
450            dialogTitle=_('Choose file to save color table'),
451            toolTip=_(
452                "Type filename or click to choose "
453                "file and save color table"),
454            buttonText=_('Save'),
455            startDirectory=os.getcwd(),
456            fileMode=wx.FD_SAVE, changeCallback=self.OnSaveRulesFile)
457
458        colorTable = ColorTablesComboBox(
459            parent=parent,
460            size=globalvar.DIALOG_COMBOBOX_SIZE,
461            choices=utils.GetColorTables(),
462            name="colorTableChoice")
463        self.btnSet = Button(
464            parent=parent,
465            id=wx.ID_ANY,
466            label=_("&Set"),
467            name='btnSet')
468        self.btnSet.Bind(wx.EVT_BUTTON, self.OnSetTable)
469        self.btnSet.Enable(False)
470
471        # layout
472        gridSizer = wx.GridBagSizer(hgap=2, vgap=2)
473
474        gridSizer.Add(StaticText(parent, label=_("Load color table:")),
475                      pos=(0, 0), flag=wx.ALIGN_CENTER_VERTICAL)
476        gridSizer.Add(colorTable, pos=(0, 1))
477        gridSizer.Add(self.btnSet, pos=(0, 2), flag=wx.ALIGN_RIGHT)
478        gridSizer.Add(
479            StaticText(
480                parent, label=_('Load color table from file:')), pos=(
481                1, 0), flag=wx.ALIGN_CENTER_VERTICAL)
482        gridSizer.Add(
483            self.loadRules, pos=(
484                1, 1), span=(
485                1, 2), flag=wx.EXPAND)
486        gridSizer.Add(
487            StaticText(
488                parent, label=_('Save color table to file:')), pos=(
489                2, 0), flag=wx.ALIGN_CENTER_VERTICAL)
490        gridSizer.Add(
491            self.saveRules, pos=(
492                2, 1), span=(
493                1, 2), flag=wx.EXPAND)
494
495        gridSizer.AddGrowableCol(1)
496        inputSizer.Add(gridSizer, proportion=1, flag=wx.EXPAND | wx.ALL,
497                       border=5)
498
499        if self.mapType == 'vector':
500            # parent is collapsible pane
501            parent.SetSizer(inputSizer)
502
503        return inputSizer
504
505    def _createPreview(self, parent):
506        """Create preview"""
507        # initialize preview display
508        self.InitDisplay()
509        self.preview = BufferedWindow(parent, id=wx.ID_ANY, size=(400, 300),
510                                      Map=self.Map)
511        self.preview.EraseMap()
512
513    def _createButtons(self, parent):
514        """Create buttons for leaving dialog"""
515        self.btnHelp = Button(parent, id=wx.ID_HELP)
516        self.btnCancel = CancelButton(parent)
517        self.btnApply = Button(parent, id=wx.ID_APPLY)
518        self.btnOK = Button(parent, id=wx.ID_OK)
519        self.btnDefault = Button(parent, id=wx.ID_ANY,
520                                 label=_("Reload default table"))
521
522        self.btnOK.SetDefault()
523        self.btnOK.Enable(False)
524        self.btnApply.Enable(False)
525        self.btnDefault.Enable(False)
526
527        # layout
528        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
529        btnSizer.Add(wx.Size(-1, -1), proportion=1)
530        btnSizer.Add(self.btnDefault,
531                     flag=wx.LEFT | wx.RIGHT, border=5)
532        btnSizer.Add(self.btnHelp,
533                     flag=wx.LEFT | wx.RIGHT, border=5)
534        btnSizer.Add(self.btnCancel,
535                     flag=wx.LEFT | wx.RIGHT, border=5)
536        btnSizer.Add(self.btnApply,
537                     flag=wx.LEFT | wx.RIGHT, border=5)
538        btnSizer.Add(self.btnOK,
539                     flag=wx.LEFT | wx.RIGHT, border=5)
540
541        return btnSizer
542
543    def _createBody(self, parent):
544        """Create dialog body consisting of rules and preview"""
545        bodySizer = wx.GridBagSizer(hgap=5, vgap=5)
546
547        row = 0
548        # label with range
549        self.cr_label = StaticText(parent, id=wx.ID_ANY)
550        bodySizer.Add(self.cr_label, pos=(row, 0), span=(1, 3),
551                      flag=wx.ALL, border=5)
552
553        row += 1
554        # color table
555        self.rulesPanel = RulesPanel(
556            parent=parent,
557            mapType=self.mapType,
558            attributeType=self.attributeType,
559            properties=self.properties)
560
561        bodySizer.Add(self.rulesPanel.mainPanel, pos=(row, 0),
562                      span=(1, 2), flag=wx.EXPAND)
563        # add two rules as default
564        self.rulesPanel.AddRules(2)
565
566        # preview window
567        self._createPreview(parent=parent)
568        bodySizer.Add(self.preview, pos=(row, 2),
569                      flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER)
570
571        row += 1
572        # add ckeck all and clear all
573        bodySizer.Add(
574            self.rulesPanel.checkAll,
575            flag=wx.ALIGN_CENTER_VERTICAL,
576            pos=(
577                row,
578                0))
579        bodySizer.Add(self.rulesPanel.clearAll, pos=(row, 1))
580
581        # preview button
582        self.btnPreview = Button(parent, id=wx.ID_ANY,
583                                    label=_("Preview"))
584        bodySizer.Add(self.btnPreview, pos=(row, 2),
585                      flag=wx.ALIGN_RIGHT)
586        self.btnPreview.Enable(False)
587        self.btnPreview.SetToolTip(
588            _("Show preview of map " "(current Map Display extent is used)."))
589
590        row += 1
591        # add rules button and spin to sizer
592        bodySizer.Add(self.rulesPanel.numRules, pos=(row, 0),
593                      flag=wx.ALIGN_CENTER_VERTICAL)
594        bodySizer.Add(self.rulesPanel.btnAdd, pos=(row, 1))
595
596        bodySizer.AddGrowableRow(1)
597        bodySizer.AddGrowableCol(2)
598
599        return bodySizer
600
601    def InitDisplay(self):
602        """Initialize preview display, set dimensions and region
603        """
604        self.width = self.Map.width = 400
605        self.height = self.Map.height = 300
606        self.Map.geom = self.width, self.height
607
608    def OnCloseWindow(self, event):
609        """Window closed
610        """
611        self.OnCancel(event)
612
613    def OnApply(self, event):
614        return self._apply()
615
616    def _apply(self, updatePreview=True):
617        """Apply selected color table
618
619        :return: True on success otherwise False
620        """
621        ret = self.CreateColorTable()
622        if not ret:
623            GMessage(parent=self, message=_("No valid color rules given."))
624        else:
625            # re-render preview and current map window
626            if updatePreview:
627                self.OnPreview(None)
628            display = self.layerTree.GetMapDisplay()
629            if display and display.IsAutoRendered():
630                display.GetWindow().UpdateMap(render=True)
631
632        return ret
633
634    def OnOK(self, event):
635        """Apply selected color table and close the dialog"""
636        if self._apply(updatePreview=False):
637            self.OnCancel(event)
638
639    def OnCancel(self, event):
640        """Do not apply any changes, remove associated
641            rendered images and close the dialog"""
642        self.Map.Clean()
643        self.Destroy()
644
645    def OnSetTable(self, event):
646        """Load pre-defined color table"""
647        ct = self.FindWindowByName("colorTableChoice").GetValue()
648        # save original color table
649        ctOriginal = RunCommand(
650            'r.colors.out',
651            read=True,
652            map=self.inmap,
653            rules='-')
654        # set new color table
655        ret, err = RunCommand('r.colors', map=self.inmap,
656                              color=ct, getErrorMsg=True)
657        if ret != 0:
658            GError(err, parent=self)
659            return
660        ctNew = RunCommand(
661            'r.colors.out',
662            read=True,
663            map=self.inmap,
664            rules='-')
665        # restore original table
666        RunCommand('r.colors', map=self.inmap, rules='-', stdin=ctOriginal)
667        # load color table
668        self.rulesPanel.Clear()
669        self.ReadColorTable(ctable=ctNew)
670
671    def OnSaveRulesFile(self, event):
672        """Save color table to file"""
673        path = event.GetString()
674        if not path:
675            return
676
677        if os.path.exists(path):
678            dlgOw = wx.MessageDialog(
679                parent,
680                message=_(
681                    "File <%s> already already exists. "
682                    "Do you want to overwrite it?") %
683                path,
684                caption=_("Overwrite?"),
685                style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
686            if dlgOw.ShowModal() != wx.ID_YES:
687                return
688
689        rulestxt = ''
690        for rule in six.itervalues(self.rulesPanel.ruleslines):
691            if 'value' not in rule:
692                continue
693            rulestxt += rule['value'] + ' ' + rule['color'] + '\n'
694        if not rulestxt:
695            GMessage(message=_("Nothing to save."),
696                     parent=self)
697            return
698
699        fd = open(path, 'w')
700        fd.write(rulestxt)
701        fd.close()
702
703    def OnLoadRulesFile(self, event):
704        """Load color table from file"""
705        path = event.GetString()
706        if not os.path.exists(path):
707            return
708
709        self.rulesPanel.Clear()
710
711        fd = open(path, 'r')
712        self.ReadColorTable(ctable=fd.read())
713        fd.close()
714
715    def ReadColorTable(self, ctable):
716        """Read color table
717
718        :param table: color table in format coming from r.colors.out"""
719
720        rulesNumber = len(ctable.splitlines())
721        self.rulesPanel.AddRules(rulesNumber)
722
723        minim = maxim = count = 0
724        for line in ctable.splitlines():
725            try:
726                value, color = map(lambda x: x.strip(), line.split(' '))
727            except ValueError:
728                GMessage(parent=self, message=_("Invalid color table format"))
729                self.rulesPanel.Clear()
730                return
731
732            self.rulesPanel.ruleslines[count]['value'] = value
733            self.rulesPanel.ruleslines[count]['color'] = color
734            self.rulesPanel.mainPanel.FindWindowById(
735                count + 1000).SetValue(value)
736            rgb = list()
737            for c in color.split(':'):
738                rgb.append(int(c))
739            self.rulesPanel.mainPanel.FindWindowById(
740                count + 2000).SetColour(rgb)
741            # range
742            try:
743                if float(value) < minim:
744                    minim = float(value)
745                if float(value) > maxim:
746                    maxim = float(value)
747            except ValueError:  # nv, default
748                pass
749            count += 1
750
751        if self.mapType == 'vector':
752            # raster min, max is known from r.info
753            self.properties['min'], self.properties['max'] = minim, maxim
754            self.SetRangeLabel()
755
756        self.OnPreview(tmp=True)
757
758    def OnLoadDefaultTable(self, event):
759        """Load internal color table"""
760        self.LoadTable()
761
762    def LoadTable(self, mapType='raster'):
763        """Load current color table (using `r(v).colors.out`)
764
765        :param mapType: map type (raster or vector)"""
766        self.rulesPanel.Clear()
767
768        if mapType == 'raster':
769            cmd = ['r.colors.out',
770                   'read=True',
771                   'map=%s' % self.inmap,
772                   'rules=-']
773        else:
774            cmd = ['v.colors.out',
775                   'read=True',
776                   'map=%s' % self.inmap,
777                   'rules=-']
778
779            if self.properties['sourceColumn'] and self.properties[
780                    'sourceColumn'] != 'cat':
781                cmd.append('column=%s' % self.properties['sourceColumn'])
782
783        cmd = cmdlist_to_tuple(cmd)
784
785        if self.inmap:
786            ctable = RunCommand(cmd[0], **cmd[1])
787        else:
788            self.OnPreview()
789            return
790
791        self.ReadColorTable(ctable=ctable)
792
793    def CreateColorTable(self, tmp=False):
794        """Creates color table
795
796        :return: True on success
797        :return: False on failure
798        """
799        rulestxt = ''
800
801        for rule in six.itervalues(self.rulesPanel.ruleslines):
802            if 'value' not in rule:  # skip empty rules
803                continue
804
805            if rule['value'] not in ('nv', 'default') and \
806                    rule['value'][-1] != '%' and \
807                    not self._IsNumber(rule['value']):
808                GError(
809                    _("Invalid rule value '%s'. Unable to apply color table.") %
810                    rule['value'], parent=self)
811                return False
812
813            rulestxt += rule['value'] + ' ' + rule['color'] + '\n'
814
815        if not rulestxt:
816            return False
817
818        gtemp = utils.GetTempfile()
819        output = open(gtemp, "w")
820        try:
821            output.write(rulestxt)
822        finally:
823            output.close()
824
825        cmd = ['%s.colors' % self.mapType[0],  # r.colors/v.colors
826               'map=%s' % self.inmap,
827               'rules=%s' % gtemp]
828        if self.mapType == 'vector' and self.properties['sourceColumn'] \
829                and self.properties['sourceColumn'] != 'cat':
830            cmd.append('column=%s' % self.properties['sourceColumn'])
831
832        cmd = cmdlist_to_tuple(cmd)
833        ret = RunCommand(cmd[0], **cmd[1])
834        if ret != 0:
835            return False
836
837        return True
838
839    def DoPreview(self, ltype, cmdlist):
840        """Update preview (based on computational region)"""
841
842        if not self.layer:
843            self.layer = self.Map.AddLayer(
844                ltype=ltype,
845                name='preview',
846                command=cmdlist,
847                active=True,
848                hidden=False,
849                opacity=1.0,
850                render=False)
851        else:
852            self.layer.SetCmd(cmdlist)
853
854        # apply new color table and display preview
855        self.CreateColorTable(tmp=True)
856        self.preview.UpdatePreview()
857
858    def RunHelp(self, cmd):
859        """Show GRASS manual page"""
860        RunCommand('g.manual',
861                   quiet=True,
862                   parent=self,
863                   entry=cmd)
864
865    def SetMap(self, name):
866        """Set map name and update dialog"""
867        self.selectionInput.SetValue(name)
868
869    def _IsNumber(self, s):
870        """Check if 's' is a number"""
871        try:
872            float(s)
873            return True
874        except ValueError:
875            return False
876
877
878class RasterColorTable(ColorTable):
879
880    def __init__(self, parent, **kwargs):
881        """Dialog for interactively entering color rules for raster maps"""
882
883        self.mapType = 'raster'
884        self.attributeType = 'color'
885        self.colorTable = True
886        # raster properties
887        self.properties = {
888            # min cat in raster map
889            'min': None,
890            # max cat in raster map
891            'max': None,
892        }
893
894        ColorTable.__init__(self, parent, title=_(
895            'Create new color table for raster map'), **kwargs)
896
897        self._initLayer()
898        self.Map.GetRenderMgr().renderDone.connect(self._restoreColorTable)
899
900        # self.SetMinSize(self.GetSize())
901        self.SetMinSize((650, 700))
902
903    def _doLayout(self):
904        """Do main layout"""
905        sizer = wx.BoxSizer(wx.VERTICAL)
906        #
907        # map selection
908        #
909        mapSelection = self._createMapSelection(parent=self.panel)
910        sizer.Add(mapSelection, proportion=0,
911                  flag=wx.ALL | wx.EXPAND, border=5)
912        #
913        # manage extern tables
914        #
915        fileSelection = self._createFileSelection(parent=self.panel)
916        sizer.Add(fileSelection, proportion=0,
917                  flag=wx.LEFT | wx.RIGHT | wx.EXPAND, border=5)
918        #
919        # body & preview
920        #
921        bodySizer = self._createBody(parent=self.panel)
922        sizer.Add(bodySizer, proportion=1,
923                  flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, border=5)
924        #
925        # buttons
926        #
927        btnSizer = self._createButtons(parent=self.panel)
928        sizer.Add(wx.StaticLine(parent=self.panel, id=wx.ID_ANY,
929                                style=wx.LI_HORIZONTAL), proportion=0,
930                  flag=wx.EXPAND | wx.ALL, border=5)
931
932        sizer.Add(btnSizer, proportion=0,
933                  flag=wx.ALL | wx.ALIGN_RIGHT, border=5)
934
935        self.panel.SetSizer(sizer)
936        sizer.Layout()
937        sizer.Fit(self.panel)
938        self.Layout()
939
940    def OnSelectionInput(self, event):
941        """Raster map selected"""
942        if event:
943            self.inmap = event.GetString()
944
945        self.loadRules.SetValue('')
946        self.saveRules.SetValue('')
947
948        if self.inmap:
949            if not grass.find_file(name=self.inmap, element='cell')['file']:
950                self.inmap = None
951
952        if not self.inmap:
953            for btn in (self.btnPreview, self.btnOK,
954                        self.btnApply, self.btnDefault, self.btnSet):
955                btn.Enable(False)
956            self.LoadTable()
957            return
958
959        info = grass.raster_info(map=self.inmap)
960
961        if info:
962            self.properties['min'] = info['min']
963            self.properties['max'] = info['max']
964            self.LoadTable()
965        else:
966            self.inmap = ''
967            self.properties['min'] = self.properties['max'] = None
968            for btn in (self.btnPreview, self.btnOK,
969                        self.btnApply, self.btnDefault, self.btnSet):
970                btn.Enable(False)
971            self.preview.EraseMap()
972            self.cr_label.SetLabel(
973                _('Enter raster category values or percents'))
974            return
975
976        if info['datatype'] == 'CELL':
977            mapRange = _('range')
978        else:
979            mapRange = _('fp range')
980        self.cr_label.SetLabel(
981            _('Enter raster category values or percents (%(range)s = %(min)d-%(max)d)') %
982            {
983                'range': mapRange,
984                'min': self.properties['min'],
985                'max': self.properties['max']})
986
987        for btn in (self.btnPreview, self.btnOK,
988                    self.btnApply, self.btnDefault, self.btnSet):
989            btn.Enable()
990
991    def OnPreview(self, tmp=True):
992        """Update preview (based on computational region)"""
993        if not self.inmap:
994            self.preview.EraseMap()
995            return
996
997        cmdlist = ['d.rast',
998                   'map=%s' % self.inmap]
999        ltype = 'raster'
1000
1001        # find existing color table and copy to temp file
1002        try:
1003            name, mapset = self.inmap.split('@')
1004        except ValueError:
1005            name = self.inmap
1006            mapset = grass.find_file(self.inmap, element='cell')['mapset']
1007            if not mapset:
1008                return
1009        self._tmp = tmp
1010        self._old_colrtable = None
1011        if mapset == grass.gisenv()['MAPSET']:
1012            self._old_colrtable = grass.find_file(
1013                name=name, element='colr')['file']
1014        else:
1015            self._old_colrtable = grass.find_file(
1016                name=name, element='colr2/' + mapset)['file']
1017
1018        if self._old_colrtable:
1019            self._colrtemp = utils.GetTempfile()
1020            shutil.copyfile(self._old_colrtable, self._colrtemp)
1021
1022        ColorTable.DoPreview(self, ltype, cmdlist)
1023
1024    def _restoreColorTable(self):
1025        # restore previous color table
1026        if self._tmp:
1027            if self._old_colrtable:
1028                shutil.copyfile(self._colrtemp, self._old_colrtable)
1029                os.remove(self._colrtemp)
1030                del self._colrtemp, self._old_colrtable
1031            else:
1032                RunCommand('r.colors',
1033                           parent=self,
1034                           flags='r',
1035                           map=self.inmap)
1036            del self._tmp
1037
1038    def OnHelp(self, event):
1039        """Show GRASS manual page"""
1040        cmd = 'r.colors'
1041        ColorTable.RunHelp(self, cmd=cmd)
1042
1043
1044class VectorColorTable(ColorTable):
1045
1046    def __init__(self, parent, attributeType, **kwargs):
1047        """Dialog for interactively entering color rules for vector maps"""
1048        # dialog attributes
1049        self.mapType = 'vector'
1050        self.attributeType = attributeType  # color, size, width
1051        # in version 7 v.colors used, otherwise color column only
1052        self.version7 = int(grass.version()['version'].split('.')[0]) >= 7
1053        self.colorTable = False
1054        self.updateColumn = True
1055        # vector properties
1056        self.properties = {
1057            # vector layer for attribute table to use for setting color
1058            'layer': 1,
1059            # vector attribute table used for setting color
1060            'table': '',
1061            # vector attribute column for assigning colors
1062            'sourceColumn': '',
1063            # vector attribute column to use for loading colors
1064            'loadColumn': '',
1065            # vector attribute column to use for storing colors
1066            'storeColumn': '',
1067            # vector attribute column for temporary storing colors
1068            'tmpColumn': 'tmp_0',
1069            # min value of attribute column/vector color table
1070            'min': None,
1071            # max value of attribute column/vector color table
1072            'max': None
1073        }
1074        self.columnsProp = {
1075            'color': {
1076                'name': 'GRASSRGB',
1077                'type1': 'varchar(11)',
1078                'type2': ['character']},
1079            'size': {
1080                'name': 'GRASSSIZE',
1081                'type1': 'integer',
1082                'type2': ['integer']},
1083            'width': {
1084                'name': 'GRASSWIDTH',
1085                'type1': 'integer',
1086                'type2': ['integer']}}
1087        ColorTable.__init__(self, parent=parent, title=_(
1088            'Create new color rules for vector map'), **kwargs)
1089
1090        # additional bindings for vector color management
1091        self.Bind(wx.EVT_COMBOBOX, self.OnLayerSelection, self.layerSelect)
1092
1093        self._columnWidgetEvtHandler()
1094        self.Bind(wx.EVT_BUTTON, self.OnAddColumn, self.addColumn)
1095
1096        self._initLayer()
1097        if self.colorTable:
1098            self.cr_label.SetLabel(
1099                _("Enter vector attribute values or percents:"))
1100        else:
1101            self.cr_label.SetLabel(_("Enter vector attribute values:"))
1102
1103        # self.SetMinSize(self.GetSize())
1104        self.SetMinSize((650, 700))
1105
1106        self.CentreOnScreen()
1107        self.Show()
1108
1109    def _createVectorAttrb(self, parent):
1110        """Create part of dialog with layer/column selection"""
1111        inputBox = StaticBox(parent=parent, id=wx.ID_ANY,
1112                             label=" %s " % _("Select vector columns"))
1113        cb_vl_label = StaticText(parent, id=wx.ID_ANY,
1114                                 label=_('Layer:'))
1115        cb_vc_label = StaticText(parent, id=wx.ID_ANY,
1116                                 label=_('Attribute column:'))
1117
1118        if self.attributeType == 'color':
1119            labels = [_("Load color from column:"), _("Save color to column:")]
1120        elif self.attributeType == 'size':
1121            labels = [_("Load size from column:"), _("Save size to column:")]
1122        elif self.attributeType == 'width':
1123            labels = [_("Load width from column:"), _("Save width to column:")]
1124
1125        if self.version7 and self.attributeType == 'color':
1126            self.useColumn = wx.CheckBox(
1127                parent, id=wx.ID_ANY,
1128                label=_("Use color column instead of color table:"))
1129            self.useColumn.Bind(wx.EVT_CHECKBOX, self.OnCheckColumn)
1130
1131        fromColumnLabel = StaticText(parent, id=wx.ID_ANY,
1132                                     label=labels[0])
1133        toColumnLabel = StaticText(parent, id=wx.ID_ANY,
1134                                   label=labels[1])
1135
1136        self.rgb_range_label = StaticText(parent, id=wx.ID_ANY)
1137        self.layerSelect = LayerSelect(parent)
1138        self.sourceColumn = ColumnSelect(parent)
1139        self.fromColumn = ColumnSelect(parent)
1140        self.toColumn = ColumnSelect(parent)
1141        self.addColumn = Button(parent, id=wx.ID_ANY,
1142                                   label=_('Add column'))
1143        self.addColumn.SetToolTip(
1144            _("Add GRASSRGB column to current attribute table."))
1145
1146        # layout
1147        inputSizer = wx.StaticBoxSizer(inputBox, wx.VERTICAL)
1148        vSizer = wx.GridBagSizer(hgap=5, vgap=5)
1149        row = 0
1150        vSizer.Add(cb_vl_label, pos=(row, 0),
1151                   flag=wx.ALIGN_CENTER_VERTICAL)
1152        vSizer.Add(self.layerSelect, pos=(row, 1),
1153                   flag=wx.ALIGN_CENTER_VERTICAL)
1154        row += 1
1155        vSizer.Add(cb_vc_label, pos=(row, 0),
1156                   flag=wx.ALIGN_CENTER_VERTICAL)
1157        vSizer.Add(self.sourceColumn, pos=(row, 1),
1158                   flag=wx.ALIGN_CENTER_VERTICAL)
1159        vSizer.Add(self.rgb_range_label, pos=(row, 2),
1160                   flag=wx.ALIGN_CENTER_VERTICAL)
1161        row += 1
1162        if self.version7 and self.attributeType == 'color':
1163            vSizer.Add(self.useColumn, pos=(row, 0), span=(1, 2),
1164                       flag=wx.ALIGN_CENTER_VERTICAL)
1165            row += 1
1166
1167        vSizer.Add(fromColumnLabel, pos=(row, 0),
1168                   flag=wx.ALIGN_CENTER_VERTICAL)
1169        vSizer.Add(self.fromColumn, pos=(row, 1),
1170                   flag=wx.ALIGN_CENTER_VERTICAL)
1171        row += 1
1172        vSizer.Add(toColumnLabel, pos=(row, 0),
1173                   flag=wx.ALIGN_CENTER_VERTICAL)
1174        vSizer.Add(self.toColumn, pos=(row, 1),
1175                   flag=wx.ALIGN_CENTER_VERTICAL)
1176        vSizer.Add(self.addColumn, pos=(row, 2),
1177                   flag=wx.ALIGN_CENTER_VERTICAL)
1178        inputSizer.Add(
1179            vSizer,
1180            flag=wx.ALL | wx.EXPAND,
1181            border=5)
1182        self.colorColumnSizer = vSizer
1183        return inputSizer
1184
1185    def _doLayout(self):
1186        """Do main layout"""
1187        scrollPanel = scrolled.ScrolledPanel(parent=self.panel, id=wx.ID_ANY,
1188                                             style=wx.TAB_TRAVERSAL)
1189        scrollPanel.SetupScrolling()
1190        sizer = wx.BoxSizer(wx.VERTICAL)
1191        #
1192        # map selection
1193        #
1194        mapSelection = self._createMapSelection(parent=scrollPanel)
1195        sizer.Add(mapSelection, proportion=0,
1196                  flag=wx.ALL | wx.EXPAND, border=5)
1197        #
1198        # manage extern tables
1199        #
1200        if self.version7 and self.attributeType == 'color':
1201            self.cp = wx.CollapsiblePane(
1202                scrollPanel,
1203                label=_("Import or export color table"),
1204                id=wx.ID_ANY,
1205                style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
1206            self.Bind(
1207                wx.EVT_COLLAPSIBLEPANE_CHANGED,
1208                self.OnPaneChanged,
1209                self.cp)
1210
1211            self._createFileSelection(parent=self.cp.GetPane())
1212            sizer.Add(self.cp, proportion=0,
1213                      flag=wx.ALL | wx.EXPAND, border=5)
1214        #
1215        # set vector attributes
1216        #
1217        vectorAttrb = self._createVectorAttrb(parent=scrollPanel)
1218        sizer.Add(vectorAttrb, proportion=0,
1219                  flag=wx.ALL | wx.EXPAND, border=5)
1220        #
1221        # body & preview
1222        #
1223        bodySizer = self._createBody(parent=scrollPanel)
1224        sizer.Add(bodySizer, proportion=1,
1225                  flag=wx.ALL | wx.EXPAND, border=5)
1226
1227        scrollPanel.SetSizer(sizer)
1228        scrollPanel.Fit()
1229
1230        #
1231        # buttons
1232        #
1233        btnSizer = self._createButtons(self.panel)
1234
1235        mainsizer = wx.BoxSizer(wx.VERTICAL)
1236        mainsizer.Add(
1237            scrollPanel,
1238            proportion=1,
1239            flag=wx.EXPAND | wx.ALL,
1240            border=5)
1241        mainsizer.Add(wx.StaticLine(parent=self.panel, id=wx.ID_ANY,
1242                                    style=wx.LI_HORIZONTAL), proportion=0,
1243                      flag=wx.EXPAND | wx.ALL, border=5)
1244        mainsizer.Add(btnSizer, proportion=0,
1245                      flag=wx.ALL | wx.EXPAND, border=5)
1246
1247        self.panel.SetSizer(mainsizer)
1248        mainsizer.Layout()
1249        mainsizer.Fit(self.panel)
1250        self.Layout()
1251
1252    def OnPaneChanged(self, event=None):
1253        # redo the layout
1254        self.panel.Layout()
1255        # and also change the labels
1256        if self.cp.IsExpanded():
1257            self.cp.SetLabel('')
1258        else:
1259            self.cp.SetLabel(_("Import or export color table"))
1260
1261    def CheckMapset(self):
1262        """Check if current vector is in current mapset"""
1263        if grass.find_file(name=self.inmap, element='vector')[
1264                'mapset'] == grass.gisenv()['MAPSET']:
1265            return True
1266        else:
1267            return False
1268
1269    def NoConnection(self, vectorName):
1270        dlg = wx.MessageDialog(
1271            parent=self,
1272            message=_(
1273                "Database connection for vector map <%s> "
1274                "is not defined in DB file.  Do you want to create and "
1275                "connect new attribute table?") %
1276            vectorName,
1277            caption=_("No database connection defined"),
1278            style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION | wx.CENTRE)
1279        if dlg.ShowModal() == wx.ID_YES:
1280            dlg.Destroy()
1281            GUI(parent=self).ParseCommand(['v.db.addtable', 'map=' + self.inmap],
1282                                          completed=(self.CreateAttrTable, self.inmap, ''))
1283        else:
1284            dlg.Destroy()
1285
1286    def OnCheckColumn(self, event):
1287        """Use color column instead of color table"""
1288        if self.useColumn.GetValue():
1289            self.properties['loadColumn'] = self.fromColumn.GetValue()
1290            self.properties['storeColumn'] = self.toColumn.GetValue()
1291            self.fromColumn.Enable(True)
1292            self.toColumn.Enable(True)
1293            self.colorTable = False
1294
1295            if self.properties['loadColumn']:
1296                self.LoadTable()
1297            else:
1298                self.rulesPanel.Clear()
1299        else:
1300            self.properties['loadColumn'] = ''
1301            self.properties['storeColumn'] = ''
1302            self.fromColumn.Enable(False)
1303            self.toColumn.Enable(False)
1304            self.colorTable = True
1305            self.LoadTable()
1306
1307    def EnableVectorAttributes(self, enable):
1308        """Enable/disable part of dialog connected with db"""
1309        for child in self.colorColumnSizer.GetChildren():
1310            child.GetWindow().Enable(enable)
1311
1312    def DisableClearAll(self):
1313        """Enable, disable the whole dialog"""
1314        self.rulesPanel.Clear()
1315        self.EnableVectorAttributes(False)
1316        self.btnPreview.Enable(False)
1317        self.btnOK.Enable(False)
1318        self.btnApply.Enable(False)
1319        self.preview.EraseMap()
1320
1321    def OnSelectionInput(self, event):
1322        """Vector map selected"""
1323        if event:
1324            if self.inmap:
1325                # switch to another map -> delete temporary column
1326                self.DeleteTemporaryColumn()
1327            self.inmap = event.GetString()
1328
1329        if self.version7 and self.attributeType == 'color':
1330            self.loadRules.SetValue('')
1331            self.saveRules.SetValue('')
1332
1333        if self.inmap:
1334            if not grass.find_file(name=self.inmap, element='vector')['file']:
1335                self.inmap = None
1336
1337        self.UpdateDialog()
1338
1339    def UpdateDialog(self):
1340        """Update dialog after map selection"""
1341
1342        if not self.inmap:
1343            self.DisableClearAll()
1344            return
1345
1346        if not self.CheckMapset():
1347            # v.colors doesn't need the map to be in current mapset
1348            if not (self.version7 and self.attributeType == 'color'):
1349                message = _(
1350                    "Selected map <%(map)s> is not in current mapset <%(mapset)s>. "
1351                    "Attribute table cannot be edited.") % {
1352                    'map': self.inmap,
1353                    'mapset': grass.gisenv()['MAPSET']}
1354                wx.CallAfter(GMessage, parent=self, message=message)
1355                self.DisableClearAll()
1356                return
1357
1358        # check for db connection
1359        self.dbInfo = VectorDBInfo(self.inmap)
1360        enable = True
1361
1362        if not len(self.dbInfo.layers):  # no connection
1363            if not (self.version7 and self.attributeType ==
1364                    'color'):  # otherwise it doesn't matter
1365                wx.CallAfter(self.NoConnection, self.inmap)
1366                enable = False
1367            for combo in (self.layerSelect, self.sourceColumn,
1368                          self.fromColumn, self.toColumn):
1369                combo.SetValue("")
1370                combo.Clear()
1371            for prop in ('sourceColumn', 'loadColumn', 'storeColumn'):
1372                self.properties[prop] = ''
1373            self.EnableVectorAttributes(False)
1374        else:  # db connection exist
1375            # initialize layer selection combobox
1376            self.EnableVectorAttributes(True)
1377            self.layerSelect.InsertLayers(self.inmap)
1378            # initialize attribute table for layer=1
1379            self.properties['layer'] = self.layerSelect.GetString(0)
1380            self.layerSelect.SetStringSelection(self.properties['layer'])
1381            layer = int(self.properties['layer'])
1382            self.properties['table'] = self.dbInfo.layers[layer]['table']
1383
1384            if self.attributeType == 'color':
1385                self.AddTemporaryColumn(type='varchar(11)')
1386            else:
1387                self.AddTemporaryColumn(type='integer')
1388
1389            # initialize column selection comboboxes
1390
1391            self.OnLayerSelection(event=None)
1392
1393        if self.version7 and self.attributeType == 'color':
1394            self.useColumn.SetValue(False)
1395            self.OnCheckColumn(event=None)
1396            self.useColumn.Enable(self.CheckMapset())
1397        else:
1398            self.LoadTable()
1399
1400        self.btnPreview.Enable(enable)
1401        self.btnOK.Enable(enable)
1402        self.btnApply.Enable(enable)
1403
1404    def AddTemporaryColumn(self, type):
1405        """Add temporary column to not overwrite the original values,
1406        need to be deleted when closing dialog and unloading map
1407
1408        :param type: type of column (e.g. vachar(11))"""
1409        if not self.CheckMapset():
1410            return
1411        # because more than one dialog with the same map can be opened we must test column name and
1412        # create another one
1413        while self.properties['tmpColumn'] in self.dbInfo.GetTableDesc(self.properties[
1414                                                                       'table']).keys():
1415            name, idx = self.properties['tmpColumn'].split('_')
1416            idx = int(idx)
1417            idx += 1
1418            self.properties['tmpColumn'] = name + '_' + str(idx)
1419
1420        if self.version7:
1421            modul = 'v.db.addcolumn'
1422        else:
1423            modul = 'v.db.addcol'
1424        ret = RunCommand(modul,
1425                         parent=self,
1426                         map=self.inmap,
1427                         layer=self.properties['layer'],
1428                         column='%s %s' % (self.properties['tmpColumn'], type))
1429
1430    def DeleteTemporaryColumn(self):
1431        """Delete temporary column"""
1432        if not self.CheckMapset():
1433            return
1434
1435        if self.inmap:
1436            if self.version7:
1437                modul = 'v.db.dropcolumn'
1438            else:
1439                modul = 'v.db.dropcol'
1440            ret = RunCommand(modul,
1441                             map=self.inmap,
1442                             layer=self.properties['layer'],
1443                             column=self.properties['tmpColumn'])
1444
1445    def OnLayerSelection(self, event):
1446        # reset choices in column selection comboboxes if layer changes
1447        vlayer = int(self.layerSelect.GetStringSelection())
1448        self._columnWidgetEvtHandler(bind=False)
1449        self.sourceColumn.InsertColumns(
1450            vector=self.inmap, layer=vlayer,
1451            type=['integer', 'double precision'],
1452            dbInfo=self.dbInfo, excludeCols=['tmpColumn'])
1453        self.sourceColumn.SetValue('cat')
1454        self.properties['sourceColumn'] = self.sourceColumn.GetValue()
1455
1456        if self.attributeType == 'color':
1457            type = ['character']
1458        else:
1459            type = ['integer']
1460        self.fromColumn.InsertColumns(
1461            vector=self.inmap,
1462            layer=vlayer,
1463            type=type,
1464            dbInfo=self.dbInfo,
1465            excludeCols=['tmpColumn'])
1466        self.toColumn.InsertColumns(
1467            vector=self.inmap,
1468            layer=vlayer,
1469            type=type,
1470            dbInfo=self.dbInfo,
1471            excludeCols=['tmpColumn'])
1472
1473        v = self.columnsProp[self.attributeType]['name']
1474        found = False
1475        if v in self.fromColumn.GetColumns():
1476            found = True
1477
1478        if found != wx.NOT_FOUND:
1479            self.fromColumn.SetValue(v)
1480            self.toColumn.SetValue(v)
1481            self.properties['loadColumn'] = v
1482            self.properties['storeColumn'] = v
1483        else:
1484            self.properties['loadColumn'] = ''
1485            self.properties['storeColumn'] = ''
1486
1487        self._columnWidgetEvtHandler()
1488
1489        if event:
1490            self.LoadTable()
1491        self.Update()
1492
1493    def OnSourceColumnSelection(self, event):
1494        self.properties['sourceColumn'] = event.GetString()
1495
1496        self.LoadTable()
1497
1498    def OnAddColumn(self, event):
1499        """Add GRASS(RGB,SIZE,WIDTH) column if it doesn't exist"""
1500        if self.columnsProp[self.attributeType][
1501                'name'] not in self.fromColumn.GetColumns():
1502            if self.version7:
1503                modul = 'v.db.addcolumn'
1504            else:
1505                modul = 'v.db.addcol'
1506            ret = RunCommand(
1507                modul, map=self.inmap, layer=self.properties['layer'], columns='%s %s' %
1508                (self.columnsProp[
1509                    self.attributeType]['name'], self.columnsProp[
1510                    self.attributeType]['type1']))
1511            self.toColumn.InsertColumns(
1512                self.inmap,
1513                self.properties['layer'],
1514                type=self.columnsProp[
1515                    self.attributeType]['type2'])
1516            self.toColumn.SetValue(
1517                self.columnsProp[
1518                    self.attributeType]['name'])
1519            self.properties['storeColumn'] = self.toColumn.GetValue()
1520
1521            self.LoadTable()
1522        else:
1523            GMessage(parent=self,
1524                     message=_("%s column already exists.") %
1525                     self.columnsProp[self.attributeType]['name'])
1526
1527    def CreateAttrTable(self, dcmd, layer, params, propwin):
1528        """Create attribute table"""
1529        if dcmd:
1530            cmd = cmdlist_to_tuple(dcmd)
1531            ret = RunCommand(cmd[0], **cmd[1])
1532            if ret == 0:
1533                self.OnSelectionInput(None)
1534                return True
1535
1536        for combo in (self.layerSelect, self.sourceColumn,
1537                      self.fromColumn, self.toColumn):
1538            combo.SetValue("")
1539            combo.Disable()
1540        return False
1541
1542    def LoadTable(self):
1543        """Load table"""
1544        if self.colorTable:
1545            ColorTable.LoadTable(self, mapType='vector')
1546        else:
1547            self.LoadRulesFromColumn()
1548
1549    def LoadRulesFromColumn(self):
1550        """Load current column (GRASSRGB, size column)"""
1551
1552        self.rulesPanel.Clear()
1553        if not self.properties['sourceColumn']:
1554            self.preview.EraseMap()
1555            return
1556
1557        busy = wx.BusyInfo(
1558            _("Please wait, loading data from attribute table..."),
1559            parent=self)
1560        wx.GetApp().Yield()
1561
1562        columns = self.properties['sourceColumn']
1563        if self.properties['loadColumn']:
1564            columns += ',' + self.properties['loadColumn']
1565
1566        sep = ';'
1567        if self.inmap:
1568            outFile = tempfile.NamedTemporaryFile(mode='w+')
1569            ret = RunCommand('v.db.select',
1570                             quiet=True,
1571                             flags='c',
1572                             map=self.inmap,
1573                             layer=self.properties['layer'],
1574                             columns=columns,
1575                             sep=sep,
1576                             stdout=outFile)
1577        else:
1578            self.preview.EraseMap()
1579            del busy
1580            return
1581
1582        outFile.seek(0)
1583        i = 0
1584        minim = maxim = 0.0
1585        limit = 1000
1586
1587        colvallist = []
1588        readvals = False
1589
1590        while True:
1591            # os.linesep doesn't work here (MSYS)
1592            record = outFile.readline().replace('\n', '')
1593            if not record:
1594                break
1595            self.rulesPanel.ruleslines[i] = {}
1596
1597            if not self.properties['loadColumn']:
1598                col1 = record
1599                col2 = None
1600            else:
1601                col1, col2 = record.split(sep)
1602
1603            if float(col1) < minim:
1604                minim = float(col1)
1605            if float(col1) > maxim:
1606                maxim = float(col1)
1607
1608            # color rules list should only have unique values of col1, not all
1609            # records
1610            if col1 not in colvallist:
1611                self.rulesPanel.ruleslines[i]['value'] = col1
1612                self.rulesPanel.ruleslines[i][self.attributeType] = col2
1613
1614                colvallist.append(col1)
1615                i += 1
1616
1617            if i > limit and readvals == False:
1618                dlg = wx.MessageDialog(parent=self, message=_(
1619                    "Number of loaded records reached %d, "
1620                    "displaying all the records will be time-consuming "
1621                    "and may lead to computer freezing, "
1622                    "do you still want to continue?") % i,
1623                    caption=_("Too many records"),
1624                    style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
1625                if dlg.ShowModal() == wx.ID_YES:
1626                    readvals = True
1627                    dlg.Destroy()
1628                else:
1629                    del busy
1630                    dlg.Destroy()
1631                    self.updateColumn = False
1632                    return
1633
1634        self.rulesPanel.AddRules(i, start=True)
1635        ret = self.rulesPanel.LoadRules()
1636
1637        self.properties['min'], self.properties['max'] = minim, maxim
1638        self.SetRangeLabel()
1639
1640        if ret:
1641            self.OnPreview()
1642        else:
1643            self.rulesPanel.Clear()
1644
1645        del busy
1646
1647    def SetRangeLabel(self):
1648        """Set labels with info about attribute column range"""
1649
1650        if self.properties['sourceColumn']:
1651            ctype = self.dbInfo.GetTableDesc(
1652                self.properties['table'])[
1653                self.properties['sourceColumn']]['ctype']
1654        else:
1655            ctype = int
1656
1657        range = ''
1658        if self.properties['min'] or self.properties['max']:
1659            if ctype == float:
1660                range = "%s: %.1f - %.1f)" % (_("range"),
1661                                              self.properties['min'],
1662                                              self.properties['max'])
1663            elif ctype == int:
1664                range = "%s: %d - %d)" % (_("range"),
1665                                          self.properties['min'],
1666                                          self.properties['max'])
1667        if range:
1668            if self.colorTable:
1669                self.cr_label.SetLabel(
1670                    _("Enter vector attribute values or percents %s:") %
1671                    range)
1672            else:
1673                self.cr_label.SetLabel(
1674                    _("Enter vector attribute values %s:") %
1675                    range)
1676        else:
1677            if self.colorTable:
1678                self.cr_label.SetLabel(
1679                    _("Enter vector attribute values or percents:"))
1680            else:
1681                self.cr_label.SetLabel(_("Enter vector attribute values:"))
1682
1683    def OnFromColSelection(self, event):
1684        """Selection in combobox (for loading values) changed"""
1685        self.properties['loadColumn'] = event.GetString()
1686
1687        self.LoadTable()
1688
1689    def OnToColSelection(self, event):
1690        """Selection in combobox (for storing values) changed"""
1691        self.properties['storeColumn'] = event.GetString()
1692
1693    def OnPreview(self, event=None, tmp=True):
1694        """Update preview (based on computational region)"""
1695        if self.colorTable:
1696            self.OnTablePreview(tmp)
1697        else:
1698            self.OnColumnPreview()
1699
1700    def OnTablePreview(self, tmp):
1701        """Update preview (based on computational region)"""
1702        if not self.inmap:
1703            self.preview.EraseMap()
1704            return
1705
1706        ltype = 'vector'
1707        cmdlist = ['d.vect',
1708                   'map=%s' % self.inmap]
1709
1710        # find existing color table and copy to temp file
1711        try:
1712            name, mapset = self.inmap.split('@')
1713        except ValueError:
1714            name = self.inmap
1715            mapset = grass.find_file(self.inmap, element='cell')['mapset']
1716            if not mapset:
1717                return
1718
1719        old_colrtable = None
1720        if mapset == grass.gisenv()['MAPSET']:
1721            old_colrtable = grass.find_file(
1722                name='colr', element=os.path.join(
1723                    'vector', name))['file']
1724        else:
1725            old_colrtable = grass.find_file(
1726                name=name, element=os.path.join(
1727                    'vcolr2', mapset))['file']
1728
1729        if old_colrtable:
1730            colrtemp = utils.GetTempfile()
1731            shutil.copyfile(old_colrtable, colrtemp)
1732
1733        ColorTable.DoPreview(self, ltype, cmdlist)
1734
1735        # restore previous color table
1736        if tmp:
1737            if old_colrtable:
1738                shutil.copyfile(colrtemp, old_colrtable)
1739                os.remove(colrtemp)
1740            else:
1741                RunCommand('v.colors',
1742                           parent=self,
1743                           flags='r',
1744                           map=self.inmap)
1745
1746    def OnColumnPreview(self):
1747        """Update preview (based on computational region)"""
1748        if not self.inmap or not self.properties['tmpColumn']:
1749            self.preview.EraseMap()
1750            return
1751
1752        cmdlist = ['d.vect',
1753                   'map=%s' % self.inmap,
1754                   'type=point,line,boundary,area']
1755
1756        if self.attributeType == 'color':
1757            cmdlist.append('rgb_column=%s' % self.properties['tmpColumn'])
1758        elif self.attributeType == 'size':
1759            cmdlist.append('size_column=%s' % self.properties['tmpColumn'])
1760        elif self.attributeType == 'width':
1761            cmdlist.append('width_column=%s' % self.properties['tmpColumn'])
1762
1763        ltype = 'vector'
1764
1765        ColorTable.DoPreview(self, ltype, cmdlist)
1766
1767    def OnHelp(self, event):
1768        """Show GRASS manual page"""
1769        cmd = 'v.colors'
1770        ColorTable.RunHelp(self, cmd=cmd)
1771
1772    def UseAttrColumn(self, useAttrColumn):
1773        """Find layers and apply the changes in d.vect command"""
1774        layers = self.layerTree.FindItemByData(key='name', value=self.inmap)
1775        if not layers:
1776            return
1777        for layer in layers:
1778            if self.layerTree.GetLayerInfo(layer, key='type') != 'vector':
1779                continue
1780            cmdlist = self.layerTree.GetLayerInfo(
1781                layer, key='maplayer').GetCmd()
1782
1783            if self.attributeType == 'color':
1784                if useAttrColumn:
1785                    cmdlist[1].update(
1786                        {'rgb_column': self.properties['storeColumn']})
1787                else:
1788                    cmdlist[1].pop('rgb_column', None)
1789            elif self.attributeType == 'size':
1790                cmdlist[1].update(
1791                    {'size_column': self.properties['storeColumn']})
1792            elif self.attributeType == 'width':
1793                cmdlist[1].update(
1794                    {'width_column': self.properties['storeColumn']})
1795            self.layerTree.SetLayerInfo(layer, key='cmd', value=cmdlist)
1796
1797    def CreateColorTable(self, tmp=False):
1798        """Create color rules (color table or color column)"""
1799        if self.colorTable:
1800            ret = ColorTable.CreateColorTable(self)
1801        else:
1802            if self.updateColumn:
1803                ret = self.UpdateColorColumn(tmp)
1804            else:
1805                ret = True
1806
1807        return ret
1808
1809    def UpdateColorColumn(self, tmp):
1810        """Creates color table
1811
1812        :return: True on success
1813        :return: False on failure
1814        """
1815        rulestxt = ''
1816
1817        for rule in six.itervalues(self.rulesPanel.ruleslines):
1818            if 'value' not in rule:  # skip empty rules
1819                break
1820
1821            if tmp:
1822                rgb_col = self.properties['tmpColumn']
1823            else:
1824                rgb_col = self.properties['storeColumn']
1825                if not self.properties['storeColumn']:
1826                    GMessage(parent=self.parent, message=_(
1827                        "Please select column to save values to."))
1828
1829            rulestxt += "UPDATE %s SET %s='%s' WHERE %s ;\n" % (
1830                self.properties['table'],
1831                rgb_col, rule[self.attributeType],
1832                rule['value'])
1833        if not rulestxt:
1834            return False
1835
1836        gtemp = utils.GetTempfile()
1837        output = open(gtemp, "w")
1838        try:
1839            output.write(rulestxt)
1840        finally:
1841            output.close()
1842
1843        RunCommand('db.execute',
1844                   parent=self,
1845                   input=gtemp)
1846        return True
1847
1848    def OnCancel(self, event):
1849        """Do not apply any changes and close the dialog"""
1850        self.DeleteTemporaryColumn()
1851        self.Map.Clean()
1852        self.Destroy()
1853
1854    def _apply(self, updatePreview=True):
1855        """Apply selected color table
1856
1857        :return: True on success otherwise False
1858        """
1859        if self.colorTable:
1860            self.UseAttrColumn(False)
1861        else:
1862            if not self.properties['storeColumn']:
1863                GError(_("No color column defined. Operation canceled."),
1864                       parent=self)
1865                return
1866
1867            self.UseAttrColumn(True)
1868
1869        return ColorTable._apply(self, updatePreview)
1870
1871    def _columnWidgetEvtHandler(self, bind=True):
1872        """Bind/Unbind Column widgets handlers"""
1873        widgets = [
1874            {
1875                'widget': self.sourceColumn,
1876                'event': wx.EVT_TEXT,
1877                'handler': self.OnSourceColumnSelection,
1878            },
1879            {
1880                'widget': self.fromColumn,
1881                'event': wx.EVT_TEXT,
1882                'handler': self.OnFromColSelection,
1883            },
1884            {
1885                'widget': self.toColumn,
1886                'event': wx.EVT_TEXT,
1887                'handler': self.OnToColSelection,
1888            },
1889        ]
1890        for widget in widgets:
1891            if bind is True:
1892                getattr(widget['widget'], 'Bind')(
1893                    widget['event'], widget['handler'],
1894                )
1895            else:
1896                getattr(widget['widget'], 'Unbind')(widget['event'])
1897
1898
1899class ThematicVectorTable(VectorColorTable):
1900
1901    def __init__(self, parent, vectorType, **kwargs):
1902        """Dialog for interactively entering color/size rules
1903        for vector maps for thematic mapping in nviz"""
1904        self.vectorType = vectorType
1905        VectorColorTable.__init__(self, parent=parent, **kwargs)
1906
1907        self.SetTitle(_("Thematic mapping for vector map in 3D view"))
1908
1909    def _initLayer(self):
1910        """Set initial layer when opening dialog"""
1911        self.inmap = self.parent.GetLayerData(nvizType='vector', nameOnly=True)
1912        self.selectionInput.SetValue(self.inmap)
1913        self.selectionInput.Disable()
1914
1915    def _apply(self, updatePreview=True):
1916        """Apply selected color table
1917
1918        :return: True on success otherwise False
1919        """
1920        ret = self.CreateColorTable()
1921        if not ret:
1922            GMessage(parent=self, message=_("No valid color rules given."))
1923
1924        data = self.parent.GetLayerData(nvizType='vector')
1925        data['vector']['points']['thematic'][
1926            'layer'] = int(self.properties['layer'])
1927
1928        value = None
1929        if self.properties['storeColumn']:
1930            value = self.properties['storeColumn']
1931
1932        if not self.colorTable:
1933            if self.attributeType == 'color':
1934                data['vector'][self.vectorType][
1935                    'thematic']['rgbcolumn'] = value
1936            else:
1937                data['vector'][self.vectorType][
1938                    'thematic']['sizecolumn'] = value
1939        else:
1940            if self.attributeType == 'color':
1941                data['vector'][self.vectorType]['thematic']['rgbcolumn'] = None
1942            else:
1943                data['vector'][self.vectorType][
1944                    'thematic']['sizecolumn'] = None
1945
1946        data['vector'][self.vectorType]['thematic']['update'] = None
1947
1948        from nviz.main import haveNviz
1949        if haveNviz:
1950            from nviz.mapwindow import wxUpdateProperties
1951
1952            event = wxUpdateProperties(data=data)
1953            wx.PostEvent(self.parent.mapWindow, event)
1954
1955        self.parent.mapWindow.Refresh(False)
1956
1957        return ret
1958
1959
1960class BufferedWindow(wx.Window):
1961    """A Buffered window class"""
1962
1963    def __init__(self, parent, id,
1964                 style=wx.NO_FULL_REPAINT_ON_RESIZE,
1965                 Map=None, **kwargs):
1966
1967        wx.Window.__init__(self, parent, id, style=style, **kwargs)
1968
1969        self.parent = parent
1970        self.Map = Map
1971
1972        # re-render the map from GRASS or just redraw image
1973        self.render = True
1974        # indicates whether or not a resize event has taken place
1975        self.resize = False
1976
1977        #
1978        # event bindings
1979        #
1980        self.Bind(wx.EVT_PAINT, self.OnPaint)
1981        self.Bind(wx.EVT_IDLE, self.OnIdle)
1982        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
1983
1984        #
1985        # render output objects
1986        #
1987        # image file to be rendered
1988        self.mapfile = None
1989        # wx.Image object (self.mapfile)
1990        self.img = None
1991
1992        self.pdc = PseudoDC()
1993        # will store an off screen empty bitmap for saving to file
1994        self._Buffer = None
1995
1996        # make sure that extents are updated at init
1997        self.Map.region = self.Map.GetRegion()
1998        self.Map.SetRegion()
1999        self.Map.GetRenderMgr().renderDone.connect(self._updatePreviewFinished)
2000
2001    def Draw(self, pdc, img=None, pdctype='image'):
2002        """Draws preview or clears window"""
2003        pdc.BeginDrawing()
2004
2005        Debug.msg(3, "BufferedWindow.Draw(): pdctype=%s" % (pdctype))
2006
2007        if pdctype == 'clear':  # erase the display
2008            bg = wx.WHITE_BRUSH
2009            pdc.SetBackground(bg)
2010            pdc.Clear()
2011            self.Refresh()
2012            pdc.EndDrawing()
2013            return
2014
2015        if pdctype == 'image' and img:
2016            bg = wx.TRANSPARENT_BRUSH
2017            pdc.SetBackground(bg)
2018            bitmap = BitmapFromImage(img)
2019            w, h = bitmap.GetSize()
2020            pdc.DrawBitmap(bitmap, 0, 0, True)  # draw the composite map
2021
2022        pdc.EndDrawing()
2023        self.Refresh()
2024
2025    def OnPaint(self, event):
2026        """Draw pseudo DC to buffer"""
2027        self._Buffer = EmptyBitmap(self.Map.width, self.Map.height)
2028        dc = wx.BufferedPaintDC(self, self._Buffer)
2029
2030        # use PrepareDC to set position correctly
2031        # probably does nothing, removed from wxPython 2.9
2032        # self.PrepareDC(dc)
2033
2034        # we need to clear the dc BEFORE calling PrepareDC
2035        bg = wx.Brush(self.GetBackgroundColour())
2036        dc.SetBackground(bg)
2037        dc.Clear()
2038
2039        # create a clipping rect from our position and size
2040        # and the Update Region
2041        rgn = self.GetUpdateRegion()
2042        r = rgn.GetBox()
2043
2044        # draw to the dc using the calculated clipping rect
2045        self.pdc.DrawToDCClipped(dc, r)
2046
2047    def OnSize(self, event):
2048        """Init image size to match window size"""
2049        # set size of the input image
2050        self.Map.width, self.Map.height = self.GetClientSize()
2051
2052        # Make new off screen bitmap: this bitmap will always have the
2053        # current drawing in it, so it can be used to save the image to
2054        # a file, or whatever.
2055        self._Buffer = EmptyBitmap(self.Map.width, self.Map.height)
2056
2057        # get the image to be rendered
2058        self.img = self.GetImage()
2059
2060        # update map display
2061        if self.img and self.Map.width + self.Map.height > 0:  # scale image during resize
2062            self.img = self.img.Scale(self.Map.width, self.Map.height)
2063            self.render = False
2064            self.UpdatePreview()
2065
2066        # re-render image on idle
2067        self.resize = True
2068
2069    def OnIdle(self, event):
2070        """Only re-render a preview image from GRASS during
2071        idle time instead of multiple times during resizing.
2072        """
2073        if self.resize:
2074            self.render = True
2075            self.UpdatePreview()
2076        event.Skip()
2077
2078    def GetImage(self):
2079        """Converts files to wx.Image"""
2080        if self.Map.mapfile and os.path.isfile(self.Map.mapfile) and \
2081                os.path.getsize(self.Map.mapfile):
2082            img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY)
2083        else:
2084            img = None
2085
2086        return img
2087
2088    def UpdatePreview(self, img=None):
2089        """Update canvas if window changes geometry"""
2090        Debug.msg(
2091            2, "BufferedWindow.UpdatePreview(%s): render=%s" %
2092            (img, self.render))
2093
2094        if not self.render:
2095            return
2096
2097        # extent is taken from current map display
2098        try:
2099            self.Map.region = copy.deepcopy(
2100                self.parent.parent.GetLayerTree().GetMap().GetCurrentRegion())
2101        except AttributeError:
2102            self.Map.region = self.Map.GetRegion()
2103        # render new map images
2104        self.mapfile = self.Map.Render(force=self.render)
2105
2106    def _updatePreviewFinished(self):
2107        if not self.render:
2108            return
2109
2110        self.img = self.GetImage()
2111        self.resize = False
2112
2113        if not self.img:
2114            return
2115
2116        # paint images to PseudoDC
2117        self.pdc.Clear()
2118        self.pdc.RemoveAll()
2119        # draw map image background
2120        self.Draw(self.pdc, self.img, pdctype='image')
2121
2122        self.resize = False
2123
2124    def EraseMap(self):
2125        """Erase preview"""
2126        self.Draw(self.pdc, pdctype='clear')
2127