mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge branch 'main' into feat/webapp-icons
This commit is contained in:
commit
a478f26eb9
326 files changed
+39867
-1817
No files matched your search
@@ -53,6 +53,29 @@ function isolate(t, env = {}) {
|
||||
}
|
||||
const json = (res) => JSON.parse(res.body);
|
||||
|
||||
/** A response that streams 1 MiB chunks of `text` well past every cap, then ends. */
|
||||
function oversizedResponse(text) {
|
||||
const state = { pulled: 0, cancelled: false };
|
||||
const chunk = new TextEncoder().encode(
|
||||
text.repeat(Math.ceil((1 << 20) / text.length)),
|
||||
);
|
||||
const response = new Response(
|
||||
new ReadableStream(
|
||||
{
|
||||
pull(controller) {
|
||||
if (++state.pulled > 40) controller.close();
|
||||
else controller.enqueue(chunk);
|
||||
},
|
||||
cancel() {
|
||||
state.cancelled = true;
|
||||
},
|
||||
},
|
||||
{ highWaterMark: 0 },
|
||||
),
|
||||
);
|
||||
return { response, state };
|
||||
}
|
||||
|
||||
test('standalone composition mounts every extracted provider exactly once without acquisition', (t) => {
|
||||
t.mock.method(globalThis, 'fetch', () => {
|
||||
throw Error('construction must not fetch');
|
||||
@@ -168,6 +191,29 @@ test('traffic middleware preserves keyless mode, caching, stale budget fallback
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('traffic middleware answers 502 to an oversized tile without caching it', async (t) => {
|
||||
isolate(t, { TOMTOM_API_KEY: 'fixture-key', TOMTOM_DAILY_TILE_BUDGET: '' });
|
||||
const bodies = [];
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
const oversized = oversizedResponse('\x01');
|
||||
bodies.push(oversized.state);
|
||||
return oversized.response;
|
||||
});
|
||||
const request = install(tomtomProxy());
|
||||
const first = await request('/flow/8/1/1.pbf');
|
||||
assert.equal(first.status, 502);
|
||||
assert.equal(bodies[0].cancelled, true);
|
||||
assert.ok(bodies[0].pulled <= 9, 'reading stops at the 8 MiB cap');
|
||||
assert.ok(
|
||||
!fsp.writeFile.mock.calls.some(({ arguments: [file] }) =>
|
||||
String(file).endsWith('.pbf'),
|
||||
),
|
||||
'no tile reached the disk cache',
|
||||
);
|
||||
assert.equal((await request('/flow/8/1/1.pbf')).status, 502);
|
||||
assert.equal(bodies.length, 2, 'nothing was cached in memory');
|
||||
});
|
||||
|
||||
test('FIRMS retains a large successful source during partial failure and filters stale data at serve time', async (t) => {
|
||||
isolate(t, { FIRMS_MAP_KEY: '' });
|
||||
let now = Date.UTC(2026, 8, 12, 12);
|
||||
|
||||
@@ -37,6 +37,30 @@ function install(plugin, preview = false) {
|
||||
};
|
||||
}
|
||||
|
||||
/** A response that streams 1 MiB chunks of `text` well past every cap, then ends. */
|
||||
function oversizedResponse(text, init = {}) {
|
||||
const state = { pulled: 0, cancelled: false };
|
||||
const chunk = new TextEncoder().encode(
|
||||
text.repeat(Math.ceil((1 << 20) / text.length)),
|
||||
);
|
||||
const response = new Response(
|
||||
new ReadableStream(
|
||||
{
|
||||
pull(controller) {
|
||||
if (++state.pulled > 40) controller.close();
|
||||
else controller.enqueue(chunk);
|
||||
},
|
||||
cancel() {
|
||||
state.cancelled = true;
|
||||
},
|
||||
},
|
||||
{ highWaterMark: 0 },
|
||||
),
|
||||
init,
|
||||
);
|
||||
return { response, state };
|
||||
}
|
||||
|
||||
function environment(t, values) {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
const original = process.env[key];
|
||||
@@ -193,6 +217,130 @@ test('OpenSky global snapshot attempts are time-limited, retried once, then fall
|
||||
assert.equal(stalled.response.headers['x-flight-source'], 'adsb.lol');
|
||||
});
|
||||
|
||||
const openSkyAnonEnv = {
|
||||
OPENSKY_CLIENT_ID: undefined,
|
||||
OPENSKY_CLIENT_SECRET: undefined,
|
||||
OPENSKY_AUTH_MODE: 'anon',
|
||||
OPENSKY_USERNAME: undefined,
|
||||
OPENSKY_PASSWORD: undefined,
|
||||
};
|
||||
const regionalFallback = () =>
|
||||
Response.json({
|
||||
now: Date.now() / 1000,
|
||||
ac: [{ hex: 'abc123', lat: 30, lon: -97, alt_baro: 10000 }],
|
||||
});
|
||||
|
||||
test('OpenSky 429 starts the cooldown from headers and cancels the body unread', async (t) => {
|
||||
environment(t, openSkyAnonEnv);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.method(console, 'error', () => {});
|
||||
let states = 0;
|
||||
let limited;
|
||||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||
if (url.includes('/states/')) {
|
||||
states++;
|
||||
limited = oversizedResponse('x', {
|
||||
status: 429,
|
||||
headers: { 'x-rate-limit-retry-after-seconds': '600' },
|
||||
});
|
||||
return limited.response;
|
||||
}
|
||||
if (url.includes('/lat/')) return regionalFallback();
|
||||
throw Error(`Unexpected URL: ${url}`);
|
||||
});
|
||||
const fresh = await import(
|
||||
`../../server/providers/aircraft/opensky.js?ratelimit=${Math.random()}`
|
||||
);
|
||||
const request = install(fresh.openSkyProxy());
|
||||
const first = await request('/api/flights', '?lat=30&lon=-97');
|
||||
assert.equal(first.statusCode, 200);
|
||||
assert.equal(first.headers['x-flight-source'], 'adsb.lol');
|
||||
assert.equal(states, 1, 'a rate-limited snapshot is not retried');
|
||||
assert.equal(limited.state.cancelled, true);
|
||||
assert.ok(limited.state.pulled <= 1, 'the 429 body is not read');
|
||||
const cooling = await request('/api/flights', '?lat=30&lon=-97');
|
||||
assert.equal(cooling.statusCode, 200);
|
||||
assert.equal(
|
||||
cooling.headers['x-opensky-auth-reason'],
|
||||
'opensky_cooldown_regional_fallback',
|
||||
);
|
||||
assert.equal(states, 1, 'the cooldown holds off the next upstream attempt');
|
||||
});
|
||||
|
||||
test('OpenSky 429 without retry-after cools down for the 2 minute default', async (t) => {
|
||||
environment(t, openSkyAnonEnv);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.method(console, 'error', () => {});
|
||||
const start = Date.now();
|
||||
let now = start;
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
let states = 0;
|
||||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||
if (url.includes('/states/')) {
|
||||
states++;
|
||||
return new Response('{}', { status: 429 });
|
||||
}
|
||||
if (url.includes('/lat/')) return regionalFallback();
|
||||
throw Error(`Unexpected URL: ${url}`);
|
||||
});
|
||||
const fresh = await import(
|
||||
`../../server/providers/aircraft/opensky.js?ratelimit-default=${Math.random()}`
|
||||
);
|
||||
const request = install(fresh.openSkyProxy());
|
||||
await request('/api/flights', '?lat=30&lon=-97');
|
||||
assert.equal(states, 1);
|
||||
// Past the 30 s floor but inside the 2 minute default.
|
||||
now = start + 90_000;
|
||||
await request('/api/flights', '?lat=30&lon=-97');
|
||||
assert.equal(states, 1, 'a missing header is not read as a zero wait');
|
||||
});
|
||||
|
||||
test('OpenSky does not retry a snapshot body over the cap', async (t) => {
|
||||
environment(t, openSkyAnonEnv);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.method(console, 'error', () => {});
|
||||
const bodies = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||
if (url.includes('/states/')) {
|
||||
const oversized = oversizedResponse(' ');
|
||||
bodies.push(oversized.state);
|
||||
return oversized.response;
|
||||
}
|
||||
if (url.includes('/lat/')) return regionalFallback();
|
||||
throw Error(`Unexpected URL: ${url}`);
|
||||
});
|
||||
const fresh = await import(
|
||||
`../../server/providers/aircraft/opensky.js?oversize=${Math.random()}`
|
||||
);
|
||||
const response = await install(fresh.openSkyProxy())(
|
||||
'/api/flights',
|
||||
'?lat=30&lon=-97',
|
||||
);
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.headers['x-flight-source'], 'adsb.lol');
|
||||
assert.equal(bodies.length, 1, 'an oversized body is not fetched twice');
|
||||
assert.equal(bodies[0].cancelled, true);
|
||||
assert.ok(bodies[0].pulled <= 9, 'reading stops at the 8 MiB cap');
|
||||
});
|
||||
|
||||
test('military aircraft route answers 502 to an oversized body without caching it', async (t) => {
|
||||
t.mock.method(console, 'error', () => {});
|
||||
const bodies = [];
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
const oversized = oversizedResponse('{"ac":[]}');
|
||||
bodies.push(oversized.state);
|
||||
return oversized.response;
|
||||
});
|
||||
const request = install(providers.adsbLolProxy());
|
||||
const first = await request('/api/military');
|
||||
assert.equal(first.statusCode, 502);
|
||||
assert.equal(bodies[0].cancelled, true);
|
||||
assert.ok(bodies[0].pulled <= 9, 'reading stops at the 8 MiB cap');
|
||||
const again = await request('/api/military');
|
||||
assert.equal(again.statusCode, 502, 'nothing was cached');
|
||||
assert.equal(bodies.length, 2);
|
||||
});
|
||||
|
||||
test('military aircraft route preserves fresh cache and stale response after upstream failure', async (t) => {
|
||||
let now = Date.now();
|
||||
let calls = 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { makeFixtureRoot } from './fixtureRoot.mjs';
|
||||
test('data providers have both hooks; credential editing stays development-only', () => {
|
||||
const plugins = localProviderPlugins();
|
||||
assert.ok(plugins.some(({ name }) => name === 'fire-perimeters'));
|
||||
assert.ok(plugins.some(({ name }) => name === 'gnss-integrity'));
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.name === 'gev-key-setup') {
|
||||
assert.equal(plugin.configurePreviewServer, undefined);
|
||||
@@ -103,6 +104,7 @@ test('real dev and built-preview servers serve provider JSON and terminate unkno
|
||||
['/api/adsbdb/type/invalid', 400],
|
||||
['/api/firms/status', 200],
|
||||
['/api/fire-perimeters/inciweb/publication/invalid', 400],
|
||||
['/api/gnss-integrity?lat=invalid', 400],
|
||||
['/api/terrain/heights?points=invalid', 400],
|
||||
['/api/overpass', 405],
|
||||
['/api/overpass/status', 200],
|
||||
|
||||
@@ -47,6 +47,29 @@ function isolateDisk(t) {
|
||||
t.mock.method(fsp, 'writeFile', async () => {});
|
||||
}
|
||||
|
||||
/** A response that streams 1 MiB chunks of `text` well past every cap, then ends. */
|
||||
function oversizedResponse(text) {
|
||||
const state = { pulled: 0, cancelled: false };
|
||||
const chunk = new TextEncoder().encode(
|
||||
text.repeat(Math.ceil((1 << 20) / text.length)),
|
||||
);
|
||||
const response = new Response(
|
||||
new ReadableStream(
|
||||
{
|
||||
pull(controller) {
|
||||
if (++state.pulled > 40) controller.close();
|
||||
else controller.enqueue(chunk);
|
||||
},
|
||||
cancel() {
|
||||
state.cancelled = true;
|
||||
},
|
||||
},
|
||||
{ highWaterMark: 0 },
|
||||
),
|
||||
);
|
||||
return { response, state };
|
||||
}
|
||||
|
||||
test('portable requests keep fixed origins, encode group data and preserve the 30-day UTC window', () => {
|
||||
const tle = celestrakTleUrl('stations&FORMAT=json');
|
||||
assert.equal(tle.origin, 'https://celestrak.org');
|
||||
@@ -120,6 +143,27 @@ test('exported CelesTrak plugin coalesces refreshes, retains stale TLEs, and rea
|
||||
assert.equal(disk.body, tle);
|
||||
});
|
||||
|
||||
test('CelesTrak answers 502 to an oversized TLE body without caching it', async (t) => {
|
||||
isolateDisk(t);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
const bodies = [];
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
const oversized = oversizedResponse(
|
||||
'ISS\n1 25544U fixture\n2 25544 fixture\n',
|
||||
);
|
||||
bodies.push(oversized.state);
|
||||
return oversized.response;
|
||||
});
|
||||
const request = install(celestrakProxy());
|
||||
const first = await request('/api/celestrak', '/stations');
|
||||
assert.equal(first.status, 502);
|
||||
assert.equal(bodies[0].cancelled, true);
|
||||
assert.ok(bodies[0].pulled <= 17, 'reading stops at the 16 MiB cap');
|
||||
assert.equal(fsp.writeFile.mock.callCount(), 0);
|
||||
assert.equal((await request('/api/celestrak', '/stations')).status, 502);
|
||||
assert.equal(bodies.length, 2, 'nothing was cached in memory');
|
||||
});
|
||||
|
||||
for (const preview of [false, true])
|
||||
test(`exported launch plugin preserves optional server auth and cache in ${preview ? 'preview' : 'development'}`, async (t) => {
|
||||
isolateDisk(t);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { inflateSync } from 'node:zlib';
|
||||
import { createApplicationStreetLevel } from '../app/layers/streetLevel.js';
|
||||
import {
|
||||
fixtureTile,
|
||||
@@ -20,6 +21,24 @@ import {
|
||||
THUMB_HOST,
|
||||
} from '../../scripts/fixtures/street-level/mapillaryGraph.mjs';
|
||||
import { redact } from '../../scripts/qa-browserEvidence.mjs';
|
||||
import {
|
||||
answerKartaViewRequest,
|
||||
describeKartaViewCall,
|
||||
KV_LINE,
|
||||
KV_OLD_STREET,
|
||||
KV_SEQUENCE_ID,
|
||||
kartaViewPhotos,
|
||||
resetKartaViewFixture,
|
||||
} from '../../scripts/fixtures/street-level/kartaView.mjs';
|
||||
import {
|
||||
maskedSegments,
|
||||
tileSegments,
|
||||
} from '../layers/streetLevel/providers/kartaview/decode.js';
|
||||
import { lonToTileX, latToTileY } from '../layers/streetLevel/tileMath.js';
|
||||
import {
|
||||
normalizePhoto,
|
||||
resizedImageUrl,
|
||||
} from '../layers/streetLevel/providers/kartaview/policy.js';
|
||||
|
||||
// The press helpers wait for paint; Node has no frames, so a tick stands in.
|
||||
globalThis.requestAnimationFrame ??= (callback) =>
|
||||
@@ -639,3 +658,279 @@ test('CI runs the gates hermetically against a production build, strict, with ev
|
||||
'a missed header press fails CI now that the rail settles',
|
||||
);
|
||||
});
|
||||
|
||||
test('KartaView fixtures draw the drive, answer nearest first, and refuse the storage host the API names', () => {
|
||||
const body = (answer) => JSON.parse(answer.body);
|
||||
const lat = (KV_LINE.south + KV_LINE.north) / 2;
|
||||
const x = lonToTileX(KV_LINE.lon, 14);
|
||||
const y = latToTileY(lat, 14);
|
||||
const tile = answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.kartaview.org/2.0/sequence/tiles/${x}/${y}/14.json`,
|
||||
});
|
||||
assert.equal(tile.known, true);
|
||||
assert.ok(body(tile).lines.length >= 1);
|
||||
const empty = answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.kartaview.org/2.0/sequence/tiles/${x + 3}/${y}/14.json`,
|
||||
});
|
||||
assert.deepEqual(body(empty).lines, []);
|
||||
const nearUrl = `https://api.kartaview.org/2.0/photo/?lat=${lat}&lng=${KV_LINE.lon + 0.0001}&zoomLevel=18&itemsPerPage=10`;
|
||||
resetKartaViewFixture();
|
||||
// The first lookup is rate-limited, readable cross-origin; the retry answers.
|
||||
const limited = answerKartaViewRequest({ method: 'GET', url: nearUrl });
|
||||
assert.equal(limited.status, 429);
|
||||
assert.equal(limited.known, true);
|
||||
assert.equal(limited.headers['Retry-After'], '0');
|
||||
assert.match(limited.headers['Access-Control-Expose-Headers'], /Retry-After/);
|
||||
const near = answerKartaViewRequest({ method: 'GET', url: nearUrl });
|
||||
assert.equal(near.status, 200);
|
||||
const records = body(near).result.data;
|
||||
assert.ok(records.length > 1);
|
||||
assert.ok(Number(records[0].distance) <= Number(records[1].distance));
|
||||
const photo = normalizePhoto(records[0]);
|
||||
assert.match(photo.urls.view, /^https:\/\/storage2\.openstreetcam\.org\//);
|
||||
assert.equal(
|
||||
answerKartaViewRequest({ method: 'GET', url: photo.urls.view }).known,
|
||||
true,
|
||||
);
|
||||
// Every odd photo is archived: its file answers 409, an even one the image.
|
||||
const photos = kartaViewPhotos();
|
||||
const storageAnswer = (index) =>
|
||||
answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: normalizePhoto(
|
||||
body(
|
||||
answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.kartaview.org/2.0/photo/${photos[index].id}`,
|
||||
}),
|
||||
).result.data,
|
||||
).urls.view,
|
||||
});
|
||||
assert.equal(photos[3].archived, true);
|
||||
assert.equal(storageAnswer(3).status, 409);
|
||||
assert.equal(storageAnswer(3).known, true);
|
||||
assert.equal(photos[4].archived, false);
|
||||
assert.equal(storageAnswer(4).status, 200);
|
||||
assert.equal(storageAnswer(4).contentType, 'image/jpeg');
|
||||
// The image proxy still serves an archived photo: the 360° step opens one.
|
||||
const pano = photos.find((p) => p.isPano);
|
||||
assert.equal(pano.archived, true);
|
||||
const panoRecord = body(
|
||||
answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.kartaview.org/2.0/photo/${pano.id}`,
|
||||
}),
|
||||
).result.data;
|
||||
assert.equal(
|
||||
answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: resizedImageUrl(normalizePhoto(panoRecord).urls.pano, [1024, 512]),
|
||||
}).status,
|
||||
200,
|
||||
);
|
||||
assert.equal(
|
||||
answerKartaViewRequest({ method: 'GET', url: records[0].fileurlLTh }).known,
|
||||
false,
|
||||
'the storage1 address the API gives is not where the photo is',
|
||||
);
|
||||
const page = answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.kartaview.org/2.0/sequence/${KV_SEQUENCE_ID}/photos?page=1&itemsPerPage=1000`,
|
||||
});
|
||||
assert.equal(body(page).result.data.length, kartaViewPhotos().length);
|
||||
assert.ok(kartaViewPhotos().some((p) => p.isPano));
|
||||
assert.equal(
|
||||
describeKartaViewCall(
|
||||
'GET',
|
||||
new URL('https://api.kartaview.org/2.0/photo/77000001'),
|
||||
),
|
||||
'GET api.kartaview.org/2.0/photo/{id}',
|
||||
);
|
||||
});
|
||||
|
||||
test('Panoramax fixtures: two sequences on one street, searches honour sequence and filters, unknown calls are flagged', async () => {
|
||||
const {
|
||||
answerPanoramaxRequest,
|
||||
describePanoramaxCall,
|
||||
PNX_LINE,
|
||||
PNX_SEQUENCES,
|
||||
panoramaxHdDownloads,
|
||||
panoramaxPictures,
|
||||
panoramaxTile,
|
||||
resetPanoramaxFixture,
|
||||
} = await import('../../scripts/fixtures/street-level/panoramax.mjs');
|
||||
const { decodeCoverageTile, decodePictures } =
|
||||
await import('../layers/streetLevel/providers/panoramax/decode.js');
|
||||
const { normalizeItem } =
|
||||
await import('../layers/streetLevel/providers/panoramax/policy.js');
|
||||
resetPanoramaxFixture();
|
||||
const pictures = panoramaxPictures();
|
||||
assert.deepEqual(
|
||||
[...new Set(pictures.map((p) => p.sequenceId))],
|
||||
[...PNX_SEQUENCES],
|
||||
);
|
||||
assert.ok(pictures.some((p) => p.isPano));
|
||||
// Coverage: both lines in a z14 tile on the street; cones in z15.
|
||||
const z = 14;
|
||||
const x = lonToTileX((PNX_LINE.west + PNX_LINE.east) / 2, z);
|
||||
const y = latToTileY(PNX_LINE.lat, z);
|
||||
const lines = decodeCoverageTile(panoramaxTile(z, x, y), { x, y, z });
|
||||
assert.deepEqual(
|
||||
lines.sequences.map((s) => s.id).sort(),
|
||||
[...PNX_SEQUENCES].sort(),
|
||||
);
|
||||
const t15 = {
|
||||
x: lonToTileX(pictures[20].lon, 15),
|
||||
y: latToTileY(PNX_LINE.lat, 15),
|
||||
z: 15,
|
||||
};
|
||||
assert.ok(
|
||||
decodePictures(panoramaxTile(15, t15.x, t15.y), t15, PNX_SEQUENCES[0])
|
||||
.length > 0,
|
||||
);
|
||||
// A search for one sequence near the street gives only its pictures, nearest first.
|
||||
const at = pictures[30];
|
||||
const ask = (query) =>
|
||||
JSON.parse(
|
||||
answerPanoramaxRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.panoramax.xyz/api/search?${query}`,
|
||||
}).body,
|
||||
).features.map(normalizeItem);
|
||||
const own = ask(
|
||||
`place_position=${at.lon},${at.lat}&place_distance=0-50&limit=5&collections=${PNX_SEQUENCES[1]}`,
|
||||
);
|
||||
assert.ok(own.length > 0);
|
||||
assert.ok(own.every((p) => p.sequenceId === PNX_SEQUENCES[1]));
|
||||
assert.equal(own[0].nextId && typeof own[0].nextId, 'string');
|
||||
const panos = ask(
|
||||
`place_position=${at.lon},${at.lat}&place_distance=0-200&limit=50&filter=field_of_view%3D360`,
|
||||
);
|
||||
assert.ok(panos.length > 0 && panos.every((p) => p.isPano));
|
||||
assert.deepEqual(
|
||||
ask(
|
||||
`place_position=${at.lon},${at.lat}&place_distance=0-200&limit=50&datetime=2024-01-01T00:00:00.000Z/..`,
|
||||
).every((p) => p.sequenceId === PNX_SEQUENCES[0]),
|
||||
true,
|
||||
'the second sequence is older',
|
||||
);
|
||||
// A box has every picture in it, those looking away included; a place
|
||||
// search leaves out pictures the point is behind (all look east).
|
||||
const d = 0.0002;
|
||||
const boxed = ask(
|
||||
`bbox=${at.lon - d},${at.lat - d},${at.lon + d},${at.lat + d}&limit=100`,
|
||||
);
|
||||
assert.ok(
|
||||
boxed.some((p) => p.lon < at.lon) && boxed.some((p) => p.lon > at.lon),
|
||||
);
|
||||
const placed = ask(
|
||||
`place_position=${at.lon},${at.lat}&place_distance=0-30&limit=50`,
|
||||
);
|
||||
assert.ok(placed.length > 0);
|
||||
assert.ok(
|
||||
placed.every((p) => p.lon <= at.lon + 1e-7),
|
||||
'none it is behind',
|
||||
);
|
||||
const one = ask(`ids=${at.id}&limit=1`);
|
||||
assert.equal(one[0].id, at.id);
|
||||
// Its pictures, and only those, are served; HD downloads are counted.
|
||||
const file = (name) =>
|
||||
answerPanoramaxRequest({
|
||||
method: 'GET',
|
||||
url: `https://panoramax.openstreetmap.fr/derivates/${name}`,
|
||||
});
|
||||
assert.equal(file(`${at.id}/sd.jpg`).status, 200);
|
||||
assert.equal(file(`${at.id}/hd.jpg`).status, 200);
|
||||
assert.equal(panoramaxHdDownloads(), 1);
|
||||
assert.equal(file('nope/sd.jpg').known, false);
|
||||
const unknown = answerPanoramaxRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.panoramax.xyz/api/collections/x/items',
|
||||
});
|
||||
assert.equal(unknown.known, false);
|
||||
assert.equal(
|
||||
describePanoramaxCall(
|
||||
'GET',
|
||||
new URL('https://api.panoramax.xyz/api/map/14/1/2.mvt'),
|
||||
),
|
||||
'GET api.panoramax.xyz/api/map/{z}/{x}/{y}.mvt',
|
||||
);
|
||||
});
|
||||
|
||||
/** The alpha of a fixture PNG (8-bit RGBA, unfiltered rows, as it writes them). */
|
||||
function fixturePngAlpha(bytes) {
|
||||
const width = bytes.readUInt32BE(16);
|
||||
const height = bytes.readUInt32BE(20);
|
||||
const idat = [];
|
||||
for (let at = 8; at < bytes.length;) {
|
||||
const length = bytes.readUInt32BE(at);
|
||||
const type = bytes.toString('ascii', at + 4, at + 8);
|
||||
if (type === 'IDAT') idat.push(bytes.subarray(at + 8, at + 8 + length));
|
||||
at += 12 + length;
|
||||
}
|
||||
const rows = inflateSync(Buffer.concat(idat));
|
||||
const alpha = new Uint8Array(width * height);
|
||||
for (let y = 0; y < height; y++)
|
||||
for (let x = 0; x < width; x++)
|
||||
alpha[y * width + x] = rows[y * (width * 4 + 1) + 1 + x * 4 + 3];
|
||||
return { width, height, alpha };
|
||||
}
|
||||
|
||||
test('KartaView fixtures filter like KartaView: PNG tiles by view and upload date, the photo search by projection', () => {
|
||||
const body = (answer) => JSON.parse(answer.body);
|
||||
const z = 14;
|
||||
const x = lonToTileX(KV_LINE.lon, z);
|
||||
const y = latToTileY((KV_LINE.south + KV_LINE.north) / 2, z);
|
||||
assert.equal(x, lonToTileX(KV_OLD_STREET.lon, z), 'both streets in a tile');
|
||||
const base = `https://api.kartaview.org/2.0/sequence/tiles/${x}/${y}/${z}`;
|
||||
const segments = tileSegments(
|
||||
body(answerKartaViewRequest({ method: 'GET', url: `${base}.json` })),
|
||||
);
|
||||
const lons = (kept) => [...new Set(kept.map((segment) => segment[0][0]))];
|
||||
const keptFor = (query) => {
|
||||
const answer = answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `${base}.png?${query}`,
|
||||
});
|
||||
assert.equal(answer.contentType, 'image/png');
|
||||
assert.equal(answer.known, true);
|
||||
return lons(
|
||||
maskedSegments(segments, fixturePngAlpha(answer.body), { x, y, z }),
|
||||
);
|
||||
};
|
||||
assert.deepEqual(
|
||||
lons(segments).sort(),
|
||||
[KV_LINE.lon, KV_OLD_STREET.lon].sort(),
|
||||
);
|
||||
assert.deepEqual(keptFor('fieldOfView=360'), [KV_LINE.lon]);
|
||||
assert.deepEqual(keptFor('fieldOfView=plane').sort(), lons(segments).sort());
|
||||
const lastYear = new Date(Date.now() - 365 * 86_400_000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
assert.deepEqual(keptFor(`startDate=${lastYear}`), [KV_LINE.lon]);
|
||||
assert.deepEqual(keptFor('startDate=2099-01-01'), []);
|
||||
assert.equal(
|
||||
describeKartaViewCall('GET', new URL(`${base}.png?fieldOfView=360`)),
|
||||
'GET api.kartaview.org/2.0/sequence/tiles/{x}/{y}/{z}.png?fieldOfView',
|
||||
);
|
||||
// The photo search: 360° only, and nothing uploaded after the drive.
|
||||
resetKartaViewFixture();
|
||||
const search = (extra) => {
|
||||
answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.kartaview.org/2.0/photo/?lat=0&lng=0',
|
||||
});
|
||||
return body(
|
||||
answerKartaViewRequest({
|
||||
method: 'GET',
|
||||
url: `https://api.kartaview.org/2.0/photo/?lat=${KV_LINE.south + 0.002}&lng=${KV_LINE.lon}&zoomLevel=18&itemsPerPage=10${extra}`,
|
||||
}),
|
||||
).result.data;
|
||||
};
|
||||
const panos = search('&projection=SPHERE');
|
||||
assert.ok(panos.length > 0);
|
||||
assert.ok(panos.every((record) => record.projection === 'SPHERE'));
|
||||
assert.deepEqual(search('&startDate=2099-01-01'), []);
|
||||
});
|
||||
@@ -675,7 +675,7 @@ test('public transit documentation uses courtesy attribution and the approved gr
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes(
|
||||
'Twenty layers and map sources. **Seventeen have a keyless path.**',
|
||||
'Twenty layers and map sources. **Eighteen have a keyless path.**',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
|
||||
Reference in new issue
Block a user