feat(map): add persisted ghost pin sandbox
This commit is contained in:
@@ -10,12 +10,13 @@ from typing import Literal
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
MAP_VIEW_SCHEMA_VERSION: Literal["missioncore.map-view/v2"] = "missioncore.map-view/v2"
|
||||
MAP_VIEW_SCHEMA_VERSION: Literal["missioncore.map-view/v3"] = "missioncore.map-view/v3"
|
||||
MapInspectorSection = Literal[
|
||||
"base-terrain",
|
||||
"atmosphere-light",
|
||||
"buildings",
|
||||
"targets",
|
||||
"ghost-pins",
|
||||
"grid-lod",
|
||||
"camera",
|
||||
"tile-cache",
|
||||
@@ -96,7 +97,76 @@ class MapCacheIntent(StrictMapModel):
|
||||
no_overwrite: bool = True
|
||||
|
||||
|
||||
class MapViewContent(StrictMapModel):
|
||||
class MapGeoPoint(StrictMapModel):
|
||||
longitude: float = Field(ge=-180.0, le=180.0)
|
||||
latitude: float = Field(ge=-90.0, le=90.0)
|
||||
|
||||
|
||||
class MapGhostPin(MapGeoPoint):
|
||||
id: str = Field(
|
||||
min_length=7,
|
||||
max_length=86,
|
||||
pattern=r"^ghost-[A-Za-z0-9-]{1,80}$",
|
||||
)
|
||||
label: str = Field(min_length=1, max_length=12, pattern=r"^\d{1,12}$")
|
||||
heading_degrees: float = Field(ge=0.0, le=360.0)
|
||||
speed_meters_per_second: float = Field(ge=0.1, le=100.0)
|
||||
|
||||
|
||||
class MapGhostPinPresentation(StrictMapModel):
|
||||
color: str = Field(default="#6fb5fb", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
opacity: float = Field(default=0.9, ge=0.0, le=1.0)
|
||||
stem_height_meters: float = Field(default=1_500.0, ge=100.0, le=10_000.0)
|
||||
head_size_px: float = Field(default=6.0, ge=1.0, le=32.0)
|
||||
stem_width_px: float = Field(default=3.0, ge=0.25, le=12.0)
|
||||
outline_color: str = Field(default="#0c0d12", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
outline_opacity: float = Field(default=0.6, ge=0.0, le=1.0)
|
||||
outline_width_px: float = Field(default=1.0, ge=0.0, le=8.0)
|
||||
label_mode: Literal["subject_id", "attributes", "none"] = "attributes"
|
||||
label_font_weight: int = Field(default=600, ge=400, le=700, multiple_of=100)
|
||||
label_size_px: float = Field(default=13.0, ge=8.0, le=32.0)
|
||||
label_color: str = Field(default="#f5f5f5", pattern=r"^#[0-9A-Fa-f]{6}$")
|
||||
label_background_color: str = Field(
|
||||
default="#0c0d12",
|
||||
pattern=r"^#[0-9A-Fa-f]{6}$",
|
||||
)
|
||||
label_background_opacity: float = Field(default=0.86, ge=0.0, le=1.0)
|
||||
label_padding_x: float = Field(default=8.0, ge=0.0, le=32.0)
|
||||
label_padding_y: float = Field(default=5.0, ge=0.0, le=32.0)
|
||||
label_offset_x: float = Field(default=15.0, ge=-100.0, le=100.0)
|
||||
label_offset_y: float = Field(default=10.0, ge=-100.0, le=100.0)
|
||||
label_max_length: int = Field(default=48, ge=1, le=64)
|
||||
label_hide_camera_height_meters: float = Field(
|
||||
default=70_000.0,
|
||||
ge=1_000.0,
|
||||
le=500_000.0,
|
||||
)
|
||||
target_hide_camera_height_meters: float = Field(
|
||||
default=100_000.0,
|
||||
ge=1_000.0,
|
||||
le=500_000.0,
|
||||
)
|
||||
|
||||
|
||||
class MapGhostPinConfiguration(StrictMapModel):
|
||||
count: int = Field(default=12, ge=1, le=100)
|
||||
radius_meters: float = Field(default=5_000.0, ge=100.0, le=100_000.0)
|
||||
simulation_enabled: bool = False
|
||||
anchor: MapGeoPoint | None = None
|
||||
positions: list[MapGhostPin] = Field(default_factory=list, max_length=100)
|
||||
presentation: MapGhostPinPresentation = Field(
|
||||
default_factory=MapGhostPinPresentation,
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_unique_positions(self) -> MapGhostPinConfiguration:
|
||||
ids = [pin.id for pin in self.positions]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError("ghost pin ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class MapViewContentBase(StrictMapModel):
|
||||
camera: MapCamera | None = Field(
|
||||
default_factory=lambda: MapCamera(
|
||||
longitude=37.618423,
|
||||
@@ -122,19 +192,35 @@ class MapViewContent(StrictMapModel):
|
||||
layer_visibility: MapLayerVisibility = Field(default_factory=MapLayerVisibility)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_selection_section(self) -> MapViewContent:
|
||||
def validate_selection_section(self) -> MapViewContentBase:
|
||||
if self.selected_subject_id is None and "selection" in self.inspector_open_sections:
|
||||
raise ValueError("selection section requires a selected stable subject")
|
||||
return self
|
||||
|
||||
|
||||
class MapViewContent(MapViewContentBase):
|
||||
ghost_pins: MapGhostPinConfiguration = Field(
|
||||
default_factory=MapGhostPinConfiguration,
|
||||
)
|
||||
|
||||
|
||||
class MapViewPut(StrictMapModel):
|
||||
revision: int = Field(ge=0)
|
||||
view: MapViewContent
|
||||
|
||||
|
||||
class MapViewDocument(MapViewPut):
|
||||
schema_version: Literal["missioncore.map-view/v2"] = MAP_VIEW_SCHEMA_VERSION
|
||||
schema_version: Literal["missioncore.map-view/v3"] = MAP_VIEW_SCHEMA_VERSION
|
||||
|
||||
|
||||
class LegacyV2MapViewContent(MapViewContentBase):
|
||||
pass
|
||||
|
||||
|
||||
class LegacyV2MapViewDocument(StrictMapModel):
|
||||
schema_version: Literal["missioncore.map-view/v2"]
|
||||
revision: int = Field(ge=0)
|
||||
view: LegacyV2MapViewContent
|
||||
|
||||
|
||||
class LegacyMapVisualSettings(StrictMapModel):
|
||||
@@ -210,6 +296,16 @@ def upgrade_legacy_map_view(document: LegacyMapViewDocument) -> MapViewDocument:
|
||||
)
|
||||
|
||||
|
||||
def upgrade_v2_map_view(document: LegacyV2MapViewDocument) -> MapViewDocument:
|
||||
return MapViewDocument(
|
||||
revision=document.revision,
|
||||
view=MapViewContent(
|
||||
**document.view.model_dump(mode="python"),
|
||||
ghost_pins=MapGhostPinConfiguration(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MapViewStore:
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = root.expanduser().resolve()
|
||||
@@ -253,6 +349,13 @@ class MapViewStore:
|
||||
and payload.get("schema_version") == "missioncore.map-view/v1"
|
||||
):
|
||||
return upgrade_legacy_map_view(LegacyMapViewDocument.model_validate(payload))
|
||||
if (
|
||||
isinstance(payload, dict)
|
||||
and payload.get("schema_version") == "missioncore.map-view/v2"
|
||||
):
|
||||
return upgrade_v2_map_view(
|
||||
LegacyV2MapViewDocument.model_validate(payload)
|
||||
)
|
||||
return MapViewDocument.model_validate(payload)
|
||||
except (OSError, ValueError, TypeError) as exc:
|
||||
raise RuntimeError("map view is corrupt") from exc
|
||||
|
||||
Reference in New Issue
Block a user