2026-04-13-1
This commit is contained in:
@@ -4,11 +4,12 @@ import { MapsService } from './maps.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]),
|
||||
TypeOrmModule.forFeature([RoadSegmentStat, CandidateRoad, RoadSpeedProfile]),
|
||||
RedisModule,
|
||||
],
|
||||
controllers: [MapsController],
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { RoadSpeedProfile } from './road-speed-profile.entity';
|
||||
import axios from 'axios';
|
||||
|
||||
@Injectable()
|
||||
export class MapsService {
|
||||
private readonly graphHopperUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@InjectRepository(RoadSpeedProfile)
|
||||
private speedProfileRepo: Repository<RoadSpeedProfile>,
|
||||
private dataSource: DataSource
|
||||
) {
|
||||
this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://routing:8080');
|
||||
}
|
||||
|
||||
@@ -25,20 +33,73 @@ 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
|
||||
};
|
||||
|
||||
console.log(`Routing Request: ${waypoints.length} points via ${profile} on ${this.graphHopperUrl}`);
|
||||
const response = await axios.post(`${this.graphHopperUrl}/route`, payload, { timeout: 10000 });
|
||||
|
||||
|
||||
console.log('Routing SUCCESS');
|
||||
const route = response.data.paths[0];
|
||||
if (!route) throw new HttpException('No route found', HttpStatus.NOT_FOUND);
|
||||
const paths = response.data.paths;
|
||||
if (!paths || paths.length === 0) throw new HttpException('No route found', HttpStatus.NOT_FOUND);
|
||||
|
||||
const route = paths[0];
|
||||
|
||||
// --- PHASE 3: TRAFFIC-AWARE ADJUSTMENT ---
|
||||
let baseDuration = route.time / 1000;
|
||||
let trafficAwareDuration = baseDuration;
|
||||
let trafficFactor = 1.0;
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const hr = now.getHours();
|
||||
const dow = now.getDay();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Process alternative routes if any without breaking existing frontend variables
|
||||
const alternatives = paths.slice(1).map(alt => ({
|
||||
distance: alt.distance,
|
||||
duration: Math.round(alt.time / 1000),
|
||||
points: alt.points,
|
||||
bbox: alt.bbox
|
||||
}));
|
||||
|
||||
return {
|
||||
distance: route.distance,
|
||||
duration: route.time / 1000,
|
||||
duration: Math.round(baseDuration),
|
||||
trafficAwareDuration: Math.round(trafficAwareDuration),
|
||||
trafficFactor: Math.round(trafficFactor * 100) / 100,
|
||||
points: route.points,
|
||||
bbox: route.bbox,
|
||||
alternatives: alternatives // NEW: array of other routes
|
||||
};
|
||||
} catch (error) {
|
||||
const msg = error.response ? `GH Error: ${JSON.stringify(error.response.data)}` : `DNS/Connection Error: ${error.message}`;
|
||||
@@ -47,6 +108,41 @@ export class MapsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual decoder for Google Polyline algorithm (Server-side spatial matching)
|
||||
*/
|
||||
private decodePolyline(encoded: string): [number, number][] {
|
||||
const points: [number, number][] = [];
|
||||
let index = 0, len = encoded.length;
|
||||
let lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
let b, shift = 0, result = 0;
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
let dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
let dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
|
||||
lng += dlng;
|
||||
|
||||
points.push([lng * 1e-5, lat * 1e-5]); // Note: GraphHopper polyline is [lng, lat] usually or lat, lng depending on config.
|
||||
// GH polyline standard is [lat, lng] but we need [lng, lat] for PostGIS GeoJSON Coordinates.
|
||||
// Let's verify: GraphHopper default polyline is Lat,Lng.
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
async getMapConfig() {
|
||||
// Return basic map configuration for clients
|
||||
// إرجاع إعدادات الخريطة للواجهة الأمامية
|
||||
|
||||
Reference in New Issue
Block a user