Return geoapi store geojson

This commit is contained in:
phil 2023-12-17 12:20:07 +05:30
parent 4048e61221
commit 1b7db43ee7
4 changed files with 44 additions and 40 deletions

View file

@ -1,5 +1,5 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Annotated from typing import Annotated, AsyncContextManager
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@ -23,7 +23,7 @@ async def get_db_session() -> AsyncSession:
yield session yield session
@asynccontextmanager @asynccontextmanager
async def db_session() -> AsyncSession: async def db_session() -> AsyncContextManager[AsyncSession]:
async with AsyncSession(engine) as session: async with AsyncSession(engine) as session:
yield session yield session

View file

@ -3,9 +3,11 @@ Geographical json stores, served under /gj
Used for displaying features on maps Used for displaying features on maps
""" """
import logging import logging
from typing import Annotated
from asyncio import CancelledError from asyncio import CancelledError
from fastapi import FastAPI, HTTPException, status, responses from fastapi import FastAPI, HTTPException, Response, status, responses, Header
from .redis_tools import store as redis_store from .redis_tools import store as redis_store
# from gisaf.live import live_server # from gisaf.live import live_server
from .registry import registry from .registry import registry
@ -41,7 +43,10 @@ async def live_layer(store: str):
return ws return ws
@api.get('/{store_name}') @api.get('/{store_name}')
async def get_geojson(store_name): async def get_geojson(store_name,
If_None_Match: Annotated[str | None, Header()] = None,
simplify: Annotated[float | None, Header()] = 50.0,
):
""" """
Some REST stores coded manually (route prefixed with "gj": geojson). Some REST stores coded manually (route prefixed with "gj": geojson).
:param store_name: name of the model :param store_name: name of the model
@ -59,22 +64,22 @@ async def get_geojson(store_name):
if await redis_store.has_channel(store_name): if await redis_store.has_channel(store_name):
## Live layers ## Live layers
data = await redis_store.get_layer_as_json(store_name) data = await redis_store.get_layer_as_json(store_name)
return web.Response(text=data.decode(), content_type='application/json') return data.decode()
# elif not model: # elif not model:
# raise HTTPException(status.HTTP_404_NOT_FOUND) # raise HTTPException(status.HTTP_404_NOT_FOUND)
if model.cache_enabled: if model.cache_enabled:
ttag = await redis_store.get_ttag(store_name) ttag = await redis_store.get_ttag(store_name)
if ttag and request.headers.get('If-None-Match') == ttag: if ttag and If_None_Match == ttag:
return web.HTTPNotModified() return status.HTTP_304_NOT_MODIFIED
if hasattr(model, 'get_geojson'): if hasattr(model, 'get_geojson'):
geojson = await model.get_geojson(simplify_tolerance=float(request.headers.get('simplify', 50.0))) geojson = await model.get_geojson(simplify_tolerance=simplify, registry=registry)
## Store to redis for caching ## Store to redis for caching
if use_cache: if use_cache:
await redis_store.store_json(model, geojson) await redis_store.store_json(model, geojson)
resp = web.Response(text=geojson, content_type='application/json') resp = geojson
elif model.can_get_features_as_df: elif model.can_get_features_as_df:
## Get the GeoDataframe (gdf) with GeoPandas ## Get the GeoDataframe (gdf) with GeoPandas
@ -86,7 +91,7 @@ async def get_geojson(store_name):
raise err raise err
except Exception as err: except Exception as err:
logger.exception(err) logger.exception(err)
raise web.HTTPInternalServerError() raise status.HTTP_500_INTERNAL_SERVER_ERROR
## The query of category defined models gets the status (not sure how and this could be skipped) ## 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 ## Other models do not have: just add it manually from the model itself
if 'status' not in gdf.columns: if 'status' not in gdf.columns:
@ -106,19 +111,19 @@ async def get_geojson(store_name):
else: else:
logger.warn(f"{model} doesn't allow using dataframe for generating json!") logger.warn(f"{model} doesn't allow using dataframe for generating json!")
attrs, features_kwargs = await model.get_features_attrs( attrs, features_kwargs = await model.get_features_attrs(simplify)
float(request.headers.get('simplify', 50.0)))
## Using gino: allows OO model (get_info, etc) ## Using gino: allows OO model (get_info, etc)
try: try:
attrs['features'] = await model.get_features_in_bulk_gino(**features_kwargs) attrs['features'] = await model.get_features_in_bulk_gino(**features_kwargs)
except Exception as err: except Exception as err:
logger.exception(err) logger.exception(err)
raise web.HTTPInternalServerError() raise status.HTTP_500_INTERNAL_SERVER_ERROR
resp = attrs resp = attrs
headers = {}
if model.cache_enabled and ttag: if model.cache_enabled and ttag:
resp.headers.add('ETag', ttag) headers['ETag'] = ttag
return resp return Response(content=resp, media_type="application/json", headers=headers)
@api.get('/gj/{store_name}/popup/{id}') @api.get('/gj/{store_name}/popup/{id}')

