@gisatcz/deckgl-geolib 1.12.0-dev.0 → 1.12.0-dev.11

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,597 +0,0 @@
1
- /* eslint 'max-len': [1, { code: 105, comments: 999, ignoreStrings: true, ignoreUrls: true }] */
2
-
3
- // import { ExtentsLeftBottomRightTop } from '@deck.gl/core/utils/positions';
4
- import { fromArrayBuffer, GeoTIFFImage, TypedArray } from 'geotiff';
5
- import chroma from 'chroma-js';
6
- import Martini from '@mapbox/martini';
7
- import { getMeshBoundingBox } from '@loaders.gl/schema';
8
- import { addSkirt } from './helpers/skirt.ts';
9
- import Delatin from './delatin/index.ts';
10
-
11
- export type Bounds = [minX: number, minY: number, maxX: number, maxY: number];
12
-
13
- // FIXME - tesselator as a parameter
14
- const tesselator = 'martini';
15
- // const tesselator = 'delatin';
16
- export type ClampToTerrainOptions = {
17
- terrainDrawMode?: string
18
- }
19
- export type GeoImageOptions = {
20
- type: 'image' | 'terrain',
21
- format?: 'uint8' | 'uint16' | 'uint32' |'int8' | 'int16' | 'int32' | 'float32' | 'float64'
22
- useHeatMap?: boolean,
23
- useColorsBasedOnValues? : boolean,
24
- useColorClasses? : boolean,
25
- useAutoRange?: boolean,
26
- useDataForOpacity?: boolean,
27
- useChannel?: Exclude<number, 0> | null,
28
- useChannelIndex?: number | null,
29
- useSingleColor?: boolean,
30
- blurredTexture? : boolean,
31
- clipLow?: number | null,
32
- clipHigh?: number | null,
33
- multiplier?: number,
34
- color?: Array<number> | chroma.Color,
35
- colorScale?: Array<string> | Array<chroma.Color>,
36
- colorScaleValueRange?: number[],
37
- colorsBasedOnValues? : [number|undefined, chroma.Color][],
38
- colorClasses? : [chroma.Color, [number, number], [boolean, boolean]?][],
39
- alpha?: number,
40
- noDataValue?: number
41
- numOfChannels?: number,
42
- nullColor?: Array<number> | chroma.Color
43
- unidentifiedColor?: Array<number> | chroma.Color,
44
- clippedColor?: Array<number> | chroma.Color,
45
- clampToTerrain?: ClampToTerrainOptions | boolean, // terrainDrawMode: 'drape',
46
- terrainColor?: Array<number> | chroma.Color,
47
- terrainSkirtHeight?: number,
48
- terrainMinValue?: number
49
- }
50
-
51
- export const DefaultGeoImageOptions: GeoImageOptions = {
52
- type: 'image',
53
- format: 'uint8',
54
- useHeatMap: true,
55
- useColorsBasedOnValues: false,
56
- useAutoRange: false,
57
- useDataForOpacity: false,
58
- useSingleColor: false,
59
- useColorClasses: false,
60
- blurredTexture: true,
61
- clipLow: null,
62
- clipHigh: null,
63
- multiplier: 1.0,
64
- color: [255, 0, 255, 255],
65
- colorScale: chroma.brewer.YlOrRd,
66
- colorScaleValueRange: [0, 255],
67
- colorsBasedOnValues: null,
68
- colorClasses: null,
69
- alpha: 100,
70
- useChannel: null,
71
- useChannelIndex: null,
72
- noDataValue: undefined,
73
- numOfChannels: undefined,
74
- nullColor: [0, 0, 0, 0],
75
- unidentifiedColor: [0, 0, 0, 0],
76
- clippedColor: [0, 0, 0, 0],
77
- terrainColor: [133, 133, 133, 255],
78
- terrainSkirtHeight: 100,
79
- terrainMinValue: 0,
80
- };
81
-
82
- export default class GeoImage {
83
- data: GeoTIFFImage | undefined;
84
-
85
- scale = (
86
- num: number,
87
- inMin: number,
88
- inMax: number,
89
- outMin: number,
90
- outMax: number,
91
- ) => ((num - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
92
-
93
- async setUrl(url: string) {
94
- // TODO - not tested
95
- const response = await fetch(url);
96
- const arrayBuffer = await response.arrayBuffer();
97
- const tiff = await fromArrayBuffer(arrayBuffer);
98
-
99
- const data = await tiff.getImage(0);
100
-
101
- this.data = data;
102
- }
103
-
104
- async getMap(
105
- input: string | {
106
- width: number,
107
- height: number,
108
- rasters: any[],
109
- bounds: Bounds
110
- },
111
- options: GeoImageOptions,
112
- meshMaxError,
113
- ) {
114
- const mergedOptions = { ...DefaultGeoImageOptions, ...options };
115
-
116
- switch (mergedOptions.type) {
117
- case 'image':
118
- return this.getBitmap(input, mergedOptions);
119
- case 'terrain':
120
- return this.getHeightmap(input, mergedOptions, meshMaxError);
121
- default:
122
- return null;
123
- }
124
- }
125
-
126
- // GetHeightmap uses only "useChannel" and "multiplier" options
127
- async getHeightmap(
128
- input: string | {
129
- bounds: Bounds,
130
- width: number,
131
- height: number,
132
- rasters: any[] },
133
- options: GeoImageOptions,
134
- meshMaxError,
135
- ) {
136
- let rasters = [];
137
- let width: number;
138
- let height: number;
139
-
140
- if (typeof (input) === 'string') {
141
- // TODO not tested
142
- // input is type of object
143
- await this.setUrl(input);
144
-
145
- rasters = (await this.data!.readRasters()) as TypedArray[];
146
- width = this.data!.getWidth();
147
- height = this.data!.getHeight();
148
- } else {
149
- rasters = input.rasters;
150
- width = input.width;
151
- height = input.height;
152
- }
153
- const optionsLocal = { ...options };
154
-
155
- let channel = rasters[0];
156
-
157
- optionsLocal.useChannelIndex ??= optionsLocal.useChannel == null ? null : optionsLocal.useChannel - 1;
158
- if (options.useChannelIndex != null) {
159
- if (rasters[optionsLocal.useChannelIndex]) {
160
- channel = rasters[optionsLocal.useChannelIndex];
161
- }
162
- }
163
-
164
- const terrain = new Float32Array((width + 1) * (height + 1));
165
-
166
- const numOfChannels = channel.length / (width * height);
167
-
168
- let pixel:number = options.useChannelIndex === null ? 0 : options.useChannelIndex;
169
-
170
- for (let i = 0, y = 0; y < height; y++) {
171
- for (let x = 0; x < width; x++, i++) {
172
- const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier!;
173
- terrain[i + y] = elevationValue;
174
- pixel += numOfChannels;
175
- }
176
- }
177
-
178
- if (tesselator === 'martini') {
179
- // backfill bottom border
180
- for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
181
- terrain[i] = terrain[i - width - 1];
182
- }
183
- // backfill right border
184
- for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
185
- terrain[i] = terrain[i - 1];
186
- }
187
- }
188
-
189
- // getMesh
190
- const { terrainSkirtHeight } = options;
191
-
192
- let mesh;
193
- switch (tesselator) {
194
- case 'martini':
195
- mesh = getMartiniTileMesh(meshMaxError, width, terrain);
196
-
197
- break;
198
- case 'delatin':
199
- mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
200
- break;
201
-
202
- default:
203
- if (width === height && !(height && (width - 1))) {
204
- // fixme get terrain to separate method
205
- // terrain = getTerrain(data, width, height, elevationDecoder, 'martini');
206
- mesh = getMartiniTileMesh(meshMaxError, width, terrain);
207
- } else {
208
- // fixme get terrain to separate method
209
- // terrain = getTerrain(data, width, height, elevationDecoder, 'delatin');
210
- mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
211
- }
212
- break;
213
- }
214
-
215
- // Martini
216
- // Martini
217
-
218
- // Delatin
219
- // Delatin
220
-
221
- const { vertices } = mesh;
222
- let { triangles } = mesh;
223
- let attributes = getMeshAttributes(vertices, terrain, width, height, input.bounds);
224
- // Compute bounding box before adding skirt so that z values are not skewed
225
- const boundingBox = getMeshBoundingBox(attributes);
226
-
227
- if (terrainSkirtHeight) {
228
- const { attributes: newAttributes, triangles: newTriangles } = addSkirt(
229
- attributes,
230
- triangles,
231
- terrainSkirtHeight,
232
- );
233
- attributes = newAttributes;
234
- triangles = newTriangles;
235
- }
236
-
237
- return {
238
- // Data return by this loader implementation
239
- loaderData: {
240
- header: {},
241
- },
242
- header: {
243
- vertexCount: triangles.length,
244
- boundingBox,
245
- },
246
- mode: 4, // TRIANGLES
247
- indices: { value: Uint32Array.from(triangles), size: 1 },
248
- attributes,
249
- };
250
- }
251
-
252
- async getBitmap(
253
- input: string | {
254
- width: number,
255
- height: number,
256
- rasters: any[] },
257
- options: GeoImageOptions,
258
- ) {
259
- // console.time('bitmap-generated-in');
260
- // const optionsLocal = { ...options };
261
- const optionsLocal = { ...options };
262
-
263
- let rasters = [];
264
- let channels: number;
265
- let width: number;
266
- let height: number;
267
-
268
- if (typeof (input) === 'string') {
269
- // TODO not tested
270
- // input is type of object
271
- await this.setUrl(input);
272
- rasters = (await this.data!.readRasters()) as TypedArray[];
273
- channels = rasters.length;
274
- width = this.data!.getWidth();
275
- height = this.data!.getHeight();
276
- } else {
277
- rasters = input.rasters;
278
- channels = rasters.length;
279
- width = input.width;
280
- height = input.height;
281
- }
282
-
283
- const canvas = document.createElement('canvas');
284
- canvas.width = width;
285
- canvas.height = height;
286
- const c = canvas.getContext('2d');
287
- const imageData: ImageData = c!.createImageData(width, height);
288
-
289
- let r; let g; let b; let
290
- a;
291
- const size = width * height * 4;
292
- // const size = width * height;
293
-
294
- if (!options.noDataValue) {
295
- console.log('Missing noData value. Raster might be displayed incorrectly.');
296
- }
297
- optionsLocal.unidentifiedColor = this.getColorFromChromaType(optionsLocal.unidentifiedColor);
298
- optionsLocal.nullColor = this.getColorFromChromaType(optionsLocal.nullColor);
299
- optionsLocal.clippedColor = this.getColorFromChromaType(optionsLocal.clippedColor);
300
- optionsLocal.color = this.getColorFromChromaType(optionsLocal.color);
301
- optionsLocal.useChannelIndex ??= options.useChannel === null ? null : options.useChannel - 1;
302
-
303
- // console.log(rasters[0])
304
- /* console.log("raster 0 length: " + rasters[0].length)
305
- console.log("image width: " + width)
306
- console.log("channels: " + channels)
307
- console.log("format: " + rasters[0].length / (width * height))
308
- */
309
-
310
- if (optionsLocal.useChannelIndex == null) {
311
- if (channels === 1) {
312
- if (rasters[0].length / (width * height) === 1) {
313
- const channel = rasters[0];
314
- // AUTO RANGE
315
- if (optionsLocal.useAutoRange) {
316
- optionsLocal.colorScaleValueRange = this.getMinMax(channel, optionsLocal);
317
- // console.log('data min: ' + optionsLocal.rangeMin + ', max: ' + optionsLocal.rangeMax);
318
- }
319
- // SINGLE CHANNEL
320
- const colorData = this.getColorValue(channel, optionsLocal, size);
321
- colorData.forEach((value, index) => {
322
- imageData.data[index] = value;
323
- });
324
- }
325
- // RGB values in one channel
326
- if (rasters[0].length / (width * height) === 3) {
327
- // console.log("geoImage: " + "RGB 1 array of length: " + rasters[0].length);
328
- let pixel = 0;
329
- for (let idx = 0; idx < size; idx += 4) {
330
- const rgbColor = [rasters[0][pixel], rasters[0][pixel + 1], rasters[0][pixel + 2]];
331
- const rgbaColor = this.hasPixelsNoData(rgbColor, optionsLocal.noDataValue)
332
- ? optionsLocal.nullColor
333
- : [...rgbColor, Math.floor(optionsLocal.alpha! * 2.55)];
334
- // eslint-disable-next-line max-len
335
- [imageData.data[idx], imageData.data[idx + 1], imageData.data[idx + 2], imageData.data[idx + 3]] = rgbaColor;
336
- pixel += 3;
337
- }
338
- }
339
- if (rasters[0].length / (width * height) === 4) {
340
- // console.log("geoImage: " + "RGBA 1 array");
341
- rasters[0].forEach((value, index) => {
342
- imageData.data[index] = value;
343
- });
344
- }
345
- }
346
- if (channels === 3) {
347
- // RGB
348
- let pixel = 0;
349
- for (let i = 0; i < size; i += 4) {
350
- r = rasters[0][pixel];
351
- g = rasters[1][pixel];
352
- b = rasters[2][pixel];
353
- a = Math.floor(optionsLocal.alpha! * 2.55);
354
-
355
- imageData.data[i] = r;
356
- imageData.data[i + 1] = g;
357
- imageData.data[i + 2] = b;
358
- imageData.data[i + 3] = a;
359
-
360
- pixel += 1;
361
- }
362
- }
363
- if (channels === 4) {
364
- // RGBA
365
- let pixel = 0;
366
- for (let i = 0; i < size; i += 4) {
367
- r = rasters[0][pixel];
368
- g = rasters[1][pixel];
369
- b = rasters[2][pixel];
370
- a = Math.floor(optionsLocal.alpha! * 2.55);
371
-
372
- imageData.data[i] = r;
373
- imageData.data[i + 1] = g;
374
- imageData.data[i + 2] = b;
375
- imageData.data[i + 3] = a;
376
-
377
- pixel += 1;
378
- }
379
- }
380
- } else if (optionsLocal.useChannelIndex < optionsLocal.numOfChannels && optionsLocal.useChannelIndex >= 0) {
381
- let channel = rasters[0];
382
- if (rasters[optionsLocal.useChannelIndex]) {
383
- channel = rasters[optionsLocal.useChannelIndex];
384
- }
385
- // AUTO RANGE
386
- if (optionsLocal.useAutoRange) {
387
- optionsLocal.colorScaleValueRange = this.getMinMax(channel, optionsLocal);
388
- // console.log('data min: ' + optionsLocal.rangeMin + ', max: ' + optionsLocal.rangeMax);
389
- }
390
- const numOfChannels = channel.length / (width * height);
391
- const colorData = this.getColorValue(channel, optionsLocal, size, numOfChannels);
392
- colorData.forEach((value, index) => {
393
- imageData.data[index] = value;
394
- });
395
- } else {
396
- // if user defined channel does not exist
397
- console.log(`Defined channel(${options.useChannel}) or channel index(${options.useChannelIndex}) does not exist, choose a different channel or set the useChannel property to null if you want to visualize RGB(A) imagery`);
398
- const defaultColorData = this.getDefaultColor(size, optionsLocal.nullColor);
399
- defaultColorData.forEach((value, index) => {
400
- imageData.data[index] = value;
401
- });
402
- }
403
- // console.timeEnd('bitmap-generated-in');
404
-
405
- c!.putImageData(imageData, 0, 0);
406
- const imageUrl = canvas.toDataURL('image/png');
407
- // console.log('Bitmap generated.');
408
- return imageUrl;
409
- }
410
-
411
- getMinMax(array, options) {
412
- let maxValue = options.maxValue ? options.maxValue : Number.MIN_VALUE;
413
- let minValue = options.minValue ? options.minValue : Number.MAX_VALUE;
414
- for (let idx = 0; idx < array.length; idx += 1) {
415
- if (options.noDataValue === undefined || array[idx] !== options.noDataValue) {
416
- if (array[idx] > maxValue) maxValue = array[idx];
417
- if (array[idx] < minValue) minValue = array[idx];
418
- }
419
- }
420
- return [minValue, maxValue];
421
- }
422
-
423
- getColorValue(dataArray:[], options:GeoImageOptions, arrayLength:number, numOfChannels = 1) {
424
- const colorScale = chroma.scale(options.colorScale).domain(options.colorScaleValueRange);
425
- // channel index is equal to channel number - 1
426
- let pixel:number = options.useChannelIndex === null ? 0 : options.useChannelIndex;
427
- const colorsArray = new Array(arrayLength);
428
-
429
- // if useColorsBasedOnValues is true
430
- const dataValues = options.colorsBasedOnValues ? options.colorsBasedOnValues.map(([first]) => first) : undefined;
431
- const colorValues = options.colorsBasedOnValues ? options.colorsBasedOnValues.map(([, second]) => [...chroma(second).rgb(), Math.floor(options.alpha * 2.55)]) : undefined;
432
-
433
- // if useClasses is true
434
- const colorClasses = options.useColorClasses ? options.colorClasses.map(([color]) => [...chroma(color).rgb(), Math.floor(options.alpha * 2.55)]) : undefined;
435
- const dataIntervals = options.useColorClasses ? options.colorClasses.map(([, interval]) => interval) : undefined;
436
- const dataIntervalBounds = options.useColorClasses ? options.colorClasses.map(([, , bounds], index) => {
437
- if (bounds !== undefined) return bounds;
438
- if (index === options.colorClasses.length - 1) return [true, true];
439
- return [true, false];
440
- }) : undefined;
441
-
442
- for (let i = 0; i < arrayLength; i += 4) {
443
- let pixelColor = options.nullColor;
444
- // FIXME
445
- // eslint-disable-next-line max-len
446
- if ((!Number.isNaN(dataArray[pixel])) && (options.noDataValue === undefined || dataArray[pixel] !== options.noDataValue)) {
447
- if (
448
- (options.clipLow != null && dataArray[pixel] <= options.clipLow)
449
- || (options.clipHigh != null && dataArray[pixel] >= options.clipHigh)
450
- ) {
451
- pixelColor = options.clippedColor;
452
- } else {
453
- if (options.useHeatMap) {
454
- // FIXME
455
- // eslint-disable-next-line
456
- pixelColor = [...colorScale(dataArray[pixel]).rgb(), Math.floor(options.alpha * 2.55)];
457
- }
458
- if (options.useColorsBasedOnValues) {
459
- const index = dataValues.indexOf(dataArray[pixel]);
460
- if (index > -1) {
461
- pixelColor = colorValues[index];
462
- } else pixelColor = options.unidentifiedColor;
463
- }
464
- if (options.useColorClasses) {
465
- const index = this.findClassIndex(dataArray[pixel], dataIntervals, dataIntervalBounds);
466
- if (index > -1) {
467
- pixelColor = colorClasses[index];
468
- } else pixelColor = options.unidentifiedColor;
469
- }
470
- if (options.useSingleColor) {
471
- // FIXME - Is this compatible with chroma.color?
472
- pixelColor = options.color;
473
- }
474
- if (options.useDataForOpacity) {
475
- // eslint-disable-next-line max-len
476
- pixelColor[3] = this.scale(dataArray[pixel], options.colorScaleValueRange[0]!, options.colorScaleValueRange.slice(-1)[0]!, 0, 255);
477
- }
478
- }
479
- }
480
- // FIXME
481
- // eslint-disable-next-line
482
- [colorsArray[i], colorsArray[i + 1], colorsArray[i + 2], colorsArray[i + 3]] = pixelColor;
483
-
484
- pixel += numOfChannels;
485
- }
486
- return colorsArray;
487
- }
488
-
489
- findClassIndex(number, intervals, bounds) {
490
- // returns index of the first class to which the number belongs
491
- for (let idx = 0; idx < intervals.length; idx += 1) {
492
- const [min, max] = intervals[idx];
493
- const [includeEqualMin, includeEqualMax] = bounds[idx];
494
- if ((includeEqualMin ? number >= min : number > min)
495
- && (includeEqualMax ? number <= max : number < max)) {
496
- return idx;
497
- }
498
- }
499
- return -1;
500
- }
501
-
502
- getDefaultColor(size, nullColor) {
503
- const colorsArray = new Array(size);
504
- for (let i = 0; i < size; i += 4) {
505
- [colorsArray[i], colorsArray[i + 1], colorsArray[i + 2], colorsArray[i + 3]] = nullColor;
506
- }
507
- return colorsArray;
508
- }
509
-
510
- getColorFromChromaType(colorDefinition) {
511
- if (!Array.isArray(colorDefinition) || colorDefinition.length !== 4) {
512
- return [...chroma(colorDefinition).rgb(), 255];
513
- }
514
- return colorDefinition;
515
- }
516
-
517
- hasPixelsNoData(pixels, noDataValue) {
518
- return noDataValue !== undefined && pixels.every((pixel) => pixel === noDataValue);
519
- }
520
- }
521
-
522
- //
523
- //
524
- //
525
-
526
- /**
527
- * Get Martini generated vertices and triangles
528
- *
529
- * @param {number} meshMaxError threshold for simplifying mesh
530
- * @param {number} width width of the input data
531
- * @param {number[] | Float32Array} terrain elevation data
532
- * @returns {{vertices: Uint16Array, triangles: Uint32Array}} vertices and triangles data
533
- */
534
- function getMartiniTileMesh(meshMaxError, width, terrain) {
535
- const gridSize = width + 1;
536
- const martini = new Martini(gridSize);
537
- const tile = martini.createTile(terrain);
538
- const { vertices, triangles } = tile.getMesh(meshMaxError);
539
-
540
- return { vertices, triangles };
541
- }
542
-
543
- function getMeshAttributes(
544
- vertices,
545
- terrain: Uint8Array,
546
- width: number,
547
- height: number,
548
- bounds: number[],
549
- ) {
550
- const gridSize = width + 1;
551
- const numOfVerticies = vertices.length / 2;
552
- // vec3. x, y in pixels, z in meters
553
- const positions = new Float32Array(numOfVerticies * 3);
554
- // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
555
- const texCoords = new Float32Array(numOfVerticies * 2);
556
-
557
- const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
558
- const xScale = (maxX - minX) / width;
559
- const yScale = (maxY - minY) / height;
560
-
561
- for (let i = 0; i < numOfVerticies; i++) {
562
- const x = vertices[i * 2];
563
- const y = vertices[i * 2 + 1];
564
- const pixelIdx = y * gridSize + x;
565
-
566
- positions[3 * i + 0] = x * xScale + minX;
567
- positions[3 * i + 1] = -y * yScale + maxY;
568
- positions[3 * i + 2] = terrain[pixelIdx];
569
-
570
- texCoords[2 * i + 0] = x / width;
571
- texCoords[2 * i + 1] = y / height;
572
- }
573
-
574
- return {
575
- POSITION: { value: positions, size: 3 },
576
- TEXCOORD_0: { value: texCoords, size: 2 },
577
- // NORMAL: {}, - optional, but creates the high poly look with lighting
578
- };
579
- }
580
-
581
- /**
582
- * Get Delatin generated vertices and triangles
583
- *
584
- * @param {number} meshMaxError threshold for simplifying mesh
585
- * @param {number} width width of the input data array
586
- * @param {number} height height of the input data array
587
- * @param {number[] | Float32Array} terrain elevation data
588
- * @returns {{vertices: number[], triangles: number[]}} vertices and triangles data
589
- */
590
- function getDelatinTileMesh(meshMaxError, width, height, terrain) {
591
- const tin = new Delatin(terrain, width + 1, height + 1);
592
- tin.run(meshMaxError);
593
- // @ts-expect-error
594
- const { coords, triangles } = tin;
595
- const vertices = coords;
596
- return { vertices, triangles };
597
- }