【実践】PythonとOpenStreetMapで学ぶ経路システム開発入門

前回はGoogle Map APIを使用して開発を行いましたが、今回はOpenStreetMapを使用した似たようなことにチャレンジしてみようと思います。Pythonを使ってOpenStreetMapの経路検索URLを生成する方法ことを目的とします。

参考

uepon.hatenadiary.com

オープンストリートマップ(OpenStreetMap)とは?

オープンストリートマップ(英語: OpenStreetMapOSM)は、自由に利用でき、なおかつ編集機能のある世界地図を作るオープンコラボレーションプロジェクトである。GPS機能を持った携帯機器、空中写真、衛星画像、他の地理情報システムからのデータをもとに作られていくのが基本だが、編集ツール上で道1本から手入力での追加も可能である。与えられた画像とベクトルデータセットはオープンデータベースライセンス(ODbL)1.0のもと再利用可能である

上記出典

オープンストリートマップ - Wikipedia

www.openstreetmap.org

商用・非商用を問わずにデータの自由な利用と再配布が行えるのでいいですね😊

概要と前提知識

今回は以下のことを目標にしたいと思います。

  • 住所から緯度経度(座標)を取得する
  • OpenStreetMapで経路を表示するURLを作る
  • 車・徒歩・自転車の移動手段を切り替える
  • 複数の経由地を設定する

プログラムを実行することで、ルート検索結果を表示するURLを生成するって感じですね。

5分で始めるクイックスタート

すぐに試したい方は、以下の手順で!

# 1. プロジェクト作成
$ mkdir osm-handson && cd osm-handson

# 2. 仮想環境作成
$ uv venv
$ source .venv/bin/activate

# 3. ライブラリインストール
$ uv pip install requests python-dotenv

# 4. メールアドレス設定
$ echo "EMAIL=yourname@gmail.com" > .env  # ← アクセスで必要となる自分のメールアドレスを**.envファイル**に格納します

# 5. コードの編集(ステップ2の完全なコードを route_generator.py に貼り付け)
# 普段お使いのエディタでお使いください。(VSCodeなど)

# 6. 実行
$ python route_generator.py

これだけで地図上に経路を表示したURLが生成されるので、これをブラウザで開けばOKです。 (ターミナルでCtrl+クリックでブラウザが開きます。)

仕組みの概要

今回は簡単に仕組みを説明すると以下のような処理を行っています。

住所(例:東京駅)
    ↓ Nominatim APIで変換
緯度・経度(35.681, 139.767)
    ↓ URL生成
OpenStreetMapのURL
    ↓ ブラウザで開く
地図上に経路が表示

重要な用語

以下、よく使う用語を抜粋して、事前に説明しておきます。

  • ジオコーディング … 住所を緯度経度に変換すること
  • Nominatim … OpenStreetMapの無料ジオコーディングサービス
  • OSRM … Open Source Routing Machine(経路計算エンジン)
  • 緯度(Latitude) … 南北の位置(-90〜90度)
  • 経度(Longitude) … 東西の位置(-180〜180度)

最低限知っておくべきNominatim API

Nominatim APIとは?

OpenStreetMapが提供する無料の住所から座標への変換サービスです。

"東京駅" → (35.681236, 139.767125)

座標の精度について

⚠️Nominatim APIが返す座標は、検索対象の「代表点」です。 同じ場所でも、公式の座標や他のサービスと若干異なる場合があります。

(例)東京駅の場合

データソース 緯度 経度 備考
Wikipediaより 35.6809591 139.7673068 世界測地系
Nominatim API 35.681236 139.767125 OSMデータベース

差の理由は建物の中心や入口など、データの登録方法により位置が異なるためです(地図毎に違う❓)

今回は…

  • 経路検索が目的なので、この程度の誤差は実用上問題視しないことにします。
  • より正確な座標が必要な場は、「座標直接指定」を使いましょう!

API使用で守るべき3つのルール

以下を行わないとErrorが発生するようです。

1. User-Agentヘッダーを設定すること!

headers = {
    "User-Agent": "MyApp/1.0 (your.email@example.com)"
}

⚠️これがないと403エラーになります。

2. リクエストの間に1秒待つこと!

