@glandais/elevation
v3.3.0
Published
TypeScript library for retrieving elevation data from geographic coordinates using terrain RGB tiles
Maintainers
Readme
@glandais/elevation
A TypeScript library for retrieving elevation data from geographic coordinates using terrain RGB tiles.
Features
- 🗺️ High-precision elevation data from SRTM, GMTED, NED, and ETOPO1 sources
- 🚀 High-performance caching with LRU cache for optimal memory usage
- 🌐 Cross-platform support for Browser and Node.js environments
- 🎯 TypeScript-first with complete type definitions
- 🔧 Configurable zoom levels (0-15) for different resolution requirements
- 📦 Multiple build formats (ES modules, CommonJS, UMD, IIFE)
- 🧪 Thoroughly tested with >95% code coverage
- 🌍 Bilinear interpolation for smoother elevation values
- 📈 Elevation profiling between coordinates and along multi-point paths
- 🎛️ Distance-based smoothing with configurable window sizes
- 🔬 Douglas-Peucker filtering for elevation profile simplification
- ⛰️ Cumulative ascent/descent measured at a stated scale, stable under resampling
Installation
npm
Browser
npm install @glandais/elevation// ES6 import (recommended)
import ElevationProvider, { Coordinates } from '@glandais/elevation';
// CommonJS
const { ElevationProvider } = require('@glandais/elevation');
// or
const ElevationProvider = require('@glandais/elevation').default;Node.js
For Node.js environments, install with optional dependencies:
npm install @glandais/elevation canvas sharp node-fetch abort-controllersharp decodes the terrain tiles. It is required for the default WebP tile source:
canvas links no libwebp and cannot decode WebP at all.
// ES6 import
import ElevationProvider from '@glandais/elevation';
// CommonJS
const { ElevationProvider } = require('@glandais/elevation');CDN
<!-- Modern ES Module (recommended) -->
<script
src="https://cdn.jsdelivr.net/npm/@glandais/elevation@3/dist/index.esm.js"
type="module"
></script>
<!-- UMD (browser global) -->
<script src="https://cdn.jsdelivr.net/npm/@glandais/elevation@3/dist/index.umd.js"></script>
<!-- Minified version -->
<script src="https://cdn.jsdelivr.net/npm/@glandais/elevation@3/dist/index.min.js"></script>Local Download
<!-- Download and host locally -->
<!-- For production, use the minified version -->
<script src="path/to/elevation.min.js"></script>
<!-- Or use the UMD version -->
<script src="path/to/elevation.umd.js"></script>Quick Start
Basic usage
// Get elevation at specific coordinates (with interpolation enabled by default)
const elevation = await elevationProvider.getElevation(47.2, -1.5);
console.log(`Elevation: ${elevation}m`);
// Get elevation without interpolation
const rawElevation = await elevationProvider.getElevation(47.2, -1.5, { interpolation: false });
console.log(`Raw elevation: ${rawElevation}m`);TypeScript
Full TypeScript support with accurate type definitions:
import ElevationProvider from '@glandais/elevation';
// Create elevation elevationProvider with default settings
const elevationProvider = new ElevationProvider();JavaScript (ES Module)
// Modern ES Module import from jsdelivr (recommended for browsers)
import ElevationProvider from 'https://cdn.jsdelivr.net/npm/@glandais/[email protected]/+esm';
// Or with a custom name (you can use any name you want)
import MyElevation from 'https://cdn.jsdelivr.net/npm/@glandais/[email protected]/+esm';
// Direct file import (alternative)
import ElevationProvider from 'https://cdn.jsdelivr.net/npm/@glandais/[email protected]/dist/index.esm.js';
// Create elevation provider with default settings
const elevationProvider = new ElevationProvider();JavaScript (UMD/minified)
// Create elevation elevationProvider with default settings
const elevationProvider = new window.Elevation.ElevationProvider();Advanced Configuration
// or new window.Elevation.ElevationProvider
const elevationProvider = new ElevationProvider({
zoomLevel: 12, // Tile zoom level (default: 12 for ~30m resolution)
cacheSize: 100, // Maximum tiles in memory cache (default: 100)
tileUrlTemplate: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
tileSize: 256,
});
// Batch elevation requests
const coordinates = [
{ latitude: 47.2, longitude: -1.5 },
{ latitude: 48.8, longitude: 2.3 },
{ latitude: 51.5, longitude: -0.1 },
];
await elevationProvider.setElevations(coordinates);
console.log('Elevations:', elevations); // [elevation1, elevation2, elevation3]Elevation Profiling
The library provides advanced elevation profiling capabilities for analyzing terrain along paths.
Simple Elevation Profile
// Get elevation profile between two points using getElevationsAlong
const profile = await elevationProvider.getElevationsAlong(
[
{ latitude: 47.2, longitude: -1.5 }, // Start point
{ latitude: 47.25, longitude: -1.45 }, // End point
],
{ step: 25 }
); // 25 meters between points
console.log(`Profile contains ${profile.length} elevation points`);
profile.forEach((point, index) => {
console.log(`Point ${index}: ${point.latitude}, ${point.longitude} - ${point.elevation}m`);
});Multi-Point Path Profile
// Define a hiking trail with multiple waypoints
const hikingTrail = [
{ latitude: 47.2, longitude: -1.5 }, // Trailhead
{ latitude: 47.22, longitude: -1.48 }, // First viewpoint
{ latitude: 47.24, longitude: -1.46 }, // Ridge
{ latitude: 47.26, longitude: -1.44 }, // Summit
];
// Get detailed elevation profile with smoothing
const trailProfile = await elevationProvider.getElevationsAlong(hikingTrail, {
step: 10, // 10 meters between points
smoothingOptions: {
enabled: true,
windowSize: 30, // 30-meter smoothing window
},
filterOptions: {
enabled: true,
tolerance: 3, // 3-meter tolerance for filtering
zExaggeration: 2, // Emphasize elevation changes
},
});
// Analyze the trail
const elevations = trailProfile.map(p => p.elevation);
const minElevation = Math.min(...elevations);
const maxElevation = Math.max(...elevations);
// Measure ascent on the unsmoothed, unfiltered profile: ElevationGain applies its own scale
const rawProfile = await elevationProvider.getElevationsAlong(hikingTrail, { step: 10 });
const { gainM, lossM } = ElevationGain.compute(rawProfile);
console.log(`Trail analysis:`);
console.log(`- Distance: ~${(rawProfile.length - 1) * 10}m`); // step = 10 m
console.log(`- Elevation range: ${minElevation}m to ${maxElevation}m`);
console.log(`- Total climb: ${gainM.toFixed(0)}m, total descent: ${lossM.toFixed(0)}m`);API Reference
ElevationProvider
The main class for elevation data retrieval.
Constructor
new ElevationProvider(config?: ElevationProviderConfig)Parameters:
config(optional): Configuration options
Configuration Options:
| Option | Type | Default | Description |
| ----------------- | -------- | ---------- | -------------------------------------------------- |
| zoomLevel | number | 12 | Tile zoom level (0-15). Higher = better resolution |
| cacheSize | number | 100 | Maximum tiles in memory cache |
| tileUrlTemplate | string | AWS S3 URL | Custom tile URL template |
Methods
getElevation(latitude: number, longitude: number, options?: GetElevationOptions): Promise<number>
Get elevation at specific coordinates.
// With default interpolation (enabled)
const elevation = await elevationProvider.getElevation(47.2, -1.5);
// Without interpolation
const rawElevation = await elevationProvider.getElevation(47.2, -1.5, { interpolation: false });setElevations(coordinates: Iterable<Coordinates>, options?: SetElevationsOptions): Promise<void>
Batch get elevations for multiple coordinates from an iterable.
await elevationProvider.setElevations([
{ latitude: 47.2, longitude: -1.5 },
{ latitude: 48.8, longitude: 2.3 },
]);
// Without interpolation
await elevationProvider.setElevations(coordinates, {
interpolation: false,
});getElevationsAlong(path: Coordinates[], options?: GetElevationsAlongOptions): Promise<CoordinatesElevation[]>
Get elevation profile along a multi-point path with optional smoothing and filtering.
const path = [
{ latitude: 47.2, longitude: -1.5 },
{ latitude: 47.25, longitude: -1.45 },
{ latitude: 47.3, longitude: -1.4 },
];
// Basic elevation profile
const profile = await elevationProvider.getElevationsAlong(path, { step: 10 });
// With smoothing and filtering
const smoothedProfile = await elevationProvider.getElevationsAlong(path, {
step: 10,
smoothingOptions: { enabled: true, windowSize: 50 },
filterOptions: { enabled: true, tolerance: 5, zExaggeration: 3 },
});getConfig(): ElevationProviderConfig
Get current configuration.
const config = elevationProvider.getConfig();Static Methods
getAttribution(): Attribution
Get required attribution information for elevation data.
const attribution = ElevationProvider.getAttribution();
console.log(attribution.text);
// "Elevation data from multiple sources including SRTM, GMTED, NED and ETOPO1.
// Data processing by Mapzen/Tilezen."ElevationSmoother
Distance-based smoothing with a triangular kernel. windowSize is a half-width in meters: a
window of 50 averages 50 m on each side, the extreme members carrying a weight of ~0.
ElevationSmoother.smooth(points: CoordinatesElevation[], windowSize = 50): CoordinatesElevation[]
Returns new points with smoothed elevations; distances are computed with Haversine. Throws if
windowSize is not positive. This is what smoothingOptions applies in getElevationsAlong.
ElevationSmoother.smoothProfile(distances: ArrayLike<number>, elevations: ArrayLike<number>, windowSize: number): number[]
The same kernel on flat arrays, for callers that already hold a profile as cumulative distances
and elevations (plain arrays or typed arrays). A windowSize that is not strictly positive means
"do not smooth" and returns a copy instead of throwing.
import { ElevationSmoother } from '@glandais/elevation';
const smoothed = ElevationSmoother.smoothProfile([0, 10, 20, 30], [100, 104, 99, 103], 15);ElevationGain
Cumulative ascent and descent (D+ / D-) measured at a stated scale.
The plain sum of positive deltas counts every wiggle, so it grows as the sampling gets finer:
halve step and the "total climb" goes up. ElevationGain instead smooths a private copy of the
profile at a narrow scale, then runs a turning-point accumulator with a hysteresis dead band:
a climb is banked once, in full, when the profile has reversed by at least the threshold. The
result depends only on local extrema, so it is stable under resampling, and a finely sampled
smooth climb is counted in full (a per-delta if (dEle > threshold) filter would report 0).
Measure on the profile before any wide smoothingOptions or filterOptions: a wide kernel
averages away real terrain and reads systematically low.
ElevationGain.compute(points: CoordinatesElevation[], options?: ElevationGainOptions): ElevationGainResult
Distances are computed with Haversine.
ElevationGain.computeProfile(distances: ArrayLike<number>, elevations: ArrayLike<number>, options?: ElevationGainOptions): ElevationGainResult
The same measurement on cumulative distances and elevations.
import { ElevationGain } from '@glandais/elevation';
const profile = await elevationProvider.getElevationsAlong(path, { step: 10 });
const { gainM, lossM } = ElevationGain.compute(profile); // default preset: 'dem'
const strava = ElevationGain.compute(profile, { preset: 'gps' });
const custom = ElevationGain.compute(profile, { preset: 'dem', thresholdM: 5 });Presets (ELEVATION_GAIN_PRESETS) pair a dead band with a smoothing half-width, because
cumulative ascent is a property of a route and a measurement scale:
| Preset | Threshold | Smoothing | Use |
| ------------ | --------- | --------- | ------------------------------------------------------- |
| raw | 0 m | 0 m | Plain sum of deltas, the control |
| barometric | 2 m | 15 m | Strava's threshold for a barometric altimeter |
| dem | 3 m | 30 m | Default. DEM-derived elevation, as this library returns |
| gps | 10 m | 50 m | Strava's threshold for a GPS-only trace |
dem is the default because DEM error is spatially correlated rather than white noise: there is
little point-to-point jitter for a large band to remove, and the 10 m gps band reports 0 m on
gentle terrain with a few meters of genuine undulation.
Types
Core Interfaces
interface ElevationProviderConfig {
readonly zoomLevel?: number;
readonly cacheSize?: number;
readonly tileUrlTemplate?: string;
}
interface Coordinates {
readonly latitude: number;
readonly longitude: number;
}
interface CoordinatesElevation extends Coordinates {
readonly elevation: number;
}
interface Attribution {
readonly text: string;
readonly url?: string;
}Options Interfaces
interface GetElevationOptions {
readonly interpolation?: boolean; // Default: true
}
interface SetElevationsOptions {
readonly interpolation?: boolean; // Default: true
}
interface GetElevationsAlongOptions {
readonly step?: number; // Distance between points in meters (default: 10)
readonly interpolation?: boolean; // Default: true
readonly smoothingOptions?: SmoothingOptions;
readonly filterOptions?: FilterOptions;
}
interface SmoothingOptions {
readonly enabled?: boolean; // Default: false
readonly windowSize?: number; // Smoothing window in meters (default: 50)
}
interface FilterOptions {
readonly enabled?: boolean; // Default: false
readonly tolerance?: number; // Max distance from simplified line in meters (default: 10)
readonly zExaggeration?: number; // Elevation exaggeration factor (default: 3)
}
type ElevationGainPreset = 'raw' | 'barometric' | 'dem' | 'gps';
interface ElevationGainOptions {
readonly preset?: ElevationGainPreset; // Default: 'dem'
readonly thresholdM?: number; // Dead band in meters, 0 disables (default: preset's)
readonly smoothWindowM?: number; // Smoothing half-width in meters, 0 disables (default: preset's)
}
interface ElevationGainResult {
readonly gainM: number; // Cumulative ascent in meters, >= 0
readonly lossM: number; // Cumulative descent in meters, >= 0
readonly rawGainM: number; // Plain sum of positive deltas on the same smoothed profile
readonly rawLossM: number; // Plain sum of negative deltas on the same smoothed profile, >= 0
readonly thresholdM: number; // Dead band applied
readonly smoothWindowM: number; // Smoothing half-width applied
readonly legCount: number; // Climbs plus descents banked (diagnostic)
}Resolution and Accuracy
| Zoom Level | Resolution at Equator | Use Case | | ------------ | --------------------- | --------------------------- | | 0-4 | >10km | Continental overview | | 5-8 | 1-10km | Regional analysis | | 9-11 | 100m-1km | Local area analysis | | 12 (default) | ~30m | Detailed terrain analysis | | 13-15 | <30m | High-precision applications |
The library uses terrain data processed from multiple sources:
- SRTM (Shuttle Radar Topography Mission) - Global 30m resolution
- GMTED (Global Multi-resolution Terrain Elevation Data)
- NED (National Elevation Dataset) - US high-resolution data
- ETOPO1 - Global relief model for bathymetry
Platform Compatibility
Browser Support
- Chrome/Edge: Last 2 versions
- Firefox: Last 2 versions
- Safari: Last 2 versions
- Mobile browsers: iOS Safari 14+, Chrome Android
Node.js Support
- Node.js 18+ with optional dependencies:
sharp: For tile decoding (WebP and PNG)canvas: For image data handlingnode-fetch: For HTTP requests (Node.js < 18)abort-controller: For request cancellation (Node.js < 15)
Performance Considerations
Memory Usage
Each tile uses approximately 262KB of memory (256×256×4 bytes). With the default cache size of 100 tiles, expect ~25MB memory usage for the cache.
Network Requests
- Consider reducing
zoomLevelfor applications with lower precision requirements
Optimization Tips
// Good: Reuse elevationProvider instance
const elevationProvider = new ElevationProvider();
const elevation1 = await elevationProvider.getElevation(47.2, -1.5);
const elevation2 = await elevationProvider.getElevation(47.3, -1.6);
// Better: Batch requests
await elevationProvider.setElevations([
{ latitude: 47.2, longitude: -1.5 },
{ latitude: 47.3, longitude: -1.6 },
]);
// Best: Use elevation profiling for paths
const profile = await elevationProvider.getElevationsAlong(
[
{ latitude: 47.2, longitude: -1.5 },
{ latitude: 47.3, longitude: -1.6 },
],
{ step: 50 }
);Error Handling
The library provides detailed error messages for common issues:
try {
const elevation = await elevationProvider.getElevation(90, 0); // Invalid latitude
} catch (error) {
console.error(error.message); // "Failed to get elevation: Invalid latitude: 90. Must be between -85.0511 and 85.0511"
}Common errors:
- Invalid coordinates (latitude: -85.0511 to 85.0511, longitude: -180 to 180)
- Network timeouts or failures
- Invalid configuration parameters
Attribution Requirements
IMPORTANT: Attribution is required by the tile data provider. The default configuration uses mapterhorn.com tiles — see mapterhorn.com/attribution/ for their requirements.
Implementation
const provider = new ElevationProvider();
// Get attribution programmatically
const attribution = provider.getAttribution();
// Display in your application's footer, about page, or credits
document.getElementById('attribution').textContent = attribution.text;
console.log('More info:', attribution.url);Custom Attribution
When using a custom tile source, pass its attribution in the config:
const provider = new ElevationProvider({
tileUrlTemplate: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
tileSize: 256,
attribution: {
text: 'Elevation data from SRTM, GMTED, NED and ETOPO1. Data processing by Mapzen/Tilezen.',
url: 'https://github.com/tilezen/joerd',
},
});- See Joerd attribution requirements when using Tilezen/AWS tiles
- See Mapterhorn attribution requirements when using mapterhorn.com tiles
Development
Setup
# Clone repository
git clone https://github.com/glandais/elevation.git
cd elevation
# Install dependencies
npm install
# Run development server
npm run devLogging (Development Only)
The library includes a lightweight logging system that is completely removed in production builds through build-time dead code elimination. This ensures zero overhead in production while providing helpful debugging information during development.
How It Works
- Build-time constant: Uses
__DEV__constant that's replaced during build - Development:
__DEV__ = true- Logging is active - Production:
__DEV__ = false- All logging code is removed by the bundler - Zero dependencies: Uses native console methods
Usage
import { createLogger } from '@glandais/elevation';
// Create a logger with a namespace
const logger = createLogger('MyModule');
// Available log methods (only in development)
logger.debug('Detailed debug information', { data });
logger.info('General information');
logger.warn('Warning message');
logger.error('Error occurred', error);
// Performance timing
logger.time('operation');
// ... perform operation ...
logger.timeEnd('operation');
// Grouped logging
logger.group('Processing batch');
logger.info('Item 1');
logger.info('Item 2');
logger.groupEnd();Example Integration
import { createLogger } from '@glandais/elevation';
class MyElevationProcessor {
private logger = createLogger('Processor');
async process(coordinates: Coordinates): Promise<number> {
this.logger.debug('Processing coordinates', coordinates);
this.logger.time('elevation-fetch');
try {
const elevation = await this.getElevation(coordinates);
this.logger.info('Elevation retrieved', { elevation });
return elevation;
} catch (error) {
this.logger.error('Failed to process', error);
throw error;
} finally {
this.logger.timeEnd('elevation-fetch');
}
}
}Note: All logging statements are completely eliminated from production builds, so you can freely add detailed logging without worrying about bundle size or performance impact.
Development Workflow
The project includes an interactive demo that showcases library capabilities:
# Build and serve demo (one-time)
npm run dev # Builds library and starts server at http://localhost:3000
# For active development (run in separate terminals):
npm run dev:serve # Serves demo at http://localhost:3000
npm run dev:watch # Watches and rebuilds library on changesDevelopment Process:
- Run
npm run devto build and start the demo server - Open http://localhost:3000 to view the interactive demo
- In a separate terminal, run
npm run dev:watchfor auto-rebuilding - Edit TypeScript files in
src/- they'll automatically rebuild - Refresh browser to see changes in the demo
Standalone script demo (Node.js / Deno / Bun)
demo/mont-blanc.mjs queries the elevation of Mont Blanc from a server-side runtime. It imports
the published library straight from the network (@3.3.0 on jsDelivr) — nothing to build first —
and runs on the three runtimes:
cd demo
node mont-blanc.mjs
deno --allow-net --allow-ffi --allow-env --allow-read mont-blanc.mjs
bun mont-blanc.mjsEach runtime needs a different mechanism, all implemented in the script itself:
| Runtime | How the remote module is loaded |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node.js | An ESM loader hook (node:module register) fetches https:// URLs — network imports were removed in Node 22 — and resolves bare specifiers against the script's own node_modules. |
| Bun | A Bun.plugin handles the https namespace and routes bare specifiers to Bun.resolveSync. |
| Deno | Deno imports URLs natively but rejects bare specifiers inside a remote module, so the source is fetched and sharp/canvas are rewritten to npm: specifiers. |
Node.js and Bun require the native optional dependencies (sharp, canvas) to be installed
(npm install at the repository root); Deno downloads them from npm on its own.
Scripts
npm run check # Verify all (lint, typecheck, test, build)
npm run build # Build library
npm run dev # Build and serve demo
npm run dev:watch # Watch and rebuild library
npm run dev:serve # Serve demo only
npm run test # Run tests
npm run test:coverage # Run tests with coverage
npm run test:browser # Run browser integration tests
npm run lint # Lint code
npm run format # Format code
npm run typecheck # Type checkingTesting
The library includes comprehensive tests with >95% coverage:
# Run all tests
npm test
npm test:browser
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test:coverageContributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests
- Ensure tests pass:
npm test - Commit your changes:
git commit -m 'feat: add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
License
MIT License - see LICENSE file for details.
Changelog
See CHANGELOG.md for version history and updates.
TODO
[ ] Consider WebWorker support for large batch operations [ ] Add streaming/progressive loading for large paths
Data Attribution: Default tile data provided by Mapterhorn — see mapterhorn.com/attribution/ for requirements.
