Skip to content

Instantly share code, notes, and snippets.

@markusand
Last active March 9, 2025 10:38
Show Gist options
  • Select an option

  • Save markusand/0d400ddc46d159882fd5a1d2a40a0e76 to your computer and use it in GitHub Desktop.

Select an option

Save markusand/0d400ddc46d159882fd5a1d2a40a0e76 to your computer and use it in GitHub Desktop.
Terrain control for mapbox-gl-js
import type { Map, RasterDemTileSource, FogSpecification, SkyLayerSpecification } from 'mapbox-gl';
export type ExtrusionOptions = {
exaggeration?: number;
sky?: Partial<SkyLayerSpecification['paint']>;
fog?: Partial<FogSpecification>;
};
const DEM_LAYER = 'mapboxgl-terrain-dem';
const SKY_LAYER = 'mapboxgl-terrain-sky';
const DEFAULTS = {
sky: {
'sky-opacity': ['interpolate', ['linear'], ['zoom'], 0, 0, 8, 1],
'sky-type': 'atmosphere',
'sky-atmosphere-sun-intensity': 5,
} satisfies SkyLayerSpecification['paint'],
fog: {
range: [0, 10],
color: '#fff',
'high-color': '#add8e6',
'space-color': '#d8f2ff',
'horizon-blend': 0.3,
'star-intensity': 0.0,
} satisfies FogSpecification,
};
export default (map: Map) => {
const isExtruded = () => map.getTerrain()?.source === DEM_LAYER;
const extrude = (options: ExtrusionOptions) => {
const { exaggeration = 1, sky, fog } = options;
map.setFog({ ...DEFAULTS.fog, ...fog });
if (!map.getSource(DEM_LAYER)) {
const terrain = {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
} satisfies Partial<RasterDemTileSource>;
map.addSource(DEM_LAYER, terrain);
}
if (sky && !map.getLayer(SKY_LAYER)) {
const paint = { ...DEFAULTS.sky, ...sky };
map.addLayer({ id: SKY_LAYER, type: 'sky', paint });
}
map.setTerrain({ source: DEM_LAYER, exaggeration });
};
const flatten = () => {
map.setFog(null).setTerrain();
if (map.getLayer(SKY_LAYER)) map.removeLayer(SKY_LAYER);
if (map.getSource(DEM_LAYER)) map.removeSource(DEM_LAYER);
};
return { isExtruded, extrude, flatten };
};
import type { Map, IControl } from 'mapbox-gl';
import useTerrain, { type ExtrusionOptions } from './terrain';
import type { Prettify } from '/@/types/utilities';
export type TerrainControlOptions = Prettify<{
init?: boolean,
pitch?: number;
} & ExtrusionOptions>;
export default class TerrainControl implements IControl {
_options: TerrainControlOptions;
_container: HTMLElement;
constructor(options: TerrainControlOptions = {}) {
this._options = options;
this._container = document.createElement('div');
this._container.classList.add('mapboxgl-ctrl', 'mapboxgl-ctrl-group');
}
onAdd(map: Map) {
const { init = false, pitch, ...options } = this._options;
let lastPitch: number = 0;
const terrain = useTerrain(map);
const button = Object.assign(document.createElement('button'), {
className: 'mapboxgl-ctrl-terrain',
style: 'color:inherit;font-weight:bold;',
innerHTML: '2D',
});
this._container.appendChild(button);
const extrude = () => {
button.innerHTML = '3D';
lastPitch = map.getPitch();
if (pitch) map.easeTo({ pitch });
return terrain.extrude(options);
};
const flatten = () => {
button.innerHTML = '2D';
map.easeTo({ pitch: lastPitch });
terrain.flatten();
};
const toggle = () => {
if (terrain.isExtruded()) flatten();
else extrude();
};
button.addEventListener('click', toggle);
if (init) extrude();
return this._container;
}
onRemove() {
this._container.parentNode?.removeChild(this._container);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment