/**
*
* @param {number} posX - position coordinate
* @param {number} posY - position coordinate
* @param {number} velocity -
* @param {VelocityField} velocityField - the velocity field where the arrow lives in
* @param {DistanceMap} distanceMap - the related distance map
* @constructor
*/
var Arrow = function (posX, posY, velocity, velocityField, distanceMap) {
var INTERPOLATION_STEP = 0.1;
var velocityField = velocityField;
this.occupiedFields = [];
this.currX = posX;
this.currY = posY;
this.len = velocity;
//TODO: length: dependent on velocity/magnitude of flowField, == integration length
this.isAlive = true;
this.opacity = 1e-6;//0;
this.strokeWidth = 2;
this.respawnTime = 0;
this.recolorTime = 0;
this.recolor = true;
this.currp0x = posX - 20;
this.currp0y = posY - 30;
this.currp1x = posX - 10;
this.currp1y = posY - 10;
this.currp2x = posX + 10;
this.currp2y = posY + 10;
this.currp3x = posX + 40;
this.currp3y = posY + 20;
this.creationDatetime = new Date().getTime(); //creation datetime or respawn datetime
//Offsets to render a circle with 5 control points
//y-axis
this.oy1 = 2;
this.oy2 = 15;
this.oy3 = 18;
this.oy4 = 15;
this.oy5 = 2;
//x-axis is to short to make a proper circle if the integration length is too small.
//so "stretch" the x-part by some offset
this.ox1 = -18;
this.ox2 = -14;
this.ox3 = 0;
this.ox4 = 14;
this.ox5 = 18;
this.RAD2DEG = (180/Math.PI);
var parent = this; //to use 'this.' variables in child functions
/**
* integrates an arrow
*/
this.integrateArrow = function () {
//sample grid at arrows position:
//map curr position to a grid-field
var idx = Math.floor(parent.currX);
var idy = Math.floor(parent.currY);
if(idx >= 0 && idy >= 0) {
//if (!(idx < 0 || idy < 0 || idx.isNan)) {
//get direction and velocity at grid field
/*
var fx = fieldData[idy*WIDTH+idx].dirX;
var fy = fieldData[idy*WIDTH+idx].dirY;
var s = fieldData[idy*WIDTH+idx].velocity; //s = arclength == length of arrow, depends on magnitude of velocity
var stepsize = s/INTEGRATION_POINTS; //idea: the longer s, the bigger the integration step, the bigger the arrow
*/
var avgPoint = velocityField.getAverageFieldData(idx, idy);
var stepsize = avgPoint.velocity / 4; //4 == Number of Integration points
parent.isCirc = 0;
if (avgPoint.velocity < 10) {
parent.isCirc = 1;
parent.yCirc = parent.currY; //all controlpoints in our circle need the same height before adding offset
}
//Our arrows are like: p0 - p1 - curr - p2 - p3 ->
//Forward integration.
var p2 = rk4avg(parent.currX, parent.currY, stepsize);
var p3 = rk4avg(p2[0], p2[1], stepsize);
//Backward integration
var p1 = rk4avg(parent.currX, parent.currY, -stepsize);
var p0 = rk4avg(p1[0], p1[1], -stepsize);
//Update positions at handle points
parent.currp0x = p0[0];
parent.currp0y = p0[1];
parent.currp1x = p1[0];
parent.currp1y = p1[1];
//conter = currX, currY, already done in advection step
parent.currp2x = p2[0];
parent.currp2y = p2[1];
parent.currp3x = p3[0];
parent.currp3y = p3[1];
}
}
/**
* advects an arrow and calls checkDomain
* @param {number} dt - delta time
*/
this.advectArrow = function (dt) {
//map curr position to a grid-field
var idx = Math.floor(parent.currX);
var idy = Math.floor(parent.currY);
var avgPoint = velocityField.getAverageFieldData(idx, idy);
parent.currX = (parent.currX + avgPoint.dirX * dt);
parent.currY = (parent.currY + avgPoint.dirY * dt);
/* TODO: If lifetime expired, reduce opacity*/
/* parent.opacity = parent.opacity - 0.1; */
checkDomain();
}
/**
* checks if arrow is colliding with another arrow
* if no collision --> marks this area in the velocity field as occupied and returns true, so the arrowManager inserts the arrow
* @returns {boolean} returns true if arrow is still alive, false if arrow is dead.
*/
this.checkAndMarkOccupied = function() {
//reset
parent.occupiedFields = [];
//get the entries at the handlepoints
var distanceMapEntry;
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp0x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp0y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp1x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp1y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currX/distanceMap.getDistanceMapDivisor()), Math.floor(parent.currY/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp2x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp2y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.floor(parent.currp3x/distanceMap.getDistanceMapDivisor()),Math.floor(parent.currp3y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
//console.log("distanceMapEntry for curr3 x: " + distanceMapEntry.xPos + " y: " + distanceMapEntry.yPos);
/*
//neighbors above/ under handlepoints and sideways
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp0x/4),Math.ceil((parent.currp0y-1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp1x/4),Math.ceil((parent.currp1y-1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currX/4), Math.ceil((parent.currY-1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp2x/4),Math.ceil((parent.currp2y-1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp3x/4),Math.ceil((parent.currp3y-1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp0x/4),Math.ceil((parent.currp0y+1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp1x/4),Math.ceil((parent.currp1y+1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currX/4), Math.ceil((parent.currY+1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp2x/4),Math.ceil((parent.currp2y+1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil(parent.currp3x/4),Math.ceil((parent.currp3y+1)/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil((parent.currp0x-1)/4),Math.ceil(parent.currp0y/4)));
parent.occupiedFields.push(distanceMap.getDistanceMapEntry(Math.ceil((parent.currp3x+1)/4),Math.ceil(parent.currp3y/4)));
*/
//TODO activate linear interpolation
//linear interpolation between handle points
//interpolation between currp0 and currp1
var step = INTERPOLATION_STEP;
while(step < 1) {
var interpolatedPoint = {};
interpolatedPoint.x = parent.linearInterpolation(parent.currp0x, parent.currp1x, step);
interpolatedPoint.y = parent.linearInterpolation(parent.currp0y, parent.currp1y, step);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.ceil(interpolatedPoint.x/distanceMap.getDistanceMapDivisor()),Math.ceil(interpolatedPoint.y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
step += INTERPOLATION_STEP;
}
//interpolation between currp1 and currp2
step = INTERPOLATION_STEP;
while(step < 1) {
var interpolatedPoint = {};
interpolatedPoint.x = parent.linearInterpolation(parent.currp1x, parent.currp2x, step);
interpolatedPoint.y = parent.linearInterpolation(parent.currp1y, parent.currp2y, step);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.ceil(interpolatedPoint.x/distanceMap.getDistanceMapDivisor()),Math.ceil(interpolatedPoint.y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
step += INTERPOLATION_STEP;
}
//interpolation between currp2 and currp3
step= INTERPOLATION_STEP
while(step < 1) {
var interpolatedPoint = {};
interpolatedPoint.x = parent.linearInterpolation(parent.currp2x, parent.currp3x, step);
interpolatedPoint.y = parent.linearInterpolation(parent.currp2y, parent.currp3y, step);
distanceMapEntry = distanceMap.getDistanceMapEntry(Math.ceil(interpolatedPoint.x/distanceMap.getDistanceMapDivisor()),Math.ceil(interpolatedPoint.y/distanceMap.getDistanceMapDivisor()));
if(distanceMapEntry != null)
parent.occupiedFields.push(distanceMapEntry);
step += INTERPOLATION_STEP;
}
//if any of those potential fields is marked, kill the arrow!
for (var i = 0; i < parent.occupiedFields.length; i++) {
var entryTemp = parent.occupiedFields[i];
if (entryTemp.v == 1) {
//kill arrow
//console.log("killing arrow after collision");
parent.isAlive = false;
parent.opacity = 1e-6;//0;
parent.strokeWidth = 1e-6;//0;
return false;
}
};
//set entries in distance map to occupied
for (var i = 0; i < parent.occupiedFields.length; i++) {
var entryTemp = parent.occupiedFields[i];
var x = entryTemp.xPos;
var y = entryTemp.yPos;
distanceMap.setDistanceMapValue(x, y, 1);
};
return true;
}
/**
* linear interpolation between two numbers
* @param {number} start - start point
* @param {number} end - end point
* @param {number} step - the interpolation step
* @returns {number} interpolated point at interpolation step
*/
this.linearInterpolation = function(start, end, step) {
return (start * step) + end * (1-step);
}
//TODO: More clever way of respawning to reduce visual artifacts?
/**
* Respawning the arrow gives it a new position at the moment and resets the handle points
* @param {number} posX - x position
* @param {number} posY - y position
*/
this.respawn = function(posX, posY) {
parent.currX = posX;
parent.currY = posY;
parent.len = 0;
parent.currp0x = 0;
parent.currp0y = 0;
parent.currp1x = 0;
parent.currp1y = 0;
parent.currp2x = 0;
parent.currp2y = 0;
parent.currp3x = 0;
parent.currp3y = 0;
//reinit
parent.isAlive = true;
parent.opacity = 1e-6;//0;
parent.strokeWidth = 1e-6;//0;
parent.respawnTime = 0;
parent.recolorTime = 0;
parent.recolor = true;
parent.creationDatetime = new Date().getTime(); //creation datetime or respawn datetime
}
//private functions
/**
* check if boundary of domain reached, if yes --> kill (reset)
* @returns {boolean} false if arrow has reached boundary
*/
var checkDomain = function () {
if (parent.currX >= velocityField.widthOfField - 1 || parent.currY >= velocityField.heightOfField - 1 || parent.currX < 0 || parent.currY < 0) {
//kill arrow if it runs out of the field
parent.isAlive = false;
parent.opacity = 1e-6;//0;
parent.strokeWidth = 1e-6//;0;
return false;
}
}
/**
* RK4 using averaged values
* @param {number} x - "real" coordinate of the current point
* @param {number} y - "real" coordinate of the current point
* @param {number} dt - stepsize
* @returns [x_next, y_next] the next coordinates
*/
var rk4avg = function (x, y, dt) {
//get indices from coordinates for lookup in or 1D-array
var idx = Math.floor(x);
var idy = Math.floor(y);
var pos = idy * velocityField.widthOfField + idx;
if (pos < 0 || pos >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
return [-1, -1];
}
var aData = velocityField.getAverageFieldData(idx, idy);
var ax = dt * aData.dirX;
var ay = dt * aData.dirY;
var posA = Math.floor(y + (ay * 0.5)) * velocityField.widthOfField + Math.floor(x + (ax * 0.5));
if (posA < 0 || posA >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
return [-1, -1];
}
var bData = velocityField.getAverageFieldData(Math.floor(x + (ax * 0.5)), Math.floor(y + (ay * 0.5)));
var bx = dt * bData.dirX;
var by = dt * bData.dirY;
var posB = Math.floor(y + (by * 0.5)) * velocityField.widthOfField + Math.floor(x + (bx * 0.5));
if (posB < 0 || posB >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
return [-1, -1];
}
var cData = velocityField.getAverageFieldData(Math.floor(x + (bx * 0.5)), Math.floor(y + (by * 0.5)));
var cx = dt * cData.dirX;
var cy = dt * cData.dirY;
var posC = Math.floor(y + cy) * velocityField.widthOfField + Math.floor(x + cx);
if (posC < 0 || posC >= (velocityField.widthOfField * velocityField.heightOfField) - 1) {
return [-1, -1];
}
var dData = velocityField.getAverageFieldData(Math.floor(x + cx), Math.floor(y + cy));
var dx = dt * dData.dirX;
var dy = dt * dData.dirY;
var xNext = idx + (ax + 2 * bx + 2 * cx + dx) / 6; //no *dt here?
var yNext = idy + (ay + 2 * by + 2 * cy + dy) / 6;
return [Math.floor(xNext), Math.floor(yNext)];
}
/**
* determines the angle between p2p3 and x-axis, to rotate the arowhead accordingly
* @returns {number} the angle
*/
this.calcArrowheadRotation = function () {
var x = parent.currp3x - parent.currp2x;
var y = parent.currp3y - parent.currp2y;
var len = Math.sqrt(x*x+y*y);
if(len <= 0){
len = 0.000000001;
}
var xNorm = x/len;
//var yNorm = y/len;
//x-Axis = (1,0), so dotproduct would be: 1*xNorm + 0*yNorm
var angle = Math.acos(xNorm)* this.RAD2DEG;
return angle;
}
/**
*
* @returns {number} the distance between first and last handle point
*/
this.getArrowLength = function () {
var xLength = parent.currp3x - parent.currp0x;
var yLength = parent.currp3y - parent.currp0y;
var arrowLength = Math.sqrt(xLength*xLength + yLength*yLength);
return arrowLength;
}
/**
*
* @returns {number} the lifetime in milliseconds
*/
this.getLifetime = function () {
if(parent.isAlive) {
return new Date().getTime() - parent.creationDatetime;
}else return 0;
}
}
/*
//Runge kutta second order
function rk2(x, y, dt) { //x,y ... "real" coordinates of the current point
//dt... stepsize
//si+1 = si + F(si + F(si)*dt/2 )*dt
//get indices from coordinates for lookup in or 1D-array
var idx = Math.floor(x);
var idy = Math.floor(y);
var pos = idy * WIDTH + idx;
var pos2 = Math.floor(y + fieldData[pos] * dt * 0.5) * WIDTH + Math.floor(x + fieldData[pos] * dt * 0.5);
var xNext = x + fieldData[pos2] * dt;
var yNext = y + fieldData[pos2] * dt;
return [xNext, yNext];
}
*/
/*
//1D example: http://mtdevans.com/2013/05/fourth-order-runge-kutta-algorithm-in-javascript-with-demo/
//CODE VIA slides: https://www.cg.tuwien.ac.at/courses/Visualisierung/Folien/VisVO-2008-FlowVis-2-6Slides.pdf
//Runge kutta 4th order (THE method)
function rk4(x, y, dt) { //x,y ... "real" coordinates of the current point
//dt... stepsize
//get indices from coordinates for lookup in or 1D-array
var idx = Math.floor(x);
var idy = Math.floor(y);
var pos = idy * WIDTH + idx;
if (pos < 0 || pos >= (WIDTH * HEIGHT) - 1) {
return [-1, -1];
}
//console.log('pos' + pos);
// a = dt*F(si)
//console.log('pos a: ' + pos);
var ax = dt * fieldData[Math.floor(pos)].dirX;
var ay = dt * fieldData[Math.floor(pos)].dirY;
//b = dt*F(si+a/2)
//console.log('pos b : '+ Math.floor(pos+(ax*0.5)));
var posA = Math.floor(y + (ay * 0.5)) * WIDTH + Math.floor(x + (ax * 0.5));
if (posA < 0 || posA >= (WIDTH * HEIGHT) - 1) {
return [-1, -1];
}
//console.log('posA ' + posA);
var bx = dt * fieldData[posA].dirX;
var by = dt * fieldData[posA].dirY;
//console.log('pos c: ' + Math.floor(pos+(bx*0.5)));
var posB = Math.floor(y + (by * 0.5)) * WIDTH + Math.floor(x + (bx * 0.5));
if (posB < 0 || posB >= (WIDTH * HEIGHT) - 1) {
return [-1, -1];
}
var cx = dt * fieldData[posB].dirX;
var cy = dt * fieldData[posB].dirY;
//console.log('pos d:' + Math.floor(pos+cx));
var posC = Math.floor(y + cy) * WIDTH + Math.floor(x + cx);
if (posC < 0 || posC >= (WIDTH * HEIGHT) - 1) {
return [-1, -1];
}
var dx = dt * fieldData[posC].dirX;
var dy = dt * fieldData[posC].dirY;
var xNext = idx + (ax + 2 * bx + 2 * cx + dx) / 6; //no *dt here?
var yNext = idy + (ay + 2 * by + 2 * cy + dy) / 6;
return [Math.floor(xNext), Math.floor(yNext)];
}
*/