osm merge, optmisation

This commit is contained in:
2026-05-02 16:44:03 +05:30
parent 2139a908e6
commit ffb27eb17f
9 changed files with 2996 additions and 298 deletions

File diff suppressed because it is too large Load Diff

272
src/LensView.tsx Normal file
View File

@@ -0,0 +1,272 @@
import { useEffect, useRef } from "react";
import maplibregl, { Map as MapLibreMap } from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { basemapById } from "./basemaps";
import type { Asset } from "./types";
import type { OsmRoadFC } from "./MapView";
type Props = {
basemapId: string;
centerLng: number;
centerLat: number;
zoom: number;
diameterPx: number;
osmRoads?: OsmRoadFC;
assets: Asset[];
metadataTrack?: Array<[number, number]>;
};
// Cursor-following magnifier: a circular MapLibre instance that re-projects
// to whatever the user's cursor is hovering on the main map, at +3 zoom. No
// deck.gl — assets and OSM roads render as native MapLibre layers from
// GeoJSON sources so each cursor move is a cheap setCenter() call instead of
// a full layer rebuild.
export function LensView({
basemapId,
centerLng,
centerLat,
zoom,
diameterPx,
osmRoads,
assets,
metadataTrack = [],
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<MapLibreMap | null>(null);
const styleReadyRef = useRef<boolean>(false);
// Hold refs to current data so installLayers can push the live state right
// after creating the empty sources — without this, the lens mounts blank
// until one of the data deps below changes (which often never happens).
const osmRoadsRef = useRef(osmRoads);
osmRoadsRef.current = osmRoads;
const assetsRef = useRef(assets);
assetsRef.current = assets;
const metadataTrackRef = useRef(metadataTrack);
metadataTrackRef.current = metadataTrack;
useEffect(() => {
if (!containerRef.current) return;
const map = new maplibregl.Map({
container: containerRef.current,
style: basemapById(basemapId).style,
center: [centerLng, centerLat],
zoom,
interactive: false,
attributionControl: false,
});
mapRef.current = map;
map.on("load", () => {
styleReadyRef.current = true;
installLayers(map);
pushAllData(map);
});
return () => {
mapRef.current = null;
styleReadyRef.current = false;
map.remove();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Re-install layers when basemap changes (style swap drops sources/layers).
useEffect(() => {
const map = mapRef.current;
if (!map) return;
styleReadyRef.current = false;
map.setStyle(basemapById(basemapId).style);
map.once("styledata", () => {
styleReadyRef.current = true;
installLayers(map);
pushAllData(map);
});
}, [basemapId]);
function pushAllData(map: MapLibreMap) {
const osmSrc = map.getSource("lens-osm-roads") as
| maplibregl.GeoJSONSource
| undefined;
if (osmSrc && osmRoadsRef.current) {
osmSrc.setData(
osmRoadsRef.current as unknown as GeoJSON.FeatureCollection<GeoJSON.Geometry>,
);
}
const assetSrc = map.getSource("lens-assets") as
| maplibregl.GeoJSONSource
| undefined;
if (assetSrc) {
const features = assetsRef.current
.filter(
(a) => typeof a.lat === "number" && typeof a.lng === "number",
)
.map((a) => ({
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [a.lng as number, a.lat as number],
},
properties: { id: a.id, side: a.side ?? "" },
}));
assetSrc.setData({ type: "FeatureCollection", features });
}
const trackSrc = map.getSource("lens-metadata") as
| maplibregl.GeoJSONSource
| undefined;
if (trackSrc) {
const t = metadataTrackRef.current;
if (t.length < 2) {
trackSrc.setData({ type: "FeatureCollection", features: [] });
} else {
trackSrc.setData({
type: "FeatureCollection",
features: [
{
type: "Feature" as const,
geometry: {
type: "LineString" as const,
coordinates: t.slice(),
},
properties: {},
},
],
});
}
}
}
// Push live data into the GeoJSON sources whenever they change.
useEffect(() => {
const map = mapRef.current;
if (!map || !styleReadyRef.current) return;
const src = map.getSource("lens-osm-roads") as
| maplibregl.GeoJSONSource
| undefined;
if (src && osmRoads) {
src.setData(
osmRoads as unknown as GeoJSON.FeatureCollection<GeoJSON.Geometry>,
);
}
}, [osmRoads]);
useEffect(() => {
const map = mapRef.current;
if (!map || !styleReadyRef.current) return;
const src = map.getSource("lens-assets") as
| maplibregl.GeoJSONSource
| undefined;
if (!src) return;
const features = assets
.filter((a) => typeof a.lat === "number" && typeof a.lng === "number")
.map((a) => ({
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [a.lng as number, a.lat as number],
},
properties: { id: a.id, side: a.side ?? "" },
}));
src.setData({ type: "FeatureCollection", features });
}, [assets]);
useEffect(() => {
const map = mapRef.current;
if (!map || !styleReadyRef.current) return;
const src = map.getSource("lens-metadata") as
| maplibregl.GeoJSONSource
| undefined;
if (!src) return;
if (metadataTrack.length < 2) {
src.setData({ type: "FeatureCollection", features: [] });
return;
}
src.setData({
type: "FeatureCollection",
features: [
{
type: "Feature" as const,
geometry: {
type: "LineString" as const,
coordinates: metadataTrack.slice(),
},
properties: {},
},
],
});
}, [metadataTrack]);
// Track the cursor world coordinate via prop changes.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
if (!Number.isFinite(centerLng) || !Number.isFinite(centerLat)) return;
map.jumpTo({ center: [centerLng, centerLat], zoom });
}, [centerLng, centerLat, zoom]);
function installLayers(map: MapLibreMap) {
if (!map.getSource("lens-osm-roads")) {
map.addSource("lens-osm-roads", {
type: "geojson",
data: { type: "FeatureCollection", features: [] },
});
map.addLayer({
id: "lens-osm-roads",
type: "line",
source: "lens-osm-roads",
paint: {
"line-color": "#00e5ff",
"line-width": 2,
"line-opacity": 0.85,
},
});
}
if (!map.getSource("lens-metadata")) {
map.addSource("lens-metadata", {
type: "geojson",
data: { type: "FeatureCollection", features: [] },
});
map.addLayer({
id: "lens-metadata",
type: "line",
source: "lens-metadata",
paint: {
"line-color": "#27ae60",
"line-width": 2,
"line-opacity": 0.9,
"line-dasharray": [2, 2],
},
});
}
if (!map.getSource("lens-assets")) {
map.addSource("lens-assets", {
type: "geojson",
data: { type: "FeatureCollection", features: [] },
});
map.addLayer({
id: "lens-assets",
type: "circle",
source: "lens-assets",
paint: {
"circle-radius": 5,
"circle-color": "#e74c3c",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 1,
},
});
}
}
return (
<div
ref={containerRef}
style={{
width: diameterPx,
height: diameterPx,
borderRadius: "50%",
overflow: "hidden",
boxShadow: "0 4px 16px rgba(0,0,0,0.45)",
border: "2px solid #fff",
background: "#000",
}}
/>
);
}

