[GRASS-SVN] r71310 - in grass/trunk/gui/wxpython: . startup
svn_grass at osgeo.org
svn_grass at osgeo.org
Sun Jul 23 19:44:12 PDT 2017
Author: wenzeslaus
Date: 2017-07-23 19:44:12 -0700 (Sun, 23 Jul 2017)
New Revision: 71310
Added:
grass/trunk/gui/wxpython/startup/
grass/trunk/gui/wxpython/startup/__init__.py
grass/trunk/gui/wxpython/startup/locdownload.py
Modified:
grass/trunk/gui/wxpython/Makefile
grass/trunk/gui/wxpython/gis_set.py
Log:
wxGUI: dialog to download locations
As Download button from the startup window.
Useful when a user starts for the first time and does not have any data.
Potentially for any source. Now for:
https://grass.osgeo.org/download/sample-data/
Modified: grass/trunk/gui/wxpython/Makefile
===================================================================
--- grass/trunk/gui/wxpython/Makefile 2017-07-23 02:21:32 UTC (rev 71309)
+++ grass/trunk/gui/wxpython/Makefile 2017-07-24 02:44:12 UTC (rev 71310)
@@ -11,7 +11,8 @@
SRCFILES := $(wildcard icons/*.py scripts/*.py xml/*) \
$(wildcard animation/*.py core/*.py datacatalog/*.py dbmgr/*.py gcp/*.py gmodeler/*.py \
gui_core/*.py iclass/*.py lmgr/*.py location_wizard/*.py mapwin/*.py mapdisp/*.py \
- mapswipe/*.py modules/*.py nviz/*.py psmap/*.py rdigit/*.py rlisetup/*.py timeline/*.py vdigit/*.py \
+ mapswipe/*.py modules/*.py nviz/*.py psmap/*.py rdigit/*.py \
+ rlisetup/*.py startup/*.py timeline/*.py vdigit/*.py \
vnet/*.py web_services/*.py wxplot/*.py iscatt/*.py tplot/*.py photo2image/*.py image2target/*.py) \
gis_set.py gis_set_error.py wxgui.py README
@@ -20,7 +21,8 @@
PYDSTDIRS := $(patsubst %,$(DSTDIR)/%,animation core datacatalog dbmgr gcp gmodeler \
gui_core iclass lmgr location_wizard mapwin mapdisp modules nviz psmap \
- mapswipe vdigit wxplot web_services rdigit rlisetup vnet timeline iscatt tplot photo2image image2target)
+ mapswipe vdigit wxplot web_services rdigit rlisetup startup \
+ vnet timeline iscatt tplot photo2image image2target)
DSTDIRS := $(patsubst %,$(DSTDIR)/%,icons scripts xml)
Modified: grass/trunk/gui/wxpython/gis_set.py
===================================================================
--- grass/trunk/gui/wxpython/gis_set.py 2017-07-23 02:21:32 UTC (rev 71309)
+++ grass/trunk/gui/wxpython/gis_set.py 2017-07-24 02:44:12 UTC (rev 71310)
@@ -197,6 +197,10 @@
# GTC Delete location
label=_("De&lete"))
self.delete_location_button.SetToolTip(_("Delete selected location"))
+ self.download_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
+ label=_("Do&wnload"))
+ self.download_location_button.SetToolTip(_("Download sample location"))
+
self.rename_mapset_button = Button(parent=self.mapset_panel, id=wx.ID_ANY,
# GTC Rename mapset
label=_("&Rename"))
@@ -238,6 +242,7 @@
self.rename_location_button.Bind(wx.EVT_BUTTON, self.RenameLocation)
self.delete_location_button.Bind(wx.EVT_BUTTON, self.DeleteLocation)
+ self.download_location_button.Bind(wx.EVT_BUTTON, self.DownloadLocation)
self.rename_mapset_button.Bind(wx.EVT_BUTTON, self.RenameMapset)
self.delete_mapset_button.Bind(wx.EVT_BUTTON, self.DeleteMapset)
@@ -389,7 +394,8 @@
panel=self.location_panel,
list_box=self.lblocations,
buttons=[self.bwizard, self.rename_location_button,
- self.delete_location_button],
+ self.delete_location_button,
+ self.download_location_button],
description=self.llocation)
mapset_boxsizer = layout_list_box(
box=self.mapset_box,
@@ -816,6 +822,26 @@
dlg.Destroy()
+ def DownloadLocation(self, event):
+ """Download location online"""
+ from startup.locdownload import LocationDownloadDialog
+
+ loc_download = LocationDownloadDialog(parent=self, database=self.gisdbase)
+ loc_download.ShowModal()
+ location = loc_download.GetLocation()
+ if location:
+ # get the new location to the list
+ self.UpdateLocations(self.gisdbase)
+ # seems to be used in similar context
+ self.UpdateMapsets(os.path.join(self.gisdbase, location))
+ self.lblocations.SetSelection(
+ self.listOfLocations.index(location))
+ # wizard does this as well, not sure if needed
+ self.SetLocation(self.gisdbase, location, 'PERMANENT')
+ # seems to be used in similar context
+ self.OnSelectLocation(None)
+ loc_download.Destroy()
+
def UpdateLocations(self, dbase):
"""Update list of locations"""
try:
Added: grass/trunk/gui/wxpython/startup/__init__.py
===================================================================
--- grass/trunk/gui/wxpython/startup/__init__.py (rev 0)
+++ grass/trunk/gui/wxpython/startup/__init__.py 2017-07-24 02:44:12 UTC (rev 71310)
@@ -0,0 +1,3 @@
+all = [
+ 'locdownload',
+]
Added: grass/trunk/gui/wxpython/startup/locdownload.py
===================================================================
--- grass/trunk/gui/wxpython/startup/locdownload.py (rev 0)
+++ grass/trunk/gui/wxpython/startup/locdownload.py 2017-07-24 02:44:12 UTC (rev 71310)
@@ -0,0 +1,480 @@
+"""
+ at package startup.locdownload
+
+ at brief GRASS Location Download Management
+
+Classes:
+ - LocationDownloadPanel
+ - LocationDownloadDialog
+ - DownloadError
+
+(C) 2017 by Vaclav Petras the GRASS Development Team
+
+This program is free software under the GNU General Public License
+(>=v2). Read the file COPYING that comes with GRASS for details.
+
+ at author Vaclav Petras <wenzeslaus gmail com>
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+import tempfile
+import shutil
+
+try:
+ from urllib2 import HTTPError, URLError
+ from urllib import urlopen, urlretrieve
+except ImportError:
+ # there is also HTTPException, perhaps change to list
+ from urllib.error import HTTPError, URLError
+ from urllib.request import urlopen, urlretrieve
+
+import wx
+from wx.lib.newevent import NewEvent
+
+from grass.script import debug
+from grass.script.utils import try_rmdir
+
+from grass.script.setup import set_gui_path
+set_gui_path()
+
+from core.debug import Debug
+from core.utils import _
+from core.gthread import gThread
+from gui_core.wrap import Button
+
+
+# TODO: labels (and descriptions) translatable?
+LOCATIONS = [
+ {
+ "label": "Complete NC location",
+ "url": "https://grass.osgeo.org/sampledata/north_carolina/nc_spm_08_grass7.tar.gz",
+ },
+ {
+ "label": "Basic NC location",
+ "url": "https://grass.osgeo.org/sampledata/north_carolina/nc_basic_spm_grass7.tar.gz",
+ },
+ {
+ "label": "World location in LatLong/WGS84",
+ "url": "https://grass.osgeo.org/sampledata/worldlocation.tar.gz",
+ },
+ {
+ "label": "Spearfish (SD) location",
+ "url": "https://grass.osgeo.org/sampledata/spearfish_grass70data-0.3.tar.gz",
+ },
+ {
+ "label": "Piemonte, Italy data set",
+ "url": "http://geodati.fmach.it/gfoss_geodata/libro_gfoss/grassdata_piemonte_utm32n_wgs84_grass7.tar.gz",
+ },
+ {
+ "label": "Slovakia 3D precipitation voxel data set",
+ "url": "https://grass.osgeo.org/uploads/grass/sampledata/slovakia3d_grass7.tar.gz",
+ },
+ {
+ "label": "Fire simulation sample data",
+ "url": "https://grass.osgeo.org/sampledata/fire_grass6data.tar.gz",
+ },
+]
+
+
+class DownloadError(Exception):
+ """Error happened during download or when processing the file"""
+ pass
+
+
+# copy from g.extension, potentially move to library
+def move_extracted_files(extract_dir, target_dir, files):
+ """Fix state of extracted file by moving them to different diretcory
+
+ When extracting, it is not clear what will be the root directory
+ or if there will be one at all. So this function moves the files to
+ a different directory in the way that if there was one directory extracted,
+ the contained files are moved.
+ """
+ debug("move_extracted_files({0})".format(locals()))
+ if len(files) == 1:
+ shutil.copytree(os.path.join(extract_dir, files[0]), target_dir)
+ else:
+ if not os.path.exists(target_dir):
+ os.mkdir(target_dir)
+ for file_name in files:
+ actual_file = os.path.join(extract_dir, file_name)
+ if os.path.isdir(actual_file):
+ # copy_tree() from distutils failed to create
+ # directories before copying files time to time
+ # (when copying to recently deleted directory)
+ shutil.copytree(actual_file,
+ os.path.join(target_dir, file_name))
+ else:
+ shutil.copy(actual_file, os.path.join(target_dir, file_name))
+
+
+# copy from g.extension, potentially move to library
+def extract_zip(name, directory, tmpdir):
+ """Extract a ZIP file into a directory"""
+ debug("extract_zip(name={name}, directory={directory},"
+ " tmpdir={tmpdir})".format(name=name, directory=directory,
+ tmpdir=tmpdir), 3)
+ try:
+ import zipfile
+ zip_file = zipfile.ZipFile(name, mode='r')
+ file_list = zip_file.namelist()
+ # we suppose we can write to parent of the given dir
+ # (supposing a tmp dir)
+ extract_dir = os.path.join(tmpdir, 'extract_dir')
+ os.mkdir(extract_dir)
+ for subfile in file_list:
+ # this should be safe in Python 2.7.4
+ zip_file.extract(subfile, extract_dir)
+ files = os.listdir(extract_dir)
+ move_extracted_files(extract_dir=extract_dir,
+ target_dir=directory, files=files)
+ except zipfile.BadZipfile as error:
+ raise DownloadError(_("ZIP file is unreadable: {0}").format(error))
+
+
+# copy from g.extension, potentially move to library
+def extract_tar(name, directory, tmpdir):
+ """Extract a TAR or a similar file into a directory"""
+ debug("extract_tar(name={name}, directory={directory},"
+ " tmpdir={tmpdir})".format(name=name, directory=directory,
+ tmpdir=tmpdir), 3)
+ try:
+ import tarfile # we don't need it anywhere else
+ tar = tarfile.open(name)
+ extract_dir = os.path.join(tmpdir, 'extract_dir')
+ os.mkdir(extract_dir)
+ tar.extractall(path=extract_dir)
+ files = os.listdir(extract_dir)
+ move_extracted_files(extract_dir=extract_dir,
+ target_dir=directory, files=files)
+ except tarfile.TarError as error:
+ raise DownloadError(_("Archive file is unreadable: {0}").format(error))
+
+extract_tar.supported_formats = ['tar.gz', 'gz', 'bz2', 'tar', 'gzip', 'targz']
+
+
+# based on g.extension, potentially move to library
+def download_end_extract(source):
+ """Download a file (archive) from URL and uncompress it"""
+ tmpdir = tempfile.mkdtemp()
+ directory = os.path.join(tmpdir, 'location')
+ if source.endswith('.zip'):
+ archive_name = os.path.join(tmpdir, 'location.zip')
+ filename, headers = urlretrieve(source, archive_name)
+ if headers.get('content-type', '') != 'application/zip':
+ raise DownloadError(
+ _("Download of <{url}> failed"
+ " or file <{name}> is not a ZIP file").format(
+ url=source, name=filename))
+ extract_zip(name=archive_name, directory=directory, tmpdir=tmpdir)
+ elif (source.endswith(".tar.gz") or
+ source.rsplit('.', 1)[1] in extract_tar.supported_formats):
+ if source.endswith(".tar.gz"):
+ ext = "tar.gz"
+ else:
+ ext = source.rsplit('.', 1)[1]
+ archive_name = os.path.join(tmpdir, 'extension.' + ext)
+ urlretrieve(source, archive_name)
+ # TODO: error handling for urlretrieve
+ extract_tar(name=archive_name, directory=directory, tmpdir=tmpdir)
+ else:
+ # probably programmer error
+ raise DownloadError(_("Unknown format '{0}'.").format(source))
+ assert os.path.isdir(directory)
+ return directory
+
+
+def download_location(url, name, database):
+ """Wrapper to return DownloadError by value
+
+ It also moves the location directory to the database.
+ """
+ try:
+ # TODO: the unpacking could go right to the path (but less
+ # robust) or replace copytree here with move
+ directory = download_end_extract(source=url)
+ destination = os.path.join(database, name)
+ if not is_location_valid(directory):
+ return _("Downloaded location is not valid")
+ shutil.copytree(src=directory, dst=destination)
+ try_rmdir(directory)
+ except DownloadError as error:
+ return error
+ return None
+
+
+# based on grass.py (to be moved to future "grass.init")
+def is_location_valid(location):
+ """Return True if GRASS Location is valid
+
+ :param location: path of a Location
+ """
+ # DEFAULT_WIND file should not be required until you do something
+ # that actually uses them. The check is just a heuristic; a directory
+ # containing a PERMANENT/DEFAULT_WIND file is probably a GRASS
+ # location, while a directory lacking it probably isn't.
+ # TODO: perhaps we can relax this and require only permanent
+ return os.access(os.path.join(location,
+ "PERMANENT", "DEFAULT_WIND"), os.F_OK)
+
+
+def location_name_from_url(url):
+ """Create location name from URL"""
+ return url.rsplit('/', 1)[1].split('.', 1)[0].replace("-", "_").replace(" ", "_")
+
+
+DownloadDoneEvent, EVT_DOWNLOAD_DONE = NewEvent()
+
+
+class LocationDownloadPanel(wx.Panel):
+ """Panel to select and initiate downloads of locations.
+
+ Has a place to report errors to user and also any potential problems
+ before the user hits the button.
+
+ In the future, it can potentially show also some details about what
+ will be downloaded. The choice widget can be also replaced.
+
+ For the future, there can be multiple panels with different methods
+ or sources, e.g. direct input of URL. These can be in separate tabs
+ of one panel (perhaps sharing the common background download and
+ message logic).
+ """
+ def __init__(self, parent, database, locations=LOCATIONS):
+ """
+
+ :param database: directory with G database to download to
+ :param locations: list of dictionaries with label and url
+ """
+ wx.Panel.__init__(self, parent=parent)
+
+ self._last_downloaded_location_name = None
+ self._download_in_progress = False
+ self.database = database
+ self.locations = locations
+
+ self.label = wx.StaticText(
+ parent=self,
+ label=_("Select from sample location at grass.osgeo.org"))
+
+ choices = []
+ for item in self.locations:
+ choices.append(item['label'])
+ self.choice = wx.Choice(parent=self, choices=choices)
+
+ self.choice.Bind(wx.EVT_CHOICE, self.OnChangeChoice)
+
+ self.download_button = Button(parent=self, id=wx.ID_ANY,
+ label=_("Do&wnload"))
+ self.download_button.SetToolTip(_("Download selected location"))
+ self.download_button.Bind(wx.EVT_BUTTON, self.OnDownload)
+ # TODO: add button for a link to an associated website?
+ # TODO: add thumbnail for each location?
+
+ # TODO: messages copied from gis_set.py, need this as API?
+ self.message = wx.StaticText(parent=self, size=(-1, 50))
+
+ # It is not clear if all wx versions supports color, so try-except.
+ # The color itself may not be correct for all platforms/system settings
+ # but in http://xoomer.virgilio.it/infinity77/wxPython/Widgets/wx.SystemSettings.html
+ # there is no 'warning' color.
+ try:
+ self.message.SetForegroundColour(wx.Colour(255, 0, 0))
+ except AttributeError:
+ pass
+
+ self._layout()
+
+ default = 0
+ self.choice.SetSelection(default)
+ self.CheckItem(self.locations[default])
+
+ self.thread = gThread()
+
+ def _layout(self):
+ """Create and layout sizers"""
+ vertical = wx.BoxSizer(wx.VERTICAL)
+ self.sizer = vertical
+
+ vertical.Add(self.label, proportion=0,
+ flag=wx.EXPAND | wx.ALL, border=10)
+ vertical.Add(self.choice, proportion=0,
+ flag=wx.EXPAND | wx.ALL, border=10)
+
+ button_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ button_sizer.AddStretchSpacer()
+ button_sizer.Add(self.download_button, proportion=0)
+
+ vertical.Add(button_sizer, proportion=0,
+ flag=wx.EXPAND | wx.ALL, border=10)
+ vertical.AddStretchSpacer()
+ vertical.Add(self.message, proportion=0,
+ flag=wx.ALIGN_CENTER_VERTICAL |
+ wx.ALIGN_LEFT | wx.ALL | wx.EXPAND, border=10)
+
+ self.SetSizer(vertical)
+ vertical.Fit(self)
+ self.Layout()
+ self.SetMinSize(self.GetBestSize())
+
+ def OnDownload(self, event):
+ """Handle user-initiated action of download"""
+ Debug.msg(1, "OnDownload")
+ if self._download_in_progress:
+ self._warning(_("Download in progress, wait until it is finished"))
+ index = self.choice.GetSelection()
+ self.DownloadItem(self.locations[index])
+
+ def DownloadItem(self, item):
+ """Download the selected item"""
+ Debug.msg(1, "DownloadItem: %s" % item)
+ # similar code as in CheckItem
+ url = item['url']
+ dirname = location_name_from_url(url)
+ destination = os.path.join(self.database, dirname)
+ if os.path.exists(destination):
+ self._error(_("Location named <%s> already exists,"
+ " download canceled") % dirname)
+ return
+
+ def download_complete_callback(event):
+ self._download_in_progress = False
+ errors = event.ret
+ if errors:
+ self._error(_("Download failed: %s") % errors)
+ else:
+ self._last_downloaded_location_name = dirname
+ self._warning(_("Download completed"))
+
+ self._download_in_progress = True
+ self._warning(_("Download in progress"))
+ self.thread.Run(callable=download_location,
+ url=url, name=dirname, database=self.database,
+ ondone=download_complete_callback)
+
+ def OnChangeChoice(self, event):
+ """React to user changing the selection"""
+ index = self.choice.GetSelection()
+ self.CheckItem(self.locations[index])
+
+ def CheckItem(self, item):
+ """Check what user selected and report potential issues"""
+ # similar code as in DownloadItem
+ url = item['url']
+ dirname = location_name_from_url(url)
+ destination = os.path.join(self.database, dirname)
+ if os.path.exists(destination):
+ self._warning(_("Location named <%s> already exists,"
+ " rename it first") % dirname)
+ return
+ else:
+ self._clearMessage()
+
+ def GetLocation(self):
+ """Get the name of the last location downloaded by the user"""
+ return self._last_downloaded_location_name
+
+ def _warning(self, text):
+ """Displays a warning, hint or info message to the user.
+
+ This function can be used for all kinds of messages except for
+ error messages.
+
+ .. note::
+ There is no cleaning procedure. You should call
+ _clearMessage() when you know that there is everything
+ correct.
+ """
+ self.message.SetLabel(text)
+ self.sizer.Layout()
+
+ def _error(self, text):
+ """Displays a error message to the user.
+
+ This function should be used only when something serious and unexpected
+ happens, otherwise _showWarning should be used.
+
+ .. note::
+ There is no cleaning procedure. You should call
+ _clearMessage() when you know that there is everything
+ correct.
+ """
+ self.message.SetLabel(_("Error: {text}").format(text=text))
+ self.sizer.Layout()
+
+ def _clearMessage(self):
+ """Clears/hides the error message."""
+ # we do no hide widget
+ # because we do not want the dialog to change the size
+ self.message.SetLabel("")
+ self.sizer.Layout()
+
+
+class LocationDownloadDialog(wx.Dialog):
+ """Dialog for download of locations
+
+ Contains the panel and Cancel button.
+ """
+ def __init__(self, parent, database,
+ title=_("GRASS GIS Location Download")):
+ """
+ :param database: database to download the location to
+ :param title: window title if the default is not appropriate
+ """
+ wx.Dialog.__init__(self, parent=parent, title=title)
+ self.panel = LocationDownloadPanel(parent=self, database=database)
+ close_button = Button(self, id=wx.ID_CLOSE)
+ close_button.Bind(wx.EVT_BUTTON, lambda event: self.Close())
+
+ sizer = wx.BoxSizer(wx.VERTICAL)
+ sizer.Add(self.panel)
+
+ button_sizer = wx.StdDialogButtonSizer()
+ button_sizer.Add(close_button, flag=wx.EXPAND)
+ button_sizer.Realize()
+
+ sizer.Add(button_sizer, flag=wx.EXPAND | wx.ALL, border=10)
+ self.SetSizer(sizer)
+ sizer.Fit(self)
+
+ self.Layout()
+
+ def GetLocation(self):
+ """Get the name of the last location downloaded by the user"""
+ return self.panel.GetLocation()
+
+
+def main():
+ """Tests the download dialog"""
+ if len(sys.argv) < 2:
+ sys.exit("Provide a test directory")
+ database = sys.argv[1]
+
+ app = wx.App()
+
+ if len(sys.argv) == 2 or sys.argv[2] == 'dialog':
+ window = LocationDownloadDialog(parent=None, database=database)
+ window.ShowModal()
+ location = window.GetLocation()
+ if location:
+ print(location)
+ window.Destroy()
+ elif sys.argv[2] == 'panel':
+ window = wx.Dialog(parent=None)
+ panel = LocationDownloadPanel(parent=window, database=database)
+ window.ShowModal()
+ location = panel.GetLocation()
+ if location:
+ print(location)
+ window.Destroy()
+ else:
+ print("Unknown settings: try dialog or panel")
+
+ app.MainLoop()
+
+
+if __name__ == '__main__':
+ main()
More information about the grass-commit
mailing list