| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- // @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;
|