Directions API cho Việt Nam: Chỉ Đường Xe Máy & Turn-by-Turn Tương Thích Google
Hướng dẫn lập trình viên tích hợp Directions API GoGoDuk: gọi /v1/directions, đọc ETA và quãng đường thực, giải mã polyline vẽ lên bản đồ, hiển thị chỉ đường rẽ từng bước và xử lý fallback ESTIMATED.
Phần lớn tính năng vị trí trong các sản phẩm Việt Nam bắt đầu bằng một vòng tròn bán kính: "giao hàng trong 5 km." Cách này dễ triển khai nhưng gần như luôn sai. Ở các đô thị thực tế, hai điểm cách nhau 3 km theo đường chim bay có thể là một cuốc xe 6 km khi tính đến sông ngòi, đường một chiều, cầu cống và hẻm nhỏ—và xe máy đi đường rất khác ô tô. Nếu phí giao hàng, ETA hay việc gán tài xế của bạn dựa trên khoảng cách chim bay, nó sẽ lệch khỏi thực tế ngay khi giao thông đông đúc.
Bài viết này hướng dẫn tích hợp Directions API của GoGoDuk từ đầu đến cuối: gọi GET /v1/directions, đọc quãng đường và ETA thực, giải mã polyline tuyến đường để vẽ lên bản đồ, hiển thị chỉ đường rẽ từng bước, và xử lý fallback đường thẳng một cách gọn gàng. Response được thiết kế tương thích Google Directions, nên nếu bạn đã quen cấu trúc đó, phần lớn code hiện tại dùng lại được.
Vì sao chỉ đường thực tế tốt hơn khoảng cách chim bay
Một engine định tuyến trả lời câu hỏi khác hẳn công thức khoảng cách. Thay vì "hai điểm này cách nhau bao xa," nó trả lời "phương tiện thực sự đi đường nào, và mất bao lâu." Với Việt Nam, chi tiết về phương tiện chính là mấu chốt:
- Xe máy và ô tô đi đường khác nhau. Xe máy luồn được ngõ hẻm, làn nhỏ mà ô tô không vào được, và được miễn một số hạn chế dành riêng cho ô tô. Định tuyến theo hồ sơ ô tô sẽ ước lượng dư cả quãng đường lẫn thời gian so với cách phần lớn cuốc giao hàng ở Việt Nam thực sự di chuyển.
- Khoảng cách chim bay vừa tính thiếu phí vừa hứa quá đà. Phí theo bán kính rẻ trong các tình huống dễ nhưng sai nặng khi qua sông hoặc trong lưới đường một chiều. ETA theo bán kính hứa thời gian giao mà tài xế không thể đạt.
- Đồ thị đường luôn thay đổi. Mạng đường thực có hạn chế rẽ, ngõ cụt và đoạn đứt kết nối. Chỉ một engine định tuyến đi qua đồ thị mới nắm được chúng.
Engine định tuyến của GoGoDuk chạy trên đồ thị đường OpenStreetMap được tinh chỉnh cho hồ sơ motobike, nên tuyến đường—và thời gian dự kiến—khớp với cách một người chạy xe máy thực sự sẽ đi. Nếu bạn mới dùng nền tảng này, bài tổng quan GoGoDuk dành cho lập trình viên giới thiệu phần còn lại của API (geocoding, suggest, ranh giới hành chính).
Tổng quan endpoint chỉ đường
Endpoint chỉ là một request GET. Bạn truyền điểm đầu và điểm cuối dưới dạng cặp lat,lng (cùng thứ tự với Google và Goong) cùng một hồ sơ phương tiện.
| Tham số | Bắt buộc | Định dạng | Ghi chú |
| --- | --- | --- | --- |
| origin | Có | lat,lng | Điểm đầu, WGS84 (vĩ độ trước) |
| destination | Có | lat,lng | Điểm cuối, WGS84 |
| vehicle | Không | enum | motobike (mặc định). car sẽ có sau. Giá trị khác trả về 400 INVALID_VEHICLE. |
Xác thực dùng header X-API-Key (giữ key phía server), và key cần scope routing:read. Một lệnh gọi tối thiểu trông như sau:
curl -G "https://api.gogoduk.com/v1/directions" \
-H "X-API-Key: $GOGODUK_API_KEY" \
--data-urlencode "origin=21.0285,105.8542" \
--data-urlencode "destination=21.2212,105.8072" \
--data-urlencode "vehicle=motobike"Response tương thích Google Directions
Response phản chiếu cấu trúc của Google Directions API, nên đội ngũ đang chuyển từ Google hay Goong hiếm khi phải đổi lại hình dạng dữ liệu. Cấu trúc là routes[] → legs[] → steps[], với cờ status ở trên cùng:
| Trường | Ý nghĩa |
| --- | --- |
| status | OK cho tuyến đường thật, ESTIMATED cho fallback đường thẳng |
| routes[].legs[].distance | { text: "5.7 km", value: 5704.1 } — value tính bằng mét |
| routes[].legs[].duration | { text: "9 phút", value: 511.2 } — value tính bằng giây |
| routes[].legs[].steps[] | Các bước rẽ với html_instructions, maneuver và polyline riêng từng bước |
| routes[].overview_polyline.points | Polyline đã mã hoá cho toàn tuyến (precision 5) |
| routes[].bounds | { northeast, southwest } để canh khung nhìn bản đồ |
Một response rút gọn:
{
"status": "OK",
"routes": [
{
"legs": [
{
"distance": { "text": "5.7 km", "value": 5704.1 },
"duration": { "text": "9 phút", "value": 511.2 },
"steps": [
{
"distance": { "text": "243 m", "value": 243.4 },
"html_instructions": "Head northwest on Pasteur",
"maneuver": "turn-right",
"polyline": { "points": "suw`Aq{fjSOF{Bx@..." },
"travel_mode": "DRIVING"
}
]
}
],
"overview_polyline": { "points": "suw`Aq{fjS..." }
}
]
}Nếu bạn đã parse Google Directions, tên các trường trùng khớp. Hai điều cần lưu ý là đơn vị (distance.value theo mét, duration.value theo giây) và cờ status—nơi chứa hành vi fallback.
Gọi API và đọc ETA cùng quãng đường
Gọi endpoint từ phía server, rồi đọc distance và duration ở cấp leg. Hai con số đó thường là tất cả những gì một luồng giao hàng hay đặt xe cần cho việc tính phí và ước lượng thời gian đến.
// Phía server: lấy tuyến đường rồi đọc quãng đường + ETA thực
type LatLng = { lat: number; lng: number };
async function getRoute(origin: LatLng, destination: LatLng) {
const params = new URLSearchParams({
origin: `${origin.lat},${origin.lng}`,
destination: `${destination.lat},${destination.lng}`,
vehicle: "motobike",
});
const res = await fetch(`https://api.gogoduk.com/v1/directions?${params}`, {
headers: { "X-API-Key": process.env.GOGODUK_API_KEY! },
});
if (!res.ok) throw new Error(`Directions thất bại: ${res.status}`);
const data = await res.json();
const leg = data.routes?.[0]?.legs?.[0];
return {
status: data.status as "OK" | "ESTIMATED",
distanceMeters: leg?.distance?.value ?? 0,
durationSeconds: leg?.duration?.value ?? 0,
distanceText: leg?.distance?.text ?? "",
durationText: leg?.duration?.text ?? "",
overview: data.routes?.[0]?.overview_polyline?.points as string | undefined,
};
}Vì cấu trúc trùng với Google, việc chuyển đổi chủ yếu là đổi URL và header xác thực. Bản Google đọc data.routes[0].legs[0].distance.value y hệt—bạn trỏ cùng một parser tới https://api.gogoduk.com/v1/directions với header X-API-Key thay cho tham số key= trên URL, đúng kiểu hoán đổi đã mô tả trong bài chuyển từ Google Maps sang API bản đồ Việt Nam.
Khi đã có distanceMeters và durationSeconds, việc tính phí trở thành một hàm thuần: phí cơ bản cộng đơn giá theo kilômét, và một ETA bạn hiển thị cho khách qua durationText.
Vẽ tuyến đường: giải mã polyline
overview_polyline.points là một polyline mã hoá ở precision 5—đúng định dạng Google dùng—nên mọi decoder chuẩn đều chạy được. Giải mã nó thành mảng [lat, lng] rồi vẽ thành đường trên MapLibre, Leaflet hoặc Mapbox.
import polyline from "@mapbox/polyline"; // hoặc @googlemaps/polyline-codec
// `overview` là routes[0].overview_polyline.points
function toGeoJsonLine(overview: string) {
const coords = polyline
.decode(overview, 5) // precision 5
.map(([lat, lng]) => [lng, lat]); // GeoJSON là [lng, lat]
return {
type: "Feature" as const,
geometry: { type: "LineString" as const, coordinates: coords },
properties: {},
};
}
// Ví dụ MapLibre GL
map.addSource("route", { type: "geojson", data: toGeoJsonLine(overview) });
map.addLayer({
id: "route",
type: "line",
source: "route",
paint: { "line-color": "#2563eb", "line-width": 5 },
});Điểm dễ vấp duy nhất là precision: giải mã với 5, và nhớ rằng decoder trả về [lat, lng] trong khi GeoJSON cần [lng, lat]. Dùng bounds của tuyến đường để canh khung nhìn sao cho toàn bộ đường hiện ra ngay lần vẽ đầu.
Chỉ đường rẽ từng bước từ mảng steps
Với giao diện điều hướng, hãy duyệt legs[].steps[]. Mỗi bước mang một html_instructions đọc được, một mã maneuver kiểu Google (turn-left, turn-right, roundabout-right, …) tuỳ chọn, distance riêng, và một polyline từng bước để bạn tô sáng khi tài xế di chuyển.
type Step = {
html_instructions: string;
maneuver?: string;
distance: { text: string; value: number };
polyline: { points: string };
};
function toDirectionsList(legs: { steps: Step[] }[]) {
return legs.flatMap((leg) =>
leg.steps.map((step) => ({
instruction: step.html_instructions, // vd "Head northwest on Pasteur"
maneuver: step.maneuver ?? "straight", // ánh xạ sang bộ icon của bạn
distanceText: step.distance.text, // "243 m"
})),
);
}Ánh xạ mỗi giá trị maneuver sang một icon trong design system của bạn, rồi render danh sách dưới bản đồ. Vì các mã trùng với Google, một bảng ánh xạ icon đã làm sẵn cho Google Directions có thể dùng lại nguyên vẹn.
Luôn có đường để vẽ: xử lý ESTIMATED
Khi không có tuyến đường thật giữa hai điểm—một hòn đảo, một mảng mạng đường bị đứt, hoặc toạ độ sai—API không trả lỗi. Thay vào đó nó trả về đường ước lượng (đường thẳng) để giao diện luôn có thứ để vẽ:
statuschuyển thành"ESTIMATED".distancelà khoảng cách chim bay nhân hệ số đường bộ;durationước lượng theo vận tốc trung bình.- Leg chỉ có một step duy nhất từ điểm đầu tới điểm cuối.
Hãy rẽ nhánh theo status để một đường ước lượng không bao giờ bị nhầm là tuyến thật:
const route = await getRoute(origin, destination);
if (route.status === "ESTIMATED") {
// Vẽ nét đứt và gắn nhãn đây là ước lượng
map.setPaintProperty("route", "line-dasharray", [2, 2]);
showBadge("Ước lượng — không tìm thấy tuyến đường thật");
} else {
map.setPaintProperty("route", "line-dasharray", [1]); // nét liền
}Một nguyên tắc đơn giản: vẽ tuyến ESTIMATED bằng nét đứt kèm nhãn, và cân nhắc chặn checkout (hoặc đánh dấu để kiểm tra) khi một báo giá giao hàng dựa trên ước lượng thay vì tuyến đường thật.
Ghép lại: luồng tính ETA giao hàng
Đây là cách các mảnh ghép lại cho một luồng "báo giá giao hàng" điển hình:
- Thu thập cả hai địa chỉ. Dùng
GET /v1/suggestcho gợi ý địa chỉ và phân giải kết quả được chọn thành toạ độ. Chi tiết UX—debounce, điều hướng bàn phím, tỷ lệ chuyển đổi—nằm trong bài gợi ý địa chỉ Việt Nam cho UX thanh toán. - Yêu cầu tuyến đường. Gọi
getRoute(pickup, dropoff)vớivehicle=motobike. - Tính phí và hứa thời gian. Tính phí từ
distanceMeters(cơ bản + theo km) và hiển thị ETA từdurationText. - Vẽ nó ra. Giải mã
overview_polylinelên bản đồ và render chỉ đường rẽ từng bước từsteps. - Canh các trường hợp biên. Nếu
status === "ESTIMATED", đánh dấu báo giá là gần đúng.
Nếu việc tính phí của bạn còn phụ thuộc điểm giao rơi vào quận/huyện hay phường/xã nào, hãy kết hợp với reverse geocoding và tra cứu ranh giới—xem bài làm việc với ranh giới hành chính Việt Nam cho logic phân vùng trên nền tuyến đường.
Tóm lại: định tuyến xe máy thực tế thay một con số phỏng đoán bằng một con số bạn có thể tính phí và một ETA bạn có thể giữ lời. Response tương thích Google, nên phần lớn việc tích hợp chỉ là đổi URL và header cộng một bước kiểm tra status cho fallback. Tạo key với scope routing:read trong dashboard (gói miễn phí 100 request/ngày, không cần thẻ), khám phá các endpoint trong tài liệu GoGoDuk, và thử một tuyến đường trực tiếp trên bản đồ Việt Nam tương tác.
Hệ thống của bạn đang gặp vấn đề hiệu năng hay mở rộng tải?
Tôi chuyên xây dựng hạ tầng bản đồ độ trễ thấp, streaming pipeline thời gian thực (Kafka, ClickHouse) và các hệ thống backend tối ưu. Hãy cùng hợp tác để nâng cấp sản phẩm của bạn.
Bài viết liên quan
12 Jun 2026
Chuyển từ Google Maps API sang API Bản Đồ Việt Nam: Chi Phí & Code
So sánh chi phí và hạn chế của Google Maps Platform tại Việt Nam, rồi chuyển geocoding, reverse geocoding và gợi ý địa chỉ sang GoGoDuk kèm code thực tế.
8 Jun 2026
Redis 8.8 Trình Làng: Cấu Trúc Array Mới, Native Rate Limiter Và Bước Nhảy Vọt Hiệu Năng
Đánh giá kỹ thuật chi tiết các tính năng mới trong bản phát hành Redis 8.8 (02/06/2026). Tìm hiểu cơ chế Array O(1) mới của antirez, lệnh rate limiter INCREX bản địa và Subkey notifications cấp độ field của Hash.
8 Jun 2026
PostGIS Performance Tuning: Từ 2s Xuống 10ms Cho Truy Vấn Địa Lý Việt Nam (Gogoduk Case Study)
Khám phá các kỹ thuật tối ưu hóa cơ sở dữ liệu PostGIS thực tế từ dự án Gogoduk Map API. Tìm hiểu cách chuyển dịch từ Geometry sang Geography, thiết kế Partial GIST Index và rút gọn đa giác để đạt tốc độ truy vấn 10ms.
8 Jun 2026
Redis Lua Script & SETNX: Giải Pháp Rate Limiting & Quota Alerting Hiệu Năng Cao Cho API
Tìm hiểu cách Gogoduk xây dựng hệ thống Rate Limiting và Quota Alerting cho API bằng Redis và Go. Hướng dẫn sử dụng Lua Script đảm bảo tính nguyên tử, chống rò rỉ RAM và dùng SETNX chống trùng lặp thông báo.