@walkthru-earth/objex-utils 1.3.0 → 1.4.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/docs/README.md CHANGED
@@ -31,18 +31,29 @@ As of v1.2, `yaml` is loaded via dynamic `import()` inside `parseMarkdownDocumen
31
31
 
32
32
  | Page | Covers |
33
33
  |------|--------|
34
- | [`geometry.md`](./geometry.md) | WKB parser, GeoArrow table builder, geometry-column detection |
35
- | [`cog.md`](./cog.md) | Cloud-Optimized GeoTIFF helpers (pipeline selection, band configs, color ramps, bounds clamping) |
34
+ | [`geometry.md`](./geometry.md) | WKB parser, GeoArrow table builder, geometry-column detection, parameterized GEOMETRY type parser (`parseGeometryTypeCrs`, `buildTransformExpr`, `wrapWkbWithCrs`) |
35
+ | [`cog.md`](./cog.md) | Cloud-Optimized GeoTIFF pure helpers (bounds clamping, sample format labels, type guards). The render-pipeline surface stays in the Svelte components package and is NOT re-exported here. |
36
36
  | [`parquet-metadata.md`](./parquet-metadata.md) | `readParquetMetadata` + CRS / bounds / geometry-types extractors |
37
37
  | [`stac-geoparquet.md`](./stac-geoparquet.md) | stac-geoparquet detection (`isStacGeoparquetSchema`) and row-to-Item transforms (`stacRowToItem`, `flattenStacBbox`, `pickStacPrimaryAsset`, `resolveStacAssetHref`) |
38
- | [`storage.md`](./storage.md) | URL parsing (`parseStorageUrl`, `resolveCloudUrl`), provider registry, `StorageAdapter` interface, `UrlAdapter` |
38
+ | [`stac-facets.md`](./stac-facets.md) | Auto-faceted state for STAC viewers, `extractItemView` / `buildFacets` / `applyFacets` / `sortViews` / `hasActiveFilters` / `emptyFacetState` + `StacItemView` / `Facet*` / `FacetState` types |
39
+ | [`stac-pushdown.md`](./stac-pushdown.md) | `FacetState` → STAC API native query + CQL2-JSON translation. `sniffApiCapabilities`, `toNativeQuery`, `toCql2Filter`, `residualState` gated by what `conformsTo` advertises |
40
+ | [`stac.md`](./stac.md) | Core STAC types and classifiers (`classifyStac`, `isStacItem`, `extractRasterBandAssets`, `extractMosaicAssets`, `buildMosaicSourceMeta`, `spatialCellKey`, `resolvePresetComposite`, `hasCompositableBands`, full type surface) |
41
+ | [`stac-source.md`](./stac-source.md) | `StacSource` contract + `createApiSource` / `createStaticSource` implementations (parquet implementation lives in the Svelte components package because it requires DuckDB-WASM) |
42
+ | [`stac-hydrate.md`](./stac-hydrate.md) | `hydrateStacItems` link-walker (Catalog / Collection / FeatureCollection / STAC API), `hasStacItemsEndpoint`, `absolutizeHref` |
43
+ | [`stac-storage-extension.md`](./stac-storage-extension.md) | STAC Storage Extension v1.0.0 / v2.0.0 hints (`extractStorageHints`, `applyStorageHintsToConnection`) |
44
+ | [`storage.md`](./storage.md) | URL parsing (`parseStorageUrl`, `resolveCloudUrl`, `classifyUrl`, `isKnownBucketHost`, `STAC_API_PATH_RE`), provider registry, `StorageAdapter` interface, `UrlAdapter`, `smokeTestHref`, `detectHostBucket`, `applyStacItemStorageHints`, `connectionIdentityKey` |
45
+ | [`cog-asset.md`](./cog-asset.md) | Vendor-neutral COG asset enumeration (`extractCogAssets`, `syntheticSelfAsset`, `pickNaturalColorComposite`, `isSingleAssetComposite`, `allChannelsBand0`) |
46
+ | [`channel-composite.md`](./channel-composite.md) | RGB composite presets and URL round-trip (`PRESETS`, `applyPreset`, `compositeFromUrl`, `compositeToUrl`, `presetMatchesComposite`) |
39
47
  | [`query-engine.md`](./query-engine.md) | `QueryEngine` interface and associated result/handle types |
40
48
  | [`file-types.md`](./file-types.md) | File-type registry (`getFileTypeInfo`, `getViewerKind`, `getDuckDbReadFn`, ...) |
41
- | [`formatting.md`](./formatting.md) | Display formatters, column-type classification, hex dump, CSV/JSON export |
49
+ | [`formatting.md`](./formatting.md) | Display formatters, column-type classification, hex dump, CSV/JSON export, clipboard helper, notebook renderer |
42
50
  | [`file-sort.md`](./file-sort.md) | `sortFileEntries`, `toggleSortField` |
43
51
  | [`markdown-sql.md`](./markdown-sql.md) | Markdown + SQL block parsing (Evidence-compatible syntax) |
44
52
  | [`local-storage.md`](./local-storage.md) | SSR-safe `loadFromStorage` / `persistToStorage` |
45
- | [`errors.md`](./errors.md) | `handleLoadError` |
53
+ | [`app-config.md`](./app-config.md) | Runtime config schema + precedence resolver (`AppConfig`, `mergeAppConfig`, `resolveSetting`, `resolveBasemap`, `parseVisibilityParam`, `coerce*`) |
54
+ | [`map-pixel-inspect.md`](./map-pixel-inspect.md) | Framework-agnostic click-to-inspect helper (`attachPixelInspector`), minimal `MapLike` shape, per-click abort coordination |
55
+ | [`lru.md`](./lru.md) | `LruCache<K,V>` move-to-end on `get`, optional `onEvict` |
56
+ | [`errors.md`](./errors.md) | `handleLoadError`, `isAbortError` |
46
57
  | [`types-constants.md`](./types-constants.md) | `Connection`, `Tab`, `FileEntry`, `WriteResult`, `Theme`, shared constants |
47
58
 
48
59
  ## Quick recipes
@@ -92,7 +103,7 @@ const rows = generateHexDump(bytes);
92
103
 
93
104
  ## Upstream source
94
105
 
95
- All modules re-export from `src/lib/` in the [objex](https://github.com/walkthru-earth/objex) monorepo. The re-export list lives at [`packages/objex-utils/src/index.ts`](../src/index.ts).
106
+ Most modules now live physically inside `packages/objex-utils/src/` (each re-exported via `export *` from `index.ts`). A handful of host-side types are still re-exported from `src/lib/` so both `@walkthru-earth/objex` and `@walkthru-earth/objex-utils` share the same shapes (Connection, StorageAdapter, QueryEngine, file-icons, constants). See [`packages/objex-utils/src/index.ts`](../src/index.ts) in the [objex](https://github.com/walkthru-earth/objex) monorepo.
96
107
 
97
108
  ## Versioning & releases
98
109
 
@@ -0,0 +1,237 @@
1
+ # App config
2
+
3
+ Runtime configuration schema and precedence resolver for objex self-hosting (a bundled `config.json`, an optional `?config=<url>` remote override, and an in-app settings panel).
4
+
5
+ Source: `packages/objex-utils/src/app-config.ts`.
6
+
7
+ ## Types
8
+
9
+ ### `BasemapConfig`
10
+
11
+ ```ts
12
+ interface BasemapConfig {
13
+ id: string;
14
+ label: string;
15
+ type: 'vector' | 'raster';
16
+ url: string;
17
+ variant?: 'light' | 'dark';
18
+ }
19
+ ```
20
+
21
+ A basemap option a host can offer. `variant` tags the basemap as suited to a light or dark theme, used by [`resolveBasemap`](#resolvebasemapconfig-variant-userid) to match the active theme. Consumed in Phase 2.
22
+
23
+ ### `ConnectionSeed`
24
+
25
+ ```ts
26
+ interface ConnectionSeed {
27
+ name: string;
28
+ provider: string;
29
+ bucket: string;
30
+ region?: string;
31
+ endpoint?: string;
32
+ anonymous?: boolean;
33
+ authMethod?: 'sigv4' | 'sas-token';
34
+ rootPrefix?: string;
35
+ }
36
+ ```
37
+
38
+ A preloaded connection definition baked into the config. **Never carries secrets**, it describes where data lives, not how to authenticate. Consumed in Phase 2.
39
+
40
+ ### `AppConfigDefaults`
41
+
42
+ ```ts
43
+ interface AppConfigDefaults {
44
+ theme: Theme;
45
+ locale: string;
46
+ featureLimit: number;
47
+ mosaicItemLimit: number;
48
+ }
49
+ ```
50
+
51
+ Default app behaviour. `theme` is the shared [`Theme`](./types-constants.md#theme) union (`'light' | 'dark' | 'system'`). `featureLimit` caps rows pulled into a map layer, `mosaicItemLimit` caps STAC mosaic items.
52
+
53
+ ### `AppConfigUi`
54
+
55
+ ```ts
56
+ interface AppConfigUi {
57
+ showConnectionRail: boolean;
58
+ showFileTree: boolean;
59
+ showSettings: boolean;
60
+ }
61
+ ```
62
+
63
+ Chrome visibility toggles for embedding objex with a trimmed UI.
64
+
65
+ ### `AppConfig`
66
+
67
+ ```ts
68
+ interface AppConfig {
69
+ defaults: AppConfigDefaults;
70
+ ui: AppConfigUi;
71
+ basemaps: BasemapConfig[];
72
+ defaultBasemap: { light?: string; dark?: string };
73
+ connections: ConnectionSeed[];
74
+ }
75
+ ```
76
+
77
+ The full resolved configuration. `defaultBasemap.light` / `defaultBasemap.dark` reference a `BasemapConfig.id` to pick the preferred basemap per theme variant.
78
+
79
+ ## Functions
80
+
81
+ ### `DEFAULT_APP_CONFIG`
82
+
83
+ ```ts
84
+ const DEFAULT_APP_CONFIG: AppConfig;
85
+ ```
86
+
87
+ The hardcoded fallback, matching the app's behaviour when no config is present:
88
+
89
+ ```ts
90
+ {
91
+ defaults: { theme: 'system', locale: 'en', featureLimit: 1000, mosaicItemLimit: 2000 },
92
+ ui: { showConnectionRail: true, showFileTree: true, showSettings: true },
93
+ basemaps: [],
94
+ defaultBasemap: {},
95
+ connections: []
96
+ }
97
+ ```
98
+
99
+ Use this as the `base` argument to [`mergeAppConfig`](#mergeappconfigbase-override) when no bundled config exists.
100
+
101
+ ### `mergeAppConfig(base, override)`
102
+
103
+ ```ts
104
+ function mergeAppConfig(base: AppConfig, override: unknown): AppConfig
105
+ ```
106
+
107
+ Merge an untrusted JSON value over a base config, field by field. Returns a fully populated `AppConfig`. Behaviour:
108
+
109
+ - When `override` is not a plain object (null, array, primitive), `base` is returned unchanged.
110
+ - Unknown fields are ignored.
111
+ - Each scalar runs through its `coerce*` validator. A malformed value (wrong type, blank string, non-positive int) falls back to the matching `base` value.
112
+ - `basemaps` and `connections` are filtered: each entry is validated and well-formed entries are kept. A basemap is dropped unless it has a non-blank `id`, `label`, `url`, and a `type` of `'vector'` or `'raster'`. A connection is dropped unless it has a non-blank `name` and `bucket`; its `provider` defaults to `'s3'` when blank. When the field is not an array at all, the whole `base` list is kept.
113
+ - Never reads secrets.
114
+
115
+ ### `resolveSetting<T>(...candidates)`
116
+
117
+ ```ts
118
+ function resolveSetting<T>(...candidates: (T | null | undefined)[]): T | undefined
119
+ ```
120
+
121
+ Returns the first candidate that is neither `null` nor `undefined`, or `undefined` when all are. This encodes objex's settings precedence chain, list candidates highest priority first:
122
+
123
+ ```
124
+ query-param > user override > config value > hardcoded fallback
125
+ ```
126
+
127
+ Because the test is strict (`!== null && !== undefined`), falsy-but-valid values such as `0`, `''`, and `false` are treated as present and win over later candidates. Pre-validate user-supplied strings with the `coerce*` helpers so an invalid value collapses to `undefined` and the chain falls through.
128
+
129
+ ### `parseVisibilityParam(value)`
130
+
131
+ ```ts
132
+ function parseVisibilityParam(value: string | null): boolean | undefined
133
+ ```
134
+
135
+ Maps the `?rail` / `?tree` visibility query param to a boolean. `'hide'` returns `false`, `'show'` returns `true`, anything else (including `null`) returns `undefined`. The `undefined` case is designed to slot in as the top candidate of [`resolveSetting`](#resolvesettingtcandidates), so an absent or invalid param defers to the config.
136
+
137
+ ### `coerceTheme(v)`
138
+
139
+ ```ts
140
+ function coerceTheme(v: unknown): Theme | undefined
141
+ ```
142
+
143
+ Returns `v` only when it is exactly `'light'`, `'dark'`, or `'system'`, otherwise `undefined`.
144
+
145
+ ### `coerceString(v)`
146
+
147
+ ```ts
148
+ function coerceString(v: unknown): string | undefined
149
+ ```
150
+
151
+ Returns the trimmed string when `v` is a string with non-whitespace content, otherwise `undefined`. Note the return value is trimmed, leading/trailing whitespace is stripped.
152
+
153
+ ### `coercePositiveInt(v)`
154
+
155
+ ```ts
156
+ function coercePositiveInt(v: unknown): number | undefined
157
+ ```
158
+
159
+ Returns `Math.floor(v)` when `v` is a finite number `>= 1`, otherwise `undefined`. Fractional inputs are floored, zero and negatives are rejected.
160
+
161
+ ### `coerceBool(v)`
162
+
163
+ ```ts
164
+ function coerceBool(v: unknown): boolean | undefined
165
+ ```
166
+
167
+ Returns `v` only when it is a real boolean, otherwise `undefined`. Truthy/falsy strings like `'true'` are **not** coerced.
168
+
169
+ ### `resolveBasemap(config, variant, userId)`
170
+
171
+ ```ts
172
+ function resolveBasemap(
173
+ config: AppConfig,
174
+ variant: 'light' | 'dark',
175
+ userId: string | undefined
176
+ ): BasemapConfig | undefined
177
+ ```
178
+
179
+ Pick the basemap a map should render. Pick order:
180
+
181
+ 1. **Explicit user pick** -- the basemap whose `id` equals `userId`, but only if it still exists in the configured list (a stale `userId` from local storage is ignored).
182
+ 2. **Configured default for the variant** -- the basemap whose `id` equals `config.defaultBasemap[variant]`, when present and still in the list.
183
+ 3. **First basemap matching the variant** -- the first entry with `variant` equal to the requested variant.
184
+ 4. **First basemap of any variant** -- `config.basemaps[0]`.
185
+
186
+ Returns `undefined` only when no basemaps are configured (`config.basemaps` is empty), signalling the caller to fall back to its own hardcoded default.
187
+
188
+ ## Example
189
+
190
+ Resolving the active theme through the full precedence chain (query param over user override over config over fallback):
191
+
192
+ ```ts
193
+ import {
194
+ DEFAULT_APP_CONFIG,
195
+ mergeAppConfig,
196
+ resolveSetting,
197
+ coerceTheme,
198
+ } from '@walkthru-earth/objex-utils';
199
+
200
+ // Bundled config.json merged over the hardcoded fallback.
201
+ const config = mergeAppConfig(DEFAULT_APP_CONFIG, {
202
+ defaults: { theme: 'light', locale: 'en' },
203
+ });
204
+
205
+ const params = new URLSearchParams(location.search);
206
+ const userTheme = loadFromStorage('theme', undefined); // may be undefined
207
+
208
+ const theme = resolveSetting(
209
+ coerceTheme(params.get('theme')), // query-param (highest priority)
210
+ coerceTheme(userTheme), // user override
211
+ config.defaults.theme, // config value
212
+ 'system', // hardcoded fallback (always wins last)
213
+ );
214
+ // ?theme=dark -> 'dark'
215
+ // no param, user picked 'light' -> 'light'
216
+ // nothing set -> config.defaults.theme === 'light'
217
+ ```
218
+
219
+ Picking the basemap for the active theme variant:
220
+
221
+ ```ts
222
+ import { resolveBasemap } from '@walkthru-earth/objex-utils';
223
+
224
+ const config = mergeAppConfig(DEFAULT_APP_CONFIG, {
225
+ basemaps: [
226
+ { id: 'osm', label: 'OSM', type: 'raster', url: 'https://…', variant: 'light' },
227
+ { id: 'dark-matter', label: 'Dark Matter', type: 'vector', url: 'https://…', variant: 'dark' },
228
+ ],
229
+ defaultBasemap: { dark: 'dark-matter' },
230
+ });
231
+
232
+ resolveBasemap(config, 'dark', undefined); // -> the 'dark-matter' entry (config default)
233
+ resolveBasemap(config, 'light', undefined); // -> the 'osm' entry (first matching variant)
234
+ resolveBasemap(config, 'dark', 'osm'); // -> the 'osm' entry (user pick wins)
235
+ resolveBasemap(config, 'dark', 'gone'); // -> 'dark-matter' (stale id ignored, default used)
236
+ resolveBasemap(DEFAULT_APP_CONFIG, 'light', undefined); // -> undefined (no basemaps configured)
237
+ ```
@@ -0,0 +1,127 @@
1
+ # Channel composite presets and URL round-trip
2
+
3
+ RGB composite presets (Natural Color, False-Color IR, SWIR, ...) and the `URLSearchParams` round-trip for the unified RGB picker. Pure TypeScript, publishable via objex-utils.
4
+
5
+ Source: `packages/objex-utils/src/channel-composite.ts`.
6
+
7
+ Presets describe a semantic band-slot triple (`red`/`green`/`blue` for Natural Color, `nir`/`red`/`green` for False-Color IR, ...). Resolving a preset against a specific item walks the band-key fallbacks in [`stac`](./stac.md) (via `resolvePresetComposite`) to map slots to actual asset keys on that item. NDVI and other single-band derived presets are intentionally not in this list.
8
+
9
+ This module builds on the [`CogAsset` / `ChannelComposite`](./cog-asset.md) types and the [`BandSlot`](./stac.md) vocabulary.
10
+
11
+ ## Types
12
+
13
+ ### `PresetDef`
14
+
15
+ ```ts
16
+ interface PresetDef {
17
+ id: string;
18
+ labelKey: string;
19
+ slots: { r: BandSlot; g: BandSlot; b: BandSlot };
20
+ }
21
+ ```
22
+
23
+ A preset definition. `id` is the stable string written into the URL (`?preset=...`), `labelKey` is the i18n key for its display label, and `slots` is the semantic band-slot triple ([`BandSlot`](./stac.md): `'red' | 'green' | 'blue' | 'nir' | 'swir1' | 'swir2' | 'rededge'`).
24
+
25
+ ## Constants
26
+
27
+ ### `PRESETS`
28
+
29
+ ```ts
30
+ const PRESETS: PresetDef[]
31
+ ```
32
+
33
+ The built-in preset list, in display order:
34
+
35
+ | `id` | `labelKey` | r / g / b slots |
36
+ |------|-----------|-----------------|
37
+ | `natural-color` | `map.multiCogPreset.trueColor` | `red` / `green` / `blue` |
38
+ | `false-color-ir` | `map.multiCogPreset.falseColorIR` | `nir` / `red` / `green` |
39
+ | `swir` | `map.multiCogPreset.swir` | `swir2` / `swir1` / `red` |
40
+ | `vegetation` | `map.multiCogPreset.vegetation` | `nir` / `swir1` / `red` |
41
+ | `agriculture` | `map.multiCogPreset.agriculture` | `swir1` / `nir` / `blue` |
42
+
43
+ ## Functions
44
+
45
+ ### `availablePresets(assets)`
46
+
47
+ ```ts
48
+ function availablePresets(assets: CogAsset[]): PresetDef[]
49
+ ```
50
+
51
+ Return the subset of `PRESETS` whose slot triple actually resolves on this item, i.e. every slot maps to a present asset. Use this to render only the presets that will produce a valid composite for the loaded item.
52
+
53
+ ### `applyPreset(assets, preset)`
54
+
55
+ ```ts
56
+ function applyPreset(assets: CogAsset[], preset: PresetDef): ChannelComposite | null
57
+ ```
58
+
59
+ Resolve a preset to a `ChannelComposite` for this item. Each resolved channel binds to its asset key at band index `0`. Returns `null` when the preset's slots do not all resolve against `assets`.
60
+
61
+ ### `presetMatchesComposite(preset, c, assets)`
62
+
63
+ ```ts
64
+ function presetMatchesComposite(
65
+ preset: PresetDef,
66
+ c: ChannelComposite,
67
+ assets: CogAsset[]
68
+ ): boolean
69
+ ```
70
+
71
+ `true` when the preset, resolved against `assets`, still matches the user's current composite `c`. The match requires the `r`/`g`/`b` asset keys to be equal and all three band indices on `c` to be `0`. Returns `false` when the preset does not resolve. Use this to highlight which preset (if any) corresponds to the user's current manual picks.
72
+
73
+ ### `compositeFromUrl(params, assets)`
74
+
75
+ ```ts
76
+ function compositeFromUrl(
77
+ params: URLSearchParams,
78
+ assets: CogAsset[]
79
+ ): ChannelComposite | null
80
+ ```
81
+
82
+ Decode a `URLSearchParams` chunk into a `ChannelComposite`.
83
+
84
+ **Format** `r=<asset>&g=<asset>&b=<asset>&band_r=<n>&band_g=<n>&band_b=<n>` plus optional `a=<asset>&band_a=<n>`. Each `band_*` defaults to `0` when absent, so legacy MultiCog URLs such as `?r=red&g=green&b=blue&preset=true-color` keep round-tripping.
85
+
86
+ **Returns** `null` when any required key (`r`, `g`, or `b`) is missing, or when any of the named `r`/`g`/`b` asset keys is not present in `assets`. The optional alpha channel is only added when its `a` key resolves to a known asset; an unresolvable `a` is silently dropped rather than failing the whole parse.
87
+
88
+ **Band clamping** Each `band_*` value is parsed as a number, floored, and clamped into `[0, bandCount - 1]` for the corresponding asset. A missing, non-finite, or negative value becomes `0`; a value at or beyond `bandCount` becomes `bandCount - 1` (or `0` when `bandCount` is non-positive).
89
+
90
+ ### `compositeToUrl(c, presetId)`
91
+
92
+ ```ts
93
+ function compositeToUrl(c: ChannelComposite, presetId: string | null): URLSearchParams
94
+ ```
95
+
96
+ Encode a composite plus the active preset id into `URLSearchParams` for the URL hash.
97
+
98
+ **Encoding** Always writes `r`, `g`, `b` asset keys. A `band_*` key is written only when that channel's band index is non-zero (so default-band composites stay compact). The optional alpha channel adds `a` (and `band_a` when non-zero). `preset` is written only when `presetId` is non-null. The result is the inverse of `compositeFromUrl` for any composite whose band indices are valid.
99
+
100
+ ## Example
101
+
102
+ ```ts
103
+ import {
104
+ PRESETS,
105
+ availablePresets,
106
+ applyPreset,
107
+ presetMatchesComposite,
108
+ compositeFromUrl,
109
+ compositeToUrl,
110
+ } from '@walkthru-earth/objex-utils';
111
+
112
+ // Which presets work on this item?
113
+ const usable = availablePresets(assets); // subset of PRESETS
114
+
115
+ // Apply False-Color IR if it resolves.
116
+ const ir = PRESETS.find((p) => p.id === 'false-color-ir');
117
+ const composite = ir ? applyPreset(assets, ir) : null;
118
+
119
+ // Round-trip through the URL hash.
120
+ if (composite) {
121
+ const params = compositeToUrl(composite, 'false-color-ir');
122
+ // 'r=B08&g=B04&b=B03&preset=false-color-ir'
123
+
124
+ const decoded = compositeFromUrl(params, assets); // ChannelComposite | null
125
+ const active = ir ? presetMatchesComposite(ir, composite, assets) : false; // true
126
+ }
127
+ ```
@@ -0,0 +1,164 @@
1
+ # COG asset enumeration
2
+
3
+ Vendor-neutral per-channel COG asset descriptors for the unified RGB picker. Pure TypeScript, no Svelte dependency. Reads `raster:bands.length` and `eo:bands` from STAC metadata without any network access.
4
+
5
+ Source: `packages/objex-utils/src/cog-asset.ts`.
6
+
7
+ `CogAsset` is the canonical shape every objex raster viewer (`CogViewer`, `MultiCogViewer`, `StacMosaicViewer`) hands to the shared ChannelPicker UI. The `self` key is used when the viewer is a single bare COG file with no STAC context.
8
+
9
+ ## Types
10
+
11
+ ### `CogAsset`
12
+
13
+ ```ts
14
+ interface CogAsset {
15
+ key: string;
16
+ href: string;
17
+ bandCount: number;
18
+ bandCountKnown: boolean;
19
+ dtype?: string;
20
+ eoCommon: string[];
21
+ roles: string[];
22
+ title?: string;
23
+ mediaType?: string;
24
+ }
25
+ ```
26
+
27
+ | Field | Meaning |
28
+ |-------|---------|
29
+ | `key` | STAC asset key (`red`, `B04`, `image`, `visual`, ...), or `'self'` for a single bare COG without STAC context. |
30
+ | `href` | Absolute or relative href as it appears in the STAC item / URL. |
31
+ | `bandCount` | Number of bands in the asset. `1` by default until a band source is found or the COG header is probed. |
32
+ | `bandCountKnown` | `true` when `bandCount` came from STAC metadata or a probe; `false` means the default `1` is a placeholder and the caller should lazily probe on first pick. |
33
+ | `dtype` | `raster:bands[0].data_type` if known. |
34
+ | `eoCommon` | `eo:bands[].common_name` (or unified `bands[].common_name`) lowercased, aligned to band-index order. Entries are `''` where no common name is present. |
35
+ | `roles` | STAC asset roles (`data`, `visual`, `reflectance`, ...). |
36
+ | `title` | Optional human title. |
37
+ | `mediaType` | Asset media type as advertised by STAC. |
38
+
39
+ ### `ChannelRef`
40
+
41
+ ```ts
42
+ interface ChannelRef {
43
+ assetKey: string;
44
+ bandIndex: number;
45
+ }
46
+ ```
47
+
48
+ A single per-channel pixel coordinate inside a STAC item: which asset, and which band within it.
49
+
50
+ ### `ChannelComposite`
51
+
52
+ ```ts
53
+ interface ChannelComposite {
54
+ r: ChannelRef;
55
+ g: ChannelRef;
56
+ b: ChannelRef;
57
+ a?: ChannelRef;
58
+ }
59
+ ```
60
+
61
+ An RGB(A) composite, one `ChannelRef` per channel. The alpha channel is optional.
62
+
63
+ ## Functions
64
+
65
+ ### `extractCogAssets(item)`
66
+
67
+ ```ts
68
+ function extractCogAssets(item: StacItem): CogAsset[]
69
+ ```
70
+
71
+ Enumerate every TIFF/COG asset on a STAC Item, keeping multi-band assets (NAIP `image`, Sentinel-2 `visual` TCI) alongside single-band per-band assets.
72
+
73
+ **Filtering** Only assets with an `href` and a TIFF/GeoTIFF media type (`image/tiff` or `image/geotiff`, case-insensitive) are kept. Assets whose `type` is absent are still kept. Assets whose roles include `thumbnail`, `overview`, or `metadata` are skipped.
74
+
75
+ **`bandCount` source priority** (first present wins):
76
+
77
+ 1. `asset.bands` (STAC 1.1 unified bands array)
78
+ 2. `asset['raster:bands']` (STAC 1.0 raster extension)
79
+ 3. `asset['eo:bands']` (STAC 1.0 eo extension)
80
+ 4. `item.properties.bands` (STAC 1.1 item-level bands, applies to all assets that do not override their own)
81
+
82
+ The item-level fallback covers catalogs (Hamilton NAIP-style 4-band COGs) which keep band metadata at the item-properties level while the single `data` asset carries none of its own. When none of the four sources yields a positive count, `bandCount` defaults to `1` with `bandCountKnown: false`.
83
+
84
+ **`eoCommon` source** Independent of the `bandCount` source. Prefers `eo:bands` (the only field guaranteed to carry `common_name` before STAC 1.1), then the unified `bands`, then item-level `properties.bands`. `raster:bands` is skipped for this lookup because it typically has no `common_name`.
85
+
86
+ **`dtype` source** First entry's `data_type` from `raster:bands`, then unified `bands`, then item-level `bands`.
87
+
88
+ **Returns** `CogAsset[]`, in the order assets appear on the item.
89
+
90
+ ### `syntheticSelfAsset(href, probedBandCount?)`
91
+
92
+ ```ts
93
+ function syntheticSelfAsset(href: string, probedBandCount?: number): CogAsset
94
+ ```
95
+
96
+ Build a single synthetic asset with key `'self'` for `CogViewer` (a single bare COG file, no STAC context) so the same ChannelPicker UI works without special-casing.
97
+
98
+ **Parameters**
99
+
100
+ | Name | Type | Meaning |
101
+ |------|------|---------|
102
+ | `href` | `string` | The COG URL. |
103
+ | `probedBandCount` | `number` (optional) | The probed `geotiff.count`, once known. |
104
+
105
+ `bandCount` defaults to `1` with `bandCountKnown: false`. When `probedBandCount` is a positive number, `bandCount` is set to it and `bandCountKnown` becomes `true`. `eoCommon` and `roles` are empty arrays.
106
+
107
+ ### `pickNaturalColorComposite(assets)`
108
+
109
+ ```ts
110
+ function pickNaturalColorComposite(
111
+ assets: CogAsset[]
112
+ ): { composite: ChannelComposite; source: 'visual-asset' | 'rgb-bands' | 'fallback' } | null
113
+ ```
114
+
115
+ Pick the most natural and most performant default composite for an item.
116
+
117
+ **Priority** (first match wins):
118
+
119
+ 1. **`'visual-asset'`** -- a 3-band pre-baked visual asset (`bandCount === 3` and either `roles` contains `visual` or `eoCommon` contains all of `red`/`green`/`blue`). All three channels bind to that one asset, using the `eoCommon` index for each color where present and falling back to bands 0/1/2. Single-layer path, one decoder, fastest.
120
+ 2. **`'rgb-bands'`** -- separate single-band assets resolvable by common name, where `eoCommon[0]` is `red`, `green`, and `blue` respectively across three distinct assets. Each channel binds to its asset at band 0.
121
+ 3. **`'fallback'`** -- the first three raster assets, band 0 each. When fewer than three assets exist, the single remaining asset is reused for all three channels: R at band 0, G at `min(1, last)`, B at `min(2, last)`, where `last = max(0, bandCount - 1)`.
122
+
123
+ **Returns** `{ composite, source }`, or `null` when `assets` is empty.
124
+
125
+ ### `isSingleAssetComposite(c)`
126
+
127
+ ```ts
128
+ function isSingleAssetComposite(c: ChannelComposite): boolean
129
+ ```
130
+
131
+ `true` when all three RGB channels (`r`, `g`, `b`) target the same asset key. The optional alpha channel is not considered.
132
+
133
+ ### `allChannelsBand0(c)`
134
+
135
+ ```ts
136
+ function allChannelsBand0(c: ChannelComposite): boolean
137
+ ```
138
+
139
+ `true` when all three RGB channels are at band index `0` (the MultiCOGLayer-compatible case). The optional alpha channel is not considered.
140
+
141
+ ## Example
142
+
143
+ ```ts
144
+ import {
145
+ extractCogAssets,
146
+ syntheticSelfAsset,
147
+ pickNaturalColorComposite,
148
+ isSingleAssetComposite,
149
+ allChannelsBand0,
150
+ } from '@walkthru-earth/objex-utils';
151
+
152
+ // From a STAC item:
153
+ const assets = extractCogAssets(item);
154
+ const pick = pickNaturalColorComposite(assets);
155
+ if (pick) {
156
+ console.log(pick.source); // 'visual-asset' | 'rgb-bands' | 'fallback'
157
+ console.log(isSingleAssetComposite(pick.composite)); // true for a pre-baked visual
158
+ console.log(allChannelsBand0(pick.composite)); // true for the rgb-bands path
159
+ }
160
+
161
+ // From a single bare COG:
162
+ const self = syntheticSelfAsset('https://example.com/scene.tif', 4);
163
+ // { key: 'self', bandCount: 4, bandCountKnown: true, eoCommon: [], roles: [], ... }
164
+ ```
package/docs/cog.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Pure, framework-agnostic helpers for working with Cloud-Optimized GeoTIFF metadata and bounds. No Svelte, MapLibre, deck.gl, or GeoTIFF library dependency.
4
4
 
5
- Source: `src/lib/utils/cog.ts`.
5
+ Source: `packages/objex-utils/src/cog-info.ts` (dependency-free subset). The app-side `src/lib/utils/cog.ts` re-exports these same bindings for in-repo consumers.
6
6
 
7
- > The render-pipeline helpers (`selectCogPipeline`, `createConfigurableGetTileData`, `normalizeCogGeotiff`, `createEpsgResolver`, `fitCogBounds`, `renderNonTiledBitmap`, etc.) live in the same source file but are **not** re-exported from `@walkthru-earth/objex-utils` because they pull in `@developmentseed/deck.gl-geotiff`, `@developmentseed/geotiff`, `@developmentseed/proj`, `proj4`, and `maplibre-gl`. If you need them, depend on the full Svelte package [`@walkthru-earth/objex`](https://www.npmjs.com/package/@walkthru-earth/objex) (they are re-exported from `src/lib/index.ts`) and install those optional peers yourself.
7
+ > The render-pipeline helpers (`selectCogPipeline`, `createConfigurableGetTileData`, `normalizeCogGeotiff`, `createEpsgResolver`, `fitCogBounds`, `renderNonTiledBitmap`, etc.) live in `src/lib/utils/cog.ts` but are **not** re-exported from `@walkthru-earth/objex-utils` because they pull in `@developmentseed/deck.gl-geotiff`, `@developmentseed/geotiff`, `@developmentseed/proj`, `proj4`, and `maplibre-gl`. If you need them, depend on the full Svelte package [`@walkthru-earth/objex`](https://www.npmjs.com/package/@walkthru-earth/objex) (they are re-exported from `src/lib/index.ts`) and install those optional peers yourself.
8
8
 
9
9
  ## Types
10
10
 
package/docs/errors.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  One function. Normalizes thrown values into a displayable string, with special-casing for `AbortError`.
4
4
 
5
- Source: `src/lib/utils/error.ts`.
5
+ Source: `packages/objex-utils/src/error.ts`.
6
6
 
7
7
  ## `handleLoadError(err)`
8
8
 
package/docs/file-sort.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Sort a `FileEntry[]` with directories pinned on top. Pure — no mutation of the input.
4
4
 
5
- Source: `src/lib/utils/file-sort.ts`.
5
+ Source: `packages/objex-utils/src/file-sort.ts`.
6
6
 
7
7
  ## Types
8
8
 
@@ -4,10 +4,10 @@ Display formatters, column-type classification, hex dump, and data serialization
4
4
 
5
5
  Sources:
6
6
 
7
- - `src/lib/utils/format.ts`
8
- - `src/lib/utils/column-types.ts`
9
- - `src/lib/utils/hex.ts`
10
- - `src/lib/utils/export.ts`
7
+ - `packages/objex-utils/src/format.ts`
8
+ - `packages/objex-utils/src/column-types.ts`
9
+ - `packages/objex-utils/src/hex.ts`
10
+ - `packages/objex-utils/src/export.ts`
11
11
 
12
12
  ## Formatters
13
13
 
package/docs/geometry.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  WKB parsing, geometry-column detection, and GeoArrow table construction. Zero-copy where possible.
4
4
 
5
- Source: `src/lib/utils/wkb.ts`, `src/lib/utils/geoarrow.ts`.
5
+ Source: `packages/objex-utils/src/wkb.ts`, `packages/objex-utils/src/geoarrow.ts`.
6
6
 
7
7
  ## Types
8
8
 
@@ -51,7 +51,7 @@ type GeoArrowGeomType =
51
51
  | 'multipolygon';
52
52
  ```
53
53
 
54
- Lowercase normalized form used by the GeoArrow builder and `@geoarrow/deck.gl-layers`.
54
+ Lowercase normalized form used by the GeoArrow builder and `@geoarrow/deck.gl-geoarrow`.
55
55
 
56
56
  ### `GeoArrowResult`
57
57
 
@@ -152,7 +152,7 @@ function buildGeoArrowTables(
152
152
  ): GeoArrowResult[]
153
153
  ```
154
154
 
155
- Build one or more Arrow `Table` objects keyed by geometry type, ready for `@geoarrow/deck.gl-layers`.
155
+ Build one or more Arrow `Table` objects keyed by geometry type, ready for `@geoarrow/deck.gl-geoarrow`.
156
156
 
157
157
  **Parameters**
158
158
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  SSR-safe JSON persistence on top of `window.localStorage`.
4
4
 
5
- Source: `src/lib/utils/local-storage.ts`.
5
+ Source: `packages/objex-utils/src/local-storage.ts`.
6
6
 
7
7
  ## Functions
8
8