Skip to content

Satellite Map with Terrain Elevation

Combine satellite imagery with 3D terrain elevation to get a realistic aerial view of the landscape. Mountains rise up from the satellite photo, making it feel like you're looking at a real landscape from above.

No three.js or external libraries needed. Satellite + terrain is built into MapMetrics GL.

How It Works

The setup is simple — just swap the base map tiles from OpenStreetMap to a satellite imagery provider:

javascript
const map = new mapmetricsgl.Map({
  container: 'map',
  pitch: 60,
  maxPitch: 85,
  style: {
    version: 8,
    sources: {
      // Satellite imagery (free from ESRI)
      satellite: {
        type: 'raster',
        tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
        tileSize: 256,
        attribution: 'Tiles © Esri',
        maxzoom: 19,
      },
      // Elevation data (free AWS terrain tiles — no API key needed)
      terrainSource: {
        type: 'raster-dem',
        tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
        tileSize: 256,
        encoding: 'terrarium',
        maxzoom: 15,
      },
    },
    layers: [
      { id: 'satellite', type: 'raster', source: 'satellite' },
    ],
    // Enable 3D terrain
    terrain: {
      source: 'terrainSource',
      exaggeration: 1.5,
    },
  },
});

Free Satellite Tile Sources

ProviderURL patternNotes
ESRI World Imagery...World_Imagery/MapServer/tile/{z}/{y}/{x}Free, global coverage
OpenStreetMaphttps://a.tile.openstreetmap.org/{z}/{x}/{y}.pngStandard road map

Note: ESRI tiles use {z}/{y}/{x} (y before x), not the standard {z}/{x}/{y}.

Add Hillshade Over Satellite

Adding a semi-transparent hillshade on top of satellite tiles gives extra depth:

javascript
{
  id: 'hillshade',
  type: 'hillshade',
  source: 'terrainSource',
  paint: {
    'hillshade-shadow-color': '#000000',
    'hillshade-exaggeration': 0.3, // subtle — don't overpower the satellite image
    'hillshade-illumination-anchor': 'viewport',
  },
}

Toggle 3D vs Flat

javascript
// Switch to 3D view
map.easeTo({ pitch: 60, duration: 800 });

// Switch to flat (top-down) view
map.easeTo({ pitch: 0, duration: 800 });

Complete Example

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
    <script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
    <style>body { margin: 0; } #map { height: 100vh; width: 100%; }</style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      const map = new mapmetricsgl.Map({
        container: 'map',
        zoom: 11,
        center: [11.39085, 47.27574],
        pitch: 60,
        maxPitch: 85,
        style: {
          version: 8,
          sources: {
            satellite: {
              type: 'raster',
              tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
              tileSize: 256,
              attribution: 'Tiles &copy; Esri',
              maxzoom: 19,
            },
            terrainSource: {
              type: 'raster-dem',
              tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
              tileSize: 256,
              encoding: 'terrarium',
              maxzoom: 15,
            },
          },
          layers: [{ id: 'satellite', type: 'raster', source: 'satellite' }],
          terrain: { source: 'terrainSource', exaggeration: 1.5 },
          sky: {},
        },
      });

      map.addControl(new mapmetricsgl.NavigationControl({ visualizePitch: true }), 'top-right');
      map.addControl(new mapmetricsgl.TerrainControl({ source: 'terrainSource', exaggeration: 1.5 }), 'top-right');
    </script>
  </body>
</html>
jsx
import React, { useEffect, useRef } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const SatelliteTerrain = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return;
    map.current = new mapmetricsgl.Map({
      container: mapContainer.current,
      zoom: 11,
      center: [11.39085, 47.27574],
      pitch: 60,
      maxPitch: 85,
      style: {
        version: 8,
        sources: {
          satellite: {
            type: 'raster',
            tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
            tileSize: 256,
            attribution: 'Tiles © Esri',
            maxzoom: 19,
          },
          terrainSource: {
            type: 'raster-dem',
            tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
            tileSize: 256,
            encoding: 'terrarium',
            maxzoom: 15,
          },
        },
        layers: [{ id: 'satellite', type: 'raster', source: 'satellite' }],
        terrain: { source: 'terrainSource', exaggeration: 1.5 },
        sky: {},
      },
    });

    map.current.addControl(new mapmetricsgl.NavigationControl({ visualizePitch: true }), 'top-right');
    map.current.addControl(new mapmetricsgl.TerrainControl({ source: 'terrainSource', exaggeration: 1.5 }), 'top-right');

    return () => { map.current?.remove(); map.current = null; };
  }, []);

  return <div ref={mapContainer} style={{ height: '100vh', width: '100%' }} />;
};

export default SatelliteTerrain;

For more information, visit the MapMetrics GitHub repository.