feat: publish tiles from API directly
This commit is contained in:
parent
c85f261292
commit
131afd5adc
19 changed files with 203 additions and 196 deletions
|
@ -5,7 +5,8 @@ WORKDIR /opt/obs/api
|
||||||
ADD scripts /opt/obs/scripts
|
ADD scripts /opt/obs/scripts
|
||||||
RUN pip install -e /opt/obs/scripts
|
RUN pip install -e /opt/obs/scripts
|
||||||
|
|
||||||
ADD requirements.txt setup.py obs /opt/obs/api/
|
ADD requirements.txt setup.py /opt/obs/api/
|
||||||
|
ADD obs /opt/obs/api/obs/
|
||||||
RUN pip install -e .
|
RUN pip install -e .
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
|
@ -1,44 +1,21 @@
|
||||||
# Bind address of the server
|
|
||||||
HOST = "0.0.0.0"
|
HOST = "0.0.0.0"
|
||||||
PORT = 3000
|
PORT = 3000
|
||||||
|
DEBUG = False
|
||||||
# Extended log output, but slower
|
AUTO_RESTART = True
|
||||||
DEBUG = True
|
SECRET = "!!!!!!!!!!!!CHANGE ME!!!!!!!!!!!!"
|
||||||
|
|
||||||
# Required to encrypt or sign sessions, cookies, tokens, etc.
|
|
||||||
SECRET = "CHANGEME!!!!!!!!!!@##@!!$$$$$$$$$$$$$!!"
|
|
||||||
|
|
||||||
# Connection to the database
|
|
||||||
POSTGRES_URL = "postgresql+asyncpg://obs:obs@postgres/obs"
|
POSTGRES_URL = "postgresql+asyncpg://obs:obs@postgres/obs"
|
||||||
|
|
||||||
# URL to the keycloak realm, as reachable by the API service. This is not
|
|
||||||
# necessarily its publicly reachable URL, keycloak advertises that iself.
|
|
||||||
KEYCLOAK_URL = "http://keycloak:8080/auth/realms/OBS%20Dev/"
|
KEYCLOAK_URL = "http://keycloak:8080/auth/realms/OBS%20Dev/"
|
||||||
|
|
||||||
# Auth client credentials
|
|
||||||
KEYCLOAK_CLIENT_ID = "portal"
|
KEYCLOAK_CLIENT_ID = "portal"
|
||||||
KEYCLOAK_CLIENT_SECRET = "76b84224-dc24-4824-bb98-9e1ba15bd58f"
|
KEYCLOAK_CLIENT_SECRET = "76b84224-dc24-4824-bb98-9e1ba15bd58f"
|
||||||
|
|
||||||
# Whether the API should run the worker loop, or a dedicated worker is used
|
|
||||||
DEDICATED_WORKER = True
|
DEDICATED_WORKER = True
|
||||||
|
FRONTEND_URL = "http://localhost:3001/"
|
||||||
# The root of the frontend. Needed for redirecting after login, and for CORS.
|
|
||||||
# Set to None if frontend is served by the API.
|
|
||||||
FRONTEND_URL = "http://localhost:3000/"
|
|
||||||
|
|
||||||
# Where to find the compiled frontend assets (must include index.html), or None
|
|
||||||
# to disable serving the frontend.
|
|
||||||
FRONTEND_DIR = None
|
FRONTEND_DIR = None
|
||||||
|
|
||||||
# Can be an object or a JSON string
|
|
||||||
FRONTEND_CONFIG = None
|
FRONTEND_CONFIG = None
|
||||||
|
TILES_FILE = "/tiles/tiles.mbtiles"
|
||||||
# Path overrides:
|
|
||||||
# API_ROOT_DIR = "??" # default: api/ inside repository
|
|
||||||
DATA_DIR = "/data"
|
DATA_DIR = "/data"
|
||||||
# PROCESSING_DIR = "??" # default: DATA_DIR/processing
|
ADDITIONAL_CORS_ORIGINS = [
|
||||||
# PROCESSING_OUTPUT_DIR = "??" # default: DATA_DIR/processing-output
|
"http://localhost:8880/", # for maputnik on 8880
|
||||||
# TRACKS_DIR = "??" # default: DATA_DIR/tracks
|
"http://localhost:8888/", # for maputnik on 8888
|
||||||
# OBS_FACE_CACHE_DIR = "??" # default: DATA_DIR/obs-face-cache
|
]
|
||||||
|
|
||||||
# vim: set ft=python :
|
# vim: set ft=python :
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
# Bind address of the server
|
|
||||||
HOST = "0.0.0.0"
|
|
||||||
PORT = 3000
|
|
||||||
|
|
||||||
# Extended log output, but slower
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
# Required to encrypt or sign sessions, cookies, tokens, etc.
|
|
||||||
SECRET = "CHANGEME!!!!!!!!!!@##@!!$$$$$$$$$$$$$!!"
|
|
||||||
|
|
||||||
# Connection to the database
|
|
||||||
POSTGRES_URL = "postgresql+asyncpg://obs:obs@postgres/obs"
|
|
||||||
|
|
||||||
# URL to the keycloak realm, as reachable by the API service. This is not
|
|
||||||
# necessarily its publicly reachable URL, keycloak advertises that iself.
|
|
||||||
KEYCLOAK_URL = "http://keycloak:8080/auth/realms/OBS%20Dev/"
|
|
||||||
|
|
||||||
# Auth client credentials
|
|
||||||
KEYCLOAK_CLIENT_ID = "portal"
|
|
||||||
KEYCLOAK_CLIENT_SECRET = "76b84224-dc24-4824-bb98-9e1ba15bd58f"
|
|
||||||
|
|
||||||
# Whether the API should run the worker loop, or a dedicated worker is used
|
|
||||||
DEDICATED_WORKER = False
|
|
||||||
|
|
||||||
# The root of the frontend. Needed for redirecting after login, and for CORS.
|
|
||||||
# Set to None if frontend is served by the API.
|
|
||||||
FRONTEND_URL = None
|
|
||||||
|
|
||||||
# Where to find the compiled frontend assets (must include index.html), or None
|
|
||||||
# to disable serving the frontend.
|
|
||||||
FRONTEND_DIR = "../frontend/build/"
|
|
||||||
|
|
||||||
# Can be an object or a JSON string
|
|
||||||
FRONTEND_CONFIG = {
|
|
||||||
"imprintUrl": "https://example.com/imprint",
|
|
||||||
"privacyPolicyUrl": "https://example.com/privacy",
|
|
||||||
"mapTileset": {
|
|
||||||
"url": "https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png",
|
|
||||||
"minZoom": 0,
|
|
||||||
"maxZoom": 18,
|
|
||||||
},
|
|
||||||
"mapHome": {"zoom": 15, "longitude": 7.8302, "latitude": 47.9755},
|
|
||||||
"obsMapSource": "http://localhost:3002/data/v3.json",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Path overrides:
|
|
||||||
# API_ROOT_DIR = "??" # default: api/ inside repository
|
|
||||||
DATA_DIR = "/data"
|
|
||||||
# PROCESSING_DIR = "??" # default: DATA_DIR/processing
|
|
||||||
# PROCESSING_OUTPUT_DIR = "??" # default: DATA_DIR/processing-output
|
|
||||||
# TRACKS_DIR = "??" # default: DATA_DIR/tracks
|
|
||||||
# OBS_FACE_CACHE_DIR = "??" # default: DATA_DIR/obs-face-cache
|
|
||||||
|
|
||||||
# vim: set ft=python :
|
|
|
@ -4,6 +4,7 @@ PORT = 3000
|
||||||
|
|
||||||
# Extended log output, but slower
|
# Extended log output, but slower
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
AUTO_RESTART = DEBUG
|
||||||
|
|
||||||
# Required to encrypt or sign sessions, cookies, tokens, etc.
|
# Required to encrypt or sign sessions, cookies, tokens, etc.
|
||||||
SECRET = "!!!<<<CHANGEME>>>!!!"
|
SECRET = "!!!<<<CHANGEME>>>!!!"
|
||||||
|
@ -40,9 +41,17 @@ FRONTEND_CONFIG = {
|
||||||
"maxZoom": 18,
|
"maxZoom": 18,
|
||||||
},
|
},
|
||||||
"mapHome": {"zoom": 15, "longitude": 7.8302, "latitude": 47.9755},
|
"mapHome": {"zoom": 15, "longitude": 7.8302, "latitude": 47.9755},
|
||||||
"obsMapSource": "http://localhost:3002/data/v3.json",
|
"obsMapSource": {
|
||||||
|
"type": "vector",
|
||||||
|
"url": "http://localhost:3002/data/v3.json",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# If the API should serve generated tiles, this is the path where the tiles are
|
||||||
|
# built. This is an experimental option and probably very inefficient, a proper
|
||||||
|
# tileserver should be prefered. Set to None to disable.
|
||||||
|
TILES_FILE = None
|
||||||
|
|
||||||
# Path overrides:
|
# Path overrides:
|
||||||
# API_ROOT_DIR = "??" # default: api/ inside repository
|
# API_ROOT_DIR = "??" # default: api/ inside repository
|
||||||
# DATA_DIR = "??" # default: $API_ROOT_DIR/..
|
# DATA_DIR = "??" # default: $API_ROOT_DIR/..
|
||||||
|
@ -51,4 +60,8 @@ FRONTEND_CONFIG = {
|
||||||
# TRACKS_DIR = "??" # default: DATA_DIR/tracks
|
# TRACKS_DIR = "??" # default: DATA_DIR/tracks
|
||||||
# OBS_FACE_CACHE_DIR = "??" # default: DATA_DIR/obs-face-cache
|
# OBS_FACE_CACHE_DIR = "??" # default: DATA_DIR/obs-face-cache
|
||||||
|
|
||||||
|
# Additional allowed origins for CORS headers. The FRONTEND_URL is included by
|
||||||
|
# default. Python list, or whitespace separated string.
|
||||||
|
ADDITIONAL_CORS_ORIGINS = None
|
||||||
|
|
||||||
# vim: set ft=python :
|
# vim: set ft=python :
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from json import JSONEncoder, dumps
|
from json import JSONEncoder, dumps
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
@ -17,9 +18,6 @@ from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from obs.api.db import User, make_session, connect_db
|
from obs.api.db import User, make_session, connect_db
|
||||||
|
|
||||||
from sanic_session.base import BaseSessionInterface
|
|
||||||
from sanic_session.utils import ExpiringDict
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = Sanic("OpenBikeSensor Portal API")
|
app = Sanic("OpenBikeSensor Portal API")
|
||||||
|
@ -30,26 +28,57 @@ api = Blueprint("api", url_prefix="/api")
|
||||||
auth = Blueprint("auth", url_prefix="")
|
auth = Blueprint("auth", url_prefix="")
|
||||||
|
|
||||||
# Configure paths
|
# Configure paths
|
||||||
c.API_ROOT_DIR = c.get("API_ROOT_DIR") or abspath(join(dirname(__file__), "..", ".."))
|
def configure_paths(c):
|
||||||
c.DATA_DIR = c.get("DATA_DIR") or normpath(join(c.API_ROOT_DIR, "../data"))
|
c.API_ROOT_DIR = c.get("API_ROOT_DIR") or abspath(
|
||||||
c.PROCESSING_DIR = c.get("PROCESSING_DIR") or join(c.DATA_DIR, "processing")
|
join(dirname(__file__), "..", "..")
|
||||||
c.PROCESSING_OUTPUT_DIR = c.get("PROCESSING_OUTPUT_DIR") or join(
|
)
|
||||||
c.DATA_DIR, "processing-output"
|
c.DATA_DIR = c.get("DATA_DIR") or normpath(join(c.API_ROOT_DIR, "../data"))
|
||||||
)
|
c.PROCESSING_DIR = c.get("PROCESSING_DIR") or join(c.DATA_DIR, "processing")
|
||||||
c.TRACKS_DIR = c.get("TRACKS_DIR") or join(c.DATA_DIR, "tracks")
|
c.PROCESSING_OUTPUT_DIR = c.get("PROCESSING_OUTPUT_DIR") or join(
|
||||||
c.OBS_FACE_CACHE_DIR = c.get("OBS_FACE_CACHE_DIR") or join(c.DATA_DIR, "obs-face-cache")
|
c.DATA_DIR, "processing-output"
|
||||||
c.FRONTEND_DIR = c.get("FRONTEND_DIR")
|
)
|
||||||
|
c.TRACKS_DIR = c.get("TRACKS_DIR") or join(c.DATA_DIR, "tracks")
|
||||||
|
c.OBS_FACE_CACHE_DIR = c.get("OBS_FACE_CACHE_DIR") or join(
|
||||||
|
c.DATA_DIR, "obs-face-cache"
|
||||||
|
)
|
||||||
|
c.FRONTEND_DIR = c.get("FRONTEND_DIR")
|
||||||
|
|
||||||
|
|
||||||
|
configure_paths(app.config)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cors(app):
|
||||||
|
frontend_url = app.config.get("FRONTEND_URL")
|
||||||
|
additional_origins = app.config.get("ADDITIONAL_CORS_ORIGINS")
|
||||||
|
if not frontend_url and not additional_origins:
|
||||||
|
# No CORS configured
|
||||||
|
return
|
||||||
|
|
||||||
|
origins = []
|
||||||
|
if frontend_url:
|
||||||
|
u = urlparse(frontend_url)
|
||||||
|
origins.append(f"{u.scheme}://{u.netloc}")
|
||||||
|
|
||||||
|
if isinstance(additional_origins, str):
|
||||||
|
origins += re.split(r"\s+", additional_origins)
|
||||||
|
elif isinstance(additional_origins, list):
|
||||||
|
origins += additional_origins
|
||||||
|
elif additional_origins is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"invalid option type for ADDITIONAL_CORS_ORIGINS, must be list or space separated str"
|
||||||
|
)
|
||||||
|
|
||||||
if c.FRONTEND_URL:
|
|
||||||
from sanic_cors import CORS
|
from sanic_cors import CORS
|
||||||
|
|
||||||
frontend_url = urlparse(c.FRONTEND_URL)
|
|
||||||
CORS(
|
CORS(
|
||||||
app,
|
app,
|
||||||
origins=[f"{frontend_url.scheme}://{frontend_url.netloc}"],
|
origins=origins,
|
||||||
supports_credentials=True,
|
supports_credentials=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
setup_cors(app)
|
||||||
|
|
||||||
# TODO: use a different interface, maybe backed by the PostgreSQL, to allow
|
# TODO: use a different interface, maybe backed by the PostgreSQL, to allow
|
||||||
# scaling the API
|
# scaling the API
|
||||||
Session(app, interface=InMemorySessionInterface())
|
Session(app, interface=InMemorySessionInterface())
|
||||||
|
@ -57,7 +86,7 @@ Session(app, interface=InMemorySessionInterface())
|
||||||
|
|
||||||
@app.before_server_start
|
@app.before_server_start
|
||||||
async def app_connect_db(app, loop):
|
async def app_connect_db(app, loop):
|
||||||
app.ctx._db_engine_ctx = connect_db(c.POSTGRES_URL)
|
app.ctx._db_engine_ctx = connect_db(app.config.POSTGRES_URL)
|
||||||
app.ctx._db_engine = await app.ctx._db_engine_ctx.__aenter__()
|
app.ctx._db_engine = await app.ctx._db_engine_ctx.__aenter__()
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,26 +147,37 @@ def json(*args, **kwargs):
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
|
||||||
INDEX_HTML = join(c.FRONTEND_DIR, "index.html")
|
INDEX_HTML = (
|
||||||
if exists(INDEX_HTML):
|
join(app.config.FRONTEND_DIR, "index.html")
|
||||||
|
if app.config.get("FRONTEND_DIR")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if INDEX_HTML and exists(INDEX_HTML):
|
||||||
|
|
||||||
@app.get("/config.json")
|
@app.get("/config.json")
|
||||||
def get_frontend_config(req):
|
def get_frontend_config(req):
|
||||||
base_path = req.server_path.replace("config.json", "")
|
base_path = req.server_path.replace("config.json", "")
|
||||||
return json_response(
|
result = {
|
||||||
{
|
**req.app.config.FRONTEND_CONFIG,
|
||||||
**req.app.config.FRONTEND_CONFIG,
|
"apiUrl": f"{req.scheme}://{req.host}{base_path}api",
|
||||||
"apiUrl": f"{req.scheme}://{req.host}{base_path}api",
|
"loginUrl": f"{req.scheme}://{req.host}{base_path}login",
|
||||||
"loginUrl": f"{req.scheme}://{req.host}{base_path}login",
|
}
|
||||||
|
print(req.app.config)
|
||||||
|
if req.app.config.get("TILES_FILE"):
|
||||||
|
result["obsMapSource"] = {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": [req.app.url_for("tiles", zoom="{zoom}", x="{x}", y="{y}")],
|
||||||
|
"minzoom": 12,
|
||||||
|
"maxzoom": 14,
|
||||||
}
|
}
|
||||||
)
|
return json_response()
|
||||||
|
|
||||||
@app.get("/<path:path>")
|
@app.get("/<path:path>")
|
||||||
def get_frontend_static(req, path):
|
def get_frontend_static(req, path):
|
||||||
if path.startswith("api/"):
|
if path.startswith("api/"):
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
file = join(c.FRONTEND_DIR, path)
|
file = join(app.config.FRONTEND_DIR, path)
|
||||||
if not exists(file) or not path or not isfile(file):
|
if not exists(file) or not path or not isfile(file):
|
||||||
file = INDEX_HTML
|
file = INDEX_HTML
|
||||||
return file_response(file)
|
return file_response(file)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from . import (
|
from . import (
|
||||||
|
info,
|
||||||
login,
|
login,
|
||||||
stats,
|
stats,
|
||||||
|
tiles,
|
||||||
tracks,
|
tracks,
|
||||||
info,
|
|
||||||
users,
|
users,
|
||||||
)
|
)
|
||||||
|
|
57
api/obs/api/routes/tiles.py
Normal file
57
api/obs/api/routes/tiles.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import gzip
|
||||||
|
from sqlite3 import connect
|
||||||
|
from sanic.response import raw
|
||||||
|
|
||||||
|
from obs.api.app import app
|
||||||
|
|
||||||
|
|
||||||
|
def get_tile(filename, zoom, x, y):
|
||||||
|
"""
|
||||||
|
Inspired by:
|
||||||
|
https://github.com/TileStache/TileStache/blob/master/TileStache/MBTiles.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(filename)
|
||||||
|
db = connect(filename)
|
||||||
|
db.text_factory = bytes
|
||||||
|
|
||||||
|
fmt = db.execute("SELECT value FROM metadata WHERE name='format'").fetchone()[0]
|
||||||
|
if fmt != b"pbf":
|
||||||
|
print(repr(b"pbf"), " versus ", repr(fmt))
|
||||||
|
raise ValueError("mbtiles file is in wrong format: %s" % fmt)
|
||||||
|
|
||||||
|
content = db.execute(
|
||||||
|
"SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?",
|
||||||
|
(zoom, x, (2 ** zoom - 1) - y),
|
||||||
|
).fetchone()
|
||||||
|
return content and content[0] or None
|
||||||
|
|
||||||
|
|
||||||
|
# regenerate approx. once each day
|
||||||
|
TILE_CACHE_MAX_AGE = 3600 * 24
|
||||||
|
|
||||||
|
if app.config.get("TILES_FILE"):
|
||||||
|
|
||||||
|
@app.route(r"/tiles/<zoom:int>/<x:int>/<y:(\d+)\.pbf>")
|
||||||
|
async def tiles(req, zoom: int, x: int, y: str):
|
||||||
|
tile = get_tile(req.app.config.TILES_FILE, int(zoom), int(x), int(y))
|
||||||
|
|
||||||
|
gzip = "gzip" in req.headers["accept-encoding"]
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
headers["Vary"] = "Accept-Encoding"
|
||||||
|
|
||||||
|
if req.app.config.DEBUG:
|
||||||
|
headers["Cache-Control"] = "no-cache"
|
||||||
|
else:
|
||||||
|
headers["Cache-Control"] = f"public, max-age={TILE_CACHE_MAX_AGE}"
|
||||||
|
|
||||||
|
# The tiles in the mbtiles file are gzip-compressed already, so we
|
||||||
|
# serve them actually as-is, and only decompress them if the browser
|
||||||
|
# doesn't accept gzip
|
||||||
|
if gzip:
|
||||||
|
headers["Content-Encoding"] = "gzip"
|
||||||
|
else:
|
||||||
|
tile = gzip.decompress(tile)
|
||||||
|
|
||||||
|
return raw(tile, content_type="application/x-protobuf", headers=headers)
|
|
@ -21,9 +21,8 @@ def user_to_json(user):
|
||||||
|
|
||||||
|
|
||||||
@api.get("/user")
|
@api.get("/user")
|
||||||
@require_auth
|
|
||||||
async def get_user(req):
|
async def get_user(req):
|
||||||
return json(user_to_json(req.ctx.user))
|
return json(user_to_json(req.ctx.user) if req.ctx.user else None)
|
||||||
|
|
||||||
|
|
||||||
@api.put("/user")
|
@api.put("/user")
|
||||||
|
|
|
@ -10,7 +10,13 @@ from obs.api.db import connect_db
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app.run(host=app.config.HOST, port=app.config.PORT, debug=app.config.DEBUG)
|
debug = app.config.DEBUG
|
||||||
|
app.run(
|
||||||
|
host=app.config.HOST,
|
||||||
|
port=app.config.PORT,
|
||||||
|
debug=debug,
|
||||||
|
auto_reload=app.config.get("AUTO_RELOAD", debug),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -39,6 +39,7 @@ services:
|
||||||
- ./api/config.dev.py:/opt/obs/api/config.py
|
- ./api/config.dev.py:/opt/obs/api/config.py
|
||||||
- ./frontend/build:/opt/obs/frontend/build
|
- ./frontend/build:/opt/obs/frontend/build
|
||||||
- ./local/api-data:/data
|
- ./local/api-data:/data
|
||||||
|
- ./tile-generator/data/:/tiles
|
||||||
links:
|
links:
|
||||||
- postgres
|
- postgres
|
||||||
- keycloak
|
- keycloak
|
||||||
|
@ -140,17 +141,6 @@ services:
|
||||||
PGHOST: postgres
|
PGHOST: postgres
|
||||||
PGPORT: 5432
|
PGPORT: 5432
|
||||||
|
|
||||||
tileserver:
|
|
||||||
image: klokantech/tileserver-gl
|
|
||||||
ports:
|
|
||||||
- 3002:80
|
|
||||||
volumes:
|
|
||||||
- ./tile-generator/tileserver-gl-config.json:/config/tileserver.json
|
|
||||||
- ./tile-generator/data/:/data
|
|
||||||
command:
|
|
||||||
- --config
|
|
||||||
- /config/tileserver.json
|
|
||||||
|
|
||||||
keycloak:
|
keycloak:
|
||||||
image: jboss/keycloak
|
image: jboss/keycloak
|
||||||
ports:
|
ports:
|
||||||
|
@ -166,19 +156,3 @@ services:
|
||||||
DB_DATABASE: obs
|
DB_DATABASE: obs
|
||||||
DB_USER: obs
|
DB_USER: obs
|
||||||
DB_PASSWORD: obs
|
DB_PASSWORD: obs
|
||||||
# DB_SCHEMA: keycloak
|
|
||||||
|
|
||||||
prod-test:
|
|
||||||
image: openbikesensor-portal
|
|
||||||
build:
|
|
||||||
context: ./
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
volumes:
|
|
||||||
- ./api/config.prod-test.py:/opt/obs/api/config.py
|
|
||||||
- ./local/api-data:/data
|
|
||||||
links:
|
|
||||||
- postgres
|
|
||||||
- keycloak
|
|
||||||
ports:
|
|
||||||
- '3000:3000'
|
|
||||||
restart: on-failure
|
|
||||||
|
|
|
@ -13,5 +13,10 @@
|
||||||
"longitude": 7.8302,
|
"longitude": 7.8302,
|
||||||
"latitude": 47.9755
|
"latitude": 47.9755
|
||||||
},
|
},
|
||||||
"obsMapSource": "http://localhost:3002/data/v3.json"
|
"obsMapSource": {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": ["http://localhost:3000/tiles/{z}/{x}/{y}.pbf"],
|
||||||
|
"minzoom": 12,
|
||||||
|
"maxzoom": 14
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,5 +13,8 @@
|
||||||
"longitude": 9.1797,
|
"longitude": 9.1797,
|
||||||
"latitude": 48.7784
|
"latitude": 48.7784
|
||||||
},
|
},
|
||||||
"obsMapSource": "http://portal.example.com/tileserver/data/v3.json"
|
"obsMapSource": {
|
||||||
|
"type": "vector",
|
||||||
|
"url": "http://portal.example.com/tileserver/data/v3.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,8 @@
|
||||||
"longitude": 7.8302,
|
"longitude": 7.8302,
|
||||||
"latitude": 47.9755
|
"latitude": 47.9755
|
||||||
},
|
},
|
||||||
"obsMapSource": "https://portal.example.com/tileset/data/v3.json"
|
"obsMapSource": {
|
||||||
|
"type": "vector",
|
||||||
|
"url": "https://portal.example.com/tileset/data/v3.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
export type MapSoure = {
|
||||||
|
type: 'vector'
|
||||||
|
url: string,
|
||||||
|
} | {
|
||||||
|
type: 'vector',
|
||||||
|
tiles: string[],
|
||||||
|
minzoom: number,
|
||||||
|
maxzoom: number,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
apiUrl: string
|
apiUrl: string
|
||||||
mapHome: {
|
mapHome: {
|
||||||
|
@ -7,7 +17,7 @@ export interface Config {
|
||||||
longitude: number
|
longitude: number
|
||||||
zoom: number
|
zoom: number
|
||||||
}
|
}
|
||||||
obsMapSource?: string
|
obsMapSource?: MapSoure
|
||||||
imprintUrl?: string
|
imprintUrl?: string
|
||||||
privacyPolicyUrl?: string
|
privacyPolicyUrl?: string
|
||||||
mapTileset?: {
|
mapTileset?: {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import _ from 'lodash'
|
||||||
import bright from './bright.json'
|
import bright from './bright.json'
|
||||||
import positron from './positron.json'
|
import positron from './positron.json'
|
||||||
|
|
||||||
function addRoadsStyle(style, sourceUrl = "http://localhost:3002/data/v3.json") {
|
function addRoadsStyle(style, mapSource) {
|
||||||
style.sources.obs = {"type": "vector", "url": sourceUrl}
|
style.sources.obs = mapSource
|
||||||
|
|
||||||
// insert before "road_oneway" layer
|
// insert before "road_oneway" layer
|
||||||
let idx = style.layers.findIndex(l => l.id === 'road_oneway')
|
let idx = style.layers.findIndex(l => l.id === 'road_oneway')
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {map, switchMap} from 'rxjs/operators'
|
||||||
|
|
||||||
import api from 'api'
|
import api from 'api'
|
||||||
import {Stats, Page} from 'components'
|
import {Stats, Page} from 'components'
|
||||||
import {useConfig} from 'config'
|
import {useConfig, MapSource} from 'config'
|
||||||
|
|
||||||
import {TrackListItem} from './TracksPage'
|
import {TrackListItem} from './TracksPage'
|
||||||
import styles from './HomePage.module.scss'
|
import styles from './HomePage.module.scss'
|
||||||
|
@ -16,7 +16,7 @@ import 'ol/ol.css'
|
||||||
import {obsRoads} from '../mapstyles'
|
import {obsRoads} from '../mapstyles'
|
||||||
import ReactMapGl from 'react-map-gl'
|
import ReactMapGl from 'react-map-gl'
|
||||||
|
|
||||||
function WelcomeMap({mapSource}: {mapSource: string}) {
|
function WelcomeMap({mapSource}: {mapSource: MapSource}) {
|
||||||
const mapStyle = React.useMemo(() => obsRoads(mapSource), [mapSource])
|
const mapStyle = React.useMemo(() => obsRoads(mapSource), [mapSource])
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const [viewport, setViewport] = React.useState({
|
const [viewport, setViewport] = React.useState({
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {obsRoads} from '../mapstyles'
|
||||||
import ReactMapGl from 'react-map-gl'
|
import ReactMapGl from 'react-map-gl'
|
||||||
|
|
||||||
function BigMap({mapSource, config}: {mapSource: string ,config: Config}) {
|
function BigMap({mapSource, config}: {mapSource: string ,config: Config}) {
|
||||||
const mapStyle = React.useMemo(() => obsRoads(mapSource), [mapSource])
|
const mapStyle = React.useMemo(() => mapSource && obsRoads(mapSource), [mapSource])
|
||||||
const [viewport, setViewport] = React.useState({
|
const [viewport, setViewport] = React.useState({
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
latitude: 0,
|
latitude: 0,
|
||||||
|
@ -25,6 +25,10 @@ function BigMap({mapSource, config}: {mapSource: string ,config: Config}) {
|
||||||
}
|
}
|
||||||
}, [config])
|
}, [config])
|
||||||
|
|
||||||
|
if (!mapStyle) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
<ReactMapGl mapStyle={mapStyle} width="100%" height="100%" onViewportChange={setViewport} {...viewport} />
|
<ReactMapGl mapStyle={mapStyle} width="100%" height="100%" onViewportChange={setViewport} {...viewport} />
|
||||||
|
|
|
@ -74,11 +74,18 @@ make generate-tiles-pg
|
||||||
|
|
||||||
## Publish vector tiles
|
## Publish vector tiles
|
||||||
|
|
||||||
The tool [tileserver-gl](http://tileserver.org/) is used to publish the vector
|
The API is capable of serving the generated mbtiles file in XYZ scheme with PBF
|
||||||
tiles separately through HTTP. The tileserver runs inside docker, so all you need to do for a development setup is start it:
|
format. Set the config variable `TILES_FILE` to point to your generated file.
|
||||||
|
|
||||||
|
The API might be inefficient at publishing the tiles. You might want to try a
|
||||||
|
proper tileserver with caching and all if you run into trouble with its
|
||||||
|
capabilities.
|
||||||
|
|
||||||
|
The URL for the tiles is:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker compose up -d tileserver
|
http://api.example.com/tiles/{z}/{x}/{y}.pbf
|
||||||
```
|
```
|
||||||
|
|
||||||
It is now available at [http://localhost:3002/](http://localhost:3002/).
|
The API generates this URL into the `/config.json` as `obsMapSource` if it is
|
||||||
|
configured to serve tiles *and* the frontend.
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"options": {
|
|
||||||
"paths": {
|
|
||||||
"root": "/usr/src/app/node_modules/tileserver-gl-styles",
|
|
||||||
"fonts": "fonts",
|
|
||||||
"styles": "styles",
|
|
||||||
"mbtiles": "/data"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styles": {
|
|
||||||
"klokantech-basic": {
|
|
||||||
"style": "klokantech-basic/style.json",
|
|
||||||
"tilejson": {
|
|
||||||
"bounds": [
|
|
||||||
7.487897,
|
|
||||||
47.80556,
|
|
||||||
8.212994,
|
|
||||||
48.224458
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"osm-bright": {
|
|
||||||
"style": "osm-bright/style.json",
|
|
||||||
"tilejson": {
|
|
||||||
"bounds": [
|
|
||||||
7.487897,
|
|
||||||
47.80556,
|
|
||||||
8.212994,
|
|
||||||
48.224458
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"v3": {
|
|
||||||
"mbtiles": "tiles.mbtiles"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue