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
(`src/data/naturalEarthRegions.js`) reads the named regions, so "outline 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
(`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 |
|------|----------------|----------|
+30 -23
View File
@@ -2,40 +2,47 @@
* 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.
*
* Borders come from Natural Earth's 1:50m admin-0 countries (public domain),
* a provider-hosted file fetched live and kept for the session, so nothing is
* bundled. Pure apart from `loadCountryOutlines`, which takes `fetchImpl`.
* Borders come from the bundled Natural Earth 1:10m admin-0 countries pack
* (public domain, pinned; see src/data/local_data/natural_earth/README.md),
* 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 =
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson';
/** The 1:50m file is about 3 MB. */
const OUTLINES_MAX_BYTES = 8 * 1024 * 1024;
const COUNTRIES_PACK_URL = new URL(
'../data/local_data/natural_earth/countries.json',
import.meta.url,
);
/** Outline modes a manifest may ask for. */
export const OUTLINE_MODES = Object.freeze(['coverage', 'countries', 'bbox']);
let pending = null;
/**
* Natural Earth country features, fetched once per page.
* @param {typeof fetch} fetchImpl
* Country features with closed outer rings, in the GeoJSON shape
* `outlineRings` reads, loaded once per page.
* @param {(url: URL) => Promise<object>} [loadPack]
* @returns {Promise<object[]>}
*/
export function loadCountryOutlines(
fetchImpl = (...args) => globalThis.fetch(...args),
) {
export function loadCountryOutlines(loadPack = loadBundledJson) {
pending ||= (async () => {
const response = await fetchImpl(COUNTRY_OUTLINES_URL, {
credentials: 'omit',
referrerPolicy: 'no-referrer',
redirect: 'error',
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const text = await readResponseTextCapped(response, OUTLINES_MAX_BYTES);
const features = JSON.parse(text)?.features;
if (!Array.isArray(features)) throw new Error('not a FeatureCollection');
return features;
const pack = await loadPack(COUNTRIES_PACK_URL);
if (!Array.isArray(pack?.features)) throw new Error('not a country pack');
const decimals = pack.meta?.decimals;
return pack.features.map((feature) => ({
type: 'Feature',
properties: { ISO_A2: feature.iso2 },
geometry: {
type: 'MultiPolygon',
coordinates: (feature.polygons || []).map((polygon) =>
polygon.map((encoded) => {
const ring = decodeRing(encoded, feature.d ?? decimals);
return ring.length ? [...ring, ring[0]] : ring;
}),
),
},
}));
})().catch((error) => {
pending = null;
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);
});