From 2dd96e1f25f37a1464cf84ddbd162c677e546f87 Mon Sep 17 00:00:00 2001 From: MiguelGFerreira Date: Sat, 12 Sep 2026 14:24:33 -0300 Subject: [PATCH 1/8] =?UTF-8?q?feat(data):=20add=20seven=20Brazilian=20Bik?= =?UTF-8?q?e=20Ita=C3=BA=20cities=20to=20the=20GBFS=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and Curitiba run on the same PBSC platform as the existing Austin, Honolulu and Chattanooga entries, so they need no new feed handling and no proxy allowlist change — isAllowedGbfsHost already accepts *.publicbikesystem.net. These are the layer's first stations outside the United States: 1,017 stations across the seven systems. --- CHANGELOG.md | 4 ++ DATA_SOURCES.md | 2 +- src/data/dataCredits.js | 2 +- src/layers/bikeshare/registry.js | 84 ++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f8245b4..3c451b4f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ## [Unreleased] +- Add seven Brazilian Bike Itaú (Tembici) cities to the GBFS bikeshare registry: + São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and + Curitiba. These are the layer's first stations outside the United States. + - Separate radio directory loading, station selection, globe presentation and playback into composed components with an explicit metadata source. - Separate submarine cable sources and rendering components, and export bundled geography lookup modules. diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 650d3fe6b..2dc8ba0d7 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -35,7 +35,7 @@ How to read this: | **City of Austin Open Data** | CCTV camera catalog + frames | City of Austin Open Data Terms of Use | "City of Austin, TX — data.austintexas.gov" | | **Caltrans (cwwp2.dot.ca.gov)** | CCTV camera catalogs + frames, California districts | Public Caltrans traffic camera data | "Caltrans — cwwp2.dot.ca.gov" (courtesy) | | **TfL Open Data (JamCams)** | CCTV camera catalog + frames, London | [TfL Open Data terms](https://tfl.gov.uk/info-for/open-data-users/) — attribution REQUIRED | "Powered by TfL Open Data. Contains OS data © Crown copyright and database rights" | -| **GBFS (Lyft / BCycle)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` | +| **GBFS (Lyft / BCycle / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle, Bike Itaú/Tembici) + its `license_url` | | **Radio Browser** | Geolocated internet-radio station directory and station-level tags | Public-domain directory data under PDDL 1.0; individual broadcaster stream terms apply | "Radio Browser" plus a link to the selected broadcaster | | **Re:Earth Terrain** (Mapterhorn) | Terrain (keyless globe stacks — OSM etc. — + `/api/terrain/heights` ellipsoidal-height lookups) | Terrain mesh: CC BY 4.0; geoid: EGM2008 (NGA, public domain) | "Terrain (keyless globe stacks): Re:Earth Terrain / Mapterhorn (CC BY 4.0) / EGM2008 (NGA)" | diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 009ff6d01..817261608 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -137,7 +137,7 @@ export const DATA_CREDITS = [ }, { key: 'gbfs', - html: 'Bikeshare availability: GBFS operator feeds (e.g. Austin BCycle)', + html: 'Bikeshare availability: GBFS operator feeds (e.g. Austin BCycle, Bike Itaú/Tembici)', }, { key: 'radio-browser', diff --git a/src/layers/bikeshare/registry.js b/src/layers/bikeshare/registry.js index ff752f664..a5a1ee32c 100644 --- a/src/layers/bikeshare/registry.js +++ b/src/layers/bikeshare/registry.js @@ -167,6 +167,90 @@ const RAW_GBFS_CITY_REGISTRY = [ 'https://chat.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', provider: 'Bike Chattanooga', }, + { + id: 'sao-paulo-bike-itau', + city: 'São Paulo, SP', + centerLat: -23.5505, + centerLon: -46.6333, + loadRadiusKm: 100, + stationInformationUrl: + 'https://saopaulo.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://saopaulo.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, + { + id: 'rio-de-janeiro-bike-itau', + city: 'Rio de Janeiro, RJ', + centerLat: -22.9068, + centerLon: -43.1729, + loadRadiusKm: 100, + stationInformationUrl: + 'https://riodejaneiro.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://riodejaneiro.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, + { + id: 'salvador-bike-itau', + city: 'Salvador, BA', + centerLat: -12.9777, + centerLon: -38.5016, + loadRadiusKm: 90, + stationInformationUrl: + 'https://salvador.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://salvador.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, + { + id: 'recife-bike-itau', + city: 'Recife, PE', + centerLat: -8.0476, + centerLon: -34.877, + loadRadiusKm: 90, + stationInformationUrl: + 'https://recife.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://recife.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, + { + id: 'porto-alegre-bike-itau', + city: 'Porto Alegre, RS', + centerLat: -30.0346, + centerLon: -51.2177, + loadRadiusKm: 90, + stationInformationUrl: + 'https://portoalegre.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://portoalegre.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, + { + id: 'brasilia-bike-itau', + city: 'Brasília, DF', + centerLat: -15.7939, + centerLon: -47.8828, + loadRadiusKm: 90, + stationInformationUrl: + 'https://brasilia.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://brasilia.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, + { + id: 'curitiba-bike-itau', + city: 'Curitiba, PR', + centerLat: -25.4284, + centerLon: -49.2733, + loadRadiusKm: 90, + stationInformationUrl: + 'https://curitiba.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://curitiba.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Itaú', + }, bcycleEntry({ id: 'boulder-bcycle', city: 'Boulder, CO', From 9c15f093a1705b52807d44ab02a57f40249ee977 Mon Sep 17 00:00:00 2001 From: Bilawal Sidhu Date: Wed, 23 Sep 2026 21:54:43 -0500 Subject: [PATCH 2/8] fix(cyclones): accept advisories NHC publishes before their nominal time NHC stamps an advisory with its nominal time and writes it about half an hour early, so the 5-minute future limit rejected the whole feed each advisory cycle and the layer showed no storms. Allow 90 minutes ahead. --- CHANGELOG.md | 5 +++++ server/providers/cyclones.js | 5 ++++- src/data/cyclonesProxy.test.mjs | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58103d04d..431d282f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Cyclone advisories no longer go dark while NHC publishes an advisory ahead + of its nominal time: advisory and position times up to 90 minutes ahead of + the server clock are accepted (previously 5 minutes), so the whole feed is + not rejected for about half an hour each advisory cycle. + - Public Overpass instances are no longer used by default. Street Traffic roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen on the layer row (TomTom / OSM / Hybrid) or with `?trafficRoads=`. With a diff --git a/server/providers/cyclones.js b/server/providers/cyclones.js index fede1aade..d580357ce 100644 --- a/server/providers/cyclones.js +++ b/server/providers/cyclones.js @@ -41,6 +41,9 @@ function number(value, min, max) { const n = Number(value); return Number.isFinite(n) && n >= min && n <= max ? n : null; } +// NHC stamps an advisory with its nominal time (e.g. 03:00Z) and publishes it +// up to about half an hour before; allow that, but not a clock far ahead. +const EARLY_ADVISORY_MS = 90 * 60_000; function iso(value, now) { if ( typeof value !== 'string' || @@ -50,7 +53,7 @@ function iso(value, now) { const n = Date.parse(value); if ( !Number.isFinite(n) || - n > now + 5 * 60_000 || + n > now + EARLY_ADVISORY_MS || now - n > 12 * HOUR || new Date(n).toISOString().replace('.000Z', 'Z') !== value.replace('.000Z', 'Z') diff --git a/src/data/cyclonesProxy.test.mjs b/src/data/cyclonesProxy.test.mjs index 04729d33a..31433b326 100644 --- a/src/data/cyclonesProxy.test.mjs +++ b/src/data/cyclonesProxy.test.mjs @@ -162,6 +162,15 @@ test('malformed, duplicate, oversized, future and expired status is rejected rat assert.deepEqual(parseCycloneStatus({ activeStorms: [] }, NOW), []); }); +test('advisories published before their nominal time are accepted, far-future ones are not', () => { + // NHC wrote the 03:00Z advisory at 02:34Z. + const early = Date.UTC(2026, 8, 16, 2, 34); + assert.equal(parseCycloneStatus(status(), early)[0].issuedAt, TIME); + assert.throws(() => + parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)), + ); +}); + test('coherent geometry is retained with holes/dateline while older advisory geometry is omitted', () => { const storms = parseCycloneStatus(status(), NOW); const parts = collections(); From b90bcf1829c4a44f16ceb9ee166a3785cb0345b4 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:00:04 +0530 Subject: [PATCH 3/8] fix(flights): don't treat OpenSky's position-less rows as a partial snapshot OpenSky's state-vector spec allows null longitude/latitude, and every live /states/all response carries such rows (about 50 of 7,800). The normaliser rejects them, so admitRecords reported complete: false on effectively every poll. While a snapshot is incomplete, FlightRecords.absence() retains any aircraft received in the last five minutes, which meant the stale fade, MISSING_POLL_LIMIT and the landed fast-cull never ran: vanished and landed aircraft stayed on the globe at full opacity for up to five minutes. admitRecords now takes an optional unpositionedId(row). A rejected row it names is a contact without a position: its id is returned in retainIds and the row does not count against completeness. OpenSky opts in for rows with an ICAO24 address and strictly null longitude and latitude; the id comes from the same helper the normaliser uses. Rows with no usable id, out-of-range or half-null coordinates, truncated rows and positioned duplicates still make the snapshot incomplete, and a nonempty feed with no positioned rows is still malformed. absence() retains exactly the retainIds aircraft under the existing five-minute bound, except likely-landed ones, which keep the landed limit. Everything else ages out normally. Military readsb admission does not opt in and is unchanged. --- CHANGELOG.md | 5 +++ docs/CURRENT-STATE.md | 8 ++++- src/layers/flights/records.js | 8 +++-- src/layers/flights/records.test.mjs | 24 ++++++++++++++ src/layers/flights/snapshotRenderer.js | 1 + src/sources/live/aircraft.js | 17 ++++++++-- src/sources/live/contract.js | 20 ++++++++++-- src/sources/live/contract.test.mjs | 44 ++++++++++++++++++++++++++ 8 files changed, 118 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24ac95ed..c608afa5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Flights age out vanished aircraft again. OpenSky's routine position-less rows + no longer mark every snapshot partial, which had kept vanished and landed + aircraft at full opacity for up to five minutes; those rows retain only their + own aircraft, and a likely-landed one still leaves on the landed limit. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 26d56e66b..15d5c50f7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -825,7 +825,13 @@ addition to the locally accumulated trail, never a promise of complete coverage. Snapshot coverage, completeness and freshness are separate fields. A partially admitted aircraft snapshot retains absent contacts for up to five minutes before -the usual missed-poll eviction. Unknown snapshot times remain unknown in stats. An invalid nonempty +the usual missed-poll eviction. An OpenSky row with an ICAO24 address but null +longitude and latitude is a contact without a position, not a malformed row: it +leaves the snapshot complete and is listed in `retainIds`, so civil flights +retain that aircraft under the same five-minute bound, unless it is likely +landed, while other absent aircraft fade and leave on the missed-poll limits. A +nonempty snapshot with no positioned rows is still malformed. Military readsb +admission is unchanged. Unknown snapshot times remain unknown in stats. An invalid nonempty snapshot retains the previous display. Empty vessel refreshes retain the existing first-connect grace and warm-data behavior. Known source failures have bounded messages; arbitrary HTTP response bodies are not surfaced as diagnostics. diff --git a/src/layers/flights/records.js b/src/layers/flights/records.js index 43809f516..fdb65c62c 100644 --- a/src/layers/flights/records.js +++ b/src/layers/flights/records.js @@ -258,10 +258,12 @@ export class FlightRecords { return { icao24, prevMeta, meta, groundFlipped, fixEpochMs }; } - absence(id, { complete, likelyLanded }) { - // Partial admissions do not prove absence, but retention remains bounded. + absence(id, { complete, retainIds, likelyLanded }) { + // Partial admissions and position-less rows do not prove absence, but + // retention remains bounded; a likely-landed position-less contact + // still takes the landed limit. if ( - !complete && + (!complete || (retainIds?.has(id) && !likelyLanded)) && Date.now() - (this.data.get(id)?.observedReceiptMs ?? 0) < 300000 ) return 'retain'; diff --git a/src/layers/flights/records.test.mjs b/src/layers/flights/records.test.mjs index d001c7811..783b6d06a 100644 --- a/src/layers/flights/records.test.mjs +++ b/src/layers/flights/records.test.mjs @@ -106,3 +106,27 @@ test('ground transitions preserve last render height while a surface cell is col assert.equal(next.meta.renderAltitudeM, 1040); assert.equal(next.meta.wasAirborne, true); }); + +test('complete snapshots age out vanished aircraft and retain position-less ones unless landed', () => { + const store = records(); + for (const id of ['abc123', 'def456', 'ghi789']) + store.receive(observation({ id, reference: id }), view()); + // abc123 and ghi789 are still listed without a position; def456 vanished. + const absent = (id, likelyLanded = false) => + store.absence(id, { + complete: true, + retainIds: new Set(['abc123', 'ghi789']), + likelyLanded, + }); + assert.deepEqual( + [absent('def456'), absent('def456'), absent('def456')], + ['stale', 'stale', 'remove'], + ); + assert.equal(absent('abc123'), 'retain'); + assert.equal(store.missingPolls.has('abc123'), false); + // A landed contact that lost its position takes the landed fast-cull. + assert.equal(absent('ghi789', true), 'remove'); + // Position-less retention stays bounded like partial admission. + store.data.get('abc123').observedReceiptMs = Date.now() - 300001; + assert.equal(absent('abc123'), 'stale'); +}); diff --git a/src/layers/flights/snapshotRenderer.js b/src/layers/flights/snapshotRenderer.js index f6e74b125..ec6c2933b 100644 --- a/src/layers/flights/snapshotRenderer.js +++ b/src/layers/flights/snapshotRenderer.js @@ -243,6 +243,7 @@ export function createFlightSnapshotRenderer({ if (currentIcaos.has(icao24)) continue; const absence = records.absence(icao24, { complete: snapshot.complete, + retainIds: snapshot.retainIds, likelyLanded: queries._likelyLanded(icao24), }); if (absence === 'retain') continue; diff --git a/src/sources/live/aircraft.js b/src/sources/live/aircraft.js index 9534a5c3c..1ba3af603 100644 --- a/src/sources/live/aircraft.js +++ b/src/sources/live/aircraft.js @@ -6,10 +6,12 @@ import { finite, } from './contract.js'; +const openSkyId = (row) => + Array.isArray(row) ? cleanText(row[0]).toLowerCase() : ''; + /** Normalize the OpenSky state-vector route, including its regional fallback. */ export function normalizeOpenSkyAircraft(row) { - if (!Array.isArray(row)) return null; - const id = cleanText(row[0]).toLowerCase(); + const id = openSkyId(row); if (!id || !coordinates(row[6], row[5])) return null; return { id, @@ -71,6 +73,16 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) { }; } +/** + * OpenSky allows null longitude/latitude for a contact that is still + * broadcasting. Strict nulls also reject rows truncated before the position. + */ +function openSkyUnpositionedId(row) { + return Array.isArray(row) && row[5] === null && row[6] === null + ? openSkyId(row) + : ''; +} + export function openSkySnapshot( payload, { @@ -84,6 +96,7 @@ export function openSkySnapshot( payload?.states, normalizeOpenSkyAircraft, 'OpenSky', + openSkyUnpositionedId, ); const observedAtMs = epoch(payload?.time, 1000); const ageMs = observedAtMs == null ? null : Math.max(0, now - observedAtMs); diff --git a/src/sources/live/contract.js b/src/sources/live/contract.js index 2415ff43c..d99b55f0d 100644 --- a/src/sources/live/contract.js +++ b/src/sources/live/contract.js @@ -53,17 +53,30 @@ export function coordinates(latitude, longitude) { ); } -/** Admit a snapshot atomically; an all-invalid nonempty feed is unavailable. */ -export function admitRecords(rows, normalize, label) { +/** + * Admit a snapshot atomically; an all-invalid nonempty feed is unavailable. + * `unpositionedId(row)` names a rejected row that is a valid contact without + * a position: it is returned in `retainIds` and does not make the snapshot + * incomplete. + */ +export function admitRecords(rows, normalize, label, unpositionedId) { if (!Array.isArray(rows)) throw new LiveSourceError('malformed', `Malformed ${label} response`); const records = []; const ids = new Set(); + const retainIds = new Set(); + let unpositioned = 0; for (const row of rows) { const record = normalize(row); if (record && !ids.has(record.id)) { records.push(record); ids.add(record.id); + } else if (!record) { + const id = unpositionedId?.(row); + if (id) { + retainIds.add(id); + unpositioned += 1; + } } } if (rows.length && !records.length) { @@ -71,7 +84,8 @@ export function admitRecords(rows, normalize, label) { } return { records, - complete: records.length === rows.length, + retainIds, + complete: records.length + unpositioned === rows.length, rejectedCount: rows.length - records.length, }; } diff --git a/src/sources/live/contract.test.mjs b/src/sources/live/contract.test.mjs index 391fe1e72..143a7fade 100644 --- a/src/sources/live/contract.test.mjs +++ b/src/sources/live/contract.test.mjs @@ -67,6 +67,50 @@ test('mixed-invalid and duplicate admissions are explicitly incomplete; all-inva assert.equal(openSkySnapshot({ states: [] }).complete, true); }); +test('position-less OpenSky rows are seen, not missing; malformed rows stay incomplete', () => { + // OpenSky's spec allows null longitude/latitude; every live /states/all + // carries such rows. + const unpositioned = ['DEF456', ...aircraft.slice(1)]; + unpositioned[5] = null; + unpositioned[6] = null; + const snapshot = openSkySnapshot({ states: [aircraft, unpositioned] }); + assert.equal(snapshot.records.length, 1); + assert.equal(snapshot.complete, true); + assert.deepEqual([...snapshot.retainIds], ['def456']); + assert.equal(snapshot.rejectedCount, 1); + const malformed = [...aircraft]; + malformed[5] = 181; + const garbled = openSkySnapshot({ + states: [aircraft, unpositioned, malformed], + }); + assert.equal(garbled.complete, false); + assert.deepEqual([...garbled.retainIds], ['def456']); + // No usable id, only half a position, a truncated row, or no row at all. + for (const row of [ + [' ', ...unpositioned.slice(1)], + Object.assign([...unpositioned], { 5: -97 }), + ['DEF456'], + null, + ]) + assert.equal(openSkySnapshot({ states: [aircraft, row] }).complete, false); + // Repeated position-less rows stay complete. + const repeated = openSkySnapshot({ + states: [aircraft, unpositioned, unpositioned], + }); + assert.equal(repeated.complete, true); + // A feed with no positions at all still cannot prove absence. + assert.throws(() => openSkySnapshot({ states: [unpositioned] }), { + code: 'malformed', + }); + // Military readsb admission keeps its existing semantics. + const military = readsbSnapshot( + { ac: [{ hex: 'abc123', lat: 30, lon: -97 }, { hex: 'def456' }] }, + { observedAtMs: null }, + ); + assert.equal(military.complete, false); + assert.equal(military.retainIds.size, 0); +}); + test('readsb units and position/contact ages are normalized once against the source epoch', () => { const row = normalizeReadsbAircraft( { From bdd64660e7d9d0de31ce58dc8450a09e7a632214 Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Wed, 16 Sep 2026 18:17:09 -0400 Subject: [PATCH 4/8] feat: ship a web app manifest and home-screen icons Adding the app to a phone home screen produced a blank tile: index.html linked only an SVG favicon, and Safari ignores SVG for apple-touch-icon, substituting a screenshot of the page. There was no manifest at all, so Android had nothing to install against either. Generate the rasters from public/logo.svg with sharp, which the repo already carries. The output is committed so a plain `vite build` needs no image toolchain; re-run scripts/generate-icons.mjs after changing the logo. The logo's viewBox is wider than it is tall, so each icon centres the mark on a square #0a0a0f canvas rather than stretching it, inset to 78% normally and 54% for the maskable variant to survive Android's circular crop. The SVG favicon stays as the browser tab icon. --- CHANGELOG.md | 6 +++ docs/CURRENT-STATE.md | 16 +++++++ index.html | 10 +++++ public/icons/icon-180.png | Bin 0 -> 12055 bytes public/icons/icon-192.png | Bin 0 -> 12993 bytes public/icons/icon-32.png | Bin 0 -> 1160 bytes public/icons/icon-512-maskable.png | Bin 0 -> 28226 bytes public/icons/icon-512.png | Bin 0 -> 40995 bytes public/manifest.webmanifest | 21 +++++++++ scripts/format-scope.json | 4 +- scripts/generate-icons.mjs | 70 +++++++++++++++++++++++++++++ src/tooling/webAppIcons.test.mjs | 70 +++++++++++++++++++++++++++++ 12 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 public/icons/icon-180.png create mode 100644 public/icons/icon-192.png create mode 100644 public/icons/icon-32.png create mode 100644 public/icons/icon-512-maskable.png create mode 100644 public/icons/icon-512.png create mode 100644 public/manifest.webmanifest create mode 100644 scripts/generate-icons.mjs create mode 100644 src/tooling/webAppIcons.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 032c6fe2a..eb61ad1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +- Ship a web app manifest and raster home-screen icons, so adding the app to a + phone home screen or installing it on desktop uses the God's Eye View mark + instead of a blank tile. The icons are generated from `public/logo.svg` by + `node scripts/generate-icons.mjs` and committed, so a plain `vite build` needs + no image toolchain. The existing SVG favicon still serves the browser tab. + - GBFS v3 station names use English translations when available, then the first usable translation or short name. Empty or malformed names no longer appear as `[object Object]` or interrupt station loading; legacy string names remain diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 0d3ef31e0..f8e6bff3a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1131,6 +1131,22 @@ services, local voice and standalone key setup. Importing them starts no network acquisition. Browser layer lifecycle, rendering and voice execution stay in their existing modules. +## Web app install icons + +`public/manifest.webmanifest` declares the installable app: standalone display, +`#0a0a0f` background and theme, and 192/512 icons plus a 512 maskable variant. +`index.html` links the manifest, a 180px `apple-touch-icon` and a 32px PNG +favicon; the existing SVG favicon still serves the browser tab. Safari ignores an +SVG apple-touch-icon and substitutes a page screenshot, so the raster is what +makes an installed icon appear at all. + +`node scripts/generate-icons.mjs` renders every raster from `public/logo.svg` and +writes `public/icons/`. Output is committed, so building needs no image +toolchain; re-run the script after changing the logo. Ordinary icons inset the +mark to 78% of the canvas and the maskable variant to 54%, keeping it inside the +circle Android may crop to. `src/tooling/webAppIcons.test.mjs` checks the links, +the manifest fields and that each committed PNG is square at its declared size. + ## Local build preview After `npm run build`, `npm run preview` serves the built app with the same data diff --git a/index.html b/index.html index 2e630c4a8..31db11b06 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,16 @@ God's Eye View + + + + + + + + diff --git a/public/icons/icon-180.png b/public/icons/icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..5655a9e705643b9a36e8d27666014bc940e3d94f GIT binary patch literal 12055 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 literal 0 HcmV?d00001 diff --git a/public/icons/icon-32.png b/public/icons/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..a19279f0690e87da20ab7989e3ef4dd401336f59 GIT binary patch literal 1160 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

jis$0avX_KC%q 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 kDTR-v5{3c)59}m9;{X5v literal 0 HcmV?d00001 diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..644a5c8eae4afb4859ea929be2c517a19e834b39 GIT binary patch literal 40995 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 literal 0 HcmV?d00001 diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 000000000..574b90faf --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "God's Eye View", + "short_name": "God's Eye", + "description": "Live spatial intelligence in the browser.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#0a0a0f", + "theme_color": "#0a0a0f", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { + "src": "/icons/icon-512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 662d33a6b..d910536a2 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -981,5 +981,7 @@ "src/tools/globePanel.test.mjs", "src/costEndpointThrottle.test.mjs", "src/sources/gbfsStations.test.mjs", - "scripts/qa-gbfs-names.mjs" + "scripts/qa-gbfs-names.mjs", + "scripts/generate-icons.mjs", + "src/tooling/webAppIcons.test.mjs" ] diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs new file mode 100644 index 000000000..3c16eb46e --- /dev/null +++ b/scripts/generate-icons.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// Renders the home-screen and favicon rasters from `public/logo.svg`. +// +// Re-run after changing the logo: `node scripts/generate-icons.mjs`. The output +// is committed, because the install icons have to exist in a plain `vite build` +// and contributors should not need an image toolchain to ship the app. +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sharp from 'sharp'; + +const REPO_ROOT = path.resolve( + fileURLToPath(new URL('.', import.meta.url)), + '..', +); +const LOGO = path.join(REPO_ROOT, 'public', 'logo.svg'); +const OUT_DIR = path.join(REPO_ROOT, 'public', 'icons'); + +/** `--bg-dark` from src/ui/styles/foundation.css, so the tile matches the app. */ +const BACKGROUND = '#0a0a0f'; + +/** + * Fraction of the canvas the mark is allowed to occupy. + * + * A maskable icon may be cropped to a circle inscribed in the middle 80% of the + * square, so its mark stays well inside that circle; the other icons are shown + * as drawn and only need optical breathing room. + */ +const INSET = { normal: 0.78, maskable: 0.54 }; + +const TARGETS = [ + { file: 'icon-32.png', size: 32, inset: INSET.normal }, + { file: 'icon-180.png', size: 180, inset: INSET.normal }, + { file: 'icon-192.png', size: 192, inset: INSET.normal }, + { file: 'icon-512.png', size: 512, inset: INSET.normal }, + { file: 'icon-512-maskable.png', size: 512, inset: INSET.maskable }, +]; + +/** Fit the logo inside `size * inset` and centre it on an opaque square. */ +async function render(svg, { size, inset }) { + const box = Math.round(size * inset); + // The logo's viewBox is wider than it is tall, so `contain` bounds it by + // width and leaves the vertical padding uneven — centring is done by the + // composite below rather than by the resize. + const mark = await sharp(svg, { density: 384 }) + .resize(box, box, { + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .png() + .toBuffer(); + return sharp({ + create: { + width: size, + height: size, + channels: 4, + background: BACKGROUND, + }, + }) + .composite([{ input: mark, gravity: 'centre' }]) + .png({ compressionLevel: 9 }) + .toBuffer(); +} + +const svg = await readFile(LOGO); +await mkdir(OUT_DIR, { recursive: true }); +for (const target of TARGETS) { + await writeFile(path.join(OUT_DIR, target.file), await render(svg, target)); + console.log(`icons: wrote ${target.file} (${target.size}px)`); +} diff --git a/src/tooling/webAppIcons.test.mjs b/src/tooling/webAppIcons.test.mjs new file mode 100644 index 000000000..2267872fe --- /dev/null +++ b/src/tooling/webAppIcons.test.mjs @@ -0,0 +1,70 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sharp from 'sharp'; + +const REPO_ROOT = path.resolve( + fileURLToPath(new URL('.', import.meta.url)), + '../..', +); +const PUBLIC_ROOT = path.join(REPO_ROOT, 'public'); +const INDEX_HTML = readFileSync(path.join(REPO_ROOT, 'index.html'), 'utf8'); + +/** `public/` is served at the site root, so a manifest href maps to a file by dropping the leading slash. */ +function publicFile(href) { + assert.ok(href.startsWith('/'), `${href} must be a root-relative URL`); + return path.join(PUBLIC_ROOT, href.slice(1)); +} + +const manifestHref = INDEX_HTML.match( + /]+rel="manifest"[^>]+href="([^"]+)"/, +)?.[1]; + +test('index.html links a web app manifest', () => { + assert.ok(manifestHref, 'no in index.html'); +}); + +test('iOS gets a raster home-screen icon', () => { + // Safari ignores SVG for apple-touch-icon and falls back to a screenshot of + // the page, which is the blank tile this whole file exists to prevent. The + // existing `rel="icon"` SVG covers the browser tab and nothing else. + const href = INDEX_HTML.match( + /]+rel="apple-touch-icon"[^>]+href="([^"]+)"/, + )?.[1]; + assert.ok(href, 'no in index.html'); + assert.match(href, /\.png$/, 'apple-touch-icon must be a PNG'); + assert.doesNotThrow(() => readFileSync(publicFile(href))); +}); + +test('the manifest declares a standalone installable app', () => { + const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); + assert.equal(manifest.display, 'standalone'); + assert.equal(manifest.start_url, '/'); + for (const field of ['name', 'short_name', 'background_color', 'theme_color']) + assert.ok(manifest[field], `manifest is missing ${field}`); + // Android's install prompt needs both, and uses the 512 for the splash screen. + const sizes = new Set(manifest.icons.map((icon) => icon.sizes)); + for (const required of ['192x192', '512x512']) + assert.ok(sizes.has(required), `manifest declares no ${required} icon`); + // Without a maskable icon Android pads the square into a circle and clips + // the mark; with one it crops to the safe zone instead. + assert.ok( + manifest.icons.some((icon) => icon.purpose?.includes('maskable')), + 'manifest declares no maskable icon', + ); +}); + +test('every declared icon is a square PNG at its declared size', async () => { + const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); + for (const icon of manifest.icons) { + const meta = await sharp(publicFile(icon.src)).metadata(); + const [width, height] = icon.sizes.split('x').map(Number); + assert.equal(meta.format, 'png', `${icon.src} is not a PNG`); + // A size the manifest overstates is worse than a missing one: the platform + // trusts the declaration and scales whatever it finds. + assert.equal(meta.width, width, `${icon.src} width`); + assert.equal(meta.height, height, `${icon.src} height`); + } +}); From fc2c372ffcc7cbb67ecda42bd8e27a08be719ca5 Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Thu, 1 Oct 2026 15:01:32 -0400 Subject: [PATCH 5/8] fix: keep install assets inside a non-root base Manifest start_url, scope and icon src resolve against the manifest URL, so root-relative values sent an app served under a path prefix to the domain root. Make them relative, and add a build test under base: '/example/' that asserts every emitted link and manifest URL stays inside the base. index.html is unchanged: Vite already rewrites its links under base. --- CHANGELOG.md | 2 + docs/CURRENT-STATE.md | 6 +++ public/manifest.webmanifest | 10 ++-- src/tooling/webAppIcons.test.mjs | 85 ++++++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb61ad1db..0c07f3c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ instead of a blank tile. The icons are generated from `public/logo.svg` by `node scripts/generate-icons.mjs` and committed, so a plain `vite build` needs no image toolchain. The existing SVG favicon still serves the browser tab. + Manifest paths are relative to the manifest, so a build served under a path + prefix installs and launches inside that prefix. - GBFS v3 station names use English translations when available, then the first usable translation or short name. Empty or malformed names no longer appear as diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index f8e6bff3a..3d51bad2a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1140,12 +1140,18 @@ favicon; the existing SVG favicon still serves the browser tab. Safari ignores a SVG apple-touch-icon and substitutes a page screenshot, so the raster is what makes an installed icon appear at all. +Manifest `start_url`, `scope` and icon paths are relative to the manifest, and +Vite rewrites the `index.html` links under `base`, so a build served from a path +prefix installs and launches inside that prefix. + `node scripts/generate-icons.mjs` renders every raster from `public/logo.svg` and writes `public/icons/`. Output is committed, so building needs no image toolchain; re-run the script after changing the logo. Ordinary icons inset the mark to 78% of the canvas and the maskable variant to 54%, keeping it inside the circle Android may crop to. `src/tooling/webAppIcons.test.mjs` checks the links, the manifest fields and that each committed PNG is square at its declared size. +It also builds the install links under `base: '/example/'` and asserts every +emitted link and manifest URL stays inside that base. ## Local build preview diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 574b90faf..cf4b2b791 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -2,17 +2,17 @@ "name": "God's Eye View", "short_name": "God's Eye", "description": "Live spatial intelligence in the browser.", - "start_url": "/", - "scope": "/", + "start_url": ".", + "scope": ".", "display": "standalone", "orientation": "any", "background_color": "#0a0a0f", "theme_color": "#0a0a0f", "icons": [ - { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, { - "src": "/icons/icon-512-maskable.png", + "src": "icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/src/tooling/webAppIcons.test.mjs b/src/tooling/webAppIcons.test.mjs index 2267872fe..6a6cdb350 100644 --- a/src/tooling/webAppIcons.test.mjs +++ b/src/tooling/webAppIcons.test.mjs @@ -1,9 +1,12 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; +import { rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import sharp from 'sharp'; +import { build } from 'vite'; +import { makeFixtureRoot } from './fixtureRoot.mjs'; const REPO_ROOT = path.resolve( fileURLToPath(new URL('.', import.meta.url)), @@ -12,7 +15,7 @@ const REPO_ROOT = path.resolve( const PUBLIC_ROOT = path.join(REPO_ROOT, 'public'); const INDEX_HTML = readFileSync(path.join(REPO_ROOT, 'index.html'), 'utf8'); -/** `public/` is served at the site root, so a manifest href maps to a file by dropping the leading slash. */ +/** `public/` is served at the site root, so an index.html href maps to a file by dropping the leading slash. */ function publicFile(href) { assert.ok(href.startsWith('/'), `${href} must be a root-relative URL`); return path.join(PUBLIC_ROOT, href.slice(1)); @@ -22,6 +25,18 @@ const manifestHref = INDEX_HTML.match( /]+rel="manifest"[^>]+href="([^"]+)"/, )?.[1]; +/** + * Manifest URLs resolve against the manifest, not the page. Keeping them + * relative is what lets a build served under a path prefix install inside it. + */ +function manifestFile(src) { + assert.ok( + !src.startsWith('/') && !/^[a-z][a-z\d+.-]*:/i.test(src), + `${src} must be relative to the manifest`, + ); + return path.join(path.dirname(publicFile(manifestHref)), src); +} + test('index.html links a web app manifest', () => { assert.ok(manifestHref, 'no in index.html'); }); @@ -41,7 +56,8 @@ test('iOS gets a raster home-screen icon', () => { test('the manifest declares a standalone installable app', () => { const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); assert.equal(manifest.display, 'standalone'); - assert.equal(manifest.start_url, '/'); + assert.equal(manifest.start_url, '.'); + assert.equal(manifest.scope, '.'); for (const field of ['name', 'short_name', 'background_color', 'theme_color']) assert.ok(manifest[field], `manifest is missing ${field}`); // Android's install prompt needs both, and uses the 512 for the splash screen. @@ -59,7 +75,7 @@ test('the manifest declares a standalone installable app', () => { test('every declared icon is a square PNG at its declared size', async () => { const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); for (const icon of manifest.icons) { - const meta = await sharp(publicFile(icon.src)).metadata(); + const meta = await sharp(manifestFile(icon.src)).metadata(); const [width, height] = icon.sizes.split('x').map(Number); assert.equal(meta.format, 'png', `${icon.src} is not a PNG`); // A size the manifest overstates is worse than a missing one: the platform @@ -68,3 +84,66 @@ test('every declared icon is a square PNG at its declared size', async () => { assert.equal(meta.height, height, `${icon.src} height`); } }); + +test('install assets stay inside a non-root Vite base', async (t) => { + // The real index.html pulls the whole Cesium app into a build, so only its + // install links are copied, verbatim, into a fixture page. + const tags = INDEX_HTML.match( + /]+rel="(?:icon|apple-touch-icon|manifest)"[^>]*>/g, + ); + assert.ok(tags?.length >= 3, 'index.html lost its install links'); + const root = await makeFixtureRoot('gev-webapp-icons-'); + t.after(() => rm(root, { recursive: true, force: true })); + await writeFile( + path.join(root, 'index.html'), + `${tags.join('')}`, + ); + const base = '/example/'; + const origin = 'https://host.test'; + const result = await build({ + root, + configFile: false, + envFile: false, + logLevel: 'silent', + base, + publicDir: PUBLIC_ROOT, + build: { write: false }, + }); + const html = result.output.find( + (item) => item.fileName === 'index.html', + ).source; + const hrefs = [...html.matchAll(/]+href="([^"]+)"/g)].map( + (match) => match[1], + ); + assert.equal(hrefs.length, tags.length); + for (const href of hrefs) { + assert.ok(href.startsWith(base), `${href} escapes ${base}`); + assert.doesNotThrow(() => + readFileSync(path.join(PUBLIC_ROOT, href.slice(base.length))), + ); + } + + // Vite copies public/ only when it writes, and copies it verbatim, so the + // deployed manifest is the committed one, served at the emitted href. + const manifestUrl = new URL( + html.match(/]+rel="manifest"[^>]+href="([^"]+)"/)[1], + origin, + ); + const manifest = JSON.parse(readFileSync(publicFile(manifestHref), 'utf8')); + const inBase = (value) => { + const url = new URL(value, manifestUrl); + assert.ok( + url.href.startsWith(origin + base), + `${value} resolves to ${url.href}, outside ${base}`, + ); + return url; + }; + assert.equal(inBase(manifest.start_url).href, origin + base); + assert.equal(inBase(manifest.scope).href, origin + base); + for (const icon of manifest.icons) { + const url = inBase(icon.src); + assert.doesNotThrow(() => + readFileSync(path.join(PUBLIC_ROOT, url.pathname.slice(base.length))), + ); + } +}); From 6d6ce364c684bffb01fb2af1ef18b4a29579d42a Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Fri, 16 Oct 2026 02:03:16 +0200 Subject: [PATCH 6/8] docs(cyclones): document the early-advisory window and pin its bounds Maintainer follow-up for #741, no runtime change. - CURRENT-STATE: advisory issue and position times up to and including 90 minutes ahead of the server clock are accepted because NHC can release advisories before their nominal time; 90 minutes is the app's tolerance, not an NHC limit. Later, non-canonical and over-12-hour-old times still reject the snapshot. - Tests: pin exact 90-minute acceptance and +1 ms rejection, and exact 12-hour acceptance and +1 ms rejection, for issuance and lastUpdate independently; the early-publication test now asserts invalid_cyclone_data. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 7 ++++++- src/data/cyclonesProxy.test.mjs | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index e5b3d2ace..0af075a6d 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -339,7 +339,12 @@ Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bound same-origin provider. The status endpoint lacks browser CORS; each installation fetches keyless public sources at runtime, with no centralized ingestion service. The five-minute snapshot covers Atlantic and eastern/central North Pacific only. -Current positions, advisory issue time and position time stay distinct. Tracks, +Current positions, advisory issue time and position time stay distinct. NHC may +release an advisory before its nominal time (routinely up to about 30 minutes, +rarely about an hour), so advisory issue and position times up to and including +90 minutes ahead of the server clock are accepted; 90 minutes is this app's +tolerance, not an NHC limit. Later, non-canonical, or more than 12-hour-old +times still reject the status snapshot. Tracks, forecast lead-hour points and cones render only when all source parts match the status advisory; a newer status shows its position with geometry pending rather than relabeling older geometry. The cone represents forecast center uncertainty, diff --git a/src/data/cyclonesProxy.test.mjs b/src/data/cyclonesProxy.test.mjs index 31433b326..23e84826c 100644 --- a/src/data/cyclonesProxy.test.mjs +++ b/src/data/cyclonesProxy.test.mjs @@ -166,11 +166,40 @@ test('advisories published before their nominal time are accepted, far-future on // NHC wrote the 03:00Z advisory at 02:34Z. const early = Date.UTC(2026, 8, 16, 2, 34); assert.equal(parseCycloneStatus(status(), early)[0].issuedAt, TIME); - assert.throws(() => - parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)), + assert.throws( + () => parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)), + /invalid_cyclone_data/, ); }); +test('advisory and position times are bounded at 90 minutes ahead and 12 hours old', () => { + const MINUTE = 60_000; + const at = (offsetMs) => new Date(NOW + offsetMs).toISOString(); + const fields = { + issuedAt: (value) => + status({ + forecastAdvisory: { + advNum: '010', + issuance: value, + url: 'https://www.nhc.noaa.gov/text/MIATCMEP5.shtml', + }, + }), + positionAt: (value) => status({ lastUpdate: value }), + }; + for (const [field, payload] of Object.entries(fields)) { + for (const offset of [90 * MINUTE, -12 * 60 * MINUTE]) { + const [storm] = parseCycloneStatus(payload(at(offset)), NOW); + assert.equal(storm[field], at(offset), `${field} ${offset}`); + } + for (const offset of [90 * MINUTE + 1, -12 * 60 * MINUTE - 1]) + assert.throws( + () => parseCycloneStatus(payload(at(offset)), NOW), + /invalid_cyclone_data/, + `${field} ${offset}`, + ); + } +}); + test('coherent geometry is retained with holes/dateline while older advisory geometry is omitted', () => { const storms = parseCycloneStatus(status(), NOW); const parts = collections(); From 3cdb440bc2166fb9bd42104b69f4ddc162c3dd31 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 15 Oct 2026 17:27:55 -0700 Subject: [PATCH 7/8] Use the opaque iOS status bar and keep the SVG tab icon black-translucent needs viewport-fit=cover and safe-area padding, which this change does not add. List the PNG favicon before the SVG so browsers that take the last usable icon keep the SVG, add mobile-web-app-capable, and give the manifest a stable id. --- index.html | 7 +++++-- public/manifest.webmanifest | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index bdc82bd25..8a01415b6 100644 --- a/index.html +++ b/index.html @@ -4,16 +4,19 @@ God's Eye View - + + + - + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index cf4b2b791..478cd9019 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -1,4 +1,5 @@ { + "id": ".", "name": "God's Eye View", "short_name": "God's Eye", "description": "Live spatial intelligence in the browser.", From 687a94c9df5226ace08c5cbb8ff888641ec919da Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 15 Oct 2026 17:27:55 -0700 Subject: [PATCH 8/8] =?UTF-8?q?Document=20Bike=20Ita=C3=BA=20(Tembici)=20a?= =?UTF-8?q?nd=20test=20the=20Brazilian=20cities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seven feeds publish no licence, operator or attribution fields, so DATA_SOURCES records that and credits the operator. Adds a registry test per city: allowed host and path, provider, and activation over its outermost live station. --- CHANGELOG.md | 3 +- DATA_SOURCES.md | 3 +- docs/CURRENT-STATE.md | 2 +- src/data/dataCredits.js | 2 +- src/layers/bikeshare/brPbscCities.test.mjs | 73 ++++++++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 src/layers/bikeshare/brPbscCities.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index f63083839..5d0194a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,8 @@ - Add seven Brazilian Bike Itaú (Tembici) cities to the GBFS bikeshare registry: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and - Curitiba. These are the layer's first stations in South America. + Curitiba (about 1,000 stations in total). These are the layer's first stations + in South America. - Add Bike Share Toronto (1,069 stations) to the bikeshare registry. The system publishes the same PBSC GBFS v2 feeds as the Austin, Honolulu and Chattanooga diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index ac1718308..66c92174e 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -65,8 +65,9 @@ How to read this: | **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" | | **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) | | **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" | -| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle, Bike Itaú/Tembici) + its `license_url` | +| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` | | **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. No licence is published: the City's open-data package declares none and the feed has no `license_url`. Attribution is given to the operator. | "Bike Share Toronto (Toronto Parking Authority)" | +| **Bike Itaú (Tembici)** (`*.publicbikesystem.net`, seven Brazilian cities) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and Curitiba | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)" | | **Oslo / Bergen / Trondheim Bysykkel** (Urban Sharing, `gbfs.urbansharing.com`) | Bikeshare availability, Oslo, Bergen and Trondheim city bikes. Keyless GBFS; the proxy sends the `Client-Identifier` header the operators ask for | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED ([Oslo](https://oslobysykkel.no/en/open-data/realtime), [Bergen](https://bergenbysykkel.no/en/apne-data/sanntid), [Trondheim](https://trondheimbysykkel.no/en/open-data/realtime)) | "Contains data under the Norwegian licence for Open Government data (NLOD) made available by Oslo Bysykkel, Bergen Bysykkel and Trondheim Bysykkel" | | **Kolumbus / Farte Bysykkel** (Entur Mobility, `api.entur.io/mobility/v2/gbfs`) | Bikeshare availability, Kolumbus Bysykkel (Stavanger/Nord-Jæren) and Farte Bysykkel (Skien/Porsgrunn). Keyless GBFS; the proxy sends the `ET-Client-Name` header Entur requires and is limited to Entur's GBFS paths | [NLOD](https://data.norge.no/nlod/en/2.0) per the [Entur Mobility API docs](https://developer.entur.org/pages-mobility-docs-mobility-v2) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) made available by Entur (Kolumbus Bysykkel, Farte Bysykkel)" | | **JCDecaux Cyclocity** (`api.cyclocity.fr`) | Bikeshare availability, Bysykkel Lillestrøm. Keyless GBFS; the proxy is limited to JCDecaux contract paths | [Licence Ouverte / Etalab](https://developer.jcdecaux.com/files/Open-Licence-fr.pdf) (the feed's `license_url`) — attribution REQUIRED | "Bysykkel Lillestrøm: JCDecaux, Licence Ouverte" | diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 61f7a0e80..7a211e806 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3232,7 +3232,7 @@ its criteria cannot be silently ignored. | CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | | Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh | | Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) | -| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto + Urban Sharing, Entur, JCDecaux NO) | `src/data/bikeshare.js` | `/api/gbfs` | 60s | +| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto and Bike Itaú (Brazil) + Urban Sharing, Entur, JCDecaux NO) | `src/data/bikeshare.js` | `/api/gbfs` | 60s | | Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change | | Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static | | Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static | diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 40537d4a9..d6c265ddc 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -304,7 +304,7 @@ export const DATA_CREDITS = [ key: 'gbfs', html: 'Bikeshare availability: GBFS operator feeds ' + - '(e.g. Austin BCycle; Bike Share Toronto, operated by the Toronto Parking Authority; Bike Itaú/Tembici)', + '(e.g. Austin BCycle; Bike Share Toronto, operated by the Toronto Parking Authority; Bike Itaú, operated by Tembici)', }, { key: 'urbansharing-gbfs', diff --git a/src/layers/bikeshare/brPbscCities.test.mjs b/src/layers/bikeshare/brPbscCities.test.mjs new file mode 100644 index 000000000..60e12c23b --- /dev/null +++ b/src/layers/bikeshare/brPbscCities.test.mjs @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { CITY_BY_ID } from './registry.js'; +import { createModel } from './model.js'; +import { createViewport } from './viewport.js'; +import { isAllowedGbfsHost, isAllowedGbfsPath } from '../../data/gbfsSource.js'; + +// Each Bike Itaú city's PBSC host, and the station farthest from its +// registered centre in the live feed (checked 2026-10-15). +const CITIES = { + 'sao-paulo-bike-itau': { + host: 'saopaulo.publicbikesystem.net', + stations: [{ name: '260 - P3', lat: -23.568235, lon: -46.740715 }], + }, + 'rio-de-janeiro-bike-itau': { + host: 'riodejaneiro.publicbikesystem.net', + stations: [{ name: '293 - Praia da Macumba', lat: -23.03135, lon: -43.479584 }], + }, + 'salvador-bike-itau': { + host: 'salvador.publicbikesystem.net', + stations: [{ name: '49 - Stella Maris', lat: -12.93645, lon: -38.32701 }], + }, + 'recife-bike-itau': { + host: 'recife.publicbikesystem.net', + stations: [{ name: '69 - Praia de Piedade', lat: -8.186205, lon: -34.918371 }], + }, + 'porto-alegre-bike-itau': { + host: 'portoalegre.publicbikesystem.net', + stations: [{ name: '72 - Avenida Tramandaí', lat: -30.135915, lon: -51.22684 }], + }, + 'brasilia-bike-itau': { + host: 'brasilia.publicbikesystem.net', + stations: [{ name: "60 - Parque Olhos d'Água", lat: -15.7431, lon: -47.88667 }], + }, + 'curitiba-bike-itau': { + host: 'curitiba.publicbikesystem.net', + stations: [{ name: '42 - Terminal Portão', lat: -25.476885, lon: -49.292608 }], + }, +}; + +// The production range check, with no viewer or fetches behind it. +const parts = { model: createModel({ state: {}, services: {}, parts: {}, source: null }) }; +const { computeInRangeCities } = createViewport({ + state: {}, + services: { render: { governorRequestRender() {} } }, + parts, + source: null, +}); + +test('the Bike Itaú systems are registered and readable through the proxy', () => { + for (const [id, expected] of Object.entries(CITIES)) { + const city = CITY_BY_ID.get(id); + assert.ok(city, `${id} is registered`); + assert.equal(city.provider, 'Bike Itaú'); + assert.deepEqual(city.hosts, [expected.host]); + for (const feed of [city.stationInformationUrl, city.stationStatusUrl]) { + const url = new URL(feed); + assert.ok(isAllowedGbfsHost(url.hostname), `${id} host ${url.hostname}`); + assert.ok(isAllowedGbfsPath(url.pathname), `${id} path ${url.pathname}`); + } + } +}); + +test('a camera over each outermost station activates its system', () => { + for (const [id, expected] of Object.entries(CITIES)) { + for (const station of expected.stations) { + assert.ok( + computeInRangeCities({ lat: station.lat, lon: station.lon }).has(id), + `${station.name} activates ${id}`, + ); + } + } +});