2020年12月25日金曜日

地図上に年表を

この記事は、ケーシーエスキャロット Advent Calendar 2020 の25日目の記事になります。

今年は趣味の「マジック研究」に時間を使うことが多かったので、AdventCalendar に書くことが無いなぁ、なんて思っていました。でも、それは 12/18 のaooki さんの記事を読むまでの話。WMS(地図画像)を Google Map 上にマッピングするというお話を読んで、過去にやりかけていたアイディアを思い出しました。

それは、歴史年表を地図上で表現したら面白いのではないかと思ったのがきっかけでした。世界地図の下にスライダーバーがあるだけのシンプルな画面をイメージしてください。スライダーを右に動かすと時代が進みます。たとえば、スライダーの位置を1926年に合わせると、地図上にはいくつかのポップアップが表示されています。アメリカ・ロサンゼルスあたりのポップアップには「マリリン・モンロー誕生」と書かれていて、イギリス・ロンドンあたりのポップアップには「エリザベス2世(現イギリス女王)誕生」と書かれています(同い年だったとは...)。そして同じポップアップの中には「アガサ・クリスティー失踪」というセンセーショナルな出来事が解説されていたりします。そんなアプリがあったら、スライダーバーを弄りながら色々な発見を楽しめるのではないかと思ったわけです。

1802年のイギリスには「蒸気機関車の発明」。それから少しだけスライダーを進めて1818年に合わせると、ドイツに「自転車の発明」と出てきます。自転車の方が後だという発見もなかなか面白いと思うのです。

とはいえ、あらゆる歴史をデータベースに入力するのはさすがに無理です。せめて各国のマジックの歴史をこのアプリで見られたら良いなぁ、という夢をもち、3年ほど前にちょっと手をつけてみたことがありました。データの準備に相当な労力がいるなぁと思いつつ、忙しくなって途中で放置していたのですが、aooki さんの記事で思い出したので、これを機にキリの良いところまでやってみることにしました。


歴史データ

FISM というマジックの国際大会についてのデータを使うことにします。FISM は1948年にスイス・ローザンヌで第1回が開催され、第5回までは毎年、それ以降は3年に1回のペースで現在まで続いています。ヨーロッパを中心にして開催されますが、第19回は横浜で開催されました。毎回場所が変わるのが今回作るアプリにとっては都合が良いところです。

Leaflet + OpenStreetMapでやってみる

leaflet は Web 上で地図を表示するための Java Script ライブラリです。
https://leafletjs.com/

OpenStreetMap は誰でも自由に使えるオープンな地図データです。
https://www.openstreetmap.org/

できた!

前置きが長くなってしまったので、動かしたときの動画を先に貼っておきます。スライダーの動きに合わせてポップアップが表示される様子がわかります。



コード解説

<head>に以下のように書き、leaflet の CSS と JavaScript ライブラリを読み込んでおきます。PC に何かをダウンロードしたりインストールしたり、という手間は一切ありません。お手軽です。
<head>
    <meta charset="UTF-8">
    <title>Histoly Map</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css">
    <script src="https://unpkg.com/leaflet@1.6.0/dist/leafet.js">&jt;/script>
</head>

<body>には、地図を置くための領域(mapcontainer)と、選択中の年を表示する領域(year)と、スライダーバー(slidecontainer)の3つだけ宣言しておきます。今回はデザインには拘らないので、単純に縦に並べました。
<div id="mapcontainer" style="width: 100%; height: calc(50vw)"></div>
<p id="year"></p>
<div class="slidecontainer">
    <input type="range" min="1948" max="2018" value="1948" class="slider" id="period">
</div>

PHP からデータベースにアクセスして、予め登録しておいたデータを全て取ってきます。(あらかじめ登録するのが一番大変だったわけですが、今回の話のポイントはそこじゃないのでサクッと省略です)
FISM の全てのデータは $data に入っていますが、それを後で JavaScript から使うために JSON に変換し、$data_json としてあります。
<?php
    try {
        $dbh = new PDO('mysql:host=xxxxx;dbname=yyyyy;port=0000;charset=utf8',
                       'account', 'password');
        $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $res = $dbh->query("select xxxx");

        $data = $res->fetchAll();
        $data_json = json_encode($data);
        $min_json = json_encode($dbh->query("select MIN(year) from history")->fetchAll());
        $max_json = json_encode($dbh->query("select MAX(year) from history")->fetchAll());
    } catch (PDOException $e) {
        print "エラー: " . $e->getMessage() . "<br/>";
        die();
    }
