mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-18 14:45:12 +02:00
Improve Circularize Action
This commit is contained in:
committed by
John Firebaugh
parent
e8b9d95e0f
commit
be30344cfd
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user