Mapbox地図内のクリックした地点で、町丁目ポリゴンと人口データを取得し、表示させてみる

## はじめに
今回は、Mapbox地図内でクリックした任意地点で、町丁目ポリゴンと人口総数データをリクエストし、地図上に表示させてみました。主な技術要素・使用しているものは以下になります。
- Mapbox および Mapbox GL JS
- JavaScript
- PHP
- TerraMap API（区画ポリゴンや統計データ等を提供するWeb API）

※今回使用している人口総数データは、令和2年国勢調査（2020年国勢調査）の結果に基づいております。

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

## 完成イメージ
今回の完成イメージは、以下の動画のようになります。
地図上の任意地点で、商圏分析の基礎データとも言える町丁目ポリゴンと人口総数データをリクエストし、表示させました。また表示したポリゴンをクリックすることで削除できるようにもしてみました。
![CoordinateAreaOnMapbox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2893989/947b8ed2-cf39-5c13-0319-9bf27d4d1b2c.gif)

:::note info
商圏分析においては、範囲を指定して複数ポリゴンを一括で扱うことが多いと思いますが、分析対象の調整等で今回のような個別の処理も必要になるかと思います。
:::

## Mapbox地図の表示、マウスカーソルの変更
実装内容を部分的に説明していきます。
まず初めに、以下コードのようにMapboxの背景地図の表示と、マウスカーソルのpointer:point_up_2:への変更を行います。
```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <title>
      Mapbox地図内のクリックした地点で、町丁目ポリゴンと人口データをリクエストし、表示させてみる
    </title>
    <script src="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js"></script>
    <link
      href="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="map" style="width: 100%; height: 600px"></div>
    <script src="./index.js"></script>
    <style>
      #map canvas {
        cursor: pointer;
      }
    </style>
  </body>
</html>
```

```javascript:index.js
mapboxgl.accessToken = "YOUR_MAPBOX_TOKEN";

const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/streets-v11",
  center: [139.662, 35.6727],
  zoom: 14,
});
```
> コード参考情報
[Mapbox GL JSを使って町丁目ポリゴンを表示してみる](https://qiita.com/Book-Ma/items/2cfc5bc68606bd6e2366)

## 座標取得、リクエスト、ポリゴン表示
次に地図内のクリックイベントとして、座標取得、サーバーサイドプログラムへのリクエスト、レスポンスされたポリゴン（GeoJSON）の表示までを実装させます。
ポリゴンのカウンターを定義し、マップロードおよびクリックイベントを記述していきます。
index.js には以下コードを追記します。
```javascript:index.js
// ポリゴンのカウンター
let polyNum = 1;

map.on("load", () => {
  map.on("click", (e) => {
    // ポリゴン・人口データの取得、表示処理
    const coordinate = [e.lngLat.lng, e.lngLat.lat];

    const requestBody = {
      coordinate,
    };

    // サーバーサイドプログラムからTerraMap APIへのリクエスト例は後述されています
    fetch(`https://YOUR_SERVER_DOMAIN`, {
      method: "POST",
      body: JSON.stringify(requestBody),
    })
      .then((response) => response.json())
      .then((areaData) => {
        if ("features" in areaData === false) {
          return;
        }

        // GeoJSONのソースを追加
        map.addSource(`cho_polygon${polyNum}`, {
          type: "geojson",
          data: areaData,
        });

        // 町丁目ポリゴンの背景色を設定します
        map.addLayer({
          id: `polygon${polyNum}`,
          type: "fill",
          source: `cho_polygon${polyNum}`,
          layout: {},
          paint: {
            "fill-color": "#1c1c1c",
            "fill-opacity": 0.5,
          },
        });

        // 町丁目ポリゴンの枠線を設定します
        map.addLayer({
          id: `outline${polyNum}`,
          type: "line",
          source: `cho_polygon${polyNum}`,
          layout: {},
          paint: {
            "line-color": "#1c1c1c",
            "line-width": 2,
          },
        });

        // ポリゴンのカウンターを加算
        polyNum += 1;
      })
      .catch((error) => {
        console.log(error);
      });
  });
});
```

> コード参考情報
[Mapbox GL JSを使って地図に多角形を描き、多角形内の人口データを持つポリゴンを取得し、表示する](https://qiita.com/yan_x/items/eebdc410deeae2f7ae12)
[Select features around a clicked point | Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/example/queryrenderedfeatures-around-point/)

## 住所と人口データのポップアップ表示
今回、サーバーサイドプログラムからレスポンスされるものは以下のようなGeoJSON（一部抜粋）になります。
```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "area": {
          "area": 0.2656327481707167
        },
        "data": [
          {
            "is_authorized": true,
            "stat_item_id": 15776,
            "stat_id": "001012000",
            "value": "6207"
          }
        ],
        "geocode": "13115000201",
        "points": [
          [
            "東京都",
            "杉並区",
            "和泉１丁目"
          ]
        ]
      },
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          [
            [
              [
                139.657280091426,
                35.6796457830488
              ],
              [
                139.657307304879,
                35.6794224193141
              ],
```
このようなGeoJSONから住所と人口データをポップアップで表示させるには、以下コードをリクエスト後の処理に記述します。
```javascript:index.js
        // 住所と人口総数データをPopupで表示
        const html =
          `住所：${areaData.features[0].properties.points[0]}` +
          "<br>" +
          `人口：${areaData.features[0].properties.data[0].value}`;
        new mapboxgl.Popup().setLngLat(e.lngLat).setHTML(html).addTo(map);
```
> 参考情報
[GEOJSON](https://geojson.org/)

## ポリゴンの削除処理
次にポリゴンをクリックして削除する処理を実装します。
クリックイベントの冒頭に以下処理を加えます。Mapbox GL JS の queryRenderedFeatures と filter を用いることでクリックした地点のポリゴンを特定することができました。
```javascript:index.js
    // 既存ポリゴン・ソースはクリックで削除
    const existingPolygon = map
      .queryRenderedFeatures(e.point)
      .filter((feature) => feature.layer.id.indexOf("polygon") === 0);

    if (existingPolygon.length > 0) {
      // polygon{n}, outline{n}レイヤーの削除
      map.removeLayer(existingPolygon[0].layer.id);
      map.removeLayer(
        existingPolygon[0].layer.id.replace("polygon", "outline")
      );
      // cho_polygon{n}ソースの削除
      map.removeSource(
        existingPolygon[0].layer.id.replace("polygon", "cho_polygon")
      );
      return;
    }
```
> コード参考情報
[queryRenderedFeatures | Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#queryrenderedfeatures)
[removeLayer | Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#removelayer)
[removeSource | Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#removesource)
[Get features under the mouse pointer | Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/example/queryrenderedfeatures)

フロントサイドの実装内容は以上になります。 

<details><summary>最終的な index.js</summary>

```javascript:index.js
mapboxgl.accessToken = "YOUR_MAPBOX_TOKEN";

const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/streets-v11",
  center: [139.662, 35.6727],
  zoom: 14,
});

