Origin-Destination Globe
Demonstration of the Texture Globe Prefab combined with GreasedLines to render flight paths originating from Atlanta International Airport, based on work by Yang et al. Height encodes the distance between origin and destination airports, and line thickness encodes the number of total number of flights along that route.
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 airports from './data/airports.csv';
import flights from './data/flights-airport.csv';
export function originDestinationGlobe(engine){
//Create an empty Scene
const scene = new BABYLON.Scene(engine);
//Add some lighting
new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 10, -5), 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(0, 2, -3);
camera.wheelPrecision = 20;
camera.minZ = 0;
camera.attachControl(true);
//Filter to flights originating from Atlanta
let filteredFlights = flights.filter(d => d.origin === 'ATL');
//Use the Texture Globe prefab to create a sphere with an OpenLayers map canvas as the texture
const globeRadius = 1;
let textureGlobe = anu.createTextureGlobe('globe', { resolution: new BABYLON.Vector3(5000, 2500), diameter: globeRadius * 2 });
//Define a function to calculate distance in kilometers between two points on Earth from their lat/lon coordinates
function haversine(lat1, lon1, lat2, lon2) {
// Convert degrees to radians
const toRadians = (degrees) => degrees * Math.PI / 180;
// Radius of Earth in kilometers
const R = 6371;
// Differences in latitudes and longitudes
const deltaLat = toRadians(lat2 - lat1);
const deltaLon = toRadians(lon2 - lon1);
// Haversine formula
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// Return distance in kilometers
return R * c;
}
//Define a function to convert a flight in our dataset to an array of Vector3 corresponding to a 3D arc on the globe for that flight
function flightToPoints(d) {
//Find the origin and destination airports in our dataset and get their 3D positions as a Vector3 on the Texture Globe's surface
const originAirport = airports.find(airport => airport.iata === d.origin);
const originPosition = textureGlobe.lonLatToVector3([originAirport.longitude, originAirport.latitude]);
const destinationAirport = airports.find(airport => airport.iata === d.destination);
const destinationPosition = textureGlobe.lonLatToVector3([destinationAirport.longitude, destinationAirport.latitude]);
//Get the midpoint between the origin and destination airports
const midpoint = originPosition.add(destinationPosition).scale(0.5);
//Adjust the height of this midpoint (from the globe's center) based on the distance between origin and destination using the Haversine formula
const direction = midpoint.subtract(textureGlobe.position).normalize();
const height = haversine(originAirport.latitude, originAirport.longitude, destinationAirport.latitude, destinationAirport.longitude);
const midPosition = direction.scale(globeRadius + height * 0.0002); //Arbitrary scaling from kilometers to Babylon scene units
//Create a Bezier curve between these three positions for this flight path, returns a list of Vector3 with the specified number of vertices
return BABYLON.Curve3.CreateQuadraticBezier(originPosition, midPosition, destinationPosition, 20).getPoints();
}
//Create D3 scale to determine the width of each flight path based on the number of flights
let scaleWidth = d3.scaleLinear().domain([0, Math.max(...filteredFlights.map(d => d.count))]).range([0.001, 0.01]);
//Select our globe object as a Selection object which will serve as our CoT
let chart = anu.selectName('globe', scene);
//Create greasedLines as children of our CoT using the flightToPoints function we defined earlier
let trajectories = chart.bind('greasedLine',
{
meshOptions: { points: (d) => flightToPoints(d) }, //Pass in our function that will convert bound data to the required array of Vector3
materialOptions: {
width: (d) => scaleWidth(Number(d.count)), //Scale the greasedLine based on the number of flights from each line's bound data
createAndAssignMaterial: true,
colors: [BABYLON.Color3.Red(), BABYLON.Color3.Green()], //Set red for origin, green for destination
useColors: true,
colorsSampling: BABYLON.Constants.TEXTURE_LINEAR_LINEAR, //Change sampling to cause a gradual color gradient throughout the line
colorDistribution: BABYLON.GreasedLineMeshColorDistribution.COLOR_DISTRIBUTION_NONE, //Use only the two colors we defined rather than generating new values for the remaining vertices
colorDistributionType: BABYLON.GreasedLineMeshColorDistributionType.COLOR_DISTRIBUTION_TYPE_LINE //Distribute these two colors throughout the entire line
}
},
filteredFlights); //Bind our data here, we calculate Vector3s and widths using anonymous functions based on this data rather than precomputing Vector3s and passing them in directly
return scene;
}