feat: implement side-by-side synchronized map comparison tool and infrastructure scripts for connectivity and data updates
This commit is contained in:
@@ -52,6 +52,21 @@ export class CandidateRoad {
|
||||
@Column({ default: 'pending' })
|
||||
status: string;
|
||||
|
||||
// Where this candidate came from: 'telemetry' (driver traces) or 'overture' (map diff)
|
||||
// مصدر الاقتراح: من تتبع السائقين أو من مقارنة بيانات Overture
|
||||
@Column({ default: 'telemetry' })
|
||||
source: string;
|
||||
|
||||
// Road name, when known (Overture provides these; telemetry candidates are unnamed)
|
||||
// اسم الطريق إن وُجد (يأتي من Overture)
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
name: string;
|
||||
|
||||
// OSM highway class carried from Overture, applied on approval
|
||||
// تصنيف الطريق المنقول من Overture ويُطبَّق عند الموافقة
|
||||
@Column({ type: 'varchar', length: 32, nullable: true })
|
||||
highway: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
discoveredAt: Date;
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@ export class MapsService {
|
||||
@InjectRepository(RoadSegmentStat)
|
||||
private roadStatRepo: Repository<RoadSegmentStat>,
|
||||
private trafficGrid: TrafficGridService,
|
||||
private geocodingService: GeocodingService
|
||||
private geocodingService: GeocodingService,
|
||||
private dataSource: DataSource,
|
||||
) {
|
||||
this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://routing:8080');
|
||||
}
|
||||
|
||||
|
||||
async getRoute(waypoints: [number, number][], profile: string = 'car', steps: boolean = false, locale: string = 'en', alternatives: boolean = false) {
|
||||
if (waypoints.length < 2) {
|
||||
throw new HttpException('At least two waypoints are required', HttpStatus.BAD_REQUEST);
|
||||
@@ -63,6 +65,53 @@ export class MapsService {
|
||||
instructions: steps,
|
||||
};
|
||||
|
||||
// ── Closure-Aware Routing ─────────────────────────────────────────────
|
||||
// Roads the telemetry analyzer flagged as closed are handed to GraphHopper
|
||||
// as custom-model "areas" so the router avoids them. Two bugs from the first
|
||||
// version are fixed here:
|
||||
// 1. Areas MUST be polygons. road_segment_stats.geometry is a LineString,
|
||||
// so we buffer it (~15 m) into a polygon in SQL before sending.
|
||||
// 2. Each rule must reference its area by the exact id the area declares
|
||||
// (in_<id>). The old code called indexOf() on a different array and
|
||||
// always produced `in_custom_area-1` (a non-existent area) — GraphHopper
|
||||
// then rejected the request with 400, taking down ALL routing.
|
||||
// closureCount lets the send step below retry without closures if GH still
|
||||
// refuses the custom model, so a bad closure can never break routing.
|
||||
let closureCount = 0;
|
||||
try {
|
||||
const closedSegments = await this.dataSource.query(`
|
||||
SELECT ST_AsGeoJSON(
|
||||
ST_Transform(ST_Buffer(ST_Transform(geometry::geometry, 3857), 15), 4326), 6
|
||||
) AS geojson
|
||||
FROM road_segment_stats
|
||||
WHERE "isClosed" = true AND geometry IS NOT NULL
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
const features = closedSegments
|
||||
.map((s: any, i: number) => {
|
||||
try {
|
||||
return { type: 'Feature', id: `closed_${i}`, geometry: JSON.parse(s.geojson), properties: {} };
|
||||
} catch { return null; }
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (features.length > 0) {
|
||||
payload['custom_model'] = {
|
||||
priority: features.map((f: any) => ({ if: `in_${f.id}`, multiply_by: '0' })),
|
||||
areas: { type: 'FeatureCollection', features },
|
||||
};
|
||||
payload['ch.disable'] = true; // request-time custom models require CH disabled
|
||||
closureCount = features.length;
|
||||
console.log(`🚧 Routing: avoiding ${closureCount} closed segment(s).`);
|
||||
}
|
||||
} catch (closureError) {
|
||||
// Non-fatal — routing continues normally without closure avoidance.
|
||||
console.warn('⚠️ Could not load road closures for routing:', closureError.message);
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
// GraphHopper ONLY supports alternative routes if there are exactly 2 points (Start and End)
|
||||
if (alternatives && waypoints.length === 2) {
|
||||
payload.algorithm = 'alternative_route';
|
||||
@@ -73,7 +122,23 @@ export class MapsService {
|
||||
}
|
||||
|
||||
console.log(`Routing Request: ${waypoints.length} points via ${profile} on ${this.graphHopperUrl} | Steps: ${steps} | Locale: ${locale}`);
|
||||
const response = await axios.post(`${this.graphHopperUrl}/route`, payload, { timeout: 10000 });
|
||||
let response: any;
|
||||
try {
|
||||
response = await axios.post(`${this.graphHopperUrl}/route`, payload, { timeout: 10000 });
|
||||
} catch (routeErr) {
|
||||
// Safety net: if the closure-aware custom model was rejected, retry once
|
||||
// WITHOUT it so an unsupported/bad closure can never take down all routing.
|
||||
if (closureCount > 0 && payload['custom_model']) {
|
||||
const detail = routeErr.response ? JSON.stringify(routeErr.response.data) : routeErr.message;
|
||||
console.warn(`⚠️ Closure-aware routing failed (${detail}). Retrying without closures...`);
|
||||
delete payload['custom_model'];
|
||||
// Keep ch.disable only if the alternative-route block below still needs it.
|
||||
if (!(alternatives && waypoints.length === 2)) delete payload['ch.disable'];
|
||||
response = await axios.post(`${this.graphHopperUrl}/route`, payload, { timeout: 10000 });
|
||||
} else {
|
||||
throw routeErr;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Routing SUCCESS');
|
||||
const paths = response.data.paths;
|
||||
|
||||
Reference in New Issue
Block a user