635 lines
16 KiB
JavaScript
635 lines
16 KiB
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;
|