Skip to content

omnilab/elevation

Omnilab Elevation: the elevation of points and of a profile between two points from a digital elevation model (SRTM, ASTER, EU-DEM, Mapzen, ETOPO1, GEBCO...), through Open Topo Data's public API or a self-hosted instance, as Points with their elevation in metres, outside the contract's shapes; no key needed (100 locations a call, a call a second, 1,000 a day on the public instance).

Floor
Provider · Omnilab
Version
1.x
Conformance suite
not wired
Verified for real
Its README does not say
Licence
MIT
composer require omnilab/elevation:1.x-dev

Installation

composer require omnilab/elevation
use Omnilab\Elevation\ElevationSourceFactory;

$dem = (new ElevationSourceFactory($httpClient))->create([
    'mailto' => 'support@glitchr.io',  // optional
    'dataset' => 'srtm30m',            // optional: a dataset, or a list (the first with a value answers)
    'interpolation' => 'bilinear',     // optional: nearest, bilinear, cubic
]);

With the Symfony bundle, the factory is registered as elevation when the bundle knows it; otherwise register ElevationSourceFactory as a service.

Credentials

None on the public instance (api.opentopodata.org): no key, no account, within 100 locations a call, a call a second and 1,000 calls a day. Beyond that, a self-hosted instance of Open Topo Data (open source) is named by base_uri.

Options

The factory's options, with the defaults it applies (ElevationSourceFactory):

Option Type Default Required Effect
dataset string|list<string> srtm30m no the dataset asked, or a list (or a comma-separated string): with several, each point takes the first that has a value there. Lower-case letters, digits, _ and - only, else \InvalidArgumentException at create()
interpolation string bilinear no nearest, bilinear or cubic (another is an \InvalidArgumentException at create())
mailto ?string null no a contact address, sent in the User-Agent
user_agent ?string null no the whole User-Agent, in place of the one made from mailto
throttle float 1 no seconds left between two calls: the public instance takes one a second
base_uri string https://api.opentopodata.org/v1/ no the instance: the public one, or a self-hosted one

Capabilities

capabilities() answers an empty list: every operation of the contract throws NotSupportedException before any call. The source answers through methods of its own: at(Point ...$points): list<Point> (a call for each hundred points), profile(Point $from, Point $to, int $samples = 100): list<Point> (2 to 100 samples, one call) and lastDatasets(): list<?string> (the dataset each point of the last answer came from).

Capability Supported
entity no nothing here is an entity
search no
series no an elevation is no series: at() and profile() answer Points
products no
occurrences no
datasets no
jobs no
open no
structure no
cross_references no
forecast no
historical no
area no at() takes points; a box is not read
station no
cloud_cover no

Which API

