Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
727f007
[143] Expose share-link and map-screenshot via plugin API
CarsonDavis Jun 25, 2026
a329c59
[143] Fix share-link and screenshot for the modern layout
CarsonDavis Jun 30, 2026
0f6eef5
[143] Engine-aware map screenshot (deck.gl/GL canvas capture)
CarsonDavis Jun 30, 2026
1e55e43
[143] Set preserveDrawingBuffer for standalone deck.gl mode
CarsonDavis Jun 30, 2026
fa68657
chore: bump version to 4.2.12-20260630 [version bump]
github-actions[bot] Jun 30, 2026
272f678
Merge remote-tracking branch 'origin/development' into feature/143-sh…
CarsonDavis Jul 1, 2026
6ef7f1a
Merge remote-tracking branch 'origin/feature/143-share-screenshot-api…
CarsonDavis Jul 1, 2026
ca68abe
[143] fix(tests): unblock unit suite (vitest import + registry count)
CarsonDavis Jul 1, 2026
705f041
[143] fix(deckgl): set standalone preserveDrawingBuffer via devicePro…
CarsonDavis Jul 1, 2026
8010171
[143] refactor(engine): own Leaflet screenshot in the engine layer
CarsonDavis Jul 1, 2026
4d0933c
[143] test: cover engine delegation, share-link guard, and Leaflet ca…
CarsonDavis Jul 1, 2026
c1512a1
Merge remote-tracking branch 'origin/development' into feature/143-sh…
CarsonDavis Jul 2, 2026
112298d
chore: bump version to 4.2.14-20260702 [version bump]
github-actions[bot] Jul 2, 2026
8df2738
[143] fix: download screenshots via Blob decode, not fetch(data:)
CarsonDavis Jul 2, 2026
d11fd77
[143] fix: harden share-link and screenshot APIs against early/failed…
CarsonDavis Jul 2, 2026
7ff1b22
[143] perf(deckgl): capture on demand, drop always-on preserveDrawing…
CarsonDavis Jul 2, 2026
973ba17
[143] test: cover getViewState and the writeCoordinateURL readiness g…
CarsonDavis Jul 2, 2026
33ff4ba
Merge remote-tracking branch 'origin/feature/143-share-screenshot-api…
CarsonDavis Jul 2, 2026
e6e4041
Restore full-mode URL shortening fallback
CarsonDavis Jul 2, 2026
35037a3
Return map screenshots as Blob results
CarsonDavis Jul 2, 2026
fd8bc88
[143] fix: pin clone fixups to kickoff-time styles; share them with A…
CarsonDavis Jul 2, 2026
5327ef3
[143] fix: review findings — CRLF hygiene, timeout unhook, dead helper
CarsonDavis Jul 2, 2026
f8091b4
[143] docs: trim comments to repo-normal density; correct Leaflet claims
CarsonDavis Jul 2, 2026
3d2c1fa
[143] docs: captureScreenshot returns a Blob result, not a data URL
CarsonDavis Jul 2, 2026
b1f040a
[143] feat: expose mmgisAPI.copyText(); upgrade the clipboard helper
CarsonDavis Jul 2, 2026
2cebc23
[143] fix: fractional zoom restore; canonical mission name in exports
CarsonDavis Jul 2, 2026
7876914
[143] fix: resetView truncated fractional zoom too
CarsonDavis Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion configure/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "configure",
"version": "4.2.13-20260701",
"version": "4.2.14-20260702",
"homepage": "./configure/build",
"private": true,
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mmgis",
"version": "4.2.13-20260701",
"version": "4.2.14-20260702",
"description": "A web-based mapping and localization solution for science operation on planetary missions.",
"homepage": "build",
"repository": {
Expand Down
26 changes: 16 additions & 10 deletions src/essence/Ancillary/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,28 @@ function showContextMenuMap(e) {
$('#contextMenuMapCopyCoords').on('click', function () {
F_.copyToClipboard(
JSON.stringify(Coordinates.getAllCoordinates(), null, 2)
)
$('#contextMenuMapCopyCoords').text('Copied!')
setTimeout(function () {
$('#contextMenuMapCopyCoords').text('Copy Coordinates')
}, 2000)
).then((copied) => {
if (!copied) return
$('#contextMenuMapCopyCoords').text('Copied!')
setTimeout(function () {
$('#contextMenuMapCopyCoords').text('Copy Coordinates')
}, 2000)
})
})

$('#contextMenuCopyable').on('click', function () {
const that = this
const key = $(that).attr('key')
const copyable = L_._toolCopyables[key]
F_.copyToClipboard(JSON.stringify(copyable.copyable, null, 2))
$(that).text('Copied!')
setTimeout(function () {
$(that).text(copyable.title)
}, 2000)
F_.copyToClipboard(JSON.stringify(copyable.copyable, null, 2)).then(
(copied) => {
if (!copied) return
$(that).text('Copied!')
setTimeout(function () {
$(that).text(copyable.title)
}, 2000)
}
)
})

contextMenuActionsFull.forEach((c) => {
Expand Down
71 changes: 41 additions & 30 deletions src/essence/Ancillary/QueryURL.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import F_ from '../Basics/Formulae_/Formulae_'
import L_ from '../Basics/Layers_/Layers_'
import T_ from '../Basics/ToolController_/ToolController_'
import calls from '../../pre/calls'
import { isStaticBuild } from '../../pre/capabilities'
import TimeControl from '../Basics/TimeControl_/TimeControl'
import TimeUI from '../Basics/TimeControl_/TimeUI'

Expand Down Expand Up @@ -50,7 +51,10 @@ var QueryURL = {
L_.FUTURES.mapView = [
parseFloat(urlMapLat),
parseFloat(urlMapLon),
urlMapZoom !== false ? parseInt(urlMapZoom) : null,
// parseFloat, not parseInt: the modern map zooms fractionally and
// truncation visibly changes the restored view (classic Leaflet
// zooms are integers either way).
urlMapZoom !== false ? parseFloat(urlMapZoom) : null,
]
}

Expand Down Expand Up @@ -277,8 +281,10 @@ var QueryURL = {
tools
"tools=camp$1.3.4,"
*/
// Builds and returns the full, self-contained view URL synchronously. This
// is the canonical share-link method (exposed publicly via
// mmgisAPI.writeCoordinateURL). It makes no backend call.
writeCoordinateURL: function (
shortenURL = true,
mapLon,
mapLat,
mapZoom,
Expand All @@ -288,12 +294,6 @@ var QueryURL = {
) {
L_.Viewer_.getLocation()

var callback
if (typeof mapLon === 'function') {
callback = mapLon
mapLon = undefined
}

//Defaults
if (mapLon == undefined) mapLon = L_.Map_.map.getCenter().lng
if (mapLat == undefined) mapLat = L_.Map_.map.getCenter().lat
Expand Down Expand Up @@ -358,7 +358,13 @@ var QueryURL = {
}

//panePercents
var pP = L_.UserInterface_.getPanelPercents()
// getPanelPercents only exists on the classic/mobile UI controllers; the
// modern layout has no such method, so fall back to a map-only split.
var pP =
L_.UserInterface_ &&
typeof L_.UserInterface_.getPanelPercents === 'function'
? L_.UserInterface_.getPanelPercents()
: { viewer: 0, map: 100, globe: 0 }
var panePercents = pP.viewer + ',' + pP.map + ',' + pP.globe
urlAppendage += '&panePercents=' + panePercents

Expand Down Expand Up @@ -427,29 +433,34 @@ var QueryURL = {

var url = encodeURI(urlAppendage)

if (shortenURL) {
calls.api(
'shortener_shorten',
{
url: url,
},
function (s) {
//Set and update the short url
L_.url =
window.location.href.split('?')[0] + '?s=' + s.body.url
window.history.replaceState('', '', L_.url)
if (typeof callback === 'function') callback()
},
function (e) {
//Set and update the full url
L_.url = window.location.href.split('?')[0] + url
window.history.replaceState('', '', L_.url)
if (typeof callback === 'function') callback()
}
)
return window.location.href.split('?')[0] + url
},
getShareURL: function (callback) {
var fullUrl = this.writeCoordinateURL()

if (isStaticBuild()) {
if (typeof callback === 'function') callback(fullUrl)
return
}

return window.location.href.split('?')[0] + url
var baseUrl = window.location.href.split('?')[0]
var urlAppendage = fullUrl.startsWith(baseUrl)
? fullUrl.substring(baseUrl.length)
: fullUrl.substring(fullUrl.indexOf('?'))

calls.api(
'shortener_shorten',
{
url: urlAppendage,
},
function (s) {
var shortUrl = baseUrl + '?s=' + s.body.url
if (typeof callback === 'function') callback(shortUrl)
},
function () {
if (typeof callback === 'function') callback(fullUrl)
}
)
},
writeSearchURL: function (searchStrs, searchFile) {
return //!!!!!!!!!!!!!!!!
Expand Down
83 changes: 52 additions & 31 deletions src/essence/Basics/Formulae_/Formulae_.js
Original file line number Diff line number Diff line change
Expand Up @@ -1803,18 +1803,18 @@ var Formulae_ = {
downloadAnchorNode.click()
downloadAnchorNode.remove()
},
downloadCanvas(canvasId, name, callback) {
var link = document.createElement('a')
name = name ? name + '.png' : 'mmgis.png'
link.setAttribute('download', name)
document.getElementById(canvasId).toBlob(function (blob) {
var objUrl = URL.createObjectURL(blob)
link.setAttribute('href', objUrl)
document.body.appendChild(link)
link.click()
link.remove()
if (typeof callback === 'function') callback()
})
// Downloads a Blob as a file via a transient object URL. Don't convert
// via fetch(dataUrl): the shipped CSP's connect-src blocks the data: scheme.
downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.setAttribute('download', filename)
link.setAttribute('href', url)
document.body.appendChild(link) // required for firefox
link.click()
link.remove()
// Deferred: an immediate revoke can abort the download.
setTimeout(() => URL.revokeObjectURL(url), 10000)
},
getMinMaxOfArray(arrayOfNumbers) {
return {
Expand Down Expand Up @@ -2043,28 +2043,49 @@ var Formulae_ = {
return arr1.filter((e) => arr2.indexOf(e) !== -1)
},
/**
* Copies input to user's clipboard
* Copies text to the user's clipboard. Resolves true on success, false on
* failure — never rejects. Tries the async Clipboard API first, falling
* back to a hidden textarea + execCommand('copy') when it is absent
* (insecure origins) OR when it rejects (e.g. an embedding iframe without
* allow="clipboard-write", or a call outside the user-gesture window).
* @param {string} text - text to copy to clipboard
* @credit https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
* @returns {Promise<boolean>}
*/
copyToClipboard(text) {
const el = document.createElement('textarea') // Create a <textarea> element
el.value = text // Set its value to the string that you want copied
el.setAttribute('readonly', '') // Make it readonly to be tamper-proof
async copyToClipboard(text) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text)
return true
} catch (err) {
// Fall through to the legacy path.
}
}
const el = document.createElement('textarea')
el.value = text
el.setAttribute('readonly', '')
el.style.position = 'absolute'
el.style.left = '-9999px' // Move outside the screen to make it invisible
document.body.appendChild(el) // Append the <textarea> element to the HTML document
const selected =
document.getSelection().rangeCount > 0 // Check if there is any content selected previously
? document.getSelection().getRangeAt(0) // Store selection if found
: false // Mark as false to know no selection existed before
el.select() // Select the <textarea> content
document.execCommand('copy') // Copy - only works as a result of a user action (e.g. click events)
document.body.removeChild(el) // Remove the <textarea> element
if (selected) {
// If a selection existed before copying
document.getSelection().removeAllRanges() // Unselect everything on the HTML document
document.getSelection().addRange(selected) // Restore the original selection
el.style.left = '-9999px'
// select() destroys the user's page selection; save and restore it.
const selection = document.getSelection()
const savedRange =
selection && selection.rangeCount > 0
? selection.getRangeAt(0)
: null
document.body.appendChild(el)
try {
el.select()
const ok = document.execCommand('copy')
if (!ok) console.warn('copyToClipboard: copy command rejected')
return ok
} catch (err) {
console.warn('copyToClipboard failed:', err)
return false
} finally {
document.body.removeChild(el)
if (selection && savedRange) {
selection.removeAllRanges()
selection.addRange(savedRange)
}
}
},
speedToMetersPerSeconds(speed, fromUnit) {
Expand Down
Loading
Loading