6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LiveViewとGoogleMapAPIで作る GPS Logger 1

Last updated at Posted at 2021-06-26

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

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

LiveViewとGoogleMapAPIで作る GPS Logger 1
LiveViewとGoogleMapAPIで作る GPS Logger 2
LiveViewとGoogleMapAPIで作る GPS Logger 3
LiveViewとGoogleMapAPIで作る GPS Logger short ver
LiveViewとGoogleMapAPIで作る GPS Logger Client watchOS APP + SwiftUI

本記事で作成するアプリの構成

  • ユーザー認証
  • 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を追加します(その2で使用)

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

次は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

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?