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

iTowns / itowns / 6979781676

24 Nov 2023 10:44AM UTC coverage: 77.111% (+0.1%) from 77.004%
6979781676

Pull #2223

github

web-flow
Merge 23836a3b7 into 1d10290b5
Pull Request #2223: Fix base alti for mesh 3d

4051 of 5992 branches covered (0.0%)

Branch coverage included in aggregate %.

216 of 238 new or added lines in 9 files covered. (90.76%)

9 existing lines in 4 files now uncovered.

7986 of 9618 relevant lines covered (83.03%)

1640.25 hits per line

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

69.92
/src/Layer/LabelLayer.js
1
import * as THREE from 'three';
1✔
2
import LayerUpdateState from 'Layer/LayerUpdateState';
1✔
3
import ObjectRemovalHelper from 'Process/ObjectRemovalHelper';
1✔
4
import GeometryLayer from 'Layer/GeometryLayer';
1✔
5
import Coordinates from 'Core/Geographic/Coordinates';
1✔
6
import Extent from 'Core/Geographic/Extent';
1✔
7
import Label from 'Core/Label';
1✔
8
import { FEATURE_TYPES } from 'Core/Feature';
1✔
9
import { readExpression, StyleContext } from 'Core/Style';
1✔
10
import { ScreenGrid } from 'Renderer/Label2DRenderer';
1,764✔
11

12
const context = new StyleContext();
1✔
13

14
const coord = new Coordinates('EPSG:4326', 0, 0, 0);
1✔
15

16
const _extent = new Extent('EPSG:4326', 0, 0, 0, 0);
1✔
17

18
const nodeDimensions = new THREE.Vector2();
1✔
19
const westNorthNode = new THREE.Vector2();
1✔
20
const labelPosition = new THREE.Vector2();
1✔
21

22
/**
1✔
23
 * DomNode is a node in the tree data structure of labels divs.
24
 *
25
 * @class DomNode
26
 */
