国土数値情報ダウンロードサイトのデータを使って路線図を書いてみる

国土交通省が公開している国土数値情報ダウンロードサイトでは、様々なGISデータをダウンロードできます。 データにより利用条件が異なりますが、一定条件の下で自由に利用することができるデータもあります。

この中には、鉄道駅や線路、バス停留所やルートの情報もあります。 今回は鉄道駅と路線を用いて、路線図を作ることを目標にしてみます。

ここでいう路線図は、駅をノードとし、駅間をアークで接続したグラフ構造の地図のことを言うことにします。 路線図があることで、経路探索などのアプリケーション開発ができるようになるでしょう。

筆者はこのデータを用いて、最終的には散歩をたのしくするアプリケーションを開発して、健康になりたいという思惑があります。

データをダウンロードする

国土数値情報ダウンロードサイト - 鉄道データから、2023年(令和5年)のファイルをダウンロードします。 この中に含まれる、 utf8/N02-23_RailroadSection.geojson および utf8/N02-23_Station.geojson を用います。

N02-23_GML
    `-- utf8
        |-- N02-23_RailroadSection.geojson
        `-- N02-23_Station.geojson

GISデータには様々なファイルフォーマットがあるようですが、今回はTypeScriptで取扱しやすいであろうJSONフォーマットをベースにした「GeoJSON」フォーマットを用います。 位置・図形情報や属性(駅名、建物名とか)を地図情報として記述できるJSON形式のファイルフォーマットです。

これらのデータはQGISというソフトウェアで地図として描画することもできます。 この記事ではQGISの使い方の説明は行いませんが、GIS実習オープン教材 が詳しいです。

駅データの一部抜粋 (utf8/N02-23_Station.geojson) を一部抜粋してみると以下のようになっています。

{
  "type": "FeatureCollection",
  "name": "N02-23_Station",
  "features": [
    { "type": "Feature", "properties": { "N02_001": "11", "N02_002": "2", "N02_003": "中央線", "N02_004": "東日本旅客鉄道", "N02_005": "水道橋", "N02_005c": "003591", "N02_005g": "003580" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.75255, 35.70209 ], [ 139.75494, 35.70191 ] ] } },
    { "type": "Feature", "properties": { "N02_001": "11", "N02_002": "2", "N02_003": "中央線", "N02_004": "東日本旅客鉄道", "N02_005": "飯田橋", "N02_005c": "003608", "N02_005g": "003595" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.74303, 35.6994 ], [ 139.7437, 35.70019 ], [ 139.74421, 35.70091 ] ] } },
    { "type": "Feature", "properties": { "N02_001": "11", "N02_002": "2", "N02_003": "中央線", "N02_004": "東日本旅客鉄道", "N02_005": "御茶ノ水", "N02_005c": "003616", "N02_005g": "003615" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.76372, 35.69995 ], [ 139.76634, 35.69926 ] ] } },
  ]
}

(「国土数値情報(鉄道データ)」(国土交通省)( https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-2023.html )を加工して作成)

駅データは駅のホームが LineString (=パスデータ)として、座標に緯度経度が用いられていることが分かります。 また、features配列のpropertiesには、路線名や鉄道事業者、駅名が格納されているほか、同名の駅名かつ300メートル以内の駅をまとめるIDを格納した N02_005g というプロパティが存在します。これは実質乗り換え駅を表現するパラメータと見なすことができそうです。

線路データの一部抜粋 (utf8/N02-23_RailroadSection.geojson) を一部抜粋してみると以下のようになっています。

{
  "type": "FeatureCollection",
  "name": "N02-23_Station",
  "features": [
    { "type": "Feature", "properties": { "N02_001": "11", "N02_002": "2", "N02_003": "中央線", "N02_004": "東日本旅客鉄道" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.74421, 35.70091 ], [ 139.7437, 35.70019 ], [ 139.74303, 35.6994 ] ] } },
    { "type": "Feature", "properties": { "N02_001": "11", "N02_002": "2", "N02_003": "中央線", "N02_004": "東日本旅客鉄道" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.76372, 35.69995 ], [ 139.76634, 35.69926 ] ] } },
  ]
}
 

(「国土数値情報(鉄道データ)」(国土交通省)( https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-2023.html )を加工して作成)

