Update models tiles and directories for source

This commit is contained in:
parent 870b3f4e42
commit aa36dea08e
13 changed files with 396 additions and 14 deletions

View file

@ -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;

View file

@ -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<TrackPoint[]>([]);
const wsRef = useRef<WebSocket | null>(null);
const [mapMode, setMapMode] = useState<"raster" | "vector">("raster");
const [tilesets, setTilesets] = useState<string[]>([]);
const [activeTileset, setActiveTileset] = useState<string>("");
const [selectedFileName, setSelectedFileName] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement | null>(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() {
<div className="card-label">GNSS source</div>
<div className="card-value">{config?.gnss_source ?? "—"}</div>
</div>
<div className="card">
<div className="card-label">Tileset</div>
<div className="card-value">
<div className="tileset-row">
<select
value={activeTileset}
onChange={(e) => activateTileset(e.target.value).catch((err) => sendFrontLog(`${err}`, "error"))}
>
{tilesets.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<button type="button" className="btn ghost" onClick={() => refreshTilesets().catch(() => {})}>
Обновить
</button>
</div>
</div>
</div>
<div className="card">
<div className="card-label">Last fix</div>
<div className="card-value">{formatTime(lastPoint?.timestamp)}</div>
@ -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() {
<option value="xyz">Папка XYZ</option>
</select>
</label>
<label className="field">
<span>Tileset (имя папки)</span>
<div className="file-row">
<input
type="text"
list="tilesets-list"
placeholder="например: moscow (пусто → активный)"
value={sliceParams.tileset}
onChange={(e) => setSliceParams((p) => ({ ...p, tileset: e.target.value }))}
/>
<button
type="button"
className="btn ghost"
onClick={() => {
const name = sliceParams.tileset.trim();
if (!name) return;
createTileset(name).catch((err) => sendFrontLog(`tileset create failed: ${err}`, "error"));
}}
>
Создать
</button>
</div>
<datalist id="tilesets-list">
{tilesets.map((t) => (
<option key={t} value={t} />
))}
</datalist>
<small className="muted">Активный: {activeTileset || "—"}</small>
</label>
<label className="field">
<span>Папка тайлов (опц.)</span>
<input
type="text"
placeholder="пусто → gnss/tiles"
placeholder="лучше не трогать (по умолчанию tileset)"
value={sliceParams.destination}
onChange={(e) => setSliceParams((p) => ({ ...p, destination: e.target.value }))}
/>
@ -601,6 +712,7 @@ export default function App() {
setSliceParams({
source: "",
destination: "",
tileset: "",
min_zoom: 10,
max_zoom: 16,
tile_format: "png",