• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iTowns / itowns / 10635241580

30 Aug 2024 03:26PM UTC coverage: 86.966% (-2.8%) from 89.766%
10635241580

push

github

jailln
feat(3dtiles): add new OGC3DTilesLayer using 3d-tiles-renderer-js

Deprecate C3DTilesLayer (replaced by OGC3DTilesLayer).
Add new iGLTFLoader that loads gltf 1.0 and 2.0 files.

2791 of 3694 branches covered (75.55%)

Branch coverage included in aggregate %.

480 of 644 new or added lines in 8 files covered. (74.53%)

2144 existing lines in 111 files now uncovered.

24319 of 27479 relevant lines covered (88.5%)

1024.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.68
/src/Utils/DEMUtils.js
1
import * as THREE from 'three';
1✔
2
import Coordinates from 'Core/Geographic/Coordinates';
1✔
3
import placeObjectOnGround from 'Utils/placeObjectOnGround';
1✔
4

1✔
5
const FAST_READ_Z = 0;
1✔
6
const PRECISE_READ_Z = 1;
1✔
7

1✔
8

1✔
9
/**
1✔
10
 * Utility module to retrieve elevation at a given coordinates. The returned
1✔
11
 * value is read in the elevation textures used by the graphics card to render
1✔
12
 * the tiles (globe or plane). This implies that the return value may change
1✔
13
 * depending on the current tile resolution.
1✔
14
 *
1✔
15
 * @module DEMUtils
1✔
16
 */
1✔
17
export default {
1✔
18
    /**
1✔
19
     * Gives the elevation value of a {@link TiledGeometryLayer}, at a specific
1✔
20
     * {@link Coordinates}.
1✔
21
     *
1✔
22
     * @param {TiledGeometryLayer} layer - The tile layer owning the elevation
1✔
23
     * textures we're going to query. This is typically a `GlobeLayer` or
1✔
24
     * `PlanarLayer` (accessible through `view.tileLayer`).
1✔
25
     * @param {Coordinates} coord - The coordinates that we're interested in.
1✔
26
     * @param {number} [method=FAST_READ_Z] - There are two available methods:
1✔
27
     * `FAST_READ_Z` (default) or `PRECISE_READ_Z`. The first one is faster,
1✔
28
     * while the second one is slower but gives better precision.
1✔
29
     * @param {TileMesh[]} [tileHint] - Optional array of tiles to speed up the
1✔
30
     * process. You can give candidates tiles likely to contain `coord`.
1✔
31
     * Otherwise the lookup process starts from the root of `layer`.
1✔
32
     *
1✔
33
     * @return {number} If found, a value in meters is returned; otherwise
1✔
34
     * `undefined`.
1✔
35
     */
1✔
36
    getElevationValueAt(layer, coord, method = FAST_READ_Z, tileHint) {
1✔
37
        const result = _readZ(layer, method, coord, tileHint || layer.level0Nodes);
209✔
38
        if (result) {
209✔
39
            return result.coord.z;
2✔
40
        }
2✔
41
    },
1✔
42

1✔
43
    /**
1✔
44
     * @typedef Terrain
1✔
45
     * @type {Object}
1✔
46
     *
1✔
47
     * @property {Coordinates} coord - Pick coordinate with the elevation in coord.z.
1✔
48
     * @property {THREE.Texture} texture - the picked elevation texture.
1✔
49
     * The texture where the `z` value has been read from
1✔
50
     * @property {TileMesh} tile - the picked tile and the tile containing the texture
1✔
51
     */
1✔
52
    /**
1✔
53
     * Gives a {@link Terrain} object, at a specific {@link Coordinates}. The returned
1✔
54
     * object is as follow:
1✔
55
     * - `coord`, Coordinate, coord.z is the value in meters of the elevation at the coordinates
1✔
56
     * - `texture`, the texture where the `z` value has been read from
1✔
57
     * - `tile`, the tile containing the texture
1✔
58
     * @example
1✔
59
     * // place mesh on the ground
1✔
60
     * const coord = new Coordinates('EPSG:4326', 6, 45);
1✔
61
     * const result = DEMUtils.getTerrainObjectAt(view.tileLayer, coord)
1✔
62
     * mesh.position.copy(result.coord.as(view.referenceCrs));
1✔
63
     * view.scene.add(mesh);
1✔
64
     * mesh.updateMatrixWorld();
1✔
65
     *
1✔
66
     *
1✔
67
     * @param {TiledGeometryLayer} layer - The tile layer owning the elevation
1✔
68
     * textures we're going to query. This is typically a `GlobeLayer` or
1✔
69
     * `PlanarLayer` (accessible through `view.tileLayer`).
1✔
70
     * @param {Coordinates} coord - The coordinates that we're interested in.
1✔
71
     * @param {number} [method=FAST_READ_Z] - There are two available methods:
1✔
72
     * `FAST_READ_Z` (default) or `PRECISE_READ_Z`. The first one is faster,
1✔
73
     * while the second one is slower but gives better precision.
1✔
74
     * @param {TileMesh[]} [tileHint] - Optional array of tiles to speed up the
1✔
75
     * process. You can give candidates tiles likely to contain `coord`.
1✔
76
     * Otherwise the lookup process starts from the root of `layer`.
1✔
77
     * @param {Object} [cache] - Object to cache previous result and speed up the next `getTerrainObjectAt`` use.
1✔
78
     *
1✔
79
     * @return {Terrain} - The {@link Terrain} object.
1✔
80
     */
1✔
81
    getTerrainObjectAt(layer, coord, method = FAST_READ_Z, tileHint, cache) {
1!
82
        return _readZ(layer, method, coord, tileHint || layer.level0Nodes, cache);
2!
83
    },
1✔
84
    FAST_READ_Z,
1✔
85
    PRECISE_READ_Z,
1✔
86
    placeObjectOnGround,
1✔
87
};
1✔
88

