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

iTowns / itowns / 27004518448

05 Jun 2026 08:33AM UTC coverage: 88.483% (+0.002%) from 88.481%
27004518448

push

github

ftoromanoff
refactor(Errors): clean up catch path of errors

2893 of 3706 branches covered (78.06%)

Branch coverage included in aggregate %.

35 of 46 new or added lines in 6 files covered. (76.09%)

11 existing lines in 2 files now uncovered.

29367 of 32753 relevant lines covered (89.66%)

1281.74 hits per line

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

89.38
/packages/Main/src/Parser/VectorTileParser.js
1
import { Vector2, Vector3 } from 'three';
2✔
2
import Protobuf from 'pbf';
2✔
3
import { VectorTile } from '@mapbox/vector-tile';
2✔
4
import { FeatureCollection, FEATURE_TYPES } from 'Core/Feature';
2✔
5
import { globalExtentTMS } from 'Core/Tile/TileGrid';
2✔
6
import { deprecatedParsingOptionsToNewOne } from 'Core/Deprecated/Undeprecator';
2✔
7
import { Coordinates } from '@itowns/geographic';
2✔
8

2✔
9
const worldDimension3857 = globalExtentTMS.get('EPSG:3857').planarDimensions();
2✔
10
const globalExtent = new Vector3(worldDimension3857.x, worldDimension3857.y, 1);
2✔
11
const lastPoint = new Vector2();
2✔
12
const firstPoint = new Vector2();
2✔
13

2✔
14
// Calculate the projected coordinates in EPSG:4326 of a given point in the VT local system
2✔
15
// adapted from @mapbox/vector-tile
2✔
16
function project(x, y, tileNumbers, tileExtent) {
48✔
17
    const size = tileExtent * 2 ** tileNumbers.z;
48✔
18
    const x0 = tileExtent * tileNumbers.x;
48✔
19
    const y0 = tileExtent * tileNumbers.y;
48✔
20
    const y2 = 180 - (y + y0) * 360 / size;
48✔
21
    return new Coordinates(
48✔
22
        'EPSG:4326',
48✔
23
        (x + x0) * 360 / size - 180,
48✔
24
        360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90,
48✔
25
    );
48✔
26
}
48✔
27

2✔
28
// Classify option, it allows to classify a full polygon and its holes.
2✔
29
// Each polygon with its holes are in one FeatureGeometry.
2✔
30
// A polygon is determined by its clockwise direction and the holes are in the opposite direction.
2✔
31
// Clockwise direction is determined by Shoelace formula https://en.wikipedia.org/wiki/Shoelace_formula
2✔
32
// Draw polygon with canvas doesn't need to classify however it is necessary for meshs.
2✔
33
function vtFeatureToFeatureGeometry(vtFeature, feature, classify = false) {
6✔
34
    let geometry = feature.bindNewGeometry();
6✔
35
    const isPolygon = feature.type === FEATURE_TYPES.POLYGON;
6✔
36
    classify = classify && isPolygon;
6!
37

6✔
38
    geometry.properties = vtFeature.properties;
6✔
39
    const pbf = vtFeature._pbf;
6✔
40
    pbf.pos = vtFeature._geometry;
6✔
41

6✔
42
    const end = pbf.readVarint() + pbf.pos;
6✔
43
    let cmd = 1;
6✔
44
    let length = 0;
6✔
45
    let x = 0;
6✔
46
    let y = 0;
6✔
47
    let count = 0;
6✔
48
    let sum = 0;
6✔
49

6✔
50
    while (pbf.pos < end) {
6✔
51
        if (length <= 0) {
60✔
52
            const cmdLen = pbf.readVarint();
36✔
53
            cmd = cmdLen & 0x7;
36✔
54
            length = cmdLen >> 3;
36✔
55
        }
36✔
56

60✔
57
        length--;
60✔
58

60✔
59
        if (cmd === 1 || cmd === 2) {
60✔
60
            x += pbf.readSVarint();
48✔
61
            y += pbf.readSVarint();
48✔
62

48✔
63
            if (cmd === 1) {
48✔
64
                if (count) {
12✔
65
                    if (classify && sum > 0 && geometry.indices.length > 0) {
6!
66
                        feature.updateExtent(geometry);
×
67
                        geometry = feature.bindNewGeometry();
×
68
                        geometry.properties = vtFeature.properties;
×
69
                    }
×
70
                    geometry.closeSubGeometry(count, feature);
6✔
71
                    geometry.getLastSubGeometry().ccw = sum < 0;
6✔
72
                }
6✔
73
                count = 0;
12✔
74
                sum = 0;
12✔
75
            }
12✔
76
            count++;
48✔
77
            const coordProj = project(
48✔
78
                x,
48✔
79
                y,
48✔
80
                vtFeature.tileNumbers,
48✔
81
                vtFeature.extent);
48✔
82
            geometry.pushCoordinatesValues(feature, { x, y }, coordProj);
48✔
83
            if (count == 1) {
48✔
84
                firstPoint.set(x, y);
12✔
85
                firstPoint.coordProj = coordProj;
12✔
86
                lastPoint.set(x, y);
12✔
87
            } else if (isPolygon && count > 1) {
48✔
88
                sum += (lastPoint.x - x) * (lastPoint.y + y);
36✔
89
                lastPoint.set(x, y);
36✔
90
            }
36✔
91
        } else if (cmd === 7) {
60✔
92
            if (count) {
12✔
93
                count++;
12✔
94
                geometry.pushCoordinatesValues(feature, { x: firstPoint.x, y: firstPoint.y }, firstPoint.coordProj);
12✔
95
                if (isPolygon) {
12✔
96
                    sum += (lastPoint.x - firstPoint.x) * (lastPoint.y + firstPoint.y);
12✔
97
                }
12✔
98
            }
12✔
99
        } else {
12!
100
            throw new Error(`unknown command ${cmd}`);
×
101
        }
×
102
    }
60✔
103

6✔
104
    if (count) {
6✔
105
        if (classify && sum > 0 && geometry.indices.length > 0) {
6!
106
            feature.updateExtent(geometry);
×
107
            geometry = feature.bindNewGeometry();
×
108
            geometry.properties = vtFeature.properties;
×
109
        }
×
110
        geometry.closeSubGeometry(count, feature);
6✔
111
        geometry.getLastSubGeometry().ccw = sum < 0;
6✔
112
    }
6✔
113
    feature.updateExtent(geometry);
6✔
114
}
6✔
115

