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);
});
});