osm_bug fix
This commit is contained in:
156
src/App.tsx
156
src/App.tsx
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user