HERE Maps API for JavaScript　町丁目ごとにポリゴンの色を変えてみる

HERE Maps API for JavaScriptを使って町丁目ポリゴンを表示して、町丁目ごとに属性の値に応じて色を変えてみます。
地図上でフィーチャーの属性値に応じて段階的に色を変えて表現する方法はコロプレスマップ（階級区分図）とも言われます。

# HERE Maps API for JavaScriptでコロプレスマップを表示するには？
すでにHERE Maps API for JavaScriptでコロプレスマップを表示している素晴らしい記事があります。e-statでダウンロードしたデータをQGISで変換してHERE Maps API for JavaScriptで表示するまで一気通貫で解説されていてとても参考になります。

https://qiita.com/miyauchi/items/8c18eb53c05a0de1453b

こちらの記事を参考にして、 **クリック地点から半径1km円内の町丁目を取得して人口密度に応じて動的に段階分けしながらコロプレスマップを表示** してみます。

HERE Mapsのアカウント登録やAPIキーの取得については以下の記事をご確認ください。

https://qiita.com/kekomat/items/d2e29bac84a27c1ec521


## 属性値に応じてフィーチャーの色を変える方法を整理する
一旦、コロプレスマップを実現する方法を整理します。この部分は読み飛ばしていただいて問題ありません。
調べた感じ、地図に描画するフィーチャーのstyleにfillColorをセットする際に属性値に応じて色や透過度を変更するロジックを組み込むのがよさそうです。
Google Maps JavaScript APIや[Leaflet](https://leafletjs.com/examples/choropleth/)でコロプレスマップを実現するのと同じような感じで行けそうです。

GeoJSONのポリゴンを使用する場合はH.data.geojson.Reader()の中でsetStyle()のfillColorの値をセットするときに属性値に応じた色や透過度を指定するようにします。

## 実現イメージ
![here-choropleth.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325378/0cf42607-4d2d-f4f7-186e-4703c11f5ecf.gif)

常住人口で人口密度を出したので、東京駅周辺は人口密度が低く、薄い赤になっています。（常住人口が0の町丁目もあります）
これを昼間人口で見てみると、常住人口とは全く違う結果になって面白いと思います。

## サンプルコードと解説
以下は地図クリックした地点から半径1km円の町丁目ポリゴンと町丁目の人口をGeoJSONの形式で取得して、人口密度に応じて色分けしてみたサンプルコードです。
クリック地点から半径1km円の町丁目の情報を取得する部分は、バックエンドで[TerraMap API](https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_here_choropleth_b0b68afb7fc5d902227b_01 "地図システム開発支援API TerraMap API")にリクエストしています。
また、雑ではありますが、円内のデータから人口密度の最小値と最大値も求めて、段階分けも動的に行ってみました。

### サンプルコード全文
<details><summary>index.html</summary>

``` index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Here Map API for JavaScript サンプル</title>
    <meta name="viewport" content="initial-scale=1.0, width=device-width" />
    <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-core.js"></script>
    <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-service.js"></script>
    <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js"></script>
    <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-ui.js" ></script>
    <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-data.js"></script>
    <link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />     
  </head>
  <body>
    <div id="mapContainer"></div>
    <script type="module" src="/index.js"></script>
  </body>
</html>
```
</details>


<details><summary>index.js</summary>

``` index.js
import './style.css'

const platform = new H.service.Platform({
  'apikey': 'YOUR-API-KEY'
});

// 日本に対応した地図にする
const omvService = platform.getOMVService({path:  'v2/vectortiles/core/mc'});
const baseUrl = 'https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/';
const style = new H.map.Style(`${baseUrl}normal.day.yaml`, baseUrl);
const omvProvider = new H.service.omv.Provider(omvService, style);
const omvlayer = new H.map.layer.TileLayer(omvProvider, {max: 22});

// mapオブジェクト生成と地図表示
const map = new H.Map(
  document.getElementById('mapContainer'),
  omvlayer,
  {
    zoom: 15,
    center: {lat: 35.68216, lng: 139.76593}
  });

// パンやズームなどの地図イベントを有効にする
const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

// ズームコントロールをセット
const ui = H.ui.UI.createDefault(map, omvlayer);

// click地点から半径1000mの円と共有部分がある町丁目を取得して表示する
map.addEventListener('tap', function (evt) {
  // click地点の座標
  const coord = map.screenToGeo(evt.currentPointer.viewportX, evt.currentPointer.viewportY);
  // 円半径1000mに設定
  const radius = 1000;
  const params = {
    lat: coord.lat,
    lng: coord.lng,
    radius: radius,
  };
  const queryParams = new URLSearchParams(params);

  // 外部APIにリクエストしてGeoJSONフォーマットでレスポンスされる場合を想定
  fetch(`http://127.0.0.1:3300/?${queryParams}`)
  .then((response) => response.json())
  .then((data) => {
      // 色分けの段階を設定するための人口密度の最小値・最大値を取得しておく
      // GeoJSONのproperties.dataオブジェクトに人口があり、properties.areaオブジェクトに面積がある想定（今回使用したGeoJSONのサンプルは下記参照）
      // 人口密度の値の配列
      const densities = data.features.map((obj) => (obj.properties.data.find(item => item.stat_item_id === 15776).value / obj.properties.area.area));
      // 人口密度の最小値
      const minDensity = Math.min.apply(null, densities);
      // 人口密度の最大値
      const maxDensity = Math.max.apply(null, densities);
      
      // GeoJSONのオブジェクトを使用するので、URLを指定せずにH.data.geojson.Readerのインスタンス生成
      const reader = new H.data.geojson.Reader(null, {
        style: function (mapObject) {
          if (mapObject instanceof H.map.Polygon) {
            // フィーチャーの面積
            const area = mapObject.data.area.area;
            // 人口
            const population = mapObject.data.data.find(item => item.stat_item_id === 15776).value;
            // 人口密度
            const density = population / area;
          
            mapObject.setStyle({
              fillColor: getColor(density, minDensity, maxDensity),
              strokeColor: 'rgba(0, 0, 255, 0.2)',
              lineWidth: 3
            });
          }
        },
        disableLegacyMode: true
      });
      
      // reader.parse()ではなくparseData()にGeoJSONのオブジェクトを指定する
      reader.parseData(data);

      // 地図にGeoJSONレイヤーを追加
      map.addLayer(reader.getLayer());

      // 指定した円ラインを描画する
      const circle = new H.map.Circle({lat: coord.lat, lng: coord.lng}, radius);
      circle.setStyle({
        fillColor: 'rgba(0, 0, 0, 0)', // 塗りつぶさない方法がわからなかったので完全に透過させる
        strokeColor: 'rgba(255, 0, 0, 0.5)',
        lineWidth: 3
      });
      map.addObject(circle);
    })
    .catch((error) => {
      console.log(error);
    });
});


