feat(gps): мосты-предположения по дорогам между GPS-якорями на LBS-участках (OSRM route, пунктир)
This commit is contained in:
@@ -410,6 +410,14 @@ export function GpsRoutesTab() {
|
|||||||
})),
|
})),
|
||||||
...(colorizeEnabled ? speedSegments : []),
|
...(colorizeEnabled ? speedSegments : []),
|
||||||
...matchedLine,
|
...matchedLine,
|
||||||
|
// Мосты-предположения по дорогам на LBS-участках — пунктир цветом объекта
|
||||||
|
...(trackData?.bridges ?? []).map((b, idx) => ({
|
||||||
|
id: `bridge-${idx}`,
|
||||||
|
points: b.points,
|
||||||
|
color: selectedAsset?.color || '#3b82f6',
|
||||||
|
dashed: true,
|
||||||
|
width: 3,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
const mapMarkers: GpsMapMarker[] = [
|
const mapMarkers: GpsMapMarker[] = [
|
||||||
// Точки записи координат (стрелки курса для GPS-точек; LBS — полые серые
|
// Точки записи координат (стрелки курса для GPS-точек; LBS — полые серые
|
||||||
@@ -561,6 +569,12 @@ export function GpsRoutesTab() {
|
|||||||
полый кружок — нет GPS (по вышкам)
|
полый кружок — нет GPS (по вышкам)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{(trackData?.bridges?.length ?? 0) > 0 && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="w-4 h-0.5" style={{ backgroundImage: 'linear-gradient(90deg,#3b82f6 55%,transparent 55%)', backgroundSize: '7px 2px' }} />
|
||||||
|
пунктир — предполагаемый участок (нет GPS)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export interface GpsTrackResponse {
|
|||||||
filteredCount?: number;
|
filteredCount?: number;
|
||||||
/** Отсеяно LBS-мусорных точек (далеко от дороги) при выдаче */
|
/** Отсеяно LBS-мусорных точек (далеко от дороги) при выдаче */
|
||||||
offroadFiltered?: number;
|
offroadFiltered?: number;
|
||||||
|
/** Мосты-предположения по дорогам между GPS-якорями на LBS-участках (OSRM route) */
|
||||||
|
bridges?: Array<{ points: Array<[number, number]>; distance: number }>;
|
||||||
sampled: boolean;
|
sampled: boolean;
|
||||||
/** Линия трека по дорогам от OSRM (при ?matched=1); null — OSRM недоступен */
|
/** Линия трека по дорогам от OSRM (при ?matched=1); null — OSRM недоступен */
|
||||||
matched?: { points: Array<[number, number]>; distance: number; duration: number } | null;
|
matched?: { points: Array<[number, number]>; distance: number; duration: number } | null;
|
||||||
|
|||||||
@@ -149,6 +149,52 @@ function badRequest(res: Response, error: string) {
|
|||||||
return res.status(400).json({ success: false, error });
|
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<TrackBridge[]> {
|
||||||
|
// Якоря — точные точки (не 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-фильтром при выдаче трека
|
// Максимум LBS-точек, проверяемых offroad-фильтром при выдаче трека
|
||||||
// (бюджет ~5 сек при ~1-2 мс на OSRM nearest и конкуренции 10)
|
// (бюджет ~5 сек при ~1-2 мс на OSRM nearest и конкуренции 10)
|
||||||
const MAX_LBS_OFFROAD_CHECK = 2000;
|
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;
|
matched = gpsOnly.length >= 2 ? await matchTrackCached(id, from, to, gpsOnly) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Мосты-предположения по дорогам между GPS-якорями на LBS-участках
|
||||||
|
const bridges = await buildBridges(points);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
points,
|
points,
|
||||||
@@ -702,6 +751,7 @@ export function registerGpsRoutes(app: Express) {
|
|||||||
offroadFiltered,
|
offroadFiltered,
|
||||||
sampled,
|
sampled,
|
||||||
...(matched !== undefined ? { matched } : {}),
|
...(matched !== undefined ? { matched } : {}),
|
||||||
|
...(bridges.length > 0 ? { bridges } : {}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user