geotimelapse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/bounds.d.ts +5 -0
  4. package/dist/bounds.d.ts.map +1 -0
  5. package/dist/bounds.js +9 -0
  6. package/dist/bounds.js.map +1 -0
  7. package/dist/clock.d.ts +24 -0
  8. package/dist/clock.d.ts.map +1 -0
  9. package/dist/clock.js +108 -0
  10. package/dist/clock.js.map +1 -0
  11. package/dist/counter.d.ts +18 -0
  12. package/dist/counter.d.ts.map +1 -0
  13. package/dist/counter.js +54 -0
  14. package/dist/counter.js.map +1 -0
  15. package/dist/duckdb-source.d.ts +24 -0
  16. package/dist/duckdb-source.d.ts.map +1 -0
  17. package/dist/duckdb-source.js +211 -0
  18. package/dist/duckdb-source.js.map +1 -0
  19. package/dist/geo-timelapse.d.ts +8 -0
  20. package/dist/geo-timelapse.d.ts.map +1 -0
  21. package/dist/geo-timelapse.js +143 -0
  22. package/dist/geo-timelapse.js.map +1 -0
  23. package/dist/glow-layer.d.ts +47 -0
  24. package/dist/glow-layer.d.ts.map +1 -0
  25. package/dist/glow-layer.js +129 -0
  26. package/dist/glow-layer.js.map +1 -0
  27. package/dist/hooks.d.ts +32 -0
  28. package/dist/hooks.d.ts.map +1 -0
  29. package/dist/hooks.js +142 -0
  30. package/dist/hooks.js.map +1 -0
  31. package/dist/index.d.ts +6 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/map-style.d.ts +3 -0
  36. package/dist/map-style.d.ts.map +1 -0
  37. package/dist/map-style.js +832 -0
  38. package/dist/map-style.js.map +1 -0
  39. package/dist/map.d.ts +11 -0
  40. package/dist/map.d.ts.map +1 -0
  41. package/dist/map.js +60 -0
  42. package/dist/map.js.map +1 -0
  43. package/dist/player-bar.d.ts +4 -0
  44. package/dist/player-bar.d.ts.map +1 -0
  45. package/dist/player-bar.js +118 -0
  46. package/dist/player-bar.js.map +1 -0
  47. package/dist/settings-menu.d.ts +17 -0
  48. package/dist/settings-menu.d.ts.map +1 -0
  49. package/dist/settings-menu.js +18 -0
  50. package/dist/settings-menu.js.map +1 -0
  51. package/dist/synthetic-source.d.ts +18 -0
  52. package/dist/synthetic-source.d.ts.map +1 -0
  53. package/dist/synthetic-source.js +192 -0
  54. package/dist/synthetic-source.js.map +1 -0
  55. package/dist/types.d.ts +62 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +2 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/utils.d.ts +9 -0
  60. package/dist/utils.d.ts.map +1 -0
  61. package/dist/utils.js +28 -0
  62. package/dist/utils.js.map +1 -0
  63. package/package.json +67 -0
  64. package/src/bounds.tsx +14 -0
  65. package/src/clock.tsx +130 -0
  66. package/src/counter.tsx +96 -0
  67. package/src/duckdb-source.ts +238 -0
  68. package/src/geo-timelapse.tsx +204 -0
  69. package/src/glow-layer.test.ts +51 -0
  70. package/src/glow-layer.ts +166 -0
  71. package/src/hooks.ts +167 -0
  72. package/src/index.ts +5 -0
  73. package/src/map-style.ts +833 -0
  74. package/src/map.tsx +107 -0
  75. package/src/player-bar.tsx +164 -0
  76. package/src/settings-menu.tsx +117 -0
  77. package/src/synthetic-source.test.ts +58 -0
  78. package/src/synthetic-source.ts +218 -0
  79. package/src/types.ts +66 -0
  80. package/src/utils.test.ts +60 -0
  81. package/src/utils.ts +32 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo-timelapse.d.ts","sourceRoot":"","sources":["../src/geo-timelapse.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAa,MAAM,YAAY,CAAC;AAqL/D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAMpD"}