1✔
89
function tileAt(pt, tile) {
207✔
90
    if (tile.extent) {
207✔
91
        if (!tile.extent.isPointInside(pt)) {
207✔
92
            return undefined;
104✔
93
        }
104✔
94

103✔
95
        for (let i = 0; i < tile.children.length; i++) {
207!
UNCOV
96
            const t = tileAt(pt, tile.children[i]);
×
UNCOV
97
            if (t) {
×
UNCOV
98
                return t;
×
UNCOV
99
            }
×
UNCOV
100
        }
✔
101
        const tileLayer = tile.material.getElevationLayer();
103✔
102
        if (tileLayer && tileLayer.level >= 0) {
207✔
103
            return tile;
4✔
104
        }
4✔
105
        return undefined;
99✔
106
    }
99✔
107
}
207✔
108

1✔
109
let _canvas;
1✔
110
function _readTextureValueAt(metadata, texture, ...uv) {
16!
111
    for (let i = 0; i < uv.length; i += 2) {
16✔
112
        uv[i] = THREE.MathUtils.clamp(uv[i], 0, texture.image.width - 1);
58✔
113
        uv[i + 1] = THREE.MathUtils.clamp(uv[i + 1], 0, texture.image.height - 1);
58✔
114
    }
58✔
115

16✔
116
    if (texture.image.data) {
16✔
117
        // read a single value
16✔
118
        if (uv.length === 2) {
16✔
119
            const v = texture.image.data[uv[1] * texture.image.width + uv[0]];
2✔
120
            return v != metadata.noDataValue ? v : undefined;
2!
121
        }
2✔
122
        // or read multiple values
14✔
123
        const result = [];
14✔
124
        for (let i = 0; i < uv.length; i += 2) {
16✔
125
            const v = texture.image.data[uv[i + 1] * texture.image.width + uv[i]];
56✔
126
            result.push(v != metadata.noDataValue ? v : undefined);
56!
127
        }
56✔
128
        return result;
14✔
129
    } else {
16!
UNCOV
130
        if (!_canvas) {
×
UNCOV
131
            _canvas = document.createElement('canvas');
×
UNCOV
132
            _canvas.width = 2;
×
UNCOV
133
            _canvas.height = 2;
×
UNCOV
134
        }
×
UNCOV
135
        let minx = Infinity;
×
136
        let miny = Infinity;
×
137
        let maxx = -Infinity;
×
138
        let maxy = -Infinity;
×
139
        for (let i = 0; i < uv.length; i += 2) {
×
140
            minx = Math.min(uv[i], minx);
×
141
            miny = Math.min(uv[i + 1], miny);
×
142
            maxx = Math.max(uv[i], maxx);
×
143
            maxy = Math.max(uv[i + 1], maxy);
×
144
        }
×
145
        const dw = maxx - minx + 1;
×
146
        const dh = maxy - miny + 1;
×
147
        _canvas.width = Math.max(_canvas.width, dw);
×
148
        _canvas.height = Math.max(_canvas.height, dh);
×
149

×
150
        const ctx = _canvas.getContext('2d', { willReadFrequently: true });
×
151
        ctx.drawImage(texture.image, minx, miny, dw, dh, 0, 0, dw, dh);
×
152
        const d = ctx.getImageData(0, 0, dw, dh);
×
153

×
154
        const result = [];
×
155
        for (let i = 0; i < uv.length; i += 2) {
×
156
            const ox = uv[i] - minx;
×
157
            const oy = uv[i + 1] - miny;
×
158

×
159
            // d is 4 bytes per pixel
×
160
            const v = THREE.MathUtils.lerp(
×
161
                metadata.colorTextureElevationMinZ,
×
162
                metadata.colorTextureElevationMaxZ,
×
163
                d.data[4 * oy * dw + 4 * ox] / 255);
×
164
            result.push(v != metadata.noDataValue ? v : undefined);
×
165
        }
×
166
        if (uv.length === 2) {
×
167
            return result[0];
×
168
        } else {
×
UNCOV
169
            return result;
×
UNCOV
170
        }
×
UNCOV
171
    }
×
172
}
16✔
173

