D3 地球表示（マウス操作で回転）

こんにちは。
"[D3 3D globe trackball rotation](http://bl.ocks.org/patricksurry/5721459)" と言う記事を見つけたので、そっくり真似してみました（d3.js v4 利用）。マウス操作で地球を回転できます。

なお地軸を左右に傾ける必要が無い場合には、回転に関する関数定義はかなり簡素化できます（下記の```trackballAngles_simple()```, ```composedRotation_simple()```）。

<img width="221" alt="スクリーンショット 2017-05-28 18.38.56.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/54617/52849e50-f2f3-3e32-85d3-f87909b53bd4.jpeg">

```d3_globe_rotation.html
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg { border: 0.5px solid #eee; }
.title {
  display: inline-block;
  font-size: 48px;
  line-height: 90px;
  text-align: center;
}
.land {
  fill: #69D2E7;
  stroke: none;
}
.countries {
  stroke: #fff;
  stroke-width: 2px;
  fill: none;
}
.graticule {
  fill: none;
  stroke: #777;
  stroke-width: 0.5px;
  stroke-opacity: 0.5;
}
</style>
<body>
<div .title><a href="http://bl.ocks.org/patricksurry/5721459">D3 Globe Trackball</a></div>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v3.min.js"></script>
<script>
var radius = 220, scale = 1.0, scaleMin = 0.1;
var projection = d3.geoOrthographic()
    .scale(radius)
    .translate([radius, radius])
    .clipAngle(90);
var path = d3.geoPath()
    .projection(projection);
var graticule = d3.geoGraticule();

var svg = d3.select("body").append("svg")
    .attr("width", radius * 2)
    .attr("height", radius * 2)
    .on("mousedown", mousedown)
    .on("mousemove", mousemove)
    .on("mouseup", mouseup)
    .on("wheel", zoom);

var circle = svg.append("circle")
    .attr("cx", radius)
    .attr("cy", radius)
    .attr("r", radius)
    .style("fill", "none")
    .style("stroke", "black");

var url = "https://unpkg.com/world-atlas@1/world/50m.json"; // topojson

d3.json(url, function(error, world) {
  svg.insert("path")
	  .datum(topojson.feature(world, world.objects.land))
	  .attr("class", "land")
	  .attr("d", path);
  svg.insert("path")
	  .datum(topojson.mesh(world, world.objects.countries))
	  .attr("class", "countries")
	  .attr("d", path);
  svg.append("path")
      .datum(graticule)
      .attr("class", "graticule")
      .attr("d", path);
});

function zoom() {
  scale *= Math.exp(d3.event.deltaY*0.01);
  scale = Math.max(scale, scaleMin);
  circle.attr("r", radius*scale);
  projection.scale(radius*scale);
  svg.selectAll("path").attr("d", path);
}

function trackballAngles(pt) {
  var r = projection.scale();
  var c = projection.translate();
  var x = pt[0] - c[0], y = - (pt[1] - c[1]), ss = x*x + y*y;
  var z = r*r > 2 * ss ? Math.sqrt(r*r - ss) : r*r / 2 / Math.sqrt(ss);  
  var lambda = Math.atan2(x, z) * 180 / Math.PI; 
  var phi = Math.atan2(y, z) * 180 / Math.PI
  return [lambda, phi];
}

function composedRotation(λ, ϕ, γ, δλ, δϕ) {
    λ = Math.PI / 180 * λ;
    ϕ = Math.PI / 180 * ϕ;
    γ = Math.PI / 180 * γ;
    δλ = Math.PI / 180 * δλ;
    δϕ = Math.PI / 180 * δϕ;

    var sλ = Math.sin(λ), sϕ = Math.sin(ϕ), sγ = Math.sin(γ), 
        sδλ = Math.sin(δλ), sδϕ = Math.sin(δϕ),
        cλ = Math.cos(λ), cϕ = Math.cos(ϕ), cγ = Math.cos(γ), 
        cδλ = Math.cos(δλ), cδϕ = Math.cos(δϕ);

    var m00 = -sδλ * sλ * cϕ + (sγ * sλ * sϕ + cγ * cλ) * cδλ,
            m01 = -sγ * cδλ * cϕ - sδλ * sϕ,
                m02 = sδλ * cλ * cϕ - (sγ * sϕ * cλ - sλ * cγ) * cδλ,
        m10 = - sδϕ * sλ * cδλ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * sδϕ - (sλ * sϕ * cγ - sγ * cλ) * cδϕ,
            m11 = sδλ * sδϕ * sγ * cϕ - sδϕ * sϕ * cδλ + cδϕ * cγ * cϕ,
                 m12 = sδϕ * cδλ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * sδϕ + (sϕ * cγ * cλ + sγ * sλ) * cδϕ,
        m20 = - sλ * cδλ * cδϕ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * cδϕ + (sλ * sϕ * cγ - sγ * cλ) * sδϕ,
            m21 = sδλ * sγ * cδϕ * cϕ - sδϕ * cγ * cϕ - sϕ * cδλ * cδϕ,
                 m22 = cδλ * cδϕ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * cδϕ - (sϕ * cγ * cλ + sγ * sλ) * sδϕ;

    if (m01 != 0 || m11 != 0) {
         γ_ = Math.atan2(-m01, m11);
         ϕ_ = Math.atan2(-m21, Math.sin(γ_) == 0 ? m11 / Math.cos(γ_) : - m01 / Math.sin(γ_));
         λ_ = Math.atan2(-m20, m22);
    } else {
         γ_ = Math.atan2(m10, m00) - m21 * λ;
         ϕ_ = - m21 * Math.PI / 2;
         λ_ = λ;       
    }

    return([λ_ * 180 / Math.PI, ϕ_ * 180 / Math.PI, γ_ * 180 / Math.PI]);
}

// simple rotation:
//   function trackballAngles_simple(pt) {return pt};
//   function composedRotation_simple(λ, ϕ, γ, δλ, δϕ) {return [λ + δλ/3, ϕ - δϕ/3]};

var o0, m0 = null;

function mousedown() {
  m0 = trackballAngles(d3.mouse(this));
  o0 = projection.rotate();
  d3.event.preventDefault();
}

function mousemove() {
  if (m0) {
    var m1 = trackballAngles(d3.mouse(this));
    o1 = composedRotation(o0[0], o0[1], o0[2], m1[0] - m0[0], m1[1] - m0[1])
    projection.rotate(o1);
    svg.selectAll("path").attr("d", path); 
  }
}

function mouseup() {
  m0 = null;
}
</script>
```

------------------
なお同様なものも多数あるようです。

- http://marcneuwirth.com/blog/2012/06/24/creating-the-earth-with-d3-js/
- https://www.jasondavies.com/maps/rotate/
- http://webpages.uncc.edu/czhang22/samples/globe/index.html 
- http://bl.ocks.org/KoGor/5994804
- http://bl.ocks.org/phoebebright/5976037
- http://bl.ocks.org/patricksurry/5721459
- https://bl.ocks.org/hugolpz/a62f1d61168eae985d5c
- http://bl.ocks.org/wetchler/b9a20341f395466b0b5e
- https://bl.ocks.org/emeeks/af3c0114adfd9ead565e6c0f4a9c494e