@@ -102,6 +102,14 @@ int DovesLapTimer::loop(double currentLat, double currentLng, float currentAltit
102102 }
103103 }
104104
105+ // Save current fix as previous for next iteration's Catmull-Rom pre-crossing point
106+ prevFixLat = currentLat;
107+ prevFixLng = currentLng;
108+ prevFixTime = millisecondsSinceMidnight;
109+ prevFixOdometer = totalDistanceTraveled;
110+ prevFixSpeedKmh = currentSpeedkmh;
111+ hasPrevFix = true ;
112+
105113 return nearAnyLine ? 0 : -1 ;
106114}
107115
@@ -256,9 +264,18 @@ bool DovesLapTimer::checkStartFinish(double currentLat, double currentLng) {
256264 debugln ();
257265 debugln (F (" we are possibly crossing" ));
258266 crossing = true ;
259- // crossingStartedLineSide = pointOnSideOfLine(currentLat, currentLng, startFinishPointALat, startFinishPointALng, startFinishPointBLat, startFinishPointBLng);
260267
261- // Capture this first point - it's important context for Catmull-Rom interpolation
268+ // Insert previous GPS fix as pre-crossing point (gives Catmull-Rom its p0 control point)
269+ if (hasPrevFix) {
270+ crossingPointBuffer[crossingPointBufferIndex].lat = prevFixLat;
271+ crossingPointBuffer[crossingPointBufferIndex].lng = prevFixLng;
272+ crossingPointBuffer[crossingPointBufferIndex].time = prevFixTime;
273+ crossingPointBuffer[crossingPointBufferIndex].odometer = prevFixOdometer;
274+ crossingPointBuffer[crossingPointBufferIndex].speedKmh = prevFixSpeedKmh;
275+ crossingPointBufferIndex = (crossingPointBufferIndex + 1 ) % crossingPointBufferSize;
276+ }
277+
278+ // Capture current point
262279 crossingPointBuffer[crossingPointBufferIndex].lat = currentLat;
263280 crossingPointBuffer[crossingPointBufferIndex].lng = currentLng;
264281 crossingPointBuffer[crossingPointBufferIndex].time = millisecondsSinceMidnight;
@@ -449,16 +466,15 @@ double DovesLapTimer::catmullRom(double p0, double p1, double p2, double p3, dou
449466void DovesLapTimer::interpolateCrossingPoint (double & crossingLat, double & crossingLng, unsigned long & crossingTime, double & crossingOdometer, double pointALat, double pointALng, double pointBLat, double pointBLng) {
450467 int numPoints = crossingPointBufferFull ? crossingPointBufferSize : crossingPointBufferIndex;
451468
452- // Variables to store the best pair of points
453- int bestIndexA = -1 ;
454- int bestIndexB = -1 ;
455- double bestSumDistances = INFINITY ;
469+ // Find the first pair of consecutive buffer points on opposite sides of the crossing line.
470+ // In a normal pass there is exactly one such pair - the two GPS fixes that straddle the line.
471+ int crossingIndexA = -1 ;
472+ int crossingIndexB = -1 ;
473+ double crossingSumDistances = INFINITY ;
456474
457- // Iterate through the crossingPointBuffer, comparing the sum of distances from the start/finish line of each pair of consecutive points
458475 for (int i = 0 ; i < numPoints - 1 ; i++) {
459476 double distA = pointLineSegmentDistance (crossingPointBuffer[i].lat , crossingPointBuffer[i].lng , pointALat, pointALng, pointBLat, pointBLng);
460477 double distB = pointLineSegmentDistance (crossingPointBuffer[i + 1 ].lat , crossingPointBuffer[i + 1 ].lng , pointALat, pointALng, pointBLat, pointBLng);
461- double sumDistances = distA + distB;
462478
463479 int sideA = pointOnSideOfLine (crossingPointBuffer[i].lat , crossingPointBuffer[i].lng , pointALat, pointALng, pointBLat, pointBLng);
464480 int sideB = pointOnSideOfLine (crossingPointBuffer[i + 1 ].lat , crossingPointBuffer[i + 1 ].lng , pointALat, pointALng, pointBLat, pointBLng);
@@ -474,71 +490,64 @@ void DovesLapTimer::interpolateCrossingPoint(double& crossingLat, double& crossi
474490 debug (F (" sideB: " ));
475491 debug (sideB);
476492 debug (F (" sum: " ));
477- debugln (sumDistances, 2 );
478-
479- // Update the best pair of points if the current pair has a smaller sum of distances and the points are on opposite sides of the line
480- // todo: investigate if accuracy "on line" is good enough to avoid interpolation
481- if (sumDistances < bestSumDistances && sideA != sideB) {
482- debug (F (" new best sum: " ));
483- debugln (sumDistances, 2 );
484- bestSumDistances = sumDistances;
485- bestIndexA = i;
486- bestIndexB = i + 1 ;
487- // this seems safe, as soon as we cross the line, no two points can be closer...
493+ debugln (distA + distB, 2 );
494+
495+ // First pair on opposite sides of the line = the crossing pair
496+ if (sideA != sideB) {
497+ crossingIndexA = i;
498+ crossingIndexB = i + 1 ;
499+ crossingSumDistances = distA + distB;
500+ debug (F (" crossing pair found, sum: " ));
501+ debugln (crossingSumDistances, 2 );
488502 break ;
489503 }
490504 }
491- debug (F (" bestSumDistances : " ));
492- debugln (bestSumDistances );
505+ debug (F (" crossingSumDistances : " ));
506+ debugln (crossingSumDistances );
493507
494- // Make sure we found a valid pair of points
495- if (bestSumDistances < crossingThresholdMeters && bestIndexA != -1 && bestIndexB != -1 ) {
508+ // Validate: the crossing pair must exist and be close enough to the line
509+ if (crossingSumDistances < crossingThresholdMeters && crossingIndexA != -1 && crossingIndexB != -1 ) {
496510 debugln (F (" ~~~ VALID CROSSING ~~~" ));
497511
512+ // Compute the interpolation factor (t) from distances and speeds at the crossing pair
513+ double distA = pointLineSegmentDistance (crossingPointBuffer[crossingIndexA].lat , crossingPointBuffer[crossingIndexA].lng , pointALat, pointALng, pointBLat, pointBLng);
514+ double distB = pointLineSegmentDistance (crossingPointBuffer[crossingIndexB].lat , crossingPointBuffer[crossingIndexB].lng , pointALat, pointALng, pointBLat, pointBLng);
515+ double t = interpolateWeight (distA, distB, crossingPointBuffer[crossingIndexA].speedKmh , crossingPointBuffer[crossingIndexB].speedKmh );
516+
517+ // Time and odometer are always interpolated linearly (monotonic values that
518+ // should not overshoot), regardless of the interpolation mode for position.
519+ double deltaOdometer = crossingPointBuffer[crossingIndexB].odometer - crossingPointBuffer[crossingIndexA].odometer ;
520+ double deltaTime = crossingPointBuffer[crossingIndexB].time - crossingPointBuffer[crossingIndexA].time ;
521+ crossingOdometer = crossingPointBuffer[crossingIndexA].odometer + t * deltaOdometer;
522+ crossingTime = crossingPointBuffer[crossingIndexA].time + t * deltaTime;
523+
498524 if (forceLinear) {
499- // Linear interpolation
500- double distA = pointLineSegmentDistance (crossingPointBuffer[bestIndexA].lat , crossingPointBuffer[bestIndexA].lng , pointALat, pointALng, pointBLat, pointBLng);
501- double distB = pointLineSegmentDistance (crossingPointBuffer[bestIndexB].lat , crossingPointBuffer[bestIndexB].lng , pointALat, pointALng, pointBLat, pointBLng);
502- double t = interpolateWeight (distA, distB, crossingPointBuffer[bestIndexA].speedKmh , crossingPointBuffer[bestIndexB].speedKmh );
503-
504- double deltaLat = crossingPointBuffer[bestIndexB].lat - crossingPointBuffer[bestIndexA].lat ;
505- double deltaLon = crossingPointBuffer[bestIndexB].lng - crossingPointBuffer[bestIndexA].lng ;
506- double deltaOdometer = crossingPointBuffer[bestIndexB].odometer - crossingPointBuffer[bestIndexA].odometer ;
507- double deltaTime = crossingPointBuffer[bestIndexB].time - crossingPointBuffer[bestIndexA].time ;
508-
509- crossingLat = crossingPointBuffer[bestIndexA].lat + t * deltaLat;
510- crossingLng = crossingPointBuffer[bestIndexA].lng + t * deltaLon;
511- crossingOdometer = crossingPointBuffer[bestIndexA].odometer + t * deltaOdometer;
512- crossingTime = crossingPointBuffer[bestIndexA].time + t * deltaTime;
525+ // Linear interpolation for position
526+ double deltaLat = crossingPointBuffer[crossingIndexB].lat - crossingPointBuffer[crossingIndexA].lat ;
527+ double deltaLon = crossingPointBuffer[crossingIndexB].lng - crossingPointBuffer[crossingIndexA].lng ;
528+
529+ crossingLat = crossingPointBuffer[crossingIndexA].lat + t * deltaLat;
530+ crossingLng = crossingPointBuffer[crossingIndexA].lng + t * deltaLon;
513531 } else {
514- // Catmull-Rom spline interpolation requires 4 control points:
515- // index0 (before A), index1 (A), index2 (B), index3 (after B)
516- // Check bounds: we need bestIndexA >= 1 and bestIndexB <= numPoints - 2
517- bool canUseCatmullRom = (bestIndexA >= 1 ) && (bestIndexB <= numPoints - 2 );
532+ // Catmull-Rom spline interpolation for position only.
533+ // Requires 4 control points: p0 (before A), p1 (A), p2 (B), p3 (after B)
534+ bool canUseCatmullRom = (crossingIndexA >= 1 ) && (crossingIndexB <= numPoints - 2 );
518535
519536 if (!canUseCatmullRom) {
520- // Not enough points for Catmull-Rom, fall back to linear interpolation
537+ // Not enough points for Catmull-Rom, fall back to linear
521538 debugln (F (" Catmull-Rom: insufficient control points, using linear fallback" ));
522539
523- double distA = pointLineSegmentDistance (crossingPointBuffer[bestIndexA].lat , crossingPointBuffer[bestIndexA].lng , pointALat, pointALng, pointBLat, pointBLng);
524- double distB = pointLineSegmentDistance (crossingPointBuffer[bestIndexB].lat , crossingPointBuffer[bestIndexB].lng , pointALat, pointALng, pointBLat, pointBLng);
525- double t = interpolateWeight (distA, distB, crossingPointBuffer[bestIndexA].speedKmh , crossingPointBuffer[bestIndexB].speedKmh );
526-
527- double deltaLat = crossingPointBuffer[bestIndexB].lat - crossingPointBuffer[bestIndexA].lat ;
528- double deltaLon = crossingPointBuffer[bestIndexB].lng - crossingPointBuffer[bestIndexA].lng ;
529- double deltaOdometer = crossingPointBuffer[bestIndexB].odometer - crossingPointBuffer[bestIndexA].odometer ;
530- double deltaTime = crossingPointBuffer[bestIndexB].time - crossingPointBuffer[bestIndexA].time ;
540+ double deltaLat = crossingPointBuffer[crossingIndexB].lat - crossingPointBuffer[crossingIndexA].lat ;
541+ double deltaLon = crossingPointBuffer[crossingIndexB].lng - crossingPointBuffer[crossingIndexA].lng ;
531542
532- crossingLat = crossingPointBuffer[bestIndexA].lat + t * deltaLat;
533- crossingLng = crossingPointBuffer[bestIndexA].lng + t * deltaLon;
534- crossingOdometer = crossingPointBuffer[bestIndexA].odometer + t * deltaOdometer;
535- crossingTime = crossingPointBuffer[bestIndexA].time + t * deltaTime;
543+ crossingLat = crossingPointBuffer[crossingIndexA].lat + t * deltaLat;
544+ crossingLng = crossingPointBuffer[crossingIndexA].lng + t * deltaLon;
536545 } else {
537546 // We have 4 valid control points for Catmull-Rom
538- int index0 = bestIndexA - 1 ;
539- int index1 = bestIndexA ;
540- int index2 = bestIndexB ;
541- int index3 = bestIndexB + 1 ;
547+ int index0 = crossingIndexA - 1 ;
548+ int index1 = crossingIndexA ;
549+ int index2 = crossingIndexB ;
550+ int index3 = crossingIndexB + 1 ;
542551
543552 debugln (F (" Catmull-Rom: using spline interpolation" ));
544553 debug (F (" indices: " ));
@@ -550,22 +559,14 @@ void DovesLapTimer::interpolateCrossingPoint(double& crossingLat, double& crossi
550559 debug (F (" , " ));
551560 debugln (index3);
552561
553- // Compute the interpolation factor based on distance and speed
554- double distA = pointLineSegmentDistance (crossingPointBuffer[index1].lat , crossingPointBuffer[index1].lng , pointALat, pointALng, pointBLat, pointBLng);
555- double distB = pointLineSegmentDistance (crossingPointBuffer[index2].lat , crossingPointBuffer[index2].lng , pointALat, pointALng, pointBLat, pointBLng);
556- double t = interpolateWeight (distA, distB, crossingPointBuffer[index1].speedKmh , crossingPointBuffer[index2].speedKmh );
557-
558- // Perform Catmull-Rom spline interpolation for latitude, longitude, time, and odometer
562+ // Catmull-Rom for lat/lng only - spline smoothing helps with curved paths
559563 crossingLat = catmullRom (crossingPointBuffer[index0].lat , crossingPointBuffer[index1].lat , crossingPointBuffer[index2].lat , crossingPointBuffer[index3].lat , t);
560564 crossingLng = catmullRom (crossingPointBuffer[index0].lng , crossingPointBuffer[index1].lng , crossingPointBuffer[index2].lng , crossingPointBuffer[index3].lng , t);
561- crossingTime = catmullRom (crossingPointBuffer[index0].time , crossingPointBuffer[index1].time , crossingPointBuffer[index2].time , crossingPointBuffer[index3].time , t);
562- crossingOdometer = catmullRom (crossingPointBuffer[index0].odometer , crossingPointBuffer[index1].odometer , crossingPointBuffer[index2].odometer , crossingPointBuffer[index3].odometer , t);
563565 }
564566 }
565567 } else {
566568 debugln (F (" ~~~ INVALID CROSSING ~~~ INVALID CROSSING ~~~ INVALID CROSSING ~~~ INVALID CROSSING ~~~" ));
567569 }
568- return ;
569570}
570571
571572// ///////// sector timing helper methods
@@ -736,7 +737,17 @@ bool DovesLapTimer::checkSectorLine(double currentLat, double currentLng, double
736737 debugln (F (" crossing zone" ));
737738 crossingFlag = true ;
738739
739- // Capture this first point - it's important context for Catmull-Rom interpolation
740+ // Insert previous GPS fix as pre-crossing point (gives Catmull-Rom its p0 control point)
741+ if (hasPrevFix) {
742+ crossingPointBuffer[crossingPointBufferIndex].lat = prevFixLat;
743+ crossingPointBuffer[crossingPointBufferIndex].lng = prevFixLng;
744+ crossingPointBuffer[crossingPointBufferIndex].time = prevFixTime;
745+ crossingPointBuffer[crossingPointBufferIndex].odometer = prevFixOdometer;
746+ crossingPointBuffer[crossingPointBufferIndex].speedKmh = prevFixSpeedKmh;
747+ crossingPointBufferIndex = (crossingPointBufferIndex + 1 ) % crossingPointBufferSize;
748+ }
749+
750+ // Capture current point
740751 crossingPointBuffer[crossingPointBufferIndex].lat = currentLat;
741752 crossingPointBuffer[crossingPointBufferIndex].lng = currentLng;
742753 crossingPointBuffer[crossingPointBufferIndex].time = millisecondsSinceMidnight;
@@ -789,6 +800,12 @@ void DovesLapTimer::reset() {
789800 positionPrevLng = 0 ;
790801 positionPrevAlt = 0 ;
791802 firstPositionReceived = false ;
803+ prevFixLat = 0 ;
804+ prevFixLng = 0 ;
805+ prevFixTime = 0 ;
806+ prevFixOdometer = 0 ;
807+ prevFixSpeedKmh = 0 ;
808+ hasPrevFix = false ;
792809
793810 // Reset the crossingPointBuffer index and full status
794811 crossing = false ;
0 commit comments