Fix scrubs to live on long tracks, fire ages in a past window, and gaps between hourly cloud frames

- World timeline: a scrub counts as at now within a share of the track
  (0.5%), now held to at most two minutes. A layer whose source covers
  years made the window weeks wide, so most of the last month snapped to
  live. Short tracks keep their window.
- Fires: card and detail ages count back from the end of the 24 hours the
  layer shows when its snapshot ends at a set time, and from now only when
  it runs to now. A window that ended months ago read "new 105d" on fires
  three hours old at the time shown.
- Weather: a product's max gap and the delay after which its latest frame
  reads late are at least one and a half of its feed's median frame
  spacing (over its latest frames). A regional cloud feed with frames an
  hour apart drew nothing for the second half of every hour while
  scrubbing and read late after twenty minutes; feeds with frames minutes
  apart keep their gaps.
This commit is contained in:
Sameh Khamis committed 2026-11-14 14:52:48 -08:00
1 parent 7451c9b372
commit 5d733eb07b
12 files changed
+327 -15

No files matched your search

+46
View File
@@ -4,6 +4,52 @@ export const RADAR_MAX_GAP_MS = 30 * 60_000;
export const REGIONAL_INFRARED_MAX_GAP_MS = 30 * 60_000;
export const GLOBAL_INFRARED_MAX_GAP_MS = 3 * 60 * 60_000;
export const LIGHTNING_MAX_GAP_MS = 30 * 60_000;
/** How many of a feed's latest frames say how far apart its frames come. */
const SPACING_FRAMES = 12;
/** A frame holds this many of its feed's frame spacings before it lapses. */
const SPACING_SLACK = 1.5;
/**
* How far apart a feed's frames come: the median spacing of its latest
* frames, in ms, or null with fewer than two. Takes UTC times or epoch ms.
* @param {Array<string|number>} times
* @returns {number|null}
*/
export function frameSpacingMs(times) {
const at = [
...new Set(
(times ?? [])
.map((time) => (typeof time === 'number' ? time : Date.parse(time)))
.filter(Number.isFinite),
),
]
.sort((a, b) => a - b)
.slice(-(SPACING_FRAMES + 1));
if (at.length < 2) return null;
const gaps = at
.slice(1)
.map((time, i) => time - at[i])
.sort((a, b) => a - b);
const middle = gaps.length >> 1;
return gaps.length % 2 ? gaps[middle] : (gaps[middle - 1] + gaps[middle]) / 2;
}
/**
* A product's least time, `leastMs`, held to its feed's own frame spacing:
* never less than one and a half spacings, so a feed whose frames come an
* hour apart covers the whole hour between them, while one with frames
* minutes apart keeps `leastMs`.
* @param {number} leastMs
* @param {Array<string|number>} times
* @returns {number}
*/
export function atLeastFrameSpacing(leastMs, times) {
const spacing = frameSpacingMs(times);
return spacing === null
? leastMs
: Math.max(leastMs, spacing * SPACING_SLACK);
}
/** The longest play waits on a frame before it goes on without it. */
export const PLAY_WAIT_MS = 8_000;
// Playing on its own, the world does not wait on the frame it is at: that