Skip to content

Commit

Permalink
Merge pull request #79 from gacarrillor/vector_tiles
Browse files Browse the repository at this point in the history
[feature] Add locator filter to load Swiss vector tiles (basemaps)
  • Loading branch information
3nids authored May 16, 2024
2 parents c24639a + 4631f04 commit 27a4b03
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 4 deletions.
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"
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,
)
)
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

0 comments on commit 27a4b03

Please sign in to comment.