鉄道の運行情報表示板に憧れて、駅間ラインデータを作成した話

## はじめに

鉄道の地理データというと駅のポイントデータや路線のラインデータが思いつきますし、今まで自分もいろいろな地図（[例1](https://qiita.com/mg_kudo/items/b685ee9dfd3e376602dc)、[例2](https://qiita.com/mg_kudo/items/a6b4b53f2cacfacac9a0)）を作ってきました。一方、鉄道の駅間毎に情報を表示したいと思う場合もあります。例えば、以下のような事例が考えられます。

* 運休区間や遅延区間等の表示
* とある特急列車の運行区間の表示
* 駅間毎の輸送量などの統計データの表示


しかしながら、駅のポイントデータや路線のラインデータはあっても、**駅間をつないだラインデータ**（駅毎に区切ってあるもの）というのは、あまり見たことがありません。そこで、今回、公開データをもとに、**全国の鉄道**についてなるべく簡易に作成しようと試行錯誤しましたので、その過程と成果を紹介します。

最終的に、タイトルにある「運行情報表示板」のようなものができたかな、とは思います。（以下は東京の地下鉄と明かり区間を示した例）

![地下鉄明かり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/e91ba6ee-4f57-520e-fdd7-b823ceb151bf.png)


## データの収集

駅のデータとしては、[国土数値情報（鉄道データ）](https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-v2_3.html)を活用することで、駅の座標値をラインデータとして入手することができます。ラインデータですので、必要に応じて、駅の代表地を示すポイントデータに変換します（変換方法の詳細は省略）。

一方、国土数値情報には、駅の「順番」を示すデータは格納されておりません。一応、id のようなものは含まれているのですが、緯度に応じて機械的に割り振られているようで、その路線内の順番を示しているわけではありません。つまり、たとえば「御茶ノ水駅」「水道橋駅」「飯田橋駅」のように東西に並んでいる場合、どのような順番で並び替えればよいかはわからないのです。元データの中（GeoJSON しか見ていませんが）も、順番に並んでいるわけではなさそうです。

「駅の順番」のデータですが、探したところ、全国分のデータは簡単に手に入るものではなさそうでした（三大都市圏については、統計データ等で見られたのですが……）。そのため、今回の試みにおいては、「駅の順番」のデータをいかに作るかがポイントになります。

## 駅の順番データの作成

### 機械的に作成する
まずは、国土数値情報から作成できないかと試みます。国土数値情報（鉄道データ）には、駅部分のラインデータの他、路線全体のラインデータが含まれています。この2つのデータを用いて駅の順番を作成できないかと試みます。

方針は以下の通りです。
1. 駅の代表点を駅部分のラインデータから作成する。
2. 駅の代表点に最も近い路線のラインデータ上の点を取得する。
3. 路線のラインデータの端点と2.で得られた駅に最も近い点の経路を取得する。
4. 得られた経路の長さ（経路長）を取得する。
5. 最終的に各駅に対して、4.で得られた経路長を比較して並び替えることで駅の順番を取得する。

こちら、処理が複雑に見えますが、[Turf.js](https://turfjs.org/) を活用することで簡単に実装できました。

```JavaScript
const turf = require('@turf/turf');
// 駅の代表点に最も近い路線のラインデータ上の点を取得する。
// ※segment は路線のラインデータ(GeoJSON)、station は駅の代表点(GeoJSON)。
const snapped = turf.nearestPointOnLine(segment, station);
// 路線のラインデータの端点と2.で得られた駅に最も近い点の経路を取得する。
const sliced = turf.lineSlice(segment.geometry.coordinates[0], snapped, segment);
// 得られた経路の長さ（経路長）を取得する。
const d = turf.length(sliced);
```

一方、この方法は大きな問題がありました。すなわち、上記のアルゴリズムを利用するためには、路線全体のラインデータが**単一のラインデータ**である必要があります。実際、国土数値情報の路線のラインデータは分断されてバラバラとなっていますので、つなぎ直さなくてはなりません。

方法としては、事業者・路線ごとに、構成する断片に対して、端点の経度・緯度が同じものを片っ端からつなげていくというプログラムを組んで回しました。

とりいそぎ、これで一本につながった路線もありましたが、すべてうまくいくわけではありません。例えば、以下のような別れた線路があると物理的に1本につなげることはできません。

* 支線（行き止まりの他、再合流する場合も含む）
	* 東海道本線美濃赤坂支線や函館本線砂原支線等
* 複線が一時的に分かれて形状として単線化するところ
	* 上越線や北陸本線などのループ線がある区間等


プログラムを回して最終的に1つのラインデータになったものには、端点からの経路長を用いた方法で順番データを作成しました。一方、2つ以上のラインデータが残ってしまった場合は、あきらめて手動で調整することとしました。

### Wikipedeia の駅一覧データをコピペ

上記の機械的な方法で上手くいかない場合は、手動で対応する必要があります。この際、データ情報源として Wikipedeia を利用しました。Wikipedia の各鉄道路線のページには、大抵は「駅一覧」という項目があります。例えば、以下は函館本線の該当部分です。ここのテーブル化されているところを中心にコピペします。コピペは、国土数値情報の事業者・路線ごとに行うことで、国土数値情報の事業者・路線との整合性（表記ゆれの防止等）を保ちます。

https://ja.wikipedia.org/wiki/%E5%87%BD%E9%A4%A8%E6%9C%AC%E7%B7%9A#%E9%A7%85%E4%B8%80%E8%A6%A7

Wikipedia は機械的なアクセスが禁じられているようなので、手動で各ページからコピペして収集しました。

綺麗に一駅ずつコピペしているのでは時間が足りないので、当該部分を一括でテキストファイルへコピペして、その後、そのテキストファイルから、駅名を正規表現で抽出します。意外とシンプルに、以下の正規表現でおおむねうまく取得できましたが、一部は加工が必要です。この取得した駅名を出現順に格納すると、駅の順番データとなります。

```JavaScript
const m = line.match(/(^|\s)(\S+)(駅|停留場)/);
if(m){
  const stationName = m[2];
  ...
```

以上の手順で、手動による駅の順番データを構築しました。

### 追記：使えたかもしれいないデータ

この記事まで作成・投稿した後に、以下のような素晴らしいデータに気づきました。こちらを使えばもう少しスマートに駅の順番データを作成できたかもしれません。こちらのデータをどのように作成されているか気になりましたが、出典を確認すると、どうやら私の試みと同じく、国土数値情報と Wikipedia のようです。

https://qiita.com/uedayou/items/b5131b5ca930fe0bef69

## 駅間をつないだラインデータの作成

駅の順番データができれば、その順番通りに駅間毎のラインデータを作成して、国土数値情報座標を付与してやれば駅間データは完成します。鉄道事業者や路線名は揺れが大きいと思われますが、すべて国土数値情報に合わせていますので、表記ゆれについては、駅名が合っていれば問題ありません。駅名の表記ゆれと思われるところがあれば、適宜手動で修正を繰り返します。

また、駅の順番を整理し、座標値を付与した途中成果として、以下のような CSV を作成しました。

```
鉄道事業者,路線,駅1,駅2,経度1,緯度1,経度2,緯度2,フラグ,value
九州旅客鉄道,指宿枕崎線,鹿児島中央,郡元,130.54216,31.58486,130.541985,31.565640000000002,1,
九州旅客鉄道,指宿枕崎線,郡元,南鹿児島,130.541985,31.565640000000002,130.5436,31.5548,1,
...
```

同じ情報（事業者・路線名や前後の駅）を何度も記載するため、ファイル全体として重複が大きいですが、**隣り合う駅（駅1～駅2）間の1区間を1レコードで独立して利用できる**ものとしています。そのため、不要なレコードを削除して、必要な分だけのレコードとすることが可能です。また、カラムも追加できるようにしています（後述）。

この CSV をもとに、1レコードごとに、駅1～駅2間のラインデータ（頂点は、駅1と駅2の2つのみ）へ変換して利用ができます。

## ウェブサイト

今回作成した駅間をつないだラインデータは、以下のサイトで公開しています。作成した駅間データと駅の代表点データは、ベクトルタイルへ変換（[tippecanoe](https://github.com/felt/tippecanoe) を用いて [PMTiles](https://github.com/protomaps/PMTiles) へ変換）して利用しています。

サイト自体は、[MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) で構築し、PMTiles の消費のため、[PMTiles レポジトリの JavaScript 用参照実装](https://github.com/protomaps/PMTiles/blob/main/js/README.md)を利用しています。背景は国土地理院最適化ベクトルタイルです。

https://mghs15.github.io/flagment-inter-station/

### 自分で加工したデータのアップロードについて

また、上記で紹介した途中成果の **CSV に情報を追加してアップロードすることで、地図上に表示できる**ようにしています。想定する CSV は以下の通りです。

まず、カラムの1～8列目は以下の通りで固定します。
1. 鉄道事業者
2. 路線
3. 駅1
4. 駅2
5. 経度1（駅1）
6. 緯度1（駅1）
7. 経度2（駅2）
8. 緯度2（駅2）

また、9列目以降は、独自のデータを追加できます。以下のカラム名にすることでウェブ地図上で表示する際のスタイルを指定できます。

（記事投稿時点の設定）
* `_sort-key`：*Number* 表示の優先度。値が大きいほど、相対的に上に表示される。
* `_width`： *Number* ラインの太さ。なお、ラインには blur をかけているので、12以上はほしい。
* `_opacity`：*Number* ラインの透過度。0～1。
* `_color`：*String* Hex (`#FFFFFF` 形式)を想定。
* `_r`、`_g`、`_b`：*Number* RGB をそれぞれ0～255で指定。`_color` が設定されている場合は無効。

今回作成した全駅間の CSV は以下から取得できます。

https://github.com/mghs15/flagment-inter-station/tree/main/sample

### 表示サンプル

* 地下鉄の明かり区間

東京の地下鉄（東京メトロ、都営地下鉄）を橙色、そのうち地上に出る区間を赤色で表示しました。本記事の冒頭で例示したものです。駅周辺で少しだけ顔を出す場合でも駅間全体を指定していることから、かなり過剰に色がついていることになります（四ツ谷駅周辺など）。

![地下鉄明かり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/e91ba6ee-4f57-520e-fdd7-b823ceb151bf.png)
※[こちらの記事](https://news.mynavi.jp/article/trivia-448/)を参考に、地理院地図で確認しながら作成しました。

* 特急「富士回廊」の運行区間

新宿駅～河口湖駅の区間を塗っています（停車駅は特に強調表示していません）。

![富士回廊.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/d3f60e7f-5ef8-c823-67cc-933af0d63e6e.png)


* 駅間の輸送定員

中央本線下りの1日合計となります。立川駅からだんだん減って、高尾駅で大きく落ちる様子が分かります。

![輸送定員（大都市交通センサス）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/2eebfd76-285d-39cb-ca76-bbcd38b52d40.png)
※第１２回大都市交通センサスのうち、鉄道の「[【首都圏】路線別着時間帯別駅輸送定員表](https://www.e-stat.go.jp/stat-search/files?page=1&layout=datalist&toukei=00600020&tstat=000001103355&cycle=0&tclass1=000001203340&tclass2=000001203342&tclass3=000001203344&stat_infid=000031598040&tclass4val=0)」を使用。


## レポジトリ

今回作成したツール、ウェブサイト、サンプルデータ等は以下に格納しております。

https://github.com/mghs15/flagment-inter-station

## 感想

「駅の順番」という手に入りそうなデータがなかなか入手できずに困りました。色々と処理方法を考えましたが及ばず、結局、Wikipedia からコピペする力業で乗り切りました。時には、複雑な処理を考えるよりも、手動作業を積み上げた方が早いです。特に今回は、1回作れば、あとは適宜小さな更新のみ、という性質のデータでしたので。

また、今回のように、人間が見れば駅の順番はすぐにわかるのに、プログラムに落とし込むは難しい、という場合、AI を使えれば強いんだろうなぁと感じながらの作業でした。

課題（割り切った部分）としては、以下を認識しています。
* 上り下りを区別していない（混在している）
* 普通鉄道・軌道・索道等の別、JR か私鉄か等も区別していない
    * JR かどうかは、会社名に「旅客鉄道」が入っているかどうかで判別中
* 路線の形状が正確でない（駅間を直線でつないだだけ）
	* また、「その路線には所属しているが、ホームのない駅」をどのように扱うか悩みどころ
* CSV の編集コストは結局高いままかもしれない。
	* 経緯度をアップロードする CSV 側に持たせているが、サイト側で持った方が良いのでは？
	* 1レコードに2つの駅を並べているが、1駅ずつのレコードとした方が良いのではないか？

いったん作成すると、このように欲望がどんどん出てきてしまいます。上記の課題は、結構な手間となりそうなので、改善に着手するかは悩みどころです。