9
2

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 1 year has passed since last update.

Elixir Livebook で佐賀県のGTFSで全バス路線を地図に描きたいのだ

Last updated at Posted at 2023-02-23

はじめに

前記事の続きです。

GTFSデータ調べの最中ですが、今回は結構面白い見える化コーディングだったのでアウトプットします。

実行環境

  • Mac Ventura 13.1
  • Livebook app

Livebookはデスクトップアプリを使ってます。

因みになのですが、Livebook app でもコマンドラインのように --home オプションで好きなディレクトリで開けたらいいなと思い、Livebook.app の中のシェルスクリプトファイルに

/Applications/Livebook.app/Contents/Resources/rel/bin/app
export HOME="開きたいlivebook用ディレクトリパス"

と無理矢理追加書き込みして使ってます。いいのかどうかはさておき、密かに便利だなーと思っています。

バス路線座標データの読み込み

早速、佐賀県が公開してくださってるデータをありがたく使わせていただきます。
まだ、使うGTPSデータとやらは手に入っておりません。

今回は佐賀県GTPSデータの中のshape.txtを読み込みます。
どうも全路線の座標データが入ってるようなのです。
これを使えば「Elixir Livebook で佐賀県のGTSFで全バス路線を地図に描きたいのだ」が出来そうじゃないですか?

最終的にこんなになってしまったセッティング。

Mix.install([
  {:explorer, "~> 0.5"},
  {:kino, "~> 0.8"},
  {:csv, "~> 3.0"},
  {:kino_maplibre, "~> 0.1.7"},
#  {:req, "~> 0.3.3"},
  {:random_colour, "~> 0.1.0"}
])

前回と同じエリアス設定

alias Explorer.DataFrame, as: Df
alias Explorer.Series, as: Sr
alias MapLibre, as: Ml
require Explorer.DataFrame

早速shape.txtを取り込んでみます。

shape_df =
  Path.absname("./gtfs-sample/saga-current/shapes.txt")
  |> Df.from_csv!()

shape_df
|> Kino.DataTable.new()

image.png

路線ごとのデータフレーム配列

生データを見るとshape_idが路線名みたいに見えます。shape_idでまとめると路線ポイントの座標が取得出来るようです。
路線ポイントの順番がshape_pt_sequenceで、連続値とは限らない昇順なのだそうです。

因みに日本で奨励しているGTFSはGTFS-JPだとのことらしいです。

まあよく知らないことは置いといて、まず何路線あるのか様子をみたいです。

shape_id_lists =
  shape_df
  |> Df.distinct([:shape_id])
  |> Df.pull(:shape_id)
  |> Sr.to_list()

image.png

なんと492本あるようです。本当に全路線かもしれない。
よくわからないので、路線ごとにデータフレームして配列を作ってみたいと思います。

routes_df =
  shape_id_lists
  |> Enum.map(fn shape ->
    shape_df
    |> Df.filter(shape_id == ^shape)
    |> Df.arrange_with(& &1[:shape_pt_sequence])
  end)

元データは綺麗に昇順で並んでいるようですが、念の為にソートはかけたいです。
で、この後どうしたら良いのでしょうか。さっぱりわかりません。

1路線だけ取り出して見える化

手っ取り早く路線が見たいので、データフレームの中から一個だけ抽出してみます。

route_df =
  routes_df
  |> hd()

lon_list =
  route_df[:shape_pt_lon]
  |> Sr.to_list()

lat_list =
  route_df[:shape_pt_lat]
  |> Sr.to_list()

lon_lat_list =
  lon_list
  |> Enum.zip(lat_list)

ここで %Geo.LineString といういかにも路線図に似合いそうなものを見つけたので、試してみましょう。

shape_geojson = %Geo.LineString{
  coordinates: lon_lat_list
}

センター座標を前記事と同じように取ります。

lon = Enum.map(shape_geojson.coordinates, &elem(&1, 0))
lat = Enum.map(shape_geojson.coordinates, &elem(&1, 1))

center = {
  (Enum.min(lon) + Enum.max(lon)) / 2,
  (Enum.min(lat) + Enum.max(lat)) / 2
}

描画します。

Ml.new(
  center: center,
  zoom: 15,
#  style: "https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
   style: :street  #これでよかったみたい。reqパッケージ要らなくなった。
)
|> Ml.add_geo_source("route", shape_geojson)
|> Ml.add_layer(
  id: "route",
  source: "route",
  type: :line,
  paint: [line_color: "#aa00ff", line_width: 4]
)

image.png

どうやら%Geo.LineString、いけるようです。

全路線に拡張

こんな感じで全部をGeoJSON化してみたいと思います。

routes_geojson = %Geo.GeometryCollection{
  geometries:
    routes_df
    |> Enum.map(fn route ->
      lon = route[:shape_pt_lon] |> Sr.to_list()
      lat = route[:shape_pt_lat] |> Sr.to_list()
      lon_lat = lon |> Enum.zip(lat)
      %Geo.LineString{coordinates: lon_lat}
    end)
}

センター座標をGeoJSONから算出したい

とここで %Geo.GeometryCollection を見ているうちに、「GeoJSON -->🐱-->センター座標」がしたいと思い立ちました。
%Geo.LineStringの配列の中の緯度経度配列を、緯度は緯度、経度は経度でグサーっと縦に串刺しにして集約すれば、総数で割った平均値がセンター座標になるイメージ。
面白そうじゃないですか?

