@oceanmetrics/engine 0.0.0-stage → 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 (51) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +192 -2
  4. package/dist/cells.d.ts +84 -0
  5. package/dist/cells.js +179 -0
  6. package/dist/chunks.d.ts +29 -0
  7. package/dist/chunks.js +49 -0
  8. package/dist/dataset.d.ts +72 -0
  9. package/dist/dataset.js +115 -0
  10. package/dist/engine.d.ts +72 -0
  11. package/dist/engine.js +154 -0
  12. package/dist/erddap.d.ts +117 -0
  13. package/dist/erddap.js +224 -0
  14. package/dist/extent.d.ts +64 -0
  15. package/dist/extent.js +151 -0
  16. package/dist/fetchRetry.d.ts +33 -0
  17. package/dist/fetchRetry.js +76 -0
  18. package/dist/index.d.ts +24 -0
  19. package/dist/index.js +16 -0
  20. package/dist/lobes.d.ts +24 -0
  21. package/dist/lobes.js +55 -0
  22. package/dist/mask.d.ts +71 -0
  23. package/dist/mask.js +363 -0
  24. package/dist/preview.d.ts +121 -0
  25. package/dist/preview.js +202 -0
  26. package/dist/sql.d.ts +10 -0
  27. package/dist/sql.js +33 -0
  28. package/dist/sqlTemplates.d.ts +1 -0
  29. package/dist/sqlTemplates.js +8 -0
  30. package/dist/wkb.d.ts +5 -0
  31. package/dist/wkb.js +80 -0
  32. package/package.json +71 -4
  33. package/sql/last_step.sql +31 -0
  34. package/sql/points_tabledap.sql +21 -0
  35. package/sql/stats_categorical.sql +36 -0
  36. package/sql/stats_daily.sql +27 -0
  37. package/sql/stats_tabledap.sql +36 -0
  38. package/src/cells.ts +190 -0
  39. package/src/chunks.ts +58 -0
  40. package/src/dataset.ts +160 -0
  41. package/src/engine.ts +190 -0
  42. package/src/erddap.ts +271 -0
  43. package/src/extent.ts +172 -0
  44. package/src/fetchRetry.ts +71 -0
  45. package/src/index.ts +37 -0
  46. package/src/lobes.ts +66 -0
  47. package/src/mask.ts +341 -0
  48. package/src/preview.ts +279 -0
  49. package/src/sql.ts +36 -0
  50. package/src/sqlTemplates.ts +8 -0
  51. package/src/wkb.ts +81 -0
