Illustrates the DrapedFeatureCollection entity.
The DrapedFeatureCollection entities loads vector features and display them on top of the terrain.
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { AmbientLight, DirectionalLight, DoubleSide, Vector3 } from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
import Coordinates from "@giro3d/giro3d/core/geographic/Coordinates.js";
import CoordinateSystem from "@giro3d/giro3d/core/geographic/CoordinateSystem.js";
import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import DrapedFeatureCollection from "@giro3d/giro3d/entities/DrapedFeatureCollection.js";
import Giro3dMap from "@giro3d/giro3d/entities/Map.js";
import BilFormat from "@giro3d/giro3d/formats/BilFormat.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import FileFeatureSource from "@giro3d/giro3d/sources/FileFeatureSource.js";
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.js";
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
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) => {
element.innerHTML = "";
options.forEach((opt) => {
const optElement = document.createElement("option");
optElement.value = opt.id;
optElement.selected = opt.selected;
optElement.textContent = opt.name;
element.appendChild(optElement);
});
};
return [callback, element.value, element, setOptions];
}
const crs = CoordinateSystem.register(
"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",
);
CoordinateSystem.register(
"IGNF:WGS84G",
'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]',
);
const instance = new Instance({
target: "view",
crs,
backgroundColor: null,
});
const mapCenter = new Coordinates(crs, 870_623, 6_396_742);
const extent = Extent.fromCenterAndSize(crs, mapCenter, 20_000, 10_000);
// create a map
const map = new Giro3dMap({
extent,
backgroundColor: "#304f66",
lighting: {
enabled: true,
elevationLayersOnly: true,
},
side: DoubleSide,
});
instance.add(map);
const noDataValue = -1000;
const url =
"https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities";
const featureSource = new FileFeatureSource({
url: "https://3d.oslandia.com/giro3d/vectors/Saou-syncline.geojson",
});
const style = {
stroke: {
color: "yellow",
depthTest: false,
renderOrder: 999,
lineWidth: 3,
},
};
const entities = {
"per-vertex": new DrapedFeatureCollection({
source: featureSource,
minLod: 0,
drapingMode: "per-vertex",
style,
}),
"per-feature": new DrapedFeatureCollection({
source: featureSource,
minLod: 0,
drapingMode: "per-feature",
style,
}),
none: new DrapedFeatureCollection({
source: featureSource,
minLod: 0,
drapingMode: "none",
style,
}),
};
function updateEntities(newMode) {
for (const key of Object.keys(entities)) {
entities[key].visible = newMode === key;
}
instance.notifyChange();
}
const [_, currentMode] = bindDropDown("mode", updateEntities);
function loadDrapedFeatures() {
entities["per-feature"].visible = false;
entities["per-vertex"].visible = false;
entities["none"].visible = false;
instance.add(entities["per-vertex"]).then(() => {
entities["per-vertex"].attach(map);
});
instance.add(entities["none"]).then(() => {
entities["none"].attach(map);
});
instance.add(entities["per-feature"]).then(() => {
entities["per-feature"].attach(map);
});
updateEntities(currentMode);
}
loadDrapedFeatures();
let elevationLayer = null;
// Let's build the elevation layer from the WMTS capabilities
WmtsSource.fromCapabilities(url, {
layer: "ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES",
format: new BilFormat(),
noDataValue,
})
.then((elevationWmts) => {
elevationLayer = new ElevationLayer({
name: "elevation",
extent: map.extent,
// We don't need the full resolution of terrain
// because we are not using any shading. This will save a lot of memory
// and make the terrain faster to load.
resolutionFactor: 1 / 2,
minmax: { min: 0, max: 5000 },
noDataOptions: {
replaceNoData: false,
},
source: elevationWmts,
});
map.addLayer(elevationLayer);
})
.catch(console.error);
// Let's build the color layer from the WMTS capabilities
WmtsSource.fromCapabilities(url, {
layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS",
})
.then((orthophotoWmts) => {
map.addLayer(
new ColorLayer({
name: "color",
resolutionFactor: 1,
extent: map.extent,
source: orthophotoWmts,
}),
);
})
.catch(console.error);
// Add a sunlight
const sun = new DirectionalLight("#ffffff", 2);
sun.position.set(1, 0, 1).normalize();
sun.updateMatrixWorld(true);
instance.scene.add(sun);
// We can look below the floor, so let's light also a bit there
const sun2 = new DirectionalLight("#ffffff", 0.5);
sun2.position.set(0, 1, 1);
sun2.updateMatrixWorld();
instance.scene.add(sun2);
// Add an ambient light
const ambientLight = new AmbientLight(0xffffff, 0.2);
instance.scene.add(ambientLight);
instance.view.camera.position.set(
mapCenter.x - 10000,
mapCenter.y - 4000,
2000,
);
const lookAt = new Vector3(mapCenter.x, mapCenter.y, 100);
instance.view.camera.lookAt(lookAt);
const controls = new MapControls(instance.view.camera, instance.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.4;
controls.target.copy(lookAt);
controls.saveState();
instance.view.setControls(controls);
Inspector.attach("inspector", instance);
bindDropDown("elevationMode", (newMode) => {
if (elevationLayer == null) {
return;
}
const currentLayers = map.getElevationLayers();
switch (newMode) {
case "enabled":
if (!currentLayers.includes(elevationLayer)) {
map.addLayer(elevationLayer);
}
elevationLayer.visible = true;
break;
case "hidden":
if (!currentLayers.includes(elevationLayer)) {
map.addLayer(elevationLayer);
}
elevationLayer.visible = false;
break;
case "disabled":
if (currentLayers.includes(elevationLayer)) {
map.removeLayer(elevationLayer);
}
break;
}
instance.notifyChange(map);
});
<!doctype html>
<html lang="en">
<head>
<title>Draped feature collection</title>
<meta charset="UTF-8" />
<meta name="name" content="draped-feature-collection" />
<meta
name="description"
content="Illustrates the <code>DrapedFeatureCollection</code> entity."
/>
<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/latest/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: 30rem">
>
<div class="card">
<div class="card-header">Parameters</div>
<div class="card-body">
<fieldset id="options">
<!-- Elevation layer -->
<div class="input-group mb-2">
<label class="input-group-text" for="elevationMode"
>Elevation layer</label
>
<select class="form-select" id="elevationMode" autocomplete="off">
<option value="enabled" selected>Present</option>
<option value="hidden">Present, but not active</option>
<option value="disabled">Absent</option>
</select>
</div>
<!-- Draping mode -->
<div class="input-group mb-2">
<label class="input-group-text" for="mode">Draping mode</label>
<select class="form-select" id="mode" autocomplete="off">
<option value="per-vertex" selected>per-vertex</option>
<option value="per-feature">per-feature</option>
<option value="none">none</option>
</select>
</div>
<div class="card-text" id="help">
<b>Draping mode</b> indicates how the geometry is deformed to
conform to the terrain.
<ul>
<li>
<code>per-feature</code> means that the feature's center is
clamped to the ground, preserving the geometry shape. Suitable
for flat geometries such as lakes.
</li>
<li>
<code>per-vertex</code> means that every vertex is clamped to
the ground, deforming the geometry. Suitable for features that
should conform to the terrain, such as roads.
</li>
<li>
<code>none</code> disables terrain conformation and preserves
the original geometry. For 2D geometries, their elevation
remains at zero. For 3D geometries, the Z coordinate of each
vertex is preserved.
</li>
</ul>
</div>
</fieldset>
</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>
{
"name": "draped-feature-collection",
"dependencies": {
"@giro3d/giro3d": "2.0.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
},
},
})