1"""
2@package gis_set
3
4GRASS start-up screen.
5
6Initialization module for wxPython GRASS GUI.
7Location/mapset management (selection, creation, etc.).
8
9Classes:
10 - gis_set::GRASSStartup
11 - gis_set::GListBox
12 - gis_set::StartUp
13
14(C) 2006-2014 by the GRASS Development Team
15
16This program is free software under the GNU General Public License
17(>=v2). Read the file COPYING that comes with GRASS for details.
18
19@author Michael Barton and Jachym Cepicky (original author)
20@author Martin Landa <landa.martin gmail.com> (various updates)
21"""
22
23import os
24import sys
25import copy
26import platform
27import codecs
28import getpass
29
30# i18n is taken care of in the grass library code.
31# So we need to import it before any of the GUI code.
32from grass.script import core as grass
33
34from core import globalvar
35import wx
36# import adv and html before wx.App is created, otherwise
37# we get annoying "Debug: Adding duplicate image handler for 'Windows bitmap file'"
38# during download location dialog start up, remove when not needed
39import wx.adv
40import wx.html
41import wx.lib.mixins.listctrl as listmix
42
43from core.gcmd import GMessage, GError, DecodeString, RunCommand
44from core.utils import GetListOfLocations, GetListOfMapsets
45from startup.utils import (
46    get_lockfile_if_present, get_possible_database_path, create_mapset)
47import startup.utils as sutils
48from startup.guiutils import SetSessionMapset, NewMapsetDialog
49import startup.guiutils as sgui
50from location_wizard.dialogs import RegionDef
51from gui_core.dialogs import TextEntryDialog
52from gui_core.widgets import GenericValidator, StaticWrapText
53from gui_core.wrap import Button, ListCtrl, StaticText, StaticBox, \
54    TextCtrl, BitmapFromImage
55
56
57class GRASSStartup(wx.Frame):
58    exit_success = 0
59    # 2 is file not found from python interpreter
60    exit_user_requested = 5
61
62    """GRASS start-up screen"""
63
64    def __init__(self, parent=None, id=wx.ID_ANY,
65                 style=wx.DEFAULT_FRAME_STYLE):
66
67        #
68        # GRASS variables
69        #
70        self.gisbase = os.getenv("GISBASE")
71        self.grassrc = sgui.read_gisrc()
72        self.gisdbase = self.GetRCValue("GISDBASE")
73
74        #
75        # list of locations/mapsets
76        #
77        self.listOfLocations = []
78        self.listOfMapsets = []
79        self.listOfMapsetsSelectable = []
80
81        wx.Frame.__init__(self, parent=parent, id=id, style=style)
82
83        self.locale = wx.Locale(language=wx.LANGUAGE_DEFAULT)
84
85        # scroll panel was used here but not properly and is probably not need
86        # as long as it is not high too much
87        self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
88
89        # i18N
90
91        #
92        # graphical elements
93        #
94        # image
95        try:
96            if os.getenv('ISISROOT'):
97                name = os.path.join(
98                    globalvar.GUIDIR,
99                    "images",
100                    "startup_banner_isis.png")
101            else:
102                name = os.path.join(
103                    globalvar.GUIDIR, "images", "startup_banner.png")
104            self.hbitmap = wx.StaticBitmap(self.panel, wx.ID_ANY,
105                                           wx.Bitmap(name=name,
106                                                     type=wx.BITMAP_TYPE_PNG))
107        except:
108            self.hbitmap = wx.StaticBitmap(
109                self.panel, wx.ID_ANY, BitmapFromImage(
110                    wx.EmptyImage(530, 150)))
111
112        # labels
113        # crashes when LOCATION doesn't exist
114        # get version & revision
115        grassVersion, grassRevisionStr = sgui.GetVersion()
116
117        self.gisdbase_box = StaticBox(
118            parent=self.panel, id=wx.ID_ANY, label=" %s " %
119            _("1. Select GRASS GIS database directory"))
120        self.location_box = StaticBox(
121            parent=self.panel, id=wx.ID_ANY, label=" %s " %
122            _("2. Select GRASS Location"))
123        self.mapset_box = StaticBox(
124            parent=self.panel, id=wx.ID_ANY, label=" %s " %
125            _("3. Select GRASS Mapset"))
126
127        self.lmessage = StaticWrapText(parent=self.panel)
128        # It is not clear if all wx versions supports color, so try-except.
129        # The color itself may not be correct for all platforms/system settings
130        # but in http://xoomer.virgilio.it/infinity77/wxPython/Widgets/wx.SystemSettings.html
131        # there is no 'warning' color.
132        try:
133            self.lmessage.SetForegroundColour(wx.Colour(255, 0, 0))
134        except AttributeError:
135            pass
136
137        self.gisdbase_panel = wx.Panel(parent=self.panel)
138        self.location_panel = wx.Panel(parent=self.panel)
139        self.mapset_panel = wx.Panel(parent=self.panel)
140
141        self.ldbase = StaticText(
142            parent=self.gisdbase_panel, id=wx.ID_ANY,
143            label=_("GRASS GIS database directory contains Locations."))
144
145        self.llocation = StaticWrapText(
146            parent=self.location_panel, id=wx.ID_ANY,
147            label=_("All data in one Location is in the same "
148                    " coordinate reference system (projection)."
149                    " One Location can be one project."
150                    " Location contains Mapsets."),
151            style=wx.ALIGN_LEFT)
152
153        self.lmapset = StaticWrapText(
154            parent=self.mapset_panel, id=wx.ID_ANY,
155            label=_("Mapset contains GIS data related"
156                    " to one project, task within one project,"
157                    " subregion or user."),
158            style=wx.ALIGN_LEFT)
159
160        try:
161            for label in [self.ldbase, self.llocation, self.lmapset]:
162                label.SetForegroundColour(
163                    wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
164        except AttributeError:
165            # for explanation of try-except see above
166            pass
167
168        # buttons
169        self.bstart = Button(parent=self.panel, id=wx.ID_ANY,
170                             label=_("Start &GRASS session"))
171        self.bstart.SetDefault()
172        self.bexit = Button(parent=self.panel, id=wx.ID_EXIT)
173        self.bstart.SetMinSize((180, self.bexit.GetSize()[1]))
174        self.bhelp = Button(parent=self.panel, id=wx.ID_HELP)
175        self.bbrowse = Button(parent=self.gisdbase_panel, id=wx.ID_ANY,
176                              label=_("&Browse"))
177        self.bmapset = Button(parent=self.mapset_panel, id=wx.ID_ANY,
178                              # GTC New mapset
179                              label=_("&New"))
180        self.bmapset.SetToolTip(_("Create a new Mapset in selected Location"))
181        self.bwizard = Button(parent=self.location_panel, id=wx.ID_ANY,
182                              # GTC New location
183                              label=_("N&ew"))
184        self.bwizard.SetToolTip(
185            _(
186                "Create a new location using location wizard."
187                " After location is created successfully,"
188                " GRASS session is started."))
189        self.rename_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
190                                             # GTC Rename location
191                                             label=_("Ren&ame"))
192        self.rename_location_button.SetToolTip(_("Rename selected location"))
193        self.delete_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
194                                             # GTC Delete location
195                                             label=_("De&lete"))
196        self.delete_location_button.SetToolTip(_("Delete selected location"))
197        self.download_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
198                                             label=_("Do&wnload"))
199        self.download_location_button.SetToolTip(_("Download sample location"))
200
201        self.rename_mapset_button = Button(parent=self.mapset_panel, id=wx.ID_ANY,
202                                           # GTC Rename mapset
203                                           label=_("&Rename"))
204        self.rename_mapset_button.SetToolTip(_("Rename selected mapset"))
205        self.delete_mapset_button = Button(parent=self.mapset_panel, id=wx.ID_ANY,
206                                           # GTC Delete mapset
207                                           label=_("&Delete"))
208        self.delete_mapset_button.SetToolTip(_("Delete selected mapset"))
209
210        # textinputs
211        self.tgisdbase = TextCtrl(
212            parent=self.gisdbase_panel, id=wx.ID_ANY, value="", size=(
213                300, -1), style=wx.TE_PROCESS_ENTER)
214
215        # Locations
216        self.lblocations = GListBox(parent=self.location_panel,
217                                    id=wx.ID_ANY, size=(180, 200),
218                                    choices=self.listOfLocations)
219        self.lblocations.SetColumnWidth(0, 180)
220
221        # TODO: sort; but keep PERMANENT on top of list
222        # Mapsets
223        self.lbmapsets = GListBox(parent=self.mapset_panel,
224                                  id=wx.ID_ANY, size=(180, 200),
225                                  choices=self.listOfMapsets)
226        self.lbmapsets.SetColumnWidth(0, 180)
227
228        # layout & properties, first do layout so everything is created
229        self._do_layout()
230        self._set_properties(grassVersion, grassRevisionStr)
231
232        # events
233        self.bbrowse.Bind(wx.EVT_BUTTON, self.OnBrowse)
234        self.bstart.Bind(wx.EVT_BUTTON, self.OnStart)
235        self.bexit.Bind(wx.EVT_BUTTON, self.OnExit)
236        self.bhelp.Bind(wx.EVT_BUTTON, self.OnHelp)
237        self.bmapset.Bind(wx.EVT_BUTTON, self.OnCreateMapset)
238        self.bwizard.Bind(wx.EVT_BUTTON, self.OnWizard)
239
240        self.rename_location_button.Bind(wx.EVT_BUTTON, self.RenameLocation)
241        self.delete_location_button.Bind(wx.EVT_BUTTON, self.DeleteLocation)
242        self.download_location_button.Bind(wx.EVT_BUTTON, self.DownloadLocation)
243        self.rename_mapset_button.Bind(wx.EVT_BUTTON, self.RenameMapset)
244        self.delete_mapset_button.Bind(wx.EVT_BUTTON, self.DeleteMapset)
245
246        self.lblocations.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectLocation)
247        self.lbmapsets.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectMapset)
248        self.lbmapsets.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnStart)
249        self.tgisdbase.Bind(wx.EVT_TEXT_ENTER, self.OnSetDatabase)
250        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
251
252    def _set_properties(self, version, revision):
253        """Set frame properties
254
255        :param version: Version in the form of X.Y.Z
256        :param revision: Version control revision with leading space
257
258        *revision* should be an empty string in case of release and
259        otherwise it needs a leading space to be separated from the rest
260        of the title.
261        """
262        self.SetTitle(_("GRASS GIS %s Startup%s") % (version, revision))
263        self.SetIcon(wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"),
264                             wx.BITMAP_TYPE_ICO))
265
266        self.bstart.SetToolTip(_("Enter GRASS session"))
267        self.bstart.Enable(False)
268        self.bmapset.Enable(False)
269        # this all was originally a choice, perhaps just mapset needed
270        self.rename_location_button.Enable(False)
271        self.delete_location_button.Enable(False)
272        self.rename_mapset_button.Enable(False)
273        self.delete_mapset_button.Enable(False)
274
275        # set database
276        if not self.gisdbase:
277            # sets an initial path for gisdbase if nothing in GISRC
278            if os.path.isdir(os.getenv("HOME")):
279                self.gisdbase = os.getenv("HOME")
280            else:
281                self.gisdbase = os.getcwd()
282        try:
283            self.tgisdbase.SetValue(self.gisdbase)
284        except UnicodeDecodeError:
285            wx.MessageBox(parent=self, caption=_("Error"),
286                          message=_("Unable to set GRASS database. "
287                                    "Check your locale settings."),
288                          style=wx.OK | wx.ICON_ERROR | wx.CENTRE)
289
290        self.OnSetDatabase(None)
291        location = self.GetRCValue("LOCATION_NAME")
292        if location == "<UNKNOWN>" or location is None:
293            return
294        if not os.path.isdir(os.path.join(self.gisdbase, location)):
295            location = None
296
297        # list of locations
298        self.UpdateLocations(self.gisdbase)
299        try:
300            self.lblocations.SetSelection(self.listOfLocations.index(location),
301                                          force=True)
302            self.lblocations.EnsureVisible(
303                self.listOfLocations.index(location))
304        except ValueError:
305            sys.stderr.write(
306                _("ERROR: Location <%s> not found\n") %
307                self.GetRCValue("LOCATION_NAME"))
308            if len(self.listOfLocations) > 0:
309                self.lblocations.SetSelection(0, force=True)
310                self.lblocations.EnsureVisible(0)
311                location = self.listOfLocations[0]
312            else:
313                return
314
315        # list of mapsets
316        self.UpdateMapsets(os.path.join(self.gisdbase, location))
317        mapset = self.GetRCValue("MAPSET")
318        if mapset:
319            try:
320                self.lbmapsets.SetSelection(self.listOfMapsets.index(mapset),
321                                            force=True)
322                self.lbmapsets.EnsureVisible(self.listOfMapsets.index(mapset))
323            except ValueError:
324                sys.stderr.write(_("ERROR: Mapset <%s> not found\n") % mapset)
325                self.lbmapsets.SetSelection(0, force=True)
326                self.lbmapsets.EnsureVisible(0)
327
328    def _do_layout(self):
329        sizer = wx.BoxSizer(wx.VERTICAL)
330        self.sizer = sizer  # for the layout call after changing message
331        dbase_sizer = wx.BoxSizer(wx.HORIZONTAL)
332
333        location_mapset_sizer = wx.BoxSizer(wx.HORIZONTAL)
334
335        gisdbase_panel_sizer = wx.BoxSizer(wx.VERTICAL)
336        gisdbase_boxsizer = wx.StaticBoxSizer(self.gisdbase_box, wx.VERTICAL)
337
338        btns_sizer = wx.BoxSizer(wx.HORIZONTAL)
339
340        self.gisdbase_panel.SetSizer(gisdbase_panel_sizer)
341
342        # gis data directory
343
344        gisdbase_boxsizer.Add(self.gisdbase_panel, proportion=1,
345                              flag=wx.EXPAND | wx.ALL,
346                              border=1)
347
348        gisdbase_panel_sizer.Add(dbase_sizer, proportion=1,
349                                 flag=wx.EXPAND | wx.ALL,
350                                 border=1)
351        gisdbase_panel_sizer.Add(self.ldbase, proportion=0,
352                                 flag=wx.EXPAND | wx.ALL,
353                                 border=1)
354
355        dbase_sizer.Add(self.tgisdbase, proportion=1,
356                        flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL,
357                        border=1)
358        dbase_sizer.Add(self.bbrowse, proportion=0,
359                        flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL,
360                        border=1)
361
362        gisdbase_panel_sizer.Fit(self.gisdbase_panel)
363
364        # location and mapset lists
365
366        def layout_list_box(box, panel, list_box, buttons, description):
367            panel_sizer = wx.BoxSizer(wx.VERTICAL)
368            main_sizer = wx.BoxSizer(wx.HORIZONTAL)
369            box_sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
370            buttons_sizer = wx.BoxSizer(wx.VERTICAL)
371
372            panel.SetSizer(panel_sizer)
373            panel_sizer.Fit(panel)
374
375            main_sizer.Add(list_box, proportion=1,
376                           flag=wx.EXPAND | wx.ALL,
377                           border=1)
378            main_sizer.Add(buttons_sizer, proportion=0,
379                           flag=wx.ALL,
380                           border=1)
381            for button in buttons:
382                buttons_sizer.Add(button, proportion=0,
383                                  flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM,
384                                  border=3)
385            box_sizer.Add(panel, proportion=1,
386                          flag=wx.EXPAND | wx.ALL,
387                          border=1)
388            panel_sizer.Add(main_sizer, proportion=1,
389                            flag=wx.EXPAND | wx.ALL,
390                            border=1)
391            panel_sizer.Add(description, proportion=0,
392                            flag=wx.EXPAND | wx.ALL,
393                            border=1)
394            return box_sizer
395
396        location_boxsizer = layout_list_box(
397            box=self.location_box,
398            panel=self.location_panel,
399            list_box=self.lblocations,
400            buttons=[self.bwizard, self.rename_location_button,
401                     self.delete_location_button,
402                     self.download_location_button],
403            description=self.llocation)
404        mapset_boxsizer = layout_list_box(
405            box=self.mapset_box,
406            panel=self.mapset_panel,
407            list_box=self.lbmapsets,
408            buttons=[self.bmapset, self.rename_mapset_button,
409                     self.delete_mapset_button],
410            description=self.lmapset)
411
412        # location and mapset sizer
413        location_mapset_sizer.Add(location_boxsizer, proportion=1,
414                                  flag=wx.LEFT | wx.RIGHT | wx.EXPAND,
415                                  border=3)
416        location_mapset_sizer.Add(mapset_boxsizer, proportion=1,
417                                  flag=wx.RIGHT | wx.EXPAND,
418                                  border=3)
419
420        # buttons
421        btns_sizer.Add(self.bstart, proportion=0,
422                       flag=wx.ALIGN_CENTER_HORIZONTAL |
423                       wx.ALIGN_CENTER_VERTICAL |
424                       wx.ALL,
425                       border=5)
426        btns_sizer.Add(self.bexit, proportion=0,
427                       flag=wx.ALIGN_CENTER_HORIZONTAL |
428                       wx.ALIGN_CENTER_VERTICAL |
429                       wx.ALL,
430                       border=5)
431        btns_sizer.Add(self.bhelp, proportion=0,
432                       flag=wx.ALIGN_CENTER_HORIZONTAL |
433                       wx.ALIGN_CENTER_VERTICAL |
434                       wx.ALL,
435                       border=5)
436
437        # main sizer
438        sizer.Add(self.hbitmap,
439                  proportion=0,
440                  flag=wx.ALIGN_CENTER_VERTICAL |
441                  wx.ALIGN_CENTER_HORIZONTAL |
442                  wx.ALL,
443                  border=3)  # image
444        sizer.Add(gisdbase_boxsizer, proportion=0,
445                  flag=wx.RIGHT | wx.LEFT | wx.TOP | wx.EXPAND,
446                  border=3)  # GISDBASE setting
447
448        # warning/error message
449        sizer.Add(self.lmessage,
450                  proportion=0,
451                  flag=wx.ALIGN_LEFT | wx.ALL | wx.EXPAND, border=5)
452        sizer.Add(location_mapset_sizer, proportion=1,
453                  flag=wx.RIGHT | wx.LEFT | wx.EXPAND,
454                  border=1)
455        sizer.Add(btns_sizer, proportion=0,
456                  flag=wx.ALIGN_CENTER_VERTICAL |
457                  wx.ALIGN_CENTER_HORIZONTAL |
458                  wx.RIGHT | wx.LEFT,
459                  border=3)
460
461        self.panel.SetAutoLayout(True)
462        self.panel.SetSizer(sizer)
463        sizer.Fit(self.panel)
464        sizer.SetSizeHints(self)
465        self.Layout()
466
467    def _showWarning(self, text):
468        """Displays a warning, hint or info message to the user.
469
470        This function can be used for all kinds of messages except for
471        error messages.
472
473        .. note::
474            There is no cleaning procedure. You should call _hideMessage when
475            you know that there is everything correct now.
476        """
477        self.lmessage.SetLabel(text)
478        self.sizer.Layout()
479
480    def _showError(self, text):
481        """Displays a error message to the user.
482
483        This function should be used only when something serious and unexpected
484        happens, otherwise _showWarning should be used.
485
486        .. note::
487            There is no cleaning procedure. You should call _hideMessage when
488            you know that there is everything correct now.
489        """
490        self.lmessage.SetLabel(_("Error: {text}").format(text=text))
491        self.sizer.Layout()
492
493    def _hideMessage(self):
494        """Clears/hides the error message."""
495        # we do no hide widget
496        # because we do not want the dialog to change the size
497        self.lmessage.SetLabel("")
498        self.sizer.Layout()
499
500    def GetRCValue(self, value):
501        """Return GRASS variable (read from GISRC)
502        """
503        if value in self.grassrc:
504            return self.grassrc[value]
505        else:
506            return None
507
508    def SuggestDatabase(self):
509        """Suggest (set) possible GRASS Database value"""
510        # only if nothing is set (<UNKNOWN> comes from init script)
511        if self.GetRCValue("LOCATION_NAME") != "<UNKNOWN>":
512            return
513        path = get_possible_database_path()
514        if path:
515            try:
516                self.tgisdbase.SetValue(path)
517            except UnicodeDecodeError:
518                # restore previous state
519                # wizard gives error in this case, we just ignore
520                path = None
521                self.tgisdbase.SetValue(self.gisdbase)
522            # if we still have path
523            if path:
524                self.gisdbase = path
525                self.OnSetDatabase(None)
526        else:
527            # nothing found
528            # TODO: should it be warning, hint or message?
529            self._showWarning(_(
530                'GRASS needs a directory (GRASS database) '
531                'in which to store its data. '
532                'Create one now if you have not already done so. '
533                'A popular choice is "grassdata", located in '
534                'your home directory. '
535                'Press Browse button to select the directory.'))
536
537    def OnWizard(self, event):
538        """Location wizard started"""
539        from location_wizard.wizard import LocationWizard
540        gWizard = LocationWizard(parent=self,
541                                 grassdatabase=self.tgisdbase.GetValue())
542        if gWizard.location is not None:
543            self.tgisdbase.SetValue(gWizard.grassdatabase)
544            self.OnSetDatabase(None)
545            self.UpdateMapsets(os.path.join(self.gisdbase, gWizard.location))
546            self.lblocations.SetSelection(
547                self.listOfLocations.index(
548                    gWizard.location))
549            self.lbmapsets.SetSelection(0)
550            self.SetLocation(self.gisdbase, gWizard.location, 'PERMANENT')
551            if gWizard.georeffile:
552                message = _("Do you want to import <%(name)s> to the newly created location?") % {
553                    'name': gWizard.georeffile}
554                dlg = wx.MessageDialog(parent=self, message=message, caption=_(
555                    "Import data?"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
556                dlg.CenterOnParent()
557                if dlg.ShowModal() == wx.ID_YES:
558                    self.ImportFile(gWizard.georeffile)
559                dlg.Destroy()
560            if gWizard.default_region:
561                defineRegion = RegionDef(self, location=gWizard.location)
562                defineRegion.CenterOnParent()
563                defineRegion.ShowModal()
564                defineRegion.Destroy()
565
566            if gWizard.user_mapset:
567                self.OnCreateMapset(event)
568
569    def ImportFile(self, filePath):
570        """Tries to import file as vector or raster.
571
572        If successfull sets default region from imported map.
573        """
574        RunCommand('db.connect', flags='c')
575        mapName = os.path.splitext(os.path.basename(filePath))[0]
576        vectors = RunCommand('v.in.ogr', input=filePath, flags='l',
577                             read=True)
578
579        wx.BeginBusyCursor()
580        wx.GetApp().Yield()
581        if vectors:
582            # vector detected
583            returncode, error = RunCommand(
584                'v.in.ogr', input=filePath, output=mapName, flags='e',
585                getErrorMsg=True)
586        else:
587            returncode, error = RunCommand(
588                'r.in.gdal', input=filePath, output=mapName, flags='e',
589                getErrorMsg=True)
590        wx.EndBusyCursor()
591
592        if returncode != 0:
593            GError(
594                parent=self,
595                message=_(
596                    "Import of <%(name)s> failed.\n"
597                    "Reason: %(msg)s") % ({
598                        'name': filePath,
599                        'msg': error}))
600        else:
601            GMessage(
602                message=_(
603                    "Data file <%(name)s> imported successfully. "
604                    "The location's default region was set from this imported map.") % {
605                    'name': filePath},
606                parent=self)
607
608    # the event can be refactored out by using lambda in bind
609    def RenameMapset(self, event):
610        """Rename selected mapset
611        """
612        location = self.listOfLocations[self.lblocations.GetSelection()]
613        mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
614        if mapset == 'PERMANENT':
615            GMessage(
616                parent=self, message=_(
617                    'Mapset <PERMANENT> is required for valid GRASS location.\n\n'
618                    'This mapset cannot be renamed.'))
619            return
620
621        dlg = TextEntryDialog(
622            parent=self,
623            message=_('Current name: %s\n\nEnter new name:') %
624            mapset,
625            caption=_('Rename selected mapset'),
626            validator=GenericValidator(
627                grass.legal_name,
628                self._nameValidationFailed))
629
630        if dlg.ShowModal() == wx.ID_OK:
631            newmapset = dlg.GetValue()
632            if newmapset == mapset:
633                dlg.Destroy()
634                return
635
636            if newmapset in self.listOfMapsets:
637                wx.MessageBox(
638                    parent=self, caption=_('Message'), message=_(
639                        'Unable to rename mapset.\n\n'
640                        'Mapset <%s> already exists in location.') %
641                    newmapset, style=wx.OK | wx.ICON_INFORMATION | wx.CENTRE)
642            else:
643                try:
644                    sutils.rename_mapset(self.gisdbase, location,
645                                         mapset, newmapset)
646                    self.OnSelectLocation(None)
647                    self.lbmapsets.SetSelection(
648                        self.listOfMapsets.index(newmapset))
649                except Exception as e:
650                    wx.MessageBox(
651                        parent=self,
652                        caption=_('Error'),
653                        message=_('Unable to rename mapset.\n\n%s') %
654                        e,
655                        style=wx.OK | wx.ICON_ERROR | wx.CENTRE)
656
657        dlg.Destroy()
658
659    def RenameLocation(self, event):
660        """Rename selected location
661        """
662        location = self.listOfLocations[self.lblocations.GetSelection()]
663
664        dlg = TextEntryDialog(
665            parent=self,
666            message=_('Current name: %s\n\nEnter new name:') %
667            location,
668            caption=_('Rename selected location'),
669            validator=GenericValidator(
670                grass.legal_name,
671                self._nameValidationFailed))
672
673        if dlg.ShowModal() == wx.ID_OK:
674            newlocation = dlg.GetValue()
675            if newlocation == location:
676                dlg.Destroy()
677                return
678
679            if newlocation in self.listOfLocations:
680                wx.MessageBox(
681                    parent=self, caption=_('Message'), message=_(
682                        'Unable to rename location.\n\n'
683                        'Location <%s> already exists in GRASS database.') %
684                    newlocation, style=wx.OK | wx.ICON_INFORMATION | wx.CENTRE)
685            else:
686                try:
687                    sutils.rename_location(self.gisdbase,
688                                           location, newlocation)
689                    self.UpdateLocations(self.gisdbase)
690                    self.lblocations.SetSelection(
691                        self.listOfLocations.index(newlocation))
692                    self.UpdateMapsets(newlocation)
693                except Exception as e:
694                    wx.MessageBox(
695                        parent=self,
696                        caption=_('Error'),
697                        message=_('Unable to rename location.\n\n%s') %
698                        e,
699                        style=wx.OK | wx.ICON_ERROR | wx.CENTRE)
700
701        dlg.Destroy()
702
703    def DeleteMapset(self, event):
704        """Delete selected mapset
705        """
706        location = self.listOfLocations[self.lblocations.GetSelection()]
707        mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
708        if mapset == 'PERMANENT':
709            GMessage(
710                parent=self, message=_(
711                    'Mapset <PERMANENT> is required for valid GRASS location.\n\n'
712                    'This mapset cannot be deleted.'))
713            return
714
715        dlg = wx.MessageDialog(
716            parent=self,
717            message=_(
718                "Do you want to continue with deleting mapset <%(mapset)s> "
719                "from location <%(location)s>?\n\n"
720                "ALL MAPS included in this mapset will be "
721                "PERMANENTLY DELETED!") %
722            {'mapset': mapset, 'location': location},
723            caption=_("Delete selected mapset"),
724            style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
725
726        if dlg.ShowModal() == wx.ID_YES:
727            try:
728                sutils.delete_mapset(self.gisdbase, location, mapset)
729                self.OnSelectLocation(None)
730                self.lbmapsets.SetSelection(0)
731            except:
732                wx.MessageBox(message=_('Unable to delete mapset'))
733
734        dlg.Destroy()
735
736    def DeleteLocation(self, event):
737        """
738        Delete selected location
739        """
740
741        location = self.listOfLocations[self.lblocations.GetSelection()]
742
743        dlg = wx.MessageDialog(
744            parent=self,
745            message=_(
746                "Do you want to continue with deleting "
747                "location <%s>?\n\n"
748                "ALL MAPS included in this location will be "
749                "PERMANENTLY DELETED!") %
750            (location),
751            caption=_("Delete selected location"),
752            style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
753
754        if dlg.ShowModal() == wx.ID_YES:
755            try:
756                sutils.delete_location(self.gisdbase, location)
757                self.UpdateLocations(self.gisdbase)
758                self.lblocations.SetSelection(0)
759                self.OnSelectLocation(None)
760                self.lbmapsets.SetSelection(0)
761            except:
762                wx.MessageBox(message=_('Unable to delete location'))
763
764        dlg.Destroy()
765
766    def DownloadLocation(self, event):
767        """Download location online"""
768        from startup.locdownload import LocationDownloadDialog
769
770        loc_download = LocationDownloadDialog(parent=self, database=self.gisdbase)
771        loc_download.Centre()
772        loc_download.ShowModal()
773        location = loc_download.GetLocation()
774        if location:
775            # get the new location to the list
776            self.UpdateLocations(self.gisdbase)
777            # seems to be used in similar context
778            self.UpdateMapsets(os.path.join(self.gisdbase, location))
779            self.lblocations.SetSelection(
780                self.listOfLocations.index(location))
781            # wizard does this as well, not sure if needed
782            self.SetLocation(self.gisdbase, location, 'PERMANENT')
783            # seems to be used in similar context
784            self.OnSelectLocation(None)
785        loc_download.Destroy()
786
787    def UpdateLocations(self, dbase):
788        """Update list of locations"""
789        try:
790            self.listOfLocations = GetListOfLocations(dbase)
791        except (UnicodeEncodeError, UnicodeDecodeError) as e:
792            GError(parent=self,
793                   message=_("Unicode error detected. "
794                             "Check your locale settings. Details: {0}").format(e),
795                   showTraceback=False)
796
797        self.lblocations.Clear()
798        self.lblocations.InsertItems(self.listOfLocations, 0)
799
800        if len(self.listOfLocations) > 0:
801            self._hideMessage()
802            self.lblocations.SetSelection(0)
803        else:
804            self.lblocations.SetSelection(wx.NOT_FOUND)
805            self._showWarning(_("No GRASS Location found in '%s'."
806                                " Create a new Location or choose different"
807                                " GRASS database directory.")
808                              % self.gisdbase)
809
810        return self.listOfLocations
811
812    def UpdateMapsets(self, location):
813        """Update list of mapsets"""
814        self.FormerMapsetSelection = wx.NOT_FOUND  # for non-selectable item
815
816        self.listOfMapsetsSelectable = list()
817        self.listOfMapsets = GetListOfMapsets(self.gisdbase, location)
818
819        self.lbmapsets.Clear()
820
821        # disable mapset with denied permission
822        locationName = os.path.basename(location)
823
824        ret = RunCommand('g.mapset',
825                         read=True,
826                         flags='l',
827                         location=locationName,
828                         gisdbase=self.gisdbase)
829
830        if ret:
831            for line in ret.splitlines():
832                self.listOfMapsetsSelectable += line.split(' ')
833        else:
834            self.SetLocation(self.gisdbase, locationName, "PERMANENT")
835            # first run only
836            self.listOfMapsetsSelectable = copy.copy(self.listOfMapsets)
837
838        disabled = []
839        idx = 0
840        for mapset in self.listOfMapsets:
841            if mapset not in self.listOfMapsetsSelectable or \
842                    get_lockfile_if_present(self.gisdbase,
843                                            locationName, mapset):
844                disabled.append(idx)
845            idx += 1
846
847        self.lbmapsets.InsertItems(self.listOfMapsets, 0, disabled=disabled)
848
849        return self.listOfMapsets
850
851    def OnSelectLocation(self, event):
852        """Location selected"""
853        if event:
854            self.lblocations.SetSelection(event.GetIndex())
855
856        if self.lblocations.GetSelection() != wx.NOT_FOUND:
857            self.UpdateMapsets(
858                os.path.join(
859                    self.gisdbase,
860                    self.listOfLocations[
861                        self.lblocations.GetSelection()]))
862        else:
863            self.listOfMapsets = []
864
865        disabled = []
866        idx = 0
867        try:
868            locationName = self.listOfLocations[
869                self.lblocations.GetSelection()]
870        except IndexError:
871            locationName = ''
872
873        for mapset in self.listOfMapsets:
874            if mapset not in self.listOfMapsetsSelectable or \
875                    get_lockfile_if_present(self.gisdbase,
876                                            locationName, mapset):
877                disabled.append(idx)
878            idx += 1
879
880        self.lbmapsets.Clear()
881        self.lbmapsets.InsertItems(self.listOfMapsets, 0, disabled=disabled)
882
883        if len(self.listOfMapsets) > 0:
884            self.lbmapsets.SetSelection(0)
885            if locationName:
886                # enable start button when location and mapset is selected
887                self.bstart.Enable()
888                self.bstart.SetFocus()
889                self.bmapset.Enable()
890                # replacing disabled choice, perhaps just mapset needed
891                self.rename_location_button.Enable()
892                self.delete_location_button.Enable()
893                self.rename_mapset_button.Enable()
894                self.delete_mapset_button.Enable()
895        else:
896            self.lbmapsets.SetSelection(wx.NOT_FOUND)
897            self.bstart.Enable(False)
898            self.bmapset.Enable(False)
899            # this all was originally a choice, perhaps just mapset needed
900            self.rename_location_button.Enable(False)
901            self.delete_location_button.Enable(False)
902            self.rename_mapset_button.Enable(False)
903            self.delete_mapset_button.Enable(False)
904
905    def OnSelectMapset(self, event):
906        """Mapset selected"""
907        self.lbmapsets.SetSelection(event.GetIndex())
908
909        if event.GetText() not in self.listOfMapsetsSelectable:
910            self.lbmapsets.SetSelection(self.FormerMapsetSelection)
911        else:
912            self.FormerMapsetSelection = event.GetIndex()
913            event.Skip()
914
915    def OnSetDatabase(self, event):
916        """Database set"""
917        gisdbase = self.tgisdbase.GetValue()
918        self._hideMessage()
919        if not os.path.exists(gisdbase):
920            self._showError(_("Path '%s' doesn't exist.") % gisdbase)
921            return
922
923        self.gisdbase = self.tgisdbase.GetValue()
924        self.UpdateLocations(self.gisdbase)
925
926        self.OnSelectLocation(None)
927
928    def OnBrowse(self, event):
929        """'Browse' button clicked"""
930        if not event:
931            defaultPath = os.getenv('HOME')
932        else:
933            defaultPath = ""
934
935        dlg = wx.DirDialog(parent=self, message=_("Choose GIS Data Directory"),
936                           defaultPath=defaultPath, style=wx.DD_DEFAULT_STYLE)
937
938        if dlg.ShowModal() == wx.ID_OK:
939            self.gisdbase = dlg.GetPath()
940            self.tgisdbase.SetValue(self.gisdbase)
941            self.OnSetDatabase(event)
942
943        dlg.Destroy()
944
945    def OnCreateMapset(self, event):
946        """Create new mapset"""
947        dlg = NewMapsetDialog(
948            parent=self,
949            default=self._getDefaultMapsetName(),
950            validation_failed_handler=self._nameValidationFailed,
951            help_hanlder=self.OnHelp,
952        )
953        if dlg.ShowModal() == wx.ID_OK:
954            mapset = dlg.GetValue()
955            return self.CreateNewMapset(mapset=mapset)
956        else:
957            return False
958
959    def CreateNewMapset(self, mapset):
960        if mapset in self.listOfMapsets:
961            GMessage(parent=self,
962                     message=_("Mapset <%s> already exists.") % mapset)
963            return False
964
965        if mapset.lower() == 'ogr':
966            dlg1 = wx.MessageDialog(
967                parent=self,
968                message=_(
969                    "Mapset <%s> is reserved for direct "
970                    "read access to OGR layers. Please consider to use "
971                    "another name for your mapset.\n\n"
972                    "Are you really sure that you want to create this mapset?") %
973                mapset,
974                caption=_("Reserved mapset name"),
975                style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
976            ret = dlg1.ShowModal()
977            dlg1.Destroy()
978            if ret == wx.ID_NO:
979                dlg1.Destroy()
980                return False
981
982        try:
983            self.gisdbase = self.tgisdbase.GetValue()
984            location = self.listOfLocations[self.lblocations.GetSelection()]
985            create_mapset(self.gisdbase, location, mapset)
986            self.OnSelectLocation(None)
987            self.lbmapsets.SetSelection(self.listOfMapsets.index(mapset))
988            self.bstart.SetFocus()
989
990            return True
991        except Exception as e:
992            GError(parent=self,
993                   message=_("Unable to create new mapset: %s") % e,
994                   showTraceback=False)
995            return False
996
997    def OnStart(self, event):
998        """'Start GRASS' button clicked"""
999        dbase = self.tgisdbase.GetValue()
1000        location = self.listOfLocations[self.lblocations.GetSelection()]
1001        mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
1002
1003        lockfile = get_lockfile_if_present(dbase, location, mapset)
1004        if lockfile:
1005            dlg = wx.MessageDialog(
1006                parent=self,
1007                message=_(
1008                    "GRASS is already running in selected mapset <%(mapset)s>\n"
1009                    "(file %(lock)s found).\n\n"
1010                    "Concurrent use not allowed.\n\n"
1011                    "Do you want to try to remove .gislock (note that you "
1012                    "need permission for this operation) and continue?") %
1013                {'mapset': mapset, 'lock': lockfile},
1014                caption=_("Lock file found"),
1015                style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE)
1016
1017            ret = dlg.ShowModal()
1018            dlg.Destroy()
1019            if ret == wx.ID_YES:
1020                dlg1 = wx.MessageDialog(
1021                    parent=self,
1022                    message=_(
1023                        "ARE YOU REALLY SURE?\n\n"
1024                        "If you really are running another GRASS session doing this "
1025                        "could corrupt your data. Have another look in the processor "
1026                        "manager just to be sure..."),
1027                    caption=_("Lock file found"),
1028                    style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE)
1029
1030                ret = dlg1.ShowModal()
1031                dlg1.Destroy()
1032
1033                if ret == wx.ID_YES:
1034                    try:
1035                        os.remove(lockfile)
1036                    except IOError as e:
1037                        GError(_("Unable to remove '%(lock)s'.\n\n"
1038                                 "Details: %(reason)s") % {'lock': lockfile, 'reason': e})
1039                else:
1040                    return
1041            else:
1042                return
1043        self.SetLocation(dbase, location, mapset)
1044        self.ExitSuccessfully()
1045
1046    def SetLocation(self, dbase, location, mapset):
1047        SetSessionMapset(dbase, location, mapset)
1048
1049    def _getDefaultMapsetName(self):
1050        """Returns default name for mapset."""
1051        try:
1052            defaultName = getpass.getuser()
1053            # raise error if not ascii (not valid mapset name)
1054            defaultName.encode('ascii')
1055        except:  # whatever might go wrong
1056            defaultName = 'user'
1057
1058        return defaultName
1059
1060    def ExitSuccessfully(self):
1061        self.Destroy()
1062        sys.exit(self.exit_success)
1063
1064    def OnExit(self, event):
1065        """'Exit' button clicked"""
1066        self.Destroy()
1067        sys.exit(self.exit_user_requested)
1068
1069    def OnHelp(self, event):
1070        """'Help' button clicked"""
1071
1072        # help text in lib/init/helptext.html
1073        RunCommand('g.manual', entry='helptext')
1074
1075    def OnCloseWindow(self, event):
1076        """Close window event"""
1077        event.Skip()
1078        sys.exit(self.exit_user_requested)
1079
1080    def _nameValidationFailed(self, ctrl):
1081        message = _(
1082            "Name <%(name)s> is not a valid name for location or mapset. "
1083            "Please use only ASCII characters excluding %(chars)s "
1084            "and space.") % {
1085            'name': ctrl.GetValue(),
1086            'chars': '/"\'@,=*~'}
1087        GError(parent=self, message=message, caption=_("Invalid name"))
1088
1089
1090class GListBox(ListCtrl, listmix.ListCtrlAutoWidthMixin):
1091    """Use wx.ListCtrl instead of wx.ListBox, different style for
1092    non-selectable items (e.g. mapsets with denied permission)"""
1093
1094    def __init__(self, parent, id, size,
1095                 choices, disabled=[]):
1096        ListCtrl.__init__(
1097            self, parent, id, size=size, style=wx.LC_REPORT | wx.LC_NO_HEADER |
1098            wx.LC_SINGLE_SEL | wx.BORDER_SUNKEN)
1099
1100        listmix.ListCtrlAutoWidthMixin.__init__(self)
1101
1102        self.InsertColumn(0, '')
1103
1104        self.selected = wx.NOT_FOUND
1105
1106        self._LoadData(choices, disabled)
1107
1108    def _LoadData(self, choices, disabled=[]):
1109        """Load data into list
1110
1111        :param choices: list of item
1112        :param disabled: list of indices of non-selectable items
1113        """
1114        idx = 0
1115        count = self.GetItemCount()
1116        for item in choices:
1117            index = self.InsertItem(count + idx, item)
1118            self.SetItem(index, 0, item)
1119
1120            if idx in disabled:
1121                self.SetItemTextColour(idx, wx.Colour(150, 150, 150))
1122            idx += 1
1123
1124    def Clear(self):
1125        self.DeleteAllItems()
1126
1127    def InsertItems(self, choices, pos, disabled=[]):
1128        self._LoadData(choices, disabled)
1129
1130    def SetSelection(self, item, force=False):
1131        if item !=  wx.NOT_FOUND and \
1132                (platform.system() != 'Windows' or force):
1133            # Windows -> FIXME
1134            self.SetItemState(
1135                item,
1136                wx.LIST_STATE_SELECTED,
1137                wx.LIST_STATE_SELECTED)
1138
1139        self.selected = item
1140
1141    def GetSelection(self):
1142        return self.selected
1143
1144
1145class StartUp(wx.App):
1146    """Start-up application"""
1147
1148    def OnInit(self):
1149        StartUp = GRASSStartup()
1150        StartUp.CenterOnScreen()
1151        self.SetTopWindow(StartUp)
1152        StartUp.Show()
1153        StartUp.SuggestDatabase()
1154
1155        return 1
1156
1157if __name__ == "__main__":
1158    if os.getenv("GISBASE") is None:
1159        sys.exit("Failed to start GUI, GRASS GIS is not running.")
1160
1161    GRASSStartUp = StartUp(0)
1162    GRASSStartUp.MainLoop()
1163