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 [tilesets, setTilesets] = useState<string[]>([]);
const [activeTileset, setActiveTileset] = useState<string>("");
const [tilesetKinds, setTilesetKinds] = useState<Record<string, string>>({});
const [selectedFileName, setSelectedFileName] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement | null>(null);
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}`);
setTilesets(data.tilesets || []);
setActiveTileset(data.active || "");
setTilesetKinds(data.kinds || {});
}, [config]);
useEffect(() => {
@ -164,8 +166,15 @@ export default function App() {
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
setActiveTileset(data.active || name);
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(
@ -273,9 +282,13 @@ export default function App() {
setMapMode("raster");
}
sendFrontLog(`map ingested: kind=${data.kind} driver=${data.driver}`, "info");
if (data.tileset) {
refreshTilesets().catch(() => {});
activateTileset(String(data.tileset)).catch(() => {});
}
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(
@ -291,6 +304,10 @@ export default function App() {
.then((res) => {
setSliceParams((p) => ({ ...p, source: res.path }));
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) => {
if (p.tileset) return p;
const proposed = String(res.filename || "")
@ -326,7 +343,11 @@ export default function App() {
],
});
if (typeof selected === "string") {
const lower = selected.toLowerCase();
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);
return;
}

View file

@ -1,3 +1,4 @@
import json
from pathlib import Path
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.tiles import TileCutError, TileService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
from ...services.tilesets import TilesetError, TilesetService
from ... import config
router = APIRouter(tags=["maps"])
@ -30,21 +33,61 @@ async def ingest_map(
if not p.exists():
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":
try:
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:
pass
try:
info = raster_mbtiles.activate(p)
except RasterMBTilesError as 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 payload.max_zoom < payload.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:
tile_service.slice_to_tiles(
source=p,
@ -55,7 +98,13 @@ async def ingest_map(
)
except TileCutError as 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")

View file

@ -5,12 +5,13 @@ from fastapi.responses import JSONResponse
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 ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileCutError, TileService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
from ... import config
from ...services.tilesets import TilesetService, TilesetError
from ...services.raster_mbtiles import RasterMBTilesService
router = APIRouter(tags=["tiles"])
@ -53,7 +54,12 @@ async def serve_tile_tms(
@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()
tileset_svc = TilesetService(tilesets_dir=config.TILESSETS_DIR, default_tileset=config.DEFAULT_TILESET)
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 = payload.destination_path(destination_default)
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"}:
src = tile_service.unpack_if_archive(source)
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:
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()
try:

View file

@ -5,10 +5,12 @@ from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response
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 ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileService
from ...services.tilesets import TilesetError, TilesetService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
router = APIRouter(tags=["tilesets"])
@ -31,6 +33,15 @@ def _is_gzipped(data: bytes) -> bool:
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"
if meta.is_file():
try:
@ -218,14 +229,40 @@ async def create_tileset(payload: CreateTileset) -> dict:
@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()
try:
active = svc.set_active(payload.name)
except TilesetError as exc:
raise HTTPException(status_code=400, detail=str(exc))
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")
@ -255,4 +292,3 @@ async def active_vector_tile(z: int, x: int, y: int) -> Response:
if _is_gzipped(data):
headers["Content-Encoding"] = "gzip"
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)
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
log_service = LogService(base_dir=Path("logs"))
raster_mbtiles_service = RasterMBTilesService()
vector_mbtiles_service = VectorMBTilesService()
raster_mbtiles_service = RasterMBTilesService(state_file=Path(config.DATA_DIR) / "raster_mbtiles.active")
vector_mbtiles_service = VectorMBTilesService(state_file=Path(config.DATA_DIR) / "vector_mbtiles.active")
gnss_service = create_gnss_service()
state.set_services(
gnss=gnss_service,

View file

@ -40,9 +40,36 @@ def _format_to_media_type(fmt: str) -> str:
class RasterMBTilesService:
def __init__(self):
def __init__(self, state_file: Optional[Path] = None):
self._lock = threading.Lock()
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:
p = Path(path).expanduser().resolve()
@ -66,6 +93,7 @@ class RasterMBTilesService:
info = RasterMBTilesInfo(path=p, metadata=metadata)
with self._lock:
self._active = info
self._persist(info)
return info
def get_active(self) -> RasterMBTilesInfo:
@ -118,4 +146,3 @@ class RasterMBTilesService:
fmt = str(info.metadata.get("format") or "").lower()
return data, _format_to_media_type(fmt)

View file

@ -29,9 +29,36 @@ def _has_table(conn: sqlite3.Connection, table: str) -> bool:
class VectorMBTilesService:
def __init__(self):
def __init__(self, state_file: Optional[Path] = None):
self._lock = threading.Lock()
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:
p = Path(path).expanduser().resolve()
@ -55,6 +82,7 @@ class VectorMBTilesService:
info = VectorMBTilesInfo(path=p, metadata=metadata)
with self._lock:
self._active = info
self._persist(info)
return info
def get_active(self) -> VectorMBTilesInfo: