Foliumでクリックした2点がそろったら自動でGoogleマップの経路を開く方法

この記事はこんな人におすすめ!

  • JupyterLab 上で Folium を使い、地図をインタラクティブに操作したい人
  • NMEA/GPS ログ(.log/.txt/.nmea)を地図に描画して素早く可視化したい人
  • 地図をクリックして「出発地/目的地」を指定し、Googleマップのルートを自動で開きたい人
  • 散歩・サイクリング・ドライブ・フィールド録音などの移動軌跡を記録・再現したい人
  • ブログや案件デモで“動く地図+ナビ連携”を最小コードで実装したいエンジニア/クリエイター
逆に、オフライン環境だけでターンバイターンの音声ナビまで完結させたい人や、 Python/Jupyter のセットアップが難しい環境では本手法は不向きです(その場合は GPX 対応の専用ナビアプリを推奨)。

Pythonの地図可視化ライブラリ Folium を使うと、Jupyter Notebook 上でインタラクティブな地図を表示できます。
さらに一工夫すると、クリックした地点を Googleマップのルート案内(ナビ機能) にそのまま渡すことが可能です。

本記事では、Foliumで「出発地(Origin)」「目的地(Destination)」を指定し、ワンクリックでGoogleマップのナビ画面を開く方法を解説します。

ライブラリ

以下のライブラリが必要ですので、追加してください。
GPS関係の環境がすでにできている方はこちらのみ。

pip install folium branca jinja2

これから構築する方は

python -m pip install -U pip
python -m pip install -U folium branca jinja2 ipython gpxpy simplekml

condaの方は

conda install -c conda-forge folium branca jinja2 ipython gpxpy simplekml -y
Package Role(役割) Notes(補足)
folium Leaflet の Python ラッパー。地図生成・タイル表示・レイヤ追加。 本プロジェクトの中核(必須)。
branca Folium の土台ユーティリティ。HTML/CSS/JS の要素管理。 Folium とセットで利用(実質必須)。
jinja2 HTML テンプレートエンジン。Folium のテンプレート展開に使用。 カスタムJS挿入やテンプレート拡張で必須。
ipython Jupyter 上での表示ユーティリティ。Notebook に地図をインライン表示。 Jupyter 環境なら通常インストール済み。
gpxpy GPX ファイルの読み書き(トラック・ウェイポイント)。 GPX に書き出す場合のみ(任意)。
simplekml KML 生成(ラインストリング等)。 KML に書き出す場合のみ(任意)。

完成イメージ

  • Folium地図をクリック → ポップアップに 緯度経度「出発地に設定」「目的地に設定」
  • 2点がそろったその瞬間に、Googleマップのルート検索自動で新タブで開く
  • モードは既定で driving(車)。必要なら引数で walking / bicycling / transit に変更可能

実装コード(Jupyter用)

実装:コピペで動く最小コード(自動オープンのみ)

そのまま Jupyter セルに貼り付けて実行してください。
マップは北海道・足寄町あたりを初期中心にしています。お好みで座標を変えてください。

import folium
from branca.element import MacroElement
from jinja2 import Template
from IPython.display import HTML, display

