FOSS4G Advent Calendar 2024 の6日目です。
最初は GDAL で何か書けないかなと思ってましたが、ネタが微妙だったので5月17日に行われた FOSS4G もくもく会 #002 でもくもくしたときのことをまとめます。
目的
地軸が傾いた地球を描きたい。
できあがり
d3.js をつかって
多くの GIS ソフトウェアでは、基本的に北を上にした「地図」を前提としています(作図した地図を回転させることができるものもあります)。
しかし地球の自転軸は公転面(黄道面)に対し約23.4度傾いています。これを何とかして描画したい! というわけで d3.js で描いてみました。
d3.js はデータビジュアライズのためのライブラリで、位置データ以外にも様々なデータをグラフィカルに表現でき、地図投影法もサポートしています。
純粋な GIS のためのライブラリでないため(?)か、非常に柔軟な表現ができ、投影法も既存のものから任意の数式までサポートしています。たとえば、地球から地図に投影する様子のアニメーションだって可能です。
そして、 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 方向)を表し、いわゆるヨー角、ピッチ角、ロール角にあたります。
ソースコード
<!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 というサービス上で公開されています。
今回のソースもいろいろ参考にしています。もくもく会だったこともあり、ほぼ切り貼りですが。 d3.js はもっと勉強したいところ。
Observable は Jupyter lab みたいにセルにコードを入れて実行することができるみたいなんですが、実行順が上から読むのか下から読むのかよくわからなかったり、 FileAttachment
などの Observable の機能が使われていたりと、純粋な HTML + JavaScript で実現する方法を確認するのに手間取りました。
あと最近の JavaScript の常識をあんまり知らず、 await
は同期非同期の指定くらいの認識で、軽い処理ならあってもなくても大差ないのかな。と最初省いて書いてしまっていました。
const world = d3.json("./land-50m.json"); // await を書かなかった
const land = topojson.feature(world, world.objects.land);
上記のように記述したとき、2行目で land
が取り出せない。なんでだ、としばらく悩み続け、ようやく JSON オブジェクトではなく Promise
が返っていることに気付きました。理解できてないなら、ちゃんと写経しないとダメですね。
おまけ
自由に回転させることができるので、凝った斜軸投影も可能となります。
これは全大陸が切れ目なく収まる斜軸モルワイデくん2。
-
普通は Y 軸の回転は +Z から +X 方向が正だと思うのですが ↩