add vector mbtiles
This commit is contained in:
parent
c3fe22fb4a
commit
60fa1eec06
10 changed files with 628 additions and 18 deletions
|
|
@ -146,6 +146,18 @@ body, #root {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.maplibre {
|
||||
height: 70vh;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-switch {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { MapContainer, Marker, Polyline, TileLayer, useMap } from "react-leaflet
|
|||
import L, { LatLngExpression } from "leaflet";
|
||||
import "./App.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
type FixMessage = {
|
||||
type: "fix";
|
||||
|
|
@ -27,6 +28,8 @@ type IncomingMessage = FixMessage | StatusMessage;
|
|||
type AppConfig = {
|
||||
tiles_url: string;
|
||||
tiles_url_tms?: string;
|
||||
vector_tiles_url?: string;
|
||||
vector_metadata_url?: string;
|
||||
ws_url: string;
|
||||
gnss_source: string;
|
||||
};
|
||||
|
|
@ -73,6 +76,7 @@ export default function App() {
|
|||
const [status, setStatus] = useState<string>("connecting");
|
||||
const [track, setTrack] = useState<TrackPoint[]>([]);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const [mapMode, setMapMode] = useState<"raster" | "vector">("raster");
|
||||
const [selectedFileName, setSelectedFileName] = useState<string>("");
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [sliceParams, setSliceParams] = useState({
|
||||
|
|
@ -85,6 +89,8 @@ export default function App() {
|
|||
source_format: "geotiff" as "geotiff" | "mbtiles" | "tms" | "xyz",
|
||||
});
|
||||
const [sliceStatus, setSliceStatus] = useState<string>("idle");
|
||||
const maplibreContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const maplibreRef = useRef<any>(null);
|
||||
|
||||
const uploadFileBlob = async (file: File) => {
|
||||
setSliceStatus("uploading");
|
||||
|
|
@ -100,6 +106,11 @@ export default function App() {
|
|||
setSelectedFileName(res.filename);
|
||||
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");
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setSliceStatus("error");
|
||||
|
|
@ -201,6 +212,181 @@ export default function App() {
|
|||
: config.tiles_url || "/tiles/{z}/{x}/{y}.png"
|
||||
}`;
|
||||
|
||||
const vectorTileUrl =
|
||||
config && `${backendBase.replace(/\/+$/, "")}${config.vector_tiles_url || "/tiles/vector/{z}/{x}/{y}.pbf"}`;
|
||||
|
||||
const vectorMetaUrl =
|
||||
config && `${backendBase.replace(/\/+$/, "")}${config.vector_metadata_url || "/tiles/vector/metadata"}`;
|
||||
|
||||
const activateVectorMbtiles = async (path: string) => {
|
||||
const res = await fetch(`${backendBase}/tiles/vector/activate`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
|
||||
sendFrontLog(`vector mbtiles activated: ${path}`, "info");
|
||||
setMapMode("vector");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mapMode !== "vector") return;
|
||||
if (!maplibreContainerRef.current) return;
|
||||
if (!vectorMetaUrl || !vectorTileUrl) return;
|
||||
|
||||
let canceled = false;
|
||||
(async () => {
|
||||
const metaRes = await fetch(vectorMetaUrl, { cache: "no-store" });
|
||||
if (!metaRes.ok) return;
|
||||
const meta = await metaRes.json();
|
||||
const vectorLayersRaw = meta.vector_layers || meta["json"] || null;
|
||||
let sourceLayers: string[] = [];
|
||||
try {
|
||||
const parsed = typeof vectorLayersRaw === "string" ? JSON.parse(vectorLayersRaw) : vectorLayersRaw;
|
||||
const layers = parsed?.vector_layers ?? parsed?.layers ?? parsed ?? [];
|
||||
if (Array.isArray(layers)) {
|
||||
sourceLayers = layers.map((l: any) => l.id).filter(Boolean);
|
||||
}
|
||||
} catch {
|
||||
sourceLayers = [];
|
||||
}
|
||||
|
||||
const { default: maplibregl } = await import("maplibre-gl");
|
||||
if (canceled) return;
|
||||
|
||||
if (maplibreRef.current) {
|
||||
try {
|
||||
maplibreRef.current.remove();
|
||||
} catch {}
|
||||
maplibreRef.current = null;
|
||||
}
|
||||
|
||||
const style: any = {
|
||||
version: 8,
|
||||
sources: {
|
||||
osm: {
|
||||
type: "vector",
|
||||
tiles: [vectorTileUrl],
|
||||
minzoom: Number(meta.minzoom ?? 0),
|
||||
maxzoom: Number(meta.maxzoom ?? 14),
|
||||
},
|
||||
track: {
|
||||
type: "geojson",
|
||||
data: { type: "FeatureCollection", features: [] },
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{ id: "bg", type: "background", paint: { "background-color": "#050816" } },
|
||||
],
|
||||
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
||||
};
|
||||
|
||||
for (const id of sourceLayers) {
|
||||
style.layers.push({
|
||||
id: `fill-${id}`,
|
||||
type: "fill",
|
||||
source: "osm",
|
||||
"source-layer": id,
|
||||
filter: ["==", ["geometry-type"], "Polygon"],
|
||||
paint: { "fill-color": "#1f2937", "fill-opacity": 0.35 },
|
||||
});
|
||||
style.layers.push({
|
||||
id: `linepoly-${id}`,
|
||||
type: "line",
|
||||
source: "osm",
|
||||
"source-layer": id,
|
||||
filter: ["==", ["geometry-type"], "Polygon"],
|
||||
paint: { "line-color": "#334155", "line-width": 1 },
|
||||
});
|
||||
style.layers.push({
|
||||
id: `line-${id}`,
|
||||
type: "line",
|
||||
source: "osm",
|
||||
"source-layer": id,
|
||||
filter: ["==", ["geometry-type"], "LineString"],
|
||||
paint: { "line-color": "#475569", "line-width": 1.2 },
|
||||
});
|
||||
style.layers.push({
|
||||
id: `point-${id}`,
|
||||
type: "circle",
|
||||
source: "osm",
|
||||
"source-layer": id,
|
||||
filter: ["==", ["geometry-type"], "Point"],
|
||||
paint: { "circle-radius": 2, "circle-color": "#94a3b8", "circle-opacity": 0.8 },
|
||||
});
|
||||
}
|
||||
|
||||
style.layers.push(
|
||||
{
|
||||
id: "track-line",
|
||||
type: "line",
|
||||
source: "track",
|
||||
paint: { "line-color": "#7dd3fc", "line-width": 4, "line-opacity": 0.9 },
|
||||
},
|
||||
{
|
||||
id: "track-point",
|
||||
type: "circle",
|
||||
source: "track",
|
||||
paint: { "circle-radius": 6, "circle-color": "#a78bfa", "circle-stroke-color": "#ffffff", "circle-stroke-width": 1 },
|
||||
},
|
||||
);
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: maplibreContainerRef.current!,
|
||||
style,
|
||||
center: [37.618423, 55.751244],
|
||||
zoom: 11,
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), "top-right");
|
||||
|
||||
map.on("load", () => {
|
||||
const bounds = meta.bounds;
|
||||
if (typeof bounds === "string") {
|
||||
const parts = bounds.split(",").map((p: string) => Number(p.trim()));
|
||||
if (parts.length === 4 && parts.every((n: number) => Number.isFinite(n))) {
|
||||
map.fitBounds(
|
||||
[
|
||||
[parts[0], parts[1]],
|
||||
[parts[2], parts[3]],
|
||||
],
|
||||
{ padding: 30, duration: 0 },
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
maplibreRef.current = map;
|
||||
})().catch((err) => {
|
||||
sendFrontLog(`maplibre init error: ${err}`, "error");
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [mapMode, vectorMetaUrl, vectorTileUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mapMode !== "vector") return;
|
||||
const map = maplibreRef.current;
|
||||
if (!map) return;
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
positions.length
|
||||
? { type: "Feature", geometry: { type: "LineString", coordinates: positions.map((p: any) => [p[1], p[0]]) } }
|
||||
: null,
|
||||
lastPoint ? { type: "Feature", geometry: { type: "Point", coordinates: [lastPoint.lon, lastPoint.lat] } } : null,
|
||||
].filter(Boolean),
|
||||
};
|
||||
try {
|
||||
const src = map.getSource("track");
|
||||
if (src && src.setData) {
|
||||
src.setData(geojson);
|
||||
}
|
||||
} catch {}
|
||||
}, [mapMode, positions, lastPoint]);
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<header className="topbar">
|
||||
|
|
@ -236,19 +422,39 @@ export default function App() {
|
|||
</section>
|
||||
|
||||
<section className="panel map-panel">
|
||||
{tileUrl ? (
|
||||
<MapContainer center={defaultCenter} zoom={13} scrollWheelZoom className="map">
|
||||
<TileLayer url={tileUrl} attribution="Local tiles" tms={sliceParams.scheme === "tms"} />
|
||||
{positions.length > 0 && (
|
||||
<>
|
||||
<Polyline positions={positions} pathOptions={{ color: "#7dd3fc", weight: 5, opacity: 0.9 }} />
|
||||
<Marker position={positions[positions.length - 1]} icon={markerIcon} />
|
||||
</>
|
||||
)}
|
||||
<FlyTo point={lastPoint} />
|
||||
</MapContainer>
|
||||
<div className="map-switch">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${mapMode === "raster" ? "primary" : "ghost"}`}
|
||||
onClick={() => setMapMode("raster")}
|
||||
>
|
||||
Raster
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${mapMode === "vector" ? "primary" : "ghost"}`}
|
||||
onClick={() => setMapMode("vector")}
|
||||
>
|
||||
Vector (MBTiles)
|
||||
</button>
|
||||
</div>
|
||||
{mapMode === "raster" ? (
|
||||
tileUrl ? (
|
||||
<MapContainer center={defaultCenter} zoom={13} scrollWheelZoom className="map">
|
||||
<TileLayer url={tileUrl} attribution="Local tiles" tms={sliceParams.scheme === "tms"} />
|
||||
{positions.length > 0 && (
|
||||
<>
|
||||
<Polyline positions={positions} pathOptions={{ color: "#7dd3fc", weight: 5, opacity: 0.9 }} />
|
||||
<Marker position={positions[positions.length - 1]} icon={markerIcon} />
|
||||
</>
|
||||
)}
|
||||
<FlyTo point={lastPoint} />
|
||||
</MapContainer>
|
||||
) : (
|
||||
<div className="empty">Нет конфигурации тайлов</div>
|
||||
)
|
||||
) : (
|
||||
<div className="empty">Нет конфигурации тайлов</div>
|
||||
<div className="maplibre" ref={maplibreContainerRef} />
|
||||
)}
|
||||
</section>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue