114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.time import to_iso8601_utc
|
|
from app.core.websocket.broadcaster import broadcaster
|
|
from app.models.earth_interactable import EarthInteractable
|
|
from app.services.earth_layer_cache import EARTH_LAYER_CACHE_PREFIX, earth_layer_cache
|
|
|
|
INTERACTABLE_ENTITY = "interactable"
|
|
INTERACTABLE_LAYER = "interactables"
|
|
|
|
|
|
def normalize_interactable_id(value: str | None = None) -> str:
|
|
raw = str(value or "").strip()
|
|
return raw or f"interactable-{uuid4().hex}"
|
|
|
|
|
|
def serialize_interactable(record: EarthInteractable) -> dict[str, Any]:
|
|
return {
|
|
"id": record.id,
|
|
"layer": record.layer,
|
|
"kind": record.kind,
|
|
"label": record.label,
|
|
"description": record.description,
|
|
"latitude": record.latitude,
|
|
"longitude": record.longitude,
|
|
"altitude": record.altitude,
|
|
"revision": record.revision,
|
|
"properties": record.properties or {},
|
|
"is_deleted": bool(record.is_deleted),
|
|
"created_at": to_iso8601_utc(record.created_at),
|
|
"updated_at": to_iso8601_utc(record.updated_at),
|
|
"deleted_at": to_iso8601_utc(record.deleted_at),
|
|
}
|
|
|
|
|
|
def interactables_to_geojson(items: list[EarthInteractable]) -> dict[str, Any]:
|
|
return {
|
|
"type": "FeatureCollection",
|
|
"features": [
|
|
{
|
|
"type": "Feature",
|
|
"id": item.id,
|
|
"geometry": {
|
|
"type": "Point",
|
|
"coordinates": [item.longitude, item.latitude],
|
|
},
|
|
"properties": serialize_interactable(item),
|
|
}
|
|
for item in items
|
|
if not item.is_deleted
|
|
],
|
|
}
|
|
|
|
|
|
def invalidate_interactable_cache(layer: str | None = None) -> int:
|
|
layer_key = str(layer or "*").strip() or "*"
|
|
deleted = earth_layer_cache.delete_pattern(
|
|
f"{EARTH_LAYER_CACHE_PREFIX}:interactables:layer:{layer_key}*"
|
|
)
|
|
if layer_key != "all":
|
|
deleted += earth_layer_cache.delete_pattern(
|
|
f"{EARTH_LAYER_CACHE_PREFIX}:interactables:layer:all*"
|
|
)
|
|
return deleted
|
|
|
|
|
|
def build_interactable_event(
|
|
*,
|
|
action: str,
|
|
record: EarthInteractable,
|
|
include_item: bool = True,
|
|
) -> dict[str, Any]:
|
|
item = serialize_interactable(record)
|
|
return {
|
|
"entity": INTERACTABLE_ENTITY,
|
|
"action": action,
|
|
"layer": record.layer,
|
|
"layers": [INTERACTABLE_LAYER],
|
|
"ids": [record.id],
|
|
"revision": record.revision,
|
|
"changed_at": item["deleted_at"] or item["updated_at"] or to_iso8601_utc(datetime.now(UTC)),
|
|
"item": item if include_item else None,
|
|
"source": "earth_interactables",
|
|
}
|
|
|
|
|
|
async def publish_interactable_event(action: str, record: EarthInteractable, *, include_item: bool = True) -> None:
|
|
await broadcaster.broadcast_earth_update(
|
|
build_interactable_event(action=action, record=record, include_item=include_item)
|
|
)
|
|
|
|
|
|
async def list_interactables(
|
|
db: AsyncSession,
|
|
*,
|
|
layer: str | None = None,
|
|
include_deleted: bool = False,
|
|
) -> list[EarthInteractable]:
|
|
stmt = select(EarthInteractable)
|
|
if layer:
|
|
stmt = stmt.where(EarthInteractable.layer == layer)
|
|
if not include_deleted:
|
|
stmt = stmt.where(EarthInteractable.is_deleted.is_(False))
|
|
stmt = stmt.order_by(EarthInteractable.updated_at.desc(), EarthInteractable.id.asc())
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|