feat: implement side-by-side synchronized map comparison tool and infrastructure scripts for connectivity and data updates

This commit is contained in:
Hamza-Ayed
2026-07-15 14:58:48 +03:00
parent 790bfcefc8
commit a39dfe1aaa
11 changed files with 1025 additions and 303 deletions
@@ -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;
+67 -2
View File
@@ -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;