@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.
package/dist/cjs/index.js CHANGED
@@ -14987,7 +14987,750 @@ async function fromArrayBuffer(arrayBuffer, signal) {
14987
14987
  return GeoTIFF.fromSource(makeBufferSource(arrayBuffer), signal);
14988
14988
  }
14989
14989
 
14990
+ class Martini {
14991
+ constructor(gridSize = 257) {
14992
+ this.gridSize = gridSize;
14993
+ const tileSize = gridSize - 1;
14994
+ if (tileSize & (tileSize - 1)) throw new Error(
14995
+ `Expected grid size to be 2^n+1, got ${gridSize}.`);
14996
+
14997
+ this.numTriangles = tileSize * tileSize * 2 - 2;
14998
+ this.numParentTriangles = this.numTriangles - tileSize * tileSize;
14999
+
15000
+ this.indices = new Uint32Array(this.gridSize * this.gridSize);
15001
+
15002
+ // coordinates for all possible triangles in an RTIN tile
15003
+ this.coords = new Uint16Array(this.numTriangles * 4);
15004
+
15005
+ // get triangle coordinates from its index in an implicit binary tree
15006
+ for (let i = 0; i < this.numTriangles; i++) {
15007
+ let id = i + 2;
15008
+ let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0;
15009
+ if (id & 1) {
15010
+ bx = by = cx = tileSize; // bottom-left triangle
15011
+ } else {
15012
+ ax = ay = cy = tileSize; // top-right triangle
15013
+ }
15014
+ while ((id >>= 1) > 1) {
15015
+ const mx = (ax + bx) >> 1;
15016
+ const my = (ay + by) >> 1;
15017
+
15018
+ if (id & 1) { // left half
15019
+ bx = ax; by = ay;
15020
+ ax = cx; ay = cy;
15021
+ } else { // right half
15022
+ ax = bx; ay = by;
15023
+ bx = cx; by = cy;
15024
+ }
15025
+ cx = mx; cy = my;
15026
+ }
15027
+ const k = i * 4;
15028
+ this.coords[k + 0] = ax;
15029
+ this.coords[k + 1] = ay;
15030
+ this.coords[k + 2] = bx;
15031
+ this.coords[k + 3] = by;
15032
+ }
15033
+ }
15034
+
15035
+ createTile(terrain) {
15036
+ return new Tile(terrain, this);
15037
+ }
15038
+ }
15039
+
15040
+ class Tile {
15041
+ constructor(terrain, martini) {
15042
+ const size = martini.gridSize;
15043
+ if (terrain.length !== size * size) throw new Error(
15044
+ `Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`);
15045
+
15046
+ this.terrain = terrain;
15047
+ this.martini = martini;
15048
+ this.errors = new Float32Array(terrain.length);
15049
+ this.update();
15050
+ }
15051
+
15052
+ update() {
15053
+ const {numTriangles, numParentTriangles, coords, gridSize: size} = this.martini;
15054
+ const {terrain, errors} = this;
15055
+
15056
+ // iterate over all possible triangles, starting from the smallest level
15057
+ for (let i = numTriangles - 1; i >= 0; i--) {
15058
+ const k = i * 4;
15059
+ const ax = coords[k + 0];
15060
+ const ay = coords[k + 1];
15061
+ const bx = coords[k + 2];
15062
+ const by = coords[k + 3];
15063
+ const mx = (ax + bx) >> 1;
15064
+ const my = (ay + by) >> 1;
15065
+ const cx = mx + my - ay;
15066
+ const cy = my + ax - mx;
15067
+
15068
+ // calculate error in the middle of the long edge of the triangle
15069
+ const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2;
15070
+ const middleIndex = my * size + mx;
15071
+ const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]);
15072
+
15073
+ errors[middleIndex] = Math.max(errors[middleIndex], middleError);
15074
+
15075
+ if (i < numParentTriangles) { // bigger triangles; accumulate error with children
15076
+ const leftChildIndex = ((ay + cy) >> 1) * size + ((ax + cx) >> 1);
15077
+ const rightChildIndex = ((by + cy) >> 1) * size + ((bx + cx) >> 1);
15078
+ errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]);
15079
+ }
15080
+ }
15081
+ }
15082
+
15083
+ getMesh(maxError = 0) {
15084
+ const {gridSize: size, indices} = this.martini;
15085
+ const {errors} = this;
15086
+ let numVertices = 0;
15087
+ let numTriangles = 0;
15088
+ const max = size - 1;
15089
+
15090
+ // use an index grid to keep track of vertices that were already used to avoid duplication
15091
+ indices.fill(0);
15092
+
15093
+ // retrieve mesh in two stages that both traverse the error map:
15094
+ // - countElements: find used vertices (and assign each an index), and count triangles (for minimum allocation)
15095
+ // - processTriangle: fill the allocated vertices & triangles typed arrays
15096
+
15097
+ function countElements(ax, ay, bx, by, cx, cy) {
15098
+ const mx = (ax + bx) >> 1;
15099
+ const my = (ay + by) >> 1;
15100
+
15101
+ if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
15102
+ countElements(cx, cy, ax, ay, mx, my);
15103
+ countElements(bx, by, cx, cy, mx, my);
15104
+ } else {
15105
+ indices[ay * size + ax] = indices[ay * size + ax] || ++numVertices;
15106
+ indices[by * size + bx] = indices[by * size + bx] || ++numVertices;
15107
+ indices[cy * size + cx] = indices[cy * size + cx] || ++numVertices;
15108
+ numTriangles++;
15109
+ }
15110
+ }
15111
+ countElements(0, 0, max, max, max, 0);
15112
+ countElements(max, max, 0, 0, 0, max);
15113
+
15114
+ const vertices = new Uint16Array(numVertices * 2);
15115
+ const triangles = new Uint32Array(numTriangles * 3);
15116
+ let triIndex = 0;
15117
+
15118
+ function processTriangle(ax, ay, bx, by, cx, cy) {
15119
+ const mx = (ax + bx) >> 1;
15120
+ const my = (ay + by) >> 1;
15121
+
15122
+ if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
15123
+ // triangle doesn't approximate the surface well enough; drill down further
15124
+ processTriangle(cx, cy, ax, ay, mx, my);
15125
+ processTriangle(bx, by, cx, cy, mx, my);
15126
+
15127
+ } else {
15128
+ // add a triangle
15129
+ const a = indices[ay * size + ax] - 1;
15130
+ const b = indices[by * size + bx] - 1;
15131
+ const c = indices[cy * size + cx] - 1;
15132
+
15133
+ vertices[2 * a] = ax;
15134
+ vertices[2 * a + 1] = ay;
15135
+
15136
+ vertices[2 * b] = bx;
15137
+ vertices[2 * b + 1] = by;
15138
+
15139
+ vertices[2 * c] = cx;
15140
+ vertices[2 * c + 1] = cy;
15141
+
15142
+ triangles[triIndex++] = a;
15143
+ triangles[triIndex++] = b;
15144
+ triangles[triIndex++] = c;
15145
+ }
15146
+ }
15147
+ processTriangle(0, 0, max, max, max, 0);
15148
+ processTriangle(max, max, 0, 0, 0, max);
15149
+
15150
+ return {vertices, triangles};
15151
+ }
15152
+ }
15153
+
15154
+ function getMeshBoundingBox(attributes) {
15155
+ let minX = Infinity;
15156
+ let minY = Infinity;
15157
+ let minZ = Infinity;
15158
+ let maxX = -Infinity;
15159
+ let maxY = -Infinity;
15160
+ let maxZ = -Infinity;
15161
+ const positions = attributes.POSITION ? attributes.POSITION.value : [];
15162
+ const len = positions && positions.length;
15163
+ for (let i = 0; i < len; i += 3) {
15164
+ const x = positions[i];
15165
+ const y = positions[i + 1];
15166
+ const z = positions[i + 2];
15167
+ minX = x < minX ? x : minX;
15168
+ minY = y < minY ? y : minY;
15169
+ minZ = z < minZ ? z : minZ;
15170
+ maxX = x > maxX ? x : maxX;
15171
+ maxY = y > maxY ? y : maxY;
15172
+ maxZ = z > maxZ ? z : maxZ;
15173
+ }
15174
+ return [[minX, minY, minZ], [maxX, maxY, maxZ]];
15175
+ }
15176
+
15177
+ function concatenateTypedArrays() {
15178
+ for (var _len2 = arguments.length, typedArrays = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
15179
+ typedArrays[_key2] = arguments[_key2];
15180
+ }
15181
+ const arrays = typedArrays;
15182
+ const TypedArrayConstructor = arrays && arrays.length > 1 && arrays[0].constructor || null;
15183
+ if (!TypedArrayConstructor) {
15184
+ throw new Error('"concatenateTypedArrays" - incorrect quantity of arguments or arguments have incompatible data types');
15185
+ }
15186
+ const sumLength = arrays.reduce((acc, value) => acc + value.length, 0);
15187
+ const result = new TypedArrayConstructor(sumLength);
15188
+ let offset = 0;
15189
+ for (const array of arrays) {
15190
+ result.set(array, offset);
15191
+ offset += array.length;
15192
+ }
15193
+ return result;
15194
+ }
15195
+
15196
+ // loaders.gl
15197
+ // SPDX-License-Identifier: MIT
15198
+ // Copyright (c) vis.gl contributors
15199
+ /**
15200
+ * Add skirt to existing mesh
15201
+ * @param {object} attributes - POSITION and TEXCOOD_0 attributes data
15202
+ * @param {any} triangles - indices array of the mesh geometry
15203
+ * @param skirtHeight - height of the skirt geometry
15204
+ * @param outsideIndices - edge indices from quantized mesh data
15205
+ * @returns - geometry data with added skirt
15206
+ */
15207
+ function addSkirt(attributes, triangles, skirtHeight, outsideIndices) {
15208
+ const outsideEdges = outsideIndices
15209
+ ? getOutsideEdgesFromIndices(outsideIndices, attributes.POSITION.value)
15210
+ : getOutsideEdgesFromTriangles(triangles);
15211
+ // 2 new vertices for each outside edge
15212
+ const newPosition = new attributes.POSITION.value.constructor(outsideEdges.length * 6);
15213
+ const newTexcoord0 = new attributes.TEXCOORD_0.value.constructor(outsideEdges.length * 4);
15214
+ // 2 new triangles for each outside edge
15215
+ const newTriangles = new triangles.constructor(outsideEdges.length * 6);
15216
+ for (let i = 0; i < outsideEdges.length; i++) {
15217
+ const edge = outsideEdges[i];
15218
+ updateAttributesForNewEdge({
15219
+ edge,
15220
+ edgeIndex: i,
15221
+ attributes,
15222
+ skirtHeight,
15223
+ newPosition,
15224
+ newTexcoord0,
15225
+ newTriangles,
15226
+ });
15227
+ }
15228
+ attributes.POSITION.value = concatenateTypedArrays(attributes.POSITION.value, newPosition);
15229
+ attributes.TEXCOORD_0.value = concatenateTypedArrays(attributes.TEXCOORD_0.value, newTexcoord0);
15230
+ const resultTriangles = triangles instanceof Array
15231
+ ? triangles.concat(newTriangles)
15232
+ : concatenateTypedArrays(triangles, newTriangles);
15233
+ return {
15234
+ attributes,
15235
+ triangles: resultTriangles,
15236
+ };
15237
+ }
15238
+ /**
15239
+ * Get geometry edges that located on a border of the mesh
15240
+ * @param {any} triangles - indices array of the mesh geometry
15241
+ * @returns {number[][]} - outside edges data
15242
+ */
15243
+ function getOutsideEdgesFromTriangles(triangles) {
15244
+ var _a, _b;
15245
+ const edges = [];
15246
+ for (let i = 0; i < triangles.length; i += 3) {
15247
+ edges.push([triangles[i], triangles[i + 1]]);
15248
+ edges.push([triangles[i + 1], triangles[i + 2]]);
15249
+ edges.push([triangles[i + 2], triangles[i]]);
15250
+ }
15251
+ edges.sort((a, b) => Math.min(...a) - Math.min(...b) || Math.max(...a) - Math.max(...b));
15252
+ const outsideEdges = [];
15253
+ let index = 0;
15254
+ while (index < edges.length) {
15255
+ if (edges[index][0] === ((_a = edges[index + 1]) === null || _a === void 0 ? void 0 : _a[1]) && edges[index][1] === ((_b = edges[index + 1]) === null || _b === void 0 ? void 0 : _b[0])) {
15256
+ index += 2;
15257
+ }
15258
+ else {
15259
+ outsideEdges.push(edges[index]);
15260
+ index++;
15261
+ }
15262
+ }
15263
+ return outsideEdges;
15264
+ }
15265
+ /**
15266
+ * Get geometry edges that located on a border of the mesh
15267
+ * @param {object} indices - edge indices from quantized mesh data
15268
+ * @param {TypedArray} position - position attribute geometry data
15269
+ * @returns {number[][]} - outside edges data
15270
+ */
15271
+ function getOutsideEdgesFromIndices(indices, position) {
15272
+ // Sort skirt indices to create adjacent triangles
15273
+ indices.westIndices.sort((a, b) => position[3 * a + 1] - position[3 * b + 1]);
15274
+ // Reverse (b - a) to match triangle winding
15275
+ indices.eastIndices.sort((a, b) => position[3 * b + 1] - position[3 * a + 1]);
15276
+ indices.southIndices.sort((a, b) => position[3 * b] - position[3 * a]);
15277
+ // Reverse (b - a) to match triangle winding
15278
+ indices.northIndices.sort((a, b) => position[3 * a] - position[3 * b]);
15279
+ const edges = [];
15280
+ for (const index in indices) {
15281
+ const indexGroup = indices[index];
15282
+ for (let i = 0; i < indexGroup.length - 1; i++) {
15283
+ edges.push([indexGroup[i], indexGroup[i + 1]]);
15284
+ }
15285
+ }
15286
+ return edges;
15287
+ }
15288
+ /**
15289
+ * Get geometry edges that located on a border of the mesh
15290
+ * @param {object} args
15291
+ * @param {number[]} args.edge - edge indices in geometry
15292
+ * @param {number} args.edgeIndex - edge index in outsideEdges array
15293
+ * @param {object} args.attributes - POSITION and TEXCOORD_0 attributes
15294
+ * @param {number} args.skirtHeight - height of the skirt geometry
15295
+ * @param {TypedArray} args.newPosition - POSITION array for skirt data
15296
+ * @param {TypedArray} args.newTexcoord0 - TEXCOORD_0 array for skirt data
15297
+ * @param {TypedArray | Array} args.newTriangles - trinagle indices array for skirt data
15298
+ * @returns {void}
15299
+ */
15300
+ function updateAttributesForNewEdge({ edge, edgeIndex, attributes, skirtHeight, newPosition, newTexcoord0, newTriangles, }) {
15301
+ const positionsLength = attributes.POSITION.value.length;
15302
+ const vertex1Offset = edgeIndex * 2;
15303
+ const vertex2Offset = edgeIndex * 2 + 1;
15304
+ // Define POSITION for new 1st vertex
15305
+ newPosition.set(attributes.POSITION.value.subarray(edge[0] * 3, edge[0] * 3 + 3), vertex1Offset * 3);
15306
+ newPosition[vertex1Offset * 3 + 2] = newPosition[vertex1Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
15307
+ // Define POSITION for new 2nd vertex
15308
+ newPosition.set(attributes.POSITION.value.subarray(edge[1] * 3, edge[1] * 3 + 3), vertex2Offset * 3);
15309
+ newPosition[vertex2Offset * 3 + 2] = newPosition[vertex2Offset * 3 + 2] - skirtHeight; // put down elevation on the skirt height
15310
+ // Use same TEXCOORDS for skirt vertices
15311
+ newTexcoord0.set(attributes.TEXCOORD_0.value.subarray(edge[0] * 2, edge[0] * 2 + 2), vertex1Offset * 2);
15312
+ newTexcoord0.set(attributes.TEXCOORD_0.value.subarray(edge[1] * 2, edge[1] * 2 + 2), vertex2Offset * 2);
15313
+ // Define new triangles
15314
+ const triangle1Offset = edgeIndex * 2 * 3;
15315
+ newTriangles[triangle1Offset] = edge[0];
15316
+ newTriangles[triangle1Offset + 1] = positionsLength / 3 + vertex2Offset;
15317
+ newTriangles[triangle1Offset + 2] = edge[1];
15318
+ newTriangles[triangle1Offset + 3] = positionsLength / 3 + vertex2Offset;
15319
+ newTriangles[triangle1Offset + 4] = edge[0];
15320
+ newTriangles[triangle1Offset + 5] = positionsLength / 3 + vertex1Offset;
15321
+ }
15322
+
15323
+ // loaders.gl
15324
+ // SPDX-License-Identifier: MIT
15325
+ // Copyright (c) vis.gl contributors
15326
+ // ISC License
15327
+ // Copyright(c) 2019, Michael Fogleman, Vladimir Agafonkin
15328
+ // @ts-nocheck
15329
+ /* eslint-disable complexity, max-params, max-statements, max-depth, no-constant-condition */
15330
+ class Delatin {
15331
+ constructor(data, width, height = width) {
15332
+ this.data = data; // height data
15333
+ this.width = width;
15334
+ this.height = height;
15335
+ this.coords = []; // vertex coordinates (x, y)
15336
+ this.triangles = []; // mesh triangle indices
15337
+ // additional triangle data
15338
+ this._halfedges = [];
15339
+ this._candidates = [];
15340
+ this._queueIndices = [];
15341
+ this._queue = []; // queue of added triangles
15342
+ this._errors = [];
15343
+ this._rms = [];
15344
+ this._pending = []; // triangles pending addition to queue
15345
+ this._pendingLen = 0;
15346
+ this._rmsSum = 0;
15347
+ const x1 = width - 1;
15348
+ const y1 = height - 1;
15349
+ const p0 = this._addPoint(0, 0);
15350
+ const p1 = this._addPoint(x1, 0);
15351
+ const p2 = this._addPoint(0, y1);
15352
+ const p3 = this._addPoint(x1, y1);
15353
+ // add initial two triangles
15354
+ const t0 = this._addTriangle(p3, p0, p2, -1, -1, -1);
15355
+ this._addTriangle(p0, p3, p1, t0, -1, -1);
15356
+ this._flush();
15357
+ }
15358
+ // refine the mesh until its maximum error gets below the given one
15359
+ run(maxError = 1) {
15360
+ while (this.getMaxError() > maxError) {
15361
+ this.refine();
15362
+ }
15363
+ }
15364
+ // refine the mesh with a single point
15365
+ refine() {
15366
+ this._step();
15367
+ this._flush();
15368
+ }
15369
+ // max error of the current mesh
15370
+ getMaxError() {
15371
+ return this._errors[0];
15372
+ }
15373
+ // root-mean-square deviation of the current mesh
15374
+ getRMSD() {
15375
+ return this._rmsSum > 0 ? Math.sqrt(this._rmsSum / (this.width * this.height)) : 0;
15376
+ }
15377
+ // height value at a given position
15378
+ heightAt(x, y) {
15379
+ return this.data[this.width * y + x];
15380
+ }
15381
+ // rasterize and queue all triangles that got added or updated in _step
15382
+ _flush() {
15383
+ const { coords } = this;
15384
+ for (let i = 0; i < this._pendingLen; i++) {
15385
+ const t = this._pending[i];
15386
+ // rasterize triangle to find maximum pixel error
15387
+ const a = 2 * this.triangles[t * 3 + 0];
15388
+ const b = 2 * this.triangles[t * 3 + 1];
15389
+ const c = 2 * this.triangles[t * 3 + 2];
15390
+ this._findCandidate(coords[a], coords[a + 1], coords[b], coords[b + 1], coords[c], coords[c + 1], t);
15391
+ }
15392
+ this._pendingLen = 0;
15393
+ }
15394
+ // rasterize a triangle, find its max error, and queue it for processing
15395
+ _findCandidate(p0x, p0y, p1x, p1y, p2x, p2y, t) {
15396
+ // triangle bounding box
15397
+ const minX = Math.min(p0x, p1x, p2x);
15398
+ const minY = Math.min(p0y, p1y, p2y);
15399
+ const maxX = Math.max(p0x, p1x, p2x);
15400
+ const maxY = Math.max(p0y, p1y, p2y);
15401
+ // forward differencing variables
15402
+ let w00 = orient(p1x, p1y, p2x, p2y, minX, minY);
15403
+ let w01 = orient(p2x, p2y, p0x, p0y, minX, minY);
15404
+ let w02 = orient(p0x, p0y, p1x, p1y, minX, minY);
15405
+ const a01 = p1y - p0y;
15406
+ const b01 = p0x - p1x;
15407
+ const a12 = p2y - p1y;
15408
+ const b12 = p1x - p2x;
15409
+ const a20 = p0y - p2y;
15410
+ const b20 = p2x - p0x;
15411
+ // pre-multiplied z values at vertices
15412
+ const a = orient(p0x, p0y, p1x, p1y, p2x, p2y);
15413
+ const z0 = this.heightAt(p0x, p0y) / a;
15414
+ const z1 = this.heightAt(p1x, p1y) / a;
15415
+ const z2 = this.heightAt(p2x, p2y) / a;
15416
+ // iterate over pixels in bounding box
15417
+ let maxError = 0;
15418
+ let mx = 0;
15419
+ let my = 0;
15420
+ let rms = 0;
15421
+ for (let y = minY; y <= maxY; y++) {
15422
+ // compute starting offset
15423
+ let dx = 0;
15424
+ if (w00 < 0 && a12 !== 0) {
15425
+ dx = Math.max(dx, Math.floor(-w00 / a12));
15426
+ }
15427
+ if (w01 < 0 && a20 !== 0) {
15428
+ dx = Math.max(dx, Math.floor(-w01 / a20));
15429
+ }
15430
+ if (w02 < 0 && a01 !== 0) {
15431
+ dx = Math.max(dx, Math.floor(-w02 / a01));
15432
+ }
15433
+ let w0 = w00 + a12 * dx;
15434
+ let w1 = w01 + a20 * dx;
15435
+ let w2 = w02 + a01 * dx;
15436
+ let wasInside = false;
15437
+ for (let x = minX + dx; x <= maxX; x++) {
15438
+ // check if inside triangle
15439
+ if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
15440
+ wasInside = true;
15441
+ // compute z using barycentric coordinates
15442
+ const z = z0 * w0 + z1 * w1 + z2 * w2;
15443
+ const dz = Math.abs(z - this.heightAt(x, y));
15444
+ rms += dz * dz;
15445
+ if (dz > maxError) {
15446
+ maxError = dz;
15447
+ mx = x;
15448
+ my = y;
15449
+ }
15450
+ }
15451
+ else if (wasInside) {
15452
+ break;
15453
+ }
15454
+ w0 += a12;
15455
+ w1 += a20;
15456
+ w2 += a01;
15457
+ }
15458
+ w00 += b12;
15459
+ w01 += b20;
15460
+ w02 += b01;
15461
+ }
15462
+ if ((mx === p0x && my === p0y) || (mx === p1x && my === p1y) || (mx === p2x && my === p2y)) {
15463
+ maxError = 0;
15464
+ }
15465
+ // update triangle metadata
15466
+ this._candidates[2 * t] = mx;
15467
+ this._candidates[2 * t + 1] = my;
15468
+ this._rms[t] = rms;
15469
+ // add triangle to priority queue
15470
+ this._queuePush(t, maxError, rms);
15471
+ }
15472
+ // process the next triangle in the queue, splitting it with a new point
15473
+ _step() {
15474
+ // pop triangle with highest error from priority queue
15475
+ const t = this._queuePop();
15476
+ const e0 = t * 3 + 0;
15477
+ const e1 = t * 3 + 1;
15478
+ const e2 = t * 3 + 2;
15479
+ const p0 = this.triangles[e0];
15480
+ const p1 = this.triangles[e1];
15481
+ const p2 = this.triangles[e2];
15482
+ const ax = this.coords[2 * p0];
15483
+ const ay = this.coords[2 * p0 + 1];
15484
+ const bx = this.coords[2 * p1];
15485
+ const by = this.coords[2 * p1 + 1];
15486
+ const cx = this.coords[2 * p2];
15487
+ const cy = this.coords[2 * p2 + 1];
15488
+ const px = this._candidates[2 * t];
15489
+ const py = this._candidates[2 * t + 1];
15490
+ const pn = this._addPoint(px, py);
15491
+ if (orient(ax, ay, bx, by, px, py) === 0) {
15492
+ this._handleCollinear(pn, e0);
15493
+ }
15494
+ else if (orient(bx, by, cx, cy, px, py) === 0) {
15495
+ this._handleCollinear(pn, e1);
15496
+ }
15497
+ else if (orient(cx, cy, ax, ay, px, py) === 0) {
15498
+ this._handleCollinear(pn, e2);
15499
+ }
15500
+ else {
15501
+ const h0 = this._halfedges[e0];
15502
+ const h1 = this._halfedges[e1];
15503
+ const h2 = this._halfedges[e2];
15504
+ const t0 = this._addTriangle(p0, p1, pn, h0, -1, -1, e0);
15505
+ const t1 = this._addTriangle(p1, p2, pn, h1, -1, t0 + 1);
15506
+ const t2 = this._addTriangle(p2, p0, pn, h2, t0 + 2, t1 + 1);
15507
+ this._legalize(t0);
15508
+ this._legalize(t1);
15509
+ this._legalize(t2);
15510
+ }
15511
+ }
15512
+ // add coordinates for a new vertex
15513
+ _addPoint(x, y) {
15514
+ const i = this.coords.length >> 1;
15515
+ this.coords.push(x, y);
15516
+ return i;
15517
+ }
15518
+ // add or update a triangle in the mesh
15519
+ _addTriangle(a, b, c, ab, bc, ca, e = this.triangles.length) {
15520
+ const t = e / 3; // new triangle index
15521
+ // add triangle vertices
15522
+ this.triangles[e + 0] = a;
15523
+ this.triangles[e + 1] = b;
15524
+ this.triangles[e + 2] = c;
15525
+ // add triangle halfedges
15526
+ this._halfedges[e + 0] = ab;
15527
+ this._halfedges[e + 1] = bc;
15528
+ this._halfedges[e + 2] = ca;
15529
+ // link neighboring halfedges
15530
+ if (ab >= 0) {
15531
+ this._halfedges[ab] = e + 0;
15532
+ }
15533
+ if (bc >= 0) {
15534
+ this._halfedges[bc] = e + 1;
15535
+ }
15536
+ if (ca >= 0) {
15537
+ this._halfedges[ca] = e + 2;
15538
+ }
15539
+ // init triangle metadata
15540
+ this._candidates[2 * t + 0] = 0;
15541
+ this._candidates[2 * t + 1] = 0;
15542
+ this._queueIndices[t] = -1;
15543
+ this._rms[t] = 0;
15544
+ // add triangle to pending queue for later rasterization
15545
+ this._pending[this._pendingLen++] = t;
15546
+ // return first halfedge index
15547
+ return e;
15548
+ }
15549
+ _legalize(a) {
15550
+ // if the pair of triangles doesn't satisfy the Delaunay condition
15551
+ // (p1 is inside the circumcircle of [p0, pl, pr]), flip them,
15552
+ // then do the same check/flip recursively for the new pair of triangles
15553
+ //
15554
+ // pl pl
15555
+ // /||\ / \
15556
+ // al/ || \bl al/ \a
15557
+ // / || \ / \
15558
+ // / a||b \ flip /___ar___\
15559
+ // p0\ || /p1 => p0\---bl---/p1
15560
+ // \ || / \ /
15561
+ // ar\ || /br b\ /br
15562
+ // \||/ \ /
15563
+ // pr pr
15564
+ const b = this._halfedges[a];
15565
+ if (b < 0) {
15566
+ return;
15567
+ }
15568
+ const a0 = a - (a % 3);
15569
+ const b0 = b - (b % 3);
15570
+ const al = a0 + ((a + 1) % 3);
15571
+ const ar = a0 + ((a + 2) % 3);
15572
+ const bl = b0 + ((b + 2) % 3);
15573
+ const br = b0 + ((b + 1) % 3);
15574
+ const p0 = this.triangles[ar];
15575
+ const pr = this.triangles[a];
15576
+ const pl = this.triangles[al];
15577
+ const p1 = this.triangles[bl];
15578
+ const { coords } = this;
15579
+ if (!inCircle(coords[2 * p0], coords[2 * p0 + 1], coords[2 * pr], coords[2 * pr + 1], coords[2 * pl], coords[2 * pl + 1], coords[2 * p1], coords[2 * p1 + 1])) {
15580
+ return;
15581
+ }
15582
+ const hal = this._halfedges[al];
15583
+ const har = this._halfedges[ar];
15584
+ const hbl = this._halfedges[bl];
15585
+ const hbr = this._halfedges[br];
15586
+ this._queueRemove(a0 / 3);
15587
+ this._queueRemove(b0 / 3);
15588
+ const t0 = this._addTriangle(p0, p1, pl, -1, hbl, hal, a0);
15589
+ const t1 = this._addTriangle(p1, p0, pr, t0, har, hbr, b0);
15590
+ this._legalize(t0 + 1);
15591
+ this._legalize(t1 + 2);
15592
+ }
15593
+ // handle a case where new vertex is on the edge of a triangle
15594
+ _handleCollinear(pn, a) {
15595
+ const a0 = a - (a % 3);
15596
+ const al = a0 + ((a + 1) % 3);
15597
+ const ar = a0 + ((a + 2) % 3);
15598
+ const p0 = this.triangles[ar];
15599
+ const pr = this.triangles[a];
15600
+ const pl = this.triangles[al];
15601
+ const hal = this._halfedges[al];
15602
+ const har = this._halfedges[ar];
15603
+ const b = this._halfedges[a];
15604
+ if (b < 0) {
15605
+ const t0 = this._addTriangle(pn, p0, pr, -1, har, -1, a0);
15606
+ const t1 = this._addTriangle(p0, pn, pl, t0, -1, hal);
15607
+ this._legalize(t0 + 1);
15608
+ this._legalize(t1 + 2);
15609
+ return;
15610
+ }
15611
+ const b0 = b - (b % 3);
15612
+ const bl = b0 + ((b + 2) % 3);
15613
+ const br = b0 + ((b + 1) % 3);
15614
+ const p1 = this.triangles[bl];
15615
+ const hbl = this._halfedges[bl];
15616
+ const hbr = this._halfedges[br];
15617
+ this._queueRemove(b0 / 3);
15618
+ const t0 = this._addTriangle(p0, pr, pn, har, -1, -1, a0);
15619
+ const t1 = this._addTriangle(pr, p1, pn, hbr, -1, t0 + 1, b0);
15620
+ const t2 = this._addTriangle(p1, pl, pn, hbl, -1, t1 + 1);
15621
+ const t3 = this._addTriangle(pl, p0, pn, hal, t0 + 2, t2 + 1);
15622
+ this._legalize(t0);
15623
+ this._legalize(t1);
15624
+ this._legalize(t2);
15625
+ this._legalize(t3);
15626
+ }
15627
+ // priority queue methods
15628
+ _queuePush(t, error, rms) {
15629
+ const i = this._queue.length;
15630
+ this._queueIndices[t] = i;
15631
+ this._queue.push(t);
15632
+ this._errors.push(error);
15633
+ this._rmsSum += rms;
15634
+ this._queueUp(i);
15635
+ }
15636
+ _queuePop() {
15637
+ const n = this._queue.length - 1;
15638
+ this._queueSwap(0, n);
15639
+ this._queueDown(0, n);
15640
+ return this._queuePopBack();
15641
+ }
15642
+ _queuePopBack() {
15643
+ const t = this._queue.pop();
15644
+ this._errors.pop();
15645
+ this._rmsSum -= this._rms[t];
15646
+ this._queueIndices[t] = -1;
15647
+ return t;
15648
+ }
15649
+ _queueRemove(t) {
15650
+ const i = this._queueIndices[t];
15651
+ if (i < 0) {
15652
+ const it = this._pending.indexOf(t);
15653
+ if (it !== -1) {
15654
+ this._pending[it] = this._pending[--this._pendingLen];
15655
+ }
15656
+ else {
15657
+ throw new Error('Broken triangulation (something went wrong).');
15658
+ }
15659
+ return;
15660
+ }
15661
+ const n = this._queue.length - 1;
15662
+ if (n !== i) {
15663
+ this._queueSwap(i, n);
15664
+ if (!this._queueDown(i, n)) {
15665
+ this._queueUp(i);
15666
+ }
15667
+ }
15668
+ this._queuePopBack();
15669
+ }
15670
+ _queueLess(i, j) {
15671
+ return this._errors[i] > this._errors[j];
15672
+ }
15673
+ _queueSwap(i, j) {
15674
+ const pi = this._queue[i];
15675
+ const pj = this._queue[j];
15676
+ this._queue[i] = pj;
15677
+ this._queue[j] = pi;
15678
+ this._queueIndices[pi] = j;
15679
+ this._queueIndices[pj] = i;
15680
+ const e = this._errors[i];
15681
+ this._errors[i] = this._errors[j];
15682
+ this._errors[j] = e;
15683
+ }
15684
+ _queueUp(j0) {
15685
+ let j = j0;
15686
+ while (true) {
15687
+ const i = (j - 1) >> 1;
15688
+ if (i === j || !this._queueLess(j, i)) {
15689
+ break;
15690
+ }
15691
+ this._queueSwap(i, j);
15692
+ j = i;
15693
+ }
15694
+ }
15695
+ _queueDown(i0, n) {
15696
+ let i = i0;
15697
+ while (true) {
15698
+ const j1 = 2 * i + 1;
15699
+ if (j1 >= n || j1 < 0) {
15700
+ break;
15701
+ }
15702
+ const j2 = j1 + 1;
15703
+ let j = j1;
15704
+ if (j2 < n && this._queueLess(j2, j1)) {
15705
+ j = j2;
15706
+ }
15707
+ if (!this._queueLess(j, i)) {
15708
+ break;
15709
+ }
15710
+ this._queueSwap(i, j);
15711
+ i = j;
15712
+ }
15713
+ return i > i0;
15714
+ }
15715
+ }
15716
+ function orient(ax, ay, bx, by, cx, cy) {
15717
+ return (bx - cx) * (ay - cy) - (by - cy) * (ax - cx);
15718
+ }
15719
+ function inCircle(ax, ay, bx, by, cx, cy, px, py) {
15720
+ const dx = ax - px;
15721
+ const dy = ay - py;
15722
+ const ex = bx - px;
15723
+ const ey = by - py;
15724
+ const fx = cx - px;
15725
+ const fy = cy - py;
15726
+ const ap = dx * dx + dy * dy;
15727
+ const bp = ex * ex + ey * ey;
15728
+ const cp = fx * fx + fy * fy;
15729
+ return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0;
15730
+ }
15731
+
14990
15732
  /* eslint 'max-len': [1, { code: 105, comments: 999, ignoreStrings: true, ignoreUrls: true }] */
