diff --git a/css/app.css b/css/app.css index 70a5eace3..ede9e7eae 100644 --- a/css/app.css +++ b/css/app.css @@ -2181,7 +2181,8 @@ img.wiki-image { /* About Section ------------------------------------------------------- */ -.about-block { +#footer { + width: 100%; position: absolute; right:0; bottom:0; @@ -2192,13 +2193,47 @@ img.wiki-image { transition: opacity 200ms; } -.about-block:hover { +#footer:hover { opacity: 1; } -#about { +#scale-block { + display: table-cell; + vertical-align: bottom; + width: 250px; + height: 30px; + float: left; + clear: left; +} + +#info-block { + float: right; + clear: right; +} + +#scale { + height: 30px; + width: 100%; +} + +#scale text { + font: 12px sans-serif; + stroke: none; + fill: #ccc; + text-anchor: start; +} + +#scale path { + fill: none; + stroke: #ccc; + stroke-width: 1; + shape-rendering: crispEdges; +} + +#about-list { text-align: right; margin-right: 10px; + clear: right; } .source-switch a { @@ -2240,29 +2275,18 @@ img.wiki-image { content: ', '; } -/* API Status */ - .api-status { - float: left; + float: right; + clear: both; + text-align: right; + width: 100%; } .api-status.offline, -.api-status.readonly { +.api-status.readonly, +.api-status.error { background: red; - padding: 5px 10px; -} - -/* Account Information */ - -.account { - float: left; - padding: 5px 10px; -} - -.account .logout { - margin-left:10px; - border-left: 1px solid white; - padding-left: 10px; + padding: 0px 5px; } /* Modals diff --git a/index.html b/index.html index 7cfc9e091..96c3b1057 100644 --- a/index.html +++ b/index.html @@ -90,6 +90,7 @@ + diff --git a/js/id/geo.js b/js/id/geo.js index 7f3eb9457..9ce5d9575 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -21,11 +21,38 @@ iD.geo.euclideanDistance = function(a, b) { var x = a[0] - b[0], y = a[1] - b[1]; return Math.sqrt((x * x) + (y * y)); }; + +// using WGS84 polar radius (6356752.314245179 m) +// const = 2 * PI * r / 360 +iD.geo.latToMeters = function(dLat) { + return dLat * 110946.257617; +}; + +// using WGS84 equatorial radius (6378137.0 m) +// const = 2 * PI * r / 360 +iD.geo.lonToMeters = function(dLon, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); +}; + +// using WGS84 polar radius (6356752.314245179 m) +// const = 2 * PI * r / 360 +iD.geo.metersToLat = function(m) { + return m / 110946.257617; +}; + +// using WGS84 equatorial radius (6378137.0 m) +// const = 2 * PI * r / 360 +iD.geo.metersToLon = function(m, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); +}; + // Equirectangular approximation of spherical distances on Earth iD.geo.sphericalDistance = function(a, b) { - var x = Math.cos(a[1]*Math.PI/180) * (a[0] - b[0]), - y = a[1] - b[1]; - return 6.3710E6 * Math.sqrt((x * x) + (y * y)) * Math.PI/180; + var x = iD.geo.lonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), + y = iD.geo.latToMeters(a[1] - b[1]); + return Math.sqrt((x * x) + (y * y)); }; iD.geo.edgeEqual = function(a, b) { diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js index 2081f82c2..04c8c7434 100644 --- a/js/id/geo/extent.js +++ b/js/id/geo/extent.js @@ -58,8 +58,8 @@ _.extend(iD.geo.Extent.prototype, { }, padByMeters: function(meters) { - var dLat = meters / 111200, - dLon = meters / 111200 / Math.abs(Math.cos(this.center()[1])); + var dLat = iD.geo.metersToLat(meters), + dLon = iD.geo.metersToLon(meters, this.center()[1]); return iD.geo.Extent( [this[0][0] - dLon, this[0][1] - dLat], [this[1][0] + dLon, this[1][1] + dLat]); diff --git a/js/id/ui.js b/js/id/ui.js index 01f8ed6df..eacf19f8f 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -80,23 +80,24 @@ iD.ui = function(context) { .attr('class', 'map-control help-control') .call(iD.ui.Help(context)); - var about = content.append('div') - .attr('class','col12 about-block fillD'); + var footer = content.append('div') + .attr('id', 'footer') + .attr('class', 'fillD'); - about.append('div') - .attr('class', 'api-status') - .call(iD.ui.Status(context)); + footer.append('div') + .attr('id', 'scale-block') + .call(iD.ui.Scale(context)); + + var linkList = footer.append('div') + .attr('id', 'info-block') + .append('ul') + .attr('id', 'about-list') + .attr('class', 'link-list'); if (!context.embed()) { - about.append('div') - .attr('class', 'account') - .call(iD.ui.Account(context)); + linkList.call(iD.ui.Account(context)); } - var linkList = about.append('ul') - .attr('id', 'about') - .attr('class', 'link-list'); - linkList.append('li') .append('a') .attr('target', '_blank') @@ -123,6 +124,10 @@ iD.ui = function(context) { .attr('tabindex', -1) .call(iD.ui.Contributors(context)); + footer.append('div') + .attr('class', 'api-status') + .call(iD.ui.Status(context)); + window.onbeforeunload = function() { return context.save(); }; diff --git a/js/id/ui/account.js b/js/id/ui/account.js index 97de778fb..d7f32b0b7 100644 --- a/js/id/ui/account.js +++ b/js/id/ui/account.js @@ -3,20 +3,25 @@ iD.ui.Account = function(context) { function update(selection) { if (!connection.authenticated()) { - selection.html('') + selection.selectAll('#userLink, #logoutLink') .style('display', 'none'); return; } - selection.style('display', 'block'); - connection.userDetails(function(err, details) { - selection.html(''); + var userLink = selection.select('#userLink'), + logoutLink = selection.select('#logoutLink'); + + userLink.html(''); + logoutLink.html(''); if (err) return; + selection.selectAll('#userLink, #logoutLink') + .style('display', 'list-item'); + // Link - var userLink = selection.append('a') + userLink.append('a') .attr('href', connection.userURL(details.display_name)) .attr('target', '_blank'); @@ -35,7 +40,7 @@ iD.ui.Account = function(context) { .attr('class', 'label') .text(details.display_name); - selection.append('a') + logoutLink.append('a') .attr('class', 'logout') .attr('href', '#') .text(t('logout')) @@ -47,7 +52,15 @@ iD.ui.Account = function(context) { } return function(selection) { - connection.on('auth', function() { update(selection); }); + selection.append('li') + .attr('id', 'logoutLink') + .style('display', 'none'); + + selection.append('li') + .attr('id', 'userLink') + .style('display', 'none'); + + connection.on('auth.account', function() { update(selection); }); update(selection); }; }; diff --git a/js/id/ui/scale.js b/js/id/ui/scale.js new file mode 100644 index 000000000..2136f2295 --- /dev/null +++ b/js/id/ui/scale.js @@ -0,0 +1,82 @@ +iD.ui.Scale = function(context) { + var projection = context.projection, + imperial = (iD.detect().locale === 'en-us'), + maxLength = 180, + tickHeight = 8; + + function scaleDefs(loc1, loc2) { + var lat = (loc2[1] + loc1[1]) / 2, + conversion = (imperial ? 3.28084 : 1), + dist = iD.geo.lonToMeters(loc2[0] - loc1[0], lat) * conversion, + scale = { dist: 0, px: 0, text: '' }, + buckets, i, val, dLon; + + if (imperial) { + buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1]; + } else { + buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1]; + } + + // determine a user-friendly endpoint for the scale + for (i = 0; i < buckets.length; i++) { + val = buckets[i]; + if (dist >= val) { + scale.dist = Math.floor(dist / val) * val; + break; + } + } + + dLon = iD.geo.metersToLon(scale.dist / conversion, lat); + scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]); + + if (imperial) { + if (scale.dist >= 5280) { + scale.dist /= 5280; + scale.text = String(scale.dist) + ' mi'; + } else { + scale.text = String(scale.dist) + ' ft'; + } + } else { + if (scale.dist >= 1000) { + scale.dist /= 1000; + scale.text = String(scale.dist) + ' km'; + } else { + scale.text = String(scale.dist) + ' m'; + } + } + + return scale; + } + + function update(selection) { + // choose loc1, loc2 along bottom of viewport (near where the scale will be drawn) + var dims = context.map().dimensions(), + loc1 = projection.invert([0, dims[1]]), + loc2 = projection.invert([maxLength, dims[1]]), + scale = scaleDefs(loc1, loc2); + + selection.select('#scalepath') + .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight); + + selection.select('#scaletext') + .attr('x', scale.px + 8) + .attr('y', tickHeight) + .text(scale.text); + } + + return function(selection) { + var g = selection.append('svg') + .attr('id', 'scale') + .append('g') + .attr('transform', 'translate(10,11)'); + + g.append('path').attr('id', 'scalepath'); + g.append('text').attr('id', 'scaletext'); + + update(selection); + + context.map().on('move.scale', function() { + update(selection); + }); + }; +}; diff --git a/test/index.html b/test/index.html index 80790e347..f389e382b 100644 --- a/test/index.html +++ b/test/index.html @@ -85,6 +85,7 @@ + diff --git a/test/spec/geo.js b/test/spec/geo.js index 289bb8911..ef2c9e0e1 100644 --- a/test/spec/geo.js +++ b/test/spec/geo.js @@ -57,6 +57,78 @@ describe('iD.geo', function() { }); }); + describe('.latToMeters', function() { + it('0 degrees latitude is 0 meters', function() { + expect(iD.geo.latToMeters(0)).to.eql(0); + }); + it('1 degree latitude is approx 111 km', function() { + expect(iD.geo.latToMeters(1)).to.be.within(110E3, 112E3); + }); + it('-1 degree latitude is approx -111 km', function() { + expect(iD.geo.latToMeters(-1)).to.be.within(-112E3, -110E3); + }); + }); + + describe('.lonToMeters', function() { + it('0 degrees longitude is 0 km', function() { + expect(iD.geo.lonToMeters(0, 0)).to.eql(0); + }); + it('distance of 1 degree longitude varies with latitude', function() { + expect(iD.geo.lonToMeters(1, 0)).to.be.within(110E3, 112E3); + expect(iD.geo.lonToMeters(1, 15)).to.be.within(107E3, 108E3); + expect(iD.geo.lonToMeters(1, 30)).to.be.within(96E3, 97E3); + expect(iD.geo.lonToMeters(1, 45)).to.be.within(78E3, 79E3); + expect(iD.geo.lonToMeters(1, 60)).to.be.within(55E3, 56E3); + expect(iD.geo.lonToMeters(1, 75)).to.be.within(28E3, 29E3); + expect(iD.geo.lonToMeters(1, 90)).to.eql(0); + }); + it('distance of -1 degree longitude varies with latitude', function() { + expect(iD.geo.lonToMeters(-1, 0)).to.be.within(-112E3, -110E3); + expect(iD.geo.lonToMeters(-1, -15)).to.be.within(-108E3, -107E3); + expect(iD.geo.lonToMeters(-1, -30)).to.be.within(-97E3, -96E3); + expect(iD.geo.lonToMeters(-1, -45)).to.be.within(-79E3, -78E3); + expect(iD.geo.lonToMeters(-1, -60)).to.be.within(-56E3, -55E3); + expect(iD.geo.lonToMeters(-1, -75)).to.be.within(-29E3, -28E3); + expect(iD.geo.lonToMeters(-1, -90)).to.eql(0); + }); + }); + + describe('.metersToLat', function() { + it('0 meters is 0 degrees latitude', function() { + expect(iD.geo.metersToLat(0)).to.eql(0); + }); + it('111 km is approx 1 degree latitude', function() { + expect(iD.geo.metersToLat(111E3)).to.be.within(0.995, 1.005); + }); + it('-111 km is approx -1 degree latitude', function() { + expect(iD.geo.metersToLat(-111E3)).to.be.within(-1.005, -0.995); + }); + }); + + describe('.metersToLon', function() { + it('0 meters is 0 degrees longitude', function() { + expect(iD.geo.metersToLon(0, 0)).to.eql(0); + }); + it('distance of 1 degree longitude varies with latitude', function() { + expect(iD.geo.metersToLon(111320, 0)).to.be.within(0.995, 1.005); + expect(iD.geo.metersToLon(107551, 15)).to.be.within(0.995, 1.005); + expect(iD.geo.metersToLon(96486, 30)).to.be.within(0.995, 1.005); + expect(iD.geo.metersToLon(78847, 45)).to.be.within(0.995, 1.005); + expect(iD.geo.metersToLon(55800, 60)).to.be.within(0.995, 1.005); + expect(iD.geo.metersToLon(28902, 75)).to.be.within(0.995, 1.005); + expect(iD.geo.metersToLon(1, 90)).to.eql(0); + }); + it('distance of -1 degree longitude varies with latitude', function() { + expect(iD.geo.metersToLon(-111320, 0)).to.be.within(-1.005, -0.995); + expect(iD.geo.metersToLon(-107551, 15)).to.be.within(-1.005, -0.995); + expect(iD.geo.metersToLon(-96486, 30)).to.be.within(-1.005, -0.995); + expect(iD.geo.metersToLon(-78847, 45)).to.be.within(-1.005, -0.995); + expect(iD.geo.metersToLon(-55800, 60)).to.be.within(-1.005, -0.995); + expect(iD.geo.metersToLon(-28902, 75)).to.be.within(-1.005, -0.995); + expect(iD.geo.metersToLon(-1, 90)).to.eql(0); + }); + }); + describe('.sphericalDistance', function() { it('distance between two same points is zero', function() { var a = [0, 0], @@ -66,22 +138,22 @@ describe('iD.geo', function() { it('a straight 1 degree line at the equator is aproximately 111 km', function() { var a = [0, 0], b = [1, 0]; - expect(iD.geo.sphericalDistance(a, b)).to.be.within(100E3,120E3); + expect(iD.geo.sphericalDistance(a, b)).to.be.within(110E3, 112E3); }); - it('a pythagorean triangle is right', function() { + it('a pythagorean triangle is (nearly) right', function() { var a = [0, 0], b = [4, 3]; - expect(iD.geo.sphericalDistance(a, b)).to.be.within(500E3,600E3); + expect(iD.geo.sphericalDistance(a, b)).to.be.within(555E3, 556E3); }); it('east-west distances at high latitude are shorter', function() { var a = [0, 60], b = [1, 60]; - expect(iD.geo.sphericalDistance(a, b)).to.be.within(50E3,60E3); + expect(iD.geo.sphericalDistance(a, b)).to.be.within(55E3, 56E3); }); it('north-south distances at high latitude are not shorter', function() { var a = [0, 60], b = [0, 61]; - expect(iD.geo.sphericalDistance(a, b)).to.be.within(100E3,120E3); + expect(iD.geo.sphericalDistance(a, b)).to.be.within(110E3, 112E3); }); });