@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
242 lines (210 loc) • 6.73 kB
text/typescript
/*
* 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];
}