mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Drop earthquakes older than the USGS feed window and pin duplicate and horizon edge cases
This commit is contained in:
1 parent
03cd379c0e
commit
736ee8d694
2 files changed
+153
No files matched your search
@@ -0,0 +1,143 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
|
||||||
|
import { isOverlayPointVisible } from '../../overlays/worldOverlay.js';
|
||||||
|
import {
|
||||||
|
createEarthquakesLayer,
|
||||||
|
createEarthquakeOverlayEntry,
|
||||||
|
normalizeEarthquakeSnapshot,
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const GENERATED_MS = Date.UTC(2026, 0, 2, 12);
|
||||||
|
|
||||||
|
const feature = (id, lon, lat, mag, time) => ({
|
||||||
|
id,
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: { type: 'Point', coordinates: [lon, lat, 10] },
|
||||||
|
properties: { mag, place: `Place ${id}`, time },
|
||||||
|
});
|
||||||
|
|
||||||
|
// The USGS all_day feed: one fresh event, one exactly on the window edge
|
||||||
|
// and one older than the feed's own 24 h window.
|
||||||
|
const FIXED_SAMPLE = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
metadata: { generated: GENERATED_MS },
|
||||||
|
features: [
|
||||||
|
feature('us-fresh', 10, 20, 4.1, GENERATED_MS - 60_000),
|
||||||
|
feature('us-edge', 11, 21, 3.2, GENERATED_MS - DAY_MS),
|
||||||
|
feature('us-expired', 12, 22, 5.5, GENERATED_MS - DAY_MS - 1),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const WITH_DUPLICATE = {
|
||||||
|
...FIXED_SAMPLE,
|
||||||
|
features: [
|
||||||
|
...FIXED_SAMPLE.features,
|
||||||
|
feature('us-fresh', 10.5, 20.5, 4.4, GENERATED_MS - 30_000),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function layerWith(payloads) {
|
||||||
|
const replaced = [];
|
||||||
|
const labelSets = [];
|
||||||
|
const queue = [...payloads];
|
||||||
|
const layer = createEarthquakesLayer({
|
||||||
|
source: {
|
||||||
|
async getSnapshot() {
|
||||||
|
const rows = normalizeEarthquakeSnapshot(queue.shift());
|
||||||
|
if (!rows) throw new Error('Malformed USGS response');
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overlayHost: {
|
||||||
|
setEntries(_source, entries) {
|
||||||
|
labelSets.push(entries.map((entry) => entry.id));
|
||||||
|
},
|
||||||
|
setVisible() {},
|
||||||
|
clearSource() {},
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
create: () => ({
|
||||||
|
setVisible() {},
|
||||||
|
replace(discs) {
|
||||||
|
replaced.push(discs.map((disc) => disc.stableId));
|
||||||
|
},
|
||||||
|
destroy() {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { layer, replaced, labelSets };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('edge:earthquakes: an expired record is dropped and a duplicate never forks the ID set', async () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
normalizeEarthquakeSnapshot(FIXED_SAMPLE).map((row) => row.stableId),
|
||||||
|
['us-fresh', 'us-edge'],
|
||||||
|
);
|
||||||
|
// A duplicate ID is a malformed feed: it is rejected whole so the last good
|
||||||
|
// snapshot (one disc per ID) stays on screen with the source error shown.
|
||||||
|
assert.equal(normalizeEarthquakeSnapshot(WITH_DUPLICATE), null);
|
||||||
|
|
||||||
|
const { layer, replaced, labelSets } = layerWith([
|
||||||
|
FIXED_SAMPLE,
|
||||||
|
WITH_DUPLICATE,
|
||||||
|
]);
|
||||||
|
layer.init(null, { engine: {} });
|
||||||
|
layer.enable();
|
||||||
|
assert.equal(await layer.update(), true);
|
||||||
|
assert.equal(await layer.update(), false);
|
||||||
|
const ids = ['us-fresh', 'us-edge'];
|
||||||
|
assert.deepEqual(replaced, [ids]);
|
||||||
|
assert.deepEqual(labelSets.at(-1).toSorted(), ids.toSorted());
|
||||||
|
assert.deepEqual(
|
||||||
|
layer.getAnalystRecords().map((row) => row.id),
|
||||||
|
ids,
|
||||||
|
);
|
||||||
|
assert.equal(layer.getStats().count, 2);
|
||||||
|
assert.match(layer.getStats().error, /Malformed/);
|
||||||
|
layer.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge:earthquakes: near the horizon the label of the same ID is kept in front and culled behind', () => {
|
||||||
|
const engine = createFakeEngine();
|
||||||
|
// 1000 km over (0, 0) looking east at the limb, which lies ~30.2° away.
|
||||||
|
engine.camera.setView({
|
||||||
|
destination: geo.cartesian3.fromDegrees(0, 0, 1_000_000),
|
||||||
|
orientation: { heading: Math.PI / 2, pitch: -0.52, roll: 0 },
|
||||||
|
});
|
||||||
|
const occluder = {
|
||||||
|
isPointVisible: (position) => engine.projector.horizonVisible(position),
|
||||||
|
};
|
||||||
|
const viewport = engine.host.size();
|
||||||
|
const rows = normalizeEarthquakeSnapshot({
|
||||||
|
features: [
|
||||||
|
feature('us-front', 29.5, 0, 4.5, null),
|
||||||
|
feature('us-behind', 31.5, 0, 4.5, null),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const visible = rows
|
||||||
|
.map((row) =>
|
||||||
|
createEarthquakeOverlayEntry({
|
||||||
|
id: row.stableId,
|
||||||
|
position: geo.cartesian3.fromDegrees(row.lon, row.lat),
|
||||||
|
magnitude: row.mag,
|
||||||
|
accent: '#f00',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.filter((entry) => {
|
||||||
|
const screen = engine.projector.worldToWindow(entry.position, {});
|
||||||
|
// Both anchors project inside the viewport; only the horizon separates them.
|
||||||
|
assert.ok(screen && screen.x > 0 && screen.x < viewport.width);
|
||||||
|
return isOverlayPointVisible(
|
||||||
|
entry,
|
||||||
|
entry.position,
|
||||||
|
screen,
|
||||||
|
viewport,
|
||||||
|
occluder,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((entry) => entry.id);
|
||||||
|
assert.deepEqual(visible, ['us-front']);
|
||||||
|
});
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
|
// The layer shows the USGS all_day feed: events within 24 h of its generation.
|
||||||
|
const FEED_WINDOW_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
/** Validate a complete feed before replacing the last good earthquake snapshot. */
|
/** Validate a complete feed before replacing the last good earthquake snapshot. */
|
||||||
export function normalizeEarthquakeSnapshot(geojson) {
|
export function normalizeEarthquakeSnapshot(geojson) {
|
||||||
if (!Array.isArray(geojson?.features)) return null;
|
if (!Array.isArray(geojson?.features)) return null;
|
||||||
|
// The feed's own clock bounds the window, so a wrong local clock cannot
|
||||||
|
// empty the layer; a feed without one is taken as published.
|
||||||
|
const generated = geojson.metadata?.generated;
|
||||||
|
const oldest = Number.isFinite(generated)
|
||||||
|
? generated - FEED_WINDOW_MS
|
||||||
|
: -Infinity;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
for (const [index, feature] of geojson.features.entries()) {
|
for (const [index, feature] of geojson.features.entries()) {
|
||||||
@@ -28,6 +37,7 @@ export function normalizeEarthquakeSnapshot(geojson) {
|
|||||||
return null;
|
return null;
|
||||||
// A missing magnitude cannot establish that this event meets M2.5+.
|
// A missing magnitude cannot establish that this event meets M2.5+.
|
||||||
if (mag == null || mag < 2.5) continue;
|
if (mag == null || mag < 2.5) continue;
|
||||||
|
if (Number.isFinite(properties.time) && properties.time < oldest) continue;
|
||||||
const stableId =
|
const stableId =
|
||||||
feature.id == null || feature.id === ''
|
feature.id == null || feature.id === ''
|
||||||
? `event-${index + 1}`
|
? `event-${index + 1}`
|
||||||
|
|||||||
Reference in new issue
Block a user