@loaders.gl/gis
Version:
Helpers for GIS category data
382 lines (381 loc) • 15.3 kB
JavaScript
/* eslint-disable indent */
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
*
* @param features
* @param geometryInfo
* @param options
* @returns filled arrays
*/
export function flatGeojsonToBinary(features, geometryInfo, options) {
const propArrayTypes = extractNumericPropTypes(features);
const numericPropKeys = Object.keys(propArrayTypes).filter((k) => propArrayTypes[k] !== Array);
return fillArrays(features, {
propArrayTypes,
...geometryInfo
}, {
numericPropKeys: (options && options.numericPropKeys) || numericPropKeys,
PositionDataType: options ? options.PositionDataType : Float32Array,
triangulate: options ? options.triangulate : true
});
}
export const TEST_EXPORTS = {
extractNumericPropTypes
};
/**
* Extracts properties that are always numeric
*
* @param features
* @returns object with numeric types
*/
function extractNumericPropTypes(features) {
const propArrayTypes = {};
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, Array is stored to prevent rechecking in the future
// Additionally, detects if 64 bit precision is required
const val = feature.properties[key];
propArrayTypes[key] = deduceArrayType(val, propArrayTypes[key]);
}
}
}
return propArrayTypes;
}
/**
* Fills coordinates into pre-allocated typed arrays
*
* @param features
* @param geometryInfo
* @param options
* @returns an accessor object with value and size keys
*/
// eslint-disable-next-line complexity, max-statements
function fillArrays(features, geometryInfo, options) {
const { pointPositionsCount, pointFeaturesCount, linePositionsCount, linePathsCount, lineFeaturesCount, polygonPositionsCount, polygonObjectsCount, polygonRingsCount, polygonFeaturesCount, propArrayTypes, coordLength } = geometryInfo;
const { numericPropKeys = [], PositionDataType = Float32Array, triangulate = true } = options;
const hasGlobalId = features[0] && 'id' in features[0];
const GlobalFeatureIdsDataType = features.length > 65535 ? Uint32Array : Uint16Array;
const points = {
type: 'Point',
positions: new PositionDataType(pointPositionsCount * coordLength),
globalFeatureIds: new GlobalFeatureIdsDataType(pointPositionsCount),
featureIds: pointFeaturesCount > 65535
? new Uint32Array(pointPositionsCount)
: new Uint16Array(pointPositionsCount),
numericProps: {},
properties: [],
fields: []
};
const lines = {
type: 'LineString',
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: [],
fields: []
};
const polygons = {
type: 'Polygon',
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),
globalFeatureIds: new GlobalFeatureIdsDataType(polygonPositionsCount),
featureIds: polygonFeaturesCount > 65535
? new Uint32Array(polygonPositionsCount)
: new Uint16Array(polygonPositionsCount),
numericProps: {},
properties: [],
fields: []
};
if (triangulate) {
polygons.triangles = [];
}
// 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
const T = propArrayTypes[propName];
object.numericProps[propName] = new T(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':
handlePoint(geometry, points, indexMap, coordLength, properties);
points.properties.push(keepStringProperties(properties, numericPropKeys));
if (hasGlobalId) {
points.fields.push({ id: feature.id });
}
indexMap.pointFeature++;
break;
case 'LineString':
handleLineString(geometry, lines, indexMap, coordLength, properties);
lines.properties.push(keepStringProperties(properties, numericPropKeys));
if (hasGlobalId) {
lines.fields.push({ id: feature.id });
}
indexMap.lineFeature++;
break;
case 'Polygon':
handlePolygon(geometry, polygons, indexMap, coordLength, properties);
polygons.properties.push(keepStringProperties(properties, numericPropKeys));
if (hasGlobalId) {
polygons.fields.push({ id: feature.id });
}
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
*
* @param geometry
* @param points
* @param indexMap
* @param coordLength
* @param properties
*/
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
*
* @param geometry
* @param lines
* @param indexMap
* @param coordLength
* @param properties
*/
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.indices.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.indices[i];
const end = i === il - 1
? geometry.data.length // last line, so read to end of data
: geometry.indices[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
*
* @param geometry
* @param polygons
* @param indexMap
* @param coordLength
* @param properties
*/
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.indices is a 2D array
for (let l = 0, ll = geometry.indices.length; l < ll; ++l) {
const startPosition = indexMap.polygonPosition;
polygons.polygonIndices[indexMap.polygonObject++] = startPosition;
const areas = geometry.areas[l];
const indices = geometry.indices[l];
const nextIndices = geometry.indices[l + 1];
for (let i = 0, il = indices.length; i < il; ++i) {
const start = indices[i];
const end = i === il - 1
? // last line, so either read to:
nextIndices === undefined
? geometry.data.length // end of data (no next indices)
: nextIndices[0] // start of first line in nextIndices
: indices[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, indices, { startPosition, endPosition, coordLength });
}
}
/**
* Triangulate polygon using earcut
*
* @param polygons
* @param areas
* @param indices
* @param param3
*/
function triangulatePolygon(polygons, areas, indices, { startPosition, endPosition, coordLength }) {
if (!polygons.triangles) {
return;
}
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 = indices[0];
const holes = indices.slice(1).map((n) => (n - offset) / coordLength);
// Compute triangulation
const triangles = 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 = triangles.length; t < tl; ++t) {
polygons.triangles.push(startPosition + triangles[t]);
}
}
/**
* Wraps an object containing array into accessors
*
* @param obj
* @param size
*/
function wrapProps(obj, size) {
const returnObj = {};
for (const key in obj) {
returnObj[key] = { value: obj[key], size };
}
return returnObj;
}
/**
* Wrap each array in an accessor object with value and size keys
*
* @param points
* @param lines
* @param polygons
* @param coordLength
* @returns object
*/
function makeAccessorObjects(points, lines, polygons, coordLength) {
const binaryFeatures = {
shape: 'binary-feature-collection',
points: {
...points,
positions: { value: points.positions, size: coordLength },
globalFeatureIds: { value: points.globalFeatureIds, size: 1 },
featureIds: { value: points.featureIds, size: 1 },
numericProps: wrapProps(points.numericProps, 1)
},
lines: {
...lines,
positions: { value: lines.positions, size: coordLength },
pathIndices: { value: lines.pathIndices, size: 1 },
globalFeatureIds: { value: lines.globalFeatureIds, size: 1 },
featureIds: { value: lines.featureIds, size: 1 },
numericProps: wrapProps(lines.numericProps, 1)
},
polygons: {
...polygons,
positions: { value: polygons.positions, size: coordLength },
polygonIndices: { value: polygons.polygonIndices, size: 1 },
primitivePolygonIndices: { value: polygons.primitivePolygonIndices, size: 1 },
globalFeatureIds: { value: polygons.globalFeatureIds, size: 1 },
featureIds: { value: polygons.featureIds, size: 1 },
numericProps: wrapProps(polygons.numericProps, 1)
} // triangles not expected
};
if (binaryFeatures.polygons && polygons.triangles) {
binaryFeatures.polygons.triangles = { value: new Uint32Array(polygons.triangles), size: 1 };
}
return binaryFeatures;
}
/**
* Add numeric properties to object
*
* @param object
* @param properties
* @param index
* @param length
*/
function fillNumericProperties(object, properties, index, length) {
for (const numericPropName in object.numericProps) {
if (numericPropName in properties) {
const value = properties[numericPropName];
object.numericProps[numericPropName].fill(value, index, index + length);
}
}
}
/**
* Keep string properties in object
*
* @param properties
* @param numericKeys
* @returns object
*/
function keepStringProperties(properties, numericKeys) {
const props = {};
for (const key in properties) {
if (!numericKeys.includes(key)) {
props[key] = properties[key];
}
}
return props;
}
/**
*
* Deduce correct array constructor to use for a given value
*
* @param x value to test
* @param constructor previous constructor deduced
* @returns PropArrayConstructor
*/
function deduceArrayType(x, constructor) {
if (constructor === Array || !Number.isFinite(x)) {
return Array;
}
// If this or previous value required 64bits use Float64Array
return constructor === Float64Array || Math.fround(x) !== x ? Float64Array : Float32Array;
}