BODIK APIを使ったJavascriptプログラムを書いてみます。

課題2:BODIK APIの検索結果を地図に表示する

問題:BODIK APIを使って、AEDを検索し、結果を地図に表示(マーカーを表示)する。

基本情報

  • Javascriptで地図を表示する場合、「Leaflet」というライブラリを使うと便利です。
  • JavascriptでLeafletを使うためには、ライブラリをインポートする必要があります。
  • Leafletにはいくつかの関数が用意されています。詳細は公式HPをご参照ください。
    Leaflet : https://leafletjs.com/
folium関数機能
L.map()マップを作成するlet map = L.map('map');
L.popup()ポップアップを作成するlet popup = L.popup()
L.marker()マーカーを作成するlet marker = L.marker(location).add_to(map);
Leafletの主な関数

swaggerで試す

最初に、WAPIのswagger(https://wapi.bodik.jp/docs)でAPIを試してみましょう。

AED(GET /aed)のエンドポイントを表示し、「try it out」のボタンを押します。

今回は位置情報が欲しいので、いくつかのパラメータを指定します。

下の方にある青い「Execute」ボタンを押します。

検索結果が表示されます。「Request URL」のところを確認します。

パラメータ部分が変わりました。

select_type=geometry
maxResults=10
lat=33.59328962901721
lon=130.35598920962553
distance=2000

Javascriptで記述

最初に、Javascriptで「BODIK API」を呼び出して位置情報を取得してみましょう。

緯度と経度を指定して、位置情報で検索します。
位置情報は、featureの「geometry」に格納されています。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>BODIK API</title>
    <script type="text/javascript">
        let api_server = 'https://wapi.bodik.jp';
        let api = 'aed';
        let lat = 33.59334325082392;
        let lon = 130.35598920962553;
        let distance = 2000;
        let api_url = `${api_server}/${api}?select_type=geometry&lat=${lat}&lon=${lon}&distance=${distance}`;
        fetch(api_url)
        .then(response => response.json())
        .then(data => {
            let array = [];
            for (let feature of data['resultsets']['features']) {
                array.push(feature);
            }
            let output = document.getElementById('output');
            output.innerHTML = JSON.stringify(array, null, 2);
        });
    </script>
</head>

<body>
    <h2>BODIK API program #2</h2>
    <pre id="output"></pre>
</body>

</html>

featureのgeometryは、次のような構造になっています。

'geometry' : {
  'type': 'Point',
  'coordinates': [ lon, lat ]    # 緯度(lat)と経度(lon)が逆になっていることに注意する
}

検索結果のgeometryを使って、地図に表示すればいいのですが、Javascriptで地図を表示するにはどうしたらいいのでしょうか?

Javascriptで地図を表示

Javascriptで地図を表示する方法として、Leafletが便利です。
Leafletを使うためには、LeafletのCSSとライブラリを参照します。

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
    crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
    crossorigin=""></script>

とりあえず、地図を表示してみましょう。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>BODIK API</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
        integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
        crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
        integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
        crossorigin=""></script>
    <script type="text/javascript">
        let map = null;
        function init() {
            map = L.map('map', { zoomControl: false });
            //  指定された緯度経度を中心とした地図
            let lat = 33.59334325082392;
            let lon = 130.35598920962553;
            let center = [lat, lon];
            map.setView(center, 14);

            //地理院地図の標準地図タイル
            let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
                { attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>" });
            gsi.addTo(map);
        }
    </script>

</head>

<body onload="init()">
    <h2>BODIK API program #2</h2>
    <div class="map" id="map" style="width:800px;height:600px"></div>
</body>

</html>

地図にマーカーを立てる

地図の指定した場所にマーカーを立てる方法を調べます。

// lat, lonで指定された場所にマーカーを作成する
let marker = L.marker([lat, lon]).addTo(map);

マーカーをクリックしたときにポップアップを表示したい。

let text = 'ポップアップに表示する文字列';
let popup = L.popup().setContent(text);

let marker = L.marker([lat, lon]).addTo(map);
marker.bindPopup(popup);

BODIK APIと組み合わせる

BODIK APIの検索結果から位置情報を取り出し、地図にマーカーを立ててみましょう。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>BODIK API</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
        integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
        crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
        integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
        crossorigin=""></script>

    <script type="text/javascript">
        let map = null;
        function init() {
            map = L.map('map', { zoomControl: false });
            //  指定された緯度経度を中心とした地図
            let lat = 33.59334325082392;
            let lon = 130.35598920962553;
            let center = [lat, lon];
            map.setView(center, 14);

            //地理院地図の標準地図タイル
            let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
                { attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>" });
            gsi.addTo(map);

            let api_server = 'https://wapi.bodik.jp';
            let api = 'aed';
            let distance = 2000;
            let api_url = `${api_server}/${api}?select_type=geometry&lat=${lat}&lon=${lon}&distance=${distance}`;
            fetch(api_url)
            .then(response => response.json())
            .then(data => {
                let features = data['resultsets']['features'];
                for (let feature of features) {
                    let properties = feature['properties'];
                    let popup = L.popup().setContent(properties['name']);

                    let geometry = feature['geometry'];
                    let location = geometry['coordinates'];                       // 位置情報(経度、緯度)を取得
                    let marker = L.marker([location[1], location[0]]).addTo(map); // 緯度、経度に並べ替え
                    marker.bindPopup(popup);
                }
            });
        }
    </script>

</head>

<body onload="init()">
    <h2>BODIK API program #2</h2>
    <div class="map" id="map" style="width:800px;height:600px"></div>
</body>

</html>

マウスクリック処理

地図をクリックされた場所で検索できるようにしてみましょう。
まずは、マーカーを立てる部分を関数にします。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>BODIK API</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
        integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
        crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
        integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
        crossorigin=""></script>

    <script type="text/javascript">
        let map = null;
        function init() {
            map = L.map('map', { zoomControl: false });
            //  指定された緯度経度を中心とした地図
            let lat = 33.59334325082392;
            let lon = 130.35598920962553;
            let center = [lat, lon];
            map.setView(center, 14);

            //地理院地図の標準地図タイル
            let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
                { attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>" });
            gsi.addTo(map);

            showMarker(lat, lon);
        }

        function showMarker(lat, lon) {
            let api_server = 'https://wapi.bodik.jp';
            let api = 'aed';
            let distance = 2000;
            let api_url = `${api_server}/${api}?select_type=geometry&lat=${lat}&lon=${lon}&distance=${distance}`;
            fetch(api_url)
            .then(response => response.json())
            .then(data => {
                let features = data['resultsets']['features'];
                for (let feature of features) {
                    let properties = feature['properties'];
                    let popup = L.popup().setContent(properties['name']);

                    let geometry = feature['geometry'];
                    let location = geometry['coordinates'];
                    let marker = L.marker([location[1], location[0]]).addTo(map);
                    marker.bindPopup(popup);
                }
            });
        } 
    </script>

</head>

<body onload="init()">
    <h2>BODIK API program #2</h2>
    <div class="map" id="map" style="width:800px;height:600px"></div>
</body>

</html>

次に、マウスのクリックハンドラを組み込みます。
任意の場所がクリックされるので、検索範囲を広げましょう。
  distance = 20000, maxResults = 100に変更する

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>BODIK API</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
        integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
        crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
        integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
        crossorigin=""></script>

    <script type="text/javascript">
        let map = null;
        function init() {
            map = L.map('map', { zoomControl: false });
            //  指定された緯度経度を中心とした地図
            let lat = 33.59334325082392;
            let lon = 130.35598920962553;
            let center = [lat, lon];
            map.setView(center, 14);

            //地理院地図の標準地図タイル
            let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
                { attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>" });
            gsi.addTo(map);

            //showMarker(lat, lon);
            // マウスのクリックハンドラをセットする
            map.on('click', onMapClick);
        }

        // マウスのクリックハンドラ
        function onMapClick(e) {
            let latlng = e.latlng;    // マウスがクリックされた位置情報を取得
            let lat = latlng.lat;     // 緯度
            let lon = latlng.lng;     // 経度
            showMarker(lat, lon);     // マーカーを立てる
        }

        function showMarker(lat, lon) {
            let api_server = 'https://wapi.bodik.jp';
            let api = 'aed';
            let distance = 20000;
            let maxResults = 100;
            let api_url = `${api_server}/${api}?select_type=geometry&lat=${lat}&lon=${lon}&distance=${distance}&maxResults=${maxResults}`;
            fetch(api_url)
            .then(response => response.json())
            .then(data => {
                let features = data['resultsets']['features'];
                for (let feature of features) {
                    let properties = feature['properties'];
                    let popup = L.popup().setContent(properties['name']);

                    let geometry = feature['geometry'];
                    let location = geometry['coordinates'];
                    let marker = L.marker([location[1], location[0]]).addTo(map);
                    marker.bindPopup(popup);
                }
            });
        } 
    </script>

</head>

<body onload="init()">
    <h2>BODIK API program #2</h2>
    <div class="map" id="map" style="width:800px;height:600px"></div>
</body>

</html>

なんとなく、アプリっぽくなりましたね。Leafletの地図はマウスを使って、(ホイールで)拡大縮小や(ドラッグで)スクロールすることができます。いろいろなところに場所を移動させて試してみてください。

この状態ですと、地図をクリックするたびに、マーカーが追加されてしまうという欠点があります。この対策としては、

  • マーカーを作成したときに記録しておき、
  • 次にクリックされたとき、前回表示したマーカーを削除する

という対応が必要です。応用編として挑戦してみてください。

プログラム(完成版)

エラー処理を入れて完成です。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>BODIK API</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
        integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
        crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
        integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
        crossorigin=""></script>

    <script type="text/javascript">
        let map = null;
        function init() {
            try {
                map = L.map('map', { zoomControl: false });
                //  指定された緯度経度を中心とした地図
                let lat = 33.59334325082392;
                let lon = 130.35598920962553;
                let center = [lat, lon];
                map.setView(center, 14);

                //地理院地図の標準地図タイル
                let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
                    { attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>" });
                gsi.addTo(map);

                showMarker(lat, lon);
                map.on('click', onMapClick);
            } catch(error) {
                alert('init:' + error);
            }
        }

        function onMapClick(e) {
            try {
                let latlng = e.latlng;
                let lat = latlng.lat;
                let lon = latlng.lng;
                showMarker(lat, lon);
            } catch(error) {
                alert('onMapClick:' + error);
            }
        }

        function showMarker(lat, lon) {
            try {
                let api_server = 'https://wapi.bodik.jp';
                let api = 'aed';
                let distance = 20000;
                let maxResults = 100;
                let api_url = `${api_server}/${api}?select_type=geometry&lat=${lat}&lon=${lon}&distance=${distance}&maxResults=${maxResults}`;
                fetch(api_url)
                .then(response => response.json())
                .then(data => {
                    if ('resultsets' in data) {
                        let resultsets = data['resultsets'];
                        if ('features' in resultsets) {
                            let features = resultsets['features'];
                            for (let feature of features) {
                                let properties = feature['properties'];
                                let popup = L.popup().setContent(properties['name']);

                                let geometry = feature['geometry'];
                                let location = geometry['coordinates'];
                                let marker = L.marker([location[1], location[0]]).addTo(map);
                                marker.bindPopup(popup);
                            }
                        } else {
                            alert('no "features" in data');
                        }
                    } else {
                        alert('no "resultsets" in data');
                    }
                });
            } catch(error) {
                alert('showMarker:' + error);
            }
        } 
    </script>

</head>

<body onload="init()">
    <h2>BODIK API program #2</h2>
    <div class="map" id="map" style="width:800px;height:600px"></div>
</body>

</html>

お疲れ様でした。