function getColor(density, min, max) {
  //最小値・最大値の幅で雑に5段階に分ける
  const step = (max - min) / 5;
  let opacity = 0.0;
  if (density < min + step){
    opacity = 0.1;
  } else if (density < min + step * 2) {
    opacity = 0.3;
  } else if (density < min + step * 3) {
    opacity = 0.5;
  } else if (density < min + step * 4) {
    opacity = 0.7;
  } else {
    opacity = 0.9;
  }

  return `rgba(255, 0, 0, ${opacity})`;
}
```
</details>


少し長いので、いくつか要点を分解してみていきます。

:::note info
クリック地点を取得してAPIにリクエストする部分
``` js
map.addEventListener('tap', function (evt) {
  // click地点の座標
  const coord = map.screenToGeo(evt.currentPointer.viewportX, evt.currentPointer.viewportY);
  // 円半径1000mに設定
  const radius = 1000;
  const params = {
    lat: coord.lat,
    lng: coord.lng,
    radius: radius,
  };
  const queryParams = new URLSearchParams(params);

  // 外部APIにリクエストしてGeoJSONフォーマットでレスポンスされる場合を想定
  fetch(`http://127.0.0.1:3300/?${queryParams}`)
  .then((response) => response.json())
  .then((data) => {
      …
    })
    .catch((error) => {
      console.log(error);
    });
});
```
:::

:::note info
APIから取得したGeoJSONを地図に表示する部分
以下は色分けや円ライン表示などの余計な部分を除いたものです。
URLを指定しないで[H.data.geojson.Reader](https://www.here.com/docs/bundle/maps-api-for-javascript-api-reference/page/H.data.geojson.Reader_11.html)のインスタンスを生成して、[parseData()](https://www.here.com/docs/bundle/maps-api-for-javascript-api-reference/page/H.data.geojson.Reader_11.html#parseData)の引数にGeoJSONのオブジェクトを指定します。
``` js
// 外部APIにリクエストしてGeoJSONフォーマットでレスポンスされる場合を想定
fetch(`http://127.0.0.1:3300/?${queryParams}`)
.then((response) => response.json())
.then((data) => {
    // GeoJSONのオブジェクトを使用するので、URLを指定せずにH.data.geojson.Readerのインスタンス生成
    const reader = new H.data.geojson.Reader(null, {
      style: function (mapObject) {
          mapObject.setStyle({
            fillColor: 'rgba(0, 255, 0, 0.3)',
            strokeColor: 'rgba(0, 0, 255, 0.2)',
            lineWidth: 3
          });
        }
      },
      disableLegacyMode: true
    });
    
    // reader.parse()ではなくparseData()にGeoJSONのオブジェクトを指定する
    reader.parseData(data);

    // 地図にGeoJSONレイヤーを追加
    map.addLayer(reader.getLayer());

  })
  .catch((error) => {
    console.log(error);
  });