View File

@@ -27,7 +27,7 @@ export type OsmRoadFC = {
features: Array<{
type: "Feature";
geometry: unknown;
properties: { id: number; name: string | null };
properties: { id: number; name: string | null; snap_ignored?: boolean };
}>;
};
@@ -76,6 +76,9 @@ type Props = {
osmEditMode?: boolean;
editingRoadId?: number | null;
onPickRoad?: (id: number) => void;
onRoadRightClick?: (id: number) => void;
selectedRoadIds?: Set<number>;
multiSelectActive?: boolean;
onUpdateRoad?: (id: number, coords: Array<[number, number]>) => void;
roadOffsetMeters?: number;
drawRoadMode?: boolean;
@@ -89,6 +92,12 @@ type Props = {
) => void;
highlightedIds?: number[];
anchorIds?: number[];
onCursorWorld?: (
lng: number,
lat: number,
screenX: number,
screenY: number,
) => void;
};
// Filled rightward-pointing arrow head, white stroke for legibility on any
@@ -138,6 +147,9 @@ export function MapView({
osmEditMode = false,
editingRoadId = null,
onPickRoad,
onRoadRightClick,
selectedRoadIds,
multiSelectActive = false,
onUpdateRoad,
roadOffsetMeters = 4,
drawRoadMode = false,
@@ -148,6 +160,7 @@ export function MapView({
onLassoComplete,
highlightedIds = [],
anchorIds = [],
onCursorWorld,
}: Props) {
// Diagnostic counters — visible in DevTools console. Should stay flat
// during pure pan/zoom; spikes on filter or import.
@@ -179,6 +192,10 @@ export function MapView({
osmEditModeRef.current = osmEditMode;
const onPickRoadRef = useRef(onPickRoad);
onPickRoadRef.current = onPickRoad;
const onRoadRightClickRef = useRef(onRoadRightClick);
onRoadRightClickRef.current = onRoadRightClick;
const onCursorWorldRef = useRef(onCursorWorld);
onCursorWorldRef.current = onCursorWorld;
const drawRoadModeRef = useRef(drawRoadMode);
drawRoadModeRef.current = drawRoadMode;
const onDrawRoadAddPointRef = useRef(onDrawRoadAddPoint);
@@ -208,20 +225,46 @@ export function MapView({
map.on("contextmenu", (e) => {
const overlay = overlayRef.current;
if (!overlay || !onAssetRightClickRef.current) return;
if (!overlay) return;
const { x, y } = e.point;
const info = overlay.pickObject({ x, y, radius: 8 }) as
| { object?: unknown; layer?: { id?: string } }
| {
object?: unknown;
layer?: { id?: string };
sourceLayer?: { id?: string };
}
| null;
if (!info || !info.object) return;
// An asset record carries `row_id` and `asset_type`; ignore picks that
// landed on roads/scopes/metadata.
// Road right-click takes priority when in OSM edit mode.
if (
osmEditModeRef.current &&
info.layer?.id === "osm-roads" &&
onRoadRightClickRef.current
) {
const f = info.object as { properties?: { id?: number } };
const rid = f.properties?.id;
if (typeof rid === "number") {
e.preventDefault();
onRoadRightClickRef.current(rid);
return;
}
}
if (!onAssetRightClickRef.current) return;
const obj = info.object as Partial<Asset>;
if (typeof obj.row_id !== "string" || typeof obj.id !== "number") return;
e.preventDefault();
onAssetRightClickRef.current(obj as Asset);
});
map.on("mousemove", (e) => {
const cb = onCursorWorldRef.current;
if (!cb) return;
cb(e.lngLat.lng, e.lngLat.lat, e.point.x, e.point.y);
});
map.on("mouseleave", () => {
onCursorWorldRef.current?.(NaN, NaN, NaN, NaN);
});
map.on("moveend", () => {
setIsInteracting(false);
// Sync the React viewport mirror once per gesture so programmatic
@@ -448,8 +491,14 @@ export function MapView({
const layers: unknown[] = [];
// Vertex-edit mode collapses to the single road being dragged so other
// roads don't clutter the editor. But when multi-select is active (focused
// video + bulk select / hide / merge workflow), keep all roads visible.
const visibleOsm: OsmRoadFC | undefined =
osmEditMode && editingRoadId !== null && osmRoads
osmEditMode &&
editingRoadId !== null &&
!multiSelectActive &&
osmRoads
? {
type: "FeatureCollection",
features: osmRoads.features.filter(
@@ -480,10 +529,28 @@ export function MapView({
data: visibleOsm,
stroked: true,
filled: false,
getLineColor: [0, 229, 255, 255],
getLineWidth: 3,
getLineColor: (f: unknown) => {
const p = (f as { properties?: { id?: number; snap_ignored?: boolean } })
.properties;
if (p && selectedRoadIds && typeof p.id === "number" && selectedRoadIds.has(p.id)) {
return [255, 196, 0, 255];
}
if (p?.snap_ignored) return [120, 120, 120, 180];
return [0, 229, 255, 255];
},
getLineWidth: (f: unknown) => {
const p = (f as { properties?: { id?: number } }).properties;
if (p && selectedRoadIds && typeof p.id === "number" && selectedRoadIds.has(p.id)) {
return 5;
}
return 3;
},
lineWidthUnits: "pixels",
pickable: osmEditMode,
updateTriggers: {
getLineColor: [selectedRoadIds],
getLineWidth: [selectedRoadIds],
},
}),
);
if (osmEditMode && roadOffsetMeters > 0) {
@@ -1406,6 +1473,8 @@ export function MapView({
zoomBand,
isInteracting,
perfMode,
selectedRoadIds,
multiSelectActive,
]);
// Manage draggable HTML markers + per-vertex popup for the actively-edited asset.
@@ -1476,7 +1545,22 @@ export function MapView({
}
cap.appendChild(document.createElement("br"));
const small = document.createElement("small");
small.textContent = selectedAsset.row_id;
// row_id · video_name on a single line so users can read both
// at a glance without the popup growing taller.
const rowSpan = document.createElement("span");
rowSpan.textContent = selectedAsset.row_id;
small.appendChild(rowSpan);
if (selectedAsset.video_name && selectedAsset.video_name !== "Not available") {
const sep = document.createElement("span");
sep.style.color = "#888";
sep.style.margin = "0 6px";
sep.textContent = "·";
small.appendChild(sep);
const vidSpan = document.createElement("span");
vidSpan.style.color = "#666";
vidSpan.textContent = selectedAsset.video_name;
small.appendChild(vidSpan);
}
cap.appendChild(small);
root.appendChild(cap);

View File

@@ -132,9 +132,9 @@ const STUBS: Record<string, Stub> = {
data_source_counts: () => ({
fixed_assets: FAKE_ASSETS.length,
range_assets: 0,
scope_polygons: 0,
osm_roads: 0,
metadata_polylines: 0,
scope_features: 0,
video_metadata: 0,
}),
list_video_names: () =>
Array.from(new Set(FAKE_ASSETS.map((a) => a.video_name))),
@@ -145,7 +145,15 @@ const STUBS: Record<string, Stub> = {
get_scope_polygons: () => [],
read_video_polylines_json: () => ({}),
read_metadata_kml: () => [],
list_osm_roads: () => ({ type: "FeatureCollection", features: [] }),
// Backend rename — App invokes get_osm_roads; the legacy list_osm_roads
// alias is kept so older browser fixtures don't crash.
get_osm_roads: () => [],
list_osm_roads: () => [],
get_video_metadata: () => ({}),
save_video_metadata: () => 0,
clear_video_metadata: () => 0,
osm_road_snapshot: () => [],
restore_road_state: () => 0,
};
export function invoke<T = unknown>(