diff --git a/Makefile b/Makefile index 2819f2884..657915e1e 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,8 @@ all: \ js/id/oauth.js \ js/id/services/*.js \ js/id/util.js \ + js/id/geo.js \ + js/id/geo/*.js \ js/id/actions.js \ js/id/actions/*.js \ js/id/behavior.js \ diff --git a/index.html b/index.html index 4e42441f7..64d839d93 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,9 @@ + + + diff --git a/js/id/geo.js b/js/id/geo.js new file mode 100644 index 000000000..06b63ae14 --- /dev/null +++ b/js/id/geo.js @@ -0,0 +1 @@ +iD.geo = {}; diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js new file mode 100644 index 000000000..80bf631c6 --- /dev/null +++ b/js/id/geo/extent.js @@ -0,0 +1,37 @@ +iD.geo.Extent = function (min, max) { + if (!(this instanceof iD.geo.Extent)) return new iD.geo.Extent(min, max); + if (min instanceof iD.geo.Extent) { + return min; + } else if (min && min.length === 2 && min[0].length === 2 && min[1].length === 2) { + this[0] = min[0]; + this[1] = min[1]; + } else { + this[0] = min || [ Infinity, Infinity]; + this[1] = max || min || [-Infinity, -Infinity]; + } +}; + +iD.geo.Extent.prototype = [[], []]; + +_.extend(iD.geo.Extent.prototype, { + extend: function (obj) { + obj = iD.geo.Extent(obj); + return iD.geo.Extent([Math.min(obj[0][0], this[0][0]), + Math.min(obj[0][1], this[0][1])], + [Math.max(obj[1][0], this[1][0]), + Math.max(obj[1][1], this[1][1])]); + }, + + center: function () { + return [(this[0][0] + this[1][0]) / 2, + (this[0][1] + this[1][1]) / 2]; + }, + + intersects: function (obj) { + obj = iD.geo.Extent(obj); + return obj[0][0] <= this[1][0] && + obj[0][1] <= this[1][1] && + obj[1][0] >= this[0][0] && + obj[1][1] >= this[0][1]; + } +}); diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js index afc152589..8c8f9a6fb 100644 --- a/js/id/graph/entity.js +++ b/js/id/graph/entity.js @@ -74,11 +74,7 @@ iD.Entity.prototype = { }, intersects: function(extent, resolver) { - var _extent = this.extent(resolver); - return _extent[0][0] > extent[0][0] && - _extent[1][0] < extent[1][0] && - _extent[0][1] < extent[0][1] && - _extent[1][1] > extent[1][1]; + return this.extent(resolver).intersects(extent); }, hasInterestingTags: function() { diff --git a/js/id/graph/node.js b/js/id/graph/node.js index 9500991fa..fd3a1cd69 100644 --- a/js/id/graph/node.js +++ b/js/id/graph/node.js @@ -2,7 +2,7 @@ iD.Node = iD.Entity.extend({ type: "node", extent: function() { - return [this.loc, this.loc]; + return iD.geo.Extent(this.loc); }, geometry: function() { diff --git a/js/id/graph/way.js b/js/id/graph/way.js index 5c3e1210b..305f12051 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -4,14 +4,11 @@ iD.Way = iD.Entity.extend({ extent: function(resolver) { return resolver.transient(this, 'extent', function() { - var extent = [[-Infinity, Infinity], [Infinity, -Infinity]]; + var extent = iD.geo.Extent(); for (var i = 0, l = this.nodes.length; i < l; i++) { var node = this.nodes[i]; if (node.loc === undefined) node = resolver.entity(node); - if (node.loc[0] > extent[0][0]) extent[0][0] = node.loc[0]; - if (node.loc[0] < extent[1][0]) extent[1][0] = node.loc[0]; - if (node.loc[1] < extent[0][1]) extent[0][1] = node.loc[1]; - if (node.loc[1] > extent[1][1]) extent[1][1] = node.loc[1]; + extent = extent.extend(node.loc); } return extent; }); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index d091fdc57..4d567fd59 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -59,8 +59,8 @@ iD.modes.Select = function (entity) { map_size = mode.map.size(), entity_extent = entity.extent(mode.history.graph()), left_edge = map_size[0] - inspector_size[0], - left = mode.map.projection(entity_extent[1])[0], - right = mode.map.projection(entity_extent[0])[0]; + left = mode.map.projection(entity_extent[0])[0], + right = mode.map.projection(entity_extent[1])[0]; if (left > left_edge && right > left_edge) mode.map.centerEase( diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index ec232d542..59f46f0ee 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -257,26 +257,23 @@ iD.Map = function() { }, 20); }; - map.extent = function(tl, br) { + map.extent = function(_) { if (!arguments.length) { - return [projection.invert([0, 0]), projection.invert(dimensions)]; + return iD.geo.Extent(projection.invert([0, dimensions[1]]), + projection.invert([dimensions[0], 0])); } else { - - var TL = projection(tl), - BR = projection(br); + var extent = iD.geo.Extent(_), + tl = projection([extent[0][0], extent[1][1]]), + br = projection([extent[1][0], extent[0][1]]); // Calculate maximum zoom that fits extent - var hFactor = (BR[0] - TL[0]) / dimensions[0], - vFactor = (BR[1] - TL[1]) / dimensions[1], + var hFactor = (br[0] - tl[0]) / dimensions[0], + vFactor = (br[1] - tl[1]) / dimensions[1], hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2, vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2, newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff); - // Calculate center of projected extent - var midPoint = [(TL[0] + BR[0]) / 2, (TL[1] + BR[1]) / 2], - midLoc = projection.invert(midPoint); - - map.zoom(newZoom).center(midLoc); + map.zoom(newZoom).center(extent.center()); } }; diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js index 87eaa7d74..4bd8c0629 100644 --- a/js/id/ui/geocoder.js +++ b/js/id/ui/geocoder.js @@ -16,7 +16,7 @@ iD.ui.geocoder = function() { .text('No location found for "' + resp.query[0] + '"'); } var bounds = resp.results[0][0].bounds; - map.extent([bounds[0], bounds[3]], [bounds[2], bounds[1]]); + map.extent(iD.geo.Extent([bounds[0], bounds[1]], [bounds[2], bounds[3]])); }); } diff --git a/js/id/ui/save.js b/js/id/ui/save.js index e40125e71..6bf8419c3 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -60,8 +60,7 @@ iD.ui.save = function() { modal.remove(); }) .on('fix', function(d) { - var ext = d.entity.extent(map.history().graph()); - map.extent(ext[0], ext[1]); + map.extent(d.entity.extent(map.history().graph())); if (map.zoom() > 19) map.zoom(19); controller.enter(iD.modes.Select(d.entity)); modal.remove(); diff --git a/test/index.html b/test/index.html index ab7b521cd..7f63be2a3 100644 --- a/test/index.html +++ b/test/index.html @@ -34,6 +34,9 @@ + + + @@ -139,6 +142,8 @@ + + diff --git a/test/index_packaged.html b/test/index_packaged.html index 9044adf66..042a0227e 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -47,6 +47,8 @@ + + diff --git a/test/spec/geo/extent.js b/test/spec/geo/extent.js new file mode 100644 index 000000000..54f328efb --- /dev/null +++ b/test/spec/geo/extent.js @@ -0,0 +1,100 @@ +describe("iD.geo.Extent", function () { + describe("constructor", function () { + it("defaults to infinitely empty extent", function () { + expect(iD.geo.Extent()).to.eql([[Infinity, Infinity], [-Infinity, -Infinity]]); + }); + + it("constructs via a point", function () { + var p = [0, 0]; + expect(iD.geo.Extent(p)).to.eql([p, p]); + }); + + it("constructs via two points", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent(min, max)).to.eql([min, max]); + }); + + it("constructs via an extent", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent([min, max])).to.eql([min, max]); + }); + + it("constructs via an iD.geo.Extent", function () { + var min = [0, 0], + max = [5, 10], + extent = iD.geo.Extent(min, max); + expect(iD.geo.Extent(extent)).to.equal(extent); + }); + + it("has length 2", function () { + expect(iD.geo.Extent().length).to.equal(2); + }); + + it("has min element", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent(min, max)[0]).to.equal(min); + }); + + it("has max element", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent(min, max)[1]).to.equal(max); + }); + }); + + describe("#center", function () { + it("returns the center point", function () { + expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]); + }); + }); + + describe("#extend", function () { + it("does not modify self", function () { + var extent = iD.geo.Extent([0, 0], [0, 0]); + extent.extend([1, 1]); + expect(extent).to.eql([[0, 0], [0, 0]]); + }); + + it("returns the minimal extent containing self and the given point", function () { + expect(iD.geo.Extent().extend([0, 0])).to.eql([[0, 0], [0, 0]]); + expect(iD.geo.Extent([0, 0], [0, 0]).extend([5, 10])).to.eql([[0, 0], [5, 10]]); + }); + + it("returns the minimal extent containing self and the given extent", function () { + expect(iD.geo.Extent().extend([[0, 0], [5, 10]])).to.eql([[0, 0], [5, 10]]); + expect(iD.geo.Extent([0, 0], [0, 0]).extend([[4, -1], [5, 10]])).to.eql([[0, -1], [5, 10]]); + }); + }); + + describe('#intersects', function () { + it("returns true for a point inside self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([2, 2])).to.be.true; + }); + + it("returns true for a point on the boundary of self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([0, 0])).to.be.true; + }); + + it("returns false for a point outside self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([6, 6])).to.be.false; + }); + + it("returns true for an extent contained by self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[1, 1], [2, 2]])).to.be.true; + expect(iD.geo.Extent([1, 1], [2, 2]).intersects([[0, 0], [5, 5]])).to.be.true; + }); + + it("returns true for an extent intersected by self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[1, 1], [6, 6]])).to.be.true; + expect(iD.geo.Extent([1, 1], [6, 6]).intersects([[0, 0], [5, 5]])).to.be.true; + }); + + it("returns false for an extent not intersected by self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[6, 6], [7, 7]])).to.be.false; + expect(iD.geo.Extent([[6, 6], [7, 7]]).intersects([[0, 0], [5, 5]])).to.be.false; + }); + }); +}); diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js index 929946b04..8045270b3 100644 --- a/test/spec/graph/node.js +++ b/test/spec/graph/node.js @@ -29,11 +29,11 @@ describe('iD.Node', function () { describe("#intersects", function () { it("returns true for a node within the given extent", function () { - expect(iD.Node({loc: [0, 0]}).intersects([[-180, 90], [180, -90]])).to.equal(true); + expect(iD.Node({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); }); it("returns false for a node outside the given extend", function () { - expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false); + expect(iD.Node({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); }); }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 3aa8045fb..19a4514db 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -41,7 +41,7 @@ describe('iD.Way', function() { node2 = iD.Node({loc: [5, 10]}), way = iD.Way({nodes: [node1.id, node2.id]}), graph = iD.Graph([node1, node2, way]); - expect(way.extent(graph)).to.eql([[5, 0], [0, 10]]); + expect(way.extent(graph)).to.eql([[0, 0], [5, 10]]); }); }); @@ -50,14 +50,14 @@ describe('iD.Way', function() { var node = iD.Node({loc: [0, 0]}), way = iD.Way({nodes: [node.id]}), graph = iD.Graph([node, way]); - expect(way.intersects([[-180, 90], [180, -90]], graph)).to.equal(true); + expect(way.intersects([[-5, -5], [5, 5]], graph)).to.equal(true); }); it("returns false for way with no nodes within the given extent", function () { - var node = iD.Node({loc: [0, 0]}), + var node = iD.Node({loc: [6, 6]}), way = iD.Way({nodes: [node.id]}), graph = iD.Graph([node, way]); - expect(way.intersects([[100, 90], [180, -90]], graph)).to.equal(false); + expect(way.intersects([[-5, -5], [5, 5]], graph)).to.equal(false); }); }); diff --git a/test/spec/renderer/map.js b/test/spec/renderer/map.js index 4cec560e9..611a2d1ff 100644 --- a/test/spec/renderer/map.js +++ b/test/spec/renderer/map.js @@ -54,16 +54,17 @@ describe('Map', function() { describe('#extent', function() { it('gets and sets extent', function() { - expect(map.size([100, 100])).to.equal(map); - expect(map.center([0, 0])).to.equal(map); + map.size([100, 100]) + .center([0, 0]); + expect(map.extent()[0][0]).to.be.closeTo(-17.5, 0.5); expect(map.extent()[1][0]).to.be.closeTo(17.5, 0.5); - expect(map.extent([10, 1], [30, 1])); + expect(map.extent([[10, 1], [30, 1]])); expect(map.extent()[0][0]).to.be.closeTo(10, 0.1); expect(map.extent()[1][0]).to.be.closeTo(30, 0.1); - expect(map.extent([-1, -20], [1, -40])); - expect(map.extent()[0][1]).to.be.closeTo(-20, 0.1); - expect(map.extent()[1][1]).to.be.closeTo(-40, 0.1); + expect(map.extent([[-1, -40], [1, -20]])); + expect(map.extent()[0][1]).to.be.closeTo(-40, 1); + expect(map.extent()[1][1]).to.be.closeTo(-20, 1); }); });