UNPKG

open-vector-tile

Version:

This library reads/writes Open Vector Tiles

363 lines 11.8 kB
/** * Mapbox Vector Feature types are all bundled in one class * to make it easier to read. Primarily contains an id, properties, and geometry. * The now deprecated S2 model extends this class to include indices and tessellation data. */ export default class MapboxVectorFeature { id; version = 5; properties = {}; extent; type = 1; isS2; #pbf; #indices = -1; #geometry = -1; #tessellation = -1; #keys; #values; /** * @param pbf - the pbf protocol we are reading from * @param end - the position to stop at * @param isS2 - whether the layer is a deprecated S2 layer or Mapbox layer. * @param extent - the extent of the vector tile * @param version - the version of the vector tile. S2 is 5, Mapbox is 1 * @param keys - the keys in the vector layer to pull from * @param values - the values in the vector layer to pull from */ constructor(pbf, end, isS2, extent, version, keys, values) { this.isS2 = isS2; this.extent = extent; this.version = version; this.#pbf = pbf; this.#keys = keys; this.#values = values; pbf.readFields(this.#readFeature, this, end); } /** @returns - the geometry type of the feature */ geoType() { const { type } = this; if (type === 1) return 'MultiPoint'; else if (type === 2) return 'MultiLineString'; else return 'MultiPolygon'; // 3, 4 } /** @returns - true if the type of the feature is points */ isPoints() { return this.type === 1; } /** @returns - true if the type of the feature is lines */ isLines() { return this.type === 2; } /** @returns - true if the type of the feature is polygons */ isPolygons() { return this.type === 3 || this.type === 4; } /** @returns - true if the type of the feature is points 3D */ isPoints3D() { return false; } /** @returns - true if the type of the feature is lines 3D */ isLines3D() { return false; } /** @returns - true if the type of the feature is polygons 3D */ isPolygons3D() { return false; } /** * @param tag - the tag to know what kind of data to read * @param feature - the feature to mutate with the new data * @param pbf - the Protobuf object to read from */ #readFeature(tag, feature, pbf) { // old spec if (feature.isS2) { if (tag === 15) feature.id = pbf.readVarint(); else if (tag === 1) feature.#readTag(pbf, feature); else if (tag === 2) feature.type = pbf.readVarint(); else if (tag === 3) feature.#geometry = pbf.pos; else if (tag === 4) feature.#indices = pbf.pos; else if (tag === 5) feature.#tessellation = pbf.pos; } else { if (tag === 1) feature.id = pbf.readVarint(); else if (tag === 2) feature.#readTag(pbf, feature); else if (tag === 3) feature.type = pbf.readVarint(); else if (tag === 4) feature.#geometry = pbf.pos; else if (tag === 5) feature.#indices = pbf.pos; else if (tag === 6) feature.#tessellation = pbf.pos; } } /** * @param pbf - the Protobuf object * @param feature - the feature to mutate relative to the tag. */ #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; } } /** @returns - MapboxVectorTile's do not support m-values so we return false */ get hasMValues() { return false; } /** * @returns - a default bbox. Since no bbox is present, the default is [0, 0, 0, 0] * also MapboxVectorTile's do not support 3D, so we only return a 2D bbox */ bbox() { return [0, 0, 0, 0]; } /** @returns - regardless of the type, we return a flattend point array */ loadPoints() { let res = []; const geometry = this.loadGeometry(); if (this.type === 1) res = geometry; else if (this.type === 2) res = geometry.flat(); else if (this.type === 3 || this.type === 4) res = geometry.flat(2); return res; } /** @returns - an array of lines. The offsets will be set to 0 */ loadLines() { if (this.type === 1) return; const geometry = this.loadGeometry(); const outLines = []; const offsets = []; const lines = this.type === 2 ? geometry : geometry.flat(); for (const line of lines) { outLines.push(line); offsets.push(0); } return [outLines, offsets]; } /** @returns - an array of polys. The offsets will be set to 0 */ loadPolys() { if (this.type === 1 || this.type === 2) return; const geometry = this.loadGeometry(); const polys = []; const offsets = []; if (this.type === 3 || this.type === 4) { for (const poly of geometry) { const polyOffset = []; for (const _ of poly) polyOffset.push(0); polys.push(poly); offsets.push(polyOffset); } } return [polys, offsets]; } /** @returns - [flattened geometry & tesslation if applicable, indices] */ loadGeometryFlat() { this.#pbf.pos = this.#geometry; const multiplier = 1 / this.extent; const geometry = []; const end = this.#pbf.readVarint() + this.#pbf.pos; let cmd = 1; let length = 0; let x = 0; let y = 0; let startX = null; let startY = null; while (this.#pbf.pos < end) { if (length <= 0) { const cmdLen = this.#pbf.readVarint(); cmd = cmdLen & 0x7; length = cmdLen >> 3; } length--; if (cmd === 1 || cmd === 2) { x += this.#pbf.readSVarint(); y += this.#pbf.readSVarint(); if (startX === null) startX = x * multiplier; if (startY === null) startY = y * multiplier; geometry.push(x * multiplier, y * multiplier); } else if (cmd === 7) { // ClosePath geometry.push(startX ?? 0, startY ?? 0); startX = null; startY = null; } } // if a poly, check if we should load indices const indices = this.readIndices(); // if a poly, check if we should load tessellation if (this.#tessellation > 0) this.addTessellation(geometry, multiplier); return [geometry, indices]; } /** @returns - vector geometry relative to feature type. */ loadGeometry() { this.#pbf.pos = this.#geometry; const points = []; let lines = []; let polys = []; const end = this.#pbf.readVarint() + this.#pbf.pos; let cmd = 1; let length = 0; let x = 0; let y = 0; let input = []; while (this.#pbf.pos < end) { if (length <= 0) { const cmdLen = this.#pbf.readVarint(); cmd = cmdLen & 7; length = cmdLen >> 3; } length--; if (cmd === 1 || cmd === 2) { x += this.#pbf.readSVarint(); y += this.#pbf.readSVarint(); if (cmd === 1) { // moveTo if (input.length > 0) { if (this.type === 1) points.push(...input); else lines.push(input); } input = []; } input.push({ x, y }); } else if (cmd === 7) { // ClosePath if (input.length > 0) { input.push({ x: input[0].x, y: input[0].y }); lines.push(input); input = []; } } else if (cmd === 4) { // ClosePolygon if (input.length > 0) lines.push(input); polys.push(lines); lines = []; input = []; } else { throw new Error('unknown command ' + String(cmd)); } } if (input.length > 0) { if (this.type === 1) points.push(...input); else lines.push(input); } // if type is polygon but we are using old mapbox spec, we might have a multipolygon if (this.type === 3 && !this.isS2) polys = classifyRings(lines); if (this.type === 1) return points; else if (polys.length > 0) return polys; return lines; } /** @returns - an array of indices for the geometry */ readIndices() { if (this.#indices <= 0) return []; this.#pbf.pos = this.#indices; let curr = 0; const end = this.#pbf.readVarint() + this.#pbf.pos; // build indices const indices = []; while (this.#pbf.pos < end) { curr += this.#pbf.readSVarint(); indices.push(curr); } return indices; } /** * Add tessellation data to the geometry * @param geometry - the geometry to add the tessellation data to * @param multiplier - the multiplier to apply the extent shift */ addTessellation(geometry, multiplier) { if (this.#tessellation <= 0) return; this.#pbf.pos = this.#tessellation; const end = this.#pbf.readVarint() + this.#pbf.pos; let x = 0; let y = 0; while (this.#pbf.pos < end) { x += this.#pbf.readSVarint(); y += this.#pbf.readSVarint(); geometry.push(x * multiplier, y * multiplier); } } } /** * @param rings - input flattened rings that need to be classified * @returns - parsed polygons */ function classifyRings(rings) { if (rings.length <= 1) return [rings]; const polygons = []; let polygon; let ccw; for (let i = 0, rl = rings.length; i < rl; i++) { const area = signedArea(rings[i]); if (area === 0) continue; if (ccw === undefined) ccw = area < 0; if (ccw === area < 0) { if (polygon !== undefined) polygons.push(polygon); polygon = [rings[i]]; } else { if (polygon === undefined) polygon = []; polygon.push(rings[i]); } } if (polygon !== undefined) polygons.push(polygon); return polygons; } /** * @param ring - linestring of points to check if it is ccw * @returns - true if the linestring is ccw */ function signedArea(ring) { let sum = 0; for (let i = 0, rl = ring.length, j = rl - 1, p1, p2; i < rl; j = i++) { p1 = ring[i]; p2 = ring[j]; sum += (p2.x - p1.x) * (p1.y + p2.y); } return sum; } //# sourceMappingURL=vectorFeature.js.map