@chrtco/sdk 0.10.2 → 0.10.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to `@chrtco/sdk` will be documented in this file.
5
5
  ## [Unreleased]
6
6
 
7
7
  ### Added
8
+ - **Render-mode lever on the Style API** (`sail#2`): `StyleUrlOptions.mode` /
9
+ `OfflineFetchStyleOptions.mode` — `"continuous"` (default) or
10
+ `"chart:{band}"` (`chart:Overview | chart:General | chart:Coastal |
11
+ chart:Approach | chart:Harbor`) — threads `?mode=` to the served style
12
+ routes (online `style.json` and offline `/v1/offline/styles/{region}`),
13
+ which lock the portrayal to one scale band with cascade fallback: the
14
+ served twin of the app's Map Mode toggle, now available to every API
15
+ consumer, online and offshore. New exports: `ChrtStyleMode`,
16
+ `CHRT_STYLE_MODES`, `normalizeStyleMode()`, `styleModeBand()`.
17
+ `OfflineStyleResponse.mode` echoes the resolved mode. Servers reject
18
+ invalid modes AND unknown style-route query params with typed 400s
19
+ (`invalid_mode` / `unknown_param`) instead of silently serving the
20
+ default document.
21
+ - **`OfflineFetchStyleOptions.symbols`** — the offline style route has
22
+ accepted `?symbols=s52|s101|chrt|raster` all along, but
23
+ `chrt.offline.fetchStyle()` had no way to send it, so native clients could
24
+ only ever fetch the default `s52` offline style. Now threaded through
25
+ (omitted → worker default `s52`), typed with the same `ChrtSymbolSet` as
26
+ the online `StyleUrlOptions.symbols`. `OfflineStyleResponse.symbols` types
27
+ the echoed set the worker already returns.
8
28
  - **Served styles now render in MapLibre Native as shipped.** Every generated
9
29
  style begins with a `chrt-background` layer (S-52 NODTA backdrop per
10
30
  palette) — previously native renderers drew a black void and
package/README.md CHANGED
@@ -243,7 +243,8 @@ curl -X POST https://api.chrt.co/v1/mint/tile-token \
243
243
  curl https://tiles.chrt.co/v1/styles/NOAA_ENC/style.json?theme=day \
244
244
  -H "Authorization: Bearer $TOKEN"
245
245
 
246
- curl https://tiles.chrt.co/v1/tiles/NOAA_ENC/12/1234/1543.mvt \
246
+ # --compressed matters: see "Tiles are gzipped" below.
247
+ curl --compressed https://tiles.chrt.co/v1/tiles/NOAA_ENC/12/1234/1543.mvt \
247
248
  -H "Authorization: Bearer $TOKEN"
