Skip to content

ElevationSetter analyzer with pluggable elevation provider #85

@Sibyx

Description

@Sibyx

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 supportgetElevations() enables efficient batch queries for API-based providers.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions