feat(api & web): Implement real-time Eco Routing, Fuel & Energy Consumption and Cost Engine

This commit is contained in:
Hamza-Ayed
2026-08-21 01:58:00 +03:00
parent e1e634fcb3
commit 7572d6b4c5
2 changed files with 108 additions and 5 deletions
+88 -5
View File
@@ -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.
*/
+20
View File
@@ -427,6 +427,26 @@ function App() {
</div>
)}
{/* Fuel & Eco Energy Impact */}
{routeData.ecoMetrics && (
<div style={{ marginTop: 10, paddingTop: 8, borderTop: '1px solid rgba(255,255,255,0.08)', fontSize: '0.78rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<span style={{ color: '#38bdf8', fontWeight: 600 }}>🌿 مؤشر الطاقة واستهلاك الوقود:</span>
<span style={{ fontSize: '0.72rem', padding: '2px 6px', borderRadius: 4, background: routeData.ecoMetrics.ecoScore >= 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)
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8', marginBottom: 4 }}>
<span>⛽ وقود مقدر: <strong style={{ color: '#f8fafc' }}>{routeData.ecoMetrics.estimatedGasolineLiters} لتر</strong></span>
<span>💰 تكلفة تقديرية: <strong style={{ color: '#4ade80' }}>{routeData.ecoMetrics.estimatedCostJOD} د.أ</strong></span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8' }}>
<span>⚡ استهلاك EV: <strong style={{ color: '#38bdf8' }}>{routeData.ecoMetrics.estimatedEvKWh} kWh</strong></span>
<span>🌱 انبعاثات الكربون: <strong style={{ color: '#94a3b8' }}>{routeData.ecoMetrics.co2Kg} كغم</strong></span>
</div>
</div>
)}
{/* Steep Slope Warnings Alert */}
{routeData.elevationSummary?.steepWarnings && routeData.elevationSummary.steepWarnings.length > 0 && (
<div style={{