osm_bug fix

This commit is contained in:
2026-05-02 11:03:27 +05:30
parent ace6c4a4f3
commit 2139a908e6
5 changed files with 1167 additions and 170 deletions

View File

@@ -149,6 +149,39 @@ function FpsCounter() {
);
}
// Lightweight indicator while the Overpass fetch is in flight. Single line,
// no animation, no fixed positioning — just a small chip in the corner so
// the user knows the click is doing something.
function OsmFetchIndicator() {
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
const start = performance.now();
const id = window.setInterval(() => {
setElapsed(Math.floor((performance.now() - start) / 1000));
}, 500);
return () => window.clearInterval(id);
}, []);
return (
<div
style={{
position: "fixed",
bottom: 12,
right: 12,
zIndex: 9500,
background: "rgba(20, 20, 20, 0.8)",
color: "#fff",
padding: "6px 10px",
borderRadius: 4,
fontFamily: "system-ui, sans-serif",
fontSize: 12,
pointerEvents: "none",
}}
>
Fetching OSM {elapsed}s
</div>
);
}
function AppShell({
currentUser,
onLogout,
@@ -238,6 +271,7 @@ function AppShell({
if (typeof window === "undefined") return false;
return window.localStorage.getItem(PERF_MODE_KEY) === "1";
});
const [osmFetchActive, setOsmFetchActive] = useState<boolean>(false);
const [filters, setFilters] = useState<Filters>({
videos: new Set(),
sides: new Set(),
@@ -1155,7 +1189,11 @@ function AppShell({
refreshScopePolygons().catch((e) =>
setStatus(`Scope load failed: ${e}`),
);
refreshOsmRoads().catch(() => {});
// Surface OSM load failures — silent catch made startup look fine
// even when the roads table couldn't be read.
refreshOsmRoads().catch((e) =>
setStatus(`OSM roads load failed: ${e}`),
);
refreshActions().catch(() => {});
refreshAssetNames().catch(() => {});
refreshVideoNames().catch(() => {});
@@ -1234,6 +1272,7 @@ function AppShell({
coords: drawRoadCoords,
});
await refreshOsmRoads();
await refreshDataCounts();
setDrawRoadMode(false);
setDrawRoadCoords([]);
setEditingRoadId(newId);
@@ -1252,6 +1291,7 @@ function AppShell({
try {
await invoke("delete_osm_road", { id: editingRoadId });
await refreshOsmRoads();
await refreshDataCounts();
setEditingRoadId(null);
setStatus(`Deleted road #${editingRoadId}.`);
} catch (e) {
@@ -1304,13 +1344,26 @@ function AppShell({
"tertiary_link",
];
}
setStatus("Importing OSM roads…");
setStatus(`Importing OSM roads from ${picked.split(/[\\/]/).pop()}`);
const count = await invoke<number>("import_osm_geojson", args);
const { bounds } = await refreshOsmRoads();
// Keep the sidebar Loaded-data card in sync. Without this the OSM
// count stayed at its last value, which made imports look like they
// hadn't done anything.
await refreshDataCounts();
// Auto-recentre the map on the imported road bbox. Without this, if
// the user's current viewport is far from the imported area, the
// roads render off-screen and the import looks silently broken.
if (bounds) {
if (count === 0) {
// Explicit message — silent "Loaded 0" looked like the import
// did nothing. Common cause: the major-only filter removed every
// road class in the file, or the GeoJSON had no LineStrings.
setStatus(
majorOnly
? `0 road segments matched the major-only filter. Re-import and click Cancel on the filter prompt to import all classes.`
: `0 road segments imported. Check that the GeoJSON has LineString features.`,
);
} else if (bounds) {
const center: [number, number] = [
(bounds.minLng + bounds.maxLng) / 2,
(bounds.minLat + bounds.maxLat) / 2,
@@ -1356,33 +1409,54 @@ function AppShell({
async function handleGenerateOsm() {
if (busy) return;
if (!boundsRef.current) {
setStatus("Pan/zoom the map first.");
return;
}
const folder = await ensureOsmFolder();
if (!folder) return;
const b = boundsRef.current;
const sep =
folder.endsWith("/") || folder.endsWith("\\") ? "" : "/";
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const path = `${folder}${sep}osm_${b.south.toFixed(4)}_${b.west.toFixed(4)}_${b.north.toFixed(4)}_${b.east.toFixed(4)}_${ts}.geojson`;
const hasAssets = dataCounts.fixed_assets + dataCounts.range_assets > 0;
setBusy(true);
setStatus("Fetching OSM from Overpass…");
setOsmFetchActive(true);
try {
const n = await invoke<number>("generate_osm_for_bbox", {
minLat: b.south,
minLng: b.west,
maxLat: b.north,
maxLng: b.east,
path,
});
setStatus(
`Generated ${n} OSM way(s) → ${path}. Use Import → OSM roads to load.`,
);
if (hasAssets) {
// Tight per-asset filter: fetch a small candidate set with
// around:60m, then keep only roads where ≥2 assets sit within 30 m
// of the centerline. That way the output is the road(s) the
// assets are actually on, not every street that happens to be
// within bbox or radius.
setStatus("Fetching OSM near assets from Overpass…");
const path = `${folder}${sep}osm_on_asset_roads_${ts}.geojson`;
const n = await invoke<number>("generate_osm_near_assets", {
radiusM: 60,
maxSamples: 800,
path,
});
setStatus(
`Generated ${n} road(s) carrying assets → ${path}. Use Import → OSM roads to load.`,
);
} else if (boundsRef.current) {
const b = boundsRef.current;
const path = `${folder}${sep}osm_${b.south.toFixed(4)}_${b.west.toFixed(4)}_${b.north.toFixed(4)}_${b.east.toFixed(4)}_${ts}.geojson`;
setStatus("Fetching OSM for visible viewport from Overpass…");
const n = await invoke<number>("generate_osm_for_bbox", {
minLat: b.south,
minLng: b.west,
maxLat: b.north,
maxLng: b.east,
path,
});
setStatus(
`Generated ${n} OSM way(s) from viewport → ${path}. Use Import → OSM roads to load.`,
);
} else {
setStatus(
"No assets imported and no map viewport set. Import assets or pan/zoom the map first.",
);
}
} catch (e) {
setStatus(`Generate OSM failed: ${e}`);
} finally {
setOsmFetchActive(false);
setBusy(false);
}
}
@@ -1422,6 +1496,8 @@ function AppShell({
}
}
await refreshOsmRoads();
// Sidebar count would otherwise show pre-prune total.
await refreshDataCounts();
setOsmToolsOpen(false);
setStatus(
summary.length > 0 ? `OSM tools: ${summary.join("; ")}.` : "Nothing selected.",
@@ -1688,7 +1764,7 @@ function AppShell({
>;
for (const e of merged) {
if (e.kind !== "target") continue;
// Find bracketing anchors by rank.
// Find bracketing anchors using row_id ordering (rank in the merged list).
let left: (typeof anchorByRank)[number] | null = null;
let right: (typeof anchorByRank)[number] | null = null;
for (const a of anchorByRank) {
@@ -1701,8 +1777,35 @@ function AppShell({
let dlat: number;
let dlng: number;
if (left && right) {
const span = right.rank - left.rank;
const tt = span > 0 ? (e.rank - left.rank) / span : 0;
// Interpolate by **geographic position** of the target projected
// onto the line segment between the bracketing anchors' original
// positions — i.e. along the road direction. This replaces the
// previous rank-based tt, which over-corrected clustered targets
// toward the next anchor regardless of how close they actually
// were geographically. Latitude is scaled by cos(centre_lat) so
// the projection isn't distorted by lat/lng's non-isotropic
// metric at the dataset's latitude (Hyderabad ≈ 17°: 1° lng is
// ~95 % of 1° lat there).
const lx = left.anchor.origLng;
const ly = left.anchor.origLat;
const rx = right.anchor.origLng;
const ry = right.anchor.origLat;
const lat0 = ((ly + ry) / 2) * (Math.PI / 180);
const cosLat = Math.cos(lat0);
const tx = (e.asset.lng as number) - lx;
const ty = (e.asset.lat as number) - ly;
const sx = rx - lx;
const sy = ry - ly;
// Scale lng deltas by cosLat so the dot product is in metres²-equiv.
const seg2 = sx * sx * cosLat * cosLat + sy * sy;
let tt: number;
if (seg2 > 0) {
const dot = tx * sx * cosLat * cosLat + ty * sy;
tt = Math.max(0, Math.min(1, dot / seg2));
} else {
// Degenerate: anchors coincide. Fall back to left's offset.
tt = 0;
}
dlat = left.anchor.dlat + (right.anchor.dlat - left.anchor.dlat) * tt;
dlng = left.anchor.dlng + (right.anchor.dlng - left.anchor.dlng) * tt;
} else {
@@ -2183,6 +2286,7 @@ function AppShell({
return (
<div className="app">
{perfMode && <FpsCounter />}
{osmFetchActive && <OsmFetchIndicator />}
<div className="map-area">
<MapView
basemapId={primaryId}
@@ -2886,10 +2990,10 @@ function AppShell({
setOsmToolsOpen(false);
handleGenerateOsm();
}}
disabled={busy || !boundsRef.current}
title="Fetch OSM ways for the visible map area from Overpass and save to your OSM folder"
disabled={busy}
title="Fetch OSM ways from Overpass for the imported assets' bbox (with a small buffer for nearby highways). Falls back to the visible map area when no assets are loaded."
>
Generate (visible bbox)
Generate roads KML
</button>
<button
className="primary-btn"