@abi-software/flatmapvuer 1.13.4 → 1.13.5-demo.1

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/flatmapvuer",
3
- "version": "1.13.4",
3
+ "version": "1.13.5-demo.1",
4
4
  "license": "Apache-2.0",
5
5
  "files": [
6
6
  "dist/*",
@@ -634,6 +634,7 @@ import { capitalise, normaliseAlertToStringArray } from './utilities.js'
634
634
  import yellowstar from '../icons/yellowstar'
635
635
  import ResizeSensor from 'css-element-queries/src/ResizeSensor'
636
636
  import flatmap from '../services/flatmapLoader.js'
637
+ import { attachSomaLocationMarkerMethods } from '../services/somaLocationMarkers.js'
637
638
  import { AnnotationService } from '@abi-software/sparc-annotation'
638
639
  import { mapState } from 'pinia'
639
640
  import { useMainStore } from '@/store/index'
@@ -1737,6 +1738,9 @@ export default {
1737
1738
  provenanceTaxonomy: taxons,
1738
1739
  alert: featuresAlert
1739
1740
  }]
1741
+ if (eventType === 'mouseenter') {
1742
+ this.lastHoveredFeature = data;
1743
+ }
1740
1744
  if (eventType === 'click') {
1741
1745
  // If multiple paths overlap at the click location,
1742
1746
  // `data` is an object with numeric keys for each feature (e.g., {0: {...}, 1: {...}, ..., mapUUID: '...'}).
@@ -2837,7 +2841,7 @@ export default {
2837
2841
  }
2838
2842
  )
2839
2843
  promise1.then((returnedObject) => {
2840
- this.mapImp = returnedObject
2844
+ this.mapImp = attachSomaLocationMarkerMethods(returnedObject)
2841
2845
  this.serverURL = this.mapImp.makeServerUrl('').slice(0, -1)
2842
2846
  let mapVersion = this.mapImp.details.version
2843
2847
  this.setFlightPathInfo(mapVersion)
@@ -3153,6 +3157,62 @@ export default {
3153
3157
  setConnectionType: function (type) {
3154
3158
  this.connectionType = type;
3155
3159
  },
3160
+ /**
3161
+ * MutationObserver callback to intercept tooltip popup DOM elements
3162
+ * and customize their content before the browser renders them.
3163
+ */
3164
+ _handleTooltipMutation: function (mutations) {
3165
+ for (const mutation of mutations) {
3166
+ for (const node of mutation.addedNodes) {
3167
+ if (node.nodeType === 1) {
3168
+ // Check if the added node itself is the tooltip popup
3169
+ if (node.matches && node.matches('.flatmap-tooltip-popup')) {
3170
+ this._applyCustomTooltipContent(node);
3171
+ } else if (node.querySelector) {
3172
+ // Check if it contains a tooltip popup
3173
+ const popup = node.querySelector('.flatmap-tooltip-popup');
3174
+ if (popup) {
3175
+ this._applyCustomTooltipContent(popup);
3176
+ }
3177
+ }
3178
+ }
3179
+ }
3180
+ }
3181
+ },
3182
+ /**
3183
+ * Replace the default tooltip content with custom content from
3184
+ * the tooltipContentProvider prop, if available.
3185
+ */
3186
+ _applyCustomTooltipContent: function (popupEl) {
3187
+ if (!this.tooltipContentProvider || !this.lastHoveredFeature) return;
3188
+ try {
3189
+ const featureData = this.lastHoveredFeature;
3190
+ // Detect multi-feature data format used by click events when multiple
3191
+ // paths overlap: {0: {...}, 1: {...}, ..., mapUUID: '...'}
3192
+ const isMultiFeature = featureData[0] && typeof featureData[0] === 'object';
3193
+ let customHtml;
3194
+ if (isMultiFeature) {
3195
+ const features = [];
3196
+ const mapUUID = featureData.mapUUID;
3197
+ for (const [key, value] of Object.entries(featureData)) {
3198
+ if (key !== 'mapUUID' && value && typeof value === 'object') {
3199
+ features.push({ ...value, mapUUID });
3200
+ }
3201
+ }
3202
+ customHtml = this.tooltipContentProvider(features.length > 1 ? features : features[0]);
3203
+ } else {
3204
+ customHtml = this.tooltipContentProvider(featureData);
3205
+ }
3206
+ if (customHtml) {
3207
+ const contentEl = popupEl.querySelector('.maplibregl-popup-content');
3208
+ if (contentEl) {
3209
+ contentEl.innerHTML = customHtml;
3210
+ }
3211
+ }
3212
+ } catch (e) {
3213
+ console.warn('[flatmapvuer] Error in tooltipContentProvider:', e);
3214
+ }
3215
+ },
3156
3216
  },
3157
3217
  props: {
3158
3218
  /**
@@ -3393,6 +3453,17 @@ export default {
3393
3453
  return []
3394
3454
  },
3395
3455
  },
3456
+ /**
3457
+ * A function that provides custom tooltip HTML content for path features.
3458
+ * The function receives the feature data from the mouseenter event (including `id`, `label`, etc.)
3459
+ * and should return an HTML string to display in the tooltip, or return null/undefined
3460
+ * to keep the default tooltip.
3461
+ * This is used by mapintegratedvuer to show 'long-label' from connectivity knowledge.
3462
+ */
3463
+ tooltipContentProvider: {
3464
+ type: Function,
3465
+ default: null,
3466
+ },
3396
3467
  },
3397
3468
  provide() {
3398
3469
  return {
@@ -3495,6 +3566,8 @@ export default {
3495
3566
  drawnCreatedEvent: {},
3496
3567
  previousEditEvent: {},
3497
3568
  previousDeletedEvent: {},
3569
+ lastHoveredFeature: null,
3570
+ tooltipObserver: null,
3498
3571
  connectionEntry: {},
3499
3572
  existDrawnFeatures: [], // Store all exist drawn features
3500
3573
  doubleClickedFeature: false,
@@ -3693,6 +3766,27 @@ export default {
3693
3766
  this.createFlatmap()
3694
3767
  }
3695
3768
  refreshFlatmapKnowledgeCache();
3769
+
3770
+ // Set up MutationObserver to customize tooltip content when
3771
+ // tooltipContentProvider prop is provided.
3772
+ if (this.tooltipContentProvider) {
3773
+ this.$nextTick(() => {
3774
+ const container = this.$refs.flatmapContainer || this.$el;
3775
+ if (container) {
3776
+ this.tooltipObserver = new MutationObserver(this._handleTooltipMutation.bind(this));
3777
+ this.tooltipObserver.observe(container, {
3778
+ childList: true,
3779
+ subtree: true,
3780
+ });
3781
+ }
3782
+ });
3783
+ }
3784
+ },
3785
+ beforeUnmount: function () {
3786
+ if (this.tooltipObserver) {
3787
+ this.tooltipObserver.disconnect();
3788
+ this.tooltipObserver = null;
3789
+ }
3696
3790
  },
3697
3791
  }
3698
3792
  </script>
@@ -94,6 +94,7 @@
94
94
  :showOpenMapButton="showOpenMapButton"
95
95
  :showPathwayFilter="showPathwayFilter"
96
96
  :externalLegends="externalLegends"
97
+ :tooltipContentProvider="tooltipContentProvider"
97
98
  />
98
99
 
99
100
  <!-- multiflatmap-error -->
@@ -893,6 +894,14 @@ export default {
893
894
  return []
894
895
  },
895
896
  },
897
+ /**
898
+ * A function that provides custom tooltip HTML content for path features.
899
+ * Passed through to each FlatmapVuer instance.
900
+ */
901
+ tooltipContentProvider: {
902
+ type: Function,
903
+ default: null,
904
+ },
896
905
  },
897
906
  data: function () {
898
907
  return {
@@ -0,0 +1,125 @@
1
+ const SOMA_MARKER_COLOUR = '#1b7f6b'
2
+ const SOMA_MARKER_SECONDARY_COLOUR = '#f4f1de'
3
+ const SOMA_MARKER_TEXT_COLOUR = '#12312b'
4
+ const SOMA_MARKER_MAX_COUNT_LABEL = '99+'
5
+
6
+ const normalizeSomaLocations = function (somaLocations) {
7
+ return (Array.isArray(somaLocations) ? somaLocations : [])
8
+ .map((item) => ({
9
+ label: String(item?.label || '').trim(),
10
+ curie: String(item?.curie || '').trim(),
11
+ count: Number(item?.count || 0),
12
+ }))
13
+ .filter((item) => item.curie && item.count > 0)
14
+ }
15
+
16
+ const formatCount = function (count) {
17
+ if (count > 99) return SOMA_MARKER_MAX_COUNT_LABEL
18
+ return String(count)
19
+ }
20
+
21
+ const createSomaLocationMarkerElement = function (somaLocation) {
22
+ const wrapper = document.createElement('div')
23
+ wrapper.className = 'flatmapvuer-soma-location-marker'
24
+ wrapper.title = somaLocation.label || somaLocation.curie
25
+ wrapper.style.width = '34px'
26
+ wrapper.style.height = '48px'
27
+ wrapper.style.display = 'flex'
28
+ wrapper.style.alignItems = 'center'
29
+ wrapper.style.justifyContent = 'center'
30
+ wrapper.style.transform = 'translate(-50%, -100%)'
31
+
32
+ const countLabel = formatCount(somaLocation.count)
33
+ wrapper.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 27 42" width="34" height="48" aria-hidden="true">
34
+ <ellipse style="fill: rgb(0, 0, 0); fill-opacity: 0.18;" cx="12" cy="36" rx="8" ry="4"/>
35
+ <path d="M12.25.25a12.254 12.254 0 0 0-12 12.494
36
+ c0 6.444 6.488 12.109 11.059 22.564.549 1.256 1.333 1.256 1.882 0
37
+ C17.762 24.853 24.25 19.186 24.25 12.744A12.254 12.254 0 0 0 12.25.25Z"
38
+ style="fill:${SOMA_MARKER_COLOUR};stroke:${SOMA_MARKER_SECONDARY_COLOUR};stroke-width:1.1"/>
39
+ <circle cx="12.5" cy="12.5" r="8" fill="${SOMA_MARKER_SECONDARY_COLOUR}"/>
40
+ <text x="12.5" y="16.1" text-anchor="middle" style="font-size:7.5px;font-weight:700;fill:${SOMA_MARKER_TEXT_COLOUR};font-family:Arial, sans-serif">${countLabel}</text>
41
+ </svg>`
42
+
43
+ return wrapper
44
+ }
45
+
46
+ const resolveMarkerPlacement = function (mapImp, curie) {
47
+ const featureIds = Array.isArray(mapImp?.modelFeatureIds?.(curie)) ? mapImp.modelFeatureIds(curie) : []
48
+ const placements = []
49
+ const seenFeatureUris = new Set()
50
+
51
+ for (const featureId of featureIds) {
52
+ const annotation = mapImp.annotation(featureId)
53
+ const featureUri = String(annotation?.uri || '').trim()
54
+ if (featureUri && !seenFeatureUris.has(featureUri)) {
55
+ seenFeatureUris.add(featureUri)
56
+ placements.push({
57
+ type: 'feature-uri',
58
+ value: featureUri,
59
+ })
60
+ }
61
+ }
62
+
63
+ // Fall back to model placement only when no feature URI can be resolved.
64
+ if (!placements.length && featureIds.length) {
65
+ placements.push({
66
+ type: 'model',
67
+ value: curie,
68
+ })
69
+ }
70
+
71
+ return placements
72
+ }
73
+
74
+ export const attachSomaLocationMarkerMethods = function (mapImp) {
75
+ if (!mapImp || mapImp.addSomaLocationMarkers) {
76
+ return mapImp
77
+ }
78
+
79
+ let markerIds = []
80
+
81
+ mapImp.clearSomaLocationMarkers = function () {
82
+ markerIds.forEach((markerId) => {
83
+ mapImp.removeMarker(markerId)
84
+ })
85
+ markerIds = []
86
+ }
87
+
88
+ mapImp.addSomaLocationMarkers = function (somaLocations) {
89
+ const normalizedSomaLocations = normalizeSomaLocations(somaLocations)
90
+ const placedSomaLocations = []
91
+
92
+ mapImp.clearSomaLocationMarkers()
93
+
94
+ normalizedSomaLocations.forEach((somaLocation) => {
95
+ const placements = resolveMarkerPlacement(mapImp, somaLocation.curie)
96
+ if (!placements.length) return
97
+
98
+ let somaLocationPlaced = false
99
+ placements.forEach((placement) => {
100
+ const markerElement = createSomaLocationMarkerElement(somaLocation)
101
+ let addedMarkerIds = []
102
+
103
+ if (placement.type === 'feature-uri') {
104
+ addedMarkerIds = mapImp.addMarkerByFeatureUri(placement.value, { element: markerElement })
105
+ } else if (placement.type === 'model') {
106
+ const markerId = mapImp.addMarker(placement.value, { element: markerElement })
107
+ addedMarkerIds = markerId > -1 ? [markerId] : []
108
+ }
109
+
110
+ if (!addedMarkerIds.length) return
111
+
112
+ markerIds.push(...addedMarkerIds)
113
+ somaLocationPlaced = true
114
+ })
115
+
116
+ if (somaLocationPlaced) {
117
+ placedSomaLocations.push(somaLocation)
118
+ }
119
+ })
120
+
121
+ return placedSomaLocations
122
+ }
123
+
124
+ return mapImp
125
+ }