このように、線路のパスが緯度経度として表記されています。また、パスは複数に分割され、それらが features 配列内に格納されていることがわかります。

線路データからJR東日本の中央線(神田〜中野駅間)を抜粋し、パスごとに色分けしたときの図です。このようにいくつかの区間に分割されていることがわかります。

中央線の線路データ。パスごとに色分けされている。

これらのデータを用いて路線図を作成するには、線路データのパスを辿り駅に順番に訪問することで、駅がどのように接続されているかを解く必要があります。

パスを順番に辿り駅に訪問することでグラフ構造の路線図を得る

線路を順番に辿る処理

線路のパスを数珠繋ぎのようにして、複数のパスで構成される線路を順番に辿れるようにします。

まず、前提としてパスデータ Path を以下のように定義します。

type Position2D = [number, number]  // 今回は緯度経度を平面直角座標に変換せず、そのまま取り扱うこととする
type Path = Position2D[]

数珠繋ぎになったPathである PathChain を定義します。パスの探索を開始する from() 関数によって、訪問関数を取得します。 訪問関数によって、訪問情報 Visited を取得し、訪問したPath path および、さらに次のPathを訪問するための訪問関数の配列 next を取得します。 枝分かれを考慮して複数の訪問関数を取得できるようにします。

type PathChain = {
  path: Path,
  isEnded: boolean,
  from: () => VisitFn
}
type VisitFn = () => Visited
type Visited = { pathchain: PathChain, next: NextFn }
type NextFn = () => VisitFn[]

GeoJSONに含まれる線路のパスデータから、PathChainを取得します。

const railroadsGeoJson = JSON.parse(readFileSync('./geojsons/railroads-all.json', 'utf-8').toString())
// 路線名でGroupByする
const railroadsFeature = Object.groupBy(railroadsGeoJson.features, (f) => f.properties.N02_003)
// 例として中央線の路線を取得する
const railroad = railroadsFeature['中央線'] || []
const paths = railroad.map(r => r.geometry.coordinates)
// 数珠繋ぎになったパスを取得する
const pathchain = toPathchain(paths)

toPathchain() では、あるPathの両端に接続されたPathを探索して接続情報を構築します。 完全な実装はGitHubの対象のコミットを参照できます。

線路上にある駅に訪問する処理

線路を順番に辿り、駅を順番に見つけていくことで駅のグラフ構造の路線図を得ることができます。

駅データは単一のパスデータで表現されています。ここで、改めて駅データのGeoJSONを見ると、coordinates のパスが駅のプラットホームを表現しています。

{
  "type": "Feature",
  "properties": {
    "N02_001": "11",
    "N02_002": "2",
    "N02_003": "中央線",
    "N02_004": "東日本旅客鉄道",
    "N02_005": "御茶ノ水",
    "N02_005c": "003616",
    "N02_005g": "003615"
  },
  "geometry": {
    "type": "LineString",
    "coordinates": [
      [ 139.76372, 35.69995 ], [ 139.76634, 35.69926 ]
    ]
  }
}

予めプラットホームの中心点を計算しておいた後、線路(PathChain)を順番に訪問し、現在のPath上にある駅(の中心点)を探します。 同じパスに駅が複数所属している場合は、訪問したパスの端からの距離で駅の順序を決定します。 (このためには、線分(=パス)と点(=駅の中心)の当たり判定が必要になりますが、2点間の線分上に点が存在することを確認したい でアルゴリズムについて説明しています)

概ね以下のように処理していきます。

  • パスの先頭から訪問する
  • パス上に駅が存在するか確認し、存在した場合は以下の処理を実行する
    • 今いるブランチのひとつ前の駅と接続する
    • 今いるブランチに駅が一つもない場合は一つ前のブランチの駅と接続する
  • 次のパスに移動する
    • パスが枝分かれしている場合は新しいブランチをつくる
  • 上記をくりかえし処理する

図示するとこのようになります。はじめに見つかった駅をブランチのスタックに格納します。 station graph 1

次に見つかった駅は、今のブランチの先頭スタックの駅(=直前の駅)に接続します。 station graph 2

枝分かれしたとき、今のブランチの先頭スタックが空なので、直前のブランチの先頭スタックの駅に接続します。 station graph 3

