import Point from '@mapbox/point-geometry'; /** @import Pbf from 'pbf' */ /** @import {Feature} from 'geojson' */ export class VectorTileFeature { /** * @param {Pbf} pbf * @param {number} end * @param {number} extent * @param {string[]} keys * @param {(number | string | boolean)[]} values */ constructor(pbf, end, extent, keys, values) { // Public /** @type {Record} */ this.properties = Object.create(null); this.extent = extent; /** @type {0 | 1 | 2 | 3} */ this.type = 0; /** @type {number | undefined} */ this.id = undefined; /** @private */ this._pbf = pbf; /** @private */ this._geometry = -1; /** @private */ this._keys = keys; /** @private */ this._values = values; while (pbf.pos < end) { const tag = pbf.readVarint(); if (tag === 8) this.id = pbf.readVarint(); else if (tag === 18) { const tagsEnd = pbf.readVarint() + pbf.pos; while (pbf.pos < tagsEnd) { const key = keys[pbf.readVarint()]; const value = values[pbf.readVarint()]; this.properties[key] = value; } } else if (tag === 24) this.type = /** @type {0 | 1 | 2 | 3} */ (pbf.readVarint()); else if (tag === 34) { this._geometry = pbf.pos; pbf.skip(tag); } else pbf.skip(tag); } } loadGeometry() { if (this._geometry < 0) throw new Error('feature has no geometry'); const pbf = this._pbf; pbf.pos = this._geometry; const end = pbf.readVarint() + pbf.pos; /** @type Point[][] */ const lines = []; /** @type Point[] | undefined */ let line; let cmd = 1; let length = 0; let x = 0; let y = 0; while (pbf.pos < end) { if (length <= 0) { const cmdLen = pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; if (length === 0) continue; } length--; if (cmd === 1) { // moveTo x += pbf.readSVarint(); y += pbf.readSVarint(); if (line) lines.push(line); line = [new Point(x, y)]; } else if (cmd === 2) { // lineTo x += pbf.readSVarint(); y += pbf.readSVarint(); if (line) line.push(new Point(x, y)); } else if (cmd === 7) { // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90 if (line) { line.push(line[0].clone()); // closePolygon } } else { throw new Error(`unknown command ${cmd}`); } } if (line) lines.push(line); return lines; } bbox() { if (this._geometry < 0) throw new Error('feature has no geometry'); const pbf = this._pbf; pbf.pos = this._geometry; const end = pbf.readVarint() + pbf.pos; let cmd = 1, length = 0, x = 0, y = 0, x1 = Infinity, x2 = -Infinity, y1 = Infinity, y2 = -Infinity; while (pbf.pos < end) { if (length <= 0) { const cmdLen = pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; if (length === 0) continue; } 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]; } /** * @param {number} x * @param {number} y * @param {number} z * @return {Feature} */ toGeoJSON(x, y, z) { const size = this.extent * Math.pow(2, z), x0 = this.extent * x, y0 = this.extent * y, vtCoords = this.loadGeometry(); /** @param {Point} p */ function projectPoint(p) { return [ (p.x + x0) * 360 / size - 180, 360 / Math.PI * Math.atan(Math.exp((1 - (p.y + y0) * 2 / size) * Math.PI)) - 90 ]; } /** @param {Point[]} line */ function projectLine(line) { return line.map(projectPoint); } /** @type {Feature["geometry"]} */ let geometry; if (this.type === 1) { const points = []; for (const line of vtCoords) { points.push(line[0]); } const coordinates = projectLine(points); geometry = points.length === 1 ? {type: 'Point', coordinates: coordinates[0]} : {type: 'MultiPoint', coordinates}; } else if (this.type === 2) { const coordinates = vtCoords.map(projectLine); geometry = coordinates.length === 1 ? {type: 'LineString', coordinates: coordinates[0]} : {type: 'MultiLineString', coordinates}; } else if (this.type === 3) { const polygons = classifyRings(vtCoords); const coordinates = []; for (const polygon of polygons) { coordinates.push(polygon.map(projectLine)); } geometry = coordinates.length === 1 ? {type: 'Polygon', coordinates: coordinates[0]} : {type: 'MultiPolygon', coordinates}; } else { throw new Error('unknown feature type'); } /** @type {Feature} */ const result = { type: 'Feature', geometry, properties: this.properties }; if (this.id != null) { result.id = this.id; } return result; } } /** @type {['Unknown', 'Point', 'LineString', 'Polygon']} */ VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon']; /** classifies an array of rings into polygons with outer rings and holes * @param {Point[][]} rings */ export function classifyRings(rings) { const len = rings.length; if (len <= 1) return [rings]; const polygons = []; let polygon, ccw; for (let i = 0; i < len; i++) { const area = signedArea(rings[i]); if (area === 0) continue; if (ccw === undefined) ccw = area < 0; if (ccw === area < 0) { if (polygon) polygons.push(polygon); polygon = [rings[i]]; } else if (polygon) { polygon.push(rings[i]); } } if (polygon) polygons.push(polygon); return polygons; } /** @param {Point[]} ring */ function signedArea(ring) { let sum = 0; for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { p1 = ring[i]; p2 = ring[j]; sum += (p2.x - p1.x) * (p1.y + p2.y); } return sum; } export class VectorTileLayer { /** * @param {Pbf} pbf * @param {number} [end] */ constructor(pbf, end) { // Public this.version = 1; this.name = ''; this.extent = 4096; this.length = 0; /** @private */ this._pbf = pbf; /** @private * @type {string[]} */ this._keys = []; /** @private * @type {(number | string | boolean)[]} */ this._values = []; /** @private * @type {number[]} */ this._features = []; if (end === undefined) end = pbf.length; while (pbf.pos < end) { const tag = pbf.readVarint(); if (tag === 10) this.name = pbf.readString(); else if (tag === 18) { this._features.push(pbf.pos); pbf.skip(tag); } else if (tag === 26) this._keys.push(pbf.readString()); else if (tag === 34) this._values.push(readValueMessage(pbf)); else if (tag === 40) this.extent = pbf.readVarint(); else if (tag === 120) this.version = pbf.readVarint(); else pbf.skip(tag); } this.length = this._features.length; } /** return feature `i` from this layer as a `VectorTileFeature` * @param {number} i */ feature(i) { if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds'); this._pbf.pos = this._features[i]; const end = this._pbf.readVarint() + this._pbf.pos; return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values); } } /** * @param {Pbf} pbf */ function readValueMessage(pbf) { let value = null; const end = pbf.readVarint() + pbf.pos; while (pbf.pos < end) { const tag = pbf.readVarint(); value = tag === 10 ? pbf.readString() : tag === 21 ? pbf.readFloat() : tag === 25 ? pbf.readDouble() : tag === 32 ? pbf.readVarint64() : tag === 40 ? pbf.readVarint() : tag === 48 ? pbf.readSVarint() : tag === 56 ? pbf.readBoolean() : (pbf.skip(tag), null); } if (value == null) { throw new Error('unknown feature value'); } return value; } export class VectorTile { /** * @param {Pbf} pbf * @param {number} [end] */ constructor(pbf, end = pbf.length) { /** @type {Record} */ const layers = Object.create(null); while (pbf.pos < end) { const tag = pbf.readVarint(); if (tag === 26) { const layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos); if (layer.length) layers[layer.name] = layer; } else pbf.skip(tag); } this.layers = layers; } }