@abi-software/map-utilities 1.2.2-beta.2 → 1.2.2-beta.4

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/map-utilities",
3
- "version": "1.2.2-beta.2",
3
+ "version": "1.2.2-beta.4",
4
4
  "files": [
5
5
  "dist/*",
6
6
  "src/*",
@@ -337,7 +337,7 @@ class CytoscapeGraph extends EventTarget
337
337
  },
338
338
  directed: true,
339
339
  style: GRAPH_STYLE,
340
- minZoom: 0.1,
340
+ minZoom: 0.5,
341
341
  maxZoom: 10,
342
342
  wheelSensitivity: 0.4,
343
343
  }).on('mouseover', 'node', this.overNode.bind(this))
@@ -1,27 +1,67 @@
1
1
  <template>
2
2
  <div class="resource-container">
3
- <template v-for="resource in resources" :key="resource.id">
4
- <div class="resource">
5
- <el-button
6
- v-if="resource.id === 'pubmed'"
7
- class="button"
8
- id="open-pubmed-button"
9
- :icon="ElIconNotebook"
10
- @click="openUrl(resource.url)"
11
- >
12
- Open publications in PubMed
13
- </el-button>
14
- </div>
15
- </template>
3
+ <div class="attribute-title-container">
4
+ <div class="attribute-title">References</div>
5
+ </div>
6
+ <div class="citation-tabs">
7
+ <el-button
8
+ link
9
+ v-for="citationOption of citationOptions"
10
+ :key="citationOption.value"
11
+ :type="citationType === citationOption.value ? 'primary' : ''"
12
+ @click="onCitationFormatChange(citationOption.value)"
13
+ >
14
+ {{ citationOption.label }}
15
+ </el-button>
16
+ </div>
17
+ <ul class="citation-list">
18
+ <li
19
+ v-for="reference of references"
20
+ :key="reference.id"
21
+ :class="{'loading': reference.citation && reference.citation[citationType] === ''}"
22
+ >
23
+ <template v-if="reference.citation && reference.citation[citationType]">
24
+ <span v-html="reference.citation[citationType]"></span>
25
+ <CopyToClipboard :content="reference.citation[citationType]" />
26
+ </template>
27
+ </li>
28
+
29
+ <li v-for="reference of openLibReferences">
30
+ <div v-html="formatCopyReference(reference)"></div>
31
+ <CopyToClipboard :content="formatCopyReference(reference)" />
32
+ </li>
33
+
34
+ <li v-for="reference of isbnDBReferences">
35
+ <a :href="reference.url">{{ reference.url }}</a>
36
+ <CopyToClipboard :content="reference.url" />
37
+ </li>
38
+ </ul>
16
39
  </div>
17
40
  </template>
18
41
 
19
42
  <script>
20
- /* eslint-disable no-alert, no-console */
21
- import { shallowRef } from "vue";
22
- import { Notebook as ElIconNotebook } from "@element-plus/icons-vue";
43
+ import CopyToClipboard from '../CopyToClipboard/CopyToClipboard.vue';
23
44
 
24
- import EventBus from "../EventBus.js";
45
+ const CROSSCITE_API_HOST = 'https://citation.crosscite.org';
46
+ const CITATION_OPTIONS = [
47
+ {
48
+ label: 'APA',
49
+ value: 'apa',
50
+ },
51
+ {
52
+ label: 'Chicago',
53
+ value: 'chicago-note-bibliography',
54
+ },
55
+ {
56
+ label: 'IEEE',
57
+ value: 'ieee',
58
+ },
59
+ {
60
+ label: 'Bibtex',
61
+ value: 'bibtex',
62
+ },
63
+ ];
64
+ const CITATION_DEFAULT = 'apa';
25
65
 
