Files
random_tools/node_modules/@cesium/engine/Source/Scene/decodeMVT.js
T
2026-08-11 09:53:42 -04:00

635 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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 (0extent)
* @property {number} y Tile-local y (0extent)
* @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;