【Rails】HERE Maps APIで地図機能を作ってみる

# 概要
今回、無料でMap機能が使えるかつGoogleMapのAPIよりすごい！という話を
[以下の記事](https://qiita.com/Akira-Isegawa/items/a31be5893e362c6f10c3)で見かけたので、Railsで実装してみました！
投稿機能を実装していれば実装できる機能なので、ぜひ遊んでみてください〜！

https://qiita.com/Akira-Isegawa/items/a31be5893e362c6f10c3

:::note warn
投稿機能の実装が必要です
:::

# アカウント登録とプロジェクトの作成
以下の記事にある、`Developerの登録`を参考に進めましょう！

https://qiita.com/Akira-Isegawa/items/a31be5893e362c6f10c3

上記の記事のようにプロジェクトの作成が終わったら
アプリのディレクトリ直下（Gemfileと同じ階層）に`.env`という名前のファイルを作成し
APIキーを`.env`に入れます。

```:.env
HERE_MAP_API_KEY=取得したAPIキー
```

# Gemfileの変更
まず、Gemfileで`geocoder`というgemを追加します。
`geocoder`は地名で緯度・経度を求めてくれるGemで
後々地名を入れるだけで自動でマップを表示できるようにするために使います！

`dotenv-rails`はRailsで環境変数（セキュリティ的に見せてはいけないもの）を入れる
`.env`ファイルを扱えるようにするものです！
```diff_ruby:Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.0.4'

# ~~~長いので省略~~~
gem 'geocoder'
gem 'dotenv-rails'
```

編集後、以下のコマンドを実行しましょう！
```zsh:ターミナル
bundle install
```

:::note warn
gemのgeocoderと相性が悪くいため、公式ガイドにある方のgeocoderの方がいいかもしれません
:::

# Migrationファイルの変更
続いて、住所、緯度、経度のカラムを追加します！

:::note warn
マイグレーションファイルの編集は`rails db:rollback`で
**該当のマイグレーションファイルをDownにしてから**行いましょう。
:::

```diff_ruby:db/migrate/~~~_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.1]
  def change
    create_table :posts do |t|
      t.string :body
+     t.string :address
+     t.float :latitude
+     t.float :longitude
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

```

編集後、以下のコマンドでDBに反映させましょう！
```zsh:ターミナル
rails db:migrate
```
# Modelの変更
Postモデルにて、geocode（地名から緯度・経度を求める機能）を有効にする
カラム名の指定とバリデーションをつけておきましょう。

```diff_ruby:models/post.rb
class Post < ApplicationRecord
+  geocoded_by :address
+  after_validation :geocode
end
```

# Controllerの変更
コントローラーでは、主にストロングパラメータの変更のみで
新たに追加した`address`（地名）のカラムを受け取れるようにします。
```diff_ruby:controllers/posts_controller.rb
class PostsController < ApplicationController
# 変更がないため省略~~~
  private

    def set_q
      @q = Post.ransack(params[:q])
    end

    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def post_params
+      params.require(:post).permit(:body, :address)
    end
end
```

# Viewの変更
## 新規投稿ページの編集
新規投稿を行うページでは地名を受け取れるように変更します！
```diff_erb:posts/new.html.erb
<%= form_with(model: post) do |form| %>
  <div class="field">
    <%= form.label :body, "文章" %>
    <%= form.text_field :body %>
  </div>

+  <div class="field">
+    <%= form.label :address, "場所" %>
+    <%= form.text_field :address %>
+  </div>

  <div class="actions">
    <%= form.submit "投稿する" %>
  </div>
<% end %>

```

## Mapを表示するページの編集
今回は、Show（詳細）ページで表示してみようと思います！

```erb:posts/show.html.erb
<p>
  <strong>Body:</strong>
  <%= @post.body %>
</p>

<% if current_user.id == @post.user_id %>
  <%= link_to 'Edit', edit_post_path(@post) %> |
<% end %>
<%= link_to 'Back', posts_path %>

<div style="width: 50vw; height: 50vh" id="map"></div>

<script>
var platform = new H.service.Platform({
  'apikey': '<%= ENV['HERE_MAP_API_KEY'] %>'
});
// configure an OMV service to use the `core` enpoint
var omvService = platform.getOMVService({path:  'v2/vectortiles/core/mc'});
var baseUrl = 'https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/';

// create a Japan specific style
var style = new H.map.Style(`${baseUrl}normal.day.yaml`, baseUrl);

// instantiate provider and layer for the basemap
var omvProvider = new H.service.omv.Provider(omvService, style);
var omvlayer = new H.map.layer.TileLayer(omvProvider);

var position = {lat: <%= @post.latitude %>, lng: <%= @post.longitude %>}

// instantiate (and display) a map:
var map = new H.Map(
    document.getElementById('map'),
    omvlayer,
    {
      zoom: 17,
      center: position
    });

map.addObject(new H.map.Marker(position));

// make map interactive
window.addEventListener('resize', () => map.getViewPort().resize());
var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
</script>
```

### 解説

#### 初期化
```javascript
var platform = new H.service.Platform({
  'apikey': '<%= ENV['HERE_MAP_API_KEY'] %>'
});
// configure an OMV service to use the `core` enpoint
var omvService = platform.getOMVService({path:  'v2/vectortiles/core/mc'});
var baseUrl = 'https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/';

// create a Japan specific style
var style = new H.map.Style(`${baseUrl}normal.day.yaml`, baseUrl);

// instantiate provider and layer for the basemap
var omvProvider = new H.service.omv.Provider(omvService, style);
var omvlayer = new H.map.layer.TileLayer(omvProvider);
```
ここではAPIを利用する際に必要な初期化などを行なっています。
`'apikey': '<%= ENV['HERE_MAP_API_KEY'] %>'`は
`.env`ファイルに記述したAPIキーを取得します。
その他はテンプレートのようなものなので、あまり気にしなくて良いです！


#### マップの作成部分
```javascript
var position = {lat: <%= @post.latitude %>, lng: <%= @post.longitude %>}

// instantiate (and display) a map:
var map = new H.Map(
    document.getElementById('map'),
    omvlayer,
    {
      zoom: 17,
      center: position
    });

map.addObject(new H.map.Marker(position));
```
`var position = {lat: <%= @post.latitude %>, lng: <%= @post.longitude %>}
`では、Geocoderで計算された緯度・経度を入れています。
JavaScriptのコードにRails（DB）の情報を入れたいときは、このような記述をします。

`{ zoom: 17, center: position }`はマップを表示する際のオプションです。
`zoom`ではどの縮尺で表示するか、`center`はマップの中心位置の指定です。

`map.addObject(new H.map.Marker(position));`では
マップにマーカーを表示する際に使います。
マーカーがいらない方は消していただいても構いません〜！

#### 実行部分

```javascript
window.addEventListener('resize', () => map.getViewPort().resize());
var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
```

このコードは、今まで作成した設定などを実行する部分です。
`addEventListner`が気になる方はMDN↓で勉強してみてください〜

https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener


# 実装画面
以下の画像のように表示されます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017749/257d6a8f-071f-1d84-3e51-7b49624fd5cc.png)

