Restructure api

Fixes in geo api, registry
Cleanups
This commit is contained in:
phil 2024-02-27 05:05:33 +05:30
parent c84dd61f6a
commit 8c299f0041
17 changed files with 212 additions and 162 deletions

View file

@ -1 +1 @@
__version__: str = '2023.4.dev34+g5dacc90.d20240212' __version__: str = '2023.4.dev37+gb00bf1f.d20240226'

View file

@ -1,6 +1,6 @@
import logging import logging
from fastapi import Depends, FastAPI, HTTPException, status, responses from fastapi import Depends, APIRouter, HTTPException, status, responses
from gisaf.models.admin import AdminBasket, BasketNameOnly from gisaf.models.admin import AdminBasket, BasketNameOnly
from gisaf.models.authentication import User from gisaf.models.authentication import User
@ -9,8 +9,10 @@ from gisaf.admin import manager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api = FastAPI( api = APIRouter(
default_response_class=responses.ORJSONResponse, tags=["admin"],
# dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
) )
@api.get('/basket') @api.get('/basket')

View file

@ -2,7 +2,7 @@ import logging
from pathlib import Path from pathlib import Path
from json import dumps from json import dumps
from fastapi import Depends, FastAPI, HTTPException, status, responses from fastapi import Depends, APIRouter, HTTPException, status, responses
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlmodel import select from sqlmodel import select
import pandas as pd import pandas as pd
@ -34,8 +34,10 @@ Gisaf is free, open source software for geomatics and GIS:
<a href="http://redmine.auroville.org.in/projects/gisaf">Gisaf</a>. <a href="http://redmine.auroville.org.in/projects/gisaf">Gisaf</a>.
''' '''
api = FastAPI( api = APIRouter(
default_response_class=responses.ORJSONResponse, tags=["dashboard"],
# dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
) )
@api.get('/groups') @api.get('/groups')

View file