class ClickNavAutoOpen(MacroElement):
    """
    クリックした地点を「出発地」「目的地」に設定する簡易UIをポップアップ内に提供。
    2点がそろった瞬間に、GoogleマップのDirections(/dir/?api=1)を
    自動で新しいタブで開きます(ユーザー操作起点なのでポップアップブロックに強い)。

    Args:
        travel_mode (str): 'driving' | 'walking' | 'bicycling' | 'transit'

    Example:
        m = folium.Map(location=[43.2, 143.6], zoom_start=14)
        m.add_child(ClickNavAutoOpen(travel_mode="driving"))
        display(HTML(m._repr_html_()))
    """
    _template = Template(u"""
        {% macro script(this, kwargs) %}
        var map = {{ this._parent.get_name() }};
        var originMarker = null, destMarker = null;
        var travelMode = "{{ this.travel_mode }}";  // 既定: driving

        function fmt(n){ return n.toFixed(6); }

        // Directions 用URLを作成
        function buildDirURL(){
            if(!(originMarker && destMarker)) return null;
            var o = originMarker.getLatLng(), d = destMarker.getLatLng();
            return 'https://www.google.com/maps/dir/?api=1'
                   + '&origin='      + fmt(o.lat) + ',' + fmt(o.lng)
                   + '&destination=' + fmt(d.lat) + ',' + fmt(d.lng)
                   + '&travelmode='  + encodeURIComponent(travelMode);
        }

        // 2点そろったら新タブで自動オープン(ボタンなし)
        function openIfReady(){
            var url = buildDirURL();
            if(url){
                try { window.open(url, '_blank', 'noopener'); } catch(e){}
            }
        }

        // クリック時に出すポップアップHTML
        function popupHTML(lat, lng){
            var la = fmt(lat), ln = fmt(lng);
            var gms = 'https://www.google.com/maps/search/?api=1&query=' + la + ',' + ln;
            return `
              <div style="min-width:240px">
                <div><b>緯度 (Lat):</b> ${la}<br><b>経度 (Lng):</b> ${ln}</div>
                <div style="margin-top:6px">
                  <a href="${gms}" target="_blank" rel="noopener">Googleマップでこの地点を見る</a>
                </div>
                <div style="margin-top:8px; display:flex; gap:8px; flex-wrap:wrap;">
                  <button id="set-origin" style="padding:4px 8px">出発地に設定</button>
                  <button id="set-dest"   style="padding:4px 8px">目的地に設定</button>
                </div>
              </div>`;
        }

        // 地図をクリック → マーカー + ポップアップ
        map.on('click', function(e){
            var marker = L.marker(e.latlng).addTo(map);
            marker.bindPopup(popupHTML(e.latlng.lat, e.latlng.lng)).openPopup();

            marker.on('popupopen', function(){
                var root = marker.getPopup().getElement();
                if(!root) return;

                // 出発地に設定
                var btnO = root.querySelector('#set-origin');
                if(btnO){
                    btnO.addEventListener('click', function(){
                        if(originMarker) map.removeLayer(originMarker);
                        originMarker = L.marker(e.latlng).addTo(map)
                            .bindTooltip('出発地', {permanent:true, direction:'top'});
                        openIfReady();  // 目的地が既にあるなら即オープン
                    });
                }

                // 目的地に設定
                var btnD = root.querySelector('#set-dest');
                if(btnD){
                    btnD.addEventListener('click', function(){
                        if(destMarker) map.removeLayer(destMarker);
                        destMarker = L.marker(e.latlng).addTo(map)
                            .bindTooltip('目的地', {permanent:true, direction:'top'});
                        openIfReady();  // 出発地が既にあるなら即オープン
                    });
                }
            });
        });
        {% endmacro %}
    """)

    def __init__(self, travel_mode: str = "driving"):
        super().__init__()
        self.travel_mode = travel_mode

# --- デモ(中心は任意) ---
m = folium.Map(location=[43.240162, 143.546634], zoom_start=15)  # 足寄町周辺
m.add_child(ClickNavAutoOpen(travel_mode="driving"))
folium.LatLngPopup().add_to(m)  # クリックだけで座標確認したいときに便利(任意)
display(HTML(m._repr_html_()))

使い方

  1. 地図をクリック → ポップアップの 「出発地に設定」 を押す
  2. 別の地点をクリック → 「目的地に設定」 を押す
  3. 2点そろった瞬間に自動で新タブが開き、Googleマップのルート検索ページに遷移します
    • 既に目的地がある状態で「出発地に設定」を押した場合も同様に即オープンします

ブラウザが厳格なポップアップ制御をしている場合、まれにブロックされることがあります。その際はブラウザの「ポップアップとリダイレクトを許可」をオンにしてください。


よくある質問(FAQ)

Q1. 移動モードを徒歩や自転車にしたい
A. クラス初期化時に travel_mode="walking"(または bicycling / transit)を渡してください。

