import colormap from "colormap" ;
import { Vector3 } from "three" ;
import { MapControls } from "three/examples/jsm/controls/MapControls.js" ;
import Extent from "@giro3d/giro3d/core/geographic/Extent.js" ;
import Instance from "@giro3d/giro3d/core/Instance.js" ;
import ColorMap from "@giro3d/giro3d/core/ColorMap.js" ;
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js" ;
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js" ;
import Inspector from "@giro3d/giro3d/gui/Inspector.js" ;
import { MODE } from "@giro3d/giro3d/renderer/PointCloudMaterial.js" ;
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.js" ;
function bindColorMapBounds ( callback ) {
const min = document. getElementById ( "min" );
if ( ! (min instanceof HTMLInputElement )) {
throw new Error (
"invalid binding element: expected HTMLInputElement, got: " +
min. constructor .name,
);
}
const max = document. getElementById ( "max" );
if ( ! (max instanceof HTMLInputElement )) {
throw new Error (
"invalid binding element: expected HTMLInputElement, got: " +
max. constructor .name,
);
}
const lower = min;
const upper = max;
callback (lower.valueAsNumber, upper.valueAsNumber);
function updateLabels () {
document. getElementById ( "minLabel" ).innerText =
`Lower bound: ${ Math . round ( lower . valueAsNumber ) }m` ;
document. getElementById ( "maxLabel" ).innerText =
`Upper bound: ${ Math . round ( upper . valueAsNumber ) }m` ;
}
lower. oninput = function oninput () {
const rawValue = lower.valueAsNumber;
const clampedValue = MathUtils. clamp (
rawValue,
Number. parseFloat (lower.min),
upper.valueAsNumber - 1 ,
);
lower.valueAsNumber = clampedValue;
callback (lower.valueAsNumber, upper.valueAsNumber);
updateLabels ();
};
upper. oninput = function oninput () {
const rawValue = upper.valueAsNumber;
const clampedValue = MathUtils. clamp (
rawValue,
lower.valueAsNumber + 1 ,
Number. parseFloat (upper.max),
);
upper.valueAsNumber = clampedValue;
callback (lower.valueAsNumber, upper.valueAsNumber);
updateLabels ();
};
const externalInput = ( min , max ) => {
lower.min = min;
lower.max = max;
upper.min = min;
upper.max = max;
lower.valueAsNumber = min;
upper.valueAsNumber = max;
callback (lower.valueAsNumber, upper.valueAsNumber);
updateLabels ();
};
return externalInput;
}
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 bindNumericalDropDown ( 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 ( parseInt (element.value));
};
const callback = ( v ) => {
element.value = v. toString ();
onChange ( parseInt (element.value));
};
return [callback, parseInt (element.value), element];
}
function bindSlider ( id , onChange ) {
const element = document. getElementById (id);
if ( ! (element instanceof HTMLInputElement )) {
throw new Error (
"invalid binding element: expected HTMLInputElement, got: " +
element. constructor .name,
);
}
element. oninput = function oninput () {
onChange (element.valueAsNumber);
};
const setValue = ( v , min , max , step ) => {
if (min != null && max != null ) {
element.min = min. toString ();
element.max = max. toString ();
if (step != null ) {
element.step = step;
}
}
element.valueAsNumber = v;
onChange (element.valueAsNumber);
};
const initialValue = element.valueAsNumber;
return [setValue, initialValue, element];
}
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 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;
}
const colorRamps = {};
function makeColorRamps () {
colorRamps.viridis = makeColorRamp ( "viridis" );
colorRamps.jet = makeColorRamp ( "jet" );
colorRamps.blackbody = makeColorRamp ( "blackbody" );
colorRamps.earth = makeColorRamp ( "earth" );
colorRamps.bathymetry = makeColorRamp ( "bathymetry" );
colorRamps.magma = makeColorRamp ( "magma" );
colorRamps.par = makeColorRamp ( "par" );
colorRamps.slope = makeColorRamp ( "RdBu" );
}
makeColorRamps ();
const tmpVec3 = new Vector3 ();
Instance. registerCRS (
"EPSG:3946" ,
"+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 " +
"+y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs" ,
);
const instance = new Instance ({
target: "view" ,
crs: "EPSG:3946" ,
backgroundColor: 0xcccccc ,
});
// Create the 3D tiles entity
const pointcloud = new Tiles3D ({
url: "https://3d.oslandia.com/3dtiles/lyon.3dtiles/tileset.json" ,
colorMap: new ColorMap ({ colors: colorRamps[ "viridis" ], min: 100 , max: 600 }),
errorTarget: 15 ,
});
let colorLayer;
function placeCamera ( position , lookAt ) {
instance.view.camera.position. set (position.x, position.y, position.z);
instance.view.camera. lookAt (lookAt);
// create controls
const controls = new MapControls (instance.view.camera, instance.domElement);
controls.target. copy (lookAt);
controls.enableDamping = true ;
controls.dampingFactor = 0.25 ;
instance.view. setControls (controls);
instance. notifyChange (instance.view.camera);
}
// add pointcloud to scene
function initializeCamera () {
const bbox = pointcloud. getBoundingBox ();
instance.view.camera.far = 2.0 * bbox. getSize (tmpVec3). length ();
const ratio = bbox. getSize (tmpVec3).x / bbox. getSize (tmpVec3).z;
const position = bbox.min
. clone ()
. add (bbox. getSize (tmpVec3). multiply ( new Vector3 ( 0 , 0 , ratio * 0.5 )));
const lookAt = bbox. getCenter (tmpVec3);
lookAt.z = bbox.min.z;
const extent = Extent. fromBox3 ( "EPSG:3946" , bbox);
placeCamera (position, lookAt);
const url =
"https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities" ;
// Let's build the color layer from the WMTS capabilities
WmtsSource. fromCapabilities (url, {
layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS" ,
})
. then (( orthophotoWmts ) => {
colorLayer = new ColorLayer ({
name: "color" ,
extent,
source: orthophotoWmts,
});
pointcloud. setColorLayer (colorLayer);
})
. catch (console.error);
instance.renderingOptions.enableEDL = true ;
instance.renderingOptions.enableInpainting = true ;
instance.renderingOptions.enablePointCloudOcclusion = true ;
pointcloud.pointCloudMode = MODE . TEXTURE ;
}
instance. add (pointcloud). then (initializeCamera);
Inspector. attach ( "inspector" , instance);
instance.domElement. addEventListener ( "dblclick" , ( e ) =>
console. log (
instance. pickObjectsAt (e, {
// Specify a radius around where we click so we don't have to precisely be on a point
// to select it
radius: 5 ,
// Limit the number of results for better performances
limit: 10 ,
// Some points are incoherent in the pointcloud, don't pick them
filter : ( p ) => ! Number. isNaN (p.point.z) && p.point.z < 1000 ,
}),
),
);
instance. notifyChange ();
bindToggle ( "edl-enable" , ( v ) => {
instance.renderingOptions.enableEDL = v;
instance. notifyChange ();
});
bindToggle ( "occlusion-enable" , ( v ) => {
instance.renderingOptions.enablePointCloudOcclusion = v;
instance. notifyChange ();
});
bindToggle ( "inpainting-enable" , ( v ) => {
instance.renderingOptions.enableInpainting = v;
instance. notifyChange ();
});
bindSlider ( "edl-radius" , ( v ) => {
instance.renderingOptions.EDLRadius = v;
instance. notifyChange ();
});
bindSlider ( "edl-intensity" , ( v ) => {
instance.renderingOptions.EDLStrength = v;
instance. notifyChange ();
});
bindSlider ( "inpainting-steps" , ( v ) => {
instance.renderingOptions.inpaintingSteps = v;
instance. notifyChange ();
});
bindSlider ( "opacity" , ( v ) => {
pointcloud.opacity = v;
document. getElementById ( "opacityLabel" ).innerText =
`Point cloud opacity: ${ Math . round ( v * 100 ) }%` ;
instance. notifyChange (pointcloud);
});
bindColorMapBounds (( min , max ) => {
pointcloud.colorMap.min = min;
pointcloud.colorMap.max = max;
instance. notifyChange (pointcloud);
});
const colorMapGroup = document. getElementById ( "colormapGroup" );
bindNumericalDropDown ( "pointcloud_mode" , ( newMode ) => {
pointcloud.pointCloudMode = newMode;
if (newMode === MODE . ELEVATION ) {
colorMapGroup.classList. remove ( "d-none" );
} else {
colorMapGroup.classList. add ( "d-none" );
}
instance. notifyChange (pointcloud);
if (colorLayer) {
colorLayer.visible = newMode === MODE . TEXTURE ;
instance. notifyChange (colorLayer);
}
});
bindDropDown ( "colormap" , ( newRamp ) => {
pointcloud.colorMap.colors = colorRamps[newRamp];
instance. notifyChange (pointcloud);
});