@ -7,7 +7,7 @@ import logging
from typing import Annotated from typing import Annotated
from asyncio import CancelledError from asyncio import CancelledError
from fastapi import (Depends, FastAPI, HTTPException, Response, Header, from fastapi import (Depends, APIRouter, HTTPException, Response, Header,
WebSocket, WebSocketDisconnect, WebSocket, WebSocketDisconnect,
status, responses) status, responses)
@ -20,8 +20,10 @@ from gisaf.security import get_current_active_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api = FastAPI( api = APIRouter(
default_response_class=responses.ORJSONResponse, tags=["geoapi"],
# dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
) )
class ConnectionManager: class ConnectionManager:
@ -103,11 +105,10 @@ async def get_geojson(store_name,
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 If_None_Match == ttag: if ttag and If_None_Match == ttag:
return status.HTTP_304_NOT_MODIFIED raise HTTPException(status.HTTP_304_NOT_MODIFIED)
if hasattr(model, 'get_geojson'): if hasattr(model, 'get_geojson'):
geojson = await model.get_geojson(simplify_tolerance=simplify, geojson = await model.get_geojson(simplify_tolerance=simplify,
preserve_topology=preserveTopology, preserve_topology=preserveTopology)
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)

View file

@ -2,7 +2,7 @@ import logging
from datetime import timedelta from datetime import timedelta
from typing import Annotated from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status, responses from fastapi import Depends, APIRouter, HTTPException, status, responses
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import select from sqlmodel import select
@ -12,7 +12,6 @@ from gisaf.models.authentication import (
Role, RoleRead, Role, RoleRead,
) )
from gisaf.models.category import Category, CategoryRead from gisaf.models.category import Category, CategoryRead
from gisaf.models.geo_models_base import LineWorkSurveyModel
from gisaf.models.to_migrate import DataProvider from gisaf.models.to_migrate import DataProvider
from gisaf.models.survey import Equipment, SurveyMeta, Surveyor from gisaf.models.survey import Equipment, SurveyMeta, Surveyor
from gisaf.config import Survey, conf from gisaf.config import Survey, conf
@ -31,19 +30,16 @@ from gisaf.models.to_migrate import (
FeatureInfo, InfoItem, Attachment, InfoCategory FeatureInfo, InfoItem, Attachment, InfoCategory
) )
from gisaf.live_utils import get_live_feature_info from gisaf.live_utils import get_live_feature_info
from gisaf.api.dashboard import api as dashboard_api
from gisaf.api.map import api as map_api
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api = FastAPI( api = APIRouter(
default_response_class=responses.ORJSONResponse, tags=["api"],
# dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
) )
#api.add_middleware(SessionMiddleware, secret_key=conf.crypto.secret) #api.add_middleware(SessionMiddleware, secret_key=conf.crypto.secret)
api.mount('/dashboard', dashboard_api)
api.mount('/map', map_api)
@api.get('/bootstrap') @api.get('/bootstrap')
async def bootstrap( async def bootstrap(

View file

@ -2,21 +2,26 @@ from collections import OrderedDict, defaultdict
import logging import logging
from json import dumps from json import dumps
from fastapi import FastAPI, Request, HTTPException, status, responses from fastapi import (APIRouter, Request, HTTPException,
Response, status)
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import NoResultFound
from sqlmodel import select from sqlmodel import select
from gisaf.models.map_bases import BaseMap, BaseMapLayer, BaseStyle, MapInitData from gisaf.models.map_bases import (BaseMap, BaseMapLayer, BaseMapWithStores,
BaseStyle, MapInitData, MaplibreStyle)
from gisaf.models.misc import Qml
from gisaf.registry import registry from gisaf.registry import registry
from gisaf.database import db_session from gisaf.database import db_session, fastapi_db_session
from gisaf.database import fastapi_db_session
from gisaf.tiles import registry as tiles_registry from gisaf.tiles import registry as tiles_registry
from gisaf.redis_tools import store as redis_store
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api = FastAPI( api = APIRouter(
default_response_class=responses.ORJSONResponse, tags=["map"],
# dependencies=[Depends(get_token_header)],
# responses={404: {"description": "Not found"}},
) )
async def get_base_styles(): async def get_base_styles():
@ -31,23 +36,21 @@ async def get_base_styles():
# base_styles.extend(tiles_registry.mbtiles.values()) # base_styles.extend(tiles_registry.mbtiles.values())
return [BaseStyle(name=bs) for bs in base_styles] # type: ignore return [BaseStyle(name=bs) for bs in base_styles] # type: ignore
async def get_base_maps() -> list[BaseMap]: async def get_base_maps() -> list[BaseMapWithStores]:
async with db_session() as session: async with db_session() as session:
query1 = select(BaseMap).options(selectinload(BaseMap.layers)) # type: ignore 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 # 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)) # type: ignore
data2 = await session.exec(query2) data2 = await session.exec(query2)
base_map_layers = data2.all() base_map_layers = data2.all()
bms = defaultdict(list) bms: dict[str, list] = defaultdict(list)
for bml in base_map_layers: for bml in base_map_layers:
breakpoint()
if bml.store: if bml.store:
bms[base_map_dict[bml.base_map_id]].append(name=bml.store) bms[base_map_dict[bml.base_map_id]].append(bml.store)
return [ return [
BaseMap(name=bm, stores=bmls) BaseMapWithStores(name=bm, stores=bmls)
for bm, bmls in OrderedDict(sorted(bms.items())).items() for bm, bmls in OrderedDict(sorted(bms.items())).items()
] ]
@ -84,3 +87,29 @@ async def get_base_style(request: Request, name: str,
else: else:
style = base_style.style # type: ignore style = base_style.style # type: ignore
return BaseStyle(name=name, style=style) # type: ignore return BaseStyle(name=name, style=style) # type: ignore
@api.get('/layer_style/{store}')
async def get_layer_style(request: Request, store: str,
response: Response,
) -> MaplibreStyle | None:
store_record = registry.stores.loc[store]
if store_record.is_live:
## No ttag for live layers' style (could be added?)
## Get layer_defs from live redis and give symbol
return await redis_store.get_maplibre_style(store)
## Set the etag based on the last modification of the model's style.
#if store in info.schema.app['registry'].geom_auto:
if store_record.custom:
## The style is in Qml
ttag_channel = 'gisaf_map.qml'
else:
## The style is in Category
ttag_channel = 'gisaf_survey.category'
## Check if the request was etagged:
ttag = await redis_store.get_ttag(ttag_channel)
if ttag and request.headers.get('If-None-Match') == ttag:
raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED)
# request.not_modified = True
# return MaplibreStyle()
response.headers['ETag'] = ttag
return await store_record.model.get_maplibre_style()

View file

@ -3,15 +3,17 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI, responses from fastapi import FastAPI, responses
from gisaf.api.v2 import api
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.tiles import registry as map_tile_registry
from gisaf.live import setup_live from gisaf.live import setup_live
from gisaf.admin import manager as admin_manager from gisaf.admin import manager as admin_manager
from gisaf.api.main import api
from gisaf.api.geoapi import api as geoapi
from gisaf.api.admin import api as admin_api from gisaf.api.admin import api as admin_api
from gisaf.api.dashboard import api as dashboard_api
from gisaf.api.map import api as map_api
logging.basicConfig(level=conf.gisaf.debugLevel) logging.basicConfig(level=conf.gisaf.debugLevel)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,6 +39,8 @@ app = FastAPI(
default_response_class=responses.ORJSONResponse, default_response_class=responses.ORJSONResponse,
) )
app.mount('/v2', api) app.include_router(api, prefix="/api")
app.mount('/gj', geoapi) app.include_router(geoapi, prefix="/api/gj")
app.mount('/admin', admin_api) app.include_router(admin_api, prefix="/api/admin")
app.include_router(dashboard_api, prefix="/api/dashboard")
app.include_router(map_api, prefix='/api/map')

View file

@ -1,3 +1,4 @@
from re import A
import geopandas as gpd import geopandas as gpd
from shapely import from_wkb from shapely import from_wkb
from json import dumps from json import dumps
@ -5,7 +6,8 @@ from json import dumps
from sqlmodel import SQLModel from sqlmodel import SQLModel
from gisaf.config import conf from gisaf.config import conf
from gisaf.models.to_migrate import MapboxPaint, MapboxLayout, FeatureInfo from gisaf.models.map_bases import MaplibreStyle
from gisaf.models.to_migrate import FeatureInfo
class BaseStore(SQLModel): class BaseStore(SQLModel):
@ -13,8 +15,8 @@ class BaseStore(SQLModel):
name: str = '<Unnamed store>' name: str = '<Unnamed store>'
description: str = '<Description>' description: str = '<Description>'
icon: str | None = None icon: str | None = None
mapbox_paint: MapboxPaint | None = None mapbox_paint: dict[str, dict] | None = None
mapbox_layout: MapboxLayout | None = None mapbox_layout: dict[str, dict] | None = None
attribution: str | None = None attribution: str | None = None
symbol: str = '\ue32b' symbol: str = '\ue32b'
base_gis_type: str = 'Point' base_gis_type: str = 'Point'
@ -117,15 +119,12 @@ class BaseStore(SQLModel):
raise NotImplementedError('Subclasses of BaseStore must implement get_item_params()') raise NotImplementedError('Subclasses of BaseStore must implement get_item_params()')
@classmethod @classmethod
async def get_mapbox_style(cls): async def get_maplibre_style(cls) -> MaplibreStyle:
""" """
Get the mapbox style (paint, layout, attribution...) Get the mapbox style (paint, layout, attribution...)
""" """
style = {} return MaplibreStyle(
if cls.mapbox_paint is not None: paint=cls.mapbox_paint,
style['paint'] = dumps(cls.mapbox_paint) layout=cls.mapbox_layout,
if cls.mapbox_layout is not None: attribution=cls.attribution
style['layout'] = dumps(cls.mapbox_layout) )
if cls.attribution is not None:
style['attribution'] = cls.attribution
return style

View file

@ -27,7 +27,7 @@ class UserBase(SQLModel):
class User(UserBase, table=True): class User(UserBase, table=True):
__table_args__ = gisaf_admin.table_args __table_args__ = gisaf_admin.table_args
id: int | None = Field(default=None, primary_key=True) id: str | None = Field(default=None, primary_key=True)
roles: list["Role"] = Relationship(back_populates="users", roles: list["Role"] = Relationship(back_populates="users",
link_model=UserRoleLink) link_model=UserRoleLink)
password: str | None = None password: str | None = None

View file

@ -61,8 +61,8 @@ class CategoryBase(BaseModel):
style: str | None = Field(sa_type=TEXT) style: str | None = Field(sa_type=TEXT)
symbol: str | None = Field(sa_type=String(1)) # type: ignore symbol: str | None = Field(sa_type=String(1)) # type: ignore
mapbox_type_custom: str | None = Field(sa_type=String(12)) # type: ignore mapbox_type_custom: str | None = Field(sa_type=String(12)) # type: ignore
mapbox_paint: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore mapbox_paint: dict[str, dict | list | float | int | str] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
mapbox_layout: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore mapbox_layout: dict[str, dict | list | float | int | str] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
viewable_role: str | None viewable_role: str | None
extra: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore extra: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore

View file

@ -14,20 +14,20 @@ import shapely # type: ignore
import pyproj import pyproj
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import select, Field, Relationship from sqlmodel import select, Field
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy import BigInteger, MetaData, String, func, and_, text from sqlalchemy import BigInteger, String, func, and_, text
from sqlalchemy.sql import sqltypes from sqlalchemy.sql import sqltypes
from sqlalchemy.orm import declared_attr from sqlalchemy.orm import declared_attr
from psycopg2.extensions import adapt from psycopg2.extensions import adapt
from geoalchemy2.shape import from_shape from geoalchemy2.shape import from_shape
from geoalchemy2.types import Geometry, WKBElement from geoalchemy2.types import Geometry, WKBElement
from shapely import wkb, from_wkb from shapely import wkb, from_wkb
from shapely.geometry import mapping from shapely.geometry import mapping # type: ignore
from shapely.ops import transform # type: ignore from shapely.ops import transform # type: ignore
from shapefile import ( from shapefile import ( # type: ignore
Writer as ShapeFileWriter, # type: ignore Writer as ShapeFileWriter,
POINT, POINTZ, POINT, POINTZ,
POLYLINE, POLYLINEZ, POLYLINE, POLYLINEZ,
POLYGON, POLYGONZ, POLYGON, POLYGONZ,
@ -35,8 +35,9 @@ from shapefile import (
from gisaf.database import db_session from gisaf.database import db_session
from gisaf.config import conf from gisaf.config import conf
from gisaf.models.map_bases import MaplibreStyle
from gisaf.models.models_base import Model from gisaf.models.models_base import Model
from gisaf.models.metadata import gisaf_survey, gisaf_admin, survey from gisaf.models.metadata import gisaf_survey, gisaf_admin, survey, raw_survey
from gisaf.models.misc import Qml from gisaf.models.misc import Qml
from gisaf.models.category import Category from gisaf.models.category import Category
from gisaf.models.to_migrate import InfoItem from gisaf.models.to_migrate import InfoItem
@ -157,6 +158,7 @@ class SurveyModel(BaseSurveyModel):
# 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
category_name: ClassVar[str]
filtered_columns_on_map: ClassVar[list[str]] = [ filtered_columns_on_map: ClassVar[list[str]] = [
'equip_id', 'equip_id',
@ -219,9 +221,8 @@ class SurveyModel(BaseSurveyModel):
@classmethod @classmethod
async def get_geojson(cls, async def get_geojson(cls,
registry=None, simplify_tolerance=0, preserve_topology=False): simplify_tolerance=0, preserve_topology=False):
if registry is None: from gisaf.registry import registry
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
@ -592,36 +593,35 @@ class GeoModelNoStatus(Model):
return zip_file return zip_file
@classmethod @classmethod
async def get_mapbox_style(cls): async def get_maplibre_style(cls) -> MaplibreStyle | None:
""" """
Get the mapbox style (paint, layout, attribution...) Get the mapbox style (paint, layout, attribution...)
""" """
## If the model is from survey, it should have a category, which has a style ## If the model is from survey, it should have a category, which has a style
## Get from database ## Get from database
style = {} style: MaplibreStyle | None
if hasattr(cls, 'category'): async with db_session() as session:
category = await Category.get_item_by_pk(pk=cls.category.name) if hasattr(cls, 'category'):
if category: category = await session.get(Category, cls.get_store_name())
if category['mapbox_paint'] is not None: if category:
style['paint'] = category['mapbox_paint'] style = MaplibreStyle(
if category['mapbox_layout'] is not None: layout=category.mapbox_layout,
style['layout'] = category['mapbox_layout'] paint=category.mapbox_paint,
)
else: else:
category = None style = None
else:
qml = await Qml.get_item_by_pk(pk=cls.__name__) qml = await session.get(Qml, cls.__name__)
if qml: if qml:
if qml['mapbox_paint']: style = MaplibreStyle(
style['paint'] = qml['mapbox_paint'] layout=qml.mapbox_layout,
if qml['mapbox_layout']: paint=qml.mapbox_paint,
style['layout'] = qml['mapbox_layout'] attribution=qml.attr,
)
if cls.attribution is not None: else:
style['attribution'] = cls.attribution style = None
return style return style
@classmethod @classmethod
async def get_features_attrs(cls, simplify_tolerance): async def get_features_attrs(cls, simplify_tolerance):
""" """
@ -1094,10 +1094,13 @@ class RawSurveyBaseModel(BaseSurveyModel, GeoPointModelNoStatus):
""" """
Abstract base class for category based raw survey point models Abstract base class for category based raw survey point models
""" """
# metadata: ClassVar[MetaData] = raw_survey __table_args__ = raw_survey.table_args
geom: Annotated[str, WKBElement] = Field(sa_type=Geometry('POINTZ', dimension=3, geom: Annotated[str, WKBElement] = Field(
srid=conf.geo.raw_survey.srid)) sa_type=Geometry('POINTZ',
status: str = Field(sa_type=String(1)) dimension=3,
srid=conf.geo.raw_survey.srid),
) # type: ignore
status: str = Field(sa_type=String(1)) # type: ignore
## store_name is set in category_models_maker.make_category_models ## store_name is set in category_models_maker.make_category_models
store_name: ClassVar[str | None] = None store_name: ClassVar[str | None] = None

View file

@ -28,30 +28,6 @@ class BaseStyle(Model, table=True):
return f'<models.BaseStyle {self.name:s}>' return f'<models.BaseStyle {self.name:s}>'
class BaseMap(Model, table=True):
__table_args__ = gisaf_map.table_args
__tablename__: str = 'base_map' # type: ignore
class Admin:
menu = 'Other'
id: int | None = Field(primary_key=True, default=None)
name: str
layers: list['BaseMapLayer'] = Relationship(back_populates='base_map')
def __repr__(self) -> str:
return f'<models.BaseMap {self.name:s}>'
def __str__(self) -> str:
return self.name
@classmethod
def selectinload(cls) -> list[list['BaseMapLayer']]:
return [
cls.layers
]
class BaseMapLayer(Model, table=True): class BaseMapLayer(Model, table=True):
__table_args__ = gisaf_map.table_args __table_args__ = gisaf_map.table_args
__tablename__: str = 'base_map_layer' # type: ignore __tablename__: str = 'base_map_layer' # type: ignore
@ -62,7 +38,7 @@ class BaseMapLayer(Model, table=True):
id: int | None = Field(primary_key=True, default=None) id: int | None = Field(primary_key=True, default=None)
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)) # type: ignore store: str = Field(sa_type=String(100)) # type: ignore
@classmethod @classmethod
@ -78,8 +54,46 @@ class BaseMapLayer(Model, table=True):
return f"{self.store or '':s}" return f"{self.store or '':s}"
class BaseMap(Model, table=True):
__table_args__ = gisaf_map.table_args
__tablename__: str = 'base_map' # type: ignore
class Admin:
menu = 'Other'
id: int | None = Field(primary_key=True, default=None)
name: str
# layers: list['BaseMapLayer'] = Relationship(
# back_populates='base_map',
# # link_model=BaseMapLayer
# )
def __repr__(self) -> str:
return f'<models.BaseMap {self.name:s}>'
def __str__(self) -> str:
return self.name
# @classmethod
# def selectinload(cls) -> list[list[BaseMapLayer]]:
# return [
# cls.layers
# ]
class BaseMapWithStores(BaseModel):
name: str
stores: list[str]
class MapInitData(BaseModel): class MapInitData(BaseModel):
baseStyles: list[BaseStyle] = [] baseStyles: list[BaseStyle] = []
baseMaps: list[BaseMap] = [] baseMaps: list[BaseMapWithStores] = []
groups: list[CategoryGroup] = [] groups: list[CategoryGroup] = []
stores: list[Store] = [] stores: list[Store] = []
class MaplibreStyle(BaseModel):
paint: dict[str, dict | list | float | int | str] | None = None
layout: dict[str, dict | list | float | int | str] | None = None
attribution: str | None = None

View file

@ -14,11 +14,11 @@ class NotADataframeError(Exception):
pass pass
class Qml(Model): class Qml(Model, table=True):
""" """
Model for storing qml (QGis style) Model for storing qml (QGis style)
""" """
model_config = ConfigDict(protected_namespaces=()) model_config = ConfigDict(protected_namespaces=()) # type: ignore
__table_args__ = gisaf_map.table_args __table_args__ = gisaf_map.table_args
class Admin: class Admin:
@ -26,11 +26,11 @@ class Qml(Model):
flask_admin_model_view = 'QmlModelView' flask_admin_model_view = 'QmlModelView'
model_name: str | None = Field(default=None, primary_key=True) model_name: str | None = Field(default=None, primary_key=True)
qml: str qml: str | None = None
attr: str attr: str | None = None
style: str style: str | None = None
mapbox_paint: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) mapbox_paint: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
mapbox_layout: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) mapbox_layout: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
def __repr__(self): def __repr__(self):
return '<models.Qml {self.model_name:s}>'.format(self=self) return '<models.Qml {self.model_name:s}>'.format(self=self)

View file

@ -1,10 +1,4 @@
from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
from gisaf.models.geo_models_base import GeoModel, RawSurveyBaseModel, GeoPointSurveyModel
class MapLibreStyle(BaseModel):
...
class StoreNameOnly(BaseModel): class StoreNameOnly(BaseModel):
@ -12,6 +6,7 @@ class StoreNameOnly(BaseModel):
class Store(StoreNameOnly): class Store(StoreNameOnly):
category: str | None = None
auto_import: bool auto_import: bool
# base_gis_type: str # base_gis_type: str
count: int | None = None count: int | None = None
@ -39,7 +34,6 @@ class Store(StoreNameOnly):
#raw_model: GeoPointSurveyModel #raw_model: GeoPointSurveyModel
#raw_model_store_name: str #raw_model_store_name: str
status: str status: str
store: str
style: str | None style: str | None
symbol: str | None symbol: str | None
title: str title: str

View file

@ -73,11 +73,3 @@ class FeatureInfo(BaseModel):
files: list[Attachment] = [] files: list[Attachment] = []
images: list[Attachment] = [] images: list[Attachment] = []
externalRecordUrl: str | None = None externalRecordUrl: str | None = None
class MapboxPaint(BaseModel):
...
class MapboxLayout(BaseModel):
...

View file

@ -16,6 +16,7 @@ from redis import asyncio as aioredis
from gisaf.config import conf from gisaf.config import conf
# from gisaf.models.live import LiveModel # from gisaf.models.live import LiveModel
from gisaf.models.map_bases import MaplibreStyle
from gisaf.utils import (SHAPELY_TYPE_TO_MAPBOX_TYPE, DEFAULT_MAPBOX_LAYOUT, from gisaf.utils import (SHAPELY_TYPE_TO_MAPBOX_TYPE, DEFAULT_MAPBOX_LAYOUT,
DEFAULT_MAPBOX_PAINT, gisTypeSymbolMap) DEFAULT_MAPBOX_PAINT, gisTypeSymbolMap)
from gisaf.registry import registry from gisaf.registry import registry
@ -281,18 +282,14 @@ class Store:
registry.geom_live_defs[model_info['store']] = model_info registry.geom_live_defs[model_info['store']] = model_info
registry.update_live_layers() registry.update_live_layers()
async def get_mapbox_style(self, store_name): async def get_maplibre_style(self, store_name) -> MaplibreStyle:
""" """
Get the http headers (mapbox style) from the store name (layer_def) Get the http headers (mapbox style) from the store name (layer_def)
""" """
paint = await self.redis.get(self.get_mapbox_paint_channel(store_name)) return MaplibreStyle(
layout = await self.redis.get(self.get_mapbox_layout_channel(store_name)) paint=(await self.redis.get(self.get_mapbox_paint_channel(store_name))).decode(),
style = {} layout=(await self.redis.get(self.get_mapbox_layout_channel(store_name))).decode(),
if paint is not None: )
style['paint'] = paint.decode()
if layout is not None:
style['layout'] = layout.decode()
return style
async def get_layer_as_json(self, store_name): async def get_layer_as_json(self, store_name):
""" """

View file

@ -12,7 +12,7 @@ from pydantic import create_model
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import selectinload, joinedload from sqlalchemy.orm import selectinload, joinedload
from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import NoResultFound
from sqlmodel import SQLModel, select, inspect, Relationship from sqlmodel import SQLModel, col, select, inspect, Relationship
import pandas as pd import pandas as pd
import numpy as np import numpy as np
@ -336,7 +336,7 @@ class ModelRegistry:
# for category in categories # for category in categories
# if self.raw_survey_models.get(category.table_name)} # if self.raw_survey_models.get(category.table_name)}
async def get_model_id_params(self, model: SQLModel, id: int) -> FeatureInfo: async def get_model_id_params(self, model: SQLModel, id: int) -> FeatureInfo | None:
""" """
Return the parameters for this item (table name, id), displayed in info pane Return the parameters for this item (table name, id), displayed in info pane
""" """
@ -389,8 +389,9 @@ class ModelRegistry:
""" """
## Utility functions used with apply method (dataframes) ## Utility functions used with apply method (dataframes)
def fill_columns_from_custom_models(row) -> tuple[str, str, str, str, str, str, str]: def fill_columns_from_custom_models(row) -> tuple[str, str, str, str, str, str, str]:
# model:
return ( return (
row.model.__name__, row.model.get_store_name(),
row.model.__name__, row.model.__name__,
row.model.description, row.model.description,
row.model.__table__.schema, row.model.__table__.schema,
@ -429,7 +430,11 @@ class ModelRegistry:
self.categories = self.categories.merge(df_raw_models, left_on='store', right_index=True) self.categories = self.categories.merge(df_raw_models, left_on='store', right_index=True)
self.categories['custom'] = False self.categories['custom'] = False
self.categories['is_db'] = True self.categories['is_db'] = True
self.categories.sort_index(inplace=True) self.categories.reset_index(inplace=True)
self.categories.rename(columns={'name': 'category'}, inplace=True)
self.categories.set_index('store', inplace=True)
self.categories.sort_values('category')
# self.categories.sort_index(inplace=True)
# self.categories['name_letter'] = self.categories.index.str.slice(0, 1) # self.categories['name_letter'] = self.categories.index.str.slice(0, 1)
# self.categories['name_number'] = self.categories.index.str.slice(1).astype('int64') # self.categories['name_number'] = self.categories.index.str.slice(1).astype('int64')
# self.categories.sort_values(['name_letter', 'name_number'], inplace=True) # self.categories.sort_values(['name_letter', 'name_number'], inplace=True)
@ -467,6 +472,7 @@ class ModelRegistry:
self.custom_models['custom'] = True self.custom_models['custom'] = True
self.custom_models['is_db'] = True self.custom_models['is_db'] = True
self.custom_models['raw_model_store_name'] = '' self.custom_models['raw_model_store_name'] = ''
self.custom_models['category'] = ''
self.custom_models['in_menu'] = self.custom_models.apply( self.custom_models['in_menu'] = self.custom_models.apply(
lambda row: getattr(row.model, 'in_menu', True), lambda row: getattr(row.model, 'in_menu', True),
axis=1 axis=1
@ -512,6 +518,7 @@ class ModelRegistry:
self.custom_stores = self.custom_stores.loc[self.custom_stores.in_menu] self.custom_stores = self.custom_stores.loc[self.custom_stores.in_menu]
self.custom_stores['auto_import'] = False self.custom_stores['auto_import'] = False
self.custom_stores['is_line_work'] = False self.custom_stores['is_line_work'] = False
self.custom_stores['category'] = ''
if len(self.custom_stores) > 0: if len(self.custom_stores) > 0:
self.custom_stores['long_name'],\ self.custom_stores['long_name'],\
@ -525,13 +532,18 @@ class ModelRegistry:
## Combine Misc (custom) and survey (auto) stores ## Combine Misc (custom) and survey (auto) stores
## Retain only one status per category (defaultStatus, 'E'/existing by default) ## Retain only one status per category (defaultStatus, 'E'/existing by default)
self.stores = pd.concat([ self.stores = pd.concat([
self.categories[self.categories.status==conf.map.defaultStatus[0]].reset_index().set_index('store').sort_values('title'), self.categories[self.categories.status==conf.map.defaultStatus[0]].sort_values('title'),
self.custom_models, self.custom_models,
self.custom_stores self.custom_stores
])#.drop(columns=['store_name']) ])#.drop(columns=['store_name'])
self.stores.drop(columns='name', inplace=True)
self.stores.index.name = 'name'
self.stores['in_menu'] = self.stores['in_menu'].astype(bool) self.stores['in_menu'] = self.stores['in_menu'].astype(bool)
self.stores['status'].fillna('E', inplace=True) self.stores['status'].fillna('E', inplace=True)
self.categories.reset_index(inplace=True)
self.categories.set_index('category', inplace=True)
## Set in the stores dataframe some useful properties, from the model class ## Set in the stores dataframe some useful properties, from the model class
## Maybe at some point it makes sense to get away from class-based definitions ## Maybe at some point it makes sense to get away from class-based definitions
def fill_columns_from_model(row): def fill_columns_from_model(row):
@ -576,18 +588,20 @@ class ModelRegistry:
## Add Misc and Live ## Add Misc and Live
self.primary_groups.append(CategoryGroup( self.primary_groups.append(CategoryGroup(
name='Misc', name='Misc',
long_name='Misc',
major=True, major=True,
long_name='Misc and old layers (not coming from our survey; ' # description]='Misc and old layers (not coming from our survey; '
'they will be organized, ' # 'they will be organized, '
'eventually as the surveys get more complete)', # 'eventually as the surveys get more complete)',
categories=[], categories=[],
)) ))
self.primary_groups.append(CategoryGroup( self.primary_groups.append(CategoryGroup(
name='Live', name='Live',
long_name='Live',
major=True, major=True,
long_name='Layers from data processing, sensors, etc, ' # long_name='Layers from data processing, sensors, etc, '
'and are updated automatically', # 'and are updated automatically',
categories=[], categories=[],
)) ))
@ -671,7 +685,7 @@ class ModelRegistry:
df_live['auto_import'] = False df_live['auto_import'] = False
df_live['base_gis_type'] = df_live['gis_type'] df_live['base_gis_type'] = df_live['gis_type']
df_live['custom'] = False df_live['custom'] = False
df_live['group'] = '' df_live['group'] = 'Live'
df_live['in_menu'] = True df_live['in_menu'] = True
df_live['is_db'] = False df_live['is_db'] = False
df_live['is_line_work'] = False df_live['is_line_work'] = False
@ -681,8 +695,11 @@ class ModelRegistry:
df_live['minor_group_2'] = '' df_live['minor_group_2'] = ''
df_live['status'] = 'E' df_live['status'] = 'E'
df_live['style'] = None df_live['style'] = None
df_live['title'] = df_live['name'] df_live['category'] = ''
df_live.rename(columns={'name': 'title'}, inplace=True)
df_live.index.name = 'name'
registry.stores = pd.concat([registry.stores, df_live]) registry.stores = pd.concat([registry.stores, df_live])
df_live.index.name = 'store'
for store, model_info in self.geom_live_defs.items(): for store, model_info in self.geom_live_defs.items():
## Add provided live layers in the stores df ## Add provided live layers in the stores df
# Create the pydantic model # Create the pydantic model