mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-31 01:09:22 +02:00
Merge pull request #2194 from bhousel/bhousel-operations
Improvements to circularize action
This commit is contained in:
1
Makefile
1
Makefile
@@ -115,6 +115,7 @@ D3_FILES = \
|
||||
node_modules/d3/src/geo/path.js \
|
||||
node_modules/d3/src/geo/stream.js \
|
||||
node_modules/d3/src/geom/polygon.js \
|
||||
node_modules/d3/src/geom/hull.js \
|
||||
node_modules/d3/src/selection/index.js \
|
||||
node_modules/d3/src/transition/index.js \
|
||||
node_modules/d3/src/xhr/index.js \
|
||||
|
||||
@@ -2,12 +2,17 @@ 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)),
|
||||
var way = graph.entity(wayId);
|
||||
|
||||
if (!way.isConvex(graph)) {
|
||||
graph = action.makeConvex(graph);
|
||||
}
|
||||
|
||||
var 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(),
|
||||
centroid = (points.length === 2) ? iD.geo.interp(points[0], points[1], 0.5) : d3.geom.polygon(points).centroid(),
|
||||
radius = d3.median(points, function(p) { return iD.geo.euclideanDistance(centroid, p); }),
|
||||
sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
|
||||
ids;
|
||||
@@ -28,16 +33,19 @@ iD.actions.Circularize = function(wayId, projection, maxAngle) {
|
||||
|
||||
// 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
|
||||
// to constant intervals 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]),
|
||||
startNode = keyNodes[i],
|
||||
endNode = keyNodes[nextKeyNodeIndex],
|
||||
startNodeIndex = nodes.indexOf(startNode),
|
||||
endNodeIndex = nodes.indexOf(endNode),
|
||||
numberNewPoints = -1,
|
||||
indexRange = endNodeIndex - startNodeIndex,
|
||||
distance, totalAngle, eachAngle, startAngle, endAngle,
|
||||
angle, loc, node, j;
|
||||
angle, loc, node, j,
|
||||
inBetweenNodes = [];
|
||||
|
||||
if (indexRange < 0) {
|
||||
indexRange += nodes.length;
|
||||
@@ -45,6 +53,7 @@ iD.actions.Circularize = function(wayId, projection, maxAngle) {
|
||||
|
||||
// position this key node
|
||||
distance = iD.geo.euclideanDistance(centroid, keyPoints[i]);
|
||||
if (distance === 0) { distance = 1e-4; }
|
||||
keyPoints[i] = [
|
||||
centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
|
||||
centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius];
|
||||
@@ -56,7 +65,7 @@ iD.actions.Circularize = function(wayId, projection, maxAngle) {
|
||||
totalAngle = endAngle - startAngle;
|
||||
|
||||
// detects looping around -pi/pi
|
||||
if (totalAngle*sign > 0) {
|
||||
if (totalAngle * sign > 0) {
|
||||
totalAngle = -sign * (2 * Math.PI - Math.abs(totalAngle));
|
||||
}
|
||||
|
||||
@@ -87,7 +96,40 @@ iD.actions.Circularize = function(wayId, projection, maxAngle) {
|
||||
graph = graph.replace(node);
|
||||
|
||||
nodes.splice(endNodeIndex + j, 0, node);
|
||||
inBetweenNodes.push(node.id);
|
||||
}
|
||||
|
||||
// Check for other ways that share these keyNodes..
|
||||
// If keyNodes are adjacent in both ways,
|
||||
// we can add inBetween nodes to that shared way too..
|
||||
if (indexRange === 1 && inBetweenNodes.length) {
|
||||
var startIndex1 = way.nodes.lastIndexOf(startNode.id),
|
||||
endIndex1 = way.nodes.lastIndexOf(endNode.id),
|
||||
wayDirection1 = (endIndex1 - startIndex1);
|
||||
if (wayDirection1 < -1) { wayDirection1 = 1;}
|
||||
|
||||
/*jshint -W083 */
|
||||
_.each(_.without(graph.parentWays(keyNodes[i]), way), function(sharedWay) {
|
||||
if (sharedWay.areAdjacent(startNode.id, endNode.id)) {
|
||||
var startIndex2 = sharedWay.nodes.lastIndexOf(startNode.id),
|
||||
endIndex2 = sharedWay.nodes.lastIndexOf(endNode.id),
|
||||
wayDirection2 = (endIndex2 - startIndex2),
|
||||
insertAt = endIndex2;
|
||||
if (wayDirection2 < -1) { wayDirection2 = 1;}
|
||||
|
||||
if (wayDirection1 !== wayDirection2) {
|
||||
inBetweenNodes.reverse();
|
||||
insertAt = startIndex2;
|
||||
}
|
||||
for (j = 0; j < inBetweenNodes.length; j++) {
|
||||
sharedWay = sharedWay.addNode(inBetweenNodes[j], insertAt + j);
|
||||
}
|
||||
graph = graph.replace(sharedWay);
|
||||
}
|
||||
});
|
||||
/*jshint +W083 */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// update the way to have all the new nodes
|
||||
@@ -100,6 +142,38 @@ iD.actions.Circularize = function(wayId, projection, maxAngle) {
|
||||
return graph;
|
||||
};
|
||||
|
||||
action.makeConvex = function(graph) {
|
||||
var way = graph.entity(wayId),
|
||||
nodes = _.uniq(graph.childNodes(way)),
|
||||
points = nodes.map(function(n) { return projection(n.loc); }),
|
||||
sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
|
||||
hull = d3.geom.hull(points);
|
||||
|
||||
// D3 convex hulls go counterclockwise..
|
||||
if (sign === -1) {
|
||||
nodes.reverse();
|
||||
points.reverse();
|
||||
}
|
||||
|
||||
for (var i = 0; i < hull.length - 1; i++) {
|
||||
var startIndex = points.indexOf(hull[i]),
|
||||
endIndex = points.indexOf(hull[i+1]),
|
||||
indexRange = (endIndex - startIndex);
|
||||
|
||||
if (indexRange < 0) {
|
||||
indexRange += nodes.length;
|
||||
}
|
||||
|
||||
// move interior nodes to the surface of the convex hull..
|
||||
for (var j = 1; j < indexRange; j++) {
|
||||
var point = iD.geo.interp(hull[i], hull[i+1], j / indexRange),
|
||||
node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));
|
||||
graph = graph.replace(node);
|
||||
}
|
||||
}
|
||||
return graph;
|
||||
};
|
||||
|
||||
action.disabled = function(graph) {
|
||||
if (!graph.entity(wayId).isClosed())
|
||||
return 'not_closed';
|
||||
|
||||
@@ -55,6 +55,30 @@ _.extend(iD.Way.prototype, {
|
||||
return this.nodes.length > 0 && this.first() === this.last();
|
||||
},
|
||||
|
||||
isConvex: function(resolver) {
|
||||
if (!this.isClosed() || this.isDegenerate()) return null;
|
||||
|
||||
var nodes = _.uniq(resolver.childNodes(this)),
|
||||
coords = _.pluck(nodes, 'loc'),
|
||||
curr = 0, prev = 0;
|
||||
|
||||
for (var i = 0; i < coords.length; i++) {
|
||||
var o = coords[(i+1) % coords.length],
|
||||
a = coords[i],
|
||||
b = coords[(i+2) % coords.length],
|
||||
res = iD.geo.cross(o, a, b);
|
||||
|
||||
curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;
|
||||
if (curr === 0) {
|
||||
continue;
|
||||
} else if (prev && curr !== prev) {
|
||||
return false;
|
||||
}
|
||||
prev = curr;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
isArea: function() {
|
||||
if (this.tags.area === 'yes')
|
||||
return true;
|
||||
|
||||
@@ -9,6 +9,13 @@ iD.geo.interp = function(p1, p2, t) {
|
||||
p1[1] + (p2[1] - p1[1]) * t];
|
||||
};
|
||||
|
||||
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
|
||||
// Returns a positive value, if OAB makes a counter-clockwise turn,
|
||||
// negative for clockwise turn, and zero if the points are collinear.
|
||||
iD.geo.cross = function(o, a, b) {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||
};
|
||||
|
||||
// http://jsperf.com/id-dist-optimization
|
||||
iD.geo.euclideanDistance = function(a, b) {
|
||||
var x = a[0] - b[0], y = a[1] - b[1];
|
||||
|
||||
@@ -9,8 +9,10 @@ iD.operations.Circularize = function(selectedIDs, context) {
|
||||
};
|
||||
|
||||
operation.available = function() {
|
||||
var entity = context.entity(entityId);
|
||||
return selectedIDs.length === 1 &&
|
||||
context.entity(entityId).type === 'way';
|
||||
entity.type === 'way' &&
|
||||
_.uniq(entity.nodes).length > 1;
|
||||
};
|
||||
|
||||
operation.disabled = function() {
|
||||
|
||||
125
js/lib/d3.v3.js
vendored
125
js/lib/d3.v3.js
vendored
@@ -4393,6 +4393,131 @@ function d3_geom_polygonClosed(coordinates) {
|
||||
b = coordinates[coordinates.length - 1];
|
||||
return !(a[0] - b[0] || a[1] - b[1]);
|
||||
}
|
||||
function d3_geom_pointX(d) {
|
||||
return d[0];
|
||||
}
|
||||
|
||||
function d3_geom_pointY(d) {
|
||||
return d[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the 2D convex hull of a set of points using Graham's scanning
|
||||
* algorithm. The algorithm has been implemented as described in Cormen,
|
||||
* Leiserson, and Rivest's Introduction to Algorithms. The running time of
|
||||
* this algorithm is O(n log n), where n is the number of input points.
|
||||
*
|
||||
* @param vertices [[x1, y1], [x2, y2], …]
|
||||
* @returns polygon [[x1, y1], [x2, y2], …]
|
||||
*/
|
||||
d3.geom.hull = function(vertices) {
|
||||
var x = d3_geom_pointX,
|
||||
y = d3_geom_pointY;
|
||||
|
||||
if (arguments.length) return hull(vertices);
|
||||
|
||||
function hull(data) {
|
||||
if (data.length < 3) return [];
|
||||
|
||||
var fx = d3_functor(x),
|
||||
fy = d3_functor(y),
|
||||
n = data.length,
|
||||
vertices, // TODO use parallel arrays
|
||||
plen = n - 1,
|
||||
points = [],
|
||||
stack = [],
|
||||
d,
|
||||
i, j, h = 0, x1, y1, x2, y2, u, v, a, sp;
|
||||
|
||||
if (fx === d3_geom_pointX && y === d3_geom_pointY) vertices = data;
|
||||
else for (i = 0, vertices = []; i < n; ++i) {
|
||||
vertices.push([+fx.call(this, d = data[i], i), +fy.call(this, d, i)]);
|
||||
}
|
||||
|
||||
// find the starting ref point: leftmost point with the minimum y coord
|
||||
for (i = 1; i < n; ++i) {
|
||||
if (vertices[i][1] < vertices[h][1]
|
||||
|| vertices[i][1] == vertices[h][1]
|
||||
&& vertices[i][0] < vertices[h][0]) h = i;
|
||||
}
|
||||
|
||||
// calculate polar angles from ref point and sort
|
||||
for (i = 0; i < n; ++i) {
|
||||
if (i === h) continue;
|
||||
y1 = vertices[i][1] - vertices[h][1];
|
||||
x1 = vertices[i][0] - vertices[h][0];
|
||||
points.push({angle: Math.atan2(y1, x1), index: i});
|
||||
}
|
||||
points.sort(function(a, b) { return a.angle - b.angle; });
|
||||
|
||||
// toss out duplicate angles
|
||||
a = points[0].angle;
|
||||
v = points[0].index;
|
||||
u = 0;
|
||||
for (i = 1; i < plen; ++i) {
|
||||
j = points[i].index;
|
||||
if (a == points[i].angle) {
|
||||
// keep angle for point most distant from the reference
|
||||
x1 = vertices[v][0] - vertices[h][0];
|
||||
y1 = vertices[v][1] - vertices[h][1];
|
||||
x2 = vertices[j][0] - vertices[h][0];
|
||||
y2 = vertices[j][1] - vertices[h][1];
|
||||
if (x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2) {
|
||||
points[i].index = -1;
|
||||
continue;
|
||||
} else {
|
||||
points[u].index = -1;
|
||||
}
|
||||
}
|
||||
a = points[i].angle;
|
||||
u = i;
|
||||
v = j;
|
||||
}
|
||||
|
||||
// initialize the stack
|
||||
stack.push(h);
|
||||
for (i = 0, j = 0; i < 2; ++j) {
|
||||
if (points[j].index > -1) {
|
||||
stack.push(points[j].index);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
sp = stack.length;
|
||||
|
||||
// do graham's scan
|
||||
for (; j < plen; ++j) {
|
||||
if (points[j].index < 0) continue; // skip tossed out points
|
||||
while (!d3_geom_hullCCW(stack[sp - 2], stack[sp - 1], points[j].index, vertices)) {
|
||||
--sp;
|
||||
}
|
||||
stack[sp++] = points[j].index;
|
||||
}
|
||||
|
||||
// construct the hull
|
||||
var poly = [];
|
||||
for (i = sp - 1; i >= 0; --i) poly.push(data[stack[i]]);
|
||||
return poly;
|
||||
}
|
||||
|
||||
hull.x = function(_) {
|
||||
return arguments.length ? (x = _, hull) : x;
|
||||
};
|
||||
|
||||
hull.y = function(_) {
|
||||
return arguments.length ? (y = _, hull) : y;
|
||||
};
|
||||
|
||||
return hull;
|
||||
};
|
||||
|
||||
// are three points in counter-clockwise order?
|
||||
function d3_geom_hullCCW(i1, i2, i3, v) {
|
||||
var t, a, b, c, d, e, f;
|
||||
t = v[i1]; a = t[0]; b = t[1];
|
||||
t = v[i2]; c = t[0]; d = t[1];
|
||||
t = v[i3]; e = t[0]; f = t[1];
|
||||
return (f - b) * (c - a) - (d - b) * (e - a) > 0;
|
||||
}
|
||||
|
||||
var d3_ease_default = function() { return d3_identity; };
|
||||
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
describe("iD.actions.Circularize", function () {
|
||||
var projection = d3.geo.mercator();
|
||||
|
||||
function isCircular(id, graph) {
|
||||
var points = _.pluck(graph.childNodes(graph.entity(id)), 'loc').map(projection),
|
||||
centroid = d3.geom.polygon(points).centroid(),
|
||||
radius = iD.geo.euclideanDistance(centroid, points[0]),
|
||||
estArea = Math.PI * radius * radius,
|
||||
trueArea = Math.abs(d3.geom.polygon(points).area()),
|
||||
pctDiff = (estArea - trueArea) / estArea;
|
||||
|
||||
return (pctDiff < 0.025); // within 2.5% of circular area..
|
||||
}
|
||||
|
||||
it("creates nodes if necessary", function () {
|
||||
// d ---- c
|
||||
// | |
|
||||
// a ---- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0, 0]}),
|
||||
iD.Node({id: 'b', loc: [2, 0]}),
|
||||
@@ -12,10 +26,14 @@ describe("iD.actions.Circularize", function () {
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
expect(graph.entity('-').nodes).to.have.length(20);
|
||||
});
|
||||
|
||||
it("reuses existing nodes", function () {
|
||||
// d,e -- c
|
||||
// | |
|
||||
// a ---- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0, 0]}),
|
||||
iD.Node({id: 'b', loc: [2, 0]}),
|
||||
@@ -28,15 +46,20 @@ describe("iD.actions.Circularize", function () {
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
|
||||
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);
|
||||
expect(nodes).to.contain('a');
|
||||
expect(nodes).to.contain('b');
|
||||
expect(nodes).to.contain('c');
|
||||
expect(nodes).to.contain('d');
|
||||
expect(nodes).to.contain('e');
|
||||
});
|
||||
|
||||
it("limits movement of nodes that are members of other ways", function () {
|
||||
// b ---- a
|
||||
// | |
|
||||
// c ---- d
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [2, 2]}),
|
||||
iD.Node({id: 'b', loc: [-2, 2]}),
|
||||
@@ -48,6 +71,7 @@ describe("iD.actions.Circularize", function () {
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
expect(iD.geo.euclideanDistance(graph.entity('d').loc, [2, -2])).to.be.lt(0.5);
|
||||
});
|
||||
|
||||
@@ -66,6 +90,9 @@ describe("iD.actions.Circularize", function () {
|
||||
}
|
||||
|
||||
it("creates circle respecting min-angle limit", function() {
|
||||
// d ---- c
|
||||
// | |
|
||||
// a ---- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0, 0]}),
|
||||
iD.Node({id: 'b', loc: [2, 0]}),
|
||||
@@ -76,6 +103,8 @@ describe("iD.actions.Circularize", function () {
|
||||
centroid, points;
|
||||
|
||||
graph = iD.actions.Circularize('-', projection, 20)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
points = _.pluck(graph.childNodes(graph.entity('-')), 'loc').map(projection);
|
||||
centroid = d3.geom.polygon(points).centroid();
|
||||
|
||||
@@ -91,6 +120,9 @@ describe("iD.actions.Circularize", function () {
|
||||
}
|
||||
|
||||
it("leaves clockwise ways clockwise", function () {
|
||||
// d ---- c
|
||||
// | |
|
||||
// a ---- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0, 0]}),
|
||||
iD.Node({id: 'b', loc: [2, 0]}),
|
||||
@@ -103,10 +135,14 @@ describe("iD.actions.Circularize", function () {
|
||||
|
||||
graph = iD.actions.Circularize('+', projection)(graph);
|
||||
|
||||
expect(isCircular('+', graph)).to.be.ok;
|
||||
expect(area('+', graph)).to.be.gt(0);
|
||||
});
|
||||
|
||||
it("leaves counter-clockwise ways counter-clockwise", function () {
|
||||
// d ---- c
|
||||
// | |
|
||||
// a ---- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0, 0]}),
|
||||
iD.Node({id: 'b', loc: [2, 0]}),
|
||||
@@ -119,6 +155,120 @@ describe("iD.actions.Circularize", function () {
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
expect(area('-', graph)).to.be.lt(0);
|
||||
});
|
||||
|
||||
it("adds new nodes on shared way wound in opposite direction", function () {
|
||||
// c ---- b ---- f
|
||||
// | / |
|
||||
// | a |
|
||||
// | \ |
|
||||
// d ---- e ---- g
|
||||
//
|
||||
// a-b-c-d-e-a is counterclockwise
|
||||
// a-b-f-g-e-a is clockwise
|
||||
//
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [ 0, 0]}),
|
||||
iD.Node({id: 'b', loc: [ 1, 2]}),
|
||||
iD.Node({id: 'c', loc: [-2, 2]}),
|
||||
iD.Node({id: 'd', loc: [-2, -2]}),
|
||||
iD.Node({id: 'e', loc: [ 1, -2]}),
|
||||
iD.Node({id: 'f', loc: [ 3, 2]}),
|
||||
iD.Node({id: 'g', loc: [ 3, -2]}),
|
||||
iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}),
|
||||
iD.Way({id: '=', nodes: ['a', 'b', 'f', 'g', 'e', 'a']})
|
||||
]);
|
||||
|
||||
expect(_.intersection(graph.entity('-').nodes, graph.entity('=').nodes).length).to.eql(3);
|
||||
expect(graph.entity('-').isConvex(graph)).to.be.false;
|
||||
expect(graph.entity('=').isConvex(graph)).to.be.true;
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
expect(_.intersection(graph.entity('-').nodes, graph.entity('=').nodes).length).to.be.gt(3);
|
||||
expect(graph.entity('-').isConvex(graph)).to.be.true;
|
||||
expect(graph.entity('=').isConvex(graph)).to.be.false;
|
||||
});
|
||||
|
||||
it("adds new nodes on shared way wound in similar direction", function () {
|
||||
// c ---- b ---- f
|
||||
// | / |
|
||||
// | a |
|
||||
// | \ |
|
||||
// d ---- e ---- g
|
||||
//
|
||||
// a-b-c-d-e-a is counterclockwise
|
||||
// a-e-g-f-b-a is counterclockwise
|
||||
//
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [ 0, 0]}),
|
||||
iD.Node({id: 'b', loc: [ 1, 2]}),
|
||||
iD.Node({id: 'c', loc: [-2, 2]}),
|
||||
iD.Node({id: 'd', loc: [-2, -2]}),
|
||||
iD.Node({id: 'e', loc: [ 1, -2]}),
|
||||
iD.Node({id: 'f', loc: [ 3, 2]}),
|
||||
iD.Node({id: 'g', loc: [ 3, -2]}),
|
||||
iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}),
|
||||
iD.Way({id: '=', nodes: ['a', 'e', 'g', 'f', 'b', 'a']})
|
||||
]);
|
||||
|
||||
expect(_.intersection(graph.entity('-').nodes, graph.entity('=').nodes).length).to.eql(3);
|
||||
expect(graph.entity('-').isConvex(graph)).to.be.false;
|
||||
expect(graph.entity('=').isConvex(graph)).to.be.true;
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
expect(_.intersection(graph.entity('-').nodes, graph.entity('=').nodes).length).to.be.gt(3);
|
||||
expect(graph.entity('-').isConvex(graph)).to.be.true;
|
||||
expect(graph.entity('=').isConvex(graph)).to.be.false;
|
||||
});
|
||||
|
||||
it("circularizes extremely concave ways with a key node on the wrong side of the centroid", function () {
|
||||
// c ------------ b -- f
|
||||
// | ___--- |
|
||||
// | a === |
|
||||
// | ---___ |
|
||||
// d ------------ e -- g
|
||||
//
|
||||
// a-b-c-d-e-a is extremely concave and 'a' is to the left of centoid..
|
||||
//
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [ 0, 0]}),
|
||||
iD.Node({id: 'b', loc: [10, 2]}),
|
||||
iD.Node({id: 'c', loc: [-2, 2]}),
|
||||
iD.Node({id: 'd', loc: [-2, -2]}),
|
||||
iD.Node({id: 'e', loc: [10, -2]}),
|
||||
iD.Node({id: 'f', loc: [15, 2]}),
|
||||
iD.Node({id: 'g', loc: [15, -2]}),
|
||||
iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}),
|
||||
iD.Way({id: '=', nodes: ['a', 'b', 'f', 'g', 'e', 'a']})
|
||||
]);
|
||||
|
||||
expect(graph.entity('-').isConvex(graph)).to.be.false;
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
expect(graph.entity('-').isConvex(graph)).to.be.true;
|
||||
expect(graph.entity('-').nodes).to.have.length(20);
|
||||
});
|
||||
|
||||
it("circularizes a closed single line way", function () {
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0, 0]}),
|
||||
iD.Node({id: 'b', loc: [0, 2]}),
|
||||
iD.Way({id: '-', nodes: ['a', 'b', 'a']}),
|
||||
]);
|
||||
|
||||
expect(area('-', graph)).to.eql(0);
|
||||
|
||||
graph = iD.actions.Circularize('-', projection)(graph);
|
||||
|
||||
expect(isCircular('-', graph)).to.be.ok;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -87,6 +87,67 @@ describe('iD.Way', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isConvex', function() {
|
||||
it('returns true for convex ways', function() {
|
||||
// d -- e
|
||||
// | \
|
||||
// | a
|
||||
// | /
|
||||
// c -- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [ 0.0003, 0.0000]}),
|
||||
iD.Node({id: 'b', loc: [ 0.0002, -0.0002]}),
|
||||
iD.Node({id: 'c', loc: [-0.0002, -0.0002]}),
|
||||
iD.Node({id: 'd', loc: [-0.0002, 0.0002]}),
|
||||
iD.Node({id: 'e', loc: [ 0.0002, 0.0002]}),
|
||||
iD.Way({id: 'w', nodes: ['a','b','c','d','e','a']})
|
||||
]);
|
||||
expect(graph.entity('w').isConvex(graph)).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false for concave ways', function() {
|
||||
// d -- e
|
||||
// | /
|
||||
// | a
|
||||
// | \
|
||||
// c -- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [ 0.0000, 0.0000]}),
|
||||
iD.Node({id: 'b', loc: [ 0.0002, -0.0002]}),
|
||||
iD.Node({id: 'c', loc: [-0.0002, -0.0002]}),
|
||||
iD.Node({id: 'd', loc: [-0.0002, 0.0002]}),
|
||||
iD.Node({id: 'e', loc: [ 0.0002, 0.0002]}),
|
||||
iD.Way({id: 'w', nodes: ['a','b','c','d','e','a']})
|
||||
]);
|
||||
expect(graph.entity('w').isConvex(graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns null for non-closed ways', function() {
|
||||
// d -- e
|
||||
// |
|
||||
// | a
|
||||
// | \
|
||||
// c -- b
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [ 0.0000, 0.0000]}),
|
||||
iD.Node({id: 'b', loc: [ 0.0002, -0.0002]}),
|
||||
iD.Node({id: 'c', loc: [-0.0002, -0.0002]}),
|
||||
iD.Node({id: 'd', loc: [-0.0002, 0.0002]}),
|
||||
iD.Node({id: 'e', loc: [ 0.0002, 0.0002]}),
|
||||
iD.Way({id: 'w', nodes: ['a','b','c','d','e']})
|
||||
]);
|
||||
expect(graph.entity('w').isConvex(graph)).to.be.null;
|
||||
});
|
||||
|
||||
it('returns null for degenerate ways', function() {
|
||||
var graph = iD.Graph([
|
||||
iD.Node({id: 'a', loc: [0.0000, 0.0000]}),
|
||||
iD.Way({id: 'w', nodes: ['a','a']})
|
||||
]);
|
||||
expect(graph.entity('w').isConvex(graph)).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isOneWay', function() {
|
||||
it('returns false when the way has no tags', function() {
|
||||
expect(iD.Way().isOneWay()).to.eql(false);
|
||||
|
||||
@@ -18,6 +18,27 @@ describe('iD.geo', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.cross', function() {
|
||||
it('cross product of right hand turn is positive', function() {
|
||||
var o = [0, 0],
|
||||
a = [2, 0],
|
||||
b = [0, 2];
|
||||
expect(iD.geo.cross(o, a, b)).to.eql(4);
|
||||
});
|
||||
it('cross product of left hand turn is negative', function() {
|
||||
var o = [0, 0],
|
||||
a = [2, 0],
|
||||
b = [0, -2];
|
||||
expect(iD.geo.cross(o, a, b)).to.eql(-4);
|
||||
});
|
||||
it('cross product of colinear points is zero', function() {
|
||||
var o = [0, 0],
|
||||
a = [-2, 0],
|
||||
b = [2, 0];
|
||||
expect(iD.geo.cross(o, a, b)).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.euclideanDistance', function() {
|
||||
it('distance between two same points is zero', function() {
|
||||
var a = [0, 0],
|
||||
|
||||
Reference in New Issue
Block a user