time.sleep(1.0)  # 連続リクエストの間に必須

⚠️これを守らないと429エラーやIPブロックの可能性があります。

3. 連絡先(メール)を含めること!

params = {
    …, # 他のパラメータ
    "email": "your.email@example.com"
}

これだけです!早速使ってみましょう。


環境準備

プロジェクトディレクトリの作成

$ mkdir osm-handson
$ cd osm-handson

仮想環境の作成

uvで仮想環境を作成します。(uvのインストールは事前に行ってください→詳細

#uvがインストールされていなければ以下を実行(Linux/WSL/Mac)
$ curl -LsSf https://astral.sh/uv/install.sh | sh
# uvがインストールされていれば以下から開始
$ uv venv

仮想環境を有効化します。

# Linux/WSL/Mac
$ source .venv/bin/activate

# Windows (PowerShell)
PS> .venv\Scripts\Activate.ps1

# Windows (cmd)
> .venv\Scripts\activate.bat

必要なライブラリのインストール

仮想環境を有効化した状態で、必要なライブラリをインストールします。

$ uv pip install requests python-dotenv

uvとは? 高速なPythonパッケージマネージャーです。pipと同じように使えますが、より高速に動作します。

python-dotenvとは? 環境変数.envファイルから読み込むライブラリです。メールアドレスなどの設定情報を安全に管理できます。

メールアドレスの設定

エディタを使用して.envファイルに以下を追加してください。メールアドレスは使用者のものを使用してください。

.env

EMAIL=your.email@example.com

⚠️ your.email@example.comを実際のメールアドレスに変更してください!

なぜ.envファイル?

  • メールアドレスの変更箇所が1箇所だけ
  • コードに直接書かないのでセキュリティ面で安全
  • .gitignoreに追加すれば、誤ってGitにコミットすることを防げる

ここまでで開発の準備が整いました☝️


ステップ 1. 住所から座標を取得する

1.1 Nominatim APIでできること

Nominatim APIは住所を座標に変換するAPIです。

"東京駅" → (35.681236, 139.767125)

上で説明した3つのルール(User-Agent1秒待機メールアドレス)を守れば使えます。

1.2 ジオコーディング用の関数を作る

実際にコードを書いてみます。

geocoding.py

import os
import time
import requests
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
load_dotenv()

NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"

def geocode_nominatim(query: str, email: str = None, delay_sec: float = 1.0):
    """
    住所やランドマーク名から緯度経度を取得する関数
    
    パラメータ:
        query (str): 検索したい住所やランドマーク名
        email (str): 連絡先メールアドレス(省略時は.envから読み込み)
        delay_sec (float): リクエスト間隔(秒)
    
    戻り値:
        tuple: (緯度, 経度) のタプル
    
    例外:
        ValueError: 検索結果が見つからない場合、またはメールアドレスが未設定の場合
    """
    # メールアドレスの取得(引数 > 環境変数)
    if email is None:
        email = os.getenv("EMAIL")
    
    # メールアドレスのチェック(ダミーそのままではエラーにしています)
    if not email or email in ["you@example.com", "your.email@example.com"]:
        raise ValueError(
            "\nエラー: メールアドレスが設定されていません!\n"
            "   .envファイルに以下を追加してください:\n"
            "   EMAIL=your.real.email@example.com\n"
        )
    
    # リクエストパラメータの設定
    params = {
        "q": query,              # 検索クエリ
        "format": "jsonv2",      # 返却形式(JSON)
        "limit": 1,              # 最大結果数
        "addressdetails": 0,     # 詳細住所情報は不要
        "email": email           # 連絡先
    }
    
    # HTTPヘッダーの設定(User-Agentは必須)
    headers = {
        "User-Agent": f"RouteURLBuilder/1.0 ({email})"
    }
    
    # APIリクエストの実行
    r = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=15)
    r.raise_for_status()  # エラーチェック
    
    # レスポンスの解析
    data = r.json()
    if not data:
        raise ValueError(f"検索結果が見つかりません: {query}")
    
    # 緯度経度の取得
    lat = float(data[0]["lat"])
    lon = float(data[0]["lon"])
    
    # 次のリクエストまで待機(マナー)
    time.sleep(delay_sec)
    
    return (lat, lon)

