import * as Cesium from 'cesium';
/**
* Per-layer data attribution registered into Cesium's credit display.
*
* Legal requirement (see DATA_SOURCES.md, findings H10/H11 in
* docs/pre-ship-audit-2026-07-01.md): every third-party data layer this app can
* display carries its own license and required attribution — ODbL (OSM
* datacenters/dams, adsb.lol, Overpass roads), CC BY-NC-SA (TeleGeography
* cables), NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic (CC BY 4.0),
* The MIT code license does NOT cover this data.
*
* These credits are registered ONCE at init as STATIC credits with
* showOnScreen=false, so they live in the expandable bottom-left "Data
* attribution" lightbox (Cesium's credit popover) rather than cluttering the
* on-globe line. Always-present is intentional and reversible: the lightbox is
* the app's canonical attribution surface and DATA_SOURCES.md is the
* machine-readable index. Strings are copied verbatim from DATA_SOURCES.md — if
* you add a data source, add it there AND here.
*/
/**
* Attribution entries. `html` is the credit markup; keep it minimal and
* link out where DATA_SOURCES.md provides a canonical URL. Order roughly
* follows DATA_SOURCES.md (live sources, then bundled snapshots).
* @type {{ key: string, html: string }[]}
*/
export const OSM_CREDIT = {
key: 'openstreetmap',
html: 'Map and place data © OpenStreetMap contributors (ODbL)',
};
export const OPENMAPTILES_CREDIT = {
key: 'openfreemap',
html: 'Vector tiles: OpenFreeMap © OpenMapTiles',
};
const osmDisplays = new WeakMap();
function updateOsmDisplay(viewer, state) {
const osm = state.owners.size > 0;
let tiles = false;
for (const usesTiles of state.owners.values()) tiles ||= usesTiles;
for (const [key, visible] of [
['osm', osm],
['tiles', tiles],
]) {
if (state[key].visible === visible) continue;
state[key].visible = visible;
viewer.creditDisplay[visible ? 'addStaticCredit' : 'removeStaticCredit'](
state[key].credit,
);
viewer.scene?.requestRender?.();
}
}
/** Keep one short inline credit while an OSM display owner has visible data. */
export function showOsmCredit(viewer, owner, { openMapTiles = false } = {}) {
if (!viewer?.creditDisplay?.addStaticCredit || !owner) return false;
let state = osmDisplays.get(viewer);
if (!state) {
state = {
owners: new Map(),
osm: {
visible: false,
credit: new Cesium.Credit(
'© OpenStreetMap',
true,
),
},
tiles: {
visible: false,
credit: new Cesium.Credit(
'© OpenMapTiles',
true,
),
},
};
osmDisplays.set(viewer, state);
}
if (state.owners.get(owner) === openMapTiles) return false;
state.owners.set(owner, openMapTiles);
updateOsmDisplay(viewer, state);
return true;
}
/** Release only this owner's credit; other displayed OSM sources retain theirs. */
export function hideOsmCredit(viewer, owner) {
const state = osmDisplays.get(viewer);
if (!state?.owners.delete(owner)) return false;
updateOsmDisplay(viewer, state);
return true;
}
export const DATA_CREDITS = [
// ── Live sources ────────────────────────────────────────────────
{
key: 'opensky',
html:
'Flights: OpenSky Network — Schäfer et al., ' +
'“Bringing Up OpenSky”, IPSN 2014 · ' +
'opensky-network.org ' +
'(non-commercial)',
},
{
key: 'adsblol',
html:
'Military flights, aircraft traces & bounded regional flight fallback: ' +
'adsb.lol ' +
'(ODbL 1.0)',
},
{
key: 'adsbdb',
html:
'Aircraft type, registration & flight routes: ' +
'adsbdb ' +
'· aircraft data from PlaneBase · ICAO-to-N-number conversion by ' +
'Guillaume Michel · route data is the work of ' +
'David Taylor, Edinburgh, and Jim Mason, Glasgow, and may not be ' +
'copied, published, or incorporated into other databases without the ' +
'explicit permission of David J Taylor, Edinburgh',
},
{
key: 'aisstream',
html:
'Live vessels (AIS): ' +
'AISStream.io',
},
{
key: 'celestrak',
html:
'Satellites (TLEs): CelesTrak ' +
'(celestrak.org), ' +
'Dr. T.S. Kelso',
},
{
key: 'launch-library-2',
html:
'Space mission launch, payload & recovery metadata: ' +
'Launch Library 2 — The Space Devs ' +
'(API documentation and rate limits)',
},
{
key: 'usgs',
html: 'Earthquakes: Data courtesy of the U.S. Geological Survey',
},
{
key: 'nasa-gibs',
html:
'Recent imagery: We acknowledge the use of imagery provided by services from ' +
"NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science " +
'Data and Information System (ESDIS). ' +
'gibs.earthdata.nasa.gov · ' +
'HLS product page',
},
{
key: 'wfigs',
html:
'Wildfire perimeters: ' +
'National Interagency Fire Center (WFIGS)' +
' · Incident information: ' +
'InciWeb',
},
OSM_CREDIT,
OPENMAPTILES_CREDIT,
{
key: 'eccc-aqhi',
html:
'Air quality (AQHI): Environment and Climate Change Canada — MSC GeoMet ' +
'(eccc-msc.github.io/open-data) ' +
'· ECCC Data Servers End-use Licence',
},
{
key: 'overture-military-names',
html: 'Military area names: Overture Maps Foundation (ODbL)',
},
{
key: 'photon-geocoder',
html: 'Keyless place search: Photon (komoot)',
},
{
key: 'open-meteo',
html:
'Cockpit current conditions: ' +
'Weather data by Open-Meteo.com ' +
'(CC BY 4.0)',
},
{
key: 'google-news-rss',
html:
'Cockpit regional headlines: ' +
'Google News RSS ' +
'(location-matched article links; publisher terms apply)',
},
{
key: 'gdelt',
html:
'Cockpit regional headlines: ' +
'GDELT Project ' +
'(location-matched article links; publisher terms apply)',
},
{
key: 'austin-cctv',
html:
'CCTV cameras & frames: City of Austin, TX — ' +
'data.austintexas.gov',
},
{
key: 'txdot-cctv',
html:
'CCTV cameras & frames (Texas): ' +
'Texas Department of Transportation (courtesy)',
},
{
key: 'deldot-cctv',
html: 'CCTV live video (Delaware): DelDOT — Delaware Department of Transportation',
},
{
key: 'caltrans-cctv',
html:
'CCTV cameras & frames (California): Caltrans — ' +
'cwwp2.dot.ca.gov',
},
{
key: 'tfl-cctv',
html:
'CCTV cameras & frames (London): ' +
'Powered by TfL Open Data. ' +
'Contains OS data © Crown copyright and database rights.',
},
{
key: 'ontario-511-cctv',
html:
'CCTV cameras & frames (Ontario): ' +
'Ontario 511 ' +
'(Open Government Licence - Ontario)',
},
{
key: 'fintraffic-cctv',
html:
'CCTV cameras & frames (Finland): Fintraffic / ' +
'digitraffic.fi, ' +
'license CC BY 4.0',
},
{
key: 'calgary-cctv',
html:
'Traffic cameras (Calgary): contains information licensed under the ' +
'Open Government Licence – City of Calgary',
},
{
key: 'gbfs',
html: 'Bikeshare availability: GBFS operator feeds (e.g. Austin BCycle)',
},
{
key: 'osrm-routing',
// The service asks for its attribution to carry a "fix the map" link, so
// a reader who spots a wrong turn can go and correct the data it came from.
html:
'Routing (voice routes and Directions): OSRM on the FOSSGIS servers — ' +
'routing.openstreetmap.de · ' +
'fix the map',
},
{
key: 'gtfs-rt',
html: 'Transit vehicles: operator GTFS-Realtime feeds (each operator is credited below when its vehicles are shown)',
},
{
key: 'radio-browser',
html:
'Internet-radio station directory: ' +
'Radio Browser ' +
'(public domain; audio delivered directly by each broadcaster)',
},
{
key: 'reearth-terrain',
html:
'Terrain (keyless globe stacks): ' +
'Re:Earth Terrain / ' +
'Mapterhorn (CC BY 4.0) / EGM2008 (NGA)',
},
{
key: 'weather-noaa',
html: 'Observed weather: NOAA nowCOAST · NWS/OAR MRMS radar; NESDIS GOES and global satellite partners. Source disclaimer.',
},
{
key: 'weather-cyclones',
html: 'Cyclone advisories: NOAA/NWS NHC / CPHC · Atlantic and eastern/central North Pacific. Forecast center uncertainty, not storm size.',
},
{
key: 'weather-lightning',
html: 'Lightning density: NOAA/NWS nowCOAST · derived from Vaisala NLDN/GLD360. Public derived density product, not raw detections.',
},
{
key: 'wind-gfs',
html:
'Wind: resampled NOAA Global Forecast System (GFS) 10 m wind via ' +
'NOAA Open Data on AWS ' +
'(U.S. public domain; forecast, not observations)',
},
{
key: 'ecmwf-ifs',
html:
'This service is based on data and products of the European Centre for Medium-Range Weather Forecasts (ECMWF). ' +
'ECMWF Open Data · ' +
'CC BY 4.0. ' +
'Modified: resampled 10 m IFS forecast vectors and animated display. ' +
'ECMWF does not accept any liability whatsoever for any error or omission in the data, their availability, or for any loss or damage arising from their use.',
},
// ── Bundled snapshots ───────────────────────────────────────────
{
key: 'dams',
html: 'Dams: Open Infrastructure Map',
},
{
key: 'firms',
html:
'Active fires: NASA FIRMS — we acknowledge the use of data and/or imagery ' +
'from NASA’s Fire Information for Resource Management System ' +
'(earthdata.nasa.gov/firms), ' +
'part of NASA’s Earth Observing System Data and Information System (EOSDIS)',
},
{
key: 'drivebc-cctv',
html:
'CCTV cameras & frames (British Columbia): ' +
'DriveBC. ' +
'Contains information licensed under the ' +
'Open Government Licence – British Columbia. ' +
'Some cameras are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada and others); each names its provider in the CCTV panel.',
},
{
key: 'tallinn-cctv',
html:
'CCTV cameras & frames (Tallinn): City of Tallinn — ' +
'ristmikud.tallinn.ee (courtesy)',
},
{
key: 'tarktee-cctv',
html:
'CCTV cameras & frames (Estonia road weather): Transpordiamet / Tarktee — ' +
'tarktee.transpordiamet.ee (courtesy)',
},
{
key: 'warendorf-cctv',
html: 'Webcam (Warendorf): Stadt Warendorf (courtesy)',
},
{
key: 'nsw-cctv',
html:
'CCTV cameras & frames (New South Wales): ' +
'Live Traffic NSW — Transport for NSW ' +
'(CC BY 4.0)',
},
{
key: 'telegeography',
html:
'Submarine cables: © TeleGeography — ' +
'submarinecablemap.com ' +
'(CC BY-NC-SA 3.0 — NonCommercial)',
},
];
/**
* Conditional credits — registered via `registerDynamicCredit` only when the
* corresponding capability actually activates (deliberately NOT part of
* DATA_CREDITS, which is always-on). TomTom terms require attribution when
* their flow data is displayed; keyless installs never show it, so the
* credit only appears once live traffic-flow mode activates.
* @type {{ key: string, html: string }}
*/
export const TOMTOM_CREDIT = {
key: 'tomtom',
html:
'Traffic flow data © ' +
'TomTom',
};
/** Registered when the first Natural Earth region or state/province outline
* resolves (public domain — no attribution required; credited as a courtesy). */
export const NATURAL_EARTH_CREDIT = {
key: 'natural-earth',
html:
'Country, state/province and physical region boundaries from ' +
'Natural Earth (public domain)',
};
/** Registered when the first bundled US county outline resolves (public
* domain — credited as a courtesy). */
export const US_CENSUS_CREDIT = {
key: 'us-census-counties',
html:
'US county boundaries from the ' +
'U.S. Census Bureau (public domain)',
};
/**
* Per-feed transit credit, registered the first time that feed's vehicles
* render (see `src/data/transitFeeds.js` for the license of each).
* @param {{ id: string, attribution: string, license: string, licenseUrl: string }} feed
* @returns {{ key: string, html: string }}
*/
export function transitFeedCredit(feed) {
const escape = (text) =>
String(text)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
return {
key: `transit-${feed.id}`,
html:
`Transit (${escape(feed.attribution)}): ` +
`${escape(feed.license)}`,
};
}
/** Registered when the Bhote Koshi event reconstruction activates. */
export const BHOTE_KOSHI_CREDIT = {
key: 'bhote-koshi-2026',
html:
'Bhote Koshi 2026 event imagery and derived reconstruction: ' +
'Vantor Open Data ' +
'and GeoPera ' +
'(CC BY-NC 4.0); terrain © Re:Earth / Mapterhorn (CC BY 4.0)',
};
/** Registered when the scene-friendly Nepal incident locator activates. */
export const BHOTE_KOSHI_LOCATOR_CREDIT = {
key: 'bhote-koshi-locator',
html:
'Flood corridor derived from the ' +
'GeoPera river centerline ' +
'(CC BY-NC 4.0)',
};
/** @type {Set} Keys of dynamic credits already registered this session. */
const _dynamicCreditKeys = new Set();
/**
* Register a conditional credit at the moment its data source activates.
* Idempotent per `credit.key`; lands in the same "Data attribution" popover
* as the static credits (showOnScreen=false).
* @param {Cesium.Viewer} viewer — the initialized Cesium viewer
* @param {{ key: string, html: string }} credit — e.g. `TOMTOM_CREDIT`
* @returns {boolean} True when the credit is (now) registered.
*/
export function registerDynamicCredit(viewer, credit) {
const creditDisplay = viewer?.creditDisplay;
if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
return false;
}
if (!credit?.key || !credit?.html) return false;
if (_dynamicCreditKeys.has(credit.key)) return true;
creditDisplay.addStaticCredit(new Cesium.Credit(credit.html, false));
_dynamicCreditKeys.add(credit.key);
return true;
}
/**
* Register every per-layer data credit into the viewer's credit display.
* Idempotent: safe to call once at init. Credits are static and always
* present in the "Data attribution" popover.
* @param {Cesium.Viewer} viewer — the initialized Cesium viewer
*/
export function registerDataCredits(viewer, credits = DATA_CREDITS) {
const creditDisplay = viewer?.creditDisplay;
if (!creditDisplay || typeof creditDisplay.addStaticCredit !== 'function') {
return;
}
for (const { html } of credits) {
// showOnScreen=false → lives in the expandable "Data attribution" popover,
// not the on-globe credit line.
creditDisplay.addStaticCredit(new Cesium.Credit(html, false));
}
}