Core: expose the share-link and map-screenshot as first-class plugin API capabilities
Motivation
The dashboard already knows how to do the two hardest parts of sharing: it can encode its current view into a self-contained link, and it can capture a picture of the map. But both abilities are locked inside the app's own built-in UI. A plugin has no supported way to say "give me a link to the current view" or "give me an image of the current map."
That's a problem for where this framework is going. Plugins are supposed to be decoupled — they build on a public API and never reach into the core's internals. Any plugin that wants to offer "share" or "export" today would have to break that rule. Exposing these two capabilities as supported, first-class parts of the plugin API is what makes a clean Share/Export plugin (and anything else that wants to share or snapshot the view) possible without coupling.
Note: this is a pure-frontend capability. The link is a full, self-contained URL — there is no backend involved. (The old server-side link shortener is going away; we are not depending on it or replacing it here.)
How it should work
The framework exposes two capabilities to plugins through its public API:
-
Get a share link to the current dashboard state. A plugin asks for it and receives a complete, self-contained URL — synchronously, with no server round-trip. Opening that link restores the same view the user had (see "What is shared" below). The URL is long because it carries the whole state; that's fine — it's self-contained and needs no backend to resolve.
-
Get an image of the current map. A plugin asks for it and receives a PNG snapshot of what's currently on screen.
A plugin uses these results however it likes (copy to clipboard, download, display) and never touches the app's internal modules. The existing built-in screenshot button continues to work exactly as before — it just uses the same shared capability under the hood.
What is shared (captured in the link)
When the link is generated, it captures the current:
- Mission / site being viewed
- 2D map center and zoom
- 3D globe center, zoom, and camera orientation
- Panel layout (the relative sizes of the viewer / map / globe panes)
- Visible layers and each one's opacity — this is also how the basemap / tiling is captured (the basemap is one of the visible layers)
- Selected feature, if one is selected
- Viewer image and its position (for image / photosphere / 3D-model viewers)
- Time state — start/end time, live mode, follow — when time is enabled
- State from existing built-in tools that already publish it (e.g. Draw's active files and filters, layer reordering, Shade, Viewshed)
What is NOT shared (won't survive the link)
Be explicit about the gaps so nobody assumes more is captured than is:
- State that lives only in newer (modern) plugins is not captured. Modern plugins don't currently contribute anything to the link. ⚠️ Concretely: if the date/time picker is ever delivered as a modern plugin, the selected date would silently stop being shared — today it only works because time still lives in the core. Closing this gap (a way for any plugin to contribute its own state to the link) is a larger, separate design and is out of scope here.
- Runtime layer restyling beyond opacity (color/width/symbol changes made on the fly).
- Unsaved drawn geometry — only which Draw files are active is shared, not in-progress drawings.
- Which tool/panel is open, beyond what an individual tool chooses to publish.
- 2D map rotation (there is none in the 2D map; only the 3D globe camera is captured).
Done when
Out of scope
- The end-user button/panel that surfaces these — that's the separate Share/Export plugin issue, which depends on this one.
- Short links / any link-shortening service. The self-contained full URL is the deliverable. External shorteners or client-side state compression are separate, optional efforts.
- PDF export.
- A general mechanism for modern plugins to contribute their own state into the link (the "What is NOT shared" gap). Larger, separate design.
- Continuously updating the address bar as the user navigates, or making shared states navigable via the browser back button (today nothing pushes browser history; this issue doesn't change that).
Draft implementation plan — written as of d85a6c66 on 2026-06-15. Rough guide; re-verify against latest code.
Current behavior
- The current view is serialized into a query-string URL by
writeCoordinateURL in src/essence/Ancillary/QueryURL.js. It aggregates everything under "What is shared." Built-in tool state is collected by walking each tool's getUrlString() via ToolController_.getToolsUrl() (src/essence/Basics/ToolController_/ToolController_.js:605). Modern TSX plugins return '' and contribute nothing — that's the "What is NOT shared" gap.
- The full URL is already exposed to plugins:
mmgisAPI.writeCoordinateURL() calls QueryURL.writeCoordinateURL(false) (src/essence/mmgisAPI/mmgisAPI.js:440, exported ~720-724) and returns the complete URL synchronously, without touching the address bar. So the "share link" half is largely done — this issue mostly formalizes/documents it as the supported share-link method.
- Shortening lives only behind
shortenURL = true in QueryURL.writeCoordinateURL (the shortener_shorten API call, ~QueryURL.js:430-449), invoked from exactly one place — the built-in flow in src/essence/Basics/UserInterface_/BottomBar.js:35. With the shortener being removed, that branch/dependency goes away; the synchronous false path is all we need.
- The map screenshot is implemented inline in the camera-button handler in
BottomBar.js (~lines 70–169) using html2canvas, including an onclone callback that fixes SVG layer positioning/z-index. It is not callable from anywhere else — this is the real core gap.
- URL writes everywhere use
history.replaceState, never pushState (essence.js:346, modern.js:184, QueryURL.js:440/446/468); the address bar is only set to ?mission=... on load. Hence no back-button history of views.
Where the change lands & rough plan
- Share-link method: the synchronous full URL is already returned by
mmgisAPI.writeCoordinateURL(). Confirm it's the supported, documented entry point; if the shortener removal touches writeCoordinateURL's signature (the shortenURL/callback params), simplify so the public method is unambiguously "return the full URL, no backend." No async, no fallback.
- Screenshot method (the real work): extract the
html2canvas logic out of the BottomBar.js button handler into a reusable function (e.g. a small ScreenshotUtils), expose it through the public API, and re-point the existing button at it so its behavior is unchanged.
- Expose the screenshot via the same direct-method /
provide pattern the API already uses (e.g. the app:* / layers:* handlers registered in Layers_.fina(), src/essence/Basics/Layers_/Layers_.js ~164–232). The event bus (on/emit) is not needed — these are request/response, not observed events.
⚠️ Gotcha: the screenshot's onclone callback does non-obvious SVG / z-index fixups; if you don't preserve it verbatim when extracting, the captured image renders wrong (mislayered or blank).
References
src/essence/Ancillary/QueryURL.js — writeCoordinateURL, shortener branch to be removed (~430–449)
src/essence/mmgisAPI/mmgisAPI.js — already-exposed full URL (~440, ~720–724)
src/essence/Basics/UserInterface_/BottomBar.js — inline screenshot (~70–169) + only shortener caller (line 35)
src/essence/Basics/Layers_/Layers_.js — provider registration pattern in fina() (~164–232)
src/essence/Basics/ToolController_/ToolController_.js — getToolsUrl() / getUrlString() aggregation (~605)
Core: expose the share-link and map-screenshot as first-class plugin API capabilities
Motivation
The dashboard already knows how to do the two hardest parts of sharing: it can encode its current view into a self-contained link, and it can capture a picture of the map. But both abilities are locked inside the app's own built-in UI. A plugin has no supported way to say "give me a link to the current view" or "give me an image of the current map."
That's a problem for where this framework is going. Plugins are supposed to be decoupled — they build on a public API and never reach into the core's internals. Any plugin that wants to offer "share" or "export" today would have to break that rule. Exposing these two capabilities as supported, first-class parts of the plugin API is what makes a clean Share/Export plugin (and anything else that wants to share or snapshot the view) possible without coupling.
Note: this is a pure-frontend capability. The link is a full, self-contained URL — there is no backend involved. (The old server-side link shortener is going away; we are not depending on it or replacing it here.)
How it should work
The framework exposes two capabilities to plugins through its public API:
Get a share link to the current dashboard state. A plugin asks for it and receives a complete, self-contained URL — synchronously, with no server round-trip. Opening that link restores the same view the user had (see "What is shared" below). The URL is long because it carries the whole state; that's fine — it's self-contained and needs no backend to resolve.
Get an image of the current map. A plugin asks for it and receives a PNG snapshot of what's currently on screen.
A plugin uses these results however it likes (copy to clipboard, download, display) and never touches the app's internal modules. The existing built-in screenshot button continues to work exactly as before — it just uses the same shared capability under the hood.
What is shared (captured in the link)
When the link is generated, it captures the current:
What is NOT shared (won't survive the link)
Be explicit about the gaps so nobody assumes more is captured than is:
Done when
Out of scope
Draft implementation plan — written as of d85a6c66 on 2026-06-15. Rough guide; re-verify against latest code.
Current behavior
writeCoordinateURLinsrc/essence/Ancillary/QueryURL.js. It aggregates everything under "What is shared." Built-in tool state is collected by walking each tool'sgetUrlString()viaToolController_.getToolsUrl()(src/essence/Basics/ToolController_/ToolController_.js:605). Modern TSX plugins return''and contribute nothing — that's the "What is NOT shared" gap.mmgisAPI.writeCoordinateURL()callsQueryURL.writeCoordinateURL(false)(src/essence/mmgisAPI/mmgisAPI.js:440, exported ~720-724) and returns the complete URL synchronously, without touching the address bar. So the "share link" half is largely done — this issue mostly formalizes/documents it as the supported share-link method.shortenURL = trueinQueryURL.writeCoordinateURL(theshortener_shortenAPI call, ~QueryURL.js:430-449), invoked from exactly one place — the built-in flow insrc/essence/Basics/UserInterface_/BottomBar.js:35. With the shortener being removed, that branch/dependency goes away; the synchronousfalsepath is all we need.BottomBar.js(~lines 70–169) usinghtml2canvas, including anonclonecallback that fixes SVG layer positioning/z-index. It is not callable from anywhere else — this is the real core gap.history.replaceState, neverpushState(essence.js:346,modern.js:184,QueryURL.js:440/446/468); the address bar is only set to?mission=...on load. Hence no back-button history of views.Where the change lands & rough plan
mmgisAPI.writeCoordinateURL(). Confirm it's the supported, documented entry point; if the shortener removal toucheswriteCoordinateURL's signature (theshortenURL/callback params), simplify so the public method is unambiguously "return the full URL, no backend." No async, no fallback.html2canvaslogic out of theBottomBar.jsbutton handler into a reusable function (e.g. a smallScreenshotUtils), expose it through the public API, and re-point the existing button at it so its behavior is unchanged.providepattern the API already uses (e.g. theapp:*/layers:*handlers registered inLayers_.fina(),src/essence/Basics/Layers_/Layers_.js~164–232). The event bus (on/emit) is not needed — these are request/response, not observed events.References
src/essence/Ancillary/QueryURL.js—writeCoordinateURL, shortener branch to be removed (~430–449)src/essence/mmgisAPI/mmgisAPI.js— already-exposed full URL (~440, ~720–724)src/essence/Basics/UserInterface_/BottomBar.js— inline screenshot (~70–169) + only shortener caller (line 35)src/essence/Basics/Layers_/Layers_.js— provider registration pattern infina()(~164–232)src/essence/Basics/ToolController_/ToolController_.js—getToolsUrl()/getUrlString()aggregation (~605)