Problem
GPS-recorded elevation data is often noisy or missing entirely. Users need a way to set/override elevation values from external sources (e.g., SRTM/HGT files, DEM APIs) before stats are calculated.
Related discussion: #67 (comment)
Proposed solution
A clean 2.x implementation using the Engine's analyzer architecture with a pluggable provider interface.
1. ElevationProviderInterface
// src/phpGPX/Analysis/ElevationProviderInterface.php
namespace phpGPX\Analysis;
/**
* Interface for external elevation data sources.
*
* Implementations can read from HGT/SRTM files, call DEM APIs,
* query local databases, etc.
*/
interface ElevationProviderInterface
{
/**
* Get elevation for a single coordinate.
*
* @return float|null Elevation in meters, or null if unavailable
*/
public function getElevation(float $latitude, float $longitude): ?float;
/**
* Get elevations for multiple coordinates in batch.
*
* Default implementation calls getElevation() in a loop.
* Override for providers that support batch queries (APIs, etc.)
*
* @param array<array{lat: float, lon: float}> $coordinates
* @return array<float|null> Elevations in same order as input
*/
public function getElevations(array $coordinates): array;
}
2. ElevationSetterMode enum
// src/phpGPX/Analysis/ElevationSetterMode.php
namespace phpGPX\Analysis;
enum ElevationSetterMode: string
{
/** Replace all elevation values (for noisy GPS records) */
case OVERRIDE = 'override';
/** Only fill missing or zero elevation values */
case FIX_NULL = 'fix_null';
}
3. ElevationSetterAnalyzer
// src/phpGPX/Analysis/ElevationSetterAnalyzer.php
namespace phpGPX\Analysis;
use phpGPX\Models\Point;
use phpGPX\Models\Stats;
/**
* Point-mutating analyzer that sets elevation from an external provider.
*
* IMPORTANT: Must be registered BEFORE DistanceAnalyzer, ElevationAnalyzer,
* and AltitudeAnalyzer so that corrected elevations are used in stats.
*/
class ElevationSetterAnalyzer extends AbstractPointAnalyzer
{
public function __construct(
private readonly ElevationProviderInterface $provider,
private readonly ElevationSetterMode $mode = ElevationSetterMode::FIX_NULL,
) {
}
public function visit(Point $current, ?Point $previous): void
{
if ($current->latitude === null || $current->longitude === null) {
return;
}
$shouldSet = match ($this->mode) {
ElevationSetterMode::OVERRIDE => true,
ElevationSetterMode::FIX_NULL => $current->elevation === null || $current->elevation == 0,
};
if ($shouldSet) {
$elevation = $this->provider->getElevation($current->latitude, $current->longitude);
if ($elevation !== null) {
$current->elevation = $elevation;
}
}
}
}
Usage
Since this analyzer has an external dependency (the provider), it is not included in Engine::default(). Users build a custom engine:
use phpGPX\phpGPX;
use phpGPX\Analysis\Engine;
use phpGPX\Analysis\ElevationSetterAnalyzer;
use phpGPX\Analysis\ElevationSetterMode;
use phpGPX\Analysis\DistanceAnalyzer;
use phpGPX\Analysis\ElevationAnalyzer;
use phpGPX\Analysis\AltitudeAnalyzer;
use phpGPX\Analysis\TimestampAnalyzer;
use phpGPX\Analysis\BoundsAnalyzer;
use phpGPX\Analysis\MovementAnalyzer;
use phpGPX\Analysis\TrackPointExtensionAnalyzer;
// User's own provider implementation
$hgtProvider = new MyHgtFileProvider('/path/to/hgt/files');
// ElevationSetter MUST be first — before distance/elevation analyzers
$engine = (new Engine())
->addAnalyzer(new ElevationSetterAnalyzer(
provider: $hgtProvider,
mode: ElevationSetterMode::FIX_NULL,
))
->addAnalyzer(new DistanceAnalyzer())
->addAnalyzer(new ElevationAnalyzer())
->addAnalyzer(new AltitudeAnalyzer())
->addAnalyzer(new TimestampAnalyzer())
->addAnalyzer(new BoundsAnalyzer())
->addAnalyzer(new MovementAnalyzer())
->addAnalyzer(new TrackPointExtensionAnalyzer());
$gpx = new phpGPX(engine: $engine);
$file = $gpx->load('track.gpx');
// Elevations are now corrected, stats use corrected values
Example: simple HGT file provider skeleton
class MyHgtFileProvider implements ElevationProviderInterface
{
public function __construct(
private readonly string $hgtDirectory,
) {
}
public function getElevation(float $latitude, float $longitude): ?float
{
// 1. Determine HGT filename from coordinates (e.g., N47E015.hgt)
// 2. Read elevation from the SRTM grid
// 3. Optionally interpolate between grid points
return $elevation;
}
public function getElevations(array $coordinates): array
{
return array_map(
fn ($coord) => $this->getElevation($coord['lat'], $coord['lon']),
$coordinates,
);
}
}
Design notes
- Point-mutating analyzer — Unlike stats-collecting analyzers, this one modifies
Point::$elevation. The architecture supports this since visit() receives point objects.
- Registration order matters — Must be registered before
DistanceAnalyzer (uses elevation for realDistance), ElevationAnalyzer (gain/loss), and AltitudeAnalyzer (min/max). This should be clearly documented.
- No core dependencies — The library provides the interface + analyzer; users bring their own provider implementation. This keeps phpGPX dependency-free.
- Batch support —
getElevations() enables efficient batch queries for API-based providers.
Problem
GPS-recorded elevation data is often noisy or missing entirely. Users need a way to set/override elevation values from external sources (e.g., SRTM/HGT files, DEM APIs) before stats are calculated.
Related discussion: #67 (comment)
Proposed solution
A clean 2.x implementation using the Engine's analyzer architecture with a pluggable provider interface.
1.
ElevationProviderInterface2.
ElevationSetterModeenum3.
ElevationSetterAnalyzerUsage
Since this analyzer has an external dependency (the provider), it is not included in
Engine::default(). Users build a custom engine:Example: simple HGT file provider skeleton
Design notes
Point::$elevation. The architecture supports this sincevisit()receives point objects.DistanceAnalyzer(uses elevation forrealDistance),ElevationAnalyzer(gain/loss), andAltitudeAnalyzer(min/max). This should be clearly documented.getElevations()enables efficient batch queries for API-based providers.