Problem
There is no way to get aggregated statistics across all tracks and routes in a GPX file. Users who have GPX files with multiple <trkseg> (e.g., devices that create new segments on pause) need to manually merge stats to get a unified view.
Related discussion: #67 (comment)
Current state
Stats exist at the segment, track, and route level, but not at the file level:
GpxFile ← no stats
├── Track[0] ← stats (aggregated from segments)
│ ├── Segment[0] ← stats
│ └── Segment[1] ← stats
├── Track[1] ← stats
└── Route[0] ← stats
Proposed solution
Add a ?Stats $stats property to GpxFile and a FileStatsAnalyzer that aggregates all track + route stats in finalizeFile().
Model change
// src/phpGPX/Models/GpxFile.php
class GpxFile implements \JsonSerializable
{
// ... existing properties ...
public ?Stats $stats = null;
}
New analyzer
// src/phpGPX/Analysis/FileStatsAnalyzer.php
class FileStatsAnalyzer extends AbstractPointAnalyzer
{
public function finalizeFile(GpxFile $gpxFile): void
{
/** @var Stats[] $allStats */
$allStats = [];
foreach ($gpxFile->tracks as $track) {
if ($track->stats !== null) {
$allStats[] = $track->stats;
}
}
foreach ($gpxFile->routes as $route) {
if ($route->stats !== null) {
$allStats[] = $route->stats;
}
}
if (empty($allStats)) {
return;
}
$fileStats = new Stats();
// Sum: distance, realDistance, elevation, duration, movingDuration
foreach ($allStats as $stats) {
$fileStats->distance = ($fileStats->distance ?? 0) + ($stats->distance ?? 0);
$fileStats->realDistance = ($fileStats->realDistance ?? 0) + ($stats->realDistance ?? 0);
$fileStats->cumulativeElevationGain = ($fileStats->cumulativeElevationGain ?? 0) + ($stats->cumulativeElevationGain ?? 0);
$fileStats->cumulativeElevationLoss = ($fileStats->cumulativeElevationLoss ?? 0) + ($stats->cumulativeElevationLoss ?? 0);
$fileStats->duration = ($fileStats->duration ?? 0) + ($stats->duration ?? 0);
$fileStats->movingDuration = ($fileStats->movingDuration ?? 0) + ($stats->movingDuration ?? 0);
}
// Extrema: altitude, timestamps
foreach ($allStats as $stats) {
if ($stats->minAltitude !== null && ($fileStats->minAltitude === null || $stats->minAltitude < $fileStats->minAltitude)) {
$fileStats->minAltitude = $stats->minAltitude;
$fileStats->minAltitudeCoords = $stats->minAltitudeCoords;
}
if ($stats->maxAltitude !== null && ($fileStats->maxAltitude === null || $stats->maxAltitude > $fileStats->maxAltitude)) {
$fileStats->maxAltitude = $stats->maxAltitude;
$fileStats->maxAltitudeCoords = $stats->maxAltitudeCoords;
}
if ($stats->startedAt !== null && ($fileStats->startedAt === null || $stats->startedAt < $fileStats->startedAt)) {
$fileStats->startedAt = $stats->startedAt;
$fileStats->startedAtCoords = $stats->startedAtCoords;
}
if ($stats->finishedAt !== null && ($fileStats->finishedAt === null || $stats->finishedAt > $fileStats->finishedAt)) {
$fileStats->finishedAt = $stats->finishedAt;
$fileStats->finishedAtCoords = $stats->finishedAtCoords;
}
}
// Derived: speed, pace (same formula as Engine::computeDerivedStats)
if ($fileStats->duration > 0 && $fileStats->distance !== null) {
$fileStats->averageSpeed = $fileStats->distance / $fileStats->duration;
}
if ($fileStats->distance > 0) {
$fileStats->averagePace = $fileStats->duration / ($fileStats->distance / 1000);
}
if ($fileStats->movingDuration > 0 && $fileStats->distance !== null) {
$fileStats->movingAverageSpeed = $fileStats->distance / $fileStats->movingDuration;
}
// Bounds from BoundsAnalyzer (already on metadata)
$fileStats->bounds = $gpxFile->metadata?->bounds;
$gpxFile->stats = $fileStats;
}
}
Registration in Engine::default()
// Added to Engine::default() — after all other analyzers
->addAnalyzer(new FileStatsAnalyzer())
Architecture fit
This follows the exact same pattern as BoundsAnalyzer which already uses finalizeFile() to merge bounds across the whole file. The FileStatsAnalyzer only implements finalizeFile() — all other lifecycle methods are no-ops via AbstractPointAnalyzer.
Usage
$gpx = new phpGPX();
$file = Engine::default()->process($gpx->load('track.gpx'));
// File-level stats (merged across all tracks/routes)
echo $file->stats->distance; // Total distance
echo $file->stats->duration; // Total duration
echo $file->stats->startedAt; // Earliest start time
echo $file->stats->finishedAt; // Latest end time
// Per-track stats still available
echo $file->tracks[0]->stats->distance;
Problem
There is no way to get aggregated statistics across all tracks and routes in a GPX file. Users who have GPX files with multiple
<trkseg>(e.g., devices that create new segments on pause) need to manually merge stats to get a unified view.Related discussion: #67 (comment)
Current state
Stats exist at the segment, track, and route level, but not at the file level:
Proposed solution
Add a
?Stats $statsproperty toGpxFileand aFileStatsAnalyzerthat aggregates all track + route stats infinalizeFile().Model change
New analyzer
Registration in
Engine::default()Architecture fit
This follows the exact same pattern as
BoundsAnalyzerwhich already usesfinalizeFile()to merge bounds across the whole file. TheFileStatsAnalyzeronly implementsfinalizeFile()— all other lifecycle methods are no-ops viaAbstractPointAnalyzer.Usage