open-vector-tile
Version:
This library reads/writes Open Vector Tiles
473 lines • 16.7 kB
JavaScript
import { encodeValue } from '../open/shape.js';
import { weave2D, weave3D, zigzag } from '../util.js';
/**
* Base Vector Feature
* Common variables and methods shared by all vector features
*/
export class VectorFeatureBase {
geometry;
properties;
id;
bbox;
type = 0;
/**
* @param geometry - the geometry of the feature
* @param properties - the properties of the feature
* @param id - the id of the feature if there is one
* @param bbox - the BBox of the feature
*/
constructor(geometry, properties = {}, id, bbox) {
this.geometry = geometry;
this.properties = properties;
this.id = id;
this.bbox = bbox;
}
/** @returns - true if the feature has BBox */
get hasBBox() {
const bbox = this.bbox;
return bbox !== undefined && bbox.some((v) => v !== 0);
}
}
//! Points & Points3D
/** Base Vector Points Feature */
export class VectorFeaturePointsBase extends VectorFeatureBase {
/**
* Points do not have this feature, so return false
* @returns false always
*/
get hasOffsets() {
return false;
}
/**
* Points do not have this feature, so return false
* @returns false always
*/
get hasMValues() {
const geometry = this.geometry;
return geometry.some(({ m }) => m !== undefined);
}
/** @returns the geometry */
loadGeometry() {
return this.geometry;
}
/** @returns the M-Values */
getMValues() {
if (!this.hasMValues)
return undefined;
const geometry = this.geometry;
return geometry.map(({ m }) => m ?? {});
}
/**
* @param cache - the column cache to store the geometry
* @param mShape - the shape of the M-values to encode the values as
* @returns the index in the points column where the geometry is stored
*/
addGeometryToCache(cache, mShape = {}) {
const { hasMValues } = this;
const geometry = this.geometry;
const is3D = this.type === 4;
const columnName = is3D ? 7 /* OColumnName.points3D */ : 6 /* OColumnName.points */;
if (geometry.length === 1) {
if (is3D) {
const { x, y, z } = geometry[0];
return weave3D(zigzag(x), zigzag(y), zigzag(z));
}
else {
const { x, y } = geometry[0];
return weave2D(zigzag(x), zigzag(y));
}
}
// othwerise store the collection of points
const indices = [];
indices.push(cache.addColumnData(columnName, geometry));
// store the mvalues indexes if they exist
if (hasMValues) {
for (const { m } of geometry) {
indices.push(encodeValue(m ?? {}, mShape, cache));
}
}
return cache.addColumnData(8 /* OColumnName.indices */, indices);
}
}
/**
* Base Vector Points Feature
* Type 1
* Extends from @see {@link VectorFeaturePointsBase}.
* Store either a single point or a list of points
*/
export class BaseVectorPointsFeature extends VectorFeaturePointsBase {
type = 1;
}
/**
* Base Vector Points 3D Feature
* Type 4
* Extends from @see {@link VectorFeaturePointsBase}.
* Store either a single point or a list of points
*/
export class BaseVectorPoints3DFeature extends VectorFeaturePointsBase {
type = 4;
}
//! Lines & Lines3D
/**
* Base Vector Lines Feature
* Common variables and methods shared by all vector lines and/or polygons features
*/
export class BaseVectorLine {
geometry;
offset;
/**
* @param geometry - the geometry of the feature
* @param offset - the offset of the feature
*/
constructor(geometry, offset = 0) {
this.geometry = geometry;
this.offset = offset;
}
}
/** Base Vector Lines Feature */
export class VectorFeatureLinesBase extends VectorFeatureBase {
/** @returns - true if the feature has offsets */
get hasOffsets() {
const geometry = this.geometry;
return geometry.some(({ offset }) => offset > 0);
}
/**
* @returns - true if the feature has M values
*/
get hasMValues() {
return this.geometry.some(({ geometry }) => {
return geometry.some(({ m }) => m !== undefined);
});
}
/** @returns the flattened geometry */
loadGeometry() {
return this.geometry.map(({ geometry }) => geometry);
}
/** @returns the flattened M values */
getMValues() {
if (!this.hasMValues)
return undefined;
return this.geometry.flatMap(({ geometry }) => geometry.map(({ m }) => m ?? {}));
}
/**
* @param cache - the column cache to store the geometry
* @param mShape - the shape of the M-values to encode the values as
* @returns the indexes in the points column where the geometry is stored
*/
addGeometryToCache(cache, mShape = {}) {
const { hasOffsets, hasMValues } = this;
const geometry = this.geometry;
const columnName = this.type === 5 ? 7 /* OColumnName.points3D */ : 6 /* OColumnName.points */;
const indices = [];
// store number of lines
if (geometry.length !== 1)
indices.push(geometry.length);
for (const line of geometry) {
// store offset for current line
if (hasOffsets)
indices.push(encodeOffset(line.offset));
// store geometry data and track its index position
indices.push(cache.addColumnData(columnName, line.geometry));
// store the mvalues indexes if they exist
if (hasMValues) {
for (const { m } of line.geometry) {
indices.push(encodeValue(m ?? {}, mShape, cache));
}
}
}
return cache.addColumnData(8 /* OColumnName.indices */, indices);
}
}
/**
* Base Vector Lines Feature
* Type 2
* Extends from @see {@link VectorFeatureBase}.
* Store either a single line or a list of lines.
*/
export class BaseVectorLinesFeature extends VectorFeatureLinesBase {
type = 2;
}
/**
* Base Vector Lines 3D Feature
* Type 5
* Extends from @see {@link VectorFeatureBase}.
* Store either a single 3D line or a list of 3D lines
*/
export class BaseVectorLines3DFeature extends VectorFeatureLinesBase {
type = 5;
}
//! Polys & Polys3D
/** Base Vector Polys Feature */
export class VectorFeaturePolysBase extends VectorFeatureBase {
geometry;
indices;
bbox;
tessellation;
/**
* @param geometry - the geometry of the feature
* @param indices - the indices of the geometry
* @param tessellation - the tessellation of the geometry
* @param properties - the properties of the feature
* @param id - the id of the feature
* @param bbox - the bbox of the feature
*/
constructor(geometry, indices = [], tessellation = [], properties = {}, id, bbox) {
super(geometry, properties, id, bbox);
this.geometry = geometry;
this.indices = indices;
this.bbox = bbox;
this.tessellation = this.#fixTessellation(tessellation);
}
/**
* @param tessellation - the tessellation of the geometry but flattened
* @returns - the tessellation of the geometry as a list of points
*/
#fixTessellation(tessellation) {
if (tessellation.length % 2 !== 0) {
throw new Error('The input tessellation must have an even number of elements.');
}
return tessellation.reduce((acc, _, index, array) => {
if (index % 2 === 0) {
acc.push({ x: array[index], y: array[index + 1] });
}
return acc;
}, []);
}
/**
* @returns true if the feature has offsets
*/
get hasOffsets() {
return this.geometry.some((poly) => poly.some(({ offset }) => offset > 0));
}
/**
* @returns - true if the feature has M values
*/
get hasMValues() {
return this.geometry.some((poly) => poly.some(({ geometry }) => {
return geometry.some(({ m }) => m !== undefined);
}));
}
/**
* @returns the flattened geometry
*/
loadGeometry() {
return this.geometry.map((poly) => poly.map((line) => line.geometry));
}
/**
* @returns the flattened M-values
*/
getMValues() {
if (!this.hasMValues)
return undefined;
return this.geometry.flatMap((poly) => {
return poly.flatMap(({ geometry }) => {
return geometry.map((point) => point.m ?? {});
});
});
}
/**
* @param cache - the column cache to store the geometry
* @param mShape - the shape of the M-values to encode the values as
* @returns the indexes in the points column where the geometry is stored
*/
addGeometryToCache(cache, mShape = {}) {
const { hasOffsets, hasMValues } = this;
const geometry = this.geometry;
const columnName = this.type === 6 ? 7 /* OColumnName.points3D */ : 6 /* OColumnName.points */;
const indices = [];
// store number of polygons
if (this.geometry.length > 1)
indices.push(geometry.length);
for (const poly of geometry) {
// store number of lines in the polygon
indices.push(poly.length);
// store each line
for (const line of poly) {
// store offset for current line
if (hasOffsets)
indices.push(encodeOffset(line.offset));
// store geometry data and track its index position
indices.push(cache.addColumnData(columnName, line.geometry));
// store the mvalues indexes if they exist
if (hasMValues) {
for (const { m } of line.geometry) {
indices.push(encodeValue(m ?? {}, mShape, cache));
}
}
}
}
return cache.addColumnData(8 /* OColumnName.indices */, indices);
}
}
/**
* Base Vector Polys Feature
* Type 3
* Extends from @see {@link VectorFeatureBase}.
* Store either a single polygon or a list of polygons
*/
export class BaseVectorPolysFeature extends VectorFeaturePolysBase {
type = 3;
}
/**
* Base Vector Polys 3D Feature
* Type 6
* Extends from @see {@link VectorFeatureBase}.
* Store either a single 3D poly or a list of 3D polys
*/
export class BaseVectorPolys3DFeature extends VectorFeaturePolysBase {
type = 6;
}
/**
* @param feature - A mapbox vector feature that's been parsed from protobuf data
* @returns - A base feature to help build a vector tile
*/
export function fromMapboxVectorFeature(feature) {
const { id, properties, extent } = feature;
const geometry = feature.loadGeometry();
const indices = feature.readIndices();
const tessellation = [];
feature.addTessellation(tessellation, 1 / extent);
switch (feature.type) {
case 1:
return new BaseVectorPointsFeature(geometry, properties, id);
case 2: {
const geo = geometry;
const baseLines = [];
for (const line of geo) {
baseLines.push(new BaseVectorLine(line));
}
return new BaseVectorLinesFeature(baseLines, properties, id);
}
case 3:
case 4: {
const geo = geometry;
const baseMultPoly = [];
for (const poly of geo) {
const baseLines = [];
for (const line of poly) {
baseLines.push(new BaseVectorLine(line));
}
baseMultPoly.push(baseLines);
}
return new BaseVectorPolysFeature(baseMultPoly, indices, tessellation, properties, id);
}
default:
throw new Error(`Unknown feature type: ${feature.type}`);
}
}
/**
* Convert an S2JSON feature to a base feature
* @param feature - An S2JSON feature
* @param extent - the extent of the vector layer
* @returns - A base feature to help build a vector tile
*/
export function fromS2JSONFeature(feature, extent) {
const { geometry, properties, id } = feature;
const { type, is3D, coordinates, bbox, offset } = geometry;
if (type === 'Point') {
if (is3D)
return new BaseVectorPoints3DFeature([transformPoint3D(coordinates, extent)], properties, id, bbox);
else
return new BaseVectorPointsFeature([transformPoint(coordinates, extent)], properties, id, bbox);
}
else if (type === 'MultiPoint') {
if (is3D)
return new BaseVectorPoints3DFeature(coordinates.map((p) => transformPoint3D(p, extent)), properties, id, bbox);
else
return new BaseVectorPointsFeature(coordinates.map((p) => transformPoint(p, extent)), properties, id, bbox);
}
else if (type === 'LineString') {
if (is3D)
return new BaseVectorLines3DFeature([
new BaseVectorLine(coordinates.map((p) => transformPoint3D(p, extent)), offset),
], properties, id, bbox);
else
return new BaseVectorLinesFeature([
new BaseVectorLine(coordinates.map((p) => transformPoint(p, extent)), offset),
], properties, id, bbox);
}
else if (type === 'MultiLineString') {
if (is3D)
return new BaseVectorLines3DFeature(coordinates.map((line, i) => {
return new BaseVectorLine(line.map((p) => transformPoint3D(p, extent)), offset?.[i]);
}), properties, id, bbox);
else
return new BaseVectorLinesFeature(coordinates.map((line, i) => {
return new BaseVectorLine(line.map((p) => transformPoint(p, extent)), offset?.[i]);
}), properties, id, bbox);
}
else if (type === 'Polygon') {
const { indices, tessellation } = geometry;
if (is3D)
return new BaseVectorPolys3DFeature([
coordinates.map((line, i) => {
return new BaseVectorLine(line.map((p) => transformPoint3D(p, extent)), offset?.[i]);
}),
], indices, tessellation, properties, id, bbox);
else
return new BaseVectorPolysFeature([
coordinates.map((line, i) => {
return new BaseVectorLine(line.map((p) => transformPoint(p, extent)), offset?.[i]);
}),
], indices, tessellation, properties, id, bbox);
}
else if (type === 'MultiPolygon') {
const { indices, tessellation } = geometry;
if (is3D)
return new BaseVectorPolys3DFeature(coordinates.map((poly, i) => {
return poly.map((line, j) => {
return new BaseVectorLine(line.map((p) => transformPoint3D(p, extent)), offset?.[i]?.[j]);
});
}), indices, tessellation, properties, id, bbox);
else
return new BaseVectorPolysFeature(coordinates.map((poly, i) => {
return poly.map((line, j) => {
return new BaseVectorLine(line.map((p) => transformPoint(p, extent)), offset?.[i]?.[j]);
});
}), indices, tessellation, properties, id, bbox);
}
else {
throw new Error(`Unknown geometry type: ${type}`);
}
}
/**
* Transform a point in place to an extent
* @param p - the point
* @param extent - the extent
* @returns - the transformed point
*/
function transformPoint(p, extent) {
const { round } = Math;
return { x: round(p.x * extent), y: round(p.y * extent) };
}
/**
* Transform a 3D point in place to an extent
* @param p - the 3D point
* @param extent - the extent
* @returns - the transformed 3D point
*/
function transformPoint3D(p, extent) {
const { round } = Math;
return {
x: round(p.x * extent),
y: round(p.y * extent),
z: round(('z' in p ? p.z : 0) * extent),
};
}
/**
* Encode offset values into a signed integer to reduce byte cost without too much loss
* @param offset - float or double value to be compressed
* @returns - a signed integer that saves 3 decimal places
*/
export function encodeOffset(offset) {
return Math.floor(offset * 1_000);
}
/**
* Decode offset from a signed integer into a float or double
* @param offset - the signed integer to be decompressed
* @returns - a float or double that restores 3 decimal places
*/
export function decodeOffset(offset) {
return offset / 1_000;
}
//# sourceMappingURL=vectorFeature.js.map