Improve Circularize Action

This commit is contained in:
Paul Mach
2013-09-25 13:42:42 -07:00
committed by John Firebaugh
parent e8b9d95e0f
commit be30344cfd
2 changed files with 133 additions and 65 deletions
+83 -41
View File
@@ -1,60 +1,102 @@
iD.actions.Circularize = function(wayId, projection, count) {
count = count || 12;
function closestIndex(nodes, loc) {
var idx, min = Infinity, dist;
for (var i = 0; i < nodes.length; i++) {
dist = iD.geo.dist(nodes[i].loc, loc);
if (dist < min) {
min = dist;
idx = i;
}
}
return idx;
}
iD.actions.Circularize = function(wayId, projection, maxAngle) {
maxAngle = (maxAngle || 20) * Math.PI / 180;
var action = function(graph) {
var way = graph.entity(wayId),
nodes = _.uniq(graph.childNodes(way)),
keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length != 1; }),
points = nodes.map(function(n) { return projection(n.loc); }),
keyPoints = keyNodes.map(function(n) { return projection(n.loc); }),
centroid = d3.geom.polygon(points).centroid(),
radius = d3.median(points, function(p) {
return iD.geo.dist(centroid, p);
}),
ids = [],
sign = d3.geom.polygon(points).area() > 0 ? -1 : 1;
radius = d3.median(points, function(p) { return iD.geo.dist(centroid, p); }),
sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
ids;
for (var i = 0; i < count; i++) {
var node,
loc = projection.invert([
centroid[0] + Math.cos(sign * (i / 12) * Math.PI * 2) * radius,
centroid[1] + Math.sin(sign * (i / 12) * Math.PI * 2) * radius]);
// we need atleast two key nodes for the algorithm to work
if (!keyNodes.length) {
keyNodes = [nodes[0]];
keyPoints = [points[0]];
}
if (nodes.length) {
var idx = closestIndex(nodes, loc);
node = nodes[idx];
nodes.splice(idx, 1);
} else {
node = iD.Node();
if (keyNodes.length == 1) {
var index = nodes.indexOf(keyNodes[0]),
oppositeIndex = Math.floor((index + nodes.length / 2) % nodes.length);
keyNodes.push(nodes[oppositeIndex]);
keyPoints.push(points[oppositeIndex]);
}
// key points and nodes are those connected to the ways,
// they are projected onto the circle, inbetween nodes are moved
// to constant internals between key nodes, extra inbetween nodes are
// added if necessary.
for (var i = 0; i < keyPoints.length; i++) {
var nextKeyNodeIndex = (i + 1) % keyNodes.length,
startNodeIndex = nodes.indexOf(keyNodes[i]),
endNodeIndex = nodes.indexOf(keyNodes[nextKeyNodeIndex]),
numberNewPoints = -1,
indexRange = endNodeIndex - startNodeIndex,
distance, totalAngle, eachAngle, startAngle, endAngle,
angle, loc, node, j;
if (indexRange < 0) {
indexRange += nodes.length;
}
ids.push(node.id);
graph = graph.replace(node.move(loc));
// position this key node
distance = iD.geo.dist(centroid, keyPoints[i]);
keyPoints[i] = [
centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius];
graph = graph.replace(keyNodes[i].move(projection.invert(keyPoints[i])));
// figure out the between delta angle we want to match to
startAngle = Math.atan2(keyPoints[i][1] - centroid[1], keyPoints[i][0] - centroid[0]);
endAngle = Math.atan2(keyPoints[nextKeyNodeIndex][1] - centroid[1], keyPoints[nextKeyNodeIndex][0] - centroid[0]);
totalAngle = endAngle - startAngle;
// detects looping around -pi/pi
if (totalAngle*sign > 0) {
totalAngle = -sign * (2 * Math.PI - Math.abs(totalAngle));
}
do {
numberNewPoints++;
eachAngle = totalAngle / (indexRange + numberNewPoints);
} while (Math.abs(eachAngle) > maxAngle);
// move existing points
for (j = 1; j < indexRange; j++) {
angle = startAngle + j * eachAngle;
loc = projection.invert([
centroid[0] + Math.cos(angle)*radius,
centroid[1] + Math.sin(angle)*radius]);
node = nodes[(j + startNodeIndex) % nodes.length].move(loc);
graph = graph.replace(node);
}
// add new inbetween nodes if necessary
for (j = 0; j < numberNewPoints; j++) {
angle = startAngle + (indexRange + j) * eachAngle;
loc = projection.invert([
centroid[0] + Math.cos(angle) * radius,
centroid[1] + Math.sin(angle) * radius]);
node = iD.Node({loc: loc});
graph = graph.replace(node);
nodes.splice(endNodeIndex + j, 0, node);
}
}
// update the way to have all the new nodes
ids = nodes.map(function(n) { return n.id; });
ids.push(ids[0]);
way = way.update({nodes: ids});
graph = graph.replace(way);
for (i = 0; i < nodes.length; i++) {
graph.parentWays(nodes[i]).forEach(function(parent) {
graph = graph.replace(parent.replaceNode(nodes[i].id,
ids[closestIndex(graph.childNodes(way), nodes[i].loc)]));
});
graph = iD.actions.DeleteNode(nodes[i].id)(graph);
}
return graph;
};
+50 -24
View File
@@ -1,7 +1,7 @@
describe("iD.actions.Circularize", function () {
var projection = d3.geo.mercator();
it("creates a circle of 12 nodes", function () {
it("creates nodes if necessary", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a', loc: [0, 0]}),
'b': iD.Node({id: 'b', loc: [2, 0]}),
@@ -12,7 +12,7 @@ describe("iD.actions.Circularize", function () {
graph = iD.actions.Circularize('-', projection)(graph);
expect(graph.entity('-').nodes).to.have.length(13);
expect(graph.entity('-').nodes).to.have.length(20);
});
it("reuses existing nodes", function () {
@@ -21,43 +21,69 @@ describe("iD.actions.Circularize", function () {
'b': iD.Node({id: 'b', loc: [2, 0]}),
'c': iD.Node({id: 'c', loc: [2, 2]}),
'd': iD.Node({id: 'd', loc: [0, 2]}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']})
'e': iD.Node({id: 'e', loc: [0, 2]}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']})
}),
nodes;
graph = iD.actions.Circularize('-', projection)(graph);
nodes = graph.entity('-').nodes;
expect(nodes.indexOf('a')).to.be.gte(0);
expect(nodes.indexOf('b')).to.be.gte(0);
expect(nodes.indexOf('c')).to.be.gte(0);
expect(nodes.indexOf('d')).to.be.gte(0);
expect(nodes.indexOf('e')).to.be.gte(0);
});
it("limits movement of nodes that are members of other ways", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a', loc: [2, 2]}),
'b': iD.Node({id: 'b', loc: [-2, 2]}),
'c': iD.Node({id: 'c', loc: [-2, -2]}),
'd': iD.Node({id: 'd', loc: [2, -2]}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}),
'=': iD.Way({id: '=', nodes: ['d']})
});
graph = iD.actions.Circularize('-', projection)(graph);
expect(graph.entity('-').nodes.slice(0, 4).sort()).to.eql(['a', 'b', 'c', 'd']);
expect(iD.geo.dist(graph.entity('d').loc, [2, -2])).to.be.lt(0.5);
});
it("deletes unused nodes that are not members of other ways", function () {
function angle(point1, point2, center) {
var vector1 = [point1[0] - center[0], point1[1] - center[1]],
vector2 = [point2[0] - center[0], point2[1] - center[1]],
distance;
distance = iD.geo.dist(vector1, [0, 0]);
vector1 = [vector1[0] / distance, vector1[1] / distance];
distance = iD.geo.dist(vector2, [0, 0]);
vector2 = [vector2[0] / distance, vector2[1] / distance];
return 180 / Math.PI * Math.acos(vector1[0] * vector2[0] + vector1[1] * vector2[1]);
}
it("creates circle respecting min-angle limit", function() {
var graph = iD.Graph({
'a': iD.Node({id: 'a', loc: [0, 0]}),
'b': iD.Node({id: 'b', loc: [2, 0]}),
'c': iD.Node({id: 'c', loc: [2, 2]}),
'd': iD.Node({id: 'd', loc: [0, 2]}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']})
});
}),
centroid, points;
graph = iD.actions.Circularize('-', projection, 3)(graph);
graph = iD.actions.Circularize('-', projection, 20)(graph);
points = _.pluck(graph.childNodes(graph.entity('-')), 'loc').map(projection);
centroid = d3.geom.polygon(points).centroid();
expect(graph.hasEntity('a')).to.be.undefined;
});
for (var i = 0; i < points.length - 1; i++) {
expect(angle(points[i], points[i+1], centroid)).to.be.lte(20);
}
it("reconnects unused nodes that are members of other ways", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a', loc: [0, 0]}),
'b': iD.Node({id: 'b', loc: [2, 0]}),
'c': iD.Node({id: 'c', loc: [2, 2]}),
'd': iD.Node({id: 'd', loc: [0, 2]}),
'e': iD.Node({id: 'e', loc: [1, 1]}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}),
'=': iD.Way({id: '=', nodes: ['a']})
});
graph = iD.actions.Circularize('-', projection, 3)(graph);
expect(graph.hasEntity('a')).to.be.undefined;
expect(graph.entity('=').nodes).to.eql(['c']);
expect(angle(points[points.length - 1], points[0], centroid)).to.be.lte(20);
});
function area(id, graph) {