diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 0c5a27778..e0f7aa436 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -39,15 +39,6 @@ export function geoMetersToLon(m, atLat) { } -export function geoOffsetToMeters(offset, tileSize) { - tileSize = tileSize || 256; - return [ - offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, - -offset[1] * TAU * POLAR_RADIUS / tileSize - ]; -} - - export function geoMetersToOffset(meters, tileSize) { tileSize = tileSize || 256; return [ @@ -57,6 +48,15 @@ export function geoMetersToOffset(meters, tileSize) { } +export function geoOffsetToMeters(offset, tileSize) { + tileSize = tileSize || 256; + return [ + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize + ]; +} + + // Equirectangular approximation of spherical distances on Earth export function geoSphericalDistance(a, b) { var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2); @@ -65,13 +65,6 @@ export function geoSphericalDistance(a, b) { } -// zoom to scale -export function geoZoomToScale(z, tileSize) { - tileSize = tileSize || 256; - return tileSize * Math.pow(2, z) / TAU; -} - - // scale to zoom export function geoScaleToZoom(k, tileSize) { tileSize = tileSize || 256; @@ -80,203 +73,11 @@ export function geoScaleToZoom(k, tileSize) { } -export function geoEdgeEqual(a, b) { - return (a[0] === b[0] && a[1] === b[1]) || - (a[0] === b[1] && a[1] === b[0]); +// zoom to scale +export function geoZoomToScale(z, tileSize) { + tileSize = tileSize || 256; + return tileSize * Math.pow(2, z) / TAU; } -// Return the counterclockwise angle in the range (-pi, pi) -// between the positive X axis and the line intersecting a and b. -export function geoAngle(a, b, projection) { - return geoVecAngle(projection(a.loc), projection(b.loc)); -} - -// Rotate all points counterclockwise around a pivot point by given angle -export function geoRotate(points, angle, around) { - return points.map(function(point) { - var radial = [point[0] - around[0], point[1] - around[1]]; - return [ - radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], - radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] - ]; - }); -} - - -// 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. -export function geoChooseEdge(nodes, point, projection, skipID) { - var dist = geoVecLength; - var points = nodes.map(function(n) { return projection(n.loc); }); - var ids = nodes.map(function(n) { return n.id; }); - var min = Infinity; - var idx; - var loc; - - for (var i = 0; i < points.length - 1; i++) { - if (ids[i] === skipID || ids[i + 1] === skipID) continue; - - var o = points[i]; - var s = geoVecSubtract(points[i + 1], o); - var v = geoVecSubtract(point, o); - var proj = geoVecDot(v, s) / geoVecDot(s, s); - var 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); - } - } - - if (idx !== undefined) { - return { index: idx, distance: min, loc: loc }; - } else { - return null; - } -} - - -// Return the intersection point of 2 line segments. -// From https://github.com/pgkelley4/line-segments-intersect -// This uses the vector cross product approach described below: -// http://stackoverflow.com/a/565282/786339 -export function geoLineIntersection(a, b) { - var p = [a[0][0], a[0][1]]; - var p2 = [a[1][0], a[1][1]]; - var q = [b[0][0], b[0][1]]; - var q2 = [b[1][0], b[1][1]]; - var r = geoVecSubtract(p2, p); - var s = geoVecSubtract(q2, q); - var uNumerator = geoVecCross(geoVecSubtract(q, p), r); - var denominator = geoVecCross(r, s); - - if (uNumerator && denominator) { - var u = uNumerator / denominator; - var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; - - if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoVecInterp(p, p2, t); - } - } - - return null; -} - - -export function geoPathIntersections(path1, path2) { - var intersections = []; - for (var i = 0; i < path1.length - 1; i++) { - for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ]; - var b = [ path2[j], path2[j+1] ]; - var hit = geoLineIntersection(a, b); - if (hit) { - intersections.push(hit); - } - } - } - return intersections; -} - - -// 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 -// -export function geoPointInPolygon(point, polygon) { - var x = point[0]; - var y = point[1]; - var 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; -} - - -export function geoPolygonContainsPolygon(outer, inner) { - return _every(inner, function(point) { - return geoPointInPolygon(point, outer); - }); -} - - -export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i + 1] ]; - var b = [ inner[j], inner[j + 1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - - function testPoints(outer, inner) { - return _some(inner, function(point) { - return geoPointInPolygon(point, outer); - }); - } - - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); -} - - -export function geoPathLength(path) { - var length = 0; - for (var i = 0; i < path.length - 1; i++) { - length += geoVecLength(path[i], path[i + 1]); - } - return length; -} - - -// If the given point is at the edge of the padded viewport, -// return a vector that will nudge the viewport in that direction -export function geoViewportEdge(point, dimensions) { - var pad = [80, 20, 50, 20]; // top, right, bottom, left - var x = 0; - var y = 0; - - if (point[0] > dimensions[0] - pad[1]) - x = -10; - if (point[0] < pad[3]) - x = 10; - if (point[1] > dimensions[1] - pad[2]) - y = -10; - if (point[1] < pad[0]) - y = 10; - - if (x || y) { - return [x, y]; - } else { - return null; - } -} diff --git a/modules/geo/geom.js b/modules/geo/geom.js new file mode 100644 index 000000000..fef500eb6 --- /dev/null +++ b/modules/geo/geom.js @@ -0,0 +1,213 @@ +import _every from 'lodash-es/every'; +import _some from 'lodash-es/some'; + +import { + geoVecAngle, + geoVecCross, + geoVecDot, + geoVecInterp, + geoVecLength, + geoVecSubtract +} from './vector.js'; + + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +export function geoAngle(a, b, projection) { + return geoVecAngle(projection(a.loc), projection(b.loc)); +} + +export function geoEdgeEqual(a, b) { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] === b[1] && a[1] === b[0]); +} + +// Rotate all points counterclockwise around a pivot point by given angle +export function geoRotate(points, angle, around) { + return points.map(function(point) { + var radial = [point[0] - around[0], point[1] - around[1]]; + return [ + radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], + radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] + ]; + }); +} + + +// 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. +export function geoChooseEdge(nodes, point, projection, skipID) { + var dist = geoVecLength; + var points = nodes.map(function(n) { return projection(n.loc); }); + var ids = nodes.map(function(n) { return n.id; }); + var min = Infinity; + var idx; + var loc; + + for (var i = 0; i < points.length - 1; i++) { + if (ids[i] === skipID || ids[i + 1] === skipID) continue; + + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, o); + var proj = geoVecDot(v, s) / geoVecDot(s, s); + var 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); + } + } + + if (idx !== undefined) { + return { index: idx, distance: min, loc: loc }; + } else { + return null; + } +} + + +// Return the intersection point of 2 line segments. +// From https://github.com/pgkelley4/line-segments-intersect +// This uses the vector cross product approach described below: +// http://stackoverflow.com/a/565282/786339 +export function geoLineIntersection(a, b) { + var p = [a[0][0], a[0][1]]; + var p2 = [a[1][0], a[1][1]]; + var q = [b[0][0], b[0][1]]; + var q2 = [b[1][0], b[1][1]]; + var r = geoVecSubtract(p2, p); + var s = geoVecSubtract(q2, q); + var uNumerator = geoVecCross(geoVecSubtract(q, p), r); + var denominator = geoVecCross(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator; + var t = geoVecCross(geoVecSubtract(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return geoVecInterp(p, p2, t); + } + } + + return null; +} + + +export function geoPathIntersections(path1, path2) { + var intersections = []; + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ]; + var b = [ path2[j], path2[j+1] ]; + var hit = geoLineIntersection(a, b); + if (hit) { + intersections.push(hit); + } + } + } + return intersections; +} + + +// 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 +// +export function geoPointInPolygon(point, polygon) { + var x = point[0]; + var y = point[1]; + var inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0]; + var yi = polygon[i][1]; + var xj = polygon[j][0]; + var yj = polygon[j][1]; + + var intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; +} + + +export function geoPolygonContainsPolygon(outer, inner) { + return _every(inner, function(point) { + return geoPointInPolygon(point, outer); + }); +} + + +export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { + function testSegments(outer, inner) { + for (var i = 0; i < outer.length - 1; i++) { + for (var j = 0; j < inner.length - 1; j++) { + var a = [ outer[i], outer[i + 1] ]; + var b = [ inner[j], inner[j + 1] ]; + if (geoLineIntersection(a, b)) return true; + } + } + return false; + } + + function testPoints(outer, inner) { + return _some(inner, function(point) { + return geoPointInPolygon(point, outer); + }); + } + + return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); +} + + +export function geoPathLength(path) { + var length = 0; + for (var i = 0; i < path.length - 1; i++) { + length += geoVecLength(path[i], path[i + 1]); + } + return length; +} + + +// If the given point is at the edge of the padded viewport, +// return a vector that will nudge the viewport in that direction +export function geoViewportEdge(point, dimensions) { + var pad = [80, 20, 50, 20]; // top, right, bottom, left + var x = 0; + var y = 0; + + if (point[0] > dimensions[0] - pad[1]) + x = -10; + if (point[0] < pad[3]) + x = 10; + if (point[1] > dimensions[1] - pad[2]) + y = -10; + if (point[1] < pad[0]) + y = 10; + + if (x || y) { + return [x, y]; + } else { + return null; + } +} diff --git a/modules/geo/index.js b/modules/geo/index.js index dca2f31dd..fe801aff6 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,23 +1,29 @@ -export { geoAngle } from './geo.js'; -export { geoChooseEdge } from './geo.js'; -export { geoEdgeEqual } from './geo.js'; export { geoExtent } from './extent.js'; -export { geoRawMercator } from './raw_mercator.js'; -export { geoRotate } from './geo.js'; + export { geoLatToMeters } from './geo.js'; -export { geoLineIntersection } from './geo.js'; export { geoLonToMeters } from './geo.js'; export { geoMetersToLat } from './geo.js'; export { geoMetersToLon } from './geo.js'; export { geoMetersToOffset } from './geo.js'; export { geoOffsetToMeters } from './geo.js'; -export { geoPathIntersections } from './geo.js'; -export { geoPathLength } from './geo.js'; -export { geoPointInPolygon } from './geo.js'; -export { geoPolygonContainsPolygon } from './geo.js'; -export { geoPolygonIntersectsPolygon } from './geo.js'; export { geoScaleToZoom } from './geo.js'; export { geoSphericalDistance } from './geo.js'; +export { geoZoomToScale } from './geo.js'; + +export { geoAngle } from './geom.js'; +export { geoChooseEdge } from './geom.js'; +export { geoEdgeEqual } from './geom.js'; +export { geoRotate } from './geom.js'; +export { geoLineIntersection } from './geom.js'; +export { geoPathIntersections } from './geom.js'; +export { geoPathLength } from './geom.js'; +export { geoPointInPolygon } from './geom.js'; +export { geoPolygonContainsPolygon } from './geom.js'; +export { geoPolygonIntersectsPolygon } from './geom.js'; +export { geoViewportEdge } from './geom.js'; + +export { geoRawMercator } from './raw_mercator.js'; + export { geoVecAdd } from './vector.js'; export { geoVecAngle } from './vector.js'; export { geoVecCross } from './vector.js'; @@ -28,5 +34,4 @@ export { geoVecInterp } from './vector.js'; export { geoVecLength } from './vector.js'; export { geoVecSubtract } from './vector.js'; export { geoVecScale } from './vector.js'; -export { geoZoomToScale } from './geo.js'; -export { geoViewportEdge } from './geo.js'; + diff --git a/test/index.html b/test/index.html index ed2cb0c54..2357d5ba1 100644 --- a/test/index.html +++ b/test/index.html @@ -74,6 +74,7 @@ + diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 9f31f1d27..76c77ca09 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,4 +1,4 @@ -describe('iD.geo', function() { +describe('iD.geo - geography', function() { describe('geoLatToMeters', function() { it('0 degrees latitude is 0 meters', function() { @@ -144,298 +144,4 @@ describe('iD.geo', function() { }); }); - describe('geoEdgeEqual', function() { - it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; - }); - - it('returns true for equal edges along same direction', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; - }); - - it('returns true for equal edges along opposite direction', function() { - expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; - }); - }); - - describe('geoAngle', function() { - it('returns angle between a and b', function() { - var projection = function (_) { return _; }; - expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); - expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); - }); - }); - - describe('geoRotate', function() { - it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]]; - var angle = Math.PI; - var around = [0, 0]; - var result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(-5, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(-5, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - - it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]]; - var angle = Math.PI; - var around = [3, 0]; - var result = iD.geoRotate(points, angle, around); - expect(result[0][0]).to.be.closeTo(1, 1e-6); - expect(result[0][1]).to.be.closeTo(0, 1e-6); - expect(result[1][0]).to.be.closeTo(1, 1e-6); - expect(result[1][1]).to.be.closeTo(-1, 1e-6); - }); - }); - - describe('geoChooseEdge', function() { - var projection = function (l) { return l; }; - projection.invert = projection; - - it('returns null for a degenerate way (no nodes)', function() { - expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; - }); - - it('returns null for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; - }); - - it('calculates the orthogonal projection of a point onto a segment', function() { - // a --*--- b - // | - // c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [2, 1]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(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]; - var b = [5, 0]; - var c = [-3, 4]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(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]; - var b = [5, 0]; - var c = [8, 4]; - var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; - var choice = iD.geoChooseEdge(nodes, c, projection); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(5); - expect(choice.loc).to.eql([5, 0]); - }); - - it('skips the given nodeID at end of way', function() { - // - // a --*-- b - // e | - // | | - // d - c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [5, 5]; - var d = [2, 5]; - var e = [2, 0.1]; // e.g. user is dragging e onto ab - var nodes = [ - iD.osmNode({id: 'a', loc: a}), - iD.osmNode({id: 'b', loc: b}), - iD.osmNode({id: 'c', loc: c}), - iD.osmNode({id: 'd', loc: d}), - iD.osmNode({id: 'e', loc: e}) - ]; - var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(0.1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('skips the given nodeID in middle of way', function() { - // - // a --*-- b - // d | - // / \ | - // e c - // - // * = [2, 0] - var a = [0, 0]; - var b = [5, 0]; - var c = [5, 5]; - var d = [2, 0.1]; // e.g. user is dragging d onto ab - var e = [0, 5]; - var nodes = [ - iD.osmNode({id: 'a', loc: a}), - iD.osmNode({id: 'b', loc: b}), - iD.osmNode({id: 'c', loc: c}), - iD.osmNode({id: 'd', loc: d}), - iD.osmNode({id: 'e', loc: e}) - ]; - var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); - expect(choice.index).to.eql(1); - expect(choice.distance).to.eql(0.1); - expect(choice.loc).to.eql([2, 0]); - }); - - it('returns null if all nodes are skipped', function() { - var nodes = [ - iD.osmNode({id: 'a', loc: [0, 0]}), - iD.osmNode({id: 'b', loc: [5, 0]}), - ]; - var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); - expect(choice).to.be.null; - }); - }); - - describe('geoLineIntersection', function() { - it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]]; - var b = [[-5, 0], [5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are colinear but disjoint', function() { - var a = [[5, 0], [10, 0]]; - var b = [[-10, 0], [-5, 0]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns null if lines are parallel', function() { - var a = [[0, 0], [10, 0]]; - var b = [[0, 5], [10, 5]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - it('returns the intersection point between 2 lines', function() { - var a = [[0, 0], [10, 0]]; - var b = [[5, 10], [5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); - }); - it('returns null if lines are not parallel but not intersecting', function() { - var a = [[0, 0], [10, 0]]; - var b = [[-5, 10], [-5, -10]]; - expect(iD.geoLineIntersection(a, b)).to.be.null; - }); - }); - - describe('geoPointInPolygon', 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.geoPointInPolygon(point, poly)).to.be.true; - }); - it('says a point outside of a polygon is outside', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 1.5]; - expect(iD.geoPointInPolygon(point, poly)).to.be.false; - }); - }); - - describe('geoPolygonContainsPolygon', 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.geoPolygonContainsPolygon(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.geoPolygonContainsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPolygonIntersectsPolygon', function() { - it('returns true when outer polygon fully contains inner', 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.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns true when outer polygon partially contains inner (some vertices contained)', 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.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - - it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; - expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; - }); - - it('returns false when outer and inner are fully disjoint', 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.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('geoPathLength', function() { - it('calculates a simple path length', function() { - var path = [[0, 0], [0, 1], [3, 5]]; - expect(iD.geoPathLength(path)).to.eql(6); - }); - - it('does not fail on single-point path', function() { - var path = [[0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - - it('estimates zero-length edges', function() { - var path = [[0, 0], [0, 0]]; - expect(iD.geoPathLength(path)).to.eql(0); - }); - }); - - describe('geoViewportEdge', function() { - var dimensions = [1000, 1000]; - it('returns null if the point is not at the edge', function() { - expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; - }); - it('nudges top edge', function() { - expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); - }); - it('nudges top-right corner', function() { - expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); - }); - it('nudges right edge', function() { - expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); - }); - it('nudges bottom-right corner', function() { - expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); - }); - it('nudges bottom edge', function() { - expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); - }); - it('nudges bottom-left corner', function() { - expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); - }); - it('nudges left edge', function() { - expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); - }); - it('nudges top-left corner', function() { - expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); - }); - }); - }); diff --git a/test/spec/geo/geom.js b/test/spec/geo/geom.js new file mode 100644 index 000000000..11b37c35a --- /dev/null +++ b/test/spec/geo/geom.js @@ -0,0 +1,297 @@ +describe('iD.geo - geometry', function() { + + describe('geoAngle', function() { + it('returns angle between a and b', function() { + var projection = function (_) { return _; }; + expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoEdgeEqual', function() { + it('returns false for inequal edges', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'c'])).to.be.false; + }); + + it('returns true for equal edges along same direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['a', 'b'])).to.be.true; + }); + + it('returns true for equal edges along opposite direction', function() { + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; + }); + }); + + describe('geoRotate', function() { + it('rotates points around [0, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [0, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(-5, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(-5, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + + it('rotates points around [3, 0]', function() { + var points = [[5, 0], [5, 1]]; + var angle = Math.PI; + var around = [3, 0]; + var result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(1, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(1, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + }); + + describe('geoChooseEdge', function() { + var projection = function (l) { return l; }; + projection.invert = projection; + + it('returns null for a degenerate way (no nodes)', function() { + expect(iD.geoChooseEdge([], [0, 0], projection)).to.be.null; + }); + + it('returns null for a degenerate way (single node)', function() { + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.be.null; + }); + + it('calculates the orthogonal projection of a point onto a segment', function() { + // a --*--- b + // | + // c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [2, 1]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(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]; + var b = [5, 0]; + var c = [-3, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(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]; + var b = [5, 0]; + var c = [8, 4]; + var nodes = [ iD.osmNode({loc: a}), iD.osmNode({loc: b}) ]; + var choice = iD.geoChooseEdge(nodes, c, projection); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(5); + expect(choice.loc).to.eql([5, 0]); + }); + + it('skips the given nodeID at end of way', function() { + // + // a --*-- b + // e | + // | | + // d - c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 5]; + var e = [2, 0.1]; // e.g. user is dragging e onto ab + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, e, projection, 'e'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('skips the given nodeID in middle of way', function() { + // + // a --*-- b + // d | + // / \ | + // e c + // + // * = [2, 0] + var a = [0, 0]; + var b = [5, 0]; + var c = [5, 5]; + var d = [2, 0.1]; // e.g. user is dragging d onto ab + var e = [0, 5]; + var nodes = [ + iD.osmNode({id: 'a', loc: a}), + iD.osmNode({id: 'b', loc: b}), + iD.osmNode({id: 'c', loc: c}), + iD.osmNode({id: 'd', loc: d}), + iD.osmNode({id: 'e', loc: e}) + ]; + var choice = iD.geoChooseEdge(nodes, d, projection, 'd'); + expect(choice.index).to.eql(1); + expect(choice.distance).to.eql(0.1); + expect(choice.loc).to.eql([2, 0]); + }); + + it('returns null if all nodes are skipped', function() { + var nodes = [ + iD.osmNode({id: 'a', loc: [0, 0]}), + iD.osmNode({id: 'b', loc: [5, 0]}), + ]; + var choice = iD.geoChooseEdge(nodes, [2, 2], projection, 'a'); + expect(choice).to.be.null; + }); + }); + + describe('geoLineIntersection', function() { + it('returns null if lines are colinear with overlap', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 0], [5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are colinear but disjoint', function() { + var a = [[5, 0], [10, 0]]; + var b = [[-10, 0], [-5, 0]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns null if lines are parallel', function() { + var a = [[0, 0], [10, 0]]; + var b = [[0, 5], [10, 5]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + it('returns the intersection point between 2 lines', function() { + var a = [[0, 0], [10, 0]]; + var b = [[5, 10], [5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.eql([5, 0]); + }); + it('returns null if lines are not parallel but not intersecting', function() { + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; + expect(iD.geoLineIntersection(a, b)).to.be.null; + }); + }); + + describe('geoPointInPolygon', 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.geoPointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.geoPointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('geoPolygonContainsPolygon', 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.geoPolygonContainsPolygon(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.geoPolygonContainsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPolygonIntersectsPolygon', function() { + it('returns true when outer polygon fully contains inner', 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.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns true when outer polygon partially contains inner (some vertices contained)', 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.geoPolygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('returns false when outer polygon partially contains inner (no vertices contained - lax test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + + it('returns true when outer polygon partially contains inner (no vertices contained - strict test)', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, -1], [1, 4], [2, 4], [2, -1], [1, -1]]; + expect(iD.geoPolygonIntersectsPolygon(outer, inner, true)).to.be.true; + }); + + it('returns false when outer and inner are fully disjoint', 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.geoPolygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('geoPathLength', function() { + it('calculates a simple path length', function() { + var path = [[0, 0], [0, 1], [3, 5]]; + expect(iD.geoPathLength(path)).to.eql(6); + }); + + it('does not fail on single-point path', function() { + var path = [[0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + + it('estimates zero-length edges', function() { + var path = [[0, 0], [0, 0]]; + expect(iD.geoPathLength(path)).to.eql(0); + }); + }); + + describe('geoViewportEdge', function() { + var dimensions = [1000, 1000]; + it('returns null if the point is not at the edge', function() { + expect(iD.geoViewportEdge([500, 500], dimensions)).to.be.null; + }); + it('nudges top edge', function() { + expect(iD.geoViewportEdge([500, 5], dimensions)).to.eql([0, 10]); + }); + it('nudges top-right corner', function() { + expect(iD.geoViewportEdge([995, 5], dimensions)).to.eql([-10, 10]); + }); + it('nudges right edge', function() { + expect(iD.geoViewportEdge([995, 500], dimensions)).to.eql([-10, 0]); + }); + it('nudges bottom-right corner', function() { + expect(iD.geoViewportEdge([995, 995], dimensions)).to.eql([-10, -10]); + }); + it('nudges bottom edge', function() { + expect(iD.geoViewportEdge([500, 995], dimensions)).to.eql([0, -10]); + }); + it('nudges bottom-left corner', function() { + expect(iD.geoViewportEdge([5, 995], dimensions)).to.eql([10, -10]); + }); + it('nudges left edge', function() { + expect(iD.geoViewportEdge([5, 500], dimensions)).to.eql([10, 0]); + }); + it('nudges top-left corner', function() { + expect(iD.geoViewportEdge([5, 5], dimensions)).to.eql([10, 10]); + }); + }); + +}); diff --git a/test/spec/geo/vector.js b/test/spec/geo/vector.js index 21968bb5a..7b69aab00 100644 --- a/test/spec/geo/vector.js +++ b/test/spec/geo/vector.js @@ -1,4 +1,4 @@ -describe('iD.geo vector', function() { +describe('iD.geo - vector', function() { describe('geoVecEqual', function() { it('tests vectors for equality', function() {