feat(import): selective GPX/KML element import and performance improvements

Add type-selector UI in the file import modal letting users choose which
GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points,
paths) to import. KML LineString placemarks are now imported as path
places with route_geometry.

Performance improvements:
- Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut
  unnecessary re-renders in PlacesSidebar
- Add weatherQueue to cap concurrent weather fetches at 3
- Replace sequential per-place deletes with a single bulkDelete API call
  (new DELETE /places/bulk endpoint + deletePlacesMany service)
- Memoize atlas/photo/weather service calls to avoid redundant requests
- Add multi-select mode to PlacesSidebar for bulk operations

Add large GPX/KML/KMZ fixtures for integration/perf testing and two
profiler analysis scripts under scripts/.
This commit is contained in:
jubnl
2026-04-18 01:28:37 +02:00
parent 9a31fcac7b
commit 6a718fccea
45 changed files with 22471 additions and 285 deletions
+29
View File
@@ -71,6 +71,30 @@ const UA = 'TREK Travel Planner (https://github.com/mauriceboe/TREK)';
// ── Photo cache (disk-backed) ────────────────────────────────────────────────
import * as placePhotoCache from './placePhotoCache';
// ── Concurrency limiter for outbound photo fetches ───────────────────────────
// Caps simultaneous Wikimedia/Google photo requests so a bulk import of hundreds
// of places cannot monopolise the event loop or trigger external API rate limits.
const MAX_CONCURRENT_PHOTO_FETCHES = 5;
let photoFetchActive = 0;
const photoFetchQueue: Array<() => void> = [];
function acquirePhotoFetchSlot(): Promise<void> {
if (photoFetchActive < MAX_CONCURRENT_PHOTO_FETCHES) {
photoFetchActive++;
return Promise.resolve();
}
return new Promise(resolve => photoFetchQueue.push(resolve));
}
function releasePhotoFetchSlot(): void {
const next = photoFetchQueue.shift();
if (next) {
next();
} else {
photoFetchActive--;
}
}
// ── API key retrieval ────────────────────────────────────────────────────────
export function getMapsKey(userId: number): string | null {
@@ -597,6 +621,8 @@ export async function getPlacePhoto(
}
const fetchPromise = (async (): Promise<{ filePath: string; attribution: string | null } | null> => {
await acquirePhotoFetchSlot();
try {
const apiKey = getMapsKey(userId);
const isCoordLookup = placeId.startsWith('coords:');
@@ -676,6 +702,9 @@ export async function getPlacePhoto(
}
return { filePath: cached.filePath, attribution };
} finally {
releasePhotoFetchSlot();
}
})();
placePhotoCache.setInFlight(placeId, fetchPromise);