mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(layers): add FAA drone restrictions airspace layer
Show US prohibited and restricted special-use airspace from the keyless FAA SUA FeatureServer as an off-by-default Infrastructure layer, with share-link token 3 and an informational-only disclaimer.
This commit is contained in:
1 parent
8b8ccc9a81
commit
ffb332472d
35 files changed
+1573
-74
No files matched your search
@@ -160,6 +160,13 @@ export const DATA_CREDITS = [
|
||||
' · Incident information: ' +
|
||||
'<a href="https://inciweb.wildfire.gov/" target="_blank" rel="noopener">InciWeb</a>',
|
||||
},
|
||||
{
|
||||
key: 'faa-sua',
|
||||
html:
|
||||
'Drone restrictions: ' +
|
||||
'<a href="https://adds-faa.opendata.arcgis.com/" target="_blank" rel="noopener">FAA Special Use Airspace</a>' +
|
||||
' (prohibited and restricted zones; informational only)',
|
||||
},
|
||||
OSM_CREDIT,
|
||||
OPENMAPTILES_CREDIT,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { droneAirspaceProxy } from '../../server/providers/droneAirspace.js';
|
||||
|
||||
const feature = (id = 310, type = 'P') => ({
|
||||
properties: {
|
||||
NAME: `${type}-${id}`,
|
||||
TYPE_CODE: type,
|
||||
OBJECTID: id,
|
||||
UPPER_DESC: 'TNI',
|
||||
},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[-77, 38],
|
||||
[-76, 38],
|
||||
[-76, 39],
|
||||
[-77, 38],
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function install(options = {}, hook = 'configureServer') {
|
||||
let handler;
|
||||
const plugin = droneAirspaceProxy(options);
|
||||
assert.equal(typeof plugin.configureServer, 'function');
|
||||
assert.equal(typeof plugin.configurePreviewServer, 'function');
|
||||
plugin[hook]({
|
||||
middlewares: {
|
||||
use(path, callback) {
|
||||
assert.equal(path, '/api/drone-airspace');
|
||||
handler = callback;
|
||||
},
|
||||
},
|
||||
});
|
||||
return async (url = '/', method = 'GET', peer = 'local') => {
|
||||
const res = {
|
||||
writeHead(status, headers) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
},
|
||||
end(body) {
|
||||
this.body = JSON.parse(body);
|
||||
},
|
||||
};
|
||||
await handler({ url, method, socket: { remoteAddress: peer } }, res);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
for (const hook of ['configureServer', 'configurePreviewServer']) {
|
||||
test(`${hook}: paging stops at five pages and normalizes the combined snapshot`, async () => {
|
||||
const calls = [];
|
||||
const request = install(
|
||||
{
|
||||
now: () => 1234,
|
||||
fetchImpl: async (url, options) => {
|
||||
calls.push(new URL(url));
|
||||
assert.ok(options.signal instanceof AbortSignal);
|
||||
assert.equal(options.redirect, 'error');
|
||||
return Response.json({
|
||||
features: [feature(calls.length)],
|
||||
exceededTransferLimit: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
hook,
|
||||
);
|
||||
const res = await request('/?url=https://invalid.example&resultOffset=999');
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.fetchedAt, 1234);
|
||||
assert.equal(res.body.rows.length, 5);
|
||||
assert.equal(calls.length, 5);
|
||||
assert.equal(calls[0].searchParams.get('where'), "TYPE_CODE IN ('P','R')");
|
||||
assert.equal(calls[0].searchParams.get('f'), 'geojson');
|
||||
assert.equal(calls[0].searchParams.has('resultOffset'), false);
|
||||
assert.equal(calls[1].searchParams.get('resultOffset'), '1');
|
||||
});
|
||||
}
|
||||
|
||||
test('a cached snapshot is reused until TTL, then refreshed', async () => {
|
||||
let now = 1000;
|
||||
let fetches = 0;
|
||||
const request = install({
|
||||
now: () => now,
|
||||
fetchImpl: async () => {
|
||||
fetches += 1;
|
||||
return Response.json({ features: [feature(fetches)] });
|
||||
},
|
||||
});
|
||||
const first = await request();
|
||||
now += 3_599_000;
|
||||
const cached = await request();
|
||||
now += 2000;
|
||||
const refreshed = await request();
|
||||
assert.equal(first.body.rows[0].stableId, '1');
|
||||
assert.equal(cached.body.rows[0].stableId, '1');
|
||||
assert.equal(refreshed.body.rows[0].stableId, '2');
|
||||
assert.equal(fetches, 2);
|
||||
});
|
||||
|
||||
test('a failed refresh serves the last good snapshot as stale', async () => {
|
||||
let now = 1000;
|
||||
let fail = false;
|
||||
const request = install({
|
||||
now: () => now,
|
||||
fetchImpl: async () => {
|
||||
if (fail) return new Response('nope', { status: 503 });
|
||||
return Response.json({ features: [feature(7)] });
|
||||
},
|
||||
});
|
||||
const first = await request();
|
||||
fail = true;
|
||||
now += 3_600_001;
|
||||
const stale = await request();
|
||||
assert.equal(first.status, 200);
|
||||
assert.equal(stale.status, 200);
|
||||
assert.equal(stale.headers['X-Data-Stale'], 'true');
|
||||
assert.equal(stale.body.stale, true);
|
||||
assert.equal(stale.body.rows[0].stableId, '7');
|
||||
});
|
||||
|
||||
test('method, unknown routes and rate limits are rejected', async () => {
|
||||
const request = install({
|
||||
fetchImpl: async () => Response.json({ features: [feature()] }),
|
||||
});
|
||||
assert.equal((await request('/', 'POST')).status, 405);
|
||||
assert.equal((await request('/nope')).status, 404);
|
||||
const limited = install({
|
||||
fetchImpl: async () => Response.json({ features: [feature()] }),
|
||||
});
|
||||
let last;
|
||||
for (let i = 0; i < 61; i++) last = await limited('/', 'GET', '10.0.0.9');
|
||||
assert.equal(last.status, 429);
|
||||
});
|
||||
|
||||
test('a cold upstream failure is 502, not an empty snapshot', async () => {
|
||||
const request = install({
|
||||
fetchImpl: async () => new Response('nope', { status: 503 }),
|
||||
});
|
||||
const res = await request();
|
||||
assert.equal(res.status, 502);
|
||||
assert.equal(res.body.error, 'drone_airspace_unavailable');
|
||||
});
|
||||
@@ -595,6 +595,11 @@ export const LAYER_STATE_REGISTRY = Object.freeze([
|
||||
optionOwner: 'cctv',
|
||||
}),
|
||||
Object.freeze({ id: 'directions', token: 'n', disposition: 'enabled-only' }),
|
||||
Object.freeze({
|
||||
id: 'drone-airspace',
|
||||
token: '3',
|
||||
disposition: 'enabled-only',
|
||||
}),
|
||||
Object.freeze({ id: 'earthquakes', token: 'e', disposition: 'enabled-only' }),
|
||||
Object.freeze({
|
||||
id: 'fire-perimeters',
|
||||
|
||||
@@ -202,8 +202,8 @@ function encode(state) {
|
||||
|
||||
test('production registry is exact, canonical, and rejects incomplete contracts', async () => {
|
||||
assert.equal(validateLayerStateRegistry(), true);
|
||||
assert.equal(REGISTERED_LAYER_IDS.length, 29);
|
||||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 29);
|
||||
assert.equal(REGISTERED_LAYER_IDS.length, 30);
|
||||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 30);
|
||||
assert.ok(REGISTERED_LAYER_IDS.includes('transit'));
|
||||
assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort());
|
||||
assert.deepEqual(LEGACY_LAYER_STATE_TOKENS, {
|
||||
@@ -242,7 +242,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
|
||||
assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token);
|
||||
}
|
||||
assert.equal(nextLayerStateToken(), '3');
|
||||
assert.equal(nextLayerStateToken(), '4');
|
||||
assert.equal(
|
||||
nextLayerStateToken({
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
@@ -307,8 +307,8 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
assert.equal(
|
||||
validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
future: '3',
|
||||
next: '4',
|
||||
future: '4',
|
||||
next: '5',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -318,7 +318,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
future: '00',
|
||||
}),
|
||||
/next free token 3/,
|
||||
/next free token 4/,
|
||||
);
|
||||
const beforeLastDigit = { ...digitsExhausted };
|
||||
delete beforeLastDigit['prior-9'];
|
||||
@@ -341,10 +341,10 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
assert.throws(
|
||||
() =>
|
||||
validateLayerStateAllocations(
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '3' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '3', competing: '3' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '4' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '4', competing: '4' },
|
||||
),
|
||||
/next free token 4/,
|
||||
/next free token 5/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLayerStateAllocations({ future: '00' }, { future: '01' }),
|
||||
@@ -2639,6 +2639,22 @@ test('the recent-imagery split is share-link only: never stored locally, and a s
|
||||
);
|
||||
});
|
||||
|
||||
test('drone restrictions uses digit 3 without colliding with fire perimeters', () => {
|
||||
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=3.2'));
|
||||
assert.deepEqual(decoded.enabledLayerIds, [
|
||||
'drone-airspace',
|
||||
'fire-perimeters',
|
||||
]);
|
||||
assert.equal(
|
||||
LAYER_STATE_REGISTRY.find(({ id }) => id === 'drone-airspace').token,
|
||||
'3',
|
||||
);
|
||||
assert.deepEqual(
|
||||
decodeLayerStateParams(new URLSearchParams(encode(decoded))),
|
||||
decoded,
|
||||
);
|
||||
});
|
||||
|
||||
test('fire perimeters uses digit 2 without colliding with wind or recent imagery', () => {
|
||||
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=2.k.1'));
|
||||
assert.deepEqual(decoded.enabledLayerIds, [
|
||||
|
||||
@@ -128,10 +128,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
{ ...parseLayerStateTokenReservations(reservationRows) },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
|
||||
);
|
||||
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 29);
|
||||
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 30);
|
||||
assert.deepEqual(
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
|
||||
{ ...LEGACY_LAYER_STATE_TOKENS, 'street-level': '0' },
|
||||
{
|
||||
...LEGACY_LAYER_STATE_TOKENS,
|
||||
'street-level': '0',
|
||||
'drone-airspace': '3',
|
||||
},
|
||||
);
|
||||
assert.throws(
|
||||
() => parseLayerStateTokenReservations(reservationRows.slice(1)),
|
||||
@@ -151,9 +155,9 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
/Duplicate layer-state token reservation/,
|
||||
);
|
||||
assert.equal(
|
||||
parseLayerStateTokenReservations([...reservationRows, ['future', '3']])
|
||||
parseLayerStateTokenReservations([...reservationRows, ['future', '4']])
|
||||
.future,
|
||||
'3',
|
||||
'4',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
@@ -171,14 +175,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
|
||||
test('published retired digits and competing PRs advance the merge-time allocation', () => {
|
||||
withPublishedBase(
|
||||
{ rows: [...reservationRows, ['retired-layer', '3']] },
|
||||
{ rows: [...reservationRows, ['retired-layer', '4']] },
|
||||
(cwd) => {
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(nextLayerStateToken(published), '4');
|
||||
assert.equal(nextLayerStateToken(published), '5');
|
||||
assert.equal(
|
||||
validateLayerStateAllocations(published, {
|
||||
...published,
|
||||
newcomer: '4',
|
||||
newcomer: '5',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -186,15 +190,15 @@ test('published retired digits and competing PRs advance the merge-time allocati
|
||||
() =>
|
||||
validateLayerStateAllocations(published, {
|
||||
...published,
|
||||
competing: '3',
|
||||
competing: '4',
|
||||
}),
|
||||
/next free token 4/,
|
||||
/next free token 5/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateLayerStateAllocations(published, {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
newcomer: '3',
|
||||
newcomer: '4',
|
||||
}),
|
||||
/changed or removed: retired-layer/,
|
||||
);
|
||||
@@ -202,8 +206,8 @@ test('published retired digits and competing PRs advance the merge-time allocati
|
||||
);
|
||||
});
|
||||
|
||||
test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => {
|
||||
withPublishedBase({ rows: [...reservationRows, ['pr-a', '3']] }, (cwd) => {
|
||||
test('PR B manually replaces provisional 4 with 5 after PR A publishes 4', () => {
|
||||
withPublishedBase({ rows: [...reservationRows, ['pr-a', '4']] }, (cwd) => {
|
||||
mkdirSync(path.join(cwd, 'scripts'));
|
||||
const fixtureChecker = path.join(
|
||||
cwd,
|
||||
@@ -224,8 +228,8 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-b', '3']],
|
||||
[{ id: 'pr-b', token: '3' }],
|
||||
[...reservationRows, ['pr-b', '4']],
|
||||
[{ id: 'pr-b', token: '4' }],
|
||||
);
|
||||
const beforeRebase = runCheck();
|
||||
assert.equal(beforeRebase.status, 1);
|
||||
@@ -233,10 +237,10 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-a', '3'], ['pr-b', '3']],
|
||||
[...reservationRows, ['pr-a', '4'], ['pr-b', '4']],
|
||||
[
|
||||
{ id: 'pr-a', token: '3' },
|
||||
{ id: 'pr-b', token: '3' },
|
||||
{ id: 'pr-a', token: '4' },
|
||||
{ id: 'pr-b', token: '4' },
|
||||
],
|
||||
);
|
||||
const staleRebase = runCheck();
|
||||
@@ -244,16 +248,16 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
assert.match(staleRebase.stderr, /Duplicate layer-state token reservation/);
|
||||
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(nextLayerStateToken(published), '4');
|
||||
const validBaselineRows = [...reservationRows, ['pr-a', '3']];
|
||||
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '3' }]);
|
||||
assert.equal(nextLayerStateToken(published), '5');
|
||||
const validBaselineRows = [...reservationRows, ['pr-a', '4']];
|
||||
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '4' }]);
|
||||
const helper = spawnSync(
|
||||
process.execPath,
|
||||
[realpathSync(fixtureNext), 'pr-b'],
|
||||
{ cwd, encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(helper.status, 0, helper.stderr);
|
||||
assert.match(helper.stdout, /pr-b: 4/);
|
||||
assert.match(helper.stdout, /pr-b: 5/);
|
||||
assert.deepEqual(
|
||||
JSON.parse(readFileSync(path.join(cwd, LEDGER_PATH), 'utf8')),
|
||||
validBaselineRows,
|
||||
@@ -261,20 +265,20 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
);
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-a', '3'], ['pr-b', '4']],
|
||||
[...reservationRows, ['pr-a', '4'], ['pr-b', '5']],
|
||||
[
|
||||
{ id: 'pr-a', token: '3' },
|
||||
{ id: 'pr-b', token: '4' },
|
||||
{ id: 'pr-a', token: '4' },
|
||||
{ id: 'pr-b', token: '5' },
|
||||
],
|
||||
);
|
||||
const corrected = runCheck();
|
||||
assert.equal(corrected.status, 0, corrected.stderr);
|
||||
assert.match(corrected.stdout, /30 published, 1 new/);
|
||||
assert.match(corrected.stdout, /31 published, 1 new/);
|
||||
});
|
||||
});
|
||||
|
||||
test('allocation batches cross the last digit and base-36 pair boundaries in order', () => {
|
||||
const digitRows = [...'3456789'].map((digit) => [`retired-${digit}`, digit]);
|
||||
const digitRows = [...'456789'].map((digit) => [`retired-${digit}`, digit]);
|
||||
const allDigits = parseLayerStateTokenReservations([
|
||||
...reservationRows,
|
||||
...digitRows,
|
||||
@@ -326,7 +330,7 @@ test('allocation batches cross the last digit and base-36 pair boundaries in ord
|
||||
test('an isolated valid two-character fixture round-trips an l field beyond the old 64-character cap', async () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), 'gev-layer-token-width-'));
|
||||
try {
|
||||
const priorDigits = [...'3456789'].map((digit) => [
|
||||
const priorDigits = [...'456789'].map((digit) => [
|
||||
`qa-prior-digit-${digit}`,
|
||||
digit,
|
||||
]);
|
||||
@@ -363,7 +367,7 @@ test('an isolated valid two-character fixture round-trips an l field beyond the
|
||||
const params = new URLSearchParams([['v', '2']]);
|
||||
codec.encodeLayerStateParams(params, state);
|
||||
assert.ok(params.get('l').length > 64, 'fixture must cross the old cap');
|
||||
assert.equal(params.get('l').length, 69);
|
||||
assert.equal(params.get('l').length, 71);
|
||||
const restored = codec.decodeLayerStateParams(params);
|
||||
assert.deepEqual(restored?.enabledLayerIds, expectedLayerIds);
|
||||
assert.deepEqual(restored?.options, state.options);
|
||||
@@ -381,7 +385,7 @@ test('checker reads a complete future base ledger and rejects retired-token reus
|
||||
},
|
||||
(cwd) => {
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(Object.keys(published).length, 30);
|
||||
assert.equal(Object.keys(published).length, 31);
|
||||
assert.equal(published['retired-layer'], '00');
|
||||
assert.throws(
|
||||
() =>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
["bikeshare", "b"],
|
||||
["cctv", "c"],
|
||||
["directions", "n"],
|
||||
["drone-airspace", "3"],
|
||||
["earthquakes", "e"],
|
||||
["fire-perimeters", "2"],
|
||||
["flights", "f"],
|
||||
|
||||
Reference in new issue
Block a user