mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
refactor(geography): separate cable sources and bundled lookup modules
This commit is contained in:
1 parent
2213c42200
commit
3ce5d2e454
21 files changed
+2423
-1437
No files matched your search
@@ -29,7 +29,8 @@ function ringAreaKm2(ring) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
const [lon1, lat1] = ring[i];
|
||||
const [lon2, lat2] = ring[(i + 1) % n];
|
||||
sum += toRad(lon2 - lon1) * (2 + Math.sin(toRad(lat1)) + Math.sin(toRad(lat2)));
|
||||
sum +=
|
||||
toRad(lon2 - lon1) * (2 + Math.sin(toRad(lat1)) + Math.sin(toRad(lat2)));
|
||||
}
|
||||
return Math.abs((sum * EARTH_RADIUS_KM * EARTH_RADIUS_KM) / 2);
|
||||
}
|
||||
@@ -37,8 +38,9 @@ function ringAreaKm2(ring) {
|
||||
function haversineKm(lon1, lat1, lon2, lat2) {
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const h = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||
const h =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * EARTH_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||
}
|
||||
|
||||
@@ -62,34 +64,34 @@ function normalizeName(s) {
|
||||
* Keys and values are both in normalizeName() form.
|
||||
*/
|
||||
const ALIASES = {
|
||||
'rockies': 'rocky mountains',
|
||||
'himalaya': 'himalayas',
|
||||
rockies: 'rocky mountains',
|
||||
himalaya: 'himalayas',
|
||||
'the himalaya': 'himalayas',
|
||||
'alps mountains': 'alps',
|
||||
'sahara desert': 'sahara',
|
||||
'gobi': 'gobi desert',
|
||||
'kalahari': 'kalahari desert',
|
||||
'atacama': 'desierto de atacama',
|
||||
gobi: 'gobi desert',
|
||||
kalahari: 'kalahari desert',
|
||||
atacama: 'desierto de atacama',
|
||||
'atacama desert': 'desierto de atacama',
|
||||
'tibetan plateau': 'plateau of tibet',
|
||||
'tibet plateau': 'plateau of tibet',
|
||||
'appalachians': 'appalachian mts',
|
||||
appalachians: 'appalachian mts',
|
||||
'appalachian mountains': 'appalachian mts',
|
||||
'caucasus': 'caucasus mts',
|
||||
caucasus: 'caucasus mts',
|
||||
'caucasus mountains': 'caucasus mts',
|
||||
'balkans': 'balkan pen',
|
||||
balkans: 'balkan pen',
|
||||
'balkan peninsula': 'balkan pen',
|
||||
'andes mountains': 'andes',
|
||||
'urals': 'ural mountains',
|
||||
urals: 'ural mountains',
|
||||
'pyrenees mountains': 'pyrenees',
|
||||
'arabian gulf': 'persian gulf',
|
||||
'gulf of arabia': 'persian gulf',
|
||||
'mediterranean': 'mediterranean sea',
|
||||
'caribbean': 'caribbean sea',
|
||||
'baja': 'baja california',
|
||||
'yucatan': 'pen de yucatan',
|
||||
mediterranean: 'mediterranean sea',
|
||||
caribbean: 'caribbean sea',
|
||||
baja: 'baja california',
|
||||
yucatan: 'pen de yucatan',
|
||||
'yucatan peninsula': 'pen de yucatan',
|
||||
'kamchatka': 'kamchatka peninsula',
|
||||
kamchatka: 'kamchatka peninsula',
|
||||
'sierra nevada mountains': 'sierra nevada',
|
||||
};
|
||||
|
||||
@@ -97,8 +99,13 @@ const ALIASES = {
|
||||
function suffixVariants(norm) {
|
||||
const v = [];
|
||||
// "x mountains" ↔ "x mts" (pack uses "Mts."; normalization strips the dot)
|
||||
if (norm.endsWith(' mountains')) v.push(norm.replace(/ mountains$/, ' mts'), norm.replace(/ mountains$/, ''));
|
||||
if (norm.endsWith(' mts')) v.push(norm.replace(/ mts$/, ' mountains'), norm.replace(/ mts$/, ''));
|
||||
if (norm.endsWith(' mountains'))
|
||||
v.push(
|
||||
norm.replace(/ mountains$/, ' mts'),
|
||||
norm.replace(/ mountains$/, ''),
|
||||
);
|
||||
if (norm.endsWith(' mts'))
|
||||
v.push(norm.replace(/ mts$/, ' mountains'), norm.replace(/ mts$/, ''));
|
||||
// "x desert" ↔ "x"
|
||||
if (norm.endsWith(' desert')) v.push(norm.replace(/ desert$/, ''));
|
||||
else v.push(norm + ' desert');
|
||||
@@ -117,9 +124,14 @@ async function loadPackFile(base) {
|
||||
// Vite bundles these JSON files as modules; the import attribute is what Node
|
||||
// needs to load the same files under node:test (same pattern as
|
||||
// neighborhoodPolygons.js). One path, so no node: import reaches the browser.
|
||||
const mod = base === 'regions'
|
||||
? await import('./local_data/natural_earth/regions.json', { with: { type: 'json' } })
|
||||
: await import('./local_data/natural_earth/marine.json', { with: { type: 'json' } });
|
||||
const mod =
|
||||
base === 'regions'
|
||||
? await import('./local_data/natural_earth/regions.json', {
|
||||
with: { type: 'json' },
|
||||
})
|
||||
: await import('./local_data/natural_earth/marine.json', {
|
||||
with: { type: 'json' },
|
||||
});
|
||||
return mod.default || mod;
|
||||
}
|
||||
|
||||
@@ -129,7 +141,10 @@ function buildEntries(pack, kind) {
|
||||
const polygons = ft.polygons || [];
|
||||
if (!polygons.length) continue;
|
||||
let areaKm2 = 0;
|
||||
let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
|
||||
let minLon = Infinity,
|
||||
minLat = Infinity,
|
||||
maxLon = -Infinity,
|
||||
maxLat = -Infinity;
|
||||
for (const ring of polygons) {
|
||||
areaKm2 += ringAreaKm2(ring);
|
||||
for (const [lon, lat] of ring) {
|
||||
@@ -169,10 +184,14 @@ const loadIndex = createRetryableLoader(async () => {
|
||||
];
|
||||
const index = new Map();
|
||||
for (const entry of _entries) {
|
||||
for (const key of new Set([normalizeName(entry.name), normalizeName(entry.namealt)])) {
|
||||
for (const key of new Set([
|
||||
normalizeName(entry.name),
|
||||
normalizeName(entry.namealt),
|
||||
])) {
|
||||
if (!key) continue;
|
||||
const list = index.get(key);
|
||||
if (list) list.push(entry); else index.set(key, [entry]);
|
||||
if (list) list.push(entry);
|
||||
else index.set(key, [entry]);
|
||||
}
|
||||
}
|
||||
// duplicate names exist in Natural Earth (e.g. two "Cordillera Oriental",
|
||||
@@ -207,7 +226,11 @@ export async function findNaturalRegion(query) {
|
||||
const norm = normalizeName(query);
|
||||
if (!norm) return null;
|
||||
const index = await loadIndex();
|
||||
const candidates = [norm, ALIASES[norm], ...suffixVariants(ALIASES[norm] || norm)];
|
||||
const candidates = [
|
||||
norm,
|
||||
ALIASES[norm],
|
||||
...suffixVariants(ALIASES[norm] || norm),
|
||||
];
|
||||
for (const key of candidates) {
|
||||
if (!key) continue;
|
||||
const list = index.get(key);
|
||||
@@ -248,8 +271,8 @@ export function pointInRing(ring, lat, lon) {
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
|
||||
const [xi, yi] = ring[i];
|
||||
const [xj, yj] = ring[j];
|
||||
const intersects = (yi > lat) !== (yj > lat)
|
||||
&& lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
||||
const intersects =
|
||||
yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
||||
if (intersects) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
@@ -274,7 +297,11 @@ export async function lookupNaturalRegionOutline(query, lat, lon) {
|
||||
const norm = normalizeName(query);
|
||||
if (!norm || !Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
||||
const index = await loadIndex();
|
||||
const candidates = [norm, ALIASES[norm], ...suffixVariants(ALIASES[norm] || norm)];
|
||||
const candidates = [
|
||||
norm,
|
||||
ALIASES[norm],
|
||||
...suffixVariants(ALIASES[norm] || norm),
|
||||
];
|
||||
const seen = new Set();
|
||||
for (const key of candidates) {
|
||||
if (!key || seen.has(key)) continue;
|
||||
|
||||
@@ -14,7 +14,14 @@ import { createRetryableLoader } from './retryableLoad.js';
|
||||
|
||||
// bbox = [west, south, east, north]; only load a city file when the point falls in its box.
|
||||
const CITY_FILES = [
|
||||
{ id: 'san-francisco', bbox: [-122.55, 37.70, -122.35, 37.84], loader: () => import('./local_data/neighborhoods/san-francisco.json', { with: { type: 'json' } }) },
|
||||
{
|
||||
id: 'san-francisco',
|
||||
bbox: [-122.55, 37.7, -122.35, 37.84],
|
||||
loader: () =>
|
||||
import('./local_data/neighborhoods/san-francisco.json', {
|
||||
with: { type: 'json' },
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -27,7 +34,11 @@ const CITY_FILES = [
|
||||
const _cityLoaders = new Map();
|
||||
|
||||
function normalize(s) {
|
||||
return String(s || '').toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return String(s || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9 ]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,10 +56,12 @@ const NAME_ALIASES = new Map([
|
||||
function pointInRing(lon, lat, ring) {
|
||||
let inside = false;
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
const xi = ring[i][0]; const yi = ring[i][1];
|
||||
const xj = ring[j][0]; const yj = ring[j][1];
|
||||
const intersect = ((yi > lat) !== (yj > lat))
|
||||
&& (lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi);
|
||||
const xi = ring[i][0];
|
||||
const yi = ring[i][1];
|
||||
const xj = ring[j][0];
|
||||
const yj = ring[j][1];
|
||||
const intersect =
|
||||
yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
@@ -70,7 +83,10 @@ function containingOuterRing(feature, lon, lat) {
|
||||
if (!pointInRing(lon, lat, outer)) continue;
|
||||
let inHole = false;
|
||||
for (let h = 1; h < poly.length; h++) {
|
||||
if (pointInRing(lon, lat, poly[h])) { inHole = true; break; }
|
||||
if (pointInRing(lon, lat, poly[h])) {
|
||||
inHole = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inHole) return outer;
|
||||
}
|
||||
@@ -108,7 +124,10 @@ async function loadCity(city) {
|
||||
} catch (error) {
|
||||
// The caller falls back to the live resolver ladder, which looks exactly
|
||||
// like "no bundled coverage" — say so once, out loud, instead.
|
||||
console.warn(`[neighborhoods] ${city.id} pack unavailable:`, error?.message || error);
|
||||
console.warn(
|
||||
`[neighborhoods] ${city.id} pack unavailable:`,
|
||||
error?.message || error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -122,7 +141,13 @@ async function loadCity(city) {
|
||||
* neighborhood name, or null when the point isn't in a covered city / no match.
|
||||
*/
|
||||
export async function lookupNeighborhoodRing(lat, lon, matchName) {
|
||||
const city = CITY_FILES.find((c) => lon >= c.bbox[0] && lat >= c.bbox[1] && lon <= c.bbox[2] && lat <= c.bbox[3]);
|
||||
const city = CITY_FILES.find(
|
||||
(c) =>
|
||||
lon >= c.bbox[0] &&
|
||||
lat >= c.bbox[1] &&
|
||||
lon <= c.bbox[2] &&
|
||||
lat <= c.bbox[3],
|
||||
);
|
||||
if (!city) return null;
|
||||
const feats = await loadCity(city);
|
||||
if (!feats.length) return null;
|
||||
@@ -153,11 +178,18 @@ export async function lookupNeighborhoodRing(lat, lon, matchName) {
|
||||
const containRing = containingOuterRing(f, lon, lat);
|
||||
const ring = containRing || largestOuterRing(f.geometry);
|
||||
if (!ring) continue;
|
||||
const cand = { ring, name: f.properties.name, contains: !!containRing, fwordCount: fWords.length };
|
||||
const cand = {
|
||||
ring,
|
||||
name: f.properties.name,
|
||||
contains: !!containRing,
|
||||
fwordCount: fWords.length,
|
||||
};
|
||||
// Prefer a feature that CONTAINS the point, then the most specific name (most words).
|
||||
if (!best
|
||||
|| (cand.contains && !best.contains)
|
||||
|| (cand.contains === best.contains && cand.fwordCount > best.fwordCount)) {
|
||||
if (
|
||||
!best ||
|
||||
(cand.contains && !best.contains) ||
|
||||
(cand.contains === best.contains && cand.fwordCount > best.fwordCount)
|
||||
) {
|
||||
best = cand;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user