diff options
-rw-r--r-- | doc/administration/configuration.rst | 4 | ||||
-rw-r--r-- | fietsboek/config.py | 15 | ||||
-rw-r--r-- | fietsboek/jinja2.py | 4 | ||||
-rw-r--r-- | fietsboek/views/tileproxy.py | 256 |
4 files changed, 159 insertions, 120 deletions
diff --git a/doc/administration/configuration.rst b/doc/administration/configuration.rst index 5f0e3d4..e77e3cc 100644 --- a/doc/administration/configuration.rst +++ b/doc/administration/configuration.rst @@ -168,6 +168,10 @@ users will be able to use the Thunderforest maps (to protect your quota), this can be changed by setting ``thunderforest.access = public`` (default is "restricted"). +You can enable `Stamen <http://maps.stamen.com>`__ support by setting +``stamen.maps`` to the desired maps, e.g. ``stamen.maps = toner terrain +watercolor``. + You can add custom tile layers in the following way: .. code:: ini diff --git a/fietsboek/config.py b/fietsboek/config.py index 41f6a64..9949198 100644 --- a/fietsboek/config.py +++ b/fietsboek/config.py @@ -127,7 +127,7 @@ class TileLayerConfig(BaseModel): layer_type: LayerType = Field(LayerType.BASE, alias="type") """Type of the layer.""" - zoom: int = 22 + zoom: typing.Optional[int] = 22 """Maximum zoom factor of the layer.""" attribution: str = "" @@ -193,6 +193,9 @@ class Config(BaseModel): thunderforest_access: LayerAccess = Field(LayerAccess.RESTRICTED, alias="thunderforest.access") """Thunderforest access restriction.""" + stamen_maps: PyramidList = Field([], alias="stamen.maps") + """Enabled stamen maps.""" + disable_tile_proxy: bool = Field(False, alias="fietsboek.tile_proxy.disable") """Disable the tile proxy.""" @@ -216,6 +219,16 @@ class Config(BaseModel): raise ValueError(f"Unknown mailing scheme {parsed.scheme}".strip()) return value + @validator("stamen_maps") + def _known_stamen(cls, value): + """Ensures that the stamen maps are known.""" + maps = set(value) + known_maps = {"toner", "terrain", "watercolor"} + bad_maps = maps - known_maps + if bad_maps: + raise ValueError("Unknown stamen maps: " + ", ".join(bad_maps)) + return value + def derive_secret(self, what_for): """Derive a secret for other parts of the application. diff --git a/fietsboek/jinja2.py b/fietsboek/jinja2.py index 6e5e7b6..07c9087 100644 --- a/fietsboek/jinja2.py +++ b/fietsboek/jinja2.py @@ -99,13 +99,13 @@ def global_embed_tile_layers(request): if request.config.disable_tile_proxy: def _url(source): - return source.url_template + return source.url else: def _url(source): return ( - request.route_url("tile-proxy", provider=source.key, x="{x}", y="{y}", z="{z}") + request.route_url("tile-proxy", provider=source.layer_id, x="{x}", y="{y}", z="{z}") .replace("%7Bx%7D", "{x}") .replace("%7By%7D", "{y}") .replace("%7Bz%7D", "{z}") diff --git a/fietsboek/views/tileproxy.py b/fietsboek/views/tileproxy.py index f0612dc..1ec609b 100644 --- a/fietsboek/views/tileproxy.py +++ b/fietsboek/views/tileproxy.py @@ -9,10 +9,11 @@ Additionally, this protects the users' IP, as only fietsboek can see it. import datetime import random import logging -from typing import NamedTuple, Optional +from typing import List from itertools import chain from pyramid.view import view_config +from pyramid.request import Request from pyramid.response import Response from pyramid.httpexceptions import HTTPBadRequest, HTTPGatewayTimeout @@ -20,26 +21,7 @@ import requests from requests.exceptions import ReadTimeout from .. import __VERSION__ -from ..config import LayerType, LayerAccess - - -class TileSource(NamedTuple): - """Represents a remote server that can provide tiles to us.""" - - key: str - """Key to indicate this source in URLs.""" - name: str - """Human-readable name of the source.""" - url_template: str - """URL with placeholders.""" - layer_type: LayerType - """Type of this layer.""" - zoom: Optional[int] - """Max zoom of this layer.""" - access: LayerAccess - """Access restrictions to use this layer.""" - attribution: str - """Attribution string.""" +from ..config import Config, LayerType, LayerAccess, TileLayerConfig LOGGER = logging.getLogger(__name__) @@ -54,14 +36,14 @@ _jb_copy = _href("https://www.j-berkemeier.de/GPXViewer", "GPXViewer") DEFAULT_TILE_LAYERS = [ # Main base layers - TileSource( - "osm", - "OSM", - "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - LayerType.BASE, - 19, - LayerAccess.PUBLIC, - "".join( + TileLayerConfig( + layer_id="osm", + name="OSM", + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + layer_type=LayerType.BASE, + zoom=19, + access=LayerAccess.PUBLIC, + attribution="".join( [ _jb_copy, " | Map data © ", @@ -71,15 +53,15 @@ DEFAULT_TILE_LAYERS = [ ] ), ), - TileSource( - "satellite", - "Satellit", - "https://server.arcgisonline.com/ArcGIS/rest/services/" + TileLayerConfig( + layer_id="satellite", + name="Satellit", + url="https://server.arcgisonline.com/ArcGIS/rest/services/" "World_Imagery/MapServer/tile/{z}/{y}/{x}", - LayerType.BASE, - 21, - LayerAccess.PUBLIC, - "".join( + layer_type=LayerType.BASE, + zoom=21, + access=LayerAccess.PUBLIC, + attribution="".join( [ _jb_copy, " | Map data © ", @@ -89,14 +71,14 @@ DEFAULT_TILE_LAYERS = [ ] ), ), - TileSource( - "osmde", - "OSMDE", - "https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png", - LayerType.BASE, - 19, - LayerAccess.PUBLIC, - "".join( + TileLayerConfig( + layer_id="osmde", + name="OSMDE", + url="https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png", + layer_type=LayerType.BASE, + zoom=19, + access=LayerAccess.PUBLIC, + attribution="".join( [ _jb_copy, " | Map data © ", @@ -106,14 +88,14 @@ DEFAULT_TILE_LAYERS = [ ] ), ), - TileSource( - "opentopo", - "Open Topo", - "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", - LayerType.BASE, - 17, - LayerAccess.PUBLIC, - "".join( + TileLayerConfig( + layer_id="opentopo", + name="Open Topo", + url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", + layer_type=LayerType.BASE, + zoom=17, + access=LayerAccess.PUBLIC, + attribution="".join( [ _jb_copy, " | Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © ", @@ -122,15 +104,15 @@ DEFAULT_TILE_LAYERS = [ ] ), ), - TileSource( - "topplusopen", - "TopPlusOpen", - "https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/" + TileLayerConfig( + layer_id="topplusopen", + name="TopPlusOpen", + url="https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/" "1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png", - LayerType.BASE, - 18, - LayerAccess.PUBLIC, - "".join( + layer_type=LayerType.BASE, + zoom=18, + access=LayerAccess.PUBLIC, + attribution="".join( [ _jb_copy, " | Kartendaten: © ", @@ -143,34 +125,85 @@ DEFAULT_TILE_LAYERS = [ ), ), # Overlay layers - TileSource( - "opensea", - "OpenSea", - "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png", - LayerType.OVERLAY, - None, - LayerAccess.PUBLIC, - 'Kartendaten: © <a href="http://www.openseamap.org">OpenSeaMap</a> contributors', + TileLayerConfig( + layer_id="opensea", + name="OpenSea", + url="https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png", + layer_type=LayerType.OVERLAY, + zoom=None, + access=LayerAccess.PUBLIC, + attribution=( + 'Kartendaten: © <a href="http://www.openseamap.org">OpenSeaMap</a> contributors' + ), + ), + TileLayerConfig( + layer_id="hiking", + name="Hiking", + url="https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png", + layer_type=LayerType.OVERLAY, + zoom=None, + access=LayerAccess.PUBLIC, + attribution=( + f'© {_href("http://waymarkedtrails.org", "Sarah Hoffmann")} ' + f'({_href("https://creativecommons.org/licenses/by-sa/3.0/", "CC-BY-SA")})' + ), + ), + TileLayerConfig( + layer_id="cycling", + name="Cycling", + url="https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png", + layer_type=LayerType.OVERLAY, + zoom=None, + access=LayerAccess.PUBLIC, + attribution=( + f'© {_href("http://waymarkedtrails.org", "Sarah Hoffmann")} ' + f'({_href("https://creativecommons.org/licenses/by-sa/3.0/", "CC-BY-SA")})' + ), ), - TileSource( - "hiking", - "Hiking", - "https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png", - LayerType.OVERLAY, - None, - LayerAccess.PUBLIC, - f'© {_href("http://waymarkedtrails.org", "Sarah Hoffmann")} ' - f'({_href("https://creativecommons.org/licenses/by-sa/3.0/", "CC-BY-SA")})', +] + +STAMEN_LAYERS = [ + TileLayerConfig( + layer_id="stamen-toner", + name="Stamen Toner", + url="https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png", + layer_type=LayerType.BASE, + zoom=12, + access=LayerAccess.PUBLIC, + attribution=( + f'{_jb_copy} | Map tiles by <a href="http://stamen.com">Stamen Design</a>, ' + 'under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. ' + 'Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under ' + '<a href="http://www.openstreetmap.org/copyright">ODbL</a>.' + ), + ), + TileLayerConfig( + layer_id="stamen-terrain", + name="Stamen Terrain", + url="https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png", + layer_type=LayerType.BASE, + zoom=12, + access=LayerAccess.PUBLIC, + attribution=( + f'{_jb_copy} | Map tiles by <a href="http://stamen.com">Stamen Design</a>, ' + 'under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. ' + 'Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under ' + '<a href="http://www.openstreetmap.org/copyright">ODbL</a>.' + ), ), - TileSource( - "cycling", - "Cycling", - "https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png", - LayerType.OVERLAY, - None, - LayerAccess.PUBLIC, - f'© {_href("http://waymarkedtrails.org", "Sarah Hoffmann")} ' - f'({_href("https://creativecommons.org/licenses/by-sa/3.0/", "CC-BY-SA")})', + TileLayerConfig( + layer_id="stamen-watercolor", + name="Stamen Watercolor", + url="https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png", + layer_type=LayerType.BASE, + zoom=12, + access=LayerAccess.PUBLIC, + attribution=( + f'{_jb_copy} | Map tiles by <a href="http://stamen.com">Stamen Design</a>, ' + 'under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. ' + 'Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under ' + '<a href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>.' + ), ), ] @@ -200,7 +233,7 @@ def tile_proxy(request): raise HTTPBadRequest("Tile proxying is disabled") provider = request.matchdict["provider"] - tile_sources = {source.key: source for source in sources_for(request)} + tile_sources = {source.layer_id: source for source in sources_for(request)} if provider not in tile_sources: raise HTTPBadRequest("Invalid provider") @@ -223,7 +256,7 @@ def tile_proxy(request): LOGGER.debug("Aborted attempt to contact %s due to previous timeouts", provider) raise HTTPGatewayTimeout(f"Avoiding request to {provider}") - url = tile_sources[provider].url_template.format(x=x, y=y, z=z, s=random.choice("abc")) + url = tile_sources[provider].url.format(x=x, y=y, z=z, s=random.choice("abc")) headers = { "user-agent": f"Fietsboek-Tile-Proxy/{__VERSION__}", } @@ -248,13 +281,11 @@ def tile_proxy(request): return Response(resp.content, content_type=resp.headers.get("Content-type", content_type)) -def sources_for(request): +def sources_for(request: Request) -> List[TileLayerConfig]: """Returns all eligible tile sources for the given request. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: A list of tile sources. - :rtype: list[TileSource] """ return [ source @@ -262,7 +293,7 @@ def sources_for(request): ( default_layer for default_layer in DEFAULT_TILE_LAYERS - if default_layer.key in request.config.default_tile_layers + if default_layer.layer_id in request.config.default_tile_layers ), extract_tile_layers(request.config), ) @@ -270,17 +301,16 @@ def sources_for(request): ] -def extract_tile_layers(config): +def extract_tile_layers(config: Config) -> List[TileLayerConfig]: """Extract all defined tile layers from the settings. :param config: The fietsboek config. - :type config: fietsboek.config.Config :return: A list of extracted tile sources. - :rtype: list[TileSource] """ layers = [] layers.extend(_extract_thunderforest(config)) - layers.extend(_extract_user_layers(config)) + layers.extend(_extract_stamen(config)) + layers.extend(config.tile_layers) return layers @@ -301,26 +331,18 @@ def _extract_thunderforest(config): f"https://tile.thunderforest.com/{tf_map}/" f"{{z}}/{{x}}/{{y}}.png?apikey={tf_api_key}" ) - yield TileSource( - f"tf-{tf_map}", - f"TF {tf_map.title()}", - url, - LayerType.BASE, - 22, - tf_access, - tf_attribution, + yield TileLayerConfig( + layer_id=f"tf-{tf_map}", + name=f"TF {tf_map.title()}", + url=url, + layer_type=LayerType.BASE, + zoom=22, + access=tf_access, + attribution=tf_attribution, ) -def _extract_user_layers(config): - # Any other custom maps - for layer in config.tile_layers: - yield TileSource( - layer.layer_id, - layer.name, - layer.url, - layer.layer_type, - layer.zoom, - layer.access, - layer.attribution, - ) +def _extract_stamen(config): + layers = {layer.layer_id: layer for layer in STAMEN_LAYERS} + for name in config.stamen_maps: + yield layers[f"stamen-{name}"] |