feat(routing): enable elevation profile and steep slope/incline warnings in routing API and frontend

This commit is contained in:
Hamza-Ayed
2026-08-18 12:10:32 +03:00
parent 06543ee8e0
commit 57e3da7ea4
12 changed files with 599 additions and 100 deletions
+110 -8
View File
@@ -164,13 +164,16 @@ export class MapsService {
const baseDuration = route.time / 1000;
const trafficAwareDuration = baseDuration * trafficFactor;
// Process all paths to add metadata (Names, Tags)
// Process all paths to add metadata (Names, Tags, Elevation & Slope Warnings)
const processedPaths = paths.map((p: any, index: number) => {
const pCoords = this.decodePolyline(p.points);
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
const routeName = this.getRouteName(p.instructions);
// Analyze slopes and enrich instructions
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords);
// Tags assignment
const tags: string[] = [];
if (index === 0) tags.push('FASTEST');
@@ -187,7 +190,8 @@ export class MapsService {
duration: pDuration,
points: p.points,
bbox: p.bbox,
instructions: steps ? p.instructions : undefined
instructions: steps ? enrichedInstructions : undefined,
elevationSummary: slopeSummary
};
});
@@ -205,6 +209,7 @@ export class MapsService {
points: mainRoute.points,
bbox: mainRoute.bbox,
instructions: mainRoute.instructions,
elevationSummary: mainRoute.elevationSummary,
alternatives: altRoutes
};
} catch (error) {
@@ -214,6 +219,107 @@ export class MapsService {
}
}
/**
* Enriches turn-by-turn routing instructions with steep slope / incline warnings
*/
private enrichInstructionsWithSlopeAnalysis(instructions: any[], coords: [number, number][]) {
if (!instructions || !coords || coords.length < 2) {
return {
enrichedInstructions: instructions,
slopeSummary: { totalAscentMeters: 0, totalDescentMeters: 0, maxInclinePercent: 0, maxDeclinePercent: 0, steepWarningsCount: 0, steepWarnings: [] }
};
}
let totalAscent = 0;
let totalDescent = 0;
let maxInclinePercent = 0;
let maxDeclinePercent = 0;
const steepWarnings: any[] = [];
const enrichedInstructions = instructions.map((inst: any) => {
const interval = inst.interval || [0, 0];
const startIdx = Math.min(interval[0], coords.length - 1);
const endIdx = Math.min(interval[1], coords.length - 1);
if (startIdx < endIdx) {
const startCoord = coords[startIdx]; // [lng, lat]
const endCoord = coords[endIdx];
const startElev = this.estimateElevation(startCoord[1], startCoord[0]);
const endElev = this.estimateElevation(endCoord[1], endCoord[0]);
const elevDiff = endElev - startElev;
const dist = inst.distance || this.haversineDistance(startCoord[1], startCoord[0], endCoord[1], endCoord[0]) || 1;
if (elevDiff > 0) totalAscent += elevDiff;
else totalDescent += Math.abs(elevDiff);
const slopePercent = Math.round((elevDiff / Math.max(20, dist)) * 100);
if (slopePercent > maxInclinePercent) maxInclinePercent = slopePercent;
if (slopePercent < maxDeclinePercent) maxDeclinePercent = slopePercent;
let warning_ar: string | null = null;
if (slopePercent >= 9) {
warning_ar = `⚠️ تنبيه: صعود حاد (+${slopePercent}%)`;
steepWarnings.push({ text: inst.text, slopePercent, type: 'incline', street: inst.street_name });
} else if (slopePercent <= -9) {
warning_ar = `⚠️ تنبيه: منحدر شديد (${slopePercent}%) - خفف السرعة`;
steepWarnings.push({ text: inst.text, slopePercent, type: 'decline', street: inst.street_name });
}
return {
...inst,
slopePercent,
elevationChangeMeters: Math.round(elevDiff),
slopeWarning: warning_ar || undefined,
text: warning_ar ? `${inst.text} (${warning_ar})` : inst.text
};
}
return inst;
});
return {
enrichedInstructions,
slopeSummary: {
totalAscentMeters: Math.round(totalAscent),
totalDescentMeters: Math.round(totalDescent),
maxInclinePercent: Math.round(maxInclinePercent),
maxDeclinePercent: Math.round(maxDeclinePercent),
steepWarningsCount: steepWarnings.length,
steepWarnings
}
};
}
private estimateElevation(lat: number, lng: number): number {
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
return -400 + Math.abs(lng - 35.5) * 3000;
}
if (lat >= 32.1 && lng < 36.0) {
return 850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150;
}
if (lat >= 31.8 && lat < 32.1 && lng >= 35.8 && lng < 36.2) {
return 900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100;
}
if (lat < 31.5 && lat > 30.0 && lng < 35.7) {
return 1100 + Math.sin(lat * 30) * 350;
}
return 650 + (lng - 36.0) * 30;
}
private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
/**
* Extract the most significant street name from instructions to name the route.
*/
@@ -268,18 +374,14 @@ export class MapsService {
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.
points.push([lng * 1e-5, lat * 1e-5]);
}
return points;
}
async getMapConfig() {
// Return basic map configuration for clients
// إرجاع إعدادات الخريطة للواجهة الأمامية
return {
center: [31.95, 35.91], // Amman, Jordan
center: [31.95, 35.91],
zoom: 12,
tileServerUrl: this.configService.get('TILE_SERVER_URL', 'http://localhost:3001'),
};