Работа с кратчайшими маршрутами в геопространственных задачах на базе Turf.js строится вокруг представления дорожной сети как графа, где рёбра имеют вес, равный длине геометрического сегмента, а вершины соответствуют точкам пересечения и конечным узлам линий. Такой подход позволяет применять классические алгоритмы теории графов поверх геометрических данных GeoJSON.
Любая транспортная сеть в Turf.js обычно представляется набором
объектов LineString или MultiLineString,
объединённых в FeatureCollection. Для корректного поиска кратчайшего
пути необходимо привести данные к графовой структуре:
Ключевая проблема исходных данных заключается в том, что пересечения линий в GeoJSON не всегда представлены как явные узлы. Поэтому первым этапом выполняется топологическая нормализация.
Для извлечения узлов используется анализ пересечений геометрий. В Turf.js для этого применяются:
lineIntersect — поиск точек пересечения линийlineSplit — разбиение линий в точках пересеченийbooleanEqual — проверка совпадений геометрийАлгоритм построения узлов:
После этого каждая линия превращается в набор сегментов, полностью соответствующих рёбрам графа.
Каждый сегмент линии преобразуется в ребро графа. Вес рассчитывается через длину геометрии:
import length from "@turf/length";
const weight = length(segment, { units: "kilometers" });
Для каждого сегмента создаются две связи (граф неориентированный):
graph.addEdge(nodeA, nodeB, weight);
graph.addEdge(nodeB, nodeA, weight);
Структура графа чаще всего реализуется через Map:
const graph = new Map();
function addEdge(a, b, w) {
if (!graph.has(a)) graph.set(a, []);
graph.get(a).push({ to: b, weight: w });
}
Геометрические данные часто содержат:
Для стабилизации графа применяется округление координат:
function normalizeCoord(coord, precision = 6) {
return coord.map(v => Number(v.toFixed(precision)));
}
После нормализации узлы можно безопасно хешировать:
function nodeKey(coord) {
return coord.join(",");
}
После построения графа применяется алгоритм Дейкстры. Он подходит для неотрицательных весов, что соответствует длинам дорог.
Базовая реализация:
function dijkstra(graph, start, end) {
const distances = new Map();
const previous = new Map();
const visited = new Set();
const pq = new Map();
for (const node of graph.keys()) {
distances.set(node, Infinity);
}
distances.set(start, 0);
pq.set(start, 0);
while (pq.size > 0) {
const current = [...pq.entries()].reduce((a, b) =>
a[1] < b[1] ? a : b
)[0];
pq.delete(current);
if (current === end) break;
visited.add(current);
for (const neighbor of graph.get(current) || []) {
if (visited.has(neighbor.to)) continue;
const newDist =
distances.get(current) + neighbor.weight;
if (newDist < distances.get(neighbor.to)) {
distances.set(neighbor.to, newDist);
previous.set(neighbor.to, current);
pq.set(neighbor.to, newDist);
}
}
}
const path = [];
let curr = end;
while (curr) {
path.unshift(curr);
curr = previous.get(curr);
}
return path;
}
Результатом работы алгоритма является последовательность узлов. Для получения линии маршрута необходимо восстановить геометрию:
import lineString from "@turf/linestring";
function buildRouteGeometry(path, coordMap) {
const coords = path.map(node => coordMap.get(node));
return lineString(coords);
}
coordMap хранит соответствие между ключом узла и
координатами.
При работе с реальными данными возникают дополнительные сложности:
Граф становится ориентированным:
graph.addEdge(a, b, w);
// нет обратного ребра
Эстакады и мосты требуют проверки высотности или тегов
layer и bridge.
Вес ребра может модифицироваться:
const adjustedWeight = length(segment) * roadFactor;
При больших графах Дейкстра становится медленным. Используются улучшения:
Замена линейного поиска минимального элемента на кучу:
Одновременный запуск от старта и финиша уменьшает пространство поиска.
Геометрии индексируются через R-tree (например, rbush),
чтобы ускорить поиск пересечений.
Полный цикл обработки начинается с FeatureCollection:
function buildGraph(features) {
const graph = new Map();
const coordMap = new Map();
features.forEach(feature => {
const coords = feature.geometry.coordinates;
for (let i = 0; i < coords.length - 1; i++) {
const a = normalizeCoord(coords[i]);
const b = normalizeCoord(coords[i + 1]);
const keyA = nodeKey(a);
const keyB = nodeKey(b);
coordMap.set(keyA, a);
coordMap.set(keyB, b);
const segment = {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [a, b]
}
};
const w = length(segment);
if (!graph.has(keyA)) graph.set(keyA, []);
if (!graph.has(keyB)) graph.set(keyB, []);
graph.get(keyA).push({ to: keyB, weight: w });
graph.get(keyB).push({ to: keyA, weight: w });
}
});
return { graph, coordMap };
}
После получения списка узлов маршрут преобразуется в GeoJSON LineString, пригодный для визуализации:
const route = buildRouteGeometry(path, coordMap);
Полученная геометрия может использоваться в Mapbox GL, Leaflet или любом GeoJSON-совместимом рендерере.
При увеличении объёма сети до миллионов рёбер ключевыми становятся:
Графовая модель остаётся неизменной, меняется только стратегия хранения и поиска.
Такой подход позволяет использовать Turf.js не только как библиотеку геометрических операций, но и как основу для полноценной системы пространственного анализа маршрутов.