@@ -0,0 +1,143 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { MapBoundsProvider } from './bounds.js';
5
+ import { DAY_SECONDS, TimelapseClockProvider, useTimelapseClock } from './clock.js';
6
+ import Counter from './counter.js';
7
+ import { useAdaptiveTrailFrames, useFps, useFrameHistory, useMinuteActivity } from './hooks.js';
8
+ import TimelapseMap from './map.js';
9
+ import PlayerBar from './player-bar.js';
10
+ import SettingsMenu from './settings-menu.js';
11
+ const defaultFormat = (value) => Math.round(value).toLocaleString('en-US');
12
+ function Stage({ source, mapboxAccessToken, initialBounds, tuning, dateLabel, timeZoneLabel, formatValue = defaultFormat, formatCount = defaultFormat, }) {
13
+ const clock = useTimelapseClock();
14
+ const stageRef = useRef(null);
15
+ const [fullscreen, setFullscreen] = useState(false);
16
+ const [loop, setLoop] = useState(true);
17
+ const [scoped, setScoped] = useState(true);
18
+ const [progress, setProgress] = useState(null);
19
+ const [ready, setReady] = useState(false);
20
+ const [loadError, setLoadError] = useState(null);
21
+ // Bumped after every completed re-scope so readers re-query the source.
22
+ const [scopeVersion, setScopeVersion] = useState(0);
23
+ // Mouse-idle state: the settings gear and the cursor itself fade out so
24
+ // nothing overlays a TV-screen replay; any mouse move brings them back.
25
+ const [uiVisible, setUiVisible] = useState(true);
26
+ const [bounds, setBounds] = useState(null);
27
+ const frames = useFrameHistory(source, ready);
28
+ const trailFrames = useAdaptiveTrailFrames();
29
+ const fps = useFps();
30
+ const activity = useMinuteActivity(source, ready, scopeVersion);
31
+ const onBoundsChange = useCallback((next) => {
32
+ setBounds((prev) => prev &&
33
+ prev.west === next.west &&
34
+ prev.south === next.south &&
35
+ prev.east === next.east &&
36
+ prev.north === next.north
37
+ ? prev
38
+ : next);
39
+ }, []);
40
+ // Load the day once, then start the replay. A swapped source re-gates every
41
+ // reader until its own load resolves.
42
+ useEffect(() => {
43
+ let disposed = false;
44
+ // Re-gating must track the source swap itself, not a render.
45
+ // oxlint-disable-next-line react/set-state-in-effect
46
+ setReady(false);
47
+ setProgress(null);
48
+ setLoadError(null);
49
+ source
50
+ .load((loadedBytes, totalBytes) => {
51
+ if (!disposed)
52
+ setProgress({ loadedBytes, totalBytes });
53
+ })
54
+ .then(() => {
55
+ if (disposed)
56
+ return;
57
+ setReady(true);
58
+ clock.play();
59
+ })
60
+ .catch((error) => {
61
+ if (!disposed)
62
+ setLoadError(error instanceof Error ? error.message : String(error));
63
+ });
64
+ return () => {
65
+ disposed = true;
66
+ };
67
+ }, [source, clock]);
68
+ useEffect(() => {
69
+ let timer;
70
+ const show = () => {
71
+ setUiVisible(true);
72
+ clearTimeout(timer);
73
+ timer = setTimeout(() => setUiVisible(false), 5000);
74
+ };
75
+ show();
76
+ window.addEventListener('pointermove', show);
77
+ return () => {
78
+ clearTimeout(timer);
79
+ window.removeEventListener('pointermove', show);
80
+ };
81
+ }, []);
82
+ useEffect(() => {
83
+ const onChange = () => setFullscreen(document.fullscreenElement === stageRef.current);
84
+ document.addEventListener('fullscreenchange', onChange);
85
+ return () => document.removeEventListener('fullscreenchange', onChange);
86
+ }, []);
87
+ // Unscoping goes back to the whole world, once per toggle.
88
+ useEffect(() => {
89
+ if (!ready || scoped)
90
+ return;
91
+ let disposed = false;
92
+ void source.setScope(null).then(() => {
93
+ if (!disposed)
94
+ setScopeVersion((version) => version + 1);
95
+ });
96
+ return () => {
97
+ disposed = true;
98
+ };
99
+ }, [source, scoped, ready]);
100
+ // Re-scope the aggregates once the viewport settles.
101
+ useEffect(() => {
102
+ if (!ready || !scoped || !bounds)
103
+ return;
104
+ let disposed = false;
105
+ const handle = setTimeout(() => void source.setScope(bounds).then(() => {
106
+ if (!disposed)
107
+ setScopeVersion((version) => version + 1);
108
+ }), 400);
109
+ return () => {
110
+ disposed = true;
111
+ clearTimeout(handle);
112
+ };
113
+ }, [source, bounds, scoped, ready]);
114
+ // The clock pauses at the end of the day; loop restarts it (play() rewinds).
115
+ useEffect(() => {
116
+ if (!loop)
117
+ return;
118
+ return clock.subscribe(() => {
119
+ if (!clock.isPlaying() && clock.getDaySeconds() >= DAY_SECONDS)
120
+ clock.play();
121
+ });
122
+ }, [loop, clock]);
123
+ const toggleFullscreen = () => {
124
+ if (document.fullscreenElement)
125
+ void document.exitFullscreen();
126
+ else
127
+ void stageRef.current?.requestFullscreen();
128
+ };
129
+ return (_jsxs("div", { ref: stageRef, className: [
130
+ 'relative h-full w-full overflow-hidden bg-black',
131
+ /* important beats the inline cursor deck.gl sets on its canvas */
132
+ uiVisible ? '' : 'cursor-none! **:cursor-none!',
133
+ ].join(' '), children: [_jsx(TimelapseMap, { frames: frames.slice(0, trailFrames), mapboxAccessToken: mapboxAccessToken, initialBounds: initialBounds, tuning: tuning, onBoundsChange: onBoundsChange }), _jsxs(MapBoundsProvider, { value: bounds, children: [_jsx(SettingsMenu, { visible: uiVisible, fullscreen: fullscreen, onToggleFullscreen: toggleFullscreen, loop: loop, onToggleLoop: () => setLoop((value) => !value), scoped: scoped, onToggleScoped: () => setScoped((value) => !value), frame: frames[0], fps: fps, trailFrames: trailFrames }), _jsx(Counter, { source: source, ready: ready, error: loadError, progress: progress, scopeVersion: scopeVersion, dateLabel: dateLabel, timeZoneLabel: timeZoneLabel, formatValue: formatValue, formatCount: formatCount })] }), _jsx(PlayerBar, { activity: activity })] }));
134
+ }
135
+ /**
136
+ * A full-day replay of geolocated events: dark glowing map, waveform seek
137
+ * bar, running totals, and a TV mode (autoplay, loop, self-hiding UI).
138
+ * Fills its parent — give the wrapping element an explicit size.
139
+ */
140
+ export function GeoTimelapse(props) {
141
+ return (_jsx(TimelapseClockProvider, { children: _jsx(Stage, { ...props }) }));
142
+ }
143
+ //# sourceMappingURL=geo-timelapse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo-timelapse.js","sourceRoot":"","sources":["../src/geo-timelapse.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import { ScatterplotLayer } from 'deck.gl';
2
+ import type { FrameBids } from './hooks.js';
3
+ export default class GlowLayer<DataT> extends ScatterplotLayer<DataT> {
4
+ static layerName: string;
5
+ getShaders(): any;
6
+ }
7
+ export declare const GLOW_PARAMETERS: {
8
+ readonly blendColorOperation: "add";
9
+ readonly blendColorSrcFactor: "src-alpha";
10
+ readonly blendColorDstFactor: "one";
11
+ readonly blendAlphaOperation: "add";
12
+ readonly blendAlphaSrcFactor: "one";
13
+ readonly blendAlphaDstFactor: "one";
14
+ };
15
+ /**
16
+ * The visual knobs of the glow trail. Radii are ground distances in meters so
17
+ * the on-screen density — and therefore the additive glow intensity — stays
18
+ * invariant across zoom levels.
19
+ */
20
+ export interface GlowTuning {
21
+ /** Master per-point transparency, 0-255: density does the brightening. */
22
+ pointAlpha: number;
23
+ /** Halo tint; the shader keeps the point core pure white. */
24
+ haloTint: [number, number, number];
25
+ /** Radius of the freshest frame's points, in meters (per unit of weight). */
26
+ flashRadius: number;
27
+ /** Ghost radii taper linearly from this down to ghostMinRadius, in meters. */
28
+ ghostMaxRadius: number;
29
+ ghostMinRadius: number;
30
+ /** Floor in pixels so isolated points survive deep zoom-outs. Applies to
31
+ * the freshest frame; ghosts get it scaled down with their age taper. */
32
+ minRadiusPx: number;
33
+ /** Asymptote of the rendered radius, in pixels: the halo tends toward it
34
+ * (tanh) as stacks grow, without ever flattening against a hard clamp. */
35
+ maxRadiusPx: number;
36
+ /** How many past frames stay visible as fading ghosts. */
37
+ trailFrames: number;
38
+ }
39
+ export declare const DEFAULT_GLOW_TUNING: GlowTuning;
40
+ /**
41
+ * One layer per remembered frame, fading and tightening with age into a ghost
42
+ * trail. The layer id is the frame's ring-buffer slot: a slot's data only
43
+ * changes when its frame rotates out, so per tick a single layer re-uploads
44
+ * attributes. Pure: same frames + tuning, same layers.
45
+ */
46
+ export declare function buildGlowLayers(frames: FrameBids[], tuning?: GlowTuning): GlowLayer<unknown>[];
47
+ //# sourceMappingURL=glow-layer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glow-layer.d.ts","sourceRoot":"","sources":["../src/glow-layer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA4C5C,MAAM,CAAC,OAAO,OAAO,SAAS,CAAC,KAAK,CAAE,SAAQ,gBAAgB,CAAC,KAAK,CAAC;IACnE,MAAM,CAAC,SAAS,SAAe;IAE/B,UAAU;CAoBX;AAGD,eAAO,MAAM,eAAe;;;;;;;CAOlB,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB;8EAC0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB;+EAC2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,eAAO,MAAM,mBAAmB,EAAE,UASjC,CAAC;AAiBF;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,UAAgC,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,CAyBnH"}
@@ -0,0 +1,129 @@
1
+ import { ScatterplotLayer } from 'deck.gl';
2
+ // ScatterplotLayer with the disc fill replaced by a point light: a ~1px core
3
+ // plus an inverse-square halo. Rendered additively (see GLOW_PARAMETERS), so
4
+ // stacked points accumulate into a bloom that reveals density.
5
+ const fs = /* glsl */ `\
6
+ #version 300 es
7
+ #define SHADER_NAME glow-layer-fragment-shader
8
+
9
+ precision highp float;
10
+
11
+ in vec4 vFillColor;
12
+ in vec2 unitPosition;
13
+ in float outerRadiusPixels;
14
+ in float vSqrtWeight;
15
+ in float vSoftRadiusPixels;
16
+
17
+ out vec4 fragColor;
18
+
19
+ void main(void) {
20
+ geometry.uv = unitPosition;
21
+
22
+ float distPixels = length(unitPosition) * outerRadiusPixels;
23
+ float core = smoothstep(1.4, 0.3, distPixels);
24
+ // Gaussian halo: a soft, wide shoulder instead of a spiky falloff, with a
25
+ // per-point amplitude low enough that white needs hundreds of stacked
26
+ // points — that slow additive build-up is what reads as glow. Its sigma
27
+ // follows the soft-capped radius so heavy stacks stay distinguishable.
28
+ float sigma = vSoftRadiusPixels * 0.35;
29
+ // Aggregation collapsed stacked points into one draw; sqrt(weight) raises
30
+ // the halo AMPLITUDE rather than the summed profile, so heavy stacks keep a
31
+ // gaussian gradient instead of clipping into a flat disc. The exponential
32
+ // shoulder (film-exposure response) approaches 1 without ever clipping, so
33
+ // intensity keeps discriminating stack sizes. Weight 1 is near-identical.
34
+ float gain = 1.0 - exp(-vSqrtWeight * 0.035);
35
+ float halo = gain * exp(-distPixels * distPixels / (2.0 * sigma * sigma));
36
+ // The core reads pure white; the halo carries the fill color's tint.
37
+ vec3 color = mix(vFillColor.rgb, vec3(1.0), core);
38
+ fragColor = vec4(color, vFillColor.a * min(core + halo, 1.0));
39
+
40
+ DECKGL_FILTER_COLOR(fragColor, geometry);
41
+ }
42
+ `;
43
+ export default class GlowLayer extends ScatterplotLayer {
44
+ static layerName = 'GlowLayer';
45
+ getShaders() {
46
+ return {
47
+ ...super.getShaders(),
48
+ fs,
49
+ // The radius attribute carries sqrt(weight); hand it to the fragment
50
+ // stage, along with a soft-capped radius: instead of the hard clamp at
51
+ // radiusMaxPixels, tanh approaches it asymptotically so ever-heavier
52
+ // stacks keep growing (less and less) instead of all looking alike.
53
+ inject: {
54
+ 'vs:#decl': 'out float vSqrtWeight;\nout float vSoftRadiusPixels;',
55
+ 'vs:#main-end': `\
56
+ vSqrtWeight = instanceRadius;
57
+ float rawRadiusPixels = project_size_to_pixel(scatterplot.radiusScale * instanceRadius, scatterplot.radiusUnits);
58
+ vSoftRadiusPixels = max(
59
+ scatterplot.radiusMaxPixels * tanh(rawRadiusPixels / scatterplot.radiusMaxPixels),
60
+ scatterplot.radiusMinPixels
61
+ );`,
62
+ },
63
+ };
64
+ }
65
+ }
66
+ // Additive blending: overlapping halos sum toward white instead of occluding.
67
+ export const GLOW_PARAMETERS = {
68
+ blendColorOperation: 'add',
69
+ blendColorSrcFactor: 'src-alpha',
70
+ blendColorDstFactor: 'one',
71
+ blendAlphaOperation: 'add',
72
+ blendAlphaSrcFactor: 'one',
73
+ blendAlphaDstFactor: 'one',
74
+ };
75
+ // Defaults calibrated on the glow-lab bench (apps/website /lab/glow); the
76
+ // ghost radii keep the 2/3 and 1/15 taper ratios of the flash radius.
77
+ export const DEFAULT_GLOW_TUNING = {
78
+ pointAlpha: 115,
79
+ haloTint: [190, 222, 255],
80
+ flashRadius: 80_000,
81
+ ghostMaxRadius: 53_000,
82
+ ghostMinRadius: 5_000,
83
+ minRadiusPx: 12,
84
+ maxRadiusPx: 80,
85
+ trailFrames: 12,
86
+ };
87
+ function radiusForAge(age, tuning) {
88
+ if (age === 0)
89
+ return tuning.flashRadius;
90
+ const taper = (age - 1) / Math.max(tuning.trailFrames - 2, 1);
91
+ return tuning.ghostMaxRadius - (tuning.ghostMaxRadius - tuning.ghostMinRadius) * taper;
92
+ }
93
+ // Ghosts fade geometrically with age. Applied in the fill color, not the
94
+ // layer opacity — deck gamma-corrects opacity (^1/2.2), which would keep the
95
+ // oldest ghost at ~32% instead of ~9%.
96
+ const GHOST_FADE = 0.8;
97
+ function alphaForFrame(age, tuning) {
98
+ return Math.round(tuning.pointAlpha * GHOST_FADE ** age);
99
+ }
100
+ /**
101
+ * One layer per remembered frame, fading and tightening with age into a ghost
102
+ * trail. The layer id is the frame's ring-buffer slot: a slot's data only
103
+ * changes when its frame rotates out, so per tick a single layer re-uploads
104
+ * attributes. Pure: same frames + tuning, same layers.
105
+ */
106
+ export function buildGlowLayers(frames, tuning = DEFAULT_GLOW_TUNING) {
107
+ return frames.map((frame, age) => new GlowLayer({
108
+ id: `glow-${frame.key % tuning.trailFrames}`,
109
+ data: {
110
+ length: frame.count,
111
+ attributes: {
112
+ // sqrt(weight) radii: point AREA stays linear in the weight.
113
+ getPosition: { value: frame.positions, size: 2 },
114
+ getRadius: { value: frame.radii, size: 1 },
115
+ },
116
+ },
117
+ getFillColor: [...tuning.haloTint, alphaForFrame(age, tuning)],
118
+ // Fill-rate is the frame budget: cost grows with the quad area (r²),
119
+ // so ghost frames shrink with age — the glow cools as it fades. The
120
+ // pixel floor tapers along, or it would resurrect every ghost.
121
+ radiusScale: radiusForAge(age, tuning),
122
+ radiusMinPixels: (tuning.minRadiusPx * radiusForAge(age, tuning)) / tuning.flashRadius,
123
+ radiusMaxPixels: tuning.maxRadiusPx,
124
+ radiusUnits: 'meters',
125
+ stroked: false,
126
+ parameters: GLOW_PARAMETERS,
127
+ }));
128
+ }
129
+ //# sourceMappingURL=glow-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glow-layer.js","sourceRoot":"","sources":["../src/glow-layer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAI3C,6EAA6E;AAC7E,6EAA6E;AAC7E,+DAA+D;AAC/D,MAAM,EAAE,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCrB,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,SAAiB,SAAQ,gBAAuB;IACnE,MAAM,CAAC,SAAS,GAAG,WAAW,CAAC;IAE/B,UAAU;QACR,OAAO;YACL,GAAG,KAAK,CAAC,UAAU,EAAE;YACrB,EAAE;YACF,qEAAqE;YACrE,uEAAuE;YACvE,qEAAqE;YACrE,oEAAoE;YACpE,MAAM,EAAE;gBACN,UAAU,EAAE,sDAAsD;gBAClE,cAAc,EAAE;;;;;;GAMrB;aACI;SACF,CAAC;IACJ,CAAC;;AAGH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,mBAAmB,EAAE,KAAK;IAC1B,mBAAmB,EAAE,WAAW;IAChC,mBAAmB,EAAE,KAAK;IAC1B,mBAAmB,EAAE,KAAK;IAC1B,mBAAmB,EAAE,KAAK;IAC1B,mBAAmB,EAAE,KAAK;CAClB,CAAC;AA2BX,0EAA0E;AAC1E,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAe;IAC7C,UAAU,EAAE,GAAG;IACf,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;IACzB,WAAW,EAAE,MAAM;IACnB,cAAc,EAAE,MAAM;IACtB,cAAc,EAAE,KAAK;IACrB,WAAW,EAAE,EAAE;IACf,WAAW,EAAE,EAAE;IACf,WAAW,EAAE,EAAE;CAChB,CAAC;AAEF,SAAS,YAAY,CAAC,GAAW,EAAE,MAAkB;IACnD,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,WAAW,CAAC;IACzC,MAAM,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9D,OAAO,MAAM,CAAC,cAAc,GAAG,CAAC,MAAM,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC,GAAG,KAAK,CAAC;AACzF,CAAC;AAED,yEAAyE;AACzE,6EAA6E;AAC7E,uCAAuC;AACvC,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,SAAS,aAAa,CAAC,GAAW,EAAE,MAAkB;IACpD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,GAAG,UAAU,IAAI,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAmB,EAAE,SAAqB,mBAAmB;IAC3F,OAAO,MAAM,CAAC,GAAG,CACf,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CACb,IAAI,SAAS,CAAC;QACZ,EAAE,EAAE,QAAQ,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,WAAW,EAAE;QAC5C,IAAI,EAAE;YACJ,MAAM,EAAE,KAAK,CAAC,KAAK;YACnB,UAAU,EAAE;gBACV,6DAA6D;gBAC7D,WAAW,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE;gBAChD,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE;aAC3C;SACF;QACD,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9D,qEAAqE;QACrE,oEAAoE;QACpE,+DAA+D;QAC/D,WAAW,EAAE,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC;QACtC,eAAe,EAAE,CAAC,MAAM,CAAC,WAAW,GAAG,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,WAAW;QACtF,eAAe,EAAE,MAAM,CAAC,WAAW;QACnC,WAAW,EAAE,QAAQ;QACrB,OAAO,EAAE,KAAK;QACd,UAAU,EAAE,eAAe;KAC5B,CAAC,CACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { GeoTimelapseSource } from './types.js';
2
+ export interface FrameBids {
3
+ /** Monotonic frame number, also the identity of the frame's layer slot. */
4
+ key: number;
5
+ /** Interleaved [lon, lat] pairs, one per distinct location, ready for a binary deck.gl attribute. */
6
+ positions: Float32Array;
7
+ /** Per-location sqrt(weight): scaling the radius by it keeps the point AREA linear in the weight. */
8
+ radii: Float32Array;
9
+ /** Distinct locations in the frame (the instance count). */
10
+ count: number;
11
+ queryMs: number;
12
+ }
13
+ /** How many past frames stay visible as fading ghost trails. */
14
+ export declare const FRAME_HISTORY = 12;
15
+ /**
16
+ * The last FRAME_HISTORY frames of events, newest first. Each frame holds
17
+ * the window since the frame before it; a seek resets the history.
18
+ */
19
+ export declare function useFrameHistory(source: GeoTimelapseSource, ready: boolean): FrameBids[];
20
+ /** Event count for each minute of the replayed day, within the current scope. */
21
+ export declare function useMinuteActivity(source: GeoTimelapseSource, ready: boolean, scopeVersion: number): Float32Array | null;
22
+ /**
23
+ * Trail length that adapts to the achieved frame rate: under 24 fps ghosts
24
+ * are shed one per second, above 58 fps they come back, up to FRAME_HISTORY.
25
+ * (58, not 60: a 60 Hz display — the TV target — never reads past 60.) The
26
+ * 1s sampling window doubles as a cooldown so the count settles instead of
27
+ * oscillating across the wide 24-58 dead band.
28
+ */
29
+ export declare function useAdaptiveTrailFrames(): number;
30
+ /** Rendered frames per second, sampled twice a second. */
31
+ export declare function useFps(): number;
32
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAQrD,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,qGAAqG;IACrG,SAAS,EAAE,YAAY,CAAC;IACxB,qGAAqG;IACrG,KAAK,EAAE,YAAY,CAAC;IACpB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gEAAgE;AAChE,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,EAAE,CAsDvF;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,GACnB,YAAY,GAAG,IAAI,CAiBrB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAuB/C;AAED,0DAA0D;AAC1D,wBAAgB,MAAM,IAAI,MAAM,CAqB/B"}
package/dist/hooks.js ADDED
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { useTimelapseClock } from './clock.js';
4
+ // One clock tick of replayed time (480x speed, 24 ticks/s).
5
+ const DEFAULT_WINDOW_S = 20;
6
+ // Widened-window cap when ticks were skipped (slow queries, render lag).
7
+ // Kept tight: giant catch-up frames feed the lag they compensate for.
8
+ const MAX_WINDOW_S = 3 * DEFAULT_WINDOW_S;
9
+ /** How many past frames stay visible as fading ghost trails. */
10
+ export const FRAME_HISTORY = 12;
11
+ /**
12
+ * The last FRAME_HISTORY frames of events, newest first. Each frame holds
13
+ * the window since the frame before it; a seek resets the history.
14
+ */
15
+ export function useFrameHistory(source, ready) {
16
+ const clock = useTimelapseClock();
17
+ const [frames, setFrames] = useState([]);
18
+ useEffect(() => {
19
+ if (!ready)
20
+ return;
21
+ let disposed = false;
22
+ let inFlight = false;
23
+ let lastQueried = -1;
24
+ let lastEpoch = -1;
25
+ let frameKey = 0;
26
+ const query = async () => {
27
+ const daySeconds = Math.floor(clock.getDaySeconds());
28
+ const epoch = clock.getEpoch();
29
+ if (inFlight || (daySeconds === lastQueried && epoch === lastEpoch))
30
+ return;
31
+ inFlight = true;
32
+ try {
33
+ const started = performance.now();
34
+ // A time jump (seek, restart) starts the trail over; mere lag only
35
+ // widens the window, capped, so skipped ticks don't drop events.
36
+ const jumped = epoch !== lastEpoch || lastQueried < 0 || daySeconds <= lastQueried;
37
+ const span = jumped ? DEFAULT_WINDOW_S : Math.min(daySeconds - lastQueried, MAX_WINDOW_S);
38
+ lastQueried = daySeconds;
39
+ lastEpoch = epoch;
40
+ const result = await source.frame(daySeconds - span, daySeconds);
41
+ if (disposed)
42
+ return;
43
+ const radii = new Float32Array(result.count);
44
+ for (let i = 0; i < result.count; i++)
45
+ radii[i] = Math.sqrt(result.weights[i]);
46
+ const frame = {
47
+ key: frameKey++,
48
+ positions: result.positions,
49
+ radii,
50
+ count: result.count,
51
+ queryMs: Math.round(performance.now() - started),
52
+ };
53
+ // Ghost trails only make sense over contiguous playback.
54
+ setFrames((history) => (jumped ? [frame] : [frame, ...history.slice(0, FRAME_HISTORY - 1)]));
55
+ }
56
+ finally {
57
+ inFlight = false;
58
+ }
59
+ };
60
+ void query();
61
+ const unsubscribe = clock.subscribe(() => void query());
62
+ return () => {
63
+ disposed = true;
64
+ unsubscribe();
65
+ };
66
+ // ready re-arms the reader once the source has loaded.
67
+ // oxlint-disable-next-line react/exhaustive-effect-dependencies
68
+ }, [clock, source, ready]);
69
+ return frames;
70
+ }
71
+ /** Event count for each minute of the replayed day, within the current scope. */
72
+ export function useMinuteActivity(source, ready, scopeVersion) {
73
+ const [activity, setActivity] = useState(null);
74
+ useEffect(() => {
75
+ if (!ready)
76
+ return;
77
+ let disposed = false;
78
+ void source.activity().then((counts) => {
79
+ if (!disposed)
80
+ setActivity(counts);
81
+ });
82
+ return () => {
83
+ disposed = true;
84
+ };
85
+ // ready and scopeVersion re-run the effect so a (re)loaded source is re-read.
86
+ // oxlint-disable-next-line react/exhaustive-effect-dependencies
87
+ }, [source, ready, scopeVersion]);
88
+ return activity;
89
+ }
90
+ /**
91
+ * Trail length that adapts to the achieved frame rate: under 24 fps ghosts
92
+ * are shed one per second, above 58 fps they come back, up to FRAME_HISTORY.
93
+ * (58, not 60: a 60 Hz display — the TV target — never reads past 60.) The
94
+ * 1s sampling window doubles as a cooldown so the count settles instead of
95
+ * oscillating across the wide 24-58 dead band.
96
+ */
97
+ export function useAdaptiveTrailFrames() {
98
+ const [count, setCount] = useState(FRAME_HISTORY);
99
+ useEffect(() => {
100
+ let rafId = 0;
101
+ let ticks = 0;
102
+ let windowStart = performance.now();
103
+ const loop = (now) => {
104
+ ticks += 1;
105
+ if (now - windowStart >= 1000) {
106
+ const fps = (ticks * 1000) / (now - windowStart);
107
+ if (fps < 24)
108
+ setCount((current) => Math.max(current - 1, 2));
109
+ else if (fps > 58)
110
+ setCount((current) => Math.min(current + 1, FRAME_HISTORY));
111
+ ticks = 0;
112
+ windowStart = now;
113
+ }
114
+ rafId = requestAnimationFrame(loop);
115
+ };
116
+ rafId = requestAnimationFrame(loop);
117
+ return () => cancelAnimationFrame(rafId);
118
+ }, []);
119
+ return count;
120
+ }
121
+ /** Rendered frames per second, sampled twice a second. */
122
+ export function useFps() {
123
+ const [fps, setFps] = useState(0);
124
+ useEffect(() => {
125
+ let rafId = 0;
126
+ let count = 0;
127
+ let windowStart = performance.now();
128
+ const loop = (now) => {
129
+ count += 1;
130
+ if (now - windowStart >= 500) {
131
+ setFps(Math.round((count * 1000) / (now - windowStart)));
132
+ count = 0;
133
+ windowStart = now;
134
+ }
135
+ rafId = requestAnimationFrame(loop);
136
+ };
137
+ rafId = requestAnimationFrame(loop);
138
+ return () => cancelAnimationFrame(rafId);
139
+ }, []);
140
+ return fps;
141
+ }
142
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { GeoTimelapse } from './geo-timelapse.js';
2
+ export { buildGlowLayers, DEFAULT_GLOW_TUNING, default as GlowLayer, GLOW_PARAMETERS } from './glow-layer.js';
3
+ export type { GlowTuning } from './glow-layer.js';
4
+ export type { FrameBids } from './hooks.js';
5
+ export type { FramePoints, GeoTimelapseProps, GeoTimelapseSource, MapBounds, Totals } from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,OAAO,IAAI,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC9G,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { GeoTimelapse } from './geo-timelapse.js';
2
+ export { buildGlowLayers, DEFAULT_GLOW_TUNING, default as GlowLayer, GLOW_PARAMETERS } from './glow-layer.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,OAAO,IAAI,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { StyleSpecification } from 'mapbox-gl';
2
+ export declare const mapStyle: StyleSpecification;
3
+ //# sourceMappingURL=map-style.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-style.d.ts","sourceRoot":"","sources":["../src/map-style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGpD,eAAO,MAAM,QAAQ,EA6zBL,kBAAkB,CAAC"}