HERE Maps API for JavaScript にコンパスアイコンを追加してみた

## はじめに

**HERE Maps API for JavaScript**の特徴の一つとしては、ベクトルタイルを使って地図をレンダリングしますので、ユーザーが地図を任意に回転や傾斜させることができます。(マウスの場合は```Alt```キー/```Option```キーを押しながらドラッグ、スマホやタッチスクリーンの場合は2本指で回転や平行移動)

しかし**HERE Maps API for JavaScript**はデフォルトでは回転後の地図の向きを示すための、いわゆるコンパスアイコンを用意してくれていないので、今回はそのコンパスアイコンに該当するものを追加してみたいと思います。

実現したい機能：
1. 地図の向きを示す
1. クリックされたら地図の向きを北向きに戻す
1. 自動的に表示/非表示 (Optional)

## 動作環境

- OS: macOS Monterey (Version 12.6.2)
- Browser: Google Chrome (Version 108.0.5359.124)
- HERE Maps API for JavaScript (Version 3.1.37.0)

## 事前準備

**HERE Maps API for JavaScript**を使うには、HERE APIKEYが必要になりますが、無料APIKEYの取得しかたはすでにたくさんの方々が詳しく紹介してくださりましたので、ここでは割愛させて頂きます。

https://qiita.com/asahina820/items/5e28cb30a3d0a89438e4

## 日本地図の表示

**HERE Maps API for JavaScript**はデフォルトでは日本の詳細地図を表示してくれないので、下記公式ドキュメントを参考にして、まずベースとなるウェブページを作ります。

https://developer.here.com/documentation/maps/3.1.37.0/dev_guide/topics/get-started-japan.html

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <title>HERE Maps API for JavaScript - Japan</title>
  <meta name="viewport" content="initial-scale=1.0, width=device-width" />
  <script src="https://js.api.here.com/v3/3.1/mapsjs-core.js"></script>
  <script src="https://js.api.here.com/v3/3.1/mapsjs-service.js"></script>
  <script src="https://js.api.here.com/v3/3.1/mapsjs-ui.js"></script>
  <script src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js"></script>
  <link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />
  <style>
    body {
      font-family: FiraGO, "Fira Sans", sans-serif;
      margin: 0;
      padding: 0;
    }
    #map {
      width: 100vw;
      height: 100vh;
    }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    const platform = new H.service.Platform({
      apikey: '{YOUR_HERE_APIKEY}',
    });
    const defaultLayers = platform.createDefaultLayers();
    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.render.webgl.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});
    const map = new H.Map(
      document.getElementById('map'),
      omvLayer,
      {
        center: {lat: 35.68, lng: 139.76},
        zoom: 12,
      }
    );
    const ui = H.ui.UI.createDefault(map, defaultLayers);
    const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
    window.onresize = function () {
      map.getViewPort().resize()
    };
  </script>
</body>
```

これで日本の詳細地図も表示できたし、日本のために特別にデザインされた地図スタイルも適用できました。ついでによく使うズームなどのボタンの追加や、地図のインターラクティブ化もできました。

ただし実はこれだけだとまだ一つ盲点があります。それは右下のレイヤー選択ボタンで一旦他のレイヤーを選んでしまうと、もう元の詳細地図に戻せなくなってしまうことです。
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958554/2938cad8-6c2b-fddc-ca04-1b399ead2540.gif)

ちょっとDirty Hack感がありますが、個人的には以下のように無理やり直しています。

```diff_javascript
...
    const omvLayer = new H.map.layer.TileLayer(omvProvider, {max: 22});
    const map = new H.Map(
      document.getElementById('map'),
      omvLayer,
      {
        center: {lat: 35.68, lng: 139.76},
        zoom: 12,
      }
    );
+   defaultLayers.vector.normal.map = omvLayer;
    const ui = H.ui.UI.createDefault(map, defaultLayers);
    const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
