feat(geo): add Pak-Afghan conflict zone and country boundary override system (#1150)

* Add Pakistan–Afghanistan hotspot and conflict zone

Introduce a new INTEL_HOTSPOTS entry (pak_afghan) to track Pakistan–Afghanistan border tensions, including location, keywords, agencies, status, escalation indicators, and humanitarian significance. Also add a CONFLICT_ZONES polygon for 'Pakistan–Afghanistan War' with center, intensity, parties, startDate (Feb 21, 2026), key developments, and displacement/casualty notes to enable monitoring of cross-border strikes, TTP activity, and regional instability.

* Update conflict zone center coordinates

Adjust the center coordinates for the specified conflict zone in src/config/geo.ts from [50, 30] to [69, 31.8] to better reflect the actual Pakistan/Afghanistan border region and improve map centering/visualization accuracy.

* Add country boundary overrides (Pakistan)

Support optional country boundary overrides by loading public/data/country-boundary-overrides.geojson and replacing main country geometries when ISO codes match. Add a script (scripts/fetch-pakistan-boundary-override.mjs) to fetch Pakistan's de facto boundary from Natural Earth and write the override file, and document the override workflow in CONTRIBUTING.md. The country-geometry service now attempts to apply overrides and updates cached polygons/bboxes; failures are ignored since overrides are optional.

* fix: neutralize language, parallel override loading, fetch timeout

- Rename conflict zone from "War" to "Border Conflict", intensity high→medium
- Rewrite description to factual language (no "open war" claim)
- Load country boundary overrides in parallel with main GeoJSON
- Neutralize comments/docs: reference Natural Earth source, remove political terms
- Add 60s timeout to Natural Earth fetch script (~24MB download)
- Add trailing newline to GeoJSON override file

* refactor: serve country boundary overrides from R2 CDN

Move country-boundary-overrides.geojson from public/data/ to R2 bucket
(worldmonitor-maps) to avoid serving large static files through Vercel.
Update fetch URL, docs, and script with rclone upload instructions.

* fix: use maps.worldmonitor.app for R2 override URL (CF-proxied)

* fix(geo): bound optional country override fetch

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
This commit is contained in:
Fasih M.
2026-03-07 06:00:03 +00:00
committed by GitHub
parent 2173340cfa
commit 6f9af63bad
4 changed files with 208 additions and 60 deletions

View File

@@ -259,10 +259,11 @@ For endpoints that deal with non-JSON payloads (XML feeds, binary data, HTML emb
### Country boundary overrides
Country outlines are loaded from `public/data/countries.geojson`. Optional higher-resolution overrides (sourced from [Natural Earth](https://www.naturalearthdata.com/)) live in `public/data/country-boundary-overrides.geojson`. The app loads overrides after the main file and replaces geometry for any country whose `ISO3166-1-Alpha-2` (or `ISO_A2`) matches. To refresh the Pakistan boundary from Natural Earth, run:
Country outlines are loaded from `public/data/countries.geojson`. Optional higher-resolution overrides (sourced from [Natural Earth](https://www.naturalearthdata.com/)) are served from R2 CDN. The app loads overrides after the main file and replaces geometry for any country whose `ISO3166-1-Alpha-2` (or `ISO_A2`) matches. To refresh the Pakistan boundary from Natural Earth, run:
```bash
node scripts/fetch-pakistan-boundary-override.mjs
rclone copy public/data/country-boundary-overrides.geojson r2:worldmonitor-maps/
```
## Adding RSS Feeds

View File

@@ -1,7 +1,8 @@
#!/usr/bin/env node
/**
* Fetches Pakistan's boundary from Natural Earth 50m Admin 0 Countries and writes
* public/data/country-boundary-overrides.geojson.
* country-boundary-overrides.geojson locally. After running, upload to R2:
* rclone copy public/data/country-boundary-overrides.geojson r2:worldmonitor-maps/
*
* Note: downloads the full NE 50m countries file (~24 MB) to extract Pakistan.
*

View File

@@ -14,8 +14,9 @@ interface CountryHit {
const COUNTRY_GEOJSON_URL = 'https://maps.worldmonitor.app/countries.geojson';
/** Optional higher-resolution boundary overrides sourced from Natural Earth. */
const COUNTRY_OVERRIDES_URL = '/data/country-boundary-overrides.geojson';
/** Optional higher-resolution boundary overrides sourced from Natural Earth (served from R2 CDN). */
const COUNTRY_OVERRIDES_URL = 'https://maps.worldmonitor.app/country-boundary-overrides.geojson';
const COUNTRY_OVERRIDE_TIMEOUT_MS = 3_000;
const POLITICAL_OVERRIDES: Record<string, string> = { 'CN-TW': 'TW' };
@@ -169,6 +170,78 @@ function buildCountryNameMatchers(): void {
}));
}
function makeTimeout(ms: number): AbortSignal {
if (typeof AbortSignal.timeout === 'function') return AbortSignal.timeout(ms);
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), ms);
return ctrl.signal;
}
function rebuildCountryIndex(data: FeatureCollection<Geometry>): void {
countryIndex.clear();
countryList = [];
iso3ToIso2.clear();
nameToIso2.clear();
codeToName.clear();
for (const feature of data.features) {
const code = normalizeCode(feature.properties);
const name = normalizeName(feature.properties);
if (!code || !name) continue;
const iso3 = feature.properties?.['ISO3166-1-Alpha-3'];
if (typeof iso3 === 'string' && /^[A-Z]{3}$/i.test(iso3.trim())) {
iso3ToIso2.set(iso3.trim().toUpperCase(), code);
}
nameToIso2.set(name.toLowerCase(), code);
if (!codeToName.has(code)) codeToName.set(code, name);
const polygons = normalizeGeometry(feature.geometry);
const bbox = computeBbox(polygons);
if (!bbox || polygons.length === 0) continue;
const indexed: IndexedCountryGeometry = { code, name, polygons, bbox };
countryIndex.set(code, indexed);
countryList.push(indexed);
}
for (const [alias, code] of Object.entries(NAME_ALIASES)) {
if (!nameToIso2.has(alias)) {
nameToIso2.set(alias, code);
}
}
buildCountryNameMatchers();
}
function applyCountryGeometryOverrides(
data: FeatureCollection<Geometry>,
overrideData: FeatureCollection<Geometry>,
): void {
const featureByCode = new Map<string, (typeof data.features)[number]>();
for (const feature of data.features) {
const code = normalizeCode(feature.properties);
if (code) featureByCode.set(code, feature);
}
for (const overrideFeature of overrideData.features) {
const code = normalizeCode(overrideFeature.properties);
if (!code || !overrideFeature.geometry) continue;
const mainFeature = featureByCode.get(code);
if (!mainFeature) continue;
mainFeature.geometry = overrideFeature.geometry;
const polygons = normalizeGeometry(overrideFeature.geometry);
const bbox = computeBbox(polygons);
if (!bbox || polygons.length === 0) continue;
const existing = countryIndex.get(code);
if (!existing) continue;
existing.polygons = polygons;
existing.bbox = bbox;
}
}
async function ensureLoaded(): Promise<void> {
if (loadedGeoJson || loadPromise) {
await loadPromise;
@@ -179,10 +252,7 @@ async function ensureLoaded(): Promise<void> {
if (typeof fetch !== 'function') return;
try {
const [response, overrideResp] = await Promise.all([
fetch(COUNTRY_GEOJSON_URL),
fetch(COUNTRY_OVERRIDES_URL).catch(() => null),
]);
const response = await fetch(COUNTRY_GEOJSON_URL);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
@@ -193,67 +263,22 @@ async function ensureLoaded(): Promise<void> {
}
loadedGeoJson = data;
countryIndex.clear();
countryList = [];
iso3ToIso2.clear();
nameToIso2.clear();
codeToName.clear();
for (const feature of data.features) {
const code = normalizeCode(feature.properties);
const name = normalizeName(feature.properties);
if (!code || !name) continue;
const iso3 = feature.properties?.['ISO3166-1-Alpha-3'];
if (typeof iso3 === 'string' && /^[A-Z]{3}$/i.test(iso3.trim())) {
iso3ToIso2.set(iso3.trim().toUpperCase(), code);
}
nameToIso2.set(name.toLowerCase(), code);
if (!codeToName.has(code)) codeToName.set(code, name);
const polygons = normalizeGeometry(feature.geometry);
const bbox = computeBbox(polygons);
if (!bbox || polygons.length === 0) continue;
const indexed: IndexedCountryGeometry = { code, name, polygons, bbox };
countryIndex.set(code, indexed);
countryList.push(indexed);
}
for (const [alias, code] of Object.entries(NAME_ALIASES)) {
if (!nameToIso2.has(alias)) {
nameToIso2.set(alias, code);
}
}
rebuildCountryIndex(data);
// Apply optional higher-resolution boundary overrides (sourced from Natural Earth)
try {
if (overrideResp?.ok) {
const overrideResp = await fetch(COUNTRY_OVERRIDES_URL, {
signal: makeTimeout(COUNTRY_OVERRIDE_TIMEOUT_MS),
});
if (overrideResp.ok) {
const overrideData = (await overrideResp.json()) as FeatureCollection<Geometry>;
if (overrideData?.type === 'FeatureCollection' && Array.isArray(overrideData.features)) {
for (const overrideFeature of overrideData.features) {
const code = normalizeCode(overrideFeature.properties);
if (!code || !overrideFeature.geometry) continue;
const mainFeature = data.features.find((f) => normalizeCode(f.properties) === code);
if (!mainFeature) continue;
mainFeature.geometry = overrideFeature.geometry;
const polygons = normalizeGeometry(overrideFeature.geometry);
const bbox = computeBbox(polygons);
if (bbox && polygons.length > 0) {
const existing = countryIndex.get(code);
if (existing) {
existing.polygons = polygons;
existing.bbox = bbox;
}
}
}
applyCountryGeometryOverrides(data, overrideData);
}
}
} catch {
// Overrides are optional; ignore fetch/parse errors
// Overrides optional; ignore fetch/parse errors
}
buildCountryNameMatchers();
} catch (err) {
console.warn('[country-geometry] Failed to load countries.geojson:', err);
}

View File

@@ -0,0 +1,121 @@
import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
const originalFetch = globalThis.fetch;
const originalAbortSignalTimeout = AbortSignal.timeout;
function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
function installFastAbortTimeout(delayMs = 5): void {
Object.defineProperty(AbortSignal, 'timeout', {
configurable: true,
writable: true,
value: () => {
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), delayMs);
return ctrl.signal;
},
});
}
function restoreGlobals(): void {
globalThis.fetch = originalFetch;
Object.defineProperty(AbortSignal, 'timeout', {
configurable: true,
writable: true,
value: originalAbortSignalTimeout,
});
}
async function loadFreshCountryGeometryModule() {
return import(`../src/services/country-geometry.ts?test=${Date.now()}-${Math.random()}`);
}
function makeFeatureCollection(maxCoord: number) {
return {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
name: 'Pakistan',
'ISO3166-1-Alpha-2': 'PK',
'ISO3166-1-Alpha-3': 'PAK',
},
geometry: {
type: 'Polygon',
coordinates: [[
[0, 0],
[maxCoord, 0],
[maxCoord, maxCoord],
[0, maxCoord],
[0, 0],
]],
},
},
],
};
}
afterEach(() => {
restoreGlobals();
});
describe('country geometry overrides', () => {
it('loads bundled geometry when override fetch times out', async () => {
installFastAbortTimeout();
let overrideAborted = false;
globalThis.fetch = ((input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
if (url === '/data/countries.geojson') {
return Promise.resolve(jsonResponse(makeFeatureCollection(1)));
}
if (url === 'https://maps.worldmonitor.app/country-boundary-overrides.geojson') {
return new Promise((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => {
overrideAborted = true;
reject(new DOMException('The operation was aborted.', 'AbortError'));
}, { once: true });
});
}
return Promise.reject(new Error(`Unexpected URL: ${url}`));
}) as typeof fetch;
const countryGeometry = await loadFreshCountryGeometryModule();
const start = Date.now();
await countryGeometry.preloadCountryGeometry();
const elapsedMs = Date.now() - start;
assert.equal(overrideAborted, true);
assert.ok(elapsedMs < 250, `Expected preload to complete quickly, got ${elapsedMs}ms`);
assert.deepEqual(countryGeometry.getCountryBbox('PK'), [0, 0, 1, 1]);
});
it('applies override geometry when the CDN responds in time', async () => {
globalThis.fetch = ((input: string | URL | Request) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
if (url === '/data/countries.geojson') {
return Promise.resolve(jsonResponse(makeFeatureCollection(1)));
}
if (url === 'https://maps.worldmonitor.app/country-boundary-overrides.geojson') {
return Promise.resolve(jsonResponse(makeFeatureCollection(2)));
}
return Promise.reject(new Error(`Unexpected URL: ${url}`));
}) as typeof fetch;
const countryGeometry = await loadFreshCountryGeometryModule();
await countryGeometry.preloadCountryGeometry();
assert.deepEqual(countryGeometry.getCountryBbox('PK'), [0, 0, 2, 2]);
assert.deepEqual(countryGeometry.getCountryAtCoordinates(1.5, 1.5), {
code: 'PK',
name: 'Pakistan',
});
});
});