[143] Expose share-link and map-screenshot as plugin API capabilities#167
Open
CarsonDavis wants to merge 27 commits into
Open
[143] Expose share-link and map-screenshot as plugin API capabilities#167CarsonDavis wants to merge 27 commits into
CarsonDavis wants to merge 27 commits into
Conversation
Extract the inline html2canvas screenshot logic from the BottomBar camera button into a reusable ScreenshotUtils.getMapScreenshot() that resolves to a PNG data URL, and expose it publicly as mmgisAPI.getMapScreenshot(). The onclone SVG/z-index fixups are preserved verbatim. The camera button shows its spinner, downloads the capture via an object URL (blob) to avoid base64 bloat on large captures, and hides the spinner on failure. Make mmgisAPI.writeCoordinateURL() the canonical share-link method by removing the frontend link-shortener dependency: drop the shortenURL branch/param from QueryURL.writeCoordinateURL so it returns the full URL synchronously with no backend call, update the BottomBar copy-link button accordingly, and remove the now-dead shortener_shorten frontend registry entries. The backend shortener endpoint/route/table and the shortener_expand path (for loading existing short links) are left untouched. Also fix a latent restore bug carried into the shared util (#mapToolBar bottom was reset to a string literal instead of its saved value) and add behavioral unit tests that drive getMapScreenshot() against injected jQuery/html2canvas fakes.
writeCoordinateURL() called UserInterface_.getPanelPercents(), which only exists on the classic/mobile UI controllers, throwing in the modern layout. getMapScreenshot() targeted #mapScreen, absent in the modern layout. Guard the panel-percents call (fall back to a map-only split) and fall back to the #map container so both capabilities work in the modern layout that plugins run in.
The public mmgisAPI.getMapScreenshot() returned a blank image on the modern deck.gl/GL map because it always went through html2canvas, which cannot rasterize a WebGL canvas. Make screenshot capture a responsibility of the active map engine. - IMapEngine: add captureScreenshot(): Promise<string> to the contract. - LeafletAdapter: implement it by delegating to the existing html2canvas helper (getMapScreenshot in ScreenshotUtils) — unchanged Leaflet logic. - DeckGLAdapter: set preserveDrawingBuffer: true at GL-map creation (both mapbox-gl and maplibre-gl paths) so the canvas can be read back, and implement captureScreenshot() that forces a repaint then reads the GL canvas via toDataURL. Overlay mode is interleaved, so the base map's single canvas already holds basemap + deck layers. - mmgisAPI.getMapScreenshot(): delegate to the active engine's captureScreenshot(), falling back to the html2canvas helper. - BottomBar camera button: route through mmgisAPI.getMapScreenshot() so it shares the engine-aware path. Tests: add deck.gl capture coverage (overlay redraw, triggerRepaint+rAF fallback, standalone, no-map rejection) and a LeafletAdapter contract check. Import MAP_ENGINE in the deck.gl spec from types/engine to avoid a leaflet transitive import that breaks module load in the Node test env.
Standalone Deck (no basemap) omitted preserveDrawingBuffer, so captureScreenshot() would read a cleared buffer and return a blank image there. Match the overlay path so capture works in standalone mode too.
✅ Version Already UpdatedThis PR includes a manual version update to No automatic version bump needed. |
…are-screenshot-api
…' into feature/143-share-screenshot-api
shareScreenshotApi.spec.js imported test/expect from @playwright/test but lives under tests/unit, which Vitest globs; Playwright's test.describe() threw at collection and failed the whole Vitest run, so the screenshot regression guards never executed. Import from 'vitest' like the sibling specs. staticHandlers.spec.js hardcoded a 40-entry count that dropped to 39 when shortener_shorten was removed from calls.js/staticHandlers.js. Update to 39; the parity tests already covered the symmetric removal.
…ps (v9)
_initStandaloneMode set glOptions: { preserveDrawingBuffer: true }, but
glOptions is a deck.gl v8 prop and does not exist in v9 (we run 9.2.10);
the surrounding 'as any' cast hid that it was silently dropped. Standalone
capture only worked because v9 defaults preserveDrawingBuffer to true.
Set it explicitly via the correct v9 shape, deviceProps.webgl, so the
guarantee no longer rides on that default. Add a spec that drives the real
init() path with the Deck/maplibre constructors mocked and asserts the
setting reaches both the standalone (Deck) and overlay (maplibre Map) paths;
the existing captureScreenshot tests bypass init() and never covered it.
The engine adapter was reaching up into the UI-shell layer: LeafletAdapter and the core mmgisAPI both imported getMapScreenshot from Basics/UserInterface_/ScreenshotUtils, inverting the intended dependency direction (engines sit below the UI and stay portable). - Move ScreenshotUtils.js -> MapEngines/Adapters/LeafletScreenshot.js, next to the adapter that owns it (it IS the Leaflet capture strategy). Chrome hide/restore stays with it, since those fixups exist only to make html2canvas rasterize the Leaflet DOM correctly. - LeafletAdapter imports it from the sibling file (same layer, no inversion). - mmgisAPI.getMapScreenshot() delegates only to the active engine and rejects when none is present, dropping the hardcoded-Leaflet fallback. Map_ assigns its engine synchronously at init, so the fallback was only reachable before any map loaded. - Fix a restore asymmetry (C2): the time UI was collapsed for the capture but never reopened; remember its state and toggle it back, with a regression test. - Reword the public getMapScreenshot() JSDoc to be engine-agnostic and note the deck.gl capture excludes HTML overlays (E1/E2).
…pture Add behavioral coverage for the claims the PR previously only asserted: - mmgisAPI.getMapScreenshot() delegates to the active engine's captureScreenshot() and rejects when no engine is loaded (drives the real facade via the L_ singleton). - QueryURL.writeCoordinateURL() returns the full view URL synchronously as a string and, in the modern layout (no UserInterface_.getPanelPercents), falls back to a map-only pane split without throwing. - LeafletAdapter.captureScreenshot() actually delegates to the Leaflet screenshot strategy and returns its promise (was existence-only). The mmgisAPI/QueryURL specs stub the Viewer_ aggregator, whose JSX-in-.js viewers can't be parsed by vite's import-analysis in the jsdom env.
…are-screenshot-api # Conflicts: # configure/package.json # package.json
🤖 Version Auto-BumpedThe version has been automatically incremented to This commit was added to your PR branch. When you merge this PR, the new version will be included. If you want a different version, update |
The shipped helmet CSP's connect-src does not allow the data: scheme, so the fetch(dataURL) round-trip failed to download on stock deployments. Decode base64 to a Blob directly in a new F_.downloadDataUrl helper (replacing the now-orphaned F_.downloadCanvas) and defer the object-URL revoke so browsers that dereference it asynchronously can't abort the download.
… calls writeCoordinateURL returns null until mission finalization instead of letting QueryURL TypeError on unassigned L_.Viewer_/L_.Map_ (a plugin's always-visible Copy Link button is clickable during load). getMapScreenshot converts a synchronous engine throw into a rejection, and LeafletScreenshot restores the hidden UI chrome on the error path.
…Buffer
preserveDrawingBuffer:true imposed a permanent per-frame buffer-copy and an
extra full-DPR framebuffer on every deck.gl dashboard to support occasional
screenshots. Capture instead via basemap.once('render') + triggerRepaint()
(same-frame readback, 3s safety timeout); the standalone path keeps its
synchronous redraw('screenshot') + toDataURL, which deck.gl v9 draws in the
same call stack. Makes the previously dead BasemapInstance.once() member
load-bearing and removes the unused loaded()/redraw() members.
…' into feature/143-share-screenshot-api
…nimation html2canvas fires onclone after this module has already restored the live UI, so re-reading tile z-indices there leaked the restored values into the clone and undid the pre-capture normalization (inherited from the old BottomBar implementation). The fixups now apply a snapshot taken at capture kickoff. The identical SVG re-parent + tile z-index workaround in the Animation tool's OffscreenMapManager now uses the same shared LeafletCloneFixups module instead of its own copy. Also drops the test-only jquery/html2canvas injection params and the redundant default export — specs use vi.mock instead.
Normalize the LF lines the fix pass introduced into three pure-CRLF files
(QueryURL.js, BottomBar.js, mmgisAPI.js). Unhook the pending once('render')
listener when a deck.gl capture times out — a backgrounded tab's timed-out
capture otherwise leaves it armed to fire a wasted capture on a later
render (regression-tested). Delete F_.downloadDataUrl, dead since the Blob
screenshot contract landed.
The fix-pass files carried 2-6x the repo's comment density; cut narration and keep only the constraints the code can't show. Also corrects the clone fixup rationale: cloned nodes keep their inline styles — the guard is against onclone reading live styles after the UI restore, not against the clone 'losing' z-index; and scopes the transform claim to zoom-animated overlay SVG containers.
F_.copyToClipboard now tries the async Clipboard API first and falls back to the legacy textarea path when it is absent (insecure origins) OR rejects (embedding iframe without allow=clipboard-write, expired user gesture) — resolving true/false, never rejecting. mmgisAPI.copyText delegates to it so plugins get the same single implementation instead of re-vendoring the fallback. The four core call sites now gate their 'Copied!' feedback on the result instead of showing success unconditionally.
Share URLs write the modern map's fractional zoom but the restore parsed it with parseInt, so pasted links reopened at a visibly different zoom (classic Leaflet was unaffected — integer zooms either way). Mission name in screenshot filenames and getViewState now prefers L_.mission (the ?mission= identity) over configData.msv.mission, a display field that can be stale (seed data ships 'New mission modern').
Second half of the zoom round-trip bug: QueryURL now preserves the fractional zoom in FUTURES.mapView, but Map_.resetView parseInt'd it again before engine.setView — so pasted share links still reopened slightly zoomed out on the modern map.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #143
Exposes share-link and map-screenshot as first-class capabilities on the public plugin API (
window.mmgisAPI), so plugins (like #168's Share/Export) can build on them without reaching into core.What
writeCoordinateURL()— the current view as a complete, self-contained URL. Returnsnulluntil the mission finishes loading. The classic Copy Link button still shortens via the backend (?s=) in full mode, falling back to the long URL; lean mode always uses long URLs.getMapScreenshot()— engine-aware capture resolving to{ blob, mimeType, extension, width, height }. Leaflet rasterizes the map container with html2canvas (its clone fixups now live in a shared module also used by the Animation tool); deck.gl reads its WebGL canvas on demand via aonce('render')readback — no always-onpreserveDrawingBuffer, so there's no per-frame cost.getViewState()—{ missionName, time, center, zoom }, e.g. for provenance-rich export filenames.The classic bottom-bar buttons are now thin consumers of these same APIs. Downloads go through a new
F_.downloadBlob(object URL, deferred revoke) —fetch(data:)is avoided because the shipped CSP blocks thedata:scheme.Known limitations
addOverlaymarkers/labels) are not in the image. Deliberate scope; compositing them onto the GL frame is a tracked follow-up. Leaflet's DOM-based capture does include them.devicePixelRatio— hi-DPI screens produce large, sharp exports by design.Verification