2✔
116
function readPBF(file, options) {
10✔
117
    options.out = options.out || {};
10!
118
    const vectorTile = new VectorTile(new Protobuf(file));
10✔
119
    const vtLayerNames = Object.keys(vectorTile.layers);
10✔
120

10✔
121
    const collection = new FeatureCollection(options.out);
10✔
122
    if (vtLayerNames.length < 1) {
10✔
123
        return Promise.resolve(collection);
2✔
124
    }
2✔
125

8✔
126
    // x,y,z tile coordinates
8✔
127
    const x = options.extent.col;
8✔
128
    const z = options.extent.zoom;
8✔
129
    // We need to move from TMS to Google/Bing/OSM coordinates
8✔
130
    // https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates/
8✔
131
    // Only if the layer.origin is top
8✔
132
    const y = options.in.isInverted ? options.extent.row : (1 << z) - options.extent.row - 1;
10✔
133

10✔
134
    const vFeature0 = vectorTile.layers[vtLayerNames[0]];
10✔
135
    // TODO: verify if size is correct because is computed with only one feature (vFeature0).
10✔
136
    const size = vFeature0.extent * 2 ** z;
10✔
137
    const center = -0.5 * size;
10✔
138

10✔
139
    collection.scale.set(globalExtent.x / size, -globalExtent.y / size, 1);
10✔
140
    collection.position.set(vFeature0.extent * x + center, vFeature0.extent * y + center, 0).multiply(collection.scale);
10✔
141
    collection.updateMatrixWorld();
10✔
142

10✔
143
    let styleLayers = options.in.layers;
10✔
144
    if (!styleLayers) {
10!
UNCOV
145
        styleLayers = {};
×
UNCOV
146
        vtLayerNames.forEach((vtLayerName, i) => {
×
UNCOV
147
            styleLayers[vtLayerName] = [{
×
UNCOV
148
                id: vtLayerName,
×
UNCOV
149
                order: i,
×
UNCOV
150
                filterExpression: { filter: () => true },
×
UNCOV
151
            }];
×
UNCOV
152
        });
×
UNCOV
153
    }
✔
154

8✔
155
    vtLayerNames.forEach((vtLayerName) => {
8✔
156
        if (!styleLayers[vtLayerName]) { return; }
8✔
157

6✔
158
        const vectorTileLayer = vectorTile.layers[vtLayerName];
6✔
159

6✔
160
        for (let i = vectorTileLayer.length - 1; i >= 0; i--) {
6✔
161
            const vtFeature = vectorTileLayer.feature(i);
6✔
162
            vtFeature.tileNumbers = { x, y: options.extent.row, z };
6✔
163

6✔
164
            // Find layers where this vtFeature is used
6✔
165
            const layers = styleLayers[vtLayerName]
6✔
166
                .filter(l => l.filterExpression.filter({ zoom: z }, vtFeature));
6✔
167

6✔
168
            for (const layer of layers) {
6✔
169
                const feature = collection.requestFeatureById(layer.id, vtFeature.type - 1);
6✔
170
                feature.id = layer.id;
6✔
171
                feature.order = layer.order;
6✔
172
                feature.style = options.in.styles?.[feature.id];
6✔
173
                vtFeatureToFeatureGeometry(vtFeature, feature);
6✔
174
            }
6✔
175

6✔
176

6✔
177
            /*
6✔
178
            // This optimization is not fully working and need to be reassessed
6✔
179
            // (see https://github.com/iTowns/itowns/pull/2469/files#r1861802136)
6✔
180
            let feature;
6✔
181
            for (const layer of layers) {
6✔
182
                if (!feature) {
6✔
183
                    feature = collection.requestFeatureById(layer.id, vtFeature.type - 1);
6✔
184
                    feature.id = layer.id;
6✔
185
                    feature.order = layer.order;
6✔
186
                    feature.style = options.in.styles[feature.id];
6✔
187
                    vtFeatureToFeatureGeometry(vtFeature, feature);
6✔
188
                } else if (!collection.features.find(f => f.id === layer.id)) {
6✔
189
                    feature = collection.newFeatureByReference(feature);
6✔
190
                    feature.id = layer.id;
6✔
191
                    feature.order = layer.order;
6✔
192
                    feature.style = options.in.styles[feature.id];
6✔
193
                }
6✔
194
            }
6✔
195
            */
6✔
196
        }
6✔
197
    });
8✔
198

8✔
199
    collection.removeEmptyFeature();
8✔
200
    // TODO Some vector tiles are already sorted
8✔
201
    collection.features.sort((a, b) => a.order - b.order);
8✔
202
    // TODO verify if is needed to updateExtent for previous features.
8✔
203
    collection.updateExtent();
8✔
204
    collection.extent = options.extent;
8✔
205
    collection.isInverted = options.in.isInverted;
8✔
206
    return Promise.resolve(collection);
8✔
207
}
8✔
208

