diff --git a/inst/htmlwidgets/screenshot.js b/inst/htmlwidgets/screenshot.js index 2194dd5..bf046f3 100644 --- a/inst/htmlwidgets/screenshot.js +++ b/inst/htmlwidgets/screenshot.js @@ -26,6 +26,41 @@ function waitForMapEvent(map, eventName, timeout = 5000, trigger = null) { }); } +function waitForInitialStyleSetup(map, timeout = 10000) { + return new Promise(resolve => { + if (!map || map._initialStyleLoaded === true) { + resolve('ready'); + return; + } + + let settled = false; + + function finish(status) { + if (settled) return; + settled = true; + clearTimeout(timer); + clearInterval(poller); + map.off('styledata', checkReady); + map.off('idle', checkReady); + map.off('render', checkReady); + resolve(status); + } + + function checkReady() { + if (map._initialStyleLoaded === true) { + finish('ready'); + } + } + + const poller = setInterval(checkReady, 50); + const timer = setTimeout(() => finish('timeout'), timeout); + + map.on('styledata', checkReady); + map.on('idle', checkReady); + map.on('render', checkReady); + }); +} + function applyMapScreenshotOptions(map, options) { const container = map.getContainer(); const hiddenElements = []; @@ -119,11 +154,18 @@ function restoreMapScreenshotOptions(map, state) { } async function prepareMapForScreenshot(map, options) { - const state = applyMapScreenshotOptions(map, options); - - // Attribution is always included to comply with map provider TOS + let state; try { + // Wait until the widget's initial style setup is complete. + // This is important when PMTiles sources are present because source + // metadata checks are async and controls may be added later. + await waitForInitialStyleSetup(map, 10000); + + state = applyMapScreenshotOptions(map, options); + + // Attribution is always included to comply with map provider TOS + // Wait briefly for the map to settle, but don't block indefinitely. // Some Linux/headless browser combinations never report a fully idle map // for remote basemap styles even though the canvas is renderable. @@ -141,7 +183,9 @@ async function prepareMapForScreenshot(map, options) { return state; } catch (error) { - restoreMapScreenshotOptions(map, state); + if (state) { + restoreMapScreenshotOptions(map, state); + } throw error; } }