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)); } }