From aa36dea08ef8c847dac4972fa5d5f1acbd622be8 Mon Sep 17 00:00:00 2001 From: "s.galanskiy" Date: Fri, 19 Dec 2025 11:13:52 +0500 Subject: [PATCH] Update models tiles and directories for source --- .gitignore | 1 + frontend/src/App.css | 10 ++ frontend/src/App.tsx | 116 +++++++++++++++++++++++- gnss/__pycache__/config.cpython-313.pyc | Bin 681 -> 940 bytes gnss/api/routes/config_route.py | 2 + gnss/api/routes/tiles.py | 20 +++- gnss/api/routes/tilesets.py | 49 ++++++++++ gnss/config.py | 13 ++- gnss/main.py | 47 ++++++++-- gnss/models/tiles.py | 6 ++ gnss/services/gnss.py | 67 ++++++++++++++ gnss/services/tiles.py | 4 + gnss/services/tilesets.py | 75 +++++++++++++++ 13 files changed, 396 insertions(+), 14 deletions(-) create mode 100644 gnss/api/routes/tilesets.py create mode 100644 gnss/services/tilesets.py diff --git a/.gitignore b/.gitignore index 9f4b5b0..2a7b188 100644 --- a/.gitignore +++ b/.gitignore @@ -244,6 +244,7 @@ pyrightconfig.json # Runtime-generated data (do not commit) logs/ gnss/tiles/ +gnss/tilesets/ gnss/uploads/ # Frontend artifacts diff --git a/frontend/src/App.css b/frontend/src/App.css index f151588..dd7d863 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -236,6 +236,16 @@ body, #root { color: #f4f7fb; } +.tileset-row { + display: flex; + gap: 10px; + align-items: center; +} + +.tileset-row select { + flex: 1; +} + .file-row { display: flex; gap: 8px; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 03eae76..8cd777a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -30,6 +30,8 @@ type AppConfig = { tiles_url_tms?: string; vector_tiles_url?: string; vector_metadata_url?: string; + tilesets_url?: string; + tilesets_activate_url?: string; ws_url: string; gnss_source: string; }; @@ -77,11 +79,14 @@ export default function App() { const [track, setTrack] = useState([]); const wsRef = useRef(null); const [mapMode, setMapMode] = useState<"raster" | "vector">("raster"); + const [tilesets, setTilesets] = useState([]); + const [activeTileset, setActiveTileset] = useState(""); const [selectedFileName, setSelectedFileName] = useState(""); const fileInputRef = useRef(null); const [sliceParams, setSliceParams] = useState({ source: "", destination: "", + tileset: "", min_zoom: 10, max_zoom: 16, tile_format: "png", @@ -104,6 +109,16 @@ export default function App() { .then((res) => { setSliceParams((p) => ({ ...p, source: res.path })); setSelectedFileName(res.filename); + setSliceParams((p) => { + if (p.tileset) return p; + const proposed = String(res.filename || "") + .replace(/\.[^.]+$/, "") + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 64); + return proposed ? { ...p, tileset: proposed } : p; + }); setSliceStatus("idle"); sendFrontLog(`uploaded source ${res.filename}`, "info"); if (res.filename?.toLowerCase?.().endsWith(".mbtiles")) { @@ -151,7 +166,10 @@ export default function App() { useEffect(() => { fetch(`${backendBase}/config`, { cache: "no-store" }) .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`status ${r.status}`)))) - .then(setConfig) + .then((cfg) => { + setConfig(cfg); + return cfg; + }) .catch((err) => { console.error("Config error", err); sendFrontLog(`config fetch failed: ${err}`, "error"); @@ -159,6 +177,46 @@ export default function App() { }); }, []); + const refreshTilesets = async () => { + const url = config?.tilesets_url ? `${backendBase}${config.tilesets_url}` : `${backendBase}/tilesets`; + const res = await fetch(url, { cache: "no-store" }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.detail || `status ${res.status}`); + setTilesets(data.tilesets || []); + setActiveTileset(data.active || ""); + }; + + useEffect(() => { + if (!config) return; + refreshTilesets().catch((err) => sendFrontLog(`tilesets refresh failed: ${err}`, "warn")); + }, [config]); + + const activateTileset = async (name: string) => { + const url = config?.tilesets_activate_url ? `${backendBase}${config.tilesets_activate_url}` : `${backendBase}/tilesets/activate`; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.detail || `status ${res.status}`); + setActiveTileset(data.active || name); + sendFrontLog(`tileset activated: ${data.active || name}`, "info"); + }; + + const createTileset = async (name: string) => { + const res = await fetch(`${backendBase}/tilesets`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.detail || `status ${res.status}`); + await refreshTilesets(); + setSliceParams((p) => ({ ...p, tileset: data.name || name })); + sendFrontLog(`tileset created: ${data.name || name}`, "info"); + }; + useEffect(() => { if (!config) return; const base = backendBase.replace(/\/+$/, ""); @@ -410,6 +468,26 @@ export default function App() {
GNSS source
{config?.gnss_source ?? "—"}
+
+
Tileset
+
+
+ + +
+
+
Last fix
{formatTime(lastPoint?.timestamp)}
@@ -479,6 +557,7 @@ export default function App() { body: JSON.stringify({ source: sliceParams.source, destination: sliceParams.destination || undefined, + tileset: sliceParams.tileset || undefined, min_zoom: Number(sliceParams.min_zoom), max_zoom: Number(sliceParams.max_zoom), tile_format: sliceParams.tile_format, @@ -490,6 +569,9 @@ export default function App() { .then((res) => { setSliceStatus("ok"); sendFrontLog(`tiles sliced to ${res.tiles_dir}`, "info"); + refreshTilesets().catch(() => {}); + if (res.tileset) setActiveTileset(res.tileset); + if (res.tileset) activateTileset(res.tileset).catch(() => {}); }) .catch((err) => { setSliceStatus("error"); @@ -552,11 +634,40 @@ export default function App() { +