// ポリゴンのカウンター
let polyNum = 1;

map.on("load", () => {
  map.on("click", (e) => {
    // 既存ポリゴン・ソースはクリックで削除
    const existingPolygon = map
      .queryRenderedFeatures(e.point)
      .filter((feature) => feature.layer.id.indexOf("polygon") === 0);

    if (existingPolygon.length > 0) {
      // polygon{n}, outline{n}レイヤーの削除
      map.removeLayer(existingPolygon[0].layer.id);
      map.removeLayer(
        existingPolygon[0].layer.id.replace("polygon", "outline")
      );
      // cho_polygon{n}ソースの削除
      map.removeSource(
        existingPolygon[0].layer.id.replace("polygon", "cho_polygon")
      );
      return;
    }

    // ポリゴン・人口データの取得、表示処理
    const coordinate = [e.lngLat.lng, e.lngLat.lat];

    const requestBody = {
      coordinate,
    };

    // サーバーサイドプログラムからTerraMap APIへのリクエスト例は後述されています
    fetch(`https://YOUR_SERVER_DOMAIN`, {
      method: "POST",
      body: JSON.stringify(requestBody),
    })
      .then((response) => response.json())
      .then((areaData) => {
        if ("features" in areaData === false) {
          return;
        }

        // GeoJSONのソースを追加
        map.addSource(`cho_polygon${polyNum}`, {
          type: "geojson",
          data: areaData,
        });

        // 町丁目ポリゴンの背景色を設定します
        map.addLayer({
          id: `polygon${polyNum}`,
          type: "fill",
          source: `cho_polygon${polyNum}`,
          layout: {},
          paint: {
            "fill-color": "#1c1c1c",
            "fill-opacity": 0.5,
          },
        });

        // 町丁目ポリゴンの枠線を設定します
        map.addLayer({
          id: `outline${polyNum}`,
          type: "line",
          source: `cho_polygon${polyNum}`,
          layout: {},
          paint: {
            "line-color": "#1c1c1c",
            "line-width": 2,
          },
        });

        // 住所と人口総数データをPopupで表示
        const html =
          `住所：${areaData.features[0].properties.points[0]}` +
          "<br>" +
          `人口：${areaData.features[0].properties.data[0].value}`;
        new mapboxgl.Popup().setLngLat(e.lngLat).setHTML(html).addTo(map);

        // ポリゴンのカウンターを加算
        polyNum += 1;
      })
      .catch((error) => {
        console.log(error);
      });
  });
});
```
</details>

## サーバーサイドプログラム（PHP）
最後は、サーバーサイドプログラムの実装内容になります。
サーバーサイドプログラムは、TerraMap APIのインターフェース仕様に従い作成しました。フロントサイドから座標情報を受け取り、住所と人口総数の属性を持つ町丁目ポリゴンをレスポンスするように作成しました。
```PHP:index.php
<?php
header('Content-Type: application/json');

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

// リクエストパラメータからのINPUT
$json = file_get_contents("php://input");
$requestParams = json_decode($json, true);

// パラメータをJSON形式でセット
$params = json_encode([
    "layer_id" => "00104", // レイヤーは町丁目を指定
    "area_type" => "coordinate", // エリアタイプは座標を指定
    "coordinates"=> array($requestParams["coordinate"]),
    "output" => "polygon,point,data", // ポリゴン、住所、データを出力する
    "stats" => [
        array(
            "stat_id" => "001012000",       // 令和2年国勢調査（2020年国勢調査）を指定
            "stat_item_id" => [15776]       // 人口総数を指定
        )
    ],
]);

$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, $params);
// URLをセット
curl_setopt($ch, CURLOPT_URL, $url);

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

curl_close($ch);
```

## おわりに
TerraMap APIは、エリアマーケティングを行う地図システム開発支援Web APIです。今回は、座標を指定して町丁目ポリゴンと人口総数データを取得してみましたが、円範囲やポリゴン範囲で指定したり、他のポリゴンや統計データを指定することも可能です。ご興味をお持ちでしたら、是非以下ページをご参照下さい。

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