From f14d40dac70bafd972ec85fe86ead705e09125a1 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 21 Aug 2026 02:15:56 +0300 Subject: [PATCH] feat(routing): Dynamic traffic delay & descent-aware mechanical braking advice --- apps/api/src/maps/maps.service.ts | 103 ++++++++++++++++++++++-------- apps/web/src/App.tsx | 7 +- 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/apps/api/src/maps/maps.service.ts b/apps/api/src/maps/maps.service.ts index c5f907d..2033a9b 100644 --- a/apps/api/src/maps/maps.service.ts +++ b/apps/api/src/maps/maps.service.ts @@ -172,7 +172,8 @@ export class MapsService { 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 pBaseDuration = p.time / 1000; + const pDuration = Math.round(pBaseDuration * pTrafficFactor); // For alternative routes, find the distinctive street that differentiates it from the main route let routeStreet = index === 0 @@ -196,12 +197,16 @@ export class MapsService { // Analyze slopes and enrich instructions const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords); - // Calculate Energy, Fuel Consumption & Eco Cost + // Calculate Energy, Fuel Consumption & Eco Cost (combining Distance + Ascent/Descent Physics + Traffic/Time Delays) const ecoMetrics = this.calculateEcoAndFuelMetrics( p.distance, slopeSummary.totalAscentMeters, slopeSummary.totalDescentMeters, slopeSummary.maxInclinePercent, + slopeSummary.maxDeclinePercent, + pBaseDuration, + pDuration, + pTrafficFactor, profile ); @@ -336,60 +341,104 @@ export class MapsService { } /** - * Calculates comprehensive fuel, energy, and eco-cost metrics based on distance, topography and vehicle profile. + * Calculates comprehensive fuel, energy, and eco-cost metrics combining distance, elevation topography, and traffic delays. */ private calculateEcoAndFuelMetrics( distanceMeters: number, totalAscentMeters: number, totalDescentMeters: number, maxInclinePercent: number, + maxDeclinePercent: number, + baseDurationSeconds: number, + actualDurationSeconds: number, + trafficFactor: number, profile: string = 'car' ) { const distKm = distanceMeters / 1000; - // Base Fuel: ~0.075 L/km for standard passenger car in urban/suburban driving - const baseFuelRateLPerKm = profile === 'truck' ? 0.28 : (profile === 'bike' || profile === 'foot') ? 0.0 : 0.075; + // 1. Base Cruising Fuel: ~0.070 L/km for standard passenger car + const baseFuelRateLPerKm = profile === 'truck' ? 0.28 : (profile === 'bike' || profile === 'foot') ? 0.0 : 0.070; const baseGasolineLiters = distKm * baseFuelRateLPerKm; - // Gravity Work Penalty: ~0.16 Liters per 100m vertical ascent + // 2. Gravity Work Penalty on Ascent: +0.16 Liters per 100m vertical ascent const ascentFuelPenaltyLiters = (totalAscentMeters / 100) * (profile === 'truck' ? 0.45 : 0.16); - // Descent Savings: ~0.04 Liters saved per 100m descent (reduced throttle / engine braking) + // 3. Descent Savings (Gravity Assist / Engine Braking): -0.04 Liters per 100m descent const descentFuelSavingLiters = (totalDescentMeters / 100) * (profile === 'truck' ? 0.10 : 0.04); - const netGasolineLiters = Math.max(0.05, baseGasolineLiters + ascentFuelPenaltyLiters - descentFuelSavingLiters); + // 4. Traffic & Congestion Delay Fuel (Idling + Stop-and-Go Re-acceleration): + const delayHours = Math.max(0, actualDurationSeconds - baseDurationSeconds) / 3600; + const congestionFactorBonus = trafficFactor > 1.15 ? (trafficFactor - 1.0) * 0.4 : 0.0; + const trafficFuelLiters = (delayHours * 1.1) + (baseGasolineLiters * congestionFactorBonus); + + // Net Gasoline Consumption + const netGasolineLiters = Math.max(0.05, baseGasolineLiters + ascentFuelPenaltyLiters - descentFuelSavingLiters + trafficFuelLiters); // Pricing in Jordan: Gasoline 90 ~ 0.920 JOD/L, Diesel ~ 0.720 JOD/L const fuelPricePerLiter = profile === 'truck' ? 0.720 : 0.920; const estimatedCostJOD = netGasolineLiters * fuelPricePerLiter; - // EV Energy Model: ~0.16 kWh/km on flat + ~0.38 kWh/100m ascent - ~0.26 kWh/100m regen descent - const baseEvKWh = distKm * 0.16; + // 5. EV Energy Model (Electric Vehicles): + const baseEvKWh = distKm * 0.15; const ascentEvKWh = (totalAscentMeters / 100) * 0.38; const regenEvKWh = (totalDescentMeters / 100) * 0.26; - const netEvKWh = Math.max(0.1, baseEvKWh + ascentEvKWh - regenEvKWh); + const trafficEvKWh = delayHours * 1.5; // HVAC & auxiliary electronics in standstill + const netEvKWh = Math.max(0.1, baseEvKWh + ascentEvKWh - regenEvKWh + trafficEvKWh); // Carbon Footprint: 2,310g CO2 per liter of gasoline const co2Grams = Math.round(netGasolineLiters * 2310); - // Eco Score (0 - 100): Evaluates efficiency relative to pure flat line + // 6. Net Elevation Differential (هل المسار صاعد أم هابط؟) + const isPredominantlyDescent = totalDescentMeters > (totalAscentMeters * 1.3); + const isPredominantlyAscent = totalAscentMeters > (totalDescentMeters * 1.3); + + // 7. Terrain Difficulty & Mechanical Guidance + let terrainDifficultyArabic = 'طريق مستوٍ مريح'; + let mechanicalAdviceArabic = 'القيادة في نطاق السرعة الطبيعي'; + + if (isPredominantlyDescent) { + if (Math.abs(maxDeclinePercent) >= 8) { + terrainDifficultyArabic = 'منحدر جبلي هابط (نزول حاد)'; + mechanicalAdviceArabic = '⚠️ استخدام الغيار المنخفض (Engine Braking) لتخفيف العبء على الفرامل وتجنب ارتفاع حرارتها'; + } else { + terrainDifficultyArabic = 'طريق منحدر خفيف (هبوط سلس)'; + mechanicalAdviceArabic = 'مسير هابط موفر للوقود مع شحن متجدد لبطارية الـ EV'; + } + } else if (isPredominantlyAscent) { + if (maxInclinePercent >= 8) { + terrainDifficultyArabic = 'طريق صاعد جبلي (عقبة صعود حادة)'; + mechanicalAdviceArabic = 'يتطلب عزم محرك إضافي واستخدام الغيارات المناسبة لمنع إجهاد المحرك'; + } else { + terrainDifficultyArabic = 'طريق صاعد معتدل'; + mechanicalAdviceArabic = 'صعود تدريجي بجهد محرك معتدل'; + } + } else { + if (maxInclinePercent >= 8 || Math.abs(maxDeclinePercent) >= 8) { + terrainDifficultyArabic = 'تضاريس جبلية وعرة (صعود وهبوط متكرر)'; + mechanicalAdviceArabic = 'تدرج مستمر بين عزم الصعود وكبح النزول'; + } else if (maxInclinePercent >= 4 || Math.abs(maxDeclinePercent) >= 4) { + terrainDifficultyArabic = 'تضاريس متموجة معتدلة'; + mechanicalAdviceArabic = 'قيادة سلسة ومريحة للمركبة'; + } + } + + // 8. Eco Score & Badge Calculation const idealFlatFuel = distKm * baseFuelRateLPerKm; const consumptionRatio = idealFlatFuel > 0 ? (netGasolineLiters / idealFlatFuel) : 1.0; - const rawScore = Math.round(100 - (consumptionRatio - 1.0) * 60); - const ecoScore = Math.max(20, Math.min(100, rawScore)); + + let rawScore = Math.round(100 - (consumptionRatio - 1.0) * 50); + if (isPredominantlyDescent) rawScore = Math.max(rawScore, 92); // Descent is naturally fuel-efficient + const ecoScore = Math.max(15, Math.min(100, rawScore)); let ecoBadgeArabic = 'مسار قياسي متوازن'; - if (ecoScore >= 88 && maxInclinePercent <= 4) { - ecoBadgeArabic = 'مسار اقتصادي موفر للوقود 🌿'; - } else if (maxInclinePercent >= 8 || consumptionRatio > 1.4) { - ecoBadgeArabic = 'مسار عالي الاستهلاك ⚠️'; - } - - let terrainDifficultyArabic = 'طريق مستوٍ مريح'; - if (maxInclinePercent >= 8) { - terrainDifficultyArabic = 'طريق جبلي حاد (يتطلب عزم محرك إضافي)'; - } else if (maxInclinePercent >= 4) { - terrainDifficultyArabic = 'تضاريس متموجة معتدلة'; + if (isPredominantlyDescent && trafficFactor < 1.2) { + ecoBadgeArabic = 'مسار موفر للوقود بالهبوط 🌿 (شحن للـ EV)'; + } else if (ecoScore >= 85 && trafficFactor < 1.15) { + ecoBadgeArabic = 'مسار اقتصادي منخفض الاستهلاك 🌿'; + } else if (trafficFactor >= 1.35) { + ecoBadgeArabic = 'مسار عالي الاستهلاك (بسبب الازدحام والتوقف) ⏳'; + } else if (isPredominantlyAscent && maxInclinePercent >= 8) { + ecoBadgeArabic = 'مسار عالي الاستهلاك في الصعود ⚠️'; } return { @@ -398,9 +447,11 @@ export class MapsService { estimatedEvKWh: Math.round(netEvKWh * 100) / 100, energyRecoveredEvKWh: Math.round(regenEvKWh * 100) / 100, co2Kg: Math.round((co2Grams / 1000) * 100) / 100, + trafficDelayMinutes: Math.round(delayHours * 60), ecoScore, ecoBadge: ecoBadgeArabic, - terrainDifficulty: terrainDifficultyArabic + terrainDifficulty: terrainDifficultyArabic, + mechanicalAdvice: mechanicalAdviceArabic }; } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index decda9d..599a683 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -440,10 +440,15 @@ function App() { ⛽ وقود مقدر: {routeData.ecoMetrics.estimatedGasolineLiters} لتر 💰 تكلفة تقديرية: {routeData.ecoMetrics.estimatedCostJOD} د.أ -
+
⚡ استهلاك EV: {routeData.ecoMetrics.estimatedEvKWh} kWh 🌱 انبعاثات الكربون: {routeData.ecoMetrics.co2Kg} كغم
+ {routeData.ecoMetrics.mechanicalAdvice && ( +
+ 💡 {routeData.ecoMetrics.mechanicalAdvice} +
+ )}
)}