Display a Hybrid Satellite Map with Terrain Elevation in Maptoolkit Maps JS
A hybrid satellite map in Maptoolkit Maps JS combines a raster tile source for satellite imagery with a raster-dem source for terrain elevation. Enabling the terrain option on the map then drapes the satellite imagery over the 3D terrain surface. Use this for outdoor navigation, route planning, or any application where both photographic context and elevation are needed at the same time.
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
zoom: 12,
center: [11.39085, 47.27574],
pitch: 70,
maxPitch: 85,
attributionControl: { compact: false }
});
map.setStyle(`https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`, {
transformStyle: (previousStyle, nextStyle) => {
nextStyle.sources = {
...nextStyle.sources,
// Sentinel-2 cloudless satellite imagery from EOX
satelliteSource: {
type: 'raster',
tiles: [
'https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2020_3857/default/g/{z}/{y}/{x}.jpg'
],
tileSize: 256
},
// Maptoolkit terrarium-encoded DEM for 3D terrain
terrainSource: {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
},
hillshadeSource: {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
}
};
nextStyle.terrain = {
source: 'terrainSource',
exaggeration: 1
};
// Add hillshade on top of satellite for terrain relief
nextStyle.layers.push({
id: 'hills',
type: 'hillshade',
source: 'hillshadeSource',
layout: { visibility: 'visible' },
paint: { 'hillshade-shadow-color': '#473B24' }
});
// Insert satellite layer below the first non-fill, non-background layer
// so that vector labels and roads render on top of the satellite imagery
const firstNonFillLayer = nextStyle.layers.find(
layer => layer.type !== 'fill' && layer.type !== 'background'
);
nextStyle.layers.splice(nextStyle.layers.indexOf(firstNonFillLayer), 0, {
id: 'satellite',
type: 'raster',
source: 'satelliteSource',
layout: { visibility: 'visible' },
paint: { 'raster-opacity': 1 }
});
return nextStyle;
}
});
map.addControl(new maptoolkit.NavigationControl({
visualizePitch: true,
showZoom: true,
showCompass: true
}), 'top-right');<!DOCTYPE html>
<html lang="en">
<head>
<title>Display a Hybrid Satellite Map with Terrain Elevation - Maptoolkit Maps JS</title>
<meta property="og:description" content="Combine raster satellite tiles with 3D terrain elevation." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
zoom: 12,
center: [11.39085, 47.27574],
pitch: 70,
maxPitch: 85,
attributionControl: { compact: false }
});
map.setStyle(`https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`, {
transformStyle: (previousStyle, nextStyle) => {
nextStyle.sources = {
...nextStyle.sources,
// Sentinel-2 cloudless satellite imagery from EOX
satelliteSource: {
type: 'raster',
tiles: [
'https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2020_3857/default/g/{z}/{y}/{x}.jpg'
],
tileSize: 256
},
// Maptoolkit terrarium-encoded DEM for 3D terrain
terrainSource: {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
},
hillshadeSource: {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
}
};
nextStyle.terrain = {
source: 'terrainSource',
exaggeration: 1
};
// Add hillshade on top of satellite for terrain relief
nextStyle.layers.push({
id: 'hills',
type: 'hillshade',
source: 'hillshadeSource',
layout: { visibility: 'visible' },
paint: { 'hillshade-shadow-color': '#473B24' }
});
// Insert satellite layer below the first non-fill, non-background layer
// so that vector labels and roads render on top of the satellite imagery
const firstNonFillLayer = nextStyle.layers.find(
layer => layer.type !== 'fill' && layer.type !== 'background'
);
nextStyle.layers.splice(nextStyle.layers.indexOf(firstNonFillLayer), 0, {
id: 'satellite',
type: 'raster',
source: 'satelliteSource',
layout: { visibility: 'visible' },
paint: { 'raster-opacity': 1 }
});
return nextStyle;
}
});
map.addControl(new maptoolkit.NavigationControl({
visualizePitch: true,
showZoom: true,
showCompass: true
}), 'top-right');
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
This example layers three things: satellite imagery as a raster source, elevation as a
raster-dem source, and the vector style’s labels on top.
transformStyle is the part worth copying. Rather than building a style by hand, it takes
the style being loaded and returns a modified copy, so you inherit every label, road and
symbol from the standard style and only add what you need. The alternative, waiting for
load and then calling addSource, works but races with the style load.
The imagery is Sentinel-2 cloudless from EOX, which is a third-party source rather than a Maptoolkit one. It carries its own attribution requirements, so check them before shipping this.
pitch: 70 and maxPitch: 85 are what make the elevation visible. Satellite imagery draped
over terrain looks almost identical to flat imagery when viewed from directly above.
Next steps
The natural next step is letting the user choose. Satellite is better for terrain and worse
for navigation, so most applications that offer it also offer a normal map and a switch
between them, which setStyle with the same transformStyle already supports.
Imagery also has a date, unlike the vector basemap, which is rebuilt weekly. Sentinel-2 mosaics are composites from a particular year, so anything that changed since then is missing, and saying which year the imagery is from is worth the one line it costs.