Reference Source

src/parsers/parseLASIntoXKTModel.js

import {parse} from '@loaders.gl/core';
import {LASLoader} from '@loaders.gl/las';

import {math} from "../lib/math.js";

const MAX_VERTICES = 500000; // TODO: Rough estimate

/**
 * @desc Parses LAS and LAZ point cloud data into an {@link XKTModel}.
 *
 * This parser handles both the LASER file format (LAS) and its compressed version (LAZ),
 * a public format for the interchange of 3-dimensional point cloud data data, developed
 * for LIDAR mapping purposes.
 *
 * ## Usage
 *
 * In the example below we'll create an {@link XKTModel}, then load an LAZ point cloud model into it.
 *
 * ````javascript
 * utils.loadArraybuffer("./models/laz/autzen.laz", async (data) => {
 *
 *     const xktModel = new XKTModel();
 *
 *     await parseLASIntoXKTModel({
 *          data,
 *          xktModel,
 *          rotateX: true,
 *          log: (msg) => { console.log(msg); }
 *     }).then(()=>{
 *        xktModel.finalize();
 *     },
 *     (msg) => {
 *         console.error(msg);
 *     });
 * });
 * ````
 *
 * @param {Object} params Parsing params.
 * @param {ArrayBuffer} params.data LAS/LAZ file data.
 * @param {XKTModel} params.xktModel XKTModel to parse into.
 * @param {boolean} [params.center=false] Set true to center the LAS point positions to [0,0,0]. This is applied before the transformation matrix, if specified.
 * @param {Boolean} [params.transform] 4x4 transformation matrix to transform point positions. Use this to rotate, translate and scale them if neccessary.
 * @param {Number|String} [params.colorDepth=8] Whether colors encoded using 8 or 16 bits. Can be set to 'auto'. LAS specification recommends 16 bits.
 * @param {Boolean} [params.fp64=false] Configures if LASLoaderPlugin assumes that LAS positions are stored in 64-bit floats instead of 32-bit.
 * @param {Number} [params.skip=1] Read one from every n points.
 * @param {Object} [params.stats] Collects statistics.
 * @param {function} [params.log] Logging callback.
 * @returns {Promise} Resolves when LAS has been parsed.
 */
