feat(api & web): Implement real-time Eco Routing, Fuel & Energy Consumption and Cost Engine
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user