2✔
209
/**
2✔
210
 * @module VectorTileParser
2✔
211
 */
2✔
212
export default {
2✔
213
    /**
2✔
214
     * Parse a vector tile file and return a [Feature]{@link module:GeoJsonParser.Feature}
2✔
215
     * or an array of Features. While multiple formats of vector tile are
2✔
216
     * available, the only one supported for the moment is the
2✔
217
     * [Mapbox Vector Tile](https://www.mapbox.com/vector-tiles/specification/).
2✔
218
     *
2✔
219
     * @param {ArrayBuffer} file - The vector tile file to parse.
2✔
220
     *
2✔
221
     * @param {object} options - Options controlling the parsing {@link ParsingOptions}.
2✔
222
     *
2✔
223
     * @param {object} options.in - Object containing all styles,
2✔
224
     * layers and informations data, see {@link InformationsData}.
2✔
225
     *
2✔
226
     * @param {object} options.in.styles - Object containing subobject with
2✔
227
     * informations on a specific style layer. Styles available is by `layer.id` and by zoom.
2✔
228
     *
2✔
229
     * @param {object} options.in.layers - Object containing subobject with
2✔
230
     *
2✔
231
     * @param {FeatureBuildingOptions} options.out - options indicates how the features should be built,
2✔
232
     * see {@link FeatureBuildingOptions}.
2✔
233
     *
2✔
234
     * @returns {Promise} A Promise resolving with a Feature or an array a
2✔
235
     * Features.
2✔
236
     */
2✔
237
    parse(file, options) {
2✔
238
        options = deprecatedParsingOptionsToNewOne(options);
10✔
239
        return Promise.resolve(readPBF(file, options));
10✔
240
    },
10✔
241
};
2✔
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