1✔
174
function _convertUVtoTextureCoords(texture, u, v) {
16✔
175
    const width = texture.image.width;
16✔
176
    const height = texture.image.height;
16✔
177

16✔
178
    const up = Math.max(0, u * width - 0.5);
16✔
179
    const vp = Math.max(0, v * height - 0.5);
16✔
180

16✔
181
    const u1 = Math.floor(up);
16✔
182
    const u2 = Math.ceil(up);
16✔
183
    const v1 = Math.floor(vp);
16✔
184
    const v2 = Math.ceil(vp);
16✔
185

16✔
186
    const wu = up - u1;
16✔
187
    const wv = vp - v1;
16✔
188

16✔
189
    return { u1, u2, v1, v2, wu, wv };
16✔
190
}
16✔
191

1✔
192
function _readTextureValueNearestFiltering(metadata, texture, vertexU, vertexV) {
2✔
193
    const coords = _convertUVtoTextureCoords(texture, vertexU, vertexV);
2✔
194

2✔
195
    const u = (coords.wu <= 0) ? coords.u1 : coords.u2;
2!
196
    const v = (coords.wv <= 0) ? coords.v1 : coords.v2;
2!
197

2✔
198
    return _readTextureValueAt(metadata, texture, u, v);
2✔
199
}
2✔
200

1✔
201
function _lerpWithUndefinedCheck(x, y, t) {
42✔
202
    if (x == undefined) {
42!
UNCOV
203
        return y;
×
204
    } else if (y == undefined) {
42!
UNCOV
205
        return x;
×
206
    } else {
42✔
207
        return THREE.MathUtils.lerp(x, y, t);
42✔
208
    }
42✔
209
}
42✔
210

1✔
211
export function readTextureValueWithBilinearFiltering(metadata, texture, vertexU, vertexV) {
1✔
212
    const coords = _convertUVtoTextureCoords(texture, vertexU, vertexV);
14✔
213

14✔
214
    const [z11, z21, z12, z22] = _readTextureValueAt(metadata, texture,
14✔
215
        coords.u1, coords.v1,
14✔
216
        coords.u2, coords.v1,
14✔
217
        coords.u1, coords.v2,
14✔
218
        coords.u2, coords.v2);
14✔
219

14✔
220

14✔
221
    // horizontal filtering
14✔
222
    const zu1 = _lerpWithUndefinedCheck(z11, z21, coords.wu);
14✔
223
    const zu2 = _lerpWithUndefinedCheck(z12, z22, coords.wu);
14✔
224
    // then vertical filtering
14✔
225
    return _lerpWithUndefinedCheck(zu1, zu2, coords.wv);
14✔
226
}
14✔
227

