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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/bounds.d.ts +5 -0
- package/dist/bounds.d.ts.map +1 -0
- package/dist/bounds.js +9 -0
- package/dist/bounds.js.map +1 -0
- package/dist/clock.d.ts +24 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +108 -0
- package/dist/clock.js.map +1 -0
- package/dist/counter.d.ts +18 -0
- package/dist/counter.d.ts.map +1 -0
- package/dist/counter.js +54 -0
- package/dist/counter.js.map +1 -0
- package/dist/duckdb-source.d.ts +24 -0
- package/dist/duckdb-source.d.ts.map +1 -0
- package/dist/duckdb-source.js +211 -0
- package/dist/duckdb-source.js.map +1 -0
- package/dist/geo-timelapse.d.ts +8 -0
- package/dist/geo-timelapse.d.ts.map +1 -0
- package/dist/geo-timelapse.js +143 -0
- package/dist/geo-timelapse.js.map +1 -0
- package/dist/glow-layer.d.ts +47 -0
- package/dist/glow-layer.d.ts.map +1 -0
- package/dist/glow-layer.js +129 -0
- package/dist/glow-layer.js.map +1 -0
- package/dist/hooks.d.ts +32 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +142 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/map-style.d.ts +3 -0
- package/dist/map-style.d.ts.map +1 -0
- package/dist/map-style.js +832 -0
- package/dist/map-style.js.map +1 -0
- package/dist/map.d.ts +11 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +60 -0
- package/dist/map.js.map +1 -0
- package/dist/player-bar.d.ts +4 -0
- package/dist/player-bar.d.ts.map +1 -0
- package/dist/player-bar.js +118 -0
- package/dist/player-bar.js.map +1 -0
- package/dist/settings-menu.d.ts +17 -0
- package/dist/settings-menu.d.ts.map +1 -0
- package/dist/settings-menu.js +18 -0
- package/dist/settings-menu.js.map +1 -0
- package/dist/synthetic-source.d.ts +18 -0
- package/dist/synthetic-source.d.ts.map +1 -0
- package/dist/synthetic-source.js +192 -0
- package/dist/synthetic-source.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +28 -0
- package/dist/utils.js.map +1 -0
- package/package.json +67 -0
- package/src/bounds.tsx +14 -0
- package/src/clock.tsx +130 -0
- package/src/counter.tsx +96 -0
- package/src/duckdb-source.ts +238 -0
- package/src/geo-timelapse.tsx +204 -0
- package/src/glow-layer.test.ts +51 -0
- package/src/glow-layer.ts +166 -0
- package/src/hooks.ts +167 -0
- package/src/index.ts +5 -0
- package/src/map-style.ts +833 -0
- package/src/map.tsx +107 -0
- package/src/player-bar.tsx +164 -0
- package/src/settings-menu.tsx +117 -0
- package/src/synthetic-source.test.ts +58 -0
- package/src/synthetic-source.ts +218 -0
- package/src/types.ts +66 -0
- package/src/utils.test.ts +60 -0
- package/src/utils.ts +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Franck Ernewein
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# geotimelapse
|
|
2
|
+
|
|
3
|
+
Replay a day of geolocated events on a dark glowing map: deck.gl point lights
|
|
4
|
+
with fading trails over a Mapbox basemap, a waveform seek bar, running totals,
|
|
5
|
+
and a TV mode (autoplay, loop, self-hiding UI and cursor).
|
|
6
|
+
|
|
7
|
+
**[Live glow lab →](https://franckernewein.github.io/geotimelapse/)**
|
|
8
|
+
|
|
9
|
+
## Why it looks the way it does
|
|
10
|
+
|
|
11
|
+
- **Additive light, not markers** — every event is a tiny point light with a
|
|
12
|
+
gaussian halo, blended additively: density builds the brightness, and
|
|
13
|
+
hotspots bloom toward white the way long-exposure photos do.
|
|
14
|
+
- **Ground-anchored radii** — point radii are meters, not pixels, so the glow
|
|
15
|
+
reads the same at every zoom level; heavy stacks approach a pixel ceiling
|
|
16
|
+
asymptotically instead of clipping into flat discs.
|
|
17
|
+
- **Weight-scaled brightness** — co-located events are aggregated into one
|
|
18
|
+
draw whose halo amplitude follows a film-exposure response on √count.
|
|
19
|
+
- **A trail that breathes** — each frame leaves ghosts that shrink and fade
|
|
20
|
+
with age, and the trail length adapts to the achieved frame rate (sheds
|
|
21
|
+
ghosts under 24 fps, grows them back above 58).
|
|
22
|
+
- **A full player** — per-minute activity waveform as the seek bar, running
|
|
23
|
+
totals, hour axis, fullscreen TV mode.
|
|
24
|
+
- **Data-agnostic** — the component talks to a small `GeoTimelapseSource`
|
|
25
|
+
contract; bring any backend, or use the bundled duckdb-wasm and synthetic
|
|
26
|
+
sources.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install geotimelapse
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Peer dependencies: `react`, `react-dom`, `deck.gl`, `mapbox-gl`,
|
|
35
|
+
`react-map-gl`. (`@duckdb/duckdb-wasm` is a regular dependency, pulled in only
|
|
36
|
+
when you import `geotimelapse/duckdb`.)
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
'use client'; // browser-only (WebGL, workers) — in Next.js, use it from a client component
|
|
42
|
+
|
|
43
|
+
import 'mapbox-gl/dist/mapbox-gl.css';
|
|
44
|
+
|
|
45
|
+
import { GeoTimelapse } from 'geotimelapse';
|
|
46
|
+
import { createDuckDbSource } from 'geotimelapse/duckdb';
|
|
47
|
+
|
|
48
|
+
const source = createDuckDbSource({
|
|
49
|
+
parquetUrl: '/data/replay-2026-09-05.parquet',
|
|
50
|
+
// engine omitted: version-matched jsDelivr CDN. Self-hosting: pass the base
|
|
51
|
+
// URL you serve @duckdb/duckdb-wasm/dist under (the copy THIS package
|
|
52
|
+
// resolves, so the wasm matches the JS), or full DuckDBBundles.
|
|
53
|
+
valueColumn: 'spend_micros',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
<div style={{ height: '100vh' }}>
|
|
57
|
+
<GeoTimelapse
|
|
58
|
+
source={source}
|
|
59
|
+
mapboxAccessToken={token}
|
|
60
|
+
dateLabel="Sat, Sep 5, 2026"
|
|
61
|
+
timeZoneLabel="EDT"
|
|
62
|
+
formatValue={(micros) => usd.format(micros / 1e6)}
|
|
63
|
+
formatCount={(count) => `${count.toLocaleString()} wins`}
|
|
64
|
+
/>
|
|
65
|
+
</div>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The component fills its parent: give the wrapper an explicit size. The source
|
|
69
|
+
holds a dedicated worker and the full in-memory table: call `source.dispose()`
|
|
70
|
+
when you are done with it (e.g. leaving the page in an SPA).
|
|
71
|
+
|
|
72
|
+
### Data contract
|
|
73
|
+
|
|
74
|
+
`GeoTimelapseSource` is the only coupling to your data: implement `load`,
|
|
75
|
+
`frame`, `totals`, `activity` and `setScope` over any backend (see
|
|
76
|
+
`src/types.ts`). Two sources ship with the package:
|
|
77
|
+
|
|
78
|
+
- `geotimelapse/duckdb` loads a day parquet (`day_second` INT sorted, a value
|
|
79
|
+
column, `lat`/`lon` FLOAT) into an in-browser duckdb-wasm table.
|
|
80
|
+
- `geotimelapse/synthetic` generates a deterministic day in memory (gaussian
|
|
81
|
+
clusters, uniform fog and single-spot stacks) — handy for demos and benches.
|
|
82
|
+
|
|
83
|
+
### Tuning
|
|
84
|
+
|
|
85
|
+
The glow is calibrated through the `tuning` prop, merged over the defaults:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<GeoTimelapse source={source} tuning={{ pointAlpha: 50 }} ... />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`pointAlpha` is the master per-point transparency (0-255). The playback trail
|
|
92
|
+
stacks 12 additive frames, so dense datasets (thousands of points per frame)
|
|
93
|
+
need it well below the single-frame value. Other knobs: `flashRadius`,
|
|
94
|
+
`ghostMaxRadius`, `ghostMinRadius` (meters), `minRadiusPx`, `maxRadiusPx`,
|
|
95
|
+
`haloTint`, `trailFrames` — see `GlowTuning`.
|
|
96
|
+
|
|
97
|
+
### Styling
|
|
98
|
+
|
|
99
|
+
Components are styled with Tailwind CSS v4 classes. Add the package to your
|
|
100
|
+
Tailwind sources so they get generated:
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
@import 'tailwindcss';
|
|
104
|
+
@source '../node_modules/geotimelapse/dist';
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/bounds.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MapBounds } from './types.js';
|
|
2
|
+
export declare const MapBoundsProvider: import("react").Provider<MapBounds | null>;
|
|
3
|
+
/** The map's current viewport bounds; null until the map has loaded. */
|
|
4
|
+
export declare function useMapBounds(): MapBounds | null;
|
|
5
|
+
//# sourceMappingURL=bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../src/bounds.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,eAAO,MAAM,iBAAiB,4CAAyB,CAAC;AAExD,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,SAAS,GAAG,IAAI,CAE/C"}
|
package/dist/bounds.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
const BoundsContext = createContext(null);
|
|
4
|
+
export const MapBoundsProvider = BoundsContext.Provider;
|
|
5
|
+
/** The map's current viewport bounds; null until the map has loaded. */
|
|
6
|
+
export function useMapBounds() {
|
|
7
|
+
return useContext(BoundsContext);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds.js","sourceRoot":"","sources":["../src/bounds.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAIlD,MAAM,aAAa,GAAG,aAAa,CAAmB,IAAI,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC,QAAQ,CAAC;AAExD,wEAAwE;AACxE,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC"}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PropsWithChildren } from 'react';
|
|
2
|
+
export declare const DAY_SECONDS: number;
|
|
3
|
+
export interface TimelapseClock {
|
|
4
|
+
play: () => void;
|
|
5
|
+
pause: () => void;
|
|
6
|
+
toggle: () => void;
|
|
7
|
+
seek: (daySeconds: number) => void;
|
|
8
|
+
/** Seconds elapsed since the start of the replayed day, in [0, DAY_SECONDS]. */
|
|
9
|
+
getDaySeconds: () => number;
|
|
10
|
+
/** Increments on every time jump (seek, end-of-day restart): consumers use it to invalidate history. */
|
|
11
|
+
getEpoch: () => number;
|
|
12
|
+
isPlaying: () => boolean;
|
|
13
|
+
/** Notified up to MAX_TICKS_PER_SECOND times while playing, and on play/pause/seek. */
|
|
14
|
+
subscribe: (listener: () => void) => () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function TimelapseClockProvider({ children }: PropsWithChildren): import("react").JSX.Element;
|
|
17
|
+
export declare function useTimelapseClock(): TimelapseClock;
|
|
18
|
+
export declare function useIsPlaying(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Current day time, quantized: the component only re-renders when the value
|
|
21
|
+
* crosses a quantum boundary, so each consumer picks its own refresh rate.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useDaySeconds(quantum?: number): number;
|
|
24
|
+
//# sourceMappingURL=clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAG/C,eAAO,MAAM,WAAW,QAAe,CAAC;AAcxC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,gFAAgF;IAChF,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,wGAAwG;IACxG,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,uFAAuF;IACvF,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;CACjD;AAyED,wBAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAIrE;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAGtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAI,GAAG,MAAM,CAIjD"}
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useState, useSyncExternalStore } from 'react';
|
|
4
|
+
export const DAY_SECONDS = 24 * 60 * 60;
|
|
5
|
+
// Wall-clock duration of a full-day replay.
|
|
6
|
+
const PLAYBACK_SECONDS = 180;
|
|
7
|
+
const RATE = DAY_SECONDS / PLAYBACK_SECONDS;
|
|
8
|
+
// Ticks are capped below the display refresh rate: extra updates are invisible
|
|
9
|
+
// and would only waste re-renders on high-refresh screens.
|
|
10
|
+
const MAX_TICKS_PER_SECOND = 24;
|
|
11
|
+
const MIN_TICK_MS = 1000 / MAX_TICKS_PER_SECOND;
|
|
12
|
+
// Bounds on the wall-clock delta applied per tick. The upper bound keeps a
|
|
13
|
+
// hidden tab (rAF suspended) or a main-thread stall from fast-forwarding the
|
|
14
|
+
// replay; the lower bound guards against the first rAF timestamp predating the
|
|
15
|
+
// performance.now() sampled in play() (vsync time is stamped before input).
|
|
16
|
+
const MAX_TICK_MS = 100;
|
|
17
|
+
function createClock() {
|
|
18
|
+
let daySeconds = 0;
|
|
19
|
+
let epoch = 0;
|
|
20
|
+
let playing = false;
|
|
21
|
+
let rafId = 0;
|
|
22
|
+
let lastTick = 0;
|
|
23
|
+
// Next tick due date, advanced on its own grid so the effective rate stays
|
|
24
|
+
// at the cap instead of snapping down to a divisor of the refresh rate.
|
|
25
|
+
let nextTickAt = 0;
|
|
26
|
+
const listeners = new Set();
|
|
27
|
+
const notify = () => listeners.forEach((listener) => listener());
|
|
28
|
+
const tick = (now) => {
|
|
29
|
+
if (now < nextTickAt) {
|
|
30
|
+
rafId = requestAnimationFrame(tick);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
nextTickAt = Math.max(nextTickAt + MIN_TICK_MS, now);
|
|
34
|
+
daySeconds += (Math.min(Math.max(now - lastTick, 0), MAX_TICK_MS) / 1000) * RATE;
|
|
35
|
+
lastTick = now;
|
|
36
|
+
if (daySeconds >= DAY_SECONDS) {
|
|
37
|
+
daySeconds = DAY_SECONDS;
|
|
38
|
+
playing = false;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
rafId = requestAnimationFrame(tick);
|
|
42
|
+
}
|
|
43
|
+
notify();
|
|
44
|
+
};
|
|
45
|
+
const play = () => {
|
|
46
|
+
if (playing)
|
|
47
|
+
return;
|
|
48
|
+
if (daySeconds >= DAY_SECONDS) {
|
|
49
|
+
daySeconds = 0;
|
|
50
|
+
epoch += 1;
|
|
51
|
+
}
|
|
52
|
+
playing = true;
|
|
53
|
+
lastTick = performance.now();
|
|
54
|
+
nextTickAt = lastTick;
|
|
55
|
+
rafId = requestAnimationFrame(tick);
|
|
56
|
+
notify();
|
|
57
|
+
};
|
|
58
|
+
const pause = () => {
|
|
59
|
+
if (!playing)
|
|
60
|
+
return;
|
|
61
|
+
playing = false;
|
|
62
|
+
cancelAnimationFrame(rafId);
|
|
63
|
+
notify();
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
play,
|
|
67
|
+
pause,
|
|
68
|
+
toggle: () => (playing ? pause() : play()),
|
|
69
|
+
seek: (value) => {
|
|
70
|
+
daySeconds = Math.min(Math.max(value, 0), DAY_SECONDS);
|
|
71
|
+
epoch += 1;
|
|
72
|
+
notify();
|
|
73
|
+
},
|
|
74
|
+
getDaySeconds: () => daySeconds,
|
|
75
|
+
getEpoch: () => epoch,
|
|
76
|
+
isPlaying: () => playing,
|
|
77
|
+
subscribe: (listener) => {
|
|
78
|
+
listeners.add(listener);
|
|
79
|
+
return () => listeners.delete(listener);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const ClockContext = createContext(null);
|
|
84
|
+
export function TimelapseClockProvider({ children }) {
|
|
85
|
+
const [clock] = useState(createClock);
|
|
86
|
+
useEffect(() => () => clock.pause(), [clock]);
|
|
87
|
+
return _jsx(ClockContext.Provider, { value: clock, children: children });
|
|
88
|
+
}
|
|
89
|
+
export function useTimelapseClock() {
|
|
90
|
+
const clock = useContext(ClockContext);
|
|
91
|
+
if (!clock)
|
|
92
|
+
throw new Error('useTimelapseClock must be used within a TimelapseClockProvider');
|
|
93
|
+
return clock;
|
|
94
|
+
}
|
|
95
|
+
export function useIsPlaying() {
|
|
96
|
+
const clock = useTimelapseClock();
|
|
97
|
+
return useSyncExternalStore(clock.subscribe, clock.isPlaying, () => false);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Current day time, quantized: the component only re-renders when the value
|
|
101
|
+
* crosses a quantum boundary, so each consumer picks its own refresh rate.
|
|
102
|
+
*/
|
|
103
|
+
export function useDaySeconds(quantum = 1) {
|
|
104
|
+
const clock = useTimelapseClock();
|
|
105
|
+
const getSnapshot = useCallback(() => Math.floor(clock.getDaySeconds() / quantum) * quantum, [clock, quantum]);
|
|
106
|
+
return useSyncExternalStore(clock.subscribe, getSnapshot, () => 0);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GeoTimelapseSource } from './types.js';
|
|
2
|
+
interface CounterProps {
|
|
3
|
+
source: GeoTimelapseSource;
|
|
4
|
+
ready: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
progress: {
|
|
7
|
+
loadedBytes: number;
|
|
8
|
+
totalBytes: number;
|
|
9
|
+
} | null;
|
|
10
|
+
scopeVersion: number;
|
|
11
|
+
dateLabel?: string;
|
|
12
|
+
timeZoneLabel?: string;
|
|
13
|
+
formatValue: (value: number) => string;
|
|
14
|
+
formatCount: (count: number) => string;
|
|
15
|
+
}
|
|
16
|
+
export default function Counter({ source, ready, error, progress, scopeVersion, dateLabel, timeZoneLabel, formatValue, formatCount, }: CounterProps): import("react").JSX.Element;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=counter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAU,MAAM,YAAY,CAAC;AAG7D,UAAU,YAAY;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,EAC9B,MAAM,EACN,KAAK,EACL,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,aAAa,EACb,WAAW,EACX,WAAW,GACZ,EAAE,YAAY,+BAiEd"}
|
package/dist/counter.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useDaySeconds, useTimelapseClock } from './clock.js';
|
|
5
|
+
import { formatDayTime12 } from './utils.js';
|
|
6
|
+
export default function Counter({ source, ready, error, progress, scopeVersion, dateLabel, timeZoneLabel, formatValue, formatCount, }) {
|
|
7
|
+
const clock = useTimelapseClock();
|
|
8
|
+
const daySeconds = useDaySeconds(60);
|
|
9
|
+
const [totals, setTotals] = useState(null);
|
|
10
|
+
const [queryError, setQueryError] = useState(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!ready)
|
|
13
|
+
return;
|
|
14
|
+
let disposed = false;
|
|
15
|
+
let inFlight = false;
|
|
16
|
+
let lastQueried = -1;
|
|
17
|
+
// One query at a time: each tick tries, and is skipped while the previous
|
|
18
|
+
// query is still running, so the counter refreshes as fast as the source
|
|
19
|
+
// can answer and never queues up.
|
|
20
|
+
const query = async () => {
|
|
21
|
+
const second = Math.floor(clock.getDaySeconds());
|
|
22
|
+
if (inFlight || second === lastQueried)
|
|
23
|
+
return;
|
|
24
|
+
inFlight = true;
|
|
25
|
+
try {
|
|
26
|
+
const result = await source.totals(second);
|
|
27
|
+
if (disposed)
|
|
28
|
+
return;
|
|
29
|
+
lastQueried = second;
|
|
30
|
+
setQueryError(null);
|
|
31
|
+
setTotals(result);
|
|
32
|
+
}
|
|
33
|
+
catch (totalsError) {
|
|
34
|
+
if (!disposed)
|
|
35
|
+
setQueryError(totalsError instanceof Error ? totalsError.message : String(totalsError));
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
inFlight = false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
void query();
|
|
42
|
+
const unsubscribe = clock.subscribe(() => void query());
|
|
43
|
+
return () => {
|
|
44
|
+
disposed = true;
|
|
45
|
+
unsubscribe();
|
|
46
|
+
};
|
|
47
|
+
// ready and scopeVersion re-run the effect so a re-scoped source is re-read.
|
|
48
|
+
// oxlint-disable-next-line react/exhaustive-effect-dependencies
|
|
49
|
+
}, [clock, source, ready, scopeVersion]);
|
|
50
|
+
return (_jsx("div", { className: "absolute top-4 right-4 h-24 w-64 rounded-lg bg-black/60 px-4 py-3 text-right font-mono text-white backdrop-blur-sm", children: (error ?? queryError) ? (_jsx("div", { className: "max-w-xs overflow-hidden text-xs text-red-400", children: error ?? queryError })) : ready && totals ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "text-xs text-white/50 tabular-nums", children: [dateLabel && `${dateLabel}, `, formatDayTime12(daySeconds), timeZoneLabel && ` ${timeZoneLabel}`] }), _jsx("div", { className: "text-2xl tabular-nums", children: formatValue(totals.value) }), _jsx("div", { className: "text-xs text-white/70 tabular-nums", children: formatCount(totals.count) })] })) : (_jsx("div", { className: "text-xs text-white/70 tabular-nums", children: progress
|
|
51
|
+
? `loading day… ${Math.round(progress.loadedBytes / 1e6)}/${Math.round(progress.totalBytes / 1e6)} MB`
|
|
52
|
+
: 'loading…' })) }));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=counter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
2
|
+
import type { GeoTimelapseSource } from './types.js';
|
|
3
|
+
export interface DuckDbSourceOptions {
|
|
4
|
+
/** URL of the day's parquet: day_second INT (seconds since the day's
|
|
5
|
+
* midnight, sorted), a value column, lat/lon FLOAT. ~60M rows is fine. */
|
|
6
|
+
parquetUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* Where the duckdb-wasm engine files come from. Omitted: version-matched
|
|
9
|
+
* jsDelivr URLs (zero config, external CDN). A string: the base URL you
|
|
10
|
+
* serve `@duckdb/duckdb-wasm/dist` under — serve the copy THIS package
|
|
11
|
+
* resolves, so the wasm matches the JS. An object: full bundle control.
|
|
12
|
+
*/
|
|
13
|
+
engine?: string | duckdb.DuckDBBundles;
|
|
14
|
+
/** Name of the parquet column summed into totals().value. */
|
|
15
|
+
valueColumn?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A GeoTimelapseSource backed by duckdb-wasm: the parquet is downloaded once
|
|
19
|
+
* and loaded into an in-browser table, then every read is a local query.
|
|
20
|
+
* Each source owns its own engine instance (worker), so two components never
|
|
21
|
+
* share state — at the price of one table copy per instance.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createDuckDbSource({ parquetUrl, engine, valueColumn, }: DuckDbSourceOptions): GeoTimelapseSource;
|
|
24
|
+
//# sourceMappingURL=duckdb-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duckdb-source.d.ts","sourceRoot":"","sources":["../src/duckdb-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,qBAAqB,CAAC;AAE9C,OAAO,KAAK,EAAe,kBAAkB,EAAqB,MAAM,YAAY,CAAC;AAErF,MAAM,WAAW,mBAAmB;IAClC;+EAC2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;IACvC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAoBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,UAAU,EACV,MAAM,EACN,WAAqB,GACtB,EAAE,mBAAmB,GAAG,kBAAkB,CA8L1C"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
2
|
+
function resolveBundles(engine) {
|
|
3
|
+
if (!engine)
|
|
4
|
+
return duckdb.getJsDelivrBundles();
|
|
5
|
+
if (typeof engine === 'string') {
|
|
6
|
+
const base = engine.replace(/\/$/, '');
|
|
7
|
+
return {
|
|
8
|
+
mvp: {
|
|
9
|
+
mainModule: `${base}/duckdb-mvp.wasm`,
|
|
10
|
+
mainWorker: `${base}/duckdb-browser-mvp.worker.js`,
|
|
11
|
+
},
|
|
12
|
+
eh: {
|
|
13
|
+
mainModule: `${base}/duckdb-eh.wasm`,
|
|
14
|
+
mainWorker: `${base}/duckdb-browser-eh.worker.js`,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return engine;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A GeoTimelapseSource backed by duckdb-wasm: the parquet is downloaded once
|
|
22
|
+
* and loaded into an in-browser table, then every read is a local query.
|
|
23
|
+
* Each source owns its own engine instance (worker), so two components never
|
|
24
|
+
* share state — at the price of one table copy per instance.
|
|
25
|
+
*/
|
|
26
|
+
export function createDuckDbSource({ parquetUrl, engine, valueColumn = 'value', }) {
|
|
27
|
+
let dbPromise = null;
|
|
28
|
+
let connPromise = null;
|
|
29
|
+
let loadPromise = null;
|
|
30
|
+
// Mutable slot so a load() call after the first (e.g. a React StrictMode
|
|
31
|
+
// remount) still gets progress reports from the single in-flight download.
|
|
32
|
+
let progressListener;
|
|
33
|
+
let scopeFilter = 'TRUE';
|
|
34
|
+
let requestedFilter = 'TRUE';
|
|
35
|
+
let scopeChain = Promise.resolve();
|
|
36
|
+
const getDB = () => (dbPromise ??= (async () => {
|
|
37
|
+
const bundle = await duckdb.selectBundle(resolveBundles(engine));
|
|
38
|
+
const workerUrl = bundle.mainWorker;
|
|
39
|
+
// A same-origin worker loads directly (and its requests carry the
|
|
40
|
+
// page's cookies); cross-origin URLs (the jsDelivr default) need
|
|
41
|
+
// createWorker's same-origin blob wrapper or the browser blocks them.
|
|
42
|
+
const sameOrigin = new URL(workerUrl, globalThis.location.href).origin === globalThis.location.origin;
|
|
43
|
+
const worker = sameOrigin ? new Worker(workerUrl) : await duckdb.createWorker(workerUrl);
|
|
44
|
+
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(duckdb.LogLevel.WARNING), worker);
|
|
45
|
+
await db.instantiate(bundle.mainModule);
|
|
46
|
+
return db;
|
|
47
|
+
})());
|
|
48
|
+
const getConn = () => (connPromise ??= getDB().then((db) => db.connect()));
|
|
49
|
+
// Running totals per second, so counters read one row per tick.
|
|
50
|
+
const CREATE_TOTALS = `
|
|
51
|
+
CREATE OR REPLACE TABLE totals_by_second AS
|
|
52
|
+
SELECT day_second,
|
|
53
|
+
sum(events) OVER w AS events,
|
|
54
|
+
sum(value) OVER w AS value
|
|
55
|
+
FROM scope_seconds
|
|
56
|
+
WINDOW w AS (ORDER BY day_second)
|
|
57
|
+
`;
|
|
58
|
+
const load = (onProgress) => {
|
|
59
|
+
if (onProgress)
|
|
60
|
+
progressListener = onProgress;
|
|
61
|
+
return (loadPromise ??= (async () => {
|
|
62
|
+
const response = await fetch(parquetUrl);
|
|
63
|
+
if (!response.ok || !response.body)
|
|
64
|
+
throw new Error(`parquet download failed (${response.status})`);
|
|
65
|
+
const totalBytes = Number(response.headers.get('content-length') ?? 0);
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let loadedBytes = 0;
|
|
68
|
+
let lastReported = 0;
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
for (;;) {
|
|
71
|
+
const { done, value } = await reader.read();
|
|
72
|
+
if (done)
|
|
73
|
+
break;
|
|
74
|
+
chunks.push(value);
|
|
75
|
+
loadedBytes += value.byteLength;
|
|
76
|
+
if (loadedBytes - lastReported >= 8 * 1024 * 1024) {
|
|
77
|
+
lastReported = loadedBytes;
|
|
78
|
+
progressListener?.(loadedBytes, totalBytes);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const buffer = new Uint8Array(loadedBytes);
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (const chunk of chunks) {
|
|
84
|
+
buffer.set(chunk, offset);
|
|
85
|
+
offset += chunk.byteLength;
|
|
86
|
+
}
|
|
87
|
+
progressListener?.(loadedBytes, totalBytes);
|
|
88
|
+
const db = await getDB();
|
|
89
|
+
await db.registerFileBuffer('replay.parquet', buffer);
|
|
90
|
+
const conn = await getConn();
|
|
91
|
+
// The value column is normalized at ingest so every later query is
|
|
92
|
+
// schema-agnostic. The source order (sorted by day_second) is what
|
|
93
|
+
// gives the in-memory table its zonemap pruning.
|
|
94
|
+
await conn.query(`
|
|
95
|
+
CREATE OR REPLACE TABLE events AS
|
|
96
|
+
SELECT day_second, ${valueColumn} AS value, lat, lon
|
|
97
|
+
FROM read_parquet('replay.parquet')
|
|
98
|
+
`);
|
|
99
|
+
// Per-second counts within the current scope, the single base both
|
|
100
|
+
// aggregates derive from — so consumers never rescan the raw rows.
|
|
101
|
+
await conn.query(`
|
|
102
|
+
CREATE OR REPLACE TABLE scope_seconds AS
|
|
103
|
+
SELECT day_second, count(*) AS events, sum(value) AS value
|
|
104
|
+
FROM events
|
|
105
|
+
GROUP BY day_second
|
|
106
|
+
`);
|
|
107
|
+
await conn.query(CREATE_TOTALS);
|
|
108
|
+
await db.dropFile('replay.parquet');
|
|
109
|
+
})());
|
|
110
|
+
};
|
|
111
|
+
const frame = async (fromSecond, toSecond) => {
|
|
112
|
+
const conn = await getConn();
|
|
113
|
+
// Integer bounds keep the comparison on the INTEGER column: a double
|
|
114
|
+
// literal would disable zonemap pruning and full-scan the table.
|
|
115
|
+
const result = await conn.query(`
|
|
116
|
+
SELECT lon, lat, count(*)::DOUBLE AS weight FROM events
|
|
117
|
+
WHERE day_second >= ${Math.floor(fromSecond)} AND day_second < ${Math.floor(toSecond)}
|
|
118
|
+
AND lat IS NOT NULL AND lon IS NOT NULL
|
|
119
|
+
GROUP BY lon, lat
|
|
120
|
+
`);
|
|
121
|
+
const lon = result.getChild('lon').toArray();
|
|
122
|
+
const lat = result.getChild('lat').toArray();
|
|
123
|
+
const weight = result.getChild('weight').toArray();
|
|
124
|
+
const positions = new Float32Array(lon.length * 2);
|
|
125
|
+
const weights = new Float32Array(lon.length);
|
|
126
|
+
for (let i = 0; i < lon.length; i++) {
|
|
127
|
+
positions[2 * i] = lon[i];
|
|
128
|
+
positions[2 * i + 1] = lat[i];
|
|
129
|
+
weights[i] = weight[i];
|
|
130
|
+
}
|
|
131
|
+
return { positions, weights, count: lon.length };
|
|
132
|
+
};
|
|
133
|
+
const totals = async (second) => {
|
|
134
|
+
const conn = await getConn();
|
|
135
|
+
const result = await conn.query(`
|
|
136
|
+
SELECT events::DOUBLE AS events, value::DOUBLE AS value
|
|
137
|
+
FROM totals_by_second
|
|
138
|
+
WHERE day_second < ${Math.floor(second)}
|
|
139
|
+
ORDER BY day_second DESC
|
|
140
|
+
LIMIT 1
|
|
141
|
+
`);
|
|
142
|
+
const row = result.numRows > 0 ? result.get(0) : null;
|
|
143
|
+
return { count: Number(row?.events ?? 0), value: Number(row?.value ?? 0) };
|
|
144
|
+
};
|
|
145
|
+
const activity = async () => {
|
|
146
|
+
const conn = await getConn();
|
|
147
|
+
const result = await conn.query(`
|
|
148
|
+
SELECT (day_second // 60)::INT AS minute, sum(events)::DOUBLE AS events
|
|
149
|
+
FROM scope_seconds
|
|
150
|
+
GROUP BY minute
|
|
151
|
+
`);
|
|
152
|
+
const minutes = result.getChild('minute').toArray();
|
|
153
|
+
const events = result.getChild('events').toArray();
|
|
154
|
+
const counts = new Float32Array(24 * 60);
|
|
155
|
+
for (let i = 0; i < minutes.length; i++)
|
|
156
|
+
counts[minutes[i]] = events[i];
|
|
157
|
+
return counts;
|
|
158
|
+
};
|
|
159
|
+
// The scan over the raw rows is chunked hour by hour (pruned on
|
|
160
|
+
// day_second) so playback tick queries interleave instead of queueing
|
|
161
|
+
// behind one long statement.
|
|
162
|
+
const setScope = (bounds) => {
|
|
163
|
+
const filter = bounds
|
|
164
|
+
? `lat BETWEEN ${bounds.south} AND ${bounds.north} AND lon BETWEEN ${bounds.west} AND ${bounds.east}`
|
|
165
|
+
: 'TRUE';
|
|
166
|
+
requestedFilter = filter;
|
|
167
|
+
scopeChain = scopeChain
|
|
168
|
+
.then(async () => {
|
|
169
|
+
// Superseded while queued, or already active.
|
|
170
|
+
if (requestedFilter !== filter || filter === scopeFilter)
|
|
171
|
+
return;
|
|
172
|
+
// The rebuild reads the events table: wait for the day to be loaded.
|
|
173
|
+
await load();
|
|
174
|
+
const conn = await getConn();
|
|
175
|
+
await conn.query(`
|
|
176
|
+
CREATE OR REPLACE TABLE scope_seconds_build AS
|
|
177
|
+
SELECT day_second, count(*) AS events, sum(value) AS value
|
|
178
|
+
FROM events WHERE false GROUP BY day_second
|
|
179
|
+
`);
|
|
180
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
181
|
+
if (requestedFilter !== filter)
|
|
182
|
+
return;
|
|
183
|
+
await conn.query(`
|
|
184
|
+
INSERT INTO scope_seconds_build
|
|
185
|
+
SELECT day_second, count(*), sum(value)
|
|
186
|
+
FROM events
|
|
187
|
+
WHERE day_second >= ${hour * 3600} AND day_second < ${(hour + 1) * 3600} AND ${filter}
|
|
188
|
+
GROUP BY day_second
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
await conn.query(`CREATE OR REPLACE TABLE scope_seconds AS FROM scope_seconds_build`);
|
|
192
|
+
await conn.query(`DROP TABLE scope_seconds_build`);
|
|
193
|
+
await conn.query(CREATE_TOTALS);
|
|
194
|
+
scopeFilter = filter;
|
|
195
|
+
})
|
|
196
|
+
// A failed rebuild keeps the previous scope; never poison the chain.
|
|
197
|
+
.catch((error) => console.error('geotimelapse scope rebuild failed', error));
|
|
198
|
+
return scopeChain;
|
|
199
|
+
};
|
|
200
|
+
const dispose = async () => {
|
|
201
|
+
const conn = connPromise ? await connPromise.catch(() => null) : null;
|
|
202
|
+
connPromise = null;
|
|
203
|
+
await conn?.close().catch(() => { });
|
|
204
|
+
const db = dbPromise ? await dbPromise.catch(() => null) : null;
|
|
205
|
+
dbPromise = null;
|
|
206
|
+
loadPromise = null;
|
|
207
|
+
await db?.terminate().catch(() => { });
|
|
208
|
+
};
|
|
209
|
+
return { load, frame, totals, activity, setScope, dispose };
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=duckdb-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duckdb-source.js","sourceRoot":"","sources":["../src/duckdb-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,qBAAqB,CAAC;AAmB9C,SAAS,cAAc,CAAC,MAAqC;IAC3D,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,CAAC,kBAAkB,EAAE,CAAC;IAChD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACvC,OAAO;YACL,GAAG,EAAE;gBACH,UAAU,EAAE,GAAG,IAAI,kBAAkB;gBACrC,UAAU,EAAE,GAAG,IAAI,+BAA+B;aACnD;YACD,EAAE,EAAE;gBACF,UAAU,EAAE,GAAG,IAAI,iBAAiB;gBACpC,UAAU,EAAE,GAAG,IAAI,8BAA8B;aAClD;SACF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,UAAU,EACV,MAAM,EACN,WAAW,GAAG,OAAO,GACD;IACpB,IAAI,SAAS,GAAuC,IAAI,CAAC;IACzD,IAAI,WAAW,GAAiD,IAAI,CAAC;IACrE,IAAI,WAAW,GAAyB,IAAI,CAAC;IAC7C,yEAAyE;IACzE,2EAA2E;IAC3E,IAAI,gBAAiF,CAAC;IACtF,IAAI,WAAW,GAAG,MAAM,CAAC;IACzB,IAAI,eAAe,GAAG,MAAM,CAAC;IAC7B,IAAI,UAAU,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAElD,MAAM,KAAK,GAAG,GAAG,EAAE,CACjB,CAAC,SAAS,KAAK,CAAC,KAAK,IAAI,EAAE;QACzB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;QACjE,MAAM,SAAS,GAAG,MAAM,CAAC,UAAW,CAAC;QACrC,kEAAkE;QAClE,iEAAiE;QACjE,sEAAsE;QACtE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtG,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QACzF,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;QAC7F,MAAM,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACxC,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,EAAE,CAAC,CAAC;IAER,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC,WAAW,KAAK,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAE3E,gEAAgE;IAChE,MAAM,aAAa,GAAG;;;;;;;GAOrB,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,UAA8D,EAAE,EAAE;QAC9E,IAAI,UAAU;YAAE,gBAAgB,GAAG,UAAU,CAAC;QAC9C,OAAO,CAAC,WAAW,KAAK,CAAC,KAAK,IAAI,EAAE;YAClC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,CAAC;YACzC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACpG,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;YACvE,MAAM,MAAM,GAAiB,EAAE,CAAC;YAChC,IAAI,WAAW,GAAG,CAAC,CAAC;YACpB,IAAI,YAAY,GAAG,CAAC,CAAC;YACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACzC,SAAS,CAAC;gBACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC5C,IAAI,IAAI;oBAAE,MAAM;gBAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,WAAW,IAAI,KAAK,CAAC,UAAU,CAAC;gBAChC,IAAI,WAAW,GAAG,YAAY,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;oBAClD,YAAY,GAAG,WAAW,CAAC;oBAC3B,gBAAgB,EAAE,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;gBAC9C,CAAC;YACH,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;YAC3C,IAAI,MAAM,GAAG,CAAC,CAAC;YACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;gBAC1B,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;YAC7B,CAAC;YACD,gBAAgB,EAAE,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;YAE5C,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,CAAC;YACzB,MAAM,EAAE,CAAC,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;YACtD,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;YAC7B,mEAAmE;YACnE,mEAAmE;YACnE,iDAAiD;YACjD,MAAM,IAAI,CAAC,KAAK,CAAC;;6BAEM,WAAW;;OAEjC,CAAC,CAAC;YACH,mEAAmE;YACnE,mEAAmE;YACnE,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;OAKhB,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAChC,MAAM,EAAE,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC;QACtC,CAAC,CAAC,EAAE,CAAC,CAAC;IACR,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,KAAK,EAAE,UAAkB,EAAE,QAAgB,EAAwB,EAAE;QACjF,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,qEAAqE;QACrE,iEAAiE;QACjE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;;4BAER,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,qBAAqB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;;;KAGtF,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAE,CAAC,OAAO,EAAkB,CAAC;QAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAE,CAAC,OAAO,EAAkB,CAAC;QAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAE,CAAC,OAAO,EAAkB,CAAC;QACpE,MAAM,SAAS,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1B,SAAS,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAC9B,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IACnD,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,KAAK,EAAE,MAAc,EAAmB,EAAE;QACvD,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;;;2BAGT,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;;;KAGxC,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC;IAC7E,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,KAAK,IAA2B,EAAE;QACjD,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;;;;KAI/B,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAE,CAAC,OAAO,EAAgB,CAAC;QACnE,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAE,CAAC,OAAO,EAAkB,CAAC;QACpE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxE,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;IAEF,gEAAgE;IAChE,sEAAsE;IACtE,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,CAAC,MAAwB,EAAiB,EAAE;QAC3D,MAAM,MAAM,GAAG,MAAM;YACnB,CAAC,CAAC,eAAe,MAAM,CAAC,KAAK,QAAQ,MAAM,CAAC,KAAK,oBAAoB,MAAM,CAAC,IAAI,QAAQ,MAAM,CAAC,IAAI,EAAE;YACrG,CAAC,CAAC,MAAM,CAAC;QACX,eAAe,GAAG,MAAM,CAAC;QACzB,UAAU,GAAG,UAAU;aACpB,IAAI,CAAC,KAAK,IAAI,EAAE;YACf,8CAA8C;YAC9C,IAAI,eAAe,KAAK,MAAM,IAAI,MAAM,KAAK,WAAW;gBAAE,OAAO;YACjE,qEAAqE;YACrE,MAAM,IAAI,EAAE,CAAC;YACb,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;YAC7B,MAAM,IAAI,CAAC,KAAK,CAAC;;;;SAIhB,CAAC,CAAC;YACH,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;gBACrC,IAAI,eAAe,KAAK,MAAM;oBAAE,OAAO;gBACvC,MAAM,IAAI,CAAC,KAAK,CAAC;;;;kCAIO,IAAI,GAAG,IAAI,qBAAqB,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,QAAQ,MAAM;;WAEtF,CAAC,CAAC;YACL,CAAC;YACD,MAAM,IAAI,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;YACtF,MAAM,IAAI,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;YACnD,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAChC,WAAW,GAAG,MAAM,CAAC;QACvB,CAAC,CAAC;YACF,qEAAqE;aACpE,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC,CAAC;QACxF,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,WAAW,GAAG,IAAI,CAAC;QACnB,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACpC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAChE,SAAS,GAAG,IAAI,CAAC;QACjB,WAAW,GAAG,IAAI,CAAC;QACnB,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,CAAC,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GeoTimelapseProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A full-day replay of geolocated events: dark glowing map, waveform seek
|
|
4
|
+
* bar, running totals, and a TV mode (autoplay, loop, self-hiding UI).
|
|
5
|
+
* Fills its parent — give the wrapping element an explicit size.
|
|
6
|
+
*/
|
|
7
|
+
export declare function GeoTimelapse(props: GeoTimelapseProps): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=geo-timelapse.d.ts.map
|