From 018fc72d9dad6642f0b8ddd43cdbe74302c157da Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 13 Apr 2026 02:34:23 +0300 Subject: [PATCH] 2026-04-13-2 --- apps/api/src/maps/maps.module.ts | 8 +- apps/api/src/maps/maps.service.ts | 62 ++++------- apps/api/src/maps/traffic-grid.service.ts | 125 ++++++++++++++++++++++ 3 files changed, 151 insertions(+), 44 deletions(-) create mode 100644 apps/api/src/maps/traffic-grid.service.ts diff --git a/apps/api/src/maps/maps.module.ts b/apps/api/src/maps/maps.module.ts index 7ebad64..1a38d69 100644 --- a/apps/api/src/maps/maps.module.ts +++ b/apps/api/src/maps/maps.module.ts @@ -1,19 +1,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MapsService } from './maps.service'; +import { TrafficGridService } from './traffic-grid.service'; import { MapsController } from './maps.controller'; import { RoadSegmentStat } from './road-stat.entity'; import { CandidateRoad } from './candidate-road.entity'; -import { RoadSpeedProfile } from './road-speed-profile.entity'; import { RedisModule } from '../common/redis.module'; @Module({ imports: [ - TypeOrmModule.forFeature([RoadSegmentStat, CandidateRoad, RoadSpeedProfile]), + TypeOrmModule.forFeature([RoadSegmentStat, CandidateRoad]), RedisModule, ], controllers: [MapsController], - providers: [MapsService], - exports: [MapsService], + providers: [MapsService, TrafficGridService], + exports: [MapsService, TrafficGridService], }) export class MapsModule {} diff --git a/apps/api/src/maps/maps.service.ts b/apps/api/src/maps/maps.service.ts index ad9d80c..1d0c2f2 100644 --- a/apps/api/src/maps/maps.service.ts +++ b/apps/api/src/maps/maps.service.ts @@ -4,6 +4,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { RoadSpeedProfile } from './road-speed-profile.entity'; import axios from 'axios'; +import { RoadSegmentStat } from './road-stat.entity'; +import { TrafficGridService } from './traffic-grid.service'; @Injectable() export class MapsService { @@ -11,9 +13,9 @@ export class MapsService { constructor( private configService: ConfigService, - @InjectRepository(RoadSpeedProfile) - private speedProfileRepo: Repository, - private dataSource: DataSource + @InjectRepository(RoadSegmentStat) + private roadStatRepo: Repository, + private trafficGrid: TrafficGridService ) { this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://routing:8080'); } @@ -33,13 +35,17 @@ export class MapsService { locale: 'en', calc_points: true, points_encoded: true, - algorithm: 'alternative_route', - 'ch.disable': true, // Required for alternative routes - 'alternative_route.max_paths': 2, // Return main route + 1 alternative - 'alternative_route.max_weight_factor': 1.6, - 'alternative_route.max_share_factor': 0.6 }; + // GraphHopper ONLY supports alternative routes if there are exactly 2 points (Start and End) + if (waypoints.length === 2) { + payload.algorithm = 'alternative_route'; + payload['ch.disable'] = true; // Required for alternative routes + payload['alternative_route.max_paths'] = 2; // Return main route + 1 alternative + payload['alternative_route.max_weight_factor'] = 1.6; + payload['alternative_route.max_share_factor'] = 0.6; + } + console.log(`Routing Request: ${waypoints.length} points via ${profile} on ${this.graphHopperUrl}`); const response = await axios.post(`${this.graphHopperUrl}/route`, payload, { timeout: 10000 }); @@ -49,40 +55,16 @@ export class MapsService { const route = paths[0]; - // --- PHASE 3: TRAFFIC-AWARE ADJUSTMENT --- - let baseDuration = route.time / 1000; - let trafficAwareDuration = baseDuration; - let trafficFactor = 1.0; + // --- PHASE 3: TRAFFIC-AWARE ADJUSTMENT (V3 Optimized) --- + const now = new Date(); + const hr = now.getHours(); + const dow = now.getDay(); - try { - const now = new Date(); - const hr = now.getHours(); - const dow = now.getDay(); + const coords = this.decodePolyline(route.points); + const trafficFactor = this.trafficGrid.getTrafficFactor(coords, hr, dow); - // Decode polyline for spatial mapping (server-side only) - const coords = this.decodePolyline(route.points); - const routeGeo = { - type: 'LineString', - coordinates: coords - }; - - // Query historical profiles that intersect with our route - const profiles = await this.dataSource.query(` - SELECT AVG(p."averageSpeed") as profile_speed, AVG(s."averageSpeed") as base_speed - FROM road_speed_profiles p - JOIN road_segment_stats s ON p."segmentId" = s."segmentId" - WHERE p."hourOfDay" = $1 AND p."dayOfWeek" = $2 - AND ST_DWithin(s.geometry, ST_GeomFromGeoJSON($3), 0.0002) - `, [hr, dow, JSON.stringify(routeGeo)]); - - if (profiles[0] && profiles[0].profile_speed && profiles[0].base_speed) { - trafficFactor = profiles[0].base_speed / Math.max(profiles[0].profile_speed, 1); - trafficFactor = Math.min(Math.max(trafficFactor, 0.8), 3.0); - trafficAwareDuration = baseDuration * trafficFactor; - } - } catch (e) { - console.warn('Traffic adjustment failed, using calibrated base:', e.message); - } + const baseDuration = route.time / 1000; + const trafficAwareDuration = baseDuration * trafficFactor; // Process alternative routes if any without breaking existing frontend variables const alternatives = paths.slice(1).map(alt => ({ diff --git a/apps/api/src/maps/traffic-grid.service.ts b/apps/api/src/maps/traffic-grid.service.ts new file mode 100644 index 0000000..d6a2bc3 --- /dev/null +++ b/apps/api/src/maps/traffic-grid.service.ts @@ -0,0 +1,125 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +/** + * Traffic Grid Service - In-Memory GeoHash Grid + * نظام شبكة التنبؤ المروري الذكي: يحول بيانات قاعدة البيانات إلى خارطة سرعة في الذاكرة + */ +@Injectable() +export class TrafficGridService implements OnModuleInit { + private readonly logger = new Logger(TrafficGridService.name); + + // Key: "geohash:hr:dow", Value: trafficFactor + private grid: Map = new Map(); + private readonly GEOHASH_PRECISION = 6; // ~100m accuracy + + constructor(private dataSource: DataSource) {} + + async onModuleInit() { + await this.refreshGrid(); + } + + /** + * Rebuilds the in-memory lookup table from PostGIS + * إعادة بناء الجدول المرجعي في الذاكرة من قاعدة البيانات + */ + async refreshGrid() { + this.logger.log('📡 Rebuilding Traffic Grid v3 (In-Memory Lookup)...'); + try { + const startTime = Date.now(); + + const data = await this.dataSource.query(` + SELECT + ST_X(ST_Centroid(geometry::geometry)) as lng, + ST_Y(ST_Centroid(geometry::geometry)) as lat, + p."hourOfDay", p."dayOfWeek", p."averageSpeed", + s."averageSpeed" as base_speed + FROM road_speed_profiles p + JOIN road_segment_stats s ON p."segmentId" = s."segmentId" + WHERE s.geometry IS NOT NULL + `); + + this.grid.clear(); + let count = 0; + + for (const row of data) { + const gh = this.encodeGeohash(row.lat, row.lng, this.GEOHASH_PRECISION); + const factor = row.base_speed / Math.max(row.averageSpeed, 1); + + // Key includes time buckets to handle day/night variations + const key = `${gh}:${row.hourOfDay}:${row.dayOfWeek}`; + this.grid.set(key, factor); + count++; + } + + this.logger.log(`✅ Grid rebuilt in ${Date.now() - startTime}ms. Cached ${count} cells (${this.grid.size} unique keys).`); + } catch (e) { + this.logger.error('❌ Failed to refresh traffic grid:', e.message); + } + } + + /** + * Fetches the average traffic factor for a decoded polyline + * جلب معامل الازدحام المتوسط لمخطط الرحلة من الذاكرة + */ + getTrafficFactor(coords: [number, number][], hr: number, dow: number): number { + if (this.grid.size === 0) return 1.0; + + let totalFactor = 0; + let matches = 0; + + // We sample points every ~100m to speed up (every 2nd or 3rd coord) + for (let i = 0; i < coords.length; i += 2) { + const [lng, lat] = coords[i]; + const gh = this.encodeGeohash(lat, lng, this.GEOHASH_PRECISION); + const key = `${gh}:${hr}:${dow}`; + + const factor = this.grid.get(key); + if (factor) { + totalFactor += factor; + matches++; + } + } + + if (matches === 0) return 1.0; + + // Average factor clipped to sane ranges (0.8 - 3.0) + let finalFactor = totalFactor / matches; + return Math.min(Math.max(finalFactor, 0.8), 3.0); + } + + /** + * Pure Math GeoHash implementation (No outside dependencies) + */ + private encodeGeohash(lat: number, lng: number, precision: number): string { + const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"; + let minLat = -90, maxLat = 90; + let minLng = -180, maxLng = 180; + let result = ""; + let isEven = true; + let bit = 0; + let ch = 0; + + while (result.length < precision) { + if (isEven) { + let mid = (minLng + maxLng) / 2; + if (lng > mid) { ch |= (1 << (4 - bit)); minLng = mid; } + else { maxLng = mid; } + } else { + let mid = (minLat + maxLat) / 2; + if (lat > mid) { ch |= (1 << (4 - bit)); minLat = mid; } + else { maxLat = mid; } + } + + isEven = !isEven; + if (bit < 4) { + bit++; + } else { + result += BASE32[ch]; + bit = 0; + ch = 0; + } + } + return result; + } +}