@gisatcz/deckgl-geolib 1.10.1-dev.0 → 1.10.1-dev.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.
@@ -3,7 +3,15 @@
3
3
  // import { ExtentsLeftBottomRightTop } from '@deck.gl/core/utils/positions';
4
4
  import { fromArrayBuffer, GeoTIFFImage, TypedArray } from 'geotiff';
5
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';
6
10
 
11
+ export type Bounds = [minX: number, minY: number, maxX: number, maxY: number];
12
+
13
+ const tesselator = 'martini';
14
+ // const tesselator = 'delatin';
7
15
  export type ClampToTerrainOptions = {
8
16
  terrainDrawMode?: string
9
17
  }
@@ -94,11 +102,13 @@ export default class GeoImage {
94
102
  input: string | {
95
103
  width: number,
96
104
  height: number,
97
- rasters: any[]
105
+ rasters: any[],
106
+ bounds: Bounds
98
107
  },
99
108
  options: GeoImageOptions,
100
109
  ) {
101
110
  const mergedOptions = { ...DefaultGeoImageOptions, ...options };
111
+ console.log('xxx_mergedOptions', mergedOptions);
102
112
 
103
113
  switch (mergedOptions.type) {
104
114
  case 'image':
@@ -113,6 +123,7 @@ export default class GeoImage {
113
123
  // GetHeightmap uses only "useChannel" and "multiplier" options
114
124
  async getHeightmap(
115
125
  input: string | {
126
+ bounds: Bounds,
116
127
  width: number,
117
128
  height: number,
118
129
  rasters: any[] },
@@ -140,37 +151,136 @@ export default class GeoImage {
140
151
 
141
152
  if (options.useChannel != null) {
142
153
  if (rasters[options.useChannel]) {
143
- channel = rasters[options.useChannel];
154
+ channel = rasters[options.useChannel]; // length = 65536
144
155
  }
145
156
  }
146
157
 
147
- const canvas = document.createElement('canvas');
148
- canvas.width = width;
149
- canvas.height = height;
150
- const c = canvas.getContext('2d');
151
- const imageData = c!.createImageData(width, height);
158
+ // const canvas = document.createElement('canvas');
159
+ // canvas.width = width;
160
+ // canvas.height = height;
161
+ // // const c = canvas.getContext('2d');
162
+ // // const imageData = c!.createImageData(width, height);
163
+
164
+ // const terrain = new Float32Array((width + 1) * (height + 1));
165
+ const terrain = new Float32Array((width + 1) * (height + 1)); // length = 66049
152
166
 
153
167
  const numOfChannels = channel.length / (width * height);
154
- const size: number = width * height * 4;
155
- let pixel:number = options.useChannel === null ? 0 : options.useChannel;
156
168
 
157
- for (let i = 0; i < size; i += 4) {
158
- // height image calculation based on:
159
- // https://deck.gl/docs/api-reference/geo-layers/terrain-layer
160
- const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier!;
161
- const colorValue = Math.floor((elevationValue + 10000) / 0.1);
162
- imageData.data[i] = Math.floor(colorValue / (256 * 256));
163
- imageData.data[i + 1] = Math.floor((colorValue / 256) % 256);
164
- imageData.data[i + 2] = colorValue % 256;
165
- imageData.data[i + 3] = 255;
169
+ // return mesh data
166
170
 
167
- pixel += numOfChannels;
171
+ // const size: number = width * height * 4;
172
+ const size: number = width * height;
173
+ console.log('xxx_size', size);
174
+
175
+ let pixel:number = options.useChannel === null ? 0 : options.useChannel;
176
+
177
+ for (let i = 0, y = 0; y < height; y++) {
178
+ for (let x = 0; x < width; x++, i++) {
179
+ const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier!;
180
+ terrain[i + y] = elevationValue;
181
+ pixel += numOfChannels;
182
+ }
168
183
  }
169
184
 
170
- c!.putImageData(imageData, 0, 0);
171
- const imageUrl = canvas.toDataURL('image/png');
185
+ // for (let i = 0; i < size; i++) {
186
+ // // height image calculation based on:
187
+ // // https://deck.gl/docs/api-reference/geo-layers/terrain-layer
188
+ // const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier!;
189
+ // terrain[i] = elevationValue;
190
+ // // const colorValue = Math.floor((elevationValue + 10000) / 0.1);
191
+ // // imageData.data[i] = Math.floor(colorValue / (256 * 256));
192
+ // // imageData.data[i + 1] = Math.floor((colorValue / 256) % 256);
193
+ // // imageData.data[i + 2] = colorValue % 256;
194
+ // // imageData.data[i + 3] = 255;
195
+
196
+ // pixel += numOfChannels;
197
+ // }
198
+
199
+ // c!.putImageData(imageData, 0, 0);
200
+ // const imageUrl = canvas.toDataURL('image/png');
172
201
  // console.log('Heightmap generated.');
173
- return imageUrl;
202
+ console.log('xxx_terrain', terrain);
203
+
204
+ if (terrain[0] > 0) {
205
+ debugger;
206
+ }
207
+
208
+ if (tesselator === 'martini') {
209
+ // backfill bottom border
210
+ for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
211
+ terrain[i] = terrain[i - width - 1];
212
+ }
213
+ // backfill right border
214
+ for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
215
+ terrain[i] = terrain[i - 1];
216
+ }
217
+ }
218
+
219
+ // getMesh
220
+ const { terrainSkirtHeight } = options;
221
+ console.log('xxx_bounds_0', input.bounds);
222
+
223
+ let mesh;
224
+ switch (tesselator) {
225
+ case 'martini':
226
+ mesh = getMartiniTileMesh(terrainSkirtHeight, width, terrain);
227
+
228
+ break;
229
+ case 'delatin':
230
+ mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
231
+ break;
232
+
233
+ default:
234
+ if (width === height && !(height && (width - 1))) {
235
+ // fixme get terrain to separate method
236
+ // terrain = getTerrain(data, width, height, elevationDecoder, 'martini');
237
+ mesh = getMartiniTileMesh(terrainSkirtHeight, width, terrain);
238
+ } else {
239
+ // fixme get terrain to separate method
240
+ // terrain = getTerrain(data, width, height, elevationDecoder, 'delatin');
241
+ mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
242
+ }
243
+ break;
244
+ }
245
+
246
+ // Martini
247
+ // Martini
248
+
249
+ // Delatin
250
+ // Delatin
251
+
252
+ const { vertices } = mesh;
253
+ let { triangles } = mesh;
254
+ let attributes = getMeshAttributes(vertices, terrain, width, height, input.bounds);
255
+ // Compute bounding box before adding skirt so that z values are not skewed
256
+ const boundingBox = getMeshBoundingBox(attributes);
257
+
258
+ // FIXME uncomment and add skirt
259
+ console.log('xxx_skirtHeight', terrainSkirtHeight);
260
+
261
+ if (terrainSkirtHeight) {
262
+ const { attributes: newAttributes, triangles: newTriangles } = addSkirt(
263
+ attributes,
264
+ triangles,
265
+ terrainSkirtHeight,
266
+ );
267
+ attributes = newAttributes;
268
+ triangles = newTriangles;
269
+ }
270
+
271
+ return {
272
+ // Data return by this loader implementation
273
+ loaderData: {
274
+ header: {},
275
+ },
276
+ header: {
277
+ vertexCount: triangles.length,
278
+ boundingBox,
279
+ },
280
+ mode: 4, // TRIANGLES
281
+ indices: { value: Uint32Array.from(triangles), size: 1 },
282
+ attributes,
283
+ };
174
284
  }
175
285
 
176
286
  async getBitmap(
@@ -213,6 +323,7 @@ export default class GeoImage {
213
323
  let r; let g; let b; let
214
324
  a;
215
325
  const size = width * height * 4;
326
+ // const size = width * height;
216
327
 
217
328
  if (!options.noDataValue) {
218
329
  console.log('Missing noData value. Raster might be displayed incorrectly.');
@@ -440,3 +551,81 @@ export default class GeoImage {
440
551
  return noDataValue !== undefined && pixels.every((pixel) => pixel === noDataValue);
441
552
  }
442
553
  }
554
+
555
+ //
556
+ //
557
+ //
558
+
559
+ /**
560
+ * Get Martini generated vertices and triangles
561
+ *
562
+ * @param {number} meshMaxError threshold for simplifying mesh
563
+ * @param {number} width width of the input data
564
+ * @param {number[] | Float32Array} terrain elevation data
565
+ * @returns {{vertices: Uint16Array, triangles: Uint32Array}} vertices and triangles data
566
+ */
567
+ function getMartiniTileMesh(meshMaxError, width, terrain) {
568
+ const gridSize = width + 1;
569
+ const martini = new Martini(gridSize);
570
+ const tile = martini.createTile(terrain);
571
+ const { vertices, triangles } = tile.getMesh(meshMaxError);
572
+
573
+ return { vertices, triangles };
574
+ }
575
+
576
+ function getMeshAttributes(
577
+ vertices,
578
+ terrain: Uint8Array,
579
+ width: number,
580
+ height: number,
581
+ bounds: number[],
582
+ ) {
583
+ const gridSize = width + 1;
584
+ const numOfVerticies = vertices.length / 2;
585
+ // vec3. x, y in pixels, z in meters
586
+ const positions = new Float32Array(numOfVerticies * 3);
587
+ // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
588
+ const texCoords = new Float32Array(numOfVerticies * 2);
589
+ console.log('xxx_bounds', bounds);
590
+
591
+ const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
592
+ const xScale = (maxX - minX) / width;
593
+ const yScale = (maxY - minY) / height;
594
+
595
+ for (let i = 0; i < numOfVerticies; i++) {
596
+ const x = vertices[i * 2];
597
+ const y = vertices[i * 2 + 1];
598
+ const pixelIdx = y * gridSize + x;
599
+
600
+ positions[3 * i + 0] = x * xScale + minX;
601
+ positions[3 * i + 1] = -y * yScale + maxY;
602
+ positions[3 * i + 2] = terrain[pixelIdx];
603
+
604
+ texCoords[2 * i + 0] = x / width;
605
+ texCoords[2 * i + 1] = y / height;
606
+ }
607
+
608
+ return {
609
+ POSITION: { value: positions, size: 3 },
610
+ TEXCOORD_0: { value: texCoords, size: 2 },
611
+ // NORMAL: {}, - optional, but creates the high poly look with lighting
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Get Delatin generated vertices and triangles
617
+ *
618
+ * @param {number} meshMaxError threshold for simplifying mesh
619
+ * @param {number} width width of the input data array
620
+ * @param {number} height height of the input data array
621
+ * @param {number[] | Float32Array} terrain elevation data
622
+ * @returns {{vertices: number[], triangles: number[]}} vertices and triangles data
623
+ */
624
+ function getDelatinTileMesh(meshMaxError, width, height, terrain) {
625
+ const tin = new Delatin(terrain, width + 1, height + 1);
626
+ tin.run(meshMaxError);
627
+ // @ts-expect-error
628
+ const { coords, triangles } = tin;
629
+ const vertices = coords;
630
+ return { vertices, triangles };
631
+ }
@@ -0,0 +1,171 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import { concatenateTypedArrays } from '@loaders.gl/loader-utils';
6
+
7
+ export type EdgeIndices = {
8
+ westIndices: number[];
9
+ northIndices: number[];
10
+ eastIndices: number[];
11
+ southIndices: number[];
12
+ };
13
+
14
+ /**
15
+ * Add skirt to existing mesh
16
+ * @param {object} attributes - POSITION and TEXCOOD_0 attributes data
17
+ * @param {any} triangles - indices array of the mesh geometry
18
+ * @param skirtHeight - height of the skirt geometry
19
+ * @param outsideIndices - edge indices from quantized mesh data
20
+ * @returns - geometry data with added skirt
21
+ */
22
+ export function addSkirt(attributes, triangles, skirtHeight: number, outsideIndices?: EdgeIndices) {
23
+ const outsideEdges = outsideIndices
24
+ ? getOutsideEdgesFromIndices(outsideIndices, attributes.POSITION.value)
25
+ : getOutsideEdgesFromTriangles(triangles);
26
+
27
+ // 2 new vertices for each outside edge
28
+ const newPosition = new attributes.POSITION.value.constructor(outsideEdges.length * 6);
29
+ const newTexcoord0 = new attributes.TEXCOORD_0.value.constructor(outsideEdges.length * 4);
30
+
31
+ // 2 new triangles for each outside edge
32
+ const newTriangles = new triangles.constructor(outsideEdges.length * 6);
33
+
34
+ for (let i = 0; i < outsideEdges.length; i++) {
35
+ const edge = outsideEdges[i];
36
+
37
+ updateAttributesForNewEdge({
38
+ edge,
39
+ edgeIndex: i,
40
+ attributes,
41
+ skirtHeight,
42
+ newPosition,
43
+ newTexcoord0,
44
+ newTriangles,
45
+ });
46
+ }
47
+
48
+ attributes.POSITION.value = concatenateTypedArrays(attributes.POSITION.value, newPosition);
49
+ attributes.TEXCOORD_0.value = concatenateTypedArrays(attributes.TEXCOORD_0.value, newTexcoord0);
50
+ const resultTriangles = triangles instanceof Array
51
+ ? triangles.concat(newTriangles)
52
+ : concatenateTypedArrays(triangles, newTriangles);
53
+
54
+ return {
55
+ attributes,
56
+ triangles: resultTriangles,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Get geometry edges that located on a border of the mesh
62
+ * @param {any} triangles - indices array of the mesh geometry
63
+ * @returns {number[][]} - outside edges data
64
+ */
65
+ function getOutsideEdgesFromTriangles(triangles) {
66
+ const edges: number[][] = [];
67
+ for (let i = 0; i < triangles.length; i += 3) {
68
+ edges.push([triangles[i], triangles[i + 1]]);
69
+ edges.push([triangles[i + 1], triangles[i + 2]]);
70
+ edges.push([triangles[i + 2], triangles[i]]);
71
+ }
72
+
73
+ edges.sort((a, b) => Math.min(...a) - Math.min(...b) || Math.max(...a) - Math.max(...b));
74
+
75
+ const outsideEdges: number[][] = [];
76
+ let index = 0;
77
+ while (index < edges.length) {
78
+ if (edges[index][0] === edges[index + 1]?.[1] && edges[index][1] === edges[index + 1]?.[0]) {
79
+ index += 2;
80
+ } else {
81
+ outsideEdges.push(edges[index]);
82
+ index++;
83
+ }
84
+ }
85
+ return outsideEdges;
86
+ }
87
+
88
+ /**
89
+ * Get geometry edges that located on a border of the mesh
90
+ * @param {object} indices - edge indices from quantized mesh data
91
+ * @param {TypedArray} position - position attribute geometry data
92
+ * @returns {number[][]} - outside edges data
93
+ */
94
+ function getOutsideEdgesFromIndices(indices: EdgeIndices, position) {
95
+ // Sort skirt indices to create adjacent triangles
96
+ indices.westIndices.sort((a, b) => position[3 * a + 1] - position[3 * b + 1]);
97
+ // Reverse (b - a) to match triangle winding
98
+ indices.eastIndices.sort((a, b) => position[3 * b + 1] - position[3 * a + 1]);
99
+ indices.southIndices.sort((a, b) => position[3 * b] - position[3 * a]);
100
+ // Reverse (b - a) to match triangle winding
101
+ indices.northIndices.sort((a, b) => position[3 * a] - position[3 * b]);
102
+
103
+ const edges: number[][] = [];
104
+ for (const index in indices) {
105
+ const indexGroup = indices[index];
106
+ for (let i = 0; i < indexGroup.length - 1; i++) {
107
+ edges.push([indexGroup[i], indexGroup[i + 1]]);
108
+ }
109
+ }
110
+ return edges;
111
+ }
112
+
113
+ /**
114
+ * Get geometry edges that located on a border of the mesh
115
+ * @param {object} args
116
+ * @param {number[]} args.edge - edge indices in geometry
117
+ * @param {number} args.edgeIndex - edge index in outsideEdges array
118
+ * @param {object} args.attributes - POSITION and TEXCOORD_0 attributes
119
+ * @param {number} args.skirtHeight - height of the skirt geometry
120
+ * @param {TypedArray} args.newPosition - POSITION array for skirt data
121
+ * @param {TypedArray} args.newTexcoord0 - TEXCOORD_0 array for skirt data
122
+ * @param {TypedArray | Array} args.newTriangles - trinagle indices array for skirt data
123
+ * @returns {void}
124
+ */
125
+ function updateAttributesForNewEdge({
126
+ edge,
127
+ edgeIndex,
128
+ attributes,
129
+ skirtHeight,
130
+ newPosition,
131
+ newTexcoord0,
132
+ newTriangles,
133
+ }) {
134
+ const positionsLength = attributes.POSITION.value.length;
135
+ const vertex1Offset = edgeIndex * 2;
136
+ const vertex2Offset = edgeIndex * 2 + 1;
137
+
138
+ // Define POSITION for new 1st vertex
139
+ newPosition.set(
140
+ attributes.POSITION.value.subarray(edge[0] * 3, edge[0] * 3 + 3),
141
+ vertex1Offset * 3,
142
+ );
143
+ newPosition[vertex1Offset * 3 + 2] = newPosition[vertex1Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
144
+
145
+ // Define POSITION for new 2nd vertex
146
+ newPosition.set(
147
+ attributes.POSITION.value.subarray(edge[1] * 3, edge[1] * 3 + 3),
148
+ vertex2Offset * 3,
149
+ );
150
+ newPosition[vertex2Offset * 3 + 2] = newPosition[vertex2Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
151
+
152
+ // Use same TEXCOORDS for skirt vertices
153
+ newTexcoord0.set(
154
+ attributes.TEXCOORD_0.value.subarray(edge[0] * 2, edge[0] * 2 + 2),
155
+ vertex1Offset * 2,
156
+ );
157
+ newTexcoord0.set(
158
+ attributes.TEXCOORD_0.value.subarray(edge[1] * 2, edge[1] * 2 + 2),
159
+ vertex2Offset * 2,
160
+ );
161
+
162
+ // Define new triangles
163
+ const triangle1Offset = edgeIndex * 2 * 3;
164
+ newTriangles[triangle1Offset] = edge[0];
165
+ newTriangles[triangle1Offset + 1] = positionsLength / 3 + vertex2Offset;
166
+ newTriangles[triangle1Offset + 2] = edge[1];
167
+
168
+ newTriangles[triangle1Offset + 3] = positionsLength / 3 + vertex2Offset;
169
+ newTriangles[triangle1Offset + 4] = edge[0];
170
+ newTriangles[triangle1Offset + 5] = positionsLength / 3 + vertex1Offset;
171
+ }
@@ -0,0 +1,55 @@
1
+ import { Mesh } from '@loaders.gl/schema';
2
+ import type { LoaderContext, LoaderWithParser } from '@loaders.gl/loader-utils';
3
+ import { parseFromContext } from '@loaders.gl/loader-utils';
4
+ // import { TerrainLoader as TerrainWorkerLoader, TerrainLoaderOptions } from './terrain-loader';
5
+ // TerrainLoader
6
+
7
+ export const CogTerrainLoader = {
8
+
9
+ dataType: null as unknown as Mesh,
10
+ batchType: null as never,
11
+
12
+ name: 'Terrain',
13
+ id: 'terrain',
14
+ module: 'terrain',
15
+ // version: VERSION,
16
+ worker: false,
17
+ extensions: ['tif'],
18
+ mimeTypes: ['image/tiff'],
19
+ options: {
20
+ terrain: {
21
+ tesselator: 'auto',
22
+ bounds: undefined!,
23
+ meshMaxError: 10,
24
+ elevationDecoder: {
25
+ rScaler: 1,
26
+ gScaler: 0,
27
+ bScaler: 0,
28
+ offset: 0,
29
+ },
30
+ skirtHeight: undefined,
31
+ },
32
+ },
33
+ parse: parseTerrain,
34
+ } as const satisfies LoaderWithParser<any, never, any>;
35
+
36
+ export async function parseTerrain(
37
+ arrayBuffer: ArrayBuffer,
38
+ options?: {},
39
+ context?: LoaderContext,
40
+ ) {
41
+ console.log('xxx');
42
+
43
+ const loadImageOptions = {
44
+ ...options,
45
+ mimeType: 'application/x.image',
46
+ image: { ...options?.image, type: 'data' },
47
+ };
48
+ const image = await parseFromContext(arrayBuffer, [], loadImageOptions, context!);
49
+ console.log('xxx', image);
50
+
51
+ // Extend function to support additional mesh generation options (square grid or delatin)
52
+ // const terrainOptions = { ...TerrainLoader.options.terrain, ...options?.terrain } as TerrainOptions;
53
+ // @ts-expect-error sort out image typing asap
54
+ // return makeTerrainMeshFromImage(image, terrainOptions);
55
+ }