Fix performance and map tilt glitch

This commit is contained in:
InfoX 2026-10-03 21:31:19 +02:00
parent 5ae40e7787
commit fa5f176004
Signed by: infox
GPG key ID: 0C5A10019B6B50F6
3 changed files with 39 additions and 8 deletions

View file

@ -128,6 +128,11 @@ export default function App() {
const labels = useMemo(() => buildLabels(places), [places]); const labels = useMemo(() => buildLabels(places), [places]);
const placeById = useMemo(() => new Map(realPlaces.map((p) => [p.id, p])), [realPlaces]); const placeById = useMemo(() => new Map(realPlaces.map((p) => [p.id, p])), [realPlaces]);
const savedCounts = useMemo(() => {
const c: Record<Verdict, number> = { match: 0, partial: 0, conflict: 0, incomplete: 0, fail: 0 };
for (const a of assessments.values()) c[a.verdict]++;
return c;
}, [assessments]);
const previewCounts = useCallback((p: Profile): Record<Verdict, number> => { const previewCounts = useCallback((p: Profile): Record<Verdict, number> => {
const c: Record<Verdict, number> = { match: 0, partial: 0, conflict: 0, incomplete: 0, fail: 0 }; const c: Record<Verdict, number> = { match: 0, partial: 0, conflict: 0, incomplete: 0, fail: 0 };
if (index) for (const pl of realPlaces) c[assessPlace(pl, p, index, reports).verdict]++; if (index) for (const pl of realPlaces) c[assessPlace(pl, p, index, reports).verdict]++;
@ -440,6 +445,7 @@ export default function App() {
onDone={() => setView('places')} onDone={() => setView('places')}
onSignOut={signOut} onSignOut={signOut}
preview={previewCounts} preview={previewCounts}
savedCounts={savedCounts}
> >
{!needsProfile && ( {!needsProfile && (
<SettingsPanel <SettingsPanel

View file

@ -145,6 +145,9 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
center: [(w + e) / 2, (s + n) / 2], center: [(w + e) / 2, (s + n) / 2],
zoom: 15, zoom: 15,
minZoom: 12, minZoom: 12,
maxPitch: 0, // flat map only: no two-finger tilt, no ctrl/right-drag tilt
dragRotate: false,
touchPitch: false,
maxBounds: [[w - 0.04, s - 0.03], [e + 0.04, n + 0.03]], maxBounds: [[w - 0.04, s - 0.03], [e + 0.04, n + 0.03]],
attributionControl: { compact: true }, attributionControl: { compact: true },
locale: { locale: {
@ -153,6 +156,8 @@ export function MapView({ places, showPlaces, selectedId, onSelect, route, route
'AttributionControl.ToggleAttribution': t('map.attrToggle'), 'AttributionControl.ToggleAttribution': t('map.attrToggle'),
}, },
}); });
map.touchZoomRotate.disableRotation();
map.keyboard.disableRotation();
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right'); map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right');
map.on('load', () => { map.on('load', () => {
map.addSource('places', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); map.addSource('places', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import type { Account } from '../lib/auth'; import type { Account } from '../lib/auth';
import { useI18n, type Key } from '../lib/i18n'; import { useI18n, type Key } from '../lib/i18n';
import { DEFAULT_PROFILE, PRESETS, activePreset, bucket } from '../lib/profile'; import { DEFAULT_PROFILE, PRESETS, activePreset, bucket } from '../lib/profile';
@ -13,23 +13,43 @@ interface Props {
onDone: () => void; onDone: () => void;
onSignOut: () => void; onSignOut: () => void;
preview: (p: Profile) => Record<Verdict, number>; preview: (p: Profile) => Record<Verdict, number>;
/** Verdict counts for the saved profile, already computed by the app: saves a second pass over every place. */
savedCounts: Record<Verdict, number> | null;
children?: React.ReactNode; children?: React.ReactNode;
} }
const VERDICTS: Verdict[] = ['match', 'partial', 'conflict', 'incomplete', 'fail']; const VERDICTS: Verdict[] = ['match', 'partial', 'conflict', 'incomplete', 'fail'];
export function ProfileView({ account, profile, onChange, onDone, onSignOut, preview, children }: Props) { export function ProfileView({ account, profile, onChange, onDone, onSignOut, preview, savedCounts, children }: Props) {
const { t } = useI18n(); const { t } = useI18n();
const [draft, setDraft] = useState<Profile>(profile ?? DEFAULT_PROFILE[account.preset ?? 'wheelchair']); const [draft, setDraft] = useState<Profile>(profile ?? DEFAULT_PROFILE[account.preset ?? 'wheelchair']);
const update = (p: Profile) => { setDraft(p); if (profile) onChange(p); }; // The draft follows the control instantly; the saved profile (which re-assesses every place) only catches up once changes pause.
const pending = useRef<Profile | null>(null);
const timer = useRef<number | undefined>(undefined);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const flush = () => {
window.clearTimeout(timer.current);
if (pending.current) { onChangeRef.current(pending.current); pending.current = null; }
};
const update = (p: Profile) => {
setDraft(p);
if (!profile) return;
pending.current = p;
window.clearTimeout(timer.current);
timer.current = window.setTimeout(flush, 300);
};
useEffect(() => flush, []); // leaving the page must not drop the last change
const set = <K extends keyof Profile>(k: K, v: Profile[K]) => update({ ...draft, [k]: v }); const set = <K extends keyof Profile>(k: K, v: Profile[K]) => update({ ...draft, [k]: v });
// Debounced live preview: how many places fall into each verdict with these limits. // Live preview: how many places fall into each verdict with these limits. A saved profile reuses the app's own assessments.
const [counts, setCounts] = useState<Record<Verdict, number> | null>(null); const [onboardCounts, setOnboardCounts] = useState<Record<Verdict, number> | null>(null);
useEffect(() => { useEffect(() => {
const id = setTimeout(() => setCounts(preview(draft)), 250); if (profile) return;
const id = setTimeout(() => setOnboardCounts(preview(draft)), 250);
return () => clearTimeout(id); return () => clearTimeout(id);
}, [draft, preview]); }, [profile, draft, preview]);
const counts = profile ? savedCounts : onboardCounts;
const active = activePreset(draft); const active = activePreset(draft);
const presets = PRESETS.filter((p) => p.kind === draft.kind); const presets = PRESETS.filter((p) => p.kind === draft.kind);
@ -151,7 +171,7 @@ export function ProfileView({ account, profile, onChange, onDone, onSignOut, pre
<p className="font-sans-xs text-base-dark margin-top-2"> <p className="font-sans-xs text-base-dark margin-top-2">
<Icon name="lock" size={2} className="margin-right-05" />{t('profile.privacy')} {profile && t('profile.autosave')} <Icon name="lock" size={2} className="margin-right-05" />{t('profile.privacy')} {profile && t('profile.autosave')}
</p> </p>
<button type="button" className="usa-button usa-button--big margin-top-2" onClick={() => { onChange(draft); onDone(); }}> <button type="button" className="usa-button usa-button--big margin-top-2" onClick={() => { pending.current = null; window.clearTimeout(timer.current); onChange(draft); onDone(); }}>
{t('profile.done')} {t('profile.done')}
<Icon name="arrow_forward" size={3} className="margin-left-1" /> <Icon name="arrow_forward" size={3} className="margin-left-1" />
</button> </button>