852 lines
29 KiB
JavaScript
852 lines
29 KiB
JavaScript
|
|
/*
|
|
jquery.earth3d.js
|
|
|
|
jQuery ui plugin that allow you to draw a beautiful 3d spinning earth on canvas
|
|
|
|
Author: Sebastien Drouyer
|
|
|
|
Based on the amazing sphere.js plug of Sam Hasler
|
|
|
|
Licensed under the MIT license (MIT-LICENSE.txt)
|
|
|
|
http://sdrdis.github.com/jquery.earth-3d/
|
|
|
|
|
|
|
|
Depends:
|
|
ui.core.js
|
|
|
|
|
|
|
|
|
|
Options:
|
|
* texture: texture map used by the planet
|
|
|
|
* sphere: rotation and size of the planet
|
|
|
|
* defaultSpeed: default spinning speed of the planet
|
|
|
|
* backToDefaultTime: time (in ms) to return by to default speed when planet is dragged
|
|
|
|
* locations: locations to display on the planet:
|
|
* Each position must have a key, an alpha and delta position (or x and y if you want to display a static location).
|
|
Any additional key can be reached via callbacks functions
|
|
Example:
|
|
{
|
|
obj1: {
|
|
alpha: Math.PI / 4,
|
|
delta: 0,
|
|
name: 'location 1'
|
|
}
|
|
}
|
|
|
|
* paths: paths and flights to display over the planet:
|
|
Each path must have a key, an origin and a destination. The values are the location's key.
|
|
You can, if you want to, define flights on these paths.
|
|
Each flight has a key, a destination (the location's key) and a position.
|
|
The position is the progress a fleet has made on its path.
|
|
Any additional key can be reach via callbacks functions.
|
|
Example:
|
|
{
|
|
path: {
|
|
origin: 'obj1',
|
|
destination: 'obj2',
|
|
flights: {
|
|
flight: {
|
|
position: 0.25,
|
|
destination: 'obj2',
|
|
name: 'Flight 1'
|
|
},
|
|
flight2: {
|
|
position: 0.25,
|
|
destination: 'obj1',
|
|
name: 'Flight 2'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
* flightsCanvas: Dom element which is a canvas and where the flights and paths are drawn
|
|
|
|
* dragElement: Dom element where we catch the mouse drag
|
|
|
|
* locationsElement: Dom elements where the locations are drawn
|
|
|
|
* flightsCanvasPosition: position of the flight canvas (can be use if you have some gap between your planet and your flights
|
|
|
|
* pixelRadiusMultiplier: (TEMPORARY) used by the getSphereRadiusInPixel (see the functions)
|
|
|
|
* onInitLocation: callback function which allows you to define what to do when the locations are initialized
|
|
* Parameters:
|
|
* location: location (coming from locations option)
|
|
* widget: earth3d widget object
|
|
|
|
* onShowLocation: callback function which allows you to define what to do when a location becomes visible (was behind the planet and is now in front of it)
|
|
* Parameters:
|
|
* location: location (coming from locations option)
|
|
* x: 2d left position
|
|
* y: 2d top position
|
|
* widget: earth3d widget object
|
|
|
|
* onRefreshLocation: callback function which allows you to define what to do when a location is refreshed (it moves)
|
|
* Parameters:
|
|
* location: location (coming from locations option)
|
|
* x: 2d left position
|
|
* y: 2d top position
|
|
* widget: earth3d widget object
|
|
|
|
* onHideLocation: callback function which allows you to define what to do when a location becomes invisible (was in front of the planet and is now behind it)
|
|
* Parameters:
|
|
* location: location (coming from locations option)
|
|
* x: 2d left position
|
|
* y: 2d top position
|
|
* widget: earth3d widget object
|
|
|
|
* onInitFlight: callback function which allows you to define what to do when the flights are initialized
|
|
* Parameters:
|
|
* flight: flight (coming from flights option)
|
|
* widget: earth3d widget object
|
|
|
|
* onShowFlight: callback function which allows you to define what to do when a flight becomes visible (was behind the planet and is now in front of it)
|
|
* Parameters:
|
|
* flight: flight (coming from flights option)
|
|
* widget: earth3d widget object
|
|
|
|
* onRefreshFlight: callback function which allows you to define what to do when a flight is refreshed (it moves)
|
|
* Parameters:
|
|
* flight: flight (coming from flights option)
|
|
* x: 2d left position
|
|
* y: 2d top position
|
|
* widget: earth3d widget object
|
|
|
|
* onHideFlight: callback function which allows you to define what to do when a flight becomes invisible (was in front of the planet and is now behind it)
|
|
* Parameters:
|
|
* flight: flight (coming from flights option)
|
|
* widget: earth3d widget object
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Functions
|
|
|
|
* getSphereRadiusInPixel: function which allows you to get the sphere radius in pixel
|
|
/!| WARNING: this function needs to be refactored, since I didn't find out (my maths courses are far away) how to
|
|
get the exact value. I did a basic linear regression, but it is not exact, and you will have to change the pixelRadiusMultiplier
|
|
option to get the correct value
|
|
|
|
* destroy: use this function when you want to destroy the object. It will throw a cancel animation frame, so the
|
|
CPU won't be used anymore.
|
|
|
|
* changePaths: use this function when you want to update paths and flights (options on widget)
|
|
it will add the callback functions support
|
|
|
|
*/
|
|
var earth3d;
|
|
(function($) {
|
|
$.widget('ui.earth3d', {
|
|
options: {
|
|
texture: '/ipfs/QmRpS3HcsawVGL6D3ZjJwP5HBFivydb4mcMNVykKem3jai/earth1024x1024.jpg',
|
|
sphere: {
|
|
tilt: 0,
|
|
turn: 0,
|
|
r: 10
|
|
},
|
|
defaultSpeed: 20,
|
|
backToDefaultTime: 4000,
|
|
locations: {
|
|
},
|
|
paths: {
|
|
},
|
|
flightsCanvas: null,
|
|
dragElement: null,
|
|
locationsElement: null,
|
|
flightsCanvasPosition: {
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
tiling: {horizontal: 1, vertical: 1},
|
|
pixelRadiusMultiplier: 0.97,
|
|
onInitLocation: function(location, widget) {
|
|
var $elem = $('<div class="location"></div>');
|
|
$elem.appendTo(widget.options.locationsElement);
|
|
$elem.click(function() {
|
|
alert('Clicked on ' + location.name + ' : ' + location.link );
|
|
window.open( location.link, "AstroTab");
|
|
});
|
|
location.$element = $elem;
|
|
},
|
|
onShowLocation: function(location, x, y) {
|
|
location.$element.show();
|
|
},
|
|
onRefreshLocation: function(location, x, y) {
|
|
//console.log(x, y);
|
|
location.$element.css({
|
|
left: x,
|
|
top: y
|
|
});
|
|
},
|
|
onHideLocation: function(location, x, y) {
|
|
location.$element.hide();
|
|
},
|
|
onDeleteLocation: function(location) {
|
|
location.$element.remove();
|
|
},
|
|
onInitFlight: function(flight, widget) {
|
|
var $elem = $('<div class="flight"></div>');
|
|
$elem.appendTo(widget.options.locationsElement);
|
|
$elem.click(function() {
|
|
alert('Clicked on ' + flight.name);
|
|
|
|
});
|
|
flight.$element = $elem;
|
|
},
|
|
onShowFlight: function(flight) {
|
|
flight.$element.show();
|
|
},
|
|
onRefreshFlight: function(flight, x, y, angle, widget) {
|
|
flight.$element.css({
|
|
left: x,
|
|
top: y,
|
|
'-webkit-transform':'rotate(' + ((angle + Math.PI / 2) * 360 / (2 * Math.PI)) + 'deg)',
|
|
'-moz-transform':'rotate(' + ((angle + Math.PI / 2) * 360 / (2 * Math.PI)) + 'deg)',
|
|
'-o-transform':'rotate(' + ((angle + Math.PI / 2) * 360 / (2 * Math.PI)) + 'deg)'
|
|
});
|
|
},
|
|
onHideFlight: function(flight) {
|
|
flight.$element.hide();
|
|
},
|
|
onDeleteFlight: function(flight) {
|
|
flight.$element.remove();
|
|
}
|
|
},
|
|
earth: null,
|
|
posVar: 24 * 3600 * 1000,
|
|
lastMousePos: null,
|
|
lastSpeed: null,
|
|
lastTime: null,
|
|
lastTurnByTime: null,
|
|
textureWidth: null,
|
|
textureHeight: null,
|
|
obj: null,
|
|
flightsCtx: null,
|
|
renderAnimationFrameId: null,
|
|
mousePressed: null,
|
|
|
|
_create: function() {
|
|
earth3d = this;
|
|
var self = this;
|
|
this.obj = $('div');
|
|
if (this.options.flightsCanvas !== null) {
|
|
this.flightsCtx = this.options.flightsCanvas[0].getContext('2d');
|
|
}
|
|
createSphere(this.element[0], this.options.texture, function(earth, textureWidth, textureHeight) { self._onSphereCreated(earth, textureWidth, textureHeight); }, this.options.tiling);
|
|
if (this.options.dragElement !== null) {
|
|
this.options.dragElement
|
|
.bind('mousedown vmousedown', function(e) {
|
|
self._mouseDragStart(e);
|
|
self.mousePressed = true;
|
|
})
|
|
.bind('mouseup vmouseup', function(e) {
|
|
self._mouseDragStop(e);
|
|
self.mousePressed = false;
|
|
})
|
|
.bind('mousemove vmousemove', function(e){
|
|
if (self.mousePressed) {
|
|
self._mouseDrag(e);
|
|
}
|
|
});
|
|
}
|
|
this._initLocations();
|
|
this._initFlights();
|
|
},
|
|
|
|
_initLocations: function() {
|
|
for (var key in this.options.locations) {
|
|
var location = this.options.locations[key];
|
|
location.visible = true;
|
|
this.options.onInitLocation(location, this);
|
|
}
|
|
},
|
|
|
|
_initFlights: function() {
|
|
for (var key in this.options.paths) {
|
|
var path = this.options.paths[key];
|
|
for (var key in path.flights) {
|
|
path.flights[key].visible = true;
|
|
this.options.onInitFlight(path.flights[key], this);
|
|
}
|
|
}
|
|
},
|
|
|
|
getSphereRadiusInPixel: function() {
|
|
return this.earth.getRadius() / 2;
|
|
},
|
|
|
|
_onSphereCreated: function(earth, textureWidth, textureHeight) {
|
|
var self = this;
|
|
this.textureWidth = textureWidth;
|
|
this.textureHeight = textureHeight;
|
|
this.earth = earth;
|
|
this.earth.init(this.options.sphere);
|
|
this.earth.turnBy = function(time) { return self._turnBy(time); };
|
|
|
|
var renderAnimationFrame = function(/* time */ time) {
|
|
/* time ~= +new Date // the unix time */
|
|
earth.renderFrame(time);
|
|
self._renderAnimationFrame(time);
|
|
self.renderAnimationFrameId = window.requestAnimationFrame(renderAnimationFrame);
|
|
};
|
|
this.renderAnimationFrameId = window.requestAnimationFrame(renderAnimationFrame);
|
|
},
|
|
|
|
destroy: function() {
|
|
window.cancelAnimationFrame(this.renderAnimationFrameId);
|
|
},
|
|
|
|
_renderAnimationFrame: function(time) {
|
|
|
|
|
|
var ry=90+this.options.sphere.tilt;
|
|
var rz=180+this.options.sphere.turn;
|
|
|
|
var RY = (90-ry);
|
|
var RZ = (180-rz);
|
|
var RX = 0,RY,RZ;
|
|
|
|
var rx=RX*Math.PI/180;
|
|
var ry=RY*Math.PI/180;
|
|
var rz=RZ*Math.PI/180;
|
|
//console.log(rx, ry, rz);
|
|
var r = this.getSphereRadiusInPixel();
|
|
|
|
var center = {
|
|
x: this.element.width() / 2,
|
|
y: this.element.height() / 2
|
|
}
|
|
|
|
for (var key in this.options.locations) {
|
|
var location = this.options.locations[key];
|
|
|
|
if (typeof location.delta === 'undefined') {
|
|
location.flatPosition = {x: location.x, y: location.y};
|
|
this.options.onRefreshLocation(location, location.x, location.y, this);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
WARNING: calculation of alphaAngle and deltaAngle is not exact
|
|
I had to create the _calibrated functions to modify the deltaAngle to make the result look good on
|
|
a spinning planet without rotation. It will totally bug with rotation!
|
|
* */
|
|
var progression = (((this.posVar + this.textureWidth * location.delta / (2 * Math.PI)) % this.textureWidth) / this.textureWidth);
|
|
var alphaAngle = progression * 2 * Math.PI;
|
|
var deltaAngle = this._calibrated(progression, location.alpha) * 2 * Math.PI;
|
|
|
|
|
|
var objAlpha = ry + location.alpha - Math.sin(alphaAngle / 2) * 0.15 * (location.alpha - Math.PI / 2) / (Math.PI / 4);
|
|
var objDelta = rz + deltaAngle;
|
|
|
|
var a = this._orbitalTo3d(objAlpha, objDelta, r);
|
|
|
|
var flatPosition = this._orthographicProjection(a);
|
|
|
|
if (a.x < 0 && !location.visible) {
|
|
this.options.onShowLocation(location, flatPosition.x, flatPosition.y, this);
|
|
}
|
|
if (a.x > 0 && location.visible) {
|
|
this.options.onHideLocation(location, flatPosition.x, flatPosition.y, this);
|
|
}
|
|
this.options.onRefreshLocation(location, flatPosition.x, flatPosition.y, this);
|
|
|
|
location.visible = a.x < 0;
|
|
location.position = a;
|
|
location.flatPosition = flatPosition;
|
|
location.rAlpha = objAlpha;
|
|
location.rDelta = objDelta;
|
|
|
|
}
|
|
|
|
if (this.flightsCtx !== null) {
|
|
this.flightsCtx.clearRect(0, 0, this.options.flightsCanvas.width(), this.options.flightsCanvas.height());
|
|
for (var key in this.options.paths) {
|
|
this._drawPath(this.options.paths[key], center, r);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
|
|
_line_circle_intersection: function(A, B, C, r) {
|
|
var d = {
|
|
x: B.x - A.x,
|
|
y: B.y - A.y
|
|
};
|
|
|
|
var f = {
|
|
x: A.x - C.x,
|
|
y: A.y - C.y
|
|
};
|
|
|
|
var a = this._dot(d, d);
|
|
var b = 2 * this._dot(f, d);
|
|
var c = this._dot(f, f) - r * r;
|
|
|
|
var discriminant = b * b - 4 * a * c;
|
|
if (discriminant < 0) {
|
|
return false;
|
|
} else {
|
|
discriminant = Math.sqrt(discriminant);
|
|
var t1 = (-b + discriminant) / (2 * a);
|
|
var t2 = (-b - discriminant) / (2 * a);
|
|
|
|
|
|
var sols = [];
|
|
|
|
if (t1 >= 0 && t1 <= 1) {
|
|
sols.push({
|
|
x:A.x + t1 * d.x,
|
|
y:A.y + t1 * d.y
|
|
});
|
|
}
|
|
|
|
if (t2 >= 0 && t2 <= 1) {
|
|
sols.push({
|
|
x:A.x + t2 * d.x,
|
|
y:A.y + t2 * d.y
|
|
});
|
|
}
|
|
|
|
return sols;
|
|
}
|
|
},
|
|
|
|
_dot: function(A, B) {
|
|
return A.x * B.x + A.y * B.y;
|
|
},
|
|
|
|
_drawPath: function(path, center, r) {
|
|
|
|
|
|
var originLocation = this.options.locations[path.origin];
|
|
var destinationLocation = this.options.locations[path.destination];
|
|
|
|
var dotSize = 50;
|
|
var spacing = 0.15;
|
|
|
|
if (typeof originLocation.delta === 'undefined' || typeof destinationLocation.delta === 'undefined') {
|
|
var pathVisible = originLocation.visible && destinationLocation.visible;
|
|
if (pathVisible) {
|
|
|
|
|
|
var flatDistance = this._distance(originLocation.flatPosition, destinationLocation.flatPosition);
|
|
|
|
var nb = flatDistance * 0.9 / 20;
|
|
// WARNING: we are drawing the paths on canvas, intensively using CPU. Could we gain by instead using SVG or the DOM ?
|
|
for (var i = 0; i < nb; i++) {
|
|
|
|
|
|
var fromFlatPosition = {
|
|
x: ((nb - i) / nb) * originLocation.flatPosition.x + (i / nb) * destinationLocation.flatPosition.x,
|
|
y: ((nb - i) / nb) * originLocation.flatPosition.y + (i / nb) * destinationLocation.flatPosition.y
|
|
};
|
|
|
|
var toFlatPosition = {
|
|
x: Math.max(((nb - (i + 1)) / nb), 0) * originLocation.flatPosition.x + Math.min(((i + 1) / nb), 1) * destinationLocation.flatPosition.x,
|
|
y: Math.max(((nb - (i + 1)) / nb), 0) * originLocation.flatPosition.y + Math.min(((i + 1) / nb), 1) * destinationLocation.flatPosition.y
|
|
};
|
|
|
|
var diff = {
|
|
x: fromFlatPosition.x - toFlatPosition.x,
|
|
y: fromFlatPosition.y - toFlatPosition.y,
|
|
z: fromFlatPosition.z - toFlatPosition.z
|
|
};
|
|
|
|
fromFlatPosition.x -= diff.x * spacing;
|
|
fromFlatPosition.y -= diff.y * spacing;
|
|
fromFlatPosition.z -= diff.z * spacing;
|
|
toFlatPosition.x += diff.x * spacing;
|
|
toFlatPosition.y += diff.y * spacing;
|
|
toFlatPosition.z += diff.z * spacing;
|
|
|
|
|
|
this.flightsCtx.lineWidth = 3;
|
|
this.flightsCtx.beginPath();
|
|
this.flightsCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
|
this.flightsCtx.moveTo(fromFlatPosition.x + this.options.flightsCanvasPosition.x, fromFlatPosition.y + this.options.flightsCanvasPosition.y);
|
|
this.flightsCtx.lineTo(toFlatPosition.x + this.options.flightsCanvasPosition.x, toFlatPosition.y + this.options.flightsCanvasPosition.y);
|
|
this.flightsCtx.stroke();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var key in path.flights) {
|
|
var flight = path.flights[key];
|
|
|
|
var position = flight.destination == path.destination ? flight.position : (1 - flight.position);
|
|
|
|
|
|
var flightFlatPosition = {
|
|
x: (1 - position) * originLocation.flatPosition.x + position * destinationLocation.flatPosition.x,
|
|
y: (1 - position) * originLocation.flatPosition.y + position * destinationLocation.flatPosition.y
|
|
};
|
|
|
|
if (!flight.visible && pathVisible) {
|
|
this.options.onShowFlight(flight, this);
|
|
flight.visible = true;
|
|
}
|
|
|
|
if (flight.visible && !pathVisible) {
|
|
this.options.onHideFlight(flight, this);
|
|
flight.visible = false;
|
|
}
|
|
|
|
var angle = Math.atan2(destinationLocation.flatPosition.y - originLocation.flatPosition.y, destinationLocation.flatPosition.x - originLocation.flatPosition.x) + (flight.destination == path.destination ? 0 : Math.PI);
|
|
//console.log(flightAheadFlatPosition.y - flightFlatPosition.y);
|
|
|
|
this.options.onRefreshFlight(flight, flightFlatPosition.x, flightFlatPosition.y, angle, this);
|
|
}
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
var locationsDistance = this._distance(originLocation.position, destinationLocation.position);
|
|
|
|
|
|
var middlePosition = {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
};
|
|
|
|
|
|
var radius = this._distance(originLocation.position, middlePosition);
|
|
|
|
var originP = {
|
|
delta: Math.atan2((originLocation.position.y - middlePosition.y), (originLocation.position.x - middlePosition.x)),
|
|
alpha: Math.acos((originLocation.position.z - middlePosition.z) / radius)
|
|
};
|
|
|
|
var destinationP = {
|
|
delta: Math.atan2((destinationLocation.position.y - middlePosition.y), (destinationLocation.position.x - middlePosition.x)),
|
|
alpha: Math.acos((destinationLocation.position.z - middlePosition.z) / radius)
|
|
};
|
|
|
|
|
|
|
|
if (Math.abs(originP.delta - destinationP.delta) > Math.PI) {
|
|
if ((originP.delta - destinationP.delta) > Math.PI) {
|
|
originP.delta -= 2 * Math.PI;
|
|
} else {
|
|
originP.delta += 2 * Math.PI;
|
|
}
|
|
}
|
|
|
|
if (path.sens) {
|
|
if (((originP.delta - destinationP.delta) > 0 ? 1 : -1) != path.sens) {
|
|
if (Math.abs(originP.delta - destinationP.delta) > Math.PI / 2) {
|
|
originP.delta += path.sens * 2 * Math.PI;
|
|
}
|
|
}
|
|
} else {
|
|
path.sens = (originP.delta - destinationP.delta) > 0 ? 1 : -1;
|
|
}
|
|
|
|
|
|
if (!path.nb) {
|
|
path.nb = Math.round(((locationsDistance / (2 * r)) * Math.PI * 2 * r + (1 - (locationsDistance / (2 * r))) * locationsDistance) / dotSize);
|
|
}
|
|
var nb = path.nb;
|
|
var maxDistance = 1.2;
|
|
for (var i = 0; i < nb; i++) {
|
|
var fromP = {
|
|
alpha: ((nb - i) / nb) * originP.alpha + (i / nb) * destinationP.alpha,
|
|
delta: ((nb - i) / nb) * originP.delta + (i / nb) * destinationP.delta
|
|
};
|
|
|
|
var toP = {
|
|
alpha: ((nb - 1 - i) / nb) * originP.alpha + ((i + 1) / nb) * destinationP.alpha,
|
|
delta: ((nb - 1 - i) / nb) * originP.delta + ((i + 1) / nb) * destinationP.delta
|
|
};
|
|
//console.log(i, fromP.alpha, fromP.delta, toP.alpha, toP.delta);
|
|
|
|
var fromPosition = this._orbitalTo3d(fromP.alpha, fromP.delta, -(Math.sin(Math.PI * i / nb) * (maxDistance - 1) + 1) * radius);
|
|
var toPosition = this._orbitalTo3d(toP.alpha, toP.delta, -(Math.sin(Math.PI * (i + 1) / nb) * (maxDistance - 1) + 1) * radius);
|
|
var diff = {
|
|
x: fromPosition.x - toPosition.x,
|
|
y: fromPosition.y - toPosition.y,
|
|
z: fromPosition.z - toPosition.z
|
|
};
|
|
|
|
|
|
|
|
fromPosition.x -= diff.x * spacing;
|
|
fromPosition.y -= diff.y * spacing;
|
|
fromPosition.z -= diff.z * spacing;
|
|
toPosition.x += diff.x * spacing;
|
|
toPosition.y += diff.y * spacing;
|
|
toPosition.z += diff.z * spacing;
|
|
|
|
|
|
fromPosition.x += middlePosition.x;
|
|
fromPosition.y += middlePosition.y;
|
|
fromPosition.z += middlePosition.z;
|
|
toPosition.x += middlePosition.x;
|
|
toPosition.y += middlePosition.y;
|
|
toPosition.z += middlePosition.z;
|
|
|
|
|
|
|
|
var fromFlatPosition = this._orthographicProjection(fromPosition);
|
|
var toFlatPosition = this._orthographicProjection(toPosition);
|
|
|
|
var fromDistanceCenter = this._distance(fromFlatPosition, center);
|
|
var toDistanceCenter = this._distance(toFlatPosition, center);
|
|
|
|
var fromVisible = true;
|
|
var toVisible = true;
|
|
if (fromPosition.x > 0) {
|
|
if (fromDistanceCenter <= r) {
|
|
fromVisible = false;
|
|
}
|
|
}
|
|
|
|
if (toPosition.x > 0) {
|
|
if (toDistanceCenter <= r) {
|
|
toVisible = false;
|
|
}
|
|
}
|
|
|
|
//console.log(i, fromVisible, toVisible);
|
|
|
|
if (!fromVisible && !toVisible) {
|
|
continue;
|
|
}
|
|
|
|
if (!fromVisible) {
|
|
var intersection = this._line_circle_intersection(fromFlatPosition, toFlatPosition, center, r);
|
|
if (intersection.length == 0) {
|
|
continue;
|
|
}
|
|
fromFlatPosition = intersection[0];
|
|
}
|
|
|
|
if (!toVisible) {
|
|
var intersection = this._line_circle_intersection(fromFlatPosition, toFlatPosition, center, r);
|
|
if (intersection.length == 0) {
|
|
continue;
|
|
}
|
|
toFlatPosition = intersection[0];
|
|
}
|
|
|
|
|
|
|
|
this.flightsCtx.lineWidth = 3;
|
|
this.flightsCtx.beginPath();
|
|
this.flightsCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
|
this.flightsCtx.moveTo(fromFlatPosition.x + this.options.flightsCanvasPosition.x, fromFlatPosition.y + this.options.flightsCanvasPosition.y);
|
|
this.flightsCtx.lineTo(toFlatPosition.x + this.options.flightsCanvasPosition.x, toFlatPosition.y + this.options.flightsCanvasPosition.y);
|
|
this.flightsCtx.stroke();
|
|
|
|
}
|
|
|
|
for (var key in path.flights) {
|
|
var flight = path.flights[key];
|
|
|
|
var position = flight.destination == path.destination ? flight.position : (1 - flight.position);
|
|
var positionAhead = flight.destination == path.destination ? (flight.position + 0.01) : (1 - (flight.position + 0.01));
|
|
|
|
var flightP = {
|
|
alpha: (1 - position) * originP.alpha + position * destinationP.alpha,
|
|
delta: (1 - position) * originP.delta + position * destinationP.delta
|
|
};
|
|
|
|
var flightAheadP = {
|
|
alpha: (1 - positionAhead) * originP.alpha + positionAhead * destinationP.alpha,
|
|
delta: (1 - positionAhead) * originP.delta + positionAhead * destinationP.delta
|
|
};
|
|
|
|
var flightPosition = this._orbitalTo3d(flightP.alpha, flightP.delta, -(Math.sin(Math.PI * position) * (maxDistance - 1) + 1) * radius);
|
|
var flightAheadPosition = this._orbitalTo3d(flightAheadP.alpha, flightAheadP.delta, -(Math.sin(Math.PI * positionAhead) * (maxDistance - 1) + 1) * radius);
|
|
|
|
flightPosition.x += middlePosition.x;
|
|
flightPosition.y += middlePosition.y;
|
|
flightPosition.z += middlePosition.z;
|
|
flightAheadPosition.x += middlePosition.x;
|
|
flightAheadPosition.y += middlePosition.y;
|
|
flightAheadPosition.z += middlePosition.z;
|
|
|
|
var flightFlatPosition = this._orthographicProjection(flightPosition);
|
|
var flightAheadFlatPosition = this._orthographicProjection(flightAheadPosition);
|
|
|
|
var flightDistanceCenter = this._distance(flightFlatPosition, center);
|
|
|
|
if (!flight.visible && (flightPosition.x < 0 || flightDistanceCenter > r)) {
|
|
this.options.onShowFlight(flight, this);
|
|
flight.visible = true;
|
|
}
|
|
|
|
if (flight.visible && (flightPosition.x > 0 && flightDistanceCenter < r)) {
|
|
this.options.onHideFlight(flight, this);
|
|
flight.visible = false;
|
|
}
|
|
|
|
var angle = Math.atan2(flightAheadFlatPosition.y - flightFlatPosition.y, flightAheadFlatPosition.x - flightFlatPosition.x);
|
|
//console.log(flightAheadFlatPosition.y - flightFlatPosition.y);
|
|
|
|
this.options.onRefreshFlight(flight, flightFlatPosition.x, flightFlatPosition.y, angle, this);
|
|
}
|
|
|
|
},
|
|
|
|
_distance: function(A, B) {
|
|
if (A.z) {
|
|
return Math.sqrt(
|
|
(A.x - B.x) * (A.x - B.x) +
|
|
(A.y - B.y) * (A.y - B.y) +
|
|
(A.z - B.z) * (A.z - B.z)
|
|
);
|
|
} else {
|
|
return Math.sqrt(
|
|
(A.x - B.x) * (A.x - B.x) +
|
|
(A.y - B.y) * (A.y - B.y)
|
|
);
|
|
}
|
|
},
|
|
|
|
// WARNING: temporary function to make the locations look good on a spinning planet without rotation
|
|
_calibrated: function(x, alpha) {
|
|
var calib = 0.3 + 0.15 * Math.abs(alpha - Math.PI / 2) / (Math.PI / 4);
|
|
//console.log(calib);
|
|
var y = calib * (4 * (x - 0.5) * (x - 0.5) * (x - 0.5) + 0.5) + (1 - calib) * x;
|
|
return y;
|
|
},
|
|
|
|
|
|
/* WARNING:
|
|
Obviously there is something wrong with _orbitalTo3d and _orthographicProjection, since
|
|
I can't get a descent display of locations when the planet is rotated. That's why I had to create the _calibrated
|
|
function in the first place. I didn't have time to look precisely into it, and I probably don't know enough math.
|
|
|
|
I leaved the _3dProjection function I found on wikipedia but is not working. (I might not have correctly understood / write it)
|
|
*/
|
|
_orbitalTo3d: function(alpha, delta, r) {
|
|
return {
|
|
x: -r * Math.sin(alpha) * Math.cos(delta),
|
|
y: -r * Math.sin(alpha) * Math.sin(delta),
|
|
z: -r * Math.cos(alpha)
|
|
};
|
|
},
|
|
|
|
_orthographicProjection: function(position) {
|
|
return {x: position.y + this.element.width() / 2, y: position.z + this.element.height() / 2};
|
|
},
|
|
|
|
_3dProjection: function(a, c, delta, e) {
|
|
// Wikipedia is your friend :) : http://en.wikipedia.org/wiki/3D_projection
|
|
var d = {x: 0, y: 0, z: 0};
|
|
d.x = Math.cos(delta.y) * (Math.sin(delta.z) * (a.y - c.y) + Math.cos(delta.z) * (a.x - c.x)) - Math.sin(delta.y) * (a.z - c.z);
|
|
d.y = Math.sin(delta.x) * (Math.cos(delta.y) * (a.z - c.z) + Math.sin(delta.y) * (Math.sin(delta.z) * (a.y - c.y) + Math.cos(delta.z) * (a.x - c.x)))
|
|
+ Math.cos(delta.x) * (Math.cos(delta.z) * (a.y - c.y) - Math.sin(delta.z) * (a.x - c.x))
|
|
d.z = Math.cos(delta.x) * (Math.cos(delta.y) * (a.z - c.z) + Math.sin(delta.y) * (Math.sin(delta.z) * (a.y - c.y) + Math.cos(delta.z) * (a.x - c.x)))
|
|
- Math.sin(delta.x) * (Math.cos(delta.z) * (a.y - c.y) - Math.sin(delta.z) * (a.x - c.x));
|
|
|
|
return {
|
|
x: d.z, //(d.x - e.x) * (e.y / d.y),
|
|
y: d.y //(d.z - e.z) * (e.y / d.y)
|
|
};
|
|
},
|
|
|
|
_mouseDragStart: function(e) {
|
|
this.lastMousePos = e.clientX;
|
|
this.lastSpeed = null;
|
|
},
|
|
|
|
_mouseDrag: function(e) {
|
|
this.lastSpeed = (e.clientX - this.lastMousePos);
|
|
this.posVar = this.posVar - this.lastSpeed;
|
|
this.lastMousePos = e.clientX;
|
|
},
|
|
_mouseDragStop: function(e) {
|
|
this.lastMousePos = null;
|
|
this.lastTime = null;
|
|
},
|
|
|
|
_turnBy: function(time) {
|
|
if (this.lastTurnByTime === null) {
|
|
this.lastTurnByTime = time;
|
|
}
|
|
var timeDiff = (time - this.lastTurnByTime) / 1000;
|
|
if (this.lastMousePos === null) {
|
|
if (this.lastSpeed !== null) {
|
|
if (this.lastTime === null) {
|
|
this.lastTime = time;
|
|
}
|
|
if (this.options.backToDefaultTime + this.lastTime - time < 0) {
|
|
this.lastSpeed = null;
|
|
} else {
|
|
var backToDef = (this.options.backToDefaultTime + this.lastTime - time) / this.options.backToDefaultTime;
|
|
this.posVar -= this.lastSpeed * backToDef + (this.options.defaultSpeed * timeDiff) * (1 - backToDef);
|
|
}
|
|
} else {
|
|
this.posVar -= this.options.defaultSpeed * timeDiff;
|
|
}
|
|
}
|
|
this.lastTurnByTime = time;
|
|
return this.posVar;
|
|
},
|
|
|
|
_getQBezierValue: function (t, p1, p2, p3) {
|
|
var iT = 1 - t;
|
|
return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3;
|
|
},
|
|
|
|
_getQBezierDerivation: function(t, p1, p2, p3) {
|
|
return (2 * p1 - 4 * p2 + 2 * p3) * t + 2 * p2 - 2 * p1;
|
|
},
|
|
|
|
_getQBezierAngle: function(startX, startY, cpX, cpY, endX, endY, position) {
|
|
var x = this._getQBezierDerivation(position, startX, cpX, endX);
|
|
var y = this._getQBezierDerivation(position, startY, cpY, endY);
|
|
return Math.atan2(y, x);
|
|
},
|
|
|
|
_getQuadraticCurvePoint: function(startX, startY, cpX, cpY, endX, endY, position) {
|
|
return {
|
|
x: this._getQBezierValue(position, startX, cpX, endX),
|
|
y: this._getQBezierValue(position, startY, cpY, endY),
|
|
angle: this._getQBezierAngle(startX, startY, cpX, cpY, endX, endY, position)
|
|
};
|
|
},
|
|
|
|
changeLocations: function(locations) {
|
|
for (var key in this.options.locations) {
|
|
this.options.onDeleteLocation(this.options.locations[key], this);
|
|
}
|
|
this.options.locations = locations;
|
|
this._initLocations();
|
|
},
|
|
|
|
changePaths: function(paths) {
|
|
for (var key in this.options.paths) {
|
|
var path = this.options.paths[key];
|
|
for (var keyFlight in path.flights) {
|
|
var flight = path.flights[keyFlight];
|
|
this.options.onDeleteFlight(flight, this);
|
|
}
|
|
}
|
|
this.options.paths = paths;
|
|
this._initFlights();
|
|
}
|
|
|
|
});
|
|
|
|
})($);
|
|
|