From 7572d6b4c5462820695193479a00ee154672236c Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 21 Aug 2026 01:58:00 +0300 Subject: [PATCH] feat(api & web): Implement real-time Eco Routing, Fuel & Energy Consumption and Cost Engine --- apps/api/src/maps/maps.service.ts | 93 +++++++++++++++++++++++++++++-- apps/web/src/App.tsx | 20 +++++++ 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/apps/api/src/maps/maps.service.ts b/apps/api/src/maps/maps.service.ts index 007895a..c5f907d 100644 --- a/apps/api/src/maps/maps.service.ts +++ b/apps/api/src/maps/maps.service.ts @@ -123,9 +123,9 @@ export class MapsService { if (alternatives && 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; + payload['alternative_route.max_paths'] = 3; // Return main route + up to 2 distinct alternatives + payload['alternative_route.max_weight_factor'] = 1.8; + payload['alternative_route.max_share_factor'] = 0.75; } console.log(`Routing Request: ${waypoints.length} points via ${profile} on ${this.graphHopperUrl} | Steps: ${steps} | Locale: ${locale}`); @@ -168,7 +168,7 @@ export class MapsService { const mainInstructions = paths[0]?.instructions || []; const mainStreetName = this.getRouteName(mainInstructions); - // Process all paths to add metadata (Real Street Names, Tags, Elevation & Slope Warnings) + // Process all paths to add metadata (Real Street Names, Tags, Elevation & Slope Warnings, Eco/Fuel metrics) const processedPaths = paths.map((p: any, index: number) => { const pCoords = this.decodePolyline(p.points); const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow); @@ -196,6 +196,15 @@ export class MapsService { // Analyze slopes and enrich instructions const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords); + // Calculate Energy, Fuel Consumption & Eco Cost + const ecoMetrics = this.calculateEcoAndFuelMetrics( + p.distance, + slopeSummary.totalAscentMeters, + slopeSummary.totalDescentMeters, + slopeSummary.maxInclinePercent, + profile + ); + // Tags assignment const tags: string[] = []; if (index === 0) tags.push('FASTEST'); @@ -204,6 +213,9 @@ export class MapsService { if (isShortest) tags.push('SHORTEST'); if (index > 0 && !isShortest) tags.push('ALTERNATIVE'); } + if (ecoMetrics.ecoScore >= 85) { + tags.push('ECO_FRIENDLY'); + } return { routeName: finalRouteName, @@ -213,7 +225,8 @@ export class MapsService { points: p.points, bbox: p.bbox, instructions: steps ? enrichedInstructions : undefined, - elevationSummary: slopeSummary + elevationSummary: slopeSummary, + ecoMetrics }; }); @@ -232,6 +245,7 @@ export class MapsService { bbox: mainRoute.bbox, instructions: mainRoute.instructions, elevationSummary: mainRoute.elevationSummary, + ecoMetrics: mainRoute.ecoMetrics, alternatives: altRoutes }; } catch (error) { @@ -321,6 +335,75 @@ export class MapsService { }; } + /** + * Calculates comprehensive fuel, energy, and eco-cost metrics based on distance, topography and vehicle profile. + */ + private calculateEcoAndFuelMetrics( + distanceMeters: number, + totalAscentMeters: number, + totalDescentMeters: number, + maxInclinePercent: 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; + const baseGasolineLiters = distKm * baseFuelRateLPerKm; + + // Gravity Work Penalty: ~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) + const descentFuelSavingLiters = (totalDescentMeters / 100) * (profile === 'truck' ? 0.10 : 0.04); + + const netGasolineLiters = Math.max(0.05, baseGasolineLiters + ascentFuelPenaltyLiters - descentFuelSavingLiters); + + // 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; + const ascentEvKWh = (totalAscentMeters / 100) * 0.38; + const regenEvKWh = (totalDescentMeters / 100) * 0.26; + const netEvKWh = Math.max(0.1, baseEvKWh + ascentEvKWh - regenEvKWh); + + // 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 + 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 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 = 'تضاريس متموجة معتدلة'; + } + + return { + estimatedGasolineLiters: Math.round(netGasolineLiters * 100) / 100, + estimatedCostJOD: Math.round(estimatedCostJOD * 100) / 100, + estimatedEvKWh: Math.round(netEvKWh * 100) / 100, + energyRecoveredEvKWh: Math.round(regenEvKWh * 100) / 100, + co2Kg: Math.round((co2Grams / 1000) * 100) / 100, + ecoScore, + ecoBadge: ecoBadgeArabic, + terrainDifficulty: terrainDifficultyArabic + }; + } + /** * Translates any English phrases from routing engines (GraphHopper) into natural, fluent Arabic. */ diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 35bfa02..decda9d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -427,6 +427,26 @@ function App() { )} + {/* Fuel & Eco Energy Impact */} + {routeData.ecoMetrics && ( +
+
+ 🌿 مؤشر الطاقة واستهلاك الوقود: + = 80 ? 'rgba(34, 197, 94, 0.2)' : 'rgba(245, 158, 11, 0.2)', color: routeData.ecoMetrics.ecoScore >= 80 ? '#4ade80' : '#fbbf24' }}> + {routeData.ecoMetrics.ecoBadge} ({routeData.ecoMetrics.ecoScore}/100) + +
+
+ ⛽ وقود مقدر: {routeData.ecoMetrics.estimatedGasolineLiters} لتر + 💰 تكلفة تقديرية: {routeData.ecoMetrics.estimatedCostJOD} د.أ +
+
+ ⚡ استهلاك EV: {routeData.ecoMetrics.estimatedEvKWh} kWh + 🌱 انبعاثات الكربون: {routeData.ecoMetrics.co2Kg} كغم +
+
+ )} + {/* Steep Slope Warnings Alert */} {routeData.elevationSummary?.steepWarnings && routeData.elevationSummary.steepWarnings.length > 0 && (