27
class DomNode {
1✔
28
    #domVisibility = false;
29

30
    constructor() {
6✔
31
        this.dom = document.createElement('div');
6✔
32

33
        this.dom.style.display = 'none';
6✔
34

35
        this.visible = true;
6✔
36
    }
1✔
37

38
    get visible() { return this.#domVisibility; }
×
39

40
    set visible(v) {
41
        if (v !== this.#domVisibility) {
12✔
42
            this.#domVisibility = v;
8✔
43
            this.dom.style.display = v ? 'block' : 'none';
8✔
44
        }
45
    }
46

47
    hide() { this.visible = false; }
1✔
48

49
    show() { this.visible = true; }
5✔
50

51
    add(node) {
52
        this.dom.append(node.dom);
2✔
53
    }
1✔
54
}
55

56
/**
57
 * LabelsNode is node of tree data structure for LabelLayer.
58
 * the node is made of dom elements and 3D labels.
59
 *
60
 * @class      LabelsNode
61
 */
62
class LabelsNode extends THREE.Group {
2✔
63
    constructor(node) {
2✔
64
        super();
2✔
65
        // attached node parent
66
        this.nodeParent = node;
2✔
67
        // When this is set, it calculates the position in that frame and resets this property to false.
68
        this.needsUpdate = true;
2✔
69
    }
70

71
    // instanciate dom elements
1✔
72
    initializeDom() {
73
        // create root dom
74
        this.domElements = new DomNode();
1✔
75
        // create labels container dom
76
        this.domElements.labels = new DomNode();
1✔
77

78
        this.domElements.add(this.domElements.labels);
1✔
79

80
        this.domElements.labels.dom.style.opacity = '0';
1✔
81
    }
82

83
    // add node label
84
    // add label 3d and dom label
85
    addLabel(label) {
86
        // add 3d object
87
        this.add(label);
1✔
88

89
        // add dom label
90
        this.domElements.labels.dom.append(label.content);
1✔
91

92
        // Batch update the dimensions of labels all at once to avoid
93
        // redraw for at least this tile.
94
        label.initDimensions();
1✔
95

96
        // add horizon culling point if it's necessary
97
        // the horizon culling is applied to nodes that trace the horizon which
98
        // corresponds to the low zoom node, that's why the culling is done for a zoom lower than 4.
99
        if (this.nodeParent.layer.isGlobeLayer && this.nodeParent.level < 4) {
1!
100
            label.horizonCullingPoint = new THREE.Vector3();
1✔
101
        }
102
    }
103

104
    // remove node label
105
    // remove label 3d and dom label
106
    removeLabel(label) {
107
        // remove 3d object
108
        this.remove(label);
×
109

110
        // remove dom label
111
        this.domElements.labels.dom.removeChild(label.content);
×
112
    }
113

114
    // update position if it's necessary
115
    updatePosition(label) {
116
        if (this.needsUpdate) {
1!
117
            // update elevation from elevation layer.
118
            if (this.needsAltitude) {
1!
UNCOV
119
                label.updateElevationFromLayer(this.nodeParent.layer, [this.nodeParent]);
×
120
            }
121

122
            // update elevation label
123
            label.update3dPosition(this.nodeParent.layer.crs);
1✔
124

125
            // update horizon culling
126
            label.updateHorizonCullingPoint();
1✔
127
        }
128
    }
129

130
    // return labels count
131
    count() {
132
        return this.children.length;
1✔
133
    }
134

135
    get labels() {
136
        return this.children;
1✔
137
    }
1✔
138
}
139

140
/**
6✔
141
 * A layer to handle a bunch of `Label`. This layer can be created on its own,
142
 * but it is better to use the option `addLabelLayer` on another `Layer` to let
143
 * it work with it (see the `vector_tile_raster_2d` example). Supported for Points features, not yet
144
 * for Lines and Polygons features.
145
 *
146
 * @property {boolean} isLabelLayer - Used to checkout whether this layer is a
147
 * LabelLayer.  Default is true. You should not change this, as it is used
148
 * internally for optimisation.
149
 */
150
class LabelLayer extends GeometryLayer {
2✔
151
    #filterGrid = new ScreenGrid();
152
    /**
153
     * @constructor
154
     * @extends Layer
155
     *
156
     * @param {string} id - The id of the layer, that should be unique. It is
157
     * not mandatory, but an error will be emitted if this layer is added a
158
     * {@link View} that already has a layer going by that id.
159
     * @param {Object} [config] - Optional configuration, all elements in it
160
     * will be merged as is in the layer. For example, if the configuration
161
     * contains three elements `name, protocol, extent`, these elements will be
162
     * available using `layer.name` or something else depending on the property
163
     * name.
164
     * @param {boolean} [config.performance=true] - remove labels that have no chance of being visible.
165
     * if the `config.performance` is set to true then the performance is improved
166
     * proportional to the amount of unnecessary labels that are removed.
167
     * Indeed, even in the best case, labels will never be displayed. By example, if there's many labels.
168
     * We advise you to not use this option if your data is optimized.
169
     * @param {domElement|function} config.domElement - An HTML domElement.
170
     * If set, all `Label` displayed within the current instance `LabelLayer`
171
     * will be this domElement.
172
     *
173
     * It can be set to a method. The single parameter of this method gives the
174
     * properties of each feature on which a `Label` is created.
175
     *
176
     * If set, all the parameters set in the `LabelLayer` `Style.text` will be overridden,
177
     * except for the `Style.text.anchor` parameter which can help place the label.
178
     */
179
    constructor(id, config = {}) {
4!
180
        const domElement = config.domElement;
4✔
181
        delete config.domElement;
4✔
182
        super(id, config.object3d || new THREE.Group(), config);
4✔
183

184
        // this.style = config.style || {};
185
        this.isLabelLayer = true;
4✔
186
        this.domElement = new DomNode();
4✔
187
        this.domElement.show();
4✔
188
        this.domElement.dom.id = `itowns-label-${this.id}`;
4✔
189
        this.buildExtent = true;
4✔
190
        this.crs = config.source.crs;
4✔
191
        this.performance = config.performance || true;
4✔
192
        this.forceClampToTerrain = config.forceClampToTerrain || false;
4✔
193

194
        this.toHide = new THREE.Group();
4✔
195

196
        this.labelDomelement = domElement;
4✔
197

198
        // The margin property defines a space around each label that cannot be occupied by another label.
199
        // For example, if some labelLayer has a margin value of 5, there will be at least 10 pixels
200
        // between each labels of the layer
201
        // TODO : this property should be moved to Style after refactoring style properties structure
202
        this.margin = config.margin;
4✔
203
    }
1✔
204

205
    get visible() {
206
        return super.visible;
5✔
207
    }
208

209
    set visible(value) {
210
        super.visible = value;
4✔
211
        if (value) {
4!
212
            this.domElement?.show();
4!
213
        } else {
214
            this.domElement?.hide();
×
215
        }
216
    }
217

218
    get submittedLabelNodes() {
219
        return this.object3d.children;
1✔
220
    }
221

222
    /**
223
     * Reads each {@link FeatureGeometry} that contains label configuration, and
224
     * creates the corresponding {@link Label}. To create a `Label`, a geometry
225
     * needs to have a `label` object with at least a few properties:
226
     * - `content`, which refers to `Label#content`
227
     * - `position`, which refers to `Label#position`
228
     * - (optional) `config`, containing miscellaneous configuration for the
229
     *   label
230
     *
231
     * The geometry (or its parent Feature) needs to have a Style set.
232
     *
233
     * @param {FeatureCollection} data - The FeatureCollection to read the
234
     * labels from.
235
     * @param {Extent} extent
236
     *
237
     * @return {Label[]} An array containing all the created labels.
238
     */
239
    convert(data, extent) {
2✔
240
        const labels = [];
2✔
241

242
        // Converting the extent now is faster for further operation
243
        extent.as(data.crs, _extent);
2✔
244
        coord.crs = data.crs;
2✔
245

246
        context.setZoom(extent.zoom);
2✔
247

248
        data.features.forEach((f) => {
2✔
249
            // TODO: add support for LINE and POLYGON
250
            if (f.type !== FEATURE_TYPES.POINT) {
4✔
251
                return;
2✔
252
            }
253
            context.setFeature(f);
2✔
254

255
            const featureField = f.style?.text?.field;
2!
256

257
            // determine if altitude style is specified by the user
258
            const altitudeStyle = f.style?.point?.base_altitude;
2!
259
            const isDefaultElevationStyle = altitudeStyle instanceof Function && altitudeStyle.name == 'baseAltitudeDefault';
2!
260

261
            // determine if the altitude needs update with ElevationLayer
262
            labels.needsAltitude = labels.needsAltitude || this.forceClampToTerrain === true || (isDefaultElevationStyle && !f.hasRawElevationData);
2!
263

264
            f.geometries.forEach((g) => {
2✔
265
                // NOTE: this only works because only POINT is supported, it
266
                // needs more work for LINE and POLYGON
267
                coord.setFromArray(f.vertices, g.size * g.indices[0].offset);
2✔
268
                // Transform coordinate to data.crs projection
269
                coord.applyMatrix4(data.matrixWorld);
2✔
270

271
                if (!_extent.isPointInside(coord)) { return; }
2!
272
                const geometryField = g.properties.style && g.properties.style.text && g.properties.style.text.field;
2!
273

274
                context.setGeometry(g);
2✔
275
                let content;
276
                this.style.setContext(context);
2✔
277
                const layerField = this.style.text && this.style.text.field;
2✔
278
                if (this.labelDomelement) {
2!
279
                    content = readExpression(this.labelDomelement, context);
×
280
                } else if (!geometryField && !featureField && !layerField) {
2!
281
                    // Check if there is an icon, with no text
NEW
282
                    if (!(g.properties.style && (g.properties.style.icon.source || g.properties.style.icon.key))
×
283
                        && !(f.style && f.style.icon && (f.style.icon.source || f.style.icon.key))
×
284
                        && !(this.style.icon && (this.style.icon.source || this.style.icon.key))) {
×
UNCOV
285
                        return;
×
286
                    }
287
                }
288

289
                const label = new Label(content, coord.clone(), this.style);
2✔
290

291
                label.layerId = this.id;
2✔
292
                label.padding = this.margin || label.padding;
2✔
293

294
                labels.push(label);
2✔
295
            });
296
        });
297

298
        return labels;
2✔
299
    }
300

301
    // placeholder
302
    preUpdate(context, sources) {
303
        if (sources.has(this.parent)) {
×
304
            this.object3d.clear();
×
305
            this.#filterGrid.width = this.parent.maxScreenSizeNode * 0.5;
×
306
            this.#filterGrid.height = this.parent.maxScreenSizeNode * 0.5;
×
307
            this.#filterGrid.resize();
×
308
        }
309
    }
310

311
    #submitToRendering(labelsNode) {
312
        this.object3d.add(labelsNode);
2✔
313
    }
314

315
    #disallowToRendering(labelsNode) {
316
        this.toHide.add(labelsNode);
1✔
317
    }
