diff --git a/js/id/geo.js b/js/id/geo.js index 06b63ae14..b05a185fa 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -1 +1,75 @@ iD.geo = {}; + +iD.geo.roundCoords = function(c) { + return [Math.floor(c[0]), Math.floor(c[1])]; +}; + +iD.geo.interp = function(p1, p2, t) { + return [p1[0] + (p2[0] - p1[0]) * t, + p1[1] + (p2[1] - p1[1]) * t]; +}; + +iD.geo.dist = function(a, b) { + return Math.sqrt(Math.pow(a[0] - b[0], 2) + + Math.pow(a[1] - b[1], 2)); +}; + +iD.geo.chooseIndex = function(way, point, map) { + var dist = iD.geo.dist, + projNodes = way.nodes.map(function(n) { + return map.projection(n.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]); + } + + var idx = _.indexOf(changes, _.min(changes)), + ratio = dist(projNodes[idx], point) / dist(projNodes[idx], projNodes[idx + 1]), + loc = iD.geo.interp(way.nodes[idx].loc, way.nodes[idx + 1].loc, ratio); + + return { + index: idx + 1, + loc: loc + }; +}; + +// Return whether point is contained in polygon. +// +// `point` should be a 2-item array of coordinates. +// `polygon` should be an array of 2-item arrays of coordinates. +// +// From https://github.com/substack/point-in-polygon. +// ray-casting algorithm based on +// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +// +iD.geo.pointInPolygon = function(point, polygon) { + var x = point[0], + y = point[1], + inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + + var intersect = ((yi > y) != (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; +}; + +iD.geo.polygonContainsPolygon = function(outer, inner) { + return _.every(inner, function (point) { + return iD.geo.pointInPolygon(point, outer); + }); +}; + +iD.geo.polygonIntersectsPolygon = function(outer, inner) { + return _.some(inner, function (point) { + return iD.geo.pointInPolygon(point, outer); + }); +}; diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index a09d6dac0..2dd325438 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -120,13 +120,13 @@ _.extend(iD.Relation.prototype, { for (o = 0; o < outers.length; o++) { outer = _.pluck(outers[o], 'loc'); - if (iD.util.geo.polygonContainsPolygon(outer, inner)) + if (iD.geo.polygonContainsPolygon(outer, inner)) return o; } for (o = 0; o < outers.length; o++) { outer = _.pluck(outers[o], 'loc'); - if (iD.util.geo.polygonIntersectsPolygon(outer, inner)) + if (iD.geo.polygonIntersectsPolygon(outer, inner)) return o; } } diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index db4e88e18..8baa8e30e 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -40,7 +40,7 @@ iD.modes.AddLine = function() { } else if (datum.type === 'way') { // begin a new way starting from an existing way - var choice = iD.util.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); + var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); node = iD.Node({ loc: choice.loc }); history.perform( diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 95719a6a9..3b3892aa6 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -80,7 +80,7 @@ iD.modes.DrawLine = function(wayId, direction) { datum.id = datum.way; choice = datum; } else { - choice = iD.util.geo.chooseIndex(datum, d3.mouse(surface.node()), map); + choice = iD.geo.chooseIndex(datum, d3.mouse(surface.node()), map); } history.replace( diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 2f203b450..3a3750d36 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -116,7 +116,7 @@ iD.modes.Select = function(entity, initial) { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity && (datum.geometry() === 'area' || datum.geometry() === 'line')) { - var choice = iD.util.geo.chooseIndex(datum, + var choice = iD.geo.chooseIndex(datum, d3.mouse(mode.map.surface.node()), mode.map), node = iD.Node({ loc: choice.loc }); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 7e74d7b66..43785e3da 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -270,7 +270,7 @@ iD.Map = function() { map.centerEase = function(loc) { var from = map.center().slice(), t = 0; d3.timer(function() { - map.center(iD.util.geo.interp(from, loc, (t += 1) / 10)); + map.center(iD.geo.interp(from, loc, (t += 1) / 10)); return t == 10; }, 20); }; diff --git a/js/id/svg.js b/js/id/svg.js index e2b1dc2ad..a3b174ddb 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -1,7 +1,7 @@ iD.svg = { RoundProjection: function (projection) { return function (d) { - return iD.util.geo.roundCoords(projection(d)); + return iD.geo.roundCoords(projection(d)); }; }, diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js index 7c7c7a0c3..a04526f83 100644 --- a/js/id/svg/midpoints.js +++ b/js/id/svg/midpoints.js @@ -11,9 +11,9 @@ iD.svg.Midpoints = function(projection) { for (var j = 0; j < entity.nodes.length - 1; j++) { var a = projection(entity.nodes[j].loc); var b = projection(entity.nodes[j + 1].loc); - if (iD.util.geo.dist(a, b) > 40) { + if (iD.geo.dist(a, b) > 40) { midpoints.push({ - loc: iD.util.geo.interp(entity.nodes[j].loc, entity.nodes[j + 1].loc, 0.5), + loc: iD.geo.interp(entity.nodes[j].loc, entity.nodes[j + 1].loc, 0.5), way: entity.id, index: j + 1, midpoint: true diff --git a/js/id/util.js b/js/id/util.js index b3ae537ec..2e6e3453d 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -70,79 +70,3 @@ iD.util.prefixCSSProperty = function(property) { return false; }; - -iD.util.geo = {}; - -iD.util.geo.roundCoords = function(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; -}; - -iD.util.geo.interp = function(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; -}; - -iD.util.geo.dist = function(a, b) { - return Math.sqrt(Math.pow(a[0] - b[0], 2) + - Math.pow(a[1] - b[1], 2)); -}; - -iD.util.geo.chooseIndex = function(way, point, map) { - var dist = iD.util.geo.dist, - projNodes = way.nodes.map(function(n) { - return map.projection(n.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]); - } - - var idx = _.indexOf(changes, _.min(changes)), - ratio = dist(projNodes[idx], point) / dist(projNodes[idx], projNodes[idx + 1]), - loc = iD.util.geo.interp(way.nodes[idx].loc, way.nodes[idx + 1].loc, ratio); - - return { - index: idx + 1, - loc: loc - }; -}; - -// Return whether point is contained in polygon. -// -// `point` should be a 2-item array of coordinates. -// `polygon` should be an array of 2-item arrays of coordinates. -// -// From https://github.com/substack/point-in-polygon. -// ray-casting algorithm based on -// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -// -iD.util.geo.pointInPolygon = function(point, polygon) { - var x = point[0], - y = point[1], - inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) != (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; -}; - -iD.util.geo.polygonContainsPolygon = function(outer, inner) { - return _.every(inner, function (point) { - return iD.util.geo.pointInPolygon(point, outer); - }); -}; - -iD.util.geo.polygonIntersectsPolygon = function(outer, inner) { - return _.some(inner, function (point) { - return iD.util.geo.pointInPolygon(point, outer); - }); -}; diff --git a/test/spec/util.js b/test/spec/util.js index 2cfa8dc16..6c12ccc0f 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -26,21 +26,21 @@ describe('Util', function() { describe('geo', function() { describe('#roundCoords', function() { - expect(iD.util.geo.roundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.util.geo.roundCoords([0, 1])).to.eql([0, 1]); - expect(iD.util.geo.roundCoords([0, 1.1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]); }); describe('#interp', function() { it('interpolates halfway', function() { var a = [0, 0], b = [10, 10]; - expect(iD.util.geo.interp(a, b, 0.5)).to.eql([5, 5]); + expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]); }); it('interpolates to one side', function() { var a = [0, 0], b = [10, 10]; - expect(iD.util.geo.interp(a, b, 0)).to.eql([0, 0]); + expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]); }); }); @@ -48,17 +48,17 @@ describe('Util', function() { it('distance between two same points is zero', function() { var a = [0, 0], b = [0, 0]; - expect(iD.util.geo.dist(a, b)).to.eql(0); + expect(iD.geo.dist(a, b)).to.eql(0); }); it('a straight 10 unit line is 10', function() { var a = [0, 0], b = [10, 0]; - expect(iD.util.geo.dist(a, b)).to.eql(10); + expect(iD.geo.dist(a, b)).to.eql(10); }); it('a pythagorean triangle is right', function() { var a = [0, 0], b = [4, 3]; - expect(iD.util.geo.dist(a, b)).to.eql(5); + expect(iD.geo.dist(a, b)).to.eql(5); }); }); @@ -66,7 +66,7 @@ describe('Util', 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]]; var point = [0.5, 0.5]; - expect(iD.util.geo.pointInPolygon(point, poly)).to.be.true; + expect(iD.geo.pointInPolygon(point, poly)).to.be.true; }); it('says a point outside of a polygon is outside', function() { var poly = [ @@ -76,7 +76,7 @@ describe('Util', function() { [1, 0], [0, 0]]; var point = [0.5, 1.5]; - expect(iD.util.geo.pointInPolygon(point, poly)).to.be.false; + expect(iD.geo.pointInPolygon(point, poly)).to.be.false; }); }); @@ -84,12 +84,12 @@ describe('Util', function() { it('says a polygon in a polygon is in', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.util.geo.polygonContainsPolygon(outer, inner)).to.be.true; + expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.true; }); it('says a polygon outside of a polygon is out', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; - expect(iD.util.geo.polygonContainsPolygon(outer, inner)).to.be.false; + expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.false; }); }); @@ -97,19 +97,19 @@ describe('Util', function() { it('says a polygon in a polygon intersects it', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; + expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; }); it('says a polygon that partially intersects does', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; + expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; }); it('says totally disjoint polygons do not intersect', function() { var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; - expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; + expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; }); }); });