248
249
  ```
249
250
 
@@ -251,6 +252,29 @@ The returned document is a standard MapLibre style — hand it to MapLibre Nativ
251
252
  `maplibre-gl`, or any renderer that speaks the style spec. Point your renderer's request transformer
252
253
  at the same `Authorization: Bearer` header so tile requests carry it too.
253
254
 
255
+ ### Tiles are gzipped — send `Accept-Encoding: gzip`
256
+
257
+ Tiles are stored gzip-compressed in the PMTiles archive, and the edge negotiates per request:
258
+ ask for `gzip` and you get gzip (`content-encoding: gzip` on the response); ask for `identity`,
259
+ or send no `Accept-Encoding` at all, and you get the uncompressed bytes.
260
+
261
+ **This changes tile-budget arithmetic by 2–2.5×.** MapLibre GL JS, MapLibre Native, every browser,
262
+ and this SDK all send `Accept-Encoding: gzip` automatically — the compressed figure is what your
263
+ users actually transfer. **Plain `curl` sends no `Accept-Encoding` at all**, so `curl -o tile.mvt`
264
+ measures the *uncompressed* size. Use `curl --compressed` (as above), or read `content-encoding`
265
+ off the response, before sizing a cache, a bundle, or a data plan.
266
+
267
+ Reference measurement, prod `NOAA_ENC`, 2026-07-27, tile `z11/620/757` (Boston):
268
+
269
+ | `Accept-Encoding` | Bytes on the wire |
270
+ |---|---|
271
+ | *(absent)* / `identity` | 1,152,506 B (1.10 MB) |
272
+ | `gzip` | 462,273 B (451 KB) |
273
+
274
+ Tile size tracks **feature density** far more than zoom — a harbor tile over a dense port dwarfs an
275
+ offshore tile at the same zoom, and a tile with no data returns `204` with no body at all. Sample a
276
+ spread of tiles across your own area of operations rather than extrapolating from one.
277
+
254
278
  The REST endpoints in the table below (`/v1/features`, `/v1/query`, `/v1/query/ahead`, `/v1/cells`,
255
279
  `/v1/coverage`) take the API key directly as `x-api-key` — no token minting needed.
256
280
 
@@ -342,6 +366,9 @@ call any language can make. Everything marked **JS-only** runs in-process and ha
342
366
  | `chrt.cells.get(id)` | `GET /v1/cells/{id}` | |
343
367
  | `chrt.coverage.list(opts?)` | `GET /v1/coverage` | |
344
368
  | `chrt.coverage.forRoute(opts)` | `GET /v1/coverage/route` | which cells a passage needs |
369
+ | `chrt.weather.conditions(opts)` | `GET /v1/weather/conditions` | observed at sea (NDBC); needs `weather:read` |
370
+ | `chrt.weather.alerts(opts)` | `GET /v1/weather/marine-alerts` | active NWS warnings; needs `weather:read` |
371
+ | `chrt.weather.forecast(opts)` | `GET /v1/weather/marine-forecast` | NWS zone forecast; needs `weather:read` |
345
372
  | `chrt.warmTiles(region?)` | `POST /v1/mint/tile-token` | |
346
373
  | `chrt.styleUrl(opts?)` | `GET {tileBaseUrl}/v1/styles/{region}/style.json` | mints first |
347
374
  | `chrt.fetchStyle(opts?)` | same, then fetches it | |
@@ -594,3 +621,55 @@ try {
594
621
  ## License
595
622
 
596
623
  MIT
624
+
625
+ ## CHRTweather (marine weather)
626
+
627
+ NOAA-backed marine weather on the same client and the same key. **U.S. waters only**, and every
628
+ call needs the **`weather:read` scope** — a chart key does not carry it implicitly, so ask CHRT to
629
+ enable it. Without it you get `403 weather_not_entitled`, which retrying will never fix.
630
+
631
+ ```ts
632
+ // Observed — a measurement from the nearest buoy, NOT live.
633
+ const now = await chrt.weather.conditions({ lat: 41.0, lon: -71.1 });
634
+ console.log(now.observed.ageMinutes); // display this: median ~40 min old
635
+ console.log(now.conditions.windSpeed); // undefined => NOT REPORTED, not calm
636
+ console.log(now.unavailable); // what this station did not report
637
+
638
+ // Warnings — poll these cheaply (~60 s) without pulling a forecast.
639
+ const { alerts } = await chrt.weather.alerts({ lat: 42.5, lon: -70.3 });
640
+
641
+ // Forecast — the NWS zone narrative.
642
+ const fc = await chrt.weather.forecast({ zone: "LEZ166" });
643
+ for (const p of fc.periods) console.log(p.label, p.text);
644
+ ```
645
+
646
+ Three rules govern reading any of it. They are the difference between a useful readout and a
647
+ dangerous one.
648
+
649
+ **An empty result never means calm water.** No alerts in force, no buoy in range, no marine zone at
650
+ this position — each means *we do not know* or *nothing is posted*, never *conditions are good*.
651
+ There is deliberately no `safe` or `status` field to bind a green tick to. Render "no warnings in
652
+ force", never "conditions are good".
653
+
654
+ **Absent is not zero.** A measurement a station did not report, or a phrase the forecast parser
655
+ could not read, is omitted rather than defaulted — `conditions.windSpeed === undefined` means the
656
+ buoy sent NDBC's `MM` missing-value sentinel, and rendering that as `0 kn` puts a false calm on a
657
+ sailing display. `"winds less than 10 knots"` gives you `maxKn` and no `minKn` for the same reason.
658
+
659
+ **On the forecast, `text` is authoritative.** The structured `wind` / `seas` / `swell` fields are
660
+ parsed from NWS's own sentence, and where they disagree the text wins — some content exists only
661
+ there. Periods are NWS's own and are neither hourly nor uniform (`OVERNIGHT`, `TUE NIGHT`,
662
+ `WED NIGHT THROUGH THU NIGHT`); a single period can span days, so do not flatten them into an
663
+ hourly series.
664
+
665
+ Two field-level traps worth naming:
666
+
667
+ - **`expires` vs `ends`** on an alert. `expires` is when the *bulletin* lapses; `ends` is when the
668
+ *hazard* passes. Routinely hours apart. Describe the hazard with `ends`.
669
+ - **`seas.minFt`/`maxFt` vs `seas.occasionalFt`.** The range is the mean of the highest **1/3**;
670
+ `occasionalFt` (Great Lakes only) is the mean of the highest **1/10**. In *"Waves 2 to 4 ft …
671
+ occasionally to 8 ft"* the 8 ft wave is the one that knocks a small boat down — never average it
672
+ into the range.
673
+
674
+ CHRT is **not** the issuing authority. Every response carries `attribution` and `disclaimer`;
675
+ display them, and always consult NWS and VHF broadcasts.