Terrain data becomes hard to use well once it leaves a desktop GIS workflow. Operational systems usually need fast elevation lookups, line-of-sight checks, terrain clearance, slope-aware routing, coverage masks, and repeatable answers from the same source data.
Those questions often end up scattered across application code, batch scripts, notebooks, and one-off preprocessing jobs.
That is an infrastructure problem, not just a map-rendering problem.
terrain-server is a Go library and HTTP service for terrain-derived answers
from Copernicus DEM elevation grids. It provides a terrain sampler, tile index,
bilinear interpolation, line-of-sight checks, terrain profiles, elevation grids,
surface analysis, viewsheds, terrain masks, cache controls, OpenAPI metadata,
and an HTTP API that other systems can call without knowing the internal
GeoTIFF layout.
The default local run reads a compact JSON tile set so the core behavior is easy to test, review, and embed. The production backend is intentionally scoped to Copernicus DEM Cloud Optimized GeoTIFFs with a prebuilt terrain index, S3 range reads, and decoded internal-tile caching.
It is designed for teams that need a small terrain service for geospatial analysis, RF planning, sensor placement, route screening, simulation, or mission tooling before investing in a larger geospatial platform.
Run the local fixture-backed server:
make runOpen:
http://127.0.0.1:8080/healthz
Query an elevation:
curl "http://127.0.0.1:8080/v1/elevation?lat=60.005&lon=24.005"Check line of sight:
curl "http://127.0.0.1:8080/v1/los?from_lat=60.000&from_lon=24.000&from_alt_m=65&to_lat=60.010&to_lon=24.010&to_alt_m=70"Run the example clients:
examples/elevation_batch.sh
examples/analysis_queries.sh
examples/cache_ops.shThe example scripts default to http://127.0.0.1:8080.
Use a different local address:
ADDR=127.0.0.1:5082 make runThen run a client against that address:
BASE_URL=http://127.0.0.1:5082 examples/analysis_queries.shRun the Go test suite:
make testBuild the server and CLI binaries:
make buildBuild and smoke-test the container image:
make container-testRun against the Copernicus object-store backend:
make run-s3That command uses config/s3-terrain.json and expects credentials in
config/credentials.env. The credentials file is ignored by git.
terrain-server handles common terrain analysis workflows:
- Loads fixture terrain from one JSON tile, a list of tiles, or a tile set.
- Validates grid dimensions, sample counts, pixel sizes, and sample values.
- Builds an in-memory tile index for point lookup.
- Samples elevation with bilinear interpolation.
- Reports whether a point is above ground.
- Computes clearance above ground for an absolute-altitude position.
- Checks line of sight between two absolute-altitude positions.
- Reports the first blocking terrain point and minimum clearance along a path.
- Samples elevation grids for map and analysis clients.
- Samples terrain profiles along point-to-point paths.
- Computes slope, aspect, and surface normal at a point.
- Samples elevations in batch for route and planning workflows.
- Computes simple viewsheds from an observer point.
- Generates terrain masks for altitude-layered grids.
- Reads cloud-hosted Copernicus DEM GeoTIFF terrain through indexed range requests.
- Decodes zlib-compressed internal terrain blocks with float32 predictor data.
- Applies effective-Earth-curvature and optional Fresnel-zone clearance in line-of-sight checks.
- Exposes binary grid and mask outputs for compact clients.
- Exposes GeoJSON profile and viewshed outputs for mapping clients.
- Exposes health, readiness, metrics, cache stats, cache clear, and cache prewarm endpoints.
- Emits structured JSON request logs.
- Returns stable error envelopes with machine-readable error codes.
- Applies configurable request, timeout, sample, mask, and prewarm limits.
- Provides CLI tooling for index inspection, coverage checks, live sampling, and quick backend benchmarks.
The HTTP server stays behind a small Go interface. Backends can change from fixture data to object-store terrain without changing endpoint behavior.
Most systems do not need a full GIS server for every terrain query. They need a small service that can answer a few operational questions consistently:
- What is the elevation at this latitude and longitude?
- Does this sensor have line of sight to that target?
- How much terrain clearance does this route or link have?
- What does the elevation profile along this route look like?
- What areas are visible from this observer height?
- Which altitude-layered grid cells are below terrain?
- Which terrain tile covers this point?
- Can this application use the same terrain logic in tests and production?
terrain-server treats those questions as service behavior. The public API
deals in latitude, longitude, meters, JSON, GeoJSON, and compact binary
responses. The Go package owns tile validation, index lookup, interpolation,
path sampling, object-store range reads, and terrain-specific analysis.
The starter JSON data format is intentionally plain. It keeps the repository easy to audit and gives tests deterministic fixtures. The object-store backend is Copernicus-specific: it uses the terrain index, one range request per internal tile, and an in-memory LRU cache for decoded blocks.
Public background:
Application or analysis job
|
| HTTP JSON API or Go interface
v
terrain-server
|
+-- tile index
+-- terrain sampler
+-- interpolation
+-- line-of-sight checker
+-- analysis endpoints
|
v
elevation grid tiles
For the default local run:
terrain-server process
example JSON tile
in-memory index
HTTP API on 127.0.0.1:8080
For object-store terrain:
terrain-server process or container
committed terrain index
ignored S3 credentials file
range reads into Copernicus DEM COG objects
decoded internal-tile LRU cache
HTTP API on 127.0.0.1:8080
Sample one point or a batch of points in decimal degrees. Batch responses can include terrain clearance when the caller provides absolute altitude.
Check whether terrain blocks a path between two absolute-altitude points. The LOS checker can include required clearance, effective-Earth curvature, and first-Fresnel-zone clearance for RF planning workflows.
Sample a point-to-point path and return terrain elevation, optional ray altitude, and clearance values along the path. Profiles can be returned as JSON or GeoJSON.
Sample a rectangular area into a regular grid. Clients can request JSON or row-major little-endian float32 output for compact local tools.
Compute elevation, gradient, slope, aspect, and surface normal around a point using central differences.
Estimate visible cells around an observer point using repeated line-of-sight checks over a bounded sample grid.
Generate a solid/clear mask for an altitude-layered grid. This is useful for simulation, planning, and routing systems that need terrain occupancy rather than raw elevation samples.
The production backend is scoped to Copernicus DEM 30m Cloud Optimized GeoTIFFs. It uses:
data/terrain-index.bin: committed prebuilt terrain index.- S3-compatible object storage.
- Range requests for only the internal GeoTIFF tile needed by a query.
- A decoded internal-tile LRU cache.
- Singleflight fetch behavior for concurrent requests to the same terrain block.
The object-store config is:
config/s3-terrain.json
Credentials live outside git:
config/credentials.env
The credentials file uses the same names understood by common S3 tooling:
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
The container image includes the terrain index so the Copernicus backend can run without a separate index mount. Credentials are not included in the image.
Rebuild the index from the configured object-store bucket:
make build-indexTest the object-store backend:
make test-s3Those tests validate live sampling, readiness, cache prewarm and clear, and terrain analysis against the configured bucket.
The terrain-tool binary exposes small inspection and backend checks from the
terminal.
Inspect the committed terrain index:
make inspect-indexVerify basic index structure and a known covered tile:
make verify-indexCheck whether a point is covered by the index:
go run ./cmd/terrain-tool coverage -lat 60.005 -lon 24.005Sample the object-store backend directly:
make sampleRun a small backend benchmark:
make benchBuild a fresh index from the configured object store:
go run ./cmd/build-terrain-index -config config/s3-terrain.json -credentials config/credentials.envThe OpenAPI 3.0 specification is available at:
openapi.yaml
The server exposes terrain and operations routes:
GET /healthz
GET /readyz
GET /metrics
GET /v1/tiles
GET /v1/cache
POST /v1/cache/clear
POST /v1/cache/prewarm
GET /v1/elevation
POST /v1/elevation/batch
GET /v1/grid
GET /v1/profile
GET /v1/surface
GET /v1/los
GET /v1/viewshed
GET /v1/mask
Health only says the process can respond:
curl http://127.0.0.1:8080/healthzReadiness verifies that the configured terrain backend is usable. For the
Copernicus object-store backend, readiness verifies that the terrain index is
loaded and the configured bucket can be reached with the configured
credentials. If the S3 connection is not alive, readiness returns HTTP 503.
curl http://127.0.0.1:8080/readyzPrometheus-style metrics include HTTP traffic, request duration, object-store reads, bytes read, index size, cache entries, cache capacity, cache hits, cache misses, cache evictions, and estimated maximum cache memory.
curl http://127.0.0.1:8080/metricsElevation example:
curl "http://127.0.0.1:8080/v1/elevation?lat=60.005&lon=24.005"Grid example:
curl "http://127.0.0.1:8080/v1/grid?min_lat=60&max_lat=60.01&min_lon=24&max_lon=24.01&resolution=16"Profile example:
curl "http://127.0.0.1:8080/v1/profile?from_lat=60&from_lon=24&to_lat=60.01&to_lon=24.01&step_m=100"Line-of-sight example:
curl "http://127.0.0.1:8080/v1/los?from_lat=60.000&from_lon=24.000&from_alt_m=65&to_lat=60.010&to_lon=24.010&to_alt_m=70&clearance_m=5"The API is local-service oriented today. Production use would need deployment policy around authentication, authorization, TLS termination, audit logging, rate limits, and object-store credential handling.
The server reads a tracked JSON config file and an ignored credentials file.
The default local config is:
config/terrain-server.json
The object-store config is:
config/s3-terrain.json
Run with a different config file:
CONFIG=/path/to/terrain-server.json make runOperational limits are configured in the JSON config:
{
"limits": {
"max_request_body_bytes": 1048576,
"default_timeout_ms": 10000,
"expensive_timeout_ms": 30000,
"readiness_timeout_ms": 3000,
"max_timeout_ms": 120000,
"max_batch_points": 262144,
"max_grid_samples": 262144,
"max_viewshed_samples": 262144,
"max_mask_cells": 1048576,
"max_prewarm_tiles": 4096
}
}The server rejects invalid numeric parameters instead of silently falling back to defaults. Latitude and longitude are range-checked, request bodies are bounded, and expensive terrain operations use bounded request timeouts.
Error responses use a stable envelope:
{
"error": {
"code": "invalid_parameter",
"message": "query parameter is required: lat"
}
}Common codes include invalid_parameter, invalid_json, request_too_large,
not_found, terrain_out_of_bounds, terrain_no_data,
backend_unavailable, backend_error, and internal_error.
The original bucket-to-bucket setup used rclone with one remote for the
public Copernicus AWS Open Data bucket and one remote for the target
S3-compatible bucket.
The checked-in example config is:
config/rclone.conf.example
The real config path is ignored by git:
config/rclone.conf
The example includes fake target credentials and the same remote names used by the mirror script:
copernicus-aws:
hetzner:
Preview what rclone would do:
make mirror-copernicus-dry-runMirror into the target bucket:
make mirror-copernicusBy default the script uses rclone copy, which is safe for a bucket that may
already contain other objects.
To make the target an exact mirror:
MIRROR_MODE=sync make mirror-copernicusThat is equivalent to the original setup command:
rclone sync copernicus-aws:copernicus-dem-30m hetzner:copernicus-dem-30m --progress --transfers 8 --checkers 16Run a small HTTP service that gives application teams stable elevation, clearance, profile, grid, and mask answers without embedding GeoTIFF parsing in each application.
Use line-of-sight, curvature, and Fresnel-zone clearance checks to screen links before moving into deeper propagation modeling.
Use elevation grids, surface analysis, and viewsheds to compare observer locations and target visibility.
Use batch elevation and profile calls to find terrain clearance along planned routes.
Use masks and terrain sampling through the Go interface to feed planning, physics, or agent simulations with repeatable terrain behavior.
For a fixture-backed integration, the deployment needs:
- A JSON tile file or generated tile set.
- A config file that points at that fixture data.
- A caller that understands latitude, longitude, and meters.
For a Copernicus object-store integration, the deployment needs:
- A Copernicus DEM bucket mirrored into an S3-compatible object store.
- A terrain index matching that bucket layout.
- Object-store credentials supplied outside git.
- Cache sizing that matches expected query locality and available memory.
- Readiness checks wired into deployment health policy.
- A strategy for request limits, timeout limits, and expensive query usage.
The service should own terrain layout and decoding. Application code should only need to ask terrain questions in geospatial units.
terrain-server is not a full GIS platform, map tile renderer, raster
catalog, spatial database, routing engine, or general-purpose GeoTIFF service.
That is intentional.
It is a focused terrain query and analysis service. It shows how Copernicus DEM data can be turned into repeatable operational answers without making every application understand DEM storage details.
cmd/terrain-server/main.go: HTTP server entrypoint.cmd/terrain-tool/main.go: CLI for index inspection, coverage, sampling, and benchmark checks.cmd/build-terrain-index/main.go: terrain index builder for Copernicus object-store buckets.server/: HTTP API, request limits, readiness, metrics, logging, and error responses.settings/: config and credentials loading.terrain/source.go: fixture terrain source and sampler.terrain/object_source.go: Copernicus object-store terrain source.terrain/object_index.go: binary terrain index loading and lookup.terrain/object_cache.go: decoded internal-tile LRU cache.terrain/los.go: line-of-sight and clearance calculations.terrain/analysis.go: grid, profile, surface, viewshed, and mask analysis.config/: tracked local, object-store, credentials, and rclone examples.data/terrain-index.bin: committed Copernicus terrain index.examples/: curl clients and fixture terrain tile.scripts/mirror-copernicus-s3.sh: rclone bucket mirror helper.openapi.yaml: machine-readable API specification.Dockerfile: container image for fixture and object-store server runs.
Run the Go unit tests:
make testRun the object-store integration tests:
make test-s3Build the server and CLI binaries:
make buildBuild and smoke-test the default container:
make container-testThe smoke test exercises:
- Container image build.
- Server startup.
- Health and readiness endpoints.
- Metrics endpoint.
- Batch elevation example client.
- Terrain analysis example client.
- Cache control example client.
The Makefile uses docker when it is installed, otherwise podman.
Build the image:
make container-buildRun the fixture-backed server:
make container-upSmoke-test a running container:
make container-smokeRun the full build, start, smoke-test, and cleanup flow:
make container-testUse a different host port:
PORT=5082 make container-testRun it with Docker:
docker run --rm -p 8080:8080 terrain-server:devRun it with Podman:
podman run --rm -p 8080:8080 terrain-server:devOverride the runtime:
CONTAINER_TOOL=podman make container-testThe Makefile defaults are:
CONTAINER_TOOL=docker when available, otherwise podman
IMAGE=terrain-server:dev
CONTAINER_NAME=terrain-server
PORT=8080
BASE_URL=http://127.0.0.1:8080
Run the Copernicus object-store backend locally:
make run-s3Run the Copernicus object-store backend in a container:
make container-up-s3That target mounts config/credentials.env read-only into the container. The
tracked config and terrain index are already present in the image.
Stop the container:
make container-downThe object-store backend is ready only when the S3 connection is alive and
verified. A failed S3 readiness check returns HTTP 503 from /readyz.
terrain-server is expected to run on standard Go-supported Unix-like systems.
The local demo path is intended for:
- macOS with Docker or Podman.
- Linux with Docker or Podman.
- S3-compatible object storage that supports range reads.
- Copernicus DEM 30m Cloud Optimized GeoTIFF bucket layouts.
The service is written in Go and has no frontend build step.
This repository is intended for transparent examples, repeatable local tests, and terrain integration work.
Do not commit real object-store credentials, private bucket details, production endpoints, API tokens, deployment-specific policy, or sensitive mission data.
For production use, add authentication, authorization, TLS, audit logging, rate limits, object-store credential rotation, operational monitoring, backup and recovery procedures, and deployment-specific access controls.
See SECURITY.md for vulnerability reporting and credential handling guidance.
Apache License 2.0. See LICENSE.
terrain-server makes terrain queries concrete.
It gives developers and integrators one place to sample Copernicus DEM elevations, check terrain line of sight, generate profiles and masks, run object-store terrain from an indexed bucket, and expose those answers through a small HTTP API or Go interface.