Display a massive point cloud by aggregating dozens of datasets.

Parameters
Loading metadata...
100% © IGN
index.js
import { Vector3 } from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";

import Instance from "@giro3d/giro3d/core/Instance.js";
import PointCloud from "@giro3d/giro3d/entities/PointCloud.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import AggregatePointCloudSource from "@giro3d/giro3d/sources/AggregatePointCloudSource";
import COPCSource from "@giro3d/giro3d/sources/COPCSource.js";
import { setLazPerfPath } from "@giro3d/giro3d/sources/las/config.js";

import ColorMap from "@giro3d/giro3d/core/layer/ColorMap.js";

function bindDropDown(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLSelectElement)) {
    throw new Error(
      "invalid binding element: expected HTMLSelectElement, got: " +
        element.constructor.name,
    );
  }

  element.onchange = () => {
    onChange(element.value);
  };

  const callback = (v) => {
    element.value = v;
    onChange(element.value);
  };

  const setOptions = (options) => {
    const items = options.map(
      (opt) =>
        `<option value=${opt.id} ${opt.selected ? "selected" : ""}>${opt.name}</option>`,
    );
    element.innerHTML = items.join("\n");
  };

  return [callback, element.value, element, setOptions];
}

function bindNumberInput(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.onchange = () => {
    onChange(element.valueAsNumber);
  };

  const callback = (v) => {
    element.value = v.toString();
    onChange(element.valueAsNumber);
  };

  return [callback, element.valueAsNumber, element];
}

function bindProgress(id) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLDivElement)) {
    throw new Error(
      "invalid binding element: expected HTMLDivElement, got: " +
        element.constructor.name,
    );
  }

  const setProgress = (normalized, text) => {
    element.style.width = `${Math.round(normalized * 100)}%`;
    if (text) {
      element.innerText = text;
    }
  };

  return [setProgress, element.parentElement];
}

function bindToggle(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLButtonElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.checked);
  };

  const callback = (v) => {
    element.checked = v;
    onChange(element.checked);
  };

  return [callback, element.checked, element];
}

function formatPointCount(count, numberFormat = undefined) {
  let displayedPointCount = count;
  let suffix = "";

  if (count > 1_000_000) {
    displayedPointCount /= 1_000_000;
    suffix = "M";
  } else if (count > 1_000_000_000) {
    displayedPointCount /= 1_000_000_000;
    suffix = "B";
  }

  if (numberFormat == null) {
    numberFormat = new Intl.NumberFormat(undefined, {
      maximumFractionDigits: 2,
    });
  }

  return numberFormat.format(displayedPointCount) + suffix;
}

function makeColorRamp(
  preset,
  discrete = false,
  invert = false,
  mirror = false,
) {
  let nshades = discrete ? 10 : 256;

  const values = colormap({ colormap: preset, nshades });

  const colors = values.map((v) => new Color(v));

  if (invert) {
    colors.reverse();
  }

  if (mirror) {
    const mirrored = [...colors, ...colors.reverse()];
    return mirrored;
  }

  return colors;
}

// LAS processing requires the WebAssembly laz-perf library
// This path is specific to your project, and must be set accordingly.
setLazPerfPath("/assets/wasm");

Instance.registerCRS(
  "EPSG:2154",
  "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs",
);

const instance = new Instance({
  target: "view",
  crs: "EPSG:2154",
  backgroundColor: "black",
  renderer: {
    logarithmicDepthBuffer: true,
  },
});

instance.renderingOptions.enableEDL = true;
instance.renderingOptions.EDLStrength = 5;

const colormaps = {
  Intensity: new ColorMap(makeColorRamp("jet"), 0, 100),
  Z: new ColorMap(makeColorRamp("portland"), 0, 100),
};

const datasets = [
  "LHD_FXX_0657_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0657_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0651_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0650_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0653_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0655_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0652_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0656_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0654_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0649_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0648_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0647_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0646_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0644_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0645_6859_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6868_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6864_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6865_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6866_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6867_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6860_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6861_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6862_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6863_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6857_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6858_PTS_O_LAMB93_IGN69.copc.laz",
  "LHD_FXX_0658_6859_PTS_O_LAMB93_IGN69.copc.laz",
];

const server = "https://3d.oslandia.com/giro3d/pointclouds/lidarhd/paris/";

const source = new AggregatePointCloudSource({
  sources: datasets.map((dataset) => new COPCSource({ url: server + dataset })),
});

const pointCloud = new PointCloud({ source });

const [setProgress, progressElement] = bindProgress("progress");

source.addEventListener("progress", () => setProgress(source.progress));

pointCloud.showVolume = true;

