Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Add locator filter to load Swiss vector tiles (basemaps) #79

Merged
merged 7 commits into from
May 16, 2024
1 change: 1 addition & 0 deletions swiss_locator/core/filters/filter_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
84 changes: 81 additions & 3 deletions swiss_locator/core/filters/swiss_locator_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
QgsLocatorContext,
QgsFeedback,
QgsRasterLayer,
QgsVectorTileLayer,
)
from qgis.gui import QgsRubberBand, QgisInterface

Expand All @@ -53,6 +54,7 @@
WMSLayerResult,
LocationResult,
FeatureResult,
VectorTilesLayerResult,
NoResult,
)
from swiss_locator.core.settings import Settings
Expand All @@ -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()


Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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"
gacarrillor marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down
89 changes: 89 additions & 0 deletions swiss_locator/core/filters/swiss_locator_filter_vector_tiles.py
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions swiss_locator/core/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion swiss_locator/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
gacarrillor marked this conversation as resolved.
Show resolved Hide resolved
)
)
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(
Expand Down
4 changes: 4 additions & 0 deletions swiss_locator/swiss_locator_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -55,6 +58,7 @@ def initGui(self):
SwissLocatorFilterLocation,
SwissLocatorFilterWMTS,
SwissLocatorFilterLayer,
SwissLocatorFilterVectorTiles,
SwissLocatorFilterFeature,
):
self.locator_filters.append(_filter(self.iface))
Expand Down