mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
The timeline marks launches and strong earthquakes, not every weather frame
The timeline drew a tick for every registered mark, and the weather clock registered every radar, satellite and lightning frame, which come every few minutes, so the track was a wall of dashes and the arrow keys crept from frame to frame. Weather products now register only the span of their frames (start and end, as before) with no marks. Holds, the gate and the next-frame wait while the world plays are unchanged. A weather row's step param goes to the previous or next weather frame (every shown product's frames, and the row's own while it is suspended) rather than to the timeline's marks. The Earthquakes layer registers with the world clock once, at init, and unregisters on destroy. While on, its quakes of magnitude 6 and up (EARTHQUAKE_MARK_MAGNITUDE) are marks, and its coverage is their own span, or none without any, so it never stretches the timeline beyond the feed. Off, it has none. It tells the clock when the set of marks changes. The map still draws every M2.5+ quake. Launches stay marks. With no mark ahead or behind, the arrow keys step along the track as shown.
This commit is contained in:
1 parent
326cab4cf2
commit
7cb854c923
11 files changed
+181
-41
No files matched your search
@@ -1,3 +1,13 @@
|
|||||||
|
- The timeline marks only events: launches, and earthquakes of magnitude 6
|
||||||
|
and up while the Earthquakes layer is on (about one every two days
|
||||||
|
worldwide). It no longer draws a tick for every weather frame, which came
|
||||||
|
every few minutes and made the track a wall of dashes; the weather frames
|
||||||
|
still set the time the timeline covers, and Play still waits at each next
|
||||||
|
frame until it loads. With no mark ahead, the arrow keys step along the
|
||||||
|
track. The map still draws every earthquake, and an earthquake layer never
|
||||||
|
widens the timeline beyond its strong quakes' own times. A weather row's
|
||||||
|
`step` param goes to the previous or next weather frame (samehkhamis,
|
||||||
|
#0000).
|
||||||
- The main timeline is the only time control. The Weather panel no longer
|
- The main timeline is the only time control. The Weather panel no longer
|
||||||
has a slider, step buttons, Latest or Play of its own; it shows the frame
|
has a slider, step buttons, Latest or Play of its own; it shows the frame
|
||||||
time instead: the newest frame and its age while live, the timeline time
|
time instead: the newest frame and its age while live, the timeline time
|
||||||
|
|||||||
+12
-4
@@ -1781,7 +1781,11 @@ Earthquake rendering is exposed through `./layers/earthquakes`. The layer owns
|
|||||||
its entities and request lifecycle; the application supplies the overlay host
|
its entities and request lifecycle; the application supplies the overlay host
|
||||||
and snapshot source. The standalone adapter keeps the existing USGS daily feed,
|
and snapshot source. The standalone adapter keeps the existing USGS daily feed,
|
||||||
M2.5+ filtering, static discs, magnitude labels and analyst records. Disabling or
|
M2.5+ filtering, static discs, magnitude labels and analyst records. Disabling or
|
||||||
destroying the layer cancels pending work and ignores late results.
|
destroying the layer cancels pending work and ignores late results. While on,
|
||||||
|
the layer registers its quakes of magnitude 6 and up
|
||||||
|
(`EARTHQUAKE_MARK_MAGNITUDE`) with the world clock as timeline marks, spanning
|
||||||
|
only their own times, and none while off; the map still draws every M2.5+
|
||||||
|
quake.
|
||||||
|
|
||||||
## Rendered-mesh floor readings — November 9, 2026
|
## Rendered-mesh floor readings — November 9, 2026
|
||||||
|
|
||||||
@@ -8533,9 +8537,13 @@ Weather combines the GFS/ECMWF surface forecast with three observation layers:
|
|||||||
NOAA Rain radar (CONUS), Satellite clouds (regional GOES or slower global infrared),
|
NOAA Rain radar (CONUS), Satellite clouds (regional GOES or slower global infrared),
|
||||||
and 15-minute lightning density (Americas and Pacific). One transient observed
|
and 15-minute lightning density (Americas and Pacific). One transient observed
|
||||||
clock moves all enabled observation rows with the world clock; the world
|
clock moves all enabled observation rows with the world clock; the world
|
||||||
timeline is the only time control. Each product's advertised times are marks on
|
timeline is the only time control. Each product's advertised times span time on
|
||||||
that timeline, which its arrow keys step to; each product selects its nearest
|
that timeline but are not marks on it: the timeline marks only events (launches,
|
||||||
frame at or before the world time, never a future frame or an interpolation.
|
and earthquakes of magnitude 6 and up while that layer is on), so its arrow keys
|
||||||
|
step to those, or along the track where there are none. A weather row's `step`
|
||||||
|
param goes to the previous or next weather frame. Each product selects its
|
||||||
|
nearest frame at or before the world time, never a future frame or an
|
||||||
|
interpolation.
|
||||||
The maximum gap is 30 minutes for radar, regional infrared and lightning, and
|
The maximum gap is 30 minutes for radar, regional infrared and lightning, and
|
||||||
3 hours for hourly global infrared, but never less than one and a half of the
|
3 hours for hourly global infrared, but never less than one and a half of the
|
||||||
feed's median frame spacing over its latest frames, so a regional feed whose
|
feed's median frame spacing over its latest frames, so a regional feed whose
|
||||||
|
|||||||
@@ -1762,7 +1762,8 @@
|
|||||||
"src/layers/earthquakes/index.js",
|
"src/layers/earthquakes/index.js",
|
||||||
"src/layers/earthquakes/model.js",
|
"src/layers/earthquakes/model.js",
|
||||||
"src/layers/earthquakes/source.js",
|
"src/layers/earthquakes/source.js",
|
||||||
"src/layers/earthquakes/records.js"
|
"src/layers/earthquakes/records.js",
|
||||||
|
"src/time/worldClock.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { worldClock } from '../../time/worldClock.js';
|
||||||
import {
|
import {
|
||||||
EARTHQUAKE_OVERLAY_SOURCE_ID,
|
EARTHQUAKE_OVERLAY_SOURCE_ID,
|
||||||
EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
createEarthquakeOverlayEntry,
|
createEarthquakeOverlayEntry,
|
||||||
selectEarthquakeOverlayCohort,
|
selectEarthquakeOverlayCohort,
|
||||||
mapAnalystRecord,
|
mapAnalystRecord,
|
||||||
|
earthquakeCoverage,
|
||||||
} from './model.js';
|
} from './model.js';
|
||||||
export * from './model.js';
|
export * from './model.js';
|
||||||
export { createUsgsEarthquakeSource } from './source.js';
|
export { createUsgsEarthquakeSource } from './source.js';
|
||||||
@@ -27,6 +29,19 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
let _lastUpdate = null;
|
let _lastUpdate = null;
|
||||||
let _lastError = null;
|
let _lastError = null;
|
||||||
let _enabled = false;
|
let _enabled = false;
|
||||||
|
// The quakes last drawn, as the timeline reads them ({ mag, time }).
|
||||||
|
let _events = [];
|
||||||
|
let _uncover = null;
|
||||||
|
// The marks last told to the clock, so a refresh with the same strong
|
||||||
|
// quakes does not tell it again.
|
||||||
|
let _marksKey = null;
|
||||||
|
const coverage = () => (_enabled ? earthquakeCoverage(_events) : null);
|
||||||
|
const coverageChanged = () => {
|
||||||
|
const key = coverage()?.marks.join() ?? null;
|
||||||
|
if (key === _marksKey) return;
|
||||||
|
_marksKey = key;
|
||||||
|
worldClock.coverageChanged();
|
||||||
|
};
|
||||||
|
|
||||||
const layer = {
|
const layer = {
|
||||||
id: 'earthquakes',
|
id: 'earthquakes',
|
||||||
@@ -41,6 +56,10 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
_dataSource = new Cesium.CustomDataSource('earthquakes');
|
_dataSource = new Cesium.CustomDataSource('earthquakes');
|
||||||
_dataSource.show = false;
|
_dataSource.show = false;
|
||||||
viewer.dataSources.add(_dataSource);
|
viewer.dataSources.add(_dataSource);
|
||||||
|
// While on, the strong quakes are marks on the world clock's timeline.
|
||||||
|
// Registered once; off, the layer has no time to show.
|
||||||
|
_marksKey = null;
|
||||||
|
_uncover = worldClock.register({ id: 'earthquakes', coverage });
|
||||||
_count = 0;
|
_count = 0;
|
||||||
_lastUpdate = null;
|
_lastUpdate = null;
|
||||||
_lastError = null;
|
_lastError = null;
|
||||||
@@ -55,6 +74,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
// layer has no per-frame animator to keep the render loop alive for.
|
// layer has no per-frame animator to keep the render loop alive for.
|
||||||
if (_dataSource) _dataSource.show = true;
|
if (_dataSource) _dataSource.show = true;
|
||||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, true);
|
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, true);
|
||||||
|
coverageChanged();
|
||||||
},
|
},
|
||||||
|
|
||||||
disable(viewer) {
|
disable(viewer) {
|
||||||
@@ -64,6 +84,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
if (_dataSource) _dataSource.show = false;
|
if (_dataSource) _dataSource.show = false;
|
||||||
overlayHost.clearSource(EARTHQUAKE_OVERLAY_SOURCE_ID);
|
overlayHost.clearSource(EARTHQUAKE_OVERLAY_SOURCE_ID);
|
||||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
||||||
|
coverageChanged();
|
||||||
},
|
},
|
||||||
|
|
||||||
async update(viewer) {
|
async update(viewer) {
|
||||||
@@ -77,6 +98,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
const nextEntities = [];
|
const nextEntities = [];
|
||||||
|
const nextEvents = [];
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const overlayEntries = [];
|
const overlayEntries = [];
|
||||||
|
|
||||||
@@ -91,6 +113,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
time,
|
time,
|
||||||
} of rows) {
|
} of rows) {
|
||||||
count++;
|
count++;
|
||||||
|
nextEvents.push({ mag, time });
|
||||||
const baseRadius = Math.pow(2, mag) * 1000;
|
const baseRadius = Math.pow(2, mag) * 1000;
|
||||||
const color = depthColor(depthKm || 0);
|
const color = depthColor(depthKm || 0);
|
||||||
const isSignificant = mag >= 5.0;
|
const isSignificant = mag >= 5.0;
|
||||||
@@ -137,6 +160,8 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
|
|
||||||
_dataSource.entities.removeAll();
|
_dataSource.entities.removeAll();
|
||||||
for (const entity of nextEntities) _dataSource.entities.add(entity);
|
for (const entity of nextEntities) _dataSource.entities.add(entity);
|
||||||
|
_events = nextEvents;
|
||||||
|
coverageChanged();
|
||||||
if (_enabled) {
|
if (_enabled) {
|
||||||
overlayHost.setEntries(
|
overlayHost.setEntries(
|
||||||
EARTHQUAKE_OVERLAY_SOURCE_ID,
|
EARTHQUAKE_OVERLAY_SOURCE_ID,
|
||||||
@@ -176,6 +201,10 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
|
|||||||
viewer.dataSources.remove(_dataSource, true);
|
viewer.dataSources.remove(_dataSource, true);
|
||||||
_dataSource = null;
|
_dataSource = null;
|
||||||
}
|
}
|
||||||
|
_events = [];
|
||||||
|
_uncover?.();
|
||||||
|
_uncover = null;
|
||||||
|
_marksKey = null;
|
||||||
_count = 0;
|
_count = 0;
|
||||||
_lastUpdate = null;
|
_lastUpdate = null;
|
||||||
_lastError = null;
|
_lastError = null;
|
||||||
|
|||||||
@@ -3,6 +3,34 @@ export const EARTHQUAKE_OVERLAY_SOURCE_ID = 'earthquakes';
|
|||||||
export const EARTHQUAKE_OVERLAY_COHORT_LIMIT = 96;
|
export const EARTHQUAKE_OVERLAY_COHORT_LIMIT = 96;
|
||||||
export const EARTHQUAKE_OVERLAY_COLLISION_CAPACITY = 48;
|
export const EARTHQUAKE_OVERLAY_COLLISION_CAPACITY = 48;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The smallest magnitude the timeline marks. Most quakes are small shakes;
|
||||||
|
* M6 and up come about once every two days worldwide, so each is an event
|
||||||
|
* worth stepping to. The map still draws every quake.
|
||||||
|
*/
|
||||||
|
export const EARTHQUAKE_MARK_MAGNITUDE = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time a world clock's timeline can show for `events` (`{mag, time}`,
|
||||||
|
* time in ms): the span of the quakes of EARTHQUAKE_MARK_MAGNITUDE and up,
|
||||||
|
* each one a mark, or null with none, so the layer never stretches the
|
||||||
|
* timeline past what its feed covers.
|
||||||
|
* @returns {{start: number, end: number, marks: number[]} | null}
|
||||||
|
*/
|
||||||
|
export function earthquakeCoverage(events) {
|
||||||
|
const marks = [
|
||||||
|
...new Set(
|
||||||
|
events
|
||||||
|
.filter(
|
||||||
|
({ mag, time }) =>
|
||||||
|
mag >= EARTHQUAKE_MARK_MAGNITUDE && Number.isFinite(time),
|
||||||
|
)
|
||||||
|
.map(({ time }) => time),
|
||||||
|
),
|
||||||
|
].sort((a, b) => a - b);
|
||||||
|
return marks.length ? { start: marks[0], end: marks.at(-1), marks } : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color by depth:
|
* Color by depth:
|
||||||
* - Shallow (<70km): Red
|
* - Shallow (<70km): Red
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { createEarthquakesLayer } from './index.js';
|
import { createEarthquakesLayer } from './index.js';
|
||||||
import { createUsgsEarthquakeSource } from './source.js';
|
import { createUsgsEarthquakeSource } from './source.js';
|
||||||
|
import { EARTHQUAKE_MARK_MAGNITUDE } from './model.js';
|
||||||
|
import { worldClock } from '../../time/worldClock.js';
|
||||||
function harness(source) {
|
function harness(source) {
|
||||||
const sources = [];
|
const sources = [];
|
||||||
const events = [];
|
const events = [];
|
||||||
@@ -88,3 +90,40 @@ test('USGS body completion honors cancellation even with an uncooperative transp
|
|||||||
name: 'AbortError',
|
name: 'AbortError',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
test('the timeline marks only strong quakes, while the layer is on', async () => {
|
||||||
|
assert.equal(EARTHQUAKE_MARK_MAGNITUDE, 6);
|
||||||
|
const rows = [
|
||||||
|
{ ...row, stableId: 'small', mag: 4.5, time: 1_000 },
|
||||||
|
{ ...row, stableId: 'strong', mag: 6, time: 5_000 },
|
||||||
|
{ ...row, stableId: 'great', mag: 7.2, time: 3_000 },
|
||||||
|
{ ...row, stableId: 'untimed', mag: 6.5, time: null },
|
||||||
|
];
|
||||||
|
let snapshot = rows;
|
||||||
|
const h = harness({ getSnapshot: async () => snapshot });
|
||||||
|
assert.equal(worldClock.coverage(), null, 'nothing loaded, no time to show');
|
||||||
|
let changes = 0;
|
||||||
|
const unsubscribe = worldClock.subscribe(() => changes++);
|
||||||
|
await h.layer.update(h.viewer);
|
||||||
|
// Every quake is drawn; only M6 and up are marks, spanning their own time.
|
||||||
|
assert.equal(h.sources[0].entities.values.length, 4);
|
||||||
|
assert.deepEqual(worldClock.coverage(), {
|
||||||
|
start: 3_000,
|
||||||
|
end: 5_000,
|
||||||
|
marks: [3_000, 5_000],
|
||||||
|
});
|
||||||
|
const told = changes;
|
||||||
|
await h.layer.update(h.viewer);
|
||||||
|
assert.equal(changes, told, 'the same strong quakes are not told again');
|
||||||
|
snapshot = rows.slice(0, 1);
|
||||||
|
await h.layer.update(h.viewer);
|
||||||
|
assert.equal(worldClock.coverage(), null, 'small shakes alone cover no time');
|
||||||
|
snapshot = rows;
|
||||||
|
await h.layer.update(h.viewer);
|
||||||
|
h.layer.disable(h.viewer);
|
||||||
|
assert.equal(worldClock.coverage(), null, 'off, no time to show');
|
||||||
|
h.layer.enable(h.viewer);
|
||||||
|
assert.deepEqual(worldClock.coverage().marks, [3_000, 5_000]);
|
||||||
|
h.layer.destroy();
|
||||||
|
assert.equal(worldClock.coverage(), null);
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
@@ -198,11 +198,13 @@ export function createWeatherClock({
|
|||||||
throw new Error(`Duplicate weather product: ${product.id}`);
|
throw new Error(`Duplicate weather product: ${product.id}`);
|
||||||
const entry = { product, controller: null };
|
const entry = { product, controller: null };
|
||||||
registry.set(product.id, entry);
|
registry.set(product.id, entry);
|
||||||
// The product moves with the world clock, and its frames are time the
|
// The product moves with the world clock, and the span of its frames is
|
||||||
// world clock's timeline can show while it is on, suspended or not, so
|
// time the world clock's timeline can show while it is on, suspended or
|
||||||
// the way back to live stays on screen. Both calls notify the world's
|
// not, so the way back to live stays on screen. The frames are not
|
||||||
// listeners, which may destroy this clock: the release is in place
|
// marks: they come every few minutes, and the timeline marks only
|
||||||
// first, and anything acquired after a destroy is let go at once.
|
// events. Both calls notify the world's listeners, which may destroy
|
||||||
|
// this clock: the release is in place first, and anything acquired
|
||||||
|
// after a destroy is let go at once.
|
||||||
let unhold = null;
|
let unhold = null;
|
||||||
let uncover = null;
|
let uncover = null;
|
||||||
const release = () => {
|
const release = () => {
|
||||||
@@ -216,9 +218,12 @@ export function createWeatherClock({
|
|||||||
uncover = world.register({
|
uncover = world.register({
|
||||||
id: product.id,
|
id: product.id,
|
||||||
coverage: () => {
|
coverage: () => {
|
||||||
const marks = timesFor(product).map((time) => Date.parse(time));
|
const times = timesFor(product);
|
||||||
return marks.length
|
return times.length
|
||||||
? { start: marks[0], end: marks.at(-1), marks }
|
? {
|
||||||
|
start: Date.parse(times[0]),
|
||||||
|
end: Date.parse(times.at(-1)),
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ test('weather clocks sharing a world: the last one destroyed leaves it live', ()
|
|||||||
assert.equal(world.isPlaying(), false);
|
assert.equal(world.isPlaying(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('each product’s frames are world clock coverage, and support the timeline while shown', (t) => {
|
test('each product’s frames span world clock coverage, with no marks, and support the timeline while shown', (t) => {
|
||||||
const { clock, world } = harness(t);
|
const { clock, world } = harness(t);
|
||||||
assert.equal(world.coverage(), null);
|
assert.equal(world.coverage(), null);
|
||||||
assert.equal(world.getState().supported, false);
|
assert.equal(world.getState().supported, false);
|
||||||
@@ -377,11 +377,15 @@ test('each product’s frames are world clock coverage, and support the timeline
|
|||||||
assert.deepEqual(world.coverage(), {
|
assert.deepEqual(world.coverage(), {
|
||||||
start: Date.parse(time(0)),
|
start: Date.parse(time(0)),
|
||||||
end: Date.parse(time(10)),
|
end: Date.parse(time(10)),
|
||||||
marks: [0, 5, 10].map((m) => Date.parse(time(m))),
|
marks: [],
|
||||||
});
|
});
|
||||||
assert.equal(world.getState().supported, true);
|
assert.equal(world.getState().supported, true);
|
||||||
unregister();
|
unregister();
|
||||||
assert.deepEqual(world.coverage().marks, [Date.parse(time(5))]);
|
assert.deepEqual(world.coverage(), {
|
||||||
|
start: Date.parse(time(5)),
|
||||||
|
end: Date.parse(time(5)),
|
||||||
|
marks: [],
|
||||||
|
});
|
||||||
clock.destroy();
|
clock.destroy();
|
||||||
assert.equal(world.coverage(), null);
|
assert.equal(world.coverage(), null);
|
||||||
assert.equal(world.getState().supported, false);
|
assert.equal(world.getState().supported, false);
|
||||||
@@ -394,10 +398,11 @@ test('a suspended product keeps its coverage, so the way back to live stays', (t
|
|||||||
radar.suspended = true;
|
radar.suspended = true;
|
||||||
void clock.refresh();
|
void clock.refresh();
|
||||||
assert.equal(world.getState().supported, true);
|
assert.equal(world.getState().supported, true);
|
||||||
assert.deepEqual(
|
assert.deepEqual(world.coverage(), {
|
||||||
world.coverage().marks,
|
start: Date.parse(time(0)),
|
||||||
[0, 10].map((m) => Date.parse(time(m))),
|
end: Date.parse(time(10)),
|
||||||
);
|
marks: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('new frames reach the world clock through refresh', (t) => {
|
test('new frames reach the world clock through refresh', (t) => {
|
||||||
|
|||||||
@@ -57,11 +57,12 @@ const utc = (value) =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Move `world`, the world clock, as its timeline does: `play` toggles play
|
* Move `world`, the world clock, as its timeline does: `play` toggles play
|
||||||
* (from the start of the covered time when live), `latest` goes live, and
|
* (from the start of the covered time when live) and `latest` goes live.
|
||||||
* `step` (-1 or 1) goes to the previous or next mark, a weather frame among
|
* `step` (-1 or 1) goes to the previous or next of `frames`, the weather
|
||||||
* them, as the timeline's arrow keys do.
|
* frames; the timeline does not mark them, so this is the row's own way to
|
||||||
|
* step through them.
|
||||||
*/
|
*/
|
||||||
function moveWorldTime(world, { play, latest, step }) {
|
function moveWorldTime(world, frames, { play, latest, step }) {
|
||||||
if (!world) return;
|
if (!world) return;
|
||||||
if (play === true) {
|
if (play === true) {
|
||||||
const state = world.getState();
|
const state = world.getState();
|
||||||
@@ -75,11 +76,14 @@ function moveWorldTime(world, { play, latest, step }) {
|
|||||||
if (latest === true) world.goLive();
|
if (latest === true) world.goLive();
|
||||||
if (step === -1 || step === 1) {
|
if (step === -1 || step === 1) {
|
||||||
const at = world.getState().timeMs;
|
const at = world.getState().timeMs;
|
||||||
const marks = world.coverage()?.marks ?? [];
|
const times = frames
|
||||||
|
.map((time) => Date.parse(time))
|
||||||
|
.filter(Number.isFinite)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
const to =
|
const to =
|
||||||
step < 0
|
step < 0
|
||||||
? marks.findLast((mark) => mark < at)
|
? times.findLast((frame) => frame < at)
|
||||||
: marks.find((mark) => mark > at);
|
: times.find((frame) => frame > at);
|
||||||
if (to === undefined) return;
|
if (to === undefined) return;
|
||||||
world.pause();
|
world.pause();
|
||||||
world.seek(to);
|
world.seek(to);
|
||||||
@@ -485,8 +489,15 @@ export function createWeatherLayer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Time is the world's: these move the world clock as its timeline
|
// Time is the world's: these move the world clock as its timeline
|
||||||
// does, and every layer that follows it moves along.
|
// does, and every layer that follows it moves along. A step goes
|
||||||
if (enabled) moveWorldTime(clock.world, params);
|
// through every shown observation's frames, and this row's own even
|
||||||
|
// while it is suspended.
|
||||||
|
if (enabled)
|
||||||
|
moveWorldTime(
|
||||||
|
clock.world,
|
||||||
|
[...clock.getTimeline(), ...(manifest?.times ?? [])],
|
||||||
|
params,
|
||||||
|
);
|
||||||
notify();
|
notify();
|
||||||
},
|
},
|
||||||
getParams() {
|
getParams() {
|
||||||
|
|||||||
@@ -592,8 +592,12 @@ test('without a clock the layer keeps one of its own on the world clock and lets
|
|||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await update;
|
await update;
|
||||||
assert.equal(worldClock.follows('weather-radar'), true);
|
assert.equal(worldClock.follows('weather-radar'), true);
|
||||||
for (const time of times)
|
// The frames span the timeline's time; they are not marks on it.
|
||||||
assert.ok(worldClock.coverage().marks.includes(Date.parse(time)));
|
assert.deepEqual(worldClock.coverage(), {
|
||||||
|
start: Date.parse(times[0]),
|
||||||
|
end: Date.parse(times.at(-1)),
|
||||||
|
marks: [],
|
||||||
|
});
|
||||||
h.layer.setParams({ play: true });
|
h.layer.setParams({ play: true });
|
||||||
assert.equal(worldClock.isPlaying(), true);
|
assert.equal(worldClock.isPlaying(), true);
|
||||||
assert.equal(worldClock.getState().stepping, false);
|
assert.equal(worldClock.getState().stepping, false);
|
||||||
@@ -616,7 +620,7 @@ test('without a clock the layer keeps one of its own on the world clock and lets
|
|||||||
assert.ok(h.clears() >= 1);
|
assert.ok(h.clears() >= 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reduced motion offers no play; steps still move the world clock from mark to mark', async () => {
|
test('reduced motion offers no play; steps still move the world clock from frame to frame', async () => {
|
||||||
const h = layerHarness({ reducedMotion: true });
|
const h = layerHarness({ reducedMotion: true });
|
||||||
const update = h.layer.update();
|
const update = h.layer.update();
|
||||||
await flush();
|
await flush();
|
||||||
@@ -626,8 +630,8 @@ test('reduced motion offers no play; steps still move the world clock from mark
|
|||||||
h.layer.getRowControls().chips.some((chip) => chip.id === 'play'),
|
h.layer.getRowControls().chips.some((chip) => chip.id === 'play'),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
// As the timeline's arrow keys do, a step back from live goes to the
|
// A step back from live goes to the newest frame, and the next one back
|
||||||
// newest frame, and the next one back to the frame before it.
|
// to the frame before it.
|
||||||
h.layer.setParams({ step: -1 });
|
h.layer.setParams({ step: -1 });
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(worldClock.now(), Date.parse(times[2]));
|
assert.equal(worldClock.now(), Date.parse(times[2]));
|
||||||
@@ -656,7 +660,7 @@ test('a row steps forward and plays from a past time through the world clock', a
|
|||||||
assert.equal(
|
assert.equal(
|
||||||
worldClock.now(),
|
worldClock.now(),
|
||||||
Date.parse(times[2]),
|
Date.parse(times[2]),
|
||||||
'no mark after the newest frame',
|
'no frame after the newest',
|
||||||
);
|
);
|
||||||
worldClock.seek(Date.parse(times[1]));
|
worldClock.seek(Date.parse(times[1]));
|
||||||
await flush();
|
await flush();
|
||||||
@@ -1829,8 +1833,8 @@ test('a row steps the world clock through every registered observation; missing
|
|||||||
.chips.filter(({ id }) =>
|
.chips.filter(({ id }) =>
|
||||||
['previous', 'play', 'next', 'latest'].includes(id),
|
['previous', 'play', 'next', 'latest'].includes(id),
|
||||||
);
|
);
|
||||||
// As the timeline's arrow keys do, a step back from live goes to the
|
// A step back from live goes to the newest frame, and the next one back
|
||||||
// newest frame, and the next one back to the frame before it.
|
// to the frame before it.
|
||||||
radar.layer.setParams({ step: -1 });
|
radar.layer.setParams({ step: -1 });
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(clock.getState().target, times[2]);
|
assert.equal(clock.getState().target, times[2]);
|
||||||
|
|||||||
@@ -294,10 +294,10 @@ const scrub = (event) => {
|
|||||||
}
|
}
|
||||||
store.scrub(Number(event.target.value), viewLength());
|
store.scrub(Number(event.target.value), viewLength());
|
||||||
};
|
};
|
||||||
// Arrows go to the previous or next mark (a launch, a frame), or a step along
|
// Arrows go to the previous or next mark (a launch, a strong earthquake), or
|
||||||
// the track as shown; Page keys take a tenth of it; Home and End go to the
|
// a step along the track as shown where there is none; Page keys take a tenth
|
||||||
// ends of all of it. Plus and minus zoom, and zero shows it all; with a
|
// of it; Home and End go to the ends of all of it. Plus and minus zoom, and
|
||||||
// modifier they are the browser's own zoom.
|
// zero shows it all; with a modifier they are the browser's own zoom.
|
||||||
const ZOOM_KEYS = ['+', '=', '-', '0'];
|
const ZOOM_KEYS = ['+', '=', '-', '0'];
|
||||||
const onKey = (event) => {
|
const onKey = (event) => {
|
||||||
if (ZOOM_KEYS.includes(event.key)) {
|
if (ZOOM_KEYS.includes(event.key)) {
|
||||||
|
|||||||
Reference in new issue
Block a user