@walkthru-earth/objex 1.2.1 → 1.3.1

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/README.md +6 -3
  2. package/dist/components/layout/ConnectionDialog.svelte +35 -3
  3. package/dist/components/layout/Sidebar.svelte +1 -2
  4. package/dist/components/viewers/CodeViewer.svelte +51 -14
  5. package/dist/components/viewers/CodeViewer.svelte.d.ts +11 -1
  6. package/dist/components/viewers/CogControls.svelte +151 -22
  7. package/dist/components/viewers/CogControls.svelte.d.ts +5 -1
  8. package/dist/components/viewers/CogViewer.svelte +75 -8
  9. package/dist/components/viewers/MultiCogViewer.svelte +416 -0
  10. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +9 -0
  11. package/dist/components/viewers/StacMapViewer.svelte +19 -5
  12. package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
  13. package/dist/components/viewers/StacMosaicViewer.svelte +785 -0
  14. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +9 -0
  15. package/dist/components/viewers/StacTabViewer.svelte +254 -0
  16. package/dist/components/viewers/StacTabViewer.svelte.d.ts +13 -0
  17. package/dist/components/viewers/ViewerRouter.svelte +155 -2
  18. package/dist/components/viewers/ViewerRouter.svelte.d.ts +1 -1
  19. package/dist/components/viewers/ZarrMapViewer.svelte +143 -4
  20. package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +8 -2
  21. package/dist/components/viewers/ZarrViewer.svelte +1 -0
  22. package/dist/i18n/ar.js +27 -0
  23. package/dist/i18n/en.js +27 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +2 -0
  26. package/dist/query/stac-geoparquet.d.ts +31 -0
  27. package/dist/query/stac-geoparquet.js +136 -0
  28. package/dist/stores/connections.svelte.d.ts +38 -23
  29. package/dist/stores/connections.svelte.js +105 -114
  30. package/dist/utils/cog-pure.d.ts +25 -0
  31. package/dist/utils/cog-pure.js +35 -0
  32. package/dist/utils/cog.d.ts +88 -43
  33. package/dist/utils/cog.js +192 -152
  34. package/dist/utils/colormap-sprite.d.ts +39 -0
  35. package/dist/utils/colormap-sprite.js +77 -0
  36. package/dist/utils/connection-identity.d.ts +51 -0
  37. package/dist/utils/connection-identity.js +97 -0
  38. package/dist/utils/host-detection.js +48 -302
  39. package/dist/utils/parquet-metadata.d.ts +7 -1
  40. package/dist/utils/parquet-metadata.js +35 -1
  41. package/dist/utils/stac-geoparquet.d.ts +90 -0
  42. package/dist/utils/stac-geoparquet.js +223 -0
  43. package/dist/utils/stac-hydrate.d.ts +38 -0
  44. package/dist/utils/stac-hydrate.js +243 -0
  45. package/dist/utils/stac.d.ts +136 -0
  46. package/dist/utils/stac.js +176 -0
  47. package/dist/utils/storage-url.d.ts +26 -0
  48. package/dist/utils/storage-url.js +164 -28
  49. package/dist/utils/zarr.d.ts +34 -0
  50. package/dist/utils/zarr.js +94 -0
  51. package/package.json +14 -13
@@ -1,13 +1,56 @@
1
1
  import { STORAGE_KEYS } from '../constants.js';
2
+ import { connectionIdentityKey } from '../utils/connection-identity.js';
2
3
  import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
3
4
  import { credentialStore, storeToNative } from './credentials.svelte.js';
