aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/administration/configuration.rst4
-rw-r--r--fietsboek/config.py15
-rw-r--r--fietsboek/jinja2.py4
-rw-r--r--fietsboek/views/tileproxy.py256
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 &copy; ",
@@ -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 &copy; ",
@@ -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 &copy; ",
@@ -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'&copy; {_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'&copy; {_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'&copy; {_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'&copy; {_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}"]