@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
635 lines (584 loc) • 16.1 kB
JavaScript
// @ts-check
import RuntimeError from "../Core/RuntimeError.js";
// Mapbox Vector Tile specification:
// https://github.com/mapbox/vector-tile-spec/tree/master/2.1
/**
* @typedef {object} MVTPoint
* @property {number} x Tile-local x (0–extent)
* @property {number} y Tile-local y (0–extent)
* @ignore
*/
/**
* @typedef {object} MVTFeature
* @property {"Point"|"LineString"|"Polygon"|"Unknown"} type
* @property {Array<MVTPoint>|Array<Array<MVTPoint>>} geometry
* @property {Record<string, string|number|boolean|bigint>} properties
* @ignore
*/
/**
* @typedef {object} MVTLayer
* @property {string} name
* @property {number} extent
* @property {MVTFeature[]} features
* @ignore
*/
/**
* @typedef {object} DecodedMVT
* @property {MVTLayer[]} layers
* @ignore
*/
/**
* @typedef {(string|number|boolean|bigint)} MVTValue
* @ignore
*/
/**
* @typedef {object} ReadTagResult
* @property {number} fieldNumber
* @property {number} wireType
* @property {number} newPos
* @ignore
*/
/**
* @typedef {object} ReadVarintResult
* @property {number} value
* @property {number} newPos
* @ignore
*/
/**
* @typedef {object} ReadBigVarintResult
* @property {bigint} value
* @property {number} newPos
* @ignore
*/
const textDecoder = new TextDecoder();
// Geometry type enum from the MVT spec
const GeomType = {
UNKNOWN: 0,
POINT: 1,
LINESTRING: 2,
POLYGON: 3,
};
// Tile message field numbers (spec §4.1)
const TileField = {
LAYERS: 3,
};
// Layer message field numbers (spec §4.1)
const LayerField = {
NAME: 1,
FEATURES: 2,
KEYS: 3,
VALUES: 4,
EXTENT: 5,
};
// Feature message field numbers (spec §4.2)
const FeatureField = {
TAGS: 2,
TYPE: 3,
GEOMETRY: 4,
};
// Value message field numbers (spec §4.4)
const ValueField = {
STRING: 1,
FLOAT: 2,
DOUBLE: 3,
INT64: 4,
UINT64: 5,
SINT64: 6,
BOOL: 7,
};
const geomTypeName = ["Unknown", "Point", "LineString", "Polygon"];
/**
* Decode a Mapbox Vector Tile (MVT / .pbf) binary buffer into layers and
* features. Geometry coordinates remain in tile-local integer space
* (0 – layer.extent, typically 4096).
*
* @param {ArrayBuffer} arrayBuffer The raw .pbf tile binary
* @returns {DecodedMVT}
* @ignore
*/
function decodeMVT(arrayBuffer) {
const bytes = new Uint8Array(arrayBuffer);
const layers = [];
let pos = 0;
while (pos < bytes.length) {
const tag = readTag(bytes, pos, bytes.length);
const fieldNumber = tag.fieldNumber;
const wireType = tag.wireType;
pos = tag.newPos;
// Tile.layers = field 3, wire type 2 (length-delimited)
if (fieldNumber === TileField.LAYERS && wireType === 2) {
const layerLength = readVarintLength(bytes, pos, bytes.length);
pos = layerLength.newPos;
const layerEnd = advanceByLength(
pos,
layerLength.value,
bytes.length,
"layer",
);
layers.push(decodeLayer(bytes, pos, layerEnd));
pos = layerEnd;
} else {
pos = skipField(bytes, pos, wireType, bytes.length);
}
}
return { layers };
}
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
* @returns {MVTLayer}
* @ignore
*/
function decodeLayer(bytes, start, end) {
let pos = start;
let name = "";
let extent = 4096;
/** @type {string[]} */
const keys = [];
/** @type {Array.<string|number|boolean|bigint|undefined>} */
const values = [];
const rawFeatures = [];
while (pos < end) {
const tag = readTag(bytes, pos, end);
const fieldNumber = tag.fieldNumber;
const wireType = tag.wireType;
pos = tag.newPos;
if (fieldNumber === LayerField.NAME && wireType === 2) {
// name
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const stringEnd = advanceByLength(pos, length.value, end, "layer name");
name = readString(bytes, pos, length.value);
pos = stringEnd;
} else if (fieldNumber === LayerField.FEATURES && wireType === 2) {
// feature
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const featureEnd = advanceByLength(pos, length.value, end, "feature");
rawFeatures.push({ start: pos, end: featureEnd });
pos = featureEnd;
} else if (fieldNumber === LayerField.KEYS && wireType === 2) {
// key
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const stringEnd = advanceByLength(pos, length.value, end, "key");
keys.push(readString(bytes, pos, length.value));
pos = stringEnd;
} else if (fieldNumber === LayerField.VALUES && wireType === 2) {
// value
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const valueEnd = advanceByLength(pos, length.value, end, "value");
const value = decodeValue(bytes, pos, valueEnd);
values.push(value);
pos = valueEnd;
} else if (fieldNumber === LayerField.EXTENT && wireType === 0) {
// extent
const value = readVarint32(bytes, pos, end);
extent = value.value;
pos = value.newPos;
} else {
pos = skipField(bytes, pos, wireType, end);
}
}
const features = rawFeatures.map(({ start: featureStart, end: featureEnd }) =>
decodeFeature(bytes, featureStart, featureEnd, keys, values),
);
return { name, extent, features };
}
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
* @param {string[]} keys
* @param {Array.<string|number|boolean|bigint|undefined>} values
* @returns {MVTFeature}
* @ignore
*/
function decodeFeature(bytes, start, end, keys, values) {
let pos = start;
let geomType = GeomType.UNKNOWN;
const tags = [];
const geometryCommands = [];
while (pos < end) {
const tag = readTag(bytes, pos, end);
const fieldNumber = tag.fieldNumber;
const wireType = tag.wireType;
pos = tag.newPos;
if (fieldNumber === FeatureField.TYPE && wireType === 0) {
// geometry type
const value = readVarint32(bytes, pos, end);
geomType = value.value;
pos = value.newPos;
} else if (fieldNumber === FeatureField.TAGS && wireType === 2) {
// tags (packed uint32)
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const tagEnd = advanceByLength(pos, length.value, end, "feature tags");
while (pos < tagEnd) {
const value = readVarint32(bytes, pos, tagEnd);
tags.push(value.value);
pos = value.newPos;
}
} else if (fieldNumber === FeatureField.GEOMETRY && wireType === 2) {
// geometry (packed uint32 commands)
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const geometryEnd = advanceByLength(
pos,
length.value,
end,
"feature geometry",
);
while (pos < geometryEnd) {
const value = readVarint32(bytes, pos, geometryEnd);
geometryCommands.push(value.value);
pos = value.newPos;
}
} else {
pos = skipField(bytes, pos, wireType, end);
}
}
// Build properties from tags
/** @type {Record<string, string|number|boolean|bigint>} */
const properties = {};
for (let i = 0; i < tags.length - 1; i += 2) {
const key = keys[tags[i]];
const value = values[tags[i + 1]];
if (typeof key !== "string" || value === undefined) {
continue;
}
properties[key] = value;
}
const geometry = decodeGeometry(geomType, geometryCommands);
return {
type: /** @type {"Point"|"LineString"|"Polygon"|"Unknown"} */ (
geomTypeName[geomType] ?? "Unknown"
),
geometry,
properties,
};
}
/**
* Decode MVT geometry commands into coordinate arrays.
* @param {number} geomType
* @param {number[]} cmds
* @returns {*}
* @ignore
*/
function decodeGeometry(geomType, cmds) {
let i = 0;
let x = 0;
let y = 0;
if (geomType === GeomType.POINT) {
const points = [];
while (i < cmds.length) {
const cmd = cmds[i++];
const cmdId = cmd & 0x7;
const count = cmd >> 3;
if (cmdId === 1) {
// MoveTo
for (let c = 0; c < count; c++) {
x += zigzag(cmds[i++]);
y += zigzag(cmds[i++]);
points.push({ x, y });
}
}
}
return points;
}
if (geomType === GeomType.LINESTRING) {
const lines = [];
let current = null;
while (i < cmds.length) {
const cmd = cmds[i++];
const cmdId = cmd & 0x7;
const count = cmd >> 3;
if (cmdId === 1) {
// MoveTo - start new line
if (current !== null) {
lines.push(current);
}
current = [];
x += zigzag(cmds[i++]);
y += zigzag(cmds[i++]);
current.push({ x, y });
} else if (cmdId === 2) {
// LineTo
for (let c = 0; c < count; c++) {
x += zigzag(cmds[i++]);
y += zigzag(cmds[i++]);
current.push({ x, y });
}
}
}
if (current !== null) {
lines.push(current);
}
return lines;
}
if (geomType === GeomType.POLYGON) {
const rings = [];
let current = null;
while (i < cmds.length) {
const cmd = cmds[i++];
const cmdId = cmd & 0x7;
const count = cmd >> 3;
if (cmdId === 1) {
// MoveTo - start ring
if (current !== null) {
rings.push(current);
}
current = [];
x += zigzag(cmds[i++]);
y += zigzag(cmds[i++]);
current.push({ x, y });
} else if (cmdId === 2) {
// LineTo
for (let c = 0; c < count; c++) {
x += zigzag(cmds[i++]);
y += zigzag(cmds[i++]);
current.push({ x, y });
}
} else if (cmdId === 7) {
// ClosePath
if (current !== null && current.length > 0) {
current.push({ x: current[0].x, y: current[0].y });
rings.push(current);
current = null;
}
}
}
if (current !== null) {
rings.push(current);
}
return rings;
}
return [];
}
/**
* @param {Uint8Array} bytes
* @param {number} start
* @param {number} end
* @returns {string|number|boolean|bigint|undefined}
* @ignore
*/
function decodeValue(bytes, start, end) {
let pos = start;
while (pos < end) {
const tag = readTag(bytes, pos, end);
const fieldNumber = tag.fieldNumber;
const wireType = tag.wireType;
pos = tag.newPos;
if (fieldNumber === ValueField.STRING && wireType === 2) {
const length = readVarintLength(bytes, pos, end);
pos = length.newPos;
const stringEnd = advanceByLength(pos, length.value, end, "string value");
return readString(bytes, pos, stringEnd - pos);
} else if (fieldNumber === ValueField.FLOAT && wireType === 5) {
// float
advanceByLength(pos, 4, end, "float value");
const v = new DataView(
bytes.buffer,
bytes.byteOffset + pos,
4,
).getFloat32(0, true);
return v;
} else if (fieldNumber === ValueField.DOUBLE && wireType === 1) {
// double
advanceByLength(pos, 8, end, "double value");
const v = new DataView(
bytes.buffer,
bytes.byteOffset + pos,
8,
).getFloat64(0, true);
return v;
} else if (fieldNumber === ValueField.INT64 && wireType === 0) {
const value = readBigVarint(bytes, pos, end);
return toSafeNumber(value.value);
} else if (fieldNumber === ValueField.UINT64 && wireType === 0) {
const value = readBigVarint(bytes, pos, end);
return toSafeNumber(value.value);
} else if (fieldNumber === ValueField.SINT64 && wireType === 0) {
const value = readBigVarint(bytes, pos, end);
return toSafeNumber(zigzagBigInt(value.value));
} else if (fieldNumber === ValueField.BOOL && wireType === 0) {
const value = readVarint32(bytes, pos, end);
return value.value !== 0;
}
pos = skipField(bytes, pos, wireType, end);
}
return undefined;
}
/**
* @param {Uint8Array} bytes
* @param {number} pos
* @param {number} limit
* @returns {ReadTagResult}
* @ignore
*/
function readTag(bytes, pos, limit) {
const value = readVarint32(bytes, pos, limit);
return {
fieldNumber: value.value >>> 3,
wireType: value.value & 0x7,
newPos: value.newPos,
};
}
/**
* @param {Uint8Array} bytes
* @param {number} pos
* @param {number} limit
* @returns {ReadVarintResult}
* @ignore
*/
function readVarint32(bytes, pos, limit) {
const value = readBigVarint(bytes, pos, limit, 5);
if (value.value > 0xffffffffn) {
throw new RuntimeError("Invalid MVT uint32 varint.");
}
return {
value: Number(value.value),
newPos: value.newPos,
};
}
/**
* @param {Uint8Array} bytes
* @param {number} pos
* @param {number} limit
* @returns {ReadVarintResult}
* @ignore
*/
function readVarintLength(bytes, pos, limit) {
const value = readBigVarint(bytes, pos, limit);
if (value.value > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new RuntimeError("Invalid MVT length varint.");
}
return {
value: Number(value.value),
newPos: value.newPos,
};
}
/**
* @param {Uint8Array} bytes
* @param {number} pos
* @param {number} limit
* @param {number} [maxBytes=10]
* @returns {ReadBigVarintResult}
* @ignore
*/
function readBigVarint(bytes, pos, limit, maxBytes) {
let result = 0n;
let shift = 0n;
let byteCount = 0;
const byteLimit = maxBytes ?? 10;
while (true) {
if (pos >= limit || pos >= bytes.length) {
throw new RuntimeError("Invalid MVT: truncated varint.");
}
const byte = bytes[pos++];
result |= BigInt(byte & 0x7f) << shift;
byteCount++;
if ((byte & 0x80) === 0) {
break;
}
if (byteCount >= byteLimit) {
throw new RuntimeError("Invalid MVT: varint is too long.");
}
shift += 7n;
}
return {
value: result,
newPos: pos,
};
}
/**
* @param {Uint8Array} bytes
* @param {number} pos
* @param {number} len
* @returns {string}
* @ignore
*/
function readString(bytes, pos, len) {
const end = advanceByLength(pos, len, bytes.length, "string");
return textDecoder.decode(bytes.subarray(pos, end));
}
/**
* @param {number} pos
* @param {number} length
* @param {number} limit
* @param {string} fieldName
* @returns {number}
* @ignore
*/
function advanceByLength(pos, length, limit, fieldName) {
if (!Number.isFinite(length) || length < 0) {
throw new RuntimeError(`Invalid MVT ${fieldName}: invalid length.`);
}
const end = pos + length;
if (!Number.isFinite(end) || end < pos || end > limit) {
throw new RuntimeError(
`Invalid MVT ${fieldName}: length exceeds available bytes.`,
);
}
return end;
}
/**
* @param {Uint8Array} bytes
* @param {number} pos
* @param {number} wireType
* @param {number} limit
* @returns {number} newPos
* @ignore
*/
function skipField(bytes, pos, wireType, limit) {
if (wireType === 0) {
return readBigVarint(bytes, pos, limit).newPos;
} else if (wireType === 1) {
return advanceByLength(pos, 8, limit, "fixed64 field");
} else if (wireType === 2) {
const length = readVarintLength(bytes, pos, limit);
return advanceByLength(
length.newPos,
length.value,
limit,
"length-delimited field",
);
} else if (wireType === 5) {
return advanceByLength(pos, 4, limit, "fixed32 field");
}
throw new RuntimeError(`Unsupported protobuf wire type: ${wireType}`);
}
/**
* Decode a zigzag-encoded signed integer.
* @param {number} n
* @returns {number}
* @ignore
*/
function zigzag(n) {
return (n >>> 1) ^ -(n & 1);
}
/**
* @param {bigint} n
* @returns {bigint}
* @ignore
*/
function zigzagBigInt(n) {
return (n >> 1n) ^ -(n & 1n);
}
/**
* @param {bigint} value
* @returns {number|bigint}
* @ignore
*/
function toSafeNumber(value) {
if (
value <= BigInt(Number.MAX_SAFE_INTEGER) &&
value >= BigInt(Number.MIN_SAFE_INTEGER)
) {
return Number(value);
}
return value;
}
export default decodeMVT;