@abi-software/mapintegratedvuer 1.22.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/{ConnectivityGraph-BBZ3Dgat.js → ConnectivityGraph-CCxk8vcE.js} +6 -6
  2. package/dist/{ContentMixin-B-O_IwPg.js → ContentMixin-Db8nqZax.js} +303 -247
  3. package/dist/{DynamicMarkerMixin-B9iqtP_f.js → DynamicMarkerMixin-LB2RYdSY.js} +1 -1
  4. package/dist/{Flatmap-BoEFI50b.js → Flatmap-CqUzOkdl.js} +5 -5
  5. package/dist/{Iframe-Slj-LQTF.js → Iframe-I-xdZ2FP.js} +2 -2
  6. package/dist/{MultiFlatmap-l6aBn6x3.js → MultiFlatmap-11L97ycJ.js} +6 -6
  7. package/dist/{Plot-CIhEip5b.js → Plot-Bd9RqyVD.js} +2 -2
  8. package/dist/{Scaffold-D5ZGCDvt.js → Scaffold-CySWXQxI.js} +46 -36
  9. package/dist/Simulation-ChUbGudZ.js +32 -0
  10. package/dist/{connectivities-Br3MpoAv.js → connectivities-LtlFwLOp.js} +6399 -5790
  11. package/dist/es-D-YkSGXX.js +436 -0
  12. package/dist/mapintegratedvuer.js +13061 -11664
  13. package/dist/mapintegratedvuer.umd.cjs +191 -190
  14. package/dist/style.css +1 -1
  15. package/package.json +7 -7
  16. package/src/assets/header-icon.scss +9 -3
  17. package/src/components/ContentBar.vue +172 -92
  18. package/src/components/ContentVuer.vue +41 -1
  19. package/src/components/ContextCard.vue +15 -3
  20. package/src/components/DatasetHeader.vue +4 -1
  21. package/src/components/DialogToolbarContent.vue +45 -4
  22. package/src/components/EventBus.js +8 -0
  23. package/src/components/MapContent.vue +85 -31
  24. package/src/components/ResizeSensor.vue +7 -9
  25. package/src/components/ScreenshotPopover.vue +526 -0
  26. package/src/components/SplitDialog.vue +94 -93
  27. package/src/components/SplitFlow.vue +158 -96
  28. package/src/components/scripts/utilities.js +56 -1
  29. package/src/components/viewers/Flatmap.vue +1 -0
  30. package/src/components/viewers/MultiFlatmap.vue +3 -0
  31. package/src/components/viewers/Scaffold.vue +25 -13
  32. package/src/components/viewers/Simulation.vue +23 -1
  33. package/src/components.d.ts +2 -0
  34. package/src/mixins/ContentMixin.js +23 -10
  35. package/src/services/screenshot.js +1186 -0
  36. package/src/stores/connectivities.js +38 -5
  37. package/src/stores/settings.js +18 -0
  38. package/vite.config.js +27 -4
  39. package/dist/Simulation-C5o5DXRb.js +0 -25
@@ -22,12 +22,13 @@
22
22
  <script>
23
23
  import ContentVuer from './ContentVuer.vue';
24
24
  import CustomSplitter from './CustomSplitter.vue';
25
- import EventBus from './EventBus';
25
+ import EventBus, { onEvents } from './EventBus';
26
26
  import { mapStores } from 'pinia';
27
27
  import { useSplitFlowStore } from '../stores/splitFlow';
28
28
  import { useSettingsStore } from '../stores/settings';
29
29
  import { useConnectivitiesStore } from '../stores/connectivities';
30
30
  import { queryPathsByRoute, queryAllConnectedPaths } from '@abi-software/map-utilities';
31
+ import { getLatestFlatmap } from './scripts/utilities.js';
31
32
 
