Doug's Compendium of Stuff
[back]
var util = require('util');
Point = function(x,y) {
return {
x: x,
y: y,
normalLeft: function() {
return Point(this.y, -this.x);
},
center: function(other_point) {
return Point((this.x + other_point.x)/2.0, (this.y + other_point.y)/2.0);
},
distance: function(other_point) {
return Math.pow(Math.pow(this.x - other_point.x, 2.0) + Math.pow(this.y + other_point.y, 2.0), 0.5);
},
polar: function() {
var theta = 0.0;
if (closeTo(this.x, 0.0)) {
if (this.y > 0) {
theta = 90.0;
} else if (this.y < 0) {
theta = 270.0;
}
} else {
theta = rad2deg(Math.atan(this.y/this.x));
if (this.x < 0.0) {
theta += 180.0;
}
if (theta < 0.0) {
theta += 360.0;
}
}
return theta;
},
angleBetween: function(p1, p2) {
vect_p1 = difference(p1,this);
vect_p2 = difference(p2,this);
theta = vect_p1.polar() - vect_p2.polar();
if (theta < 0.0) {
theta += 360.0;
}
if (theta > 180.0) {
theta = 360.0 - theta;
}
return theta;
},
equalTo: function(other_point) {
return closeTo(this.x, other_point.x) && closeTo(this.y, other_point.y);
},
to_s: function() {
return "(" + this.x + "," + this.y + ")";
},
compareTo: function(other_point) {
//console.log("Comparing self " + this.to_s() + " to " + other_point.to_s());
if (closeTo(this.x, other_point.x)) {
if (closeTo(this.y, other_point.y)) {
return 0;
}
return (this.y - other_point.y);
} else {
//console.log("x coords are different, returning " + (this.x - other_point.x));
return (this.x - other_point.x);
}
},
scale: function(factor) {
return Point(this.x * factor, this.y * factor);
}
};
}
function sum(p1, p2) {
return Point(p1.x + p2.x, p1.y + p2.y);
}
function difference(p1, p2) {
return Point(p1.x - p2.x, p1.y - p2.y);
}
function randomPoint() {
var radius = Math.random();
var theta = 360 * Math.random();
return Point(radius * Math.cos(theta), radius * Math.sin(theta));
}
function closeTo(x, y) {
return Math.abs(x-y) < 1e-7;
}
function rad2deg(a) {
return (a * 180.0) / Math.PI;
}
function infLineIntersection(base1, v1, base2, v2) {
if (closeTo(v1.x, 0.0) && closeTo(v2.y, 0.0)) {
return Point(base1.x, base2.y);
}
if (closeTo(v1.y, 0.0) && closeTo(v2.x, 0.0)) {
return Point(base2.x, base1.y);
}
var m1 = !closeTo(v1.x, 0.0) ? v1.y / v1.x : 0.0
var m2 = !closeTo(v2.x, 0.0) ? v2.y / v2.x : 0.0
if (closeTo(m1, m2)) {
return null;
}
var c1 = base1.y - m1 * base1.x;
var c2 = base2.y = m2 * base2.x;
if (closeTo(v1.x, 0.0)) {
return Point(base1.x, base1.x * m2 + c2);
}
if (closeTo(v2.x, 0.0)) {
return Point(base2.x, base2.x * m1 + c1);
}
var ix = (c2 - c1) / (m1-m2);
var iy = m1 * ix + c1;
return Point(ix,iy);
}
Circle = function() {
return {
center: null,
radius: null,
contains: function(point) {
return point.distance(this.center) < this.radius || closeTo(point.distance(this.center) - this.radius, 0.0);
}
};
}
Solver = function() {
return {
mec: Circle(),
points: [],
solve: function(a) {
a.sort(function(p1,p2) { return p1.compareTo(p2); });
for (var i in a) {
this.addPoint(a[i]);
}
return this.mec;
},
direction: function(p0, p1, p2) {
return (p0.x-p1.x)*(p2.y-p1.y) - (p2.x-p1.x)*(p0.y-p1.y);
},
halfHull: function(left, right, input, factor) {
input = input.slice(0);
input.push(right);
var half = [];
half.push(left);
for (var i in input) {
half.push(input[i]);
//console.log("HALF: " + util.inspect(half) + " LENGTH: " + half.length);
while (half.length >= 3) {
var len = half.length;
var dir = factor * this.direction(half[len-3], half[len-1], half[len-2]);
if (dir <= 0) {
half.splice(len-2,1)
} else {
break;
}
}
}
return half;
},
convexHull: function() {
var left = this.points[0];
var a = this.points.slice(1);
var right = a.pop();
var upper = [];
var lower = [];
for (var i in a) {
var p = a[i];
var dir = this.direction(left, right, p);
if (dir < 0) {
upper.push(p);
} else {
lower.push(p);
}
}
var upper_hull = this.halfHull(left, right, upper, -1);
var lower_hull = this.halfHull(left, right, lower, 1);
var combined = upper_hull.concat(lower_hull);
var ret = [];
for (var i in combined) {
var e = combined[i];
var has = false;
for (var j in ret) {
if (ret[j].equalTo(e)) {
has = true;
break;
}
}
if (!has) {
ret.push(e);
}
}
return ret;
},
addPoint: function(point) {
this.points.push(point);
if (this.mec.center) {
if (!this.mec.contains(point)) {
this.iterate();
}
} else {
this.iterate();
}
},
iterate: function() {
if (this.points.length == 1) {
this.mec.center = this.points[0];
this.mec.radius = 0;
} else if (this.points.length == 2) {
this.mec.center = this.points[0].center(this.points[1]);
this.mec.radius = this.points[0].distance(this.mec.center);
} else {
var a = this.convexHull();
var point_a = a[0];
var point_c = a[1];
while (true) {
var point_b = null;
var best_theta = 180.0;
for (var i in this.points) {
var point = this.points[i];
if (point != point_a && point != point_c) {
var theta_abc = point.angleBetween(point_a, point_c);
if (theta_abc < best_theta) {
point_b = point;
best_theta = theta_abc;
}
}
}
if (best_theta >= 90.0 || point_b == null) {
this.mec.center = point_a.center(point_c);
this.mec.radius = point_a.distance(this.mec.center);
return this.mec;
}
var ang_bca = point_c.angleBetween(point_b, point_a);
var ang_cab = point_a.angleBetween(point_c, point_b);
if (ang_bca > 90.0) {
point_c = point_b;
} else if (ang_cab <= 90.0) {
break;
} else {
point_a = point_b;
}
}
var ch1 = difference(point_b, point_a).scale(0.5);
var ch2 = difference(point_c, point_a).scale(0.5);
var n1 = ch1.normalLeft();
var n2 = ch2.normalLeft();
ch1 = sum(point_a, ch1);
ch2 = sum(point_a, ch2);
this.mec.center = infLineIntersection(ch1, n1, ch2, n2);
this.mec.radius = this.mec.center.distance(point_a);
}
return this.mec;
}
};
}
var s = Solver();
console.log("Solver: " + util.inspect(s));
for (var iter = 0; iter < 5; iter++) {
var start = new Date().getTime();
var n = Math.pow(10,iter);
for (var j = 0; j < 25; j++) {
var a = [];
for (var i = 0; i < n; i++) {
a.push(randomPoint());
}
var circle = s.solve(a);
}
var stop = new Date().getTime();
var delta = (stop - start) / 1000.0;
console.log("Points: " + n + ": " + delta);
}