From 6ac1b25d09dec204d09e0888a6a3799b514f5bb4 Mon Sep 17 00:00:00 2001 From: bensynapse <118375461+bensynapse@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:32:39 +0300 Subject: [PATCH] Fix statistics timestamps for partially timed paths --- src/phpGPX/Models/Route.php | 9 ++- src/phpGPX/Models/Segment.php | 9 ++- src/phpGPX/Models/Track.php | 9 ++- tests/StatsTimestampsTest.php | 115 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 tests/StatsTimestampsTest.php diff --git a/src/phpGPX/Models/Route.php b/src/phpGPX/Models/Route.php index 491a284..c8cfb5f 100644 --- a/src/phpGPX/Models/Route.php +++ b/src/phpGPX/Models/Route.php @@ -96,9 +96,7 @@ public function recalculateStats() $firstPoint = &$this->points[0]; $lastPoint = end($this->points); - $this->stats->startedAt = $firstPoint->time; $this->stats->startedAtCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - $this->stats->finishedAt = $lastPoint->time; $this->stats->finishedAtCoords = ["lat" => $lastPoint->latitude, "lng" => $lastPoint->longitude]; $this->stats->minAltitude = $firstPoint->elevation; $this->stats->minAltitudeCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; @@ -111,6 +109,13 @@ public function recalculateStats() $this->stats->realDistance = $calculator->getRealDistance(); for ($p = 0; $p < $pointCount; $p++) { + if ($this->points[$p]->time !== null) { + if ($this->stats->startedAt === null) { + $this->stats->startedAt = $this->points[$p]->time; + } + $this->stats->finishedAt = $this->points[$p]->time; + } + if ((phpGPX::$IGNORE_ELEVATION_0 === false || $this->points[$p]->elevation > 0) && $this->stats->minAltitude > $this->points[$p]->elevation) { $this->stats->minAltitude = $this->points[$p]->elevation; $this->stats->minAltitudeCoords = ["lat" => $this->points[$p]->latitude, "lng" => $this->points[$p]->longitude]; diff --git a/src/phpGPX/Models/Segment.php b/src/phpGPX/Models/Segment.php index c66e5b0..b181aab 100644 --- a/src/phpGPX/Models/Segment.php +++ b/src/phpGPX/Models/Segment.php @@ -91,9 +91,7 @@ public function recalculateStats() $firstPoint = &$this->points[0]; $lastPoint = end($this->points); - $this->stats->startedAt = $firstPoint->time; $this->stats->startedAtCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - $this->stats->finishedAt = $lastPoint->time; $this->stats->finishedAtCoords = ["lat" => $lastPoint->latitude, "lng" => $lastPoint->longitude]; $this->stats->minAltitude = $firstPoint->elevation; $this->stats->minAltitudeCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; @@ -106,6 +104,13 @@ public function recalculateStats() $this->stats->realDistance = $calculator->getRealDistance(); for ($i = 0; $i < $count; $i++) { + if ($this->points[$i]->time !== null) { + if ($this->stats->startedAt === null) { + $this->stats->startedAt = $this->points[$i]->time; + } + $this->stats->finishedAt = $this->points[$i]->time; + } + if ($this->stats->maxAltitude < $this->points[$i]->elevation) { $this->stats->maxAltitude = $this->points[$i]->elevation; $this->stats->maxAltitudeCoords = ["lat" => $this->points[$i]->latitude, "lng" => $this->points[$i]->longitude]; diff --git a/src/phpGPX/Models/Track.php b/src/phpGPX/Models/Track.php index a4f7e01..79c22ea 100644 --- a/src/phpGPX/Models/Track.php +++ b/src/phpGPX/Models/Track.php @@ -114,9 +114,7 @@ public function recalculateStats() $lastSegment = end($this->segments); $lastPoint = end(end($this->segments)->points); - $this->stats->startedAt = $firstPoint->time; $this->stats->startedAtCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; - $this->stats->finishedAt = $lastPoint->time; $this->stats->finishedAtCoords = ["lat" => $lastPoint->latitude, "lng" => $lastPoint->longitude]; $this->stats->minAltitude = $firstPoint->elevation; $this->stats->minAltitudeCoords = ["lat" => $firstPoint->latitude, "lng" => $firstPoint->longitude]; @@ -124,6 +122,13 @@ public function recalculateStats() for ($s = 0; $s < $segmentsCount; $s++) { $this->segments[$s]->recalculateStats(); + if ($this->stats->startedAt === null) { + $this->stats->startedAt = $this->segments[$s]->stats->startedAt; + } + if ($this->segments[$s]->stats->finishedAt !== null) { + $this->stats->finishedAt = $this->segments[$s]->stats->finishedAt; + } + $this->stats->cumulativeElevationGain += $this->segments[$s]->stats->cumulativeElevationGain; $this->stats->cumulativeElevationLoss += $this->segments[$s]->stats->cumulativeElevationLoss; diff --git a/tests/StatsTimestampsTest.php b/tests/StatsTimestampsTest.php new file mode 100644 index 0000000..417cb16 --- /dev/null +++ b/tests/StatsTimestampsTest.php @@ -0,0 +1,115 @@ + $time) { + $point = new Point($class === Route::class ? Point::ROUTEPOINT : Point::TRACKPOINT); + $point->latitude = 48 + $index / 1000; + $point->longitude = 17; + $point->time = $time === null ? null : new \DateTime($time); + $points[] = $point; + } + + $collection = new $class(); + if ($collection instanceof Track) { + $segment = new Segment(); + $segment->points = $points; + $collection->segments[] = $segment; + } else { + $collection->points = $points; + } + $collection->recalculateStats(); + + $this->assertSame($start === null ? null : $points[$start]->time, $collection->stats->startedAt); + $this->assertSame($finish === null ? null : $points[$finish]->time, $collection->stats->finishedAt); + $this->assertSame($points, $collection->getPoints()); + $this->assertSame([ + "lat" => $points[0]->latitude, + "lng" => $points[0]->longitude, + ], $collection->stats->startedAtCoords); + $this->assertSame([ + "lat" => end($points)->latitude, + "lng" => end($points)->longitude, + ], $collection->stats->finishedAtCoords); + + // A partial time range does not measure the duration of the whole path. + if ($times[0] === null || end($times) === null) { + $this->assertNull($collection->stats->duration); + $this->assertNull($collection->stats->averageSpeed); + $this->assertNull($collection->stats->averagePace); + } + + foreach ($points as $point) { + $point->time = null; + } + $collection->recalculateStats(); + $this->assertNull($collection->stats->startedAt); + $this->assertNull($collection->stats->finishedAt); + } + + public function timestampCases() + { + $first = '2020-01-02T10:00:00Z'; + $last = '2020-01-02T10:05:00Z'; + $cases = [ + 'untimed start' => [[null, null, $first, $last], 2, 3], + 'untimed finish' => [[$first, $last, null, null], 0, 1], + 'untimed boundaries and gap' => [[null, $first, null, $last, null], 1, 3], + 'one timed point' => [[null, $first, null], 1, 1], + 'no timestamps' => [[null, null], null, null], + 'fully timed' => [[$first, $last], 0, 1], + 'recorded order' => [[null, $last, $first, null], 1, 2], + ]; + + foreach ([Route::class, Segment::class, Track::class] as $class) { + foreach ($cases as $name => $case) { + yield $class . ': ' . $name => [$class, $case[0], $case[1], $case[2]]; + } + } + } + + public function testLoadedTrackSkipsUntimedSegments() + { + $file = phpGPX::parse(' + + + + + + + + + + + + + '); + $track = $file->tracks[0]; + $this->assertEquals(new \DateTime('2020-01-02T10:00:00Z'), $track->stats->startedAt); + $this->assertEquals(new \DateTime('2020-01-02T10:05:00Z'), $track->stats->finishedAt); + $this->assertCount(4, $track->segments); + $this->assertCount(6, $track->getPoints()); + $this->assertSame($track->segments[1]->stats->startedAt, $track->stats->startedAt); + $this->assertSame($track->segments[2]->stats->finishedAt, $track->stats->finishedAt); + + $roundTrip = phpGPX::parse($file->toXML()->saveXML()); + $this->assertEquals($track->stats->startedAt, $roundTrip->tracks[0]->stats->startedAt); + $this->assertEquals($track->stats->finishedAt, $roundTrip->tracks[0]->stats->finishedAt); + } +}