Q2. クリック順序は決まっていますか?
A. いいえ。出発地→目的地、目的地→出発地のどちらでも、2点そろった時点で自動オープンします。

Q3. 1地点だけで開いてしまうことがある
A. ポップアップの「Googleマップでこの地点を見る」は単点検索です。ルートを開くのは2点がそろった時のみです。

仕組み

  • Googleマップのルート検索は /dir/?api=1 エンドポイントを使います
    origin=lat,lng&destination=lat,lng&travelmode=driving を組み立てて window.open
  • クリック時のボタン操作(ユーザー操作)直後に開くため、ポップアップブロックに強い実装です
  • 経由地(waypoints)に対応したい場合は、配列でクリック地点を保持し、&waypoints=lat,lng|lat,lng|... を付与すればOKです。
  • スマホでの運用も基本同様に動作します(端末のポップアップ設定に依存)。

取得したログデータも描画

ここからが重要なポイントですよね。

指定したフォルダ内のNMEAログを描画しつつ、「出発地/目的地」2点が揃った瞬間に自動でGoogleマップのルートを新規タブで開く最小実装です。

NMEAログのデータ変換はこちらの記事を参照してください。

【GPSログをGoogleマップに表示】|PythonでNMEAファイルをGPX/KMLに変換して可視化する手順

JupyterLabでコピペ実行できます(インデントはすべて4スペース)。

# -*- coding: utf-8 -*-
"""
JupyterLab 用:NMEAログの軌跡を描画 + 2点そろったら自動でGoogleマップの経路を開く

依存:
    pip install folium branca jinja2

使い方:
    1) 下の FOLDER をあなたのログフォルダに設定
    2) セルを実行 → 地図表示
    3) 地図をクリック → ポップアップの「出発地に設定」/「目的地に設定」を押す
    4) 2点が揃った瞬間に、通常のGoogleマップ(/dir/?api=1)が新しいタブで開く
"""

from pathlib import Path
from typing import Iterable, List, Optional, Tuple, Dict
import folium
from branca.element import MacroElement
from jinja2 import Template
from IPython.display import HTML, display

# ====== 設定 ======
FOLDER = "あなたのパス"  # ★対象フォルダ
DRAW_POINTS = False                           # 各点を点描画(多いと重い→通常は False)
ZOOM_START = 18                               # 初期ズーム
TRAVEL_MODE = "driving"                       # "driving" | "walking" | "bicycling" | "transit"
PATTERNS = ("*.log", "*.txt", "*.nmea")       # 探索する拡張子
# ==================


# ---------- NMEA 1行から (lat, lon) を抽出 ----------
def parse_nmea_latlon(nmea_sentence: str) -> Optional[Tuple[float, float]]:
    """
    $GPRMC/$GPGGA(+ 一部機種の $GNRMC/$GNGGA)から (lat, lon) を抽出。抽出不可は None。
    """
    s = nmea_sentence.strip()
    if "*" in s:
        s = s.split("*", 1)[0]
    parts = s.split(",")
    if not parts:
        return None
    head = parts[0]
    if head not in ("$GPRMC", "$GPGGA", "$GNRMC", "$GNGGA"):
        return None

    try:
        if head.endswith("RMC"):
            # RMC: [2]=Status A(有効), [3]=lat, [4]=N/S, [5]=lon, [6]=E/W
            status = parts[2] if len(parts) > 2 else ""
            if status != "A":
                return None
            lat_raw, lat_dir = parts[3], parts[4]
            lon_raw, lon_dir = parts[5], parts[6]
        else:
            # GGA: [2]=lat, [3]=N/S, [4]=lon, [5]=E/W
            lat_raw, lat_dir = parts[2], parts[3]
            lon_raw, lon_dir = parts[4], parts[5]

        if not lat_raw or not lon_raw or not lat_dir or not lon_dir:
            return None

        def dm_to_deg(dm: str, is_lat: bool) -> float:
            """度分(DDMM.MMMM / DDDMM.MMMM)を10進度に変換"""
            if is_lat:
                deg, minutes = float(dm[:2]), float(dm[2:])
            else:
                deg, minutes = float(dm[:3]), float(dm[3:])
            return deg + minutes / 60.0

        lat = dm_to_deg(lat_raw, True)
        lon = dm_to_deg(lon_raw, False)
        if lat_dir == "S":
            lat = -lat
        if lon_dir == "W":
            lon = -lon

        if not (-90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0):
            return None
        return (lat, lon)
    except Exception:
        return None