?>

いよいよ OpenStreetMap を貼り付けるところですが、非常に簡単です。事前に読み込んだライブラリの中に定義されている L.map()に、地図を表示するための領域(mapcontainer)を渡してインスタンス化します。setView() では東京の座標を指定しました。これで世界地図の中心が東京になります。

また、L.tileLayer() では実際に貼り付ける地図データを指定します。ここではシンプルな白地図を指定してあります。zoom = 2 にすると、ちょうど世界地図の全体が一画面に表示される程度の大きさだったので、これを最小値としました。zoom = 15 まで段階的にズームしていくことができます。
<script>
    const MIN_ZOOM = 2;
    const MAX_ZOOM = 15;
    var mymap = L.map('mapcontainer').setView([35.4122, 139.4130], MIN_ZOOM);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        minZoom: MIN_ZOOM,
        maxZoom: MAX_ZOOM,
        attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(mymap);

データベースから取得した全てのデータは $php_json に入っているので、あとはスラーダーバーの位置から西暦を割り出し、その年のデータを地図上にマーカーとして表示するだけです。そしてこれも leaflet が用意してくれています。GPS 座標を指定して L.marker() を作るだけです。
    var js_array = <?php echo $php_json; ?>;

    var slider = document.getElementById("period");
    var min_j = <?php echo $min_json; ?>;
    slider.min = min_j[0][0];
    var max_j = <?php echo $max_json; ?>;
    slider.max = max_j[0][0];
            
    var selected_year = document.getElementById("year");
    selected_year.innerHTML = slider.min;
            
    var markers = [];
    slider.oninput = function () {
        selected_year.innerHTML = slider.value;
        while (markers.length > 0) {
            var marker = markers.pop();
            mymap.removeLayer(marker);
        }
        markers = [];
        for (var i=0; i<js_array.length; i++) {
            var row = js_array[i];
            if (row['year'] < (parseInt(slider.value)-1)) {
                continue;
            }
            if (row['year'] > (parseInt(slider.value)+1)) {
                continue;
            }
            var popupContent = '<img src="' + './images/' + row['banner'] +
                               '">' + '<br><h2>' +
                               row['year'] + " : " + row['event'] + "</h2>" +
                               "<p>" + row['city'] + " (" + row['country'] + ")";
            var marker = L.marker([row['city_latitude'],  
                                   row['city_longitude']]).bindPopup(popupContent);
                    
            var gap = Math.abs(row['year'] - parseInt(slider.value));
            mymap.addLayer(marker);
            switch (gap) {
                case 0:
                    marker.setOpacity(1.0).openPopup();
                    break;
                case 1:
                    marker.setOpacity(0.5);
                    break;
            }
            markers.push(marker);
        }
    };
</script>
これくらいのコードで先程の動画のような動きができるようになります。動きもスムーズでストレスなく使えました。Leaflet + OpenStreetMap の組み合わせ、素晴らしいです。


余談

ところで、地図アプリの性能として、マーカーの位置はどの程度正確に表現されているのかも気になります。ズームすると少しずれていた、なんてことがないか一応確認してみました。第25回のブラックプールの辺りをズームしてみた動画を貼っておきます。zoom = 13 までズームしたところ、ブラックプールの地区の中にポップアップが表示されていることが確認できました。

ちなみに、zoom = 15 までズームすると通りの名前が一通り表示されます。
FISM のデータだけでは何も新しい発見はありませんでしたが、「マジシャンの興行履歴」「主な専門書の発行記録」「クラシックマジックの原案の公表記録」等々、データが増えれば楽しくなるんじゃないかと。多分。。きっと。。。おそらく。。。。

0 件のコメント:

コメントを投稿