0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Maps APIで距離指定ルート生成を実装する

0
Posted at

はじめに

今回は、地図アプリ等での活用を前提とした、スタート地点と目標距離を渡すとループコースを返す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では curlInvoke-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>

以下のようにブラウザ上で確認できればOK。
スクリーンショット 2026-03-04 204100.png


CORSエラーへの対処

Live ServerなどでHTMLを開くと、異なるポート間の通信がブロックされるCORSエラーが発生する。バックエンド側で cors パッケージを導入して解決した。

const cors = require('cors');
app.use(cors());

まとめ

今回は以下の項目を解説した。

  • Google Maps Platform(Routes API)のセットアップ完了
  • スタート地点と目標距離を渡すとループコースを返すAPIの実装
  • 生成されたコースの地図上への描画確認
  • APIキーをコードにハードコーディングしない構成
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?