UNPKG

@loaders.gl/mvt

Version:

Loader for Mapbox Vector Tiles

341 lines 11.2 kB
// loaders.gl // SPDX-License-Identifier: MIT // Copyright vis.gl contributors import { classifyRings, classifyRingsFlat, projectToLngLat, projectToLngLatFlat, convertToLocalCoordinates, convertToLocalCoordinatesFlat } from "../utils/geometry-utils.js"; export class VectorTileFeature { properties; extent; type; id; _pbf; _geometry; _keys; _values; _geometryInfo; static types = ['Unknown', 'Point', 'LineString', 'Polygon']; // eslint-disable-next-line max-params constructor(pbf, end, extent, keys, values, geometryInfo) { // Public this.properties = {}; this.extent = extent; this.type = 0; this.id = null; // Private this._pbf = pbf; this._geometry = -1; this._keys = keys; this._values = values; // Only used by binary tiles this._geometryInfo = geometryInfo; pbf.readFields(readFeature, this, end); } toGeoJSONFeature(coordinates, tileIndex) { const coords = this.loadGeometry(); switch (coordinates) { case 'wgs84': return _toGeoJSONFeature(this, coords, (line) => projectToLngLat(line, tileIndex, this.extent)); default: return _toGeoJSONFeature(this, coords, convertToLocalCoordinates); } } /** * * @param options * @returns */ toBinaryFeature(coordinates, tileIndex) { const geom = this.loadFlatGeometry(); switch (coordinates) { case 'wgs84': return this._toBinaryCoordinates(geom, (coords) => projectToLngLatFlat(coords, tileIndex, this.extent)); default: return this._toBinaryCoordinates(geom, convertToLocalCoordinatesFlat); } } /** Read a bounding box from the feature */ // eslint-disable-next-line max-statements bbox() { const pbf = this._pbf; pbf.pos = this._geometry; const end = pbf.readVarint() + pbf.pos; let cmd = 1; let length = 0; let x = 0; let y = 0; let x1 = Infinity; let x2 = -Infinity; let y1 = Infinity; let y2 = -Infinity; while (pbf.pos < end) { if (length <= 0) { const cmdLen = pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; } length--; if (cmd === 1 || cmd === 2) { x += pbf.readSVarint(); y += pbf.readSVarint(); if (x < x1) x1 = x; if (x > x2) x2 = x; if (y < y1) y1 = y; if (y > y2) y2 = y; } else if (cmd !== 7) { throw new Error(`unknown command ${cmd}`); } } return [x1, y1, x2, y2]; } // BINARY HELPERS /** * * @param transform * @returns result */ _toBinaryCoordinates(geom, transform) { let geometry; // Apply the supplied transformation to data transform(geom.data, this.extent); const coordLength = 2; // eslint-disable-next-line default-case switch (this.type) { case 1: // Point this._geometryInfo.pointFeaturesCount++; this._geometryInfo.pointPositionsCount += geom.indices.length; geometry = { type: 'Point', ...geom }; break; case 2: // LineString this._geometryInfo.lineFeaturesCount++; this._geometryInfo.linePathsCount += geom.indices.length; this._geometryInfo.linePositionsCount += geom.data.length / coordLength; geometry = { type: 'LineString', ...geom }; break; case 3: // Polygon geometry = classifyRingsFlat(geom); // Unlike Point & LineString geom.indices is a 2D array, thanks // to the classifyRings method this._geometryInfo.polygonFeaturesCount++; this._geometryInfo.polygonObjectsCount += geometry.indices.length; for (const indices of geometry.indices) { this._geometryInfo.polygonRingsCount += indices.length; } this._geometryInfo.polygonPositionsCount += geometry.data.length / coordLength; break; default: throw new Error(`Invalid geometry type: ${this.type}`); } const result = { type: 'Feature', geometry, properties: this.properties }; if (this.id !== null) { result.id = this.id; } return result; } // GEOJSON HELPER // eslint-disable-next-line complexity, max-statements loadGeometry() { const pbf = this._pbf; pbf.pos = this._geometry; const end = pbf.readVarint() + pbf.pos; let cmd = 1; let length = 0; let x = 0; let y = 0; const lines = []; let line; while (pbf.pos < end) { if (length <= 0) { const cmdLen = pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; } length--; switch (cmd) { case 1: case 2: x += pbf.readSVarint(); y += pbf.readSVarint(); if (cmd === 1) { // moveTo if (line) lines.push(line); line = []; } if (line) line.push([x, y]); break; case 7: // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90 if (line) { line.push(line[0].slice()); // closePolygon } break; default: throw new Error(`unknown command ${cmd}`); } } if (line) lines.push(line); return lines; } /** * Expands the protobuf data to an intermediate Flat GeoJSON * data format, which maps closely to the binary data buffers. * It is similar to GeoJSON, but rather than storing the coordinates * in multidimensional arrays, we have a 1D `data` with all the * coordinates, and then index into this using the `indices` * parameter, e.g. * * geometry: { * type: 'Point', data: [1,2], indices: [0] * } * geometry: { * type: 'LineString', data: [1,2,3,4,...], indices: [0] * } * geometry: { * type: 'Polygon', data: [1,2,3,4,...], indices: [[0, 2]] * } * Thus the indices member lets us look up the relevant range * from the data array. * The Multi* versions of the above types share the same data * structure, just with multiple elements in the indices array */ // eslint-disable-next-line complexity, max-statements loadFlatGeometry() { const pbf = this._pbf; pbf.pos = this._geometry; const endPos = pbf.readVarint() + pbf.pos; let cmd = 1; let cmdLen; let length = 0; let x = 0; let y = 0; let i = 0; // Note: I attempted to replace the `data` array with a // Float32Array, but performance was worse, both using // `set()` and direct index access. Also, we cannot // know how large the buffer should be, so it would // increase memory usage const indices = []; // Indices where geometries start const data = []; // Flat array of coordinate data while (pbf.pos < endPos) { if (length <= 0) { cmdLen = pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; } length--; if (cmd === 1 || cmd === 2) { x += pbf.readSVarint(); y += pbf.readSVarint(); if (cmd === 1) { // New line indices.push(i); } data.push(x, y); i += 2; } else if (cmd === 7) { // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90 if (i > 0) { const start = indices[indices.length - 1]; // start index of polygon data.push(data[start], data[start + 1]); // closePolygon i += 2; } } else { throw new Error(`unknown command ${cmd}`); } } return { data, indices }; } } function _toGeoJSONFeature(vtFeature, coords, transform) { let type = VectorTileFeature.types[vtFeature.type]; let i; let j; let coordinates; switch (vtFeature.type) { case 1: const points = []; for (i = 0; i < coords.length; i++) { points[i] = coords[i][0]; } coordinates = points; transform(coordinates, vtFeature.extent); break; case 2: coordinates = coords; for (i = 0; i < coordinates.length; i++) { transform(coordinates[i], vtFeature.extent); } break; case 3: coordinates = classifyRings(coords); for (i = 0; i < coordinates.length; i++) { for (j = 0; j < coordinates[i].length; j++) { transform(coordinates[i][j], vtFeature.extent); } } break; default: throw new Error('illegal vector tile type'); } if (coordinates.length === 1) { // @ts-expect-error coordinates = coordinates[0]; } else { type = `Multi${type}`; } const result = { type: 'Feature', geometry: { type: type, coordinates: coordinates }, properties: vtFeature.properties }; if (vtFeature.id !== null) { result.properties ||= {}; result.properties.id = vtFeature.id; } return result; } // PBF READER UTILS /** * * @param tag * @param feature * @param pbf */ function readFeature(tag, feature, pbf) { if (feature && pbf) { if (tag === 1) feature.id = pbf.readVarint(); else if (tag === 2) readTag(pbf, feature); else if (tag === 3) feature.type = pbf.readVarint(); else if (tag === 4) feature._geometry = pbf.pos; } } /** * * @param pbf * @param feature */ function readTag(pbf, feature) { const end = pbf.readVarint() + pbf.pos; while (pbf.pos < end) { const key = feature._keys[pbf.readVarint()]; const value = feature._values[pbf.readVarint()]; feature.properties[key] = value; } } //# sourceMappingURL=vector-tile-feature.js.map