Draw Data Library coverage outlines from the bundled Natural Earth countries

Imagery outlines fetched Natural Earth's 1:50m countries (about 3 MB)
from the natural-earth-vector repository's master branch on GitHub at
run time: a moving, rate-limited source whose failures silently removed
the outlines. Core already ships the pinned 1:10m countries pack, so
the outlines now read that, once, the first time an outline needs it.
This commit is contained in:
Sameh Khamis committed 2026-11-12 16:02:07 -08:00
1 parent 8d90cac7dc
commit 9e57fd84c8
3 files changed
+55 -25

No files matched your search

+3 -2
View File
@@ -3,9 +3,10 @@
Offline polygons with no network dependency. The voice-annotation resolver Offline polygons with no network dependency. The voice-annotation resolver
(`src/data/naturalEarthRegions.js`) reads the named regions, so "outline the (`src/data/naturalEarthRegions.js`) reads the named regions, so "outline the
Alps" resolves to the real range geometry; area annotations read the Alps" resolves to the real range geometry; area annotations read the
countries, states and provinces (`src/data/adminBoundaries.js`); and the News countries, states and provinces (`src/data/adminBoundaries.js`); the News
layer reads the countries and the disputed areas layer reads the countries and the disputed areas
(`server/providers/news/areas.js`). (`server/providers/news/areas.js`); and Data Library imagery draws its
coverage outlines from the countries (`src/library/outlines.js`).
| File | Source dataset | Features | | File | Source dataset | Features |
|------|----------------|----------| |------|----------------|----------|
+30 -23
View File
@@ -2,40 +2,47 @@
* Coverage outlines for Data Library imagery: the borders of the countries an * Coverage outlines for Data Library imagery: the borders of the countries an
* entry covers, so the edge of an aerial photo layer is plain on the map. * entry covers, so the edge of an aerial photo layer is plain on the map.
* *
* Borders come from Natural Earth's 1:50m admin-0 countries (public domain), * Borders come from the bundled Natural Earth 1:10m admin-0 countries pack
* a provider-hosted file fetched live and kept for the session, so nothing is * (public domain, pinned; see src/data/local_data/natural_earth/README.md),
* bundled. Pure apart from `loadCountryOutlines`, which takes `fetchImpl`. * read once the first outline needs it. Pure apart from
* `loadCountryOutlines`.
*/ */
import { readResponseTextCapped } from '../sources/httpBody.js'; import { loadBundledJson } from '../data/bundledJson.js';
import { decodeRing } from '../data/adminBoundaries.js';
export const COUNTRY_OUTLINES_URL = const COUNTRIES_PACK_URL = new URL(
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson'; '../data/local_data/natural_earth/countries.json',
/** The 1:50m file is about 3 MB. */ import.meta.url,
const OUTLINES_MAX_BYTES = 8 * 1024 * 1024; );
/** Outline modes a manifest may ask for. */ /** Outline modes a manifest may ask for. */
export const OUTLINE_MODES = Object.freeze(['coverage', 'countries', 'bbox']); export const OUTLINE_MODES = Object.freeze(['coverage', 'countries', 'bbox']);
let pending = null; let pending = null;
/** /**
* Natural Earth country features, fetched once per page. * Country features with closed outer rings, in the GeoJSON shape
* @param {typeof fetch} fetchImpl * `outlineRings` reads, loaded once per page.
* @param {(url: URL) => Promise<object>} [loadPack]
* @returns {Promise<object[]>} * @returns {Promise<object[]>}
*/ */
export function loadCountryOutlines( export function loadCountryOutlines(loadPack = loadBundledJson) {
fetchImpl = (...args) => globalThis.fetch(...args),
) {
pending ||= (async () => { pending ||= (async () => {
const response = await fetchImpl(COUNTRY_OUTLINES_URL, { const pack = await loadPack(COUNTRIES_PACK_URL);
credentials: 'omit', if (!Array.isArray(pack?.features)) throw new Error('not a country pack');
referrerPolicy: 'no-referrer', const decimals = pack.meta?.decimals;
redirect: 'error', return pack.features.map((feature) => ({
}); type: 'Feature',
if (!response.ok) throw new Error(`HTTP ${response.status}`); properties: { ISO_A2: feature.iso2 },
const text = await readResponseTextCapped(response, OUTLINES_MAX_BYTES); geometry: {
const features = JSON.parse(text)?.features; type: 'MultiPolygon',
if (!Array.isArray(features)) throw new Error('not a FeatureCollection'); coordinates: (feature.polygons || []).map((polygon) =>
return features; polygon.map((encoded) => {
const ring = decodeRing(encoded, feature.d ?? decimals);
return ring.length ? [...ring, ring[0]] : ring;
}),
),
},
}));
})().catch((error) => { })().catch((error) => {
pending = null; pending = null;
throw error; throw error;
+22
View File
@@ -68,3 +68,25 @@ test('a bbox outline is the coverage rectangle, with no border file needed', ()
], ],
]); ]);
}); });
test('outlines come from the bundled Natural Earth countries, closed and in coverage', async () => {
const { loadCountryOutlines, resetCountryOutlines } = await import(
'./outlines.js'
);
resetCountryOutlines();
const countries = await loadCountryOutlines();
for (const id of ['ign-orthophotos-france', 'ee-maa-amet-ortofoto']) {
const entry = libraryEntry(id);
const rings = outlineRings(countries, entry);
assert.ok(rings.length > 0, id);
for (const ring of rings) assert.deepEqual(ring.at(-1), ring[0]);
}
// A pack that cannot load is retried on the next call.
resetCountryOutlines();
await assert.rejects(
loadCountryOutlines(async () => {
throw new Error('unreadable');
}),
);
assert.ok((await loadCountryOutlines()).length > 200);
});