Origin-Destination Globe
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.
import { Vector3, Scene, HemisphericLight, ArcRotateCamera, Color3, Curve3, GreasedLineMeshColorDistributionType, Constants } from '@babylonjs/core';
import * as anu from '@jpmorganchase/anu';
import * as d3 from 'd3';
import airports from './data/airports.csv';
import flights from './data/flights-airport.csv';
export function originDestinationGlobe(engine){
//Babylon boilerplate
const scene = new Scene(engine);
const light = new HemisphericLight('light1', new Vector3(0, 10, -10), scene)
const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
camera.wheelPrecision = 20;
camera.minZ = 0;
camera.attachControl(true);
camera.position = new Vector3(0, 2, -3)
//Filter to flights originating from Atlanta
let filteredFlights = flights.filter(d => d.origin === "ATL");
//Create a Texture Globe
const globeRadius = 1;
let textureGlobe = anu.createTextureGlobe('globe', { resolution: new Vector3(5000, 2500), diameter: globeRadius * 2 }, scene);
//Function to calculate distance between points on Earth using 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;
}
//Function to convert a flight in our dataset to an array of Vector3 correpsonding to a 3D arc on the globe for that flight
function flightToPoints(d) {
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 middle position that corresponds to the top of the curve
const midpoint = originPosition.add(destinationPosition).scale(0.5);
const direction = midpoint.subtract(textureGlobe.position).normalize();
//Here we base the height on the distance between the origin and destination as the crow flies using the Haversine formula
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
return Curve3.CreateQuadraticBezier(originPosition, midPosition, destinationPosition, 20).getPoints(); //Returns a list of Vector3
}
//Create D3 scale to determine the width of the trajectories
let scaleWidth = d3.scaleLinear().domain([0, Math.max(...filteredFlights.map(d => d.count))]).range([0.001, 0.01]);
//Create our globe and trajectories
let CoT = anu.create("cot", "globe");
let globe = anu.selectName("globe", scene);
let trajectories = globe.bind('greasedLine', { points: (d,n,i) => flightToPoints(d) }, filteredFlights) //Because we have multiple trajectories, we pass in an arrow function to convert each flight to an array of Vector3 that corresponds to the 3D arc
.run((d,n,i) => { //Material properties of the greasedLine need to be set separately for now in .run()
n.greasedLineMaterial.width = scaleWidth(Number(d.count)); //Scale the greasedLine based on the number of flights
n.greasedLineMaterial.useColors = true;
n.greasedLineMaterial.colorsSampling = Constants.TEXTURE_LINEAR_LINEAR; //Change sampling to cause a gradual color gradient throughout the line
n.greasedLineMaterial.colorsDistributionType = GreasedLineMeshColorDistributionType.COLOR_DISTRIBUTION_TYPE_LINE; //Make the colors not repeat throughout the line since we are only going to set 2 colors
n.greasedLineMaterial.colors = [Color3.Red(), Color3.Green()]; //Set red for origin, green for destination. This needs to be set after setting sampling and distribution type
})
return scene;
}