Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw circles with radii given in kilometers accurately on world map

Tags:

maps

d3.js

geo

I'm using the d3.geo.conicEquidistant() projection on a world map approximately centered on the Pacific, my aim is to draw circles from a point, illustrating a specific distance, say 1000km, from this point on the map.

How can i calculate the correct radius on the projection, given specific kilometers?

Here is my code example (yes, it's about missile reaches from North Korea. Using artificially Pyongyang as launching point :), building on this question: Scale a circle's radius (given in meters) to D3.js d3.geo.mercator map

<!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 out above, but was not quite able to apply it to my code (i'm quite new to d3 to be honest, there might be something very obvious wrong): https://stackoverflow.com/a/31616927/4623519

Further question: is this solution specific to the mercator projection?

(Let's ignore that we really need to hide Antarctica in this projection.)

like image 322
JJxyz Avatar asked Dec 19 '22 05:12

JJxyz


2 Answers

There are two ways to achieve this.

One (the easier option) uses d3's geoCircle functionality to create a geographically circular feature:

var circle = d3.geoCircle().center([x,y]).radius(r);

For this, 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 into degrees - which is easiest if we assume a round earth (The 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 really more potato shaped, so that too will induce error). Using a mean radius of 6,371 km we can get a rough formula like:

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>

The other option is to create a geojson feature on the fly from custom functions rather than through d3. The easiest way may be to take a point, and calculate the points that are x meters away at bearings spaced 10 degrees apart (for 36 points to the circle). This requires calculating a point using start point, bearing and distance, the formula can be found here. A while ago I built an example tissot's indicatrix using this method: Tissot's Indicatrix Bl.ock.

like image 167
Andrew Reid Avatar answered Apr 19 '23 23:04

Andrew Reid


Building on Andrew's answer, here is how i solved my problem:

Added styling, 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>

The d3 script:

var width = 1200;
var height = 400;

var svg = d3.select("body")
  .append("svg")
  .attr("width",width)
  .attr("height",height);

Using the conic equidistant projection:

var projection = d3.geoConicEquidistant()
    .center([0, 20])
    .scale((width + 1) / 2 / Math.PI)
    .rotate([-140,0]);

var path = d3.geoPath()
    .projection(projection);

Creating circles from Andrew's answer, one with 1000km and other with 3500km radius. These could be arranged more nicely into lists/objects i guess.

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 the json file and altered the code so the circles are drawn above the world map. I have a working jfiddle here: https://jsfiddle.net/y8qn8tr1/2/

like image 42
JJxyz Avatar answered Apr 20 '23 00:04

JJxyz