View file

@ -16,11 +16,12 @@ import shapely
import pyproj import pyproj
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field
from sqlmodel.ext.asyncio.session import AsyncSession
from pydantic import BaseModel from pydantic import BaseModel
from geoalchemy2.shape import from_shape from geoalchemy2.shape import from_shape
from sqlalchemy.dialects.postgresql import BIGINT from sqlalchemy.dialects.postgresql import BIGINT
from sqlalchemy import BigInteger, Column, String, func, and_ from sqlalchemy import BigInteger, Column, MetaData, String, func, and_, text
from sqlalchemy.sql import sqltypes from sqlalchemy.sql import sqltypes
from psycopg2.extensions import adapt from psycopg2.extensions import adapt
@ -36,13 +37,16 @@ from shapefile import (Writer as ShapeFileWriter,
POLYGON, POLYGONZ, POLYGON, POLYGONZ,
) )
from ..database import db_session
from ..config import conf from ..config import conf
from .models_base import Model from .models_base import Model
from ..models.metadata import survey, raw_survey
from ..utils import upsert_df
from .survey import Equipment, Surveyor, Accuracy from .survey import Equipment, Surveyor, Accuracy
from .misc import Qml from .misc import Qml
from .category import Category from .category import Category
from .project import Project from .project import Project
from ..utils import upsert_df
LOCALE_DATE_FORMAT = locale.nl_langinfo(locale.D_FMT) LOCALE_DATE_FORMAT = locale.nl_langinfo(locale.D_FMT)
@ -138,6 +142,7 @@ class SurveyModel(BaseSurveyModel):
""" """
Base mixin class for defining final (reprojected) survey data, with a status Base mixin class for defining final (reprojected) survey data, with a status
""" """
metadata: ClassVar[MetaData] = survey
status: str = Field(sa_type=String(1)) status: str = Field(sa_type=String(1))
get_gdf_with_related: ClassVar[bool] = False get_gdf_with_related: ClassVar[bool] = False
@ -198,7 +203,9 @@ class SurveyModel(BaseSurveyModel):
'] #' + df.index.astype('U') '] #' + df.index.astype('U')
@classmethod @classmethod
async def get_geojson(cls, simplify_tolerance=0): async def get_geojson(cls, registry=None, simplify_tolerance=0):
if registry is None:
from ..registry import registry
## Fastest, but the id is in properties and needs the front end (eg Gisaf for mapbox) ## Fastest, but the id is in properties and needs the front end (eg Gisaf for mapbox)
## to move it at the feature level ## to move it at the feature level
@ -211,7 +218,7 @@ class SurveyModel(BaseSurveyModel):
FROM ( FROM (
SELECT f.geom, SELECT f.geom,
f.id::varchar, f.id::varchar,
{description} || ' [' || '{model.category.group}' || '-' || '{model.category.minor_group_1}' || '] #' || f.id as popup, {description} || ' [' || '{category.group}' || '-' || '{category.minor_group_1}' || '] #' || f.id as popup,
f.status f.status
FROM "{schema}"."{table}" as f FROM "{schema}"."{table}" as f
WHERE f.geom is not null WHERE f.geom is not null
@ -232,7 +239,7 @@ class SurveyModel(BaseSurveyModel):
'geometry', ST_AsGeoJSON(geom)::jsonb, 'geometry', ST_AsGeoJSON(geom)::jsonb,
'properties', jsonb_build_object( 'properties', jsonb_build_object(
'popup', 'popup',
{description} || ' [' || '{model.category.group}' || '-' || '{model.category.minor_group_1}' || '] #' || inputs.id::varchar, {description} || ' [' || '{category.group}' || '-' || '{category.minor_group_1}' || '] #' || inputs.id::varchar,
'status', status 'status', status
) )
) AS feature ) AS feature
@ -241,16 +248,20 @@ class SurveyModel(BaseSurveyModel):
""" """
sql_query_for_geojson = sql_query_for_geojson_fast sql_query_for_geojson = sql_query_for_geojson_fast
category = registry.categories.loc[cls.category_name]
async with db.acquire(reuse=False) as conn: session: AsyncSession
async with db_session() as session:
query = sql_query_for_geojson.format( query = sql_query_for_geojson.format(
model=cls, model=cls,
schema=cls.__table_args__['schema'], category=category,
table=cls.__tablename__, schema=cls.metadata.schema,
description=adapt(cls.category.description), #table=cls.__tablename__,
# FIXME: should be __tablename__, but see SQLModel.__tablename__ which use lower(__name__)
table=cls.__name__,
description=adapt(category.description),
) )
result = await conn.scalar(query) result = await session.exec(text(query))
return result return result.first()[0]
def to_row(self): def to_row(self):
@ -1048,6 +1059,7 @@ class RawSurveyBaseModel(BaseSurveyModel, GeoPointMModel):
Abstract base class for category based raw survey point models Abstract base class for category based raw survey point models
""" """
#__abstract__ = True #__abstract__ = True
metadata: ClassVar[MetaData] = raw_survey
geom: Annotated[str, WKBElement] = Field(sa_type=Geometry('POINTZ', dimension=3, geom: Annotated[str, WKBElement] = Field(sa_type=Geometry('POINTZ', dimension=3,
srid=conf.geo.raw_survey.srid)) srid=conf.geo.raw_survey.srid))
status: str = Field(sa_type=String(1)) status: str = Field(sa_type=String(1))

