earthjs
Version:
D3 Earth JS using SVG, Canvas & THREE js, build with some plugins.
497 lines (451 loc) • 17.8 kB
JavaScript
// http://callumprentice.github.io/apps/flight_stream/index.html
// https://stackoverflow.com/questions/9695687/javascript-converting-colors-numbers-strings-vice-versa
export default (jsonUrl, imgUrl, height=150) => {
/*eslint no-console: 0 */
const _ = {
data: [],
sphereObject: null,
track_lines_object: null,
track_points_object: null,
lightFlow: true,
linewidth: 3,
texture: null,
maxVal: 1,
onHover: {},
onHoverVals: [],
};
const lineScale = d3.scaleLinear().domain([30,2500]).range([0.001, 0.005]);
const PI180 = Math.PI / 180.0;
let colorRange = [d3.rgb('#ff0000'),d3.rgb("#aaffff")];
let min_arc_distance = +Infinity;
let max_arc_distance = -Infinity;
let cur_arc_distance = 0;
let point_spacing = 100;
let point_opacity = 0.8;
let point_speed = 1.0;
let point_cache = [];
let all_tracks = [];
let ttl_num_points = 0;
function generateControlPoints(radius) {
for (let f = 0; f < _.data.length; ++f) {
const start_lat = _.data[f][0];
const start_lng = _.data[f][1];
const end_lat = _.data[f][2];
const end_lng = _.data[f][3];
const value = _.data[f][4];
if (start_lat === end_lat && start_lng === end_lng) {
continue;
}
let points = [];
const spline_control_points = 8;
const max_height = Math.random() * height + 0.05;
for (let i = 0; i < spline_control_points + 1; i++) {
const arc_angle = i * 180.0 / spline_control_points;
const arc_radius = radius + (Math.sin(arc_angle * PI180)) * max_height;
const latlng = lat_lng_inter_point(start_lat, start_lng, end_lat, end_lng, i / spline_control_points);
const pos = xyz_from_lat_lng(latlng.lat, latlng.lng, arc_radius);
points.push(new THREE.Vector3(pos.x, pos.y, pos.z));
}
let point_positions = [];
const spline = new THREE.CatmullRomCurve3(points);
const arc_distance = lat_lng_distance(start_lat, start_lng, end_lat, end_lng, radius);
for (let t = 0; t < arc_distance; t += point_spacing) {
const offset = t / arc_distance;
point_positions.push(spline.getPoint(offset));
}
const arc_distance_miles = (arc_distance / (2 * Math.PI)) * 24901;
if (arc_distance_miles < min_arc_distance) {
min_arc_distance = arc_distance_miles;
}
if (arc_distance_miles > max_arc_distance) {
max_arc_distance = parseInt(Math.ceil(arc_distance_miles / 1000.0) * 1000);
cur_arc_distance = max_arc_distance;
}
const color = value ? _.color(value) : 'rgb(255,255,255)';
const default_speed = Math.random()*600+400;
const speed = default_speed * point_speed;
const num_points = parseInt(arc_distance / point_spacing) + 1;
const spd_points = speed * num_points;
ttl_num_points += num_points;
all_tracks.push({
spline,
num_points,
spd_points,
arc_distance,
arc_distance_miles,
point_positions,
default_speed,
value,
color,
speed
});
}
}
function xyz_from_lat_lng(lat, lng, radius) {
const phi = (90 - lat) * PI180;
const theta = (360 - lng) * PI180;
return {
x: radius * Math.sin(phi) * Math.cos(theta),
y: radius * Math.cos(phi),
z: radius * Math.sin(phi) * Math.sin(theta)
};
}
function lat_lng_distance(lat1, lng1, lat2, lng2, radius) {
const a = Math.sin(((lat2 - lat1) * PI180) / 2) *
Math.sin(((lat2 - lat1) * PI180) / 2) +
Math.cos(lat1 * PI180) *
Math.cos(lat2 * PI180) *
Math.sin(((lng2 - lng1) * PI180) / 2) *
Math.sin(((lng2 - lng1) * PI180) / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}
function lat_lng_inter_point(lat1, lng1, lat2, lng2, offset) {
lat1 = lat1 * PI180;
lng1 = lng1 * PI180;
lat2 = lat2 * PI180;
lng2 = lng2 * PI180;
const d = 2 * Math.asin(Math.sqrt(Math.pow((Math.sin((lat1 - lat2) / 2)), 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin((lng1 - lng2) / 2), 2)));
const A = Math.sin((1 - offset) * d) / Math.sin(d);
const B = Math.sin(offset * d) / Math.sin(d);
const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2);
const y = A * Math.cos(lat1) * Math.sin(lng1) + B * Math.cos(lat2) * Math.sin(lng2);
const z = A * Math.sin(lat1) + B * Math.sin(lat2);
const lat = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))) * 180 / Math.PI;
const lng = Math.atan2(y, x) * 180 / Math.PI;
return {
lat: lat,
lng: lng
};
}
let positions;
function generate_point_cloud() {
positions = new Float32Array(ttl_num_points * 3);
const colors = new Float32Array(ttl_num_points * 3);
const values = new Float32Array(ttl_num_points);
const sizes = new Float32Array(ttl_num_points);
let index = 0;
for (let i = 0; i < all_tracks.length; ++i) {
const {value, color, point_positions} = all_tracks[i];
const {r,g,b} = new THREE.Color(color); //.setHSL(1-value/_.maxVal, 0.4, 0.8);
const pSize = _.point(value || 1);
for (let j = 0; j < point_positions.length; ++j) {
positions[3 * index + 0] = 0;
positions[3 * index + 1] = 0;
positions[3 * index + 2] = 0;
colors[3 * index + 0] = r;
colors[3 * index + 1] = g;
colors[3 * index + 2] = b;
values[index] = value || 1;
sizes[index] = pSize; //_.point_size;
++index;
}
point_cache[i] = [];
}
const point_cloud_geom = new THREE.BufferGeometry();
point_cloud_geom.addAttribute('position', new THREE.BufferAttribute(positions, 3));
point_cloud_geom.addAttribute('customColor', new THREE.BufferAttribute(colors, 3));
point_cloud_geom.addAttribute('value', new THREE.BufferAttribute(values, 1));
point_cloud_geom.addAttribute('size', new THREE.BufferAttribute(sizes, 1));
point_cloud_geom.computeBoundingBox();
_.track_points_object = new THREE.Points(point_cloud_geom, _.shaderMaterial);
_.attr_position = _.track_points_object.geometry.attributes.position;
return _.track_points_object;
}
function update_point_cloud() {
const i_length = all_tracks.length;
const dates = Date.now();
let index = 0;
for (let i= 0; i < i_length; ++i) {
const {
speed,
spline,
num_points,
spd_points,
arc_distance,
arc_distance_miles
} = all_tracks[i];
if (arc_distance_miles <= cur_arc_distance) {
const normalized = point_spacing / arc_distance;
const time_scale = (dates % speed) / spd_points;
for (let j = 0; j < num_points; j++) {
const t = j * normalized + time_scale;
const {x,y,z}= fast_get_spline_point(i, t, spline);
const index3 = 3 * index;
positions[index3 + 0] = x;
positions[index3 + 1] = y;
positions[index3 + 2] = z;
index++;
}
} else {
for (let j = 0; j < num_points; j++) {
const index3 = 3 * index;
positions[index3 + 0] = Infinity;
positions[index3 + 1] = Infinity;
positions[index3 + 2] = Infinity;
index++;
}
}
}
_.attr_position.needsUpdate = true;
}
function fast_get_spline_point(i, t, spline) {
// point_cache set in generate_point_cloud()
const pcache = point_cache[i];
const tc = parseInt(t * 1000);
if (pcache[tc] === undefined) {
pcache[tc] = spline.getPoint(t);
}
return pcache[tc];
}
const line_opacity = 0.4;
const curve_points = 24;
const curve_length = curve_points - 1;
const material = new THREE.LineBasicMaterial({
vertexColors: THREE.VertexColors,
opacity: line_opacity,
transparent: true,
depthWrite: false,
depthTest: true,
color: 0xffffff,
linewidth: _.linewidth
});
function generate_track_lines() {
const {length} = all_tracks;
const total_arr = length * 6 * curve_points;
const geometry = new THREE.BufferGeometry();
const colors = new Float32Array(total_arr);
const line_positions = new Float32Array(total_arr);
for (let i = 0; i < length; ++i) {
const l = i * curve_points;
const {spline, color} = all_tracks[i];
const {r,g,b} = new THREE.Color(color);
for (let j = 0; j < curve_length; ++j) {
const k = j+1;
const c1 = spline.getPoint(j / curve_length);
const c2 = spline.getPoint(k / curve_length);
line_positions[i_curve + 0] = c1.x;
line_positions[i_curve + 1] = c1.y;
line_positions[i_curve + 2] = c1.z;
line_positions[i_curve + 3] = c2.x;
line_positions[i_curve + 4] = c2.y;
line_positions[i_curve + 5] = c2.z;
const i_curve = (j + l) * 6;
colors[i_curve + 0] = r;
colors[i_curve + 1] = g;
colors[i_curve + 2] = b;
colors[i_curve + 3] = r;
colors[i_curve + 4] = g;
colors[i_curve + 5] = b;
}
}
geometry.addAttribute('position', new THREE.BufferAttribute(line_positions, 3));
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.computeBoundingSphere();
_.track_lines_object = new THREE.Line(geometry, material, THREE.LineSegments);
return _.track_lines_object;
}
// const resolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
function generate_track_lines2() {
const {length}= all_tracks;
const group = new THREE.Group();
const lineWidth = lineScale(_.SCALE); // this._.proj.scale()
for (let i = 0; i < length; ++i) {
const {spline, color} = all_tracks[i];
const lines = new Float32Array(3 * curve_points);
const material = new MeshLineMaterial({
color: new THREE.Color(color),
useMap: false,
opacity: 1,
lineWidth,
});
for (let j = 0; j <= curve_length; ++j) {
const i_curve = j * 3;
const {x,y,z} = spline.getPoint(j / curve_length);
lines[i_curve + 0] = x;
lines[i_curve + 1] = y;
lines[i_curve + 2] = z;
}
let meshLine = new MeshLine();
meshLine.setGeometry(lines);
group.add(new THREE.Mesh(meshLine.geometry, material));
}
_.track_lines_object = group;
return _.track_lines_object;
}
const vertexshader = `
attribute float size;
attribute vec3 customColor;
varying vec3 vColor;
void main() {
vColor = customColor;
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_PointSize = size * ( 300.0 / length( mvPosition.xyz ) );
gl_Position = projectionMatrix * mvPosition;
}`;
const fragmentshader = `
uniform vec3 color;
uniform sampler2D texture;
uniform float opacity;
varying vec3 vColor;
void main() {
gl_FragColor = vec4( color * vColor, opacity );
gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord );
}`;
function loadFlights() {
const uniforms = {
color: {
type: "c",
value: new THREE.Color(0xaaaaaa)
},
texture: {
type: "t",
value: _.texture
},
opacity: {
type: "f",
value: point_opacity
}
};
_.shaderMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexshader,
fragmentShader: fragmentshader,
blending: THREE.AdditiveBlending,
depthTest: true,
depthWrite: false,
transparent: true,
});
const group = new THREE.Group();
generateControlPoints(_.SCALE+1);
group.add(!window.MeshLineMaterial ?
generate_track_lines.call(this) :
generate_track_lines2.call(this));
group.add(generate_point_cloud.call(this));
group.name = 'flightLineThreejs';
if (this.threejsPlugin.domEvents) {
this.threejsPlugin.domEvents.addEventListener(_.track_lines_object, 'mousemove', function(event){
for (let v of _.onHoverVals) {
v.call(event.target, event);
}
}, false);
}
_.sphereObject = group;
_.sphereObject.name = _.me.name;
_.loaded = true;
}
function init() {
_.SCALE = this._.proj.scale() + (this.__plugins('3d').length>0 ? 4 : 0);
_.texture = this.threejsPlugin.texture(imgUrl);
}
function create() {
if (_.texture && !_.sphereObject && !_.loaded) {
loadFlights.call(this);
}
const tj = this.threejsPlugin;
tj.addGroup(_.sphereObject);
}
function reload() {
all_tracks = [];
point_cache = [];
loadFlights.call(this);
const tj = this.threejsPlugin;
const arr = tj.group.children;
const idx = arr.findIndex(obj=>obj.name==='flightLineThreejs');
tj.group.remove(arr[idx]);
tj.group.add(_.sphereObject);
tj.renderThree();
}
let start = 0;
function interval(timestamp) {
if ((timestamp - start)>30) {
start = timestamp;
update_point_cloud();
this.threejsPlugin.renderThree();
}
}
function resize() {
const ps = this._.proj.scale();
const sc = _.resize(ps);
const pt = _.sphereObject.children[1];
const {size,value} = pt.geometry.attributes;
size.array = value.array.map((v)=>_.point(v)*sc);
size.needsUpdate = true;
if (window.MeshLineMaterial) {
_.track_lines_object.children.forEach(mesh=>{
mesh.material.uniforms.lineWidth.value = lineScale(ps);
mesh.material.needsUpdate = true;
})
}
}
return {
name: 'flightLineThreejs',
urls: jsonUrl && [jsonUrl],
onReady(err, data) {
_.me.data(data);
},
onInit(me) {
_.me = me;
init.call(this);
this._.options.showFlightLine = true;
},
onResize() {
resize.call(this);
},
onInterval(t) {
if (!this._.drag && _.lightFlow)
interval.call(this, t);
},
onCreate() {
create.call(this);
},
onHover(obj) {
Object.assign(_.onHover, obj);
_.onHoverVals = Object.keys(_.onHover).map(k => _.onHover[k]);
},
reload() {
reload.call(this);
},
data(data, colorR, pointR=[50,500], h=150, o=0.8) {
if (data) {
_.data = data;
if (colorR) {
if (!Array.isArray(colorR)) {
colorR = ['#ff0000','#aaffff'];
}
const d = d3.extent(data.map(x=>x[4]));
colorRange = [d3.rgb(colorR[0]),d3.rgb(colorR[1])];
_.color = d3.scaleLinear().domain(d).interpolate(d3.interpolateHcl).range(colorRange);
_.point = d3.scaleLinear().domain(d).range(pointR);
_.maxVal= d[1];
} else {
_.color = () => 'rgb(255, 255, 255)';
_.point = () => 150;
_.maxVal= 1;
}
height = h;
point_opacity = o;
_.resize= d3.scaleLinear().domain([30,this._.proj.scale()]).range([0.1, 1]);
} else {
return _.data;
}
},
sphere() {
return _.sphereObject;
},
pointSize(one) {
const pt = _.sphereObject.children[1];
const {size} = pt.geometry.attributes;
size.array = size.array.map((v)=>v*one);
size.needsUpdate = true;
},
lightFlow(forceState) {
if (forceState!==undefined) {
_.lightFlow = forceState;
} else {
return _.lightFlow;
}
}
}
}