Add missing dependencies

Add tile server
Config: add defults
Cosmetic refactorings
This commit is contained in:
phil 2024-02-10 19:26:38 +05:30
parent 7e9e266157
commit 5dacc908f2
12 changed files with 434 additions and 187 deletions

View file

@ -1 +1 @@
__version__ = '2023.4.dev28+ge3ed311.d20240107'
__version__ = '2023.4.dev33+g7e9e266.d20240210'

View file

@ -4,6 +4,7 @@ import logging
from gisaf.live import live_server
from gisaf.redis_tools import Store
from gisaf.baskets import Basket
logger = logging.getLogger('Gisaf admin manager')
@ -13,6 +14,7 @@ class AdminManager:
One instance only, handled by Gisaf's process.
"""
store: Store
baskets: dict[str, Basket]
async def setup_admin(self, app):
"""
Create the default baskets, scan and create baskets

View file

@ -1,15 +1,17 @@
from collections import OrderedDict, defaultdict
import logging
from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException, status, responses
from json import dumps
from fastapi import FastAPI, Request, HTTPException, status, responses
from sqlalchemy.orm import selectinload
from sqlalchemy.exc import NoResultFound
from sqlmodel import select
from gisaf.config import conf
from gisaf.models.map_bases import BaseMap, BaseMapLayer, BaseStyle, MapInitData
from gisaf.registry import registry
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__)
@ -21,20 +23,20 @@ async def get_base_styles():
async with db_session() as session:
query = select(BaseStyle.name)\
.where(BaseStyle.enabled==True)\
.order_by(BaseStyle.id)
.order_by(BaseStyle.id) # type: ignore # noqa: E712
data = await session.exec(query)
base_styles = data.all()
## TODO: tiles_registry
logger.warning('TODO: tiles_registry')
# 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 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)
base_maps = data1.all()
return base_maps
return base_maps # type: ignore
base_map_dict = {bm.id: bm.name for bm in base_maps}
query2 = select(BaseMapLayer).options(selectinload(BaseMapLayer.base_map))
data2 = await session.exec(query2)
@ -58,5 +60,27 @@ async def get_init_data() -> MapInitData:
baseStyles=await get_base_styles(),
baseMaps=await get_base_maps(),
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

View file