if __name__ == "__main__":
    # メールアドレスは.envから読み込み
    # 東京駅の座標を取得
    print("東京駅の座標を取得中...")
    tokyo_coords = geocode_nominatim("東京駅")
    print(f"成功!緯度={tokyo_coords[0]}, 経度={tokyo_coords[1]}")

ファイルを保存したら、以下を実行してください。

$ python geocoding.py

これで住所緯度・経度の座標に変換できるようになりました!


ステップ 2. 経路URLを生成する

2.1 OpenStreetMap Directionsの仕組み

続いては経路検索の機能を実装していきます。

OpenStreetMapの経路検索ページは、以下のようなURLの構造を持っています。

https://www.openstreetmap.org/directions?engine=XXXX&route=lat1,lon1;lat2,lon2;...

URLパラメータの説明:

パラメータ 説明
engine 移動手段(ルーティングエンジン) fossgis_osrm_car(車)
route 経路の座標列(セミコロン区切り) 35.681,139.767;34.702,135.496

2.2 利用可能な移動手段

手段に関しては以下のようなものがあります。

from typing import Literal

# 移動手段の型定義
Engine = Literal[
    "fossgis_osrm_car",   # 自動車
    "fossgis_osrm_foot",  # 徒歩
    "fossgis_osrm_bike"   # 自転車
]

2.3 URL生成関数を作成する

では、URLの構造を生成するプログラムを作成します。 先程の住所から座標を取得するプログラムに、経路URLを生成する処理を加えます。

route_generator.py

"""
ステップ1 + ステップ2 住所から座標を取得し、経路URLを生成する
"""
import os
import time
import urllib.parse
import requests
from typing import List, Tuple, Literal
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
load_dotenv()

# === API設定 ===
NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"
OSM_BASE = "https://www.openstreetmap.org/directions"

Engine = Literal["fossgis_osrm_car", "fossgis_osrm_foot", "fossgis_osrm_bike"]

# === ステップ1 ジオコーディング関数 ===

def geocode_nominatim(query: str, email: str = None, 
                     delay_sec: float = 1.0) -> Tuple[float, float]:
    """住所から座標を取得"""
    # メールアドレスの取得
    if email is None:
        email = os.getenv("EMAIL")
    
    if not email or email in ["you@example.com", "your.email@example.com"]:
        raise ValueError(
            "\nエラー: メールアドレスが設定されていません!\n"
            "   .envファイルに以下を追加してください:\n"
            "   EMAIL=your.real.email@example.com\n"
        )
    
    params = {
        "q": query,
        "format": "jsonv2",
        "limit": 1,
        "addressdetails": 0,
        "email": email
    }
    headers = {
        "User-Agent": f"RouteURLBuilder/1.0 ({email})"
    }
    
    r = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=15)
    r.raise_for_status()
    
    data = r.json()
    if not data:
        raise ValueError(f"検索結果が見つかりません: {query}")
    
    lat = float(data[0]["lat"])
    lon = float(data[0]["lon"])
    time.sleep(delay_sec)
    
    return (lat, lon)


# === ステップ2 URL生成関数 ===

def build_osm_directions_url(points: List[Tuple[float, float]],
                             engine: Engine = "fossgis_osrm_car") -> str:
    """OpenStreetMap経路検索URLを生成"""
    if len(points) < 2:
        raise ValueError("少なくとも2点が必要です")
    
    route_param = ";".join(f"{lat:.6f},{lon:.6f}" for lat, lon in points)
    query = {
        "engine": engine,
        "route": route_param
    }
    
    return f"{OSM_BASE}?{urllib.parse.urlencode(query)}"


# === テスト実行 ===

if __name__ == "__main__":
    print("=" * 60)
    print("住所から座標を取得し、経路URLを生成する")
    print("=" * 60)
    
    # ステップ1 住所から座標を取得(メールアドレスは.envから自動読み込み)
    print("\n座標を取得中...")
    tokyo = geocode_nominatim("東京駅")
    print(f"   東京駅: {tokyo}")
    
    osaka = geocode_nominatim("大阪駅")
    print(f"   大阪駅: {osaka}")
    
    # ステップ2 経路URLを生成
    print("\n経路URLを生成中...")
    url = build_osm_directions_url([tokyo, osaka], engine="fossgis_osrm_car")
    print(f"\n完成!\n{url}")
    print("\nこのURLをブラウザで開いてください!")