async function onInitialized(entity) {
  progressElement.style.display = "none";
  document.getElementById("options").style.display = "block";

  entity.colorMap = colormaps.Z;
  entity.setActiveAttribute("Z");

  document.getElementById("point-count").innerText = formatPointCount(
    entity.pointCount,
  );

  document.getElementById("file-count").innerText =
    source.sources.length.toString();

  const volume = entity.getBoundingBox();
  const center = volume.getCenter(new Vector3());

  const camera = instance.view.camera;
  const lookAt = new Vector3(center.x, center.y, volume.min.z);

  camera.position.set(center.x, center.y - 1, volume.max.z * 10);

  camera.lookAt(lookAt);

  const controls = new MapControls(camera, instance.domElement);
  controls.target.copy(lookAt);
  controls.enableDamping = true;
  controls.dampingFactor = 0.25;
  instance.view.setControls(controls);

  instance.notifyChange(camera);

  const metadata = await source.getMetadata();

  // Update the Z colormap with the min/max height of the datasets.
  colormaps.Z.min = volume.min.z * 1.1;
  colormaps.Z.max = volume.max.z * 0.6;

  colormaps.Intensity.min = 0;
  colormaps.Intensity.max = 5000;

  const [, , , setAvailableAttributes] = bindDropDown(
    "attribute",
    (attribute) => {
      entity.setActiveAttribute(attribute);
      entity.colorMap = colormaps[attribute];
    },
  );

  setAvailableAttributes(
    metadata.attributes.map((att, index) => ({
      id: att.name,
      name: att.name,
      selected: index === 0,
    })),
  );

  bindToggle("show-volume", (show) => (entity.showVolume = show));
  bindToggle("show-tile-volumes", (show) => (entity.showNodeVolumes = show));
  bindToggle("edl", (edl) => {
    instance.renderingOptions.enableEDL = edl;
    instance.notifyChange();
  });
  bindNumberInput("point-budget", (v) => {
    if (v <= 0) {
      entity.pointBudget = null;
    } else {
      entity.pointBudget = v;
    }
  });

  instance.addEventListener("update-end", () => {
    document.getElementById("displayed-point-count").innerText =
      formatPointCount(entity.displayedPointCount);
  });
}

instance.add(pointCloud).then(onInitialized).catch(console.error);

Inspector.attach("inspector", instance);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Massive point cloud</title>
    <meta charset="UTF-8" />
    <meta name="name" content="aggregate_pointcloud" />
    <meta
      name="description"
      content="Display a massive point cloud by aggregating dozens of datasets."
    />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="icon" href="https://giro3d.org/images/favicon.svg" />
    <link
      href="https://giro3d.org/assets/bootstrap-custom.css"
      rel="stylesheet"
    />
    <script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
    <link
      rel="stylesheet"
      type="text/css"
      href="https://giro3d.org/next/examples/css/example.css"
    />
  </head>

  <body>
    <div id="view" class="m-0 p-0 w-100 h-100"></div>
    <div
      id="inspector"
      class="position-absolute top-0 start-0 mh-100 overflow-auto"
    ></div>

    <div class="side-pane-with-status-bar" style="width: 25rem">
      <!--Parameters -->
      <div class="card">
        <div class="card-header">Parameters</div>

        <div class="card-body">
          <div class="progress" role="progressbar">
            <div
              class="progress-bar bg-info progress-bar-striped progress-bar-animated text-dark"
              id="progress"
              style="width: 0%"
            >
              Loading metadata...
            </div>
          </div>

          <div id="options" style="display: none">
            <ul class="list-group mb-3" id="table">
              <li class="list-group-item">
                Total points
                <b id="point-count" class="d-float float-end"></b>
              </li>
              <li
                class="list-group-item"
                title="The number of points currently displayed"
              >
                Displayed points
                <b id="displayed-point-count" class="d-float float-end"></b>
              </li>
              <li class="list-group-item">
                Files: <b id="file-count" class="d-float float-end"></b>
              </li>
            </ul>

            <!-- Active attribute selector -->
            <div class="input-group" id="attribute-group">
              <label class="input-group-text col-5" for="attribute"
                >Dimension</label
              >
              <select
                class="form-select"
                id="attribute"
                autocomplete="off"
                title="Sets the active attribute of the point cloud"
              ></select>
            </div>

            <!-- Point budget -->
            <div class="input-group mt-1" id="attribute-group">
              <label class="input-group-text col-5" for="attribute"
                >Point budget</label
              >
              <input
                type="number"
                min="-1"
                max="99999999999"
                value="-1"
                step="1"
                class="form-control"
                id="point-budget"
                autocomplete="off"
              />
            </div>

            <!-- Show volume -->
            <div class="form-check form-switch mt-2">
              <input
                class="form-check-input"
                type="checkbox"
                role="switch"
                checked
                id="show-volume"
                autocomplete="off"
              />
              <label
                title="Show the volume of the octree"
                class="form-check-label"
                for="show-volume"
                >Show dataset volume</label
              >
            </div>

            <!-- Show octree volumes -->
            <div class="form-check form-switch">
              <input
                class="form-check-input"
                type="checkbox"
                role="switch"
                id="show-tile-volumes"
                autocomplete="off"
              />
              <label
                title="Show the volume of the octree"
                class="form-check-label"
                for="show-tile-volumes"
                >Show octrees</label
              >
            </div>

            <!-- Eye Dome Lighting -->
            <div class="form-check form-switch">
              <input
                class="form-check-input"
                type="checkbox"
                role="switch"
                checked
                id="edl"
                autocomplete="off"
              />
              <label
                title="Toggles Eye Dome Lighting post-processing effect"
                class="form-check-label"
                for="edl"
                >Eye Dome Lighting</label
              >
            </div>
          </div>
        </div>
      </div>
    </div>

    <script type="module" src="index.js"></script>
    <script>
      /* activate popovers */
      const popoverTriggerList = [].slice.call(
        document.querySelectorAll('[data-bs-toggle="popover"]'),
      );
      popoverTriggerList.map(
        // bootstrap is used as script in the template, disable warning about undef
        // eslint-disable-next-line no-undef
        (popoverTriggerEl) =>
          new bootstrap.Popover(popoverTriggerEl, {
            trigger: "hover",
            placement: "left",
            content: document.getElementById(
              popoverTriggerEl.getAttribute("data-bs-content"),
            ).innerHTML,
            html: true,
          }),
      );
    </script>
  </body>
</html>
package.json
{
    "name": "aggregate_pointcloud",
    "dependencies": {
        "@giro3d/giro3d": "git+https://gitlab.com/giro3d/giro3d.git"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}