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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue