NHT

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.

Nguyen Hoang TuanNguyen Hoang Tuan16 thg 6, 20269 phút đọc

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, maneuverpolyline 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 distanceduration ở 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ó distanceMetersdurationSeconds, 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ẽ:

  • status chuyển thành "ESTIMATED".
  • distance là 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:

  1. Thu thập cả hai địa chỉ. Dùng GET /v1/suggest cho 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.
  2. Yêu cầu tuyến đường. Gọi getRoute(pickup, dropoff) với vehicle=motobike.
  3. 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.
  4. Vẽ nó ra. Giải mã overview_polyline lên bản đồ và render chỉ đường rẽ từng bước từ steps.
  5. 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.

Hợp tác ngay

Tác giả

Nguyen Hoang Tuan

Nguyen Hoang Tuan

Full-stack developer focused on practical backend architecture, web performance, and production delivery.

Bài viết liên quan