@loaders.gl/mvt
Version:
Loader for Mapbox Vector Tiles
263 lines • 8.52 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright vis.gl contributors
// This code is inspired by https://github.com/mapbox/vector-tile-js under BSD 3-clause license.
import Protobuf from 'pbf';
import * as MVT from "./mvt-constants.js";
import { readBoundingBoxFromPBF, loadFlatGeometryFromPBF } from "./parse-geometry-from-pbf.js";
const DEFAULT_LAYER = {
version: 1,
name: '',
extent: 4096,
length: 0,
schema: { fields: [], metadata: {} },
columns: {},
idColumn: [],
geometryTypeColumn: [],
geometryColumn: [],
boundingBoxColumn: []
};
const DEFAULT_LAYER_DATA = {
currentFeature: 0,
keys: [],
values: [],
types: [],
columnTypes: [],
columnNullable: [],
featurePositions: [],
geometryPositions: []
};
/** Parse an MVT tile from an ArrayBuffer */
export function parseMVT(arrayBuffer) {
const pbf = new Protobuf(arrayBuffer);
return parseMVTTile(pbf);
}
/** Parse an MVT tile from a PBF buffer */
export function parseMVTTile(pbf, end) {
const tile = { layers: {} };
try {
pbf.readFields(readTileFieldFromPBF, tile, end);
}
catch (error) {
// eslint-disable-next-line no-console
console.warn(error);
}
return tile;
}
/**
* Protobuf read callback for a top-level tile object's PBF tags
* @param tag
* @param layers
* @param pbf
*/
function readTileFieldFromPBF(tag, tile, pbf) {
if (!pbf || !tile) {
return;
}
switch (tag) {
case MVT.TileInfo.layers:
// get the byte length and end of the layer
const byteLength = pbf.readVarint();
const end = byteLength + pbf.pos;
const layer = parseLayer(pbf, end);
tile.layers[layer.name] = layer;
break;
default:
// ignore? log?
}
}
/** Parse an MVT layer from BPF at current position */
export function parseLayer(pbf, end) {
const layerData = { layer: { ...DEFAULT_LAYER }, ...DEFAULT_LAYER_DATA };
pbf.readFields(readLayerFieldFromPBF, layerData, end);
// Read features
for (let featureIndex = 0; featureIndex < layerData.featurePositions.length; featureIndex++) {
// Determine start and end of feature in PBF
const featurePosition = layerData.featurePositions[featureIndex];
pbf.pos = featurePosition;
const byteLength = pbf.readVarint();
const end = byteLength + pbf.pos;
layerData.currentFeature = featureIndex;
pbf.readFields(readFeatureFieldFromPBF, layerData, end);
readBoundingBoxesFromPDF(pbf, layerData);
readGeometriesFromPBF(pbf, layerData);
}
// Post processing
const { layer } = layerData;
layer.length = layerData.featurePositions.length;
layer.schema = makeMVTSchema(layerData);
return layer;
}
/**
*
* @param tag
* @param layer
* @param pbf
*/
function readLayerFieldFromPBF(tag, layerData, pbf) {
if (!layerData || !pbf) {
return;
}
switch (tag) {
case MVT.LayerInfo.version:
layerData.layer.version = pbf.readVarint();
break;
case MVT.LayerInfo.name:
layerData.layer.name = pbf.readString();
break;
case MVT.LayerInfo.extent:
layerData.layer.extent = pbf.readVarint();
break;
case MVT.LayerInfo.features:
layerData.featurePositions.push(pbf.pos);
break;
case MVT.LayerInfo.keys:
layerData.keys.push(pbf.readString());
break;
case MVT.LayerInfo.values:
const [value, type] = parseValues(pbf);
layerData.values.push(value);
layerData.types.push(type);
break;
default:
// ignore? Log?
}
}
/**
* @param pbf
* @returns value
*/
function parseValues(pbf) {
const end = pbf.readVarint() + pbf.pos;
let value = null;
// not a valid property type so we use it to detect multiple values
let type = -1;
// TODO - can we have multiple values?
while (pbf.pos < end) {
if (type !== -1) {
throw new Error('MVT: Multiple values not supported');
}
type = pbf.readVarint() >> 3;
value = readValueFromPBF(pbf, type);
}
return [value, type];
}
/** Read a type tagged value from the protobuf at current position */
function readValueFromPBF(pbf, type) {
switch (type) {
case MVT.PropertyType.string_value:
return pbf.readString();
case MVT.PropertyType.float_value:
return pbf.readFloat();
case MVT.PropertyType.double_value:
return pbf.readDouble();
case MVT.PropertyType.int_value:
return pbf.readVarint64();
case MVT.PropertyType.uint_value:
return pbf.readVarint();
case MVT.PropertyType.sint_value:
return pbf.readSVarint();
case MVT.PropertyType.bool_value:
return pbf.readBoolean();
default:
return null;
}
}
/**
*
* @param tag
* @param feature
* @param pbf
*/
function readFeatureFieldFromPBF(tag, layerData, pbf) {
if (!pbf || !layerData) {
return;
}
switch (tag) {
case MVT.FeatureInfo.id:
const id = pbf.readVarint();
layerData.layer.idColumn[layerData.currentFeature] = id;
break;
case MVT.FeatureInfo.tags:
parseColumnValues(pbf, layerData);
break;
case MVT.FeatureInfo.type:
const type = pbf.readVarint();
layerData.layer.geometryTypeColumn[layerData.currentFeature] = type;
break;
case MVT.FeatureInfo.geometry:
layerData.geometryPositions[layerData.currentFeature] = pbf.pos;
break;
default:
// ignore? log?
}
}
/**
*
* @param pbf
* @param feature
*/
function parseColumnValues(pbf, layerData) {
const end = pbf.readVarint() + pbf.pos;
while (pbf.pos < end) {
const keyIndex = pbf.readVarint();
const valueIndex = pbf.readVarint();
const columnName = layerData.keys[keyIndex];
const value = layerData.values[valueIndex];
layerData.columnTypes[columnName] = layerData.types[valueIndex];
layerData.columnNullable[columnName] ||= value === null;
layerData.layer.columns[columnName] ||= [];
layerData.layer.columns[columnName].push(value);
}
}
// Geometry readers
function readBoundingBoxesFromPDF(pbf, layerData) {
for (let row = 0; row < layerData.geometryPositions.length; row++) {
pbf.pos = layerData.geometryPositions[row];
const boundingBox = readBoundingBoxFromPBF(pbf);
layerData.layer.boundingBoxColumn[row] = boundingBox;
}
}
function readGeometriesFromPBF(pbf, layerData) {
for (let row = 0; row < layerData.geometryPositions.length; row++) {
pbf.pos = layerData.geometryPositions[row];
const flatGeometry = loadFlatGeometryFromPBF(pbf);
layerData.layer.geometryColumn[row] = flatGeometry;
}
}
// Schema Builder
function makeMVTSchema(layerData) {
const { keys, columnTypes, columnNullable } = layerData;
const fields = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const nullable = columnNullable[key];
switch (columnTypes[key]) {
case MVT.PropertyType.string_value:
fields.push({ name: keys[i], type: 'utf8', nullable });
break;
case MVT.PropertyType.float_value:
fields.push({ name: keys[i], type: 'float32', nullable });
break;
case MVT.PropertyType.double_value:
fields.push({ name: keys[i], type: 'float64', nullable });
break;
case MVT.PropertyType.int_value:
fields.push({ name: keys[i], type: 'int32', nullable });
break;
case MVT.PropertyType.uint_value:
fields.push({ name: keys[i], type: 'uint32', nullable });
break;
case MVT.PropertyType.sint_value:
fields.push({ name: keys[i], type: 'int32', nullable });
break;
case MVT.PropertyType.bool_value:
fields.push({ name: keys[i], type: 'bool', nullable });
break;
default:
// ignore?
}
}
return { fields, metadata: {} };
}
//# sourceMappingURL=parse-mvt-from-pbf.js.map