fix tilesets for vectors map

This commit is contained in:
parent 2715c56b4f
commit f009a57b8e
7 changed files with 209 additions and 18 deletions

View file

@ -38,6 +38,7 @@ export default function App() {
const [vectorSource, setVectorSource] = useState<"mbtiles" | "tileset">("mbtiles"); const [vectorSource, setVectorSource] = useState<"mbtiles" | "tileset">("mbtiles");
const [tilesets, setTilesets] = useState<string[]>([]); const [tilesets, setTilesets] = useState<string[]>([]);
const [activeTileset, setActiveTileset] = useState<string>(""); const [activeTileset, setActiveTileset] = useState<string>("");
const [tilesetKinds, setTilesetKinds] = useState<Record<string, string>>({});
const [selectedFileName, setSelectedFileName] = useState<string>(""); const [selectedFileName, setSelectedFileName] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const [sliceParams, setSliceParams] = useState<SliceParams>(initialSliceParams); const [sliceParams, setSliceParams] = useState<SliceParams>(initialSliceParams);
@ -96,6 +97,7 @@ export default function App() {
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`); if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
setTilesets(data.tilesets || []); setTilesets(data.tilesets || []);
setActiveTileset(data.active || ""); setActiveTileset(data.active || "");
setTilesetKinds(data.kinds || {});
}, [config]); }, [config]);
useEffect(() => { useEffect(() => {
@ -164,8 +166,15 @@ export default function App() {
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`); if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
setActiveTileset(data.active || name); setActiveTileset(data.active || name);
sendFrontLog(`tileset activated: ${data.active || name}`, "info"); sendFrontLog(`tileset activated: ${data.active || name}`, "info");
const kind = (data.kind as string | undefined) || tilesetKinds[name] || "";
if (kind.startsWith("vector")) {
setMapMode("vector");
setVectorSource(kind === "vector-dir" ? "tileset" : "mbtiles");
} else if (kind.startsWith("raster")) {
setMapMode("raster");
}
}, },
[config, sendFrontLog], [config, sendFrontLog, tilesetKinds],
); );
const createTileset = useCallback( const createTileset = useCallback(
@ -273,9 +282,13 @@ export default function App() {
setMapMode("raster"); setMapMode("raster");
} }
sendFrontLog(`map ingested: kind=${data.kind} driver=${data.driver}`, "info"); sendFrontLog(`map ingested: kind=${data.kind} driver=${data.driver}`, "info");
if (data.tileset) {
refreshTilesets().catch(() => {});
activateTileset(String(data.tileset)).catch(() => {});
}
return data; return data;
}, },
[sendFrontLog, sliceParams.max_zoom, sliceParams.min_zoom, sliceParams.tileset], [activateTileset, refreshTilesets, sendFrontLog, sliceParams.max_zoom, sliceParams.min_zoom, sliceParams.tileset],
); );
const uploadFileBlob = useCallback( const uploadFileBlob = useCallback(
@ -291,6 +304,10 @@ export default function App() {
.then((res) => { .then((res) => {
setSliceParams((p) => ({ ...p, source: res.path })); setSliceParams((p) => ({ ...p, source: res.path }));
setSelectedFileName(res.filename); setSelectedFileName(res.filename);
const lower = String(res.filename || "").toLowerCase();
if (lower.endsWith(".mbtiles")) setSliceParams((p) => ({ ...p, source_format: "mbtiles" }));
if (lower.endsWith(".tif") || lower.endsWith(".tiff") || lower.endsWith(".vrt"))
setSliceParams((p) => ({ ...p, source_format: "geotiff" }));
setSliceParams((p) => { setSliceParams((p) => {
if (p.tileset) return p; if (p.tileset) return p;
const proposed = String(res.filename || "") const proposed = String(res.filename || "")
@ -326,7 +343,11 @@ export default function App() {
], ],
}); });
if (typeof selected === "string") { if (typeof selected === "string") {
const lower = selected.toLowerCase();
setSliceParams((p) => ({ ...p, source: selected })); setSliceParams((p) => ({ ...p, source: selected }));
if (lower.endsWith(".mbtiles")) setSliceParams((p) => ({ ...p, source_format: "mbtiles" }));
if (lower.endsWith(".tif") || lower.endsWith(".tiff") || lower.endsWith(".vrt"))
setSliceParams((p) => ({ ...p, source_format: "geotiff" }));
setSelectedFileName(selected.split("/").pop() || selected); setSelectedFileName(selected.split("/").pop() || selected);
return; return;
} }

View file

@ -1,3 +1,4 @@
import json
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@ -7,6 +8,8 @@ from ..dependencies import get_raster_mbtiles_service, get_tile_service, get_vec
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileCutError, TileService from ...services.tiles import TileCutError, TileService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
from ...services.tilesets import TilesetError, TilesetService
from ... import config
router = APIRouter(tags=["maps"]) router = APIRouter(tags=["maps"])
@ -30,21 +33,61 @@ async def ingest_map(
if not p.exists(): if not p.exists():
raise HTTPException(status_code=400, detail=f"path not found: {p}") raise HTTPException(status_code=400, detail=f"path not found: {p}")
tileset_svc = TilesetService(tilesets_dir=config.TILESSETS_DIR, default_tileset=config.DEFAULT_TILESET)
tileset_svc.ensure()
suggested_tileset = (payload.tileset or p.stem or "").strip()
tileset_name: str | None = None
if suggested_tileset:
try:
tileset_name = tileset_svc.create(suggested_tileset)
tileset_svc.set_active(tileset_name)
tile_service.set_tiles_dir(tileset_svc.resolve(tileset_name))
except TilesetError as exc:
raise HTTPException(status_code=400, detail=str(exc))
if p.suffix.lower() == ".mbtiles": if p.suffix.lower() == ".mbtiles":
try: try:
info = vector_mbtiles.activate(p) info = vector_mbtiles.activate(p)
return {"status": "ok", "kind": "vector", "driver": "mbtiles", "path": str(info.path), "format": info.metadata.get("format")} if tileset_name:
manifest = {"kind": "vector-mbtiles", "source": str(info.path), "format": info.metadata.get("format")}
(tileset_svc.resolve(tileset_name) / "gnss_tileset.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return {
"status": "ok",
"kind": "vector",
"driver": "mbtiles",
"path": str(info.path),
"format": info.metadata.get("format"),
"tileset": tileset_name,
}
except VectorMBTilesError: except VectorMBTilesError:
pass pass
try: try:
info = raster_mbtiles.activate(p) info = raster_mbtiles.activate(p)
except RasterMBTilesError as exc: except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "kind": "raster", "driver": "mbtiles", "path": str(info.path), "format": info.metadata.get("format")} if tileset_name:
manifest = {"kind": "raster-mbtiles", "source": str(info.path), "format": info.metadata.get("format")}
(tileset_svc.resolve(tileset_name) / "gnss_tileset.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return {
"status": "ok",
"kind": "raster",
"driver": "mbtiles",
"path": str(info.path),
"format": info.metadata.get("format"),
"tileset": tileset_name,
}
if p.suffix.lower() in {".tif", ".tiff", ".vrt"}: if p.suffix.lower() in {".tif", ".tiff", ".vrt"}:
if payload.max_zoom < payload.min_zoom: if payload.max_zoom < payload.min_zoom:
raise HTTPException(status_code=400, detail="max_zoom must be >= min_zoom") raise HTTPException(status_code=400, detail="max_zoom must be >= min_zoom")
if tileset_name:
tile_service.set_tiles_dir(tileset_svc.resolve(tileset_name))
try: try:
tile_service.slice_to_tiles( tile_service.slice_to_tiles(
source=p, source=p,
@ -55,7 +98,13 @@ async def ingest_map(
) )
except TileCutError as exc: except TileCutError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "kind": "raster", "driver": "gdal2tiles", "path": str(p), "tiles_dir": str(tile_service.tiles_dir)} return {
"status": "ok",
"kind": "raster",
"driver": "gdal2tiles",
"path": str(p),
"tiles_dir": str(tile_service.tiles_dir),
"tileset": tileset_name,
}
raise HTTPException(status_code=400, detail="unsupported input; expected .mbtiles or GeoTIFF/VRT") raise HTTPException(status_code=400, detail="unsupported input; expected .mbtiles or GeoTIFF/VRT")

View file

@ -5,12 +5,13 @@ from fastapi.responses import JSONResponse
import json import json
from ...api.dependencies import get_raster_mbtiles_service, get_tile_service from ...api.dependencies import get_raster_mbtiles_service, get_tile_service, get_vector_mbtiles_service
from ...models.tiles import TileSliceRequest from ...models.tiles import TileSliceRequest
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileCutError, TileService from ...services.tiles import TileCutError, TileService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
from ... import config from ... import config
from ...services.tilesets import TilesetService, TilesetError from ...services.tilesets import TilesetService, TilesetError
from ...services.raster_mbtiles import RasterMBTilesService
router = APIRouter(tags=["tiles"]) router = APIRouter(tags=["tiles"])
@ -53,7 +54,12 @@ async def serve_tile_tms(
@router.post("/tiles/slice") @router.post("/tiles/slice")
async def slice_tiles(payload: TileSliceRequest, tile_service: TileService = Depends(get_tile_service)) -> dict: async def slice_tiles(
payload: TileSliceRequest,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
vector_mbtiles: VectorMBTilesService = Depends(get_vector_mbtiles_service),
) -> dict:
source = payload.source_path() source = payload.source_path()
tileset_svc = TilesetService(tilesets_dir=config.TILESSETS_DIR, default_tileset=config.DEFAULT_TILESET) tileset_svc = TilesetService(tilesets_dir=config.TILESSETS_DIR, default_tileset=config.DEFAULT_TILESET)
tileset_svc.ensure() tileset_svc.ensure()
@ -66,6 +72,28 @@ async def slice_tiles(payload: TileSliceRequest, tile_service: TileService = Dep
destination_default = tileset_svc.resolve(tileset_name) destination_default = tileset_svc.resolve(tileset_name)
destination = payload.destination_path(destination_default) destination = payload.destination_path(destination_default)
try: try:
if payload.source_format == "mbtiles":
try:
info = vector_mbtiles.activate(source)
kind = "vector"
manifest = {"kind": "vector-mbtiles", "source": str(info.path), "format": info.metadata.get("format")}
except VectorMBTilesError:
info = raster_mbtiles.activate(source)
kind = "raster"
manifest = {"kind": "raster-mbtiles", "source": str(info.path), "format": info.metadata.get("format")}
active_tileset = tileset_name or tileset_svc.get_active()
(tileset_svc.resolve(active_tileset) / "gnss_tileset.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return {
"status": "ok",
"kind": kind,
"driver": "mbtiles",
"path": str(source),
"scheme": payload.scheme,
"tileset": active_tileset,
}
if payload.source_format in {"tms", "xyz"}: if payload.source_format in {"tms", "xyz"}:
src = tile_service.unpack_if_archive(source) src = tile_service.unpack_if_archive(source)
tile_service.copy_tile_tree(src, destination) tile_service.copy_tile_tree(src, destination)
@ -83,6 +111,8 @@ async def slice_tiles(payload: TileSliceRequest, tile_service: TileService = Dep
) )
except TileCutError as exc: except TileCutError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
except (RasterMBTilesError, VectorMBTilesError) as exc:
raise HTTPException(status_code=400, detail=str(exc))
active_tileset = tileset_name or tileset_svc.get_active() active_tileset = tileset_name or tileset_svc.get_active()
try: try:

View file

@ -5,10 +5,12 @@ from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel from pydantic import BaseModel
from ..dependencies import get_tile_service from ..dependencies import get_raster_mbtiles_service, get_tile_service, get_vector_mbtiles_service
from ... import config from ... import config
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileService from ...services.tiles import TileService
from ...services.tilesets import TilesetError, TilesetService from ...services.tilesets import TilesetError, TilesetService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
router = APIRouter(tags=["tilesets"]) router = APIRouter(tags=["tilesets"])
@ -31,6 +33,15 @@ def _is_gzipped(data: bytes) -> bool:
def _detect_kind(path: Path) -> str: def _detect_kind(path: Path) -> str:
manifest = path / "gnss_tileset.json"
if manifest.is_file():
try:
parsed = json.loads(manifest.read_text(encoding="utf-8"))
kind = str(parsed.get("kind") or "").lower()
if kind in {"vector-mbtiles", "raster-mbtiles"}:
return kind
except Exception:
pass
meta = path / "metadata.json" meta = path / "metadata.json"
if meta.is_file(): if meta.is_file():
try: try:
@ -218,14 +229,40 @@ async def create_tileset(payload: CreateTileset) -> dict:
@router.post("/tilesets/activate") @router.post("/tilesets/activate")
async def activate_tileset(payload: ActivateTileset, tile_service: TileService = Depends(get_tile_service)) -> dict: async def activate_tileset(
payload: ActivateTileset,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
vector_mbtiles: VectorMBTilesService = Depends(get_vector_mbtiles_service),
) -> dict:
svc = _tileset_service() svc = _tileset_service()
try: try:
active = svc.set_active(payload.name) active = svc.set_active(payload.name)
except TilesetError as exc: except TilesetError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
tile_service.set_tiles_dir(svc.resolve(active)) tile_service.set_tiles_dir(svc.resolve(active))
return {"status": "ok", "active": active} tileset_dir = svc.resolve(active)
kind = _detect_kind(tileset_dir)
activated: dict | None = None
manifest_path = tileset_dir / "gnss_tileset.json"
if manifest_path.is_file() and kind in {"vector-mbtiles", "raster-mbtiles"}:
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
src = manifest.get("source") or manifest.get("path") or manifest.get("mbtiles")
if not src:
raise ValueError("gnss_tileset.json missing 'source'")
mbtiles_path = Path(str(src)).expanduser().resolve()
if kind == "vector-mbtiles":
info = vector_mbtiles.activate(mbtiles_path)
activated = {"kind": "vector", "driver": "mbtiles", "path": str(info.path)}
else:
info = raster_mbtiles.activate(mbtiles_path)
activated = {"kind": "raster", "driver": "mbtiles", "path": str(info.path)}
except (VectorMBTilesError, RasterMBTilesError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=400, detail=f"failed to activate mbtiles tileset: {exc}")
return {"status": "ok", "active": active, "kind": kind, "activated": activated}
@router.get("/tilesets/active/vector/metadata") @router.get("/tilesets/active/vector/metadata")
@ -255,4 +292,3 @@ async def active_vector_tile(z: int, x: int, y: int) -> Response:
if _is_gzipped(data): if _is_gzipped(data):
headers["Content-Encoding"] = "gzip" headers["Content-Encoding"] = "gzip"
return Response(content=data, media_type="application/x-protobuf", headers=headers) return Response(content=data, media_type="application/x-protobuf", headers=headers)

View file

@ -53,8 +53,8 @@ tiles_dir = tileset_service.resolve(None)
placeholder_tile = Path(config.TILE_PLACEHOLDER) placeholder_tile = Path(config.TILE_PLACEHOLDER)
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR)) tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
log_service = LogService(base_dir=Path("logs")) log_service = LogService(base_dir=Path("logs"))
raster_mbtiles_service = RasterMBTilesService() raster_mbtiles_service = RasterMBTilesService(state_file=Path(config.DATA_DIR) / "raster_mbtiles.active")
vector_mbtiles_service = VectorMBTilesService() vector_mbtiles_service = VectorMBTilesService(state_file=Path(config.DATA_DIR) / "vector_mbtiles.active")
gnss_service = create_gnss_service() gnss_service = create_gnss_service()
state.set_services( state.set_services(
gnss=gnss_service, gnss=gnss_service,

View file

@ -40,9 +40,36 @@ def _format_to_media_type(fmt: str) -> str:
class RasterMBTilesService: class RasterMBTilesService:
def __init__(self): def __init__(self, state_file: Optional[Path] = None):
self._lock = threading.Lock() self._lock = threading.Lock()
self._active: Optional[RasterMBTilesInfo] = None self._active: Optional[RasterMBTilesInfo] = None
self._state_file = Path(state_file).expanduser().resolve() if state_file else None
self._restore()
def _restore(self) -> None:
if not self._state_file:
return
try:
raw = self._state_file.read_text(encoding="utf-8").strip()
except FileNotFoundError:
return
except Exception:
return
if not raw:
return
try:
self.activate(Path(raw))
except Exception:
return
def _persist(self, info: RasterMBTilesInfo) -> None:
if not self._state_file:
return
try:
self._state_file.parent.mkdir(parents=True, exist_ok=True)
self._state_file.write_text(str(info.path), encoding="utf-8")
except Exception:
return
def activate(self, path: Path) -> RasterMBTilesInfo: def activate(self, path: Path) -> RasterMBTilesInfo:
p = Path(path).expanduser().resolve() p = Path(path).expanduser().resolve()
@ -66,6 +93,7 @@ class RasterMBTilesService:
info = RasterMBTilesInfo(path=p, metadata=metadata) info = RasterMBTilesInfo(path=p, metadata=metadata)
with self._lock: with self._lock:
self._active = info self._active = info
self._persist(info)
return info return info
def get_active(self) -> RasterMBTilesInfo: def get_active(self) -> RasterMBTilesInfo:
@ -118,4 +146,3 @@ class RasterMBTilesService:
fmt = str(info.metadata.get("format") or "").lower() fmt = str(info.metadata.get("format") or "").lower()
return data, _format_to_media_type(fmt) return data, _format_to_media_type(fmt)

View file

@ -29,9 +29,36 @@ def _has_table(conn: sqlite3.Connection, table: str) -> bool:
class VectorMBTilesService: class VectorMBTilesService:
def __init__(self): def __init__(self, state_file: Optional[Path] = None):
self._lock = threading.Lock() self._lock = threading.Lock()
self._active: Optional[VectorMBTilesInfo] = None self._active: Optional[VectorMBTilesInfo] = None
self._state_file = Path(state_file).expanduser().resolve() if state_file else None
self._restore()
def _restore(self) -> None:
if not self._state_file:
return
try:
raw = self._state_file.read_text(encoding="utf-8").strip()
except FileNotFoundError:
return
except Exception:
return
if not raw:
return
try:
self.activate(Path(raw))
except Exception:
return
def _persist(self, info: VectorMBTilesInfo) -> None:
if not self._state_file:
return
try:
self._state_file.parent.mkdir(parents=True, exist_ok=True)
self._state_file.write_text(str(info.path), encoding="utf-8")
except Exception:
return
def activate(self, path: Path) -> VectorMBTilesInfo: def activate(self, path: Path) -> VectorMBTilesInfo:
p = Path(path).expanduser().resolve() p = Path(path).expanduser().resolve()
@ -55,6 +82,7 @@ class VectorMBTilesService:
info = VectorMBTilesInfo(path=p, metadata=metadata) info = VectorMBTilesInfo(path=p, metadata=metadata)
with self._lock: with self._lock:
self._active = info self._active = info
self._persist(info)
return info return info
def get_active(self) -> VectorMBTilesInfo: def get_active(self) -> VectorMBTilesInfo: