Source: TrajectoryHelper.js

/**
 * TrajectoryHelper handles trajectories
 *
 * @alias TrajectoryHelper
 * @constructor
 *
 * @param {WindDataProvider} windDataProvider The wind data file.
 * @param {number} [arrowLengthProportion] scale wind length data, default 10
 */
function TrajectoryHelper(windDataProvider, arrowLengthProportion) {
    this.windDataProvider = windDataProvider;

	// step size for Runge-Kutta (RK2)
	this._stepSize = 1;
	this._arrowLengthProportion = arrowLengthProportion || 10;
    this._arrowMaxProportion = 20;
}


/**
 * Returns the head/tail positions of an arrow on the trajectory and its length
 *
 * @param {Arrow} arrow The arrow to calculate
 * @param {int} time Timestep to calculate for
 * @return {Object} Object with points in "head" and "tail" and "length"
 */
TrajectoryHelper.prototype.getTimeData = function(arrow, time){

	if (arrow.timeData[time] === undefined){
		// TODO We have some problems when we get out of the world at poles (<-90 >-90), what to do?
		var dir = this.windDataProvider.interpolate(arrow.position[time].x, arrow.position[time].y); // local direction
        if (dir === null) console.log(arrow);
		var length = Math.min(dir[2] * this._arrowLengthProportion, arrow.width * this._arrowMaxProportion); // length is proportional to length of local direction

		// forward integration
		var head = this.integrate(arrow.position[time].clone(), this._stepSize, length, arrow);

		// backward integration
		var tail = this.integrate(arrow.position[time].clone(), this._stepSize * -1, length, arrow);

		return {length: length, head: head, tail: tail};
	} else {
		var dir = this.windDataProvider.interpolate(arrow.timeData[time].head.x, arrow.timeData[time].head.y); // local direction
		var length = Math.min(dir[2] * this._arrowLengthProportion, arrow.width * this._arrowMaxProportion); // length is proportional to length of local direction

		var head = this.integrate(arrow.timeData[time].head.clone(), this._stepSize, length, arrow);

		var tail = arrow.position[time];

		arrow.position[time] = arrow.timeData[time].head;

		return {length: length, head: head, tail: tail};
	}

}

/**
 * Integration using Runge-Kutta (RK2)
 *
 * @param {Object} s Point to integrate
 * @param {Number} stepSize Step size
 * @param {Number} trajectoryLength Length of the trajectory that is integrated.
 * @return {Object} Endpoint of integration
 */
TrajectoryHelper.prototype.integrate = function(s, stepSize, trajectoryLength, arrow){

	var length = 0;
	var d1, d2, s1, newlength, p1;

	while (length < trajectoryLength * 0.5) {
		d1 = this.windDataProvider.interpolate(s.x, s.y); // half euler step for stepSize = 1
		d2 = this.windDataProvider.interpolate(s.x+d1[0]* (stepSize/2), s.y+d1[1]* (stepSize/2)); // flow vector at half euler step

		if (stepSize < 0) { // backward integration
			d2[0] = d2[0] * -1;
			d2[1] = d2[1] * -1;
		}

        var newlength = Math.min(d2[2] * this._arrowLengthProportion, arrow.width * this._arrowMaxProportion);
        if (newlength < d2[2]){
            //we would overestimate this, maybe...
            p1 = new Point(d2[0],d2[1]);
            p1.normalize();
            p1.scale(newlength);
            s1 = Point.add(s, p1);
        } else {
            s1 = new Point(s.x + d2[0],s.y + d2[1]); // origin + flow vector at half euler step
        }

		length = length + newlength;
		s = s1;

        if (d2[2]<0.01) break;
	}

	return s1;
}