mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
A weather layer without a shared clock keeps its own on the world clock
Built without a weather clock, the weather layer still ran a frame replay of its own: params.play stepped its frames every two seconds and went round from the last to the first, params.step and params.latest picked frames locally, and the layer tracked whether it followed the newest frame. That was a second way to move time. The layer now builds a weather clock on the page's world clock when none is passed, and destroys it with the layer, so every layer follows the world clock and its params move the world clock as the timeline does. The local replay, its timer, the follow-latest flag, the wrap to the first frame, the timerActive diagnostic and the reduced-motion "manual history available" message are removed.
This commit is contained in:
1 parent
3cfd414e70
commit
326cab4cf2
16 files changed
+141
-242
No files matched your search
@@ -8557,7 +8557,9 @@ and the timeline time alone at a past time.
|
|||||||
The catalog exposes `weatherClock` and destroys it with its lifetime signal. It
|
The catalog exposes `weatherClock` and destroys it with its lifetime signal. It
|
||||||
has no time controls of its own: `weatherClock.world` is the world clock it
|
has no time controls of its own: `weatherClock.world` is the world clock it
|
||||||
follows, where time moves. A weather row's `play`, `latest` and `step` params
|
follows, where time moves. A weather row's `play`, `latest` and `step` params
|
||||||
move that world clock as the timeline does.
|
move that world clock as the timeline does. A weather layer built without a shared
|
||||||
|
clock keeps one of its own on the page's world clock for its lifetime; it has
|
||||||
|
no frame replay of its own either way.
|
||||||
|
|
||||||
Source, opacity and coverage navigation live in the WEATHER panel's cards, not
|
Source, opacity and coverage navigation live in the WEATHER panel's cards, not
|
||||||
in the rows; the panel shows the frame time and has no time controls. The
|
in the rows; the panel shows the frame time and has no time controls. The
|
||||||
|
|||||||
@@ -2032,7 +2032,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60° ج–60° ش · بتأخير 2–3 س عادةً",
|
"60°S–60°N · typically 2–3 h delayed": "60° ج–60° ش · بتأخير 2–3 س عادةً",
|
||||||
"North America · infrared imagery": "أمريكا الشمالية · صور بالأشعة تحت الحمراء",
|
"North America · infrared imagery": "أمريكا الشمالية · صور بالأشعة تحت الحمراء",
|
||||||
"Reduced motion · history playback unavailable": "تقليل الحركة · تشغيل السجل غير متاح",
|
"Reduced motion · history playback unavailable": "تقليل الحركة · تشغيل السجل غير متاح",
|
||||||
"Reduced motion · manual history available": "تقليل الحركة · التصفح اليدوي للسجل متاح",
|
|
||||||
"Rain radar · US": "رادار الأمطار · الولايات المتحدة",
|
"Rain radar · US": "رادار الأمطار · الولايات المتحدة",
|
||||||
"Lightning density · 15 min": "كثافة البرق · 15 د",
|
"Lightning density · 15 min": "كثافة البرق · 15 د",
|
||||||
"Satellite clouds": "الغيوم من الأقمار الصناعية",
|
"Satellite clouds": "الغيوم من الأقمار الصناعية",
|
||||||
|
|||||||
@@ -1936,7 +1936,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · meist 2–3 h verzögert",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · meist 2–3 h verzögert",
|
||||||
"North America · infrared imagery": "Nordamerika · Infrarotbilder",
|
"North America · infrared imagery": "Nordamerika · Infrarotbilder",
|
||||||
"Reduced motion · history playback unavailable": "Reduzierte Bewegung · Verlaufswiedergabe nicht verfügbar",
|
"Reduced motion · history playback unavailable": "Reduzierte Bewegung · Verlaufswiedergabe nicht verfügbar",
|
||||||
"Reduced motion · manual history available": "Reduzierte Bewegung · manueller Verlauf verfügbar",
|
|
||||||
"Rain radar · US": "Regenradar · USA",
|
"Rain radar · US": "Regenradar · USA",
|
||||||
"Lightning density · 15 min": "Blitzdichte · 15 min",
|
"Lightning density · 15 min": "Blitzdichte · 15 min",
|
||||||
"Satellite clouds": "Wolken (Satellit)",
|
"Satellite clouds": "Wolken (Satellit)",
|
||||||
|
|||||||
@@ -1436,7 +1436,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · normalmente con 2–3 h de retraso",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · normalmente con 2–3 h de retraso",
|
||||||
"North America · infrared imagery": "Norteamérica · imágenes infrarrojas",
|
"North America · infrared imagery": "Norteamérica · imágenes infrarrojas",
|
||||||
"Reduced motion · history playback unavailable": "Movimiento reducido · reproducción del historial no disponible",
|
"Reduced motion · history playback unavailable": "Movimiento reducido · reproducción del historial no disponible",
|
||||||
"Reduced motion · manual history available": "Movimiento reducido · puedes recorrer el historial a mano",
|
|
||||||
"Rain radar · US": "Radar de lluvia · EE. UU.",
|
"Rain radar · US": "Radar de lluvia · EE. UU.",
|
||||||
"Lightning density · 15 min": "Densidad de rayos · 15 min",
|
"Lightning density · 15 min": "Densidad de rayos · 15 min",
|
||||||
"Satellite clouds": "Nubes por satélite",
|
"Satellite clouds": "Nubes por satélite",
|
||||||
|
|||||||
@@ -1350,7 +1350,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · retard habituel de 2 à 3 h",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · retard habituel de 2 à 3 h",
|
||||||
"North America · infrared imagery": "Amérique du Nord · imagerie infrarouge",
|
"North America · infrared imagery": "Amérique du Nord · imagerie infrarouge",
|
||||||
"Reduced motion · history playback unavailable": "Animations réduites · lecture de l'historique indisponible",
|
"Reduced motion · history playback unavailable": "Animations réduites · lecture de l'historique indisponible",
|
||||||
"Reduced motion · manual history available": "Animations réduites · historique parcourable manuellement",
|
|
||||||
"Rain radar · US": "Radar de pluie · États-Unis",
|
"Rain radar · US": "Radar de pluie · États-Unis",
|
||||||
"Lightning density · 15 min": "Densité de foudre · 15 min",
|
"Lightning density · 15 min": "Densité de foudre · 15 min",
|
||||||
"Satellite clouds": "Nuages par satellite",
|
"Satellite clouds": "Nuages par satellite",
|
||||||
|
|||||||
@@ -1208,7 +1208,6 @@
|
|||||||
"Color: strikes/km²/min ×10³": "צבע: פגיעות ברק/קמ\"ר/דק׳ ×10³",
|
"Color: strikes/km²/min ×10³": "צבע: פגיעות ברק/קמ\"ר/דק׳ ×10³",
|
||||||
"Color: {units}": "צבע: {units}",
|
"Color: {units}": "צבע: {units}",
|
||||||
"Reduced motion · history playback unavailable": "תנועה מופחתת · הצגת ההיסטוריה אינה זמינה",
|
"Reduced motion · history playback unavailable": "תנועה מופחתת · הצגת ההיסטוריה אינה זמינה",
|
||||||
"Reduced motion · manual history available": "תנועה מופחתת · היסטוריה ידנית זמינה",
|
|
||||||
"Rain radar · US": "רדאר גשם · ארה\"ב",
|
"Rain radar · US": "רדאר גשם · ארה\"ב",
|
||||||
"Lightning density · 15 min": "צפיפות ברקים · 15 דק׳",
|
"Lightning density · 15 min": "צפיפות ברקים · 15 דק׳",
|
||||||
"Satellite clouds": "עננים מלוויין",
|
"Satellite clouds": "עננים מלוויין",
|
||||||
|
|||||||
@@ -1350,7 +1350,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · di solito 2–3 h di ritardo",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · di solito 2–3 h di ritardo",
|
||||||
"North America · infrared imagery": "Nord America · immagini all'infrarosso",
|
"North America · infrared imagery": "Nord America · immagini all'infrarosso",
|
||||||
"Reduced motion · history playback unavailable": "Movimento ridotto · riproduzione dello storico non disponibile",
|
"Reduced motion · history playback unavailable": "Movimento ridotto · riproduzione dello storico non disponibile",
|
||||||
"Reduced motion · manual history available": "Movimento ridotto · storico manuale disponibile",
|
|
||||||
"Rain radar · US": "Radar pioggia · USA",
|
"Rain radar · US": "Radar pioggia · USA",
|
||||||
"Lightning density · 15 min": "Densità di fulmini · 15 min",
|
"Lightning density · 15 min": "Densità di fulmini · 15 min",
|
||||||
"Satellite clouds": "Nuvole da satellite",
|
"Satellite clouds": "Nuvole da satellite",
|
||||||
|
|||||||
@@ -1312,7 +1312,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常2–3時間遅れ",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常2–3時間遅れ",
|
||||||
"North America · infrared imagery": "北米 · 赤外画像",
|
"North America · infrared imagery": "北米 · 赤外画像",
|
||||||
"Reduced motion · history playback unavailable": "動きを減らす設定 · 履歴再生は利用不可",
|
"Reduced motion · history playback unavailable": "動きを減らす設定 · 履歴再生は利用不可",
|
||||||
"Reduced motion · manual history available": "動きを減らす設定 · 履歴は手動で表示可能",
|
|
||||||
"Rain radar · US": "降水レーダー · 米国",
|
"Rain radar · US": "降水レーダー · 米国",
|
||||||
"Lightning density · 15 min": "雷密度 · 15分",
|
"Lightning density · 15 min": "雷密度 · 15分",
|
||||||
"Satellite clouds": "衛星雲画像",
|
"Satellite clouds": "衛星雲画像",
|
||||||
|
|||||||
@@ -1348,7 +1348,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · normalmente com 2–3 h de atraso",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · normalmente com 2–3 h de atraso",
|
||||||
"North America · infrared imagery": "América do Norte · imagens infravermelhas",
|
"North America · infrared imagery": "América do Norte · imagens infravermelhas",
|
||||||
"Reduced motion · history playback unavailable": "Movimento reduzido · reprodução do histórico indisponível",
|
"Reduced motion · history playback unavailable": "Movimento reduzido · reprodução do histórico indisponível",
|
||||||
"Reduced motion · manual history available": "Movimento reduzido · é possível percorrer o histórico manualmente",
|
|
||||||
"Rain radar · US": "Radar de chuva · EUA",
|
"Rain radar · US": "Radar de chuva · EUA",
|
||||||
"Lightning density · 15 min": "Densidade de raios · 15 min",
|
"Lightning density · 15 min": "Densidade de raios · 15 min",
|
||||||
"Satellite clouds": "Nuvens por satélite",
|
"Satellite clouds": "Nuvens por satélite",
|
||||||
|
|||||||
@@ -1458,7 +1458,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60° ю. ш.–60° с. ш. · обычно задержка 2–3 ч",
|
"60°S–60°N · typically 2–3 h delayed": "60° ю. ш.–60° с. ш. · обычно задержка 2–3 ч",
|
||||||
"North America · infrared imagery": "Северная Америка · ИК-снимки",
|
"North America · infrared imagery": "Северная Америка · ИК-снимки",
|
||||||
"Reduced motion · history playback unavailable": "Уменьшение движения · воспроизведение истории недоступно",
|
"Reduced motion · history playback unavailable": "Уменьшение движения · воспроизведение истории недоступно",
|
||||||
"Reduced motion · manual history available": "Уменьшение движения · историю можно листать вручную",
|
|
||||||
"Rain radar · US": "Радар осадков · США",
|
"Rain radar · US": "Радар осадков · США",
|
||||||
"Lightning density · 15 min": "Плотность молний · 15 мин",
|
"Lightning density · 15 min": "Плотность молний · 15 мин",
|
||||||
"Satellite clouds": "Спутниковая облачность",
|
"Satellite clouds": "Спутниковая облачность",
|
||||||
|
|||||||
@@ -1858,7 +1858,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60° S–60° N · oftast 2–3 h fördröjning",
|
"60°S–60°N · typically 2–3 h delayed": "60° S–60° N · oftast 2–3 h fördröjning",
|
||||||
"North America · infrared imagery": "Nordamerika · infraröda bilder",
|
"North America · infrared imagery": "Nordamerika · infraröda bilder",
|
||||||
"Reduced motion · history playback unavailable": "Minskad rörelse · uppspelning av historik otillgänglig",
|
"Reduced motion · history playback unavailable": "Minskad rörelse · uppspelning av historik otillgänglig",
|
||||||
"Reduced motion · manual history available": "Minskad rörelse · manuell historik tillgänglig",
|
|
||||||
"Rain radar · US": "Regnradar · USA",
|
"Rain radar · US": "Regnradar · USA",
|
||||||
"Lightning density · 15 min": "Blixttäthet · 15 min",
|
"Lightning density · 15 min": "Blixttäthet · 15 min",
|
||||||
"Satellite clouds": "Satellitmoln",
|
"Satellite clouds": "Satellitmoln",
|
||||||
|
|||||||
@@ -1368,7 +1368,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60° пд. ш.–60° пн. ш. · зазвичай затримка 2–3 год",
|
"60°S–60°N · typically 2–3 h delayed": "60° пд. ш.–60° пн. ш. · зазвичай затримка 2–3 год",
|
||||||
"North America · infrared imagery": "Північна Америка · ІЧ-знімки",
|
"North America · infrared imagery": "Північна Америка · ІЧ-знімки",
|
||||||
"Reduced motion · history playback unavailable": "Зменшення руху · відтворення історії недоступне",
|
"Reduced motion · history playback unavailable": "Зменшення руху · відтворення історії недоступне",
|
||||||
"Reduced motion · manual history available": "Зменшення руху · історію можна гортати вручну",
|
|
||||||
"Rain radar · US": "Радар опадів · США",
|
"Rain radar · US": "Радар опадів · США",
|
||||||
"Lightning density · 15 min": "Щільність блискавок · 15 хв",
|
"Lightning density · 15 min": "Щільність блискавок · 15 хв",
|
||||||
"Satellite clouds": "Супутникова хмарність",
|
"Satellite clouds": "Супутникова хмарність",
|
||||||
|
|||||||
@@ -1288,7 +1288,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常延迟 2–3 小时",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常延迟 2–3 小时",
|
||||||
"North America · infrared imagery": "北美洲 · 红外影像",
|
"North America · infrared imagery": "北美洲 · 红外影像",
|
||||||
"Reduced motion · history playback unavailable": "减弱动效 · 历史回放不可用",
|
"Reduced motion · history playback unavailable": "减弱动效 · 历史回放不可用",
|
||||||
"Reduced motion · manual history available": "减弱动效 · 可手动浏览历史",
|
|
||||||
"Rain radar · US": "降雨雷达 · 美国",
|
"Rain radar · US": "降雨雷达 · 美国",
|
||||||
"Lightning density · 15 min": "闪电密度 · 15 分钟",
|
"Lightning density · 15 min": "闪电密度 · 15 分钟",
|
||||||
"Satellite clouds": "卫星云图",
|
"Satellite clouds": "卫星云图",
|
||||||
|
|||||||
@@ -1283,7 +1283,6 @@
|
|||||||
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常延遲 2–3 小時",
|
"60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常延遲 2–3 小時",
|
||||||
"North America · infrared imagery": "北美洲 · 紅外影像",
|
"North America · infrared imagery": "北美洲 · 紅外影像",
|
||||||
"Reduced motion · history playback unavailable": "減弱動效 · 歷史回放不可用",
|
"Reduced motion · history playback unavailable": "減弱動效 · 歷史回放不可用",
|
||||||
"Reduced motion · manual history available": "減弱動效 · 可手動瀏覽歷史",
|
|
||||||
"Rain radar · US": "降雨雷達 · 美國",
|
"Rain radar · US": "降雨雷達 · 美國",
|
||||||
"Lightning density · 15 min": "閃電密度 · 15 分鐘",
|
"Lightning density · 15 min": "閃電密度 · 15 分鐘",
|
||||||
"Satellite clouds": "衛星雲圖",
|
"Satellite clouds": "衛星雲圖",
|
||||||
|
|||||||
+49
-160
@@ -9,7 +9,9 @@ import {
|
|||||||
GLOBAL_INFRARED_MAX_GAP_MS,
|
GLOBAL_INFRARED_MAX_GAP_MS,
|
||||||
LIGHTNING_MAX_GAP_MS,
|
LIGHTNING_MAX_GAP_MS,
|
||||||
atLeastFrameSpacing,
|
atLeastFrameSpacing,
|
||||||
|
createWeatherClock,
|
||||||
} from './clock.js';
|
} from './clock.js';
|
||||||
|
import { worldClock } from '../../time/worldClock.js';
|
||||||
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
|
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
|
||||||
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
||||||
|
|
||||||
@@ -86,12 +88,14 @@ function moveWorldTime(world, { play, latest, step }) {
|
|||||||
|
|
||||||
/** A per-application observation layer, using the existing layer lifecycle and
|
/** A per-application observation layer, using the existing layer lifecycle and
|
||||||
* row controls. Its time is the world clock's, so a link made away from live
|
* row controls. Its time is the world clock's, so a link made away from live
|
||||||
* opens its imagery at that time; one made live opens the latest. The feed
|
* opens its imagery at that time; one made live opens the latest. Its frames
|
||||||
* may also name its source with `sourceName(kind)`, `kind` being 'radar',
|
* follow `clock`, a weather clock several layers share; without one the layer
|
||||||
* 'lightning' or 'satellite'; NOAA nowCOAST is named otherwise. */
|
* keeps its own on the page's world clock, for its lifetime. The feed may also
|
||||||
|
* name its source with `sourceName(kind)`, `kind` being 'radar', 'lightning'
|
||||||
|
* or 'satellite'; NOAA nowCOAST is named otherwise. */
|
||||||
export function createWeatherLayer({
|
export function createWeatherLayer({
|
||||||
feed,
|
feed,
|
||||||
clock,
|
clock: sharedClock,
|
||||||
id = 'weather-radar',
|
id = 'weather-radar',
|
||||||
cesium = Cesium,
|
cesium = Cesium,
|
||||||
createRendering = createWeatherRendering,
|
createRendering = createWeatherRendering,
|
||||||
@@ -101,6 +105,10 @@ export function createWeatherLayer({
|
|||||||
} = {}) {
|
} = {}) {
|
||||||
if (typeof feed?.getSnapshot !== 'function')
|
if (typeof feed?.getSnapshot !== 'function')
|
||||||
throw new TypeError('Weather requires a snapshot source');
|
throw new TypeError('Weather requires a snapshot source');
|
||||||
|
const ownClock = sharedClock
|
||||||
|
? null
|
||||||
|
: createWeatherClock({ world: worldClock });
|
||||||
|
const clock = sharedClock ?? ownClock;
|
||||||
// A feed may also supply decoded frames; see weatherFrameSource.
|
// A feed may also supply decoded frames; see weatherFrameSource.
|
||||||
const radar = id === 'weather-radar';
|
const radar = id === 'weather-radar';
|
||||||
const lightning = id === 'weather-lightning';
|
const lightning = id === 'weather-lightning';
|
||||||
@@ -128,11 +136,8 @@ export function createWeatherLayer({
|
|||||||
request = null,
|
request = null,
|
||||||
listener = null;
|
listener = null;
|
||||||
let enabled = false,
|
let enabled = false,
|
||||||
loading = false,
|
loading = false;
|
||||||
playing = false,
|
|
||||||
followLatest = true;
|
|
||||||
let generation = 0,
|
let generation = 0,
|
||||||
timer = null,
|
|
||||||
error = null,
|
error = null,
|
||||||
removeCamera = null;
|
removeCamera = null;
|
||||||
let motion = null;
|
let motion = null;
|
||||||
@@ -172,13 +177,12 @@ export function createWeatherLayer({
|
|||||||
return spaced;
|
return spaced;
|
||||||
};
|
};
|
||||||
const maxGap = () => spacedTimes().gap;
|
const maxGap = () => spacedTimes().gap;
|
||||||
const isLatest = () =>
|
const isLatest = () => clock.getState().mode === 'latest';
|
||||||
clock ? clock.getState().mode === 'latest' : followLatest;
|
|
||||||
const getHost = () =>
|
const getHost = () =>
|
||||||
imageryHost?.() ?? { collection: viewer?.imageryLayers, kind: 'globe' };
|
imageryHost?.() ?? { collection: viewer?.imageryLayers, kind: 'globe' };
|
||||||
const notify = () => listener?.();
|
const notify = () => listener?.();
|
||||||
const shownTime = () => (noFrame ? null : rendering?.getDiagnostics().time);
|
const shownTime = () => (noFrame ? null : rendering?.getDiagnostics().time);
|
||||||
const unsubscribeClock = clock?.subscribe(() => {
|
const unsubscribeClock = clock.subscribe(() => {
|
||||||
if (!clock.getState().advancing || suspended())
|
if (!clock.getState().advancing || suspended())
|
||||||
rendering?.cancelPrefetch?.();
|
rendering?.cancelPrefetch?.();
|
||||||
notify();
|
notify();
|
||||||
@@ -186,18 +190,10 @@ export function createWeatherLayer({
|
|||||||
const observationDelayed = () =>
|
const observationDelayed = () =>
|
||||||
manifest?.latest &&
|
manifest?.latest &&
|
||||||
Date.now() - Date.parse(manifest.latest) > spacedTimes().delay;
|
Date.now() - Date.parse(manifest.latest) > spacedTimes().delay;
|
||||||
const stop = () => {
|
|
||||||
if (clock) return;
|
|
||||||
playing = false;
|
|
||||||
rendering?.cancelPrefetch?.();
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
};
|
|
||||||
const suspended = () => hostHidden || documentRef?.hidden || motion?.matches;
|
const suspended = () => hostHidden || documentRef?.hidden || motion?.matches;
|
||||||
const onVisibility = () => {
|
const onVisibility = () => {
|
||||||
if (suspended()) rendering?.cancelPrefetch?.();
|
if (suspended()) rendering?.cancelPrefetch?.();
|
||||||
if (clock) void clock.refresh();
|
void clock.refresh();
|
||||||
else if (suspended()) stop();
|
|
||||||
notify();
|
notify();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,46 +208,15 @@ export function createWeatherLayer({
|
|||||||
if (!changed || !rendering) return;
|
if (!changed || !rendering) return;
|
||||||
++generation;
|
++generation;
|
||||||
rendering.rehome?.();
|
rendering.rehome?.();
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
loading = Boolean(request && !request.signal.aborted);
|
loading = Boolean(request && !request.signal.aborted);
|
||||||
if (clock && enabled) {
|
if (enabled && resume) {
|
||||||
if (resume) {
|
void clock.refresh();
|
||||||
void clock.refresh();
|
if (!hostHidden && manifest && isLatest()) void show(manifest.latest);
|
||||||
if (!hostHidden && manifest && isLatest()) void show(manifest.latest);
|
|
||||||
}
|
|
||||||
notify();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (resume && enabled && !hostHidden && manifest) {
|
|
||||||
const time =
|
|
||||||
followLatest || !manifest.times.includes(shownTime())
|
|
||||||
? manifest.latest
|
|
||||||
: shownTime();
|
|
||||||
if (
|
|
||||||
time !== shownTime() ||
|
|
||||||
(satellite && rendering.getDiagnostics().infrared !== infrared)
|
|
||||||
)
|
|
||||||
void show(time);
|
|
||||||
else schedule();
|
|
||||||
}
|
}
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
const onMapStackChanged = () => checkHost();
|
const onMapStackChanged = () => checkHost();
|
||||||
|
|
||||||
function schedule() {
|
|
||||||
if (clock) return;
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
if (!enabled || !playing || suspended()) return;
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
timer = null;
|
|
||||||
const times = manifest?.times || [];
|
|
||||||
const next = (times.indexOf(shownTime()) + 1) % times.length;
|
|
||||||
if (times[next]) void show(times[next]);
|
|
||||||
else stop();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
async function applyClockTime(time, { signal }) {
|
async function applyClockTime(time, { signal }) {
|
||||||
if (!enabled || signal.aborted) return false;
|
if (!enabled || signal.aborted) return false;
|
||||||
if (time !== null) return show(time, signal);
|
if (time !== null) return show(time, signal);
|
||||||
@@ -265,7 +230,7 @@ export function createWeatherLayer({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function registerClock() {
|
function registerClock() {
|
||||||
if (!clock || unregisterClock || !rendering || !enabled) return;
|
if (unregisterClock || !rendering || !enabled) return;
|
||||||
unregisterClock = clock.register({
|
unregisterClock = clock.register({
|
||||||
id,
|
id,
|
||||||
get maxGapMs() {
|
get maxGapMs() {
|
||||||
@@ -278,18 +243,11 @@ export function createWeatherLayer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function warmNext(time) {
|
async function warmNext(time) {
|
||||||
if (
|
if (!enabled || suspended() || !clock.getState().advancing) return;
|
||||||
!enabled ||
|
// The world plays on past the newest frame, never round to the first.
|
||||||
suspended() ||
|
|
||||||
!(clock ? clock.getState().advancing : playing)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const times = manifest?.times ?? [];
|
const times = manifest?.times ?? [];
|
||||||
// The world plays on past the newest frame; this layer's own replay
|
const next = times[times.indexOf(time) + 1];
|
||||||
// (without a clock) goes round to the first.
|
if (!next) return;
|
||||||
const after = times.indexOf(time) + 1;
|
|
||||||
const next = clock ? times[after] : times[after % times.length];
|
|
||||||
if (!next || next === time) return;
|
|
||||||
try {
|
try {
|
||||||
await rendering.prefetch?.(manifest, next, { infrared });
|
await rendering.prefetch?.(manifest, next, { infrared });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -324,13 +282,10 @@ export function createWeatherLayer({
|
|||||||
error = ok
|
error = ok
|
||||||
? null
|
? null
|
||||||
: msg('Frame unavailable; previous observation retained');
|
: msg('Frame unavailable; previous observation retained');
|
||||||
if (!ok) stop();
|
|
||||||
return ok;
|
return ok;
|
||||||
} catch {
|
} catch {
|
||||||
if (owner === generation && !controller.signal.aborted) {
|
if (owner === generation && !controller.signal.aborted)
|
||||||
error = msg('Weather imagery unavailable');
|
error = msg('Weather imagery unavailable');
|
||||||
stop();
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
signal?.removeEventListener('abort', abort);
|
signal?.removeEventListener('abort', abort);
|
||||||
@@ -338,8 +293,7 @@ export function createWeatherLayer({
|
|||||||
if (owner === generation) {
|
if (owner === generation) {
|
||||||
loading = Boolean(request && !request.signal.aborted);
|
loading = Boolean(request && !request.signal.aborted);
|
||||||
notify();
|
notify();
|
||||||
if (clock && isLatest()) void clock.refresh();
|
if (isLatest()) void clock.refresh();
|
||||||
schedule();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,26 +361,20 @@ export function createWeatherLayer({
|
|||||||
noFrame = false;
|
noFrame = false;
|
||||||
request?.abort();
|
request?.abort();
|
||||||
request = null;
|
request = null;
|
||||||
stop();
|
rendering?.cancelPrefetch?.();
|
||||||
rendering?.clear();
|
rendering?.clear();
|
||||||
manifest = null;
|
manifest = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
error = null;
|
error = null;
|
||||||
followLatest = true;
|
|
||||||
},
|
},
|
||||||
async update(_viewer, { signal } = {}) {
|
async update(_viewer, { signal } = {}) {
|
||||||
if (!enabled) return false;
|
if (!enabled) return false;
|
||||||
checkHost(false);
|
checkHost(false);
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
request?.abort();
|
request?.abort();
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
if (signal?.aborted) controller.abort(signal.reason);
|
if (signal?.aborted) controller.abort(signal.reason);
|
||||||
request = controller;
|
request = controller;
|
||||||
const abort = () => {
|
const abort = () => controller.abort(signal.reason);
|
||||||
controller.abort(signal.reason);
|
|
||||||
stop();
|
|
||||||
};
|
|
||||||
signal?.addEventListener('abort', abort, { once: true });
|
signal?.addEventListener('abort', abort, { once: true });
|
||||||
loading = true;
|
loading = true;
|
||||||
notify();
|
notify();
|
||||||
@@ -442,36 +390,15 @@ export function createWeatherLayer({
|
|||||||
error = msg(
|
error = msg(
|
||||||
'Weather source unavailable; previous observation retained',
|
'Weather source unavailable; previous observation retained',
|
||||||
);
|
);
|
||||||
stop();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
manifest = snapshot;
|
manifest = snapshot;
|
||||||
error = null;
|
error = null;
|
||||||
// The shown frame may stay; its credit follows the refreshed manifest.
|
// The shown frame may stay; its credit follows the refreshed manifest.
|
||||||
rendering?.updateSnapshot?.(snapshot);
|
rendering?.updateSnapshot?.(snapshot);
|
||||||
if (clock) {
|
await clock.refresh();
|
||||||
await clock.refresh();
|
if (isLatest() && request === controller && !controller.signal.aborted)
|
||||||
if (
|
await show(snapshot.latest, controller.signal);
|
||||||
isLatest() &&
|
|
||||||
request === controller &&
|
|
||||||
!controller.signal.aborted
|
|
||||||
)
|
|
||||||
await show(snapshot.latest, controller.signal);
|
|
||||||
return (
|
|
||||||
enabled &&
|
|
||||||
!signal?.aborted &&
|
|
||||||
(!controller.signal.aborted || request !== controller)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const time =
|
|
||||||
followLatest || !snapshot.times.includes(shownTime())
|
|
||||||
? snapshot.latest
|
|
||||||
: shownTime();
|
|
||||||
if (
|
|
||||||
shownTime() !== time ||
|
|
||||||
(satellite && rendering.getDiagnostics().infrared !== infrared)
|
|
||||||
)
|
|
||||||
await show(time, controller.signal);
|
|
||||||
return (
|
return (
|
||||||
enabled &&
|
enabled &&
|
||||||
!signal?.aborted &&
|
!signal?.aborted &&
|
||||||
@@ -481,7 +408,6 @@ export function createWeatherLayer({
|
|||||||
if (controller.signal.aborted || request !== controller)
|
if (controller.signal.aborted || request !== controller)
|
||||||
return enabled && !signal?.aborted && request !== controller;
|
return enabled && !signal?.aborted && request !== controller;
|
||||||
error = cause?.message || msg('Weather unavailable');
|
error = cause?.message || msg('Weather unavailable');
|
||||||
stop();
|
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
signal?.removeEventListener('abort', abort);
|
signal?.removeEventListener('abort', abort);
|
||||||
@@ -489,7 +415,6 @@ export function createWeatherLayer({
|
|||||||
request = null;
|
request = null;
|
||||||
loading = Boolean(rendering?.getDiagnostics().loading);
|
loading = Boolean(rendering?.getDiagnostics().loading);
|
||||||
notify();
|
notify();
|
||||||
schedule();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -524,19 +449,16 @@ export function createWeatherLayer({
|
|||||||
noFrame = false;
|
noFrame = false;
|
||||||
request?.abort();
|
request?.abort();
|
||||||
request = null;
|
request = null;
|
||||||
stop();
|
rendering?.cancelPrefetch?.();
|
||||||
manifest = null;
|
manifest = null;
|
||||||
error = null;
|
error = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
followLatest = true;
|
|
||||||
rendering?.clear();
|
rendering?.clear();
|
||||||
if (clock) void clock.refresh();
|
void clock.refresh();
|
||||||
if (enabled) void layer.update(viewer);
|
if (enabled) void layer.update(viewer);
|
||||||
}
|
}
|
||||||
if (infraredChanged && enabled && manifest) {
|
if (infraredChanged && enabled && manifest) {
|
||||||
clearTimeout(timer);
|
if (!isLatest()) void clock.refresh();
|
||||||
timer = null;
|
|
||||||
if (clock && !isLatest()) void clock.refresh();
|
|
||||||
else void show(shownTime() || manifest.latest);
|
else void show(shownTime() || manifest.latest);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -562,36 +484,9 @@ export function createWeatherLayer({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// With a clock, time is the world's: these move the world clock as its
|
// Time is the world's: these move the world clock as its timeline
|
||||||
// timeline does, and every layer that follows it moves along.
|
// does, and every layer that follows it moves along.
|
||||||
if (clock && enabled) moveWorldTime(clock.world, params);
|
if (enabled) moveWorldTime(clock.world, params);
|
||||||
if (
|
|
||||||
!clock &&
|
|
||||||
params.play === true &&
|
|
||||||
enabled &&
|
|
||||||
manifest?.times?.length > 1 &&
|
|
||||||
!suspended()
|
|
||||||
) {
|
|
||||||
playing = !playing;
|
|
||||||
followLatest = false;
|
|
||||||
if (playing) schedule();
|
|
||||||
else stop();
|
|
||||||
}
|
|
||||||
if (!clock && params.latest === true && manifest) {
|
|
||||||
stop();
|
|
||||||
followLatest = true;
|
|
||||||
void show(manifest.latest);
|
|
||||||
}
|
|
||||||
if (!clock && [-1, 1].includes(params.step) && manifest && !loading) {
|
|
||||||
stop();
|
|
||||||
followLatest = false;
|
|
||||||
const at = manifest.times.indexOf(shownTime());
|
|
||||||
const next = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(manifest.times.length - 1, at + params.step),
|
|
||||||
);
|
|
||||||
void show(manifest.times[next]);
|
|
||||||
}
|
|
||||||
notify();
|
notify();
|
||||||
},
|
},
|
||||||
getParams() {
|
getParams() {
|
||||||
@@ -602,13 +497,13 @@ export function createWeatherLayer({
|
|||||||
: { product, opacity };
|
: { product, opacity };
|
||||||
},
|
},
|
||||||
getRowControls() {
|
getRowControls() {
|
||||||
const shared = clock?.getState();
|
const shared = clock.getState();
|
||||||
const followLatest = isLatest();
|
const followLatest = isLatest();
|
||||||
// Source-described text is data: shown as is, never translated.
|
// Source-described text is data: shown as is, never translated.
|
||||||
const display = weatherDisplay(manifest);
|
const display = weatherDisplay(manifest);
|
||||||
const missing =
|
const missing =
|
||||||
noFrame &&
|
noFrame &&
|
||||||
shared?.mode === 'history' &&
|
shared.mode === 'history' &&
|
||||||
shared.products.find((entry) => entry.id === id)?.selected === null
|
shared.products.find((entry) => entry.id === id)?.selected === null
|
||||||
? maxGap() / 60_000 < 60
|
? maxGap() / 60_000 < 60
|
||||||
? t('No frame within {gap} min of {time}', {
|
? t('No frame within {gap} min of {time}', {
|
||||||
@@ -734,9 +629,7 @@ export function createWeatherLayer({
|
|||||||
const stops =
|
const stops =
|
||||||
display.legend ?? (radar ? STOPS : lightning ? LIGHTNING_STOPS : []);
|
display.legend ?? (radar ? STOPS : lightning ? LIGHTNING_STOPS : []);
|
||||||
const infoMotion = motion?.matches
|
const infoMotion = motion?.matches
|
||||||
? clock
|
? t('Reduced motion · history playback unavailable')
|
||||||
? t('Reduced motion · history playback unavailable')
|
|
||||||
: t('Reduced motion · manual history available')
|
|
||||||
: '';
|
: '';
|
||||||
const controls = {
|
const controls = {
|
||||||
readout: true,
|
readout: true,
|
||||||
@@ -926,27 +819,23 @@ export function createWeatherLayer({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getDiagnostics() {
|
getDiagnostics() {
|
||||||
const shared = clock?.getState();
|
const shared = clock.getState();
|
||||||
return {
|
return {
|
||||||
...rendering?.getDiagnostics(),
|
...rendering?.getDiagnostics(),
|
||||||
playing: shared ? shared.advancing : playing && !hostHidden,
|
playing: shared.advancing,
|
||||||
followLatest: isLatest(),
|
followLatest: isLatest(),
|
||||||
...(shared
|
clock: {
|
||||||
? {
|
mode: shared.mode,
|
||||||
clock: {
|
target: shared.target,
|
||||||
mode: shared.mode,
|
playing: shared.advancing,
|
||||||
target: shared.target,
|
},
|
||||||
playing: shared.advancing,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
historyFrames: manifest?.times?.length || 0,
|
historyFrames: manifest?.times?.length || 0,
|
||||||
timerActive: timer !== null,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
layer.disable();
|
layer.disable();
|
||||||
unsubscribeClock?.();
|
unsubscribeClock();
|
||||||
|
ownClock?.destroy();
|
||||||
removeCamera?.();
|
removeCamera?.();
|
||||||
removeCamera = null;
|
removeCamera = null;
|
||||||
motion?.removeEventListener?.('change', onVisibility);
|
motion?.removeEventListener?.('change', onVisibility);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import test from 'node:test';
|
import test, { afterEach } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createWeatherRendering, READY_WAKE_MS } from './rendering.js';
|
import { createWeatherRendering, READY_WAKE_MS } from './rendering.js';
|
||||||
import { createWeatherLayer } from './index.js';
|
import { createWeatherLayer } from './index.js';
|
||||||
import { createWeatherClock } from './clock.js';
|
import { createWeatherClock } from './clock.js';
|
||||||
|
import { worldClock } from '../../time/worldClock.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { Color, ImageryLayerCollection, GeographicTilingScheme } from 'cesium';
|
import { Color, ImageryLayerCollection, GeographicTilingScheme } from 'cesium';
|
||||||
import { NO_IMAGERY_HOST } from './imageryHost.js';
|
import { NO_IMAGERY_HOST } from './imageryHost.js';
|
||||||
@@ -482,6 +483,14 @@ test('failed and timed-out stages retain the previous observation; a healthy rep
|
|||||||
h.rendering.clear();
|
h.rendering.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A layer built without a clock keeps its own on the page's world clock:
|
||||||
|
// each test leaves that clock live, with no layer of its own on it.
|
||||||
|
const harnessed = new Set();
|
||||||
|
afterEach(() => {
|
||||||
|
for (const layer of harnessed) layer.destroy();
|
||||||
|
harnessed.clear();
|
||||||
|
worldClock.goLive();
|
||||||
|
});
|
||||||
function layerHarness({
|
function layerHarness({
|
||||||
clock,
|
clock,
|
||||||
reducedMotion = false,
|
reducedMotion = false,
|
||||||
@@ -562,6 +571,7 @@ function layerHarness({
|
|||||||
});
|
});
|
||||||
layer.init(viewer);
|
layer.init(viewer);
|
||||||
layer.enable();
|
layer.enable();
|
||||||
|
harnessed.add(layer);
|
||||||
return {
|
return {
|
||||||
layer,
|
layer,
|
||||||
rendering,
|
rendering,
|
||||||
@@ -575,72 +585,91 @@ function layerHarness({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('history has one owned timer, stops while hidden, and releases all work on disable/destroy', async (t) => {
|
test('without a clock the layer keeps one of its own on the world clock and lets it go on destroy', async () => {
|
||||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
||||||
const h = layerHarness();
|
const h = layerHarness();
|
||||||
const update = h.layer.update();
|
const update = h.layer.update();
|
||||||
await flush();
|
await flush();
|
||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await update;
|
await update;
|
||||||
|
assert.equal(worldClock.follows('weather-radar'), true);
|
||||||
|
for (const time of times)
|
||||||
|
assert.ok(worldClock.coverage().marks.includes(Date.parse(time)));
|
||||||
h.layer.setParams({ play: true });
|
h.layer.setParams({ play: true });
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, true);
|
assert.equal(worldClock.isPlaying(), true);
|
||||||
t.mock.timers.tick(2000);
|
assert.equal(worldClock.getState().stepping, false);
|
||||||
assert.equal(h.stages.length, 2);
|
assert.equal(h.layer.getDiagnostics().playing, true);
|
||||||
assert.equal(
|
|
||||||
h.layer.getDiagnostics().timerActive,
|
|
||||||
false,
|
|
||||||
'next timer waits for stage settlement',
|
|
||||||
);
|
|
||||||
h.stages[1].finish();
|
|
||||||
await flush();
|
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, true);
|
|
||||||
h.documentRef.hidden = true;
|
h.documentRef.hidden = true;
|
||||||
h.documentRef.emit('visibilitychange');
|
h.documentRef.emit('visibilitychange');
|
||||||
assert.equal(h.layer.getDiagnostics().playing, false);
|
await flush();
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
assert.equal(worldClock.isPlaying(), true, 'hiding suspends the layer only');
|
||||||
t.mock.timers.tick(20_000);
|
|
||||||
assert.equal(h.stages.length, 2);
|
|
||||||
h.documentRef.hidden = false;
|
h.documentRef.hidden = false;
|
||||||
h.documentRef.emit('visibilitychange');
|
h.documentRef.emit('visibilitychange');
|
||||||
h.layer.setParams({ play: true });
|
h.layer.setParams({ play: true });
|
||||||
|
assert.equal(worldClock.isPlaying(), false);
|
||||||
h.layer.disable();
|
h.layer.disable();
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
|
||||||
assert.equal(h.layer.getDiagnostics().historyFrames, 0);
|
assert.equal(h.layer.getDiagnostics().historyFrames, 0);
|
||||||
t.mock.timers.tick(20_000);
|
|
||||||
assert.equal(h.stages.length, 2);
|
|
||||||
h.layer.destroy();
|
h.layer.destroy();
|
||||||
|
assert.equal(worldClock.follows('weather-radar'), false);
|
||||||
assert.equal(h.documentRef.size, 0);
|
assert.equal(h.documentRef.size, 0);
|
||||||
assert.equal(h.motion.size, 0);
|
assert.equal(h.motion.size, 0);
|
||||||
assert.equal(h.moveEnd.size, 0);
|
assert.equal(h.moveEnd.size, 0);
|
||||||
assert.ok(h.clears() >= 1);
|
assert.ok(h.clears() >= 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reduced motion blocks autoplay but permits manual history and stops newly suspended playback', async (t) => {
|
test('reduced motion offers no play; steps still move the world clock from mark to mark', async () => {
|
||||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
||||||
const h = layerHarness({ reducedMotion: true });
|
const h = layerHarness({ reducedMotion: true });
|
||||||
const update = h.layer.update();
|
const update = h.layer.update();
|
||||||
await flush();
|
await flush();
|
||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await update;
|
await update;
|
||||||
h.layer.setParams({ play: true });
|
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
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
|
||||||
|
// newest frame, and the next one back to the frame before it.
|
||||||
h.layer.setParams({ step: -1 });
|
h.layer.setParams({ step: -1 });
|
||||||
assert.equal(h.stages[1].time, times[1]);
|
|
||||||
h.stages[1].finish();
|
|
||||||
await flush();
|
await flush();
|
||||||
h.motion.matches = false;
|
assert.equal(worldClock.now(), Date.parse(times[2]));
|
||||||
h.motion.emit('change');
|
h.layer.setParams({ step: -1 });
|
||||||
|
await flush();
|
||||||
|
assert.equal(worldClock.now(), Date.parse(times[1]));
|
||||||
|
assert.equal(h.stages.at(-1).time, times[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a row steps forward and plays from a past time through the world clock', async () => {
|
||||||
|
const h = layerHarness();
|
||||||
|
const update = h.layer.update();
|
||||||
|
await flush();
|
||||||
|
h.stages[0].finish();
|
||||||
|
await update;
|
||||||
|
worldClock.seek(Date.parse(times[0]));
|
||||||
|
await flush();
|
||||||
|
h.layer.setParams({ step: 1 });
|
||||||
|
assert.equal(worldClock.now(), Date.parse(times[1]));
|
||||||
|
assert.equal(worldClock.isPlaying(), false);
|
||||||
|
await flush();
|
||||||
|
assert.equal(h.stages.at(-1).time, times[1]);
|
||||||
|
h.layer.setParams({ step: 1 });
|
||||||
|
assert.equal(worldClock.now(), Date.parse(times[2]));
|
||||||
|
h.layer.setParams({ step: 1 });
|
||||||
|
assert.equal(
|
||||||
|
worldClock.now(),
|
||||||
|
Date.parse(times[2]),
|
||||||
|
'no mark after the newest frame',
|
||||||
|
);
|
||||||
|
worldClock.seek(Date.parse(times[1]));
|
||||||
|
await flush();
|
||||||
h.layer.setParams({ play: true });
|
h.layer.setParams({ play: true });
|
||||||
assert.equal(h.layer.getDiagnostics().playing, true);
|
assert.equal(worldClock.isPlaying(), true);
|
||||||
h.motion.matches = true;
|
assert.equal(worldClock.getState().stepping, false);
|
||||||
h.motion.emit('change');
|
assert.ok(
|
||||||
assert.equal(h.layer.getDiagnostics().playing, false);
|
worldClock.now() - Date.parse(times[1]) < 1_000,
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
'away from live, play goes on from the time shown',
|
||||||
h.layer.destroy();
|
);
|
||||||
|
h.layer.setParams({ step: 1 });
|
||||||
|
assert.equal(worldClock.isPlaying(), false, 'a step holds play');
|
||||||
|
assert.equal(worldClock.now(), Date.parse(times[2]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('disable invalidates an abort-insensitive source result and an already staged frame', async (t) => {
|
test('disable invalidates an abort-insensitive source result and an already staged frame', async (t) => {
|
||||||
@@ -669,7 +698,6 @@ test('disable invalidates an abort-insensitive source result and an already stag
|
|||||||
await next;
|
await next;
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(h.layer.getStats().count, 0);
|
assert.equal(h.layer.getStats().count, 0);
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
|
||||||
assert.equal(h.layer.getStats().loading, false);
|
assert.equal(h.layer.getStats().loading, false);
|
||||||
h.layer.destroy();
|
h.layer.destroy();
|
||||||
});
|
});
|
||||||
@@ -729,7 +757,6 @@ test('update cancellation reaches imagery after source acquisition has completed
|
|||||||
assert.equal(h.stages[0].signal.aborted, true);
|
assert.equal(h.stages[0].signal.aborted, true);
|
||||||
assert.equal(h.layer.getStats().count, 0);
|
assert.equal(h.layer.getStats().count, 0);
|
||||||
assert.equal(h.layer.getStats().loading, false);
|
assert.equal(h.layer.getStats().loading, false);
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
|
||||||
h.layer.destroy();
|
h.layer.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -837,21 +864,22 @@ test('coverage navigation requires the shared shell handoff and detaches cleanly
|
|||||||
h.layer.destroy();
|
h.layer.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('manifest refresh during a manual history stage does not strand loading controls', async () => {
|
test('manifest refresh during a history stage restages the same time and does not strand loading controls', async () => {
|
||||||
const h = layerHarness();
|
const h = layerHarness();
|
||||||
const first = h.layer.update();
|
const first = h.layer.update();
|
||||||
await flush();
|
await flush();
|
||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await first;
|
await first;
|
||||||
h.layer.setParams({ step: -1 });
|
worldClock.seek(Date.parse(times[1]));
|
||||||
|
await flush();
|
||||||
assert.equal(h.layer.getStats().loading, true);
|
assert.equal(h.layer.getStats().loading, true);
|
||||||
await h.layer.update();
|
const refreshed = h.layer.update();
|
||||||
assert.equal(
|
await flush();
|
||||||
h.stages.length,
|
assert.equal(h.stages.length, 3);
|
||||||
2,
|
assert.equal(h.stages[1].signal.aborted, true);
|
||||||
'metadata refresh preserves the staged manual selection',
|
assert.equal(h.stages[2].time, times[1], 'the refresh keeps the time');
|
||||||
);
|
h.stages[2].finish();
|
||||||
h.stages[1].finish();
|
await refreshed;
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(h.layer.getStats().loading, false);
|
assert.equal(h.layer.getStats().loading, false);
|
||||||
assert.equal(h.layer.getStats().observedAt, times[1]);
|
assert.equal(h.layer.getStats().observedAt, times[1]);
|
||||||
@@ -881,7 +909,8 @@ test('historical observations do not relabel a fresh lightning feed as stale', a
|
|||||||
await flush();
|
await flush();
|
||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await first;
|
await first;
|
||||||
h.layer.setParams({ step: -1 });
|
worldClock.seek(Date.parse(old));
|
||||||
|
await flush();
|
||||||
h.stages[1].finish();
|
h.stages[1].finish();
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(h.layer.getStats().stale, false);
|
assert.equal(h.layer.getStats().stale, false);
|
||||||
@@ -1256,8 +1285,7 @@ test('404 fails a frame without retry', async () => {
|
|||||||
h.rendering.clear();
|
h.rendering.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('no host pauses history, refreshes metadata, and restores retained time and playback', async (t) => {
|
test('no host holds the shown history frame, refreshes metadata, and restores the retained time', async () => {
|
||||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
||||||
let refreshes = 0;
|
let refreshes = 0;
|
||||||
const h = layerHarness({
|
const h = layerHarness({
|
||||||
feed: {
|
feed: {
|
||||||
@@ -1273,28 +1301,23 @@ test('no host pauses history, refreshes metadata, and restores retained time and
|
|||||||
await flush();
|
await flush();
|
||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await update;
|
await update;
|
||||||
h.layer.setParams({ play: true });
|
worldClock.seek(Date.parse(times[0]));
|
||||||
t.mock.timers.tick(2000);
|
await flush();
|
||||||
assert.equal(h.stages.length, 2);
|
assert.equal(h.stages.length, 2);
|
||||||
|
h.stages[1].finish();
|
||||||
|
await flush();
|
||||||
host = { collection: null, kind: 'none' };
|
host = { collection: null, kind: 'none' };
|
||||||
h.eventTarget.emit('gev:map-stack-changed');
|
h.eventTarget.emit('gev:map-stack-changed');
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(h.layer.getDiagnostics().playing, false);
|
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, false);
|
|
||||||
assert.equal(h.layer.getRowControls().summary.status, NO_IMAGERY_HOST);
|
assert.equal(h.layer.getRowControls().summary.status, NO_IMAGERY_HOST);
|
||||||
assert.equal(h.layer.getRowControls().info, NO_IMAGERY_HOST);
|
assert.equal(h.layer.getRowControls().info, NO_IMAGERY_HOST);
|
||||||
await h.layer.update();
|
await h.layer.update();
|
||||||
assert.equal(refreshes, 2);
|
assert.equal(refreshes, 2);
|
||||||
t.mock.timers.tick(10_000);
|
assert.equal(h.stages.length, 2, 'nothing stages without a host');
|
||||||
assert.equal(h.stages.length, 2);
|
|
||||||
assert.equal(h.layer.getDiagnostics().time, times[2]);
|
|
||||||
host = { collection: {}, kind: 'globe' };
|
host = { collection: {}, kind: 'globe' };
|
||||||
h.eventTarget.emit('gev:map-stack-changed');
|
h.eventTarget.emit('gev:map-stack-changed');
|
||||||
assert.equal(h.layer.getDiagnostics().playing, true);
|
await flush();
|
||||||
assert.equal(h.layer.getDiagnostics().timerActive, true);
|
assert.equal(h.stages.at(-1).time, times[0], 'the retained time restages');
|
||||||
t.mock.timers.tick(2000);
|
|
||||||
assert.equal(h.stages.length, 3);
|
|
||||||
assert.equal(h.stages[2].time, times[0]);
|
|
||||||
h.layer.destroy();
|
h.layer.destroy();
|
||||||
assert.equal(h.eventTarget.size, 0);
|
assert.equal(h.eventTarget.size, 0);
|
||||||
});
|
});
|
||||||
@@ -1431,25 +1454,21 @@ for (const [id, product] of [
|
|||||||
const [shell] = h.shells();
|
const [shell] = h.shells();
|
||||||
assert.equal(shell.show, true);
|
assert.equal(shell.show, true);
|
||||||
|
|
||||||
layer.setParams({ play: true });
|
worldClock.seek(Date.parse(times[0]));
|
||||||
t.mock.timers.tick(2000);
|
|
||||||
await flush();
|
await flush();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
layer.getDiagnostics().time,
|
layer.getDiagnostics().time,
|
||||||
times[0],
|
times[0],
|
||||||
'history advances at street level',
|
'history moves at street level',
|
||||||
);
|
);
|
||||||
camera.positionCartographic.height = 59_999;
|
camera.positionCartographic.height = 59_999;
|
||||||
camera.moveEnd.emit();
|
camera.moveEnd.emit();
|
||||||
assert.equal(shell.show, true, 'no height gate');
|
assert.equal(shell.show, true, 'no height gate');
|
||||||
assert.equal(layer.getDiagnostics().playing, true);
|
|
||||||
assert.equal(layer.getDiagnostics().timerActive, true);
|
|
||||||
|
|
||||||
host = { collection: null, kind: 'none' };
|
host = { collection: null, kind: 'none' };
|
||||||
events.emit('gev:map-stack-changed');
|
events.emit('gev:map-stack-changed');
|
||||||
assert.equal(shell.show, false);
|
assert.equal(shell.show, false);
|
||||||
assert.equal(layer.getRowControls().summary.status, NO_IMAGERY_HOST);
|
assert.equal(layer.getRowControls().summary.status, NO_IMAGERY_HOST);
|
||||||
assert.equal(layer.getDiagnostics().timerActive, false);
|
|
||||||
host = { collection: h.viewer.imageryLayers, kind: 'globe' };
|
host = { collection: h.viewer.imageryLayers, kind: 'globe' };
|
||||||
events.emit('gev:map-stack-changed');
|
events.emit('gev:map-stack-changed');
|
||||||
assert.equal(shell.destroyed, true);
|
assert.equal(shell.destroyed, true);
|
||||||
@@ -1648,10 +1667,12 @@ test('satellite mode chips restage history without changing time or latest-follo
|
|||||||
await flush();
|
await flush();
|
||||||
h.stages[0].finish();
|
h.stages[0].finish();
|
||||||
await update;
|
await update;
|
||||||
h.layer.setParams({ step: -1 });
|
worldClock.seek(Date.parse(times[1]));
|
||||||
|
await flush();
|
||||||
h.stages[1].finish();
|
h.stages[1].finish();
|
||||||
await flush();
|
await flush();
|
||||||
h.layer.setParams({ infrared: 'full' });
|
h.layer.setParams({ infrared: 'full' });
|
||||||
|
await flush();
|
||||||
assert.equal(h.stages[2].time, times[1]);
|
assert.equal(h.stages[2].time, times[1]);
|
||||||
assert.equal(h.stages[2].infrared, 0);
|
assert.equal(h.stages[2].infrared, 0);
|
||||||
assert.equal(h.layer.getParams().infrared, 0);
|
assert.equal(h.layer.getParams().infrared, 0);
|
||||||
|
|||||||
Reference in new issue
Block a user