26
66
  export default {
27
67
  name: "ExternalResourceCard",
@@ -33,26 +73,422 @@ export default {
33
73
  },
34
74
  data: function () {
35
75
  return {
36
- pubmeds: [],
37
- pubmedIds: [],
38
- ElIconNotebook: shallowRef(ElIconNotebook),
39
- };
76
+ references: [],
77
+ pubMedReferences: [],
78
+ openLibReferences: [],
79
+ isbnDBReferences: [],
80
+ citationOptions: CITATION_OPTIONS,
81
+ citationType: CITATION_DEFAULT,
82
+ }
83
+ },
84
+ watch: {
85
+ resources: function (_resources) {
86
+ this.formatReferences([..._resources]);
87
+ }
88
+ },
89
+ mounted: function () {
90
+ this.formatReferences([...this.resources]);
91
+ this.getCitationText(CITATION_DEFAULT);
40
92
  },
41
93
  methods: {
42
- capitalise: function (string) {
43
- return string.charAt(0).toUpperCase() + string.slice(1);
94
+ formatReferences: function (references) {
95
+ const nonPubMedReferences = this.extractNonPubMedReferences(references);
96
+ const pubMedReferences = references.filter((reference) => !nonPubMedReferences.includes(reference));
97
+
98
+ this.pubMedReferences = pubMedReferences.map((reference) =>
99
+ (typeof reference === 'object') ?
100
+ this.extractPublicationIdFromURLString(reference[0]) :
101
+ this.extractPublicationIdFromURLString(reference)
102
+ );
103
+
104
+ this.formatNonPubMedReferences(nonPubMedReferences).then((responses) => {
105
+ this.openLibReferences = responses.filter((response) => response.type === 'openlib');
106
+ this.isbnDBReferences = responses.filter((response) => response.type === 'isbndb');
107
+
108
+ this.formatOpenLibReferences();
109
+ });
110
+
111
+ this.references = [
112
+ ...this.pubMedReferences,
113
+ ];
114
+ },
115
+ extractNonPubMedReferences: function (references) {
116
+ const extractedReferences = [];
117
+ const pubmedDomains = this.getPubMedDomains();
118
+
119
+ references.forEach((reference) => {
120
+ let count = 0;
121
+ pubmedDomains.forEach((name) => {
122
+ if (reference.includes(name)) {
123
+ count++;
124
+ }
125
+ });
126
+ if (!count) {
127
+ extractedReferences.push(reference);
128
+ }
129
+ });
130
+
131
+ return extractedReferences;
132
+ },
133
+ formatNonPubMedReferences: async function (references) {
134
+ const transformedReferences = [];
135
+ const filteredReferences = references.filter((referenceURL) => referenceURL.indexOf('isbn') !== -1);
136
+ const isbnIDs = filteredReferences.map((url) => {
137
+ const isbnId = url.split('/').pop();
138
+ return 'ISBN:' + isbnId;
139
+ });
140
+ const isbnIDsKey = isbnIDs.join(',');
141
+ const failedIDs = isbnIDs.slice();
142
+
143
+ const openlibAPI = `https://openlibrary.org/api/books?bibkeys=${isbnIDsKey}&format=json`;
144
+ const response = await fetch(openlibAPI);
145
+ const data = await response.json();
146
+
147
+ for (const key in data) {
148
+ const successKeyIndex = failedIDs.indexOf(key);
149
+ failedIDs.splice(successKeyIndex, 1);
150
+
151
+ const url = data[key].info_url;
152
+ const urlSegments = url.split('/');
153
+ const endpointIndex = urlSegments.indexOf('books');
154
+ const bookId = urlSegments[endpointIndex + 1];
155
+
156
+ transformedReferences.push({
157
+ id: key.split(':')[1], // Key => "ISBN:1234"
158
+ type: 'openlib',
159
+ url: url,
160
+ bookId: bookId,
161
+ });
162
+ }
163
+
164
+ failedIDs.forEach((failedID) => {
165
+ const id = failedID.split(':')[1];
166
+ // Data does not exist in OpenLibrary
167
+ // Provide ISBNDB link for reference
168
+ const url = `https://isbndb.com/book/${id}`;
169
+ transformedReferences.push({
170
+ id: id,
171
+ url: url,
172
+ type: 'isbndb'
173
+ });
174
+ });
175
+
176
+ return transformedReferences;
177
+ },
178
+ getURLsForPubMed: function (data) {
179
+ return new Promise((resolve) => {
180
+ const ids = data.map((id) =>
181
+ (typeof id === 'object') ?
182
+ this.extractPublicationIdFromURLString(id[0]) :
183
+ this.extractPublicationIdFromURLString(id)
184
+ )
185
+ this.convertPublicationIds(ids).then((pmids) => {
186
+ if (pmids.length > 0) {
187
+ const transformedIDs = [];
188
+ pmids.forEach(pmid => {
189
+ transformedIDs.push({
190
+ id: pmid,
191
+ link: this.pubmedSearchUrl(pmid),
192
+ })
193
+ })
194
+ resolve(transformedIDs)
195
+ } else {
196
+ resolve([])
197
+ }
198
+ })
199
+ })
200
+ },
201
+ extractPublicationIdFromURLString: function (urlStr) {
202
+ if (!urlStr) return
203
+
204
+ const str = decodeURIComponent(urlStr)
205
+
206
+ let term = {id: '', type: '', citation: {}}
207
+
208
+ const names = this.getPubMedDomains()
209
+
210
+ names.forEach((name) => {
211
+ const lastIndex = str.lastIndexOf(name)
212
+ if (lastIndex !== -1) {
213
+ term.id = str.slice(lastIndex + name.length)
214
+ if (name === 'doi.org/') {
215
+ term.type = "doi"
216
+ } else if (name === 'pmc/articles/') {
217
+ term.type = "pmc"
218
+ } else {
219
+ term.type = "pmid"
220
+ }
221
+ }
222
+ })
223
+
224
+ //Backward compatability with doi: and PMID:
225
+ if (term.id === '') {
226
+ if (urlStr.includes("doi:")) {
227
+ term.id = this.stripPMIDPrefix(urlStr)
228
+ term.type = "doi"
229
+ } else if (urlStr.includes("PMID:")) {
230
+ term.id = this.stripPMIDPrefix(urlStr)
231
+ term.type = "pmid"
232
+ }
233
+ }
234
+
235
+ if (term.id.endsWith('/')) {
236
+ term.id = term.id.slice(0, -1)
237
+ }
238
+
239
+ return term
240
+ },
241
+ getPubMedDomains: function () {
242
+ const names = [
243
+ 'doi.org/',
244
+ 'nih.gov/pubmed/',
245
+ 'pmc/articles/',
246
+ 'pubmed.ncbi.nlm.nih.gov/',
247
+ ]
248
+
249
+ return names;
250
+ },
251
+ convertPublicationIds: function (ids) {
252
+ return new Promise((resolve) => {
253
+ const pmids = []
254
+ const toBeConverted = []
255
+ ids.forEach((id) => {
256
+ if (id.type === "pmid") {
257
+ pmids.push(id.id)
258
+ } else if (id.type === "doi" || id.type === "pmc") {
259
+ toBeConverted.push(id.id)
260
+ }
261
+ })
262
+ this.getPMID(toBeConverted).then((idList) => {
263
+ pmids.push(...idList)
264
+ resolve(pmids)
265
+ })
266
+ .catch(() => {
267
+ resolve(pmids)
268
+ })
269
+ })
270
+ },
271
+ pubmedSearchUrl: function (ids) {
272
+ let url = 'https://pubmed.ncbi.nlm.nih.gov/?'
273
+ let params = new URLSearchParams()
274
+ params.append('term', ids)
275
+ return url + params.toString()
276
+ },
277
+ stripPMIDPrefix: function (pubmedId) {
278
+ return pubmedId.split(':')[1]
279
+ },
280
+ getPMID: function(idsList) {
281
+ return new Promise((resolve) => {
282
+ if (idsList.length > 0) {
283
+ //Muliple term search does not work well,
284
+ //DOIs term get splitted unexpectedly
285
+ //
286
+ const promises = []
287
+ const results = []
288
+ let wrapped = ''
289
+ idsList.forEach((id, i) => {
290
+ wrapped += i > 0 ? 'OR"' + id + '"' : '"' + id + '"'
291
+ })
292
+
293
+ const params = new URLSearchParams({
294
+ db: 'pubmed',
295
+ term: wrapped,
296
+ format: 'json'
297
+ })
298
+ const promise = fetch(`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?${params}`, {
299
+ method: 'GET',
300
+ })
301
+ .then((response) => response.json())
302
+ .then((data) => {
303
+ const newIds = data.esearchresult ? data.esearchresult.idlist : []
304
+ results.push(...newIds)
305
+ })
306
+ promises.push(promise)
307
+
308
+ Promise.all(promises).then(() => {
309
+ resolve(results)
310
+ }).catch(() => {
311
+ resolve(results)
312
+ })
313
+ } else {
314
+ resolve([])
315
+ }
316
+ })
317
+ },
318
+ onCitationFormatChange: function(citationType) {
319
+ this.citationType = citationType;
320
+ this.getCitationText(citationType);
321
+ },
322
+ getCitationText: function(citationType) {
323
+ this.references.forEach((reference) => {
324
+ const { id, type, doi } = reference;
325
+
326
+ if (
327
+ !(reference.citation && reference.citation[citationType])
328
+ && id
329
+ ) {
330
+ reference.citation[citationType] = ''; // loading
331
+
332
+ if (type === 'doi' || doi) {
333
+ const doiID = type === 'doi' ? id : doi;
334
+
335
+ this.getCitationTextByDOI(doiID)
336
+ .then((text) => {
337
+ reference.citation[citationType] = this.replaceLinkInText(text);
338
+ });
339
+ } else if (type === 'pmid') {
340
+ this.getDOIFromPubMedID(id)
341
+ .then((data) => {
342
+ if (data?.result) {
343
+ const resultObj = data.result[id];
344
+ const articleIDs = resultObj?.articleids || [];
345
+ const doiObj = articleIDs.find((item) => item.idtype === 'doi');
346
+ const doiID = doiObj?.value;
347
+ reference['doi'] = doiID;
348
+
349
+ this.getCitationTextByDOI(doiID)
350
+ .then((text) => {
351
+ reference.citation[citationType] = this.replaceLinkInText(text);
352
+ });
353
+ }
354
+ });
355
+ }
356
+ }
357
+ });
358
+ },
359
+ replaceLinkInText: function (text) {
360
+ const protocol = 'https://';
361
+ let linkBody = text.split(protocol)[1];
362
+
363
+ if (linkBody) {
364
+ linkBody = linkBody.split(' ')[0];
365
+ linkBody = linkBody.trim();
366
+
367
+ if (linkBody.endsWith('.')) {
368
+ linkBody = linkBody.substring(0, linkBody.length - 1);
369
+ }
370
+
371
+ const fullLink = protocol + linkBody;
372
+ const htmlLink = `<a href="${fullLink}" target="_blank">${fullLink}</a>`;
373
+
374
+ return text.replace(fullLink, htmlLink);
375
+ }
376
+
377
+ return text;
378
+ },
379
+ getCitationTextByDOI: async function (id) {
380
+ const citationAPI = `${CROSSCITE_API_HOST}/format?doi=${id}&style=${this.citationType}&lang=en-US`;
381
+ try {
382
+ const response = await fetch(citationAPI);
383
+ if (!response.ok) {
384
+ throw new Error(`Response status: ${response.status}`);
385
+ }
386
+ const data = await response.text();
387
+ return data;
388
+ } catch (error) {
389
+ console.error(`Fetch citation text error: ${error}`);
390
+ }
44
391
  },
45
- openUrl: function (url) {
46
- EventBus.emit("open-pubmed-url", url);
47
- window.open(url, "_blank");
392
+ getDOIFromPubMedID: async function (pubmedId) {
393
+ const summaryAPI = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${pubmedId}&format=json`;
394
+ try {
395
+ const response = await fetch(summaryAPI);
396
+ if (!response.ok) {
397
+ throw new Error(`Response status: ${response.status}`);
398
+ }
399
+ const data = await response.json();
400
+ return data;
401
+ } catch (error) {
402
+ console.error(`Fetch article summary error: ${error}`);
403
+ }
404
+ },
405
+ formatOpenLibReferences: function () {
406
+ this.openLibReferences.forEach((reference) => {
407
+ const { bookId } = reference;
408
+ this.getBookData(bookId)
409
+ .then((data) => {
410
+ const { title, authors, publish_date } = data;
411
+ if (title) {
412
+ reference['title'] = title;
413
+ }
414
+
415
+ if (publish_date) {
416
+ reference['date'] = publish_date;
417
+ }
418
+
419
+ if (authors) {
420
+ reference['authors'] = [];
421
+
422
+ authors.forEach((author) => {
423
+ this.getBookAuthor(author.key)
424
+ .then((data) => {
425
+ const { name } = data;
426
+ if (name) {
427
+ reference['authors'].push(name);
428
+ }
429
+ });
430
+ });
431
+ }
432
+ });
433
+ });
434
+ },
435
+ getBookData: async function (bookId) {
436
+ const apiURL = `https://openlibrary.org/books/${bookId}.json`;
437
+ try {
438
+ const response = await fetch(apiURL);
439
+ if (!response.ok) {
440
+ throw new Error(`Response status: ${response.status}`);
441
+ }
442
+ const data = await response.json();
443
+ return data;
444
+ } catch (error) {
445
+ console.error(`Fetch book data error: ${error}`);
446
+ }
447
+ },
448
+ getBookAuthor: async function (key) {
449
+ const apiURL = `https://openlibrary.org${key}.json`;
450
+ try {
451
+ const response = await fetch(apiURL);
452
+ if (!response.ok) {
453
+ throw new Error(`Response status: ${response.status}`);
454
+ }
455
+ const data = await response.json();
456
+ return data;
457
+ } catch (error) {
458
+ console.error(`Fetch book author error: ${error}`);
459
+ }
460
+ },
461
+ formatCopyReference: function (reference) {
462
+ const copyContents = [];
463
+ const { title, date, authors, url } = reference;
464
+
465
+ if (title) {
466
+ copyContents.push(title);
467
+ }
468
+
469
+ if (date) {
470
+ copyContents.push(`(${date})`);
471
+ }
472
+
473
+ if (authors) {
474
+ copyContents.push(`- ${authors.join(', ')}`);
475
+ }
476
+
477
+ copyContents.push(`<div><a href="${url}" target="_blank">${url}</a></div>`);
478
+
479
+ return copyContents.join(' ');
48
480
  },
49
481
  },
50
- };
482
+ }
51
483
  </script>
