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

iTowns / itowns / 25318013074

04 May 2026 12:06PM UTC coverage: 88.397% (+0.002%) from 88.395%
25318013074

Pull #2756

github

web-flow
Merge e473e5a9c into 74811a183
Pull Request #2756: Feat/tms and pbf

2842 of 3662 branches covered (77.61%)

Branch coverage included in aggregate %.

55 of 75 new or added lines in 9 files covered. (73.33%)

2 existing lines in 1 file now uncovered.

28882 of 32226 relevant lines covered (89.62%)

948.1 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';
1✔
2
import Protobuf from 'pbf';
1✔
3
import { VectorTile } from '@mapbox/vector-tile';
1✔
4
import { FeatureCollection, FEATURE_TYPES } from 'Core/Feature';
1✔
5
import { globalExtentTMS } from 'Core/Tile/TileGrid';
1✔
6
import { deprecatedParsingOptionsToNewOne } from 'Core/Deprecated/Undeprecator';
1✔
7
import { Coordinates } from '@itowns/geographic';
1✔
8

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

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

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

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

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

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

30✔
57
        length--;
30✔
58

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
176

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

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

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

© 2026 Coveralls, Inc