performance improvement

This commit is contained in:
2026-04-28 16:17:46 +05:30
parent 1f70e01d89
commit ace6c4a4f3
4 changed files with 400 additions and 63 deletions

View File

@@ -1,11 +1,12 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { invoke, IS_BROWSER_MODE } from "./tauriShim";
import { open, save } from "@tauri-apps/plugin-dialog";
const IMAGE_FOLDER_KEY = "kml_map_tool.image_folder";
const POPUP_SIZE_KEY = "kml_map_tool.popup_size";
const POPUP_ENABLED_KEY = "kml_map_tool.popup_enabled";
const POPUP_ASPECT_KEY = "kml_map_tool.popup_aspect";
const PERF_MODE_KEY = "kml_map_tool.perf_mode";
type PopupSize = "S" | "M" | "L" | "XL";
const POPUP_WIDTH_PX: Record<PopupSize, number> = {
@@ -104,6 +105,50 @@ export default function App() {
}} />;
}
// Diagnostic FPS overlay. Mounted only when Settings → Performance mode is on.
// Updates twice a second so the readout is stable enough to read while panning.
function FpsCounter() {
const [fps, setFps] = useState(0);
useEffect(() => {
let raf = 0;
let frames = 0;
let last = performance.now();
const tick = () => {
frames++;
const now = performance.now();
if (now - last >= 500) {
setFps(Math.round((frames * 1000) / (now - last)));
frames = 0;
last = now;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
return (
<div
style={{
position: "fixed",
top: 8,
left: "50%",
transform: "translateX(-50%)",
zIndex: 9000,
background: "rgba(0,0,0,0.75)",
color: fps >= 50 ? "#7CFC00" : fps >= 30 ? "#FFD700" : "#FF6B6B",
padding: "4px 10px",
borderRadius: 4,
fontFamily: "monospace",
fontSize: 12,
fontWeight: 700,
pointerEvents: "none",
}}
>
{fps} fps · perf
</div>
);
}
function AppShell({
currentUser,
onLogout,
@@ -111,7 +156,11 @@ function AppShell({
currentUser: CurrentUser;
onLogout: () => void;
}) {
const [viewport, setViewport] = useState<Viewport>(INITIAL_VIEWPORT);
const [viewport, setViewport] = useState<Viewport>(
IS_BROWSER_MODE
? { center: [78.347, 17.398], zoom: 14 }
: INITIAL_VIEWPORT,
);
const [bounds, setBounds] = useState<BoundsBox | null>(null);
const [primaryId, setPrimaryId] = useState<string>(DEFAULT_BASEMAP_ID);
const [secondaryId, setSecondaryId] = useState<string>("esri-imagery");
@@ -182,6 +231,13 @@ function AppShell({
const v = window.localStorage.getItem(POPUP_ASPECT_KEY) as PopupAspect | null;
return v && v in POPUP_ASPECT_VAL ? v : "4:3";
});
// Diagnostic: while pan/zoom is active, render only basemap + asset
// ScatterplotLayer + range PathLayer. Lets us isolate whether residual
// lag is from layer complexity vs the WebView itself.
const [perfMode, setPerfMode] = useState<boolean>(() => {
if (typeof window === "undefined") return false;
return window.localStorage.getItem(PERF_MODE_KEY) === "1";
});
const [filters, setFilters] = useState<Filters>({
videos: new Set(),
sides: new Set(),
@@ -323,6 +379,9 @@ function AppShell({
useEffect(() => {
window.localStorage.setItem(POPUP_ENABLED_KEY, popupEnabled ? "1" : "0");
}, [popupEnabled]);
useEffect(() => {
window.localStorage.setItem(PERF_MODE_KEY, perfMode ? "1" : "0");
}, [perfMode]);
// Counts on the current bbox slice (live update as user pans/zooms).
const counts = useMemo(() => {
@@ -887,15 +946,43 @@ function AppShell({
const list = await invoke<
Array<{ id: number; name: string | null; geojson: string }>
>("get_osm_roads");
setOsmRoads({
type: "FeatureCollection",
features: list.map((r) => ({
type: "Feature",
geometry: JSON.parse(r.geojson),
let mnLat = Infinity;
let mxLat = -Infinity;
let mnLng = Infinity;
let mxLng = -Infinity;
const features = list.map((r) => {
const geom = JSON.parse(r.geojson) as {
type: string;
coordinates: Array<[number, number]>;
};
if (geom.type === "LineString" && Array.isArray(geom.coordinates)) {
for (const c of geom.coordinates) {
const lng = c[0];
const lat = c[1];
if (
typeof lat === "number" &&
typeof lng === "number" &&
isFinite(lat) &&
isFinite(lng)
) {
if (lat < mnLat) mnLat = lat;
if (lat > mxLat) mxLat = lat;
if (lng < mnLng) mnLng = lng;
if (lng > mxLng) mxLng = lng;
}
}
}
return {
type: "Feature" as const,
geometry: geom,
properties: { id: r.id, name: r.name },
})),
};
});
return list;
setOsmRoads({ type: "FeatureCollection", features });
const bounds = Number.isFinite(mnLat)
? { minLat: mnLat, maxLat: mxLat, minLng: mnLng, maxLng: mxLng }
: null;
return { list, bounds };
}
async function refreshAssetNames() {
@@ -1219,8 +1306,30 @@ function AppShell({
}
setStatus("Importing OSM roads…");
const count = await invoke<number>("import_osm_geojson", args);
await refreshOsmRoads();
setStatus(`Loaded ${count} road segment(s)`);
const { bounds } = await refreshOsmRoads();
// 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) {
const center: [number, number] = [
(bounds.minLng + bounds.maxLng) / 2,
(bounds.minLat + bounds.maxLat) / 2,
];
const dLat = bounds.maxLat - bounds.minLat;
const dLng = bounds.maxLng - bounds.minLng;
const span = Math.max(dLat, dLng);
// Pick a zoom level that roughly fits the bbox in the viewport.
// Web Mercator: at z=0 the world spans 360°, so zoom for a span s
// is ~log2(360 / s) a small margin for padding.
const zoom =
span > 0 ? Math.max(8, Math.min(17, Math.log2(360 / span) - 0.5)) : 14;
setViewport({ center, zoom });
setStatus(
`Loaded ${count} road segment(s). Centred on imported area.`,
);
} else {
setStatus(`Loaded ${count} road segment(s)`);
}
} catch (e) {
setStatus(`OSM import failed: ${e}`);
} finally {
@@ -2073,6 +2182,7 @@ function AppShell({
return (
<div className="app">
{perfMode && <FpsCounter />}
<div className="map-area">
<MapView
basemapId={primaryId}
@@ -2158,6 +2268,7 @@ function AppShell({
onPlaceAt={handlePlaceAt}
imageFolder={imageFolder}
popupEnabled={popupEnabled}
perfMode={perfMode}
anchorIds={Array.from(anchors.keys())}
colorBy={colorBy}
osmEditMode={osmEditMode}
@@ -2220,6 +2331,7 @@ function AppShell({
onPositionChange={handlePositionChange}
imageFolder={imageFolder}
popupEnabled={popupEnabled}
perfMode={perfMode}
anchorIds={Array.from(anchors.keys())}
colorBy={
new Set(filteredAssets.map((a) => displayVideo(a.video_name)))
@@ -2864,6 +2976,18 @@ function AppShell({
/>
Show image popup when an asset is selected
</label>
<h4 style={{ marginTop: 12 }}>Diagnostics</h4>
<label style={{ display: "flex", alignItems: "center", gap: 6 }}>
<input
type="checkbox"
checked={perfMode}
onChange={(e) => setPerfMode(e.target.checked)}
/>
Performance mode: while panning/zooming, show only basemap +
asset markers + range paths (hides roads, scope, metadata, links,
labels). Diagnostic leave off for normal use.
</label>
<h4 style={{ marginTop: 12 }}>Popup width</h4>
<div className="size-picker">
{(["S", "M", "L", "XL"] as PopupSize[]).map((s) => {