mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-09 00:08:52 -04:00
* [metasearch] Add setting to enable/disable debug logging * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1116 lines
39 KiB
Python
1116 lines
39 KiB
Python
###############################################################################
|
|
#
|
|
# CSW Client
|
|
# ---------------------------------------------------------
|
|
# QGIS Catalog Service client.
|
|
#
|
|
# Copyright (C) 2010 NextGIS (http://nextgis.org),
|
|
# Alexander Bruy (alexander.bruy@gmail.com),
|
|
# Maxim Dubinin (sim@gis-lab.info)
|
|
#
|
|
# Copyright (C) 2017 Tom Kralidis (tomkralidis@gmail.com)
|
|
#
|
|
# This source is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free
|
|
# Software Foundation; either version 2 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# This code is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
###############################################################################
|
|
|
|
import json
|
|
import os.path
|
|
from urllib.request import build_opener, install_opener, ProxyHandler
|
|
|
|
from qgis.PyQt.QtCore import Qt
|
|
from qgis.PyQt.QtWidgets import (
|
|
QDialog,
|
|
QComboBox,
|
|
QDialogButtonBox,
|
|
QMessageBox,
|
|
QTreeWidgetItem,
|
|
QWidget,
|
|
)
|
|
from qgis.PyQt.QtGui import QColor
|
|
|
|
from qgis.core import (
|
|
Qgis,
|
|
QgsApplication,
|
|
QgsCoordinateReferenceSystem,
|
|
QgsCoordinateTransform,
|
|
QgsGeometry,
|
|
QgsPointXY,
|
|
QgsProviderRegistry,
|
|
QgsSettings,
|
|
QgsProject,
|
|
QgsRectangle,
|
|
QgsSettingsTree,
|
|
)
|
|
from qgis.gui import QgsRubberBand, QgsGui
|
|
from qgis.utils import OverrideCursor
|
|
|
|
try:
|
|
from owslib.util import Authentication
|
|
except ImportError:
|
|
pass
|
|
|
|
from MetaSearch import link_types
|
|
from MetaSearch.dialogs.manageconnectionsdialog import ManageConnectionsDialog
|
|
from MetaSearch.dialogs.newconnectiondialog import NewConnectionDialog
|
|
from MetaSearch.dialogs.recorddialog import RecordDialog
|
|
from MetaSearch.dialogs.apidialog import APIRequestResponseDialog
|
|
from MetaSearch.search_backend import get_catalog_service
|
|
from MetaSearch.util import (
|
|
clean_ows_url,
|
|
get_connections_from_file,
|
|
get_ui_class,
|
|
get_help_url,
|
|
normalize_text,
|
|
open_url,
|
|
render_template,
|
|
serialize_string,
|
|
StaticContext,
|
|
)
|
|
|
|
BASE_CLASS = get_ui_class("maindialog.ui")
|
|
|
|
|
|
class MetaSearchDialog(QDialog, BASE_CLASS):
|
|
"""main dialogue"""
|
|
|
|
def __init__(self, iface):
|
|
"""init window"""
|
|
|
|
QDialog.__init__(self)
|
|
self.setupUi(self)
|
|
|
|
self.iface = iface
|
|
self.map = iface.mapCanvas()
|
|
self.settings = QgsSettings()
|
|
self.catalog = None
|
|
self.catalog_url = None
|
|
self.catalog_username = None
|
|
self.catalog_password = None
|
|
self.catalog_type = None
|
|
self.context = StaticContext()
|
|
|
|
self.leKeywords.setShowSearchIcon(True)
|
|
self.leKeywords.setPlaceholderText(self.tr("Search keywords"))
|
|
|
|
self.setWindowTitle(self.tr("MetaSearch"))
|
|
|
|
self.rubber_band = QgsRubberBand(self.map, Qgis.GeometryType.Polygon)
|
|
self.rubber_band.setColor(QColor(255, 0, 0, 75))
|
|
self.rubber_band.setWidth(5)
|
|
|
|
# form inputs
|
|
self.startfrom = 1
|
|
self.constraints = []
|
|
self.maxrecords = int(self.settings.value("/MetaSearch/returnRecords", 10))
|
|
self.timeout = int(self.settings.value("/MetaSearch/timeout", 10))
|
|
self.disable_ssl_verification = self.settings.value(
|
|
"/MetaSearch/disableSSL", False, bool
|
|
)
|
|
self.log_debugging_messages = self.settings.value(
|
|
"/MetaSearch/logDebugging", False, bool
|
|
)
|
|
|
|
# Services tab
|
|
self.cmbConnectionsServices.activated.connect(self.save_connection)
|
|
self.cmbConnectionsSearch.activated.connect(self.save_connection)
|
|
self.btnServerInfo.clicked.connect(self.connection_info)
|
|
self.btnAddDefault.clicked.connect(self.add_default_connections)
|
|
self.btnRawAPIResponse.clicked.connect(self.show_api)
|
|
self.tabWidget.currentChanged.connect(self.populate_connection_list)
|
|
|
|
# server management buttons
|
|
self.btnNew.clicked.connect(self.add_connection)
|
|
self.btnEdit.clicked.connect(self.edit_connection)
|
|
self.btnDelete.clicked.connect(self.delete_connection)
|
|
self.btnLoad.clicked.connect(self.load_connections)
|
|
self.btnSave.clicked.connect(save_connections)
|
|
|
|
# Search tab
|
|
self.treeRecords.itemSelectionChanged.connect(self.record_clicked)
|
|
self.treeRecords.itemDoubleClicked.connect(self.show_metadata)
|
|
self.btnSearch.clicked.connect(self.search)
|
|
self.leKeywords.returnPressed.connect(self.search)
|
|
# prevent dialog from closing upon pressing enter
|
|
self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setAutoDefault(
|
|
False
|
|
)
|
|
# launch help from button
|
|
self.buttonBox.helpRequested.connect(self.help)
|
|
self.btnCanvasBbox.setAutoDefault(False)
|
|
self.btnCanvasBbox.clicked.connect(self.set_bbox_from_map)
|
|
self.btnGlobalBbox.clicked.connect(self.set_bbox_global)
|
|
|
|
# navigation buttons
|
|
self.btnFirst.clicked.connect(self.navigate)
|
|
self.btnPrev.clicked.connect(self.navigate)
|
|
self.btnNext.clicked.connect(self.navigate)
|
|
self.btnLast.clicked.connect(self.navigate)
|
|
|
|
self.mActionAddWms.triggered.connect(self.add_to_ows)
|
|
self.mActionAddWfs.triggered.connect(self.add_to_ows)
|
|
self.mActionAddWcs.triggered.connect(self.add_to_ows)
|
|
self.mActionAddAms.triggered.connect(self.add_to_ows)
|
|
self.mActionAddAfs.triggered.connect(self.add_to_ows)
|
|
self.mActionAddGisFile.triggered.connect(self.add_gis_file)
|
|
self.btnViewRawAPIResponse.clicked.connect(self.show_api)
|
|
|
|
self.manageGui()
|
|
|
|
def manageGui(self):
|
|
"""open window"""
|
|
|
|
def _on_timeout_change(value):
|
|
self.settings.setValue("/MetaSearch/timeout", value)
|
|
self.timeout = value
|
|
|
|
def _on_records_change(value):
|
|
self.settings.setValue("/MetaSearch/returnRecords", value)
|
|
self.maxrecords = value
|
|
|
|
def _on_ssl_state_change(state):
|
|
self.settings.setValue("/MetaSearch/disableSSL", bool(state))
|
|
self.disable_ssl_verification = bool(state)
|
|
|
|
def _on_debugging_state_change(state):
|
|
self.settings.setValue("/MetaSearch/logDebugging", bool(state))
|
|
self.log_debugging_messages = bool(state)
|
|
|
|
self.tabWidget.setCurrentIndex(0)
|
|
self.populate_connection_list()
|
|
self.btnRawAPIResponse.setEnabled(False)
|
|
|
|
# load settings
|
|
self.spnRecords.setValue(self.maxrecords)
|
|
self.spnRecords.valueChanged.connect(_on_records_change)
|
|
self.spnTimeout.setValue(self.timeout)
|
|
self.spnTimeout.valueChanged.connect(_on_timeout_change)
|
|
self.disableSSLVerification.setChecked(self.disable_ssl_verification)
|
|
self.disableSSLVerification.stateChanged.connect(_on_ssl_state_change)
|
|
self.logDebuggingMessages.setChecked(self.log_debugging_messages)
|
|
self.logDebuggingMessages.stateChanged.connect(_on_debugging_state_change)
|
|
|
|
key = "/MetaSearch/%s" % self.cmbConnectionsSearch.currentText()
|
|
self.catalog_url = self.settings.value("%s/url" % key)
|
|
self.catalog_username = self.settings.value("%s/username" % key)
|
|
self.catalog_password = self.settings.value("%s/password" % key)
|
|
self.catalog_type = self.settings.value("%s/catalog-type" % key)
|
|
|
|
self.set_bbox_global()
|
|
|
|
self.reset_buttons()
|
|
|
|
# install proxy handler if specified in QGIS settings
|
|
self.install_proxy()
|
|
|
|
# Services tab
|
|
|
|
def populate_connection_list(self):
|
|
"""populate select box with connections"""
|
|
|
|
self.settings.beginGroup("/MetaSearch/")
|
|
self.cmbConnectionsServices.clear()
|
|
self.cmbConnectionsServices.addItems(self.settings.childGroups())
|
|
self.cmbConnectionsSearch.clear()
|
|
self.cmbConnectionsSearch.addItems(self.settings.childGroups())
|
|
self.settings.endGroup()
|
|
|
|
self.set_connection_list_position()
|
|
|
|
if self.cmbConnectionsServices.count() == 0:
|
|
# no connections - disable various buttons
|
|
state_disabled = False
|
|
self.btnSave.setEnabled(state_disabled)
|
|
# and start with connection tab open
|
|
self.tabWidget.setCurrentIndex(1)
|
|
# tell the user to add services
|
|
msg = self.tr(
|
|
"No services/connections defined. To get "
|
|
"started with MetaSearch, create a new "
|
|
"connection by clicking 'New' or click "
|
|
"'Add default services'."
|
|
)
|
|
self.textMetadata.setHtml("<p><h3>%s</h3></p>" % msg)
|
|
else:
|
|
# connections - enable various buttons
|
|
state_disabled = True
|
|
|
|
self.btnServerInfo.setEnabled(state_disabled)
|
|
self.btnEdit.setEnabled(state_disabled)
|
|
self.btnDelete.setEnabled(state_disabled)
|
|
|
|
def set_connection_list_position(self):
|
|
"""set the current index to the selected connection"""
|
|
to_select = self.settings.value("/MetaSearch/selected")
|
|
conn_count = self.cmbConnectionsServices.count()
|
|
|
|
if conn_count == 0:
|
|
self.btnDelete.setEnabled(False)
|
|
self.btnServerInfo.setEnabled(False)
|
|
self.btnEdit.setEnabled(False)
|
|
|
|
# does to_select exist in cmbConnectionsServices?
|
|
exists = False
|
|
for i in range(conn_count):
|
|
if self.cmbConnectionsServices.itemText(i) == to_select:
|
|
self.cmbConnectionsServices.setCurrentIndex(i)
|
|
self.cmbConnectionsSearch.setCurrentIndex(i)
|
|
exists = True
|
|
break
|
|
|
|
# If we couldn't find the stored item, but there are some, default
|
|
# to the last item (this makes some sense when deleting items as it
|
|
# allows the user to repeatidly click on delete to remove a whole
|
|
# lot of items)
|
|
if not exists and conn_count > 0:
|
|
# If to_select is null, then the selected connection wasn't found
|
|
# by QgsSettings, which probably means that this is the first time
|
|
# the user has used CSWClient, so default to the first in the list
|
|
# of connetions. Otherwise default to the last.
|
|
if not to_select:
|
|
current_index = 0
|
|
else:
|
|
current_index = conn_count - 1
|
|
|
|
self.cmbConnectionsServices.setCurrentIndex(current_index)
|
|
self.cmbConnectionsSearch.setCurrentIndex(current_index)
|
|
|
|
def save_connection(self):
|
|
"""save connection"""
|
|
|
|
caller = self.sender().objectName()
|
|
|
|
if caller == "cmbConnectionsServices": # servers tab
|
|
current_text = self.cmbConnectionsServices.currentText()
|
|
elif caller == "cmbConnectionsSearch": # search tab
|
|
current_text = self.cmbConnectionsSearch.currentText()
|
|
|
|
self.settings.setValue("/MetaSearch/selected", current_text)
|
|
key = "/MetaSearch/%s" % current_text
|
|
|
|
if caller == "cmbConnectionsSearch": # bind to service in search tab
|
|
self.catalog_url = self.settings.value("%s/url" % key)
|
|
self.catalog_username = self.settings.value("%s/username" % key)
|
|
self.catalog_password = self.settings.value("%s/password" % key)
|
|
self.catalog_type = self.settings.value("%s/catalog-type" % key)
|
|
|
|
if caller == "cmbConnectionsServices": # clear server metadata
|
|
self.textMetadata.clear()
|
|
|
|
self.btnRawAPIResponse.setEnabled(False)
|
|
|
|
def connection_info(self):
|
|
"""show connection info"""
|
|
|
|
current_text = self.cmbConnectionsServices.currentText()
|
|
key = "/MetaSearch/%s" % current_text
|
|
self.catalog_url = self.settings.value("%s/url" % key)
|
|
self.catalog_username = self.settings.value("%s/username" % key)
|
|
self.catalog_password = self.settings.value("%s/password" % key)
|
|
self.catalog_type = self.settings.value("%s/catalog-type" % key)
|
|
|
|
# connect to the server
|
|
if not self._get_catalog():
|
|
return
|
|
|
|
if self.catalog: # display service metadata
|
|
self.btnRawAPIResponse.setEnabled(True)
|
|
metadata = render_template(
|
|
"en",
|
|
self.context,
|
|
self.catalog.conn,
|
|
self.catalog.service_info_template,
|
|
)
|
|
style = QgsApplication.reportStyleSheet()
|
|
self.textMetadata.clear()
|
|
self.textMetadata.document().setDefaultStyleSheet(style)
|
|
self.textMetadata.setHtml(metadata)
|
|
|
|
# clear results and disable buttons in Search tab
|
|
self.clear_results()
|
|
|
|
def add_connection(self):
|
|
"""add new service"""
|
|
|
|
conn_new = NewConnectionDialog()
|
|
conn_new.setWindowTitle(self.tr("New Catalog Service"))
|
|
if conn_new.exec() == QDialog.DialogCode.Accepted: # add to service list
|
|
self.populate_connection_list()
|
|
self.textMetadata.clear()
|
|
|
|
def edit_connection(self):
|
|
"""modify existing connection"""
|
|
|
|
current_text = self.cmbConnectionsServices.currentText()
|
|
|
|
url = self.settings.value("/MetaSearch/%s/url" % current_text)
|
|
|
|
conn_edit = NewConnectionDialog(current_text)
|
|
conn_edit.setWindowTitle(self.tr("Edit Catalog Service"))
|
|
conn_edit.leName.setText(current_text)
|
|
conn_edit.leURL.setText(url)
|
|
conn_edit.leUsername.setText(
|
|
self.settings.value("/MetaSearch/%s/username" % current_text)
|
|
)
|
|
conn_edit.lePassword.setText(
|
|
self.settings.value("/MetaSearch/%s/password" % current_text)
|
|
)
|
|
|
|
conn_edit.cmbCatalogType.setCurrentText(
|
|
self.settings.value("/MetaSearch/%s/catalog-type" % current_text)
|
|
)
|
|
|
|
if conn_edit.exec() == QDialog.DialogCode.Accepted: # update service list
|
|
self.populate_connection_list()
|
|
|
|
def delete_connection(self):
|
|
"""delete connection"""
|
|
|
|
current_text = self.cmbConnectionsServices.currentText()
|
|
|
|
key = "/MetaSearch/%s" % current_text
|
|
|
|
msg = self.tr("Remove service {0}?").format(current_text)
|
|
|
|
result = QMessageBox.question(
|
|
self,
|
|
self.tr("Delete Service"),
|
|
msg,
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if result == QMessageBox.StandardButton.Yes: # remove service from list
|
|
self.settings.remove(key)
|
|
index_to_delete = self.cmbConnectionsServices.currentIndex()
|
|
self.cmbConnectionsServices.removeItem(index_to_delete)
|
|
self.cmbConnectionsSearch.removeItem(index_to_delete)
|
|
self.set_connection_list_position()
|
|
|
|
def load_connections(self):
|
|
"""load services from list"""
|
|
|
|
ManageConnectionsDialog(1).exec()
|
|
self.populate_connection_list()
|
|
|
|
def add_default_connections(self):
|
|
"""add default connections"""
|
|
|
|
filename = os.path.join(
|
|
self.context.ppath, "resources", "connections-default.xml"
|
|
)
|
|
|
|
doc = get_connections_from_file(self, filename)
|
|
if doc is None:
|
|
return
|
|
|
|
self.settings.beginGroup("/MetaSearch/")
|
|
keys = self.settings.childGroups()
|
|
self.settings.endGroup()
|
|
|
|
for server in doc.findall("csw"):
|
|
name = server.attrib.get("name")
|
|
# check for duplicates
|
|
if name in keys:
|
|
msg = self.tr("{0} exists. Overwrite?").format(name)
|
|
res = QMessageBox.warning(
|
|
self,
|
|
self.tr("Loading connections"),
|
|
msg,
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
if res != QMessageBox.StandardButton.Yes:
|
|
continue
|
|
|
|
# no dups detected or overwrite is allowed
|
|
key = "/MetaSearch/%s" % name
|
|
self.settings.setValue("%s/url" % key, server.attrib.get("url"))
|
|
self.settings.setValue(
|
|
"%s/catalog-type" % key,
|
|
server.attrib.get("catalog-type", "OGC CSW 2.0.2"),
|
|
)
|
|
|
|
self.populate_connection_list()
|
|
|
|
# Settings tab
|
|
|
|
def set_ows_save_title_ask(self):
|
|
"""save ows save strategy as save ows title, ask if duplicate"""
|
|
|
|
self.settings.setValue("/MetaSearch/ows_save_strategy", "title_ask")
|
|
|
|
def set_ows_save_title_no_ask(self):
|
|
"""save ows save strategy as save ows title, do NOT ask if duplicate"""
|
|
|
|
self.settings.setValue("/MetaSearch/ows_save_strategy", "title_no_ask")
|
|
|
|
def set_ows_save_temp_name(self):
|
|
"""save ows save strategy as save with a temporary name"""
|
|
|
|
self.settings.setValue("/MetaSearch/ows_save_strategy", "temp_name")
|
|
|
|
# Search tab
|
|
|
|
def set_bbox_from_map(self):
|
|
"""set bounding box from map extent"""
|
|
|
|
crs = self.map.mapSettings().destinationCrs()
|
|
crsid = crs.authid()
|
|
|
|
extent = self.map.extent()
|
|
|
|
if crsid != "EPSG:4326": # reproject to EPSG:4326
|
|
src = QgsCoordinateReferenceSystem(crsid)
|
|
dest = QgsCoordinateReferenceSystem("EPSG:4326")
|
|
xform = QgsCoordinateTransform(src, dest, QgsProject.instance())
|
|
minxy = xform.transform(QgsPointXY(extent.xMinimum(), extent.yMinimum()))
|
|
maxxy = xform.transform(QgsPointXY(extent.xMaximum(), extent.yMaximum()))
|
|
minx, miny = minxy
|
|
maxx, maxy = maxxy
|
|
else: # EPSG:4326
|
|
minx = extent.xMinimum()
|
|
miny = extent.yMinimum()
|
|
maxx = extent.xMaximum()
|
|
maxy = extent.yMaximum()
|
|
|
|
self.leNorth.setText(str(maxy)[0:9])
|
|
self.leSouth.setText(str(miny)[0:9])
|
|
self.leWest.setText(str(minx)[0:9])
|
|
self.leEast.setText(str(maxx)[0:9])
|
|
|
|
def set_bbox_global(self):
|
|
"""set global bounding box"""
|
|
self.leNorth.setText("90")
|
|
self.leSouth.setText("-90")
|
|
self.leWest.setText("-180")
|
|
self.leEast.setText("180")
|
|
|
|
def search(self):
|
|
"""execute search"""
|
|
|
|
self.catalog = None
|
|
self.constraints = []
|
|
|
|
# clear all fields and disable buttons
|
|
self.clear_results()
|
|
|
|
# set current catalog
|
|
current_text = self.cmbConnectionsSearch.currentText()
|
|
key = "/MetaSearch/%s" % current_text
|
|
self.catalog_url = self.settings.value("%s/url" % key)
|
|
self.catalog_username = self.settings.value("%s/username" % key)
|
|
self.catalog_password = self.settings.value("%s/password" % key)
|
|
self.catalog_type = self.settings.value("%s/catalog-type" % key)
|
|
|
|
# start position and number of records to return
|
|
self.startfrom = 1
|
|
|
|
# bbox
|
|
# CRS is WGS84 with axis order longitude, latitude
|
|
# defined by 'urn:ogc:def:crs:OGC:1.3:CRS84'
|
|
minx = self.leWest.text()
|
|
miny = self.leSouth.text()
|
|
maxx = self.leEast.text()
|
|
maxy = self.leNorth.text()
|
|
bbox = [minx, miny, maxx, maxy]
|
|
keywords = self.leKeywords.text()
|
|
|
|
# build request
|
|
if not self._get_catalog():
|
|
return
|
|
|
|
# TODO: allow users to select resources types
|
|
# to find ('service', 'dataset', etc.)
|
|
try:
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
self.catalog.query_records(
|
|
bbox, keywords, self.maxrecords, self.startfrom
|
|
)
|
|
|
|
except Exception as err:
|
|
QMessageBox.warning(
|
|
self, self.tr("Search error"), self.tr("Search error: {0}").format(err)
|
|
)
|
|
return
|
|
|
|
if self.catalog.matches == 0:
|
|
self.lblResults.setText(self.tr("0 results"))
|
|
return
|
|
|
|
self.display_results()
|
|
|
|
def display_results(self):
|
|
"""display search results"""
|
|
|
|
self.treeRecords.clear()
|
|
|
|
position = self.catalog.returned + self.startfrom - 1
|
|
|
|
msg = self.tr(
|
|
"Showing {0} - {1} of %n result(s)",
|
|
"number of results",
|
|
self.catalog.matches,
|
|
).format(self.startfrom, position)
|
|
|
|
self.lblResults.setText(msg)
|
|
|
|
for rec in self.catalog.records():
|
|
item = QTreeWidgetItem(self.treeRecords)
|
|
if rec["type"]:
|
|
item.setText(0, normalize_text(rec["type"]))
|
|
else:
|
|
item.setText(0, "unknown")
|
|
if rec["title"]:
|
|
item.setText(1, normalize_text(rec["title"]))
|
|
if rec["identifier"]:
|
|
set_item_data(item, "identifier", rec["identifier"])
|
|
|
|
self.btnViewRawAPIResponse.setEnabled(True)
|
|
|
|
if self.catalog.matches < self.maxrecords:
|
|
disabled = False
|
|
else:
|
|
disabled = True
|
|
|
|
self.btnFirst.setEnabled(disabled)
|
|
self.btnPrev.setEnabled(disabled)
|
|
self.btnNext.setEnabled(disabled)
|
|
self.btnLast.setEnabled(disabled)
|
|
self.btnRawAPIResponse.setEnabled(False)
|
|
|
|
def clear_results(self):
|
|
"""clear search results"""
|
|
|
|
self.lblResults.clear()
|
|
self.treeRecords.clear()
|
|
self.reset_buttons()
|
|
|
|
def record_clicked(self):
|
|
"""record clicked signal"""
|
|
|
|
# disable only service buttons
|
|
self.reset_buttons(True, False, False)
|
|
|
|
self.rubber_band.reset()
|
|
|
|
if not self.treeRecords.selectedItems():
|
|
return
|
|
|
|
item = self.treeRecords.currentItem()
|
|
if not item:
|
|
return
|
|
|
|
identifier = get_item_data(item, "identifier")
|
|
try:
|
|
record = next(
|
|
item
|
|
for item in self.catalog.records()
|
|
if item["identifier"] == identifier
|
|
)
|
|
except KeyError:
|
|
QMessageBox.warning(
|
|
self,
|
|
self.tr("Record parsing error"),
|
|
"Unable to locate record identifier",
|
|
)
|
|
return
|
|
|
|
# if the record has a bbox, show a footprint on the map
|
|
if record["bbox"] is not None:
|
|
bx = record["bbox"]
|
|
rt = QgsRectangle(
|
|
float(bx["minx"]),
|
|
float(bx["miny"]),
|
|
float(bx["maxx"]),
|
|
float(bx["maxy"]),
|
|
)
|
|
geom = QgsGeometry.fromRect(rt)
|
|
|
|
if geom is not None:
|
|
src = QgsCoordinateReferenceSystem("EPSG:4326")
|
|
dst = self.map.mapSettings().destinationCrs()
|
|
if src.postgisSrid() != dst.postgisSrid():
|
|
ctr = QgsCoordinateTransform(src, dst, QgsProject.instance())
|
|
try:
|
|
geom.transform(ctr)
|
|
except Exception as err:
|
|
QMessageBox.warning(
|
|
self, self.tr("Coordinate Transformation Error"), str(err)
|
|
)
|
|
self.rubber_band.setToGeometry(geom, None)
|
|
|
|
# figure out if the data is interactive and can be operated on
|
|
self.find_services(record, item)
|
|
|
|
def find_services(self, record, item):
|
|
"""scan record for WMS/WMTS|WFS|WCS endpoints"""
|
|
|
|
services = {}
|
|
for link in record["links"]:
|
|
link = self.catalog.parse_link(link)
|
|
if "scheme" in link:
|
|
link_type = link["scheme"]
|
|
elif "protocol" in link:
|
|
link_type = link["protocol"]
|
|
else:
|
|
link_type = None
|
|
|
|
if link_type is not None:
|
|
link_type = link_type.upper()
|
|
|
|
wmswmst_link_types = list(map(str.upper, link_types.WMSWMST_LINK_TYPES))
|
|
wfs_link_types = list(map(str.upper, link_types.WFS_LINK_TYPES))
|
|
wcs_link_types = list(map(str.upper, link_types.WCS_LINK_TYPES))
|
|
ams_link_types = list(map(str.upper, link_types.AMS_LINK_TYPES))
|
|
afs_link_types = list(map(str.upper, link_types.AFS_LINK_TYPES))
|
|
gis_file_link_types = list(map(str.upper, link_types.GIS_FILE_LINK_TYPES))
|
|
|
|
# if the link type exists, and it is one of the acceptable
|
|
# interactive link types, then set
|
|
all_link_types = (
|
|
wmswmst_link_types
|
|
+ wfs_link_types
|
|
+ wcs_link_types
|
|
+ ams_link_types
|
|
+ afs_link_types
|
|
+ gis_file_link_types
|
|
)
|
|
|
|
if all([link_type is not None, link_type in all_link_types]):
|
|
if link_type in wmswmst_link_types:
|
|
services["wms"] = link["url"]
|
|
self.mActionAddWms.setEnabled(True)
|
|
if link_type in wfs_link_types:
|
|
services["wfs"] = link["url"]
|
|
self.mActionAddWfs.setEnabled(True)
|
|
if link_type in wcs_link_types:
|
|
services["wcs"] = link["url"]
|
|
self.mActionAddWcs.setEnabled(True)
|
|
if link_type in ams_link_types:
|
|
services["ams"] = link["url"]
|
|
self.mActionAddAms.setEnabled(True)
|
|
if link_type in afs_link_types:
|
|
services["afs"] = link["url"]
|
|
self.mActionAddAfs.setEnabled(True)
|
|
if link_type in gis_file_link_types:
|
|
services["gis_file"] = link["url"]
|
|
services["title"] = record.get("title", "")
|
|
self.mActionAddGisFile.setEnabled(True)
|
|
self.tbAddData.setEnabled(True)
|
|
|
|
set_item_data(item, "link", json.dumps(services))
|
|
|
|
def navigate(self):
|
|
"""manage navigation / paging"""
|
|
|
|
caller = self.sender().objectName()
|
|
|
|
if caller == "btnFirst":
|
|
self.startfrom = 1
|
|
elif caller == "btnLast":
|
|
self.startfrom = self.catalog.matches - self.maxrecords + 1
|
|
elif caller == "btnNext":
|
|
if self.startfrom > self.catalog.matches - self.maxrecords:
|
|
msg = self.tr("End of results. Go to start?")
|
|
res = QMessageBox.information(
|
|
self,
|
|
self.tr("Navigation"),
|
|
msg,
|
|
(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel),
|
|
)
|
|
if res == QMessageBox.StandardButton.Ok:
|
|
self.startfrom = 1
|
|
else:
|
|
return
|
|
else:
|
|
self.startfrom += self.maxrecords
|
|
elif caller == "btnPrev":
|
|
if self.startfrom == 1:
|
|
msg = self.tr("Start of results. Go to end?")
|
|
res = QMessageBox.information(
|
|
self,
|
|
self.tr("Navigation"),
|
|
msg,
|
|
(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel),
|
|
)
|
|
if res == QMessageBox.StandardButton.Ok:
|
|
self.startfrom = self.catalog.matches - self.maxrecords + 1
|
|
else:
|
|
return
|
|
elif self.startfrom <= self.maxrecords:
|
|
self.startfrom = 1
|
|
else:
|
|
self.startfrom -= self.maxrecords
|
|
|
|
# bbox
|
|
# CRS is WGS84 with axis order longitude, latitude
|
|
# defined by 'urn:ogc:def:crs:OGC:1.3:CRS84'
|
|
minx = self.leWest.text()
|
|
miny = self.leSouth.text()
|
|
maxx = self.leEast.text()
|
|
maxy = self.leNorth.text()
|
|
bbox = [minx, miny, maxx, maxy]
|
|
keywords = self.leKeywords.text()
|
|
|
|
try:
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
self.catalog.query_records(
|
|
bbox, keywords, limit=self.maxrecords, offset=self.startfrom
|
|
)
|
|
except Exception as err:
|
|
QMessageBox.warning(
|
|
self, self.tr("Search error"), self.tr("Search error: {0}").format(err)
|
|
)
|
|
return
|
|
|
|
self.display_results()
|
|
|
|
def add_to_ows(self):
|
|
"""add to OWS provider connection list"""
|
|
|
|
conn_name_matches = []
|
|
|
|
item = self.treeRecords.currentItem()
|
|
|
|
if not item:
|
|
return
|
|
|
|
item_data = json.loads(get_item_data(item, "link"))
|
|
|
|
caller = self.sender().objectName()
|
|
|
|
if caller == "mActionAddWms":
|
|
service_type = "OGC:WMS/OGC:WMTS"
|
|
sname = "WMS"
|
|
dyn_param = ["wms"]
|
|
provider_name = "wms"
|
|
setting_node = (
|
|
QgsSettingsTree.node("connections")
|
|
.childNode("ows")
|
|
.childNode("connections")
|
|
)
|
|
data_url = item_data["wms"]
|
|
elif caller == "mActionAddWfs":
|
|
service_type = "OGC:WFS"
|
|
sname = "WFS"
|
|
dyn_param = ["wfs"]
|
|
provider_name = "WFS"
|
|
setting_node = (
|
|
QgsSettingsTree.node("connections")
|
|
.childNode("ows")
|
|
.childNode("connections")
|
|
)
|
|
data_url = item_data["wfs"]
|
|
elif caller == "mActionAddWcs":
|
|
service_type = "OGC:WCS"
|
|
sname = "WCS"
|
|
dyn_param = ["wcs"]
|
|
provider_name = "wcs"
|
|
setting_node = (
|
|
QgsSettingsTree.node("connections")
|
|
.childNode("ows")
|
|
.childNode("connections")
|
|
)
|
|
data_url = item_data["wcs"]
|
|
elif caller == "mActionAddAfs":
|
|
service_type = "ESRI:ArcGIS:FeatureServer"
|
|
sname = "AFS"
|
|
dyn_param = []
|
|
provider_name = "arcgisfeatureserver"
|
|
setting_node = QgsSettingsTree.node("connections").childNode(
|
|
"arcgisfeatureserver"
|
|
)
|
|
data_url = item_data["afs"].split("FeatureServer")[0] + "FeatureServer"
|
|
|
|
keys = setting_node.items(dyn_param)
|
|
|
|
sname = "%s from MetaSearch" % sname
|
|
for key in keys:
|
|
if key.startswith(sname):
|
|
conn_name_matches.append(key)
|
|
if conn_name_matches:
|
|
sname = conn_name_matches[-1]
|
|
|
|
# check for duplicates
|
|
if sname in keys: # duplicate found
|
|
msg = self.tr("Connection {0} exists. Overwrite?").format(sname)
|
|
res = QMessageBox.warning(
|
|
self,
|
|
self.tr("Saving server"),
|
|
msg,
|
|
QMessageBox.StandardButton.Yes
|
|
| QMessageBox.StandardButton.No
|
|
| QMessageBox.StandardButton.Cancel,
|
|
)
|
|
if res == QMessageBox.StandardButton.No: # assign new name with serial
|
|
sname = serialize_string(sname)
|
|
elif res == QMessageBox.StandardButton.Cancel:
|
|
return
|
|
|
|
# no dups detected or overwrite is allowed
|
|
dyn_param.append(sname)
|
|
setting_node.childSetting("url").setValue(clean_ows_url(data_url), dyn_param)
|
|
|
|
# open provider window
|
|
ows_provider = QgsGui.sourceSelectProviderRegistry().createSelectionWidget(
|
|
provider_name,
|
|
self,
|
|
Qt.WindowType.Widget,
|
|
QgsProviderRegistry.WidgetMode.Embedded,
|
|
)
|
|
|
|
# connect dialog signals to iface slots
|
|
if service_type == "OGC:WMS/OGC:WMTS":
|
|
ows_provider.addRasterLayer.connect(self.iface.addRasterLayer)
|
|
conn_cmb = ows_provider.findChild(QWidget, "cmbConnections")
|
|
connect = "btnConnect_clicked"
|
|
elif service_type == "OGC:WFS":
|
|
|
|
def addVectorLayer(path, name):
|
|
self.iface.addVectorLayer(path, name, "WFS")
|
|
|
|
ows_provider.addVectorLayer.connect(addVectorLayer)
|
|
conn_cmb = ows_provider.findChild(QWidget, "cmbConnections")
|
|
connect = "connectToServer"
|
|
elif service_type == "OGC:WCS":
|
|
ows_provider.addRasterLayer.connect(self.iface.addRasterLayer)
|
|
conn_cmb = ows_provider.findChild(QWidget, "mConnectionsComboBox")
|
|
connect = "mConnectButton_clicked"
|
|
elif service_type == "ESRI:ArcGIS:FeatureServer":
|
|
|
|
def addAfsLayer(path, name):
|
|
self.iface.addVectorLayer(path, name, "afs")
|
|
|
|
ows_provider.addVectorLayer.connect(addAfsLayer)
|
|
conn_cmb = ows_provider.findChild(QComboBox)
|
|
connect = "connectToServer"
|
|
|
|
ows_provider.setModal(False)
|
|
ows_provider.show()
|
|
|
|
# open provider dialogue against added OWS
|
|
index = conn_cmb.findText(sname)
|
|
if index > -1:
|
|
conn_cmb.setCurrentIndex(index)
|
|
# only for wfs
|
|
if service_type == "OGC:WFS":
|
|
ows_provider.cmbConnections_activated(index)
|
|
elif service_type == "ESRI:ArcGIS:FeatureServer":
|
|
ows_provider.cmbConnections_activated(index)
|
|
getattr(ows_provider, connect)()
|
|
|
|
def add_gis_file(self):
|
|
"""add GIS file from result"""
|
|
item = self.treeRecords.currentItem()
|
|
|
|
if not item:
|
|
return
|
|
|
|
item_data = json.loads(get_item_data(item, "link"))
|
|
gis_file = item_data["gis_file"]
|
|
|
|
title = item_data["title"]
|
|
|
|
layer = self.iface.addVectorLayer(gis_file, title, "ogr")
|
|
if not layer:
|
|
self.iface.messageBar().pushWarning(None, "Layer failed to load!")
|
|
|
|
def show_metadata(self):
|
|
"""show record metadata"""
|
|
|
|
if not self.treeRecords.selectedItems():
|
|
return
|
|
|
|
item = self.treeRecords.currentItem()
|
|
if not item:
|
|
return
|
|
|
|
identifier = get_item_data(item, "identifier")
|
|
|
|
auth = None
|
|
|
|
if self.disable_ssl_verification:
|
|
try:
|
|
auth = Authentication(verify=False)
|
|
except NameError:
|
|
pass
|
|
|
|
try:
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
cat = get_catalog_service(
|
|
self.catalog_url, # spellok
|
|
catalog_type=self.catalog_type,
|
|
timeout=self.timeout,
|
|
username=self.catalog_username or None,
|
|
password=self.catalog_password or None,
|
|
auth=auth,
|
|
)
|
|
record = cat.get_record(identifier)
|
|
if cat.type == "OGC API - Records":
|
|
record["url"] = cat.conn.request
|
|
elif cat.type == "OGC CSW 2.0.2":
|
|
record.url = cat.conn.request
|
|
|
|
except Exception as err:
|
|
QMessageBox.warning(
|
|
self,
|
|
self.tr("GetRecords error"),
|
|
self.tr("Error getting response: {0}").format(err),
|
|
)
|
|
return
|
|
except KeyError as err:
|
|
QMessageBox.warning(
|
|
self,
|
|
self.tr("Record parsing error"),
|
|
self.tr("Unable to locate record identifier: {0}").format(err),
|
|
)
|
|
return
|
|
|
|
crd = RecordDialog()
|
|
metadata = render_template(
|
|
"en", self.context, record, self.catalog.record_info_template
|
|
)
|
|
|
|
style = QgsApplication.reportStyleSheet()
|
|
crd.textMetadata.document().setDefaultStyleSheet(style)
|
|
crd.textMetadata.setHtml(metadata)
|
|
crd.exec()
|
|
|
|
def show_api(self):
|
|
"""show API request / response"""
|
|
|
|
crd = APIRequestResponseDialog(
|
|
self.catalog.request, self.catalog.response, self.catalog.format
|
|
)
|
|
crd.exec()
|
|
|
|
def reset_buttons(self, services=True, api=True, navigation=True):
|
|
"""Convenience function to disable WMS/WMTS|WFS|WCS buttons"""
|
|
|
|
if services:
|
|
self.tbAddData.setEnabled(False)
|
|
self.mActionAddWms.setEnabled(False)
|
|
self.mActionAddWfs.setEnabled(False)
|
|
self.mActionAddWcs.setEnabled(False)
|
|
self.mActionAddAms.setEnabled(False)
|
|
self.mActionAddAfs.setEnabled(False)
|
|
self.mActionAddGisFile.setEnabled(False)
|
|
|
|
if api:
|
|
self.btnViewRawAPIResponse.setEnabled(False)
|
|
|
|
if navigation:
|
|
self.btnFirst.setEnabled(False)
|
|
self.btnPrev.setEnabled(False)
|
|
self.btnNext.setEnabled(False)
|
|
self.btnLast.setEnabled(False)
|
|
|
|
def help(self):
|
|
"""launch help"""
|
|
|
|
open_url(get_help_url())
|
|
|
|
def reject(self):
|
|
"""back out of dialogue"""
|
|
|
|
QDialog.reject(self)
|
|
self.rubber_band.reset()
|
|
|
|
def _get_catalog(self):
|
|
"""convenience function to init catalog wrapper"""
|
|
|
|
auth = None
|
|
|
|
if self.disable_ssl_verification:
|
|
try:
|
|
auth = Authentication(verify=False)
|
|
except NameError:
|
|
pass
|
|
|
|
# connect to the server
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
try:
|
|
self.catalog = get_catalog_service(
|
|
self.catalog_url,
|
|
catalog_type=self.catalog_type,
|
|
timeout=self.timeout,
|
|
username=self.catalog_username or None,
|
|
password=self.catalog_password or None,
|
|
auth=auth,
|
|
)
|
|
return True
|
|
except Exception as err:
|
|
msg = self.tr("Error connecting to service: {0}").format(err)
|
|
|
|
QMessageBox.warning(self, self.tr("CSW Connection error"), msg)
|
|
return False
|
|
|
|
def install_proxy(self):
|
|
"""set proxy if one is set in QGIS network settings"""
|
|
|
|
# initially support HTTP for now
|
|
if self.settings.value("/proxy/proxyEnabled") == "true":
|
|
if self.settings.value("/proxy/proxyType") == "HttpProxy":
|
|
ptype = "http"
|
|
else:
|
|
return
|
|
|
|
user = self.settings.value("/proxy/proxyUser")
|
|
password = self.settings.value("/proxy/proxyPassword")
|
|
host = self.settings.value("/proxy/proxyHost")
|
|
port = self.settings.value("/proxy/proxyPort")
|
|
|
|
proxy_up = ""
|
|
proxy_port = ""
|
|
|
|
if all([user != "", password != ""]):
|
|
proxy_up = f"{user}:{password}@"
|
|
|
|
if port != "":
|
|
proxy_port = ":%s" % port
|
|
|
|
conn = f"{ptype}://{proxy_up}{host}{proxy_port}"
|
|
install_opener(build_opener(ProxyHandler({ptype: conn})))
|
|
|
|
|
|
def save_connections():
|
|
"""save servers to list"""
|
|
|
|
ManageConnectionsDialog(0).exec()
|
|
|
|
|
|
def get_item_data(item, field):
|
|
"""return identifier for a QTreeWidgetItem"""
|
|
|
|
return item.data(_get_field_value(field), 32)
|
|
|
|
|
|
def set_item_data(item, field, value):
|
|
"""set identifier for a QTreeWidgetItem"""
|
|
|
|
item.setData(_get_field_value(field), 32, value)
|
|
|
|
|
|
def _get_field_value(field):
|
|
"""convenience function to return field value integer"""
|
|
|
|
value = 0
|
|
|
|
if field == "identifier":
|
|
value = 0
|
|
if field == "link":
|
|
value = 1
|
|
|
|
return value
|