@@ -0,0 +1,8 @@
1
+ // generated by scripts/sql-templates.mjs from sql/*.sql: do not edit (edit the .sql, then run the script)
2
+ export const TEMPLATES: Readonly<Record<string, string>> = {
3
+ last_step: "-- the masked cells of the **last** time step in the slab, for the map layer.\n-- {{expr}} the value expression over the slab alias `s` (e.g. s.\"CRW_SST\", or\n-- (s.\"analysed_sst\" - 273.15) to convert Kelvin to Celsius)\n-- {{slab}} the registered griddap file (parquet) or view\n-- {{mask}} the mask table: latitude, longitude, weight (0,1]\n-- one row per cell: the mask's own latitude/longitude (so the squares land on the dataset grid), the\n-- area weight and the value. cells with no data on that step are kept with a NULL value, so the map\n-- draws the hole instead of moving the neighbouring squares. the join is the same exact equality on\n-- ROUND(x, 3) as stats_daily.sql (float32 axis vs float64 mask).\nWITH joined AS (\n SELECT\n s.\"time\" AS time,\n m.latitude AS latitude,\n m.longitude AS longitude,\n m.weight AS weight,\n {{expr}} AS value\n FROM {{slab}} s\n JOIN {{mask}} m\n ON ROUND(s.latitude::DOUBLE, 3) = ROUND(m.latitude::DOUBLE, 3)\n AND ROUND(s.longitude::DOUBLE, 3) = ROUND(m.longitude::DOUBLE, 3)\n)\nSELECT\n -- utc day; via epoch_ms because duckdb-wasm ships without icu, so TIMESTAMPTZ::DATE is unimplemented there\n make_timestamp(epoch_ms(time) * 1000)::DATE AS date,\n latitude,\n longitude,\n weight,\n value\nFROM joined\nWHERE time = (SELECT max(time) FROM joined)\nORDER BY latitude, longitude\n",
4
+ points_tabledap: "-- one row per masked sampling station, for the map layer of a tabledap run.\n-- {{expr}} the value expression over the slab alias `s`\n-- {{slab}} the registered tabledap file (parquet) or view\n-- {{mask}} the station table: latitude, longitude\n-- a tabledap window holds many casts at the same position and many depths per cast, so the map\n-- shows each **station**: its mean value over the window, the number of measurements behind it and\n-- the last time it was sampled.\nSELECT\n m.latitude AS latitude,\n m.longitude AS longitude,\n 1.0 AS weight,\n avg({{expr}}) AS value,\n count({{expr}}) AS n,\n make_timestamp(epoch_ms(max(s.\"time\")) * 1000)::DATE AS date\nFROM {{slab}} s\nJOIN {{mask}} m\n ON ROUND(s.latitude::DOUBLE, 5) = ROUND(m.latitude::DOUBLE, 5)\n AND ROUND(s.longitude::DOUBLE, 5) = ROUND(m.longitude::DOUBLE, 5)\nWHERE {{expr}} IS NOT NULL\nGROUP BY 1, 2\nORDER BY 1, 2\n",
5
+ stats_categorical: "-- per date x class composition for one place from a categorical ERDDAP griddap slab (seascapeR's\n-- sum_ss_grds_to_ts(): how much of the place is in each class on each date).\n-- {{expr}} the class expression over the slab alias `s` (e.g. s.\"CLASS\"); cast to BIGINT here\n-- {{slab}} the registered griddap file (parquet) or view\n-- {{mask}} the mask table: latitude, longitude, weight (0,1]\n-- `fraction` is area-weighted (partial boundary cells count for their overlap) and sums to exactly 1\n-- over the classes of a date; `percent_cells` is the unweighted cell count share.\n-- the join is the same exact equality on ROUND(x, 3) as stats_daily.sql (float32 axis vs float64 mask).\nWITH cells AS (\n SELECT\n -- utc day; via epoch_ms because duckdb-wasm ships without icu, so TIMESTAMPTZ::DATE is unimplemented there\n make_timestamp(epoch_ms(s.\"time\") * 1000)::DATE AS date,\n CAST({{expr}} AS BIGINT) AS class,\n m.weight AS weight\n FROM {{slab}} s\n JOIN {{mask}} m\n ON ROUND(s.latitude::DOUBLE, 3) = ROUND(m.latitude::DOUBLE, 3)\n AND ROUND(s.longitude::DOUBLE, 3) = ROUND(m.longitude::DOUBLE, 3)\n WHERE {{expr}} IS NOT NULL\n),\ntotals AS (\n SELECT date, count(*) AS n_total, sum(weight) AS weight_total\n FROM cells\n GROUP BY 1\n)\nSELECT\n c.date AS date,\n c.class AS class,\n count(*) AS n,\n sum(c.weight) AS weight,\n sum(c.weight) / any_value(t.weight_total) AS fraction,\n 100.0 * count(*) / any_value(t.n_total) AS percent_cells\nFROM cells c\nJOIN totals t USING (date)\nGROUP BY c.date, c.class\nORDER BY 1, 2\n",
6
+ stats_daily: "-- daily key statistics for one place from an ERDDAP griddap slab.\n-- {{expr}} the value expression over the slab alias `s` (e.g. s.\"CRW_SST\", or\n-- (s.\"analysed_sst\" - 273.15) to convert Kelvin to Celsius)\n-- {{slab}} the registered griddap file (parquet) or view\n-- {{mask}} the mask table: latitude, longitude, weight (0,1]\n-- the join is an exact equality on ROUND(x, 3): the ERDDAP axis is float32 in the parquet and float64\n-- in the JS mask, so 21.375 arrives as 21.374999... on one side; rounding to the grid's 3 decimals\n-- (0.05 spacing) makes the two match without a tolerance join.\nSELECT\n -- utc day; via epoch_ms because duckdb-wasm ships without icu, so TIMESTAMPTZ::DATE is unimplemented there\n make_timestamp(epoch_ms(s.\"time\") * 1000)::DATE AS date,\n count({{expr}}) AS n,\n avg({{expr}}) AS mean,\n sum({{expr}} * m.weight) / sum(m.weight) AS mean_wt,\n stddev_samp({{expr}}) AS sd,\n min({{expr}}) AS min,\n max({{expr}}) AS max,\n quantile_cont({{expr}}, 0.10) AS p10,\n quantile_cont({{expr}}, 0.90) AS p90,\n sum(m.weight) AS weight_sum\nFROM {{slab}} s\nJOIN {{mask}} m\n ON ROUND(s.latitude::DOUBLE, 3) = ROUND(m.latitude::DOUBLE, 3)\n AND ROUND(s.longitude::DOUBLE, 3) = ROUND(m.longitude::DOUBLE, 3)\nWHERE {{expr}} IS NOT NULL\nGROUP BY 1\nORDER BY 1\n",
7
+ stats_tabledap: "-- monthly key statistics for one place from an ERDDAP **tabledap** slab (point samples).\n-- {{expr}} the value expression over the slab alias `s` (e.g. s.\"measurement_value\")\n-- {{slab}} the registered tabledap file (parquet) or view\n-- {{mask}} the station table: latitude, longitude (weight is 1 — a point is in or out)\n-- tabledap points are sparse and irregular, so the roll-up is by **month**, not by day: a CalCOFI\n-- quarterly cruise gives a handful of days a year, and a daily grouping would be mostly empty.\n-- `n_casts` counts the distinct sampling events (time x position) behind the n measurements, since\n-- one cast contributes many depths.\n-- the join is an exact equality on ROUND(x, 5): the station positions come back out of DuckDB, go\n-- through the JS point-in-polygon mask and come back in, so five decimals (~1 m) is exact here.\nWITH pts AS (\n SELECT\n -- utc, via epoch_ms because duckdb-wasm ships without icu: TIMESTAMPTZ::DATE is unimplemented there\n make_timestamp(epoch_ms(s.\"time\") * 1000) AS ts,\n s.longitude::DOUBLE AS longitude,\n s.latitude::DOUBLE AS latitude,\n {{expr}} AS value\n FROM {{slab}} s\n JOIN {{mask}} m\n ON ROUND(s.latitude::DOUBLE, 5) = ROUND(m.latitude::DOUBLE, 5)\n AND ROUND(s.longitude::DOUBLE, 5) = ROUND(m.longitude::DOUBLE, 5)\n WHERE {{expr}} IS NOT NULL\n)\nSELECT\n date_trunc('month', ts)::DATE AS date,\n count(value) AS n,\n count(DISTINCT (ts, longitude, latitude)) AS n_casts,\n avg(value) AS mean,\n stddev_samp(value) AS sd,\n min(value) AS min,\n max(value) AS max,\n quantile_cont(value, 0.10) AS p10,\n quantile_cont(value, 0.90) AS p90\nFROM pts\nGROUP BY 1\nORDER BY 1\n",
8
+ }
package/src/wkb.ts ADDED
@@ -0,0 +1,81 @@
1
+ // WKB -> GeoJSON, just enough for the gazetteer: (Multi)Point, (Multi)LineString, (Multi)Polygon and
2
+ // GeometryCollection. DuckDB-WASM has no spatial extension, so the `geometry` column of
3
+ // places.parquet (WKB MULTIPOLYGON, already split at +-180) is decoded here in JS instead.
4
+ //
5
+ // Both byte orders are handled, plus the two common type-code dialects:
6
+ // ISO WKB : type + 1000 (Z), + 2000 (M), + 3000 (ZM)
7
+ // EWKB : high bits 0x80000000 (Z), 0x40000000 (M), 0x20000000 (SRID follows the type)
8
+ // Z/M ordinates are read and dropped: GeoJSON here is 2-D on purpose (gridMask only wants x, y).
9
+ import type { Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon } from 'geojson'
10
+
11
+ interface Cursor { view: DataView; off: number; le: boolean }
12
+
13
+ const u32 = (c: Cursor) => { const v = c.view.getUint32(c.off, c.le); c.off += 4; return v }
14
+ const f64 = (c: Cursor) => { const v = c.view.getFloat64(c.off, c.le); c.off += 8; return v }
15
+
16
+ /** byte order + type code of the geometry at the cursor; leaves the cursor after the header. */
17
+ function header(c: Cursor): { type: number; dims: number } {
18
+ const order = c.view.getUint8(c.off); c.off += 1
19
+ if (order !== 0 && order !== 1) throw new Error(`WKB: bad byte order ${order} at ${c.off - 1}`)
20
+ c.le = order === 1
21
+ const raw = u32(c)
22
+ const ewkbZ = (raw & 0x80000000) !== 0
23
+ const ewkbM = (raw & 0x40000000) !== 0
24
+ const srid = (raw & 0x20000000) !== 0
25
+ const base = raw & 0x0fffffff
26
+ const iso = Math.floor(base / 1000) // 0 = XY, 1 = Z, 2 = M, 3 = ZM
27
+ const type = base % 1000
28
+ if (srid) u32(c) // skip the SRID
29
+ const dims = 2 + (ewkbZ ? 1 : 0) + (ewkbM ? 1 : 0) + (iso === 1 || iso === 2 ? 1 : iso === 3 ? 2 : 0)
30
+ return { type, dims }
31
+ }
32
+
33
+ const point = (c: Cursor, dims: number): number[] => {
34
+ const xy = [f64(c), f64(c)]
35
+ for (let i = 2; i < dims; i++) f64(c) // drop Z / M
36
+ return xy
37
+ }
38
+ const ring = (c: Cursor, dims: number): number[][] => {
39
+ const n = u32(c), out: number[][] = new Array(n)
40
+ for (let i = 0; i < n; i++) out[i] = point(c, dims)
41
+ return out
42
+ }
43
+ const rings = (c: Cursor, dims: number): number[][][] => {
44
+ const n = u32(c), out: number[][][] = new Array(n)
45
+ for (let i = 0; i < n; i++) out[i] = ring(c, dims)
46
+ return out
47
+ }
48
+
49
+ function geometry(c: Cursor): Geometry {
50
+ const { type, dims } = header(c)
51
+ switch (type) {
52
+ case 1: return { type: 'Point', coordinates: point(c, dims) } as Point
53
+ case 2: return { type: 'LineString', coordinates: ring(c, dims) } as LineString
54
+ case 3: return { type: 'Polygon', coordinates: rings(c, dims) } as Polygon
55
+ case 4: case 5: case 6: case 7: {
56
+ const n = u32(c)
57
+ const parts: Geometry[] = new Array(n)
58
+ for (let i = 0; i < n; i++) parts[i] = geometry(c) // each part carries its own header
59
+ if (type === 7) return { type: 'GeometryCollection', geometries: parts } as GeometryCollection
60
+ if (type === 4) return { type: 'MultiPoint', coordinates: parts.map((p) => (p as Point).coordinates) } as MultiPoint
61
+ if (type === 5) return { type: 'MultiLineString', coordinates: parts.map((p) => (p as LineString).coordinates) } as MultiLineString
62
+ return { type: 'MultiPolygon', coordinates: parts.map((p) => (p as Polygon).coordinates) } as MultiPolygon
63
+ }
64
+ default: throw new Error(`WKB: unsupported geometry type ${type}`)
65
+ }
66
+ }
67
+
68
+ /** decode one WKB (or EWKB) geometry to GeoJSON, 2-D. */
69
+ export function wkbToGeoJSON(bytes: Uint8Array | ArrayBuffer): Geometry {
70
+ const u = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
71
+ const view = new DataView(u.buffer, u.byteOffset, u.byteLength)
72
+ return geometry({ view, off: 0, le: true })
73
+ }
74
+
75
+ /** every Polygon of a geometry, as a MultiPolygon-style coordinate array. */
76
+ export function polygonCoordinates(g: Geometry): number[][][][] {
77
+ if (g.type === 'Polygon') return [g.coordinates]
78
+ if (g.type === 'MultiPolygon') return g.coordinates
79
+ if (g.type === 'GeometryCollection') return g.geometries.flatMap(polygonCoordinates)
80
+ return []
81
+ }