電通総研 テックブログ

電通総研が運営する技術ブログ

オープンソースのJavaScriptライブラリ「Leaflet」で地図表示を試してみる

XI本部スマートソサエティセンターの野網です。データ連携基盤に関連する自治体案件に携わっています。この自治体案件ではデータ連携基盤だけでなく、基盤上で提供するサービスについても検討しています。防災やインフラ管理の観点では、地図上でさまざまなデータを重ね合わせて可視化するGIS(地理情報システム)の実現が重要視されています。

Leaflet

今回は地図機能を提供するオープンソースJavaScriptライブラリである「Leaflet」を触って色々と機能を確認してみました。

  • ベースとなる地図を指定できるか
  • 地図上にピンを立てることができるか(Google Mapsとかでよく見るもの)
  • 地図上に任意の点/線/面を表示できるか
  • 地図上にシェープファイルを表示できるか

ベースマップ&ピンの表示

ベースマップとして、OpenStreetMap1地理院地図2を使用しました。また、駅の位置にピンを表示するレイヤーを作成しました。Control.Layersを利用することで、ベースマップの切り替えやレイヤーの表示/非表示を実現しています。

// ベースマップ
let osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap'
});
let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
    attribution: '© 地理院地図'
});

// ピン
let shinagawa = L.marker([35.6291112, 139.7389313]).bindPopup('品川駅');
let takanawa_gateway = L.marker([35.6355406, 139.7407245]).bindPopup('高輪ゲートウェイ駅');
let stations = L.layerGroup([shinagawa, takanawa_gateway]);

// 地図セットアップ
map = L.map('map', {
    center: [35.62455060680114, 139.7395781036654],
    zoom: 16,
    layers: [osm, stations]
});

let baseMaps = {
    "OpenStreetMap": osm,
    "地理院地図": gsi
};

let overlayMaps = {
    "駅": stations
};

let layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);

点/線/面、シェープファイルの表示

最も基本的な描画機能である点/線/面の表示も確認します。これらはGeoJSON形式のファイルを読み込む形としており、非同期処理になっています。このファイルは手打ちで作成しました。
また、GISでよく出てくるシェープファイルの表示も行いました。シェープファイル国土数値情報で公開されている道路のデータを取得しています。Leafletには様々なプラグインがあり、シェープファイルの表示に対応するものもありましたが、日本語に対応していないという記事があったので、この記事で紹介されているmapshaperを利用してシェープファイルをGeoJSON形式に事前に変換するという方法をとりました。
これらを先ほどのControl.Layersに追加し、期待していた機能を試すことができました。

async function fetchGeoJson(filename) {
    let response = await fetch(filename);
    return await response.json();
}

async function add_layer() {
    let my_geo_json = await fetchGeoJson("my_geojson_data.json");

    // GeoJSONレイヤーを追加
    let my_geo_json_layer = L.geoJSON(my_geo_json, {
        onEachFeature: function (feature, layer) {
            if (feature.properties && feature.properties.name) {
                layer.bindPopup(feature.properties.name);
            }
        },
        style: function (feature) {
            switch (feature.geometry.type) {
                case 'Point': return {color: "#ff0000"};
                case 'LineString': return {color: "#0000ff"};
                case 'Polygon': return {color: "#00ff00"};
            }
        },
        pointToLayer: function (feature, latlng) {
            return L.circleMarker(latlng, {
                radius: 8,
                fillColor: "#ff0000",
                color: "#000",
                weight: 1,
                opacity: 1,
                fillOpacity: 0.8
            });
        }
    }).addTo(map);

    my_geo_json_layer_group = L.layerGroup([my_geo_json_layer]);
    layerControl.addOverlay(my_geo_json_layer_group, "点/線/面")

    // 道路を追加
    let road_feature = await fetchGeoJson("N01-07L-2K-13_Road.json");
    let road_layer = L.geoJSON(road_feature, {
        style: function (feature) {
            return {color: "#ff00ff"};
        }
    }).addTo(map);

    road_layer_group = L.layerGroup([road_layer]);
    layerControl.addOverlay(road_layer_group, "道路")
}
add_layer();

サンプルコード

