はじめに
今回は、地図アプリ等での活用を前提とした、スタート地点と目標距離を渡すとループコースを返すAPIを実装する。
APIキーをコードにハードコーディングしない構成とする。
事前準備:Google Cloud Platformのセットアップ
GCPのアカウントと請求設定、プロジェクト作成が済んでいる前提で進める。
必要なAPIを有効化する
Google Cloud Consoleの「APIとサービス」→「ライブラリ」から以下の3つを有効化する。
- Routes API
- Places API (New) ※「Places API」と「Places API (New)」は別物なので注意
- Maps JavaScript API
APIキーを作成して制限をかける
「APIとサービス」→「認証情報」→「APIキー」で作成する。作成直後は制限がない状態なので、編集画面で「APIの制限」から上記3つのAPIのみを許可しておく。
本番用キーは別途作成するため、このキーには dev などの名前をつけておくとよい。
コスト上限のアラートを設定する
Routes APIとPlaces APIは従量課金なので、開発中に意図しない課金が発生するリスクがある。「お支払い」→「予算とアラート」から月額の上限目安(開発段階なら1,000〜3,000円程度)でアラートを設定しておく。
curlで動作確認
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Goog-Api-Key: MY_API_KEY" \
-H "X-Goog-FieldMask: routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline" \
'https://routes.googleapis.com/directions/v2:computeRoutes' \
-d '{
"origin": {
"location": {
"latLng": { "latitude": 35.6812, "longitude": 139.7671 }
}
},
"destination": {
"location": {
"latLng": { "latitude": 35.6895, "longitude": 139.6917 }
}
},
"travelMode": "WALK"
}'
以下のようなレスポンスが返れば成功。
{
"routes": [
{
"distanceMeters": 8115,
"duration": "7014s",
"polyline": {
"encodedPolyline": "..."
}
}
]
}
Windowsユーザーへの注意
PowerShellでは curl が Invoke-WebRequest のエイリアスになっており、上記の構文がそのまま動かない。curl.exe と明示するか、Git Bashを使うと解決する。
# PowerShellの場合はcurl.exeと書く
curl.exe -X POST ...
Node.jsバックエンドの実装
プロジェクトの初期化
mkdir my-project
cd my-project
npm init -y
npm install express dotenv axios cors
環境変数の設定
.env ファイルを作成してAPIキーを管理する。
GOOGLE_MAPS_API_KEY=MY_API_KEY
PORT=3000
.gitignore も忘れずに作成する。
node_modules/
.env
index.jsの実装
const express = require('express');
const axios = require('axios');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
// フロントエンドにMaps APIキーを渡すエンドポイント
// preview.htmlにAPIキーをハードコーディングしないための措置
app.get('/config', (req, res) => {
res.json({ mapsApiKey: process.env.GOOGLE_MAPS_API_KEY });
});
// スタート・ゴール固定のルート生成(動作確認用)
app.post('/generate-route', async (req, res) => {
const { originLat, originLng, destLat, destLng } = req.body;
try {
const response = await axios.post(
'https://routes.googleapis.com/directions/v2:computeRoutes',
{
origin: {
location: { latLng: { latitude: originLat, longitude: originLng } }
},
destination: {
location: { latLng: { latitude: destLat, longitude: destLng } }
},
travelMode: 'WALK'
},
{
headers: {
'Content-Type': 'application/json',
'X-Goog-Api-Key': process.env.GOOGLE_MAPS_API_KEY,
'X-Goog-FieldMask': 'routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline'
}
}
);
const route = response.data.routes[0];
res.json({
distanceMeters: route.distanceMeters,
durationSeconds: parseInt(route.duration),
polyline: route.polyline.encodedPolyline
});
} catch (error) {
console.error('Routes API エラー:', error.response?.data || error.message);
res.status(500).json({ error: 'ルートの生成に失敗しました' });
}
});
// 指定距離・方向からウェイポイント座標を計算する
function calcWaypoint(lat, lng, distanceKm, bearingDeg) {
const R = 6371;
const d = distanceKm / R;
const bearing = (bearingDeg * Math.PI) / 180;
const lat1 = (lat * Math.PI) / 180;
const lng1 = (lng * Math.PI) / 180;
const lat2 = Math.asin(
Math.sin(lat1) * Math.cos(d) +
Math.cos(lat1) * Math.sin(d) * Math.cos(bearing)
);
const lng2 = lng1 + Math.atan2(
Math.sin(bearing) * Math.sin(d) * Math.cos(lat1),
Math.cos(d) - Math.sin(lat1) * Math.sin(lat2)
);
return {
latitude: (lat2 * 180) / Math.PI,
longitude: (lng2 * 180) / Math.PI
};
}
// 距離指定でコースを生成するエンドポイント
app.post('/generate-course', async (req, res) => {
const { lat, lng, targetDistanceKm } = req.body;
// スタート地点から目標距離の1/5の距離にウェイポイントを2つ置く
// 係数の調整については後述
const wp1 = calcWaypoint(lat, lng, targetDistanceKm / 5, 90);
const wp2 = calcWaypoint(lat, lng, targetDistanceKm / 5, 180);
try {
const response = await axios.post(
'https://routes.googleapis.com/directions/v2:computeRoutes',
{
origin: {
location: { latLng: { latitude: lat, longitude: lng } }
},
destination: {
location: { latLng: { latitude: lat, longitude: lng } }
},
intermediates: [
{ location: { latLng: wp1 } },
{ location: { latLng: wp2 } }
],
travelMode: 'WALK'
},
{
headers: {
'Content-Type': 'application/json',
'X-Goog-Api-Key': process.env.GOOGLE_MAPS_API_KEY,
'X-Goog-FieldMask': 'routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline'
}
}
);
const route = response.data.routes[0];
res.json({
distanceMeters: route.distanceMeters,
durationSeconds: parseInt(route.duration),
polyline: route.polyline.encodedPolyline
});
} catch (error) {
console.error('Routes API エラー:', error.response?.data || error.message);
res.status(500).json({ error: 'コースの生成に失敗しました' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`サーバー起動: http://localhost:${PORT}`);
});
APIキーをフロントエンドにハードコーディングしない
地図の描画にはブラウザ側でもMaps JavaScript APIを読み込む必要があり、そのためにAPIキーが必要になる。しかしHTMLファイルにAPIキーを直接書くとGitHubに誤ってコミットするリスクがある。
今回は /config エンドポイントをバックエンドに設け、フロントエンド起動時にそこからAPIキーを取得する方式にした。
// preview.html側の実装
async function bootstrap() {
const configRes = await fetch("http://localhost:3000/config");
const config = await configRes.json();
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=${config.mapsApiKey}&callback=initMap&libraries=geometry`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
これでHTMLファイルにはAPIキーが一切含まれなくなり、.gitignore で .env を除外すればGitHubに安全にプッシュできる。
動作確認:地図上にコースを描画する
preview.html 全体のコードは以下の通り。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>コースプレビュー</title>
<style>
body { margin: 0; }
#map { height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
async function bootstrap() {
const configRes = await fetch("http://localhost:3000/config");
const config = await configRes.json();
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=${config.mapsApiKey}&callback=initMap&libraries=geometry`;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
async function initMap() {
const { Map } = await google.maps.importLibrary("maps");
const { encoding } = await google.maps.importLibrary("geometry");
const map = new Map(document.getElementById("map"), {
center: { lat: 35.6812, lng: 139.7671 },
zoom: 15,
});
const res = await fetch("http://localhost:3000/generate-course", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lat: 35.6812, lng: 139.7671, targetDistanceKm: 3 })
});
const data = await res.json();
const path = encoding.decodePath(data.polyline);
new google.maps.Polyline({
path,
geodesic: true,
strokeColor: "#FF6600",
strokeWeight: 4,
map,
});
console.log(`距離: ${(data.distanceMeters / 1000).toFixed(2)}km`);
console.log(`時間: ${Math.round(data.durationSeconds / 60)}分`);
}
bootstrap();
</script>
</body>
</html>
CORSエラーへの対処
Live ServerなどでHTMLを開くと、異なるポート間の通信がブロックされるCORSエラーが発生する。バックエンド側で cors パッケージを導入して解決した。
const cors = require('cors');
app.use(cors());
まとめ
今回は以下の項目を解説した。
- Google Maps Platform(Routes API)のセットアップ完了
- スタート地点と目標距離を渡すとループコースを返すAPIの実装
- 生成されたコースの地図上への描画確認
- APIキーをコードにハードコーディングしない構成
