open-vector-tile
Version:
This library reads/writes Open Vector Tiles
635 lines • 23.7 kB
JavaScript
import { decodeOffset } from '../base/index.js';
import { PbfReader, Pbf as Protobuf } from 'pbf-ts';
import { decodeValue, encodeValue } from './shape.js';
import { unweave2D, unweave3D, zagzig } from '../util.js';
/**
* Vector Feature Base
* Common variables and functions shared by all vector features
*/
export class OVectorFeatureBase {
cache;
id;
properties;
mShape;
extent;
geometryIndices;
single;
bboxIndex;
hasOffsets;
hasMValues;
indicesIndex;
tessellationIndex;
type = 0;
/**
* @param cache - the column cache for future retrieval
* @param id - the id of the feature
* @param properties - the properties of the feature
* @param mShape - the shape of the feature's mValues if they exist
* @param extent - the extent of the feature
* @param geometryIndices - the indices of the geometry in the cache
* @param single - if true, you know the initial length is 1
* @param bboxIndex - index to the values column where the BBox is stored
* @param hasOffsets - if true, the geometryIndices has offsets encoded into it
* @param hasMValues - if true, the feature has M values
* @param indicesIndex - if greater than 0, the feature has indices to parse
* @param tessellationIndex - if greater than 0, the feature has tessellation
*/
constructor(cache, id, properties, mShape, extent, geometryIndices, single, bboxIndex, // -1 if there is no bbox
hasOffsets, hasMValues, indicesIndex, // -1 if there are no indices
tessellationIndex) {
this.cache = cache;
this.id = id;
this.properties = properties;
this.mShape = mShape;
this.extent = extent;
this.geometryIndices = geometryIndices;
this.single = single;
this.bboxIndex = bboxIndex;
this.hasOffsets = hasOffsets;
this.hasMValues = hasMValues;
this.indicesIndex = indicesIndex;
this.tessellationIndex = tessellationIndex;
}
/** @returns - the geometry type of the feature */
geoType() {
const { type } = this;
if (type === 1 || type === 4)
return 'MultiPoint';
else if (type === 2 || type === 5)
return 'MultiLineString';
else
return 'MultiPolygon'; // 3, 6
}
/** @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;
}
/** @returns - true if the type of the feature is points 3D */
isPoints3D() {
return this.type === 4;
}
/** @returns - true if the type of the feature is lines 3D */
isLines3D() {
return this.type === 5;
}
/** @returns - true if the type of the feature is polygons 3D */
isPolygons3D() {
return this.type === 6;
}
/**
* adds the tessellation to the geometry
* @param geometry - the input geometry to add to
* @param multiplier - the multiplier to multiply the geometry by
*/
// we need to disable the eslint rule here so that the docs register the parameters correctly
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addTessellation(geometry, multiplier) { }
/** @returns the geometry as an array of lines */
loadLines() {
return undefined;
}
/** @returns the geometry as an array of lines objects that include offsets */
loadPolys() {
return undefined;
}
/** @returns an empty geometry */
loadGeometryFlat() {
return [[], []];
}
/** @returns the indices for the feature */
readIndices() {
return [];
}
}
/**
* Vector Feature Base 2D.
* Extends from @see {@link OVectorFeatureBase}.
*/
export class OVectorFeatureBase2D extends OVectorFeatureBase {
/** @returns the BBox of the feature (in lon-lat space) */
bbox() {
if (this.bboxIndex === -1)
return [0, 0, 0, 0];
return this.cache.getColumn(10 /* OColumnName.bbox */, this.bboxIndex);
}
}
/**
* Vector Feature Base 3D.
* Extends from @see {@link OVectorFeatureBase}.
*/
export class OVectorFeatureBase3D extends OVectorFeatureBase {
/** @returns the BBox3D of the feature (in lon-lat space) */
bbox() {
if (this.bboxIndex === -1)
return [0, 0, 0, 0, 0, 0];
return this.cache.getColumn(10 /* OColumnName.bbox */, this.bboxIndex);
}
}
/**
* Points Vector Feature
* Type 1
* Extends from @see {@link OVectorFeatureBase}.
* store either a single point or a list of points
*/
export class OVectorPointsFeature extends OVectorFeatureBase2D {
type = 1;
geometry;
/** @returns the geometry as an array of points */
loadPoints() {
return this.loadGeometry();
}
/** @returns the geometry as an array of points */
loadGeometry() {
const { cache, hasMValues, single, geometryIndices: indices } = this;
let indexPos = 0;
const geometryIndex = indices[indexPos++];
if (this.geometry === undefined) {
if (single) {
const { a, b } = unweave2D(geometryIndex);
this.geometry = [{ x: zagzig(a), y: zagzig(b) }];
}
else {
this.geometry = cache.getColumn(6 /* OColumnName.points */, geometryIndex);
// load m values if they exist
if (hasMValues) {
const length = this.geometry.length;
for (let j = 0; j < length; j++) {
const valueIndex = indices[indexPos++];
this.geometry[j].m = decodeValue(valueIndex, this.mShape, cache);
}
}
}
}
return this.geometry;
}
}
/**
* Lines Vector Feature
* Type 2
* Extends from @see {@link OVectorFeatureBase2D}.
* Store either a single line or a list of lines
*/
export class OVectorLinesFeature extends OVectorFeatureBase2D {
type = 2;
geometry;
/** @returns the geometry as a flattened array of points */
loadPoints() {
return this.loadGeometry().flat();
}
/** @returns the geometry as an array of lines objects that include offsets */
loadLines() {
if (this.geometry !== undefined)
return this.geometry;
// prepare variables
const { hasOffsets, hasMValues, geometryIndices: indices, cache, single } = this;
const lines = [];
const offsets = [];
let indexPos = 0;
const lineCount = single ? 1 : indices[indexPos++];
for (let i = 0; i < lineCount; i++) {
// get offset if it exists
const offset = hasOffsets ? decodeOffset(indices[indexPos++]) : 0;
// get geometry
const geometry = cache.getColumn(6 /* OColumnName.points */, indices[indexPos++]);
// inject m values if they exist
if (hasMValues) {
const length = geometry.length;
for (let j = 0; j < length; j++) {
const valueIndex = indices[indexPos++];
geometry[j].m = decodeValue(valueIndex, this.mShape, cache);
}
}
lines.push(geometry);
offsets.push(offset);
}
this.geometry = [lines, offsets];
return [lines, offsets];
}
/** @returns the geometry as an array of flattened line geometry */
loadGeometry() {
return this.loadLines()[0];
}
}
/**
* Polys Vector Feature
* Type 3
* Extends from @see {@link OVectorFeatureBase2D}.
* Stores either one or multiple polygons. Polygons are an abstraction to polylines, and
* each polyline can contain an offset.
*/
export class OVectorPolysFeature extends OVectorFeatureBase2D {
type = 3;
geometry;
/**
* Stores the geometry incase it's used again
* @returns the geometry as an array of lines objects that include offsets
*/
#loadLinesWithOffsets() {
if (this.geometry !== undefined)
return this.geometry;
// prepare variables
const { hasOffsets, hasMValues, geometryIndices: indices, cache, single } = this;
const polys = [];
const offsets = [];
let indexPos = 0;
const polyCount = single ? 1 : indices[indexPos++];
for (let i = 0; i < polyCount; i++) {
const lineCount = indices[indexPos++];
const poly = [];
const polyOffsets = [];
for (let j = 0; j < lineCount; j++) {
// get offset if it exists
const offset = hasOffsets ? decodeOffset(indices[indexPos++]) : 0;
// get geometry
const geometry = cache.getColumn(6 /* OColumnName.points */, indices[indexPos++]);
// inject m values if they exist
if (hasMValues) {
const length = geometry.length;
for (let j = 0; j < length; j++) {
const valueIndex = indices[indexPos++];
geometry[j].m = decodeValue(valueIndex, this.mShape, cache);
}
}
poly.push(geometry);
polyOffsets.push(offset);
}
polys.push(poly);
offsets.push(polyOffsets);
}
this.geometry = [polys, offsets];
return [polys, offsets];
}
/** @returns the geometry as a flattened array of points */
loadPoints() {
return this.loadGeometry().flat(2);
}
/** @returns the geometry flattened into an array with offsets */
loadLines() {
const [lines, offsets] = this.#loadLinesWithOffsets();
return [lines.flat(), offsets.flat()];
}
/** @returns the geometry as an array of lines objects that include offsets */
loadPolys() {
return this.#loadLinesWithOffsets();
}
/** @returns the geometry as an array of raw poly geometry */
loadGeometry() {
return this.#loadLinesWithOffsets()[0];
}
/**
* Automatically adds the tessellation to the geometry if the tessellationIndex exists
* @returns the geometry as an array of totally flattend poly geometry with indices
*/
loadGeometryFlat() {
const [geo] = this.#loadLinesWithOffsets();
const multiplier = 1 / this.extent;
const geometry = [];
for (const poly of geo) {
for (const line of poly) {
for (const point of line) {
geometry.push(point.x * multiplier, point.y * multiplier);
}
}
}
this.addTessellation(geometry, multiplier);
return [geometry, this.readIndices()];
}
/** @returns the indices of the geometry */
readIndices() {
if (this.indicesIndex === -1)
return [];
return this.cache.getColumn(8 /* OColumnName.indices */, this.indicesIndex);
}
/**
* adds the tessellation to the geometry
* @param geometry - the geometry of the feature
* @param multiplier - the multiplier to apply the extent shift
*/
addTessellation(geometry, multiplier) {
if (this.tessellationIndex === -1)
return;
const data = this.cache.getColumn(6 /* OColumnName.points */, this.tessellationIndex);
for (const point of data) {
geometry.push(point.x * multiplier, point.y * multiplier);
}
}
}
/**
* 3D Point Vector Feature
* Type 4.
* Extends from @see {@link OVectorFeatureBase3D}.
* Store either a single 3D point or a list of 3D points.
*/
export class OVectorPoints3DFeature extends OVectorFeatureBase3D {
type = 4;
geometry;
/** @returns the geometry as a flattened array of points */
loadPoints() {
return this.loadGeometry();
}
/**
* Read in the 3D Point Geometry. Can be more than one point.
* @returns the 3D Point Geometry
*/
loadGeometry() {
const { cache, hasMValues, single, geometryIndices: indices } = this;
let indexPos = 0;
const geometryIndex = indices[indexPos++];
if (this.geometry === undefined) {
if (single) {
const { a, b, c } = unweave3D(geometryIndex);
this.geometry = [{ x: zagzig(a), y: zagzig(b), z: zagzig(c) }];
}
else {
this.geometry = cache.getColumn(7 /* OColumnName.points3D */, geometryIndex);
// load m values if they exist
if (hasMValues) {
const length = this.geometry.length;
for (let j = 0; j < length; j++) {
const valueIndex = indices[indexPos++];
this.geometry[j].m = decodeValue(valueIndex, this.mShape, cache);
}
}
}
}
return this.geometry;
}
}
/**
* 3D Lines Vector Feature
* Type 5
* Extends from @see {@link OVectorFeatureBase3D}.
* Store either a single 3D line or a list of 3D lines.
*/
export class OVectorLines3DFeature extends OVectorFeatureBase3D {
type = 5;
geometry;
/** @returns the geometry as a flattened array of points */
loadPoints() {
return this.loadGeometry().flat();
}
/** @returns the geometry as an array of lines objects that include offsets */
loadLines() {
if (this.geometry !== undefined)
return this.geometry;
// prepare variables
const { hasOffsets, hasMValues, geometryIndices: indices, cache, single } = this;
const lines = [];
const offsets = [];
let indexPos = 0;
const lineCount = single ? 1 : indices[indexPos++];
for (let i = 0; i < lineCount; i++) {
// get offset if it exists
const offset = hasOffsets ? decodeOffset(indices[indexPos++]) : 0;
// get geometry
const geometry = cache.getColumn(7 /* OColumnName.points3D */, indices[indexPos++]);
// inject m values if they exist
if (hasMValues) {
const length = geometry.length;
for (let j = 0; j < length; j++) {
const valueIndex = indices[indexPos++];
geometry[j].m = decodeValue(valueIndex, this.mShape, cache);
}
}
lines.push(geometry);
offsets.push(offset);
}
this.geometry = [lines, offsets];
return [lines, offsets];
}
/** @returns the geometry as an array of flattened line geometry */
loadGeometry() {
return this.loadLines()[0];
}
}
/**
* 3D Polygons Vector Feature
* Type 6
* Extends from @see {@link OVectorFeatureBase3D}.
* Store either a single 3D polygon or a list of 3D polygons.
*/
export class OVectorPolys3DFeature extends OVectorFeatureBase3D {
type = 6;
geometry;
/**
* Stores the geometry incase it's used again
* @returns the geometry as an array of lines objects that include offsets
*/
#loadLinesWithOffsets() {
if (this.geometry !== undefined)
return this.geometry;
// prepare variables
const { hasOffsets, hasMValues, geometryIndices: indices, cache, single } = this;
const polys = [];
const offsets = [];
let indexPos = 0;
const polyCount = single ? 1 : indices[indexPos++];
for (let i = 0; i < polyCount; i++) {
const lineCount = indices[indexPos++];
const poly = [];
const polyOffsets = [];
for (let j = 0; j < lineCount; j++) {
// get offset if it exists
const offset = hasOffsets ? decodeOffset(indices[indexPos++]) : 0;
// get geometry
const geometry = cache.getColumn(7 /* OColumnName.points3D */, indices[indexPos++]);
// inject m values if they exist
if (hasMValues) {
const length = geometry.length;
for (let j = 0; j < length; j++) {
const valueIndex = indices[indexPos++];
geometry[j].m = decodeValue(valueIndex, this.mShape, cache);
}
}
poly.push(geometry);
polyOffsets.push(offset);
}
polys.push(poly);
offsets.push(polyOffsets);
}
this.geometry = [polys, offsets];
return [polys, offsets];
}
/** @returns the geometry as a flattened array of points */
loadPoints() {
return this.loadGeometry().flat(2);
}
/** @returns the geometry flattened into an array with offsets */
loadLines() {
const [lines, offsets] = this.#loadLinesWithOffsets();
return [lines.flat(), offsets.flat()];
}
/** @returns the geometry as an array of lines objects that include offsets */
loadPolys() {
return this.#loadLinesWithOffsets();
}
/** @returns the geometry as an array of raw poly geometry */
loadGeometry() {
return this.#loadLinesWithOffsets()[0];
}
/**
* Automatically adds the tessellation to the geometry if the tessellationIndex exists
* @returns the geometry as an array of totally flattend poly geometry with indices
*/
loadGeometryFlat() {
const [geo] = this.#loadLinesWithOffsets();
const multiplier = 1 / this.extent;
const geometry = [];
for (const poly of geo) {
for (const line of poly) {
for (const point of line) {
geometry.push(point.x * multiplier, point.y * multiplier, point.z * multiplier);
}
}
}
this.addTessellation(geometry, multiplier);
return [geometry, this.readIndices()];
}
/** @returns the indices of the geometry */
readIndices() {
if (this.indicesIndex === -1)
return [];
return this.cache.getColumn(8 /* OColumnName.indices */, this.indicesIndex);
}
/**
* adds the tessellation to the geometry
* @param geometry - the geometry of the feature
* @param multiplier - the multiplier to apply the extent shift
*/
addTessellation(geometry, multiplier) {
if (this.tessellationIndex === -1)
return;
const data = this.cache.getColumn(7 /* OColumnName.points3D */, this.tessellationIndex);
for (const point of data) {
geometry.push(point.x * multiplier, point.y * multiplier, point.z * multiplier);
}
}
}
/**
* @param bytes - the bytes to read from
* @param extent - the extent of the vector layer to help decode the geometry
* @param cache - the column cache to read from
* @param shape - the shape of the feature's properties data
* @param mShape - the shape of the feature's m-values if they exist
* @returns - the decoded feature
*/
export function readFeature(bytes, extent, cache, shape, mShape = {}) {
const pbf = new PbfReader(bytes);
// pull in the type
const type = pbf.readVarint();
// next the flags
const flags = pbf.readVarint();
// read the id if it exists
const id = (flags & 1) > 0 ? pbf.readVarint() : undefined;
const hashBBOX = (flags & (1 << 1)) > 0;
const hasOffsets = (flags & (1 << 2)) > 0;
const hasIndices = (flags & (1 << 3)) > 0;
const hasTessellation = (flags & (1 << 4)) > 0;
const hasMValues = (flags & (1 << 5)) > 0;
const single = (flags & (1 << 6)) !== 0;
// read the properties
const valueIndex = pbf.readVarint();
const properties = decodeValue(valueIndex, shape, cache);
// if type is 1 or 4, read geometry as a single index, otherwise as an array
let Constructor;
let geometryIndices;
let indices = -1;
let tessellationIndex = -1;
if (type === 1 || type === 4) {
if (single)
geometryIndices = [pbf.readVarint()];
else
geometryIndices = cache.getColumn(8 /* OColumnName.indices */, pbf.readVarint());
if (type === 1)
Constructor = OVectorPointsFeature;
else
Constructor = OVectorPoints3DFeature;
}
else {
geometryIndices = cache.getColumn(8 /* OColumnName.indices */, pbf.readVarint());
if (type === 2)
Constructor = OVectorLinesFeature;
else if (type === 3)
Constructor = OVectorPolysFeature;
else if (type === 5)
Constructor = OVectorLines3DFeature;
else if (type === 6)
Constructor = OVectorPolys3DFeature;
else
throw new Error('Type is not supported.');
}
// read indices and tessellation if they exist
if (type === 3 || type === 6) {
if (hasIndices)
indices = pbf.readVarint();
if (hasTessellation)
tessellationIndex = pbf.readVarint();
}
const bboxIndex = hashBBOX ? pbf.readVarint() : -1;
return new Constructor(cache, id, properties, mShape, extent, geometryIndices, single, bboxIndex, hasOffsets, hasMValues, indices, tessellationIndex);
}
/**
* @param feature - BaseVectorFeature to build a buffer from
* @param shape - The shape of the feature's properties data
* @param mShape - The shape of the feature's m-values if they exist
* @param cache - where to store all feature data to in columns
* @returns - Compressed indexes for the feature
*/
export function writeOVFeature(feature, shape, mShape = {}, cache) {
// write id, type, properties, bbox, geometry, indices, tessellation, mValues
const pbf = new Protobuf();
// type is just stored as a varint
pbf.writeVarint(feature.type);
// store flags if each one exists or not into a single byte
const hasID = feature.id !== undefined;
const hasIndices = 'indices' in feature && feature.indices.length !== 0;
const hasTessellation = 'tessellation' in feature && feature.tessellation.length !== 0;
const hasOffsets = feature.hasOffsets;
const hasBBox = 'bbox' in feature && feature.hasBBox;
const hasMValues = feature.hasMValues;
const single = feature.geometry.length === 1;
let flags = 0;
if (hasID)
flags += 1;
if (hasBBox)
flags += 1 << 1;
if (hasOffsets)
flags += 1 << 2;
if (hasIndices)
flags += 1 << 3;
if (hasTessellation)
flags += 1 << 4;
if (hasMValues)
flags += 1 << 5;
if (single)
flags += 1 << 6;
pbf.writeVarint(flags); // just 1 byte
// id is stored in unsigned column
if (hasID)
pbf.writeVarint(feature.id ?? 0);
// index to values column
const valueIndex = encodeValue(feature.properties, shape, cache);
pbf.writeVarint(valueIndex);
// geometry
const storedGeo = feature.addGeometryToCache(cache, mShape);
pbf.writeVarint(storedGeo);
// indices
if ('indices' in feature && hasIndices)
pbf.writeVarint(cache.addColumnData(8 /* OColumnName.indices */, feature.indices));
// tessellation
if ('tessellation' in feature && hasTessellation)
pbf.writeVarint(cache.addColumnData(6 /* OColumnName.points */, feature.tessellation));
// bbox is stored in double column.
if (hasBBox)
pbf.writeVarint(cache.addColumnData(10 /* OColumnName.bbox */, feature.bbox));
return pbf.commit();
}
//# sourceMappingURL=vectorFeature.js.map