Add existing to tracked
This commit is contained in:
+634
@@ -0,0 +1,634 @@
|
||||
// @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;
|
||||
Reference in New Issue
Block a user