UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

242 lines (210 loc) 6.73 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import type { Dimension } from 'copc'; import type { Box3 } from 'three'; import type { PointCloudAttribute } from '../PointCloudSource'; import type { DimensionFilter } from './filter'; // The list of supported LAS dimensions. export type DimensionName = | 'X' | 'Y' | 'Z' | 'Intensity' | 'ReturnNumber' | 'NumberOfReturns' | 'ScanDirectionFlag' | 'EdgeOfFlightLine' | 'Classification' | 'ScanAngle' | 'ScanAngleRank' | 'Red' | 'Green' | 'Blue' | 'UserData' | 'ScannerChannel' | 'PointSourceId' | 'Infrared' | 'ScanChannel' | 'ScannerChannel' | 'GpsTime'; const bitRange = (bits: number): { min: number; max: number } => ({ min: 0, max: 2 ** bits - 1 }); const boolRange = { min: 0, max: 1 }; const u8Range = { min: 0, max: 255 }; const u16Range = { min: 0, max: 65536 }; /** * Default min/max values for various LAS dimensions. Most default values are directly dependent on * the data type of the dimension (e.g a `Uint8` dimensions will have a default range of 0-255), but * some dimensions have narrower min/max, for example `ScanAngle`: * * ```js * // Get the default min/max for return number * const { min, max } = DEFAULT_VALUE_RANGES['ReturnNumber']; * // { min: 0, max: 8 } * ``` * @internal */ export const DEFAULT_VALUE_RANGES: Record<DimensionName, { min?: number; max?: number }> = { // Point Data Record Format 0 X: { min: undefined, max: undefined }, Y: { min: undefined, max: undefined }, Z: { min: undefined, max: undefined }, Intensity: u16Range, ReturnNumber: bitRange(3), NumberOfReturns: bitRange(3), ScanDirectionFlag: boolRange, EdgeOfFlightLine: boolRange, Classification: u8Range, ScanAngle: { min: -90, max: +90 }, ScanAngleRank: { min: -90, max: +90 }, UserData: u8Range, PointSourceId: u16Range, ScanChannel: u16Range, GpsTime: { min: 0, max: +9999 }, Red: u16Range, Green: u16Range, Blue: u16Range, ScannerChannel: bitRange(2), Infrared: u16Range, }; /** * Given a size in bytes for a LAS dimension, return a size in bytes for point cloud attributes, * performing downcasting for unsupported sizes (i.e 64-bit numbers). */ function getAttributeByteSize(input: Dimension['size']): PointCloudAttribute['size'] { switch (input) { case 8: // Since shaders do not support 64-bit numbers, we have to downcast them to 32-bit. return 4; default: // Other sizes are well supported in shaders, no need for downcasting. return input; } } /** * Extracts attributes from LAS dimensions. * @internal */ export function extractAttributes( dimensions: Dimension.Map, volume: Box3, compressColors: boolean, gpsTimeRange: [number, number] | null, ): PointCloudAttribute[] { const dimensionKeys = new Set(Object.keys(dimensions)); const has = (...dims: DimensionName[]): boolean => { for (const dim of dims) { if (!dimensionKeys.has(dim)) { return false; } } return true; }; function getInterpretation( dimensionName: DimensionName, ): PointCloudAttribute['interpretation'] { if (dimensionName === 'Classification') { return 'classification'; } return 'unknown'; } const result: PointCloudAttribute[] = []; // Pseudo-dimension 'Color' if (has('Red', 'Green', 'Blue')) { result.push({ name: 'Color', interpretation: 'color', type: 'unsigned', size: compressColors ? 1 : 2, dimension: 3, min: 0, max: compressColors ? 255 : 65535, }); } for (const entry of Object.entries(dimensions)) { const name = entry[0] as DimensionName; const dimension = entry[1]; if (dimension == null) { continue; } if (name === 'X' || name === 'Y') { continue; } if (name === 'Z') { result.push({ dimension: 1, interpretation: 'unknown', type: 'float', size: getAttributeByteSize(dimension.size), name: 'Z', min: volume.min.z, max: volume.max.z, }); continue; } // Special case for GPS time range, since we may // have the bounds in the file header if (name === 'GpsTime' && gpsTimeRange != null) { result.push({ dimension: 1, interpretation: 'unknown', type: 'float', size: getAttributeByteSize(dimension.size), name: 'GpsTime', min: gpsTimeRange[0], max: gpsTimeRange[1], }); continue; } const type = dimension.type; if (type == null) { continue; } const range = DEFAULT_VALUE_RANGES[name as DimensionName]; const attr: PointCloudAttribute = { name, dimension: 1, size: getAttributeByteSize(dimension.size), type, interpretation: getInterpretation(name as DimensionName), min: range?.min, max: range?.max, }; result.push(attr); } return result; } /** * Return all the dimensions that we expect to read from a given view. The dimensions are the union * of the required dimensions (the X, Y, Z dimension), the optional requested attribute, and all the * dimensions that are concerned by filters, if any. * * For example: if we want to read intensities, but exclude some classifications and GPS time, we * have to read dimensions: * - `X`, `Y`, `Z` for the point position, * - `Intensity` for the requested attribute * - `Classification` and `GpsTime` for filtering. * @internal */ export function getDimensionsToRead( attributes: PointCloudAttribute[], readPosition: boolean, filters: DimensionFilter[], ): DimensionName[] { const set = new Set<DimensionName>(readPosition ? ['X', 'Y', 'Z'] : []); for (const filter of filters) { set.add(filter.dimension); } for (const attribute of attributes) { if (attribute.interpretation === 'color') { set.add('Red'); set.add('Green'); set.add('Blue'); } else { set.add(attribute.name as DimensionName); } } return [...set]; }