318

319
    #findClosestDomElement(node) {
320
        if (node.parent?.isTileMesh) {
1!
321
            return node.parent.link[this.id]?.domElements || this.#findClosestDomElement(node.parent);
×
322
        } else {
323
            return this.domElement;
1✔
324
        }
325
    }
326

327
    #hasLabelChildren(object) {
3✔
328
        return object.children.every(c => c.layerUpdateState && c.layerUpdateState[this.id]?.hasFinished());
5!
329
    }
330

331
    // Remove all labels invisible with pre-culling with screen grid
332
    // We use the screen grid with maximum size of node on screen
20✔
333
    #removeCulledLabels(node) {
1✔
334
        // copy labels array
335
        const labels = node.children.slice();
1✔
336

337
        // reset filter
338
        this.#filterGrid.reset();
1✔
339

340
        // sort labels by order
341
        labels.sort((a, b) => b.order - a.order);
1✔
342

343
        labels.forEach((label) => {
1✔
344
            // get node dimensions
345
            node.nodeParent.extent.planarDimensions(nodeDimensions);
1✔
346
            coord.crs = node.nodeParent.extent.crs;
1✔
347

348
            // get west/north node coordinates
349
            coord.setFromValues(node.nodeParent.extent.west, node.nodeParent.extent.north, 0).toVector3(westNorthNode);
1✔
350

351
            // get label position
352
            coord.copy(label.coordinates).as(node.nodeParent.extent.crs, coord).toVector3(labelPosition);
1✔
353

354
            // transform label position to local node system
355
            labelPosition.sub(westNorthNode);
1✔
356
            labelPosition.y += nodeDimensions.y;
1✔
357
            labelPosition.divide(nodeDimensions).multiplyScalar(this.#filterGrid.width);
1✔
358

359
            // update the projected position to transform to local filter grid sytem
360
            label.updateProjectedPosition(labelPosition.x, labelPosition.y);
1✔
361

362
            // use screen grid to remove all culled labels
363
            if (!this.#filterGrid.insert(label)) {
1!
364
                node.removeLabel(label);
×
365
            }
366
        });
367
    }