@ -8,6 +8,7 @@ from gisaf.api.geoapi import api as geoapi
from gisaf.config import conf
from gisaf.registry import registry
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
logging.basicConfig(level=conf.gisaf.debugLevel)
@ -20,8 +21,10 @@ async def lifespan(app: FastAPI):
await setup_redis()
await setup_redis_cache()
await setup_live()
await map_tile_registry.setup()
yield
await shutdown_redis()
await map_tile_registry.shutdown()
app = FastAPI(
debug=False,

View file

@ -23,81 +23,81 @@ config_files = [
]
class DashboardHome(BaseSettings):
title: str
content_file: str
footer_file: str
title: str = 'Gisaf - home/dashboards'
content_file: str = '/etc/gisaf/dashboard_home_content.html'
footer_file: str = '/etc/gisaf/dashboard_home_footer.html'
class GisafConfig(BaseSettings):
title: str
windowTitle: str
debugLevel: str
dashboard_home: DashboardHome
title: str = 'Gisaf'
windowTitle: str = 'Gisaf'
debugLevel: str = 'INFO'
dashboard_home: DashboardHome = DashboardHome()
redirect: str = ''
use_pretty_errors: bool = False
class SpatialSysRef(BaseSettings):
author: str
ellps: str
k: int
lat_0: float
lon_0: float
no_defs: bool
proj: str
towgs84: str
units: str
x_0: float
y_0: float
author: str = 'AVSM'
ellps: str = 'WGS84'
k: int = 1
lat_0: float = 12.01605433
lon_0: float = 79.80998934
no_defs: bool = True
proj: str = 'tmerc'
towgs84: str = '0,0,0,0,0,0,0'
units: str = 'm'
x_0: float = 370455.630
y_0: float = 1328608.994
class RawSurvey(BaseSettings):
spatial_sys_ref: SpatialSysRef
srid: int
spatial_sys_ref: SpatialSysRef = SpatialSysRef()
srid: int = 910001
class Geo(BaseSettings):
raw_survey: RawSurvey
simplify_geom_factor: int
raw_survey: RawSurvey = RawSurvey()
simplify_geom_factor: int = 10000000
simplify_preserve_topology: bool = False
srid: int
srid_for_proj: int
srid: int = 4326
srid_for_proj: int = 32644
class Flask(BaseSettings):
secret_key: str
debug: int
# class Flask(BaseSettings):
# secret_key: str
# debug: int
class MQTT(BaseSettings):
broker: str = 'localhost'
port: int = 1883
class GisafLive(BaseSettings):
hostname: str
port: int
scheme: str
redis: str
mqtt: MQTT
hostname: str = 'localhost'
port: int = 80
scheme: str = 'http'
redis: str = 'redis://localhost'
mqtt: MQTT = MQTT()
class DefaultSurvey(BaseSettings):
surveyor_id: int
equipment_id: int
surveyor_id: int = 1
equipment_id: int = 1
class Survey(BaseSettings):
model_config = ConfigDict(extra='ignore')
db_schema_raw: str
db_schema: str
default: DefaultSurvey
# model_config = ConfigDict(extra='ignore')
db_schema_raw: str = 'raw_survey'
db_schema: str = 'survey'
default: DefaultSurvey = DefaultSurvey()
class Crypto(BaseSettings):
secret: str
algorithm: str
expire: float
secret: str = 'Gisaf big secret'
algorithm: str = 'HS256'
expire: float = 21600
class DB(BaseSettings):
uri: str
host: str
# uri: str
host: str = 'localhost'
port: int = 5432
user: str
db: str
password: str
debug: bool
info: bool
user: str = 'gisaf'
db: str = 'gisaf'
password: str = 'secret'
debug: bool = False
info: bool = True
pool_size: int = 10
max_overflow: int = 10
echo: bool = False
@ -110,119 +110,121 @@ class DB(BaseSettings):
class Log(BaseSettings):
level: str
level: str = 'WARNING'
class OGCAPILicense(BaseSettings):
name: str
url: str
name: str = 'CC-BY 4.0 license'
url: str = 'https://creativecommons.org/licenses/by/4.0/'
class OGCAPIProvider(BaseSettings):
name: str
url: str
name: str = 'Organization Name'
url: str = 'https://pygeoapi.io'
class OGCAPIServerContact(BaseSettings):
name: str
address: str
city: str
stateorprovince: str
postalcode: int
country: str
email: str
name: str = 'Lastname, Firstname'
position: str = 'Position Title'
address: str = 'Mailing Address'
city: str = 'City'
stateorprovince: str = 'Administrative Area'
postalcode: int = 0
country: str = 'Country'
email: str = 'you@example.org'
url: str | None = None
class OGCAPIIdentification(BaseSettings):
title: str
description: str
keywords: list[str]
keywords_type: str
terms_of_service: str
url: str
title: str = 'pygeoapi default instance'
description: str = 'pygeoapi provides an API to geospatial data'
keywords: list[str] = ['geospatial', 'data', 'api']
keywords_type: str = 'theme'
terms_of_service: str = 'https://creativecommons.org/licenses/by/4.0/'
url: str = 'http://example.org'
class OGCAPIMetadata(BaseSettings):
identification: OGCAPIIdentification
license: OGCAPILicense
provider: OGCAPIProvider
contact: OGCAPIServerContact
identification: OGCAPIIdentification = OGCAPIIdentification()
license: OGCAPILicense = OGCAPILicense()
provider: OGCAPIProvider = OGCAPIProvider()
contact: OGCAPIServerContact = OGCAPIServerContact()
class ServerBind(BaseSettings):
host: str
port: int
host: str = '0.0.0.0'
port: int = 5000
class OGCAPIServerMap(BaseSettings):
url: str
attribution: str
url: str = 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png'
attribution: str = '''<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia maps</a> | Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'''
class OGCAPIServer(BaseSettings):
bind: ServerBind
url: str
mimetype: str
encoding: str
language: str
pretty_print: bool
limit: int
map: OGCAPIServerMap
bind: ServerBind = ServerBind()
url: str = 'https://example.org/ogcapi'
mimetype: str = 'application/json; charset=UTF-8'
encoding: str = 'utf-8'
language: str = 'en-US'
pretty_print: bool = False
limit: int = 1000
map: OGCAPIServerMap = OGCAPIServerMap()
class OGCAPI(BaseSettings):
base_url: str
bbox: list[float]
log: Log
metadata: OGCAPIMetadata
server: OGCAPIServer
base_url: str = 'http://example.org/ogcapi'
bbox: list[float] = [-180, -90, 180, 90]
log: Log = Log()
metadata: OGCAPIMetadata = OGCAPIMetadata()
server: OGCAPIServer = OGCAPIServer()
class TileServer(BaseSettings):
baseDir: str
baseDir: str = '/path/to/mbtiles_files_dir'
useRequestUrl: bool = False
spriteBaseDir: str
spriteUrl: str
spriteBaseUrl: str
spriteBaseDir: str = '/path/to/mbtiles_sprites_dir'
spriteUrl: str = '/tiles/sprite/sprite'
spriteBaseUrl: str = 'https://gisaf.example.org'
openMapTilesKey: str | None = None
class Map(BaseSettings):
tileServer: TileServer | None = None
zoom: int
pitch: int
lat: float
lng: float
bearing: float
style: str
opacity: float
attribution: str
status: list[str]
defaultStatus: list[str] # FIXME: should be str
tagKeys: list[str]
tileServer: TileServer = TileServer()
zoom: int = 14
pitch: int = 45
lat: float = 12.0000
lng: float = 79.8106
bearing: float = 0
style: str = 'OSM (vector)'
opacity: float = 1
attribution: str = ''
status: list[str] = ['E', 'F', 'D']
defaultStatus: list[str] = ['E'] # FIXME: should be str
tagKeys: list[str] = ['source']
class Measures(BaseSettings):
defaultStore: str
defaultStore: str | None = None
class BasketDefault(BaseSettings):
surveyor: str
equipment: str
project: str | None
status: str
store: str | None
surveyor: str = 'Default surveyor'
equipment: str = 'Default equipment'
project: str = 'Default project'
status: str = 'E'
store: str | None = None
class BasketOldDef(BaseSettings):
base_dir: str
# class BasketOldDef(BaseSettings):
# base_dir: str
class Basket(BaseSettings):
base_dir: str
default: BasketDefault
base_dir: str = '/var/local/gisaf/baskets'
default: BasketDefault = BasketDefault()
class Plot(BaseSettings):
maxDataSize: int
maxDataSize: int = 10000
class Dashboard(BaseSettings):
base_source_url: str
base_storage_dir: str
base_source_url: str = 'http://url.to.jupyter/lab/tree/'
base_storage_dir: str = '/var/lib/share/gisaf/dashboard'
base_storage_url: str = '/dashboard-attachment/'
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):
basket: Basket
basket: Basket = Basket()
class Attachments(BaseSettings):
base_dir: str
base_dir: str = '/var/local/gisaf/attachments'
class Job(BaseSettings):
id: str
@ -256,7 +258,7 @@ class Config(BaseSettings):
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: 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):
# super().__init__(**kwargs)
@ -268,29 +270,27 @@ class Config(BaseSettings):
# 'web_mercator': 'epsg:3857',
# }
admin: Admin
attachments: Attachments
basket: BasketOldDef
admin: Admin = Admin()
attachments: Attachments = Attachments()
# basket: BasketOldDef = BasketOldDef()
# crs: Crs
crypto: Crypto
dashboard: Dashboard
db: DB
flask: Flask
geo: Geo
gisaf: GisafConfig
gisaf_live: GisafLive
jobs: list[Job]
map: Map
measures: Measures
ogcapi: OGCAPI
plot: Plot
plugins: dict[str, dict[str, Any]]
survey: Survey
version: str
weather_station: dict[str, dict[str, Any]]
widgets: Widgets
#engine: AsyncEngine
#session_maker: sessionmaker
crypto: Crypto = Crypto()
dashboard: Dashboard = Dashboard()
db: DB = DB()
# flask: Flask
geo: Geo = Geo()
gisaf: GisafConfig = GisafConfig()
gisaf_live: GisafLive = GisafLive()
jobs: list[Job] = []
map: Map = Map()
measures: Measures = Measures()
ogcapi: OGCAPI = OGCAPI()
plot: Plot = Plot()
plugins: dict[str, dict[str, Any]] = {}
survey: Survey = Survey()
version: str = __version__
weather_station: dict[str, dict[str, Any]] = {}
widgets: Widgets = Widgets()
@property
def crs(self) -> Crs:
@ -328,7 +328,7 @@ def load_yaml(path: Path) -> dict[str, Any]:
return config
conf = Config(version=__version__)
conf = Config()
# def set_app_config(app) -> None:
# raw_configs = []

