mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge remote-tracking branch 'upstream/main' into fix/tracked-info-vertical-rate
This commit is contained in:
commit
a34deb8015
16 files changed
+452
-3
No files matched your search
@@ -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)',
|
||||
'(e.g. Austin BCycle; Bike Share Toronto, operated by the Toronto Parking Authority; Bike Itaú, operated by Tembici)',
|
||||
},
|
||||
{
|
||||
key: 'urbansharing-gbfs',
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -246,6 +246,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ú',
|
||||
},
|
||||
{
|
||||
id: 'toronto-bike-share',
|
||||
city: 'Toronto, ON',
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
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)),
|
||||
'../..',
|
||||
);
|
||||
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 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));
|
||||
}
|
||||
|
||||
const manifestHref = INDEX_HTML.match(
|
||||
/<link[^>]+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 <link rel="manifest"> 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(
|
||||
/<link[^>]+rel="apple-touch-icon"[^>]+href="([^"]+)"/,
|
||||
)?.[1];
|
||||
assert.ok(href, 'no <link rel="apple-touch-icon"> 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, '.');
|
||||
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.
|
||||
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(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
|
||||
// trusts the declaration and scales whatever it finds.
|
||||
assert.equal(meta.width, width, `${icon.src} width`);
|
||||
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(
|
||||
/<link[^>]+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'),
|
||||
`<!doctype html><html><head>${tags.join('')}</head><body></body></html>`,
|
||||
);
|
||||
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(/<link[^>]+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(/<link[^>]+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))),
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user