# ---------- 単一ファイルの座標列を抽出 ----------
def read_points_from_file(filepath: Path) -> List[Tuple[float, float]]:
    pts: List[Tuple[float, float]] = []
    try:
        with filepath.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                pos = parse_nmea_latlon(line)
                if pos:
                    pts.append(pos)
    except Exception:
        pass
    return pts


# ---------- フォルダ内のログを走査 ----------
def collect_points_by_file(folder: Path,
                           patterns: Iterable[str]) -> Dict[Path, List[Tuple[float, float]]]:
    out: Dict[Path, List[Tuple[float, float]]] = {}
    for pat in patterns:
        for fp in sorted(folder.glob(pat)):
            pts = read_points_from_file(fp)
            if pts:
                out[fp] = pts
    return out


# ---------- 中心(重心) ----------
def mean_center(all_points: List[Tuple[float, float]]) -> Tuple[float, float]:
    n = len(all_points)
    return (sum(p[0] for p in all_points) / n, sum(p[1] for p in all_points) / n)


# ---------- 1ファイル分のトラックをレイヤ追加 ----------
def add_track_layer(m: folium.Map,
                    points: List[Tuple[float, float]],
                    name: str,
                    draw_points: bool = False) -> None:
    fg = folium.FeatureGroup(name=name, show=True)
    folium.PolyLine(points, color="blue", weight=2.5, opacity=0.9).add_to(fg)
    (s_lat, s_lon), (e_lat, e_lon) = points[0], points[-1]
    folium.CircleMarker((s_lat, s_lon), radius=5, color="green",
                        fill=True, fill_color="green", tooltip=f"{name} START").add_to(fg)
    folium.CircleMarker((e_lat, e_lon), radius=5, color="red",
                        fill=True, fill_color="red", tooltip=f"{name} END").add_to(fg)
    if draw_points:
        for lat, lon in points:
            folium.CircleMarker((lat, lon), radius=2, color="blue",
                                fill=True, fill_color="blue").add_to(fg)
    fg.add_to(m)


