GPS情報から近くの道路種別（国道・県道・市道・・・）を判定する

## わたしのこと
今年の8月からエンタープライズな分野を離れ、新しいことにチャレンジする部署に異動になりました。
新しい部署のちょっとした企画の中で、個人的に触れたことのない地図データを扱うことになりました。

## やりたいこと
やりたいことは、GPSの位置情報から近くの道路種別（国道・県道・市道など）を判定することです。

https://sakinokoto.esm.co.jp/plan-detail/machitopia

そんなことできるのか？と思いつつも、ChatGPTさんに協力してもらいながら以下の2パターンで実現可能性を調査してみました。
- GISを利用する方法
- OpenStreetMapAPIを利用する方法

Google Maps API を使う方法もありそうでしたが、お金がかかってしまいそうなので今回は調査対象にしていません。もちろん他にも方法はあると思うのですが、当記事では上記2パターンで調査した内容を備忘として残します。

## GISを利用する方法
GISというのは、地理情報をコンピューター上で取り扱うためのシステムのことです。（Wikipediaより）

https://ja.wikipedia.org/wiki/%E5%9C%B0%E7%90%86%E6%83%85%E5%A0%B1%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0

国土交通省が道路データをシェープファイル形式で公開しているので、今回はその中の福井県のデータを利用して調査をしてみました。

https://nlftp.mlit.go.jp/ksj/gmlold/datalist/gmlold_KsjTmplt-N01.html

このシェープファイルには以下のような道路種別データを持っているので、GPS情報からこのデータが取得できればやりたいことは実現できそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/361154/35d2ae4b-6d5b-30e4-ac6d-6e2c4be4a087.png)
以下が実装してみたコードです。
（注）シェープファイルのフォーマットは国土交通省のデータに合わせて実装しています。

```ts:TypeScript
import fs from 'fs'
import shp from 'shpjs'
import * as turf from '@turf/turf'
import { Feature, FeatureCollection, LineString } from 'geojson'

type RoadFeature = {
  // propertiesの属性はShapefileに依存
  properties: {
    N01_001: string
    N01_002: string
    N01_003: string
    N01_004: string
  };
} & Feature<LineString>

// 道路データ
type NearestRoad = {
  roadType: string  // 道路種別
  distance: number  // 道路までの距離
} | undefined

// 経度・緯度の位置情報から最も近い道路データを返す
const getNearestRoad = async (pointCoordinates: [number, number]): Promise<NearestRoad> => {

  // 福井県の道路データをShapefile (.zip, .shp, .dbf, .shx)　形式を読み込む
  const shpBuffer = fs.readFileSync('N01-07L-18-01.0_GML.zip')
  const geojsonData = await shp(shpBuffer) as FeatureCollection<LineString>
  
  // ポイントオブジェクトを作成
  const point = turf.point(pointCoordinates)

  // 各フィーチャーをループして最も近い道路を探す
  let nearestRoad: NearestRoad
  let minDistance = Infinity

  geojsonData.features.forEach((feature: any) => {
    const roadFeature = feature as RoadFeature
    const roadLine = turf.lineString(roadFeature.geometry.coordinates)
    const roadType = roadFeature.properties.N01_001
    const distanceToPoint = turf.distance(point, turf.nearestPointOnLine(roadLine, point))

    if (distanceToPoint < minDistance) {
      minDistance = distanceToPoint
      nearestRoad = {
        roadType,
        distance: minDistance
      }
    }
  })

  return nearestRoad
}

const main = async () => {
  // 経度
  const longitude = 136.216990
  // 緯度
  const latitude = 36.061248

  // 経度・緯度の位置情報から最も近い道路データを取得する
  const nearestRoad = await getNearestRoad([longitude, latitude])

  console.log(nearestRoad)
}

main()
```
経度・緯度は固定値にして実行してみると…
```shell
$ npx ts-node index.ts
{ roadType: '3', distance: 0.10704069342470654 }
```

はい。位置情報（経度・緯度）から最も近くの道路データを取得することができました。

ただ、国土交通省の道路データでは道路種別（1〜3）までのデータしか保持してなさそうでしたので、道路種別（4〜7）までを含めて判定するためにはもう少し詳細なシェープファイルを入手する必要がありそうです。逆説的に言えば、完全なシェープファイルさえ入手できれば、やりたいことは実現できそうです。

## OpenStreetMapAPIを利用する方法
OpenStreetMapとは誰でも自由に利用でき、なおかつ編集機能のある世界地図を作る共同作業プロジェクトです。（Wikipediaより）

https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%BC%E3%83%97%E3%83%B3%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%83%E3%83%97

OpenStreetMapにはOverpass APIというWebAPIが用意されており、地図データにアクセスすることができるようになっています。また、取得できる道路データの中にhighwayタグという値を持っており道路種別を判定できるようになっていますが、明確に国道・県道・市道という形式で道路種別を保持していないため、ある程度こちらで補完してあげる必要があります。

以下がhighwayタグの値の意味です。

