UNPKG

@loaders.gl/gis

Version:

Helpers for GIS category data

290 lines (256 loc) 9.15 kB
// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import type { BinaryGeometry, BinaryGeometryType, BinaryPointGeometry, BinaryLineGeometry, BinaryPolygonGeometry, BinaryFeatureCollection, BinaryFeature, // BinaryPointFeature, // BinaryLineFeature, // BinaryPolygonFeature, BinaryAttribute, Feature, Geometry, Position, GeoJsonProperties, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon } from '@loaders.gl/schema'; // Note:L We do not handle GeometryCollection, define a limited Geometry type that always has coordinates. // type FeatureGeometry = Point | MultiPoint | LineString | MultiLineString | Polygon | MultiPolygon; type BinaryToGeoJsonOptions = { type?: BinaryGeometryType; globalFeatureId?: number; }; /** * Convert binary geometry representation to GeoJSON * @param data geometry data in binary representation * @param options * @param options.type Input data type: Point, LineString, or Polygon * @param options.featureId Global feature id. If specified, only a single feature is extracted * @return GeoJSON objects */ export function binaryToGeojson( data: BinaryFeatureCollection, options?: BinaryToGeoJsonOptions ): Feature[] | Feature { const globalFeatureId = options?.globalFeatureId; if (globalFeatureId !== undefined) { return getSingleFeature(data, globalFeatureId); } return parseFeatures(data, options?.type); } /** * Return a single feature from a binary geometry representation as GeoJSON * @param data geometry data in binary representation * @return GeoJSON feature */ function getSingleFeature(data: BinaryFeatureCollection, globalFeatureId: number): Feature { const dataArray = normalizeInput(data); for (const data of dataArray) { let lastIndex = 0; let lastValue = data.featureIds.value[0]; // Scan through data until we find matching feature for (let i = 0; i < data.featureIds.value.length; i++) { const currValue = data.featureIds.value[i]; if (currValue === lastValue) { // eslint-disable-next-line no-continue continue; } if (globalFeatureId === data.globalFeatureIds.value[lastIndex]) { return parseFeature(data, lastIndex, i); } lastIndex = i; lastValue = currValue; } if (globalFeatureId === data.globalFeatureIds.value[lastIndex]) { return parseFeature(data, lastIndex, data.featureIds.value.length); } } throw new Error(`featureId:${globalFeatureId} not found`); } function parseFeatures(data: BinaryFeatureCollection, type?: BinaryGeometryType): Feature[] { const dataArray = normalizeInput(data, type); return parseFeatureCollection(dataArray); } /** Parse input binary data and return a valid GeoJSON geometry object */ export function binaryToGeometry( data: BinaryGeometry, startIndex?: number, endIndex?: number ): Geometry { switch (data.type) { case 'Point': return pointToGeoJson(data, startIndex, endIndex); case 'LineString': return lineStringToGeoJson(data, startIndex, endIndex); case 'Polygon': return polygonToGeoJson(data, startIndex, endIndex); default: const unexpectedInput: never = data; throw new Error(`Unsupported geometry type: ${(unexpectedInput as any)?.type}`); } } // Normalize features // Return an array of data objects, each of which have a type key function normalizeInput(data: BinaryFeatureCollection, type?: BinaryGeometryType): BinaryFeature[] { const features: BinaryFeature[] = []; if (data.points) { data.points.type = 'Point'; features.push(data.points); } if (data.lines) { data.lines.type = 'LineString'; features.push(data.lines); } if (data.polygons) { data.polygons.type = 'Polygon'; features.push(data.polygons); } return features; } /** Parse input binary data and return an array of GeoJSON Features */ function parseFeatureCollection(dataArray: BinaryFeature[]): Feature[] { const features: Feature[] = []; for (const data of dataArray) { if (data.featureIds.value.length === 0) { // eslint-disable-next-line no-continue continue; } let lastIndex = 0; let lastValue = data.featureIds.value[0]; // Need to deduce start, end indices of each feature for (let i = 0; i < data.featureIds.value.length; i++) { const currValue = data.featureIds.value[i]; if (currValue === lastValue) { // eslint-disable-next-line no-continue continue; } features.push(parseFeature(data, lastIndex, i)); lastIndex = i; lastValue = currValue; } // Last feature features.push(parseFeature(data, lastIndex, data.featureIds.value.length)); } return features; } /** Parse input binary data and return a single GeoJSON Feature */ function parseFeature(data: BinaryFeature, startIndex?: number, endIndex?: number): Feature { const geometry = binaryToGeometry(data, startIndex, endIndex); const properties = parseProperties(data, startIndex, endIndex); const fields = parseFields(data, startIndex, endIndex); return {type: 'Feature', geometry, properties, ...fields}; } /** Parse input binary data and return an object of fields */ function parseFields(data, startIndex: number = 0, endIndex?: number): GeoJsonProperties { return data.fields && data.fields[data.featureIds.value[startIndex]]; } /** Parse input binary data and return an object of properties */ function parseProperties(data, startIndex: number = 0, endIndex?: number): GeoJsonProperties { const properties = Object.assign({}, data.properties[data.featureIds.value[startIndex]]); for (const key in data.numericProps) { properties[key] = data.numericProps[key].value[startIndex]; } return properties; } /** Parse binary data of type Polygon */ function polygonToGeoJson( data: BinaryPolygonGeometry, startIndex: number = -Infinity, endIndex: number = Infinity ): Polygon | MultiPolygon { const {positions} = data; const polygonIndices = data.polygonIndices.value.filter((x) => x >= startIndex && x <= endIndex); const primitivePolygonIndices = data.primitivePolygonIndices.value.filter( (x) => x >= startIndex && x <= endIndex ); const multi = polygonIndices.length > 2; // Polygon if (!multi) { const coordinates: Position[][] = []; for (let i = 0; i < primitivePolygonIndices.length - 1; i++) { const startRingIndex = primitivePolygonIndices[i]; const endRingIndex = primitivePolygonIndices[i + 1]; const ringCoordinates = ringToGeoJson(positions, startRingIndex, endRingIndex); coordinates.push(ringCoordinates); } return {type: 'Polygon', coordinates}; } // MultiPolygon const coordinates: Position[][][] = []; for (let i = 0; i < polygonIndices.length - 1; i++) { const startPolygonIndex = polygonIndices[i]; const endPolygonIndex = polygonIndices[i + 1]; const polygonCoordinates = polygonToGeoJson( data, startPolygonIndex, endPolygonIndex ).coordinates; coordinates.push(polygonCoordinates as Position[][]); } return {type: 'MultiPolygon', coordinates}; } /** Parse binary data of type LineString */ function lineStringToGeoJson( data: BinaryLineGeometry, startIndex: number = -Infinity, endIndex: number = Infinity ): LineString | MultiLineString { const {positions} = data; const pathIndices = data.pathIndices.value.filter((x) => x >= startIndex && x <= endIndex); const multi = pathIndices.length > 2; if (!multi) { const coordinates = ringToGeoJson(positions, pathIndices[0], pathIndices[1]); return {type: 'LineString', coordinates}; } const coordinates: Position[][] = []; for (let i = 0; i < pathIndices.length - 1; i++) { const ringCoordinates = ringToGeoJson(positions, pathIndices[i], pathIndices[i + 1]); coordinates.push(ringCoordinates); } return {type: 'MultiLineString', coordinates}; } /** Parse binary data of type Point */ function pointToGeoJson(data: BinaryPointGeometry, startIndex, endIndex): Point | MultiPoint { const {positions} = data; const coordinates = ringToGeoJson(positions, startIndex, endIndex); const multi = coordinates.length > 1; if (multi) { return {type: 'MultiPoint', coordinates}; } return {type: 'Point', coordinates: coordinates[0]}; } /** * Parse a linear ring of positions to a GeoJSON linear ring * * @param positions Positions TypedArray * @param startIndex Start index to include in ring * @param endIndex End index to include in ring * @returns GeoJSON ring */ function ringToGeoJson( positions: BinaryAttribute, startIndex?: number, endIndex?: number ): Position[] { startIndex = startIndex || 0; endIndex = endIndex || positions.value.length / positions.size; const ringCoordinates: Position[] = []; for (let j = startIndex; j < endIndex; j++) { const coord = Array<number>(); for (let k = j * positions.size; k < (j + 1) * positions.size; k++) { coord.push(Number(positions.value[k])); } ringCoordinates.push(coord); } return ringCoordinates; }