Source: Arrow.js

/**
 *
 * @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)];
     }
     */