Add missing dependencies
Add tile server Config: add defults Cosmetic refactorings
This commit is contained in:
parent
7e9e266157
commit
5dacc908f2
12 changed files with 434 additions and 187 deletions
35
pdm.lock
generated
35
pdm.lock
generated
|
@ -5,7 +5,7 @@
|
||||||
groups = ["default", "dev", "mqtt"]
|
groups = ["default", "dev", "mqtt"]
|
||||||
strategy = ["cross_platform"]
|
strategy = ["cross_platform"]
|
||||||
lock_version = "4.4.1"
|
lock_version = "4.4.1"
|
||||||
content_hash = "sha256:d6bc84b5bf12fda8fd24858515794677046aca3dea340a40679d1276ae7a6ea9"
|
content_hash = "sha256:75aa4cd0effa4fc41f312763423aa949a48e14e235b75bbe0a67fa762bc9660c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiomqtt"
|
name = "aiomqtt"
|
||||||
|
@ -20,6 +20,16 @@ files = [
|
||||||
{file = "aiomqtt-1.2.1.tar.gz", hash = "sha256:7582f4341f08ef7110dd9ab3a559454dc28ccda1eac502ff8f08a73b238ecede"},
|
{file = "aiomqtt-1.2.1.tar.gz", hash = "sha256:7582f4341f08ef7110dd9ab3a559454dc28ccda1eac502ff8f08a73b238ecede"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosqlite"
|
||||||
|
version = "0.19.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "asyncio bridge to the standard sqlite3 module"
|
||||||
|
files = [
|
||||||
|
{file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"},
|
||||||
|
{file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -308,17 +318,17 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.108.0"
|
version = "0.109.2"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4",
|
"pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4",
|
||||||
"starlette<0.33.0,>=0.29.0",
|
"starlette<0.37.0,>=0.36.3",
|
||||||
"typing-extensions>=4.8.0",
|
"typing-extensions>=4.8.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.108.0-py3-none-any.whl", hash = "sha256:8c7bc6d315da963ee4cdb605557827071a9a7f95aeb8fcdd3bde48cdc8764dd7"},
|
{file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"},
|
||||||
{file = "fastapi-0.108.0.tar.gz", hash = "sha256:5056e504ac6395bf68493d71fcfc5352fdbd5fda6f88c21f6420d80d81163296"},
|
{file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1162,15 +1172,15 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.32.0.post1"
|
version = "0.36.3"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "The little ASGI library that shines."
|
summary = "The little ASGI library that shines."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyio<5,>=3.4.0",
|
"anyio<5,>=3.4.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "starlette-0.32.0.post1-py3-none-any.whl", hash = "sha256:cd0cb10ddb49313f609cedfac62c8c12e56c7314b66d89bb077ba228bada1b09"},
|
{file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"},
|
||||||
{file = "starlette-0.32.0.post1.tar.gz", hash = "sha256:e54e2b7e2fb06dff9eac40133583f10dfa05913f5a85bf26f427c7a40a9a3d02"},
|
{file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1202,6 +1212,15 @@ files = [
|
||||||
{file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"},
|
{file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pyyaml"
|
||||||
|
version = "6.0.12.12"
|
||||||
|
summary = "Typing stubs for PyYAML"
|
||||||
|
files = [
|
||||||
|
{file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"},
|
||||||
|
{file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.8.0"
|
version = "4.8.0"
|
||||||
|
|
|
@ -8,7 +8,7 @@ authors = [
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apscheduler>=3.10.4",
|
"apscheduler>=3.10.4",
|
||||||
"asyncpg>=0.28.0",
|
"asyncpg>=0.28.0",
|
||||||
"fastapi>=0.104.1",
|
"fastapi>=0.104.2",
|
||||||
"geoalchemy2>=0.14.2",
|
"geoalchemy2>=0.14.2",
|
||||||
"geopandas>=0.14.0",
|
"geopandas>=0.14.0",
|
||||||
"itsdangerous>=2.1.2",
|
"itsdangerous>=2.1.2",
|
||||||
|
@ -26,6 +26,7 @@ dependencies = [
|
||||||
"sqlmodel>=0.0.14",
|
"sqlmodel>=0.0.14",
|
||||||
"uvicorn>=0.23.2",
|
"uvicorn>=0.23.2",
|
||||||
"websockets>=12.0",
|
"websockets>=12.0",
|
||||||
|
"aiosqlite>=0.19.0",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11,<4"
|
requires-python = ">=3.11,<4"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -51,4 +52,5 @@ dev = [
|
||||||
"pandas-stubs>=2.1.4.231218",
|
"pandas-stubs>=2.1.4.231218",
|
||||||
"pretty-errors>=1.2.25",
|
"pretty-errors>=1.2.25",
|
||||||
"types-psycopg2>=2.9.21.20",
|
"types-psycopg2>=2.9.21.20",
|
||||||
|
"types-PyYAML>=6.0.12.12",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '2023.4.dev28+ge3ed311.d20240107'
|
__version__ = '2023.4.dev33+g7e9e266.d20240210'
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
|
|
||||||
from gisaf.live import live_server
|
from gisaf.live import live_server
|
||||||
from gisaf.redis_tools import Store
|
from gisaf.redis_tools import Store
|
||||||
|
from gisaf.baskets import Basket
|
||||||
|
|
||||||
logger = logging.getLogger('Gisaf admin manager')
|
logger = logging.getLogger('Gisaf admin manager')
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ class AdminManager:
|
||||||
One instance only, handled by Gisaf's process.
|
One instance only, handled by Gisaf's process.
|
||||||
"""
|
"""
|
||||||
store: Store
|
store: Store
|
||||||
|
baskets: dict[str, Basket]
|
||||||
async def setup_admin(self, app):
|
async def setup_admin(self, app):
|
||||||
"""
|
"""
|
||||||
Create the default baskets, scan and create baskets
|
Create the default baskets, scan and create baskets
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
from collections import OrderedDict, defaultdict
|
from collections import OrderedDict, defaultdict
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from json import dumps
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status, responses
|
|
||||||
|
from fastapi import FastAPI, Request, HTTPException, status, responses
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
from gisaf.config import conf
|
|
||||||
from gisaf.models.map_bases import BaseMap, BaseMapLayer, BaseStyle, MapInitData
|
from gisaf.models.map_bases import BaseMap, BaseMapLayer, BaseStyle, MapInitData
|
||||||
from gisaf.registry import registry
|
from gisaf.registry import registry
|
||||||
from gisaf.database import db_session
|
from gisaf.database import db_session
|
||||||
from gisaf.models.authentication import User
|
from gisaf.database import fastapi_db_session
|
||||||
|
from gisaf.tiles import registry as tiles_registry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -21,20 +23,20 @@ async def get_base_styles():
|
||||||
async with db_session() as session:
|
async with db_session() as session:
|
||||||
query = select(BaseStyle.name)\
|
query = select(BaseStyle.name)\
|
||||||
.where(BaseStyle.enabled==True)\
|
.where(BaseStyle.enabled==True)\
|
||||||
.order_by(BaseStyle.id)
|
.order_by(BaseStyle.id) # type: ignore # noqa: E712
|
||||||
data = await session.exec(query)
|
data = await session.exec(query)
|
||||||
base_styles = data.all()
|
base_styles = data.all()
|
||||||
## TODO: tiles_registry
|
## TODO: tiles_registry
|
||||||
logger.warning('TODO: tiles_registry')
|
logger.warning('TODO: tiles_registry')
|
||||||
# base_styles.extend(tiles_registry.mbtiles.values())
|
# base_styles.extend(tiles_registry.mbtiles.values())
|
||||||
return [BaseStyle(name=bs) for bs in base_styles]
|
return [BaseStyle(name=bs) for bs in base_styles] # type: ignore
|
||||||
|
|
||||||
async def get_base_maps() -> list[BaseMap]:
|
async def get_base_maps() -> list[BaseMap]:
|
||||||
async with db_session() as session:
|
async with db_session() as session:
|
||||||
query1 = select(BaseMap).options(selectinload(BaseMap.layers))
|
query1 = select(BaseMap).options(selectinload(BaseMap.layers)) # type: ignore
|
||||||
data1 = await session.exec(query1)
|
data1 = await session.exec(query1)
|
||||||
base_maps = data1.all()
|
base_maps = data1.all()
|
||||||
return base_maps
|
return base_maps # type: ignore
|
||||||
base_map_dict = {bm.id: bm.name for bm in base_maps}
|
base_map_dict = {bm.id: bm.name for bm in base_maps}
|
||||||
query2 = select(BaseMapLayer).options(selectinload(BaseMapLayer.base_map))
|
query2 = select(BaseMapLayer).options(selectinload(BaseMapLayer.base_map))
|
||||||
data2 = await session.exec(query2)
|
data2 = await session.exec(query2)
|
||||||
|
@ -58,5 +60,27 @@ async def get_init_data() -> MapInitData:
|
||||||
baseStyles=await get_base_styles(),
|
baseStyles=await get_base_styles(),
|
||||||
baseMaps=await get_base_maps(),
|
baseMaps=await get_base_maps(),
|
||||||
groups=registry.primary_groups,
|
groups=registry.primary_groups,
|
||||||
stores=df.to_dict(orient='records')
|
stores=df.to_dict(orient='records') # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api.get('/base_style/{name}')
|
||||||
|
async def get_base_style(request: Request, name: str,
|
||||||
|
db_session: fastapi_db_session,
|
||||||
|
) -> BaseStyle:
|
||||||
|
data = await db_session.exec(select(BaseStyle).where(BaseStyle.name==name))
|
||||||
|
try:
|
||||||
|
base_style = data.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
# return BaseStyle(
|
||||||
|
# name=name,
|
||||||
|
# style=dumps({})
|
||||||
|
# )
|
||||||
|
if name in tiles_registry.mbtiles:
|
||||||
|
## Try to get base_style from tiles_registry
|
||||||
|
tiles = tiles_registry.mbtiles['name']
|
||||||
|
style = dumps(await tiles.get_style(style_record=base_style,
|
||||||
|
request=request))
|
||||||
|
else:
|
||||||
|
style = base_style.style # type: ignore
|
||||||
|
return BaseStyle(name=name, style=style) # type: ignore
|
||||||
|
|
|
@ -8,6 +8,7 @@ from gisaf.api.geoapi import api as geoapi
|
||||||
from gisaf.config import conf
|
from gisaf.config import conf
|
||||||
from gisaf.registry import registry
|
from gisaf.registry import registry
|
||||||
from gisaf.redis_tools import setup_redis, setup_redis_cache, shutdown_redis
|
from gisaf.redis_tools import setup_redis, setup_redis_cache, shutdown_redis
|
||||||
|
from gisaf.tiles import registry as map_tile_registry
|
||||||
from gisaf.live import setup_live
|
from gisaf.live import setup_live
|
||||||
|
|
||||||
logging.basicConfig(level=conf.gisaf.debugLevel)
|
logging.basicConfig(level=conf.gisaf.debugLevel)
|
||||||
|
@ -20,8 +21,10 @@ async def lifespan(app: FastAPI):
|
||||||
await setup_redis()
|
await setup_redis()
|
||||||
await setup_redis_cache()
|
await setup_redis_cache()
|
||||||
await setup_live()
|
await setup_live()
|
||||||
|
await map_tile_registry.setup()
|
||||||
yield
|
yield
|
||||||
await shutdown_redis()
|
await shutdown_redis()
|
||||||
|
await map_tile_registry.shutdown()
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
debug=False,
|
debug=False,
|
||||||
|
|
|
@ -23,81 +23,81 @@ config_files = [
|
||||||
]
|
]
|
||||||
|
|
||||||
class DashboardHome(BaseSettings):
|
class DashboardHome(BaseSettings):
|
||||||
title: str
|
title: str = 'Gisaf - home/dashboards'
|
||||||
content_file: str
|
content_file: str = '/etc/gisaf/dashboard_home_content.html'
|
||||||
footer_file: str
|
footer_file: str = '/etc/gisaf/dashboard_home_footer.html'
|
||||||
|
|
||||||
class GisafConfig(BaseSettings):
|
class GisafConfig(BaseSettings):
|
||||||
title: str
|
title: str = 'Gisaf'
|
||||||
windowTitle: str
|
windowTitle: str = 'Gisaf'
|
||||||
debugLevel: str
|
debugLevel: str = 'INFO'
|
||||||
dashboard_home: DashboardHome
|
dashboard_home: DashboardHome = DashboardHome()
|
||||||
redirect: str = ''
|
redirect: str = ''
|
||||||
use_pretty_errors: bool = False
|
use_pretty_errors: bool = False
|
||||||
|
|
||||||
class SpatialSysRef(BaseSettings):
|
class SpatialSysRef(BaseSettings):
|
||||||
author: str
|
author: str = 'AVSM'
|
||||||
ellps: str
|
ellps: str = 'WGS84'
|
||||||
k: int
|
k: int = 1
|
||||||
lat_0: float
|
lat_0: float = 12.01605433
|
||||||
lon_0: float
|
lon_0: float = 79.80998934
|
||||||
no_defs: bool
|
no_defs: bool = True
|
||||||
proj: str
|
proj: str = 'tmerc'
|
||||||
towgs84: str
|
towgs84: str = '0,0,0,0,0,0,0'
|
||||||
units: str
|
units: str = 'm'
|
||||||
x_0: float
|
x_0: float = 370455.630
|
||||||
y_0: float
|
y_0: float = 1328608.994
|
||||||
|
|
||||||
class RawSurvey(BaseSettings):
|
class RawSurvey(BaseSettings):
|
||||||
spatial_sys_ref: SpatialSysRef
|
spatial_sys_ref: SpatialSysRef = SpatialSysRef()
|
||||||
srid: int
|
srid: int = 910001
|
||||||
|
|
||||||
class Geo(BaseSettings):
|
class Geo(BaseSettings):
|
||||||
raw_survey: RawSurvey
|
raw_survey: RawSurvey = RawSurvey()
|
||||||
simplify_geom_factor: int
|
simplify_geom_factor: int = 10000000
|
||||||
simplify_preserve_topology: bool = False
|
simplify_preserve_topology: bool = False
|
||||||
srid: int
|
srid: int = 4326
|
||||||
srid_for_proj: int
|
srid_for_proj: int = 32644
|
||||||
|
|
||||||
class Flask(BaseSettings):
|
# class Flask(BaseSettings):
|
||||||
secret_key: str
|
# secret_key: str
|
||||||
debug: int
|
# debug: int
|
||||||
|
|
||||||
class MQTT(BaseSettings):
|
class MQTT(BaseSettings):
|
||||||
broker: str = 'localhost'
|
broker: str = 'localhost'
|
||||||
port: int = 1883
|
port: int = 1883
|
||||||
|
|
||||||
class GisafLive(BaseSettings):
|
class GisafLive(BaseSettings):
|
||||||
hostname: str
|
hostname: str = 'localhost'
|
||||||
port: int
|
port: int = 80
|
||||||
scheme: str
|
scheme: str = 'http'
|
||||||
redis: str
|
redis: str = 'redis://localhost'
|
||||||
mqtt: MQTT
|
mqtt: MQTT = MQTT()
|
||||||
|
|
||||||
class DefaultSurvey(BaseSettings):
|
class DefaultSurvey(BaseSettings):
|
||||||
surveyor_id: int
|
surveyor_id: int = 1
|
||||||
equipment_id: int
|
equipment_id: int = 1
|
||||||
|
|
||||||
class Survey(BaseSettings):
|
class Survey(BaseSettings):
|
||||||
model_config = ConfigDict(extra='ignore')
|
# model_config = ConfigDict(extra='ignore')
|
||||||
db_schema_raw: str
|
db_schema_raw: str = 'raw_survey'
|
||||||
db_schema: str
|
db_schema: str = 'survey'
|
||||||
default: DefaultSurvey
|
default: DefaultSurvey = DefaultSurvey()
|
||||||
|
|
||||||
class Crypto(BaseSettings):
|
class Crypto(BaseSettings):
|
||||||
secret: str
|
secret: str = 'Gisaf big secret'
|
||||||
algorithm: str
|
algorithm: str = 'HS256'
|
||||||
expire: float
|
expire: float = 21600
|
||||||
|
|
||||||
class DB(BaseSettings):
|
class DB(BaseSettings):
|
||||||
uri: str
|
# uri: str
|
||||||
host: str
|
host: str = 'localhost'
|
||||||
port: int = 5432
|
port: int = 5432
|
||||||
user: str
|
user: str = 'gisaf'
|
||||||
db: str
|
db: str = 'gisaf'
|
||||||
password: str
|
password: str = 'secret'
|
||||||
debug: bool
|
debug: bool = False
|
||||||
info: bool
|
info: bool = True
|
||||||
pool_size: int = 10
|
pool_size: int = 10
|
||||||
max_overflow: int = 10
|
max_overflow: int = 10
|
||||||
echo: bool = False
|
echo: bool = False
|
||||||
|
@ -110,119 +110,121 @@ class DB(BaseSettings):
|
||||||
|
|
||||||
|
|
||||||
class Log(BaseSettings):
|
class Log(BaseSettings):
|
||||||
level: str
|
level: str = 'WARNING'
|
||||||
|
|
||||||
class OGCAPILicense(BaseSettings):
|
class OGCAPILicense(BaseSettings):
|
||||||
name: str
|
name: str = 'CC-BY 4.0 license'
|
||||||
url: str
|
url: str = 'https://creativecommons.org/licenses/by/4.0/'
|
||||||
|
|
||||||
class OGCAPIProvider(BaseSettings):
|
class OGCAPIProvider(BaseSettings):
|
||||||
name: str
|
name: str = 'Organization Name'
|
||||||
url: str
|
url: str = 'https://pygeoapi.io'
|
||||||
|
|
||||||
class OGCAPIServerContact(BaseSettings):
|
class OGCAPIServerContact(BaseSettings):
|
||||||
name: str
|
name: str = 'Lastname, Firstname'
|
||||||
address: str
|
position: str = 'Position Title'
|
||||||
city: str
|
address: str = 'Mailing Address'
|
||||||
stateorprovince: str
|
city: str = 'City'
|
||||||
postalcode: int
|
stateorprovince: str = 'Administrative Area'
|
||||||
country: str
|
postalcode: int = 0
|
||||||
email: str
|
country: str = 'Country'
|
||||||
|
email: str = 'you@example.org'
|
||||||
|
url: str | None = None
|
||||||
|
|
||||||
class OGCAPIIdentification(BaseSettings):
|
class OGCAPIIdentification(BaseSettings):
|
||||||
title: str
|
title: str = 'pygeoapi default instance'
|
||||||
description: str
|
description: str = 'pygeoapi provides an API to geospatial data'
|
||||||
keywords: list[str]
|
keywords: list[str] = ['geospatial', 'data', 'api']
|
||||||
keywords_type: str
|
keywords_type: str = 'theme'
|
||||||
terms_of_service: str
|
terms_of_service: str = 'https://creativecommons.org/licenses/by/4.0/'
|
||||||
url: str
|
url: str = 'http://example.org'
|
||||||
|
|
||||||
class OGCAPIMetadata(BaseSettings):
|
class OGCAPIMetadata(BaseSettings):
|
||||||
identification: OGCAPIIdentification
|
identification: OGCAPIIdentification = OGCAPIIdentification()
|
||||||
license: OGCAPILicense
|
license: OGCAPILicense = OGCAPILicense()
|
||||||
provider: OGCAPIProvider
|
provider: OGCAPIProvider = OGCAPIProvider()
|
||||||
contact: OGCAPIServerContact
|
contact: OGCAPIServerContact = OGCAPIServerContact()
|
||||||
|
|
||||||
class ServerBind(BaseSettings):
|
class ServerBind(BaseSettings):
|
||||||
host: str
|
host: str = '0.0.0.0'
|
||||||
port: int
|
port: int = 5000
|
||||||
|
|
||||||
class OGCAPIServerMap(BaseSettings):
|
class OGCAPIServerMap(BaseSettings):
|
||||||
url: str
|
url: str = 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png'
|
||||||
attribution: str
|
attribution: str = '''<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia maps</a> | Map data © <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'''
|
||||||
|
|
||||||
class OGCAPIServer(BaseSettings):
|
class OGCAPIServer(BaseSettings):
|
||||||
bind: ServerBind
|
bind: ServerBind = ServerBind()
|
||||||
url: str
|
url: str = 'https://example.org/ogcapi'
|
||||||
mimetype: str
|
mimetype: str = 'application/json; charset=UTF-8'
|
||||||
encoding: str
|
encoding: str = 'utf-8'
|
||||||
language: str
|
language: str = 'en-US'
|
||||||
pretty_print: bool
|
pretty_print: bool = False
|
||||||
limit: int
|
limit: int = 1000
|
||||||
map: OGCAPIServerMap
|
map: OGCAPIServerMap = OGCAPIServerMap()
|
||||||
|
|
||||||
class OGCAPI(BaseSettings):
|
class OGCAPI(BaseSettings):
|
||||||
base_url: str
|
base_url: str = 'http://example.org/ogcapi'
|
||||||
bbox: list[float]
|
bbox: list[float] = [-180, -90, 180, 90]
|
||||||
log: Log
|
log: Log = Log()
|
||||||
metadata: OGCAPIMetadata
|
metadata: OGCAPIMetadata = OGCAPIMetadata()
|
||||||
server: OGCAPIServer
|
server: OGCAPIServer = OGCAPIServer()
|
||||||
|
|
||||||
class TileServer(BaseSettings):
|
class TileServer(BaseSettings):
|
||||||
baseDir: str
|
baseDir: str = '/path/to/mbtiles_files_dir'
|
||||||
useRequestUrl: bool = False
|
useRequestUrl: bool = False
|
||||||
spriteBaseDir: str
|
spriteBaseDir: str = '/path/to/mbtiles_sprites_dir'
|
||||||
spriteUrl: str
|
spriteUrl: str = '/tiles/sprite/sprite'
|
||||||
spriteBaseUrl: str
|
spriteBaseUrl: str = 'https://gisaf.example.org'
|
||||||
openMapTilesKey: str | None = None
|
openMapTilesKey: str | None = None
|
||||||
|
|
||||||
class Map(BaseSettings):
|
class Map(BaseSettings):
|
||||||
tileServer: TileServer | None = None
|
tileServer: TileServer = TileServer()
|
||||||
zoom: int
|
zoom: int = 14
|
||||||
pitch: int
|
pitch: int = 45
|
||||||
lat: float
|
lat: float = 12.0000
|
||||||
lng: float
|
lng: float = 79.8106
|
||||||
bearing: float
|
bearing: float = 0
|
||||||
style: str
|
style: str = 'OSM (vector)'
|
||||||
opacity: float
|
opacity: float = 1
|
||||||
attribution: str
|
attribution: str = ''
|
||||||
status: list[str]
|
status: list[str] = ['E', 'F', 'D']
|
||||||
defaultStatus: list[str] # FIXME: should be str
|
defaultStatus: list[str] = ['E'] # FIXME: should be str
|
||||||
tagKeys: list[str]
|
tagKeys: list[str] = ['source']
|
||||||
|
|
||||||
class Measures(BaseSettings):
|
class Measures(BaseSettings):
|
||||||
defaultStore: str
|
defaultStore: str | None = None
|
||||||
|
|
||||||
class BasketDefault(BaseSettings):
|
class BasketDefault(BaseSettings):
|
||||||
surveyor: str
|
surveyor: str = 'Default surveyor'
|
||||||
equipment: str
|
equipment: str = 'Default equipment'
|
||||||
project: str | None
|
project: str = 'Default project'
|
||||||
status: str
|
status: str = 'E'
|
||||||
store: str | None
|
store: str | None = None
|
||||||
|
|
||||||
class BasketOldDef(BaseSettings):
|
# class BasketOldDef(BaseSettings):
|
||||||
base_dir: str
|
# base_dir: str
|
||||||
|
|
||||||
class Basket(BaseSettings):
|
class Basket(BaseSettings):
|
||||||
base_dir: str
|
base_dir: str = '/var/local/gisaf/baskets'
|
||||||
default: BasketDefault
|
default: BasketDefault = BasketDefault()
|
||||||
|
|
||||||
class Plot(BaseSettings):
|
class Plot(BaseSettings):
|
||||||
maxDataSize: int
|
maxDataSize: int = 10000
|
||||||
|
|
||||||
class Dashboard(BaseSettings):
|
class Dashboard(BaseSettings):
|
||||||
base_source_url: str
|
base_source_url: str = 'http://url.to.jupyter/lab/tree/'
|
||||||
base_storage_dir: str
|
base_storage_dir: str = '/var/lib/share/gisaf/dashboard'
|
||||||
base_storage_url: str = '/dashboard-attachment/'
|
base_storage_url: str = '/dashboard-attachment/'
|
||||||
|
|
||||||
class Widgets(BaseSettings):
|
class Widgets(BaseSettings):
|
||||||
footer: str
|
footer: str = """Generated by <span class='link' onclick="window.open('https://redmine.auroville.org.in/projects/gisaf/')">Gisaf</span>"""
|
||||||
|
|
||||||
class Admin(BaseSettings):
|
class Admin(BaseSettings):
|
||||||
basket: Basket
|
basket: Basket = Basket()
|
||||||
|
|
||||||
class Attachments(BaseSettings):
|
class Attachments(BaseSettings):
|
||||||
base_dir: str
|
base_dir: str = '/var/local/gisaf/attachments'
|
||||||
|
|
||||||
class Job(BaseSettings):
|
class Job(BaseSettings):
|
||||||
id: str
|
id: str
|
||||||
|
@ -256,7 +258,7 @@ class Config(BaseSettings):
|
||||||
dotenv_settings: PydanticBaseSettingsSource,
|
dotenv_settings: PydanticBaseSettingsSource,
|
||||||
file_secret_settings: PydanticBaseSettingsSource,
|
file_secret_settings: PydanticBaseSettingsSource,
|
||||||
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
||||||
return env_settings, init_settings, file_secret_settings, config_file_settings
|
return env_settings, init_settings, file_secret_settings, config_file_settings # type: ignore
|
||||||
|
|
||||||
# def __init__(self, **kwargs):
|
# def __init__(self, **kwargs):
|
||||||
# super().__init__(**kwargs)
|
# super().__init__(**kwargs)
|
||||||
|
@ -268,29 +270,27 @@ class Config(BaseSettings):
|
||||||
# 'web_mercator': 'epsg:3857',
|
# 'web_mercator': 'epsg:3857',
|
||||||
# }
|
# }
|
||||||
|
|
||||||
admin: Admin
|
admin: Admin = Admin()
|
||||||
attachments: Attachments
|
attachments: Attachments = Attachments()
|
||||||
basket: BasketOldDef
|
# basket: BasketOldDef = BasketOldDef()
|
||||||
# crs: Crs
|
# crs: Crs
|
||||||
crypto: Crypto
|
crypto: Crypto = Crypto()
|
||||||
dashboard: Dashboard
|
dashboard: Dashboard = Dashboard()
|
||||||
db: DB
|
db: DB = DB()
|
||||||
flask: Flask
|
# flask: Flask
|
||||||
geo: Geo
|
geo: Geo = Geo()
|
||||||
gisaf: GisafConfig
|
gisaf: GisafConfig = GisafConfig()
|
||||||
gisaf_live: GisafLive
|
gisaf_live: GisafLive = GisafLive()
|
||||||
jobs: list[Job]
|
jobs: list[Job] = []
|
||||||
map: Map
|
map: Map = Map()
|
||||||
measures: Measures
|
measures: Measures = Measures()
|
||||||
ogcapi: OGCAPI
|
ogcapi: OGCAPI = OGCAPI()
|
||||||
plot: Plot
|
plot: Plot = Plot()
|
||||||
plugins: dict[str, dict[str, Any]]
|
plugins: dict[str, dict[str, Any]] = {}
|
||||||
survey: Survey
|
survey: Survey = Survey()
|
||||||
version: str
|
version: str = __version__
|
||||||
weather_station: dict[str, dict[str, Any]]
|
weather_station: dict[str, dict[str, Any]] = {}
|
||||||
widgets: Widgets
|
widgets: Widgets = Widgets()
|
||||||
#engine: AsyncEngine
|
|
||||||
#session_maker: sessionmaker
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crs(self) -> Crs:
|
def crs(self) -> Crs:
|
||||||
|
@ -328,7 +328,7 @@ def load_yaml(path: Path) -> dict[str, Any]:
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
conf = Config(version=__version__)
|
conf = Config()
|
||||||
|
|
||||||
# def set_app_config(app) -> None:
|
# def set_app_config(app) -> None:
|
||||||
# raw_configs = []
|
# raw_configs = []
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import Annotated, Literal, Any
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.orm import joinedload, QueryableAttribute, InstrumentedAttribute
|
from sqlalchemy.orm import joinedload, QueryableAttribute
|
||||||
from sqlalchemy.sql.selectable import Select
|
from sqlalchemy.sql.selectable import Select
|
||||||
from sqlmodel import SQLModel, select
|
from sqlmodel import SQLModel, select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
@ -76,7 +76,7 @@ class BaseModel(SQLModel):
|
||||||
columns.add(*(col.name for col in cls.__table__.primary_key.columns))
|
columns.add(*(col.name for col in cls.__table__.primary_key.columns))
|
||||||
query = select(*(getattr(cls, col) for col in columns))
|
query = select(*(getattr(cls, col) for col in columns))
|
||||||
if where is not None:
|
if where is not None:
|
||||||
query.append_whereclause(where)
|
query = query.where(where)
|
||||||
## Get the joined tables
|
## Get the joined tables
|
||||||
joined_tables = cls.selectinload()
|
joined_tables = cls.selectinload()
|
||||||
if with_related and len(joined_tables) > 0:
|
if with_related and len(joined_tables) > 0:
|
||||||
|
|
|
@ -186,7 +186,7 @@ class SurveyModel(BaseSurveyModel):
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __tablename__(cls) -> str:
|
def __tablename__(cls) -> str:
|
||||||
return cls.__name__ # type: nocheck
|
return cls.__name__ # type: ignore
|
||||||
|
|
||||||
async def get_survey_info(self):
|
async def get_survey_info(self):
|
||||||
info = await super(SurveyModel, self).get_survey_info()
|
info = await super(SurveyModel, self).get_survey_info()
|
||||||
|
|
|
@ -11,7 +11,7 @@ from gisaf.models.store import Store
|
||||||
|
|
||||||
class BaseStyle(Model, table=True):
|
class BaseStyle(Model, table=True):
|
||||||
__table_args__ = gisaf_map.table_args
|
__table_args__ = gisaf_map.table_args
|
||||||
__tablename__ = 'map_base_style'
|
__tablename__: str = 'map_base_style' # type: ignore
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
menu = 'Other'
|
menu = 'Other'
|
||||||
|
@ -19,18 +19,18 @@ class BaseStyle(Model, table=True):
|
||||||
|
|
||||||
id: int | None = Field(primary_key=True, default=None)
|
id: int | None = Field(primary_key=True, default=None)
|
||||||
name: str
|
name: str
|
||||||
style: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True))
|
style: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||||
mbtiles: str = Field(sa_type=String(50))
|
mbtiles: str = Field(sa_type=String(50)) # type: ignore
|
||||||
static_tiles_url: str
|
static_tiles_url: str
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<models.BaseStyle {self.name:s}>'.format(self=self)
|
return f'<models.BaseStyle {self.name:s}>'
|
||||||
|
|
||||||
|
|
||||||
class BaseMap(Model, table=True):
|
class BaseMap(Model, table=True):
|
||||||
__table_args__ = gisaf_map.table_args
|
__table_args__ = gisaf_map.table_args
|
||||||
__tablename__ = 'base_map'
|
__tablename__: str = 'base_map' # type: ignore
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
menu = 'Other'
|
menu = 'Other'
|
||||||
|
@ -39,14 +39,14 @@ class BaseMap(Model, table=True):
|
||||||
name: str
|
name: str
|
||||||
layers: list['BaseMapLayer'] = Relationship(back_populates='base_map')
|
layers: list['BaseMapLayer'] = Relationship(back_populates='base_map')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return '<models.BaseMap {self.name:s}>'.format(self=self)
|
return f'<models.BaseMap {self.name:s}>'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectinload(cls):
|
def selectinload(cls) -> list[list['BaseMapLayer']]:
|
||||||
return [
|
return [
|
||||||
cls.layers
|
cls.layers
|
||||||
]
|
]
|
||||||
|
@ -54,7 +54,7 @@ class BaseMap(Model, table=True):
|
||||||
|
|
||||||
class BaseMapLayer(Model, table=True):
|
class BaseMapLayer(Model, table=True):
|
||||||
__table_args__ = gisaf_map.table_args
|
__table_args__ = gisaf_map.table_args
|
||||||
__tablename__ = 'base_map_layer'
|
__tablename__: str = 'base_map_layer' # type: ignore
|
||||||
|
|
||||||
class Admin:
|
class Admin:
|
||||||
menu = 'Other'
|
menu = 'Other'
|
||||||
|
@ -63,7 +63,7 @@ class BaseMapLayer(Model, table=True):
|
||||||
base_map_id: int = Field(foreign_key=gisaf_map.table('base_map.id'),
|
base_map_id: int = Field(foreign_key=gisaf_map.table('base_map.id'),
|
||||||
index=True)
|
index=True)
|
||||||
base_map: BaseMap = Relationship(back_populates='layers')
|
base_map: BaseMap = Relationship(back_populates='layers')
|
||||||
store: str = Field(sa_type=String(100))
|
store: str = Field(sa_type=String(100)) # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectinload(cls):
|
def selectinload(cls):
|
||||||
|
|
|
@ -40,6 +40,17 @@ class TokenData(BaseModel):
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
||||||
|
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
expired_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Token has expired",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
def get_password_hash(password: str):
|
def get_password_hash(password: str):
|
||||||
return pwd_context.hash(password)
|
return pwd_context.hash(password)
|
||||||
|
@ -97,17 +108,7 @@ def verify_password(user: User, plain_password):
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
token: str = Depends(oauth2_scheme)) -> UserRead | None:
|
token: str = Depends(oauth2_scheme)) -> UserRead | None:
|
||||||
credentials_exception = HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Could not validate credentials",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
expired_exception = HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Token has expired",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
if token is None:
|
if token is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
|
196
src/gisaf/tiles.py
Normal file
196
src/gisaf/tiles.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
"""
|
||||||
|
mbtile server
|
||||||
|
|
||||||
|
Instructions (example):
|
||||||
|
|
||||||
|
cd map ## Matches tilesBaseDir in config
|
||||||
|
|
||||||
|
curl -O http://download.geofabrik.de/asia/india/southern-zone-latest.osm.pbf
|
||||||
|
|
||||||
|
tilemaker \
|
||||||
|
--config /usr/local/lib/gisaf_src/tilemaker_src/tilemaker/resources/config-openmaptiles.json \
|
||||||
|
--process /usr/local/lib/gisaf_src/tilemaker_src/tilemaker/resources/process-openmaptiles.lua \
|
||||||
|
--input southern-zone-latest.osm.pbf \
|
||||||
|
--output southern-zone-latest.mbtile
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Get the style from https://github.com/openmaptiles, eg.
|
||||||
|
curl -o osm-bright-full.json https://raw.githubusercontent.com/openmaptiles/osm-bright-gl-style/master/style.json
|
||||||
|
## Minify json:
|
||||||
|
python -c 'import json, sys;json.dump(json.load(sys.stdin), sys.stdout)' < osm-bright-full.json > osm-bright.json
|
||||||
|
|
||||||
|
And copy the style to the gisaf_map.map_base_style table,
|
||||||
|
with the name matching the file name
|
||||||
|
(TODO: this would need de-coupling the source mbtile and the style)
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Get the sprites from openmaptiles:`
|
||||||
|
|
||||||
|
cd tiles ## Matches tilesSpriteBaseDir in config
|
||||||
|
|
||||||
|
curl -O 'https://openmaptiles.github.io/osm-bright-gl-style/sprite.png'
|
||||||
|
curl -O 'https://openmaptiles.github.io/osm-bright-gl-style/sprite.json'
|
||||||
|
curl -O 'https://openmaptiles.github.io/osm-bright-gl-style/sprite@2x.png'
|
||||||
|
curl -O 'https://openmaptiles.github.io/osm-bright-gl-style/sprite@2x.json'
|
||||||
|
|
||||||
|
TODO: TO migrate - 3/1/2024: this was copied from legacy gisaf without change
|
||||||
|
See: https://github.com/gis-ops/tutorials/blob/master/webservices/fastapi/fastapi_auth_vector_tiles.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from json import loads, dumps
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Response, responses, HTTPException, status
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
import aiosqlite
|
||||||
|
|
||||||
|
from gisaf.config import conf
|
||||||
|
|
||||||
|
logger = logging.getLogger('gisaf tile server')
|
||||||
|
|
||||||
|
api = FastAPI(
|
||||||
|
default_response_class=responses.ORJSONResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
OSM_ATTRIBUTION = '<a href=\"http://www.openstreetmap.org/about/\" target=\"_blank\">© OpenStreetMap contributors</a>'
|
||||||
|
|
||||||
|
class MBTiles:
|
||||||
|
def __init__(self, file_path, style_name):
|
||||||
|
self.file_path = file_path
|
||||||
|
self.name = style_name
|
||||||
|
self.scheme = 'tms'
|
||||||
|
self.etag = f'W/"{hex(int(file_path.stat().st_mtime))[2:]}"'
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
self.db = await aiosqlite.connect(self.file_path)
|
||||||
|
self.metadata = {}
|
||||||
|
try:
|
||||||
|
async with self.db.execute('select name, value from metadata') as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
self.metadata[row[0]] = row[1]
|
||||||
|
except aiosqlite.DatabaseError as err:
|
||||||
|
logger.warning(f'Cannot read {self.file_path}, will not be able to serve tiles (error: {err.args[0]})')
|
||||||
|
|
||||||
|
self.metadata['bounds'] = [float(v) for v in self.metadata['bounds'].split(',')]
|
||||||
|
self.metadata['maxzoom'] = int(self.metadata['maxzoom'])
|
||||||
|
self.metadata['minzoom'] = int(self.metadata['minzoom'])
|
||||||
|
|
||||||
|
async def get_style(self, style_record, request):
|
||||||
|
"""
|
||||||
|
Generate on the fly the style
|
||||||
|
"""
|
||||||
|
if conf.map.tileServer.useRequestUrl:
|
||||||
|
base_url = request.url.parent
|
||||||
|
else:
|
||||||
|
base_url = conf.map.tileServer.spriteBaseUrl
|
||||||
|
base_tiles_url = f"{base_url}/tiles/{self.name}"
|
||||||
|
scheme = self.scheme
|
||||||
|
## TODO: avoid parse and serialize at every request
|
||||||
|
layers = loads(style_record['style'])['layers']
|
||||||
|
for layer in layers:
|
||||||
|
if 'source' in layer:
|
||||||
|
layer['source'] = 'gisafTiles'
|
||||||
|
resp = {
|
||||||
|
'basename': self.file_path.stem,
|
||||||
|
#'center': self.center,
|
||||||
|
'description': f'Extract of {self.file_path.stem} from OSM, powered by Gisaf',
|
||||||
|
'format': self.metadata['format'],
|
||||||
|
'id': f'gisaftiles_{self.name}',
|
||||||
|
'maskLevel': 5,
|
||||||
|
'name': self.name,
|
||||||
|
#'pixel_scale': 256,
|
||||||
|
#'planettime': '1499040000000',
|
||||||
|
'tilejson': '2.0.0',
|
||||||
|
'version': 8,
|
||||||
|
'glyphs': f"/assets/fonts/glyphs/{{fontstack}}/{{range}}.pbf",
|
||||||
|
'sprite': f"{base_url}{conf.map.tileServer.spriteUrl}",
|
||||||
|
'sources': {
|
||||||
|
'gisafTiles': {
|
||||||
|
'type': 'vector',
|
||||||
|
'tiles': [
|
||||||
|
f'{base_tiles_url}/{{z}}/{{x}}/{{y}}.pbf',
|
||||||
|
],
|
||||||
|
'maxzoom': self.metadata['maxzoom'],
|
||||||
|
'minzoom': self.metadata['minzoom'],
|
||||||
|
'bounds': self.metadata['bounds'],
|
||||||
|
'scheme': scheme,
|
||||||
|
'attribution': OSM_ATTRIBUTION,
|
||||||
|
'version': self.metadata['version'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'layers': layers,
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
class MBTilesRegistry:
|
||||||
|
mbtiles: dict[str, MBTiles]
|
||||||
|
async def setup(self):
|
||||||
|
"""
|
||||||
|
Read all mbtiles, construct styles
|
||||||
|
"""
|
||||||
|
self.mbtiles = {}
|
||||||
|
for file_path in Path(conf.map.tileServer.baseDir).glob('*.mbtiles'):
|
||||||
|
mbtiles = MBTiles(file_path, file_path.stem)
|
||||||
|
self.mbtiles[file_path.stem] = mbtiles
|
||||||
|
await mbtiles.connect()
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
"""
|
||||||
|
Tear down the connection to the mbtiles files
|
||||||
|
"""
|
||||||
|
for mbtiles in self.mbtiles.values():
|
||||||
|
await mbtiles.db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@api.get('/{style_name}/{z}/{x}/{y}.pbf')
|
||||||
|
async def get_tile(request, style_name: str, z:int, x: int, y: int,
|
||||||
|
response: Response):
|
||||||
|
"""
|
||||||
|
Return the specific tile
|
||||||
|
"""
|
||||||
|
if style_name not in registry.mbtiles:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
mbtiles = registry.mbtiles[style_name]
|
||||||
|
|
||||||
|
if request.headers.get('If-None-Match') == mbtiles.etag:
|
||||||
|
request.not_modified = True
|
||||||
|
return {}
|
||||||
|
|
||||||
|
response.headers['Content-Encoding'] = 'gzip'
|
||||||
|
response.headers['Content-Type'] = 'application/octet-stream'
|
||||||
|
request.response_etag = mbtiles.etag
|
||||||
|
async with mbtiles.db.execute('select tile_data from tiles where zoom_level=? and tile_column=? and tile_row=?',
|
||||||
|
(z, x, y)) as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
return row[0]
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
#@routes.get('/sprite/{name:\S+}')
|
||||||
|
#async def get_sprite(request):
|
||||||
|
|
||||||
|
|
||||||
|
@api.get('/{style_name}')
|
||||||
|
async def get_style(request, style_name: str):
|
||||||
|
"""
|
||||||
|
Return the base style.
|
||||||
|
Note that normal operations are processed through graphql (resolve_base_style)
|
||||||
|
:param request:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if style_name not in registry.mbtiles:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
mbtiles = registry.mbtiles[style_name]
|
||||||
|
return await mbtiles.get_style()
|
||||||
|
|
||||||
|
|
||||||
|
registry = MBTilesRegistry()
|
||||||
|
|
||||||
|
api.mount("/sprite",
|
||||||
|
StaticFiles(directory=conf.map.tileServer.spriteBaseDir),
|
||||||
|
name="sprites")
|
Loading…
Add table
Add a link
Reference in a new issue