このコードを保存して実行すれば、STEP 2までの作業は完了です!

このURLをコピーしてブラウザで開くと…

  • OpenStreetMapの地図が表示される
  • 東京駅から大阪駅への車のルートが表示される
  • 距離と所要時間も確認できる

実際に試してみましょう! URLが正しく開けば成功です!

実行するとURLが生成されます。

URLをブラウザで開くと以下のような地図にルート表示が行われます。


3. 複数経由地対応したルート地図を作成する

先程は2地点間の経路URLを生成しましたでは、経由地があった場合のルート表示はどうでしょうか?実は、OpenStreetMapのみでは複数の経由地のルート表示ができないようです。そのため、複数の経由地を含むルートをインタラクティブな地図HTMLとして生成する方法についても行ってみようと思います。

3.1. 新たに使用する技術

経由地がある場合には、新たに以下の技術を使用することになります。

3.1.1. Folium(Python地図ライブラリ)

Foliumは、Leaflet.jsPythonラッパーです。Pythonコードだけでインタラクティブな地図を作成できます。

github.com

以下を実行してライブラリを追加インストールしてください。

# ライブラリのインストール
$ uv pip install folium

Leaflet.jsとは?

JavaScriptで書かれた、最も人気のあるオープンソースの地図ライブラリです。軽量で、モバイル対応も優れています。多くの地図サービス(OpenStreetMap、Mapbox、Google Mapsなど)で使われています。

leafletjs.com

Foliumは何をしてくれる?

Leaflet.jsをPythonから簡単に使えるようにしたラッパーライブラリです。Pythonコードを書くだけで、Leaflet.jsベースの美しい地図HTMLを生成できます。JavaScriptを書かなくても、Leaflet.jsの機能をPythonで使えることになります。

3.1.2. OSRM API(経路計算)

OSRM (Open Source Routing Machine) は、OpenStreetMapのデータを使った経路計算エンジンです。無料で使えるAPIが公開されています。

OSRM Route APIを使うと、実際の道路に沿った詳細な経路データ(GeoJSON形式)を取得できます。これを使用することで複数の経由地にも対応することができます。

APIの仕様は以下の通りです。

エンドポイント

http://router.project-osrm.org/route/v1/{profile}/{coordinates}

パラメータ - profile: driving(車)、walking(徒歩)、cycling(自転車) - coordinates: lon,lat;lon,lat;... 形式(複数指定可能) - overview: full(全経路)、simplified(簡略化) - geometries: geojsonpolylinepolyline6

レスポンスの例

{
  "routes": [{
    "distance": 420532.1,
    "duration": 18734.2,
    "geometry": {
      "type": "LineString",
      "coordinates": [[139.767125, 35.681236], ...]
    }
  }]
}

では、この2つを使用して複数経由地にも対応したルート検索表示を行います。


3.2. 実装

今回はmultiwaypoint_route_generator.pyという名前で新たにファイルを作成します。

multiwaypoint_route_generator.py

"""
OpenStreetMap 複数経由地対応ルート生成ツール
複数の経由地を含むピン付き地図HTML生成(Folium + OSRM)
"""
import os
import time
import requests
import folium
from typing import List, Tuple
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
load_dotenv()

# === API設定 ===
NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"

# === ジオコーディング関数 ===

def geocode_nominatim(query: str, email: str = None, 
                     delay_sec: float = 1.0) -> Tuple[float, float]:
    """住所から座標を取得"""
    if email is None:
        email = os.getenv("EMAIL")
    
    if not email or email in ["you@example.com", "your.email@example.com"]:
        raise ValueError(
            "\nエラー: メールアドレスが設定されていません!\n"
            "   .envファイルに以下を追加してください:\n"
            "   EMAIL=your.real.email@example.com\n"
        )
    
    params = {
        "q": query,
        "format": "jsonv2",
        "limit": 1,
        "addressdetails": 0,
        "email": email
    }
    headers = {
        "User-Agent": f"RouteURLBuilder/1.0 ({email})"
    }
    
    r = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=15)
    r.raise_for_status()
    
    data = r.json()
    if not data:
        raise ValueError(f"検索結果が見つかりません: {query}")
    
    lat = float(data[0]["lat"])
    lon = float(data[0]["lon"])
    time.sleep(delay_sec)
    
    return (lat, lon)


