Files
intelligence-terminal/apis/sources/gdelt.mjs
calesthio a8682c50d0 Add zoom-aware symbology, fix globe popovers, expand geo coverage (#34)
* Add zoom-aware symbology, fix globe popovers, expand geo coverage

Map rendering improvements based on user feedback:
- Globe markers now scale with camera altitude (onZoom hook)
- Priority-based visibility culls low-priority markers at world view
- Globe popovers use getScreenCoords for accurate positioning
- Flat map labels hidden at low zoom, revealed progressively
- Default globe altitude lowered from 2.5 to 1.8 for better fill
- Americas region zoom tightened to CONUS focus

Geographic coverage expansion:
- 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa
- Flight corridors now span Americas and Africa
- NOAA alerts extract centroid lat/lon from GeoJSON geometry
- EPA RadNet stations geocoded with hardcoded coords for 10 US cities
- ISS + Tiangong positions estimated from TLE orbital elements
- GDELT geoEvents() now called in briefing for mapped event points
- New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event

* Fix null-safe coordinate checks and remove injected data blob

- Use `!= null` instead of truthy checks for lat/lon in noaa.mjs
  and inject.mjs so valid 0-coordinates (equator/prime meridian)
  are not silently dropped
- Reset jarvis.html `let D` back to null placeholder so generated
  runtime data is not part of the PR diff

* Remove re-injected data blob from jarvis.html

Reset let D back to null — previous commit was correct but
inject.mjs build verification re-injected the payload.
2026-03-17 19:34:08 -07:00

139 lines
4.3 KiB
JavaScript

// GDELT — Global Database of Events, Language, and Tone
// No auth required. Updates every 15 minutes. Monitors news in 100+ languages.
// DOC 2.0 API: full-text search across last 3 months of global news
// GEO 2.0 API: geolocation mapping of events
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://api.gdeltproject.org/api/v2';
// Search recent global events/articles by keyword
export async function searchEvents(query = '', opts = {}) {
const {
mode = 'ArtList', // ArtList, TimelineVol, TimelineVolInfo, TimelineTone, TimelineLang, TimelineSourceCountry
maxRecords = 75,
timespan = '24h', // e.g. "24h", "7d", "3m"
format = 'json',
sortBy = 'DateDesc', // DateDesc, DateAsc, ToneDesc, ToneAsc
} = opts;
// If no query, use broad geopolitical terms
const q = query || 'conflict OR crisis OR military OR sanctions OR war OR economy';
const params = new URLSearchParams({
query: q,
mode,
maxrecords: String(maxRecords),
timespan,
format,
sort: sortBy,
});
return safeFetch(`${BASE}/doc/doc?${params}`);
}
// Get tone/sentiment timeline for a topic
export async function toneTrend(query, timespan = '7d') {
const params = new URLSearchParams({
query,
mode: 'TimelineTone',
timespan,
format: 'json',
});
return safeFetch(`${BASE}/doc/doc?${params}`);
}
// Get volume timeline for a topic (how much coverage)
export async function volumeTrend(query, timespan = '7d') {
const params = new URLSearchParams({
query,
mode: 'TimelineVol',
timespan,
format: 'json',
});
return safeFetch(`${BASE}/doc/doc?${params}`);
}
// GEO API — geographic event mapping
export async function geoEvents(query = '', opts = {}) {
const {
mode = 'PointData',
timespan = '24h',
format = 'GeoJSON',
maxPoints = 500,
} = opts;
const q = query || 'conflict OR military OR protest OR explosion';
const params = new URLSearchParams({
query: q,
mode,
timespan,
format,
maxpoints: String(maxPoints),
});
return safeFetch(`${BASE}/geo/geo?${params}`);
}
// Compact article for briefing
function compactArticle(a) {
return {
title: a.title,
url: a.url,
date: a.seendate,
domain: a.domain,
language: a.language,
country: a.sourcecountry,
};
}
// GDELT rate limit: 1 request per 5 seconds
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
// Briefing mode — get top global events summary (sequential due to rate limit)
export async function briefing() {
// Single broad query to stay within rate limits
const all = await searchEvents(
'conflict OR military OR economy OR crisis OR war OR sanctions OR tariff OR strike OR outbreak',
{ maxRecords: 50, timespan: '24h' }
);
const articles = (all?.articles || []).map(compactArticle);
// Categorize by keyword matching in titles
const categorize = (keywords) => articles.filter(a =>
keywords.some(k => a.title?.toLowerCase().includes(k))
);
// Geo events — get mapped event locations (separate API, respects rate limit)
await delay(5500);
let geoPoints = [];
try {
const geo = await geoEvents('conflict OR military OR protest OR crisis', { maxPoints: 30, timespan: '24h' });
geoPoints = (geo?.features || []).filter(f => f.geometry?.coordinates).map(f => ({
lat: f.geometry.coordinates[1],
lon: f.geometry.coordinates[0],
name: f.properties?.name || f.properties?.html || '',
count: f.properties?.count || 1,
type: f.properties?.type || 'event',
}));
} catch (e) { /* geo endpoint optional — don't break briefing */ }
return {
source: 'GDELT',
timestamp: new Date().toISOString(),
totalArticles: articles.length,
allArticles: articles,
geoPoints,
conflicts: categorize(['military', 'conflict', 'war', 'strike', 'missile', 'attack', 'bomb', 'troops']),
economy: categorize(['economy', 'recession', 'inflation', 'market', 'sanctions', 'tariff', 'trade', 'gdp']),
health: categorize(['pandemic', 'outbreak', 'epidemic', 'disease', 'virus', 'health']),
crisis: categorize(['crisis', 'disaster', 'emergency', 'refugee', 'famine']),
};
}
// Run standalone
if (process.argv[1]?.endsWith('gdelt.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}