@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/dist/flatmapvuer.js +1249 -1116
- package/dist/flatmapvuer.umd.cjs +18 -10
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/FlatmapVuer.vue +95 -1
- package/src/components/MultiFlatmapVuer.vue +9 -0
- package/src/services/somaLocationMarkers.js +125 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|