|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8"> |
|
|
|
<style> |
|
svg { |
|
background: #9ecae1; |
|
} |
|
|
|
.boundary { |
|
fill:none; |
|
stroke: white; |
|
} |
|
|
|
.land { |
|
fill: #41ab5d; |
|
} |
|
|
|
.scaleText { |
|
font-family: Arial; |
|
font-size: 14px; |
|
} |
|
|
|
|
|
</style> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://d3js.org/d3-geo-projection.v1.min.js"></script> |
|
<script src="https://d3js.org/topojson.v1.min.js"></script> |
|
</head> |
|
<body> |
|
|
|
|
|
<script type="text/javascript"> |
|
|
|
var width = 960, |
|
height = 500; |
|
|
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
var projection = d3.geoConicEqualArea() |
|
projection.rotate([-132,0]).center([0,-28]).parallels([-18,-36]).scale(750); |
|
|
|
|
|
var path = d3.geoPath().projection(projection); |
|
var g = svg.append("g"); |
|
|
|
d3.json("world.json", function(error, world) { |
|
|
|
g.insert("path", ".land") |
|
.datum(topojson.feature(world, world.objects.land)) |
|
.attr("class", "land") |
|
.attr("d", path); |
|
|
|
g.insert("path", ".boundary") |
|
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; })) |
|
.attr("class", "boundary") |
|
.attr("d", path); |
|
|
|
// Start Scale --------------------------------------------------------- |
|
// baseWidth refers to ideal scale width on the screen it also is the width of the initial measurement point |
|
var baseWidth = width / 4; |
|
var p1 = projection.invert([width/2 - baseWidth/2, height / 2]); |
|
var p2 = projection.invert([width/2 + baseWidth/2, height / 2]); |
|
var distance = getDistance(p1,p2); |
|
var unit = "m"; |
|
var multiply = 1; |
|
var bestFit = 1; |
|
var increment = 0.1; // This could be scaled to map width maybe width/10000; |
|
var scaleDistance = 0; |
|
var scaleWidth = 0; |
|
|
|
if ( distance > 1000 ) { |
|
unit = "km"; multiply = 0.001; |
|
} |
|
// Adjust distance to a round(er) number |
|
var i = 0; |
|
while (i < 400) { |
|
var temp = getDistance( projection.invert([ width/2 - (baseWidth / 2) + (increment * i), height / 2 ]), projection.invert([ width/2 + baseWidth/2 - (increment * i), height / 2 ])); |
|
var ratio = temp / temp.toPrecision(1); |
|
|
|
// If the second distance is moving away from a cleaner number, reverse direction. |
|
if (i == 1) { |
|
if (Math.abs(1 - ratio) > bestFit) { increment = - increment; } |
|
} |
|
// If we are moving away from a best fit after that, break |
|
else if (i > 2) { |
|
if (Math.abs(1 - ratio) > bestFit) { break } |
|
} |
|
// See if the current distance is the cleanest number |
|
if (Math.abs(1-ratio) < bestFit) { |
|
bestFit = Math.abs(1 - ratio); |
|
scaleDistance = temp; |
|
scaleWidth = (baseWidth) - (2 * increment * i); |
|
} |
|
i++; |
|
} |
|
|
|
// Now to build the scale |
|
var bars = []; |
|
var smallBars = 10; |
|
var bigBars = 4; |
|
var odd = true; |
|
var label = false; |
|
|
|
// Populate an array to represent the bars on the scale |
|
for (i = 0; i < smallBars; i++) { |
|
if (smallBars - 1 > i ) { label = false; } else { label = true; } |
|
bars.push( {width: 1 / (smallBars * (bigBars + 1)), offset: i / (smallBars * (bigBars + 1)), label: label, odd: odd } ); |
|
odd = !odd; |
|
} |
|
for (i = 0; i < bigBars; i++) { |
|
bars.push( {width: 1 / (bigBars + 1), offset: (i + 1) / (bigBars + 1), label: true, odd: odd } ); |
|
odd = !odd; |
|
} |
|
|
|
// Append the scale |
|
g.selectAll(".scaleBar") |
|
.data(bars).enter() |
|
.append("rect") |
|
.attr("x", function(d) { return d.offset * scaleWidth + 20 }) |
|
.attr("y", height - 30) |
|
.attr("width", function(d) { return d.width * scaleWidth}) |
|
.attr("height", 10) |
|
.attr("fill", function (d) { if (d.odd) { return "#eee"; } else { return "#222"; } }); |
|
g.selectAll(".scaleText") |
|
.data(bars).enter() |
|
.filter( function (d) { return d.label == true }) |
|
.append("text") |
|
.attr("class","scaleText") |
|
.attr("x",0) |
|
.attr("y",0) |
|
.style("text-anchor","start") |
|
.text(function(d) { return d3.format(",")(((d.offset + d.width) * scaleDistance).toPrecision(2) * multiply); }) |
|
.attr("transform", function(d) { return "translate("+ ((d.offset + d.width) * scaleWidth + 20 )+","+ (height - 35) +") rotate(-45)" }); |
|
g.append("text") |
|
.attr("x", scaleWidth/2 + 20) |
|
.attr("y", height - 5) |
|
.text( function() { if(unit == "km") { return "kilometers"; } else { return "metres";} }) |
|
.style("text-anchor","middle") |
|
.attr("class","scaleText"); |
|
// End Scale ----------------------------------------- |
|
}); |
|
|
|
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ |
|
/* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2016 */ |
|
/* MIT Licence */ |
|
/* www.movable-type.co.uk/scripts/latlong.html */ |
|
/* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html */ |
|
function getDistance(p1,p2) { |
|
|
|
var lat1 = p1[1]; |
|
var lat2 = p2[1]; |
|
var lon1 = p1[0]; |
|
var lon2 = p2[0]; |
|
|
|
var R = 6371e3; // metres |
|
var φ1 = lat1* Math.PI / 180; |
|
var φ2 = lat2* Math.PI / 180; |
|
var Δφ = (lat2-lat1)* Math.PI / 180; |
|
var Δλ = (lon2-lon1)* Math.PI / 180; |
|
|
|
var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + |
|
Math.cos(φ1) * Math.cos(φ2) * |
|
Math.sin(Δλ/2) * Math.sin(Δλ/2); |
|
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); |
|
|
|
var distance = R * c; |
|
|
|
return distance; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
</script> |
|
</body> |