aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/__init__.py8
-rw-r--r--fietsboek/jinja2.py29
-rw-r--r--fietsboek/static/osm-monkeypatch.js74
-rw-r--r--fietsboek/templates/layout.jinja21
-rw-r--r--fietsboek/views/tileproxy.py207
5 files changed, 244 insertions, 75 deletions
diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py
index 6dc435a..21674f5 100644
--- a/fietsboek/__init__.py
+++ b/fietsboek/__init__.py
@@ -51,7 +51,8 @@ def locale_negotiator(request):
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- # pylint: disable=unused-argument
+ # pylint: disable=unused-argument, import-outside-toplevel, cyclic-import
+ from .views import tileproxy
if settings.get('session_key', '<EDIT THIS>') == '<EDIT THIS>':
raise ValueError("Please set a session signing key (session_key) in your settings!")
@@ -71,6 +72,10 @@ def main(global_config, **settings):
settings.get('available_locales', 'en'))
settings['fietsboek.pages'] = aslist(
settings.get('fietsboek.pages', ''))
+ settings['fietsboek.default_tile_layers'] = aslist(
+ settings.get('fietsboek.default_tile_layers',
+ 'osm satellite osmde opentopo topplusopen opensea cycling hiking'))
+ settings['fietsboek.tile_layers'] = tileproxy.extract_tile_layers(settings)
# Load the pages
page_manager = Pages()
@@ -104,5 +109,6 @@ def main(global_config, **settings):
jinja2_env.filters['format_decimal'] = fiets_jinja2.filter_format_decimal
jinja2_env.filters['format_datetime'] = fiets_jinja2.filter_format_datetime
jinja2_env.filters['local_datetime'] = fiets_jinja2.filter_local_datetime
+ jinja2_env.globals['embed_tile_layers'] = fiets_jinja2.global_embed_tile_layers
return config.make_wsgi_app()
diff --git a/fietsboek/jinja2.py b/fietsboek/jinja2.py
index 6043791..ff60660 100644
--- a/fietsboek/jinja2.py
+++ b/fietsboek/jinja2.py
@@ -1,5 +1,6 @@
"""Custom filters for Jinja2."""
import datetime
+import json
import jinja2
from markupsafe import Markup
@@ -77,3 +78,31 @@ def filter_local_datetime(ctx, value):
return Markup(
f'<span class="fietsboek-local-datetime" data-utc-timestamp="{timestamp}">{fallback}</span>'
)
+
+
+def global_embed_tile_layers(request):
+ """Renders the available tile servers for the current user, as a JSON object.
+
+ The returned value is wrapped as a :class:`~markupsafe.Markup` so that it
+ won't get escaped by jinja.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The available tile servers.
+ :rtype: markupsafe.Markup
+ """
+ # pylint: disable=import-outside-toplevel,cyclic-import
+ from .views import tileproxy
+ tile_sources = tileproxy.sources_for(request)
+ return Markup(json.dumps([
+ {
+ "name": source.name,
+ "url": request.route_url("tile-proxy", provider=source.key, x="{x}", y="{y}", z="{z}")
+ .replace("%7Bx%7D", "{x}")
+ .replace("%7By%7D", "{y}")
+ .replace("%7Bz%7D", "{z}"),
+ "attribution": source.attribution,
+ "type": source.layer_type.value,
+ }
+ for source in tile_sources
+ ]))
diff --git a/fietsboek/static/osm-monkeypatch.js b/fietsboek/static/osm-monkeypatch.js
index 203a4ca..a8310a2 100644
--- a/fietsboek/static/osm-monkeypatch.js
+++ b/fietsboek/static/osm-monkeypatch.js
@@ -19,77 +19,31 @@
const mycp = '<a href="https://www.j-berkemeier.de/GPXViewer" title="GPX Viewer '+JB.GPX2GM.ver+'">GPXViewer</a> | ';
- this.baseLayers = {};
-
- this.baseLayers["OSM"] = L.tileLayer(BASE_URL + 'tile/osm/{z}/{x}/{y}', {
- maxZoom: 19,
- attribution: mycp+'Map data &copy; <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank">CC-BY-SA</a>'
- });
-
- this.baseLayers["Satellit"] = L.tileLayer(BASE_URL + 'tile/satellite/{z}/{x}/{y}', {
- maxZoom: 21,
- attribution: mycp+'Map data &copy; <a href="https://www.esri.com/">Esri</a>, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
- });
-
- this.baseLayers["OSMDE"] = L.tileLayer(BASE_URL + 'tile/osmde/{z}/{x}/{y}', {
- maxZoom: 19,
- attribution: mycp+'Map data &copy; <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank">CC-BY-SA</a>'
- });
-
- this.baseLayers["Open Topo"] = L.tileLayer(BASE_URL + 'tile/opentopo/{z}/{x}/{y}', {
- maxZoom: 17,
- attribution: mycp+'Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © <a href="https://opentopomap.org/about">OpenTopoMap</a> (CC-BY-SA)'
- });
+ this.baseLayers = {};
+ this.overlayLayers = {};
- this.baseLayers["TopPlusOpen"] = L.tileLayer(BASE_URL + 'tile/topplusopen/{z}/{x}/{y}', {
- maxZoom: 18,
- attribution: mycp+'Kartendaten: © <a href="https://www.bkg.bund.de/SharedDocs/Produktinformationen/BKG/DE/P-2017/170922-TopPlus-Web-Open.html" target==_blank"">Bundesamt für Kartographie und Geodäsie</a>'
- });
+ for (let layer of TILE_LAYERS) {
+ if (layer.type === "base") {
+ this.baseLayers[layer.name] = L.tileLayer(layer.url, {
+ maxZoom: layer.zoom,
+ attribution: layer.attribution,
+ });
+ } else if (layer.type === "overlay") {
+ this.overlayLayers[layer.name] = L.tileLayer(layer.url, {
+ attribution: layer.attribution,
+ });
+ }
+ }
// https://tileserver.4umaps.com/${z}/${x}/${y}.png
// zoomlevel 16
// https://www.4umaps.com/
- if(JB.GPX2GM.OSM_Cycle_Api_Key && JB.GPX2GM.OSM_Cycle_Api_Key.length>0) {
- this.baseLayers["Cycle"] = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey='+JB.GPX2GM.OSM_Cycle_Api_Key, {
- maxZoom: 22,
- attribution: mycp+'Map data &copy; <a href="https://www.thunderforest.com/" target="_blank">OpenCycleMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank">CC-BY-SA</a>'
- });
- }
-
- if(JB.GPX2GM.OSM_Landscape_Api_Key && JB.GPX2GM.OSM_Landscape_Api_Key.length>0) {
- this.baseLayers["Landscape"] = L.tileLayer('https://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey='+JB.GPX2GM.OSM_Landscape_Api_Key, {
- maxZoom: 22,
- attribution: mycp+'Map data &copy; <a href="https://www.thunderforest.com/" target="_blank">OpenLandscapeMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank">CC-BY-SA</a>'
- });
- }
-
- if(JB.GPX2GM.OSM_Outdoors_Api_Key && JB.GPX2GM.OSM_Outdoors_Api_Key.length>0) {
- this.baseLayers["Outdoors"] = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey='+JB.GPX2GM.OSM_Outdoors_Api_Key, {
- maxZoom: 22,
- attribution: mycp+'Map data &copy; <a href="https://www.thunderforest.com/" target="_blank">OpenOutdoorsMap</a> and contributors <a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_blank">CC-BY-SA</a>'
- });
- }
-
this.baseLayers[JB.GPX2GM.strings[JB.GPX2GM.parameters.doclang].noMap]= L.tileLayer(JB.GPX2GM.Path+"Icons/Grau256x256.png", {
maxZoom: 22,
attribution: mycp
});
- this.overlayLayers = {};
-
- this.overlayLayers["Open Sea"] = L.tileLayer(BASE_URL + 'tile/opensea/{z}/{x}/{y}', {
- attribution: 'Kartendaten: © <a href="http://www.openseamap.org">OpenSeaMap</a> contributors'
- });
-
- this.overlayLayers["Hiking"] = L.tileLayer(BASE_URL + 'tile/hiking/{z}/{x}/{y}', {
- attribution: '&copy; <a href="http://waymarkedtrails.org">Sarah Hoffmann</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)',
- });
-
- this.overlayLayers["Cycling"] = L.tileLayer(BASE_URL + 'tile/cycling/{z}/{x}/{y}', {
- attribution: '&copy; <a href="http://waymarkedtrails.org">Sarah Hoffmann</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)',
- });
-
this.layerNameTranslate = {
satellit: "Satellit",
satellite: "Satellit",
diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2
index bbd84f8..bf30143 100644
--- a/fietsboek/templates/layout.jinja2
+++ b/fietsboek/templates/layout.jinja2
@@ -20,6 +20,7 @@
<script>
const FRIENDS_URL = {{ request.route_url('json-friends') | tojson }};
+const TILE_LAYERS = {{ embed_tile_layers(request) }};
const BASE_URL = {{ request.route_url('home') | tojson }};
const LOCALE = {{ request.localizer.locale_name.replace('_', '-') | tojson }};
{% if request.registry.settings.get("thunderforest.api_key") %}
diff --git a/fietsboek/views/tileproxy.py b/fietsboek/views/tileproxy.py
index 5195c91..42e1a67 100644
--- a/fietsboek/views/tileproxy.py
+++ b/fietsboek/views/tileproxy.py
@@ -9,6 +9,10 @@ Additionally, this protects the users' IP, as only fietsboek can see it.
import datetime
import random
import logging
+import re
+from enum import Enum
+from typing import NamedTuple
+from itertools import chain
from pyramid.view import view_config
from pyramid.response import Response
@@ -20,23 +24,152 @@ from requests.exceptions import ReadTimeout
from .. import __VERSION__
+class LayerType(Enum):
+ """Enum to distinguish base layers and overlay layers."""
+ BASE = "base"
+ OVERLAY = "overlay"
+
+
+class LayerAccess(Enum):
+ """Enum discerning whether a layer is publicly accessible or restriced to
+ logged-in users.
+
+ Note that in the future, a finer-grained distinction might be possible.
+ """
+ PUBLIC = "public"
+ RESTRICTED = "restricted"
+
+
+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: int
+ """Max zoom of this layer."""
+ access: LayerAccess
+ """Access restrictions to use this layer."""
+ attribution: str
+ """Attribution string."""
+
+
LOGGER = logging.getLogger(__name__)
-TILE_SOURCES = {
+
+def _href(url, text):
+ return f'<a href="{url}" target="_blank">{text}</a>'
+
+
+_jb_copy = _href("https://www.j-berkemeier.de/GPXViewer", "GPXViewer")
+
+
+DEFAULT_TILE_LAYERS = [
# Main base layers
- 'osm': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
- 'osmde': 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
- 'opentopo': 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
- 'topplusopen': 'https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/'
- '1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png',
- 'satellite': 'https://server.arcgisonline.com/ArcGIS/rest/services/'
- 'World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ TileSource(
+ 'osm',
+ 'OSM',
+ 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ LayerType.BASE,
+ 19,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Map data &copy; ',
+ _href("https://www.openstreetmap.org/", "OpenStreetMap"), ' and contributors ',
+ _href("https://creativecommons.org/licenses/by-sa/2.0/", "CC-BY-SA"),
+ ]),
+ ),
+ TileSource(
+ 'satellite',
+ 'Satellit',
+ 'https://server.arcgisonline.com/ArcGIS/rest/services/'
+ 'World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ LayerType.BASE,
+ 21,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Map data &copy; ', _href("https://www.esri.com", "Esri"),
+ ', i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, ',
+ 'IGP, UPR-EGP, and the GIS User Community',
+ ]),
+ ),
+ TileSource(
+ 'osmde',
+ 'OSMDE',
+ 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
+ LayerType.BASE,
+ 19,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Map data &copy; ',
+ _href("https://www.openstreetmap.org/", "OpenStreetMap"), ' and contributors ',
+ _href("https://creativecommons.org/licenses/by-sa/2.0/", "CC-BY-SA")
+ ]),
+ ),
+ TileSource(
+ 'opentopo',
+ 'Open Topo',
+ 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
+ LayerType.BASE,
+ 17,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy,
+ ' | Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © ',
+ _href("https://opentopomap.org/about", "OpenTopoMap"), ' (CC-BY-SA)',
+ ]),
+ ),
+ TileSource(
+ 'topplusopen',
+ 'TopPlusOpen',
+ 'https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/'
+ '1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png',
+ LayerType.BASE,
+ 18,
+ LayerAccess.PUBLIC,
+ ''.join([
+ _jb_copy, ' | Kartendaten: © ',
+ _href("https://www.bkg.bund.de/SharedDocs/Produktinformationen"
+ "/BKG/DE/P-2017/170922-TopPlus-Web-Open.html",
+ "Bundesamt für Kartographie und Geodäsie"),
+ ]),
+ ),
# Overlay layers
- 'opensea': 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
- 'hiking': 'https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png',
- 'cycling': 'https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png',
-}
+ 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',
+ ),
+ 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")})',
+ ),
+ 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")})',
+ ),
+]
TTL = datetime.timedelta(days=7)
"""Time to live of cached tiles."""
@@ -61,7 +194,8 @@ def tile_proxy(request):
:rtype: pyramid.response.Response
"""
provider = request.matchdict['provider']
- if provider not in TILE_SOURCES:
+ tile_sources = {source.key: source for source in sources_for(request)}
+ if provider not in tile_sources:
return HTTPBadRequest("Invalid provider")
x, y, z = (int(request.matchdict['x']), int(request.matchdict['y']),
@@ -81,7 +215,7 @@ def tile_proxy(request):
LOGGER.debug("Aborted attempt to contact %s due to previous timeouts", provider)
return HTTPGatewayTimeout(f"Avoiding request to {provider}")
- url = TILE_SOURCES[provider].format(x=x, y=y, z=z, s=random.choice("abc"))
+ url = tile_sources[provider].url_template.format(x=x, y=y, z=z, s=random.choice("abc"))
headers = {
"user-agent": f"Fietsboek-Tile-Proxy/{__VERSION__}",
}
@@ -99,3 +233,48 @@ def tile_proxy(request):
else:
request.redis.set(cache_key, resp.content, ex=TTL)
return Response(resp.content, content_type=resp.headers.get("Content-type", content_type))
+
+
+def sources_for(request):
+ """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]
+ """
+ settings = request.registry.settings
+ return [
+ source for source in chain(
+ (default_layer for default_layer in DEFAULT_TILE_LAYERS
+ if default_layer.key in settings["fietsboek.default_tile_layers"]),
+ settings["fietsboek.tile_layers"]
+ )
+ if source.access == LayerAccess.PUBLIC or request.identity is not None
+ ]
+
+
+def extract_tile_layers(settings):
+ """Extract all defined tile layers from the settings.
+
+ :param settings: The application settings.
+ :type settings: dict
+ :return: A list of extracted tile sources.
+ :rtype: list[TileSource]
+ """
+ layers = []
+ for key in settings.keys():
+ match = re.match("^fietsboek\\.tile_layer\\.([A-Za-z0-9_-]+)$", key)
+ if not match:
+ continue
+
+ provider_id = match.group(1)
+ name = settings[key]
+ url = settings[f"{key}.url"]
+ layer_type = LayerType(settings.get(f"{key}.type", "base"))
+ zoom = int(settings.get(f"{key}.zoom", 22))
+ attribution = settings.get(f"{key}.attribution", _jb_copy)
+ access = LayerAccess(settings.get(f"{key}.access", "public"))
+
+ layers.append(TileSource(provider_id, name, url, layer_type, zoom, access, attribution))
+ return layers