|
| 1 | +const numberOrNull = (value) => { |
| 2 | + const numeric = Number.parseFloat(String(value ?? "").replace(",", ".")); |
| 3 | + return Number.isFinite(numeric) ? numeric : null; |
| 4 | +}; |
| 5 | + |
| 6 | +const rounded = (value) => Math.round(value * 1000) / 1000; |
| 7 | + |
| 8 | +const canonicalTime = (trip) => trip?.end_time || trip?.start_time || null; |
| 9 | + |
| 10 | +const endMileage = (trip) => { |
| 11 | + const explicit = numberOrNull(trip?.end_mileage ?? trip?.end_mileage_km); |
| 12 | + if (explicit !== null) return explicit; |
| 13 | + const start = numberOrNull(trip?.start_mileage ?? trip?.start_mileage_km); |
| 14 | + const distance = numberOrNull(trip?.distance_km ?? trip?.distance); |
| 15 | + return start !== null && distance !== null ? start + distance : null; |
| 16 | +}; |
| 17 | + |
| 18 | +const usableAnchor = (trip) => { |
| 19 | + if (!trip || trip.valid_for_statistics === false) return false; |
| 20 | + const start = numberOrNull(trip.start_mileage ?? trip.start_mileage_km); |
| 21 | + const end = endMileage(trip); |
| 22 | + return start !== null && end !== null && start >= 0 && end >= start; |
| 23 | +}; |
| 24 | + |
| 25 | +export function semanticTripTime(trip) { |
| 26 | + const attributes = trip?.attributes ?? {}; |
| 27 | + return attributes.end_time || attributes.start_time || trip?.end_time || trip?.start_time || |
| 28 | + trip?.last_updated || trip?.last_changed || null; |
| 29 | +} |
| 30 | + |
| 31 | +export function tripFilterTime(trip) { |
| 32 | + if (trip?.reconstructed_gap || trip?.attributes?.reconstructed_gap) { |
| 33 | + const attributes = trip?.attributes ?? {}; |
| 34 | + return attributes.gap_before_time || trip?.gap_before_time || |
| 35 | + attributes.gap_after_time || trip?.gap_after_time || null; |
| 36 | + } |
| 37 | + return semanticTripTime(trip); |
| 38 | +} |
| 39 | + |
| 40 | +export function insertOdometerGapRows(trips, { toleranceKm = 1, maxGapKm = 1000 } = {}) { |
| 41 | + const ordered = Array.isArray(trips) ? [...trips] : []; |
| 42 | + const result = []; |
| 43 | + |
| 44 | + for (let index = 0; index < ordered.length; index += 1) { |
| 45 | + const current = ordered[index]; |
| 46 | + const previous = index > 0 ? ordered[index - 1] : null; |
| 47 | + |
| 48 | + if (previous && usableAnchor(previous) && usableAnchor(current)) { |
| 49 | + const previousEnd = endMileage(previous); |
| 50 | + const currentStart = numberOrNull(current.start_mileage ?? current.start_mileage_km); |
| 51 | + const gap = previousEnd !== null && currentStart !== null |
| 52 | + ? rounded(currentStart - previousEnd) |
| 53 | + : null; |
| 54 | + |
| 55 | + if (gap !== null && gap > toleranceKm && gap <= maxGapKm) { |
| 56 | + const previousId = String(previous.server_id || previous.id || index - 1); |
| 57 | + const currentId = String(current.server_id || current.id || index); |
| 58 | + result.push({ |
| 59 | + id: `odometer-gap:${previousId}:${currentId}`, |
| 60 | + server_id: `odometer-gap:${previousId}:${currentId}`, |
| 61 | + source: "reconstructed_odometer_gap", |
| 62 | + sources: ["odometer_continuity"], |
| 63 | + reconstructed_gap: true, |
| 64 | + status: "reconstructed", |
| 65 | + confidence: "high", |
| 66 | + start_time: null, |
| 67 | + end_time: null, |
| 68 | + gap_after_time: canonicalTime(previous), |
| 69 | + gap_before_time: current.start_time || current.end_time || null, |
| 70 | + duration_seconds: null, |
| 71 | + distance_km: gap, |
| 72 | + start_mileage: rounded(previousEnd), |
| 73 | + end_mileage: rounded(currentStart), |
| 74 | + start_mileage_km: rounded(previousEnd), |
| 75 | + end_mileage_km: rounded(currentStart), |
| 76 | + average_speed: null, |
| 77 | + average_speed_kmh: null, |
| 78 | + energy_kwh: null, |
| 79 | + energy_per_100_km: null, |
| 80 | + fuel_consumption_l: null, |
| 81 | + fuel_consumption_l_100km: null, |
| 82 | + trip_type: "unknown", |
| 83 | + valid_for_statistics: false, |
| 84 | + quality_flags: ["reconstructed_odometer_gap"], |
| 85 | + }); |
| 86 | + } |
| 87 | + } |
| 88 | + result.push(current); |
| 89 | + } |
| 90 | + return result; |
| 91 | +} |
0 commit comments