...
```

この一行を足すだけです。つまりユーザーがMap viewをクリックする度に、```ui```は```defaultLayers```を参照して、現在表示レイヤーを```defaultLayers.vector.normal.map```に戻していますが、その```defaultLayers.vector.normal.map```の中身を無理やり我々の```omvLayer```に変えてしまえばとりあえず期待通りの動きになる、ということになりです。

これらを全部まとめると、以下になります。これは個人的によく使っているテンプレートにもなっています。

<p class="codepen" data-height="400" data-default-tab="js,result" data-slug-hash="mdjbxKm" data-editable="true" data-user="kairyu1103" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kairyu1103/pen/mdjbxKm">
  HERE Maps API for JavaScript - Japan</a> by kairyu (<a href="https://codepen.io/kairyu1103">@kairyu1103</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## コンパスアイコンの表示

ここからはやっと本題に入ります、まずはいい感じのアイコンを見つけて来ます。SVGが理想的ですね。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="bGjbPBr" data-editable="true" data-user="kairyu1103" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kairyu1103/pen/bGjbPBr">
  Untitled</a> by kairyu (<a href="https://codepen.io/kairyu1103">@kairyu1103</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

アイコンを表示させる方法が色々ありますが、せっかくなので**HERE Maps API for JavaScript**の```H.ui.Control```機能を使ってネイティブっぽく実装してみたいと思います。

```diff_javascript
...
    window.onresize = function () {
      map.getViewPort().resize()
    };
+   const label = '<svg class="H_icon" viewBox="0 0 1000 1000"><path transform-origin="center" d="M388.56 500L500 834.037 611.299 500 500 165.963zm173.453-1.202l-60.81 231.79-62.51-230.8z"/></svg>';
+   const compassButton = new H.ui.base.Button({label});
+   const compassControl = new H.ui.Control();
+   compassControl.addChild(compassButton);
+   compassControl.setAlignment('top-right');
+   ui.addControl('compass-control', compassControl);
...
```

これで地図ビューの右上にネイティブっぽいボタンを追加出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958554/9b208f96-ddb3-225c-2da6-44ca8dd95d25.png)

ただしそのままだと、アイコンが少し小さすぎる気がするし、形的にもコンパスっぽくないので、少しスタイルを調整します。

```diff_javascript
...
    const compassButton = new H.ui.base.Button({label});
    const compassControl = new H.ui.Control();
+   compassControl.addClass('compass');
    compassControl.addChild(compassButton);
    compassControl.setAlignment('top-right');
    ui.addControl('compass-control', compassControl);
...
```
```diff_html
  <style>
    body {
      font-family: FiraGO, "Fira Sans", sans-serif;
      margin: 0;
      padding: 0;
    }
    #map {
      width: 100vw;
      height: 100vh;
    }
+   .compass .H_btn {
+     border-radius: 50%;
+   }
+   .compass .H_icon {
+     width: auto;
+     height: auto;
+   }
  <style>
```

```border-radius```で四角を円にするというよくある技の応用です。これで見た目はOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958554/a9174e3d-e958-63e5-9fdd-9c8ed87fb04e.png)

## コンパスアイコンを動かせる

次はコンパスアイコンを地図の回転と連動させるために、コードを追加します。

```javascript
    map.addEventListener('mapviewchange', function (evt) {
      if (evt.modifiers & H.map.ChangeEvent.Type.HEADING) {
        const path = document.querySelector('.compass svg path');
        const heading = 180 - evt.newValue.lookAt.heading;
        path.setAttribute('transform', `rotate(${heading})`);
      }
    });
