Guest User

Untitled

a guest
Aug 5th, 2026
39
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.20 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Neuroglancer Auto Import Settings
  3. // @namespace eyewire-elite
  4. // @version 2.1.0
  5. // @description Imports saved settings only when a Neuroglancer link is first opened, not when the page is refreshed.
  6. // @match https://spelunker.cave-explorer.org/*
  7. // @run-at document-idle
  8. // @grant unsafeWindow
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. const CONFIG = Object.freeze({
  15. settingsStorageKey: 'ng-settings',
  16.  
  17. /*
  18. * Never import immediately, even when the state initially appears stable.
  19. */
  20. minimumWaitMilliseconds: 3000,
  21.  
  22. /*
  23. * The meaningful viewer state must remain unchanged for this long.
  24. */
  25. requiredStableMilliseconds: 2500,
  26.  
  27. /*
  28. * How often readiness is checked.
  29. */
  30. checkIntervalMilliseconds: 250,
  31.  
  32. /*
  33. * Stop trying after this amount of time.
  34. */
  35. maximumWaitMilliseconds: 45000
  36. });
  37.  
  38. /*
  39. * Do not auto-import after:
  40. *
  41. * - Refresh
  42. * - Ctrl+R
  43. * - F5
  44. * - Ctrl+F5
  45. * - Browser reload button
  46. *
  47. * A newly opened link or normal navigation has type "navigate" and is
  48. * allowed to continue.
  49. */
  50. if (isReloadNavigation()) {
  51. console.log(
  52. '[Auto import] Page reload detected. ' +
  53. 'Automatic settings import was skipped.'
  54. );
  55.  
  56. return;
  57. }
  58.  
  59. /*
  60. * If no settings have been exported, leave the viewer unchanged.
  61. */
  62. if (!localStorage.getItem(CONFIG.settingsStorageKey)) {
  63. console.log(
  64. '[Auto import] No saved ng-settings entry found. ' +
  65. 'The current viewer will remain unchanged.'
  66. );
  67.  
  68. return;
  69. }
  70.  
  71. const startedAt = performance.now();
  72.  
  73. let lastSnapshot = null;
  74. let stableSince = null;
  75. let importTriggered = false;
  76.  
  77. const intervalId = window.setInterval(
  78. checkViewer,
  79. CONFIG.checkIntervalMilliseconds
  80. );
  81.  
  82.  
  83. /**
  84. * Determines whether this page load resulted from a refresh.
  85. */
  86. function isReloadNavigation() {
  87. try {
  88. const navigationEntries =
  89. performance.getEntriesByType('navigation');
  90.  
  91. if (
  92. Array.isArray(navigationEntries) &&
  93. navigationEntries.length > 0
  94. ) {
  95. return navigationEntries[0].type === 'reload';
  96. }
  97. } catch (error) {
  98. console.warn(
  99. '[Auto import] Could not read Navigation Timing data:',
  100. error
  101. );
  102. }
  103.  
  104. /*
  105. * Fallback for older browsers.
  106. *
  107. * TYPE_RELOAD has the numeric value 1.
  108. */
  109. try {
  110. if (
  111. performance.navigation &&
  112. performance.navigation.type === 1
  113. ) {
  114. return true;
  115. }
  116. } catch (error) {
  117. // Treat an unavailable fallback as a normal navigation.
  118. }
  119.  
  120. return false;
  121. }
  122.  
  123.  
  124. function checkViewer() {
  125. if (importTriggered) {
  126. window.clearInterval(intervalId);
  127. return;
  128. }
  129.  
  130. const now = performance.now();
  131. const totalElapsed = now - startedAt;
  132.  
  133. if (
  134. totalElapsed >=
  135. CONFIG.maximumWaitMilliseconds
  136. ) {
  137. window.clearInterval(intervalId);
  138.  
  139. console.warn(
  140. '[Auto import] Viewer did not reach a stable state within ' +
  141. Math.round(
  142. CONFIG.maximumWaitMilliseconds / 1000
  143. ) +
  144. ' seconds. Automatic import was skipped; press / manually.'
  145. );
  146.  
  147. return;
  148. }
  149.  
  150. if (
  151. totalElapsed <
  152. CONFIG.minimumWaitMilliseconds
  153. ) {
  154. return;
  155. }
  156.  
  157. const pageViewer = getPageViewer();
  158.  
  159. if (!isViewerReady(pageViewer)) {
  160. resetStability();
  161. return;
  162. }
  163.  
  164. if (isLoginPromptVisible()) {
  165. resetStability();
  166. return;
  167. }
  168.  
  169. let snapshot;
  170.  
  171. try {
  172. snapshot =
  173. createMeaningfulSnapshot(pageViewer);
  174. } catch (error) {
  175. resetStability();
  176. return;
  177. }
  178.  
  179. if (snapshot !== lastSnapshot) {
  180. lastSnapshot = snapshot;
  181. stableSince = now;
  182. return;
  183. }
  184.  
  185. if (stableSince === null) {
  186. stableSince = now;
  187. return;
  188. }
  189.  
  190. const stableElapsed =
  191. now - stableSince;
  192.  
  193. if (
  194. stableElapsed >=
  195. CONFIG.requiredStableMilliseconds
  196. ) {
  197. importTriggered = true;
  198. window.clearInterval(intervalId);
  199.  
  200. triggerExistingImporter();
  201.  
  202. console.log(
  203. '[Auto import] Viewer stabilized after ' +
  204. (totalElapsed / 1000).toFixed(2) +
  205. ' seconds. Settings import triggered.'
  206. );
  207. }
  208. }
  209.  
  210.  
  211. function getPageViewer() {
  212. try {
  213. if (
  214. typeof unsafeWindow !== 'undefined' &&
  215. unsafeWindow &&
  216. unsafeWindow.viewer
  217. ) {
  218. return unsafeWindow.viewer;
  219. }
  220. } catch (error) {
  221. // Continue to the normal window check.
  222. }
  223.  
  224. try {
  225. if (window.viewer) {
  226. return window.viewer;
  227. }
  228. } catch (error) {
  229. // Viewer is not available yet.
  230. }
  231.  
  232. return null;
  233. }
  234.  
  235.  
  236. function isViewerReady(pageViewer) {
  237. try {
  238. if (
  239. !pageViewer ||
  240. !pageViewer.state ||
  241. typeof pageViewer.state.toJSON !==
  242. 'function'
  243. ) {
  244. return false;
  245. }
  246.  
  247. const state =
  248. pageViewer.state.toJSON();
  249.  
  250. if (
  251. !state ||
  252. !Array.isArray(state.layers) ||
  253. state.layers.length === 0
  254. ) {
  255. return false;
  256. }
  257.  
  258. const hasSegmentationLayer =
  259. state.layers.some(function (layer) {
  260. return Boolean(
  261. layer &&
  262. layer.type === 'segmentation' &&
  263. layer.source
  264. );
  265. });
  266.  
  267. if (!hasSegmentationLayer) {
  268. return false;
  269. }
  270.  
  271. const topBar =
  272. document.querySelector(
  273. '.neuroglancer-viewer-top-row'
  274. );
  275.  
  276. const renderedPanel =
  277. document.querySelector(
  278. '.neuroglancer-rendered-data-panel, canvas'
  279. );
  280.  
  281. return Boolean(
  282. topBar &&
  283. renderedPanel
  284. );
  285. } catch (error) {
  286. return false;
  287. }
  288. }
  289.  
  290.  
  291. /**
  292. * Makes a snapshot of state values that commonly change during initial load.
  293. *
  294. * Large annotation arrays are reduced to counts so comparison stays cheap.
  295. */
  296. function createMeaningfulSnapshot(
  297. pageViewer
  298. ) {
  299. const state =
  300. pageViewer.state.toJSON();
  301.  
  302. const layers =
  303. state.layers.map(function (layer) {
  304. const snapshotLayer = {
  305. name: layer.name || '',
  306. type: layer.type || '',
  307. source:
  308. normalizeSource(layer.source),
  309.  
  310. visible:
  311. Object.prototype
  312. .hasOwnProperty.call(
  313. layer,
  314. 'visible'
  315. )
  316. ? layer.visible
  317. : null
  318. };
  319.  
  320. if (
  321. layer.type ===
  322. 'segmentation'
  323. ) {
  324. snapshotLayer.segments =
  325. Array.isArray(
  326. layer.segments
  327. )
  328. ? layer.segments.map(
  329. String
  330. )
  331. : [];
  332.  
  333. snapshotLayer.segmentCount =
  334. snapshotLayer.segments.length;
  335. }
  336.  
  337. if (
  338. layer.type ===
  339. 'annotation'
  340. ) {
  341. snapshotLayer.annotationCount =
  342. Array.isArray(
  343. layer.annotations
  344. )
  345. ? layer.annotations.length
  346. : 0;
  347. }
  348.  
  349. return snapshotLayer;
  350. });
  351.  
  352. const snapshot = {
  353. url: window.location.href,
  354.  
  355. dimensions:
  356. Object.prototype
  357. .hasOwnProperty.call(
  358. state,
  359. 'dimensions'
  360. )
  361. ? state.dimensions
  362. : null,
  363.  
  364. position:
  365. Object.prototype
  366. .hasOwnProperty.call(
  367. state,
  368. 'position'
  369. )
  370. ? state.position
  371. : null,
  372.  
  373. crossSectionScale:
  374. Object.prototype
  375. .hasOwnProperty.call(
  376. state,
  377. 'crossSectionScale'
  378. )
  379. ? state.crossSectionScale
  380. : null,
  381.  
  382. projectionScale:
  383. Object.prototype
  384. .hasOwnProperty.call(
  385. state,
  386. 'projectionScale'
  387. )
  388. ? state.projectionScale
  389. : null,
  390.  
  391. projectionDepth:
  392. Object.prototype
  393. .hasOwnProperty.call(
  394. state,
  395. 'projectionDepth'
  396. )
  397. ? state.projectionDepth
  398. : null,
  399.  
  400. projectionOrientation:
  401. Object.prototype
  402. .hasOwnProperty.call(
  403. state,
  404. 'projectionOrientation'
  405. )
  406. ? state.projectionOrientation
  407. : null,
  408.  
  409. layerCount: layers.length,
  410. layers,
  411.  
  412. canvasCount:
  413. document.querySelectorAll(
  414. 'canvas'
  415. ).length,
  416.  
  417. renderedPanelCount:
  418. document.querySelectorAll(
  419. '.neuroglancer-rendered-data-panel'
  420. ).length
  421. };
  422.  
  423. return JSON.stringify(snapshot);
  424. }
  425.  
  426.  
  427. function normalizeSource(source) {
  428. if (
  429. source === undefined ||
  430. source === null
  431. ) {
  432. return null;
  433. }
  434.  
  435. if (typeof source === 'string') {
  436. return source;
  437. }
  438.  
  439. try {
  440. return JSON.stringify(source);
  441. } catch (error) {
  442. return String(source);
  443. }
  444. }
  445.  
  446.  
  447. function isLoginPromptVisible() {
  448. const bodyText =
  449. document.body
  450. ? document.body.innerText
  451. .toLowerCase()
  452. : '';
  453.  
  454. return (
  455. bodyText.includes(
  456. 'login required'
  457. ) ||
  458. bodyText.includes(
  459. 'request login'
  460. )
  461. );
  462. }
  463.  
  464.  
  465. function resetStability() {
  466. lastSnapshot = null;
  467. stableSince = null;
  468. }
  469.  
  470.  
  471. /**
  472. * Triggers the "/" shortcut registered by the Import/export settings script.
  473. */
  474. function triggerExistingImporter() {
  475. const target =
  476. document.body ||
  477. document.documentElement;
  478.  
  479. const event =
  480. new KeyboardEvent(
  481. 'keyup',
  482. {
  483. key: '/',
  484. code: 'Slash',
  485. keyCode: 191,
  486. which: 191,
  487. bubbles: true,
  488. cancelable: true
  489. }
  490. );
  491.  
  492. target.dispatchEvent(event);
  493. }
  494. })();
Advertisement
Add Comment
Please, Sign In to add comment