この記事はこんな人におすすめ!
- JupyterLab 上で Folium を使い、地図をインタラクティブに操作したい人
- NMEA/GPS ログ(.log/.txt/.nmea)を地図に描画して素早く可視化したい人
- 地図をクリックして「出発地/目的地」を指定し、Googleマップのルートを自動で開きたい人
- 散歩・サイクリング・ドライブ・フィールド録音などの移動軌跡を記録・再現したい人
- ブログや案件デモで“動く地図+ナビ連携”を最小コードで実装したいエンジニア/クリエイター
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_()))
使い方
- 地図をクリック → ポップアップの 「出発地に設定」 を押す
- 別の地点をクリック → 「目的地に設定」 を押す
- 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
使い方

- セル実行 → 軌跡が描画されたFolium地図が表示
- 地図をクリック → ポップアップの
「出発地に設定」(Set Origin)/「目的地に設定」(Set Destination) - 2点が揃った瞬間(目的地を設定ボタンを押すと)自動でGoogleマップのルート検索が新しいタブで開きます
新しいタブが開かない場合は、ブラウザの「ポップアップとリダイレクトを許可」を一時的にオンにしてください。