LiveViewとGoogleMapAPIで作る GPS Logger  1

本記事ではPhoenixのLiveViewでGoogleMapAPIを使用してGPS Loggerを作成していきます

今回はプロジェクト作成からGoogleMapの表示までを行います

[LiveViewとGoogleMapAPIで作る GPS Logger 1](https://qiita.com/the_haigo/items/c1eaf7fde3f88eadb4be)
[LiveViewとGoogleMapAPIで作る GPS Logger 2](https://qiita.com/the_haigo/items/addc4e0e5c4ad398ca5c)
[LiveViewとGoogleMapAPIで作る GPS Logger 3](https://qiita.com/the_haigo/items/bae2203ffdb7f6963f72)
[LiveViewとGoogleMapAPIで作る GPS Logger short ver](https://qiita.com/the_haigo/items/9cb8d6446492c50d690c)
[LiveViewとGoogleMapAPIで作る GPS Logger Client watchOS APP + SwiftUI](https://qiita.com/the_haigo/items/6a8ddf84434cfa47a129)

# 本記事で作成するアプリの構成
- ユーザー認証
- LiveViewでGoogleMapを表示する
- 登録された座標があった場合はマーカーを描画する
- 座標を登録するAPIを作成
- 座標が登録された際にLiveViewで表示しているGoogleMapをリロードすることなくマーカーを描画する
- 座標を登録するAPIはJWTで認証し、IoT機器、スマートウォッチやスマートフォンのアプリからGPSの情報を受け取ることができる

# プロジェクト作成

```
mix phx.new live_map --live
cd live_map
mix ecto.create
```
webページ側の認証にphx_gen_auth,
API側の認証にguardianを追加します(その２で使用)

```mix.exs
defmodule LiveMap.MixProject do
...
  defp deps do
    [
      {:phoenix, "~> 1.5.9"},
      {:phoenix_ecto, "~> 4.1"},
      {:ecto_sql, "~> 3.4"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_live_view, "~> 0.15.1"},
      {:floki, ">= 0.30.0", only: :test},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_dashboard, "~> 0.4"},
      {:telemetry_metrics, "~> 0.4"},
      {:telemetry_poller, "~> 0.4"},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false}, # add this
      {:guardian, "~> 2.0"} # add this
    ]
  end
...
end
```
# Userモデル作成
次に phx_gen_authで認証機能がついたUserモデルを作成します

```
mix do deps.get, deps.compile
mix phx.gen.auth Accounts User users
mix deps.get
```
IoT機器用に認証トークンのフィールドを追加します

```2021xxxxxxxx_create_users_auth_tables.exs
defmodule LiveMap.Repo.Migrations.CreateUsersAuthTables do
  use Ecto.Migration

  def change do
    execute "CREATE EXTENSION IF NOT EXISTS citext", ""

    create table(:users) do
      add :email, :citext, null: false
      add :hashed_password, :string, null: false
      add :confirmed_at, :naive_datetime
      add :token, :string # add this
      timestamps()
    end
    ...
  end
end

```
# MapとPointモデル作成
次にscaffoldingで LiveViewでMapモデルとCRUD画面を作成し、
軽度緯度を保存するPointをモデルだけ作成します

```
mix phx.gen.live Loggers Map maps name:string description:string user_id:references:users
mix phx.gen.schema Loggers.Point points lat:decimal lng:decimal user_id:references:users map_id:references:maps
mix ecto.migrate
```
モデルの作成が完了したので、リレーションを組んでいきます

```lib/live_map/accounts/user.ex
defmodule LiveMap.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  @derive {Inspect, except: [:password]}
  schema "users" do
    field :email, :string
    field :password, :string, virtual: true
    field :hashed_password, :string
    field :confirmed_at, :naive_datetime
    field :token, :string

    has_many :maps, LiveMap.Loggers.Map # add this
    timestamps()
  end
...
end
```
referencesで指定した値は関連先を参照できるようにする場合は belongs_toに書き換え、
参照までは必要なくIDだけ欲しい場合はfieldのままにしておきます。
has_manyのon_delete オプションで削除時にリレーションを組んだモデルも削除するかを指定できます
changesetのcastにuser_id等の外部キーを入れ忘れないようにしましょう

```lib/live_map/loggers/map.ex
defmodule LiveMap.Loggers.Map do
  use Ecto.Schema
  import Ecto.Changeset

  schema "maps" do
    field :description, :string
    field :name, :string

    belongs_to :user, LiveMap.Accounts.User # add this
    has_many :points, LiveMap.Loggers.Point, on_delete: :delete_all # add this

    timestamps()
  end

  @doc false
  def changeset(map, attrs) do
    map
    |> cast(attrs, [:name, :description, :user_id]) # modify this
    |> validate_required([:name, :user_id]) # modify this
  end
end

```

```lib/live_map/loggers/point.ex
defmodule LiveMap.Loggers.Point do
  use Ecto.Schema
  import Ecto.Changeset

  schema "points" do
    field :lat, :decimal
    field :lng, :decimal
    field :user_id, :id

    belongs_to :map, LiveMap.Loggers.Map # add this

    timestamps()
  end

  @doc false
  def changeset(point, attrs) do
    point
    |> cast(attrs, [:lat, :lng, :map_id, :user_id]) # modify this
    |> validate_required([:lat, :lng, :map_id, :user_id]) # modify this
  end
end

```
model側はこれで完了なのでView側に入る前にphx.gen.liveで最後に表示されたrouteを追加します


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

    get "/users/settings", UserSettingsController, :edit
    put "/users/settings", UserSettingsController, :update
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email

    # add below
    live "/maps", MapLive.Index, :index
    live "/maps/new", MapLive.Index, :new
    live "/maps/:id/edit", MapLive.Index, :edit

    live "/maps/:id", MapLive.Show, :show
    live "/maps/:id/show/edit", MapLive.Show, :edit
  end
...
end
```
ログイン時にsessionにuser_idを入れるように変更

```lib/live_map_web/controllers/user_auth.ex
defmodule LiveMapWeb.UserAuth do
...
  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    user_return_to = get_session(conn, :user_return_to)

    conn
    |> renew_session()
    |> put_session(:user_token, token)
    |> put_session(:user_id, user.id) # add this
    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
    |> maybe_write_remember_me_cookie(token, params)
    |> redirect(to: user_return_to || signed_in_path(conn))
  end
...
end
```
mount時にsessionのuser_idをsocketにアサイン
new actionのときにMap Structの初期値にuser_id追加

```lib/live_map_web/live/map_live/index.ex
defmodule LiveMapWeb.MapLive.Index do
  use LiveMapWeb, :live_view

  alias LiveMap.Loggers
  alias LiveMap.Loggers.Map

  @impl true
  def mount(_params, session, socket) do
    {
      :ok,
      socket
      |> assign(:user_id, session["user_id"]) # add this
      |> assign(:maps, list_maps())
    }
  end
...
  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Map")
    |> assign(:map, %Map{ user_id: socket.assigns.user_id }) # modify this
  end
...
end
```
hidden_inputでuser_idを追加

```[lib/live_map_web/live/map_live/form_component.html.leex].html
<h2><%= @title %></h2>

<%= f = form_for @changeset, "#",
  id: "map-form",
  phx_target: @myself,
  phx_change: "validate",
  phx_submit: "save" %>

  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

  <%= label f, :description %>
  <%= text_input f, :description %>
  <%= error_tag f, :description %>

  <%= hidden_input f, :user_id %> # add this
  <%= submit "Save", phx_disable_with: "Saving..." %>
</form>
```

# GoogleMapの表示
ユーザーに紐付いたMapを作れるようになったので次はGoogleMapを表示します
cdnでもいいんですが、初期化の際にうまく表示されないことがあるので、ライブラリの方を使用します

```
cd assets
npm install @googlemaps/js-api-loader
cd ..
```
apiKeyにはご自分で取得したgoogle api keyに置き換えてください
centerは福岡あたり,zoomは九州北部が範囲になるくらいの9に設定しています
mountedはLiveView側でmountされたときに実行されて、handleEventで定義した関数がLiveView側から発火できます

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

Hooks.Map = {
  mounted() {
    this.handleEvent("init_map", () => {
      const loader = new Loader({
        apiKey: "your API key",
        version: "weekly",
      });

      loader.load().then(() => {
        const center = {
          lat: 33.30639,
          lng: 130.41806,
        };

        const map = new google.maps.Map(document.getElementById("map"), {
          center: center,
          zoom: 9,
        });
        window.map = map;
      });
    });
  },
};

export default Hooks;
```
liveSocketのhooksのオプションにわたすことでLiveViewのphx-hookから使用できるようになります

```assets/js/app.js
...
import Hooks from './hooks.js' 
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: Hooks,
  params: {_csrf_token: csrfToken}
})
...
```
google mapはheightを指定する必要があるのでスタイルを当てます

```assets/css/app.scss
...
#map {
  height: 400px;
  border-radius: 2em;
}

```
handleEventで定義した関数は、push_eventで発火できます
push_event(socket,イベント名、ペイロード)
ペイロードは必ず必要で、渡す値がない場合は %{}を入れます

```lib/live_map_web/live/map_live/show.ex
defmodule LiveMapWeb.MapLive.Show do
...
  @impl true
  def handle_params(%{"id" => id}, _, socket) do
    {
      :noreply,
      socket
      |> assign(:page_title, page_title(socket.assigns.live_action))
      |> assign(:map, Loggers.get_map!(id))
      |> push_event("init_map", %{}) # add this
    }
  end
...
end
```
最後にviewにセットして完了です
google mapのようにライブラリが要素内を描画するものはphx-updateをignoreにしておかないと、変更が検知された際に初期化されるので注意しましょう
phx-hookで使用するhookを指定します、
phx-hookを指定する要素はid属性が必要なので忘れないようにしましょう

```[lib/live_map_web/live/map_live/show.html.leex].html
...
<section id="googleMap" phx-update="ignore" phx-hook="Map">
  <div id="map"></div>
</section>

<span><%= live_patch "Edit", to: Routes.map_show_path(@socket, :edit, @map), class: "button" %></span>
<span><%= live_redirect "Back", to: Routes.map_index_path(@socket, :index) %></span>
```

無事GoogleMapが表示されました
![スクリーンショット 2021-06-26 20.34.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1c3c5020-1e2f-e3af-373d-42f4e16e049f.png)

次はGPS Logger部分を実装していきます

コード
https://github.com/thehaigo/live_map/tree/qiita
https://github.com/thehaigo/live_map/tree/init_project_to_render_googlemap

参考ページ
https://medium.com/swlh/how-to-use-google-maps-with-ecto-and-phoenix-liveview-2b81bed570a9
https://github.com/FrancescoZ/live-map-app
https://www.poeticoding.com/phoenix-liveview-javascript-hooks-and-select2/
https://hexdocs.pm/phx_gen_auth/Mix.Tasks.Phx.Gen.Auth.html#content
https://hexdocs.pm/phoenix/mix_tasks.html#phoenix-tasks
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_event/3
https://github.com/googlemaps/js-api-loader