@abi-software/mapintegratedvuer 1.21.2 → 1.23.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.
package/eslint.config.js CHANGED
@@ -12,7 +12,7 @@ const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
12
12
  export default [
13
13
  includeIgnoreFile(gitignorePath, { gitignoreResolution: true }),
14
14
  {
15
- ignores: ['dist/**', 'docs/.vitepress/**', 'docs/components/**', 'test/**'],
15
+ ignores: ['dist/**', 'docs/.vitepress/**', 'docs/components/**', 'test/**', 'CHANGELOG.md'],
16
16
  },
17
17
  js.configs.recommended,
18
18
  ...vue.configs['flat/essential'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abi-software/mapintegratedvuer",
3
- "version": "1.21.2",
3
+ "version": "1.23.0",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": "^24.20.0"
@@ -58,12 +58,12 @@
58
58
  "*.js"
59
59
  ],
60
60
  "dependencies": {
61
- "@abi-software/flatmapvuer": "1.17.0",
62
- "@abi-software/map-side-bar": "2.19.0",
63
- "@abi-software/map-utilities": "1.12.0",
61
+ "@abi-software/flatmapvuer": "1.20.0",
62
+ "@abi-software/map-side-bar": "2.20.0",
63
+ "@abi-software/map-utilities": "1.13.0",
64
64
  "@abi-software/plotvuer": "1.3.0",
65
- "@abi-software/scaffoldvuer": "1.21.0",
66
- "@abi-software/simulationvuer": "4.3.0",
65
+ "@abi-software/scaffoldvuer": "2.1.0",
66
+ "@abi-software/simulationvuer": "4.4.0",
67
67
  "@abi-software/sparc-annotation": "0.3.2",
68
68
  "@abi-software/svg-sprite": "1.4.0",
69
69
  "@element-plus/icons-vue": "^2.3.2",
package/src/App.vue CHANGED
@@ -30,29 +30,12 @@
30
30
  <el-button @click="setFlatmap()" size="small">Set Flatmap</el-button>
31
31
  <el-button @click="setSearch()" size="small">Set Search</el-button>
32
32
  </div>
33
- <div class="row">
34
- <div style="font-weight: 600">Connectivity tooltip labels:</div>
35
- </div>
36
- <div class="row">
37
- <el-checkbox @change="onShowLongLabelChange" v-model="showLongLabel" size="small">
38
- Show long label
39
- </el-checkbox>
40
- <el-checkbox
41
- @change="onTruncateLongLabelChange"
42
- :disabled="!showLongLabel"
43
- v-model="truncateLongLabel"
44
- size="small"
45
- >
46
- Truncate long label
47
- </el-checkbox>
48
- <el-checkbox
49
- @change="onShowIdInTooltipChange"
50
- :disabled="!showLongLabel"
51
- v-model="showIdInTooltip"
52
- size="small"
53
- >
54
- Show ID in tooltip
55
- </el-checkbox>
33
+ <div class="row radio-row">
34
+ <span class="radio-label">Open new species in:</span>
35
+ <el-radio-group v-model="openNewSpeciesInSplitView" size="small">
36
+ <el-radio :value="false">Current view</el-radio>
37
+ <el-radio :value="true">Split view</el-radio>
38
+ </el-radio-group>
56
39
  </div>
57
40
  </div>
58
41
  <template #reference>
@@ -76,6 +59,7 @@
76
59
  :allClosable="false"
77
60
  :showGlobalSettings="true"
78
61
  :showOpenMapButton="true"
62
+ :openNewSpeciesInSplitView="openNewSpeciesInSplitView"
79
63
  @updateShareLinkRequested="updateUUID"
80
64
  @isReady="viewerIsReady"
81
65
  @mapLoaded="mapIsLoaded"
@@ -183,6 +167,7 @@ export default {
183
167
  showLongLabel: true,
184
168
  truncateLongLabel: true,
185
169
  showIdInTooltip: true,
170
+ openNewSpeciesInSplitView: true,
186
171
  };
187
172
  },
188
173
  computed: {
@@ -334,15 +319,6 @@ export default {
334
319
  this.$refs.map.openSearch([], '10.26275/1uno-tynt');
335
320
  this.closePopover();
336
321
  },
337
- onShowLongLabelChange: function () {
338
- this.closePopover();
339
- },
340
- onTruncateLongLabelChange: function () {
341
- this.closePopover();
342
- },
343
- onShowIdInTooltipChange: function () {
344
- this.closePopover();
345
- },
346
322
  mapIsLoaded: function (map) {
347
323
  console.log('map is loaded', map);
348
324
  // map.changeViewingMode('Annotation')
@@ -573,6 +549,14 @@ body {
573
549
  text-align: center;
574
550
  }
575
551
 
552
+ .radio-row {
553
+ align-items: center;
554
+ }
555
+
556
+ .radio-label {
557
+ font-size: 12px;
558
+ }
559
+
576
560
  .map-icon {
577
561
  color: $app-primary-color !important;
578
562
  }
@@ -434,6 +434,7 @@ export default {
434
434
  border-color: $app-primary-color;
435
435
  color: #fff;
436
436
  font-size: 11px !important;
437
+
437
438
  &:hover {
438
439
  color: #fff !important;
439
440
  background-color: #ac76c5 !important;
@@ -7,7 +7,7 @@
7
7
  class="context-card"
8
8
  >
9
9
  <div class="card-left">
10
- <img :src="banner" class="context-image" />
10
+ <img :crossorigin="crossorigin" :src="banner" class="context-image" />
11
11
  </div>
12
12
  <div class="card-right scrollbar">
13
13
  <div>
@@ -54,7 +54,11 @@
54
54
  </div>
55
55
  <template v-for="(view, i) in contextData.views" :key="i + '_1'">
56
56
  <div @click="openViewFile(view)" class="context-card-view">
57
- <img class="view-image" :src="getFileFromPath(view.thumbnail)" />
57
+ <img
58
+ :crossorigin="crossorigin"
59
+ class="view-image"
60
+ :src="getFileFromPath(view.thumbnail)"
61
+ />
58
62
  <div class="view-description">{{ view.description }}</div>
59
63
  </div>
60
64
  <div class="padding" />
@@ -72,6 +76,7 @@
72
76
  :style="'background-color:' + sample.color"
73
77
  ></div>
74
78
  <img
79
+ :crossorigin="crossorigin"
75
80
  class="key-image"
76
81
  v-else-if="sample.thumbnail"
77
82
  :src="getFileFromPath(sample.thumbnail)"
@@ -99,7 +104,11 @@
99
104
  </div>
100
105
  <template v-for="(view, i) in contextData.views" :key="i + '_1'">
101
106
  <span @click="viewClicked(view, i)" class="context-card-view">
102
- <img class="view-image" :src="getFileFromPath(view.thumbnail)" />
107
+ <img
108
+ :crossorigin="crossorigin"
109
+ class="view-image"
110
+ :src="getFileFromPath(view.thumbnail)"
111
+ />
103
112
  <div class="view-description">
104
113
  {{ view.description }}
105
114
  <i class="el-icon-warning-outline info"></i>
@@ -171,6 +180,9 @@ export default {
171
180
  CopyToClipboard,
172
181
  },
173
182
  mixins: [S3Bucket],
183
+ inject: {
184
+ crossorigin: { default: 'anonymous' },
185
+ },
174
186
  props: {
175
187
  /**
176
188
  * Object containing information for
@@ -18,7 +18,7 @@
18
18
  class="dataset-card"
19
19
  ref="card"
20
20
  >
21
- <img :src="entry.datasetImage" class="image" />
21
+ <img :crossorigin="crossorigin" :src="entry.datasetImage" class="image" />
22
22
  <div style="padding: 14px">
23
23
  <span class="dataset-description">{{ entry.datasetDescription }}</span>
24
24
  <div>
@@ -33,6 +33,9 @@
33
33
  <script>
34
34
  export default {
35
35
  name: 'DatasetHeader',
36
+ inject: {
37
+ crossorigin: { default: 'anonymous' },
38
+ },
36
39
  props: {
37
40
  /**
38
41
  * Object containing information for
@@ -132,9 +132,27 @@ export default {
132
132
  type: Boolean,
133
133
  default: true,
134
134
  },
135
+ /**
136
+ * The option to open a species from the species dropdown's "open new view"
137
+ * in a split view. From a single view, it opens as a vertical split on the right.
138
+ * From a split view, it replaces the other pane's view.
139
+ * Default is `true`.
140
+ */
141
+ openNewSpeciesInSplitView: {
142
+ type: Boolean,
143
+ default: true,
144
+ },
145
+ /**
146
+ * The `crossorigin` attribute value used for images.
147
+ */
148
+ crossorigin: {
149
+ type: String,
150
+ default: 'anonymous',
151
+ },
135
152
  },
136
153
  provide: function () {
137
154
  return {
155
+ crossorigin: computed(() => this.crossorigin),
138
156
  showGlobalSettings: this.showGlobalSettings,
139
157
  showOpenMapButton: this.showOpenMapButton,
140
158
  truncateLongLabel: computed(() => this.truncateLongLabel),
@@ -308,7 +326,11 @@ export default {
308
326
  discoverId: state.dataset_id,
309
327
  version: state.dataset_version,
310
328
  };
311
- this.$refs.flow.createNewEntry(newView);
329
+ // sourceId is only provided by "open new view" from the species dropdown
330
+ this.$refs.flow.createNewEntry(newView, {
331
+ openInSplitView: this.openNewSpeciesInSplitView && state.sourceId !== undefined,
332
+ sourceId: state.sourceId,
333
+ });
312
334
  } else {
313
335
  //State for multiflatmap containing the following items:
314
336
  // taxo - taxo of species to set
@@ -466,6 +488,12 @@ export default {
466
488
  */
467
489
  this.$emit('mapLoaded', map);
468
490
  });
491
+ // Allow other components to request opening a map via setCurrentEntry
492
+ EventBus.on('SetCurrentEntry', (entry) => {
493
+ if (entry) {
494
+ this.setCurrentEntry(entry);
495
+ }
496
+ });
469
497
  this.isReady = true;
470
498
  this.settingsStore.updateUseHelpModeDialog(this.useHelpModeDialog);
471
499
  this.settingsStore.updateConnectivityInfoSidebar(this.connectivityInfoSidebar);
@@ -835,8 +835,10 @@ export default {
835
835
  /**
836
836
  * Add new entry which will sequentially create a
837
837
  * new dialog.
838
+ * Set `options.openInSplitView` with `options.sourceId` to open the
839
+ * new entry next to the source entry instead of the primary pane.
838
840
  */
839
- createNewEntry: function (data) {
841
+ createNewEntry: function (data, options = {}) {
840
842
  let newEntry = {};
841
843
  newEntry.viewUrl = undefined;
842
844
  newEntry.state = undefined;
@@ -845,7 +847,15 @@ export default {
845
847
  newEntry.id = this.getNewEntryId();
846
848
  newEntry.discoverId = data.discoverId;
847
849
  this.entriesStore.addNewEntry(newEntry);
848
- this.splitFlowStore.setIdToPrimaryPane(newEntry.id);
850
+ if (options.openInSplitView) {
851
+ this.splitFlowStore.placeEntryInSplitView({
852
+ id: newEntry.id,
853
+ sourceId: options.sourceId,
854
+ entries: this.entries,
855
+ });
856
+ } else {
857
+ this.splitFlowStore.setIdToPrimaryPane(newEntry.id);
858
+ }
849
859
  //close sidebar on entry creation to see the context card
850
860
  if (this.$refs.sideBar) {
851
861
  this.$refs.sideBar.setDrawerOpen(false);
@@ -37,6 +37,7 @@
37
37
  :showOpenMapButton="showOpenMapButton"
38
38
  @pan-zoom-callback="flatmapPanZoomCallback"
39
39
  @open-map="openMap"
40
+ @open-map-species="openMapSpecies"
40
41
  @finish-help-mode="endHelp"
41
42
  @pathway-selection-changed="onPathwaySelectionChanged"
42
43
  @open-pubmed-url="onOpenPubmedUrl"
@@ -135,6 +136,19 @@ export default {
135
136
  //First search and show the result
136
137
  return flatmap.searchAndShowResult(term, true, true);
137
138
  },
139
+ // Open the given species as a new view.
140
+ openMapSpecies: function (species) {
141
+ EventBus.emit('SetCurrentEntry', {
142
+ resource: species,
143
+ type: 'MultiFlatmap',
144
+ mode: 'main',
145
+ state: undefined,
146
+ label: '',
147
+ discoverId: undefined,
148
+ sourceId: this.entry.id,
149
+ });
150
+ this.trackOpenMap(`open_new_AC_map_${species}`);
151
+ },
138
152
  /**
139
153
  * Append the list of suggested terms to suggestions
140
154
  */
@@ -1,5 +1,14 @@
1
1
  <template>
2
- <SimulationVuer :apiLocation="apiLocation" :id="id" ref="simulation" />
2
+ <SimulationVuer
3
+ v-if="isCrossOriginIsolated"
4
+ :apiLocation="apiLocation"
5
+ :id="id"
6
+ ref="simulation"
7
+ />
8
+ <div v-else class="simulation-unsupported">
9
+ Simulations require a cross-origin isolated page (for SharedArrayBuffer support). This browser
10
+ or host is not configured for it.
11
+ </div>
3
12
  </template>
4
13
 
5
14
  <script>
@@ -13,6 +22,13 @@ export default {
13
22
  components: {
14
23
  SimulationVuer,
15
24
  },
25
+ data: function () {
26
+ return {
27
+ // libOpenCOR (wasm) uses threads, which need SharedArrayBuffer and therefore COOP/COEP headers.
28
+ // Without them it aborts on Safari.
29
+ isCrossOriginIsolated: window.crossOriginIsolated === true,
30
+ };
31
+ },
16
32
  computed: {
17
33
  id: function () {
18
34
  //resource field is only available for simulation omex file and it will run locally.
@@ -22,3 +38,9 @@ export default {
22
38
  },
23
39
  };
24
40
  </script>
41
+
42
+ <style scoped lang="scss">
43
+ .simulation-unsupported {
44
+ padding: 1rem;
45
+ }
46
+ </style>
@@ -349,6 +349,37 @@ export const useSplitFlowStore = defineStore('splitFlow', {
349
349
  this.customLayout[currentKey].id = firstPaneId;
350
350
  }
351
351
  },
352
+ /**
353
+ * Place a newly created entry alongside the source entry.
354
+ * Single view: switch to vertical split with the new entry on the right.
355
+ * Multiple views: replace the other pane (pane-2 if the source is in
356
+ * pane-1, otherwise pane-1). The replaced entry remains in the entries list.
357
+ * Falls back to the primary pane if the source is not currently visible.
358
+ */
359
+ placeEntryInSplitView(payload) {
360
+ const { id, sourceId, entries } = payload;
361
+ if (this.activeView === 'singlepanel') {
362
+ const sourceKey = findKeyWithId(this.customLayout, sourceId);
363
+ if (sourceKey !== 'pane-1') {
364
+ this.setIdToPrimaryPane(id);
365
+ return;
366
+ }
367
+ this.updateActiveView({ view: '2vertpanel', entries }, false);
368
+ this.assignOrSwapPaneWithIds({ source: id, target: this.customLayout['pane-2'].id }, false);
369
+ } else {
370
+ const sourceKey = findKeyWithId(this.customLayout, sourceId);
371
+ if (!sourceKey || !this.isPaneActive(sourceKey)) {
372
+ this.setIdToPrimaryPane(id);
373
+ return;
374
+ }
375
+ const targetKey = sourceKey === 'pane-1' ? 'pane-2' : 'pane-1';
376
+ this.assignOrSwapPaneWithIds(
377
+ { source: id, target: this.customLayout[targetKey].id },
378
+ false,
379
+ );
380
+ }
381
+ this.updateSplitPanels();
382
+ },
352
383
  reset() {
353
384
  const original = getOriginalState();
354
385
  this.activeView = original.activeView;
package/vite.config.js CHANGED
@@ -9,6 +9,32 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const pathSrc = path.resolve(__dirname, './src');
11
11
 
12
+ // SimulationVuer (libOpenCOR wasm) needs SharedArrayBuffer, which requires cross-origin isolation.
13
+ // Safari/WebKit does not support COEP `credentialless`,
14
+ // so serve `require-corp` to WebKit browsers and `credentialless` to the rest.
15
+ const isWebKitOnly = (ua = '') => ua.includes('AppleWebKit') && !/(Chrome|Chromium|Edg)\//.test(ua);
16
+
17
+ const crossOriginIsolation = () => {
18
+ const middleware = (req, res, next) => {
19
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
20
+ res.setHeader(
21
+ 'Cross-Origin-Embedder-Policy',
22
+ isWebKitOnly(req.headers['user-agent']) ? 'require-corp' : 'credentialless',
23
+ );
24
+ res.setHeader('Vary', 'User-Agent');
25
+ next();
26
+ };
27
+ return {
28
+ name: 'cross-origin-isolation',
29
+ configureServer: (server) => {
30
+ server.middlewares.use(middleware);
31
+ },
32
+ configurePreviewServer: (server) => {
33
+ server.middlewares.use(middleware);
34
+ },
35
+ };
36
+ };
37
+
12
38
  // https://vitejs.dev/config/
13
39
  export default defineConfig(({ command }) => {
14
40
  const config = {
@@ -53,11 +79,8 @@ export default defineConfig(({ command }) => {
53
79
  if (command === 'serve') {
54
80
  config.server = {
55
81
  port: 8081,
56
- headers: {
57
- 'Cross-Origin-Opener-Policy': 'same-origin',
58
- 'Cross-Origin-Embedder-Policy': 'credentialless',
59
- },
60
82
  };
83
+ config.plugins.push(crossOriginIsolation());
61
84
  config.define = {
62
85
  'process.env.HTTP_PROXY': 8081,
63
86
  global: 'globalThis',
@@ -1,25 +0,0 @@
1
- import { _ as e } from "./connectivities-DyeS8w0c.js";
2
- import { t } from "./ContentMixin-Xg0PjBqG.js";
3
- import { createBlock as n, openBlock as r, resolveComponent as i } from "vue";
4
- import { SimulationVuer as a } from "@abi-software/simulationvuer";
5
- import "@abi-software/simulationvuer/dist/style.css";
6
- //#region src/components/viewers/Simulation.vue
7
- var o = {
8
- name: "Simulation",
9
- mixins: [t],
10
- components: { SimulationVuer: a },
11
- computed: { id: function() {
12
- return this.entry.resource ? this.entry.resource : this.entry.discoverId;
13
- } }
14
- };
15
- function s(e, t, a, o, s, c) {
16
- let l = i("SimulationVuer");
17
- return r(), n(l, {
18
- apiLocation: e.apiLocation,
19
- id: c.id,
20
- ref: "simulation"
21
- }, null, 8, ["apiLocation", "id"]);
22
- }
23
- var c = /*#__PURE__*/ e(o, [["render", s]]);
24
- //#endregion
25
- export { c as default };