fix tilesets for vectors map
This commit is contained in:
parent
2715c56b4f
commit
f009a57b8e
7 changed files with 209 additions and 18 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue