From 2e2b037e36c51427ce0a329cd668c79b4ba114e3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 18 Dec 2017 15:05:42 -0500 Subject: [PATCH] Move a bunch of commonly used vector and projection math functions into geo - geoVecAdd - geoVecSubtract - geoVecScale - geoZoomToScale - geoScaleToZoom --- modules/actions/move.js | 88 ++++----- modules/geo/geo.js | 180 +++++++++++-------- modules/geo/index.js | 8 +- modules/modes/drag_node.js | 14 +- modules/osm/way.js | 15 +- modules/renderer/tile_layer.js | 114 ++++++------ modules/services/osm.js | 28 +-- modules/svg/labels.js | 8 +- modules/svg/points.js | 7 +- modules/svg/vertices.js | 13 +- modules/ui/edit_menu.js | 4 +- modules/ui/fields/restrictions.js | 5 +- modules/ui/map_in_map.js | 119 ++++++------- modules/ui/radial_menu.js | 4 +- test/spec/geo/geo.js | 287 +++++++++++++++++------------- test/spec/svg/areas.js | 5 +- test/spec/svg/layers.js | 5 +- test/spec/svg/lines.js | 5 +- test/spec/svg/midpoints.js | 5 +- test/spec/svg/points.js | 5 +- test/spec/svg/vertices.js | 5 +- 21 files changed, 485 insertions(+), 439 deletions(-) diff --git a/modules/actions/move.js b/modules/actions/move.js index ddc1da8a8..baa75fb32 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -11,22 +11,21 @@ import _without from 'lodash-es/without'; import { osmNode } from '../osm'; import { - geoChooseEdge, geoAngle, + geoChooseEdge, geoInterp, geoPathIntersections, geoPathLength, - geoSphericalDistance + geoSphericalDistance, + geoVecAdd, + geoVecSubtract } from '../geo'; // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as export function actionMove(moveIds, tryDelta, projection, cache) { - var delta = tryDelta; - - function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1]]; } - function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; } + var _delta = tryDelta; function setupCache(graph) { function canMove(nodeId) { @@ -118,11 +117,11 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Place a vertex where the moved vertex used to be, to preserve way shape.. - function replaceMovedVertex(nodeId, wayId, graph, delta) { - var way = graph.entity(wayId), - moved = graph.entity(nodeId), - movedIndex = way.nodes.indexOf(nodeId), - len, prevIndex, nextIndex; + function replaceMovedVertex(nodeId, wayId, graph, _delta) { + var way = graph.entity(wayId); + var moved = graph.entity(nodeId); + var movedIndex = way.nodes.indexOf(nodeId); + var len, prevIndex, nextIndex; if (way.isClosed()) { len = way.nodes.length - 1; @@ -134,14 +133,14 @@ export function actionMove(moveIds, tryDelta, projection, cache) { nextIndex = movedIndex + 1; } - var prev = graph.hasEntity(way.nodes[prevIndex]), - next = graph.hasEntity(way.nodes[nextIndex]); + var prev = graph.hasEntity(way.nodes[prevIndex]); + var next = graph.hasEntity(way.nodes[nextIndex]); // Don't add orig vertex at endpoint.. if (!prev || !next) return graph; - var key = wayId + '_' + nodeId, - orig = cache.replacedVertex[key]; + var key = wayId + '_' + nodeId; + var orig = cache.replacedVertex[key]; if (!orig) { orig = osmNode(); cache.replacedVertex[key] = orig; @@ -149,9 +148,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } var start, end; - if (delta) { + if (_delta) { start = projection(cache.startLoc[nodeId]); - end = projection.invert(vecAdd(start, delta)); + end = projection.invert(geoVecAdd(start, _delta)); } else { end = cache.startLoc[nodeId]; } @@ -184,24 +183,24 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Reorder nodes around intersections that have moved.. function unZorroIntersection(intersection, graph) { - var vertex = graph.entity(intersection.nodeId), - way1 = graph.entity(intersection.movedId), - way2 = graph.entity(intersection.unmovedId), - isEP1 = intersection.movedIsEP, - isEP2 = intersection.unmovedIsEP; + var vertex = graph.entity(intersection.nodeId); + var way1 = graph.entity(intersection.movedId); + var way2 = graph.entity(intersection.unmovedId); + var isEP1 = intersection.movedIsEP; + var isEP2 = intersection.unmovedIsEP; // don't move the vertex if it is the endpoint of both ways. if (isEP1 && isEP2) return graph; - var nodes1 = _without(graph.childNodes(way1), vertex), - nodes2 = _without(graph.childNodes(way2), vertex); + var nodes1 = _without(graph.childNodes(way1), vertex); + var nodes2 = _without(graph.childNodes(way2), vertex); if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]); if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]); - var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection), - edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection), - loc; + var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection); + var edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection); + var loc; // snap vertex to nearest edge (or some point between them).. if (!isEP1 && !isEP2) { @@ -236,7 +235,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { function cleanupIntersections(graph) { _each(cache.intersection, function(obj) { - graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta); + graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, _delta); graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null); graph = unZorroIntersection(obj, graph); }); @@ -245,7 +244,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } - // check if moving way endpoint can cross an unmoved way, if so limit delta.. + // check if moving way endpoint can cross an unmoved way, if so limit _delta.. function limitDelta(graph) { _each(cache.intersection, function(obj) { // Don't limit movement if this is vertex joins 2 endpoints.. @@ -253,27 +252,28 @@ export function actionMove(moveIds, tryDelta, projection, cache) { // Don't limit movement if this vertex is not an endpoint anyway.. if (!obj.movedIsEP) return; - var node = graph.entity(obj.nodeId), - start = projection(node.loc), - end = vecAdd(start, delta), - movedNodes = graph.childNodes(graph.entity(obj.movedId)), - movedPath = _map(_map(movedNodes, 'loc'), - function(loc) { return vecAdd(projection(loc), delta); }), - unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)), - unmovedPath = _map(_map(unmovedNodes, 'loc'), projection), - hits = geoPathIntersections(movedPath, unmovedPath); + var node = graph.entity(obj.nodeId); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); + var movedNodes = graph.childNodes(graph.entity(obj.movedId)); + var movedPath = _map(_map(movedNodes, 'loc'), function(loc) { + return geoVecAdd(projection(loc), _delta); + }); + var unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)); + var unmovedPath = _map(_map(unmovedNodes, 'loc'), projection); + var hits = geoPathIntersections(movedPath, unmovedPath); for (var i = 0; i < hits.length; i++) { if (_isEqual(hits[i], end)) continue; var edge = geoChooseEdge(unmovedNodes, end, projection); - delta = vecSub(projection(edge.loc), start); + _delta = geoVecSubtract(projection(edge.loc), start); } }); } var action = function(graph) { - if (delta[0] === 0 && delta[1] === 0) return graph; + if (_delta[0] === 0 && _delta[1] === 0) return graph; setupCache(graph); @@ -282,9 +282,9 @@ export function actionMove(moveIds, tryDelta, projection, cache) { } _each(cache.nodes, function(id) { - var node = graph.entity(id), - start = projection(node.loc), - end = vecAdd(start, delta); + var node = graph.entity(id); + var start = projection(node.loc); + var end = geoVecAdd(start, _delta); graph = graph.replace(node.move(projection.invert(end))); }); @@ -297,7 +297,7 @@ export function actionMove(moveIds, tryDelta, projection, cache) { action.delta = function() { - return delta; + return _delta; }; diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 2f74eb40a..e96bc81b9 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -2,94 +2,130 @@ import _every from 'lodash-es/every'; import _some from 'lodash-es/some'; -export function geoRoundCoords(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; +// constants +var TAU = 2 * Math.PI; +var EQUATORIAL_RADIUS = 6356752.314245179; +var POLAR_RADIUS = 6378137.0; + + +// vector addition +export function geoVecAdd(a, b) { + return [ a[0] + b[0], a[1] + b[1] ]; } +// vector subtraction +export function geoVecSubtract(a, b) { + return [ a[0] - b[0], a[1] - b[1] ]; +} +// vector multiplication +export function geoVecScale(a, b) { + return [ a[0] * b, a[1] * b ]; +} + +// vector rounding (was: geoRoundCoordinates) +export function geoVecFloor(a) { + return [ Math.floor(a[0]), Math.floor(a[1]) ]; +} + +// linear interpolation export function geoInterp(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; + return [ + p1[0] + (p2[0] - p1[0]) * 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. +// dot product +export function geoDot(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[0] - origin[0]) + + (a[1] - origin[1]) * (b[1] - origin[1]); +} + + +// 2D cross product of OA and OB vectors, returns magnitude of Z vector // Returns a positive value, if OAB makes a counter-clockwise turn, // negative for clockwise turn, and zero if the points are collinear. -export function geoCross(o, a, b) { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); +export function geoCross(a, b, origin) { + origin = origin || [0, 0]; + return (a[0] - origin[0]) * (b[1] - origin[1]) - + (a[1] - origin[1]) * (b[0] - origin[0]); } // http://jsperf.com/id-dist-optimization export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0], y = a[1] - b[1]; + var x = a[0] - b[0]; + var y = a[1] - b[1]; return Math.sqrt((x * x) + (y * y)); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoLatToMeters(dLat) { - return dLat * 110946.257617; + return dLat * (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoLonToMeters(dLon, atLat) { return Math.abs(atLat) >= 90 ? 0 : - dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); + dLon * (TAU * EQUATORIAL_RADIUS / 360) * Math.abs(Math.cos(atLat * (Math.PI / 180))); } -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 export function geoMetersToLat(m) { - return m / 110946.257617; + return m / (TAU * POLAR_RADIUS / 360); } -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 export function geoMetersToLon(m, atLat) { return Math.abs(atLat) >= 90 ? 0 : - m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); + m / (TAU * EQUATORIAL_RADIUS / 360) / Math.abs(Math.cos(atLat * (Math.PI / 180))); } -export function geoOffsetToMeters(offset) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoOffsetToMeters(offset, tileSize) { + tileSize = tileSize || 256; return [ - offset[0] * 2 * Math.PI * equatRadius / tileSize, - -offset[1] * 2 * Math.PI * polarRadius / tileSize + offset[0] * TAU * EQUATORIAL_RADIUS / tileSize, + -offset[1] * TAU * POLAR_RADIUS / tileSize ]; } -export function geoMetersToOffset(meters) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - +export function geoMetersToOffset(meters, tileSize) { + tileSize = tileSize || 256; return [ - meters[0] * tileSize / (2 * Math.PI * equatRadius), - -meters[1] * tileSize / (2 * Math.PI * polarRadius) + meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS), + -meters[1] * tileSize / (TAU * POLAR_RADIUS) ]; } // Equirectangular approximation of spherical distances on Earth export function geoSphericalDistance(a, b) { - var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), - y = geoLatToMeters(a[1] - b[1]); + var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2); + var y = geoLatToMeters(a[1] - b[1]); return Math.sqrt((x * x) + (y * y)); } +// 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; + var log2ts = Math.log(tileSize) * Math.LOG2E; + return Math.log(k * TAU) / Math.LN2 - log2ts; +} + + export function geoEdgeEqual(a, b) { return (a[0] === b[0] && a[1] === b[1]) || (a[0] === b[1] && a[1] === b[0]); @@ -122,23 +158,18 @@ export function geoRotate(points, angle, around) { // 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) { - var dist = geoEuclideanDistance, - points = nodes.map(function(n) { return projection(n.loc); }), - min = Infinity, - idx, loc; - - function dot(p, q) { - return p[0] * q[0] + p[1] * q[1]; - } + var dist = geoEuclideanDistance; + var points = nodes.map(function(n) { return projection(n.loc); }); + var min = Infinity; + var idx; + var loc; 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; + var o = points[i]; + var s = geoVecSubtract(points[i + 1], o); + var v = geoVecSubtract(point, o); + var proj = geoDot(v, s) / geoDot(s, s); + var p; if (proj < 0) { p = o; @@ -169,25 +200,18 @@ export function geoChooseEdge(nodes, point, projection) { // This uses the vector cross product approach described below: // http://stackoverflow.com/a/565282/786339 export function geoLineIntersection(a, b) { - function subtractPoints(point1, point2) { - return [point1[0] - point2[0], point1[1] - point2[1]]; - } - function crossProduct(point1, point2) { - return point1[0] * point2[1] - point1[1] * point2[0]; - } - - var p = [a[0][0], a[0][1]], - p2 = [a[1][0], a[1][1]], - q = [b[0][0], b[0][1]], - q2 = [b[1][0], b[1][1]], - r = subtractPoints(p2, p), - s = subtractPoints(q2, q), - uNumerator = crossProduct(subtractPoints(q, p), r), - denominator = crossProduct(r, s); + 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 = geoCross(geoVecSubtract(q, p), r); + var denominator = geoCross(r, s); if (uNumerator && denominator) { - var u = uNumerator / denominator, - t = crossProduct(subtractPoints(q, p), s) / denominator; + var u = uNumerator / denominator; + var t = geoCross(geoVecSubtract(q, p), s) / denominator; if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { return geoInterp(p, p2, t); @@ -202,10 +226,12 @@ 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] ], - b = [ path2[j], path2[j+1] ], - hit = geoLineIntersection(a, b); - if (hit) intersections.push(hit); + 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; @@ -222,9 +248,9 @@ export function geoPathIntersections(path1, path2) { // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html // export function geoPointInPolygon(point, polygon) { - var x = point[0], - y = point[1], - inside = false; + 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]; @@ -250,8 +276,8 @@ 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] ], - b = [ inner[j], inner[j+1] ]; + var a = [ outer[i], outer[i +1 ] ]; + var b = [ inner[j], inner[j + 1] ]; if (geoLineIntersection(a, b)) return true; } } diff --git a/modules/geo/index.js b/modules/geo/index.js index 8966c218b..2a1c8fa31 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,12 +1,12 @@ export { geoAngle } from './geo.js'; export { geoChooseEdge } from './geo.js'; export { geoCross } from './geo.js'; +export { geoDot } from './geo.js'; export { geoEdgeEqual } from './geo.js'; export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; -export { geoRoundCoords } from './geo.js'; export { geoRotate } from './geo.js'; export { geoLatToMeters } from './geo.js'; export { geoLineIntersection } from './geo.js'; @@ -20,5 +20,11 @@ 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 { geoVecAdd } from './geo.js'; +export { geoVecFloor } from './geo.js'; +export { geoVecSubtract } from './geo.js'; +export { geoVecScale } from './geo.js'; +export { geoZoomToScale } from './geo.js'; export { geoViewportEdge } from './geo.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index b52c2da97..915706bbd 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -23,7 +23,12 @@ import { modeSelect } from './index'; -import { geoChooseEdge, geoViewportEdge } from '../geo'; +import { + geoChooseEdge, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + import { osmNode } from '../osm'; import { utilEntitySelector } from '../util'; import { uiFlash } from '../ui'; @@ -46,11 +51,6 @@ export function modeDragNode(context) { var _lastLoc; - function vecSub(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - function startNudge(entity, nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); _nudgeInterval = window.setInterval(function() { @@ -135,7 +135,7 @@ export function modeDragNode(context) { nudge = nudge || [0, 0]; var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); - var currMouse = vecSub(currPoint, nudge); + var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); if (!_nudgeInterval) { diff --git a/modules/osm/way.js b/modules/osm/way.js index 76918a52e..c93224146 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -133,15 +133,16 @@ _extend(osmWay.prototype, { isConvex: function(resolver) { if (!this.isClosed() || this.isDegenerate()) return null; - var nodes = _uniq(resolver.childNodes(this)), - coords = _map(nodes, 'loc'), - curr = 0, prev = 0; + var nodes = _uniq(resolver.childNodes(this)); + var coords = _map(nodes, 'loc'); + var curr = 0; + var 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 = geoCross(o, a, b); + var o = coords[(i+1) % coords.length]; + var a = coords[i]; + var b = coords[(i+2) % coords.length]; + var res = geoCross(a, b, o); curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; if (curr === 0) { diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js index ceb846117..9d143f045 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -2,28 +2,29 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; -import { geoEuclideanDistance } from '../geo'; +import { geoEuclideanDistance, geoScaleToZoom } from '../geo'; import { utilPrefixCSSProperty } from '../util'; export function rendererTileLayer(context) { - var tileSize = 256, - geotile = d3_geoTile(), - projection, - cache = {}, - tileOrigin, - z, - transformProp = utilPrefixCSSProperty('Transform'), - source; + var tileSize = 256; + var transformProp = utilPrefixCSSProperty('Transform'); + var geotile = d3_geoTile(); + + var _projection; + var _cache = {}; + var _tileOrigin; + var _zoom; + var _source; // blacklist overlay tiles around Null Island.. function nearNullIsland(x, y, z) { if (z >= 7) { - var center = Math.pow(2, z - 1), - width = Math.pow(2, z - 6), - min = center - (width / 2), - max = center + (width / 2) - 1; + var center = Math.pow(2, z - 1); + var width = Math.pow(2, z - 6); + var min = center - (width / 2); + var max = center + (width / 2) - 1; return x >= min && x <= max && y >= min && y <= max; } return false; @@ -31,8 +32,8 @@ export function rendererTileLayer(context) { function tileSizeAtZoom(d, z) { - var epsilon = 0.002; - return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon; + var EPSILON = 0.002; + return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON; } @@ -49,7 +50,7 @@ export function rendererTileLayer(context) { function lookUp(d) { for (var up = -1; up > -d[2]; up--) { var tile = atZoom(d, up); - if (cache[source.url(tile)] !== false) { + if (_cache[_source.url(tile)] !== false) { return tile; } } @@ -57,7 +58,8 @@ export function rendererTileLayer(context) { function uniqueBy(a, n) { - var o = [], seen = {}; + var o = []; + var seen = {}; for (var i = 0; i < a.length; i++) { if (seen[a[i][n]] === undefined) { o.push(a[i]); @@ -69,37 +71,37 @@ export function rendererTileLayer(context) { function addSource(d) { - d.push(source.url(d)); + d.push(_source.url(d)); return d; } // Update tiles based on current state of `projection`. function background(selection) { - z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0); + _zoom = geoScaleToZoom(_projection.scale(), tileSize); var pixelOffset; - if (source) { + if (_source) { pixelOffset = [ - source.offset()[0] * Math.pow(2, z), - source.offset()[1] * Math.pow(2, z) + _source.offset()[0] * Math.pow(2, _zoom), + _source.offset()[1] * Math.pow(2, _zoom) ]; } else { pixelOffset = [0, 0]; } var translate = [ - projection.translate()[0] + pixelOffset[0], - projection.translate()[1] + pixelOffset[1] + _projection.translate()[0] + pixelOffset[0], + _projection.translate()[1] + pixelOffset[1] ]; geotile - .scale(projection.scale() * 2 * Math.PI) + .scale(_projection.scale() * 2 * Math.PI) .translate(translate); - tileOrigin = [ - projection.scale() * Math.PI - translate[0], - projection.scale() * Math.PI - translate[1] + _tileOrigin = [ + _projection.scale() * Math.PI - translate[0], + _projection.scale() * Math.PI - translate[1] ]; render(selection); @@ -107,36 +109,36 @@ export function rendererTileLayer(context) { // Derive the tiles onscreen, remove those offscreen and position them. - // Important that this part not depend on `projection` because it's + // Important that this part not depend on `_projection` because it's // rentered when tiles load/error (see #644). function render(selection) { - if (!source) return; + if (!_source) return; var requests = []; - var showDebug = context.getDebug('tile') && !source.overlay; + var showDebug = context.getDebug('tile') && !_source.overlay; - if (source.validZoom(z)) { + if (_source.validZoom(_zoom)) { geotile().forEach(function(d) { addSource(d); if (d[3] === '') return; if (typeof d[3] !== 'string') return; // Workaround for #2295 requests.push(d); - if (cache[d[3]] === false && lookUp(d)) { + if (_cache[d[3]] === false && lookUp(d)) { requests.push(addSource(lookUp(d))); } }); requests = uniqueBy(requests, 3).filter(function(r) { - if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) { + if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) { return false; } // don't re-request tiles which have failed in the past - return cache[r[3]] !== false; + return _cache[r[3]] !== false; }); } function load(d) { - cache[d[3]] = true; + _cache[d[3]] = true; d3_select(this) .on('error', null) .on('load', null) @@ -145,7 +147,7 @@ export function rendererTileLayer(context) { } function error(d) { - cache[d[3]] = false; + _cache[d[3]] = false; d3_select(this) .on('error', null) .on('load', null) @@ -154,19 +156,19 @@ export function rendererTileLayer(context) { } function imageTransform(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); - var scale = tileSizeAtZoom(d, z); + var ts = tileSize * Math.pow(2, _zoom - d[2]); + var scale = tileSizeAtZoom(d, _zoom); return 'translate(' + - ((d[0] * _ts) - tileOrigin[0]) + 'px,' + - ((d[1] * _ts) - tileOrigin[1]) + 'px) ' + + ((d[0] * ts) - _tileOrigin[0]) + 'px,' + + ((d[1] * ts) - _tileOrigin[1]) + 'px) ' + 'scale(' + scale + ',' + scale + ')'; } function tileCenter(d) { - var _ts = tileSize * Math.pow(2, z - d[2]); + var ts = tileSize * Math.pow(2, _zoom - d[2]); return [ - ((d[0] * _ts) - tileOrigin[0] + (_ts / 2)), - ((d[1] * _ts) - tileOrigin[1] + (_ts / 2)) + ((d[0] * ts) - _tileOrigin[0] + (ts / 2)), + ((d[1] * ts) - _tileOrigin[1] + (ts / 2)) ]; } @@ -178,10 +180,10 @@ export function rendererTileLayer(context) { // Pick a representative tile near the center of the viewport // (This is useful for sampling the imagery vintage) - var dims = geotile.size(), - mapCenter = [dims[0] / 2, dims[1] / 2], - minDist = Math.max(dims[0], dims[1]), - nearCenter; + var dims = geotile.size(); + var mapCenter = [dims[0] / 2, dims[1] / 2]; + var minDist = Math.max(dims[0], dims[1]); + var nearCenter; requests.forEach(function(d) { var c = tileCenter(d); @@ -255,8 +257,8 @@ export function rendererTileLayer(context) { .selectAll('.tile-label-debug-vintage') .each(function(d) { var span = d3_select(this); - var center = context.projection.invert(tileCenter(d)); - source.getMetadata(center, d, function(err, result) { + var center = context._projection.invert(tileCenter(d)); + _source.getMetadata(center, d, function(err, result) { span.text((result && result.vintage && result.vintage.range) || t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown') ); @@ -268,8 +270,8 @@ export function rendererTileLayer(context) { background.projection = function(_) { - if (!arguments.length) return projection; - projection = _; + if (!arguments.length) return _projection; + _projection = _; return background; }; @@ -282,10 +284,10 @@ export function rendererTileLayer(context) { background.source = function(_) { - if (!arguments.length) return source; - source = _; - cache = {}; - geotile.scaleExtent(source.scaleExtent); + if (!arguments.length) return _source; + _source = _; + _cache = {}; + geotile.scaleExtent(_source.scaleExtent); return background; }; diff --git a/modules/services/osm.js b/modules/services/osm.js index cf33659a3..af96685a3 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -500,8 +500,8 @@ export default { } // update blacklists - var elements = xml.getElementsByTagName('blacklist'), - regexes = []; + var elements = xml.getElementsByTagName('blacklist'); + var regexes = []; for (var i = 0; i < elements.length; i++) { var regex = elements[i].getAttribute('regex'); // needs unencode? if (regex) { @@ -516,8 +516,8 @@ export default { if (rateLimitError) { callback(rateLimitError, 'rateLimited'); } else { - var apiStatus = xml.getElementsByTagName('status'), - val = apiStatus[0].getAttribute('api'); + var apiStatus = xml.getElementsByTagName('status'); + var val = apiStatus[0].getAttribute('api'); callback(undefined, val); } @@ -544,14 +544,14 @@ export default { loadTiles: function(projection, dimensions, callback) { if (off) return; - var that = this, - s = projection.scale() * 2 * Math.PI, - z = Math.max(Math.log(s) / Math.log(2) - 8, 0), - ts = 256 * Math.pow(2, z - tileZoom), - origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; + var that = this; + var s = projection.scale() * 2 * Math.PI; + var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); + var ts = 256 * Math.pow(2, z - tileZoom); + var origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1] + ]; var tiles = d3_geoTile() .scaleExtent([tileZoom, tileZoom]) @@ -559,8 +559,8 @@ export default { .size(dimensions) .translate(projection.translate())() .map(function(tile) { - var x = tile[0] * ts - origin[0], - y = tile[1] * ts - origin[1]; + var x = tile[0] * ts - origin[0]; + var y = tile[1] * ts - origin[1]; return { id: tile.toString(), diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 2e65d593a..15c3d2b08 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -13,7 +13,8 @@ import { geoEuclideanDistance, geoInterp, geoPolygonIntersectsPolygon, - geoPathLength + geoPathLength, + geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; @@ -27,9 +28,6 @@ import { } from '../util'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - export function svgLabels(projection, context) { var path = d3_geoPath(projection); @@ -261,7 +259,7 @@ export function svgLabels(projection, context) { function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var labelable = []; var renderNodeAs = {}; diff --git a/modules/svg/points.js b/modules/svg/points.js index 124dba5bc..8c41f7a1a 100644 --- a/modules/svg/points.js +++ b/modules/svg/points.js @@ -1,12 +1,9 @@ import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform, svgTagClasses } from './index'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - - export function svgPoints(projection, context) { function markerPath(selection, klass) { @@ -55,7 +52,7 @@ export function svgPoints(projection, context) { function drawPoints(selection, graph, entities, filter) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); // points with a direction will render as vertices at higher zooms function renderAsPoint(entity) { diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index 06ab1e7b6..df681a2d1 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -4,14 +4,11 @@ import _values from 'lodash-es/values'; import { select as d3_select } from 'd3-selection'; import { dataFeatureIcons } from '../../data'; +import { geoScaleToZoom } from '../geo'; import { osmEntity } from '../osm'; import { svgPointTransform } from './index'; -var TAU = 2 * Math.PI; -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } - - export function svgVertices(projection, context) { var radiuses = { // z16-, z17, z18+, w/icon @@ -46,7 +43,7 @@ export function svgVertices(projection, context) { var icons = {}; var directions = {}; var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2); @@ -258,7 +255,7 @@ export function svgVertices(projection, context) { function drawVertices(selection, graph, entities, filter, extent, fullRedraw) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); var mode = context.mode(); var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id); @@ -318,7 +315,7 @@ export function svgVertices(projection, context) { // partial redraw - only update the selected items.. drawVertices.drawSelected = function(selection, graph, target, extent) { var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); _prevSelected = _currSelected || {}; _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom); @@ -334,7 +331,7 @@ export function svgVertices(projection, context) { if (target === _currHoverTarget) return; // continue only if something changed var wireframe = context.surface().classed('fill-wireframe'); - var zoom = ktoz(projection.scale()); + var zoom = geoScaleToZoom(projection.scale()); _prevHover = _currHover || {}; _currHoverTarget = target; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index b0856c56f..f29503bec 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { textDirection } from '../util/locale'; import { uiTooltipHtml } from './tooltipHtml'; @@ -81,7 +81,7 @@ export function uiEditMenu(context, operations) { .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; }) .classed('disabled', function (d) { return d.disabled(); }) .attr('transform', function (d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ 0, m + i * buttonHeight ]).join(',') + ')'; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index a89c7a9b5..f56bc868d 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -26,7 +26,8 @@ import { import { geoExtent, - geoRawMercator + geoRawMercator, + geoZoomToScale } from '../../geo'; import { @@ -84,7 +85,7 @@ export function uiFieldRestrictions(field, context) { var z = 24; projection - .scale(256 * Math.pow(2, z) / (2 * Math.PI)); + .scale(geoZoomToScale(z)); var s = projection(vertex.loc); diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 4a6609c98..08ca8b44d 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -13,45 +13,44 @@ import { import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { svgDebug, svgGpx } from '../svg'; -import { geoRawMercator } from '../geo'; +import { + geoRawMercator, + geoScaleToZoom, + geoVecSubtract, + geoVecScale, + geoZoomToScale, +} from '../geo'; + import { rendererTileLayer } from '../renderer'; +import { svgDebug, svgGpx } from '../svg'; import { utilSetTransform } from '../util'; import { utilGetDimensions } from '../util/dimensions'; -var TAU = 2 * Math.PI; -function ztok(z) { return 256 * Math.pow(2, z) / TAU; } -function ktoz(k) { return Math.log(k * TAU) / Math.LN2 - 8; } -function vecSub(a, b) { return [ a[0] - b[0], a[1] - b[1] ]; } -function vecScale(a, b) { return [ a[0] * b, a[1] * b ]; } - - export function uiMapInMap(context) { - function map_in_map(selection) { - var backgroundLayer = rendererTileLayer(context), - overlayLayers = {}, - projection = geoRawMercator(), - gpxLayer = svgGpx(projection, context).showLabels(false), - debugLayer = svgDebug(projection, context), - zoom = d3_zoom() - .scaleExtent([ztok(0.5), ztok(24)]) - .on('start', zoomStarted) - .on('zoom', zoomed) - .on('end', zoomEnded), - isTransformed = false, - isHidden = true, - skipEvents = false, - gesture = null, - zDiff = 6, // by default, minimap renders at (main zoom - 6) - wrap = d3_select(null), - tiles = d3_select(null), - viewport = d3_select(null), - tStart, // transform at start of gesture - tCurr, // transform at most recent event - timeoutId; + var backgroundLayer = rendererTileLayer(context); + var overlayLayers = {}; + var projection = geoRawMercator(); + var gpxLayer = svgGpx(projection, context).showLabels(false); + var debugLayer = svgDebug(projection, context); + var zoom = d3_zoom() + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)]) + .on('start', zoomStarted) + .on('zoom', zoomed) + .on('end', zoomEnded); + var isTransformed = false; + var isHidden = true; + var skipEvents = false; + var gesture = null; + var zDiff = 6; // by default, minimap renders at (main zoom - 6) + var wrap = d3_select(null); + var tiles = d3_select(null); + var viewport = d3_select(null); + var tStart; // transform at start of gesture + var tCurr; // transform at most recent event + var timeoutId; function zoomStarted() { @@ -64,11 +63,11 @@ export function uiMapInMap(context) { function zoomed() { if (skipEvents) return; - var x = d3_event.transform.x, - y = d3_event.transform.y, - k = d3_event.transform.k, - isZooming = (k !== tStart.k), - isPanning = (x !== tStart.x || y !== tStart.y); + var x = d3_event.transform.x; + var y = d3_event.transform.y; + var k = d3_event.transform.k; + var isZooming = (k !== tStart.k); + var isPanning = (x !== tStart.x || y !== tStart.y); if (!isZooming && !isPanning) { return; // no change @@ -79,12 +78,12 @@ export function uiMapInMap(context) { gesture = isZooming ? 'zoom' : 'pan'; } - var tMini = projection.transform(), - tX, tY, scale; + var tMini = projection.transform(); + var tX, tY, scale; if (gesture === 'zoom') { - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); scale = k / tMini.k; tX = (cMini[0] / scale - cMini[0]) * scale; tY = (cMini[1] / scale - cMini[1]) * scale; @@ -100,8 +99,8 @@ export function uiMapInMap(context) { isTransformed = true; tCurr = d3_zoomIdentity.translate(x, y).scale(k); - var zMain = ktoz(context.projection.scale()), - zMini = ktoz(k); + var zMain = geoScaleToZoom(context.projection.scale()); + var zMini = geoScaleToZoom(k); zDiff = zMain - zMini; @@ -115,29 +114,29 @@ export function uiMapInMap(context) { updateProjection(); gesture = null; - var dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); context.map().center(projection.invert(cMini)); // recenter main map.. } function updateProjection() { - var loc = context.map().center(), - dMini = utilGetDimensions(wrap), - cMini = vecScale(dMini, 0.5), - tMain = context.projection.transform(), - zMain = ktoz(tMain.k), - zMini = Math.max(zMain - zDiff, 0.5), - kMini = ztok(zMini); + var loc = context.map().center(); + var dMini = utilGetDimensions(wrap); + var cMini = geoVecScale(dMini, 0.5); + var tMain = context.projection.transform(); + var zMain = geoScaleToZoom(tMain.k); + var zMini = Math.max(zMain - zDiff, 0.5); + var kMini = geoZoomToScale(zMini); projection .translate([tMain.x, tMain.y]) .scale(kMini); - var point = projection(loc), - mouse = (gesture === 'pan') ? vecSub([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0], - xMini = cMini[0] - point[0] + tMain.x + mouse[0], - yMini = cMini[1] - point[1] + tMain.y + mouse[1]; + var point = projection(loc); + var mouse = (gesture === 'pan') ? geoVecSubtract([tCurr.x, tCurr.y], [tStart.x, tStart.y]) : [0, 0]; + var xMini = cMini[0] - point[0] + tMain.x + mouse[0]; + var yMini = cMini[1] - point[1] + tMain.y + mouse[1]; projection .translate([xMini, yMini]) @@ -152,7 +151,7 @@ export function uiMapInMap(context) { } zoom - .scaleExtent([ztok(0.5), ztok(zMain - 3)]); + .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]); skipEvents = true; wrap.call(zoom.transform, tCurr); @@ -166,8 +165,8 @@ export function uiMapInMap(context) { updateProjection(); - var dMini = utilGetDimensions(wrap), - zMini = ktoz(projection.scale()); + var dMini = utilGetDimensions(wrap); + var zMini = geoScaleToZoom(projection.scale()); // setup tile container tiles = wrap @@ -249,8 +248,8 @@ export function uiMapInMap(context) { // redraw viewport bounding box if (gesture !== 'pan') { - var getPath = d3_geoPath(projection), - bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; + var getPath = d3_geoPath(projection); + var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] }; viewport = wrap.selectAll('.map-in-map-viewport') .data([0]); diff --git a/modules/ui/radial_menu.js b/modules/ui/radial_menu.js index 01b6d2458..3afd4f738 100644 --- a/modules/ui/radial_menu.js +++ b/modules/ui/radial_menu.js @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection'; -import { geoRoundCoords } from '../geo'; +import { geoVecFloor } from '../geo'; import { uiTooltipHtml } from './tooltipHtml'; @@ -58,7 +58,7 @@ export function uiRadialMenu(context, operations) { .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) .classed('disabled', function(d) { return d.disabled(); }) .attr('transform', function(d, i) { - return 'translate(' + geoRoundCoords([ + return 'translate(' + geoVecFloor([ r * Math.sin(a0 + i * a), r * Math.cos(a0 + i * a)]).join(',') + ')'; }); diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index b4ac874c6..989c55714 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -1,60 +1,96 @@ describe('iD.geo', function() { - describe('geoRoundCoords', function() { - it('rounds coordinates', function() { - expect(iD.geoRoundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1])).to.eql([0, 1]); - expect(iD.geoRoundCoords([0, 1.1])).to.eql([0, 1]); + + describe('geoVecAdd', function() { + it('adds vectors', function() { + expect(iD.geoVecAdd([1, 2], [3, 4])).to.eql([4, 6]); + expect(iD.geoVecAdd([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecAdd([1, 2], [-3, -4])).to.eql([-2, -2]); + }); + }); + + describe('geoVecSubtract', function() { + it('subtracts vectors', function() { + expect(iD.geoVecSubtract([1, 2], [3, 4])).to.eql([-2, -2]); + expect(iD.geoVecSubtract([1, 2], [0, 0])).to.eql([1, 2]); + expect(iD.geoVecSubtract([1, 2], [-3, -4])).to.eql([4, 6]); + }); + }); + + describe('geoVecScale', function() { + it('multiplies vectors', function() { + expect(iD.geoVecScale([1, 2], 0)).to.eql([0, 0]); + expect(iD.geoVecScale([1, 2], 1)).to.eql([1, 2]); + expect(iD.geoVecScale([1, 2], 2)).to.eql([2, 4]); + expect(iD.geoVecScale([1, 2], 0.5)).to.eql([0.5, 1]); + }); + }); + + describe('geoVecFloor (was: geoRoundCoordinates)', function() { + it('rounds vectors', function() { + expect(iD.geoVecFloor([0.1, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1])).to.eql([0, 1]); + expect(iD.geoVecFloor([0, 1.1])).to.eql([0, 1]); }); }); describe('geoInterp', function() { it('interpolates halfway', function() { - var a = [0, 0], - b = [10, 10]; + var a = [0, 0]; + var b = [10, 10]; expect(iD.geoInterp(a, b, 0.5)).to.eql([5, 5]); }); it('interpolates to one side', function() { - var a = [0, 0], - b = [10, 10]; + var a = [0, 0]; + var b = [10, 10]; expect(iD.geoInterp(a, b, 0)).to.eql([0, 0]); }); }); + describe('geoDot', function() { + it('dot product of right angle is zero', function() { + var a = [1, 0]; + var b = [0, 1]; + expect(iD.geoDot(a, b)).to.eql(0); + }); + it('dot product of same vector multiplies', function() { + var a = [2, 0]; + var b = [2, 0]; + expect(iD.geoDot(a, b)).to.eql(4); + }); + }); + describe('geoCross', function() { - it('cross product of right hand turn is positive', function() { - var o = [0, 0], - a = [2, 0], - b = [0, 2]; - expect(iD.geoCross(o, a, b)).to.eql(4); + it('2D cross product of right hand turn is positive', function() { + var a = [2, 0]; + var b = [0, 2]; + expect(iD.geoCross(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.geoCross(o, a, b)).to.eql(-4); + it('2D cross product of left hand turn is negative', function() { + var a = [2, 0]; + var b = [0, -2]; + expect(iD.geoCross(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.geoCross(o, a, b)).to.equal(0); + it('2D cross product of colinear points is zero', function() { + var a = [-2, 0]; + var b = [2, 0]; + expect(iD.geoCross(a, b)).to.equal(0); }); }); describe('geoEuclideanDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoEuclideanDistance(a, b)).to.eql(0); }); it('a straight 10 unit line is 10', function() { - var a = [0, 0], - b = [10, 0]; + var a = [0, 0]; + var b = [10, 0]; expect(iD.geoEuclideanDistance(a, b)).to.eql(10); }); it('a pythagorean triangle is right', function() { - var a = [0, 0], - b = [4, 3]; + var a = [0, 0]; + var b = [4, 3]; expect(iD.geoEuclideanDistance(a, b)).to.eql(5); }); }); @@ -64,10 +100,10 @@ describe('iD.geo', function() { expect(iD.geoLatToMeters(0)).to.eql(0); }); it('1 degree latitude is approx 111 km', function() { - expect(iD.geoLatToMeters(1)).to.be.within(110E3, 112E3); + expect(iD.geoLatToMeters(1)).to.be.closeTo(111319, 10); }); it('-1 degree latitude is approx -111 km', function() { - expect(iD.geoLatToMeters(-1)).to.be.within(-112E3, -110E3); + expect(iD.geoLatToMeters(-1)).to.be.closeTo(-111319, 10); }); }); @@ -76,21 +112,21 @@ describe('iD.geo', function() { expect(iD.geoLonToMeters(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(1, 0)).to.be.within(110E3, 112E3); - expect(iD.geoLonToMeters(1, 15)).to.be.within(107E3, 108E3); - expect(iD.geoLonToMeters(1, 30)).to.be.within(96E3, 97E3); - expect(iD.geoLonToMeters(1, 45)).to.be.within(78E3, 79E3); - expect(iD.geoLonToMeters(1, 60)).to.be.within(55E3, 56E3); - expect(iD.geoLonToMeters(1, 75)).to.be.within(28E3, 29E3); + expect(iD.geoLonToMeters(1, 0)).to.be.closeTo(110946, 10); + expect(iD.geoLonToMeters(1, 15)).to.be.closeTo(107165, 10); + expect(iD.geoLonToMeters(1, 30)).to.be.closeTo(96082, 10); + expect(iD.geoLonToMeters(1, 45)).to.be.closeTo(78450, 10); + expect(iD.geoLonToMeters(1, 60)).to.be.closeTo(55473, 10); + expect(iD.geoLonToMeters(1, 75)).to.be.closeTo(28715, 10); expect(iD.geoLonToMeters(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoLonToMeters(-1, 0)).to.be.within(-112E3, -110E3); - expect(iD.geoLonToMeters(-1, -15)).to.be.within(-108E3, -107E3); - expect(iD.geoLonToMeters(-1, -30)).to.be.within(-97E3, -96E3); - expect(iD.geoLonToMeters(-1, -45)).to.be.within(-79E3, -78E3); - expect(iD.geoLonToMeters(-1, -60)).to.be.within(-56E3, -55E3); - expect(iD.geoLonToMeters(-1, -75)).to.be.within(-29E3, -28E3); + expect(iD.geoLonToMeters(-1, -0)).to.be.closeTo(-110946, 10); + expect(iD.geoLonToMeters(-1, -15)).to.be.closeTo(-107165, 10); + expect(iD.geoLonToMeters(-1, -30)).to.be.closeTo(-96082, 10); + expect(iD.geoLonToMeters(-1, -45)).to.be.closeTo(-78450, 10); + expect(iD.geoLonToMeters(-1, -60)).to.be.closeTo(-55473, 10); + expect(iD.geoLonToMeters(-1, -75)).to.be.closeTo(-28715, 10); expect(iD.geoLonToMeters(-1, -90)).to.eql(0); }); }); @@ -100,10 +136,10 @@ describe('iD.geo', function() { expect(iD.geoMetersToLat(0)).to.eql(0); }); it('111 km is approx 1 degree latitude', function() { - expect(iD.geoMetersToLat(111E3)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLat(111319)).to.be.closeTo(1, 0.0001); }); it('-111 km is approx -1 degree latitude', function() { - expect(iD.geoMetersToLat(-111E3)).to.be.within(-1.005, -0.995); + expect(iD.geoMetersToLat(-111319)).to.be.closeTo(-1, 0.0001); }); }); @@ -112,22 +148,22 @@ describe('iD.geo', function() { expect(iD.geoMetersToLon(0, 0)).to.eql(0); }); it('distance of 1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(111320, 0)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(107551, 15)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(96486, 30)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(78847, 45)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(55800, 60)).to.be.within(0.995, 1.005); - expect(iD.geoMetersToLon(28902, 75)).to.be.within(0.995, 1.005); + expect(iD.geoMetersToLon(110946, 0)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(107165, 15)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(96082, 30)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(78450, 45)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(55473, 60)).to.be.closeTo(1, 1e-4); + expect(iD.geoMetersToLon(28715, 75)).to.be.closeTo(1, 1e-4); expect(iD.geoMetersToLon(1, 90)).to.eql(0); }); it('distance of -1 degree longitude varies with latitude', function() { - expect(iD.geoMetersToLon(-111320, 0)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-107551, 15)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-96486, 30)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-78847, 45)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-55800, 60)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-28902, 75)).to.be.within(-1.005, -0.995); - expect(iD.geoMetersToLon(-1, 90)).to.eql(0); + expect(iD.geoMetersToLon(-110946, -0)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-107165, -15)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-96082, -30)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-78450, -45)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-55473, -60)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-28715, -75)).to.be.closeTo(-1, 1e-4); + expect(iD.geoMetersToLon(-1, -90)).to.eql(0); }); }); @@ -159,43 +195,61 @@ describe('iD.geo', function() { describe('geoSphericalDistance', function() { it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; + var a = [0, 0]; + var b = [0, 0]; expect(iD.geoSphericalDistance(a, b)).to.eql(0); }); it('a straight 1 degree line at the equator is aproximately 111 km', function() { - var a = [0, 0], - b = [1, 0]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 0]; + var b = [1, 0]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(110946, 10); }); it('a pythagorean triangle is (nearly) right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(555E3, 556E3); + var a = [0, 0]; + var b = [4, 3]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(555282, 10); }); it('east-west distances at high latitude are shorter', function() { - var a = [0, 60], - b = [1, 60]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(55E3, 56E3); + var a = [0, 60]; + var b = [1, 60]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(55473, 10); }); it('north-south distances at high latitude are not shorter', function() { - var a = [0, 60], - b = [0, 61]; - expect(iD.geoSphericalDistance(a, b)).to.be.within(110E3, 112E3); + var a = [0, 60]; + var b = [0, 61]; + expect(iD.geoSphericalDistance(a, b)).to.be.closeTo(111319, 10); + }); + }); + + describe('geoZoomToScale', function() { + it('converts from zoom to projection scale (tileSize = 256)', function() { + expect(iD.geoZoomToScale(17)).to.be.closeTo(5340353.715440872, 1e-6); + }); + it('converts from zoom to projection scale (tileSize = 512)', function() { + expect(iD.geoZoomToScale(17, 512)).to.be.closeTo(10680707.430881744, 1e-6); + }); + }); + + describe('geoScaleToZoom', function() { + it('converts from projection scale to zoom (tileSize = 256)', function() { + expect(iD.geoScaleToZoom(5340353.715440872)).to.be.closeTo(17, 1e-6); + }); + it('converts from projection scale to zoom (tileSize = 512)', function() { + expect(iD.geoScaleToZoom(10680707.430881744, 512)).to.be.closeTo(17, 1e-6); }); }); describe('geoEdgeEqual', function() { it('returns false for inequal edges', function() { - expect(iD.geoEdgeEqual(['a','b'], ['a','c'])).to.be.false; + 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; + 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; + expect(iD.geoEdgeEqual(['a', 'b'], ['b', 'a'])).to.be.true; }); }); @@ -211,10 +265,10 @@ describe('iD.geo', function() { describe('geoRotate', function() { it('rotates points around [0, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [0, 0], - result = iD.geoRotate(points, angle, around); + 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); @@ -222,10 +276,10 @@ describe('iD.geo', function() { }); it('rotates points around [3, 0]', function() { - var points = [[5, 0], [5, 1]], - angle = Math.PI, - around = [3, 0], - result = iD.geoRotate(points, angle, around); + 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); @@ -246,7 +300,7 @@ describe('iD.geo', function() { }); it('returns undefined properties for a degenerate way (single node)', function() { - expect(iD.geoChooseEdge([iD.Node({loc: [0, 0]})], [0, 0], projection)).to.eql({ + expect(iD.geoChooseEdge([iD.osmNode({loc: [0, 0]})], [0, 0], projection)).to.eql({ index: undefined, distance: Infinity, loc: undefined @@ -259,14 +313,10 @@ describe('iD.geo', function() { // c // // * = [2, 0] - var a = [0, 0], - b = [5, 0], - c = [2, 1], - nodes = [ - iD.Node({loc: a}), - iD.Node({loc: b}) - ]; - + 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); @@ -274,14 +324,10 @@ describe('iD.geo', function() { }); 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 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); @@ -289,14 +335,10 @@ describe('iD.geo', function() { }); 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 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); @@ -306,28 +348,28 @@ describe('iD.geo', function() { describe('geoLineIntersection', function() { it('returns null if lines are colinear with overlap', function() { - var a = [[0, 0], [10, 0]], - b = [[-5, 0], [5, 0]]; + 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]], - b = [[-10, 0], [-5, 0]]; + 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]], - b = [[0, 5], [10, 5]]; + 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]], - b = [[5, 10], [5, -10]]; + 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]], - b = [[-5, 10], [-5, -10]]; + var a = [[0, 0], [10, 0]]; + var b = [[-5, 10], [-5, -10]]; expect(iD.geoLineIntersection(a, b)).to.be.null; }); }); @@ -339,12 +381,7 @@ describe('iD.geo', function() { 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 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; }); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 140576398..1b9e59e08 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,13 +1,10 @@ describe('iD.svgAreas', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var all = function() { return true; }; var none = function() { return false; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index fa70b6565..75c957daa 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -1,11 +1,8 @@ describe('iD.svgLayers', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, container; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 2a414afcd..d10fe09a6 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -1,13 +1,10 @@ describe('iD.svgLines', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var all = function() { return true; }; var none = function() { return false; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js index 230ad8446..c30530fae 100644 --- a/test/spec/svg/midpoints.js +++ b/test/spec/svg/midpoints.js @@ -1,13 +1,10 @@ describe('iD.svgMidpoints', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var _selectedIDs = []; var filter = function() { return true; }; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index 8faaa54c2..c1012a7cd 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,11 +1,8 @@ describe('iD.svgPoints', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context, surface; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]); beforeEach(function () { diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 3f7789643..1c29ca70d 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,12 +1,9 @@ describe('iD.svgVertices', function () { - var TAU = 2 * Math.PI; - function ztok(z) { return 256 * Math.pow(2, z) / TAU; } - var context; var surface; var projection = d3.geoProjection(function(x, y) { return [x, -y]; }) .translate([0, 0]) - .scale(ztok(17)) + .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [Infinity, Infinity]]);