この日記は、RDBMS-GIS(MySQL,PostgreSQLなど) Advent Calendar 2019 の2日目の記事です。
( https://qiita.com/advent-calendar/2019/rdbms_gis )
経度一度ってどれくらい?
赤道付近の一周の長さが だいたい 40,000km だというのはみんな知っていますよね。実際はもう少し長いのですが、とりあえず感覚的なものでいいです。で、経度というのは、緯度が上がっていくごとに円が小さくなりますから、1度の長さもどんどん短くなるはずです。どんな感じなのかな、とMySQLを使って見てみました。
MySQL、まだまだ関数が少ないので(とくに集約系や分析的なもの)、関数が少ないうちに、少ないこれらの関数を遊び倒しましょう。できることが限られているというのは、アイデアを膨らませるという点でメリットでもあります!
経度1度を求める
まず、テーブル g1 を作って適当にデータを入れます。ここでは、緯度0度から10度刻みで、緯度135度と136度の2つの位置情報をカラムに入れることにしました。測地系は、JGD2011を使います。
CREATE TABLE g1 ( id integer, pos1 GEOMETRY SRID 6668, pos2 GEOMETRY SRID 6668 ); INSERT INTO g1 VALUES (0, ST_GeomFromText('POINT(0 135)', 6668), ST_GeomFromText('POINT(0 136)', 6668)); INSERT INTO g1 VALUES (10, ST_GeomFromText('POINT(10 135)', 6668), ST_GeomFromText('POINT(10 136)', 6668)); INSERT INTO g1 VALUES (20, ST_GeomFromText('POINT(20 135)', 6668), ST_GeomFromText('POINT(20 136)', 6668)); INSERT INTO g1 VALUES (30, ST_GeomFromText('POINT(30 135)', 6668), ST_GeomFromText('POINT(30 136)', 6668)); INSERT INTO g1 VALUES (35, ST_GeomFromText('POINT(35 135)', 6668), ST_GeomFromText('POINT(35 136)', 6668)); INSERT INTO g1 VALUES (40, ST_GeomFromText('POINT(40 135)', 6668), ST_GeomFromText('POINT(40 136)', 6668)); INSERT INTO g1 VALUES (50, ST_GeomFromText('POINT(50 135)', 6668), ST_GeomFromText('POINT(50 136)', 6668)); INSERT INTO g1 VALUES (60, ST_GeomFromText('POINT(60 135)', 6668), ST_GeomFromText('POINT(60 136)', 6668)); INSERT INTO g1 VALUES (70, ST_GeomFromText('POINT(70 135)', 6668), ST_GeomFromText('POINT(70 136)', 6668)); INSERT INTO g1 VALUES (80, ST_GeomFromText('POINT(80 135)', 6668), ST_GeomFromText('POINT(80 136)', 6668)); INSERT INTO g1 VALUES (90, ST_GeomFromText('POINT(90 135)', 6668), ST_GeomFromText('POINT(90 136)', 6668));
このテーブルの各レコードの長さは、以下のSQLで求めることができます。
SELECT id, ST_Distance(pos1, pos2) FROM g1 ORDER BY ID; +------+-------------------------+ | id | ST_Distance(pos1, pos2) | +------+-------------------------+ | 0 | 111319.49079326246 | | 10 | 109639.3390102644 | | 20 | 104646.97563995633 | | 30 | 96486.0081494232 | | 35 | 91287.79089978167 | | 40 | 85393.3619519223 | | 50 | 71695.04015879167 | | 60 | 55799.17666195067 | | 70 | 38185.80088710373 | | 80 | 19393.044953483724| | 90 | 0 | +------+-------------------------+ 11 rows in set (0.00 sec)
赤道付近で、1度あたり111km、日本がある北緯35度付近では91km、70度で38km、80度で19kmと一気に狭くなっていくのがわかりますね。感覚とも一致します。
1度ではなく1分は?
何十キロもの長さは日常でイメージしにくいので、もう少し小さい単位である「分」の単位で距離を求めてみましょう。1分は約 0.0167度 なので以下のようなデータを作ります。各緯度で、経度135度から135.0167度を表すデータ群です。
CREATE TABLE g2 (id integer, pos1 GEOMETRY SRID 6668, pos2 GEOMETRY SRID 6668); INSERT INTO g2 VALUES (0, ST_GeomFromText('POINT(0 135)', 6668), ST_GeomFromText('POINT(0 135.0167)', 6668)); INSERT INTO g2 VALUES (10, ST_GeomFromText('POINT(10 135)', 6668), ST_GeomFromText('POINT(10 135.0167)', 6668)); INSERT INTO g2 VALUES (20, ST_GeomFromText('POINT(20 135)', 6668), ST_GeomFromText('POINT(20 135.0167)', 6668)); INSERT INTO g2 VALUES (30, ST_GeomFromText('POINT(30 135)', 6668), ST_GeomFromText('POINT(30 135.0167)', 6668)); INSERT INTO g2 VALUES (35, ST_GeomFromText('POINT(35 135)', 6668), ST_GeomFromText('POINT(35 135.0167)', 6668)); INSERT INTO g2 VALUES (40, ST_GeomFromText('POINT(40 135)', 6668), ST_GeomFromText('POINT(40 135.0167)', 6668)); INSERT INTO g2 VALUES (50, ST_GeomFromText('POINT(50 135)', 6668), ST_GeomFromText('POINT(50 135.0167)', 6668)); INSERT INTO g2 VALUES (60, ST_GeomFromText('POINT(60 135)', 6668), ST_GeomFromText('POINT(60 135.0167)', 6668)); INSERT INTO g2 VALUES (70, ST_GeomFromText('POINT(70 135)', 6668), ST_GeomFromText('POINT(70 135.0167)', 6668)); INSERT INTO g2 VALUES (80, ST_GeomFromText('POINT(80 135)', 6668), ST_GeomFromText('POINT(80 135.0167)', 6668)); INSERT INTO g2 VALUES (90, ST_GeomFromText('POINT(90 135)', 6668), ST_GeomFromText('POINT(90 135.0167)', 6668));
SELECT id, ST_Distance(pos1, pos2) FROM g2 ORDER BY ID; +------+-------------------------+ | id | ST_Distance(pos1, pos2) | +------+-------------------------+ | 0 | 1859.0354970687047 | | 10 | 1830.9776594574585 | | 20 | 1747.607087970476 | | 30 | 1611.3214483744373 | | 35 | 1524.512474129115 | | 40 | 1426.0766207422205 | | 50 | 1197.3160879967666 | | 60 | 931.8551188371557 | | 70 | 637.7100204965047 | | 80 | 323.86784178140675| | 90 | 0 | +------+-------------------------+
赤道付近で1.9km、北緯35度付近で1.5kmです。念のため赤道付近の「分」の結果を60倍して答え合わせをしてみましょうか。
mysql> SELECT 1859.0354970687047 * 60 ; +-----------------------+ | 1859.0354970687047*60 | +-----------------------+ | 111542.1298241222820 | +-----------------------+ 1 row in set (0.00 sec)
あれれ。本来の赤道付近の距離 111319.49079326246 よりも 少し大きくなっていまいましたね。考えてみたら、
「1度」を 0.0167 としましたが、これがちょっと切り上げが雑すぎたのかもしれません。0.016666666666667くらいでやるべきだったのでしょうか。まぁこれは感覚とも一致するのでよしとしましょう。
というかそもそも、0.0166666....とか言っている時点で誤差許容なので、もうちょっとまともな計算をしましょうか。
経度1度、1分、1秒の長さを求める(別解)
最初に「度」を求めたのですから、度を60で割って分を求めたり、度を60*60で割って秒を求めたりするほうが(先ほどみたいに途中で不正確な小数を持ち出すよりも)正確な値が求まります。ここでは、そのやり方で。最初に作った g1 テーブルを使います。
WITH cte_g1 AS ( SELECT id, ST_Distance(pos1, pos2) dist FROM g1 ) SELECT id, round(dist,2) do, round(dist/60,2) fun, round(dist/60/60,2) byou FROM cte_g1; +------+-----------+---------+-------+ | id | do | fun | byou | +------+-----------+---------+-------+ | 0 | 111319.49 | 1855.32 | 30.92 | | 10 | 109639.34 | 1827.32 | 30.46 | | 20 | 104646.98 | 1744.12 | 29.07 | | 30 | 96486.01 | 1608.10 | 26.80 | | 35 | 91287.79 | 1521.46 | 25.36 | | 40 | 85393.36 | 1423.22 | 23.72 | | 50 | 71695.04 | 1194.92 | 19.92 | | 60 | 55799.18 | 929.99 | 15.50 | | 70 | 38185.80 | 636.43 | 10.61 | | 80 | 19393.04 | 323.22 | 5.39 | | 90 | 0.00 | 0.00 | 0.00 | +------+-----------+---------+-------+ 11 rows in set (0.00 sec)
うーん、CTE便利! MySQL 8.0 最高! まぁこの程度なら、FROMのサブクエリに書いてもいいんですけどね。
少数以下が長くあっても仕方がないので、ここでは小数第2位(センチメートル)で丸めて出力しました。
赤道付近での1秒は30.92m、北緯35度では 25.36m だとわかります。これくらいの長さだと、そのへんの通り道を眺めながら「あそこまでで1秒かー」とイメージが湧きやすいですね。
緯度はどうなるか
ここまでやったらならば、一方の緯度はどうなるか確認してみたくなります。
緯度は経度みたいには細くなっていかないので、一定のままだろう、と予想して、テーブルg3とそのデータを作って試します。
CREATE TABLE g3 (id integer, pos1 GEOMETRY SRID 6668, pos2 GEOMETRY SRID 6668); INSERT INTO g3 VALUES (0, ST_GeomFromText('POINT(0 135)', 6668), ST_GeomFromText('POINT(1 135)', 6668)); INSERT INTO g3 VALUES (10, ST_GeomFromText('POINT(10 135)', 6668), ST_GeomFromText('POINT(11 135)', 6668)); INSERT INTO g3 VALUES (20, ST_GeomFromText('POINT(20 135)', 6668), ST_GeomFromText('POINT(21 135)', 6668)); INSERT INTO g3 VALUES (30, ST_GeomFromText('POINT(30 135)', 6668), ST_GeomFromText('POINT(31 135)', 6668)); INSERT INTO g3 VALUES (35, ST_GeomFromText('POINT(35 135)', 6668), ST_GeomFromText('POINT(36 135)', 6668)); INSERT INTO g3 VALUES (40, ST_GeomFromText('POINT(40 135)', 6668), ST_GeomFromText('POINT(41 135)', 6668)); INSERT INTO g3 VALUES (50, ST_GeomFromText('POINT(50 135)', 6668), ST_GeomFromText('POINT(51 135)', 6668)); INSERT INTO g3 VALUES (60, ST_GeomFromText('POINT(60 135)', 6668), ST_GeomFromText('POINT(61 135)', 6668)); INSERT INTO g3 VALUES (70, ST_GeomFromText('POINT(70 135)', 6668), ST_GeomFromText('POINT(71 135)', 6668)); INSERT INTO g3 VALUES (80, ST_GeomFromText('POINT(80 135)', 6668), ST_GeomFromText('POINT(81 135)', 6668)); INSERT INTO g3 VALUES (90, ST_GeomFromText('POINT(89 135)', 6668), ST_GeomFromText('POINT(90 135)', 6668));
mysql> SELECT id, ST_Distance(pos1, pos2) FROM g3 ORDER BY ID; +------+-------------------------+ | id | ST_Distance(pos1, pos2) | +------+-------------------------+ | 0 | 110573.13812416371 | | 10 | 110610.23595555034 | | 20 | 110710.37167306663 | | 30 | 110861.46743133431 | | 35 | 110950.61420268985 | | 40 | 111045.29885197058 | | 50 | 111239.69315258414 | | 60 | 111421.20351156592 | | 70 | 111567.93710081652 | | 80 | 111662.1956839133 | | 90 | 111692.61028462648 | +------+-------------------------+
あれれ。みんなだいたい111kmではありますが(赤道付近の経度1度ともほぼ合致していますね)、よく見ると少しずつ値が異なりますね。そうか、地球(JGD2011でのモデルであるGRS80)は真球ではなく回転楕円体だから、緯度があがるごとに少しずつ値が変化するわけですね。確かにそうだ。
いや、でもよく見てみると、、、、緯度があがるほうが1度あたりの距離が長くなっていますね。イメージを真球から極半径を共有した回転楕円体へと拡張していくと、赤道付近のほうがぐーっと伸びているのだから、そちらのほうが1度あたりの距離が(真球と比べて)より長くなるようなイメージを持っていたのですが・・・・・かるく調べてみると値が間違っているわけではないようですが、感覚的に納得できません。。ここまでで今日の私はギブアップ。
誰か、「なぜ極付近のほうが緯度1度の距離が長くなるのか」、わかりやすくおしえてください!