View file

@ -145,14 +145,7 @@ class ModelRegistry:
__model_name=category.raw_survey_table_name, __model_name=category.raw_survey_table_name,
__cls_kwargs__={ __cls_kwargs__={
'table': True, 'table': True,
'metadata': raw_survey,
'__tablename__': category.raw_survey_table_name, '__tablename__': category.raw_survey_table_name,
# ## FIXME: RawSurveyBaseModel.category should be a Category, not category.name
# 'category_name': category.name,
# ## FIXME: Same for RawSurveyBaseModel.group
# 'group_name': category.category_group.name,
# 'viewable_role': category.viewable_role,
# 'store_name': raw_store_name,
}, },
**raw_survey_field_definitions **raw_survey_field_definitions
) )
@ -180,13 +173,7 @@ class ModelRegistry:
__model_name=category.table_name, __model_name=category.table_name,
__cls_kwargs__={ __cls_kwargs__={
'table': True, 'table': True,
'metadata': survey,
'__tablename__': category.table_name, '__tablename__': category.table_name,
# 'category_name': category.name,
# 'group_name': category.category_group.name,
# 'raw_store_name': raw_store_name,
# 'viewable_role': category.viewable_role,
# 'symbol': category.symbol,
}, },
**survey_field_definitions, **survey_field_definitions,
) )