performance improvement
This commit is contained in:
146
src/App.tsx
146
src/App.tsx
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user