Add redis (live layers)
Add base for geo_api (WIP)
This commit is contained in:
parent
049b8c9927
commit
4048e61221
11 changed files with 694 additions and 55 deletions
132
src/gisaf/geoapi.py
Normal file
132
src/gisaf/geoapi.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
Geographical json stores, served under /gj
|
||||
Used for displaying features on maps
|
||||
"""
|
||||
import logging
|
||||
from asyncio import CancelledError
|
||||
|
||||
from fastapi import FastAPI, HTTPException, status, responses
|
||||
from .redis_tools import store as redis_store
|
||||
# from gisaf.live import live_server
|
||||
from .registry import registry
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
api = FastAPI(
|
||||
default_response_class=responses.ORJSONResponse,
|
||||
)
|
||||
|
||||
@api.get('/live/{store}')
|
||||
async def live_layer(store: str):
|
||||
"""
|
||||
Websocket for live layer updates
|
||||
"""
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
async for msg in ws:
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
if msg.data == 'close':
|
||||
await ws.close()
|
||||
else:
|
||||
msg_data = msg.json()
|
||||
if 'message' in msg_data:
|
||||
if msg_data['message'] == 'subscribeLiveLayer':
|
||||
live_server.add_subscription(ws, store)
|
||||
elif msg_data['message'] == 'unsubscribeLiveLayer':
|
||||
live_server.remove_subscription(ws, store)
|
||||
elif msg.type == WSMsgType.ERROR:
|
||||
logger.exception(ws.exception())
|
||||
logger.debug('websocket connection closed')
|
||||
return ws
|
||||
|
||||
@api.get('/{store_name}')
|
||||
async def get_geojson(store_name):
|
||||
"""
|
||||
Some REST stores coded manually (route prefixed with "gj": geojson).
|
||||
:param store_name: name of the model
|
||||
:return: json
|
||||
"""
|
||||
use_cache = False
|
||||
try:
|
||||
model = registry.stores.loc[store_name].model
|
||||
except KeyError:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if hasattr(model, 'viewable_role') and model.viewable_role:
|
||||
await check_permission(request, model.viewable_role)
|
||||
|
||||
if await redis_store.has_channel(store_name):
|
||||
## Live layers
|
||||
data = await redis_store.get_layer_as_json(store_name)
|
||||
return web.Response(text=data.decode(), content_type='application/json')
|
||||
|
||||
# elif not model:
|
||||
# raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if model.cache_enabled:
|
||||
ttag = await redis_store.get_ttag(store_name)
|
||||
if ttag and request.headers.get('If-None-Match') == ttag:
|
||||
return web.HTTPNotModified()
|
||||
|
||||
if hasattr(model, 'get_geojson'):
|
||||
geojson = await model.get_geojson(simplify_tolerance=float(request.headers.get('simplify', 50.0)))
|
||||
## Store to redis for caching
|
||||
if use_cache:
|
||||
await redis_store.store_json(model, geojson)
|
||||
resp = web.Response(text=geojson, content_type='application/json')
|
||||
|
||||
elif model.can_get_features_as_df:
|
||||
## Get the GeoDataframe (gdf) with GeoPandas
|
||||
## get_popup and get_propertites get the gdf as argument and can use vectorised operations
|
||||
try:
|
||||
gdf = await model.get_geo_df(cast=True, with_related=True, filter_columns=True)
|
||||
except CancelledError as err:
|
||||
logger.debug(f'Request for {store_name} cancelled while getting gdf')
|
||||
raise err
|
||||
except Exception as err:
|
||||
logger.exception(err)
|
||||
raise web.HTTPInternalServerError()
|
||||
## The query of category defined models gets the status (not sure how and this could be skipped)
|
||||
## Other models do not have: just add it manually from the model itself
|
||||
if 'status' not in gdf.columns:
|
||||
gdf['status'] = model.status
|
||||
if 'popup' not in gdf.columns:
|
||||
gdf['popup'] = await model.get_popup(gdf)
|
||||
properties = await model.get_properties(gdf)
|
||||
columns = ['geometry', 'status', 'popup']
|
||||
for property, values in properties.items():
|
||||
columns.append(property)
|
||||
gdf[property] = values
|
||||
geojson = gdf[columns].to_json(separators=(',', ':'), check_circular=False)
|
||||
## Store to redis for caching
|
||||
if use_cache:
|
||||
await redis_store.store_json(model, geojson)
|
||||
resp = geojson
|
||||
|
||||
else:
|
||||
logger.warn(f"{model} doesn't allow using dataframe for generating json!")
|
||||
attrs, features_kwargs = await model.get_features_attrs(
|
||||
float(request.headers.get('simplify', 50.0)))
|
||||
## Using gino: allows OO model (get_info, etc)
|
||||
try:
|
||||
attrs['features'] = await model.get_features_in_bulk_gino(**features_kwargs)
|
||||
except Exception as err:
|
||||
logger.exception(err)
|
||||
raise web.HTTPInternalServerError()
|
||||
resp = attrs
|
||||
|
||||
if model.cache_enabled and ttag:
|
||||
resp.headers.add('ETag', ttag)
|
||||
return resp
|
||||
|
||||
|
||||
@api.get('/gj/{store_name}/popup/{id}')
|
||||
async def gj_popup(store_name: str, id: int):
|
||||
model = registry.geom.get(store_name)
|
||||
if not hasattr(model, 'get_popup_dynamic'):
|
||||
return ''
|
||||
obj = await model.get(id)
|
||||
## Escape characters for json
|
||||
popup_more = obj.get_popup_dynamic().replace('"', '\\"').replace('\n', '\\n')
|
||||
return {"text": popup_more}
|
Loading…
Add table
Add a link
Reference in a new issue