配送またはライドシェアアプリにおいて、追跡マップはユーザー参加度が最も高い画面です。お客様は配達を待つ間、この画面を見続けます。スムーズな動き、正確なETA、明確なルート線を実装することが、プロフェッショナルで信頼できるアプリと、そうでないアプリの違いを生み出します。
このチュートリアルでは、顧客向けの追跡マップを一から構築します。WebSocketでGPS位置情報をブロードキャストするバックエンド、それを受け取ってドライバーマーカーをジャンプさせずに移動させるフロントエンド、MapAtlas Routing APIからのルート線、そしてライブETA表示です。完全な実装は70行未満のクライアント側JavaScriptであり、WebSocketメッセージを送信できるあらゆるバックエンドと統合できるよう設計されています。
このアーキテクチャは食品配達、食料品配達、ライドシェア、フィールドサービス、および車両が固定目的地に向かい顧客がリアルタイムで見守るあらゆるユースケースに対応しています。
アーキテクチャの概要
コーディングの前に、データフローを理解することが役に立ちます。
- ドライバーアプリ(モバイル、GPSハードウェア)が3~5秒ごとに緯度経度をバックエンドに送信します。
- バックエンド(Node.js、Python、Go、お好みで選択)が最後の既知の位置を保存し、WebSocketで接続されているすべての注文購読者にブロードキャストします。
- 顧客ブラウザがWebSocketメッセージを受け取り、補間アニメーションを使用してマップ上のマーカーを移動させます。
- Routing APIは注文作成時に一度だけ呼ばれ、計画されたルートを取得します。デコードされたポリラインはラインレイヤーとして表示されます。
- ETAは残りの距離を平均速度と比較するか、ドライバーの現在の位置からRouting APIを再度呼び出すことで再計算されます。
バックエンドの実装はこのチュートリアルの範囲外ですが、以下の形式でメッセージを送信するあらゆるWebSocketサーバーがフロントエンドコードと動作します。
{
"type": "position_update",
"orderId": "order-8821",
"lat": 52.3741,
"lng": 4.8952,
"heading": 92,
"speed": 28,
"timestamp": 1738234521000
}
ステップ1: マップの初期化
配達の出発地を中心としてマップをセットアップします。Routing APIの呼び出しはステップ4で行われるため、ここではキャンバスを初期化するだけです。
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'tracking-map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 14
});
// 目的地マーカー(レストランまたはピックアップポイント)
const destination = [4.9001, 52.3791];
new mapmetricsgl.Marker({ color: '#EF4444' })
.setLngLat(destination)
.setPopup(new mapmetricsgl.Popup().setHTML('<strong>Pickup point</strong>'))
.addTo(map);
ステップ2: ドライバーマーカーとヘディング回転
ドライバーマーカーを別途作成して、GPSピングのたびに位置を更新できるようにします。カスタムHTML要素を使うことで、マーカーアイコンをドライバーのヘディングを反映するように回転させることができます。この小さなディテールが追跡をより現実的に感じさせます。
// カスタム要素なので回転させることができます
const driverEl = document.createElement('div');
driverEl.innerHTML = '🚗';
driverEl.style.cssText = 'font-size:28px;transform-origin:center;transition:transform 0.3s';
const driverMarker = new mapmetricsgl.Marker({ element: driverEl, anchor: 'center' })
.setLngLat([4.9041, 52.3676])
.addTo(map);
function setDriverHeading(heading) {
driverEl.style.transform = `rotate(${heading}deg)`;
}
ステップ3: WebSocket接続とスムーズな補間
これは追跡マップのコアです。WebSocketへの接続は1行ですが、興味深い部分はGPSピング間でマーカー位置を補間して、瞬間移動ではなく滑らかに移動させることです。
let prevPos = null; // { lat, lng }
let animFrame = null;
function interpolateMarker(fromLat, fromLng, toLat, toLng, durationMs) {
const startTime = performance.now();
function step(now) {
const elapsed = now - startTime;
const t = Math.min(elapsed / durationMs, 1); // 0 → 1
const lat = fromLat + (toLat - fromLat) * t;
const lng = fromLng + (toLng - fromLng) * t;
driverMarker.setLngLat([lng, lat]);
if (t < 1) {
animFrame = requestAnimationFrame(step);
}
}
if (animFrame) cancelAnimationFrame(animFrame);
animFrame = requestAnimationFrame(step);
}
const ws = new WebSocket('wss://your-backend.example.com/track/order-8821');
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== 'position_update') return;
const { lat, lng, heading } = msg;
if (prevPos) {
// 前の位置から新しい位置へのスムーズなアニメーション
interpolateMarker(prevPos.lat, prevPos.lng, lat, lng, 400);
} else {
// 最初のピング、マーカーを即座に配置
driverMarker.setLngLat([lng, lat]);
map.flyTo({ center: [lng, lat], zoom: 15 });
}
setDriverHeading(heading);
updateETA(lat, lng);
prevPos = { lat, lng };
});
ws.addEventListener('close', () => {
console.log('Driver has arrived or connection closed.');
});
400msの補間ウィンドウは典型的なGPSピング間隔の3~5秒と妥当にマッチし、マーカーは常に現実より少し遅れていますが、目立つジャンプは決して起こりません。
ステップ4: Routing APIから計画されたルートを描画
注文が割り当てられたときにフルルートを取得します。ポリライン座標を保存し、GeoJSONラインレイヤーとして描画します。Route Optimization APIチュートリアルはマルチストップシナリオをカバーしていますが、シンプルなA-to-B配達の場合、リクエストは簡潔です。
async function fetchAndDrawRoute(originLat, originLng, destLat, destLng) {
const url = new URL('https://api.mapatlas.eu/v1/routing/route');
url.searchParams.set('origin', `${originLat},${originLng}`);
url.searchParams.set('destination', `${destLat},${destLng}`);
url.searchParams.set('profile', 'driving');
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (!data.routes?.length) return;
const route = data.routes[0];
map.on('load', () => {
map.addSource('route', {
type: 'geojson',
data: {
type: 'Feature',
geometry: route.geometry // GeoJSON LineString
}
});
map.addLayer({
id: 'route-line',
type: 'line',
source: 'route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: {
'line-color': '#3B82F6',
'line-width': 4,
'line-opacity': 0.8
}
});
});
return route.duration; // seconds
}
ステップ5: ETA計算と表示
ドライバーの現在位置を目的地と比較してETAを計算します。高い精度のために、30秒ごとにドライバーの現在位置からRouting APIを再度呼び出して、新しい移動時間推定値を取得します。
function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
let lastRouteFetch = 0;
async function updateETA(driverLat, driverLng) {
const [destLng, destLat] = destination;
const distKm = haversineKm(driverLat, driverLng, destLat, destLng);
// 迅速なクライアント側推定(都市部平均30 km/hと仮定)
const minutesEstimate = Math.ceil((distKm / 30) * 60);
document.getElementById('eta').textContent =
distKm < 0.1
? 'Arriving now'
: `ETA: ${minutesEstimate} min (${distKm.toFixed(1)} km)`;
// 精度のために30秒ごとにRouting APIから再取得
const now = Date.now();
if (now - lastRouteFetch > 30_000) {
lastRouteFetch = now;
const url = new URL('https://api.mapatlas.eu/v1/routing/route');
url.searchParams.set('origin', `${driverLat},${driverLng}`);
url.searchParams.set('destination', `${destLat},${destLng}`);
url.searchParams.set('profile', 'driving');
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (data.routes?.length) {
const mins = Math.ceil(data.routes[0].duration / 60);
document.getElementById('eta').textContent = `ETA: ${mins} min`;
}
}
}
ドライバー追跡のGDPR考慮事項
ドライバーのGPS座標はGDPR第4条の個人データです。EU食品配達およびライドシェアプラットフォームを管理する規制は、この点に関して明白です。
データ最小化: ディスパッチに必要なフィールドのみを追跡します。位置、ヘディング、速度。運用上必要な範囲を超えてGPS履歴を記録しないでください。
保持制限: 細粒度のトリップ追跡データは、注文が完了したら削除または不可逆的に匿名化する必要があります。集計されたルートデータ(個別のドライバーへのリンクなし)はネットワーク最適化のために長期保持できます。
法的根拠: 第6条(1)(f)に基づく正当な利益がリアルタイムディスパッチ追跡をカバーしています。追跡データの二次利用(分析、ベンチマーキング)については、別の根拠を文書化する必要があります。
ドライバーの透明性: ドライバーのオンボーディング時に明確な追跡開示を含めます。ドライバーは何が収集されているか、保持期間、誰がアクセスできるかを知る必要があります。
データ居住地: MapAtlasはすべてのAPI要求をEU内で処理します。これにより、米国ベースのマッピングプロバイダーから生じる第三国への転送の懸念が排除されます。詳細なコンプライアンス状況については、「EU開発者向けGDPR準拠マップAPIガイド」を参照してください。
特にライドシェアリング・モビリティ業界のユースケースについては、MapAtlasは標準としてDPA文書とEUサーバー保証を含みます。ロジスティクスおよび配達産業ページは、フリートおよびマルチドライバーシナリオをカバーしています。
本番環境対応
顧客に追跡機能を提供する前に、これらの項目をチェックしてください。
- WebSocket再接続: 指数バックオフを使用して
ws.addEventListener('close', reconnect)を追加します。モバイルネットワークは頻繁に接続を切ります。 - 古い位置の処理: 15秒以上アップデートが到着しない場合、最後の位置を表示したままにするのではなく、「ドライバーの位置を特定中」状態を表示します。
- 到着検出:
distKm < 0.1のとき、「到着」状態をトリガーし、WebSocketを閉じ、確認画面を表示します。 - カメラはドライバーをフォロー: 各位置アップデートで
map.panTo([lng, lat])を呼び出してドライバーを中央に保ちます。ユーザーにマップを探索したい場合はフォローモードを無効にする「ロック」トグルを与えます。
次のステップ
- 無料のMapAtlas APIキーにサインアップして構築を始めましょう
- Route Optimization APIチュートリアルを読んで、配達アプリにマルチストップディスパッチを追加してください
- 不動産物件マップチュートリアルで、動的でデータ駆動のマップレイヤーの別の例を探索してください
よくある質問
ドライバーの位置をマップ上でスムーズに移動させるにはどうすればよいですか?
GPSピングは数秒ごとに到着し、マーカー位置を直接更新すると目立つジャンプが生じます。スムーズな補間は、マーカーを前の位置から新しい位置に短い期間(300~500ms)かけてアニメーションし、requestAnimationFrame を使ってマーカーを小さなインクリメントで移動させます。これにより、不定期なGPSアップデートでも連続した動きの外観が得られます。
ドライバーの位置データはGDPRの対象ですか?
はい。ドライバーのリアルタイムGPS座標はGDPR第4条の個人データです。EU食品配達およびライドシェアプラットフォームはデータ保持を最小化する必要があり、追跡データはトリップが完了したら削除または匿名化する必要があります。処理には法的根拠が必要であり、ドライバーのプライバシー通知に開示される必要があります。
MapAtlas Routing APIを使用して、追跡マップに計画されたルートを表示できますか?
はい。トリップが作成されたときにRouting APIからルートを取得し、ポリラインをデコードし、GeoJSONラインレイヤーとしてマップに追加します。ドライバーが移動するにつれて、必要に応じて現在の位置からルートを再取得してETAを動的に再計算することができます。
