Source: ArrowManager.js

/**
 * Manages all arrows
 * @param {VelocityField} velocityField - the related velocityField
 * @param {DistanceMap} distanceMap - the related distanceMap
 * @param {number} num_seed_points - the number of seed points
 * @param {number} num_arrows - number of maximum arrows
 * @constructor
 */
var ArrowManager = function (velocityField, distanceMap, num_seed_points, num_arrows) {

    var parent = this;
    //private variables
    var arrows = [];
    var arrowHeads = [];
    var seedPositions = [];
    var NUM_ARROWS = num_arrows;
    var NUM_SEED_POINTS = num_seed_points;
    var velocityField = velocityField;
    var distanceMap = distanceMap;
    var RESPAWN_DELAY = 2;
    var COLOR_DELAY = 3;
    
    var activeSeedPosition = 0;

    /**
     * inits the Seedpositions and Arrows
     */
    this.init = function () {
        initSeedPositions();
        initArrowsAtSeedPositions();
    }

    /**
     * inits the Seedpositions
     */
    var initSeedPositions = function () {
        /*
        velocityField.wheaterStations.forEach(function(wheaterStation) {
            var velocityFieldPoint = velocityField.getVelocityFieldPoint(wheaterStation.xPos, wheaterStation.yPos);
            console.log("velocity: " + velocityFieldPoint.velocity);
            seedPositions.push(velocityFieldPoint);
        });
        */


        for(var i = 0; i < NUM_SEED_POINTS; i++)
        {
            var randomX = Math.floor(Math.random() * (velocityField.widthOfField + 1));
            var randomY = Math.floor(Math.random() * (velocityField.heightOfField + 1));

            //TODO DEBUG
            //randomX = 330 + 100*i;
            //randomY = 100;

            var velocityFieldPoint = velocityField.getVelocityFieldPoint(randomX, randomY);
            seedPositions.push(velocityFieldPoint);
        }
    }

    /**
     * creates arrows at seed positions
     */
    var initArrowsAtSeedPositions = function() {
        for(var i = 0; i < NUM_ARROWS; i++) {
            var velocityFieldPoint = seedPositions[activeSeedPosition];
            //increase activeSeedPosition
            activeSeedPosition++;
            if(activeSeedPosition >= seedPositions.length)
                activeSeedPosition = 0;

            var newArrow = new Arrow(velocityFieldPoint.xPos, velocityFieldPoint.yPos, velocityFieldPoint.velocity, velocityField, distanceMap);
            newArrow.advectArrow(1);
            newArrow.integrateArrow();
            newArrow.checkAndMarkOccupied();

            arrows.push(newArrow);
            arrowHeads.push(new ArrowHead(0, 0, 0));
            console.log("ADDED ARROW");
        }
    }

    /**
     * updates all arrows
     */
    this.updateArrows = function() {
        //first advect and integrate existing arrows 
        advectArrows();
        integrateArrows();
        
        //clear distanceMap
        distanceMap.resetDistanceMap();

        //copy arrows array to order it and check collision
        //d3 needs unordered array

        var arrowsCopy = arrows.slice();
        //sort arrows by length so that long arrows live longer
        sortArrowsByLengthAndLifetime(arrowsCopy);

        //if some arrows are dead, respawn them at a different place
        checkCollisionForLivingArrows(arrowsCopy);
        respawnDeadArrows(arrowsCopy);
        calcArrowheads();
        //hack to avoid visual artifacts
        adjustOpacity();

        //TODO DEBUG output
        var countAlive = 0;
        for(var i = 0; i < arrows.length; i++) {
            if(arrows[i].isAlive)
                countAlive++;

            //console.log("lifetime: " + arrows[i].getLifetime());
        }
        //console.log("Arrows alive: " + countAlive);
    }

    this.setArrows = function (arrows) {
        parent.arrows = arrows;
    }

    /**
     *
     * @returns [Arrow] all arrows
     */
    this.getArrows = function () {
        return arrows;
    }

    /**
     *
     * @returns [ArrowHead] all Arrowheads
     */
    this.getArrowheads = function () {
        return arrowHeads;
    }

    /**
     * advects all arrows
     */
    var advectArrows = function () {
        var dt = 1;
        
        for (var i = 0; i < arrows.length; i++) {
            var arrow = arrows[i];
            arrow.advectArrow(dt);
        }
        ;
    }

    /**
     * integrates all arrows
     */
    var integrateArrows = function () {
        for (var i = 0; i < arrows.length; i++) {
            var arrow = arrows[i];
            arrow.integrateArrow();
        }
        ;
    }

    /**
     * checks collision of living arrows
     * @param {Array.<Arrow>} arrows array of arrows
     */
    var checkCollisionForLivingArrows = function (arrows) {
        for (var i = 0; i < arrows.length; i++) {
            var arrow = arrows[i];

            if(arrow.isAlive)
                arrow.checkAndMarkOccupied();
        };
    }

    /**
     * respawns dead arrows in array
     * @param {Array.<Arrow>} arrows array of arrows
     */
    var respawnDeadArrows = function (arrows) {
        for (var i = arrows.length-1; i >= 0; i--) {
            var arrow = arrows[i];


            //delay before being respawned

            if (!arrow.isAlive && arrow.respawnTime < RESPAWN_DELAY) {
                arrow.respawnTime++;
                //console.log("respawn wait: " + arrow.respawnTime);
            }

            if(!arrow.isAlive && arrow.respawnTime >= RESPAWN_DELAY) {
                console.log("respawn");
                //reposition it
                var velocityFieldPoint = seedPositions[activeSeedPosition];
                //increase activeSeedPosition
                activeSeedPosition++;
                if(activeSeedPosition >= seedPositions.length)
                    activeSeedPosition = 0;

                arrow.respawn(velocityFieldPoint.xPos, velocityFieldPoint.yPos);
                //integrate to render it at the new position
                arrow.integrateArrow();
                //check collision
                arrow.checkAndMarkOccupied();
            } 
        };
    }

    /**
     * function that regulates with a counter when a respawned arrow is set to full opacity again.
     * kinda hack-ish, but avoids visual artifacts
     */
    var adjustOpacity = function() {
        for (var i = 0; i < arrows.length; i++) {
            var arrow = arrows[i];
            //var arrowHead = arrowHeads[i];

            if(arrow.recolor) {
                arrow.recolorTime = arrow.recolorTime + 1;
            }

            if(arrow.recolor && arrow.recolorTime >= COLOR_DELAY){
                arrow.opacity = 1;
                arrow.strokeWidth = 2;
                arrow.recolorTime = 0;
                arrow.recolor = false;

                //arrowHead.opacity = 1;
                //arrowHead.strokeWidth = 2;
            }
        };
    }

    /**
     * calculates the correct position and rotation of the Arrowheads
     */
    var calcArrowheads = function () {
        for (var i = 0; i < arrows.length; i++) {
            var arrow = arrows[i]; 
            var angle = arrow.calcArrowheadRotation();
            arrowHeads[i].rotation = angle;
            arrowHeads[i].posX = arrow.currp3x;
            arrowHeads[i].posY = arrow.currp3y;
        };
    }

    /**
     * sorts arrows by length first and then by lifetime
     * @param {Array.<Arrow>} arrows array of arrows
     */
    var sortArrowsByLengthAndLifetime = function(arrows) {
        //Source: https://github.com/Teun/thenBy.js
        var firstBy=function(){function n(n){return n}function t(n){return"string"==typeof n?n.toLowerCase():n}function r(r,e){if(e="number"==typeof e?{direction:e}:e||{},"function"!=typeof r){var u=r;r=function(n){return n[u]?n[u]:""}}if(1===r.length){var i=r,o=e.ignoreCase?t:n;r=function(n,t){return o(i(n))<o(i(t))?-1:o(i(n))>o(i(t))?1:0}}return-1===e.direction?function(n,t){return-r(n,t)}:r}function e(n,t){return n=r(n,t),n.thenBy=u,n}function u(n,t){var u=this;return n=r(n,t),e(function(t,r){return u(t,r)||n(t,r)})}return e}();

        arrows.sort(
            firstBy(function(v) { return v.getArrowLength(); }, -1)
            .thenBy(function(v) { return v.getLifetime(); }, -1)
        );

    }
}