1✔
228

1✔
229
function _readZFast(layer, texture, uv) {
2✔
230
    const elevationLayer = layer.attachedLayers.filter(l => l.isElevationLayer)[0];
2✔
231
    return _readTextureValueNearestFiltering(elevationLayer, texture, uv.x, uv.y);
2✔
232
}
2✔
233

1✔
234
const bary = new THREE.Vector3();
1✔
235
function _readZCorrect(layer, texture, uv, tileDimensions, tileOwnerDimensions) {
2✔
236
    // We need to emulate the vertex shader code that does 2 thing:
2✔
237
    //   - interpolate (u, v) between triangle vertices: u,v will be multiple of 1/nsegments
2✔
238
    //     (for now assume nsegments == 16)
2✔
239
    //   - read elevation texture at (u, v) for
2✔
240

2✔
241
    // Determine u,v based on the vertices count.
2✔
242
    // 'modulo' is the gap (in [0, 1]) between 2 successive vertices in the geometry
2✔
243
    // e.g if you have 5 vertices, the only possible values for u (or v) are: 0, 0.25, 0.5, 0.75, 1
2✔
244
    // so modulo would be 0.25
2✔
245
    // note: currently the number of segments is hard-coded to 16 (see TileProvider) => 17 vertices
2✔
246
    const modulo = (tileDimensions.x / tileOwnerDimensions.x) / (17 - 1);
2✔
247
    let u = Math.floor(uv.x / modulo) * modulo;
2✔
248
    let v = Math.floor(uv.y / modulo) * modulo;
2✔
249

2✔
250
    if (u == 1) {
2!
UNCOV
251
        u -= modulo;
×
UNCOV
252
    }
×
253
    if (v == 1) {
2!
UNCOV
254
        v -= modulo;
×
UNCOV
255
    }
×
256

2✔
257
    // Build 4 vertices, 3 of them will be our triangle:
2✔
258
    //    11---21
2✔
259
    //    |   / |
2✔
260
    //    |  /  |
2✔
261
    //    | /   |
2✔
262
    //    21---22
2✔
263
    const u1 = u;
2✔
264
    const u2 = u + modulo;
2✔
265
    const v1 = v;
2✔
266
    const v2 = v + modulo;
2✔
267

2✔
268
    // Our multiple z-value will be weigh-blended, depending on the distance of the real point
2✔
269
    // so lu (resp. lv) are the weight. When lu -> 0 (resp. 1) the final value -> z at u1 (resp. u2)
2✔
270
    const lu = (uv.x - u) / modulo;
2✔
271
    const lv = (uv.y - v) / modulo;
2✔
272

2✔
273

2✔
274
    // Determine if we're going to read the vertices from the top-left or lower-right triangle
2✔
275
    // (low-right = on the line 21-22 or under the diagonal lu = 1 - lv)
2✔
276
    const lowerRightTriangle = (lv == 1) || lu / (1 - lv) >= 1;
2!
UNCOV
277

×
UNCOV
278
    const tri = new THREE.Triangle(
×
UNCOV
279
        new THREE.Vector3(u1, v2),
×
UNCOV
280
        new THREE.Vector3(u2, v1),
×
281
        lowerRightTriangle ? new THREE.Vector3(u2, v2) : new THREE.Vector3(u1, v1));
2✔
282

2✔
283
    // bary holds the respective weight of each vertices of the triangles
2✔
284
    tri.getBarycoord(new THREE.Vector3(uv.x, uv.y), bary);
2✔
285

2✔
286
    const elevationLayer = layer.attachedLayers.filter(l => l.isElevationLayer)[0];
2✔
287

2✔
288
    // read the 3 interesting values
2✔
289
    const z1 = readTextureValueWithBilinearFiltering(elevationLayer, texture, tri.a.x, tri.a.y);
2✔
290
    const z2 = readTextureValueWithBilinearFiltering(elevationLayer, texture, tri.b.x, tri.b.y);
2✔
291
    const z3 = readTextureValueWithBilinearFiltering(elevationLayer, texture, tri.c.x, tri.c.y);
2✔
292

2✔
293
    // Blend with bary
2✔
294
    return z1 * bary.x + z2 * bary.y + z3 * bary.z;
2✔
295
}
2✔
296