52
484
 
53
485
  <style lang="scss" scoped>
54
486
  .resource-container {
55
- margin-top: 0.5em;
487
+ margin-top: 1em;
488
+ }
489
+
490
+ .attribute-title-container {
491
+ margin-bottom: 0.5rem;
56
492
  }
57
493
 
58
494
  .attribute-title {
@@ -62,47 +498,89 @@ export default {
62
498
  text-transform: uppercase;
63
499
  }
64
500
 
65
- .attribute-content {
66
- font-size: 14px;
67
- font-weight: 400;
68
- }
501
+ .citation-list {
502
+ margin: 0;
503
+ margin-top: 0.5rem;
504
+ padding: 0;
505
+ list-style: none;
506
+ line-height: 1.3;
507
+
508
+ li {
509
+ margin: 0;
510
+ padding: 0.5rem 1.5rem 0.5rem 0.75rem;
511
+ border-radius: var(--el-border-radius-base);
512
+ background-color: var(--el-bg-color-page);
513
+ position: relative;
514
+
515
+ :deep(a) {
516
+ word-wrap: break-word;
517
+ }
518
+
519
+ + li {
520
+ margin-top: 0.5rem;
521
+ }
522
+
523
+ &.loading {
524
+ padding: 1rem;
69
525
 
70
- .el-link {
71
- color: $app-primary-color;
72
- text-decoration: none;
73
- word-wrap: break-word;
74
- &:hover,
75
- &:focus {
76
- color: $app-primary-color;
526
+ &::before {
527
+ content: "";
528
+ display: block;
529
+ width: 100%;
530
+ height: 100%;
531
+ position: absolute;
532
+ top: 0;
533
+ left: 0;
534
+ animation-duration: 3s;
535
+ animation-fill-mode: forwards;
536
+ animation-iteration-count: infinite;
537
+ animation-name: loadingAnimation;
538
+ animation-timing-function: linear;
539
+ background: linear-gradient(to right,
540
+ var(--el-bg-color-page) 5%,
541
+ var(--el-color-info-light-8) 15%,
542
+ var(--el-bg-color-page) 30%
543
+ );
544
+ }
545
+ }
546
+
547
+ :deep(.copy-clipboard-button) {
548
+ position: absolute;
549
+ bottom: 0.25rem;
550
+ right: 0.25rem;
551
+ opacity: 0;
552
+ visibility: hidden;
553
+ }
554
+
555
+ &:hover {
556
+ :deep(.copy-clipboard-button) {
557
+ opacity: 1;
558
+ visibility: visible;
559
+ }
560
+ }
77
561
  }
78
562
  }
79
563
 
80
- :deep(.el-carousel__button) {
81
- background-color: $app-primary-color;
82
- }
564
+ .citation-tabs {
565
+ .el-button {
566
+ &:hover,
567
+ &:focus,
568
+ &:active {
569
+ color: $app-primary-color;
570
+ }
571
+ }
83
572
 
84
- .attribute-title {
85
- font-size: 16px;
86
- font-weight: 600;
87
- /* font-weight: bold; */
88
- text-transform: uppercase;
573
+ .el-button + .el-button {
574
+ margin-left: 0.25rem;
575
+ }
89
576
  }
90
577
 
91
- .button {
92
- margin-left: 0px !important;
93
- margin-top: 0px !important;
94
- font-size: 14px !important;
95
- background-color: $app-primary-color;
96
- color: #fff;
97
- &:hover {
98
- color: #fff !important;
99
- background: #ac76c5 !important;
100
- border: 1px solid #ac76c5 !important;
578
+ @keyframes loadingAnimation {
579
+ 0% {
580
+ background-position: -30vw 0;
101
581
  }
102
- & + .button {
103
- margin-top: 10px !important;
104
- background-color: $app-primary-color;
105
- color: #fff;
582
+ 100% {
583
+ background-position: 70vw 0;
106
584
  }
107
585
  }
108
586
  </style>
@@ -164,7 +164,7 @@
164
164
  Search for data on components
165
165
  </el-button>
166
166
 
167
- <external-resource-card :resources="resources"></external-resource-card>
167
+ <external-resource-card :resources="resources" v-if="resources.length"></external-resource-card>
168
168
  </div>
169
169
  </transition>
170
170
  </div>
@@ -6,6 +6,7 @@ import DrawToolbar from "./DrawToolbar/DrawToolbar.vue";
6
6
  import HelpModeDialog from "./HelpModeDialog/HelpModeDialog.vue";
7
7
  import Tooltip from "./Tooltip/Tooltip.vue";
8
8
  import TreeControls from "./TreeControls/TreeControls.vue";
9
+ import ExternalResourceCard from "./Tooltip/ExternalResourceCard.vue";
9
10
 
10
11
  export {
11
12
  AnnotationPopup,
@@ -16,4 +17,5 @@ export {
16
17
  HelpModeDialog,
17
18
  Tooltip,
18
19
  TreeControls,
20
+ ExternalResourceCard,
19
21
  };