fix interface

This commit is contained in:
parent 042d0f8fa7
commit f61801e855
7 changed files with 287 additions and 15 deletions

View file

@ -32,6 +32,10 @@ export default function App() {
const [track, setTrack] = useState<TrackPoint[]>([]);
const wsRef = useRef<WebSocket | null>(null);
const [mapMode, setMapMode] = useState<MapMode>("raster");
const [gnssMode, setGnssMode] = useState<"simulated" | "serial" | "unknown">("unknown");
const [simCenterLat, setSimCenterLat] = useState<string>("");
const [simCenterLon, setSimCenterLon] = useState<string>("");
const [vectorSource, setVectorSource] = useState<"mbtiles" | "tileset">("mbtiles");
const [tilesets, setTilesets] = useState<string[]>([]);
const [activeTileset, setActiveTileset] = useState<string>("");
const [selectedFileName, setSelectedFileName] = useState<string>("");
@ -70,6 +74,21 @@ export default function App() {
});
}, [sendFrontLog]);
const refreshGnssState = useCallback(async () => {
const url = config?.gnss_state_url ? `${backendBase}${config.gnss_state_url}` : `${backendBase}/gnss/state`;
const res = await fetch(url, { cache: "no-store" });
const data = await res.json();
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
const mode = (data?.gnss?.mode as string | undefined) || "unknown";
if (mode === "simulated" || mode === "serial") {
setGnssMode(mode);
} else {
setGnssMode("unknown");
}
if (typeof data?.gnss?.lat === "number") setSimCenterLat(String(data.gnss.lat));
if (typeof data?.gnss?.lon === "number") setSimCenterLon(String(data.gnss.lon));
}, [config]);
const refreshTilesets = useCallback(async () => {
const url = config?.tilesets_url ? `${backendBase}${config.tilesets_url}` : `${backendBase}/tilesets`;
const res = await fetch(url, { cache: "no-store" });
@ -82,7 +101,54 @@ export default function App() {
useEffect(() => {
if (!config) return;
refreshTilesets().catch((err) => sendFrontLog(`tilesets refresh failed: ${err}`, "warn"));
}, [config, refreshTilesets, sendFrontLog]);
refreshGnssState().catch((err) => sendFrontLog(`gnss refresh failed: ${err}`, "warn"));
}, [config, refreshGnssState, refreshTilesets, sendFrontLog]);
useEffect(() => {
if (!config?.gnss_source) return;
if (config.gnss_source === "simulated" || config.gnss_source === "serial") setGnssMode(config.gnss_source);
}, [config]);
const setGnssModeRequest = useCallback(
async (mode: "simulated" | "serial") => {
const url = config?.gnss_mode_url ? `${backendBase}${config.gnss_mode_url}` : `${backendBase}/gnss/mode`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
sendFrontLog(`gnss mode set: ${mode}`, "info");
setGnssMode(mode);
setConfig((prev) => (prev ? { ...prev, gnss_source: mode } : prev));
if (mode === "simulated") {
if (typeof data?.gnss?.lat === "number") setSimCenterLat(String(data.gnss.lat));
if (typeof data?.gnss?.lon === "number") setSimCenterLon(String(data.gnss.lon));
}
},
[config, sendFrontLog],
);
const applySimCenter = useCallback(async () => {
const lat = Number(simCenterLat);
const lon = Number(simCenterLon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
sendFrontLog("invalid simulated center coordinates", "warn");
return;
}
const url = config?.gnss_sim_center_url
? `${backendBase}${config.gnss_sim_center_url}`
: `${backendBase}/gnss/sim/center`;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lat, lon }),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
sendFrontLog(`sim center updated: ${lat}, ${lon}`, "info");
}, [config, sendFrontLog, simCenterLat, simCenterLon]);
const activateTileset = useCallback(
async (name: string) => {
@ -171,25 +237,45 @@ export default function App() {
}`;
const vectorTileUrl =
config && `${backendBase.replace(/\/+$/, "")}${config.vector_tiles_url || "/tiles/vector/{z}/{x}/{y}.pbf"}`;
config &&
`${backendBase.replace(/\/+$/, "")}${
vectorSource === "tileset"
? config.vector_tileset_url || "/tilesets/active/vector/{z}/{x}/{y}.pbf"
: config.vector_tiles_url || "/tiles/vector/{z}/{x}/{y}.pbf"
}`;
const vectorMetaUrl =
config && `${backendBase.replace(/\/+$/, "")}${config.vector_metadata_url || "/tiles/vector/metadata"}`;
config &&
`${backendBase.replace(/\/+$/, "")}${
vectorSource === "tileset"
? config.vector_tileset_metadata_url || "/tilesets/active/vector/metadata"
: config.vector_metadata_url || "/tiles/vector/metadata"
}`;
const activateVectorMbtiles = useCallback(
const ingestMap = useCallback(
async (path: string) => {
const res = await fetch(`${backendBase}/tiles/vector/activate`, {
const res = await fetch(`${backendBase}/maps/ingest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
body: JSON.stringify({
path,
tileset: sliceParams.tileset || undefined,
min_zoom: Number(sliceParams.min_zoom),
max_zoom: Number(sliceParams.max_zoom),
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
sendFrontLog(`vector mbtiles activated: ${path}`, "info");
setMapMode("vector");
if (data.kind === "vector") {
setVectorSource("mbtiles");
setMapMode("vector");
} else if (data.kind === "raster") {
setMapMode("raster");
}
sendFrontLog(`map ingested: kind=${data.kind} driver=${data.driver}`, "info");
return data;
},
[sendFrontLog],
[sendFrontLog, sliceParams.max_zoom, sliceParams.min_zoom, sliceParams.tileset],
);
const uploadFileBlob = useCallback(
@ -218,9 +304,7 @@ export default function App() {
setSliceStatus("idle");
sendFrontLog(`uploaded source ${res.filename}`, "info");
if (res.filename?.toLowerCase?.().endsWith(".mbtiles")) {
activateVectorMbtiles(res.path).catch((err) => {
sendFrontLog(`vector activate failed: ${err}`, "warn");
});
ingestMap(res.path).catch((err) => sendFrontLog(`ingest failed: ${err}`, "warn"));
}
})
.catch((err) => {
@ -228,7 +312,7 @@ export default function App() {
sendFrontLog(`upload error: ${err}`, "error");
});
},
[activateVectorMbtiles, sendFrontLog],
[ingestMap, sendFrontLog],
);
const triggerFileSelect = useCallback(async () => {
@ -345,6 +429,16 @@ export default function App() {
status={status}
theme={theme}
onToggleTheme={() => setTheme((t) => (t === "dark" ? "light" : "dark"))}
gnssMode={gnssMode}
simCenterLat={simCenterLat}
simCenterLon={simCenterLon}
onGnssRefresh={() => refreshGnssState().catch((err) => sendFrontLog(`${err}`, "warn"))}
onGnssModeChange={(mode) => setGnssModeRequest(mode).catch((err) => sendFrontLog(`${err}`, "error"))}
onSimCenterLatChange={setSimCenterLat}
onSimCenterLonChange={setSimCenterLon}
onSimCenterApply={() => applySimCenter().catch((err) => sendFrontLog(`${err}`, "error"))}
vectorSource={vectorSource}
onVectorSourceChange={setVectorSource}
tilesets={tilesets}
activeTileset={activeTileset}
onActivateTileset={(name) => activateTileset(name).catch((err) => sendFrontLog(`${err}`, "error"))}

View file

@ -9,6 +9,16 @@ type SettingsPanelProps = {
status: string;
theme: "dark" | "light";
onToggleTheme: () => void;
gnssMode: "simulated" | "serial" | "unknown";
simCenterLat: string;
simCenterLon: string;
onGnssRefresh: () => void;
onGnssModeChange: (mode: "simulated" | "serial") => void;
onSimCenterLatChange: (value: string) => void;
onSimCenterLonChange: (value: string) => void;
onSimCenterApply: () => void;
vectorSource: "mbtiles" | "tileset";
onVectorSourceChange: (source: "mbtiles" | "tileset") => void;
tilesets: string[];
activeTileset: string;
onActivateTileset: (name: string) => void;
@ -32,6 +42,16 @@ export const SettingsPanel = ({
status,
theme,
onToggleTheme,
gnssMode,
simCenterLat,
simCenterLon,
onGnssRefresh,
onGnssModeChange,
onSimCenterLatChange,
onSimCenterLonChange,
onSimCenterApply,
vectorSource,
onVectorSourceChange,
tilesets,
activeTileset,
onActivateTileset,
@ -74,6 +94,44 @@ export const SettingsPanel = ({
</div>
</div>
<div className="panel-header">
<div>
<div className="panel-title">GNSS</div>
<div className="panel-subtitle">Режим и точка симуляции</div>
</div>
<button type="button" className="btn ghost small" onClick={onGnssRefresh}>
Обновить
</button>
</div>
<label className="field">
<span>Режим</span>
<select
value={gnssMode}
onChange={(e) => onGnssModeChange(e.target.value as "simulated" | "serial")}
disabled={gnssMode === "unknown"}
>
<option value="simulated">Симуляция</option>
<option value="serial">Реальный (Serial NMEA)</option>
</select>
</label>
{gnssMode === "simulated" && (
<div className="slice-grid">
<label className="field">
<span>Lat</span>
<input value={simCenterLat} onChange={(e) => onSimCenterLatChange(e.target.value)} />
</label>
<label className="field">
<span>Lon</span>
<input value={simCenterLon} onChange={(e) => onSimCenterLonChange(e.target.value)} />
</label>
<button type="button" className="btn ghost" onClick={onSimCenterApply}>
Применить
</button>
</div>
)}
<div className="panel-header">
<div>
<div className="panel-title">Тема</div>
@ -103,6 +161,14 @@ export const SettingsPanel = ({
</button>
</div>
<label className="field">
<span>Векторный источник</span>
<select value={vectorSource} onChange={(e) => onVectorSourceChange(e.target.value as "mbtiles" | "tileset")}>
<option value="mbtiles">MBTiles (/tiles/vector)</option>
<option value="tileset">Tileset папка (/tilesets/active/vector)</option>
</select>
</label>
<div className="panel-header">
<div>
<div className="panel-title">Нарезка тайлов</div>

View file

@ -27,10 +27,15 @@ export type AppConfig = {
tiles_url_tms?: string;
vector_tiles_url?: string;
vector_metadata_url?: string;
vector_tileset_url?: string;
vector_tileset_metadata_url?: string;
tilesets_url?: string;
tilesets_activate_url?: string;
ws_url: string;
gnss_source: string;
gnss_state_url?: string;
gnss_mode_url?: string;
gnss_sim_center_url?: string;
};
export type TrackPoint = {