# ルートごとの緯度経度のsumを作る
lon_lat_sum =
  routes_geojson.geometries
  |> Enum.map(fn line_string ->
    line_string.coordinates
    |> Enum.reduce(fn {lon, lat}, {acc_lon, acc_lat} ->
      {lon + acc_lon, lat + acc_lat}
    end)
  end)

# 総数を取得する
count = shape_df[:shape_pt_lon] |> Sr.to_list() |> Enum.count()

lon_lat_sum
|> Enum.reduce(fn {lon, lat}, {acc_lon, acc_lat} ->
  {lon + acc_lon, lat + acc_lat}
end)
|> then(&{elem(&1, 0) / count, elem(&1, 1) / count})

image.png

なんとなく、ぽいのが出てきたので、パイプラインひと繋がりにします。
小数点も元データが多くて7桁っぽいのでここは合わせた方が良いのでしょうか。

count = shape_df[:shape_pt_lon] |> Sr.to_list() |> Enum.count()

all_center =
  routes_geojson.geometries
  |> Enum.map(fn line_string ->
    line_string.coordinates
    |> Enum.reduce(fn {lon, lat}, {acc_lon, acc_lat} ->
      {lon + acc_lon, lat + acc_lat}
    end)
  end)
  |> Enum.reduce(fn {lon, lat}, {acc_lon, acc_lat} ->
    {lon + acc_lon, lat + acc_lat}
  end)
  |> then(&{ Float.round(elem(&1, 0) / count, 7), Float.round(elem(&1, 1) / count, 7)})

image.png

一つの流れになって動いてしまいましたが、もうすでに自分の理解の限界を超えてしまっている気がします。

ここで、気がつきました。Flat化すれば全然シンプルになるはず。

all_center =
  routes_geojson.geometries
  |> Enum.map(fn line_string -> line_string.coordinates end)
  |>  List.flatten()
  |> Enum.reduce(fn {lon, lat}, {acc_lon, acc_lat} ->
    {lon + acc_lon, lat + acc_lat}
  end)
  |> then(& {Float.round(elem(&1, 0) / count, 7), Float.round(elem(&1, 1) / count, 7)} )

イメージが収束して限界値から少し離れた気がします。

全路線の描画

Ml.new(
  center: all_center,
  zoom: 9,
#  style: "https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
  style: :street
)
|> Ml.add_geo_source("routes", routes_geojson)
|> Ml.add_layer(
  id: "route",
  source: "routes",
  type: :line,
  paint: [line_color: "#aa00ff"]
)

image.png

いい感じがします。けど、カラフルな路線図が見たくなりました。

路線ごとの色分け描画

とはいえ全く見当がつきません。一体どうやれば色分けできるのでしょうか。

途方にくれたので、とりあえずデータを両手に持てる数に絞ってゴニョることにしました。

route0 = routes_geojson.geometries |> Enum.at(30)
route1 = routes_geojson.geometries |> Enum.at(50)

map =
  Ml.new(
    center: all_center,
    zoom: 9
  )

map
|> Ml.add_geo_source("route0", route0)
|> Ml.add_layer(
  id: "route0",
  source: "route0",
  type: :line,
  paint: [line_color: "#ff0000"]
)
|> Ml.add_geo_source("route1", route1)
|> Ml.add_layer(
  id: "route1",
  source: "route1",
  type: :line,
  paint: [line_color: "#00ffff"]
)

image.png

どうやらどんどんデータを足していくことで、個々に描画出来るようです。

このイメージは Enum.reduce による集約? 畳み込み? 的な実装で出来るかもしれない?!
本当になんとなくの思い付きだけで言っています。

因みにぼくの畳み込みのイメージはこんな感じです。
image.png
口では「次元降下させることかな」なんて残念ななんちゃって発言するかもしれませんが、頭の中はこんなのです。

reduceついでにmapのイメージはこうです。
image.png

map =
  Ml.new(
    center: all_center,
    zoom: 9,
#    style: "https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
    style: :street
  )

draw_routes = fn routes ->
  routes
  |> Enum.with_index()
  |> Enum.reduce(map, fn {line_string, index}, acc ->
    id = Integer.to_string(index)
    acc
    |> Ml.add_geo_source(id, line_string)
    |> Ml.add_layer(
      id: id,
      source: id,
      type: :line,
      paint: [line_color: RandomColour.generate(), line_width: 5]
    )
  end)
end

draw_routes.(routes_geojson.geometries)

image.png

なんと!! 描画できてしまったみたいです。
驚いたことに、あんな幼稚園児みたいな畳み込みイメージでも機能したようなのです!
Elixir Livebook 凄すぎます。

因みにidのところはユニークな文字列にしたくて、見かけた記事からアイディアを頂戴しました。
アルケミストのお方々はこんなときどうやるのでしょうか。

@the_haigoさんに教えていただいたユニーク文字列の取得を取り入れてみました。
さっぱりわからなかったので調べてみました。
Erlangからのcrypto系からのASCII文字列取得って感じですね(なんてぼんやりしたイメージなのだろう)。
勉強になります! (2023-02-24)

@the_haigoさんのコメントで、with_indexでとっていくのが由緒正しい感じがしたので、こっちにコードを変えました。
すごいアルケミストから教えてもらえて嬉しい。 (2023-02-24)

@the_haigoさんのコメントで教えてもらったSmart Cell、:style にURL要らなかったみたいなので修正。 (2023-02-24)

まとめ

やっぱり、GTFSとお友達になるためにElixir Livebookを使うのは楽しい。
実はまだ要件に辿り着いていません、がLivebookと共に歩けば必ず辿り着く気がします。

9
2
2

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
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?