feat(routing): Dynamic traffic delay & descent-aware mechanical braking advice
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -440,10 +440,15 @@ function App() {
|
||||
<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' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8', marginBottom: 4 }}>
|
||||
<span>⚡ استهلاك EV: <strong style={{ color: '#38bdf8' }}>{routeData.ecoMetrics.estimatedEvKWh} kWh</strong></span>
|
||||
<span>🌱 انبعاثات الكربون: <strong style={{ color: '#94a3b8' }}>{routeData.ecoMetrics.co2Kg} كغم</strong></span>
|
||||
</div>
|
||||
{routeData.ecoMetrics.mechanicalAdvice && (
|
||||
<div style={{ marginTop: 6, padding: '5px 8px', borderRadius: 6, background: 'rgba(56, 189, 248, 0.1)', color: '#bae6fd', fontSize: '0.74rem' }}>
|
||||
💡 {routeData.ecoMetrics.mechanicalAdvice}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user