@kibeo/loaders.gl-mvt
Version:
Loader for Mapbox Vector Tiles
354 lines (316 loc) • 12.3 kB
JavaScript
// @ts-nocheck
import {earcut} from '@math.gl/polygon';
/**
* Convert binary features to flat binary arrays. Similar to
* `geojsonToBinary` helper function, except that it expects
* a binary representation of the feature data, which enables
* 2X-3X speed increase in parse speed, compared to using
* geoJSON. See `binary-vector-tile/VectorTileFeature` for
* data format detais
*/
export function featuresToBinary(features, firstPassData, options = {}) {
return fillArrays(features, firstPassData, {
numericPropKeys: options.numericPropKeys || extractNumericPropKeys(features),
PositionDataType: options.PositionDataType || Float32Array
});
}
export const TEST_EXPORTS = {
extractNumericPropKeys,
fillArrays
};
// Extracts properties that are always numeric
// eslint-disable-next-line complexity, max-statements
function extractNumericPropKeys(features) {
const numericPropKeys = {};
for (const feature of features) {
if (feature.properties) {
for (const key in feature.properties) {
// If property has not been seen before, or if property has been numeric
// in all previous features, check if numeric in this feature
// If not numeric, false is stored to prevent rechecking in the future
const numericSoFar = numericPropKeys[key];
// eslint-disable-next-line max-depth
if (numericSoFar || numericSoFar === undefined) {
const val = feature.properties[key];
numericPropKeys[key] = isNumeric(val);
}
}
}
}
return Object.keys(numericPropKeys).filter((k) => numericPropKeys[k]);
}
// Fills coordinates into pre-allocated typed arrays
// eslint-disable-next-line complexity
function fillArrays(features, firstPassData = {}, options = {}) {
const {
pointPositionsCount,
pointFeaturesCount,
linePositionsCount,
linePathsCount,
lineFeaturesCount,
polygonPositionsCount,
polygonObjectsCount,
polygonRingsCount,
polygonFeaturesCount
} = firstPassData;
const {numericPropKeys, PositionDataType = Float32Array} = options;
const coordLength = 2;
const GlobalFeatureIdsDataType = features.length > 65535 ? Uint32Array : Uint16Array;
const points = {
positions: new PositionDataType(pointPositionsCount * coordLength),
globalFeatureIds: new GlobalFeatureIdsDataType(pointPositionsCount),
featureIds:
pointFeaturesCount > 65535
? new Uint32Array(pointPositionsCount)
: new Uint16Array(pointPositionsCount),
numericProps: {},
properties: []
};
const lines = {
pathIndices:
linePositionsCount > 65535
? new Uint32Array(linePathsCount + 1)
: new Uint16Array(linePathsCount + 1),
positions: new PositionDataType(linePositionsCount * coordLength),
globalFeatureIds: new GlobalFeatureIdsDataType(linePositionsCount),
featureIds:
lineFeaturesCount > 65535
? new Uint32Array(linePositionsCount)
: new Uint16Array(linePositionsCount),
numericProps: {},
properties: []
};
const polygons = {
polygonIndices:
polygonPositionsCount > 65535
? new Uint32Array(polygonObjectsCount + 1)
: new Uint16Array(polygonObjectsCount + 1),
primitivePolygonIndices:
polygonPositionsCount > 65535
? new Uint32Array(polygonRingsCount + 1)
: new Uint16Array(polygonRingsCount + 1),
positions: new PositionDataType(polygonPositionsCount * coordLength),
triangles: [],
globalFeatureIds: new GlobalFeatureIdsDataType(polygonPositionsCount),
featureIds:
polygonFeaturesCount > 65535
? new Uint32Array(polygonPositionsCount)
: new Uint16Array(polygonPositionsCount),
numericProps: {},
properties: []
};
// Instantiate numeric properties arrays; one value per vertex
for (const object of [points, lines, polygons]) {
for (const propName of numericPropKeys) {
// If property has been numeric in all previous features in which the property existed, check
// if numeric in this feature
object.numericProps[propName] = new Float32Array(object.positions.length / coordLength);
}
}
// Set last element of path/polygon indices as positions length
lines.pathIndices[linePathsCount] = linePositionsCount;
polygons.polygonIndices[polygonObjectsCount] = polygonPositionsCount;
polygons.primitivePolygonIndices[polygonRingsCount] = polygonPositionsCount;
const indexMap = {
pointPosition: 0,
pointFeature: 0,
linePosition: 0,
linePath: 0,
lineFeature: 0,
polygonPosition: 0,
polygonObject: 0,
polygonRing: 0,
polygonFeature: 0,
feature: 0
};
for (const feature of features) {
const geometry = feature.geometry;
const properties = feature.properties || {};
switch (geometry.type) {
case 'Point':
case 'MultiPoint':
handlePoint(geometry, points, indexMap, coordLength, properties);
points.properties.push(keepStringProperties(properties, numericPropKeys));
indexMap.pointFeature++;
break;
case 'LineString':
case 'MultiLineString':
handleLineString(geometry, lines, indexMap, coordLength, properties);
lines.properties.push(keepStringProperties(properties, numericPropKeys));
indexMap.lineFeature++;
break;
case 'Polygon':
case 'MultiPolygon':
handlePolygon(geometry, polygons, indexMap, coordLength, properties);
polygons.properties.push(keepStringProperties(properties, numericPropKeys));
indexMap.polygonFeature++;
break;
default:
throw new Error('Invalid geometry type');
}
indexMap.feature++;
}
// Wrap each array in an accessor object with value and size keys
return makeAccessorObjects(points, lines, polygons, coordLength);
}
// Fills (Multi)Point coordinates into points object of arrays
function handlePoint(geometry, points, indexMap, coordLength, properties) {
points.positions.set(geometry.data, indexMap.pointPosition * coordLength);
const nPositions = geometry.data.length / coordLength;
fillNumericProperties(points, properties, indexMap.pointPosition, nPositions);
points.globalFeatureIds.fill(
indexMap.feature,
indexMap.pointPosition,
indexMap.pointPosition + nPositions
);
points.featureIds.fill(
indexMap.pointFeature,
indexMap.pointPosition,
indexMap.pointPosition + nPositions
);
indexMap.pointPosition += nPositions;
}
// Fills (Multi)LineString coordinates into lines object of arrays
function handleLineString(geometry, lines, indexMap, coordLength, properties) {
lines.positions.set(geometry.data, indexMap.linePosition * coordLength);
const nPositions = geometry.data.length / coordLength;
fillNumericProperties(lines, properties, indexMap.linePosition, nPositions);
lines.globalFeatureIds.fill(
indexMap.feature,
indexMap.linePosition,
indexMap.linePosition + nPositions
);
lines.featureIds.fill(
indexMap.lineFeature,
indexMap.linePosition,
indexMap.linePosition + nPositions
);
for (let i = 0, il = geometry.lines.length; i < il; ++i) {
// Extract range of data we are working with, defined by start
// and end indices (these index into the geometry.data array)
const start = geometry.lines[i];
const end =
i === il - 1
? geometry.data.length // last line, so read to end of data
: geometry.lines[i + 1]; // start index for next line
lines.pathIndices[indexMap.linePath++] = indexMap.linePosition;
indexMap.linePosition += (end - start) / coordLength;
}
}
// Fills (Multi)Polygon coordinates into polygons object of arrays
function handlePolygon(geometry, polygons, indexMap, coordLength, properties) {
polygons.positions.set(geometry.data, indexMap.polygonPosition * coordLength);
const nPositions = geometry.data.length / coordLength;
fillNumericProperties(polygons, properties, indexMap.polygonPosition, nPositions);
polygons.globalFeatureIds.fill(
indexMap.feature,
indexMap.polygonPosition,
indexMap.polygonPosition + nPositions
);
polygons.featureIds.fill(
indexMap.polygonFeature,
indexMap.polygonPosition,
indexMap.polygonPosition + nPositions
);
// Unlike Point & LineString geometry.lines is a 2D array
for (let l = 0, ll = geometry.lines.length; l < ll; ++l) {
const startPosition = indexMap.polygonPosition;
polygons.polygonIndices[indexMap.polygonObject++] = startPosition;
const areas = geometry.areas[l];
const lines = geometry.lines[l];
const nextLines = geometry.lines[l + 1];
for (let i = 0, il = lines.length; i < il; ++i) {
const start = lines[i];
const end =
i === il - 1
? // last line, so either read to:
nextLines === undefined
? geometry.data.length // end of data (no next lines)
: nextLines[0] // start of first line in nextLines
: lines[i + 1]; // start index for next line
polygons.primitivePolygonIndices[indexMap.polygonRing++] = indexMap.polygonPosition;
indexMap.polygonPosition += (end - start) / coordLength;
}
const endPosition = indexMap.polygonPosition;
triangulatePolygon(polygons, areas, lines, {startPosition, endPosition, coordLength});
}
}
/**
* Triangulate polygon using earcut
*/
function triangulatePolygon(polygons, areas, lines, {startPosition, endPosition, coordLength}) {
const start = startPosition * coordLength;
const end = endPosition * coordLength;
// Extract positions and holes for just this polygon
const polygonPositions = polygons.positions.subarray(start, end);
// Holes are referenced relative to outer polygon
const offset = lines[0];
const holes = lines.slice(1).map((n) => (n - offset) / coordLength);
// Compute triangulation
const indices = earcut(polygonPositions, holes, coordLength, areas);
// Indices returned by triangulation are relative to start
// of polygon, so we need to offset
for (let t = 0, tl = indices.length; t < tl; ++t) {
polygons.triangles.push(startPosition + indices[t]);
}
}
// Wrap each array in an accessor object with value and size keys
function makeAccessorObjects(points, lines, polygons, coordLength) {
const returnObj = {
points: {
positions: {value: points.positions, size: coordLength},
globalFeatureIds: {value: points.globalFeatureIds, size: 1},
featureIds: {value: points.featureIds, size: 1},
numericProps: points.numericProps,
properties: points.properties
},
lines: {
pathIndices: {value: lines.pathIndices, size: 1},
positions: {value: lines.positions, size: coordLength},
globalFeatureIds: {value: lines.globalFeatureIds, size: 1},
featureIds: {value: lines.featureIds, size: 1},
numericProps: lines.numericProps,
properties: lines.properties
},
polygons: {
polygonIndices: {value: polygons.polygonIndices, size: 1},
primitivePolygonIndices: {value: polygons.primitivePolygonIndices, size: 1},
positions: {value: polygons.positions, size: coordLength},
triangles: {value: new Uint32Array(polygons.triangles), size: 1},
globalFeatureIds: {value: polygons.globalFeatureIds, size: 1},
featureIds: {value: polygons.featureIds, size: 1},
numericProps: polygons.numericProps,
properties: polygons.properties
}
};
for (const geomType in returnObj) {
for (const numericProp in returnObj[geomType].numericProps) {
returnObj[geomType].numericProps[numericProp] = {
value: returnObj[geomType].numericProps[numericProp],
size: 1
};
}
}
return returnObj;
}
// Add numeric properties to object
function fillNumericProperties(object, properties, index, length) {
for (const numericPropName in object.numericProps) {
if (numericPropName in properties) {
object.numericProps[numericPropName].fill(properties[numericPropName], index, index + length);
}
}
}
// Keep string properties in object
function keepStringProperties(properties, numericKeys) {
const props = {};
for (const key in properties) {
if (!numericKeys.includes(key)) {
props[key] = properties[key];
}
}
return props;
}
function isNumeric(x) {
return Number.isFinite(x);
}