View file

@ -3,7 +3,7 @@ from typing import Annotated, Literal, Any
from collections.abc import AsyncGenerator
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 sqlmodel import SQLModel, select
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))
query = select(*(getattr(cls, col) for col in columns))
if where is not None:
query.append_whereclause(where)
query = query.where(where)
## Get the joined tables
joined_tables = cls.selectinload()
if with_related and len(joined_tables) > 0:

View file

@ -186,7 +186,7 @@ class SurveyModel(BaseSurveyModel):
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__ # type: nocheck
return cls.__name__ # type: ignore
async def get_survey_info(self):
info = await super(SurveyModel, self).get_survey_info()

View file

@ -11,7 +11,7 @@ from gisaf.models.store import Store
class BaseStyle(Model, table=True):
__table_args__ = gisaf_map.table_args
__tablename__ = 'map_base_style'
__tablename__: str = 'map_base_style' # type: ignore
class Admin:
menu = 'Other'
@ -19,18 +19,18 @@ class BaseStyle(Model, table=True):
id: int | None = Field(primary_key=True, default=None)
name: str
style: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True))
mbtiles: str = Field(sa_type=String(50))
style: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
mbtiles: str = Field(sa_type=String(50)) # type: ignore
static_tiles_url: str
enabled: bool = True
def __repr__(self):
return '<models.BaseStyle {self.name:s}>'.format(self=self)
return f'<models.BaseStyle {self.name:s}>'
class BaseMap(Model, table=True):
__table_args__ = gisaf_map.table_args
__tablename__ = 'base_map'
__tablename__: str = 'base_map' # type: ignore
class Admin:
menu = 'Other'
@ -39,14 +39,14 @@ class BaseMap(Model, table=True):
name: str
layers: list['BaseMapLayer'] = Relationship(back_populates='base_map')
def __repr__(self):
return '<models.BaseMap {self.name:s}>'.format(self=self)
def __repr__(self) -> str:
return f'<models.BaseMap {self.name:s}>'
def __str__(self):
def __str__(self) -> str:
return self.name
@classmethod
def selectinload(cls):
def selectinload(cls) -> list[list['BaseMapLayer']]:
return [
cls.layers
]
@ -54,7 +54,7 @@ class BaseMap(Model, table=True):
class BaseMapLayer(Model, table=True):
__table_args__ = gisaf_map.table_args
__tablename__ = 'base_map_layer'
__tablename__: str = 'base_map_layer' # type: ignore
class Admin:
menu = 'Other'
@ -63,7 +63,7 @@ class BaseMapLayer(Model, table=True):
base_map_id: int = Field(foreign_key=gisaf_map.table('base_map.id'),
index=True)
base_map: BaseMap = Relationship(back_populates='layers')
store: str = Field(sa_type=String(100))
store: str = Field(sa_type=String(100)) # type: ignore
@classmethod
def selectinload(cls):

View file

@ -40,6 +40,17 @@ class TokenData(BaseModel):
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):
return pwd_context.hash(password)
@ -97,17 +108,7 @@ def verify_password(user: User, plain_password):
async def get_current_user(
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"},
)
token: str = Depends(oauth2_scheme)) -> UserRead | None:
if token is None:
return None
try:

196
src/gisaf/tiles.py Normal file
View 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\">&copy; 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")