Chuyển tới nội dung chính

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

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);