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

# はじめに

前記事の続きです。

https://qiita.com/hozyo/items/0ea0c253b652fba0f836

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

# 実行環境
* Mac Ventura 13.1
* Livebook app

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

https://livebook.dev/

因みになのですが、Livebook app でもコマンドラインのように --home オプションで好きなディレクトリで開けたらいいなと思い、Livebook.app の中のシェルスクリプトファイルに
```shell:/Applications/Livebook.app/Contents/Resources/rel/bin/app
export HOME="開きたいlivebook用ディレクトリパス"
```
と無理矢理追加書き込みして使ってます。いいのかどうかはさておき、密かに便利だなーと思っています。

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

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

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

最終的にこんなになってしまったセッティング。
```elixir
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"}
])
```
前回と同じエリアス設定
```elixir
alias Explorer.DataFrame, as: Df
alias Explorer.Series, as: Sr
alias MapLibre, as: Ml
require Explorer.DataFrame
```

早速shape.txtを取り込んでみます。
```elixir
shape_df =
  Path.absname("./gtfs-sample/saga-current/shapes.txt")
  |> Df.from_csv!()

shape_df
|> Kino.DataTable.new()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/c791fc7d-a4ac-e66a-bb17-5deaef23ef58.png)

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

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

https://developers.google.com/transit/gtfs/reference?hl=ja

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

まあよく知らないことは置いといて、まず何路線あるのか様子をみたいです。
```elixir
shape_id_lists =
  shape_df
  |> Df.distinct([:shape_id])
  |> Df.pull(:shape_id)
  |> Sr.to_list()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/87630ce0-9865-fb8e-b7ba-33e1a59b6d10.png)

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

```elixir
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路線だけ取り出して見える化
手っ取り早く路線が見たいので、データフレームの中から一個だけ抽出してみます。
```elixir
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 といういかにも路線図に似合いそうなものを見つけたので、試してみましょう。
```elixir
shape_geojson = %Geo.LineString{
  coordinates: lon_lat_list
}
```
センター座標を前記事と同じように取ります。
```elixir
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
}
```
描画します。
```elixir
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/800ae55e-17bd-623d-04bb-83997f38eb42.png)

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

# 全路線に拡張

こんな感じで全部をGeoJSON化してみたいと思います。
```elixir
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の配列の中の緯度経度配列を、緯度は緯度、経度は経度でグサーっと縦に串刺しにして集約すれば、総数で割った平均値がセンター座標になるイメージ。
面白そうじゃないですか?

```elixir
# ルートごとの緯度経度の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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/b29affc2-ad21-78cc-b832-f8b3ed0cbcd9.png)

なんとなく、ぽいのが出てきたので、パイプラインひと繋がりにします。
小数点も元データが多くて7桁っぽいのでここは合わせた方が良いのでしょうか。
```elixir
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/b176d1b1-9039-5d99-07c9-537a739112c5.png)

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

ここで、気がつきました。Flat化すれば全然シンプルになるはず。
```elixir
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)} )
```
イメージが収束して限界値から少し離れた気がします。

### 全路線の描画
```elixir
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/57be2d59-0490-8c69-3794-bcd55f9110e2.png)

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

# 路線ごとの色分け描画

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

途方にくれたので、とりあえずデータを両手に持てる数に絞ってゴニョることにしました。
```elixir
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/558a8633-b12e-46e7-5289-4a13cbb80d69.png)

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

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

因みにぼくの畳み込みのイメージはこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/7be60b96-9e9a-4db8-ca1a-d252f5c6cabd.png)
口では「次元降下させることかな」なんて残念ななんちゃって発言するかもしれませんが、頭の中はこんなのです。

reduceついでにmapのイメージはこうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/4f5746ac-c9e3-f236-1243-f060a289195e.png)

```elixir
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657/dba0e70c-88f8-672e-aa87-724b64b3c27c.png)

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

<del>因みにidのところはユニークな文字列にしたくて、見かけた記事からアイディアを頂戴しました。
アルケミストのお方々はこんなときどうやるのでしょうか。</del>
> @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と共に歩けば必ず辿り着く気がします。