/**
* Represents one Arrow with: position, time lived, time till death, length, forward speed, width
*
* @alias Arrow
* @constructor
*
* @param {TrajectoryHelper} trajectoryHelper TrajectoryHelper to be used for this arrow
*/
function Arrow(trajectoryHelper) {
this._trajectoryHelper = trajectoryHelper;
/**
* List of positions over time
* @member
* @type {list}
* @readonly
*/
this.position = []; //Position list based on timestep
/**
* List of head/tail positions and length over time
* @member
* @type {list}
* @readonly
*/
this.timeData = []; //Stores timedependent values: length, head, tail
/**
* Width of this arrow
* @member
* @type {number}
* @readonly
*/
this.width = 0;
/**
* Timestep when this arrow was born
* @member
* @type {int}
* @readonly
*/
this.timeBorn = 0;
/**
* Timestep when this arrow dies
* @member
* @type {int}
* @readonly
*/
this.timeDeath = 0;
this.polygon = []; //Saves the polygons of timesteps for speed up
}
/**
* Returns a polygon that is defined by the linesegment
* from tail -> position -> head with the width of this.width
* @param {int} time time for calculation
* @param {float} [border] border to add around polygon, default 0
* @return {SAT.Polygon} Polygon approximation
*/
Arrow.prototype.getPolygon = function(time, border){
border = border | 0;
//If we have a polygon cached, return that
if (this.polygon[time] !== undefined &&
this.polygon[time][border] !== undefined){
return this.polygon[time][border];
}
var dHead = Point.sub(this.timeData[time].head,this.position[time]);
var dTail = Point.sub(this.position[time],this.timeData[time].tail);
// Calculate normals
var normalHead = new Point(-dHead.y,dHead.x);
var normalTail = new Point(-dTail.y,dTail.x);
// Calculate interpolated normal for position
var normalPosition = new Point((normalHead.x+normalTail.x)/2, (normalHead.y+normalTail.y)/2);
// Normalize
normalHead.normalize();
normalTail.normalize();
normalPosition.normalize();
// Add arrow width and border offset to head/position/tail offset
normalHead = Point.add(normalHead.clone().scale(this.width/8), normalHead.scale(border));
normalPosition = Point.add(normalPosition.clone().scale(this.width/4), normalPosition.scale(border));
normalTail = Point.add(normalTail.clone().scale(this.width/2), normalTail.scale(border));
// Create points
var headPoint1 = Point.add(this.timeData[time].head, normalHead);
var headPoint2 = Point.add(this.timeData[time].head, normalHead.reverse());
var positionPoint1 = Point.add(this.position[time], normalPosition);
var positionPoint2 = Point.add(this.position[time], normalPosition.reverse());
var tailPoint1 = Point.add(this.timeData[time].tail, normalTail);
var tailPoint2 = Point.add(this.timeData[time].tail, normalTail.reverse());
return new SAT.Polygon(new SAT.Vector(), [
headPoint1, positionPoint1, tailPoint1, tailPoint2, positionPoint2, headPoint2
]);
}
/**
* Returns true if distance is kept.
* @param {Arrow} other The arrow to calculate the distance to
* @param {int} time time for calculation
* @param {float} dist the dist to check for
* @return {boolean} true if distance is greater than dist
*/
Arrow.prototype.distance = function(other, time, dist){
// three points for line segments of this arrow
var hp1 = this.timeData[time].head;
var p1 = this.position[time];
var tp1 = this.timeData[time].tail;
// three points for line segments of other arrow
var hp2 = other.timeData[time].head;
var p2 = other.position[time];
var tp2 = other.timeData[time].tail;
// calculate the distances from every point of an arrow to every
// line segment of the other arrow
//
// line segments are:
// segment 1: from headpoint to point
// segment 2: from point to tailpoint
minDist = Number.MAX_SAFE_INTEGER;
// distances for head point 1
minDist = Math.min(this._distancePointLine(hp1, hp2, p2), minDist);
minDist = Math.min(this._distancePointLine(hp1, p2, tp2), minDist);
// distances for point 1
minDist = Math.min(this._distancePointLine(p1, hp2, p2), minDist);
minDist = Math.min(this._distancePointLine(p1, p2, tp2), minDist);
// distances for tail point 1
minDist = Math.min(this._distancePointLine(tp1, hp2, p2), minDist);
minDist = Math.min(this._distancePointLine(tp1, p2, tp2), minDist);
// distances for head point 2
minDist = Math.min(this._distancePointLine(hp2, hp1, p1), minDist);
minDist = Math.min(this._distancePointLine(hp2, p1, tp1), minDist);
// distances for point 2
minDist = Math.min(this._distancePointLine(p2, hp1, p1), minDist);
minDist = Math.min(this._distancePointLine(p2, p1, tp1), minDist);
// distances for tail point 2
minDist = Math.min(this._distancePointLine(tp2, hp1, p1), minDist);
minDist = Math.min(this._distancePointLine(tp2, p1, tp1), minDist);
// return true if the minimum distance from all points to all other
// line segments is larger than the allowed distance
return (minDist > dist);
}
/**
* Returns the distance from a point to a line segment
* @private
* @param {Point} point Point
* @param {Point} lineStart Start point of line
* @param {Point} lineEnd End point of line
* @return {number} distance
*/
Arrow.prototype._distancePointLine = function(point, lineStart, lineEnd){
// from http://paulbourke.net/geometry/pointlineplane/
var lineMag = (Point.sub(lineStart,lineEnd)).len();
var u = ( ( ( point.x - lineStart.x ) * ( lineEnd.x - lineStart.x ) ) +
( ( point.y - lineStart.y ) * ( lineEnd.y - lineStart.y ) ) ) /
( lineMag * lineMag );
if (u < 0.0 || u > 1.0){
return Math.min(Point.sub(point,lineStart).len(), Point.sub(point,lineEnd).len());
}
var iX = lineStart.x + u * ( lineEnd.x - lineStart.x );
var iY = lineStart.y + u * ( lineEnd.y - lineStart.y );
return (Point.sub(point, new Point(iX,iY))).len();
}
/**
* Draws the arrow on a canvas
* @param {Context2D} ctx Context for drawing
* @param {number} time Timestep for drawing (including fraction times)
* @param {number} scale scales the whole arrow by scale
*/
Arrow.prototype.draw = function(ctx, time, scale){
// Default use 1 as scale
scale = scale || 1;
var alphaBegin = Math.min((time-this.timeBorn),1);
var alphaEnd = Math.min((this.timeDeath-time),1);
var alpha = Math.min(alphaBegin, alphaEnd);
ctx.fillStyle = 'rgba(255, 255, 255, ' + alpha + ')';
ctx.strokeStyle = 'rgba(96, 215, 235, ' + alpha + ')';
ctx.lineWidth = 1;
var tp = undefined;
var pos = undefined;
var hp = undefined;
var length = undefined;
var width = this.width*scale;// * 3;
var flooredTime = Math.floor(time);
var fracTime = time % 1;
if (Math.abs(fracTime) > 0 && time <= this.timeDeath){
//We are drawing a inbetween arrow...
tp = Point.interpolate(this.timeData[flooredTime].tail,this.timeData[flooredTime+1].tail,fracTime).scale(scale);
pos = Point.interpolate(this.position[flooredTime],this.position[flooredTime+1],fracTime).scale(scale);
hp = Point.interpolate(this.timeData[flooredTime].head,this.timeData[flooredTime+1].head,fracTime).scale(scale);
length = (this.timeData[flooredTime].length*(1-fracTime)+this.timeData[flooredTime+1].length*(fracTime))*scale;
} else {
tp = Point.scale(this.timeData[flooredTime].tail, scale);
pos = Point.scale(this.position[flooredTime], scale);
hp = Point.scale(this.timeData[flooredTime].head, scale);
length = this.timeData[flooredTime].length*scale;
}
if (length < width * 2 || Point.angle(Point.sub(tp, pos), Point.sub(hp, pos)) < 0.9) {
// draw circle
ctx.beginPath();
ctx.arc(pos.x,pos.y,width * 1.5,0,2*Math.PI);
ctx.stroke();
ctx.fill();
} else {
// draw arrow
var linePoints = [tp, pos, hp];
// arrowhead width factor, depending on width of the arrow
var awf = 2;
// arrowhead length
var ahl = 0.2 * length;
// list of point coordinates left to the line, with distance this.width, going from tail to head
var l = [];
// list of point coordinates right to the line, with distance this.width, going from tail to head
var r = [];
// arrowhead left, right and front point coordinates
var al, ar, af;
// temporal variables
var v, v2, p1, p2, p3, p, angle, wFactor, len;
for (i = 0; i < linePoints.length; i++){
if (i == 0){
p1 = linePoints[i];
p2 = linePoints[i+1];
// vector from first to second point
v = Point.sub(p2, p1).normalize().scale(width);
// rotate vector by 90 degrees
p = Point.add(p1, v.rotate(-Math.PI/2));
l.push({"x": p.x, "y": p.y});
// rotate vector by 180 degrees in other direction
p = Point.add(p1, v.rotate(Math.PI));
r.push({"x": p.x, "y": p.y});
} else if (i < linePoints.length-1) {
p1 = linePoints[i];
p2 = linePoints[i-1];
p3 = linePoints[i+1];
// vector from current point to previous point
v = Point.sub(p2, p1);
// vector from current point to next point
v2 = Point.sub(p3, p1);
// angle and sign between vectors v and v2
aSign = (v.x * v2.y - v.y * v2.x) < 0 ? -1 : 1;
angle = aSign * Point.angle(v, v2)/2;
// width changes based on the angle between the vectors v and v2
wfactor = -(width / Math.cos(Math.abs(Math.PI/2 - angle))) / width;
// scale to width
v = v.normalize().scale(width * -wfactor);
// rotate vector
p = Point.add(p1, v.rotate(angle));
l.push({"x": p.x, "y": p.y});
// rotate vector by 180 degrees in other direction
p = Point.add(p1, v.rotate(-Math.PI));
r.push({"x": p.x, "y": p.y});
} else {
p1 = linePoints[i];
p2 = linePoints[i-1];
// vector from current point to previous point
v = Point.sub(p2, p1);
len = v.len();
// position at half distance
p1 = Point.add(p1, v.normalize().scale(len/2));
// normalize and scale to width
v = v.normalize().scale(width);
// rotate vector by 90 degrees
p = Point.add(p1, v.rotate(Math.PI/2));
l.push({"x": p.x, "y": p.y});
// rotate vector by 180 degrees in other direction
p = Point.add(p1, v.rotate(-Math.PI));
r.push({"x": p.x, "y": p.y});
}
}
// draw arrowhead at the end of the line
p = Point.add(p1, v.scale(awf));
ar = {"x": p.x, "y": p.y};
p = Point.add(p1, v.rotate(-Math.PI));
al = {"x": p.x, "y": p.y};
p = Point.add(p1, v.rotate(Math.PI/2).normalize().scale(len/2));
af = {"x": p.x, "y": p.y};
var lineFuncInterpolate = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("basis"); // other interpolation might work better
var lineFuncLinear = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var path1 = lineFuncInterpolate(l);
var path2 = lineFuncLinear([l[l.length-1], al, af, ar, r[r.length-1]]);
var path3 = lineFuncInterpolate(r.reverse());
var path = path1 + "L" + path2.substr(1, path2.length-1)
+ "L" + path3.substr(1, path3.length-1)
+ "Z";
var p = new Path2D(path);
ctx.stroke(p);
ctx.fill(p);
}
}
/**
* Returns true if distance to all is kept
* @param {Arrow} others The arrows to calculate the distance to
* @param {int} time time for calculation
* @param {float} dist the dist to check for
* @return {boolean} true if distance is greater than dist
*/
Arrow.prototype.distanceToMultiple = function(others, time, dist){
var that = this;
// Array.every will stop if any return false...
return others.every(elem => that.distance(elem, time, dist));
}
/**
* Calculates the length, and head and tail of this arrow
* this.position has to be set
* @param {int} time The time to calculate this values for
*/
Arrow.prototype.calcDataValues = function(time){
this.timeData[time] = this._trajectoryHelper.getTimeData(this, time);
}
/**
* returns wether this arrow is alive at the given time
* @param {int} time asked for timestep
* @return {boolean} true if arrow is alive at given position
*/
Arrow.prototype.isAlive = function(time){
return this.timeBorn <= time && this.timeDeath >= time;
}
/**
* adds a position for the arrow at "time" timestep,
* based on the position of startTime and calculates the Data values
* for this timestep
* @param {int} startTime known timestep
* @param {int} time asked for timestep
*/
Arrow.prototype.propagateToTime = function(startTime, time){
if (time == startTime) return;
if (time > startTime){
// forward pass
this.position[time] = Point.interpolate(this.position[startTime], this.timeData[startTime].head, 0.4);
this.calcDataValues(time);
// use old tail as new tail, to avoid jumping based on changed streamline integration
this.timeData[time].tail = Point.interpolate(this.position[startTime], this.timeData[startTime].tail, 0.6);
} else {
// Backward pass
this.position[time] = Point.interpolate(this.position[startTime], this.timeData[startTime].tail, 0.4);
this.calcDataValues(time);
this.timeData[time].head = Point.interpolate(this.position[startTime], this.timeData[startTime].head, 0.6);
}
}