(以下はあくまで疑似コードになります。完全な実装は GitHubに掲載したコードの toStationGraph()関数 付近にあります。)

const branchStack = new Map<string, Station>()
const firstPathchainVisitFn = pathChain.from()
const firstVisited = firstPathchainVisitFn()

branchProcess(stations, [], new Map(), firstVisited)

function branchProcess(stationPositions: Station[], branchIdStack: string[], branchStack: Map<string, Station[]>, visited: Visited) {
  // いま訪問しているPathに駅が存在するか
  const found = stationPositions.some(s => pointInPath(s.position, visited.pathchain.path))

  // branchStackの初期化
  const currentBranchId = branchIdStack.at(-1)
  if (!branchStack.has(currentBranchId)) {
    branchStack.set(currentBranchId, [])
  }

  // 見つかったら前の駅と接続する
  if (found) {
    const previousBranchId = branchIdStack.at(-2)

    // 前の駅は、いまのブランチの先頭のスタックに存在するはずだが、もしいまのブランチの先頭の駅がまだない場合は前のブランチの先頭スタック
    const previousStationInCurrentBranch = branchStack.get(currentBranchId)?.at(-1)
    const previousStationInPreviousBranch = branchStack.get(currentBranchId)?.at(-1)
    const previousStation = previousStationInCurrentBranch || previousStationInPreviousBranch

    // 前の駅が見つかったら接続する
    if (previousStation) {
      previousStation.connect(currentStation)
    }
  }

  // 次のパスに移動する
  const nextVisitFns = visited.next()

  if (nextVisitFns.length === 1) {
    // 訪問先が1つしかないときはブランチIDもそのまま(=新しいブランチをつくらない)で処理を繰り返す
    branchProcess(nextVisitFns[0](), branchIdStack)
  } elseif (nextVisitFns.length > 1) {
    // 2つ以上の分岐がある場合は新しいブランチIDを発行(=新しいブランチをつくる)してブランチ毎に処理を繰り返す
    for (const visitFn of nextVisitFns) {
      branchProcess(visitFn(), [...branchIdStack, createBranchId()])
    }
  }
}

乗り換え駅の接続

utf8/N02-23_Station.geojsonN02_005g プロパティに示されるIDが一致している駅同士を乗り換え駅と見なし、これらの駅同士を接続します。

N02_005g は300メートル以内の同じ駅名をまとめるパラメータなので、同じ駅名でも必ずしも接続されるわけではありません。 たとえば東京都の浅草駅を例に出すと、東武スカイツリーライン、東京メトロ銀座線、都営地下鉄浅草線の浅草駅は N02_005g の値が同じですが、つくばエクスプレス線の浅草駅は異なる値になっています。 (実際のところ、これら3駅とつくばエクスプレス線の浅草駅はかなり距離があるので、日常的に乗り換えのための駅として利用するのは現実的ではなさそうです。)

浅草駅は東武スカイツリーライン・東京メトロ・都営地下鉄線で乗り換えできる

また、実質乗り換え駅として機能している駅も、これらの駅名が異なれば N02_005g も異なる値になります (たとえば:千葉県の京成船橋駅(京成線)と船橋駅(JR総武線・東武アーバンパークライン))。 乗り換え駅を完璧に知ることは、このGeoJSONデータからはできなさそうです 座標的に近い駅をまとめる処理をつくるなどの工夫が必要そうです。

路線図を描画する処理

完成したグラフ構造のデータをDOT言語に変換して出力します(ソースコード)。

そしてgraphvizを用いてSVGファイルを作成します。

 $ dot -Tsvg stationGraph.dot > out.svg
路線図の一部

路線図の全体は 作成した路線図の全体(SVG, 6MB程度) から確認できます。 (「国土数値情報(鉄道データ)」(国土交通省)( https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-2023.html )を加工して作成)

縦横幅が大きいので、アクセスしても真っ白なエリアしか画面に映らないのではないかと思いますが、縮小するなどしてご覧ください。

まとめ

鉄道の路線図を作ってみました。実は少々バグもありますが、なんとなくうまくいきつつあるので、バス路線も同様に試してみたり、経路探索をしてみたりしたいなと思っています。

さんぽをたのしくするアプリを作って健康になるぞ。。。!

ではでは。

クリックすると匿名でいいねできます。

(@piyoppi/counter-tools を使っています )