How to draw circles with radii given in kilometers exactly on the world map
I am using the d3.geo.conicEquidistant () projection on a world map roughly oriented towards the Pacific Ocean, my goal is to draw circles from a point illustrating a certain distance, say 1000 km, from that point on the map.
How can I calculate the correct radius on the projection given the specific kilometers?
Here is my example code (yes, this is for a missile coming from North Korea. Artificially artificial Pyongyang as a launch point :), based on this question: Increase the circle radius (given in meters) to the map D3.js d3.geo.mercator
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: white;
stroke-width: 0.25px;
fill: grey;
}
circle {
stroke: red;
stroke-width: 1px;
stroke-dasharray: 5;
fill: transparent;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script>
var width = 960,
height = 500;
var projection = d3.geo.conicEquidistant()
.center([0, 5 ])
.scale((width + 1) / 2 / Math.PI)//.scale(150)
.rotate([-160,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
var missiles = [
{
name: "missile1",
location: { // appx lat&long of pyongyang
latitude: 125.6720717,
longitude: 39.0292506
},
reach: 1000 //radius of circle in kilometers on map
},
{
name: "missile2",
location: { // appx lat&long of pyongyang
latitude: 125.6720717,
longitude: 39.0292506
},
reach: 3500 //radius of circle in kilometers on map
},
];
// load and display the World
d3.json("https://s3-us-west-2.amazonaws.com/vida-public/geo/world-topo-min.json", function(error, topology) {
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
svg.selectAll(".pin")
.data(missiles)
.enter().append("circle", ".pin")
.attr("r", scaledRadius)
/*function(d) {
return d.reach
})*/
.attr("transform", function(d) {
return "translate(" + projection([
d.location.latitude,
d.location.longitude
]) + ")"
})
</script>
</body>
</html>
I looked at this answer, trying it above, but was unable to apply it to my code (I'm pretty new to d3, to be honest there might be something very obvious): https://stackoverflow.com/a/2423932/ ...
Further question: is this a solution specific to the mercator projection?
(Let's not ignore that we really need to hide Antarctica in this projection.)
source to share
There are two ways to accomplish this.
One (simpler option) uses the d3 function geoCircle
to create a geographically circular function:
var circle = d3.geoCircle().center([x,y]).radius(r);
For that, x and y are your center point in degrees, and r is the radius of the circle in degrees. To find the radius of a circle in meters, we need to convert meters to degrees - this is easiest if we assume a round earth (Earth is only slightly ellipsoid, so this is an induced error of up to 0.3%, but even if using an ellipsoid, the earth is indeed larger than potatoes so that will throw an error too). Using an average radius of 6,371 km, we can get a rough formula:
var circumference = 6371000 * Math.PI * 2;
var angle = distance in meters / circumference * 360;
var circle = d3.geoCircle().center([x,y]).radius(angle);
This gives us something like:
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var projection = d3.geoAlbers()
.scale(200)
.translate([width/2,height/2]);
var path = d3.geoPath().projection(projection);
var usa = {"type":"FeatureCollection", "features": [
{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-94.81758,49.38905],[-88.378114,48.302918],[-82.550925,45.347517],[-82.439278,41.675105],[-71.50506,45.0082],[-69.237216,47.447781],[-66.96466,44.8097],[-70.11617,43.68405],[-70.64,41.475],[-73.982,40.628],[-75.72205,37.93705],[-75.72749,35.55074],[-81.49042,30.72999],[-80.056539,26.88],[-81.17213,25.20126],[-83.70959,29.93656],[-89.18049,30.31598],[-94.69,29.48],[-99.02,26.37],[-100.9576,29.38071],[-104.45697,29.57196],[-106.50759,31.75452],[-111.02361,31.33472],[-117.12776,32.53534],[-120.36778,34.44711],[-123.7272,38.95166],[-124.53284,42.76599],[-124.68721,48.184433],[-122.84,49],[-116.04818,49],[-107.05,49],[-100.65,49],[-94.81758,49.38905]]],[[[-155.06779,71.147776],[-140.985988,69.711998],[-140.99777,60.306397],[-148.018066,59.978329],[-157.72277,57.570001],[-166.121379,61.500019],[-164.562508,63.146378],[-168.11056,65.669997],[-161.908897,70.33333],[-155.06779,71.147776]]]]},"properties":{"name":"United States of America"},"id":"USA"}
]};
var circumference = 6371000 * Math.PI * 2;
var angle = 1000000 / circumference * 360;
var circle = d3.geoCircle().center([-100,40]).radius(angle);
svg.append("path")
.attr("d",path(usa));
svg.append("path")
.attr("d", path(circle()))
.attr("fill","steelblue");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Another option is to create the geojson function "on the fly" from UDFs rather than via d3. The easiest way is to take a point and calculate the points located at a distance of x meters, on supports located at a distance of 10 degrees (36 points in a circle). This requires calculating a point using the start point, pivot and distance, the formula can be found here. Some time ago I built an example of a cloth indicatrix using this method: Tissot Indicatrix Bl.ock .
source to share
Based on Andrew's answer, this is how I solved my problem:
Added style, changed my code to d3 v4:
</!DOCTYPE html>
<html>
<head>
<title></title>
<style>
path {
stroke: white;
stroke-width: 0.25px;
fill: grey;
}
.circle {
stroke: red;
stroke-width: 1px;
stroke-dasharray: 5;
fill: transparent;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
d3 script:
var width = 1200;
var height = 400;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
Using a conical equidistant projection:
var projection = d3.geoConicEquidistant()
.center([0, 20])
.scale((width + 1) / 2 / Math.PI)
.rotate([-140,0]);
var path = d3.geoPath()
.projection(projection);
Making circles from Andrew's answer, one with 1000 km and others with a radius of 3500 km. They might be more convenient in lists / objects I assume.
var circumference = 6371000 * Math.PI * 2;
var angle1 = 1000000 / circumference * 360;
var missile1 = d3.geoCircle().center([125.6720717,39.0292506]).radius(angle1);
var angle2 = 3500000 / circumference * 360;
var missile2 = d3.geoCircle().center([125.6720717,39.0292506]).radius(angle2);
Loading and displaying the world:
var url = "http://enjalot.github.io/wwsd/data/world/world-110m.geojson";
//display the world
d3.json(url, function(err, geojson) {
svg.append("path")
.attr("d", path(geojson));
//append the circles
svg.append("path")
.attr("d", path(missile1()))
.attr("class","circle");
svg.append("path")
.attr("d", path(missile2()))
.attr("class","circle");
});
Edit: Added https to json file and changed the code so that circles are drawn over the world map. I have a working script here: https://jsfiddle.net/y8qn8tr1/2/
source to share