diff --git a/client/src/pages/gps/RoutesTab.tsx b/client/src/pages/gps/RoutesTab.tsx
index 1f3b0ac..b0e7c3b 100644
--- a/client/src/pages/gps/RoutesTab.tsx
+++ b/client/src/pages/gps/RoutesTab.tsx
@@ -410,6 +410,14 @@ export function GpsRoutesTab() {
})),
...(colorizeEnabled ? speedSegments : []),
...matchedLine,
+ // Мосты-предположения по дорогам на LBS-участках — пунктир цветом объекта
+ ...(trackData?.bridges ?? []).map((b, idx) => ({
+ id: `bridge-${idx}`,
+ points: b.points,
+ color: selectedAsset?.color || '#3b82f6',
+ dashed: true,
+ width: 3,
+ })),
];
const mapMarkers: GpsMapMarker[] = [
// Точки записи координат (стрелки курса для GPS-точек; LBS — полые серые
@@ -561,6 +569,12 @@ export function GpsRoutesTab() {
полый кружок — нет GPS (по вышкам)
)}
+ {(trackData?.bridges?.length ?? 0) > 0 && (
+
+
+ пунктир — предполагаемый участок (нет GPS)
+
+ )}
)}
diff --git a/client/src/pages/gps/types.ts b/client/src/pages/gps/types.ts
index e96b2dd..5c1f144 100644
--- a/client/src/pages/gps/types.ts
+++ b/client/src/pages/gps/types.ts
@@ -80,6 +80,8 @@ export interface GpsTrackResponse {
filteredCount?: number;
/** Отсеяно LBS-мусорных точек (далеко от дороги) при выдаче */
offroadFiltered?: number;
+ /** Мосты-предположения по дорогам между GPS-якорями на LBS-участках (OSRM route) */
+ bridges?: Array<{ points: Array<[number, number]>; distance: number }>;
sampled: boolean;
/** Линия трека по дорогам от OSRM (при ?matched=1); null — OSRM недоступен */
matched?: { points: Array<[number, number]>; distance: number; duration: number } | null;
diff --git a/server/gps/routes.ts b/server/gps/routes.ts
index edcbbbd..c0463ef 100644
--- a/server/gps/routes.ts
+++ b/server/gps/routes.ts
@@ -149,6 +149,52 @@ function badRequest(res: Response, error: string) {
return res.status(400).json({ success: false, error });
}
+/**
+ * Мосты-предположения: если между двумя соседними по времени точными
+ * GPS-точками (якорями) есть хотя бы одна LBS-точка, строим между якорями
+ * маршрут по дорогам через OSRM Route API — целый трек без мусорных
+ * вышечных линий. Максимум 50 мостов на трек; OSRM недоступен — без мостов.
+ */
+const MAX_BRIDGES = 50;
+const OSRM_ROUTE_TIMEOUT_MS = 3000;
+
+interface TrackBridge {
+ points: [number, number][]; // [lat, lng]
+ distance: number;
+}
+
+async function buildBridges(points: Array<{ lat: number; lng: number; recordedAt: Date; isLbs: boolean }>): Promise {
+ // Якоря — точные точки (не LBS) по времени
+ const anchors = points.filter((p) => !p.isLbs);
+ const bridges: TrackBridge[] = [];
+ for (let i = 1; i < anchors.length && bridges.length < MAX_BRIDGES; i++) {
+ const a = anchors[i - 1];
+ const b = anchors[i];
+ const aTs = a.recordedAt.getTime();
+ const bTs = b.recordedAt.getTime();
+ // Мост нужен, только если между якорями есть хотя бы одна LBS-точка
+ const hasLbsBetween = points.some(
+ (p) => p.isLbs && p.recordedAt.getTime() > aTs && p.recordedAt.getTime() < bTs
+ );
+ if (!hasLbsBetween) continue;
+ try {
+ const url = `${OSRM_URL}/route/v1/driving/${a.lng},${a.lat};${b.lng},${b.lat}?overview=full&geometries=geojson`;
+ const res = await fetch(url, { signal: AbortSignal.timeout(OSRM_ROUTE_TIMEOUT_MS) });
+ if (!res.ok) continue;
+ const data = (await res.json()) as { code: string; routes?: Array<{ distance: number; geometry: { coordinates: [number, number][] } }> };
+ const route = data.routes?.[0];
+ if (data.code !== "Ok" || !route) continue;
+ bridges.push({
+ points: route.geometry.coordinates.map(([lng, lat]) => [lat, lng] as [number, number]),
+ distance: Math.round(route.distance),
+ });
+ } catch {
+ // OSRM недоступен для этого моста — просто пропускаем
+ }
+ }
+ return bridges;
+}
+
// Максимум LBS-точек, проверяемых offroad-фильтром при выдаче трека
// (бюджет ~5 сек при ~1-2 мс на OSRM nearest и конкуренции 10)
const MAX_LBS_OFFROAD_CHECK = 2000;
@@ -694,6 +740,9 @@ export function registerGpsRoutes(app: Express) {
matched = gpsOnly.length >= 2 ? await matchTrackCached(id, from, to, gpsOnly) : null;
}
+ // Мосты-предположения по дорогам между GPS-якорями на LBS-участках
+ const bridges = await buildBridges(points);
+
res.json({
success: true,
points,
@@ -702,6 +751,7 @@ export function registerGpsRoutes(app: Express) {
offroadFiltered,
sampled,
...(matched !== undefined ? { matched } : {}),
+ ...(bridges.length > 0 ? { bridges } : {}),
});
});