Fly a drone over a GeoJSON line
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import {
Color,
CubicInterpolant,
DirectionalLight,
HemisphereLight,
Vector3,
} from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.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 Map from "@giro3d/giro3d/entities/Map.js";
import Shape from "@giro3d/giro3d/entities/Shape";
import BilFormat from "@giro3d/giro3d/formats/BilFormat.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.js";
const epsg2154 = 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(
"urn:ogc:def:crs:OGC:1.3:CRS84",
"+proj=longlat +datum=WGS84 +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: epsg2154,
backgroundColor: null,
});
const center = new Coordinates(CoordinateSystem.epsg4326, 6.636, 45.935).as(
instance.coordinateSystem,
);
const extent = Extent.fromCenterAndSize(
epsg2154,
{ x: center.x, y: center.y },
20_000,
20_000,
);
const map = new Map({
extent,
backgroundColor: "gray",
lighting: {
enabled: true,
hillshadeIntensity: 0.6,
elevationLayersOnly: true,
},
});
instance.add(map);
const capabilitiesUrl =
"https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities";
WmtsSource.fromCapabilities(capabilitiesUrl, {
layer: "ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES",
format: new BilFormat(),
noDataValue: -1000,
})
.then((source) => {
map.addLayer(
new ElevationLayer({
extent: map.extent,
resolutionFactor: 1 / 8,
source: source,
}),
);
})
.catch(console.error);
WmtsSource.fromCapabilities(capabilitiesUrl, {
layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS",
})
.then((source) => {
map.addLayer(
new ColorLayer({
extent: map.extent,
source: source,
}),
);
})
.catch(console.error);
const camera = instance.view.camera;
Inspector.attach("inspector", instance);
// https://airbornescience.nasa.gov/content/3D_Models_Gallery
// Non-promotional commercial use:
// https://www.nasa.gov/nasa-brand-center/images-and-media/
const path = "https://3d.oslandia.com/giro3d/gltf/G3_JSC_UAVSAR_AIR_0824.glb";
const loader = new GLTFLoader();
loader.load(path, (gltf) => {
const airplane = gltf.scene;
airplane.scale.set(100, 100, 100);
airplane.rotation.x = Math.PI / 2;
airplane.rotation.y = Math.PI;
airplane.updateMatrixWorld();
instance.add(airplane);
const hemiLight = new HemisphereLight(0xffffff, 0x444444, 3);
const sun = new DirectionalLight();
instance.scene.add(sun);
instance.scene.add(sun.target);
sun.target.position.set(0, 0, 0);
sun.position.set(1000, 1000, 1000);
sun.updateMatrixWorld(true);
hemiLight.position.set(0, 200, 0);
instance.scene.add(hemiLight);
instance.scene.background = new Color("#2ddaed");
const cameraPosition = new Coordinates(
CoordinateSystem.epsg4326,
6.63125,
45.93506,
).as(instance.coordinateSystem);
camera.position.set(cameraPosition.x, cameraPosition.y, 1600);
fetch("data/drone_path.geojson")
.then((response) => response.json())
.then((json) => {
const coordinates = new Coordinates(CoordinateSystem.epsg4326, 0, 0, 0);
const reprojected = new Coordinates(instance.coordinateSystem, 0, 0, 0);
const points = json.geometry.coordinates.map((c) => {
// Reproject points once and for all in target coordinate system
coordinates.set(CoordinateSystem.epsg4326, c[0], c[1]);
coordinates.as(instance.coordinateSystem, reprojected);
return [reprojected.x, reprojected.y];
});
const parameterPositions = new Float32Array(points.length);
const sampleValues = new Float32Array(points.length * 2);
const shapePoints = Array.from({ length: 1000 });
const ANIMATION_DURATION_S = 50.0;
const AIRPLANE_ALTITUDE = 1500.0;
for (let i = 0; i < points.length; i++) {
// We'll interpolate with constant time between each values from the path
parameterPositions[i] = i / (points.length - 1);
const x = points[i][0];
const y = points[i][1];
sampleValues[i * 2 + 0] = x;
sampleValues[i * 2 + 1] = y;
}
const interpolant = new CubicInterpolant(
parameterPositions,
sampleValues,
2,
);
for (let i = 0; i <= 1000; i += 1) {
const t = i / 1000;
const [x, y] = interpolant.evaluate(t);
shapePoints[i] = new Vector3(x, y, AIRPLANE_ALTITUDE);
}
let oldTime = 0.0;
let animationAlpha = 0.0;
// Let's represent the flight path with a Shape entity.
const shape = new Shape({ showVertices: false, lineWidth: 3 });
shape.setPoints(shapePoints);
// points.map(([x, y]) => new Vector3(x, y, AIRPLANE_ALTITUDE - 5)));
shape.color = "yellow";
shape.depthTest = true;
instance.add(shape);
const lookAt = new Vector3();
const animate = (time) => {
const frameTime = (time - oldTime) / 1000.0;
oldTime = time;
// Move the airplane
const position = interpolant.evaluate(animationAlpha);
airplane.position.set(position[0], position[1], AIRPLANE_ALTITUDE);
// Set the heading
// note: evaluate returns a reference, so at this point `position` is outdated
const nextPosition = interpolant.evaluate((animationAlpha + 0.01) % 1);
lookAt.set(nextPosition[0], nextPosition[1], AIRPLANE_ALTITUDE);
airplane.lookAt(lookAt);
airplane.updateMatrixWorld();
// Move the camera
camera.lookAt(airplane.position);
instance.notifyChange(camera);
instance.notifyChange(airplane);
animationAlpha =
(animationAlpha + frameTime / ANIMATION_DURATION_S) % 1;
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
});
});
<!doctype html>
<html lang="en">
<head>
<title>Drone Flight</title>
<meta charset="UTF-8" />
<meta name="name" content="drone" />
<meta name="description" content="Fly a drone over a GeoJSON line" />
<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>
<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": "drone",
"dependencies": {
"@giro3d/giro3d": "3.0.0-dev"
},
"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',
},
},
})