# ---------- クリック → 2点そろったら自動でDirectionsを開く ----------
class ClickNavAutoOpen(MacroElement):
    """
    クリックした地点を出発地/目的地に設定するボタンをポップアップに表示。
    2点がそろった瞬間に GoogleマップのDirections (/dir/?api=1) を新タブで開く。
    """
    _template = Template(u"""
        {% macro script(this, kwargs) %}
        var map = {{ this._parent.get_name() }};
        var originMarker = null, destMarker = null;
        var travelMode = "{{ this.travel_mode }}";  // driving/walking/bicycling/transit

        function fmt(n){ return n.toFixed(6); }

        function buildDirURL(){
            if(!(originMarker && destMarker)) return null;
            var o = originMarker.getLatLng(), d = destMarker.getLatLng();
            return 'https://www.google.com/maps/dir/?api=1'
                   + '&origin='      + fmt(o.lat) + ',' + fmt(o.lng)
                   + '&destination=' + fmt(d.lat) + ',' + fmt(d.lng)
                   + '&travelmode='  + encodeURIComponent(travelMode);
        }

        function openIfReady(){
            var url = buildDirURL();
            if(url){
                try { window.open(url, '_blank', 'noopener'); } catch(e){}
            }
        }

        function popupHTML(lat, lng){
            var la = fmt(lat), ln = fmt(lng);
            var gms = 'https://www.google.com/maps/search/?api=1&query=' + la + ',' + ln;
            return `
              <div style="min-width:240px">
                <div><b>緯度 (Lat):</b> ${la}<br><b>経度 (Lng):</b> ${ln}</div>
                <div style="margin-top:6px">
                  <a href="${gms}" target="_blank" rel="noopener">Googleマップでこの地点を見る</a>
                </div>
                <div style="margin-top:8px; display:flex; gap:8px; flex-wrap:wrap;">
                  <button id="set-origin" style="padding:4px 8px">出発地に設定</button>
                  <button id="set-dest"   style="padding:4px 8px">目的地に設定</button>
                </div>
              </div>`;
        }

        map.on('click', function(e){
            var marker = L.marker(e.latlng).addTo(map);
            marker.bindPopup(popupHTML(e.latlng.lat, e.latlng.lng)).openPopup();

            marker.on('popupopen', function(){
                var root = marker.getPopup().getElement();
                if(!root) return;

                var btnO = root.querySelector('#set-origin');
                if(btnO){
                    btnO.addEventListener('click', function(){
                        if(originMarker) map.removeLayer(originMarker);
                        originMarker = L.marker(e.latlng).addTo(map)
                            .bindTooltip('出発地', {permanent:true, direction:'top'});
                        openIfReady();  // 既に目的地があれば即オープン
                    });
                }

                var btnD = root.querySelector('#set-dest');
                if(btnD){
                    btnD.addEventListener('click', function(){
                        if(destMarker) map.removeLayer(destMarker);
                        destMarker = L.marker(e.latlng).addTo(map)
                            .bindTooltip('目的地', {permanent:true, direction:'top'});
                        openIfReady();  // 既に出発地があれば即オープン
                    });
                }
            });
        });
        {% endmacro %}
    """)

    def __init__(self, travel_mode: str = "driving"):
        super().__init__()
        self.travel_mode = travel_mode


# ---------- フォルダからマップ作成 ----------
def make_map_from_folder(folder_path: str,
                         draw_points: bool = False,
                         zoom_start: int = 17,
                         travel_mode: str = "driving") -> folium.Map:
    folder = Path(folder_path)
    if not folder.exists() or not folder.is_dir():
        raise FileNotFoundError(f"Folder not found: {folder_path}")

    by_file = collect_points_by_file(folder, PATTERNS)
    if not by_file:
        raise ValueError("フォルダ内に座標が抽出できるログがありません(*.log, *.txt, *.nmea)。")

    all_points = [p for pts in by_file.values() for p in pts]
    m = folium.Map(location=mean_center(all_points), zoom_start=zoom_start)

    for fp, pts in by_file.items():
        add_track_layer(m, pts, fp.name, draw_points=draw_points)

    folium.LayerControl(collapsed=False).add_to(m)

    # クリック → 2点そろったら自動で Directions を開く
    m.add_child(ClickNavAutoOpen(travel_mode=travel_mode))

    # クリックだけで座標を即出すおまけ
    folium.LatLngPopup().add_to(m)
    return m


# ---------- Jupyter でインライン表示 ----------
def show_map_interactive(folder_path: str,
                         draw_points: bool = False,
                         zoom_start: int = 17,
                         travel_mode: str = "driving"):
    """
    対話型(Jupyter)表示用。地図をセル出力に描画して返す。
    """
    m = make_map_from_folder(folder_path,
                             draw_points=draw_points,
                             zoom_start=zoom_start,
                             travel_mode=travel_mode)
    display(HTML(m._repr_html_()))
    return m


# ---------- 実行 ----------
m = show_map_interactive(FOLDER,
                         draw_points=DRAW_POINTS,
                         zoom_start=ZOOM_START,
                         travel_mode=TRAVEL_MODE)
m

使い方

  1. セル実行 → 軌跡が描画されたFolium地図が表示
  2. 地図をクリック → ポップアップの
      「出発地に設定」(Set Origin)/「目的地に設定」(Set Destination)
  3. 2点が揃った瞬間(目的地を設定ボタンを押すと)自動でGoogleマップのルート検索が新しいタブで開きます

新しいタブが開かない場合は、ブラウザの「ポップアップとリダイレクトを許可」を一時的にオンにしてください。