Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
188 lines
7.3 KiB
Python
188 lines
7.3 KiB
Python
"""Registry of target schemas supported by mapped custom data sources."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel, Field, ValidationError, field_validator
|
||
|
||
|
||
class VesselAISRecord(BaseModel):
|
||
mmsi: int = Field(ge=100000000, le=999999999)
|
||
lat: float = Field(ge=-90, le=90)
|
||
lon: float = Field(ge=-180, le=180)
|
||
sog: float | None = None
|
||
cog: float | None = Field(default=None, ge=0, le=360)
|
||
heading: int | None = Field(default=None, ge=0, le=511)
|
||
nav_status: int | None = None
|
||
name: str | None = None
|
||
callsign: str | None = None
|
||
vessel_type: str | int | None = None
|
||
vessel_type_name: str | None = None
|
||
received_at: datetime | None = None
|
||
|
||
|
||
class GeoPointRecord(BaseModel):
|
||
lat: float = Field(ge=-90, le=90)
|
||
lon: float = Field(ge=-180, le=180)
|
||
name: str | None = None
|
||
type: str | None = None
|
||
source_id: str | None = None
|
||
observed_at: datetime | None = None
|
||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class GenericRecord(BaseModel):
|
||
data: dict[str, Any] = Field(default_factory=dict)
|
||
source_id: str | None = None
|
||
observed_at: datetime | None = None
|
||
|
||
@field_validator("data")
|
||
@classmethod
|
||
def require_payload(cls, value: dict[str, Any]) -> dict[str, Any]:
|
||
if not value:
|
||
raise ValueError("generic_records requires a non-empty data object")
|
||
return value
|
||
|
||
|
||
class EarthBoundarySourceRecord(BaseModel):
|
||
source_id: str | None = None
|
||
source_kind: str = Field(pattern="^(admin0-boundaries|coastline|claim-lines)$")
|
||
name: str | None = None
|
||
geometry: dict[str, Any] | None = None
|
||
properties: dict[str, Any] = Field(default_factory=dict)
|
||
artifact_path: str | None = None
|
||
sha256: str | None = None
|
||
feature_count: int | None = Field(default=None, ge=0)
|
||
license: str | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class TargetField:
|
||
name: str
|
||
type: str
|
||
required: bool = False
|
||
description: str = ""
|
||
example: Any = None
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"name": self.name,
|
||
"type": self.type,
|
||
"required": self.required,
|
||
"description": self.description,
|
||
"example": self.example,
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class TargetSchema:
|
||
key: str
|
||
label: str
|
||
description: str
|
||
fields: tuple[TargetField, ...]
|
||
model: type[BaseModel]
|
||
destination: str
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"key": self.key,
|
||
"label": self.label,
|
||
"description": self.description,
|
||
"destination": self.destination,
|
||
"fields": [field.to_dict() for field in self.fields],
|
||
}
|
||
|
||
def validate_record(self, record: dict[str, Any]) -> tuple[dict[str, Any] | None, list[str]]:
|
||
try:
|
||
return self.model.model_validate(record).model_dump(mode="json"), []
|
||
except ValidationError as exc:
|
||
return None, [
|
||
".".join(str(part) for part in error["loc"]) + f": {error['msg']}"
|
||
for error in exc.errors()
|
||
]
|
||
|
||
|
||
TARGET_SCHEMAS: dict[str, TargetSchema] = {
|
||
"vessel_ais": TargetSchema(
|
||
key="vessel_ais",
|
||
label="船舶 AIS",
|
||
description="船只位置、航速、航向、MMSI 等 AIS 数据。",
|
||
destination="vessel_position",
|
||
model=VesselAISRecord,
|
||
fields=(
|
||
TargetField("mmsi", "integer", True, "MMSI 九位船舶标识", 257123000),
|
||
TargetField("lat", "float", True, "纬度", 59.91),
|
||
TargetField("lon", "float", True, "经度", 10.75),
|
||
TargetField("sog", "float", False, "对地航速,单位节", 12.4),
|
||
TargetField("cog", "float", False, "对地航向,0-360 度", 184.5),
|
||
TargetField("heading", "integer", False, "船首向,0-511", 186),
|
||
TargetField("nav_status", "integer", False, "导航状态码", 0),
|
||
TargetField("name", "string", False, "船名", "OSLO EXPRESS"),
|
||
TargetField("callsign", "string", False, "呼号", "LAAB"),
|
||
TargetField("vessel_type", "string", False, "船型代码", 70),
|
||
TargetField("vessel_type_name", "string", False, "船型名称", "Cargo"),
|
||
TargetField("received_at", "datetime", False, "数据接收时间", "2026-04-28T00:00:00Z"),
|
||
),
|
||
),
|
||
"geo_points": TargetSchema(
|
||
key="geo_points",
|
||
label="通用地理点",
|
||
description="带经纬度的通用实体或事件点位。",
|
||
destination="generic_geo_points",
|
||
model=GeoPointRecord,
|
||
fields=(
|
||
TargetField("lat", "float", True, "纬度", 1.3),
|
||
TargetField("lon", "float", True, "经度", 103.8),
|
||
TargetField("name", "string", False, "点位名称", "Singapore"),
|
||
TargetField("type", "string", False, "点位类型", "datacenter"),
|
||
TargetField("source_id", "string", False, "来源侧 ID", "sg-1"),
|
||
TargetField("observed_at", "datetime", False, "观测时间", "2026-04-28T00:00:00Z"),
|
||
TargetField("metadata", "object", False, "扩展字段", {"provider": "example"}),
|
||
),
|
||
),
|
||
"generic_records": TargetSchema(
|
||
key="generic_records",
|
||
label="通用结构化记录",
|
||
description="未知结构数据沉淀,不直接进入 Earth 图层。",
|
||
destination="collected_data",
|
||
model=GenericRecord,
|
||
fields=(
|
||
TargetField("data", "object", True, "结构化记录主体", {"raw": "value"}),
|
||
TargetField("source_id", "string", False, "来源侧 ID", "record-1"),
|
||
TargetField("observed_at", "datetime", False, "观测时间", "2026-04-28T00:00:00Z"),
|
||
),
|
||
),
|
||
"earth_boundary_source": TargetSchema(
|
||
key="earth_boundary_source",
|
||
label="Earth 边界矢量源",
|
||
description="Admin-0 国界、海岸线、主张线等 Earth 边界源数据 artifact。",
|
||
destination="collected_data",
|
||
model=EarthBoundarySourceRecord,
|
||
fields=(
|
||
TargetField("source_id", "string", False, "来源侧 ID", "feature-1"),
|
||
TargetField("source_kind", "string", True, "边界源类型", "admin0-boundaries"),
|
||
TargetField("name", "string", False, "记录名称", "China"),
|
||
TargetField("geometry", "object", False, "GeoJSON geometry", {"type": "Polygon", "coordinates": []}),
|
||
TargetField("properties", "object", False, "GeoJSON properties", {"ISO_A3": "CHN"}),
|
||
TargetField("artifact_path", "string", False, "完整源数据本地 artifact 路径", "data/earth-boundary-sources/earth_admin0_boundaries/abc.geojson"),
|
||
TargetField("sha256", "string", False, "源数据 artifact SHA-256", "abc123"),
|
||
TargetField("feature_count", "integer", False, "源数据 feature 数", 1),
|
||
TargetField("license", "string", False, "源数据许可", "ODbL"),
|
||
),
|
||
),
|
||
}
|
||
|
||
|
||
def list_target_schemas() -> list[dict[str, Any]]:
|
||
return [schema.to_dict() for schema in TARGET_SCHEMAS.values()]
|
||
|
||
|
||
def get_target_schema(key: str) -> TargetSchema:
|
||
try:
|
||
return TARGET_SCHEMAS[key]
|
||
except KeyError as exc:
|
||
raise ValueError(f"Unsupported target schema: {key}") from exc
|