<html>
<head>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
      crossorigin="anonymous">
    </script>

    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <script>
        class Gene
        {
            /**
             * Constructs a new Gene to store in a chromosome.
             * @param min minimum value that this gene can store
             * @param max value this gene can possibly be
             * @param value normalized value
             */
            constructor(min, max, value)
            {
                this.min = min;
                this.max = max;
                this.value = value;
            }

            /**
             * De-normalizes the value of the gene
             * @returns {*}
             */
            getRealValue()
            {
                return (this.max - this.min) * this.value + this.min;
            }

            getValue()
            {
                return this.value;
            }

            setValue(val)
            {
                this.value = val;
            }

            makeClone()
            {
                return new Gene(this.min, this.max, this.value);
            }

            makeRandomGene()
            {
                return new Gene(this.min, this.max, Math.random());
            }
        }


        class Chromosome
        {
            /**
             * Constructs a chromosome by making a copy of
             * a list of genes.
             * @param geneArray
             */
            constructor(geneArray)
            {
                this.genes = [];
                for(let i = 0; i < geneArray.length; i++)
                {
                    this.genes.push(geneArray[i].makeClone());
                }
            }

            getGenes()
            {
                return this.genes;
            }

            /**
             * Mutates a random gene.
             */
            mutate()
            {
                this.genes[Math.round(Math.random() * (this.genes.length-1))].setValue(Math.random());
            }

            /**
             * Creates a totally new chromosome with same
             * genetic structure as this chromosome but different
             * values.
             * @returns {Chromosome}
             */
            createRandomChromosome()
            {
                let geneAr = [];
                for(let i = 0; i < this.genes.length; i++)
                {
                    geneAr.push(this.genes[i].makeRandomGene());
                }
                return new Chromosome(geneAr);
            }
        }


        /**
         * Mates two chromosomes using the blending method
         * and returns a list of 2 offspring.
         * @param father
         * @param mother
         * @returns {Chromosome[]}
         */
        const breed = function(father, mother)
        {
            let son = new Chromosome(father.getGenes());
            let daughter = new Chromosome(mother.getGenes());

            for(let i = 0;i < son.getGenes().length; i++)
            {
                let blendCoef = Math.random();
                blendGene(son.getGenes()[i], daughter.getGenes()[i], blendCoef);
            }
            return [son, daughter];
        };


        /**
         * Blends two genes together based on a random blend
         * coefficient.
         **/
        const blendGene = function(gene1, gene2, blendCoef)
        {
            let value1 = (blendCoef * gene1.getValue()) +
                (gene2.getValue() * (1- blendCoef));
            let value2 = ((1-blendCoef) * gene1.getValue()) +
                (gene2.getValue() * blendCoef);

            gene1.setValue(value1);
            gene2.setValue(value2);
        };

        /**
         * Helper function to sort an array
         *
         * @param prop name of JSON property to sort by
         * @returns {function(*, *): number}
         */
        function predicateBy(prop)
        {
            return function(a,b)
            {
                var result;
                if(a[prop] > b[prop])
                {
                    result =  1;
                }
                else if(a[prop] < b[prop])
                {
                    result = -1;
                }
                return result;
            }
        }

        /**
         * Function which computes the fitness of everyone in the
         * population and returns the most fit survivors. Method
         * known as elitism.
         *
         * @param population
         * @param keepNumber
         * @param fitnessFunction
         * @returns {{average: number,
         * survivors: Array, bestFit: Chromosome }}
         */
        const naturalSelection = function(population, keepNumber, fitnessFunction)
        {
            let fitnessArray = [];
            let total = 0;
            for(let i = 0; i < population.length; i++)
            {
                const fitness = fitnessFunction(population[i]);
                console.log(fitness);
                fitnessArray.push({fit:fitness, chrom: population[i]});
                total+= fitness;
            }

            fitnessArray.sort(predicateBy("fit"));

            let survivors = [];
            let bestFitness = fitnessArray[0].fit;
            let bestChromosome = fitnessArray[0].chrom;
            for(let i = 0; i < keepNumber; i++)
            {
                survivors.push(fitnessArray[i].chrom);
            }
            return {average: total/population.length, survivors: survivors, bestFit: bestFitness, bestChrom: bestChromosome};
        };


        /**
         * Randomly  everyone in the population
         *
         * @param population
         * @param desiredPopulationSize
         */
        const matePopulation = function(population, desiredPopulationSize)
        {
            const originalLength = population.length;
            while(population.length < desiredPopulationSize)
            {
                let index1 = Math.round(Math.random() * (originalLength-1));
                let index2 = Math.round(Math.random() * (originalLength-1));
                if(index1 !== index2)
                {
                    const babies = breed(population[index1], population[index2]);
                    population.push(babies[0]);
                    population.push(babies[1]);
                }
            }
        };

        /**
         * Randomly mutates the population
         **/
        const mutatePopulation = function(population, mutatePercentage)
        {
            if(population.length >= 2)
            {
                let mutations = mutatePercentage *
                                population.length *
                                population[0].getGenes().length;
                for(let i = 0; i < mutations; i++)
                {
                    population[i].mutate();
                }
            }
            else
            {
                console.log("Error, population too small to mutate");
            }
        };

        /**
         * Introduces x random chromosomes to the population.
         * @param population
         * @param immigrationSize
         */
        const newBlood = function(population, immigrationSize)
        {
            for(let i = 0; i < immigrationSize; i++)
            {
                let geneticChromosome = population[0];
                population.push(geneticChromosome.createRandomChromosome());
            }
        };


        let costx = Math.random() * 10;
        let costy = Math.random() * 10;

        /** Defines the cost as the "distance" to a 2-d point.
         * @param chromosome
         * @returns {number}
         */
        const basicCostFunction = function(chromosome)
        {
            return Math.abs(chromosome.getGenes()[0].getRealValue() - costx) +
                Math.abs(chromosome.getGenes()[1].getRealValue() - costy);
        };


        /**
         * Creates a totally random population based  on a desired size
         * and a prototypical chromosome.
         *
         * @param geneticChromosome
         * @param populationSize
         * @returns {Array}
         */
        const createRandomPopulation = function(geneticChromosome, populationSize)
        {
            let population = [];
            for(let i = 0; i < populationSize; i++)
            {
                population.push(geneticChromosome.createRandomChromosome());
            }
            return population;
        };


        /**
         * Runs the genetic algorithm by going through the processes of
         * natural selection, mutation, mating, and immigrations. This
         * process will continue until an adequately performing chromosome
         * is found or a generation threshold is passed.
         *
         * @param geneticChromosome Prototypical chromosome: used so algo knows
         *                            what the dna of the population looks like.
         * @param costFunction Function which defines how bad a Chromosome is
         * @param populationSize Desired population size for population
         * @param maxGenerations Cut off level for number of generations to run
         * @param desiredCost Sufficient cost to terminate program at
         * @param mutationRate Number between [0,1] representing proportion of genes
         * to mutate each generation
         * @param keepNumber Number of Organisms which survive each generation
         * @param newBloodNumber Number of random immigrants to introduce into
         * the population each generation.
         * @returns {*}
         */
        const runGeneticOptimization = function(geneticChromosome, costFunction,
                                                populationSize, maxGenerations,
                                                desiredCost, mutationRate, keepNumber,
                                                newBloodNumber)
        {
            let population = createRandomPopulation(geneticChromosome, populationSize);
            let generation = 0;
            let bestCost = Number.MAX_VALUE;
            let bestChromosome = geneticChromosome;
            do
            {
                matePopulation(population, populationSize);
                newBlood(population, newBloodNumber);
                mutatePopulation(population, mutationRate);
                let generationResult = naturalSelection(population, keepNumber, costFunction);

                if(bestCost > generationResult.bestFit)
                {
                    bestChromosome = generationResult.bestChrom;
                    bestCost = generationResult.bestFit;
                }
                population = generationResult.survivors;

                generation++;
                console.log("Generation " + generation + " Best Cost: " + bestCost);
            }while(generation < maxGenerations && bestCost > desiredCost);
            return bestChromosome;
        };


        /**
         * Ugly globals used to keep track of population state for the graph.
         */
        let genericChromosomeG, costFunctionG,
                        populationSizeG, maxGenerationsG,
                        desiredCostG, mutationRateG, keepNumberG,
                        newBloodNumberG, populationG, generationG,
                        bestCostG = Number.MAX_VALUE, bestChromosomeG = genericChromosomeG;
        const runGeneticOptimizationForGraph = function()
        {
            let generationResult = naturalSelection(populationG, keepNumberG, costFunctionG);

            stats.push([generationG, generationResult.bestFit, generationResult.average]);

            if(bestCostG > generationResult.bestFit)
            {
                bestChromosomeG = generationResult.bestChrom;
                bestCostG = generationResult.bestFit;
            }
            populationG = generationResult.survivors;

            generationG++;

            console.log("Generation " + generationG + " Best Cost: " + bestCostG);

            console.log(generationResult);

            matePopulation(populationG, populationSizeG);
            newBlood(populationG, newBloodNumberG);
            mutatePopulation(populationG, mutationRateG);

            createGraph();
        };


        let stats = [];
        const createGraph = function()
        {
            var dataPoints = [];

            console.log(dataPoints);

            var data = new google.visualization.DataTable();
            data.addColumn('number', 'Gene 1');
            data.addColumn('number', 'Gene 2');

            for(let i = 0; i < populationG.length; i++)
            {
                data.addRow([populationG[i].getGenes()[0].getRealValue(),
                        populationG[i].getGenes()[1].getRealValue()]);
            }

            var options = {
              title: 'Genetic Evolution On Two Genes Generation: ' + generationG,
              hAxis: {title: 'Gene 1', minValue: 0, maxValue: 10},
              vAxis: {title: 'Gene 2', minValue: 0, maxValue: 10},
            };

            var chart = new google.visualization.ScatterChart(document.getElementById('chart_div'));

            chart.draw(data, options);

            //line chart stuff
              var line_data = new google.visualization.DataTable();
            line_data.addColumn('number', 'Generation');
              line_data.addColumn('number', 'Best');
              line_data.addColumn('number', 'Average');

                  line_data.addRows(stats);

              console.log(stats);

              var lineChartOptions = {
                hAxis: {
                  title: 'Generation'
                },
                vAxis: {
                  title: 'Cost'
                },
                colors: ['#AB0D06', '#007329']
              };

              var chart = new google.visualization.LineChart(document.getElementById('line_chart'));
              chart.draw(line_data, lineChartOptions);
        };


        let gene1 = new Gene(1,10,10);
        let gene2 = new Gene(1,10,0.4);
        let geneList = [gene1, gene2];

        let exampleOrganism = new Chromosome(geneList);


        genericChromosomeG = exampleOrganism;
        costFunctionG = basicCostFunction;
        populationSizeG = 100;
        maxGenerationsG = 30;
        desiredCostG = 0.00001;
        mutationRateG = 0.3;
        keepNumberG = 30;
        newBloodNumberG = 10;
        generationG = 0;


        function verifyForm()
        {
            if(Number($("#populationSize").val()) <= 1)
            {
                alert("Population size must be greater than one.");
                return false;
            }

            if(Number($("#mutationRate").val()) > 1 ||
                Number($("#mutationRate").val()) < 0)
            {
                alert("Mutation rate must be between zero and one.");
                return false;
            }

            if(Number($("#survivalSize").val()) < 0)
            {
                alert("Survival size can't be less than one.");
                return false;
            }

            if(Number($("#newBlood").val()) < 0)
            {
                alert("New organisms can't be a negative number.");
                return false;
            }
            return true;
        }

        function resetPopulation()
        {
            if(verifyForm())
            {
                stats = [];
                autoRunning = false;
                $("#runAutoOptimizer").val("Auto Run");

                populationSizeG = $("#populationSize").val();
                mutationRateG = $("#mutationRate").val();
                keepNumberG = $("#survivalSize").val();
                newBloodNumberG = $("#newBlood").val();
                generationG = 0;

                populationG = createRandomPopulation(genericChromosomeG, populationSizeG);
                createGraph();
            }
        }

        populationG = createRandomPopulation(genericChromosomeG, populationSizeG);


        window.onload = function (){
            google.charts.load('current', {packages: ['corechart', 'line']});
            google.charts.load('current', {'packages':['corechart']}).then(function()
            {
                createGraph();
            })
        };


        let autoRunning = false;
        function runAutoOptimizer()
        {
            if(autoRunning === true)
            {
                runGeneticOptimizationForGraph();
                setTimeout(runAutoOptimizer, 1000);
            }
        }

        function startStopAutoRun()
        {
            autoRunning = !autoRunning;

            if(autoRunning)
            {
                $("#runAutoOptimizer").val("Stop Auto Run");
            }
            else
            {
                $("#runAutoOptimizer").val("Resume Auto Run");
            }

            runAutoOptimizer();
        }

        //runGeneticOptimization(exampleOrganism, basicCostFunction, 100, 50, 0.01, 0.3, 20, 10);

    </script>

