3D Trajectory on a Map
Demonstration of the Texture Map Prefab combined with a GreasedLine to render a drone flight path, based on a sample log from Data Comets by Saffo et al. For a better visualization, you can replace the URL of the XYZ tile provider to one that provides satellite images.
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.
import * as anu from '@jpmorganchase/anu';
import * as BABYLON from '@babylonjs/core';
import * as d3 from 'd3';
import data from './data/drone-path.json';
import { XYZ } from 'ol/source';
import TileLayer from 'ol/layer/Tile';
export function trajectory3D(engine){
//Create an empty Scene
const scene = new BABYLON.Scene(engine);
//Add some lighting
new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 10, -10), scene);
//Add a camera that rotates around the origin and adjust its properties
const camera = new BABYLON.ArcRotateCamera('Camera', 0, 0, 0, new BABYLON.Vector3(0, 0, 0), scene);
camera.position = new BABYLON.Vector3(2, 10, -15);
camera.wheelPrecision = 20;
camera.minZ = 0;
camera.attachControl(true);
//Use the Texture Map prefab to create a plane with an OpenLayers map canvas as the texture
let textureMap = anu.createTextureMap('map',
{
layers: [new TileLayer({
source: new XYZ({
crossOrigin: 'anonymous', //Required
urls: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'] //Update the urls array with an XYZ tile provider of your choice
})
})],
mapHeight: 1000,
mapWidth: 1000
});
//Get the OpenLayers map object from the prefab which we will need to customize its settings
let map = textureMap.map;
//Get the center lon/lat of our dataset
let center = [
(Math.min(...data.map(d => d.longitude)) + Math.max(...data.map(d => d.longitude))) / 2,
(Math.min(...data.map(d => d.latitude)) + Math.max(...data.map(d => d.latitude))) / 2
];
//Change the view parameters of the map to focus on our calculated center
map.getView().setCenter(center);
map.getView().setZoom(17);
//To help create our trajectory, the Texture Map prefab generates scale functions for us to convert lon/lat to positions in Babylon's coordinate space
let scaleLon = textureMap.scaleLon;
let scaleLat = textureMap.scaleLat;
//Create a D3 scale for altitude, our dataset uses sea level so here we'll treat the minimum altitude value as ground level
let scaleAlt = d3.scaleLinear().domain([Math.min(...data.map(d => d.altitude)), Math.max(...data.map(d => d.altitude))]).range([0, 2]);
//Create a D3 scale for color, using Anu helper functions map scale outputs to Color3 objects based on the 'interpolateOrRd' palette from D3
let scaleC = d3.scaleSequential(anu.sequentialChromatic('OrRd').toColor3()).domain([0,Math.max(...data.map(d => d.velocity))]);
//Select our map object as a Selection object which will serve as our CoT
let chart = anu.selectName('map', scene);
//We want to render our trajectory after the map has finished loading, use OpenLayers' callback functions for this
//N.B: Texture Map's scale functions are only created after the map is fully rendered, so we need to use this callback regardless
map.once('postrender', () => {
//Create arrays for our flight path using our scales to convert data values to Vector3 coordinates and Color3 objects
let flightPath = data.map(d => new BABYLON.Vector3(scaleLon([d.longitude, d.latitude]), scaleAlt(d.altitude), scaleLat([d.longitude, d.latitude])));
let flightColors = data.map(d => scaleC(d.velocity));
//Create a greasedLine as a child of our CoT using the flight path and colors we just calculated
let trajectories = chart.bind('greasedLine',
{
meshOptions: { points: flightPath },
materialOptions: { useColors: true, colors: flightColors }
});
});
return scene;
}