|highwayタグ|説明|
|:--|:--|
|motorway|高速道路。一般的にはインターチェンジでしか出入りできない。|
|trunk|主要道路。高速道路とは異なり、一般的に信号や交差点が存在する。|
|primary|主要な一般道。大きな町や都市をつなぐ道路。|
|secondary|二次道路。主要な地域道路。|
|tertiary|三次道路。地域内の道路で、通常は住宅エリアやビジネスエリアをつなぐ。|
|unclassified|分類されていない道路。通常は住宅エリア内などで見られる。|
|residential|主に住宅エリア内の道路。|
|service|サービス道路。通常は駐車場、ドライブウェイ、アクセス道路など。|
|pedestrian|歩行者専用の道路や通路。|
|cycleway|自転車専用道路。|
|footway|歩道。|
|bridleway|馬などが通行可能な道。|

何箇所かで調べてみましたが、福井県の場合はmotorwayとtrunkは国道、primaryとsecondaryは県道、その他は市道と判定しても良さそうでした。ということでそれらの補完を入れた上で道路種別を判定できるかやってみました。

```ts:TypeScript
import axios from 'axios'

type NearestRoad = {
  highway: string,
  type: string,
  name: string,
} | undefined

// 補完関係を定義
const roadTypes = new Map<string, string>([
  ['motorway', '国道'],
  ['trunk', '国道'],
  ['primary', '県道'],
  ['secondary', '県道'],
]) 

const getRoadType = (highway: string) => {
  return roadTypes.get(highway) ?? '市道'
}

// 2点間の距離を計算（緯度、経度は十進数で）
const calculateDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => {

  // 地球の半径（km）
  const R = 6371 

  const dLat = (lat2 - lat1) * Math.PI / 180
  const dLon = (lon2 - lon1) * Math.PI / 180
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
            Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
            Math.sin(dLon / 2) * Math.sin(dLon / 2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))

  // 距離をメートルで返す
  return R * c * 1000 
}

// 緯度・緯度の位置情報から最も近い道路データを返す
const getNearestRoad = async (lat: number, lon: number): Promise<NearestRoad> => {

  // 半径（メートル）
  const radius = 8

  // Overpass　APIで経度・緯度から半径8メートルの道路を検索する
  const overpassUrl = 'http://overpass-api.de/api/interpreter'
  const overpassQuery = `
    [out:json];
    (
      way["highway"]
        (around:${radius},${lat},${lon});
    );
    out body;
    >;
    out skel;
  `
  const overpassResponse = await axios.get(overpassUrl, {
    params: { data: overpassQuery }
  })

  const elements = overpassResponse.data.elements
  const nodes = elements.filter((element: any) => element.type === 'node')
  const nodeMap: { [key: number]: any } = {}
  nodes.forEach((node: any) => {
    nodeMap[node.id] = node
  })

  let nearestRoad: NearestRoad
  let minDistance = Infinity

  const roads = elements.filter((element: any) => element.type === 'way')
  
  roads.forEach((road: any) => {
    road.nodes.forEach((nodeId: number) => {
      const node = nodeMap[nodeId]
      if (node) {
        const nodeLat = node.lat
        const nodeLon = node.lon
        const distance = calculateDistance(lat, lon, nodeLat, nodeLon)
        if (distance < minDistance) {
          minDistance = distance
          nearestRoad = {
            highway: road.tags.highway,
            type: getRoadType(road.tags.highway),
            name: road.tags.name,
          }
        }
      }
    })
  })

  return nearestRoad
}

const main = async () => {
  // 緯度
  const latitude = 36.061248
  // 経度
  const longitude = 136.216990

  // 緯度・経度の位置情報から最も近い道路データを取得する
  const nearestRoad = await getNearestRoad(latitude, longitude)

  console.log(nearestRoad)
}

main()
```
今回も緯度・経度は固定して実施すると…
```shell
npx ts-node index.ts
{ highway: 'primary', type: '県道', name: '福井加賀線' }
```

はい。こちらも位置情報（経度・緯度）を元に、最も近くの道路データを取得することができました。

ただし、管轄が違う道路同士の交差点付近は100%ではないですね。GPS情報から検索する半径を調整するか、一律で上位の道路として判定（市道と県道の交差点なら県道と判定）するなど、アルゴリズムの調整が必要かと思います。

## 地図データを触ってみて
今回はかなりニッチな調査となりました（笑）。
ただ、今までずっとエンタープライズの分野で生きてきたので、なかなか触れることのない分野でとても新鮮で楽しかったです！

ところで今回はChatGPTさんにもかなりヒントを貰いながら調査しました。
生成AIがなかったらもっと調査に時間がかかっていたことは間違いありません。
特に2地点間の距離の求め方（calculateDistance関数）とか絶対に自分だけでは分かりません（笑）。
うまく使いこなすことでエンジニアの生産性はだいぶ違ってくるなぁという印象でした。

## 後日談・・・
ちなみに企画の方はサービス開発に着手するには至りませんでした。その辺りの経緯などもブログも書いてますので、ぜひご覧になってください。

https://sakinokoto.esm.co.jp/blog-detail/machitopia-report