地球儀っぽく🌏

FOSS4G Advent Calendar 2024 の6日目です。

最初は GDAL で何か書けないかなと思ってましたが、ネタが微妙だったので5月17日に行われた [FOSS4G もくもく会 #002](https://www.osgeo.jp/archives/3965) でもくもくしたときのことをまとめます。


## 目的

地軸が傾いた地球を描きたい。

## できあがり

![globe_rotation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215290/c9f63aaf-4594-0911-c1ef-29bb85ca8104.gif)


## d3.js をつかって

多くの GIS ソフトウェアでは、基本的に北を上にした「地図」を前提としています（作図した地図を回転させることができるものもあります）。

しかし地球の自転軸は公転面（黄道面）に対し約23.4度傾いています。これを何とかして描画したい！　というわけで d3.js で描いてみました。

https://d3js.org/

d3.js はデータビジュアライズのためのライブラリで、位置データ以外にも様々なデータをグラフィカルに表現でき、地図投影法もサポートしています。

純粋な GIS のためのライブラリでないため（？）か、非常に柔軟な表現ができ、投影法も既存のものから任意の数式までサポートしています。たとえば、地球から地図に投影する様子のアニメーションだって可能です。

https://observablehq.com/@d3/orthographic-to-equirectangular

そして、 d3.js には `projection.rotate(lambda, phi, gamma)` があります。これは地図ではなく地球の3軸に対し回転角を設定することができるものです。

地球における X 軸は本初子午線と赤道の交点方向、 Y 軸は東経90度子午線と赤道の交点方向、 Z 軸は北極方向です。各引数の `lambda` は Z 軸の回転角（ +X から +Y 方向）、 `phi` は Y 軸の回転角（ +X から +Z 方向[^1]）、 `gamma` は X 軸の回転角（ +Y から +Z 方向）を表し、いわゆるヨー角、ピッチ角、ロール角にあたります。

[^1]: 普通は Y 軸の回転は +Z から +X 方向が正だと思うのですが

https://observablehq.com/@d3/three-axis-rotation

## ソースコード

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/topojson@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
    <script src="https://cdn.jsdelivr.net/npm/d3-geo@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/d3-geo-projection@4"></script>
  </head>
  <body>
    <div id="container" style="margin:50px;"></div>
    <script type="module">
      // データの読み込み
      // TopoJSON に変換した NaturalEarth
      const world = await d3.json("./land-50m.json");
      const land = topojson.feature(world, world.objects.land);
      const graticules = await d3.json("./ne_10m_graticules_15.geojson");

      // 投影法および大きさなどの設定
      let sphere = ({type: "Sphere"});
      let projection = d3.geoOrthographic();

      const width = 600;
      const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
      const height = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), height);
      projection.scale(projection.scale() * (l - 1) / l).precision(0.2);

      // canvas の用意
      let canvas = d3.select("div#container").append('canvas')
        .attr("width", width)
        .attr("height", height);
      let context = canvas.node().getContext('2d');
      let path = d3.geoPath(projection, context);

      // 地球の描画を行う関数
      function renderGlobe(lambda, phi, gamma){
        // 回転角によって回転させる
        projection.rotate([lambda, phi, gamma]);
        context.clearRect(0, 0, width, height);
        context.beginPath(), path(sphere), context.fillStyle = "#fff", context.fill();
        context.beginPath(), path(land), context.fillStyle = "#000", context.fill();
        context.beginPath(), path(graticules), context.strokeStyle = "#666", context.stroke();
        context.beginPath(), path(sphere), context.stroke();
      }

      // 初期の回転角
      const lambda0 = 110.0;
      const phi0    = -16.67;
      const gamma0  = -16.67;
      renderGlobe(lambda0, phi0, gamma0);

      // アニメーション
      let i = 0;
      const tick_angle = 2;
      const t = d3.timer(elapsed => {
        renderGlobe(lambda0+i/tick_angle, phi0, gamma0);
        i = (i+1) % (360*tick_angle);
      }, 10);
    </script>
  </body>
</html>
```

## 苦労したところ

d3.js は豊富にギャラリーが用意されているのですが、 observablehq.com というサービス上で公開されています。

https://observablehq.com/@d3/gallery

今回のソースもいろいろ参考にしています。もくもく会だったこともあり、ほぼ切り貼りですが。 d3.js はもっと勉強したいところ。

Observable は Jupyter lab みたいにセルにコードを入れて実行することができるみたいなんですが、実行順が上から読むのか下から読むのかよくわからなかったり、 `FileAttachment` などの Observable の機能が使われていたりと、純粋な HTML + JavaScript で実現する方法を確認するのに手間取りました。

あと最近の JavaScript の常識をあんまり知らず、 `await` は同期非同期の指定くらいの認識で、軽い処理ならあってもなくても大差ないのかな。と最初省いて書いてしまっていました。

```javascript
      const world = d3.json("./land-50m.json"); // await を書かなかった
      const land = topojson.feature(world, world.objects.land);
```

上記のように記述したとき、2行目で `land` が取り出せない。なんでだ、としばらく悩み続け、ようやく JSON オブジェクトではなく `Promise` が返っていることに気付きました。理解できてないなら、ちゃんと写経しないとダメですね。

## おまけ

自由に回転させることができるので、凝った斜軸投影も可能となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215290/48419437-a65b-83ab-ae7c-e7b2fb2c8cf1.png)

これは全大陸が切れ目なく収まる斜軸モルワイデくん[^2]。

[^2]: https://www2.jpgu.org/meeting/2011/yokou/MTT033-P01.pdf