4
- // ---------------------------------------------------------------------------
5
- // Store
6
- // ---------------------------------------------------------------------------
5
+ function toConnection(id, config) {
6
+ return {
7
+ id,
8
+ name: config.name,
9
+ provider: config.provider,
10
+ endpoint: config.endpoint,
11
+ bucket: config.bucket,
12
+ region: config.region,
13
+ anonymous: config.anonymous,
14
+ authMethod: config.authMethod,
15
+ rootPrefix: config.rootPrefix
16
+ };
17
+ }
18
+ function applyCredentials(id, config) {
19
+ if (config.anonymous) {
20
+ credentialStore.remove(id);
21
+ return;
22
+ }
23
+ if (config.sas_token) {
24
+ const creds = { type: 'sas-token', sasToken: config.sas_token };
25
+ credentialStore.set(id, creds);
26
+ storeToNative(id, creds).catch(() => { });
27
+ return;
28
+ }
29
+ if (config.access_key && config.secret_key) {
30
+ const creds = {
31
+ type: 'sigv4',
32
+ accessKey: config.access_key,
33
+ secretKey: config.secret_key
34
+ };
35
+ credentialStore.set(id, creds);
36
+ storeToNative(id, creds).catch(() => { });
37
+ return;
38
+ }
39
+ credentialStore.remove(id);
40
+ }
7
41
  function createConnectionsStore() {
8
42
  let connections = $state([]);
9
43
  let loaded = $state(false);
10
44
  let dialogRequest = $state(0);
45
+ function persist() {
46
+ persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
47
+ }
48
+ function findByIdentity(input, excludeId) {
49
+ const key = connectionIdentityKey(input);
50
+ if (!key)
51
+ return undefined;
52
+ return connections.find((c) => c.id !== excludeId && connectionIdentityKey(c) === key);
53
+ }
11
54
  return {
12
55
  get items() {
13
56
  return connections;
@@ -17,7 +60,7 @@ function createConnectionsStore() {
17
60
  },
18
61
  /**
19
62
  * Load connections from localStorage.
20
- * Safe to call multiple times subsequent calls are no-ops.
63
+ * Safe to call multiple times, subsequent calls are no-ops.
21
64
  */
22
65
  async load() {
23
66
  if (loaded)
@@ -25,112 +68,58 @@ function createConnectionsStore() {
25
68
  connections = loadFromStorage(STORAGE_KEYS.CONNECTIONS, []);
26
69
  loaded = true;
27
70
  },
28
- /**
29
- * Force-reload connections.
30
- */
31
71
  async reload() {
32
72
  loaded = false;
33
73
  await this.load();
34
74
  },
35
75
  /**
36
- * Save a new connection to localStorage.
76
+ * Persist a connection. If an existing connection shares the same
77
+ * identity (see `connectionIdentityKey`), it's reused and its
78
+ * credentials are refreshed from the new config instead of spawning
79
+ * a duplicate record. Returns `{ id, existed }` so UI can distinguish
80
+ * "created" from "merged".
37
81
  */
38
82
  async save(config) {
39
- const id = crypto.randomUUID();
40
- const conn = {
41
- id,
42
- name: config.name,
43
- provider: config.provider,
44
- endpoint: config.endpoint,
45
- bucket: config.bucket,
46
- region: config.region,
47
- anonymous: config.anonymous,
48
- authMethod: config.authMethod,
49
- rootPrefix: config.rootPrefix
50
- };
51
- connections = [...connections, conn];
52
- persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
53
- // Store credentials in memory (never persisted to localStorage).
54
- if (!config.anonymous) {
55
- if (config.sas_token) {
56
- const creds = { type: 'sas-token', sasToken: config.sas_token };
57
- credentialStore.set(id, creds);
58
- storeToNative(id, creds).catch(() => { });
59
- }
60
- else if (config.access_key && config.secret_key) {
61
- const creds = {
62
- type: 'sigv4',
63
- accessKey: config.access_key,
64
- secretKey: config.secret_key
65
- };
66
- credentialStore.set(id, creds);
67
- storeToNative(id, creds).catch(() => { });
68
- }
83
+ const existing = findByIdentity(config);
84
+ if (existing) {
85
+ applyCredentials(existing.id, config);
86
+ return { id: existing.id, existed: true };
69
87
  }
70
- return id;
88
+ const id = crypto.randomUUID();
89
+ connections = [...connections, toConnection(id, config)];
90
+ persist();
91
+ applyCredentials(id, config);
92
+ return { id, existed: false };
71
93
  },
72
94
  /**
73
- * Update an existing connection.
95
+ * Update an existing connection. Throws `DuplicateConnectionError`
96
+ * when the new identity would collide with a different saved row,
97
+ * rather than silently overwriting and leaving a phantom duplicate.
74
98
  */
75
99
  async update(id, config) {
76
100
  const idx = connections.findIndex((c) => c.id === id);
77
101
  if (idx === -1)
78
102
  return false;
79
- connections[idx] = {
80
- ...connections[idx],
81
- name: config.name,
82
- provider: config.provider,
83
- endpoint: config.endpoint,
84
- bucket: config.bucket,
85
- region: config.region,
86
- anonymous: config.anonymous,
87
- authMethod: config.authMethod,
88
- rootPrefix: config.rootPrefix
89
- };
103
+ const collision = findByIdentity(config, id);
104
+ if (collision) {
105
+ throw new DuplicateConnectionError(collision.id, collision.name);
106
+ }
107
+ connections[idx] = toConnection(id, config);
90
108
  connections = [...connections];
91
- persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
109
+ persist();
92
110
  // Invalidate cached adapter for this connection
93
111
  import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(id));
94
- // Update in-memory credentials.
95
- if (!config.anonymous) {
96
- if (config.sas_token) {
97
- const creds = { type: 'sas-token', sasToken: config.sas_token };
98
- credentialStore.set(id, creds);
99
- storeToNative(id, creds).catch(() => { });
100
- }
101
- else if (config.access_key && config.secret_key) {
102
- const creds = {
103
- type: 'sigv4',
104
- accessKey: config.access_key,
105
- secretKey: config.secret_key
106
- };
107
- credentialStore.set(id, creds);
108
- storeToNative(id, creds).catch(() => { });
109
- }
110
- else {
111
- credentialStore.remove(id);
112
- }
113
- }
114
- else {
115
- credentialStore.remove(id);
116
- }
112
+ applyCredentials(id, config);
117
113
  return true;
118
114
  },
119
- /**
120
- * Remove a connection by ID.
121
- */
122
115
  async remove(id) {
123
116
  const before = connections.length;
124
117
  connections = connections.filter((c) => c.id !== id);
125
- persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
118
+ persist();
126
119
  credentialStore.remove(id);
127
- // Invalidate cached adapter for this connection
128
120
  import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(id));
129
121
  return connections.length < before;
130
122
  },
131
- /**
132
- * Test whether a connection is reachable via a lightweight list.
133
- */
134
123
  async test(id) {
135
124
  const { getAdapter } = await import('../storage/index.js');
136
125
  const adapter = getAdapter('remote', id);
@@ -143,23 +132,11 @@ function createConnectionsStore() {
143
132
  */
144
133
  async testWithConfig(config, existingId) {
145
134
  const tempId = existingId ?? `temp-test-${Date.now()}`;
146
- const tempConn = {
147
- id: tempId,
148
- name: config.name,
149
- provider: config.provider,
150
- endpoint: config.endpoint,
151
- bucket: config.bucket,
152
- region: config.region,
153
- anonymous: config.anonymous,
154
- authMethod: config.authMethod,
155
- rootPrefix: config.rootPrefix
156
- };
157
- // Temporarily register connection + credentials so the adapter can find them
135
+ const tempConn = toConnection(tempId, config);
158
136
  const hadConn = connections.some((c) => c.id === tempId);
159
137
  const prevCreds = credentialStore.get(tempId);
160
138
  if (!hadConn) {
161
139
  connections = [...connections, tempConn];
162
- // Don't persist — this is a temp test connection
163
140
  }
164
141
  if (!config.anonymous) {
165
142
  if (config.sas_token) {
@@ -180,56 +157,55 @@ function createConnectionsStore() {
180
157
  return true;
181
158
  }
182
159
  finally {
183
- // Cleanup: remove temp connection if we added it
184
160
  if (!hadConn) {
185
161
  connections = connections.filter((c) => c.id !== tempId);
186
- // Don't persist — was never in localStorage
187
162
  }
188
- // Restore previous credentials or remove temp ones
189
163
  if (prevCreds) {
190
164
  credentialStore.set(tempId, prevCreds);
191
165
  }
192
166
  else if (!hadConn) {
193
167
  credentialStore.remove(tempId);
194
168
  }
195
- // Also clear any cached adapter for the temp connection
196
169
  import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(tempId));
197
170
  }
198
171
  },
199
- /** True when a dialog open has been requested and not yet consumed. */
200
172
  get dialogRequested() {
201
173
  return dialogRequest > 0;
202
174
  },
203
- /** Request opening the new-connection dialog from anywhere. */
204
175
  requestDialog() {
205
176
  dialogRequest++;
206
177
  },
207
- /** Mark the dialog request as consumed. */
208
178
  clearDialogRequest() {
209
179
  dialogRequest = 0;
210
180
  },
211
- /**
212
- * Synchronous lookup by ID (from the already-loaded list).
213
- */
214
181
  getById(id) {
215
182
  return connections.find((c) => c.id === id);
216
183
  },
217
184
  /**
218
- * Find an existing connection that matches bucket + endpoint.
185
+ * Find an already-saved connection that matches the canonical identity
186
+ * of `input` (provider + bucket + endpoint/region per provider rules).
187
+ * Used by auto-detect, manual-add dedup, and edit-collision checks.
219
188
  */
220
- findByBucketEndpoint(bucket, endpoint) {
221
- return connections.find((c) => c.bucket === bucket && c.endpoint === endpoint);
189
+ findByIdentity(input) {
190
+ return findByIdentity(input);
222
191
  },
223
192
  /**
224
- * Create a connection from a DetectedHost, deduplicating by bucket+endpoint.
225
- * Returns the connection ID (existing or newly created).
193
+ * Auto-connect path for a URL-detected bucket. Reuses an existing
194
+ * connection when identity matches, otherwise creates one anonymously.
195
+ * Always returns the final connection ID.
226
196
  */
227
197
  async saveHostConnection(detected) {
228
- const existing = this.findByBucketEndpoint(detected.bucket, detected.endpoint);
198
+ const identity = {
199
+ provider: detected.provider,
200
+ endpoint: detected.endpoint,
201
+ bucket: detected.bucket,
202
+ region: detected.region
203
+ };
204
+ const existing = findByIdentity(identity);
229
205
  if (existing)
230
206
  return existing.id;
231
207
  const name = detected.bucket === '$web' ? `Azure Static Web` : detected.bucket;
232
- const id = await this.save({
208
+ const result = await this.save({
233
209
  name,
234
210
  provider: detected.provider === 'unknown' ? 's3' : detected.provider,
235
211
  endpoint: detected.endpoint,
@@ -238,9 +214,24 @@ function createConnectionsStore() {
238
214
  anonymous: true,
239
215
  rootPrefix: detected.rootPrefix || undefined
240
216
  });
241
- return id;
217
+ return result.id;
242
218
  }
243
219
  };
244
220
  }
221
+ /**
222
+ * Thrown by `update()` when the proposed identity collides with a different
223
+ * saved connection. Lets the UI tell the user which connection already owns
224
+ * that identity instead of silently producing a phantom duplicate.
225
+ */
226
+ export class DuplicateConnectionError extends Error {
227
+ existingId;
228
+ existingName;
229
+ constructor(existingId, existingName) {
230
+ super(`A connection already exists for this bucket: "${existingName}"`);
231
+ this.name = 'DuplicateConnectionError';
232
+ this.existingId = existingId;
233
+ this.existingName = existingName;
234
+ }
235
+ }
245
236
  export const connectionStore = createConnectionsStore();
246
237
  export { connectionStore as connections };
@@ -0,0 +1,25 @@
1
+ /** SampleFormat tag value → human label. */
2
+ export declare const SF_LABELS: Record<number, string>;
3
+ export interface GeoBounds {
4
+ west: number;
5
+ south: number;
6
+ east: number;
7
+ north: number;
8
+ }
9
+ export interface CogInfo {
10
+ width: number;
11
+ height: number;
12
+ bandCount: number;
13
+ dataType: string;
14
+ bounds: GeoBounds;
15
+ downsampled?: boolean;
16
+ }
17
+ /** Safely clamp a number to a range, treating NaN/Infinity as the fallback. */
18
+ export declare function safeClamp(v: number, lo: number, hi: number, fallback: number): number;
19
+ /** Clamp geographic bounds to valid MapLibre web-Mercator range. */
20
+ export declare function clampBounds(b: GeoBounds): GeoBounds;
21
+ /**
22
+ * Build a data-type label from GeoTIFF sample format and bits per sample.
23
+ * e.g. "uint8", "float32", "int16"
24
+ */
25
+ export declare function buildDataTypeLabel(sampleFormat: number, bitsPerSample: number): string;
@@ -0,0 +1,35 @@
1
+ // Dependency-free subset of `cog.ts` so that `@walkthru-earth/objex-utils`
2
+ // can re-export these helpers without dragging in `@developmentseed/epsg`,
3
+ // `@developmentseed/geotiff`, `@developmentseed/proj`, `proj4`, or
4
+ // `maplibre-gl`. tsup preserves bare side-effect imports from externalized
5
+ // modules even when all named bindings are tree-shaken, so the pure surface
6
+ // MUST live in a module that has zero heavy imports.
7
+ /** SampleFormat tag value → human label. */
8
+ export const SF_LABELS = {
9
+ 1: 'uint',
10
+ 2: 'int',
11
+ 3: 'float',
12
+ 4: 'void',
13
+ 5: 'complex int',
14
+ 6: 'complex float'
15
+ };
16
+ /** Safely clamp a number to a range, treating NaN/Infinity as the fallback. */
17
+ export function safeClamp(v, lo, hi, fallback) {
18
+ return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : fallback;
19
+ }
20
+ /** Clamp geographic bounds to valid MapLibre web-Mercator range. */
21
+ export function clampBounds(b) {
22
+ return {
23
+ west: safeClamp(b.west, -180, 180, -180),
24
+ south: safeClamp(b.south, -85.051129, 85.051129, -85.051129),
25
+ east: safeClamp(b.east, -180, 180, 180),
26
+ north: safeClamp(b.north, -85.051129, 85.051129, 85.051129)
27
+ };
28
+ }
29
+ /**
30
+ * Build a data-type label from GeoTIFF sample format and bits per sample.
31
+ * e.g. "uint8", "float32", "int16"
32
+ */
33
+ export function buildDataTypeLabel(sampleFormat, bitsPerSample) {
34
+ return `${SF_LABELS[sampleFormat] ?? `sf${sampleFormat}`}${bitsPerSample ?? ''}`;
35
+ }
@@ -1,17 +1,20 @@
1
1
  import type { GetTileDataOptions, MinimalDataT } from '@developmentseed/deck.gl-geotiff';
2
- import type { RenderTileResult } from '@developmentseed/deck.gl-raster';
2
+ import type { RasterModule, RenderTileResult } from '@developmentseed/deck.gl-raster';
3
3
  import type { GeoTIFF as GeoTIFFType, Overview } from '@developmentseed/geotiff';
4
4
  import { GeoTIFF } from '@developmentseed/geotiff';
5
5
  import type { EpsgResolver } from '@developmentseed/proj';
6
+ import type { Device } from '@luma.gl/core';
6
7
  import type maplibregl from 'maplibre-gl';
7
- /** SampleFormat tag value human label. */
8
- export declare const SF_LABELS: Record<number, string>;
9
- export type ColorRampId = 'grayscale' | 'terrain' | 'viridis' | 'magma' | 'turbo' | 'spectral';
10
- export declare const COLOR_RAMP_STOPS: Record<ColorRampId, [number, number, number][]>;
11
- /** Interpolate a normalized value (0..1) into an RGB color from a ramp. */
12
- export declare function interpolateRamp(stops: [number, number, number][], t: number): [number, number, number];
13
- /** Generate a CSS linear-gradient string for a color ramp. */
14
- export declare function rampToGradientCss(id: ColorRampId): string;
8
+ import { buildDataTypeLabel, type CogInfo, clampBounds, type GeoBounds, SF_LABELS, safeClamp } from './cog-pure.js';
9
+ import { type ColormapName } from './colormap-sprite.js';
10
+ export { buildDataTypeLabel, type CogInfo, clampBounds, type GeoBounds, SF_LABELS, safeClamp };
11
+ /**
12
+ * Any of the 107 named ramps shipped in `@developmentseed/deck.gl-raster`'s
13
+ * `colormaps.png` sprite (matplotlib + rio-tiler + cmocean). Rendering is
14
+ * GPU-side via the `Colormap` shader module; switching ramps is a uniform
15
+ * update, no tile re-decode.
16
+ */
17
+ export type ColorRampId = ColormapName;
15
18
  export interface BandConfig {
16
19
  mode: 'rgb' | 'single';
17
20
  /** 0-indexed band indices for RGB channels */
@@ -78,6 +81,18 @@ export declare function createRescaledPipeline(geotiff: GeoTIFFType, rescale: Re
78
81
  getTileData: (image: GeoTIFFType | Overview, options: GetTileDataOptions) => Promise<MinimalDataT>;
79
82
  renderTile: (data: MinimalDataT) => RenderTileResult;
80
83
  };
84
+ export interface BandRenderPipelineOptions {
85
+ /** Value treated as "no-data" and zeroed out by `FilterNoDataVal`. */
86
+ noDataVal?: number | null;
87
+ /** Linear rescale applied after no-data masking. Omit for no rescaling. */
88
+ rescale?: RescaleConfig;
89
+ }
90
+ /**
91
+ * Build a `renderPipeline` array for `MultiCOGLayer` / raster mosaics.
92
+ * Combines optional `FilterNoDataVal` + `LinearRescale` stages in the order
93
+ * the GPU expects (no-data mask first, then rescale).
94
+ */
95
+ export declare function buildBandRenderPipeline(opts?: BandRenderPipelineOptions): RasterModule[];
81
96
  /**
82
97
  * Apply the two upstream-bug workarounds a GeoTIFF needs before being handed
83
98
  * to `COGLayer`:
@@ -123,29 +138,6 @@ export interface SelectCogPipelineOptions {
123
138
  * viewers can call it per sub-COG without re-implementing the decision tree.
124
139
  */
125
140
  export declare function selectCogPipeline(geotiff: GeoTIFFType, opts?: SelectCogPipelineOptions): ResolvedCogPipeline;
126
- export interface GeoBounds {
127
- west: number;
128
- south: number;
129
- east: number;
130
- north: number;
131
- }
132
- export interface CogInfo {
133
- width: number;
134
- height: number;
135
- bandCount: number;
136
- dataType: string;
137
- bounds: GeoBounds;
138
- downsampled?: boolean;
139
- }
140
- /** Safely clamp a number to a range, treating NaN/Infinity as the fallback. */
141
- export declare function safeClamp(v: number, lo: number, hi: number, fallback: number): number;
142
- /** Clamp geographic bounds to valid MapLibre web-Mercator range. */
143
- export declare function clampBounds(b: GeoBounds): GeoBounds;
144
- /**
145
- * Build a data-type label from GeoTIFF sample format and bits per sample.
146
- * e.g. "uint8", "float32", "int16"
147
- */
148
- export declare function buildDataTypeLabel(sampleFormat: number, bitsPerSample: number): string;
149
141
  /**
150
142
  * Query the GPU's MAX_TEXTURE_SIZE from MapLibre's WebGL context.
151
143
  * Falls back to 4096 (lowest common denominator for mobile GPUs).
@@ -178,42 +170,95 @@ export interface CustomTileData {
178
170
  imageData: ImageData;
179
171
  width: number;
180
172
  height: number;
173
+ /**
174
+ * `sampler2DArray` colormap texture for single-band renders. Set by
175
+ * `createConfigurableGetTileData` / `createCustomGetTileData` when the
176
+ * first tile resolves the device-bound sprite texture; `undefined` for
177
+ * RGB-mode tiles (no colormap needed). Passed through to `renderTile`
178
+ * so the Colormap shader module can bind it on every layer.
179
+ */
180
+ colormapTexture?: Texture;
181
+ /**
182
+ * Normalized `color.r` sentinel value for nodata pixels in single-band
183
+ * mode. The `Colormap` shader module overwrites all 4 output channels
184
+ * from the 1D ramp sample, destroying the α=0 flag, so we reserve
185
+ * `r = 0` for nodata and renormalize valid data into `(0, 1]`.
186
+ * `FilterNoDataVal` then discards matching fragments before the ramp
187
+ * lookup. `undefined` for RGB tiles.
188
+ */
189
+ nodataSentinel?: number;
190
+ /**
191
+ * Per-tile 64-bin normalized histogram (0..1, nodata excluded) baked during
192
+ * single-band CPU decoding. `undefined` for RGB tiles. deck.gl's TileLayer
193
+ * caches the returned tile object, so this array is retained alongside the
194
+ * bitmap without a rebake on pan/zoom revisits. Summing the histograms of
195
+ * currently-visible tiles, via the TileLayer `onViewportLoad` hook, gives a
196
+ * cloud-native "histogram of what COG tiles the viewport currently shows at
197
+ * the active overview level", matching COG pyramid behavior.
198
+ */
199
+ histogram?: Uint32Array;
181
200
  }
201
+ type Texture = import('@luma.gl/core').Texture;
182
202
  /**
183
203
  * Check whether a GeoTIFF needs a custom render pipeline.
184
204
  * v0.3's inferRenderPipeline only supports unsigned integers (SampleFormat 1).
185
205
  * Signed int (2) and float (3) need custom getTileData/renderTile.
186
206
  */
187
207
  export declare function needsCustomPipeline(geotiff: GeoTIFFType): boolean;
208
+ /**
209
+ * Shared options for the CPU tile-baking factories.
210
+ *
211
+ * The previous `onHistogram` callback accumulated a single closure-owned buffer
212
+ * across every tile ever baked, which grew unbounded on pan/zoom and never
213
+ * reflected "what the viewport currently shows". Histograms are now attached
214
+ * per tile to `CustomTileData.histogram` and aggregated by the viewer from
215
+ * TileLayer's `onViewportLoad(visibleTiles)` hook, matching COG overview-level
216
+ * behavior (few big tiles when zoomed out, small AOI-scoped tiles when zoomed
217
+ * in) and reusing deck.gl's tile cache for free.
218
+ */
219
+ export type CustomGetTileDataOptions = Record<string, never>;
220
+ /** Number of histogram buckets produced by the CPU bake. */
221
+ export declare const HISTOGRAM_BIN_COUNT = 64;
188
222
  /**
189
223
  * Create custom getTileData for non-uint COGs.
190
224
  * Reads band 0, normalizes using GDAL statistics / per-tile adaptive stretch,
191
- * applies terrain color ramp for single-band data.
225
+ * bakes a grayscale `r`-channel image so the GPU `Colormap` shader module
226
+ * (wired downstream by `selectCogPipeline`) can apply the ramp by sampling
227
+ * `colormaps.png`. Reserves `r = 0` for nodata so `FilterNoDataVal` can
228
+ * discard those fragments before the ramp sample.
192
229
  */
193
- export declare function createCustomGetTileData(geotiff: GeoTIFFType): (image: GeoTIFFType | Overview, options: {
230
+ export declare function createCustomGetTileData(geotiff: GeoTIFFType, _opts?: CustomGetTileDataOptions): (image: GeoTIFFType | Overview, options: {
194
231
  x: number;
195
232
  y: number;
196
233
  pool: unknown;
197
234
  signal?: AbortSignal;
235
+ device: Device;
198
236
  }) => Promise<CustomTileData>;
199
237
  /**
200
- * Custom renderTile for non-uint COGs.
201
- * v0.5 RasterLayer requires a RenderTileResult with `image` or `renderPipeline`.
202
- * We produce an ImageData and pass it through the `image` slot. deck.gl manages
203
- * the texture lifecycle and prepends a CreateTexture module automatically.
238
+ * Custom renderTile for COGs that use the CPU pipeline. For RGB mode (and
239
+ * legacy multi-band non-uint), the `image` slot carries a fully-baked RGBA
240
+ * `ImageData` and there is nothing to append on the GPU. For single-band
241
+ * mode, the image carries a normalized `r`-channel and this function
242
+ * appends `FilterNoDataVal` (to discard r=0 nodata sentinels), optional
243
+ * `LinearRescale` (brightness/contrast slider), and the sprite-based
244
+ * `Colormap` module so switching ramps is a uniform update — no tile
245
+ * re-decode required. The `colormapTexture` is stashed on `data` by the
246
+ * corresponding `getTileData` factory; if the sprite failed to resolve we
247
+ * fall back to the plain grayscale image.
204
248
  */
205
- export declare function customRenderTile(data: CustomTileData): {
206
- image: ImageData;
207
- };
249
+ export declare function buildCustomRenderTile(config: BandConfig, rescale?: RescaleConfig): (data: CustomTileData) => RenderTileResult;
208
250
  /**
209
251
  * Create a configurable getTileData that respects BandConfig.
210
- * Supports both RGB mode (multi-band → R,G,B) and single-band mode (color ramp).
252
+ * Supports RGB mode (multi-band → R,G,B with alpha=255, fully baked) and
253
+ * single-band mode (band N normalized into the `r` channel; the ramp is
254
+ * applied downstream by the GPU `Colormap` module via `buildCustomRenderTile`).
211
255
  */
212
- export declare function createConfigurableGetTileData(geotiff: GeoTIFFType, config: BandConfig): (image: GeoTIFFType | Overview, options: {
256
+ export declare function createConfigurableGetTileData(geotiff: GeoTIFFType, config: BandConfig, _opts?: CustomGetTileDataOptions): (image: GeoTIFFType | Overview, options: {
213
257
  x: number;
214
258
  y: number;
215
259
  pool: unknown;
216
260
  signal?: AbortSignal;
261
+ device: Device;
217
262
  }) => Promise<CustomTileData>;
218
263
  export interface PixelValue {
219
264
  lng: number;