東京駅のバージョン
若干ズレてますね、、、😇
やはり、公式が用意しているgeocoderの方がいいかもしれないです🤔
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017749/0fe39c64-d6fd-3b3d-f270-3a893f89b379.png)

# 表示する場所の精度を上げる方法

HERE Maps APIには`AutoSuggest`という、スペルミスなどに対応したgeocode機能があります。
そちらを利用します！！

## コード
```javascript
var platform = new H.service.Platform({
  'apikey': '<%= ENV['HERE_MAP_API_KEY'] %>'
});
// configure an OMV service to use the `core` enpoint
var omvService = platform.getOMVService({path:  'v2/vectortiles/core/mc'});
var baseUrl = 'https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/';

// create a Japan specific style
var style = new H.map.Style(`${baseUrl}normal.day.yaml`, baseUrl);

// instantiate provider and layer for the basemap
var omvProvider = new H.service.omv.Provider(omvService, style);
var omvlayer = new H.map.layer.TileLayer(omvProvider);
var position = {lat: <%= @post.latitude %>, lng: <%= @post.longitude %>}

// Get an instance of the geocoding service:
var service = platform.getSearchService();

// Call the "autosuggest" method with the search parameters,
// the callback and an error callback function (called if a
// communication error occurs):
service.autosuggest({
  // Search query
  q: '<%= @post.address %>',
  // Center of the search context
  at: '<%= @post.latitude %>,<%= @post.longitude %>'
}, (result) => {
  let center = result.items[0].position
  // instantiate (and display) a map:
  var map = new H.Map(
      document.getElementById('map'),
      omvlayer,
      {
        zoom: 17,
        center: center ?? position
      });
  map.addObject(new H.map.Marker(center))

  // make map interactive
  window.addEventListener('resize', () => map.getViewPort().resize());
  var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
}, (error) => {
  console.error(error)
});
```
## 解説

### 引数
`autosuggest`は、引数に地名と緯度・経度を入れることで
最も近い候補を5つ提供してくれます。
```javascript
service.autosuggest({
  // Search query
  q: '<%= @post.address %>',
  // Center of the search context
  at: '<%= @post.latitude %>,<%= @post.longitude %>'
}, 
```

### 動作
動作としては、成功した時に1つ目の関数を
失敗した時に2つ目の関数を実行します。
```javascript
 service.autosuggest({
  // Search query
  q: '<%= @post.address %>',
  // Center of the search context
  at: '<%= @post.latitude %>,<%= @post.longitude %>'
}, 
// 成功時に動作
(res) => {},
// 失敗時に動作
(err) => {})
```

## 実装画面
これで実装すると、、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017749/d99153ca-49fc-de01-156d-d78ad8810fd6.png)

おおお！東京駅のど真ん中にマーカーが刺さりました！
成功です！
こんな感じでautosuggestを利用すると、表示の精度が上がるようです✨

# 終わりに
本記事はいかがでしたでしょうか？
GoogleのMapsAPIよりも簡単かつ無料で地図の表示ができたのではないかな、と思います！
HEREのMaps APIはもっとカスタマイズできますし、経路の計算もできるので
ぜひ遊んでみてください〜〜🙌

# 参考文献

https://jp.developer.here.com/documentation/maps/3.1.34.1/dev_guide/index.html

https://qiita.com/Akira-Isegawa/items/a31be5893e362c6f10c3

https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener