Snap To Roads
Demo tương tác
Vẽ tuyến trực tiếp trên bản đồ để so sánh giữa tuyến thô (xanh dương) và tuyến đã được snap (đỏ). Nhấn biểu tượng bút chì để bật chế độ vẽ, click các điểm trên bản đồ, rồi click lại điểm cuối để hoàn tất — API Snap To Road sẽ được gọi tự động. Bật/tắt Interpolate để so sánh kết quả sau khi snap có hoặc không có nội suy.
Đang tải demo Snap To Roads…
Code example
- 1. Draw Polyline
- 2. Snap To Road
- 3. Draw Snapped Route
- 4. Clear Route
Khởi tạo bản đồ MapLibre GL JS, thu thập tuyến thô bằng cách vẽ trên bản đồ hoặc dán chuỗi lat,lng, rồi chuyển sang gọi API.
// Cấu hình & state
const API_KEY = 'YOUR_API_KEY';
const RAW_ROUTE_COLOR = '#3b82f6';
const SNAPPED_ROUTE_COLOR = '#ef4444';
const FINISH_CLICK_THRESHOLD_PX = 16;
let isDrawMode = false;
let drawingPoints = [];
let mousePosition = null;
let rawRoute = [];
let snappedRoute = [];
let interpolate = true;
let isSnapping = false;
let snapResult = null;
let snapError = null;
const map = new maplibregl.Map({
container: 'map',
style:
'{{API_BASE_URL}}/api/styles/v1/gtelmaps-streets-v1/style.json?apikey=' +
encodeURIComponent(API_KEY),
center: [105.85395, 21.02885],
zoom: 14,
});
map.addControl(new maplibregl.NavigationControl(), 'bottom-right');
// GeoJSON helpers
function lngLatToPoint(lngLat) {
return { lat: lngLat.lat, lon: lngLat.lng };
}
function isClickNearPoint(clickLngLat, point, thresholdPx) {
if (thresholdPx === undefined) {
thresholdPx = FINISH_CLICK_THRESHOLD_PX;
}
const pointPx = map.project([point.lon, point.lat]);
const clickPx = map.project(clickLngLat);
return Math.hypot(pointPx.x - clickPx.x, pointPx.y - clickPx.y) < thresholdPx;
}
function pointsToFeatureCollection(points) {
return {
type: 'FeatureCollection',
features: points.map(function (point, index) {
return {
type: 'Feature',
properties: { index: index },
geometry: { type: 'Point', coordinates: [point.lon, point.lat] },
};
}),
};
}
function lineToFeature(points) {
if (points.length < 2) {
return { type: 'FeatureCollection', features: [] };
}
return {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: points.map(function (point) {
return [point.lon, point.lat];
}),
},
};
}
function parseTraceInput(value) {
const pairs = value
.split(';')
.map(function (pair) { return pair.trim(); })
.filter(Boolean);
if (pairs.length < 2) {
return { error: 'Cần ít nhất 2 cặp tọa độ lat,lng.' };
}
const points = [];
for (let index = 0; index < pairs.length; index += 1) {
const parts = pairs[index].split(',').map(function (part) { return part.trim(); });
if (parts.length !== 2) {
return { error: 'Dùng định dạng lat,lng tại vị trí ' + (index + 1) + '.' };
}
const lat = Number(parts[0]);
const lon = Number(parts[1]);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
return { error: 'Tọa độ không hợp lệ tại vị trí ' + (index + 1) + '.' };
}
points.push({ lat: lat, lon: lon });
}
return { points: points };
}
// Quản lý toàn bộ quy trình vẽ tuyến:
// bắt đầu vẽ, cập nhật các đỉnh, hiển thị preview,
// hoàn tất tuyến và chuẩn bị dữ liệu cho Snap To Road.
function updateDrawingLayers(points, previewPoint) {
map.getSource('drawing-points').setData(pointsToFeatureCollection(points));
map.getSource('drawing-line').setData(lineToFeature(points));
if (points.length > 0 && previewPoint) {
const lastPoint = points[points.length - 1];
map.getSource('drawing-preview').setData({
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [
[lastPoint.lon, lastPoint.lat],
[previewPoint.lon, previewPoint.lat],
],
},
});
} else {
map.getSource('drawing-preview').setData({ type: 'FeatureCollection', features: [] });
}
}
function startDrawMode() {
isDrawMode = true;
drawingPoints = [];
mousePosition = null;
rawRoute = [];
snappedRoute = [];
snapResult = null;
snapError = null;
updateDrawingLayers([], null);
}
function completeDrawing(points) {
isDrawMode = false;
rawRoute = points;
drawingPoints = [];
mousePosition = null;
updateDrawingLayers([], null);
onRouteCompleted(rawRoute);
}
function applyTraceFromInput() {
const parsed = parseTraceInput(document.getElementById('trace-input').value);
if (parsed.error) {
console.error(parsed.error);
return;
}
rawRoute = parsed.points;
isDrawMode = false;
drawingPoints = [];
mousePosition = null;
updateDrawingLayers([], null);
onRouteCompleted(rawRoute);
}
// Khởi tạo các layer hỗ trợ vẽ, cho phép người dùng
// chọn các điểm trên bản đồ, hiển thị tuyến tạm thời
// trong quá trình vẽ và hoàn tất tuyến để chuẩn bị
// gửi tới API Snap To Road.
map.on('load', function () {
map.addSource('drawing-points', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addSource('drawing-line', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addSource('drawing-preview', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addLayer({
id: 'drawing-line',
type: 'line',
source: 'drawing-line',
paint: {
'line-color': RAW_ROUTE_COLOR,
'line-width': 3,
'line-dasharray': [2, 2],
},
});
map.addLayer({
id: 'drawing-preview',
type: 'line',
source: 'drawing-preview',
paint: {
'line-color': RAW_ROUTE_COLOR,
'line-width': 2.5,
'line-opacity': 0.65,
'line-dasharray': [2, 2],
},
});
map.addLayer({
id: 'drawing-points',
type: 'circle',
source: 'drawing-points',
paint: {
'circle-radius': 6,
'circle-color': RAW_ROUTE_COLOR,
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 1.5,
},
});
initResultLayers();
});
map.on('click', function (event) {
if (!isDrawMode) return;
const lastPoint = drawingPoints[drawingPoints.length - 1];
if (drawingPoints.length >= 2 && lastPoint && isClickNearPoint(event.lngLat, lastPoint)) {
completeDrawing(drawingPoints);
return;
}
const clickedPoint = lngLatToPoint(event.lngLat);
drawingPoints = drawingPoints.concat([clickedPoint]);
mousePosition = clickedPoint;
updateDrawingLayers(drawingPoints, mousePosition);
});
map.on('mousemove', function (event) {
if (!isDrawMode || drawingPoints.length === 0) return;
mousePosition = lngLatToPoint(event.lngLat);
updateDrawingLayers(drawingPoints, mousePosition);
});
document.getElementById('draw-button').addEventListener('click', startDrawMode);
document.getElementById('apply-trace-button').addEventListener('click', applyTraceFromInput);
Gọi POST /roads/v1/snap-to-roads với path và interpolate, xử lý phản hồi và lỗi, rồi chuyển kết quả sang bước hiển thị.
// Gửi toạ độ tuyến thô tới API Snap To Roads, xử lý kết quả trả về,
// chuyển đổi các điểm đã được khớp vào mạng lưới đường,
// cập nhật tuyến snapped route và tự động gọi lại API khi tùy chọn Interpolate thay đổi.
function resolveLocation(location) {
const lat = location.lat != null ? location.lat : location.latitude;
const lon = location.lon != null ? location.lon : location.longitude;
if (typeof lat !== 'number' || typeof lon !== 'number') {
return null;
}
return { lat: lat, lon: lon };
}
function snapToRoads(path, interpolateValue) {
return fetch(
'{{API_BASE_URL}}/api/roads/v1/snap-to-roads?apikey=' + encodeURIComponent(API_KEY),
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'app-version': '1.1',
},
body: JSON.stringify({
path: path,
interpolate: interpolateValue,
}),
},
).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok && payload.status !== 'ERROR') {
throw new Error(response.statusText || 'Snap To Roads request failed');
}
return payload;
});
});
}
function runSnapToRoad(path, interpolateValue) {
if (path.length < 2) {
return Promise.resolve();
}
isSnapping = true;
snapError = null;
return snapToRoads(path, interpolateValue)
.then(function (result) {
snapResult = result;
if (result.status !== 'OK') {
snapError = (result.error && result.error.message) || 'Snap To Roads failed';
snappedRoute = [];
return;
}
snappedRoute = result.data.snappedPoints
.map(function (point) {
return resolveLocation(point.location);
})
.filter(Boolean);
onSnapResultReady(rawRoute, snappedRoute);
})
.catch(function (error) {
snapError = error.message;
snapResult = null;
snappedRoute = [];
})
.finally(function () {
isSnapping = false;
});
}
function onRouteCompleted(path) {
runSnapToRoad(path, interpolate);
}
function onInterpolateChange(checked) {
interpolate = checked;
if (rawRoute.length < 2) return;
runSnapToRoad(rawRoute, interpolate);
}
document.getElementById('interpolate-toggle').addEventListener('change', function (event) {
onInterpolateChange(event.target.checked);
});
Chuyển snappedPoints sang GeoJSON và vẽ song song tuyến gốc (xanh dương) với tuyến đã snap (đỏ).
// Hiển thị đồng thời tuyến thô và tuyến đã được Snap To Road,
// giúp so sánh trực quan sự khác biệt giữa dữ liệu đầu vào
// và kết quả sau khi khớp vào mạng lưới đường.
function initResultLayers() {
map.addSource('raw-route-line', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addSource('raw-route-points', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addSource('snapped-route-line', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addSource('snapped-route-points', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addLayer({
id: 'raw-route-line',
type: 'line',
source: 'raw-route-line',
paint: {
'line-color': RAW_ROUTE_COLOR,
'line-width': 4,
},
});
map.addLayer({
id: 'raw-route-points',
type: 'circle',
source: 'raw-route-points',
paint: {
'circle-radius': 6,
'circle-color': RAW_ROUTE_COLOR,
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 1.5,
},
});
map.addLayer({
id: 'snapped-route-line',
type: 'line',
source: 'snapped-route-line',
paint: {
'line-color': SNAPPED_ROUTE_COLOR,
'line-width': 4,
},
});
map.addLayer({
id: 'snapped-route-points',
type: 'circle',
source: 'snapped-route-points',
paint: {
'circle-radius': 6,
'circle-color': SNAPPED_ROUTE_COLOR,
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 1.5,
},
});
}
function updateRawRouteLayers(points) {
map.getSource('raw-route-line').setData(lineToFeature(points));
map.getSource('raw-route-points').setData(pointsToFeatureCollection(points));
}
function updateSnappedRouteLayers(points) {
map.getSource('snapped-route-line').setData(lineToFeature(points));
map.getSource('snapped-route-points').setData(pointsToFeatureCollection(points));
}
function onSnapResultReady(rawPoints, snappedPoints) {
updateRawRouteLayers(rawPoints);
updateSnappedRouteLayers(snappedPoints);
}
Xóa layer, reset state và cho phép người dùng bắt đầu tuyến mới.
// Đặt lại toàn bộ trạng thái của demo, xóa các điểm đang vẽ,
// Raw Route, Snapped Route,
// kết quả API và các lớp hiển thị trên bản đồ để bắt đầu
// một phiên vẽ tuyến mới.
function clearDrawingLayers() {
map.getSource('drawing-points').setData({ type: 'FeatureCollection', features: [] });
map.getSource('drawing-line').setData({ type: 'FeatureCollection', features: [] });
map.getSource('drawing-preview').setData({ type: 'FeatureCollection', features: [] });
}
function clearRawRouteLayers() {
map.getSource('raw-route-line').setData({ type: 'FeatureCollection', features: [] });
map.getSource('raw-route-points').setData({ type: 'FeatureCollection', features: [] });
}
function clearSnappedRouteLayers() {
map.getSource('snapped-route-line').setData({ type: 'FeatureCollection', features: [] });
map.getSource('snapped-route-points').setData({ type: 'FeatureCollection', features: [] });
}
function clearRoute() {
isDrawMode = false;
drawingPoints = [];
mousePosition = null;
rawRoute = [];
snappedRoute = [];
snapResult = null;
snapError = null;
isSnapping = false;
interpolate = true;
document.getElementById('interpolate-toggle').checked = true;
document.getElementById('trace-input').value = '';
clearDrawingLayers();
clearRawRouteLayers();
clearSnappedRouteLayers();
}
document.getElementById('clear-button').addEventListener('click', clearRoute);