Leafletを使って地図に多角形を描き、人口データの属性を持つ町丁目ポリゴンを取得し、表示する

## 概要説明
Leafletの描画ツールを使ってフリーハンドで多角形を描きサーバーと連携、多角形内の町丁目ポリゴンを取得・表示し、町丁目ポリゴンが持つ人口データを出力するようにします。

## 使用したもの

### Leaflet
比較的軽量なオープンソースのJavaScript地図ライブラリです。

https://leafletjs.com/

### 町丁目ポリゴン、令和2年国勢調査結果/人口総数
今回使用する町丁目ポリゴンおよび令和2年国勢調査結果/人口総数数データは、TerraMap API からレスポンスされたGeoJSON形式のデータです。

https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_leaflet_freehand_polygon_f7e3f9df20e004d12f71_01

### その他
JavaScript
PHP


## 完成イメージ
全体の完成イメージは、以下の動画のようになります。今回は TerraMap API から町丁目ポリゴンと人口データを取得する際に、サーバーへのリクエストを行います。

**多角形の描画**　→　**サーバーへのリクエスト**　
　→　**TerraMap APIからポリゴンや人口を取得・表示**　→　**クリックイベントでの住所・人口出力**
 
![Animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916977/e609e106-26db-6700-225f-b6361ccdc1e9.gif)

## 地図の表示
まずは地図だけを表示させてみます。
[LeafletのQuick Start Guide](https://leafletjs.com/examples/quick-start/)のサンプルを元にindex.htmlとmain.js作成して以下のように編集します。
```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <style>
      body { margin: 0; }
      #map { height: 100vh; }
    </style>
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
      integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
      crossorigin=""
    />
    <script
      src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
      integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
      crossorigin=""
    ></script>
  </head>
  <body>
    <div id="map"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>
```
:::note
Leafletのバージョンを最新の1.9.4にすると後ほど利用するLeaflet Draw APIでエラーが発生したのでバージョンは1.9.3にしています。
:::
```javascript:main.js
const latLng = [35.69804, 139.91581];
const map = L.map('map');
map.setView(latLng, 15);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
```
### 表示結果
地図が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916977/75061be2-3024-2f1f-7728-d77647a7c2a2.png)

## 描画ツールの表示と多角形の描画
地図に多角形を描画するためには、[Leaflet Draw API](https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html)を利用する必要があるのでindex.htmlに以下のコードを追加します。

```html:index.html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
```
JavaScriptの方でもLeaflet Draw APIの設定を行います。
```javaScript:main.js
const latLng = [35.69804, 139.91581];
const map = L.map('map');
map.setView(latLng, 15);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);

const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
  draw: {
    polyline: false,
    circle: false,
    rectangle: false,
    marker: false,
    circlemarker: false,
    polygon: {
      shapeOptions: {
        color: "#FFD700", 
        fill: false,
      }
    },
  },  
  edit: {
    featureGroup: drawnItems
  }
});
map.addControl(drawControl);

map.on(L.Draw.Event.CREATED, function (e) {
  drawnItems.addLayer(e.layer);
});
```
### 表示結果
画面左側に描画ツールが表示され、フリーハンドで多角形を描くことができるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916977/0f860ce6-4351-3d2a-a02d-76c40e479b72.png)

