From ef922e2344b029be7afe41f41913b6af07c8aca2 Mon Sep 17 00:00:00 2001 From: Simon Planzer Date: Thu, 21 Mar 2019 15:42:13 +1300 Subject: [PATCH] #47 improved srs selection (#54) --- .travis.yml | 9 -- README.md | 2 +- linz-data-importer/__init__.py | 4 + linz-data-importer/gui/Service_dialog.py | 21 ++- linz-data-importer/gui/Service_dialog_base.ui | 13 +- linz-data-importer/gui/__init__.py | 0 linz-data-importer/linz_data_importer.py | 25 ++- linz-data-importer/metadata.txt | 6 +- linz-data-importer/plugin_upload.py | 107 ------------- linz-data-importer/service_data.py | 42 +++-- linz-data-importer/tablemodel.py | 145 ++++++++++++++---- .../tests/test_ldi_integration.py | 13 +- linz-data-importer/tests/test_ldi_plugin.py | 4 +- 13 files changed, 214 insertions(+), 177 deletions(-) delete mode 100644 linz-data-importer/gui/__init__.py delete mode 100644 linz-data-importer/plugin_upload.py diff --git a/.travis.yml b/.travis.yml index 04a1efc..1766e8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,23 +11,14 @@ env: matrix: - QGIS_VERSION_TAG=master language: python -cache: - directories: - - "$HOME/.cache/pip" python: - '3.6' branches: only: - master_qgis3 -addons: - apt: - packages: - - git - - python-software-properties before_install: - docker pull ${IMAGE}:${QGIS_VERSION_TAG} install: -- pip install --upgrade pip - docker run -d --name qgis-testing-environment -v ${TRAVIS_BUILD_DIR}:/tests_directory -e LDI_LINZ_KEY -e LDI_MFE_KEY -e LDI_NZDF_KEY -e DISPLAY=:99 ${IMAGE}:${QGIS_VERSION_TAG} - sleep 10 diff --git a/README.md b/README.md index be17c8d..168e595 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ the plugin is started. The left hand panel allows users to filter by service / protocol types (either, All, WFS, WMS, WMTS). All column headers can be toggled to allow ascending or descending ordering of their data. Text can be entered in the "Filter Data Sets" search bar to filter the datasets by keyword. -## Source Code, Further Documentation and Feedback +## Source Code and Feedback Please see the [LINZ-Data-Importer](https://github.com/linz/linz-data-importer/) repository on GitHub. ## Dev Notes diff --git a/linz-data-importer/__init__.py b/linz-data-importer/__init__.py index ae00100..1f3ac77 100644 --- a/linz-data-importer/__init__.py +++ b/linz-data-importer/__init__.py @@ -19,6 +19,10 @@ This script initializes the plugin, making it known to QGIS. """ +import sys +from os.path import dirname, abspath +sys.path.append(dirname(abspath(__file__))) + # noinspection PyPep8Naming def classFactory(iface): # pylint: disable=invalid-name diff --git a/linz-data-importer/gui/Service_dialog.py b/linz-data-importer/gui/Service_dialog.py index 14e0817..4d776d9 100644 --- a/linz-data-importer/gui/Service_dialog.py +++ b/linz-data-importer/gui/Service_dialog.py @@ -1,9 +1,22 @@ -#TODO// header - +""" +/*************************************************************************** + LINZ Data Importer + A QGIS plugin + Import LINZ (and others) OGC Datasets into QGIS + ------------------- + begin : 2018-04-07 + git sha : $Format:%H$ + copyright : (C) 2017 by Land Information New Zealand + email : splanzer@linz.govt.nz + ***************************************************************************/ +/*************************************************************************** + * This program is released under the terms of the 3 clause BSD license. * + * see the LICENSE file for more information * + ***************************************************************************/ +""" import os - from PyQt5 import QtGui, QtWidgets, uic FORM_CLASS, _ = uic.loadUiType(os.path.join( @@ -44,4 +57,4 @@ def __init__(self, parent=None): # } # """ # ) - \ No newline at end of file + diff --git a/linz-data-importer/gui/Service_dialog_base.ui b/linz-data-importer/gui/Service_dialog_base.ui index 17964e4..3c9512e 100644 --- a/linz-data-importer/gui/Service_dialog_base.ui +++ b/linz-data-importer/gui/Service_dialog_base.ui @@ -132,6 +132,9 @@ true + + QAbstractItemView::SingleSelection + QAbstractItemView::SelectRows @@ -146,7 +149,7 @@ - + 160 @@ -819,9 +822,15 @@ + + + ExtendedCombobox + QComboBox +
tablemodel.h
+
+
uTextFilter - uCRSCombo uBtnImport uListOptions uTableView diff --git a/linz-data-importer/gui/__init__.py b/linz-data-importer/gui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/linz-data-importer/linz_data_importer.py b/linz-data-importer/linz_data_importer.py index 9e57c47..3b37b7a 100644 --- a/linz-data-importer/linz_data_importer.py +++ b/linz-data-importer/linz_data_importer.py @@ -14,7 +14,6 @@ * see the LICENSE file for more information * ***************************************************************************/ """ -from __future__ import absolute_import # This program is released under the terms of the 3 clause BSD license. See the # LICENSE file for more information. @@ -24,12 +23,12 @@ from qgis.PyQt.QtCore import QSettings, QTranslator, qVersion, QCoreApplication, Qt, QRegExp, QSize, Qt from qgis.PyQt.QtWidgets import QAction, QListWidgetItem, QHeaderView, QMenu, QToolButton -from qgis.PyQt.QtGui import QIcon, QPixmap, QImage +from qgis.PyQt.QtGui import QIcon, QPixmap, QImage, QStandardItemModel, QStandardItem from qgis.PyQt.QtCore import QSortFilterProxyModel from qgis.core import (QgsRasterLayer, QgsVectorLayer, QgsProject, QgsCoordinateReferenceSystem, Qgis) from qgis.gui import QgsMessageBar -from .tablemodel import TableModel, TableView +from .tablemodel import TableModel from .service_data import ServiceData, Localstore, ApiKey import re @@ -238,7 +237,9 @@ def initGui(self): self.dlg.uListOptions.itemClicked.connect(self.showSelectedOption) self.dlg.uListOptions.itemClicked.emit(self.dlg.uListOptions.item(0)) self.curr_list_wid_index=0 - + + model = QStandardItemModel() + self.dlg.uCRSCombo.setModel(model) self.dlg.uCRSCombo.currentIndexChanged.connect(self.layerCrsSelected) self.dlg.uLabelWarning.setStyleSheet('color:red') @@ -541,9 +542,17 @@ def showSelectedOption(self, item): self.dlg.uStackedWidget.setCurrentIndex(2) def layerCrsSelected(self): - self.selected_crs = str(self.dlg.uCRSCombo.currentText()) - if self.selected_crs: - self.selected_crs_int = int(self.selected_crs.strip('EPSG:')) + """ + Track the user selected crs. Check validity to + ensure only well formed crs are provided. + """ + + valid = re.compile('^EPSG\:\d+') + crs_text = self.dlg.uCRSCombo.currentText() + if valid.match(crs_text): + self.selected_crs = str(self.dlg.uCRSCombo.currentText()) + if self.selected_crs: + self.selected_crs_int = int(self.selected_crs.strip('EPSG:')) def getPreview(self, res, res_timeout): """ @@ -706,7 +715,7 @@ def importDataset(self): Import the selected dataset to QGIS """ - if not self.layers_loaded: + if not self.layers_loaded and not self.data_type == 'table': self.setProjectSRID() if self.service == "WFS": diff --git a/linz-data-importer/metadata.txt b/linz-data-importer/metadata.txt index 2e6933d..eac4d2c 100644 --- a/linz-data-importer/metadata.txt +++ b/linz-data-importer/metadata.txt @@ -18,7 +18,11 @@ repository=https://github.com/linz/linz-data-importer # Recommended items: # Uncomment the following line and add your changelog: -changelog= Upgraded to QGIS3 API +changelog= v2.0.1-beta: + - #48 mac osx text encoding + - #47 crs filtering +

v2.0.0-beta: + - Upgrade to QGIS3 API # Tags are comma separated with spaces allowed tags=wmts, wms, wfs, webservice, web, LINZ, LDS, MFE, Stats NZ, LRIS, NZDF diff --git a/linz-data-importer/plugin_upload.py b/linz-data-importer/plugin_upload.py deleted file mode 100644 index 8a23854..0000000 --- a/linz-data-importer/plugin_upload.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 -"""This script uploads a plugin package on the server. - Authors: A. Pasotti, V. Picavet - git sha : $TemplateVCSFormat -""" - -import sys -import getpass -import xmlrpclib -from optparse import OptionParser - -# Configuration -PROTOCOL = 'http' -SERVER = 'plugins.qgis.org' -PORT = '80' -ENDPOINT = '/plugins/RPC2/' -VERBOSE = False - - -def main(parameters, arguments): - """Main entry point. - - :param parameters: Command line parameters. - :param arguments: Command line arguments. - """ - address = "%s://%s:%s@%s:%s%s" % ( - PROTOCOL, - parameters.username, - parameters.password, - parameters.server, - parameters.port, - ENDPOINT) - print "Connecting to: %s" % hide_password(address) - - server = xmlrpclib.ServerProxy(address, verbose=VERBOSE) - - try: - plugin_id, version_id = server.plugin.upload( - xmlrpclib.Binary(open(arguments[0]).read())) - print "Plugin ID: %s" % plugin_id - print "Version ID: %s" % version_id - except xmlrpclib.ProtocolError, err: - print "A protocol error occurred" - print "URL: %s" % hide_password(err.url, 0) - print "HTTP/HTTPS headers: %s" % err.headers - print "Error code: %d" % err.errcode - print "Error message: %s" % err.errmsg - except xmlrpclib.Fault, err: - print "A fault occurred" - print "Fault code: %d" % err.faultCode - print "Fault string: %s" % err.faultString - - -def hide_password(url, start=6): - """Returns the http url with password part replaced with '*'. - - :param url: URL to upload the plugin to. - :type url: str - - :param start: Position of start of password. - :type start: int - """ - start_position = url.find(':', start) + 1 - end_position = url.find('@') - return "%s%s%s" % ( - url[:start_position], - '*' * (end_position - start_position), - url[end_position:]) - - -if __name__ == "__main__": - parser = OptionParser(usage="%prog [options] plugin.zip") - parser.add_option( - "-w", "--password", dest="password", - help="Password for plugin site", metavar="******") - parser.add_option( - "-u", "--username", dest="username", - help="Username of plugin site", metavar="user") - parser.add_option( - "-p", "--port", dest="port", - help="Server port to connect to", metavar="80") - parser.add_option( - "-s", "--server", dest="server", - help="Specify server name", metavar="plugins.qgis.org") - options, args = parser.parse_args() - if len(args) != 1: - print "Please specify zip file.\n" - parser.print_help() - sys.exit(1) - if not options.server: - options.server = SERVER - if not options.port: - options.port = PORT - if not options.username: - # interactive mode - username = getpass.getuser() - print "Please enter user name [%s] :" % username, - res = raw_input() - if res != "": - options.username = res - else: - options.username = username - if not options.password: - # interactive mode - options.password = getpass.getpass() - main(options, args) diff --git a/linz-data-importer/service_data.py b/linz-data-importer/service_data.py index 22de951..92577d5 100644 --- a/linz-data-importer/service_data.py +++ b/linz-data-importer/service_data.py @@ -24,7 +24,7 @@ from owslib.wmts import WebMapTileService from owslib.util import ServiceException -from qgis.core import QgsMessageLog, QgsApplication +from qgis.core import QgsApplication import os.path @@ -40,6 +40,7 @@ from qgis.PyQt.QtCore import QSettings + class ApiKey(object): """ Store API Keys for each domain. Required to @@ -166,8 +167,6 @@ def delAllLocalServiceXML(self, services=['wms','wfs','wmts']): file = os.path.join(dir, f) self.delLocalSeviceXML(file) - - def purgeCache(self): """ Delete all cached documents but the @@ -281,7 +280,7 @@ def isEnabled(self): def getServiceData(self): """ - Select method to obtain capbilties doc. + Select method to obtain capabilities doc. Either via localstore or internet """ @@ -381,30 +380,45 @@ def getServiceXml(self): elif hasattr(e, 'code'): self.err = 'Error: ({0}) {1}'.format(self.domain, e.reason) - + + def sortCrs(self): + # wms returns some no valid crs values + valid = re.compile('^EPSG\:\d+') + self.crs = [s for s in self.crs if valid.match(s)] + # sort + self.crs.sort(key = lambda x: int(x.split(':')[1])) + def formatForUI(self): """ Format the service data to display in the UI """ - + + wms_crs = [] service_data = [] cont = self.obj.contents - for dataset_id, dataset_obj in cont.items(): - crs=[] + self.crs=[] full_id = re.search(r'([aA-zZ]+\\.[aA-zZ]+\\.[aA-zZ]+\\.[aA-zZ]+\\:)?(?P[aA-zZ]+)-(?P[0-9]+)', dataset_obj.id) type = full_id.group('type') id = full_id.group('id') # Get and standarise espg codes if self.service == 'wmts': - crs = dataset_obj.tilematrixsets + self.crs = dataset_obj.tilematrixsets + self.sortCrs() elif self.service in ('wfs'): - crs = dataset_obj.crsOptions - crs = ['EPSG:{0}'.format(item.code) for item in crs] + self.crs = dataset_obj.crsOptions + self.crs = ['EPSG:{0}'.format(item.code) for item in self.crs] + self.sortCrs() elif self.service in ('wms'): - crs = dataset_obj.crsOptions - crs = ['EPSG:{0}'.format(item.strip('urn:ogc:def:crs:EPSG::')) for item in crs] + if wms_crs: + self.crs = wms_crs + else: + self.crs = dataset_obj.crsOptions + self.sortCrs() + wms_crs = self.crs + service_data.append([self.domain, type, self.service.upper(), id, - dataset_obj.title, dataset_obj.abstract, crs]) + dataset_obj.title, dataset_obj.abstract, self.crs]) self.info = service_data + diff --git a/linz-data-importer/tablemodel.py b/linz-data-importer/tablemodel.py index f1f8c2c..f51f1dd 100644 --- a/linz-data-importer/tablemodel.py +++ b/linz-data-importer/tablemodel.py @@ -16,36 +16,37 @@ """ from builtins import str -from qgis.PyQt.QtCore import QAbstractTableModel, Qt -from qgis.PyQt.QtWidgets import QTableView +from qgis.PyQt.QtCore import QAbstractTableModel, Qt, QSortFilterProxyModel +from qgis.PyQt.QtWidgets import QComboBox, QApplication, QCompleter +from qgis.PyQt.QtGui import QStandardItem import sys - -class TableView(QTableView): - - """ - :param QTableView: Inherits from QtGui.QWidget - :param QTableView: QtGui.QTableView() - """ - - def __init__( self, parent=None ): - """ - Initialise View for AIMS Queues - - :param parent: QModelIndex - :param parent: PyQt4.QtCore.QModelIndex - """ - - QTableView.__init__( self, parent ) - # Change default settings - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.horizontalHeader().setStretchLastSection(True) - self.horizontalHeader().setHighlightSections(False) - - self.verticalHeader().setVisible(False) - self.verticalHeader().setDefaultSectionSize(17) - self.setSortingEnabled(True) - self.setEditTriggers(QAbstractItemView.AllEditTriggers) +## Below model not currently in-use +# class TableView(QTableView): +# +# """ +# :param QTableView: Inherits from QtGui.QWidget +# :param QTableView: QtGui.QTableView() +# """ +# +# def __init__( self, parent=None ): +# """ +# Initialise View for AIMS Queues +# +# :param parent: QModelIndex +# :param parent: PyQt4.QtCore.QModelIndex +# """ +# +# QTableView.__init__( self, parent ) +# # Change default settings +# self.setSelectionBehavior(QAbstractItemView.SelectRows) +# self.horizontalHeader().setStretchLastSection(True) +# self.horizontalHeader().setHighlightSections(False) +# +# self.verticalHeader().setVisible(False) +# self.verticalHeader().setDefaultSectionSize(17) +# self.setSortingEnabled(True) +# self.setEditTriggers(QAbstractItemView.AllEditTriggers) class TableModel(QAbstractTableModel): """ @@ -62,6 +63,7 @@ def __init__(self, data = [[]], headers = [], parent=None): :param parent: None :param parent: None """ + QAbstractTableModel.__init__(self, parent) self.arraydata = data self.header = headers @@ -166,3 +168,90 @@ def flags(self, index): """ return Qt.ItemIsEnabled | Qt.ItemIsSelectable + +class ExtendedCombobox( QComboBox ): + """ + Overwrite combobox to provide text filtering of + combobox list. + """ + + def __init__(self, parent): + """ + Initialise ExtendedCombobox + + :param parent: Parent of combobox + :type parent: PyQt5.QtWidgets.QWidget + """ + + super(ExtendedCombobox, self).__init__(parent) + + self.setFocusPolicy(Qt.StrongFocus) + self.setEditable(True) + self.completer = QCompleter(self) + + # always show all completions + self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) + self.pFilterModel = QSortFilterProxyModel(self) + self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.completer.setPopup(self.view()) + self.setCompleter(self.completer) + self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString) + self.completer.activated.connect(self.setTextIfCompleterIsClicked) + + def setModel(self, model): + """ + Set the model to use the Filter model + + :param model: The model to be used by the combobox + :type model: PyQt5.QtGui.QStandardItemModel + """ + + super(ExtendedCombobox, self).setModel(model) + self.pFilterModel.setSourceModel(model) + self.completer.setModel(self.pFilterModel) + + def setModelColumn(self, column): + """ + :param model: The model to be used by the combobox + :type model: PyQt5.QtGui.QStandardItemModel + """ + + self.completer.setCompletionColumn(column) + self.pFilterModel.setFilterKeyColumn(column) + super(ExtendedCombobox, self).setModelColumn(column) + + def view(self): + """ + A QListView of items stored in the model + + :return: items stored in the model + :rtype: PyQt5.QtWidgets.QListView + """ + + + return self.completer.popup() + + def index(self): + """ + Index of the current item in the combobox. + + :return: index of the current item + :rtype: int + """ + + return self.currentIndex() + + def setTextIfCompleterIsClicked(self, text): + """ + :param text: The current text of the qlineedit + :type text: str + + If the combobx lineedit is clicked, set the lineedits + current item as the combobox's current item + """ + + if text: + index = self.findText(text) + self.setCurrentIndex(index) + + diff --git a/linz-data-importer/tests/test_ldi_integration.py b/linz-data-importer/tests/test_ldi_integration.py index 4f6867e..618a16d 100644 --- a/linz-data-importer/tests/test_ldi_integration.py +++ b/linz-data-importer/tests/test_ldi_integration.py @@ -227,7 +227,7 @@ def tearDownClass(cls): """ # Runs at TestCase teardown. - QSettings().setValue('linz_data_importer/apikey', cls.testers_keys) + QSettings().setValue('linz_data_importer/apikeys', cls.testers_keys) def setUp(self): """ @@ -333,6 +333,17 @@ def test_all_services(self): self.assertEqual(sorted([u'WMS', u'WFS', u'WMTS']), sorted(list(data_types))) + + def test_crs_combo_filter(self): + """ + Test the importing of WMS layers into QGIS + """ + + #set text + self.ldi.dlg.uCRSCombo.lineEdit().setText('ESPG:2193') + #check that the lineEdit set the correct item in combobox + self.assertEqual('ESPG:2193', self.ldi.dlg.uCRSCombo.currentText()) + # def suite(): # suite = unittest.TestSuite() # suite.addTests(unittest.makeSuite(ApiKeyTest, 'test')) diff --git a/linz-data-importer/tests/test_ldi_plugin.py b/linz-data-importer/tests/test_ldi_plugin.py index 6b472e3..fcb94f3 100644 --- a/linz-data-importer/tests/test_ldi_plugin.py +++ b/linz-data-importer/tests/test_ldi_plugin.py @@ -44,12 +44,12 @@ def setUpClass(cls): # """Runs at TestCase init.""" # Get the test executors current key so that # We can revert back to when tests are complete - cls.testers_keys = QSettings().value('linz-data-importer/apikeys') + cls.testers_keys = QSettings().value('linz_data_importer/apikeys') @classmethod def tearDownClass(cls): # Runs at TestCase teardown. - QSettings().setValue('linz-data-importer/apikey', cls.testers_keys) + QSettings().setValue('linz_data_importer/apikeys', cls.testers_keys) def copyTestData(self): """