本記事では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で使用)
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機器用に認証トークンのフィールドを追加します
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
モデルの作成が完了したので、リレーションを組んでいきます
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等の外部キーを入れ忘れないようにしましょう
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
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を追加します
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を入れるように変更
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追加
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を追加
<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側から発火できます
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から使用できるようになります
...
import Hooks from './hooks.js'
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks,
params: {_csrf_token: csrfToken}
})
...
google mapはheightを指定する必要があるのでスタイルを当てます
...
#map {
height: 400px;
border-radius: 2em;
}
handleEventで定義した関数は、push_eventで発火できます
push_event(socket,イベント名、ペイロード)
ペイロードは必ず必要で、渡す値がない場合は %{}を入れます
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属性が必要なので忘れないようにしましょう
...
<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>
次は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