diff --git a/.gitignore b/.gitignore
index 023fc7e..58c7371 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea/
-pb_tool.cfg
\ No newline at end of file
+pb_tool.cfg
+resources.py
diff --git a/simplewcs_dialog.py b/simplewcs_dialog.py
index 84760cf..d600ca8 100644
--- a/simplewcs_dialog.py
+++ b/simplewcs_dialog.py
@@ -7,12 +7,14 @@
licence: GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
"""
import os
+import json
import urllib
import xml.etree.ElementTree as ET
from typing import List, Optional, Tuple
from urllib.error import HTTPError, URLError
from qgis.PyQt.QtCore import (Qt,
+ QSettings,
QUrl,)
from qgis.PyQt.QtGui import (QAction,
QKeySequence,)
@@ -37,6 +39,7 @@
from qgis.PyQt import uic
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtWidgets import (QDialog,
+ QFileDialog,
QProgressBar,)
from .resources import * # magically sets up icon etc...
@@ -54,6 +57,8 @@
GENERATED_CLASS, BASE = uic.loadUiType(os.path.join(os.path.dirname(__file__), 'simplewcs_dialog_base.ui'))
wcs_ns = '{http://www.opengis.net/wcs/2.0}'
+SETTINGS_SAVED_SERVICES_KEY = 'plugins/simplewcs2/saved_services'
+SETTINGS_LAST_SERVICE_KEY = 'plugins/simplewcs2/last_saved_service'
class SimpleWCSDialog(BASE, GENERATED_CLASS):
@@ -89,6 +94,8 @@ def __init__(self) -> None:
self.mapCrs: str = self.getMapCrs()
self.acceptedWcsVersions = ['2.1.0', '2.0.1', '2.0.0']
+ self.settings = QSettings()
+ self.savedServices: List[dict] = []
self.setupUi(self)
@@ -127,7 +134,8 @@ def setupUrlTab(self) -> None:
"""
self.cbVersion.addItems(self.acceptedWcsVersions)
self.cbVersion.setCurrentIndex(1)
- self.btnGetCapabilities.setEnabled(False)
+ self.loadSavedServices()
+ self.updateUrlManagerButtons()
def setupGetCoverageTab(self) -> None:
"""
@@ -150,7 +158,15 @@ def setupGetCoverageTab(self) -> None:
def connectSignals(self) -> None:
self.leBaseUrl.textChanged.connect(self.enableBtnGetCapabilities)
+ self.leBaseUrl.textChanged.connect(self.updateUrlManagerButtons)
+ self.leServiceName.textChanged.connect(self.updateUrlManagerButtons)
self.btnGetCapabilities.clicked.connect(self.adjustGetCoverageAndInformationTabsToService)
+ self.cbSavedServices.currentIndexChanged.connect(self.onSavedServiceSelected)
+ self.btnNewService.clicked.connect(self.prepareNewService)
+ self.btnSaveService.clicked.connect(self.saveCurrentService)
+ self.btnDeleteService.clicked.connect(self.deleteCurrentService)
+ self.btnImportServices.clicked.connect(self.importSavedServices)
+ self.btnExportServices.clicked.connect(self.exportSavedServices)
self.cbCoverage.currentIndexChanged.connect(self.adjustCovTabToCovIdAndCreateBB)
self.cbUseSubset.stateChanged.connect(self.showAndHideSubsetExtentWidget)
@@ -163,6 +179,283 @@ def connectSignals(self) -> None:
self.btnGetCoverage.clicked.connect(self.getCovTask)
+ def formatSavedServiceLabel(self, service: dict) -> str:
+ serviceName = service.get('name', '').strip()
+ if serviceName:
+ return serviceName
+ return f"{service['url']} [{service['version']}]"
+
+ def getSelectedSavedServiceIndex(self) -> Optional[int]:
+ return self.cbSavedServices.currentData()
+
+ def normalizeSavedService(self, service: dict) -> Optional[dict]:
+ if not isinstance(service, dict):
+ return None
+
+ name = str(service.get('name', '')).strip()
+ url = str(service.get('url', '')).strip()
+ version = str(service.get('version', '')).strip()
+
+ if not url or version not in self.acceptedWcsVersions:
+ return None
+
+ return {'name': name, 'url': url, 'version': version}
+
+ def getSavedServiceIdentity(self, service: dict) -> Tuple[str, str]:
+ return service['url'], service['version']
+
+ def loadSavedServices(self) -> None:
+ savedServicesRaw = self.settings.value(SETTINGS_SAVED_SERVICES_KEY, '[]')
+ self.savedServices = []
+
+ try:
+ if isinstance(savedServicesRaw, str):
+ parsedServices = json.loads(savedServicesRaw)
+ else:
+ parsedServices = savedServicesRaw
+ except (TypeError, json.JSONDecodeError):
+ logWarnMessage('Could not load saved WCS services from settings')
+ parsedServices = []
+
+ if isinstance(parsedServices, list):
+ for service in parsedServices:
+ normalizedService = self.normalizeSavedService(service)
+ if normalizedService:
+ self.savedServices.append(normalizedService)
+
+ self.refreshSavedServicesCombo(selectLastService=True)
+
+ def persistSavedServices(self, selectedIndex: Optional[int] = None) -> None:
+ self.settings.setValue(SETTINGS_SAVED_SERVICES_KEY, json.dumps(self.savedServices))
+
+ if selectedIndex is None:
+ self.settings.remove(SETTINGS_LAST_SERVICE_KEY)
+ elif 0 <= selectedIndex < len(self.savedServices):
+ self.settings.setValue(SETTINGS_LAST_SERVICE_KEY, self.formatSavedServiceLabel(self.savedServices[selectedIndex]))
+ else:
+ self.settings.remove(SETTINGS_LAST_SERVICE_KEY)
+
+ def refreshSavedServicesCombo(self,
+ selectedIndex: Optional[int] = None,
+ selectLastService: bool = False) -> None:
+ currentLabel = None
+ if selectLastService:
+ currentLabel = self.settings.value(SETTINGS_LAST_SERVICE_KEY, '', type=str)
+
+ self.cbSavedServices.blockSignals(True)
+ self.cbSavedServices.clear()
+ self.cbSavedServices.addItem('Saved services', None)
+
+ resolvedIndex = selectedIndex
+ for index, service in enumerate(self.savedServices):
+ label = self.formatSavedServiceLabel(service)
+ self.cbSavedServices.addItem(label, index)
+ if currentLabel and label == currentLabel:
+ resolvedIndex = index
+
+ if resolvedIndex is not None and 0 <= resolvedIndex < len(self.savedServices):
+ self.cbSavedServices.setCurrentIndex(resolvedIndex + 1)
+ else:
+ self.cbSavedServices.setCurrentIndex(0)
+
+ self.cbSavedServices.blockSignals(False)
+ self.updateUrlManagerButtons()
+
+ if resolvedIndex is not None and 0 <= resolvedIndex < len(self.savedServices):
+ self.applySavedService(resolvedIndex)
+
+ def applySavedService(self, index: int) -> None:
+ if not 0 <= index < len(self.savedServices):
+ return
+
+ service = self.savedServices[index]
+ self.leServiceName.setText(service.get('name', ''))
+ self.leBaseUrl.setText(service['url'])
+ versionIndex = self.cbVersion.findText(service['version'])
+ if versionIndex >= 0:
+ self.cbVersion.setCurrentIndex(versionIndex)
+ self.persistSavedServices(selectedIndex=index)
+ self.updateUrlManagerButtons()
+
+ def onSavedServiceSelected(self) -> None:
+ selectedIndex = self.getSelectedSavedServiceIndex()
+ if selectedIndex is None:
+ self.persistSavedServices(selectedIndex=None)
+ self.updateUrlManagerButtons()
+ return
+
+ self.applySavedService(selectedIndex)
+
+ def prepareNewService(self) -> None:
+ self.cbSavedServices.blockSignals(True)
+ self.cbSavedServices.setCurrentIndex(0)
+ self.cbSavedServices.blockSignals(False)
+ self.persistSavedServices(selectedIndex=None)
+ self.leServiceName.clear()
+ self.leBaseUrl.clear()
+ self.cbVersion.setCurrentIndex(1)
+ self.updateUrlManagerButtons()
+ self.leServiceName.setFocus()
+
+ def saveCurrentService(self) -> None:
+ serviceName = self.leServiceName.text().strip()
+ baseUrl = self.leBaseUrl.text().strip()
+ if not baseUrl:
+ self.writeToPluginMessageBar('Please enter a WCS URL before saving.',
+ level=Qgis.Warning,
+ duration=4)
+ return
+
+ service = {
+ 'name': serviceName,
+ 'url': baseUrl,
+ 'version': self.cbVersion.currentText()
+ }
+ selectedIndex = self.getSelectedSavedServiceIndex()
+ serviceIdentity = self.getSavedServiceIdentity(service)
+
+ duplicateIndex = next((index for index, savedService in enumerate(self.savedServices)
+ if self.getSavedServiceIdentity(savedService) == serviceIdentity
+ and index != selectedIndex), None)
+ if duplicateIndex is not None:
+ self.savedServices[duplicateIndex] = service
+ selectedIndex = duplicateIndex
+ infoMessage = 'WCS service updated.'
+ elif selectedIndex is None:
+ self.savedServices.append(service)
+ selectedIndex = len(self.savedServices) - 1
+ infoMessage = 'WCS service saved.'
+ else:
+ self.savedServices[selectedIndex] = service
+ infoMessage = 'WCS service updated.'
+
+ self.persistSavedServices(selectedIndex=selectedIndex)
+ self.refreshSavedServicesCombo(selectedIndex=selectedIndex)
+ self.writeToPluginMessageBar(infoMessage,
+ level=Qgis.Info,
+ duration=4)
+
+ def deleteCurrentService(self) -> None:
+ selectedIndex = self.getSelectedSavedServiceIndex()
+ if selectedIndex is None:
+ self.writeToPluginMessageBar('Select a saved WCS service to delete it.',
+ level=Qgis.Warning,
+ duration=4)
+ return
+
+ del self.savedServices[selectedIndex]
+ self.persistSavedServices(selectedIndex=None)
+ self.refreshSavedServicesCombo()
+ self.leServiceName.clear()
+ self.leBaseUrl.clear()
+ self.cbVersion.setCurrentIndex(1)
+ self.writeToPluginMessageBar('WCS service deleted.',
+ level=Qgis.Info,
+ duration=4)
+
+ def exportSavedServices(self) -> None:
+ if not self.savedServices:
+ self.writeToPluginMessageBar('There are no saved WCS services to export.',
+ level=Qgis.Warning,
+ duration=4)
+ return
+
+ filePath, _ = QFileDialog.getSaveFileName(self,
+ 'Export saved WCS services',
+ 'simplewcs2-services.json',
+ 'JSON files (*.json);;All files (*)')
+ if not filePath:
+ return
+
+ try:
+ with open(filePath, 'w', encoding='utf-8') as exportFile:
+ json.dump(self.savedServices, exportFile, indent=2)
+ except OSError as e:
+ self.writeToPluginMessageBar(f'Export failed: {e}',
+ level=Qgis.Warning,
+ duration=6)
+ logWarnMessage(f'Export of saved WCS services failed: {e}')
+ return
+
+ self.writeToPluginMessageBar('Saved WCS services exported.',
+ level=Qgis.Info,
+ duration=4)
+
+ def importSavedServices(self) -> None:
+ filePath, _ = QFileDialog.getOpenFileName(self,
+ 'Import saved WCS services',
+ '',
+ 'JSON files (*.json);;All files (*)')
+ if not filePath:
+ return
+
+ try:
+ with open(filePath, 'r', encoding='utf-8') as importFile:
+ importedServices = json.load(importFile)
+ except (OSError, json.JSONDecodeError) as e:
+ self.writeToPluginMessageBar(f'Import failed: {e}',
+ level=Qgis.Warning,
+ duration=6)
+ logWarnMessage(f'Import of saved WCS services failed: {e}')
+ return
+
+ if not isinstance(importedServices, list):
+ self.writeToPluginMessageBar('Import failed: file does not contain a service list.',
+ level=Qgis.Warning,
+ duration=6)
+ return
+
+ selectedIndex = None
+ importedCount = 0
+ skippedCount = 0
+
+ for importedService in importedServices:
+ normalizedService = self.normalizeSavedService(importedService)
+ if not normalizedService:
+ skippedCount += 1
+ continue
+
+ importedCount += 1
+ serviceIdentity = self.getSavedServiceIdentity(normalizedService)
+ existingIndex = next((index for index, savedService in enumerate(self.savedServices)
+ if self.getSavedServiceIdentity(savedService) == serviceIdentity), None)
+
+ if existingIndex is None:
+ self.savedServices.append(normalizedService)
+ selectedIndex = len(self.savedServices) - 1
+ else:
+ self.savedServices[existingIndex] = normalizedService
+ selectedIndex = existingIndex
+
+ if importedCount == 0:
+ self.writeToPluginMessageBar('Import contained no valid saved WCS services.',
+ level=Qgis.Warning,
+ duration=6)
+ return
+
+ self.persistSavedServices(selectedIndex=selectedIndex)
+ self.refreshSavedServicesCombo(selectedIndex=selectedIndex)
+
+ infoMessage = f'Imported {importedCount} saved WCS service'
+ if importedCount != 1:
+ infoMessage += 's'
+ if skippedCount:
+ infoMessage += f'; skipped {skippedCount} invalid entr'
+ infoMessage += 'y.' if skippedCount == 1 else 'ies.'
+ else:
+ infoMessage += '.'
+
+ self.writeToPluginMessageBar(infoMessage,
+ level=Qgis.Info,
+ duration=6)
+
+ def updateUrlManagerButtons(self) -> None:
+ hasBaseUrl = len(self.leBaseUrl.text().strip()) > 0
+ self.btnGetCapabilities.setEnabled(hasBaseUrl)
+ self.btnSaveService.setEnabled(hasBaseUrl)
+ self.btnDeleteService.setEnabled(self.getSelectedSavedServiceIndex() is not None)
+ self.btnExportServices.setEnabled(len(self.savedServices) > 0)
+
def adjustBoundingBoxesToCrsIfVisible(self) -> None:
"""
Slot called when the crs of the project has changed:
@@ -792,10 +1085,7 @@ def checkUrlSyntax(self, url: str) -> str:
def enableBtnGetCapabilities(self) -> None:
"""Enables GetCapabilities button if a wcs service url is entered"""
- if len(self.leBaseUrl.text()) > 0:
- self.btnGetCapabilities.setEnabled(True)
- else:
- self.btnGetCapabilities.setEnabled(False)
+ self.btnGetCapabilities.setEnabled(len(self.leBaseUrl.text().strip()) > 0)
def enableBtnGetCoverage(self) -> None:
self.btnGetCoverage.setEnabled(True)
diff --git a/simplewcs_dialog_base.ui b/simplewcs_dialog_base.ui
index 66248a9..1b09a28 100644
--- a/simplewcs_dialog_base.ui
+++ b/simplewcs_dialog_base.ui
@@ -39,52 +39,165 @@
URL
- -
-
-
-
- 8
- 75
- true
-
-
-
- Enter WCS 2.X URL:
-
-
- false
-
-
-
- -
-
-
-
-
-
-
-
-
- e.g. https://example.org/wcs?
-
-
-
- -
-
-
-
- 75
- true
-
-
-
- Choose Version
-
-
-
- -
-
-
+ -
+
+
+ Saved Services
+
+
+
-
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ New
+
+
+
+ -
+
+
+ Save
+
+
+
+ -
+
+
+ Delete
+
+
+
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Import
+
+
+
+ -
+
+
+ Export
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Service Details
+
+
+
-
+
+
+
+ 8
+ 75
+ true
+
+
+
+ Service Name:
+
+
+
+ -
+
+
+ e.g. Brandenburg DGM
+
+
+
+ -
+
+
+
+ 8
+ 75
+ true
+
+
+
+ Enter WCS 2.X URL:
+
+
+ false
+
+
+
+ -
+
+
+
+
+
+
+
+
+ e.g. https://example.org/wcs?
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Choose Version
+
+
+
+ -
+
+
+
+
+
-