diff --git a/swiss_locator/core/filters/filter_type.py b/swiss_locator/core/filters/filter_type.py index 87f480c..aa9c59d 100644 --- a/swiss_locator/core/filters/filter_type.py +++ b/swiss_locator/core/filters/filter_type.py @@ -6,3 +6,4 @@ class FilterType(Enum): Layers = "layers" # this is used in map.geo.admin as the search type Feature = "featuresearch" # this is used in map.geo.admin as the search type WMTS = "wmts" + VectorTiles = "vectortiles" diff --git a/swiss_locator/core/filters/swiss_locator_filter.py b/swiss_locator/core/filters/swiss_locator_filter.py index e63ed66..aa4a0e5 100644 --- a/swiss_locator/core/filters/swiss_locator_filter.py +++ b/swiss_locator/core/filters/swiss_locator_filter.py @@ -43,6 +43,7 @@ QgsLocatorContext, QgsFeedback, QgsRasterLayer, + QgsVectorTileLayer, ) from qgis.gui import QgsRubberBand, QgisInterface @@ -53,6 +54,7 @@ WMSLayerResult, LocationResult, FeatureResult, + VectorTilesLayerResult, NoResult, ) from swiss_locator.core.settings import Settings @@ -74,6 +76,8 @@ def result_from_data(result: QgsLocatorResult): return LocationResult.from_dict(dict_data) if dict_data["type"] == "FeatureResult": return FeatureResult.from_dict(dict_data) + if dict_data["type"] == "VectorTilesLayerResult": + return VectorTilesLayerResult.from_dict(dict_data) return NoResult() @@ -384,7 +388,7 @@ def triggerResult(self, result: QgsLocatorResult): url_with_params = "&".join([f"{k}={v}" for (k, v) in params.items()]) self.info(f"Loading layer: {url_with_params}") - wms_layer = QgsRasterLayer(url_with_params, result.displayString, "wms") + ch_layer = QgsRasterLayer(url_with_params, result.displayString, "wms") label = QLabel() label.setTextFormat(Qt.RichText) label.setTextInteractionFlags(Qt.TextBrowserInteraction) @@ -399,7 +403,7 @@ def triggerResult(self, result: QgsLocatorResult): ) ) - if not wms_layer.isValid(): + if not ch_layer.isValid(): msg = self.tr( "Cannot load Layers layer: {} ({})".format( swiss_result.title, swiss_result.layer @@ -415,7 +419,7 @@ def triggerResult(self, result: QgsLocatorResult): ) level = Qgis.Info - QgsProject.instance().addMapLayer(wms_layer) + QgsProject.instance().addMapLayer(ch_layer) self.message_emitted.emit(self.displayName(), msg, level, label) @@ -427,6 +431,80 @@ def triggerResult(self, result: QgsLocatorResult): if self.settings.value("show_map_tip"): self.show_map_tip(swiss_result.layer, swiss_result.feature_id, point) + # Vector tiles + elif type(swiss_result) == VectorTilesLayerResult: + params = dict() + params["styleUrl"] = swiss_result.style or "" + params["url"] = swiss_result.url + params["type"] = "xyz" + # Max and min zoom levels cound be retrieved from metadata JSON files like: + # https://vectortiles.geo.admin.ch/tiles/ch.swisstopo.base.vt/v1.0.0/tiles.json + # All Swiss services use 0-14 levels (level 14 goes up to buildings) + params["zmax"] = "14" + params["zmin"] = "0" + + url_with_params = "&".join([f"{k}={v}" for (k, v) in params.items()]) + + self.info(f"Loading layer: {url_with_params}") + ch_layer = QgsVectorTileLayer(url_with_params, result.displayString) + + if not ch_layer.isValid(): + msg = self.tr( + "Cannot load Vector Tiles layer: {}".format( + swiss_result.title + ) + ) + level = Qgis.Warning + self.info(msg, level) + else: + ch_layer.setLabelsEnabled(True) + ch_layer.loadDefaultMetadata() + + error, warnings = '', [] + res, sublayers = ch_layer.loadDefaultStyleAndSubLayers(error, warnings) + + if sublayers: + msg = self.tr( + "Sublayers found ({}): {}".format( + swiss_result.title, + "; ".join([sublayer.name() for sublayer in sublayers]) + ) + ) + level = Qgis.Info + self.info(msg, level) + if error or warnings: + msg = self.tr( + "Error/warning found while loading default styles and sublayers for layer {}. Error: {} Warning: {}".format( + swiss_result.title, + error, + "; ".join(warnings) + ) + ) + level = Qgis.Warning + self.info(msg, level) + + msg = self.tr( + "Layer added to the map: {}".format( + swiss_result.title + ) + ) + level = Qgis.Info + self.info(msg, level) + + # Load basemap layers at the bottom of the layer tree + root = QgsProject.instance().layerTreeRoot() + if sublayers: + # Sublayers should be loaded on top of the vector tile + # layer. We group them to keep them all together. + group = root.insertGroup(-1, ch_layer.name()) + all_layers = sublayers + [ch_layer] + QgsProject.instance().addMapLayers(all_layers, False) + for _layer in all_layers: + group.addLayer(_layer) + else: + QgsProject.instance().addMapLayer(ch_layer, False) + root.insertLayer(-1, ch_layer) + # Location else: point = QgsGeometry.fromPointXY(swiss_result.point) diff --git a/swiss_locator/core/filters/swiss_locator_filter_vector_tiles.py b/swiss_locator/core/filters/swiss_locator_filter_vector_tiles.py new file mode 100644 index 0000000..cc8b937 --- /dev/null +++ b/swiss_locator/core/filters/swiss_locator_filter_vector_tiles.py @@ -0,0 +1,89 @@ +from qgis.gui import QgisInterface +from qgis.core import ( + QgsApplication, + QgsBlockingNetworkRequest, + QgsFetchedContent, + QgsLocatorResult, + QgsFeedback, +) +from swiss_locator.core.filters.swiss_locator_filter import ( + SwissLocatorFilter, +) +from swiss_locator.core.filters.filter_type import FilterType +from swiss_locator.core.results import VectorTilesLayerResult + + +class SwissLocatorFilterVectorTiles(SwissLocatorFilter): + def __init__(self, iface: QgisInterface = None, crs: str = None): + super().__init__(FilterType.VectorTiles, iface, crs) + + # Show all available base maps without requiring a search + self.minimum_search_length = 0 + + def clone(self): + return SwissLocatorFilterVectorTiles(crs=self.crs) + + def displayName(self): + return self.tr("Swiss Geoportal Vector Tile Base Map Layers") + + def prefix(self): + return "chb" + + def hasConfigWidget(self): + return False + + def perform_fetch_results(self, search: str, feedback: QgsFeedback): + data = { + "base map": { + "title": "Base map", + "description": "", + "url": "https://vectortiles.geo.admin.ch/tiles/ch.swisstopo.base.vt/v1.0.0/{z}/{x}/{y}.pbf", + "style": "https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json" + }, + "light base map": { + "title": "Light base map", "description": "", + "url": "https://vectortiles.geo.admin.ch/tiles/ch.swisstopo.base.vt/v1.0.0/{z}/{x}/{y}.pbf", + "style": "https://vectortiles.geo.admin.ch/styles/ch.swisstopo.lightbasemap.vt/style.json" + }, + "imagery base map": { + "title": "Imagery base map", "description": "", + "url": "https://vectortiles.geo.admin.ch/tiles/ch.swisstopo.base.vt/v1.0.0/{z}/{x}/{y}.pbf", + "style": "https://vectortiles.geo.admin.ch/styles/ch.swisstopo.imagerybasemap.vt/style.json" + }, + "leichte-basiskarte": { + "title": "leichte-basiskarte", "description": "", + "url": "https://vectortiles.geo.admin.ch/tiles/ch.swisstopo.leichte-basiskarte.vt/v3.0.1/{z}/{x}/{y}.pbf", + "style": "https://vectortiles.geo.admin.ch/styles/ch.swisstopo.leichte-basiskarte.vt/style.json" + }, + "leichte-basiskarte-imagery": { + "title": "leichte-basiskarte-imagery", "description": "", + "url": "https://vectortiles.geo.admin.ch/tiles/ch.swisstopo.leichte-basiskarte.vt/v3.0.1/{z}/{x}/{y}.pbf", + "style": "https://vectortiles.geo.admin.ch/styles/ch.swisstopo.leichte-basiskarte-imagery.vt/style.json" + }, + } + + for keyword in list(data.keys()): + results = {} + score = 1 + if not search or search.lower() in keyword: + result = QgsLocatorResult() + result.filter = self + result.icon = QgsApplication.getThemeIcon("/mActionAddVectorTileLayer.svg") + + result.displayString = data[keyword]["title"] + result.description = data[keyword]["description"] + result.userData = VectorTilesLayerResult( + layer=data[keyword]["title"], + title=data[keyword]["title"], + url=data[keyword]["url"], + style=data[keyword]["style"], + ).as_definition() + + results[result] = score + + # sort the results with score + #results = sorted([result for (result, score) in results.items()]) + + for result in results: + self.resultFetched.emit(result) + self.result_found = True diff --git a/swiss_locator/core/results.py b/swiss_locator/core/results.py index 3cbc81e..1bdabb6 100644 --- a/swiss_locator/core/results.py +++ b/swiss_locator/core/results.py @@ -127,6 +127,39 @@ def as_definition(self): return json.dumps(definition) +class VectorTilesLayerResult: + def __init__( + self, + layer, + title, + url, + style: str = None, + ): + self.title = title + self.layer = layer + self.url = url + self.style = style + + @staticmethod + def from_dict(dict_data: dict): + return VectorTilesLayerResult( + dict_data["layer"], + dict_data["title"], + dict_data["url"], + style=dict_data.get("style"), + ) + + def as_definition(self): + definition = { + "type": "VectorTilesLayerResult", + "title": self.title, + "layer": self.layer, + "url": self.url, + "style": self.style, + } + return json.dumps(definition) + + class NoResult: def __init__(self): pass diff --git a/swiss_locator/core/settings.py b/swiss_locator/core/settings.py index c702cf9..c2889a6 100644 --- a/swiss_locator/core/settings.py +++ b/swiss_locator/core/settings.py @@ -76,11 +76,19 @@ def __init__(self): self.add_setting(Integer(f"{FilterType.WMTS.value}_limit", Scope.Global, 8)) self.add_setting( Enum( - f"{FilterType.Feature.value}_priority", + f"{FilterType.VectorTiles.value}_priority", Scope.Global, QgsLocatorFilter.Medium, ) ) + self.add_setting(Integer(f"{FilterType.VectorTiles.value}_limit", Scope.Global, 8)) + self.add_setting( + Enum( + f"{FilterType.Feature.value}_priority", + Scope.Global, + QgsLocatorFilter.Highest, + ) + ) self.add_setting(Integer(f"{FilterType.Feature.value}_limit", Scope.Global, 8)) self.add_setting( Enum( diff --git a/swiss_locator/swiss_locator_plugin.py b/swiss_locator/swiss_locator_plugin.py index 29c9afc..9896152 100644 --- a/swiss_locator/swiss_locator_plugin.py +++ b/swiss_locator/swiss_locator_plugin.py @@ -33,6 +33,9 @@ SwissLocatorFilterLocation, ) from swiss_locator.core.filters.swiss_locator_filter_wmts import SwissLocatorFilterWMTS +from swiss_locator.core.filters.swiss_locator_filter_vector_tiles import ( + SwissLocatorFilterVectorTiles, +) class SwissLocatorPlugin: @@ -55,6 +58,7 @@ def initGui(self): SwissLocatorFilterLocation, SwissLocatorFilterWMTS, SwissLocatorFilterLayer, + SwissLocatorFilterVectorTiles, SwissLocatorFilterFeature, ): self.locator_filters.append(_filter(self.iface))