mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
Assert Wildlife Sightings on both engines in the strict acceptance
qa-wildlife-switch.mjs serves four Salish Sea sightings (three from iNaturalist, one obscured, and an Acartia orca) through the app's own iNaturalist and Acartia proxies over faked upstream feeds, then on Cesium, Navara, Cesium and Navara again checks the badge set and its positions, picks, the density glow, the privacy cell outline and a clicked card. Two of the switches are made from the keyboard and must keep the selection; the last is made with the pointer on the Globe Engine picker, whose click closes the card like any click outside it.
This commit is contained in:
1 parent
145adc08e0
commit
fc75e245a6
4 files changed
+767
-1
No files matched your search
@@ -0,0 +1,261 @@
|
||||
// Wildlife Sightings without the network: the app's own iNaturalist and
|
||||
// Acartia proxies (server/providers) answer the preview's /api routes in
|
||||
// this process, over upstream feeds faked here. Four sightings over the
|
||||
// Salish Sea: three iNaturalist observations (one with an obscured
|
||||
// location, drawn as its privacy cell) and one Acartia orca report.
|
||||
import { installInatProxy } from '../../server/providers/inaturalist/proxy.fixture.mjs';
|
||||
import { syntheticGrid } from '../../server/providers/inaturalist/grid.fixture.mjs';
|
||||
import { acartiaProxy } from '../../server/providers/acartia.js';
|
||||
|
||||
/** The view the scenario frames (straight down). */
|
||||
export const WILDLIFE_VIEW = Object.freeze({
|
||||
lat: 48.4,
|
||||
lon: -123.1,
|
||||
alt: 200000,
|
||||
});
|
||||
|
||||
const HOUR_MS = 3_600_000;
|
||||
// iNaturalist iconic taxon ids, as the UTFGrid carries them.
|
||||
const ICONIC_IDS = { Aves: 3, Mammalia: 40151, Plantae: 47126 };
|
||||
|
||||
/**
|
||||
* The sightings as the browser draws them (`lat`/`lon` after the proxy's
|
||||
* projection: an obscured record sits at the centre of its 0.2° cell, an
|
||||
* Acartia report is rounded to 0.01°), with what the upstream feeds send.
|
||||
*/
|
||||
export const WILDLIFE_SIGHTINGS = Object.freeze([
|
||||
{
|
||||
id: 'inat:910001',
|
||||
name: 'Bald Eagle',
|
||||
scientific: 'Haliaeetus leucocephalus',
|
||||
iconic: 'Aves',
|
||||
raw: [48.4, -123.1],
|
||||
lat: 48.4,
|
||||
lon: -123.1,
|
||||
},
|
||||
{
|
||||
id: 'inat:910002',
|
||||
name: "Douglas's Squirrel",
|
||||
scientific: 'Tamiasciurus douglasii',
|
||||
iconic: 'Mammalia',
|
||||
raw: [48.46, -122.92],
|
||||
lat: 48.46,
|
||||
lon: -122.92,
|
||||
},
|
||||
{
|
||||
id: 'inat:910003',
|
||||
name: 'Western Redcedar',
|
||||
scientific: 'Thuja plicata',
|
||||
iconic: 'Plantae',
|
||||
obscured: true,
|
||||
raw: [48.31, -123.29],
|
||||
lat: 48.3,
|
||||
lon: -123.3,
|
||||
cell: { south: 48.2, west: -123.4, north: 48.4, east: -123.2 },
|
||||
},
|
||||
{
|
||||
id: 'acartia:qa-orca-1',
|
||||
name: 'Orca',
|
||||
origin: 'acartia',
|
||||
raw: [48.5234, -123.2167],
|
||||
lat: 48.52,
|
||||
lon: -123.22,
|
||||
},
|
||||
]);
|
||||
|
||||
const inat = WILDLIFE_SIGHTINGS.filter((s) => s.id.startsWith('inat:'));
|
||||
const acartia = WILDLIFE_SIGHTINGS.filter((s) => s.origin === 'acartia');
|
||||
|
||||
/** An API v2 observation, with the fields the proxy asks for. */
|
||||
function observation(sighting, nowMs) {
|
||||
const id = Number(sighting.id.slice(5));
|
||||
const [lat, lon] = sighting.raw;
|
||||
const time = new Date(nowMs - 3 * HOUR_MS);
|
||||
return {
|
||||
id,
|
||||
uri: `https://www.inaturalist.org/observations/${id}`,
|
||||
location: `${lat},${lon}`,
|
||||
geojson: { type: 'Point', coordinates: [lon, lat] },
|
||||
geoprivacy: sighting.obscured ? 'obscured' : null,
|
||||
taxon_geoprivacy: null,
|
||||
obscured: sighting.obscured === true,
|
||||
quality_grade: 'research',
|
||||
observed_on: time.toISOString().slice(0, 10),
|
||||
time_observed_at: time.toISOString(),
|
||||
faves_count: 1,
|
||||
num_identification_agreements: 2,
|
||||
// The feed asks for observations with photos; these carry no open
|
||||
// licence, so no thumbnail is fetched.
|
||||
photos: [
|
||||
{
|
||||
url: `https://inaturalist-open-data.s3.amazonaws.com/photos/${id}/square.jpeg`,
|
||||
attribution: '(c) QA observer, all rights reserved',
|
||||
license_code: null,
|
||||
},
|
||||
],
|
||||
taxon: {
|
||||
name: sighting.scientific,
|
||||
preferred_common_name: sighting.name,
|
||||
iconic_taxon_name: sighting.iconic,
|
||||
threatened: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Web Mercator tile coordinates of a point at zoom z (fractional). */
|
||||
function tileOf(lat, lon, z) {
|
||||
const count = 2 ** z;
|
||||
const sin = Math.sin((lat * Math.PI) / 180);
|
||||
return {
|
||||
x: ((lon + 180) / 360) * count,
|
||||
y: (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * count,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The faked upstream feeds: iNaturalist observations (filtered by the
|
||||
* request's box), its UTFGrid density tiles, and the Acartia snapshot.
|
||||
* `calls` counts what each upstream was asked.
|
||||
*/
|
||||
export function wildlifeUpstream({ now = () => Date.now() } = {}) {
|
||||
const calls = { observations: 0, grid: 0, acartia: 0 };
|
||||
async function fetchImpl(href) {
|
||||
const url = new URL(href);
|
||||
if (url.host === 'acartia.io') {
|
||||
calls.acartia++;
|
||||
const created = new Date(now() - 2 * HOUR_MS)
|
||||
.toISOString()
|
||||
.slice(0, 19)
|
||||
.replace('T', ' ');
|
||||
return Response.json(
|
||||
acartia.map((s) => ({
|
||||
entry_id: s.id.slice(8),
|
||||
latitude: s.raw[0],
|
||||
longitude: s.raw[1],
|
||||
created,
|
||||
type: s.name,
|
||||
no_sighted: '3',
|
||||
data_source_name: 'QA Whale Network',
|
||||
data_source_entity: 'Community reports',
|
||||
trusted: true,
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (url.host !== 'api.inaturalist.org')
|
||||
throw Error('unexpected upstream ' + url.host);
|
||||
const grid = /^\/v1\/grid\/(\d+)\/(\d+)\/(\d+)\.grid\.json$/.exec(
|
||||
url.pathname,
|
||||
);
|
||||
if (grid) {
|
||||
calls.grid++;
|
||||
const [z, x, y] = grid.slice(1).map(Number);
|
||||
const cells = [];
|
||||
for (const s of inat) {
|
||||
const at = tileOf(s.raw[0], s.raw[1], z);
|
||||
if (Math.floor(at.x) !== x || Math.floor(at.y) !== y) continue;
|
||||
cells.push({
|
||||
row: Math.min(63, Math.floor((at.y - y) * 64)),
|
||||
column: Math.min(63, Math.floor((at.x - x) * 64)),
|
||||
count: 4,
|
||||
iconic: ICONIC_IDS[s.iconic],
|
||||
});
|
||||
}
|
||||
return Response.json(syntheticGrid({ z, x, y, cells }));
|
||||
}
|
||||
if (url.pathname === '/v2/observations') {
|
||||
calls.observations++;
|
||||
const box = ['swlat', 'swlng', 'nelat', 'nelng'].map((key) =>
|
||||
url.searchParams.has(key) ? Number(url.searchParams.get(key)) : null,
|
||||
);
|
||||
const inside = ({ raw: [lat, lon] }) =>
|
||||
box[0] === null ||
|
||||
(lat >= box[0] && lat <= box[2] && lon >= box[1] && lon <= box[3]);
|
||||
const results = inat.filter(inside).map((s) => observation(s, now()));
|
||||
return Response.json({
|
||||
total_results: results.length,
|
||||
page: 1,
|
||||
per_page: 200,
|
||||
results,
|
||||
});
|
||||
}
|
||||
throw Error('unexpected upstream path ' + url.pathname);
|
||||
}
|
||||
return { fetchImpl, calls };
|
||||
}
|
||||
|
||||
/** Mount an /api/acartia proxy over synthetic HTTP objects. */
|
||||
function installAcartiaProxy(options) {
|
||||
let handler;
|
||||
acartiaProxy({ warn() {}, ...options }).configurePreviewServer({
|
||||
middlewares: {
|
||||
use(mount, callback) {
|
||||
if (mount !== '/api/acartia') throw Error('acartia mount ' + mount);
|
||||
handler = callback;
|
||||
},
|
||||
},
|
||||
});
|
||||
return async (url, { headers = {} } = {}) => {
|
||||
const res = {
|
||||
headers: {},
|
||||
writeHead(status, values) {
|
||||
this.status = status;
|
||||
Object.assign(this.headers, values);
|
||||
},
|
||||
setHeader(key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
end(value) {
|
||||
this.status ??= this.statusCode;
|
||||
this.body = JSON.parse(value);
|
||||
},
|
||||
};
|
||||
await handler(
|
||||
{ url, method: 'GET', headers, socket: { remoteAddress: '127.0.0.1' } },
|
||||
res,
|
||||
);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict-harness routes for the preview's wildlife API: each request is
|
||||
* answered by the app's own proxy in this process (it never reaches the
|
||||
* preview, so the preview never calls the real providers). `asked` counts
|
||||
* the browser's requests per route.
|
||||
*/
|
||||
export function wildlifeRoutes(origin, options = {}) {
|
||||
const upstream = wildlifeUpstream(options);
|
||||
const proxies = {
|
||||
'/api/inaturalist': installInatProxy('/observations', {
|
||||
fetchImpl: upstream.fetchImpl,
|
||||
now: options.now ?? (() => Date.now()),
|
||||
}),
|
||||
'/api/acartia': installAcartiaProxy({
|
||||
fetchImpl: upstream.fetchImpl,
|
||||
now: options.now ?? (() => Date.now()),
|
||||
}),
|
||||
};
|
||||
const asked = { observations: 0, grid: 0, acartia: 0 };
|
||||
const route = (mount) => ({
|
||||
pattern: `${origin}${mount}/*`,
|
||||
async respond(request) {
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname.slice(mount.length);
|
||||
asked[mount === '/api/acartia' ? 'acartia' : path.slice(1)]++;
|
||||
const headers = { host: url.host };
|
||||
for (const [name, value] of Object.entries(request.headers ?? {}))
|
||||
headers[name.toLowerCase()] = value;
|
||||
const res = await proxies[mount](path + url.search, { headers });
|
||||
return {
|
||||
status: res.status,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(res.body),
|
||||
};
|
||||
},
|
||||
});
|
||||
return {
|
||||
routes: [route('/api/inaturalist'), route('/api/acartia')],
|
||||
asked,
|
||||
upstream: upstream.calls,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user