</head>


<body>
    <div id="chart_div" style="width: 900px; height: 500px;"></div>

    <div id="line_chart"></div>

    <input class='btn btn-primary' id="runOptimizer" onclick='runGeneticOptimizationForGraph()' type="button" value="Next Generation">
    <input class='btn btn-primary' id="runAutoOptimizer" onclick='startStopAutoRun()' type="button" value="Auto Run">

    <div class="card">
        <div class="card-header">
            <h2>Population Variables</h2>
        </div>
        <form class="card-body">
            <div class="row p-2">
                <div class="col">
                    <label for="populationSize">Population Size</label>
                    <input type="text" class="form-control" value="100" id="populationSize" placeholder="Population Size" required>
                </div>
                <div class="col">
                    <label for="populationSize">Survival Size</label>
                    <input type="text" class="form-control" value="20" id="survivalSize" placeholder="Survival Size" required>
                </div>
            </div>
            <div class="row p-2">
                <div class="col">
                    <label for="populationSize">Mutation Rate</label>
                    <input type="text" class="form-control" value="0.03" id="mutationRate" placeholder="Mutation Rate" required>
                </div>
                <div class="col">
                    <label for="populationSize">New Organisms Per Generation</label>
                    <input type="text" class="form-control" value="5" id="newBlood" placeholder="New Organisms" required>
                </div>
            </div>
            <br>
            <input class='btn btn-primary' id="reset" onclick='resetPopulation()' type="button" value="Reset Population">
        </form>
    </div>
</body>


</html>