@@ -2,6 +2,7 @@ import {ElevationCoverageManager} from '../source/elevation_coverage_manager';
22import SourceCache from '../../src/source/source_cache' ;
33import { RenderSourceType } from '../../src/source/render_source_type' ;
44import { getNameFromFQID , getOuterScopeFromFQID , makeFQID } from '../../src/util/fqid' ;
5+ import { terrainEnabled } from '../../src/style/terrain' ;
56
67import type Tile from '../../src/source/tile' ;
78import type { TypedStyleLayer } from '../../src/style/style_layer/typed_style_layer' ;
@@ -44,7 +45,6 @@ function layerUsesHdRoadSourceLayer(layer: TypedStyleLayer): boolean {
4445}
4546
4647/// True when every provider-only source has finished loading.
47- /// Dual-role sources are excluded to avoid circular deferral.
4848function areElevationProvidersReady ( style : Style , providerFQIDs : Set < string > ) : boolean {
4949 for ( const fqid of providerFQIDs ) {
5050 const sc = resolveIngestSourceCache ( style , fqid ) as unknown as SourceCacheTiles | undefined ;
@@ -109,7 +109,7 @@ export function collectElevationProviderSourceFQIDs(style: Style): Set<string> {
109109 return providers ;
110110}
111111
112- /// True when a consumer resolves elevation from a different source.
112+ /// True when a consumer gets elevation from a different source.
113113export function needsCrossSourceElevation ( style : Style ) : boolean {
114114 const consumers = collectElevationConsumerSourceFQIDs ( style ) ;
115115 if ( consumers . size === 0 ) return false ;
@@ -125,26 +125,19 @@ export function needsCrossSourceElevation(style: Style): boolean {
125125/// Recompute the cached cross-source gate after a source change.
126126export function updateCrossSourceElevationGate ( style : Style ) : void {
127127 style . _crossSourceElevationActive = needsCrossSourceElevation ( style ) ;
128- if ( Object . keys ( style . _mergedHdRoadElevationSourceCaches ) . length === 0 ) return ;
128+ // HdElevationState only needed once dedicated elevation caches exist.
129+ const hdCaches = style . _mergedHdRoadElevationSourceCaches ;
130+ if ( ! hdCaches || Object . keys ( hdCaches ) . length === 0 ) return ;
129131 if ( ! style . _hdElevation ) style . _hdElevation = new HdElevationState ( ) ;
130132 style . _hdElevation . _needsCrossSourceElevation = style . _crossSourceElevationActive ;
131133}
132134
133- /// Reparse consumer tiles after terrain on/off (flat ↔ elevated geometry).
134- export function handleTerrainToggle ( style : Style , hadTerrain : boolean ) : void {
135- if ( hadTerrain === ! ! style . terrain ) return ;
136- reparseElevationConsumerTiles ( style ) ;
137- }
138-
139135/// Cached cross-source gate; refreshed on source change and each frame.
140136export function crossSourceElevationEnabledForStyle ( style : Style ) : boolean {
141137 return style . _crossSourceElevationActive === true ;
142138}
143139
144- /// Sources whose tiles feed the elevation snapshot (providers and dual-role).
145- /// This is exactly every source with an hd_road_* source-layer: provider-only
146- /// and dual-role sources together make up the full set, so no consumer/provider
147- /// split is needed.
140+ /// Sources whose tiles are scanned to build the cross-source snapshot.
148141export function collectElevationIngestSourceFQIDs ( style : Style ) : Set < string > {
149142 const ingest = new Set < string > ( ) ;
150143 for ( const layerId in style . _mergedLayers ) {
@@ -166,16 +159,19 @@ export function markElevationIngestSourceCachesUsed(style: Style): void {
166159 }
167160}
168161
169- /// Reload consumer tiles (optionally filtered). Skips non-consumers like raster-dem .
162+ /// Reload hd-road-markup consumer tiles .
170163export function reparseElevationConsumerTiles (
171164 style : Style ,
172165 shouldReload ?: ( tile : Tile ) => boolean ,
166+ includeSameSourceConsumers ?: boolean ,
173167) : void {
174- // Skip dual-role ingest sources — reparsing them causes a reload loop.
175168 const consumerFQIDs = collectElevationConsumerSourceFQIDs ( style ) ;
176169 if ( consumerFQIDs . size === 0 ) return ;
177- const ingestFQIDs = collectElevationIngestSourceFQIDs ( style ) ;
178- for ( const fqid of ingestFQIDs ) consumerFQIDs . delete ( fqid ) ;
170+ if ( ! includeSameSourceConsumers ) {
171+ // Same-source: elevation is parsed from the consumer tile itself.
172+ const ingestFQIDs = collectElevationIngestSourceFQIDs ( style ) ;
173+ for ( const fqid of ingestFQIDs ) consumerFQIDs . delete ( fqid ) ;
174+ }
179175 if ( consumerFQIDs . size === 0 ) return ;
180176
181177 const caches = [ style . _mergedOtherSourceCaches , style . _mergedSymbolSourceCaches ] ;
@@ -200,12 +196,14 @@ export class HdElevationState {
200196 manager : ElevationCoverageManager ;
201197 _needsCrossSourceElevation : boolean ;
202198 _ingestFQIDs : Set < string > ;
199+ _terrainActiveLast : boolean | undefined ;
203200
204201 constructor ( ) {
205202 this . elevationSourceCaches = { } ;
206203 this . manager = new ElevationCoverageManager ( ) ;
207204 this . _needsCrossSourceElevation = false ;
208205 this . _ingestFQIDs = new Set ( ) ;
206+ this . _terrainActiveLast = undefined ;
209207 }
210208}
211209
@@ -237,18 +235,27 @@ function deactivateCrossSourceElevation(style: Style): void {
237235 style . _hdElevation . _needsCrossSourceElevation = false ;
238236 style . _hdElevation . _ingestFQIDs . clear ( ) ;
239237 style . _hdElevation . manager . clear ( ) ;
238+ style . _hdElevation . _terrainActiveLast = undefined ;
240239}
241240
242241/// Setup provider caches, merge fragments, ingest tiles, and reparse consumers.
243242export function setupAndUpdateElevationCoverage ( style : Style ) : void {
244- // Under terrain: skip. Use style.terrain, not painter.terrain (lags a frame). Keep gate for toggle.
245- if ( style . terrain ) {
246- if ( style . map . painter ) style . map . painter . elevationCoverageSnapshot = null ;
243+ if ( ! hasElevationConsumers ( style ) ) {
244+ deactivateCrossSourceElevation ( style ) ;
247245 return ;
248246 }
249247
250- if ( ! hasElevationConsumers ( style ) ) {
251- deactivateCrossSourceElevation ( style ) ;
248+ const terrainActive = terrainEnabled ( style , style . map && style . map . transform ) ;
249+ if ( ! style . _hdElevation ) style . _hdElevation = new HdElevationState ( ) ;
250+ const prevTerrainActive = style . _hdElevation . _terrainActiveLast ;
251+ style . _hdElevation . _terrainActiveLast = terrainActive ;
252+ const flipped = prevTerrainActive !== undefined && prevTerrainActive !== terrainActive ;
253+ if ( flipped ) {
254+ reparseElevationConsumerTiles ( style , undefined , true ) ;
255+ }
256+
257+ if ( terrainActive ) {
258+ if ( style . map . painter ) style . map . painter . elevationCoverageSnapshot = null ;
252259 return ;
253260 }
254261
@@ -258,9 +265,12 @@ export function setupAndUpdateElevationCoverage(style: Style): void {
258265 return ;
259266 }
260267
261- if ( ! style . _hdElevation ) {
262- style . _hdElevation = new HdElevationState ( ) ;
268+ // First frame with cross-source active and terrain inactive: reparse consumers so they
269+ // pick up elevation immediately rather than waiting for a snapshot change.
270+ if ( prevTerrainActive === undefined ) {
271+ reparseElevationConsumerTiles ( style , undefined , true ) ;
263272 }
273+
264274 const state = style . _hdElevation ;
265275 state . _needsCrossSourceElevation = true ;
266276 style . _crossSourceElevationActive = true ;
@@ -308,8 +318,8 @@ export function updateHdElevationSourceCache(style: Style, state: HdElevationSta
308318
309319/// Ingest provider tiles and reparse consumers whose covering changed.
310320export function updateElevationCoverage ( style : Style , state : HdElevationState ) {
311- // Under terrain: clear snapshot ( lines drape flat) .
312- if ( style . terrain ) {
321+ // Active terrain: markup lines drape flat, no snapshot .
322+ if ( terrainEnabled ( style , style . map && style . map . transform ) ) {
313323 state . manager . clear ( ) ;
314324 if ( style . map . painter ) style . map . painter . elevationCoverageSnapshot = null ;
315325 return ;
0 commit comments