import type mapboxgl from 'mapbox-gl' // "mapbox/standard" and "mapbox/standard-satellite" ship their own 3D // buildings and terrain. For every other style we inject a fill-extrusion // layer against the classic `composite` vector source so the user still // gets real 3D buildings (not just a tilted 2D view) when they toggle 3D. export function isStandardFamily(style: string): boolean { return style === 'mapbox://styles/mapbox/standard' || style === 'mapbox://styles/mapbox/standard-satellite' } // Terrain is only genuinely useful for the satellite imagery styles — on // clean flat styles like streets/light/dark it nudges route lines onto // the DEM while our HTML markers stay at Z=0, which causes the visible // offset when the map is pitched. Restrict terrain to satellite. export function wantsTerrain(style: string): boolean { return style === 'mapbox://styles/mapbox/satellite-v9' || style === 'mapbox://styles/mapbox/satellite-streets-v12' } // 3D can be added to every style now — the standard family has it built-in // and for everything else we either reuse the style's own `composite` // building layer or attach the public `mapbox-streets-v8` tileset as an // extra source (needed for pure satellite, which has no vector data). export function supportsCustom3d(style: string): boolean { return !isStandardFamily(style) } // Add a 3D buildings extrusion layer to a non-Standard Mapbox style. For // the pure satellite style we lazily attach `mapbox-streets-v8` as a // fallback source so real building volumes sit on top of the imagery — // the Apple Maps-style "3D satellite" look the user asked for. export function addCustom3dBuildings(map: mapboxgl.Map, dark: boolean) { if (map.getLayer('trek-3d-buildings')) return const baseColor = dark ? '#3b3b3f' : '#cfd2d6' // Styles without a `composite` source (pure satellite) need a fallback // vector tileset for building geometry. let sourceId = 'composite' if (!map.getSource('composite')) { sourceId = 'mapbox-streets-v8' if (!map.getSource(sourceId)) { try { map.addSource(sourceId, { type: 'vector', url: 'mapbox://mapbox.mapbox-streets-v8' }) } catch { return } } } try { // Place extrusions below the first label layer so text stays readable. const layers = map.getStyle()?.layers || [] const firstSymbolId = layers.find(l => l.type === 'symbol')?.id map.addLayer({ id: 'trek-3d-buildings', source: sourceId, 'source-layer': 'building', filter: ['==', 'extrude', 'true'], type: 'fill-extrusion', minzoom: 14, paint: { 'fill-extrusion-color': baseColor, 'fill-extrusion-height': [ 'interpolate', ['linear'], ['zoom'], 14, 0, 15.5, ['coalesce', ['get', 'height'], 0], ], 'fill-extrusion-base': [ 'interpolate', ['linear'], ['zoom'], 14, 0, 15.5, ['coalesce', ['get', 'min_height'], 0], ], 'fill-extrusion-opacity': 0.85, }, }, firstSymbolId) } catch { /* building source-layer unavailable */ } } // Terrain + sky that works against any style that has the DEM source. // The Standard family already handles terrain internally, skip there. export function addTerrainAndSky(map: mapboxgl.Map) { try { if (!map.getSource('mapbox-dem')) { map.addSource('mapbox-dem', { type: 'raster-dem', url: 'mapbox://mapbox.mapbox-terrain-dem-v1', tileSize: 512, maxzoom: 14, }) } map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.2 }) if (!map.getLayer('sky')) { map.addLayer({ id: 'sky', type: 'sky', paint: { 'sky-type': 'atmosphere', 'sky-atmosphere-sun-intensity': 15, } as unknown as mapboxgl.SkyLayerSpecification['paint'], }) } } catch { /* style doesn't support terrain */ } }