function parseLASIntoXKTModel({
                                  data,
                                  xktModel,
                                  center = false,
                                  transform = null,
                                  colorDepth = "auto",
                                  fp64 = false,
                                  skip = 1,
                                  stats,
                                  log = () => {
                                  }
                              }) {

    if (log) {
        log("Using parser: parseLASIntoXKTModel");
    }

    return new Promise(function (resolve, reject) {

        if (!data) {
            reject("Argument expected: data");
            return;
        }

        if (!xktModel) {
            reject("Argument expected: xktModel");
            return;
        }

        log("Converting LAZ/LAS");

        log(`center: ${center}`);
        if (transform) {
            log(`transform: [${transform}]`);
        }
        log(`colorDepth: ${colorDepth}`);
        log(`fp64: ${fp64}`);
        log(`skip: ${skip}`);

        parse(data, LASLoader, {
            las: {
                colorDepth,
                fp64
            }
        }).then((parsedData) => {

            const attributes = parsedData.attributes;

            const loaderData = parsedData.loaderData;
            const pointsFormatId = loaderData.pointsFormatId !== undefined ? loaderData.pointsFormatId : -1;

            if (!attributes.POSITION) {
                log("No positions found in file (expected for all LAS point formats)");
                return;
            }

            let readAttributes = {};

            switch (pointsFormatId) {
                case 0:
                    if (!attributes.intensity) {
                        log("No intensities found in file (expected for LAS point format 0)");
                        return;
                    }

                    readAttributes = readIntensities(attributes.POSITION, attributes.intensity);
                    break;
                case 1:
                    if (!attributes.intensity) {
                        log("No intensities found in file (expected for LAS point format 1)");
                        return;
                    }
                    readAttributes = readIntensities(attributes.POSITION, attributes.intensity);
                    break;
                case 2:
                    if (!attributes.intensity) {
                        log("No intensities found in file (expected for LAS point format 2)");
                        return;
                    }

                    readAttributes = readColorsAndIntensities(attributes.POSITION, attributes.COLOR_0, attributes.intensity);
                    break;
                case 3:
                    if (!attributes.intensity) {
                        log("No intensities found in file (expected for LAS point format 3)");
                        return;
                    }
                    readAttributes = readColorsAndIntensities(attributes.POSITION, attributes.COLOR_0, attributes.intensity);
                    break;
            }

            const pointsChunks = chunkArray(readPositions(readAttributes.positions), MAX_VERTICES * 3);
            const colorsChunks = chunkArray(readAttributes.colors, MAX_VERTICES * 4);

            const meshIds = [];

            for (let j = 0, lenj = pointsChunks.length; j < lenj; j++) {

                const geometryId = `geometry-${j}`;
                const meshId = `mesh-${j}`;

                meshIds.push(meshId);

                xktModel.createGeometry({
                    geometryId: geometryId,
                    primitiveType: "points",
                    positions: pointsChunks[j],
                    colorsCompressed: colorsChunks[j]
                });

                xktModel.createMesh({
                    meshId,
                    geometryId
                });
            }

            const entityId = math.createUUID();

            xktModel.createEntity({
                entityId,
                meshIds
            });

            const rootMetaObjectId = math.createUUID();

            xktModel.createMetaObject({
                metaObjectId: rootMetaObjectId,
                metaObjectType: "Model",
                metaObjectName: "Model"
            });

            xktModel.createMetaObject({
                metaObjectId: entityId,
                metaObjectType: "PointCloud",
                metaObjectName: "PointCloud (LAZ)",
                parentMetaObjectId: rootMetaObjectId
            });

            if (stats) {
                stats.sourceFormat = "LAS";
                stats.schemaVersion = "";
                stats.title = "";
                stats.author = "";
                stats.created = "";
                stats.numMetaObjects = 2;
                stats.numPropertySets = 0;
                stats.numObjects = 1;
                stats.numGeometries = 1;
                stats.numVertices = readAttributes.positions.length / 3;
            }

            resolve();

        }, (errMsg) => {
            reject(errMsg);
        });
    });

    function readPositions(positionsValue) {
        if (positionsValue) {
            if (center) {
                const centerPos = math.vec3();
                const numPoints = positionsValue.length;
                for (let i = 0, len = positionsValue.length; i < len; i += 3) {
                    centerPos[0] += positionsValue[i + 0];
                    centerPos[1] += positionsValue[i + 1];
                    centerPos[2] += positionsValue[i + 2];
                }
                centerPos[0] /= numPoints;
                centerPos[1] /= numPoints;
                centerPos[2] /= numPoints;
                for (let i = 0, len = positionsValue.length; i < len; i += 3) {
                    positionsValue[i + 0] -= centerPos[0];
                    positionsValue[i + 1] -= centerPos[1];
                    positionsValue[i + 2] -= centerPos[2];
                }
            }
            if (transform) {
                const mat = math.mat4(transform);
                const pos = math.vec3();
                for (let i = 0, len = positionsValue.length; i < len; i += 3) {
                    pos[0] = positionsValue[i + 0];
                    pos[1] = positionsValue[i + 1];
                    pos[2] = positionsValue[i + 2];
                    math.transformPoint3(mat, pos, pos);
                    positionsValue[i + 0] = pos[0];
                    positionsValue[i + 1] = pos[1];
                    positionsValue[i + 2] = pos[2];
                }
            }
        }
        return positionsValue;
    }

    function readColorsAndIntensities(attributesPosition, attributesColor, attributesIntensity) {
        const positionsValue = attributesPosition.value;
        const colors = attributesColor.value;
        const colorSize = attributesColor.size;
        const intensities = attributesIntensity.value;
        const colorsCompressedSize = intensities.length * 4;
        const positions = [];
        const colorsCompressed = new Uint8Array(colorsCompressedSize / skip);
        let count = skip;
        for (let i = 0, j = 0, k = 0, l = 0, m = 0, n=0,len = intensities.length; i < len; i++, k += colorSize, j += 4, l += 3) {
            if (count <= 0) {
                colorsCompressed[m++] = colors[k + 0];
                colorsCompressed[m++] = colors[k + 1];
                colorsCompressed[m++] = colors[k + 2];
                colorsCompressed[m++] = Math.round((intensities[i] / 65536) * 255);
                positions[n++] = positionsValue[l + 0];
                positions[n++] = positionsValue[l + 1];
                positions[n++] = positionsValue[l + 2];
                count = skip;
            } else {
                count--;
            }
        }
        return {
            positions,
            colors: colorsCompressed
        };
    }

    function readIntensities(attributesPosition, attributesIntensity) {
        const positionsValue = attributesPosition.value;
        const intensities = attributesIntensity.value;
        const colorsCompressedSize = intensities.length * 4;
        const positions = [];
        const colorsCompressed = new Uint8Array(colorsCompressedSize / skip);
        let count = skip;
        for (let i = 0, j = 0, k = 0, l = 0, m = 0, n = 0, len = intensities.length; i < len; i++, k += 3, j += 4, l += 3) {
            if (count <= 0) {
                colorsCompressed[m++] = 0;
                colorsCompressed[m++] = 0;
                colorsCompressed[m++] = 0;
                colorsCompressed[m++] = Math.round((intensities[i] / 65536) * 255);
                positions[n++] = positionsValue[l + 0];
                positions[n++] = positionsValue[l + 1];
                positions[n++] = positionsValue[l + 2];
                count = skip;
            } else {
                count--;
            }
        }
        return {
            positions,
            colors: colorsCompressed
        };
    }

    function chunkArray(array, chunkSize) {
        if (chunkSize >= array.length) {
            return [array]; // One chunk
        }
        let result = [];
        for (let i = 0; i < array.length; i += chunkSize) {
            result.push(array.slice(i, i + chunkSize));
        }
        return result;
    }

}

export {parseLASIntoXKTModel};