```
:::


:::note info
色分け部分
H.data.geojson.Readerの引数のstyleで塗りつぶし色やラインの色をセットします。fillColorにgetColor()を指定しました。
getColor()の中では、人口密度の値に応じて透過具合を調整しています。段階分けに使用する人口密度の最小値・最大値は事前に求めておきます。

``` js
// 色分けの段階を設定するための人口密度の最小値・最大値を取得しておく
// GeoJSONのproperties.dataオブジェクトに人口があり、properties.areaオブジェクトに面積がある想定（今回使用したGeoJSONのサンプルは下記参照）
// 人口密度の値の配列
const densities = data.features.map((obj) => (obj.properties.data.find(item => item.stat_item_id === 15776).value / obj.properties.area.area));
// 人口密度の最小値
const minDensity = Math.min.apply(null, densities);
// 人口密度の最大値
const maxDensity = Math.max.apply(null, densities);

// GeoJSONのオブジェクトを使用するので、URLを指定せずにH.data.geojson.Readerのインスタンス生成
const reader = new H.data.geojson.Reader(null, {
  style: function (mapObject) {
    if (mapObject instanceof H.map.Polygon) {
      // フィーチャーの面積
      const area = mapObject.data.area.area;
      // 人口
      const population = mapObject.data.data.find(item => item.stat_item_id === 15776).value;
      // 人口密度
      const density = population / area;
    
      mapObject.setStyle({
        fillColor: getColor(density, minDensity, maxDensity),
        strokeColor: 'rgba(0, 0, 255, 0.2)',
        lineWidth: 3
      });
    }
  },
  disableLegacyMode: true
});
```
``` js
function getColor(density, min, max) {

  //最小値・最大値の幅で雑に5段階に分ける
  const step = (max - min) / 5;
  let opacity = 0.0;
  if (density < min + step){
    opacity = 0.1;
  } else if (density < min + step * 2) {
    opacity = 0.3;
  } else if (density < min + step * 3) {
    opacity = 0.5;
  } else if (density < min + step * 4) {
    opacity = 0.7;
  } else {
    opacity = 0.9;
  }

  return `rgba(255, 0, 0, ${opacity})`;
}
```
:::


:::note info
GeoJSONのフォーマットについて
面積や人口データを以下にように取得しました。この部分は今回使用したGeoJSONのフォーマットを見ないと分かりづらいと思います。
``` js
// GeoJSONのproperties.dataオブジェクトに人口があり、properties.areaオブジェクトに面積がある想定（今回使用したGeoJSONのサンプルは下記参照）
// 人口密度の値の配列
const densities = data.features.map((obj) => (obj.properties.data.find(item => item.stat_item_id === 15776).value / obj.properties.area.area));


// フィーチャーの面積
const area = mapObject.data.area.area;
// 人口
const population = mapObject.data.data.find(item => item.stat_item_id === 15776).value;
```
今回使用したのは以下のようなフォーマットのGeoJSONです。
``` js
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "area": {
                    "area": 0.3747514990303365,
                    "ratio_area": [
                        0.3747514990303365
                    ],
                    "ratio": [
                        1.0
                    ]
                },
                "data": [
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "10"
                        ],
                        "stat_item_id": 15776,
                        "stat_id": "001012000",
                        "value": "10"
                    }
                ],
                "point_coordinates": [
                    139.76593,
                    35.68216
                ],
                "geocode": "13101000101",
                "points": [
                    [
                        "東京都",
                        "千代田区",
                        "丸の内１丁目"
                    ]
                ]
            },
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [
                        [
                            [
                                139.765734319,
                                35.684679251
                            ],
                            [
                                139.766367166,
                                35.684545208
                            ],
```
:::



## おまけ　その他の参考情報
公式のblog記事にもコロプレスマップを実現しているものがありましたのでリンクを載せておきます。（ソースコードはブラウザのデベロッパーツールなどで見る必要があります）

https://www.here.com/learn/blog/how-we-built-ev-charging-desert-germany

また、HERE Map Widget for Jupyterを使う方法もあるようです。

https://github.com/heremaps/here-map-widget-for-jupyter

Jupyter Notebookはブラウザ上でコードの実行や実行結果の可視化、データの分析などを行うことができるアプリケーションらしいです。（全然知らなかった…のでAIに教えてもらいました）
HERE Map Widget for Jupyterを使うと、Jupyter Notebook上でPythonを使用してHERE Maps API for JavaScriptでインタラクティブな地図の操作や表現ができるようです。
HERE Map Widget for Jupyterでは[Choropleth Layer](https://here-map-widget-for-jupyter.readthedocs.io/en/latest/api_reference/layer/choropleth.html)という機能が用意されていて、これを使ってコロプレスマップを表示することができます。

Jupyter Notebookを使いたいわけではなく、HERE Maps API for JavaScriptで直接コロプレスマップを実現させたいので、今回はこの方法はスキップしました。