Маршруты
Расстояние, время, варианты поездки и геометрия — через RoutingClient, без обязательной панели.
1. Рассчитайте маршрут
Карта для расчёта не нужна. JavaScript возвращает Promise<RoutingResponse>, Flutter — Future<RoutingResponse>. Создайте клиент один раз на время работы экрана.
Вкладка «Ответ» показывает сокращённый пример: первый вариант и начало геометрии первого участка. Полный ответ доступен ниже.
import { RoutingClient } from "@umap-llc/map-react/routing";
const routing = new RoutingClient({
styleUrl: "YOUR_STYLE_URL",
accessToken: "YOUR_ACCESS_TOKEN",
});
const result = await routing.calculate({
from: { coordinates: [71.4106836, 51.1355788], name: "Астана Опера" },
to: { coordinates: [71.4304544, 51.1282903], name: "Байтерек" },
mode: "car",
alternatives: 3,
});
for (const route of result.routes) {
console.log(route.id, Math.ceil(route.durationS / 60), route.distanceM);
}2. Выберите транспорт и точки
name — подпись точки; клиент не ищет её координаты по названию. Сначала получите координаты через PlacesClient или свои данные.
3. Прочитайте ответ
Время является оценкой сервиса. Для показа линии используйте геометрию legs, включая подходы и пересадки. Полный исходный ответ во Flutter доступен в result.json.
4. Покажите результат на карте
JavaScript-пример добавляет GeoJSON линий. Во Flutter передайте result.routes в UMapView.routes и выбранный id в selectedRouteId. Переключение между уже полученными вариантами не требует повторного расчёта.
import * as sdk from "@umap-llc/map-react";
// HTML: <div id="map" style="height:480px;position:relative"></div>
const map = sdk.createMap({
container: "map",
style: "YOUR_STYLE_URL",
accessToken: "YOUR_ACCESS_TOKEN",
center: [71.423, 51.128],
zoom: 14,
});
async function setupExample(map, sdk, report = console.log) {
const result = await map.routing.calculate({
from: { coordinates: [71.4106836, 51.1355788], name: "Астана Опера" },
to: { coordinates: [71.4304544, 51.1282903], name: "Байтерек" },
mode: "car",
alternatives: 3,
});
const route = result.routes[0];
if (!route) {
report("Маршрут не найден");
return;
}
await map.addSource("route", {
type: "geojson",
data: {
type: "FeatureCollection",
features: route.legs.map((leg) => ({
type: "Feature",
properties: {},
geometry: leg.geometry,
})),
},
});
map.addLayer({
id: "route-border",
type: "line",
source: "route",
"rendering-order": "overlay",
paint: { "line-color": "#fff", "line-width": 9 },
});
map.addLayer({
id: "route-path",
type: "line",
source: "route",
"rendering-order": "overlay",
paint: { "line-color": "#1769e0", "line-width": 5 },
});
map.fitBounds(
[
[route.bounds[0], route.bounds[1]],
[route.bounds[2], route.bounds[3]],
],
{ padding: 65, maxZoom: 15 },
);
report(
`${Math.ceil(route.durationS / 60)} мин · ${(route.distanceM / 1000).toFixed(1)} км · ${result.routes.length} варианта`,
);
const options = document.createElement("div");
options.style.cssText =
"position:absolute;top:16px;left:16px;display:flex;gap:8px;flex-wrap:wrap;max-width:calc(100% - 120px);z-index:5";
for (const candidate of result.routes) {
const button = document.createElement("button");
button.type = "button";
button.textContent = `${Math.ceil(candidate.durationS / 60)} мин`;
button.setAttribute("aria-pressed", String(candidate.id === route.id));
const paint = (selected) => {
button.style.cssText = `border:1px solid #dbe3ed;border-radius:12px;padding:10px 14px;cursor:pointer;font:600 14px system-ui;background:${selected ? "#1769e0" : "white"};color:${selected ? "white" : "#253348"}`;
};
paint(candidate.id === route.id);
button.onclick = () => {
map.setData("route", {
type: "FeatureCollection",
features: candidate.legs.map((leg) => ({
type: "Feature",
properties: {},
geometry: leg.geometry,
})),
});
for (const sibling of options.querySelectorAll("button")) {
sibling.setAttribute("aria-pressed", "false");
sibling.style.background = "white";
sibling.style.color = "#253348";
}
button.setAttribute("aria-pressed", "true");
paint(true);
map.fitBounds(
[
[candidate.bounds[0], candidate.bounds[1]],
[candidate.bounds[2], candidate.bounds[3]],
],
{ padding: 65, maxZoom: 15 },
);
report(
`${Math.ceil(candidate.durationS / 60)} мин · ${(candidate.distanceM / 1000).toFixed(1)} км`,
);
};
options.append(button);
}
map.getContainer().append(options);
return () => options.remove();
}
map.once("load", async () => {
const cleanup = await setupExample(map, sdk);
if (cleanup) map.once("remove", cleanup);
});
5. Обработайте ошибку и отмену
const abort = new AbortController();
try {
const result = await routing.calculate(request, { signal: abort.signal });
console.log(result.routes);
} catch (error) {
if (error.name !== "AbortError") console.error(error.status, error.message);
}
// При уходе со страницы: abort.abort();