15733
+ const tesselator = 'martini';
14991
15734
  const DefaultGeoImageOptions = {
14992
15735
  type: 'image',
14993
15736
  format: 'uint8',
@@ -15034,6 +15777,7 @@ class GeoImage {
15034
15777
  getMap(input, options) {
15035
15778
  return __awaiter(this, void 0, void 0, function* () {
15036
15779
  const mergedOptions = Object.assign(Object.assign({}, DefaultGeoImageOptions), options);
15780
+ console.log('xxx_mergedOptions', mergedOptions);
15037
15781
  switch (mergedOptions.type) {
15038
15782
  case 'image':
15039
15783
  return this.getBitmap(input, mergedOptions);
@@ -15066,32 +15810,111 @@ class GeoImage {
15066
15810
  let channel = rasters[0];
15067
15811
  if (options.useChannel != null) {
15068
15812
  if (rasters[options.useChannel]) {
15069
- channel = rasters[options.useChannel];
15813
+ channel = rasters[options.useChannel]; // length = 65536
15070
15814
  }
15071
15815
  }
15072
- const canvas = document.createElement('canvas');
15073
- canvas.width = width;
15074
- canvas.height = height;
15075
- const c = canvas.getContext('2d');
15076
- const imageData = c.createImageData(width, height);
15816
+ // const canvas = document.createElement('canvas');
15817
+ // canvas.width = width;
15818
+ // canvas.height = height;
15819
+ // // const c = canvas.getContext('2d');
15820
+ // // const imageData = c!.createImageData(width, height);
15821
+ // const terrain = new Float32Array((width + 1) * (height + 1));
15822
+ const terrain = new Float32Array((width + 1) * (height + 1)); // length = 66049
15077
15823
  const numOfChannels = channel.length / (width * height);
15078
- const size = width * height * 4;
15824
+ // return mesh data
15825
+ // const size: number = width * height * 4;
15826
+ const size = width * height;
15827
+ console.log('xxx_size', size);
15079
15828
  let pixel = options.useChannel === null ? 0 : options.useChannel;
15080
- for (let i = 0; i < size; i += 4) {
15081
- // height image calculation based on:
15082
- // https://deck.gl/docs/api-reference/geo-layers/terrain-layer
15083
- const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier;
15084
- const colorValue = Math.floor((elevationValue + 10000) / 0.1);
15085
- imageData.data[i] = Math.floor(colorValue / (256 * 256));
15086
- imageData.data[i + 1] = Math.floor((colorValue / 256) % 256);
15087
- imageData.data[i + 2] = colorValue % 256;
15088
- imageData.data[i + 3] = 255;
15089
- pixel += numOfChannels;
15829
+ for (let i = 0, y = 0; y < height; y++) {
15830
+ for (let x = 0; x < width; x++, i++) {
15831
+ const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier;
15832
+ terrain[i + y] = elevationValue;
15833
+ pixel += numOfChannels;
15834
+ }
15090
15835
  }
15091
- c.putImageData(imageData, 0, 0);
15092
- const imageUrl = canvas.toDataURL('image/png');
15836
+ // for (let i = 0; i < size; i++) {
15837
+ // // height image calculation based on:
15838
+ // // https://deck.gl/docs/api-reference/geo-layers/terrain-layer
15839
+ // const elevationValue = (options.noDataValue && channel[pixel] === options.noDataValue) ? options.terrainMinValue : channel[pixel] * options.multiplier!;
15840
+ // terrain[i] = elevationValue;
15841
+ // // const colorValue = Math.floor((elevationValue + 10000) / 0.1);
15842
+ // // imageData.data[i] = Math.floor(colorValue / (256 * 256));
15843
+ // // imageData.data[i + 1] = Math.floor((colorValue / 256) % 256);
15844
+ // // imageData.data[i + 2] = colorValue % 256;
15845
+ // // imageData.data[i + 3] = 255;
15846
+ // pixel += numOfChannels;
15847
+ // }
15848
+ // c!.putImageData(imageData, 0, 0);
15849
+ // const imageUrl = canvas.toDataURL('image/png');
15093
15850
  // console.log('Heightmap generated.');
15094
- return imageUrl;
15851
+ console.log('xxx_terrain', terrain);
15852
+ if (terrain[0] > 0) {
15853
+ debugger;
15854
+ }
15855
+ {
15856
+ // backfill bottom border
15857
+ for (let i = (width + 1) * width, x = 0; x < width; x++, i++) {
15858
+ terrain[i] = terrain[i - width - 1];
15859
+ }
15860
+ // backfill right border
15861
+ for (let i = height, y = 0; y < height + 1; y++, i += height + 1) {
15862
+ terrain[i] = terrain[i - 1];
15863
+ }
15864
+ }
15865
+ // getMesh
15866
+ const { terrainSkirtHeight } = options;
15867
+ console.log('xxx_bounds_0', input.bounds);
15868
+ let mesh;
15869
+ switch (tesselator) {
15870
+ case 'martini':
15871
+ mesh = getMartiniTileMesh(terrainSkirtHeight, width, terrain);
15872
+ break;
15873
+ case 'delatin':
15874
+ mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
15875
+ break;
15876
+ default:
15877
+ if (width === height && !(height && (width - 1))) {
15878
+ // fixme get terrain to separate method
15879
+ // terrain = getTerrain(data, width, height, elevationDecoder, 'martini');
15880
+ mesh = getMartiniTileMesh(terrainSkirtHeight, width, terrain);
15881
+ }
15882
+ else {
15883
+ // fixme get terrain to separate method
15884
+ // terrain = getTerrain(data, width, height, elevationDecoder, 'delatin');
15885
+ mesh = getDelatinTileMesh(meshMaxError, width, height, terrain);
15886
+ }
15887
+ break;
15888
+ }
15889
+ // Martini
15890
+ // Martini
15891
+ // Delatin
15892
+ // Delatin
15893
+ const { vertices } = mesh;
15894
+ let { triangles } = mesh;
15895
+ let attributes = getMeshAttributes(vertices, terrain, width, height, input.bounds);
15896
+ // Compute bounding box before adding skirt so that z values are not skewed
15897
+ const boundingBox = getMeshBoundingBox(attributes);
15898
+ // FIXME uncomment and add skirt
15899
+ console.log('xxx_skirtHeight', terrainSkirtHeight);
15900
+ if (terrainSkirtHeight) {
15901
+ const { attributes: newAttributes, triangles: newTriangles } = addSkirt(attributes, triangles, terrainSkirtHeight);
15902
+ attributes = newAttributes;
15903
+ triangles = newTriangles;
15904
+ }
15905
+ return {
15906
+ // Data return by this loader implementation
15907
+ loaderData: {
15908
+ header: {},
15909
+ },
15910
+ header: {
15911
+ vertexCount: triangles.length,
15912
+ boundingBox,
15913
+ },
15914
+ mode: 4,
15915
+ indices: { value: Uint32Array.from(triangles), size: 1 },
15916
+ attributes,
15917
+ };
15095
15918
  });
15096
15919
  }
15097
15920
  getBitmap(input, options) {
@@ -15128,6 +15951,7 @@ class GeoImage {
15128
15951
  let b;
15129
15952
  let a;
15130
15953
  const size = width * height * 4;
15954
+ // const size = width * height;
15131
15955
  if (!options.noDataValue) {
15132
15956
  console.log('Missing noData value. Raster might be displayed incorrectly.');
15133
15957
  }
@@ -15348,6 +16172,68 @@ class GeoImage {
15348
16172
  return noDataValue !== undefined && pixels.every((pixel) => pixel === noDataValue);
15349
16173
  }
15350
16174
  }
16175
+ //
16176
+ //
16177
+ //
16178
+ /**
16179
+ * Get Martini generated vertices and triangles
16180
+ *
16181
+ * @param {number} meshMaxError threshold for simplifying mesh
16182
+ * @param {number} width width of the input data
16183
+ * @param {number[] | Float32Array} terrain elevation data
16184
+ * @returns {{vertices: Uint16Array, triangles: Uint32Array}} vertices and triangles data
16185
+ */
16186
+ function getMartiniTileMesh(meshMaxError, width, terrain) {
16187
+ const gridSize = width + 1;
16188
+ const martini = new Martini(gridSize);
16189
+ const tile = martini.createTile(terrain);
16190
+ const { vertices, triangles } = tile.getMesh(meshMaxError);
16191
+ return { vertices, triangles };
16192
+ }
16193
+ function getMeshAttributes(vertices, terrain, width, height, bounds) {
16194
+ const gridSize = width + 1;
16195
+ const numOfVerticies = vertices.length / 2;
16196
+ // vec3. x, y in pixels, z in meters
16197
+ const positions = new Float32Array(numOfVerticies * 3);
16198
+ // vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
16199
+ const texCoords = new Float32Array(numOfVerticies * 2);
16200
+ console.log('xxx_bounds', bounds);
16201
+ const [minX, minY, maxX, maxY] = bounds || [0, 0, width, height];
16202
+ const xScale = (maxX - minX) / width;
16203
+ const yScale = (maxY - minY) / height;
16204
+ for (let i = 0; i < numOfVerticies; i++) {
16205
+ const x = vertices[i * 2];
16206
+ const y = vertices[i * 2 + 1];
16207
+ const pixelIdx = y * gridSize + x;
16208
+ positions[3 * i + 0] = x * xScale + minX;
16209
+ positions[3 * i + 1] = -y * yScale + maxY;
16210
+ positions[3 * i + 2] = terrain[pixelIdx];
16211
+ texCoords[2 * i + 0] = x / width;
16212
+ texCoords[2 * i + 1] = y / height;
16213
+ }
16214
+ return {
16215
+ POSITION: { value: positions, size: 3 },
16216
+ TEXCOORD_0: { value: texCoords, size: 2 },
16217
+ // NORMAL: {}, - optional, but creates the high poly look with lighting
16218
+ };
16219
+ }
16220
+ /**
16221
+ * Get Delatin generated vertices and triangles
16222
+ *
16223
+ * @param {number} meshMaxError threshold for simplifying mesh
16224
+ * @param {number} width width of the input data array
16225
+ * @param {number} height height of the input data array
16226
+ * @param {number[] | Float32Array} terrain elevation data
16227
+ * @returns {{vertices: number[], triangles: number[]}} vertices and triangles data
16228
+ */
16229
+ function getDelatinTileMesh(meshMaxError, width, height, terrain) {
16230
+ const tin = new Delatin(terrain, width + 1, height + 1);
16231
+ tin.run(meshMaxError);
16232
+ // @ts-expect-error
16233
+ const { coords, triangles } = tin;
16234
+ const vertices = coords;
16235
+ return { vertices, triangles };
16236
+ }
15351
16237
 
15352
16238
  const EARTH_CIRCUMFERENCE = 40075000.0;
15353
16239
  const EARTH_HALF_CIRCUMFERENCE = 20037500.0;
@@ -15441,7 +16327,7 @@ class CogTiles {
15441
16327
  const cartographicPositionAdjusted = [cartographicPosition[0], -cartographicPosition[1]];
15442
16328
  return cartographicPositionAdjusted;
15443
16329
  }
15444
- getTile(x, y, z) {
16330
+ getTile(x, y, z, bounds) {
15445
16331
  return __awaiter(this, void 0, void 0, function* () {
15446
16332
  const wantedMpp = this.getResolutionFromZoomLevel(this.tileSize, z);
15447
16333
  const img = this.cog.getImageByResolution(wantedMpp);
@@ -15487,7 +16373,7 @@ class CogTiles {
15487
16373
  // console.log("Bits per sample: " + bitsPerSample)
15488
16374
  // console.log("Single channel pixel format: " + bitsPerSample/)
15489
16375
  if (x - ox >= 0 && y - oy >= 0 && x - ox < tilesX && y - oy < tilesY) {
15490
- // console.log("getting tile: " + [x - ox, y - oy]);
16376
+ // console.log(`getting tile: ${[x - ox, y - oy]}`);
15491
16377
  const tile = yield img.getTile((x - ox), (y - oy));
15492
16378
  // console.time("Request to data time: ")
15493
16379
  switch (img.compression) {
@@ -15533,10 +16419,12 @@ class CogTiles {
15533
16419
  default: decompressedFormatted = null;
15534
16420
  }
15535
16421
  // console.log(decompressedFormatted)
16422
+ // const { meshMaxError, bounds, elevationDecoder } = this.options;
15536
16423
  decompressed = yield this.geo.getMap({
15537
16424
  rasters: [decompressedFormatted],
15538
16425
  width: this.tileSize,
15539
16426
  height: this.tileSize,
16427
+ bounds,
15540
16428
  }, this.options);
15541
16429
  // console.log(decompressed.length)
15542
16430
  return decompressed;
@@ -15778,7 +16666,7 @@ class CogTerrainLayer extends core.CompositeLayer {
15778
16666
  const cog = yield this.terrainCogTiles.initializeCog(terrainUrl);
15779
16667
  this.tileSize = this.terrainCogTiles.getTileSize(cog);
15780
16668
  const zoomRange = this.terrainCogTiles.getZoomRange(cog);
15781
- [this.minZoom, this.maxZoom] = zoomRange;
16669
+ [this.props.minZoom, this.props.maxZoom] = zoomRange;
15782
16670
  this.setState({ initialized: true });
15783
16671
  });
15784
16672
  }