Redo iD.geo.chooseIndex

It now calculates proper orthogonal projections and finds
the minimal one. Rename it iD.geo.chooseEdge and minimize
its coupling to context.

This version also copes with 0- and 1-node ways.

Fixes #1428.
This commit is contained in:
John Firebaugh
2013-05-07 18:34:49 -07:00
parent 6f3f756fd2
commit 08949d2fe9
8 changed files with 119 additions and 20 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ iD.behavior.Draw = function(context) {
function click() {
var d = datum();
if (d.type === 'way') {
var choice = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context),
var choice = iD.geo.chooseEdge(context.childNodes(d), d3.mouse(context.surface().node()), context.projection),
edge = [d.nodes[choice.index - 1], d.nodes[choice.index]];
event.clickWay(choice.loc, edge);
+1 -1
View File
@@ -37,7 +37,7 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) {
} else if (datum.type === 'node') {
loc = datum.loc;
} else if (datum.type === 'way') {
loc = iD.geo.chooseIndex(datum, d3.mouse(context.surface().node()), context).loc;
loc = iD.geo.chooseEdge(context.childNodes(datum), d3.mouse(context.surface().node()), context.projection).loc;
}
context.replace(iD.actions.MoveNode(end.id, loc));
+36 -12
View File
@@ -15,24 +15,48 @@ iD.geo.dist = function(a, b) {
return Math.sqrt((x * x) + (y * y));
};
iD.geo.chooseIndex = function(way, point, context) {
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
iD.geo.chooseEdge = function(nodes, point, projection) {
var dist = iD.geo.dist,
graph = context.graph(),
nodes = graph.childNodes(way),
projNodes = nodes.map(function(n) { return context.projection(n.loc); });
points = nodes.map(function(n) { return projection(n.loc); }),
min = Infinity,
idx, loc;
for (var i = 0, changes = []; i < projNodes.length - 1; i++) {
changes[i] =
(dist(projNodes[i], point) + dist(point, projNodes[i + 1])) /
dist(projNodes[i], projNodes[i + 1]);
function dot(p, q) {
return p[0] * q[0] + p[1] * q[1];
}
var idx = _.indexOf(changes, _.min(changes)),
ratio = dist(projNodes[idx], point) / dist(projNodes[idx], projNodes[idx + 1]),
loc = iD.geo.interp(nodes[idx].loc, nodes[idx + 1].loc, ratio);
for (var i = 0; i < points.length - 1; i++) {
var o = points[i],
s = [points[i + 1][0] - o[0],
points[i + 1][1] - o[1]],
v = [point[0] - o[0],
point[1] - o[1]],
proj = dot(v, s) / dot(s, s),
p;
if (proj < 0) {
p = o;
} else if (proj > 1) {
p = points[i + 1];
} else {
p = [o[0] + proj * s[0], o[1] + proj * s[1]];
}
var d = dist(p, point);
if (d < min) {
min = d;
idx = i + 1;
loc = projection.invert(p);
}
}
return {
index: idx + 1,
index: idx,
distance: min,
loc: loc
};
};
+4
View File
@@ -88,6 +88,10 @@ window.iD = function () {
return history.graph().entity(id);
};
context.childNodes = function(way) {
return history.graph().childNodes(way);
};
context.geometry = function(id) {
return context.entity(id).geometry(history.graph());
};
+2 -2
View File
@@ -101,7 +101,7 @@ iD.modes.DragNode = function(context) {
if (d.type === 'node' && d.id !== entity.id) {
loc = d.loc;
} else if (d.type === 'way') {
loc = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context).loc;
loc = iD.geo.chooseEdge(context.childNodes(d), d3.mouse(context.surface().node()), context.projection).loc;
}
context.replace(
@@ -115,7 +115,7 @@ iD.modes.DragNode = function(context) {
var d = datum();
if (d.type === 'way') {
var choice = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context);
var choice = iD.geo.chooseEdge(context.childNodes(d), d3.mouse(context.surface().node()), context.projection);
context.replace(
iD.actions.AddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity),
connectAnnotation(d));
+2 -2
View File
@@ -131,8 +131,8 @@ iD.modes.Select = function(context, selection) {
datum = target.datum();
if (datum instanceof iD.Way && !target.classed('fill')) {
var choice = iD.geo.chooseIndex(datum,
d3.mouse(context.surface().node()), context),
var choice = iD.geo.chooseEdge(context.childNodes(datum),
d3.mouse(context.surface().node()), context.projection),
node = iD.Node();
var prev = datum.nodes[choice.index - 1],
+2 -2
View File
@@ -20,11 +20,11 @@ iD.ui.preset.address = function(field, context) {
var loc = context.projection([
(extent[0][0] + extent[1][0]) / 2,
(extent[0][1] + extent[1][1]) / 2]),
closest = context.projection(iD.geo.chooseIndex(d, loc, context).loc);
choice = iD.geo.chooseEdge(context.childNodes(d), loc, context.projection);
return {
title: d.tags.name,
value: d.tags.name,
dist: iD.geo.dist(closest, loc)
dist: choice.distance
};
}).sort(function(a, b) {
return a.dist - b.dist;
+71
View File
@@ -36,6 +36,77 @@ describe('iD.geo', function() {
});
});
describe('.chooseEdge', function() {
var projection = function (l) { return l; };
projection.invert = projection;
it('returns undefined properties for a degenerate way (no nodes)', function() {
expect(iD.geo.chooseEdge([], [0, 0], projection)).to.eql({
index: undefined,
distance: Infinity,
loc: undefined
})
});
it('returns undefined properties for a degenerate way (single node)', function() {
expect(iD.geo.chooseEdge([iD.Node({loc: [0, 0]})], [0, 0], projection)).to.eql({
index: undefined,
distance: Infinity,
loc: undefined
})
});
it('calculates the orthogonal projection of a point onto a segment', function() {
// a --*--- b
// |
// c
//
// * = [2, 0]
var a = [0, 0],
b = [5, 0],
c = [2, 1],
nodes = [
iD.Node({loc: a}),
iD.Node({loc: b})
];
var choice = iD.geo.chooseEdge(nodes, c, projection);
expect(choice.index).to.eql(1);
expect(choice.distance).to.eql(1);
expect(choice.loc).to.eql([2, 0]);
});
it('returns the starting vertex when the orthogonal projection is < 0', function() {
var a = [0, 0],
b = [5, 0],
c = [-3, 4],
nodes = [
iD.Node({loc: a}),
iD.Node({loc: b})
];
var choice = iD.geo.chooseEdge(nodes, c, projection);
expect(choice.index).to.eql(1);
expect(choice.distance).to.eql(5);
expect(choice.loc).to.eql([0, 0]);
});
it('returns the ending vertex when the orthogonal projection is > 1', function() {
var a = [0, 0],
b = [5, 0],
c = [8, 4],
nodes = [
iD.Node({loc: a}),
iD.Node({loc: b})
];
var choice = iD.geo.chooseEdge(nodes, c, projection);
expect(choice.index).to.eql(1);
expect(choice.distance).to.eql(5);
expect(choice.loc).to.eql([5, 0]);
});
});
describe('.pointInPolygon', function() {
it('says a point in a polygon is on a polygon', function() {
var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]];