-
+ {!onMapPage && (
+
+ )}
0.8) {
- ctx.fillStyle = `rgba(0,140,130,${(t - 0.8) * 0.5})`;
+ ctx.fillStyle = isLight
+ ? `rgba(0,140,130,${(t - 0.8) * 0.5})`
+ : `rgba(82,212,200,${(t - 0.8) * 0.45})`;
ctx.beginPath();
ctx.arc(x, y, r * 2.6, 0, 6.283);
ctx.fill();
diff --git a/src/lib/map/geogrid.ts b/src/lib/map/geogrid.ts
new file mode 100644
index 0000000..36bbe40
--- /dev/null
+++ b/src/lib/map/geogrid.ts
@@ -0,0 +1,41 @@
+import { ZARR_STORE } from "@/lib/constants/store";
+import type { GeoPoint, GridCell } from "@/types/map";
+
+const { grid, dimensions } = ZARR_STORE;
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, value));
+}
+
+function snapToAxis(value: number, start: number, step: number, count: number): {
+ index: number;
+ coordinate: number;
+} {
+ const index = clamp(Math.round((value - start) / step), 0, count - 1);
+ return {
+ index,
+ coordinate: start + index * step,
+ };
+}
+
+/** Snap a WGS-84 point to the nearest ESDC 2.5° grid cell. */
+export function geoPointToZarrGrid(point: GeoPoint): GridCell {
+ const lon = snapToAxis(point.lon, grid.lonStart, grid.lonStep, dimensions.lon);
+ const lat = snapToAxis(point.lat, grid.latStart, grid.latStep, dimensions.lat);
+
+ return {
+ lon: lon.coordinate,
+ lat: lat.coordinate,
+ lonIndex: lon.index,
+ latIndex: lat.index,
+ };
+}
+
+export function formatCoordinate(value: number, digits = 3): string {
+ const direction = value >= 0 ? "" : "-";
+ return `${direction}${Math.abs(value).toFixed(digits)}°`;
+}
+
+export function formatGeoPoint(point: GeoPoint, digits = 3): string {
+ return `${formatCoordinate(point.lon, digits)}, ${formatCoordinate(point.lat, digits)}`;
+}
diff --git a/src/lib/map/viewState.ts b/src/lib/map/viewState.ts
new file mode 100644
index 0000000..3a92fd9
--- /dev/null
+++ b/src/lib/map/viewState.ts
@@ -0,0 +1,14 @@
+import type { MapViewState } from "@/types/map";
+
+export const DEFAULT_MAP_VIEW: MapViewState = {
+ longitude: 10,
+ latitude: 30,
+ zoom: 1.4,
+ bearing: 0,
+ pitch: 0,
+};
+
+export const MAP_BASE_STYLES = {
+ dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
+ light: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
+} as const;
diff --git a/src/lib/zarr/store.ts b/src/lib/zarr/store.ts
new file mode 100644
index 0000000..7968058
--- /dev/null
+++ b/src/lib/zarr/store.ts
@@ -0,0 +1,31 @@
+import * as zarr from "zarrita";
+import { ZARR_STORE } from "@/lib/constants/store";
+import type { GridCell } from "@/types/map";
+
+export type ZarrStore = Awaited>;
+
+export async function openZarrStore(url = ZARR_STORE.url) {
+ const raw = new zarr.FetchStore(url);
+ const store = await zarr.withConsolidatedMetadata(raw);
+ return {
+ store,
+ root: zarr.root(store),
+ };
+}
+
+export async function fetchZarrTimeSeries(
+ ds: ZarrStore,
+ grid: GridCell,
+ variable = ZARR_STORE.defaultVariable,
+): Promise<{ values: Float32Array; variable: string; units?: string }> {
+ const array = await zarr.open(ds.root.resolve(variable), { kind: "array" });
+ const result = await zarr.get(array, [null, null, grid.latIndex, grid.lonIndex]); /* days, hours, lat, lon */
+ const units =
+ typeof array.attrs.units === "string" ? array.attrs.units : undefined;
+
+ return {
+ values: result.data as Float32Array,
+ variable,
+ units,
+ };
+}
diff --git a/src/types/map.ts b/src/types/map.ts
new file mode 100644
index 0000000..ec55036
--- /dev/null
+++ b/src/types/map.ts
@@ -0,0 +1,22 @@
+export type GeoPoint = {
+ lon: number;
+ lat: number;
+};
+
+export type GridCell = GeoPoint & {
+ lonIndex: number;
+ latIndex: number;
+};
+
+export type MapSelection = {
+ click: GeoPoint;
+ grid: GridCell;
+};
+
+export type MapViewState = {
+ longitude: number;
+ latitude: number;
+ zoom: number;
+ bearing?: number;
+ pitch?: number;
+};