Two issues from the first derived-regions pass:
- The 'Resolving N...' counter only updated when the whole pass finished, so it
looked frozen. fillDerivedRegions now reports progress per pin, the hook ticks
the counter down, and the card refreshes every 5 resolved pins so the numbers
climb while you watch.
- A country could appear twice ('United Kingdom' 1 + 1) when one pin carried a
country_code and another only a name. Learn name->code from pins that have
both and fold name-only pins into the same bucket.
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
/**
|
|
* useDeriveRegions — keeps the stats' "Spots & Shops by Region" card in sync with
|
|
* the map by resolving a region for untagged pins.
|
|
*
|
|
* It scans the community relay for spots/shops that have no `city`/`country`
|
|
* tag, reverse-geocodes their pin coordinates ONCE (cached in localStorage by
|
|
* `lib/derivedRegions`), then invalidates the stats query so the region card
|
|
* picks the derived regions up. Mount it once on the stats page.
|
|
*
|
|
* Nominatim allows ~1 request/second, so the first run on a fresh device takes a
|
|
* while; `pending` lets the UI say so. Later visits read the cache instantly.
|
|
*/
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
import { SKATE_SPOT_KIND, SKATE_SHOP_KIND } from '@/lib/types';
|
|
import { coordsOf, getDerivedRegion, fillDerivedRegions } from '@/lib/derivedRegions';
|
|
|
|
export interface DeriveRegionsState {
|
|
/** Untagged pins still awaiting a (cached) reverse geocode. */
|
|
pending: number;
|
|
}
|
|
|
|
export function useDeriveRegions(): DeriveRegionsState {
|
|
const { queryCanonical } = useSkateRelays();
|
|
const queryClient = useQueryClient();
|
|
const running = useRef(false);
|
|
const [pending, setPending] = useState(0);
|
|
|
|
const scan = useQuery({
|
|
queryKey: ['derive-regions-scan'],
|
|
// One scan per five minutes is plenty — this only changes when a new
|
|
// untagged pin appears.
|
|
staleTime: 5 * 60 * 1000,
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(15000);
|
|
const [spots, shops] = await Promise.all([
|
|
queryCanonical([{ kinds: [SKATE_SPOT_KIND], '#t': ['skatespot'], limit: 2000 }], { signal }).catch(() => []),
|
|
queryCanonical([{ kinds: [SKATE_SHOP_KIND], limit: 2000 }], { signal }).catch(() => []),
|
|
]);
|
|
|
|
const coords: Array<{ lat: number; lng: number }> = [];
|
|
for (const event of [...spots, ...shops]) {
|
|
const tagged = event.tags.some(
|
|
([name, value]) => (name === 'city' || name === 'country') && !!value?.trim(),
|
|
);
|
|
if (tagged) continue;
|
|
const c = coordsOf(event);
|
|
if (c) coords.push(c);
|
|
}
|
|
return coords;
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
const coords = scan.data;
|
|
if (!coords || running.current) return;
|
|
|
|
const misses = coords.filter((c) => !getDerivedRegion(c.lat, c.lng));
|
|
setPending(misses.length);
|
|
if (misses.length === 0) return;
|
|
|
|
running.current = true;
|
|
void (async () => {
|
|
let lastRefresh = 0;
|
|
try {
|
|
await fillDerivedRegions(misses, (done, total) => {
|
|
setPending(Math.max(0, total - done));
|
|
// Refresh the card as pins resolve (not just at the end) so the numbers
|
|
// climb while you watch, instead of jumping once.
|
|
if (done - lastRefresh >= 5) {
|
|
lastRefresh = done;
|
|
queryClient.invalidateQueries({ queryKey: ['growth-stats'] });
|
|
}
|
|
});
|
|
const unresolved = misses.filter((c) => !getDerivedRegion(c.lat, c.lng)).length;
|
|
setPending(unresolved);
|
|
// Final recompute now that every pin has (or has failed to get) a region.
|
|
queryClient.invalidateQueries({ queryKey: ['growth-stats'] });
|
|
} finally {
|
|
running.current = false;
|
|
}
|
|
})();
|
|
}, [scan.data, queryClient]);
|
|
|
|
return { pending };
|
|
}
|