@abi-software/scaffoldvuer 1.3.3-beta.1 → 1.4.0-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abi-software/scaffoldvuer",
3
- "version": "1.3.3-beta.1",
3
+ "version": "1.4.0-beta.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "*.js"
42
42
  ],
43
43
  "dependencies": {
44
- "@abi-software/map-utilities": "^1.0.0",
44
+ "@abi-software/map-utilities": "^1.1.0-beta.2",
45
45
  "@abi-software/sparc-annotation": "^0.3.1",
46
46
  "@abi-software/svg-sprite": "^1.0.0",
47
47
  "@element-plus/icons-vue": "^2.3.1",
@@ -55,12 +55,12 @@
55
55
  "vue": "^3.4.15",
56
56
  "vue-router": "^4.2.5",
57
57
  "vue3-component-svg-sprite": "^0.0.1",
58
- "zincjs": "^1.11.0"
58
+ "zincjs": "^1.11.3"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@vitejs/plugin-vue": "^4.6.2",
62
62
  "@vuese/markdown-render": "^2.11.3",
63
- "@vuese/parser": "^2.10.3",
63
+ "@vuese/parser": "^2.9.1",
64
64
  "auto-changelog": "^2.4.0",
65
65
  "babel-eslint": "^10.1.0",
66
66
  "babel-plugin-component": "^1.1.1",
package/src/App.vue CHANGED
@@ -28,6 +28,7 @@
28
28
  :format="format"
29
29
  :marker-labels="markerLabels"
30
30
  :enableLocalAnnotations="false"
31
+ :sparcAPI="sparcAPI"
31
32
  @open-map="openMap"
32
33
  @on-ready="onReady"
33
34
  @scaffold-selected="onSelected"
@@ -321,6 +322,9 @@ import {
321
322
  import { useRoute, useRouter } from 'vue-router'
322
323
  import { HelpModeDialog } from '@abi-software/map-utilities'
323
324
  import '@abi-software/map-utilities/dist/style.css'
325
+ import { mapStores } from 'pinia';
326
+ import { useSettingsStore } from '@/stores/settings';
327
+ import { getOrganCuries } from '@/services/scicrunchQueries'
324
328
 
325
329
  let texture_prefix = undefined;
326
330
 
@@ -358,7 +362,7 @@ export default {
358
362
  },
359
363
  data: function () {
360
364
  return {
361
- consoleOn: true,
365
+ consoleOn: false,
362
366
  createLinesWithNormal: false,
363
367
  url: undefined,
364
368
  input: undefined,
@@ -406,9 +410,14 @@ export default {
406
410
  router: useRouter(),
407
411
  ElIconSetting: shallowRef(ElIconSetting),
408
412
  ElIconFolderOpened: shallowRef(ElIconFolderOpened),
409
- auto: NaN
413
+ auto: NaN,
414
+ sparcAPI: import.meta.env.VITE_SPARC_API,
415
+ // sparcAPI: "http://localhost:8000/",
410
416
  };
411
417
  },
418
+ computed: {
419
+ ...mapStores(useSettingsStore),
420
+ },
412
421
  watch: {
413
422
  input: function () {
414
423
  this.parseInput();
@@ -422,7 +431,7 @@ export default {
422
431
  "body proper": 9,
423
432
  "Spinal cord": 8,
424
433
  "lung": 11,
425
- "stomach": {number:12, imgURL: 'https://mapcore-bucket1.s3.us-west-2.amazonaws.com/texture/arm1/jpg/0984.jpg'},
434
+ "stomach": 12,
426
435
  "urinary bladder": 11,
427
436
  "Brainstem": 11,
428
437
  "heart": 9,
@@ -454,6 +463,7 @@ export default {
454
463
  },
455
464
  mounted: function () {
456
465
  this._objects = [];
466
+ getOrganCuries(this.sparcAPI).then((organCuries) => this.settingsStore.updateOrganCuries(organCuries))
457
467
  },
458
468
  created: function () {
459
469
  texture_prefix = import.meta.env.VITE_TEXTURE_FOOT_PREFIX;
@@ -14,7 +14,7 @@
14
14
  <template #default>
15
15
  <div class="tooltip-text">{{ label }}</div>
16
16
  <div class="tooltip-text" v-if="region">Region: {{ region }}</div>
17
- <CreateTooltiipContent
17
+ <CreateTooltipContent
18
18
  v-show="createData.toBeConfirmed"
19
19
  :createData="createData"
20
20
  @confirm-create="$emit('confirm-create', $event)"
@@ -22,10 +22,14 @@
22
22
  />
23
23
  <Tooltip
24
24
  class="p-tooltip"
25
- v-show="annotationDisplay && !createData.toBeConfirmed"
26
- ref="annotationTooltip"
27
- :annotationDisplay="true"
25
+ v-show="
26
+ (annotationDisplay && !createData.toBeConfirmed) ||
27
+ imageEntry.length
28
+ "
29
+ ref="tooltip"
30
+ :tooltipType="annotationDisplay ? 'annotation' : 'image'"
28
31
  :annotationEntry="annotationEntry"
32
+ :imageEntry="imageEntry"
29
33
  />
30
34
  <div v-if="createData.toBeDeleted" class="delete-container">
31
35
  <el-row>
@@ -66,9 +70,9 @@ import {
66
70
  import {
67
71
  Delete as ElIconDelete,
68
72
  } from '@element-plus/icons-vue'
69
- import CreateTooltiipContent from "./CreateTooltipContent.vue";
70
73
  import { mapState } from 'pinia';
71
- import { useMainStore } from "@/store/index";
74
+ import { useMainStore } from "@/stores/index";
75
+ import CreateTooltipContent from "./CreateTooltipContent.vue";
72
76
  import { Tooltip } from '@abi-software/map-utilities'
73
77
  import '@abi-software/map-utilities/dist/style.css'
74
78
 
@@ -79,7 +83,7 @@ export default {
79
83
  name: "ScaffoldTooltip",
80
84
  components: {
81
85
  Col,
82
- CreateTooltiipContent,
86
+ CreateTooltipContent,
83
87
  ElIconDelete,
84
88
  Icon,
85
89
  Popover,
@@ -121,6 +125,14 @@ export default {
121
125
  type: Number,
122
126
  default: 200,
123
127
  },
128
+ imageThumbnails: {
129
+ type: Object,
130
+ default: {},
131
+ },
132
+ imageThumbnailSidebar: {
133
+ type: Boolean,
134
+ default: false,
135
+ },
124
136
  },
125
137
  inject: ['scaffoldUrl'],
126
138
  provide() {
@@ -130,7 +142,6 @@ export default {
130
142
  },
131
143
  data: function () {
132
144
  return {
133
- display: false,
134
145
  annotationEntry: { },
135
146
  ElIconDelete: shallowRef(ElIconDelete),
136
147
  };
@@ -145,11 +156,26 @@ export default {
145
156
  const x = this.x - 40;
146
157
  return { left: x + "px", top: this.y - yOffset + "px" };
147
158
  },
159
+ imageEntry: function () {
160
+ let imageEntries = []
161
+ const imageThumbnailsEntries = Object.assign({},
162
+ Object.fromEntries(
163
+ Object.entries(this.imageThumbnails)
164
+ .filter(([key, value]) => value.length > 0)
165
+ .map(([key, value]) => [key.toLowerCase(), value])))
166
+ if (this.label in imageThumbnailsEntries) {
167
+ imageEntries = imageThumbnailsEntries[this.label];
168
+ }
169
+ if (this.imageThumbnailSidebar) {
170
+ this.$emit('image-thumbnail-open', imageEntries)
171
+ return [];
172
+ }
173
+ return imageEntries;
174
+ },
148
175
  },
149
176
  methods: {
150
177
  checkForDisplay: function () {
151
178
  if (this.visible && this.label && this.label !== "") {
152
- this.display = true;
153
179
  if (this.annotationDisplay) {
154
180
  const region = this.region ? this.region +"/" : "";
155
181
  this.annotationEntry = {
@@ -158,9 +184,7 @@ export default {
158
184
  "resource": encodeURIComponent(this.scaffoldUrl),
159
185
  };
160
186
  }
161
- }
162
- else {
163
- this.display = false;
187
+ } else {
164
188
  this.annotationEntry = { };
165
189
  }
166
190
  },
@@ -15,9 +15,12 @@
15
15
  :x="tData.x"
16
16
  :y="tData.y"
17
17
  :annotationDisplay="annotationDisplay"
18
+ :imageThumbnailSidebar="imageThumbnailSidebar"
19
+ :imageThumbnails="imageThumbnailsEntry"
18
20
  @confirm-create="confirmCreate($event)"
19
21
  @cancel-create="cancelCreate()"
20
22
  @confirm-delete="confirmDelete($event)"
23
+ @image-thumbnail-open="onImageThumbnailOpen"
21
24
  />
22
25
  <div
23
26
  id="organsDisplayArea"
@@ -284,7 +287,7 @@
284
287
  ref="backgroundPopover"
285
288
  :virtual-ref="backgroundIconRef"
286
289
  placement="top-start"
287
- width="128"
290
+ width="330"
288
291
  :teleported="false"
289
292
  trigger="click"
290
293
  popper-class="background-popper non-selectable h-auto"
@@ -292,7 +295,7 @@
292
295
  >
293
296
  <div>
294
297
  <el-row class="backgroundText">Viewing Mode</el-row>
295
- <el-row class="backgroundControl">
298
+ <el-row class="backgroundChooser">
296
299
  <div style="margin-bottom: 2px;">
297
300
  <template
298
301
  v-for="(value, key, index) in viewingModes"
@@ -310,6 +313,41 @@
310
313
  {{ viewingModes[viewingMode] }}
311
314
  </el-row>
312
315
  </el-row>
316
+ <el-row class="backgroundSpacer" v-if="viewingMode === 'Exploration'"></el-row>
317
+ <el-row class="backgroundText" v-if="viewingMode === 'Exploration'">Markers display</el-row>
318
+ <el-row class="backgroundChooser" v-if="viewingMode === 'Exploration'">
319
+ <el-col :span="14">
320
+ <el-radio-group
321
+ v-model="imageRadio"
322
+ class="flatmap-radio"
323
+ @change="setImage"
324
+ >
325
+ <el-radio :value="false">Standard</el-radio>
326
+ <el-radio :value="true">Image</el-radio>
327
+ </el-radio-group>
328
+ </el-col>
329
+ <el-col :span="10" v-if="imageRadio">
330
+ <el-select
331
+ :teleported="false"
332
+ v-model="imageType"
333
+ placeholder="Select"
334
+ class="scaffold-select-box imageSelector"
335
+ popper-class="scaffold_viewer_dropdown"
336
+ @change="setImageType"
337
+ >
338
+ <el-option
339
+ v-for="item in imageTypes"
340
+ :key="item"
341
+ :label="item"
342
+ :value="item"
343
+ >
344
+ <el-row>
345
+ <el-col :span="12">{{ item }}</el-col>
346
+ </el-row>
347
+ </el-option>
348
+ </el-select>
349
+ </el-col>
350
+ </el-row>
313
351
  <el-row class="backgroundSpacer"></el-row>
314
352
  <el-row class="backgroundText"> Change background </el-row>
315
353
  <el-row class="backgroundChooser">
@@ -382,7 +420,7 @@
382
420
 
383
421
  <script>
384
422
  /* eslint-disable no-alert, no-console */
385
- import { markRaw, shallowRef } from 'vue';
423
+ import { markRaw, toRaw, shallowRef } from 'vue';
386
424
  import {
387
425
  WarningFilled as ElIconWarningFilled,
388
426
  ArrowDown as ElIconArrowDown,
@@ -421,8 +459,16 @@ import { AnnotationService } from '@abi-software/sparc-annotation';
421
459
  import { EventNotifier } from "../scripts/EventNotifier.js";
422
460
  import { OrgansViewer } from "../scripts/OrgansRenderer.js";
423
461
  import { SearchIndex } from "../scripts/Search.js";
424
- import { mapState } from 'pinia';
425
- import { useMainStore } from "@/store/index";
462
+ import { mapState, mapStores } from 'pinia';
463
+ import { useMainStore } from "@/stores/index";
464
+ import { useSettingsStore } from '@/stores/settings'
465
+ import {
466
+ getBiolucidaThumbnails,
467
+ getSegmentationThumbnails,
468
+ getScaffoldThumbnails,
469
+ getPlotThumbnails
470
+ } from '../services/scicrunchQueries'
471
+ import imageMixin from '../mixins/imageMixin.js'
426
472
 
427
473
  /**
428
474
  * A vue component of the scaffold viewer.
@@ -432,6 +478,7 @@ import { useMainStore } from "@/store/index";
432
478
  */
433
479
  export default {
434
480
  name: "ScaffoldVuer",
481
+ mixins: [imageMixin],
435
482
  components: {
436
483
  Button,
437
484
  Col,
@@ -689,10 +736,24 @@ export default {
689
736
  /**
690
737
  * Enable local annotations
691
738
  */
692
- enableLocalAnnotations: {
739
+ enableLocalAnnotations: {
693
740
  type: Boolean,
694
741
  default: false
695
742
  },
743
+ /**
744
+ * Specify the endpoint of the SPARC API.
745
+ */
746
+ sparcAPI: {
747
+ type: String,
748
+ default: 'https://api.sparc.science/',
749
+ },
750
+ /**
751
+ * The option to show image thumbnail in sidebar
752
+ */
753
+ imageThumbnailSidebar: {
754
+ type: Boolean,
755
+ default: false,
756
+ },
696
757
  },
697
758
  provide() {
698
759
  return {
@@ -700,6 +761,7 @@ export default {
700
761
  scaffoldUrl: this.url,
701
762
  $annotator: this.annotator,
702
763
  boundingDims: this.boundingDims,
764
+ getFeaturesAlert: () => undefined,
703
765
  };
704
766
  },
705
767
  data: function () {
@@ -790,7 +852,8 @@ export default {
790
852
  active: false,
791
853
  },
792
854
  fileFormat: "metadata",
793
- previousMarkerLabels: markRaw({}),
855
+ markerLabelEntry: markRaw({}),
856
+ previousMarkerLabelEntry: markRaw({}),
794
857
  viewingMode: "Exploration",
795
858
  viewingModes: {
796
859
  "Exploration": "View and explore detailed visualization of 3D scaffolds",
@@ -812,6 +875,10 @@ export default {
812
875
  centre: [0, 0, 0],
813
876
  size:[1, 1, 1],
814
877
  },
878
+ imageRadio: false,
879
+ imageType: 'Image',
880
+ imageTypes: ['Image', 'Segmentation', 'Scaffold', 'Plot'],
881
+ imageClicked: '',
815
882
  };
816
883
  },
817
884
  watch: {
@@ -891,14 +958,17 @@ export default {
891
958
  },
892
959
  immediate: true,
893
960
  },
894
- markerLabels: function(labels) {
895
- for (const [key, value] of Object.entries(this.previousMarkerLabels)) {
961
+ markerLabels: function (labels) {
962
+ this.markerLabelEntry = markRaw({...labels})
963
+ },
964
+ markerLabelEntry: function (entry) {
965
+ for (const [key, value] of Object.entries(this.previousMarkerLabelEntry)) {
896
966
  this.setMarkerModeForObjectsWithName(key, value, "off");
897
967
  }
898
- for (const [key, value] of Object.entries(labels)) {
968
+ for (const [key, value] of Object.entries(entry)) {
899
969
  this.setMarkerModeForObjectsWithName(key, value, "on");
900
970
  }
901
- this.previousMarkerLabels = markRaw({...labels});
971
+ this.previousMarkerLabelEntry = markRaw({...entry});
902
972
  },
903
973
  },
904
974
  beforeCreate: function () {
@@ -938,9 +1008,13 @@ export default {
938
1008
  },
939
1009
  computed: {
940
1010
  ...mapState(useMainStore, ['userToken']),
1011
+ ...mapStores(useSettingsStore),
941
1012
  annotationDisplay: function() {
942
1013
  return this.viewingMode === 'Annotation' && this.tData.active === true &&
943
1014
  (this.activeDrawMode === 'Edit' || this.activeDrawMode === 'Delete');
1015
+ },
1016
+ imageThumbnailsEntry: function() {
1017
+ return this.imageClicked ? this.convertUberonToName() : {};
944
1018
  }
945
1019
  },
946
1020
  methods: {
@@ -1503,6 +1577,13 @@ export default {
1503
1577
  this.$refs.scaffoldTreeControls.removeActive(false);
1504
1578
  }
1505
1579
  }
1580
+ if (this.imageRadio && event.identifiers.length && event.identifiers[0]) {
1581
+ this.imageClicked = event.identifiers[0].data.id
1582
+ ? event.identifiers[0].data.id
1583
+ : event.identifiers[0].data.group;
1584
+ } else {
1585
+ this.imageClicked = ''
1586
+ }
1506
1587
  //Emit when an object is selected
1507
1588
  //@arg Identifier of selected objects
1508
1589
  this.$emit("scaffold-selected", event.identifiers);
@@ -1553,6 +1634,12 @@ export default {
1553
1634
  this.createEditTemporaryLines(event.identifiers[0].extraData.worldCoords);
1554
1635
  }
1555
1636
  this.createEditTemporaryLines(event.identifiers[0].extraData.worldCoords);
1637
+ const id = event.identifiers[0].data.id
1638
+ ? event.identifiers[0].data.id
1639
+ : event.identifiers[0].data.group;
1640
+ if (this.imageClicked !== id) {
1641
+ this.imageClicked = ''
1642
+ }
1556
1643
  }
1557
1644
  }
1558
1645
  }
@@ -2327,15 +2414,67 @@ export default {
2327
2414
  this.$module.toggleSyncControl(flag, rotateMode);
2328
2415
  this.$module.setSyncControlCallback(this.syncControlCallback);
2329
2416
  },
2330
-
2331
2417
  /**
2332
2418
  * Set the markers for the scene.
2333
2419
  */
2334
2420
  setMarkers: function () {
2335
- for (const [key, value] of Object.entries(this.markerLabels)) {
2421
+ for (const [key, value] of Object.entries(this.markerLabelEntry)) {
2336
2422
  this.setMarkerModeForObjectsWithName(key, value, "on");
2337
2423
  }
2338
2424
  },
2425
+ removeImageThumbnails: function () {
2426
+ this.imageThumbnails = {}
2427
+ this.markerLabelEntry = markRaw(this.markerLabels)
2428
+ },
2429
+ setImage: function (flag) {
2430
+ if (flag) {
2431
+ this.setImageType(this.imageType)
2432
+ } else {
2433
+ this.removeImageThumbnails()
2434
+ }
2435
+ },
2436
+ setImageType: async function (type) {
2437
+ this.imageType = type
2438
+ if (!this.settingsStore.imageTypeCached(type)) {
2439
+ this.loading = true
2440
+ await this.fetchImageThumbnails(type)
2441
+ this.loading = false
2442
+ }
2443
+ this.populateImageThumbnails(type)
2444
+ },
2445
+ fetchImageThumbnails: async function (type) {
2446
+ let thumbnails = {}
2447
+ const organCuries = this.settingsStore.organCuries
2448
+ if (type === 'Image') {
2449
+ thumbnails = await getBiolucidaThumbnails(this.sparcAPI, organCuries, type)
2450
+ } else if (type === 'Segmentation') {
2451
+ thumbnails = await getSegmentationThumbnails(this.sparcAPI, organCuries, type)
2452
+ } else if (type === 'Scaffold') {
2453
+ thumbnails = await getScaffoldThumbnails(this.sparcAPI, organCuries, type)
2454
+ } else if (type === 'Plot') {
2455
+ thumbnails = await getPlotThumbnails(this.sparcAPI, organCuries, type)
2456
+ }
2457
+ this.settingsStore.updateImageThumbnails(type, thumbnails)
2458
+ },
2459
+ convertUberonToName: function () {
2460
+ const organCuries = this.settingsStore.organCuries
2461
+ const identifiers = organCuries.filter((curie) => curie.name in this.markerLabels).map((curie) => curie.id)
2462
+ const imageThumbnails = this.settingsStore.getImageThumbnails(this.imageType, identifiers)
2463
+ return Object.assign({},
2464
+ Object.fromEntries(
2465
+ Object.entries(imageThumbnails)
2466
+ .map(([key, value]) => [organCuries.filter((curie) => curie.id === key)[0].name, value])))
2467
+ },
2468
+ populateImageThumbnails: async function (type) {
2469
+ this.removeImageThumbnails()
2470
+ const thumbnails = this.convertUberonToName()
2471
+ this.loading = true
2472
+ this.markerLabelEntry = markRaw(await this.populateMapWithImages(thumbnails, type))
2473
+ this.loading = false
2474
+ },
2475
+ onImageThumbnailOpen: function (payload) {
2476
+ this.$emit('image-thumbnail-open', payload);
2477
+ },
2339
2478
  },
2340
2479
  };
2341
2480
  </script>
@@ -2495,7 +2634,7 @@ export default {
2495
2634
  background-color: #ffffff;
2496
2635
  border: 1px solid $app-primary-color;
2497
2636
  box-shadow: 0px 2px 12px 0px rgba(0, 0, 0, 0.06);
2498
- height: 140px;
2637
+ height: fit-content;
2499
2638
  min-width: 200px;
2500
2639
  .el-popper__arrow {
2501
2640
  &:before {
@@ -2752,6 +2891,10 @@ export default {
2752
2891
  min-height: 24px
2753
2892
  }
2754
2893
  }
2894
+
2895
+ &.imageSelector {
2896
+ width: 125px!important;
2897
+ }
2755
2898
  }
2756
2899
 
2757
2900
  :deep(.scaffold_viewer_dropdown) {
@@ -26,6 +26,8 @@ declare module 'vue' {
26
26
  ElMain: typeof import('element-plus/es')['ElMain']
27
27
  ElOption: typeof import('element-plus/es')['ElOption']
28
28
  ElPopover: typeof import('element-plus/es')['ElPopover']
29
+ ElRadio: typeof import('element-plus/es')['ElRadio']
30
+ ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
29
31
  ElRow: typeof import('element-plus/es')['ElRow']
30
32
  ElSelect: typeof import('element-plus/es')['ElSelect']
31
33
  ElSlider: typeof import('element-plus/es')['ElSlider']
package/src/main.js CHANGED
@@ -2,7 +2,7 @@ import { createApp } from 'vue'
2
2
  import { createPinia } from 'pinia'
3
3
  import * as VueRouter from 'vue-router'
4
4
  import App from './App.vue'
5
- import { useMainStore } from '@/store/index'
5
+ import { useMainStore } from '@/stores/index'
6
6
 
7
7
  const routes = [
8
8
  { path: '/'},
@@ -0,0 +1,89 @@
1
+ export default {
2
+ methods: {
3
+ populateMapWithImages: async function (images, type) {
4
+ let imageMarkerLabels = {};
5
+ for (const [key, list] of Object.entries(images)) {
6
+ const response = await this.downloadImageThumbnail(key, list, type);
7
+ if (response) {
8
+ imageMarkerLabels[key] = response;
9
+ } else {
10
+ imageMarkerLabels[key] = this.markerLabels[key];
11
+ }
12
+ }
13
+ return imageMarkerLabels;
14
+ },
15
+ downloadImageThumbnail: async function (key, list, type) {
16
+ const count = list.length;
17
+ if (count > 0) {
18
+ //Pick a random image
19
+ const index = Math.floor(Math.random() * count);
20
+ const thumbnail = list[index].thumbnail;
21
+ try {
22
+ const response = await this.getImageThumbnail(thumbnail, type);
23
+ const markerObject = await this.addImageThumbnailMarker(key, response);
24
+ return markerObject;
25
+ } catch (error) {
26
+ // Failed to download, pick another one
27
+ list.splice(index);
28
+ this.downloadImageThumbnail(key, list, type);
29
+ }
30
+ }
31
+ },
32
+ getImageThumbnail: async function (url, type) {
33
+ return new Promise((resolve, reject) => {
34
+ if (type === "Image" || type === "Segmentation") {
35
+ this.getBinaryThumbnail(url)
36
+ .then((response) => resolve(response))
37
+ .catch((response) => reject(response));
38
+ } else {
39
+ this.getGenericThumbnail(url)
40
+ .then((response) => resolve(response))
41
+ .catch((response) => reject(response));
42
+ }
43
+ });
44
+ },
45
+ getBinaryThumbnail: async function (url) {
46
+ return new Promise((resolve, reject) => {
47
+ fetch(url)
48
+ .then((response) => {
49
+ if (response.status >= 200 && response.status < 300) {
50
+ return response.text();
51
+ } else {
52
+ reject();
53
+ }
54
+ })
55
+ .then((data) => {
56
+ if (data) {
57
+ let img = new Image();
58
+ img.onload = function () {
59
+ resolve(`data:'image/png';base64,${data}`);
60
+ };
61
+ img.onerror = function () {
62
+ reject(new Error("Failed to load image at " + url));
63
+ };
64
+ img.src = `data:'image/png';base64,${data}`;
65
+ } else {
66
+ reject(new Error("Failed to load image at " + url));
67
+ }
68
+ });
69
+ });
70
+ },
71
+ getGenericThumbnail: async function (url) {
72
+ return new Promise((resolve, reject) => {
73
+ let img = new Image();
74
+ img.onload = function () {
75
+ resolve(url);
76
+ };
77
+ img.onerror = function () {
78
+ reject(new Error("Failed to load image at " + url));
79
+ };
80
+ img.src = url;
81
+ });
82
+ },
83
+ addImageThumbnailMarker: async function (id, source) {
84
+ const blob = await (await fetch(source)).blob();
85
+ const blobUrl = URL.createObjectURL(blob);
86
+ return { number: this.markerLabels[id], imgURL: blobUrl };
87
+ },
88
+ },
89
+ };