368

369
    update(context, layer, node, parent) {
4✔
370
        if (!parent && node.link[layer.id]) {
4!
371
            // if node has been removed dispose three.js resource
372
            ObjectRemovalHelper.removeChildrenAndCleanupRecursively(this, node);
×
373
            return;
×
374
        }
375

376
        const labelsNode = node.link[layer.id] || new LabelsNode(node);
4✔
377
        node.link[layer.id] = labelsNode;
4✔
378

379
        if (this.frozen || !node.visible || !this.visible) {
4!
380
            return;
×
381
        }
382

383
        if (!node.material.visible && this.#hasLabelChildren(node)) {
4!
384
            return this.#disallowToRendering(labelsNode);
×
385
        }
386

387
        const extentsDestination = node.getExtentsByProjection(this.source.crs) || [node.extent];
4!
388
        const zoomDest = extentsDestination[0].zoom;
4✔
389

390
        if (zoomDest < layer.zoom.min || zoomDest > layer.zoom.max) {
4!
391
            return this.#disallowToRendering(labelsNode);
×
392
        }
393

394
        if (node.layerUpdateState[this.id] === undefined) {
4✔
395
            node.layerUpdateState[this.id] = new LayerUpdateState();
2✔
396
        }
397

398
        if (!this.source.extentInsideLimit(node.extent, zoomDest)) {
4✔
399
            node.layerUpdateState[this.id].noMoreUpdatePossible();
1✔
400
            return;
1✔
401
        } else if (this.#hasLabelChildren(node.parent)) {
3✔
402
            if (!node.material.visible) {
2!
403
                labelsNode.needsUpdate = true;
×
404
            }
405
            this.#submitToRendering(labelsNode);
2✔
406
            return;
2✔
407
        } else if (!node.layerUpdateState[this.id].canTryUpdate()) {
1!
408
            return;
×
409
        }
410

411
        node.layerUpdateState[this.id].newTry();
1✔
412

413
        const command = {
1✔
414
            layer: this,
415
            extentsSource: extentsDestination,
416
            view: context.view,
417
            requester: node,
418
        };
419

420
        return context.scheduler.execute(command).then((result) => {
1✔
421
            if (!result) { return; }
1!
422

423
            const renderer = context.view.mainLoop.gfxEngine.label2dRenderer;
1✔
424

425
            labelsNode.initializeDom();
1✔
426

427
            this.#findClosestDomElement(node).add(labelsNode.domElements);
1✔
428

429
            result.forEach((labels) => {
1✔
430
                // Clean if there isnt' parent
431
                if (!node.parent) {
1!
432
                    labels.forEach((l) => {
×
433
                        ObjectRemovalHelper.removeChildrenAndCleanupRecursively(this, l);
×
434
                        renderer.removeLabelDOM(l);
×
435
                    });
436
                    return;
×
437
                }
438

439
                labelsNode.needsAltitude = labelsNode.needsAltitude || labels.needsAltitude;
1✔
440

441
                // Add all labels for this tile at once to batch it
442
                labels.forEach((label) => {
1✔
443
                    if (node.extent.isPointInside(label.coordinates)) {
1!
444
                        labelsNode.addLabel(label);
1✔
445
                    }
446
                });
447
            });
448

449
            if (labelsNode.count()) {
1!
450
                labelsNode.domElements.labels.hide();
1✔
451
                labelsNode.domElements.labels.dom.style.opacity = '1.0';
1✔
452

453
                node.addEventListener('show', () => labelsNode.domElements.labels.show());
1✔
454

455
                node.addEventListener('hidden', () => this.#disallowToRendering(labelsNode));
1✔
456

457
                // Necessary event listener, to remove any Label attached to
458
                node.addEventListener('removed', () => this.removeNodeDomElement(node));
1✔
459

460
                if (labelsNode.needsAltitude && node.material.getElevationLayer()) {
1!
461
                    node.material.getElevationLayer().addEventListener('rasterElevationLevelChanged', () => { labelsNode.needsUpdate = true; });
×
462
                }
463

464
                if (this.performance) {
1!
465
                    this.#removeCulledLabels(labelsNode);
1✔
466
                }
467
            }
468

469
            node.layerUpdateState[this.id].noMoreUpdatePossible();
1✔
470
        });
471
    }
472

473
    removeLabelsFromNodeRecursive(node) {
×
474
        node.children.forEach((c) => {
×
475
            if (c.link[this.id]) {
×
476
                delete c.link[this.id];
×
477
            }
478
            this.removeLabelsFromNodeRecursive(c);
×
479
        });
480

481
        this.removeNodeDomElement(node);
×
482
    }
483

484
    removeNodeDomElement(node) {
485
        if (node.link[this.id]?.domElements) {
×
486
            const child = node.link[this.id].domElements.dom;
×
487
            child.parentElement.removeChild(child);
×
488
            delete node.link[this.id].domElements;
×
489
        }
490
    }
491

492
    /**
493
     * All layer's objects and domElements are removed.
494
     * @param {boolean} [clearCache=false] Whether to clear the layer cache or not
495
     */
496
    delete(clearCache) {
×
497
        if (clearCache) {
×
498
            this.cache.clear();
×
499
        }
500
        this.domElement.dom.parentElement.removeChild(this.domElement.dom);
×
501

502
        this.parent.level0Nodes.forEach(obj => this.removeLabelsFromNodeRecursive(obj));
×
503
    }
1✔
504
}
505

506
export default LabelLayer;
1✔
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

© 2025 Coveralls, Inc