32
33
  export default {
33
34
  name: 'SplitDialog',
@@ -314,13 +315,8 @@ export default {
314
315
  return ids;
315
316
  },
316
317
  getLatestFlatmapUUID: async function (flatmapAPI, id) {
317
- const flatmapResponse = await fetch(flatmapAPI);
318
- const flatmapJson = await flatmapResponse.json();
319
- const latestFlatmap = flatmapJson
320
- .filter((f) => f.id === id)
321
- .sort((a, b) => b.created.localeCompare(a.created))[0];
322
- const flatmapUUID = latestFlatmap.uuid;
323
- return flatmapUUID;
318
+ const latestFlatmap = await getLatestFlatmap(flatmapAPI, id);
319
+ return latestFlatmap.uuid;
324
320
  },
325
321
  parseSearchTerms: function (query) {
326
322
  return query
@@ -587,93 +583,98 @@ export default {
587
583
  },
588
584
  },
589
585
  mounted: function () {
590
- EventBus.on('PaneResize', (payload) => {
591
- this.setStyles(payload.refName, payload.rect);
592
- this.updateFlatmapMinimap();
593
- });
594
- EventBus.on('PaneUnmounted', (payload) => {
595
- this.hidePane(payload.refName);
596
- });
597
- EventBus.on('species-layout-connectivity-update', () => {
598
- this.onSpeciesLayoutConnectivityUpdate();
599
- });
600
- EventBus.on('connectivity-query-filter', (payload) => {
601
- this.connectivityQueryFilter(payload);
602
- });
603
- //The followings are migrated from ContentVuer and its child components to here
604
- EventBus.on('hoverUpdate', (payload) => {
605
- const contents = this.getActiveContents();
606
- contents.forEach((content) => {
607
- content.onHoverUpdate(payload);
608
- });
609
- });
610
- EventBus.on('startHelp', () => {
611
- const contents = this.getActiveContents();
612
- contents.forEach((content) => {
613
- content.onStartHelp();
614
- });
615
- });
616
- EventBus.on('connectivity-item-close', () => {
617
- const contents = this.getActiveContents();
618
- contents.forEach((content) => {
619
- content.onConnectivityItemClose();
620
- });
621
- });
622
- EventBus.on('sidebar-annotation-close', () => {
623
- const contents = this.getActiveContents();
624
- contents.forEach((content) => {
625
- content.onSidebarAnnotationClose();
626
- });
627
- });
628
- EventBus.on('globalViewerSettingsUpdate', () => {
629
- const contents = this.$refs['content'];
630
- contents.forEach((content) => {
631
- content.onGlobalViewerSettingsUpdate();
632
- });
633
- });
634
- EventBus.on('markerUpdate', () => {
635
- const contents = this.$refs['content'];
636
- contents.forEach((content) => {
637
- content.onFlatmapMarkerUpdate();
638
- });
639
- });
640
- EventBus.on('connectivity-hovered', (payload) => {
641
- const contents = this.getActiveContents();
642
- contents.forEach((content) => {
643
- content.onShowConnectivityTooltips(payload);
644
- });
645
- });
646
- EventBus.on('connectivity-source-change', (payload) => {
647
- const contents = this.getActiveContents();
648
- //Use ongoingSource array to make sure, the knowledge
649
- //from each source is only loaded once.
650
- //TODO: This can be improved with using just one viewer to give us
651
- //the new knowlegdge with the species/sources specified which
652
- //will require some UI changes on the sidebar
653
- const ongoingSources = [];
654
- contents.forEach((content) => {
655
- content.onConnectivitySourceChange(payload, ongoingSources);
656
- });
657
- });
658
- EventBus.on('show-connectivity', (payload) => {
659
- const contents = this.getActiveContents();
660
- contents.forEach((content) => {
661
- content.onShowConnectivity(payload);
662
- });
663
- });
664
- EventBus.on('show-reference-connectivities', (payload) => {
665
- const contents = this.getActiveContents();
666
- contents.forEach((content) => {
667
- content.onShowReferenceConnectivity(payload);
668
- });
669
- });
670
- EventBus.on('filter-visibility', (payload) => {
671
- const contents = this.getActiveContents();
672
- contents.forEach((content) => {
673
- content.onFilterVisibility(payload);
674
- });
586
+ this._offBus = onEvents({
587
+ PaneResize: (payload) => {
588
+ this.setStyles(payload.refName, payload.rect);
589
+ this.updateFlatmapMinimap();
590
+ },
591
+ PaneUnmounted: (payload) => {
592
+ this.hidePane(payload.refName);
593
+ },
594
+ 'species-layout-connectivity-update': () => {
595
+ this.onSpeciesLayoutConnectivityUpdate();
596
+ },
597
+ 'connectivity-query-filter': (payload) => {
598
+ this.connectivityQueryFilter(payload);
599
+ },
600
+ //The followings are migrated from ContentVuer and its child components to here
601
+ hoverUpdate: (payload) => {
602
+ const contents = this.getActiveContents();
603
+ contents.forEach((content) => {
604
+ content.onHoverUpdate(payload);
605
+ });
606
+ },
607
+ startHelp: () => {
608
+ const contents = this.getActiveContents();
609
+ contents.forEach((content) => {
610
+ content.onStartHelp();
611
+ });
612
+ },
613
+ 'connectivity-item-close': () => {
614
+ const contents = this.getActiveContents();
615
+ contents.forEach((content) => {
616
+ content.onConnectivityItemClose();
617
+ });
618
+ },
619
+ 'sidebar-annotation-close': () => {
620
+ const contents = this.getActiveContents();
621
+ contents.forEach((content) => {
622
+ content.onSidebarAnnotationClose();
623
+ });
624
+ },
625
+ globalViewerSettingsUpdate: () => {
626
+ const contents = this.$refs['content'];
627
+ contents.forEach((content) => {
628
+ content.onGlobalViewerSettingsUpdate();
629
+ });
630
+ },
631
+ markerUpdate: () => {
632
+ const contents = this.$refs['content'];
633
+ contents.forEach((content) => {
634
+ content.onFlatmapMarkerUpdate();
635
+ });
636
+ },
637
+ 'connectivity-hovered': (payload) => {
638
+ const contents = this.getActiveContents();
639
+ contents.forEach((content) => {
640
+ content.onShowConnectivityTooltips(payload);
641
+ });
642
+ },
643
+ 'connectivity-source-change': (payload) => {
644
+ const contents = this.getActiveContents();
645
+ //Use ongoingSource array to make sure, the knowledge
646
+ //from each source is only loaded once.
647
+ //TODO: This can be improved with using just one viewer to give us
648
+ //the new knowlegdge with the species/sources specified which
649
+ //will require some UI changes on the sidebar
650
+ const ongoingSources = [];
651
+ contents.forEach((content) => {
652
+ content.onConnectivitySourceChange(payload, ongoingSources);
653
+ });
654
+ },
655
+ 'show-connectivity': (payload) => {
656
+ const contents = this.getActiveContents();
657
+ contents.forEach((content) => {
658
+ content.onShowConnectivity(payload);
659
+ });
660
+ },
661
+ 'show-reference-connectivities': (payload) => {
662
+ const contents = this.getActiveContents();
663
+ contents.forEach((content) => {
664
+ content.onShowReferenceConnectivity(payload);
665
+ });
666
+ },
667
+ 'filter-visibility': (payload) => {
668
+ const contents = this.getActiveContents();
669
+ contents.forEach((content) => {
670
+ content.onFilterVisibility(payload);
671
+ });
672
+ },
675
673
  });
676
674
  },
675
+ beforeUnmount: function () {
676
+ this._offBus?.();
677
+ },
677
678
  };
678
679
  </script>
679
680
  <!-- Add "scoped" attribute to limit CSS to this component only -->
@@ -11,11 +11,16 @@
11
11
  @onFullscreen="onFullscreen"
12
12
  @local-search="onDisplaySearch"
13
13
  @fetch-suggestions="fetchSuggestions"
14
+ :captureScreenshot="captureScreenshot"
15
+ :getScreenshotTarget="getScreenshotElement"
14
16
  ref="dialogToolbar"
15
17
  />
16
18
  </el-header>
17
19
  <el-main class="dialog-main">
18
- <div style="width: 100%; height: 100%; position: relative; overflow: hidden">
20
+ <div
21
+ ref="captureArea"
22
+ style="width: 100%; height: 100%; position: relative; overflow: hidden"
23
+ >
19
24
  <SideBar
20
25
  ref="sideBar"
21
26
  :envVars="envVars"
@@ -67,7 +72,8 @@
67
72
  import { provide, markRaw, computed } from 'vue';
68
73
  import Tagging from '../services/tagging.js';
69
74
  import DialogToolbarContent from './DialogToolbarContent.vue';
70
- import EventBus from './EventBus';
75
+ import { captureElement, ScreenshotError } from '../services/screenshot';
76
+ import EventBus, { onEvents } from './EventBus';
71
77
  import SplitDialog from './SplitDialog.vue';
72
78
  // import contextCards from './context-cards'
73
79
  import { SideBar } from '@abi-software/map-side-bar';
@@ -887,6 +893,56 @@ export default {
887
893
  onFullscreen: function (val) {
888
894
  this.$emit('onFullscreen', val);
889
895
  },
896
+ /**
897
+ * Capture a screenshot and download it.
898
+ * Captures the pane with `paneId` if provided, otherwise all
899
+ * visible panes including the sidebar.
900
+ * With `legend` set to `only`, the legend of each pane is downloaded as a separate image.
901
+ * @param {Object} options - { paneId, scale, legend, toolbar }
902
+ */
903
+ captureScreenshot: async function (options = {}) {
904
+ const { paneId } = options;
905
+ const contents = this.$refs.splitdialog?.getActiveContents() || [];
906
+ const scale = options.scale || this.settingsStore.screenshot.scale;
907
+ const legend = options.legend || this.settingsStore.screenshot.legend;
908
+ const toolbar = options.toolbar ?? this.settingsStore.screenshot.toolbar;
909
+
910
+ if (paneId !== undefined) {
911
+ const content = this.$refs.splitdialog?.getContentsWithId(paneId);
912
+
913
+ if (!content) {
914
+ return Promise.reject(new Error(`Pane ${paneId} not found`));
915
+ }
916
+
917
+ return content.captureScreenshot(options);
918
+ }
919
+
920
+ if (legend === 'only') {
921
+ const withLegend = contents.filter((content) => content.hasLegend?.());
922
+ const results = [];
923
+
924
+ if (!withLegend.length) {
925
+ throw new ScreenshotError('no-legend', 'No legend to capture');
926
+ }
927
+
928
+ for (const content of withLegend) {
929
+ results.push(...(await content.captureScreenshot({ ...options, scale, legend })));
930
+ }
931
+
932
+ return results;
933
+ }
934
+
935
+ return captureElement(this.$refs.captureArea, {
936
+ scale,
937
+ legend,
938
+ toolbar,
939
+ title: 'maps-view',
940
+ viewers: contents,
941
+ });
942
+ },
943
+ getScreenshotElement: function () {
944
+ return this.$refs.captureArea;
945
+ },
890
946
  resetApp: function () {
891
947
  this.setState(initialDefaultState());
892
948
  },
@@ -1166,117 +1222,123 @@ export default {
1166
1222
  this._externalStateSet = false;
1167
1223
  },
1168
1224
  mounted: function () {
1169
- EventBus.on('CreateNewEntry', (newView) => {
1170
- this.createNewEntry(newView);
1171
- });
1172
- EventBus.on('RemoveEntryRequest', (id) => {
1173
- this.removeEntry(id);
1174
- });
1175
- EventBus.on('PopoverActionClick', (payload) => {
1176
- this.actionClick(payload);
1177
- });
1178
- EventBus.on('annotation-open', (payload) => {
1179
- this.openAnnotation(payload);
1180
- });
1181
- EventBus.on('sidebar-annotation-close', () => {
1182
- const globalSettings = { ...this.settingsStore.globalSettings };
1183
- const { interactiveMode, viewingMode } = globalSettings;
1225
+ this._offBus = onEvents({
1226
+ CreateNewEntry: (newView) => {
1227
+ this.createNewEntry(newView);
1228
+ },
1229
+ RemoveEntryRequest: (id) => {
1230
+ this.removeEntry(id);
1231
+ },
1232
+ PopoverActionClick: (payload) => {
1233
+ this.actionClick(payload);
1234
+ },
1235
+ 'annotation-open': (payload) => {
1236
+ this.openAnnotation(payload);
1237
+ },
1238
+ 'sidebar-annotation-close': () => {
1239
+ const globalSettings = { ...this.settingsStore.globalSettings };
1240
+ const { interactiveMode, viewingMode } = globalSettings;
1184
1241
 
1185
- // Sidebar annotation close event emits whenever viewing mode is changed.
1186
- // if annotation state is being restored on first load for annotation viewing mode,
1187
- // open the anootation tab and return.
1188
- if (this.sidebarAnnotationState && viewingMode === 'Annotation') {
1189
- this.sidebarAnnotationState = false;
1190
- this.$refs.sideBar.tabClicked({ id: 3, type: 'annotation' });
1191
- return;
1192
- }
1242
+ // Sidebar annotation close event emits whenever viewing mode is changed.
1243
+ // if annotation state is being restored on first load for annotation viewing mode,
1244
+ // open the anootation tab and return.
1245
+ if (this.sidebarAnnotationState && viewingMode === 'Annotation') {
1246
+ this.sidebarAnnotationState = false;
1247
+ this.$refs.sideBar.tabClicked({ id: 3, type: 'annotation' });
1248
+ return;
1249
+ }
1193
1250
 
1194
- this.annotationEntry = [];
1195
- this.createData = {};
1251
+ this.annotationEntry = [];
1252
+ this.createData = {};
1196
1253
 
1197
- if (this.$refs.sideBar) {
1198
- if (interactiveMode === 'dataset') {
1199
- this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
1200
- } else if (interactiveMode === 'connectivity') {
1254
+ if (this.$refs.sideBar) {
1255
+ if (interactiveMode === 'dataset') {
1256
+ this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
1257
+ } else if (interactiveMode === 'connectivity') {
1258
+ this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1259
+ }
1260
+
1261
+ this.$refs.sideBar.closeConnectivity();
1262
+ EventBus.emit('connectivity-item-close');
1263
+ }
1264
+ },
1265
+ 'update-offline-annotation-enabled': (payload) => {
1266
+ this.settingsStore.updateOfflineAnnotationEnabled(payload);
1267
+ },
1268
+ 'connectivity-info-open': (payload) => {
1269
+ this.openConnectivityInfo(payload);
1270
+ },
1271
+ 'connectivity-info-close': (_payload) => {
1272
+ if (this.$refs.sideBar) {
1273
+ this.connectivityProcessed = false;
1274
+ this.$refs.sideBar.resetConnectivitySearch();
1275
+ }
1276
+ },
1277
+ 'connectivity-error': (payload) => {
1278
+ if (this.$refs.sideBar) {
1279
+ this.$refs.sideBar.updateConnectivityError(payload.data);
1280
+ }
1281
+ },
1282
+ 'neuron-connection-feature-click': (payload) => {
1283
+ if (this.$refs.sideBar) {
1284
+ const { filters, search } = payload;
1285
+ this.$refs.sideBar.openConnectivitySearch(filters, search);
1201
1286
  this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1287
+ this.$refs.sideBar.setDrawerOpen(true);
1288
+ }
1289
+ },
1290
+ OpenNewMap: (type) => {
1291
+ this.updateFlatmapMinimap();
1292
+ this.openNewMap(type);
1293
+ },
1294
+ startHelp: () => {
1295
+ if (this.$refs.sideBar) {
1296
+ this.$refs.sideBar.close();
1297
+ }
1298
+ },
1299
+ 'connectivity-knowledge': (payload) => {
1300
+ // Ignore the default/global connectivity dump (emitted when no active
1301
+ // viewer's map/species keys are resolved yet) while restoring a
1302
+ // permalink's sidebar connectivityEntries, otherwise the sidebar would
1303
+ // briefly flash the entire, unfiltered connectivity list.
1304
+ if (!(payload.isDefaultKnowledge && this.restoringConnectivityState)) {
1305
+ this.connectivityKnowledge = payload.data;
1306
+ this.connectivityHighlight = payload.highlight;
1307
+ this.connectivityProcessed = payload.processed;
1202
1308
  }
1203
1309
 
1204
- this.$refs.sideBar.closeConnectivity();
1205
- EventBus.emit('connectivity-item-close');
1206
- }
1207
- });
1208
- EventBus.on('update-offline-annotation-enabled', (payload) => {
1209
- this.settingsStore.updateOfflineAnnotationEnabled(payload);
1210
- });
1211
- EventBus.on('connectivity-info-open', (payload) => {
1212
- this.openConnectivityInfo(payload);
1213
- });
1214
- EventBus.on('connectivity-info-close', (_payload) => {
1215
- if (this.$refs.sideBar) {
1216
- this.connectivityProcessed = false;
1217
- this.$refs.sideBar.resetConnectivitySearch();
1218
- }
1219
- });
1220
- EventBus.on('connectivity-error', (payload) => {
1221
- if (this.$refs.sideBar) {
1222
- this.$refs.sideBar.updateConnectivityError(payload.data);
1223
- }
1224
- });
1225
- EventBus.on('neuron-connection-feature-click', (payload) => {
1226
- if (this.$refs.sideBar) {
1227
- const { filters, search } = payload;
1228
- this.$refs.sideBar.openConnectivitySearch(filters, search);
1229
- this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1230
- this.$refs.sideBar.setDrawerOpen(true);
1231
- }
1232
- });
1233
- EventBus.on('OpenNewMap', (type) => {
1234
- this.updateFlatmapMinimap();
1235
- this.openNewMap(type);
1236
- });
1237
- EventBus.on('startHelp', () => {
1238
- if (this.$refs.sideBar) {
1239
- this.$refs.sideBar.close();
1240
- }
1241
- });
1242
- EventBus.on('connectivity-knowledge', (payload) => {
1243
- // Ignore the default/global connectivity dump (emitted when no active
1244
- // viewer's map/species keys are resolved yet) while restoring a
1245
- // permalink's sidebar connectivityEntries, otherwise the sidebar would
1246
- // briefly flash the entire, unfiltered connectivity list.
1247
- if (!(payload.isDefaultKnowledge && this.restoringConnectivityState)) {
1248
- this.connectivityKnowledge = payload.data;
1249
- this.connectivityHighlight = payload.highlight;
1250
- this.connectivityProcessed = payload.processed;
1251
- }
1252
-
1253
- // Restore sidebar state if it exists and not restored yet
1254
- // after loading connectivity knowledge
1255
- if (!this.sidebarStateRestored) {
1256
- this.restoreSidebarState(this.state);
1257
- }
1258
- });
1259
- EventBus.on('modeUpdate', (payload) => {
1260
- if (payload === 'dataset') {
1261
- this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
1262
- } else if (payload === 'connectivity') {
1263
- this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1264
- }
1265
- });
1266
- EventBus.on('connectivity-filter-options', (payload) => {
1267
- this.filterOptions = payload;
1310
+ // Restore sidebar state if it exists and not restored yet
1311
+ // after loading connectivity knowledge
1312
+ if (!this.sidebarStateRestored) {
1313
+ this.restoreSidebarState(this.state);
1314
+ }
1315
+ },
1316
+ modeUpdate: (payload) => {
1317
+ if (payload === 'dataset') {
1318
+ this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
1319
+ } else if (payload === 'connectivity') {
1320
+ this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1321
+ }
1322
+ },
1323
+ 'connectivity-filter-options': (payload) => {
1324
+ this.filterOptions = payload;
1325
+ },
1268
1326
  });
1269
1327
  this.$nextTick(() => {
1270
1328
  if (this.search === '' && this._facets.length === 0) {
1271
1329
  if (this.$refs.sideBar) {
1272
1330
  this.$refs.sideBar.close();
1273
1331
  }
1274
- setTimeout(() => {
1332
+ this._startUpTimer = setTimeout(() => {
1275
1333
  this.startUp = false;
1276
1334
  }, 2000);
1277
1335
  } else this.openSearch(this._facets, this.search);
1278
1336
  });
1279
1337
  },
1338
+ beforeUnmount: function () {
1339
+ this._offBus?.();
1340
+ clearTimeout(this._startUpTimer);
1341
+ },
1280
1342
  computed: {
1281
1343
  ...mapStores(useEntriesStore, useSettingsStore, useSplitFlowStore, useConnectivitiesStore),
1282
1344
  envVars: function () {
@@ -1,5 +1,46 @@
1
+ import { markRaw } from 'vue';
2
+
1
3
  const defaultSpecies = 'Human Male';
2
4
 
5
+ // Session-level caches, so viewers and searches share one request instead of
6
+ // re-fetching the same data.
7
+ const flatmapListCache = new Map();
8
+ const flatmapPathwaysCache = new Map();
9
+ const bodyScaffoldInfoCache = new Map();
10
+
11
+ // Cache the promise so concurrent callers share a single request.
12
+ // Failed requests are dropped so a later call can retry.
13
+ const cachedRequest = (cache, key, load) => {
14
+ if (!cache.has(key)) {
15
+ const promise = load().catch((error) => {
16
+ cache.delete(key);
17
+ throw error;
18
+ });
19
+ cache.set(key, promise);
20
+ }
21
+ return cache.get(key);
22
+ };
23
+
24
+ const fetchJson = async (url) => {
25
+ const response = await fetch(url);
26
+ if (!response.ok) {
27
+ throw new Error(`Request failed (${response.status}): ${url}`);
28
+ }
29
+ return response.json();
30
+ };
31
+
32
+ // Latest version of the flatmap with the given id, e.g. 'human-flatmap_male'
33
+ const getLatestFlatmap = async (flatmapAPI, id) => {
34
+ const flatmaps = await cachedRequest(flatmapListCache, flatmapAPI, () => fetchJson(flatmapAPI));
35
+ return flatmaps.filter((f) => f.id === id).sort((a, b) => b.created.localeCompare(a.created))[0];
36
+ };
37
+
38
+ // Pathways are large and read-only, so they are shared and kept non-reactive
39
+ const getFlatmapPathways = (flatmapAPI, uuid) => {
40
+ const url = `${flatmapAPI}/flatmap/${uuid}/pathways`;
41
+ return cachedRequest(flatmapPathwaysCache, url, () => fetchJson(url).then(markRaw));
42
+ };
43
+
3
44
  const initialDefaultState = () => {
4
45
  return {
5
46
  entries: [
@@ -174,7 +215,19 @@ const extractS3BucketNameAndPrefix = (uri) => {
174
215
  return undefined;
175
216
  };
176
217
 
177
- const getBodyScaffoldInfo = async (sparcApi, species) => {
218
+ const getBodyScaffoldInfo = (sparcApi, species) => {
219
+ const key = `${sparcApi}|${species}`;
220
+ return cachedRequest(bodyScaffoldInfoCache, key, async () => {
221
+ const info = await fetchBodyScaffoldInfo(sparcApi, species);
222
+ // Do not keep the fallback url, so a later call can retry the API
223
+ if (!info.datasetInfo) {
224
+ bodyScaffoldInfoCache.delete(key);
225
+ }
226
+ return info;
227
+ });
228
+ };
229
+
230
+ const fetchBodyScaffoldInfo = async (sparcApi, species) => {
178
231
  //Get body scaffold information
179
232
  let url = '';
180
233
  let datasetInfo = undefined;
@@ -243,6 +296,8 @@ export {
243
296
  initialState,
244
297
  initialDefaultState,
245
298
  getBodyScaffoldInfo,
299
+ getLatestFlatmap,
300
+ getFlatmapPathways,
246
301
  getNewMapEntry,
247
302
  intersectArrays,
248
303
  transformObjToString,
@@ -100,6 +100,7 @@ export default {
100
100
  },
101
101
  flatmapReadyCall: function (flatmap) {
102
102
  this.flatmapReady = true;
103
+ this.$emit('viewer-ready', true);
103
104
  const mapImp = this.getFlatmapImp();
104
105
  if (mapImp?.mapMetadata?.name) {
105
106
  this.updateEntryLabel(mapImp?.mapMetadata?.name);
@@ -228,6 +228,8 @@ export default {
228
228
  flatmapChanged: async function (activeSpecies) {
229
229
  this.activeSpecies = activeSpecies;
230
230
  this.openMapOptions = getOpenMapOptions(activeSpecies);
231
+ // The selected species' map may still be loading, multiFlatmapReady will set it ready
232
+ this.$emit('viewer-ready', !!this.$refs.multiflatmap?.getCurrentFlatmap()?.mapImp);
231
233
  const flatmapImp = this.getFlatmapImp();
232
234
  this.updateProvCard();
233
235
  //If the viewer is loading a new map, flatmapImp is not defined here yet.
@@ -254,6 +256,7 @@ export default {
254
256
  if (flatmap) {
255
257
  flatmap.enablePanZoomEvents(true); // Use zoom events for dynamic markers
256
258
  this.flatmapReady = true;
259
+ this.$emit('viewer-ready', true);
257
260
  const flatmapImp = flatmap.mapImp;
258
261
  this.flatmapMarkerUpdate(flatmapImp);
259
262
  this.updateProvCard();