## サーバーへのリクエスト、TerraMap APIからポリゴンや人口を取得・表示
次に、多角形の描画を終えたときに、多角形の座標情報をサーバーへPOSTします。レスポンスされた町丁目ポリゴンのGeoJSONは、指定されたスタイルで地図に表示させます。
map.onイベント内のコードを以下のように変更しました。
```javaScript:main.js
map.on(L.Draw.Event.CREATED, function (e) {
  drawnItems.addLayer(e.layer);
  const latLngs = e.layer.getLatLngs()[0];
  const polygon = latLngs.map(point => [point.lng, point.lat]);
  polygon.push([latLngs[0].lng, latLngs[0].lat]);
  const params = {
    // 渡したいパラメータをJSON形式で書く
    polygon,
  };
  // レスポンスされた町丁目ポリゴンのスタイル設定
  const polygonStyleOptions = {
    color: "#000080",
    fillColor: "#000080",
    fillOpacity: 0.5,
  };
  // Leaflet Drawで描いた多角形(zIndex:400)の下に配置するために多角形より小さいzIndexを設定する
  map.createPane("pane").style.zIndex = 399;
  // サーバーサイドプログラムからTerraMap APIへのリクエスト例はページ下部に説明があります
  fetch(`https://YOUR_SERVER_DOMAIN`, {
    method: "POST",
    body: JSON.stringify(params),
  })
    .then((response) => response.json())
    .then((data) => {
      L.geoJSON(data, {
        style: polygonStyleOptions,
        pane: "pane"
      }).addTo(map);
    })
    .catch((error) => {
      console.log(error);
    });
});
```
レスポンスされる町丁目ポリゴン（一部抜粋）
```response.geojson
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "area": {
                    "area": 0.17486035487320273
                },
                "data": [
                    {
                        "is_authorized": true,
                        "stat_item_id": 15776,
                        "stat_id": "001012000",
                        "value": "3574"
                    }
                ],
                "point_coordinates": [
                    139.89703,
                    35.69547
                ],
                "geocode": "13123001402",
                "points": [
                    [
                        "東京都",
                        "江戸川区",
                        "瑞江２丁目"
                    ]
                ]
            },
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [
                        [
                            [
                                139.899335335,
                                35.697865938
                            ],
                            [
                                139.89942751,
                                35.69769306
                            ],
                            [
                                139.899592679,
                                35.697450423
                            ],
```
## クリックイベントでの住所・人口出力
町丁目ポリゴンのクリックイベントを利用してクリックした地点の住所と人口を出力するようにします。
fetchメソッド処理を以下のように変更します。
```javaScript:main.js
fetch(`https://YOUR_SERVER_DOMAIN`, {
    method: "POST",
    body: JSON.stringify(params),
  })
    .then((response) => response.json())
    .then((data) => {
      L.geoJSON(data, {
        style: polygonStyleOptions,
        pane: "pane",
        onEachFeature: (feature, layer) => {
          // クリックイベントを設定
          layer.on('click', (event) => {
            const address = feature.properties.points[0].join(""); // 住所
            const value = feature.properties.data[0].value; // 人口データ
            const html = `住所：${address}<br>人口：${value}`;
            // ポップアップの作成と表示
            L.popup()
            .setLatLng(event.latlng)
            .setContent(html)
            .openOn(map);
          });
        }
      }).addTo(map);
    })
    .catch((error) => {
      console.log(error);
    });
```
## サーバーサイド (PHP)
PHPでの実装例です。
サーバーサイドでは、フロントエンドから多角形の座標情報を受け取り、TerraMap APIにリクエストします。TerraMap APIからレスポンスされた住所と国勢調査2020の人口総数の属性を持つ町丁目ポリゴンを返すようにしています。
```index.php
<?php

header('Content-Type: application/json');

// URLとAPIキーはダミーです
$url = 'https://tmapi.example.jp/api/area';
$apiKey = 'YOUR_TERRAMAP_API_KEY';

$json = file_get_contents("php://input");
$params = json_decode($json, true);

// パラメータをJSON形式でセット
$requestBody  = json_encode([
    "layer_id" => "00104",          // レイヤーは町丁目を指定
    "area_type" => "polygon",       // エリアタイプはポリゴンを指定
    "polygons" => array(
        "type" => "FeatureCollection",
        "features" => [
            array(
                "type" => "Feature",
                "geometry" => array(
                    "type" => "Polygon",
                    "coordinates" => [
                        $params['polygon'],     // パラメータの座標情報をセット
                    ],
                ),
            ),
        ]
    ),
    "output" => "polygon,point,data",       // ポリゴン、住所、データを出力する
    "stats" => [
        array(
            "stat_id" => "001012000",       // 国勢調査2020を指定
            "stat_item_id" => [15776]       // 人口総数20を指定
        )
    ],
]);

$ch = curl_init();
// Content-TypeとAPIキーをセット
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'X-API-KEY: ' . $apiKey]);
// POSTメソッドを指定
curl_setopt($ch, CURLOPT_POST, true);
// パラメータをセット
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);
// URLをセット
curl_setopt($ch, CURLOPT_URL, $url);

// レスポンスをブラウザに渡す
curl_exec($ch);

curl_close($ch);
```