LiveViewでGoogleMapを操作する

# はじめに
この記事は[fukuoka.ex Elixir／Phoenix Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fukuokaex)の13日目の記事です。
12日目は  @the_haigo LiveViewとNxで画像処理の記事でした


LiveViewとJS Hookを使用してGoogleMapでいろいろします

# 環境
macOS Monterey m1
phoenix 1.6.4
erlang 24.1.7
elixir 1.13.0

# setup
DBは使わないので --no-ectoでプロジェクト作成

```
mix phx.new live_map --no-ecto
```

# 空のLiveViewページを作成
hello world的に作ります
LiveViewは最低mount関数で`{:ok, socket}`を返して
render関数か同名のhtml.heexを作成してください

```lib/live_map_web/live/page_live.ex
defmodule LiveMapWeb.PageLive do
  use LiveMapWeb, :live_view

  def mount(_params, _session, socket) do
    {
      :ok,
      socket
    }
  end
end
```

```leex:lib/live_map_web/live/page_live.html.heex
<div>
  LiveMap
</div>
```

ルーティングを追加します
LiveViewの場合は 
live エンドポイント名, LiveViewモジュール名で指定します

```lib/live_map_web/router.ex
lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
...
  scope "/", LiveMapWeb do
    pipe_through :browser

    live "/", PageLive
  end
...
end
```
これでLiveViewページが表示されました
![スクリーンショット 2021-12-14 15.23.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/eb05837f-40da-d3e1-47e4-c0c7b5515b08.png)

# npm ライブラリの追加
phoenix 1.6になってからwebpackからesbuildに移行してnpm非依存になりましたが
npm installすれば普通に使用できます

assetsに移動してGoogleMap loaderを追加します 

```
cd assets
npm install @googlemaps/js-api-loader
```


#JS HookでGoogleMapの表示
LiveViewにはJS Hookというものがあって、
backendとfrontendでデータをやり取りする場合は　APIを通すか、それ用のライブラリをインストールする必要があり、面倒くさいものですが

- JS側のhandleEvent, pushEvent 
- Elixir側のpushEvent

という関数を使って相互に JSONとMapのやり取りができます

mount時にGoogleMapを表示するようにします
試す場合は GoogleMapAPIのキーを取得してください

```assets/js/hooks.js
import { Loader } from "@googlemaps/js-api-loader";
let Hooks = {}
const apiKey = "your api key"
Hooks.Map = {
  mounted() {
    const loader = new Loader({
      apiKey: apiKey,
      version: "weekly",
    })

    loader.load().then(() => {
      const map = new google.maps.Map(
        document.getElementById("map"),
        {
          center: { lat: 33.30639, lng: 130.41806 },
          zoom: 9
        }
      );
      window.map = map;
    })
  }
}

export default Hooks;
```
mount()関数が LiveView のmount後に実行されるので、
初期データの作成他に待ち受けるイベントを記述したりします

hooksファイルができたら,liveSocketのoptionsの箇所に `hooks: Hooks` で追加します

```assets/js/app.js
...
import Hooks from "./hooks"
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
...
```
Hooksを読み込む際の注意点ですが
Hooksを読み込むタグにIDをつけて読み込むモジュールを`phx-hook="Map"`で指定します
また、canvasなどタグ内が頻繁に変化する箇所はLiveViewが変更を検知して再レンダリングされるので、
LiveViewが変更しないように`phx-update="ignore"`を指定します


```leex:lib/live_map_web/live/page_live.html.heex
<div class="row">
  <div class="column column-20">
    <h1>Show Map</h1>
  </div>
  <div class="column column-80">
    <div id="googleMap" phx-update="ignore" phx-hook="Map">
      <div id="map"></div>
    </div>
  </div>
</div>
```
max widthを増やして、
google mapの表示領域の高さを600pxに指定します。
heightを指定しないとmapが表示されないので忘れないようにしましょう

```assets/css/phoenix.css
.container{
  margin: 0 auto;
  max-width: 120.0rem; /* 80 -> 120 */
  padding: 0 2.0rem;
  position: relative;
  width: 100%
}
#map{
  height: 600px
}
```
GoogleMapが表示されました！

![スクリーンショット 2021-12-14 16.07.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/8987c87a-17d0-5208-f8f9-4d284b09196f.png)

# PlaceSearch
Places APIを使って施設や店舗を検索してその結果を表示します

## 検索フォーム
phx-submitでsubmitボタンを押した時に search eventを発火させます


```leex:lib/live_map_web/live/page_live.html.heex
<div class="row">
  <div class="column column-20">
    <!--- 以下追加 --->
    <form phx-submit="search">
      <input  type="text" name="keyword" value={@text}>
      <button type="submit">Search</button>
    </form>
　　　　</div>
  ...
</div>
```
paramsはname属性がそのまま使用されるので keywordで取得できます
検索ワードをassignで保持させて
push_eventでJS Hookのsearchイベントを {keyword: keyword}なJSONを送信しています

```lib/live_map_web/live/page_live.ex
defmodule LiveMapWeb.PageLive do
  use LiveMapWeb, :live_view

  def mount(_params, _session, socket) do
    {
      :ok,
      socket
      |> assign(:text, "")
    }
  end

  def handle_event("search", %{"keyword" => keyword}, socket) do
    {
      :noreply,
      socket
      |> assign(:text, keyword)
      |> push_event("search", %{keyword: keyword})
    }
  end
end
```