参考に今回の機能確認で利用したコードの全容を記載します。
index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Leaflet Sample Map</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- Leaflet CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>

    <!-- Leaflet Plugin CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet-switch-basemap@1.0.6/src/L.switchBasemap.css" crossorigin=""/>
    
    <style>
        #map {
            width: 100%;
            height: 600px;
        }
    </style>
</head>
<body>
    <h1>Leaflet Sample Map</h1>
    <div id="map"></div>

    <!-- Leaflet JavaScript -->
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>

    <!-- アプリケーションのJavaScript -->
    <script src="app.js"></script>
</body>
</html>

app.js:

// ベースマップ
let osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; OpenStreetMap'
});
let gsi = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
    attribution: '&copy; 地理院地図'
});

// ピン
let shinagawa = L.marker([35.6291112, 139.7389313]).bindPopup('品川駅');
let takanawa_gateway = L.marker([35.6355406, 139.7407245]).bindPopup('高輪ゲートウェイ駅');
let stations = L.layerGroup([shinagawa, takanawa_gateway]);

// 地図セットアップ
map = L.map('map', {
    center: [35.62455060680114, 139.7395781036654],
    zoom: 16,
    layers: [osm, stations]
});

let baseMaps = {
    "OpenStreetMap": osm,
    "地理院地図": gsi
};

let overlayMaps = {
    "駅": stations
};

let layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);


async function fetchGeoJson(filename) {
    let response = await fetch(filename);
    return await response.json();
}

async function add_layer() {
    let my_geo_json = await fetchGeoJson("my_geojson_data.json");

    // GeoJSONレイヤーを追加
    let my_geo_json_layer = L.geoJSON(my_geo_json, {
        onEachFeature: function (feature, layer) {
            if (feature.properties && feature.properties.name) {
                layer.bindPopup(feature.properties.name);
            }
        },
        style: function (feature) {
            switch (feature.geometry.type) {
                case 'Point': return {color: "#ff0000"};
                case 'LineString': return {color: "#0000ff"};
                case 'Polygon': return {color: "#00ff00"};
            }
        },
        pointToLayer: function (feature, latlng) {
            return L.circleMarker(latlng, {
                radius: 8,
                fillColor: "#ff0000",
                color: "#000",
                weight: 1,
                opacity: 1,
                fillOpacity: 0.8
            });
        }
    }).addTo(map);

    my_geo_json_layer_group = L.layerGroup([my_geo_json_layer]);
    layerControl.addOverlay(my_geo_json_layer_group, "点/線/面")

    // 道路を追加
    let road_feature = await fetchGeoJson("N01-07L-2K-13_Road.json");
    let road_layer = L.geoJSON(road_feature, {
        style: function (feature) {
            return {color: "#ff00ff"};
        }
    }).addTo(map);

    road_layer_group = L.layerGroup([road_layer]);
    layerControl.addOverlay(road_layer_group, "道路")
}
add_layer();

app.jsで参照しているN01-07L-2K-13_Road.jsonは国土数値情報から取得した道路データです。色々なデータで試してみてください。

my_geojson_data.json:

[
    {
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": [139.7395781036654, 35.62455060680114]
        },
        "properties": {
            "name": "Sample Point"
        }
    },
    {
        "type": "Feature",
        "geometry": {
            "type": "LineString",
            "coordinates": [
                [139.73944850383708, 35.62501310494848],
                [139.73909069834616, 35.62487771334422],
                [139.73946289831088, 35.62424755434228]
            ]
        },
        "properties": {
            "name": "Sample Line"
        }
    },
    {
        "type": "Feature",
        "geometry": {
            "type": "Polygon",
            "coordinates": [
                [
                    [139.74004599801373, 35.62494392209728],
                    [139.73961416239794, 35.62473352999296],
                    [139.7398032581179, 35.62440431429395],
                    [139.7401841317666, 35.62449152388262]
                ]
            ]
        },
        "properties": {
            "name": "Sample Polygon"
        }
    }
]

おわりに

シェープファイルをGeoJSON形式に変換が必要となる部分など、詰まることもありましたが、Leafletのような、オープンソースのツールを使うことで、手軽にGISの可能性を試すことができるのは魅力的だと感じました。プラグインも含めて機能が多数あり、サンプルも充実しているので、地図サービスに興味があればチェックしていただく価値があると思います。

執筆:@noami.shunsuke、レビュー:Ishizawa Kento (@kent)
Shodoで執筆されました