```

**HERE Maps API for JavaScript**は```mapviewchange```というイベントを提供しています。名前の通りに地図のビューに何かしらの変更が加えられた時に発生するイベントで、その都度に予めに渡した関数をコールバックしてくれますが、今回は地図の向きの変更にしか興味がないので、```evt.modifiers & H.map.ChangeEvent.Type.HEADING```でさらに範囲を絞って無駄を減らします。

コンパスアイコンの向きの変更はSVG内の図形のパスそのものに回転を適用させることで実現しています。実際の効果は以下のようになります。
![2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958554/0880bb73-2407-6746-2907-cc642bc482f4.gif)

## 地図向きのリセット

地図の向きが自由に変えられるのは良いものの、デフォルトにリセットしたい時もありますよね。そんな時のために、コンパスアイコンがクリックされたら地図の向きを北向きに戻す機能を実装します。

コンパスアイコンのボタンを作成する時のコードに少し手を加えます。

```diff_javascript
...
-   const compassButton = new H.ui.base.Button({label});
+   const compassButton = new H.ui.base.Button({
+     label,
+     onStateChange: function (evt) {
+       if (evt.target.getState() === H.ui.base.Button.State.UP) {
+         const viewModel = map.getViewModel();
+         const lookAtData = Object.assign(viewModel.getLookAtData(), {
+           heading: 180,
+           incline: 0,
+           tilt: 0,
+         };
+         viewModel.setLookAtData(lookAtData, true)
+       }
+     },
+   });
    const compassControl = new H.ui.Control();
...
```

```label```以外に新たに```onStateChange```を指定することによってクリックイベントをハンドリングします。地図ビューの```setLookAtData```関数を呼び出すことによって任意にビューの視点を変えられますが、この```LookAtData```には向きと傾斜以外に中心やズームレベルなどのデータも含まれますので、変えたくないデータは```Object.assign```でコピーします。

そして```setLookAtData```の2番めの引数に```true```を渡すことによって、視点がいきなり変わるのではなく、自動的にアニメーション付きでゆっくり変化してくれるので、かなりいい感じです。実際の効果は以下のようになります。
![3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958554/01b15c16-e76c-2500-ee3f-f4024f70d760.gif)

## 自動的に表示/非表示

正直ここまでできたらほぼ満足していますが、せっかくですので某地図アプリみたいに、地図がすでに北向きになっている時にコンパスアイコンが自動的に隠れるようにさらに実装してみたいと思います。

```mapviewchange```イベントをハンドリングするコードに少し手を入れます。

```diff_javascript
...
    compassControl.addChild(compassButton);
    compassControl.setAlignment('top-right');
-   ui.addControl('compass-control', compassControl);
...
    map.addEventListener('mapviewchange', function (evt) {
      if (evt.modifiers & H.map.ChangeEvent.Type.HEADING) {
        const path = document.querySelector('.compass svg path');
-       const heading = 180 - evt.newValue.lookAt.heading;
-       path.setAttribute('transform', `rotate(${heading})`);
+       if (path) {
+         const heading = 180 - evt.newValue.lookAt.heading;
+         path.setAttribute('transform', `rotate(${heading})`);
+       }
      }
+
+     if (evt.modifiers & (
+       H.map.ChangeEvent.Type.HEADING |
+       H.map.ChangeEvent.Type.INCLINE |
+       H.map.ChangeEvent.Type.TILT
+     )) {
+       if (evt.newValue.lookAt.heading != 180 ||
+         evt.newValue.lookAt.incline != 0 ||
+         evt.newValue.lookAt.tilt != 0
+       ) {
+         if (!ui.getControl('compass-control')) {
+           ui.addControl('compass-control', compassControl);
+         }
+       }
+       else {
+         if (ui.getControl('compass-control')) {
+           ui.removeControl('compass-control', compassControl);
+         }
+       }
+     }
+   });
...
```

最終的にこんな感じになりました。
![4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958554/9df68c0e-bca4-d719-77fe-de586e843285.gif)

## おわりに

**HERE Maps API for JavaScript**の高いカスタマイズ性と柔軟性のおかげで、無事にコンパスアイコンにまつわる一連の機能をすべて実装できました。

最終のコードをまとめて以下に置いておきました。似たような機能を実現されたい方のご参考になれば幸いです。

<p class="codepen" data-height="400" data-default-tab="js,result" data-slug-hash="xxJxdoQ" data-editable="true" data-user="kairyu1103" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kairyu1103/pen/xxJxdoQ">
  HERE Maps API for JavaScript - Japan</a> by kairyu (<a href="https://codepen.io/kairyu1103">@kairyu1103</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>