1✔
297
const temp = {
1✔
298
    v: new THREE.Vector3(),
1✔
299
    coord1: new Coordinates('EPSG:4978'),
1✔
300
    coord2: new Coordinates('EPSG:4978'),
1✔
301
    offset: new THREE.Vector2(),
1✔
302
};
1✔
303

1✔
304
const dimension = new THREE.Vector2();
1✔
305

1✔
306
function offsetInExtent(point, extent, target = new THREE.Vector2()) {
4!
307
    if (point.crs != extent.crs) {
4!
UNCOV
308
        throw new Error(`Unsupported mix: ${point.crs} and ${extent.crs}`);
×
UNCOV
309
    }
×
310

4✔
311
    extent.planarDimensions(dimension);
4✔
312

4✔
313
    const originX = (point.x - extent.west) / dimension.x;
4✔
314
    const originY = (extent.north - point.y) / dimension.y;
4✔
315

4✔
316
    return target.set(originX, originY);
4✔
317
}
4✔
318

1✔
319
function _readZ(layer, method, coord, nodes, cache) {
211✔
320
    const pt = coord.as(layer.extent.crs, temp.coord1);
211✔
321

211✔
322
    let tileWithValidElevationTexture = null;
211✔
323
    // first check in cache
211✔
324
    if (cache?.tile?.material) {
211!
UNCOV
325
        tileWithValidElevationTexture = tileAt(pt, cache.tile);
×
UNCOV
326
    }
×
327
    for (let i = 0; !tileWithValidElevationTexture && i < nodes.length; i++) {
211✔
328
        tileWithValidElevationTexture = tileAt(pt, nodes[i]);
207✔
329
    }
207✔
330

211✔
331
    if (!tileWithValidElevationTexture) {
211✔
332
        // failed to find a tile, abort
207✔
333
        return;
207✔
334
    }
207✔
335

4✔
336
    const tile = tileWithValidElevationTexture;
4✔
337
    const tileLayer = tile.material.getElevationLayer();
4✔
338
    const src = tileLayer.textures[0];
4✔
339

4✔
340
    // check cache value if existing
4✔
341
    if (cache) {
211!
UNCOV
342
        if (cache.id === src.id && cache.version === src.version) {
×
343
            return { coord: pt, texture: src, tile };
×
344
        }
×
UNCOV
345
    }
✔
346

4✔
347
    // Assuming that tiles are split in 4 children, we lookup the parent that
4✔
348
    // really owns this texture
4✔
349
    const stepsUpInHierarchy = Math.round(Math.log2(1.0 / tileLayer.offsetScales[0].z));
4✔
350
    for (let i = 0; i < stepsUpInHierarchy; i++) {
211!
UNCOV
351
        tileWithValidElevationTexture = tileWithValidElevationTexture.parent;
×
UNCOV
352
    }
✔
353

4✔
354
    // offset = offset from top-left
4✔
355
    offsetInExtent(pt, tileWithValidElevationTexture.extent, temp.offset);
4✔
356

4✔
357
    // At this point we have:
4✔
358
    //   - tileWithValidElevationTexture.texture.image which is the current image
4✔
359
    //     used for rendering
4✔
360
    //   - offset which is the offset in this texture for the coordinate we're
4✔
361
    //     interested in
4✔
362
    // We now have 2 options:
4✔
363
    //   - the fast one: read the value of tileWithValidElevationTexture.texture.image
4✔
364
    //     at (offset.x, offset.y) and we're done
4✔
365
    //   - the correct one: emulate the vertex shader code
4✔
366
    if (method == PRECISE_READ_Z) {
211✔
367
        pt.z = _readZCorrect(layer, src, temp.offset, tile.extent.planarDimensions(), tileWithValidElevationTexture.extent.planarDimensions());
2✔
368
    } else {
2✔
369
        pt.z = _readZFast(layer, src, temp.offset);
2✔
370
    }
2✔
371

4✔
372
    if (pt.z != undefined) {
4✔
373
        return { coord: pt, texture: src, tile };
4✔
374
    }
4✔
375
}
211✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc