@gisatcz/deckgl-geolib 1.12.0-dev.1 → 1.12.0-dev.3

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.
@@ -1,13 +1,9 @@
1
1
  /* eslint 'max-len': [1, { code: 100, comments: 999, ignoreStrings: true, ignoreUrls: true }] */
2
2
  // COG loading
3
- import { Tiff, TiffImage } from '@cogeotiff/core';
4
- import { SourceHttp } from '@chunkd/source-http';
3
+ import { fromUrl, GeoTIFF, GeoTIFFImage } from 'geotiff';
5
4
 
6
5
  // Image compression support
7
- import { inflate } from 'pako';
8
- import jpeg from 'jpeg-js';
9
6
  import { worldToLngLat } from '@math.gl/web-mercator';
10
- import LZWDecoder from './lzw';
11
7
 
12
8
  // Bitmap styling
13
9
  import GeoImage, { GeoImageOptions } from '../geoimage/geoimage.ts';
@@ -16,28 +12,32 @@ export type Bounds = [minX: number, minY: number, maxX: number, maxY: number];
16
12
 
17
13
  const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
18
14
  const EARTH_HALF_CIRCUMFERENCE = EARTH_CIRCUMFERENCE / 2;
15
+ const webMercatorOrigin = [-20037508.342789244, 20037508.342789244];
16
+ const webMercatorRes0 = 156543.03125;
19
17
 
20
18
  const CogTilesGeoImageOptionsDefaults = {
21
19
  blurredTexture: true,
22
20
  };
23
21
 
24
22
  class CogTiles {
25
- cog: Tiff;
23
+ cog: GeoTIFF;
24
+
25
+ cogZoomLookup = [0];
26
+
27
+ cogResolutionLookup = [0];
28
+
29
+ cogOrigin = [0, 0];
26
30
 
27
31
  zoomRange = [0, 0];
28
32
 
29
33
  tileSize: number;
30
34
 
31
- lowestOriginTileOffset = [0, 0];
32
-
33
- lowestOriginTileSize = 0;
35
+ bounds: Bounds;
34
36
 
35
37
  loaded: boolean = false;
36
38
 
37
39
  geo: GeoImage = new GeoImage();
38
40
 
39
- lzw: LZWDecoder = new LZWDecoder();
40
-
41
41
  options: GeoImageOptions;
42
42
 
43
43
  constructor(options: GeoImageOptions) {
@@ -45,48 +45,32 @@ class CogTiles {
45
45
  }
46
46
 
47
47
  async initializeCog(url: string) {
48
- // Set native fetch instead node-fetch to SourceHttp
49
- SourceHttp.fetch = async (input, init) => {
50
- const res = await fetch(input, init);
51
- return res;
52
- };
53
-
54
- const source = new SourceHttp(url);
55
- this.cog = await Tiff.create(source);
56
-
57
- this.cog.images.forEach((image:TiffImage) => {
58
- image.loadGeoTiffTags();
59
- });
60
-
61
- this.tileSize = this.getTileSize(this.cog);
62
-
63
- this.lowestOriginTileOffset = this.getImageTileIndex(
64
- this.cog.images[this.cog.images.length - 1],
65
- );
66
-
67
- this.zoomRange = this.getZoomRange(this.cog);
68
-
69
- return this.cog;
48
+ this.cog = await fromUrl(url);
49
+ const image = await this.cog.getImage(); // by default, the first image is read.
50
+ this.cogOrigin = image.getOrigin();
51
+ this.options.noDataValue ??= this.getNoDataValue(image);
52
+ this.options.format ??= this.getDataTypeFromTags(image);
53
+ this.options.numOfChannels = this.getNumberOfChannels(image);
54
+ this.options.planarConfig = this.getPlanarConfiguration(image);
55
+ [this.cogZoomLookup, this.cogResolutionLookup] = await this.buildCogZoomResolutionLookup(this.cog);
56
+ this.tileSize = image.getTileWidth();
57
+ this.zoomRange = this.calculateZoomRange(image, await this.cog.getImageCount());
58
+ this.bounds = this.calculateBoundsAsLatLon(image);
70
59
  }
71
60
 
72
- getTileSize(cog: Tiff) {
73
- return cog.images[cog.images.length - 1].tileSize.width;
61
+ getZoomRange() {
62
+ return this.zoomRange;
74
63
  }
75
64
 
76
- getZoomRange(cog: Tiff) {
77
- const img = cog.images[cog.images.length - 1];
78
-
79
- const minZoom = this.getZoomLevelFromResolution(
80
- cog.images[cog.images.length - 1].tileSize.width,
81
- img.resolution[0],
82
- );
83
- const maxZoom = minZoom + (cog.images.length - 1);
65
+ calculateZoomRange(img: GeoTIFFImage, imgCount: number) {
66
+ const maxZoom = this.getZoomLevelFromResolution(img.getTileWidth(), img.getResolution()[0]);
67
+ const minZoom = maxZoom - (imgCount - 1);
84
68
 
85
69
  return [minZoom, maxZoom];
86
70
  }
87
71
 
88
- getBoundsAsLatLon(cog: Tiff) {
89
- const { bbox } = cog.images[cog.images.length - 1];
72
+ calculateBoundsAsLatLon(image: GeoTIFFImage) {
73
+ const bbox = image.getBoundingBox();
90
74
 
91
75
  const minX = Math.min(bbox[0], bbox[2]);
92
76
  const maxX = Math.max(bbox[0], bbox[2]);
@@ -99,40 +83,14 @@ class CogTiles {
99
83
  return [minXYDeg[0], minXYDeg[1], maxXYDeg[0], maxXYDeg[1]] as [number, number, number, number];
100
84
  }
101
85
 
102
- getOriginAsLatLon(cog: Tiff) {
103
- const { origin } = cog.images[cog.images.length - 1];
104
- return this.getLatLon(origin);
105
- }
106
-
107
- getImageTileIndex(img: TiffImage) {
108
- const ax = EARTH_HALF_CIRCUMFERENCE + img.origin[0];
109
- const ay = -(EARTH_HALF_CIRCUMFERENCE + (img.origin[1] - EARTH_CIRCUMFERENCE));
110
- // let mpt = img.resolution[0] * img.tileSize.width;
111
-
112
- const mpt = img.tileSize.width * this.getResolutionFromZoomLevel(
113
- img.tileSize.width,
114
- this.getZoomLevelFromResolution(
115
- img.tileSize.width,
116
- img.resolution[0],
117
- ),
118
- );
119
-
120
- const ox = Math.round(ax / mpt);
121
- const oy = Math.round(ay / mpt);
122
-
123
- const oz = this.getZoomLevelFromResolution(img.tileSize.width, img.resolution[0]);
124
-
125
- return [ox, oy, oz];
126
- }
127
-
128
- getResolutionFromZoomLevel(tileSize: number, z: number) {
129
- return (EARTH_CIRCUMFERENCE / tileSize) / (2 ** z);
130
- }
131
-
132
86
  getZoomLevelFromResolution(tileSize: number, resolution: number) {
133
87
  return Math.round(Math.log2(EARTH_CIRCUMFERENCE / (resolution * tileSize)));
134
88
  }
135
89
 
90
+ getBoundsAsLatLon() {
91
+ return this.bounds;
92
+ }
93
+
136
94
  getLatLon(input: number[]) {
137
95
  const ax = EARTH_HALF_CIRCUMFERENCE + input[0];
138
96
  const ay = -(EARTH_HALF_CIRCUMFERENCE + (input[1] - EARTH_CIRCUMFERENCE));
@@ -147,169 +105,315 @@ class CogTiles {
147
105
  return cartographicPositionAdjusted;
148
106
  }
149
107
 
150
- async getTile(x: number, y: number, z: number, bounds:Bounds, meshMaxError: number) {
151
- const wantedMpp = this.getResolutionFromZoomLevel(this.tileSize, z);
152
- const img = this.cog.getImageByResolution(wantedMpp);
153
- // await img.loadGeoTiffTags(1)
154
- let offset: number[] = [0, 0];
155
-
156
- if (z === this.zoomRange[0]) {
157
- offset = this.lowestOriginTileOffset;
158
- } else {
159
- const power = 2 ** (z - this.zoomRange[0]);
160
- offset[0] = Math.floor(this.lowestOriginTileOffset[0] * power);
161
- offset[1] = Math.floor(this.lowestOriginTileOffset[1] * power);
108
+ /**
109
+ * Builds lookup tables for zoom levels and estimated resolutions from a Cloud Optimized GeoTIFF (COG) object.
110
+ *
111
+ * It is assumed that inn web mapping, COG data is visualized in the Web Mercator coordinate system.
112
+ * At zoom level 0, the Web Mercator resolution is defined by the constant `webMercatorRes0`
113
+ * (e.g., 156543.03125 m/pixel). At each subsequent zoom level, this resolution is halved.
114
+ *
115
+ * This function calculates, for each image (overview) in the COG, its estimated resolution and
116
+ * corresponding zoom level based on the base image's resolution and width.
117
+ *
118
+ * @param {object} cog - A Cloud Optimized GeoTIFF object loaded via geotiff.js.
119
+ * @returns {Promise<[number[], number[]]>} A promise resolving to a tuple of two arrays:
120
+ * - The first array (`zoomLookup`) maps each image index to its computed zoom level.
121
+ * - The second array (`resolutionLookup`) maps each image index to its estimated resolution (m/pixel).
122
+ */
123
+ async buildCogZoomResolutionLookup(cog) {
124
+ // Retrieve the total number of images (overviews) in the COG.
125
+ const imageCount = await cog.getImageCount();
126
+
127
+ // Use the first image as the base reference.
128
+ const baseImage = await cog.getImage(0);
129
+ const baseResolution = baseImage.getResolution()[0]; // Resolution (m/pixel) of the base image.
130
+ const baseWidth = baseImage.getWidth();
131
+
132
+ // Initialize arrays to store the zoom level and resolution for each image.
133
+ const zoomLookup = [];
134
+ const resolutionLookup = [];
135
+
136
+ // Iterate over each image (overview) in the COG.
137
+ for (let idx = 0; idx < imageCount; idx++) {
138
+ const image = await cog.getImage(idx);
139
+ const width = image.getWidth();
140
+
141
+ // Calculate the scale factor relative to the base image.
142
+ const scaleFactor = baseWidth / width;
143
+ const estimatedResolution = baseResolution * scaleFactor;
144
+
145
+ // Calculate the zoom level using the Web Mercator resolution standard:
146
+ // webMercatorRes0 is the resolution at zoom level 0; each zoom level halves the resolution.
147
+ const zoomLevel = Math.round(Math.log2(webMercatorRes0 / estimatedResolution));
148
+ // console.log(`buildCogZoomResolutionLookup: Image index ${idx}: Estimated Resolution = ${estimatedResolution} m/pixel, Zoom Level = ${zoomLevel}`);
149
+
150
+ zoomLookup[idx] = zoomLevel;
151
+ resolutionLookup[idx] = estimatedResolution;
162
152
  }
163
- const tilesX = img.tileCount.x;
164
- const tilesY = img.tileCount.y;
165
- // console.log("------OFFSET IS------ " + offset[0] + " ; " + offset[1])
166
-
167
- const ox = offset[0];
168
- const oy = offset[1];
169
153
 
170
- // console.log("Asking for " + Math.floor(x - ox) + " : " + Math.floor(y - oy))
154
+ return [zoomLookup, resolutionLookup];
155
+ }
171
156
 
172
- let decompressed: string;
173
- let decoded: any;
157
+ /**
158
+ * Determines the appropriate image index from the Cloud Optimized GeoTIFF (COG)
159
+ * that best matches a given zoom level.
160
+ *
161
+ * This function utilizes precomputed lookup tables (`cogZoomLookup`) that map
162
+ * each image index in the COG to its corresponding zoom level. It ensures that
163
+ * the selected image index provides the closest resolution to the desired zoom level.
164
+ *
165
+ * @param {number} zoom - The target zoom level for which the image index is sought.
166
+ * @returns {number} The index of the image in the COG that best matches the specified zoom level.
167
+ */
168
+ getImageIndexForZoomLevel(zoom) {
169
+ // Retrieve the minimum and maximum zoom levels from the lookup table.
170
+ const minZoom = this.cogZoomLookup[this.cogZoomLookup.length - 1];
171
+ const maxZoom = this.cogZoomLookup[0];
172
+ if (zoom > maxZoom) return 0;
173
+ if (zoom < minZoom) return this.cogZoomLookup.length - 1;
174
+
175
+ // For zoom levels within the available range, find the exact or closest matching index.
176
+ const exactMatchIndex = this.cogZoomLookup.indexOf(zoom);
177
+ if (exactMatchIndex === -1) {
178
+ // TO DO improve the condition if the match index is not found
179
+ console.log('getImageIndexForZoomLevel: error in retrieving image by zoom index');
180
+ }
181
+ return exactMatchIndex;
182
+ }
174
183
 
175
- this.options.numOfChannels = Number(img.tags.get(277).value);
176
- this.options.noDataValue = this.getNoDataValue(img.tags);
184
+ async getTileFromImage(tileX, tileY, zoom) {
185
+ const imageIndex = this.getImageIndexForZoomLevel(zoom);
186
+ const targetImage = await this.cog.getImage(imageIndex);
177
187
 
178
- if (!this.options.format) {
179
- // More information about TIFF tags: https://www.awaresystems.be/imaging/tiff/tifftags.html
180
- this.options.format = this.getFormat(
181
- img.tags.get(339).value as Array<number>,
182
- img.tags.get(258).value as Array<number>,
183
- );
188
+ // Ensure the image is tiled
189
+ const tileWidth = targetImage.getTileWidth();
190
+ const tileHeight = targetImage.getTileHeight();
191
+ if (!tileWidth || !tileHeight) {
192
+ throw new Error('The image is not tiled.');
184
193
  }
185
194
 
186
- let bitsPerSample = img.tags.get(258)!.value;
187
- if (Array.isArray(bitsPerSample)) {
188
- if (this.options.type === 'terrain') {
189
- let c = 0;
190
- bitsPerSample.forEach((sample) => {
191
- c += sample;
195
+ // Calculate the map offset between the global Web Mercator origin and the COG's origin.
196
+ // (Difference in map units.)
197
+ // if X offset is large and positive (COG is far to the right of global origin)
198
+ // if Y offset is large and positive (COG is far below global origin — expected)
199
+ const offsetXMap = this.cogOrigin[0] - webMercatorOrigin[0];
200
+ const offsetYMap = webMercatorOrigin[1] - this.cogOrigin[1];
201
+
202
+ const tileResolution = (EARTH_CIRCUMFERENCE / tileWidth) / 2 ** zoom;
203
+ const cogResolution = this.cogResolutionLookup[imageIndex];
204
+
205
+ // Convert map offsets into pixel offsets.
206
+ const offsetXPixel = Math.floor(offsetXMap / tileResolution);
207
+ const offsetYPixel = Math.floor(offsetYMap / tileResolution);
208
+
209
+ // Calculate the pixel boundaries for the tile.
210
+ const window = [
211
+ tileX * tileWidth - offsetXPixel, // startX
212
+ tileY * tileHeight - offsetYPixel, // startY
213
+ (tileX + 1) * tileWidth - offsetXPixel, // endX (exclusive)
214
+ (tileY + 1) * tileHeight - offsetYPixel, // endY (exclusive)
215
+ ];
216
+
217
+ const [windowStartX, windowStartY, windowEndX, windowEndY] = window;
218
+
219
+ const imageHeight = targetImage.getHeight();
220
+ const imageWidth = targetImage.getWidth();
221
+
222
+ // Determine the effective (valid) window inside the image:
223
+ const effectiveStartX = Math.max(0, windowStartX);
224
+ const effectiveStartY = Math.max(0, windowStartY);
225
+ const effectiveEndX = windowEndX;
226
+ const effectiveEndY = windowEndY;
227
+
228
+ // Calculate how many pixels are missing from the left and top due to negative windowStart.
229
+ const missingLeft = Math.max(0, 0 - windowStartX);
230
+ const missingTop = Math.max(0, 0 - windowStartY);
231
+
232
+ // Read only the valid window from the image.
233
+ const validWindow = [effectiveStartX, effectiveStartY, effectiveEndX, effectiveEndY];
234
+
235
+ // Read the raster data for the tile window with shifted origin.
236
+ if (missingLeft > 0 || missingTop > 0) {
237
+ // Prepare the final tile buffer and fill it with noDataValue.
238
+ const tileBuffer = this.createTileBuffer(this.options.format, tileWidth);
239
+ tileBuffer.fill(this.options.noDataValue);
240
+
241
+ // Calculate the width of the valid window.
242
+ const validWidth = Math.min(imageWidth, effectiveEndX - effectiveStartX);
243
+ const validHeight = Math.min(imageHeight, effectiveEndY - effectiveStartY);
244
+
245
+ // if the valid window is smaller than tile size, it gets the image size width and height, thus validRasterData.width must be used as below
246
+ const validRasterData = await targetImage.readRasters({ window: validWindow });
247
+
248
+ // FOR MULTI-BAND - the result is one array with sequentially typed bands, firstly all data for the band 0, then for band 1
249
+ // I think this is less practical then the commented solution above, but I do it so it works with the code in geoimage.ts in deck.gl-geoimage in function getColorValue.
250
+ const validImageData = Array(validRasterData.length * validRasterData[0].length);
251
+ validImageData.fill(this.options.noDataValue);
252
+
253
+ // Place the valid pixel data into the tile buffer.
254
+ for (let band = 0; band < validRasterData.length; band++) {
255
+ for (let row = 0; row < validHeight; row++) {
256
+ for (let col = 0; col < validWidth; col++) {
257
+ // Compute the destination position in the tile buffer.
258
+ // We shift by the number of missing pixels (if any) at the top/left.
259
+ const destRow = missingTop + row;
260
+ const destCol = missingLeft + col;
261
+ if (destRow < tileWidth && destCol < tileHeight) {
262
+ tileBuffer[destRow * tileWidth + destCol] = validRasterData[band][row * validRasterData.width + col];
263
+ } else {
264
+ console.log('error in assigning data to tile buffer');
265
+ }
266
+ }
267
+ }
268
+ tileBuffer.forEach((rasterValue, index) => {
269
+ validImageData[index * this.options.numOfChannels + band] = rasterValue;
192
270
  });
193
- bitsPerSample = c;
194
- } else {
195
- [bitsPerSample] = bitsPerSample;
196
271
  }
272
+ return [validImageData];
197
273
  }
198
274
 
199
- // const samplesPerPixel = img.tags.get(277)!.value
200
- // console.log("Samples per pixel:" + samplesPerPixel)
201
- // console.log("Bits per sample: " + bitsPerSample)
202
- // console.log("Single channel pixel format: " + bitsPerSample/)
203
-
204
- if (x - ox >= 0 && y - oy >= 0 && x - ox < tilesX && y - oy < tilesY) {
205
- // console.log(`getting tile: ${[x - ox, y - oy]}`);
206
- const tile = await img.getTile((x - ox), (y - oy));
207
- // console.time("Request to data time: ")
208
-
209
- switch (img.compression) {
210
- case 'image/jpeg':
211
- decoded = jpeg.decode(tile!.bytes, { useTArray: true });
212
- break;
213
- case 'application/deflate':
214
- decoded = await inflate(tile!.bytes);
215
- break;
216
- case 'application/lzw':
217
- decoded = this.lzw.decodeBlock(tile!.bytes.buffer);
218
- break;
219
- default:
220
- console.warn(`Unexpected compression method: ${img.compression}`);
221
- }
275
+ // Read the raster data for the non shifted tile window.
276
+ const tileData = await targetImage.readRasters({ window, interleave: true });
277
+ // console.log(`data that starts at the left top corner of the tile ${tileX}, ${tileY}`);
278
+ return [tileData];
279
+ }
222
280
 
223
- let decompressedFormatted;
224
- // bitsPerSample = 8
225
-
226
- switch (this.options.format) {
227
- case 'uint8':
228
- decompressedFormatted = new Uint8Array(decoded.buffer); break;
229
- case 'uint16':
230
- decompressedFormatted = new Uint16Array(decoded.buffer); break;
231
- case 'uint32':
232
- decompressedFormatted = new Uint32Array(decoded.buffer); break;
233
- case 'int8':
234
- decompressedFormatted = new Int8Array(decoded.buffer); break;
235
- case 'int16':
236
- decompressedFormatted = new Int16Array(decoded.buffer); break;
237
- case 'int32':
238
- decompressedFormatted = new Int32Array(decoded.buffer); break;
239
- case 'float32':
240
- decompressedFormatted = new Float32Array(decoded.buffer); break;
241
- case 'float64':
242
- decompressedFormatted = new Float64Array(decoded.buffer); break;
243
- default: decompressedFormatted = null;
244
- }
281
+ async getTile(x: number, y: number, z: number, bounds:Bounds, meshMaxError: number) {
282
+ const tileData = await this.getTileFromImage(x, y, z);
283
+
284
+ return this.geo.getMap({
285
+ rasters: [tileData[0]],
286
+ width: this.tileSize,
287
+ height: this.tileSize,
288
+ bounds,
289
+ }, this.options, meshMaxError);
290
+ }
245
291
 
246
- // console.log(decompressedFormatted)
292
+ /**
293
+ * Determines the data type (e.g., "Int32", "Float64") of a GeoTIFF image
294
+ * by reading its TIFF tags.
295
+ *
296
+ * @param {GeoTIFFImage} image - A GeoTIFF.js image.
297
+ * @returns {Promise<string>} - A string representing the data type.
298
+ */
299
+ getDataTypeFromTags(image) {
300
+ // Retrieve the file directory containing TIFF tags.
301
+ const fileDirectory = image.getFileDirectory();
302
+
303
+ // In GeoTIFF, BitsPerSample (tag 258) and SampleFormat (tag 339) provide the type info.
304
+ // They can be either a single number or an array if there are multiple samples.
305
+ const sampleFormat = fileDirectory.SampleFormat; // Tag 339
306
+ const bitsPerSample = fileDirectory.BitsPerSample; // Tag 258
307
+
308
+ // If multiple bands exist, we assume all bands share the same type.
309
+ const format = (sampleFormat && typeof sampleFormat.length === 'number' && sampleFormat.length > 0)
310
+ ? sampleFormat[0]
311
+ : sampleFormat;
312
+
313
+ const bits = (bitsPerSample && typeof bitsPerSample.length === 'number' && bitsPerSample.length > 0)
314
+ ? bitsPerSample[0]
315
+ : bitsPerSample;
316
+
317
+ // Map the sample format to its corresponding type string.
318
+ // The common definitions are:
319
+ // 1: Unsigned integer
320
+ // 2: Signed integer
321
+ // 3: Floating point
322
+ let typePrefix;
323
+ if (format === 1) {
324
+ typePrefix = 'UInt';
325
+ } else if (format === 2) {
326
+ typePrefix = 'Int';
327
+ } else if (format === 3) {
328
+ typePrefix = 'Float';
329
+ } else {
330
+ typePrefix = 'Unknown';
331
+ }
332
+ // console.log(`data type ${typePrefix}${bits}`);
333
+ return `${typePrefix}${bits}`;
334
+ }
247
335
 
248
- // const { meshMaxError, bounds, elevationDecoder } = this.options;
336
+ /**
337
+ * Extracts the noData value from a GeoTIFF.js image.
338
+ * Returns the noData value as a number if available, otherwise undefined.
339
+ *
340
+ * @param {GeoTIFFImage} image - The GeoTIFF.js image.
341
+ * @returns {number|undefined} The noData value as a number, or undefined if not available.
342
+ */
343
+ getNoDataValue(image) {
344
+ // Attempt to retrieve the noData value via the GDAL method.
345
+ const noDataRaw = image.getGDALNoData();
346
+
347
+ if (noDataRaw === undefined || noDataRaw === null) {
348
+ console.log('noDataValue is undefined or null,raster might be displayed incorrectly.');
349
+ // No noData value is defined
350
+ return undefined;
351
+ }
249
352
 
250
- decompressed = await this.geo.getMap({
251
- rasters: [decompressedFormatted],
252
- width: this.tileSize,
253
- height: this.tileSize,
254
- bounds,
255
- }, this.options, meshMaxError);
353
+ // In geotiff.js, the noData value is typically returned as a string.
354
+ // Clean up the string by removing any null characters or extra whitespace.
355
+ const cleanedValue = String(noDataRaw).replace(/\0/g, '').trim();
256
356
 
257
- // console.log(decompressed.length)
357
+ const parsedValue = Number(cleanedValue);
358
+ return Number.isNaN(parsedValue) ? undefined : parsedValue;
359
+ }
258
360
 
259
- return decompressed;
260
- }
261
- return null;
361
+ /**
362
+ * Retrieves the number of channels (samples per pixel) in a GeoTIFF image.
363
+ *
364
+ * @param {GeoTIFFImage} image - A GeoTIFFImage object from which to extract the number of channels.
365
+ * @returns {number} The number of channels in the image.
366
+ */
367
+ getNumberOfChannels(image) {
368
+ return image.getSamplesPerPixel();
262
369
  }
263
370
 
264
- getFormat(sampleFormat: number[]|number, bitsPerSample:number[]|number) {
265
- // TO DO: what if there are different channels formats
266
- let uniqueSampleFormat = sampleFormat;
267
- let uniqueBitsPerSample = bitsPerSample;
268
- if (Array.isArray(sampleFormat)) { [uniqueSampleFormat] = sampleFormat; }
269
- if (Array.isArray(bitsPerSample)) { [uniqueBitsPerSample] = bitsPerSample; }
270
-
271
- let dataType;
272
- switch (uniqueSampleFormat) {
273
- case 1: // Unsigned integer
274
- switch (uniqueBitsPerSample) {
275
- case 8: dataType = 'uint8'; break;
276
- case 16: dataType = 'uint16'; break;
277
- case 32: dataType = 'uint32'; break;
278
- default: dataType = null;
279
- }
280
- break;
281
- case 2: // Signed integer
282
- switch (uniqueBitsPerSample) {
283
- case 8: dataType = 'int8'; break;
284
- case 16: dataType = 'int16'; break;
285
- case 32: dataType = 'int32'; break;
286
- default: dataType = null;
287
- }
288
- break;
289
- case 3: // Floating point
290
- switch (uniqueBitsPerSample) {
291
- case 32: dataType = 'float32'; break;
292
- case 64: dataType = 'float64'; break;
293
- default: dataType = null;
294
- }
295
- break;
296
- default:
297
- throw new Error('Unknown data format.');
371
+ /**
372
+ * Retrieves the PlanarConfiguration value from a GeoTIFF image.
373
+ *
374
+ * @param {GeoTIFFImage} image - The GeoTIFF image object.
375
+ * @returns {number} The PlanarConfiguration value (1 for Chunky format, 2 for Planar format).
376
+ */
377
+ getPlanarConfiguration(image) {
378
+ // Access the PlanarConfiguration tag directly
379
+ const planarConfiguration = image.fileDirectory.PlanarConfiguration;
380
+
381
+ // If the tag is not present, default to 1 (Chunky format)
382
+ if (planarConfiguration !== 1 && planarConfiguration !== 2) {
383
+ throw new Error('Invalid planar configuration.');
298
384
  }
299
- // console.log('Data type is: ', dataType)
300
- return dataType;
385
+ return planarConfiguration;
301
386
  }
302
387
 
303
- getNoDataValue(tags) {
304
- if (tags.has(42113)) {
305
- const noDataValue = tags.get(42113).value;
306
- if (typeof noDataValue === 'string' || noDataValue instanceof String) {
307
- const parsedValue = noDataValue.replace(/[\0\s]/g, '');
308
- return Number(parsedValue);
309
- }
310
- return Number.isNaN(Number(noDataValue)) ? undefined : Number(noDataValue);
388
+ /**
389
+ * Creates a tile buffer of the specified size using a typed array corresponding to the provided data type.
390
+ *
391
+ * @param {string} dataType - A string specifying the data type (e.g., "Int32", "Float64", "UInt16", etc.).
392
+ * @param {number} tileSize - The width/height of the square tile.
393
+ * @returns {TypedArray} A typed array buffer of length tileSize * tileSize.
394
+ */
395
+ createTileBuffer(dataType, tileSize) {
396
+ const length = tileSize * tileSize;
397
+ switch (dataType) {
398
+ case 'UInt8':
399
+ return new Uint8Array(length);
400
+ case 'Int8':
401
+ return new Int8Array(length);
402
+ case 'UInt16':
403
+ return new Uint16Array(length);
404
+ case 'Int16':
405
+ return new Int16Array(length);
406
+ case 'UInt32':
407
+ return new Uint32Array(length);
408
+ case 'Int32':
409
+ return new Int32Array(length);
410
+ case 'Float32':
411
+ return new Float32Array(length);
412
+ case 'Float64':
413
+ return new Float64Array(length);
414
+ default:
415
+ throw new Error(`Unsupported data type: ${dataType}`);
311
416
  }
312
- return undefined;
313
417
  }
314
418
  }
315
419
 
@@ -45,7 +45,8 @@ export type GeoImageOptions = {
45
45
  clampToTerrain?: ClampToTerrainOptions | boolean, // terrainDrawMode: 'drape',
46
46
  terrainColor?: Array<number> | chroma.Color,
47
47
  terrainSkirtHeight?: number,
48
- terrainMinValue?: number
48
+ terrainMinValue?: number,
49
+ planarConfig?: number,
49
50
  }
50
51
 
51
52
  export const DefaultGeoImageOptions: GeoImageOptions = {
@@ -77,6 +78,7 @@ export const DefaultGeoImageOptions: GeoImageOptions = {
77
78
  terrainColor: [133, 133, 133, 255],
78
79
  terrainSkirtHeight: 100,
79
80
  terrainMinValue: 0,
81
+ planarConfig: undefined,
80
82
  };
81
83
 
82
84
  export default class GeoImage {
@@ -291,9 +293,9 @@ export default class GeoImage {
291
293
  const size = width * height * 4;
292
294
  // const size = width * height;
293
295
 
294
- if (!options.noDataValue) {
295
- console.log('Missing noData value. Raster might be displayed incorrectly.');
296
- }
296
+ // if (!options.noDataValue) {
297
+ // console.log('Missing noData value. Raster might be displayed incorrectly.');
298
+ // }
297
299
  optionsLocal.unidentifiedColor = this.getColorFromChromaType(optionsLocal.unidentifiedColor);
298
300
  optionsLocal.nullColor = this.getColorFromChromaType(optionsLocal.nullColor);
299
301
  optionsLocal.clippedColor = this.getColorFromChromaType(optionsLocal.clippedColor);
@@ -387,8 +389,8 @@ export default class GeoImage {
387
389
  optionsLocal.colorScaleValueRange = this.getMinMax(channel, optionsLocal);
388
390
  // console.log('data min: ' + optionsLocal.rangeMin + ', max: ' + optionsLocal.rangeMax);
389
391
  }
390
- const numOfChannels = channel.length / (width * height);
391
- const colorData = this.getColorValue(channel, optionsLocal, size, numOfChannels);
392
+ // const numOfChannels = channel.length / (width * height);
393
+ const colorData = this.getColorValue(channel, optionsLocal, size, optionsLocal.numOfChannels);
392
394
  colorData.forEach((value, index) => {
393
395
  imageData.data[index] = value;
394
396
  });