Open Topo Data (https://api.opentopodata.org/v1/{datasets}?locations=lat,lng|lat,lng&interpolation= and &samples= for a profile). Its public instance served on 2026-10-16: aster30m, bkg200m, emod2018, etopo1, eudem25m, gebco2020, mapzen, ned10m, nzdem8m, srtm30m, srtm90m and others (its datasets list). A dataset answers null outside its extent (SRTM at sea, at 0, -150). Errors: {"error": ..., "status": "INVALID_REQUEST"} with a 400 (an unknown dataset), a ProviderException with the message; a 429 a RateLimitedException.

Its shape: none of the contract's

capabilities() is empty; series(), entity() and the rest throw NotSupportedException.

Method Answers
at(Point ...$points) the points, in their order, each with its elevation (m, rounded to the centimetre; null where no dataset has a value): a call for each hundred
profile(Point $from, Point $to, int $samples = 100) $samples points (2 to 100) evenly spaced along the great circle, the ends included, each with its elevation: one call
lastDatasets() the dataset each point of the last answer came from (null for none)

The Collector

No path: nothing here is an entity.

What the API does not give here

The datasets' list (/datasets), the health endpoint, POST requests of many locations, and the nodata_value option (null is kept for "no value") are not used.

Example

<?php

require __DIR__.'/vendor/autoload.php';

use Omnilab\Elevation\ElevationSourceFactory;
use Omnilab\Model\Point;
use Symfony\Component\HttpClient\HttpClient;

$dem = (new ElevationSourceFactory(HttpClient::create()))->create([
    'mailto' => 'you@example.org',
    'dataset' => ['srtm30m', 'gebco2020'],   // SRTM on land, GEBCO where SRTM has no value
]);

$points = $dem->at(new Point(48.5734, 7.7521), new Point(45.8326, 6.8652), new Point(0, -150));
foreach ($points as $i => $point) {
    printf("%s,%s  %s m  (%s)\n", $point->latitude, $point->longitude, $point->elevation ?? 'null', $dem->lastDatasets()[$i] ?? 'no dataset');
}

$profile = $dem->profile(new Point(48.5734, 7.7521), new Point(45.8326, 6.8652), 5);
echo 'Strasbourg to Mont Blanc: ', implode(', ', array_map(static fn (Point $p) => $p->elevation, $profile)), " m\n";
48.5734,7.7521  150 m  (srtm30m)
45.8326,6.8652  4791 m  (srtm30m)
0,-150  -4449 m  (gebco2020)
Strasbourg to Mont Blanc: 150, 210, 882, 1021, 4791 m

(as answered on 2026-10-27, two calls a second apart)

# config/packages/omnilab.yaml
omnilab:
    sources:
        elevation: { factory: elevation, options: { mailto: '%env(OMNILAB_MAILTO)%', dataset: [srtm30m, gebco2020] } }

The bundle injects it as SourceInterface $elevation; at() and profile() are methods of Omnilab\Elevation\ElevationSource, not of the interface: check the instance ($elevation instanceof ElevationSource) before calling them.

Errors

Every call goes through HttpSource: a 404 or a 410 (and a 204) is null - an empty page for a list -; a 429 is a RateLimitedException (retryAfter from Retry-After); a 5xx, a time-out or a host that does not answer an UnavailableException; any other 4xx a ProviderException with its status and what the service said; an answer that is not JSON a ProviderException, a web page answered for JSON an UnavailableException.

Besides: a 404 (an address the instance does not serve) is a ProviderException that asks whether the dataset exists, not a null; an answer whose status is not OK a ProviderException with Open Topo Data's error (an unknown dataset: INVALID_REQUEST, with a 400); an answer without a result's location, or with fewer results than points asked, a ProviderException. A profile of fewer than 2 or more than 100 samples is an \InvalidArgumentException before any call; every operation of the contract a NotSupportedException.

Verified, and not

Against the public API on 2026-10-16: Strasbourg (150 m), the summit of Mont Blanc (4,791 m in SRTM 30 m, 4,780.3 m in EU-DEM 25 m) and a point of the Pacific (null in SRTM; -4,449 m in GEBCO 2020) in one call each; the profile from Strasbourg to Mont Blanc in five samples (150, 210, 882, 1,021, 4,791 m); an unknown dataset (400). The fixtures in Tests/Fixtures/ are its answers.

Again on 2026-10-27: the same three points and the same profile, the same elevations (the example above).

Not verified: the daily limit of 1,000 calls (not reached) and the public instance's answer past it; a self-hosted instance.

Responsibility

This package is an independent, unofficial client: it is not affiliated with, endorsed by or sponsored by Open Topo Data nor by the producers of its datasets. Each dataset keeps its own licence and attribution (SRTM: NASA/USGS; EU-DEM: Copernicus; GEBCO: the GEBCO Compilation Group; ASTER GDEM: NASA/METI...), as Open Topo Data's dataset pages state and the README's Terms of use sums up. Whoever runs the package keeps to the public instance's limits, credits the dataset used, and is responsible for any use made of what it returns: see the README's Responsibility section.

Propose a change docs/index.md