# === ピン付き地図生成(複数経由地対応) ===

def get_route_from_osrm(coords: List[Tuple[float, float]]) -> dict:
    """
    OSRM APIを使って経路データを取得(複数経由地対応)
    
    パラメータ:
        coords: 座標のリスト [(lat1, lon1), (lat2, lon2), ...]
    
    戻り値:
        経路データ(GeoJSON形式)
    """
    # OSRMは (lon, lat) の順番なので注意
    coord_str = ";".join([f"{lon},{lat}" for lat, lon in coords])
    
    # OSRM Route API エンドポイント
    url = f"http://router.project-osrm.org/route/v1/driving/{coord_str}"
    params = {
        "overview": "full",
        "geometries": "geojson"
    }
    
    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()
    
    return r.json()


def create_map_with_route(places: List[str], 
                          output_file: str = "route_map.html",
                          email: str = None) -> str:
    """
    経由地にピンを立てた地図HTMLを作成(複数経由地対応)
    
    パラメータ:
        places: 住所のリスト(2地点以上)
        output_file: 出力するHTMLファイル名
        email: メールアドレス(省略時は.envから読み込み)
    
    戻り値:
        出力したファイルのパス
    """
    print("=" * 60)
    print("ピン付き地図を作成中...")
    print("=" * 60)
    
    # 座標を取得
    coords = []
    place_names = []
    print("\n各地点の座標を取得中...")
    for i, place in enumerate(places, 1):
        coord = geocode_nominatim(place, email=email)
        coords.append(coord)
        place_names.append(place)
        print(f"   {i}. {place}: {coord}")
    
    # 地図の中心点を計算(全座標の平均)
    center_lat = sum(lat for lat, lon in coords) / len(coords)
    center_lon = sum(lon for lat, lon in coords) / len(coords)
    
    # 地図を作成
    print("\n地図を作成中...")
    m = folium.Map(
        location=[center_lat, center_lon],
        zoom_start=7,
        tiles='OpenStreetMap'
    )
    
    # マーカー(ピン)を追加
    print("マーカーを追加中...")
    for i, (coord, name) in enumerate(zip(coords, place_names), 1):
        folium.Marker(
            location=coord,
            popup=f"<b>{i}. {name}</b>",
            tooltip=name,
            icon=folium.Icon(
                color='red' if i == 1 else ('blue' if i == len(coords) else 'orange'),
                icon='play' if i == 1 else ('stop' if i == len(coords) else 'pause'),
                prefix='glyphicon'
            )
        ).add_to(m)
    
    # OSRMで経路データを取得
    print("経路データを取得中...")
    try:
        route_data = get_route_from_osrm(coords)
        
        # ルートを地図に追加
        if "routes" in route_data and len(route_data["routes"]) > 0:
            geometry = route_data["routes"][0]["geometry"]
            
            # GeoJSONの座標は [lon, lat] の順番なので注意
            route_coords = [[lat, lon] for lon, lat in geometry["coordinates"]]
            
            folium.PolyLine(
                route_coords,
                color='blue',
                weight=5,
                opacity=0.7,
                popup='経路'
            ).add_to(m)
            
            # 距離と時間を取得
            distance_km = route_data["routes"][0]["distance"] / 1000
            duration_min = route_data["routes"][0]["duration"] / 60
            
            print(f"   経路: 約 {distance_km:.1f} km / 約 {duration_min:.0f} 分")
        else:
            print("   警告: 経路データが取得できませんでした。直線で接続します。")
            folium.PolyLine(
                coords,
                color='red',
                weight=3,
                opacity=0.5,
                dash_array='10'
            ).add_to(m)
    
    except Exception as e:
        print(f"   警告: OSRM APIエラー: {e}")
        print("   直線で接続します。")
        folium.PolyLine(
            coords,
            color='red',
            weight=3,
            opacity=0.5,
            dash_array='10'
        ).add_to(m)
    
    # HTMLファイルとして保存
    print(f"\n{output_file} に保存中...")
    m.save(output_file)
    
    print(f"\n完成!")
    print(f"   作業ディレクトリに {output_file} が生成されました")
    print(f"\n   【開き方】")
    print(f"   1. エクスプローラー/Finderでこのファイルを探す")
    print(f"   2. ダブルクリックでブラウザが起動します")
    print("=" * 60)
    
    return output_file


# === メイン処理 ===

def main():
    """複数経由地のピン付き地図を生成"""
    print("\n" + "=" * 70)
    print("OpenStreetMap 複数経由地ルート生成ツール")
    print("=" * 70)
    print("メールアドレスは .env ファイルから読み込まれます")
    print("=" * 70 + "\n")
    
    # 経由地を指定して地図を生成
    places = ["東京駅", "名古屋駅", "大阪駅"]
    create_map_with_route(places, output_file="route_map.html")


if __name__ == "__main__":
    main()

3.3. 実行方法

3.3.1. プログラムを実行

python multiwaypoint_route_generator.py

これだけで、以下が自動的に行われます: - 各地点の座標を取得 - 経路データを計算 - route_map.html を生成

3.3.2. 生成されたHTMLファイルを開く

作業ディレクトリ(osm-handsonフォルダ)にroute_map.htmlが生成されます。 このファイルをブラウザで開くことで地図が開きます。

Tips

  • WSL環境の場合は、Windowsエクスプローラーから\\wsl$\ディストリビューション名\home\...\osm-handsonという形でアクセスすることで、ファイルを開けます。
  • ファイルをブラウザにドラッグ&ドロップでも開けます


3.4. カスタマイズを行う場合

3.4.1. 経由地をカスタマイズする場合は

multiwaypoint_route_generator.pymain()関数内の以下の部分を編集します。

def main():
    # 経由地を指定して地図を生成
    places = ["東京駅", "名古屋駅", "大阪駅"]  # ← ここを変更
    create_map_with_route(places, output_file="route_map.html")

横浜駅京都駅を経由地として追加した場合。

静岡駅では、別の県の別の場所がプロットされてしましました。これは情報が足りないためのようです。その場合はもう少し住所の情報を追加すると良いようです。

3.4.2. マーカーの色を変更

folium.Marker(
    location=coord,
    icon=folium.Icon(
        color='green',  # 色を変更(red, blue, green, purple, orange, darkred, etc.)
        icon='star',    # アイコンを変更
        prefix='glyphicon'
    )
).add_to(m)

3.4.3. ルートの色・太さを変更

folium.PolyLine(
    route_coords,
    color='green',    # 色
    weight=8,         # 太さ
    opacity=0.9       # 不透明度
).add_to(m)

3.4.4. 地図のスタイルを変更

m = folium.Map(
    location=[center_lat, center_lon],
    zoom_start=7,
    tiles='Stamen Terrain'  # 'OpenStreetMap', 'Stamen Terrain', 'Stamen Toner'
)

4. 使い分け

ステップ2のルート表示はURLを生成する処理になっていましたが、ステップ3ではHTMLファイルを生成しています。少し処理が異なるので使い分けに関しても記述しておきます。

項目 ステップ2の機能(URL生成) ステップ3の機能(地図HTML生成)
対応する地点数 2地点のみ 2地点以上(経由地OK)
出力 URL HTMLファイル
表示方法 URLをブラウザで開く ファイルをダブルクリック
利点 シンプル、すぐ開ける 複数経由地対応、カスタマイズ可能
使用技術 OpenStreetMap UI Folium + OSRM API
推奨用途 簡単な2地点ルート 複数経由地のある旅行計画

おわりに

今回は以下の機能を3つのステップで実装しました。

GoogleMapAPIも良いですが、オープンソースOpenStreetMapを使うことで、無料で自由に地図アプリケーションを作成できます。Pythonの豊富なライブラリを活用することで、簡単に高度な地図機能を実装できるのが魅力ですね🤩

ぜひ、みなさんも活用してみてください!