## JS Hook Search Event
Google Places API を叩いて結果を必要なものだけにして、
pushEventでElixir側の `search_result` イベントをJSONをMapにして発火させています


```assets/js/hooks.js
import { Loader } from "@googlemaps/js-api-loader";

let Hooks = {}
const apiKey = "your api key"
Hooks.Map = {
  mounted() {
    const loader = new Loader({
      apiKey: apiKey,
      version: "weekly",
      libraries: ["places"] // places apiを追加
    })

    loader.load().then(() => {
      const map = new google.maps.Map(
        document.getElementById("map"),
        {
          center: { lat: 33.30639, lng: 130.41806 },
          zoom: 9
        }
      );
      // serviceを作成してwindow追加
      const service = new google.maps.places.PlacesService(map); 
      window.map = map;
      window.service = service
    })

    this.handleEvent("search", ({keyword}) => {
      const that = this
      const request = { query: keyword };
      window.service.textSearch(request, function(results, status) {
        if (status === google.maps.places.PlacesServiceStatus.OK) {
          let result = results.map((r) => { 
            return {
              name: r.name,
              address: r.formatted_address.split(/\s/).pop(),
              location: r.geometry.location,
              icon: r.icon,
              place_id: r.place_id
            };
          })
          that.pushEvent("search_result" , {result: result});
        }
      });
    })
  }
}

export default Hooks;
```

```lib/live_map_web/live/page_live.ex
defmodule LiveMapWeb.PageLive do
  use LiveMapWeb, :live_view

  def mount(_params, _session, socket) do
    {
      :ok,
      socket
      |> assign(:text, "")
      |> assign(:search_result, [])
    }
  end

  ...
  def handle_event("search_result", %{"result" => result}, socket) do
    {
      :noreply,
      socket
      |> assign(:search_result, result)
    }
  end
end
```
assignをしてstateの変更を検知して検索結果をリスト表示します
heexの変数展開はパラメーター(src)は`{}`それ以外は`<%=%>`で表記します

```leex:lib/live_map_web/live/page_live.html.heex
<div class="row">
  <div class="column column-20">
    ...
    <%= for spot <- @search_result do %>
      <div class="row">
        <img src={spot["icon"]} width="16px" height="16px" />
        <span><%= spot["name"] %></span>
      </div>
      <div class="row">
        <%= spot["address"] %>
      </div>
      <br />
    <% end%>
  </div>
  ...
</div>
```

[![Image from Gyazo](https://i.gyazo.com/cf9c359f3cc0d9902acba60f4d16b80d.gif)](https://gyazo.com/cf9c359f3cc0d9902acba60f4d16b80d)

# Markerの追加
`phx-click`にJS.pushを発火するようにします
前は`phx-value`で値をセットしたりしていましたが、
`JS.push`を使うことでvalueとかをまとめて指定して`handle_event`を発火させます
JS hooksのイベント発火をやってくれそうな字面ですがElixirの`handle_event`が実行されます

```leex:lib/live_map_web/live/page_live.html.heex
<div class="row">
  <div class="column column-20">
    <%= for spot <- @search_result do %>
      <div class="row">
        <img src={spot["icon"]} width="16px" height="16px" />
        <span><%= spot["name"] %></span>
      </div>
      <div class="row">
        <%= spot["address"] %>
      </div>
      <!--- 以下追加 --->
      <div class="row">
        <button
          class="column"
          phx-click={JS.push("add_marker", value: %{location: spot["location"]})}
        >
          add marker
        </button>
      </div>
      <!--- ここまで --->
      <br />
    <% end%>
  </div>
</div>
```
JS.pushを使う場合は`alias Phoenix.LiveView.JS`で短縮しておくといいでしょう
add_markerはそのままJS Hookを発火させます

```lib/live_map_web/live/page_live.ex
defmodule LiveMapWeb.PageLive do
  use LiveMapWeb, :live_view
  alias Phoenix.LiveView.JS

  def handle_event("add_marker", location, socket) do
    {
      :noreply,
      socket
      |> push_event("add_marker", location)
    }
  end
end
```
マーカー作ってmapに追加して、センターを変更します

```assets/js/hooks.js
import { Loader } from "@googlemaps/js-api-loader";

let Hooks = {}
const apiKey = "your api key"
Hooks.Map = {
  mounted() {
    ...
    this.handleEvent("add_marker", ({location}) => {
      let marker = new google.maps.Marker({position: location})
      marker.setMap(window.map)
      window.map.setCenter(location)
    })
  }
}

export default Hooks;
```
コードができたので動作確認をしましょう！

[![Image from Gyazo](https://i.gyazo.com/7a6df2704a993b2ffabf592c81a86651.gif)](https://gyazo.com/7a6df2704a993b2ffabf592c81a86651)

# 最後に
JS Hookを通して、Map API, Place APIを使って簡単にデータの取得してElixirとのやり取りができるのがわかったのではないでしょうか？

Canvasも問題なく使えるのでWebGL2やThreeJSでいろいろできてと楽しそうです
本記事は以上になります

次は @tomoaki-kimura さんの [Railsエンジニアの比較用Phoenixコマンド集](https://qiita.com/tomoaki-kimura/items/7f0f1732c3df0f69155e) になります

# コード
https://github.com/thehaigo/live_map_qiita