You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

989 lines
26 KiB
JavaScript

/*!
* SvgNest
* Licensed under the MIT license
*/
(function(root){
'use strict';
root.SvgNest = new SvgNest();
function SvgNest(){
var self = this;
var svg = null;
// keep a reference to any style nodes, to maintain color/fill info
this.style = null;
var parts = null;
var tree = null;
var bin = null;
var binPolygon = null;
var binBounds = null;
var nfpCache = {};
var config = {
clipperScale: 10000000,
curveTolerance: 0.3,
spacing: 0,
rotations: 4,
populationSize: 10,
mutationRate: 10,
useHoles: false,
exploreConcave: false
};
this.working = false;
var GA = null;
var best = null;
var workerTimer = null;
var progress = 0;
this.parsesvg = function(svgstring){
// reset if in progress
this.stop();
bin = null;
binPolygon = null;
tree = null;
// parse svg
svg = SvgParser.load(svgstring);
this.style = SvgParser.getStyle();
svg = SvgParser.clean();
tree = this.getParts(svg.childNodes);
//re-order elements such that deeper elements are on top, so they can be moused over
function zorder(paths){
// depth-first
var length = paths.length;
for(var i=0; i<length; i++){
if(paths[i].children && paths[i].children.length > 0){
zorder(paths[i].children);
}
}
}
return svg;
}
this.setbin = function(element){
if(!svg){
return;
}
bin = element;
}
this.config = function(c){
// clean up inputs
if(!c){
return config;
}
if(c.curveTolerance && !GeometryUtil.almostEqual(parseFloat(c.curveTolerance), 0)){
config.curveTolerance = parseFloat(c.curveTolerance);
}
if('spacing' in c){
config.spacing = parseFloat(c.spacing);
}
if(c.rotations && parseInt(c.rotations) > 0){
config.rotations = parseInt(c.rotations);
}
if(c.populationSize && parseInt(c.populationSize) > 2){
config.populationSize = parseInt(c.populationSize);
}
if(c.mutationRate && parseInt(c.mutationRate) > 0){
config.mutationRate = parseInt(c.mutationRate);
}
if('useHoles' in c){
config.useHoles = !!c.useHoles;
}
if('exploreConcave' in c){
config.exploreConcave = !!c.exploreConcave;
}
SvgParser.config({ tolerance: config.curveTolerance});
best = null;
nfpCache = {};
binPolygon = null;
GA = null;
return config;
}
// progressCallback is called when progress is made
// displayCallback is called when a new placement has been made
this.start = function(progressCallback, displayCallback){
if(!svg || !bin){
return false;
}
parts = Array.prototype.slice.call(svg.childNodes);
var binindex = parts.indexOf(bin);
if(binindex >= 0){
// don't process bin as a part of the tree
parts.splice(binindex, 1);
}
// build tree without bin
tree = this.getParts(parts.slice(0));
offsetTree(tree, 0.5*config.spacing, this.polygonOffset.bind(this));
// offset tree recursively
function offsetTree(t, offset, offsetFunction){
for(var i=0; i<t.length; i++){
var offsetpaths = offsetFunction(t[i], offset);
if(offsetpaths.length == 1){
// replace array items in place
Array.prototype.splice.apply(t[i], [0, t[i].length].concat(offsetpaths[0]));
}
if(t[i].childNodes && t[i].childNodes.length > 0){
offsetTree(t[i].childNodes, -offset, offsetFunction);
}
}
}
binPolygon = SvgParser.polygonify(bin);
binPolygon = this.cleanPolygon(binPolygon);
if(!binPolygon || binPolygon.length < 3){
return false;
}
binBounds = GeometryUtil.getPolygonBounds(binPolygon);
if(config.spacing > 0){
var offsetBin = this.polygonOffset(binPolygon, -0.5*config.spacing);
if(offsetBin.length == 1){
// if the offset contains 0 or more than 1 path, something went wrong.
binPolygon = offsetBin.pop();
}
}
binPolygon.id = -1;
// put bin on origin
var xbinmax = binPolygon[0].x;
var xbinmin = binPolygon[0].x;
var ybinmax = binPolygon[0].y;
var ybinmin = binPolygon[0].y;
for(var i=1; i<binPolygon.length; i++){
if(binPolygon[i].x > xbinmax){
xbinmax = binPolygon[i].x;
}
else if(binPolygon[i].x < xbinmin){
xbinmin = binPolygon[i].x;
}
if(binPolygon[i].y > ybinmax){
ybinmax = binPolygon[i].y;
}
else if(binPolygon[i].y < ybinmin){
ybinmin = binPolygon[i].y;
}
}
for(i=0; i<binPolygon.length; i++){
binPolygon[i].x -= xbinmin;
binPolygon[i].y -= ybinmin;
}
binPolygon.width = xbinmax-xbinmin;
binPolygon.height = ybinmax-ybinmin;
// all paths need to have the same winding direction
if(GeometryUtil.polygonArea(binPolygon) > 0){
binPolygon.reverse();
}
// remove duplicate endpoints, ensure counterclockwise winding direction
for(i=0; i<tree.length; i++){
var start = tree[i][0];
var end = tree[i][tree[i].length-1];
if(start == end || (GeometryUtil.almostEqual(start.x,end.x) && GeometryUtil.almostEqual(start.y,end.y))){
tree[i].pop();
}
if(GeometryUtil.polygonArea(tree[i]) > 0){
tree[i].reverse();
}
}
var self = this;
this.working = false;
workerTimer = setInterval(function(){
if(!self.working){
self.launchWorkers.call(self, tree, binPolygon, config, progressCallback, displayCallback);
self.working = true;
}
progressCallback(progress);
}, 100);
}
this.launchWorkers = function(tree, binPolygon, config, progressCallback, displayCallback){
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex ;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
var i,j;
if(GA === null){
// initiate new GA
var adam = tree.slice(0);
// seed with decreasing area
adam.sort(function(a, b){
return Math.abs(GeometryUtil.polygonArea(b)) - Math.abs(GeometryUtil.polygonArea(a));
});
GA = new GeneticAlgorithm(adam, binPolygon, config);
}
var individual = null;
// evaluate all members of the population
for(i=0; i<GA.population.length; i++){
if(!GA.population[i].fitness){
individual = GA.population[i];
break;
}
}
if(individual === null){
// all individuals have been evaluated, start next generation
GA.generation();
individual = GA.population[1];
}
var placelist = individual.placement;
var rotations = individual.rotation;
var ids = [];
for(i=0; i<placelist.length; i++){
ids.push(placelist[i].id);
placelist[i].rotation = rotations[i];
}
var nfpPairs = [];
var key;
var newCache = {};
for(i=0; i<placelist.length; i++){
var part = placelist[i];
key = {A: binPolygon.id, B: part.id, inside: true, Arotation: 0, Brotation: rotations[i]};
if(!nfpCache[JSON.stringify(key)]){
nfpPairs.push({A: binPolygon, B: part, key: key});
}
else{
newCache[JSON.stringify(key)] = nfpCache[JSON.stringify(key)]
}
for(j=0; j<i; j++){
var placed = placelist[j];
key = {A: placed.id, B: part.id, inside: false, Arotation: rotations[j], Brotation: rotations[i]};
if(!nfpCache[JSON.stringify(key)]){
nfpPairs.push({A: placed, B: part, key: key});
}
else{
newCache[JSON.stringify(key)] = nfpCache[JSON.stringify(key)]
}
}
}
// only keep cache for one cycle
nfpCache = newCache;
var worker = new PlacementWorker(binPolygon, placelist.slice(0), ids, rotations, config, nfpCache);
var p = new Parallel(nfpPairs, {
env: {
binPolygon: binPolygon,
searchEdges: config.exploreConcave,
useHoles: config.useHoles
},
evalPath: 'util/eval.js'
});
p.require('matrix.js');
p.require('geometryutil.js');
p.require('placementworker.js');
p.require('clipper.js');
var self = this;
var spawncount = 0;
p._spawnMapWorker = function (i, cb, done, env, wrk){
// hijack the worker call to check progress
progress = spawncount++/nfpPairs.length;
return Parallel.prototype._spawnMapWorker.call(p, i, cb, done, env, wrk);
}
p.map(function(pair){
if(!pair || pair.length == 0){
return null;
}
var searchEdges = global.env.searchEdges;
var useHoles = global.env.useHoles;
var A = rotatePolygon(pair.A, pair.key.Arotation);
var B = rotatePolygon(pair.B, pair.key.Brotation);
var nfp;
if(pair.key.inside){
if(GeometryUtil.isRectangle(A, 0.001)){
nfp = GeometryUtil.noFitPolygonRectangle(A,B);
}
else{
nfp = GeometryUtil.noFitPolygon(A,B,true,searchEdges);
}
// ensure all interior NFPs have the same winding direction
if(nfp && nfp.length > 0){
for(var i=0; i<nfp.length; i++){
if(GeometryUtil.polygonArea(nfp[i]) > 0){
nfp[i].reverse();
}
}
}
else{
// warning on null inner NFP
// this is not an error, as the part may simply be larger than the bin or otherwise unplaceable due to geometry
log('NFP Warning: ', pair.key);
}
}
else{
if(searchEdges){
nfp = GeometryUtil.noFitPolygon(A,B,false,searchEdges);
}
else{
nfp = minkowskiDifference(A,B);
}
// sanity check
if(!nfp || nfp.length == 0){
log('NFP Error: ', pair.key);
log('A: ',JSON.stringify(A));
log('B: ',JSON.stringify(B));
return null;
}
for(var i=0; i<nfp.length; i++){
if(!searchEdges || i==0){ // if searchedges is active, only the first NFP is guaranteed to pass sanity check
if(Math.abs(GeometryUtil.polygonArea(nfp[i])) < Math.abs(GeometryUtil.polygonArea(A))){
log('NFP Area Error: ', Math.abs(GeometryUtil.polygonArea(nfp[i])), pair.key);
log('NFP:', JSON.stringify(nfp[i]));
log('A: ',JSON.stringify(A));
log('B: ',JSON.stringify(B));
nfp.splice(i,1);
return null;
}
}
}
if(nfp.length == 0){
return null;
}
// for outer NFPs, the first is guaranteed to be the largest. Any subsequent NFPs that lie inside the first are holes
for(var i=0; i<nfp.length; i++){
if(GeometryUtil.polygonArea(nfp[i]) > 0){
nfp[i].reverse();
}
if(i > 0){
if(GeometryUtil.pointInPolygon(nfp[i][0], nfp[0])){
if(GeometryUtil.polygonArea(nfp[i]) < 0){
nfp[i].reverse();
}
}
}
}
// generate nfps for children (holes of parts) if any exist
if(useHoles && A.childNodes && A.childNodes.length > 0){
var Bbounds = GeometryUtil.getPolygonBounds(B);
for(var i=0; i<A.childNodes.length; i++){
var Abounds = GeometryUtil.getPolygonBounds(A.childNodes[i]);
// no need to find nfp if B's bounding box is too big
if(Abounds.width > Bbounds.width && Abounds.height > Bbounds.height){
var cnfp = GeometryUtil.noFitPolygon(A.childNodes[i],B,true,searchEdges);
// ensure all interior NFPs have the same winding direction
if(cnfp && cnfp.length > 0){
for(var j=0; j<cnfp.length; j++){
if(GeometryUtil.polygonArea(cnfp[j]) < 0){
cnfp[j].reverse();
}
nfp.push(cnfp[j]);
}
}
}
}
}
}
function log(){
if(typeof console !== "undefined") {
console.log.apply(console,arguments);
}
}
function toClipperCoordinates(polygon){
var clone = [];
for(var i=0; i<polygon.length; i++){
clone.push({
X: polygon[i].x,
Y: polygon[i].y
});
}
return clone;
};
function toNestCoordinates(polygon, scale){
var clone = [];
for(var i=0; i<polygon.length; i++){
clone.push({
x: polygon[i].X/scale,
y: polygon[i].Y/scale
});
}
return clone;
};
function minkowskiDifference(A, B){
var Ac = toClipperCoordinates(A);
ClipperLib.JS.ScaleUpPath(Ac, 10000000);
var Bc = toClipperCoordinates(B);
ClipperLib.JS.ScaleUpPath(Bc, 10000000);
for(var i=0; i<Bc.length; i++){
Bc[i].X *= -1;
Bc[i].Y *= -1;
}
var solution = ClipperLib.Clipper.MinkowskiSum(Ac, Bc, true);
var clipperNfp;
var largestArea = null;
for(i=0; i<solution.length; i++){
var n = toNestCoordinates(solution[i], 10000000);
var sarea = GeometryUtil.polygonArea(n);
if(largestArea === null || largestArea > sarea){
clipperNfp = n;
largestArea = sarea;
}
}
for(var i=0; i<clipperNfp.length; i++){
clipperNfp[i].x += B[0].x;
clipperNfp[i].y += B[0].y;
}
return [clipperNfp];
}
return {key: pair.key, value: nfp};
}).then(function(generatedNfp){
if(generatedNfp){
for(var i=0; i<generatedNfp.length; i++){
var Nfp = generatedNfp[i];
if(Nfp){
// a null nfp means the nfp could not be generated, either because the parts simply don't fit or an error in the nfp algo
var key = JSON.stringify(Nfp.key);
nfpCache[key] = Nfp.value;
}
}
}
worker.nfpCache = nfpCache;
// can't use .spawn because our data is an array
var p2 = new Parallel([placelist.slice(0)], {
env: {
self: worker
},
evalPath: 'util/eval.js'
});
p2.require('json.js');
p2.require('clipper.js');
p2.require('matrix.js');
p2.require('geometryutil.js');
p2.require('placementworker.js');
p2.map(worker.placePaths).then(function(placements){
if(!placements || placements.length == 0){
return;
}
individual.fitness = placements[0].fitness;
var bestresult = placements[0];
for(var i=1; i<placements.length; i++){
if(placements[i].fitness < bestresult.fitness){
bestresult = placements[i];
}
}
if(!best || bestresult.fitness < best.fitness){
best = bestresult;
var placedArea = 0;
var totalArea = 0;
var numParts = placelist.length;
var numPlacedParts = 0;
for(i=0; i<best.placements.length; i++){
totalArea += Math.abs(GeometryUtil.polygonArea(binPolygon));
for(var j=0; j<best.placements[i].length; j++){
placedArea += Math.abs(GeometryUtil.polygonArea(tree[best.placements[i][j].id]));
numPlacedParts++;
}
}
displayCallback(self.applyPlacement(best.placements), placedArea/totalArea, numPlacedParts, numParts);
}
else{
displayCallback();
}
self.working = false;
}, function (err) {
console.log(err);
});
}, function (err) {
console.log(err);
});
}
// assuming no intersections, return a tree where odd leaves are parts and even ones are holes
// might be easier to use the DOM, but paths can't have paths as children. So we'll just make our own tree.
this.getParts = function(paths){
var i, j;
var polygons = [];
var numChildren = paths.length;
for(i=0; i<numChildren; i++){
var poly = SvgParser.polygonify(paths[i]);
poly = this.cleanPolygon(poly);
// todo: warn user if poly could not be processed and is excluded from the nest
if(poly && poly.length > 2 && Math.abs(GeometryUtil.polygonArea(poly)) > config.curveTolerance*config.curveTolerance){
poly.source = i;
polygons.push(poly);
}
}
// turn the list into a tree
toTree(polygons);
function toTree(list, idstart){
var parents = [];
var i,j;
// assign a unique id to each leaf
var id = idstart || 0;
for(i=0; i<list.length; i++){
var p = list[i];
var ischild = false;
for(j=0; j<list.length; j++){
if(j==i){
continue;
}
if(GeometryUtil.pointInPolygon(p[0], list[j]) === true){
if(!list[j].children){
list[j].children = [];
}
list[j].children.push(p);
p.parent = list[j];
ischild = true;
break;
}
}
if(!ischild){
parents.push(p);
}
}
for(i=0; i<list.length; i++){
if(parents.indexOf(list[i]) < 0){
list.splice(i, 1);
i--;
}
}
for(i=0; i<parents.length; i++){
parents[i].id = id;
id++;
}
for(i=0; i<parents.length; i++){
if(parents[i].children){
id = toTree(parents[i].children, id);
}
}
return id;
};
return polygons;
};
// use the clipper library to return an offset to the given polygon. Positive offset expands the polygon, negative contracts
// note that this returns an array of polygons
this.polygonOffset = function(polygon, offset){
if(!offset || offset == 0 || GeometryUtil.almostEqual(offset, 0)){
return polygon;
}
var p = this.svgToClipper(polygon);
var miterLimit = 2;
var co = new ClipperLib.ClipperOffset(miterLimit, config.curveTolerance*config.clipperScale);
co.AddPath(p, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etClosedPolygon);
var newpaths = new ClipperLib.Paths();
co.Execute(newpaths, offset*config.clipperScale);
var result = [];
for(var i=0; i<newpaths.length; i++){
result.push(this.clipperToSvg(newpaths[i]));
}
return result;
};
// returns a less complex polygon that satisfies the curve tolerance
this.cleanPolygon = function(polygon){
var p = this.svgToClipper(polygon);
// remove self-intersections and find the biggest polygon that's left
var simple = ClipperLib.Clipper.SimplifyPolygon(p, ClipperLib.PolyFillType.pftNonZero);
if(!simple || simple.length == 0){
return null;
}
var biggest = simple[0];
var biggestarea = Math.abs(ClipperLib.Clipper.Area(biggest));
for(var i=1; i<simple.length; i++){
var area = Math.abs(ClipperLib.Clipper.Area(simple[i]));
if(area > biggestarea){
biggest = simple[i];
biggestarea = area;
}
}
// clean up singularities, coincident points and edges
var clean = ClipperLib.Clipper.CleanPolygon(biggest, config.curveTolerance*config.clipperScale);
if(!clean || clean.length == 0){
return null;
}
return this.clipperToSvg(clean);
}
// converts a polygon from normal float coordinates to integer coordinates used by clipper, as well as x/y -> X/Y
this.svgToClipper = function(polygon){
var clip = [];
for(var i=0; i<polygon.length; i++){
clip.push({X: polygon[i].x, Y: polygon[i].y});
}
ClipperLib.JS.ScaleUpPath(clip, config.clipperScale);
return clip;
}
this.clipperToSvg = function(polygon){
var normal = [];
for(var i=0; i<polygon.length; i++){
normal.push({x: polygon[i].X/config.clipperScale, y: polygon[i].Y/config.clipperScale});
}
return normal;
}
// returns an array of SVG elements that represent the placement, for export or rendering
this.applyPlacement = function(placement){
var i, j, k;
var clone = [];
for(i=0; i<parts.length; i++){
clone.push(parts[i].cloneNode(false));
}
var svglist = [];
for(i=0; i<placement.length; i++){
var newsvg = svg.cloneNode(false);
newsvg.setAttribute('viewBox', '0 0 '+binBounds.width+' '+binBounds.height);
newsvg.setAttribute('width',binBounds.width + 'px');
newsvg.setAttribute('height',binBounds.height + 'px');
var binclone = bin.cloneNode(false);
binclone.setAttribute('class','bin');
binclone.setAttribute('transform','translate('+(-binBounds.x)+' '+(-binBounds.y)+')');
newsvg.appendChild(binclone);
for(j=0; j<placement[i].length; j++){
var p = placement[i][j];
var part = tree[p.id];
// the original path could have transforms and stuff on it, so apply our transforms on a group
var partgroup = document.createElementNS(svg.namespaceURI, 'g');
partgroup.setAttribute('transform','translate('+p.x+' '+p.y+') rotate('+p.rotation+')');
partgroup.appendChild(clone[part.source]);
if(part.children && part.children.length > 0){
var flattened = _flattenTree(part.children, true);
for(k=0; k<flattened.length; k++){
var c = clone[flattened[k].source];
// add class to indicate hole
if(flattened[k].hole && (!c.getAttribute('class') || c.getAttribute('class').indexOf('hole') < 0)){
c.setAttribute('class',c.getAttribute('class')+' hole');
}
partgroup.appendChild(c);
}
}
newsvg.appendChild(partgroup);
}
svglist.push(newsvg);
}
// flatten the given tree into a list
function _flattenTree(t, hole){
var flat = [];
for(var i=0; i<t.length; i++){
flat.push(t[i]);
t[i].hole = hole;
if(t[i].children && t[i].children.length > 0){
flat = flat.concat(_flattenTree(t[i].children, !hole));
}
}
return flat;
}
return svglist;
}
this.stop = function(){
this.working = false;
if(workerTimer){
clearInterval(workerTimer);
}
};
}
function GeneticAlgorithm(adam, bin, config){
this.config = config || { populationSize: 10, mutationRate: 10, rotations: 4 };
this.binBounds = GeometryUtil.getPolygonBounds(bin);
// population is an array of individuals. Each individual is a object representing the order of insertion and the angle each part is rotated
var angles = [];
for(var i=0; i<adam.length; i++){
angles.push(this.randomAngle(adam[i]));
}
this.population = [{placement: adam, rotation: angles}];
while(this.population.length < config.populationSize){
var mutant = this.mutate(this.population[0]);
this.population.push(mutant);
}
}
// returns a random angle of insertion
GeneticAlgorithm.prototype.randomAngle = function(part){
var angleList = [];
for(var i=0; i<Math.max(this.config.rotations,1); i++){
angleList.push(i*(360/this.config.rotations));
}
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
angleList = shuffleArray(angleList);
for(i=0; i<angleList.length; i++){
var rotatedPart = GeometryUtil.rotatePolygon(part, angleList[i]);
// don't use obviously bad angles where the part doesn't fit in the bin
if(rotatedPart.width < this.binBounds.width && rotatedPart.height < this.binBounds.height){
return angleList[i];
}
}
return 0;
}
// returns a mutated individual with the given mutation rate
GeneticAlgorithm.prototype.mutate = function(individual){
var clone = {placement: individual.placement.slice(0), rotation: individual.rotation.slice(0)};
for(var i=0; i<clone.placement.length; i++){
var rand = Math.random();
if(rand < 0.01*this.config.mutationRate){
// swap current part with next part
var j = i+1;
if(j < clone.placement.length){
var temp = clone.placement[i];
clone.placement[i] = clone.placement[j];
clone.placement[j] = temp;
}
}
rand = Math.random();
if(rand < 0.01*this.config.mutationRate){
clone.rotation[i] = this.randomAngle(clone.placement[i]);
}
}
return clone;
}
// single point crossover
GeneticAlgorithm.prototype.mate = function(male, female){
var cutpoint = Math.round(Math.min(Math.max(Math.random(), 0.1), 0.9)*(male.placement.length-1));
var gene1 = male.placement.slice(0,cutpoint);
var rot1 = male.rotation.slice(0,cutpoint);
var gene2 = female.placement.slice(0,cutpoint);
var rot2 = female.rotation.slice(0,cutpoint);
var i;
for(i=0; i<female.placement.length; i++){
if(!contains(gene1, female.placement[i].id)){
gene1.push(female.placement[i]);
rot1.push(female.rotation[i]);
}
}
for(i=0; i<male.placement.length; i++){
if(!contains(gene2, male.placement[i].id)){
gene2.push(male.placement[i]);
rot2.push(male.rotation[i]);
}
}
function contains(gene, id){
for(var i=0; i<gene.length; i++){
if(gene[i].id == id){
return true;
}
}
return false;
}
return [{placement: gene1, rotation: rot1},{placement: gene2, rotation: rot2}];
}
GeneticAlgorithm.prototype.generation = function(){
// Individuals with higher fitness are more likely to be selected for mating
this.population.sort(function(a, b){
return a.fitness - b.fitness;
});
// fittest individual is preserved in the new generation (elitism)
var newpopulation = [this.population[0]];
while(newpopulation.length < this.population.length){
var male = this.randomWeightedIndividual();
var female = this.randomWeightedIndividual(male);
// each mating produces two children
var children = this.mate(male, female);
// slightly mutate children
newpopulation.push(this.mutate(children[0]));
if(newpopulation.length < this.population.length){
newpopulation.push(this.mutate(children[1]));
}
}
this.population = newpopulation;
}
// returns a random individual from the population, weighted to the front of the list (lower fitness value is more likely to be selected)
GeneticAlgorithm.prototype.randomWeightedIndividual = function(exclude){
var pop = this.population.slice(0);
if(exclude && pop.indexOf(exclude) >= 0){
pop.splice(pop.indexOf(exclude),1);
}
var rand = Math.random();
var lower = 0;
var weight = 1/pop.length;
var upper = weight;
for(var i=0; i<pop.length; i++){
// if the random number falls between lower and upper bounds, select this individual
if(rand > lower && rand < upper){
return pop[i];
}
lower = upper;
upper += 2*weight * ((pop.length-i)/pop.length);
}
return pop[0];
}
})(window);