diff --git a/src/components/FlatmapVuer.vue b/src/components/FlatmapVuer.vue index 8944c0d5..39c3fe1b 100644 --- a/src/components/FlatmapVuer.vue +++ b/src/components/FlatmapVuer.vue @@ -11,7 +11,11 @@ style="height: 100%; width: 100%; position: relative; overflow-y: none" > -
+
@@ -25,7 +29,7 @@ :visible="hoverVisibilities[7].value" ref="warningPopover" > - + + + +
- +
@@ -196,23 +272,14 @@ Please use `const` to assign meaningful names to them... content="Zoom out" placement="top-end" :teleported="false" - trigger="manual" + :auto-close="1300" width="70" popper-class="flatmap-popper" - :visible="hoverVisibilities[2].value" ref="zoomOutPopover" > @@ -238,16 +305,14 @@ Please use `const` to assign meaningful names to them... @mouseover="showTooltip(3)" @mouseout="hideTooltip(3)" > - + @@ -636,24 +711,24 @@ import ResizeSensor from 'css-element-queries/src/ResizeSensor' import flatmap from '../services/flatmapLoader.js' import { AnnotationService } from '@abi-software/sparc-annotation' import { mapState } from 'pinia' -import { useMainStore } from '@/store/index' +import { useMainStore } from '../store/index.js' import { fetchLabels, DrawToolbar, Tooltip, TreeControls, - getFlatmapFilterOptions + getFlatmapFilterOptions, } from '@abi-software/map-utilities' import '@abi-software/map-utilities/dist/style.css' import EventBus from './EventBus.js' import FlatmapError from './FlatmapError.vue' -const ERROR_MESSAGE = 'cannot be found on the map.'; +const ERROR_MESSAGE = 'cannot be found on the map.' const centroid = (geometry) => { - let featureGeometry = { lng: 0, lat: 0, } + let featureGeometry = { lng: 0, lat: 0 } let coordinates - if (geometry.type === "Polygon") { + if (geometry.type === 'Polygon') { if (geometry.coordinates.length) { coordinates = geometry.coordinates[0] } @@ -727,6 +802,7 @@ export default { ElIconArrowDown, ElIconArrowLeft, DrawToolbar, + DynamicLegends, FlatmapError, }, beforeCreate: function () { @@ -738,7 +814,7 @@ export default { setup(props) { let annotator = inject('$annotator') if (!annotator) { - annotator = markRaw(new AnnotationService(`${props.flatmapAPI}annotator`)); + annotator = markRaw(new AnnotationService(`${props.flatmapAPI}annotator`)) provide('$annotator', annotator) } return { annotator } @@ -763,17 +839,17 @@ export default { if (this.mapImp) { if (this.mapImp.contextLost) { if (filter) { - this.filterToRestore = markRaw(JSON.parse(JSON.stringify(filter))); + this.filterToRestore = markRaw(JSON.parse(JSON.stringify(filter))) } else { - this.filterToRestore = undefined; + this.filterToRestore = undefined } } else { if (filter) { - this.mapImp.setVisibilityFilter(filter); + this.mapImp.setVisibilityFilter(filter) } else { - this.mapImp.clearVisibilityFilter(); + this.mapImp.clearVisibilityFilter() } - this.filterToRestore = undefined; + this.filterToRestore = undefined } } }, @@ -782,7 +858,7 @@ export default { * Function to manually send aborted signal when annotation tooltip popup or sidebar tab closed. */ manualAbortedOnClose: function () { - if (this.annotationSidebar) this.$emit("annotation-close") + if (this.annotationSidebar) this.$emit('annotation-close') this.closeTooltip() this.annotationEventCallback({}, { type: 'aborted' }) this.initialiseDrawing() @@ -803,12 +879,14 @@ export default { */ cancelDrawnFeature: function () { if (this.isValidDrawnCreated) { - if (this.annotationSidebar) this.$emit("annotation-close") + if (this.annotationSidebar) this.$emit('annotation-close') this.closeTooltip() - this.annotationEntry = [{ - ...this.drawnCreatedEvent.feature, - resourceId: this.serverURL, - }] + this.annotationEntry = [ + { + ...this.drawnCreatedEvent.feature, + resourceId: this.serverURL, + }, + ] this.rollbackAnnotationEvent() this.initialiseDrawing() } @@ -824,7 +902,11 @@ export default { const numericId = Number(value) const featureObject = numericId ? this.mapImp.featureProperties(numericId) - : { feature: this.existDrawnFeatures.find(feature => feature.id === value.trim()) }; + : { + feature: this.existDrawnFeatures.find( + (feature) => feature.id === value.trim() + ), + } let payload = { feature: featureObject } this.checkAndCreatePopups([payload], false) } else { @@ -855,7 +937,7 @@ export default { * @arg {String} `name` */ toolbarEvent: function (type, name) { - if (this.isValidDrawnCreated) return; + if (this.isValidDrawnCreated) return this.manualAbortedOnClose() this.doubleClickedFeature = false // Deselect any feature when draw mode/tool is changed @@ -866,7 +948,10 @@ export default { // Remove any unsubmitted drawn this.cancelDrawnFeature() if (name) { - const tool = name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) + const tool = name.replace( + /[A-Z]/g, + (letter) => `_${letter.toLowerCase()}` + ) this.changeAnnotationDrawMode({ mode: `draw${tool}` }) } this.activeDrawTool = name @@ -884,7 +969,7 @@ export default { if (data.feature.feature.geometry.type !== 'Point') { this.changeAnnotationDrawMode({ mode: 'direct_select', - options: { featureId: data.feature.feature.id } + options: { featureId: data.feature.feature.id }, }) this.modifyAnnotationFeature() } @@ -893,7 +978,7 @@ export default { } else if (this.activeDrawMode === 'Delete') { this.changeAnnotationDrawMode({ mode: 'simple_select', - options: { featureIds: [data.feature.feature.id] } + options: { featureIds: [data.feature.feature.id] }, }) this.modifyAnnotationFeature() } @@ -908,7 +993,9 @@ export default { type: 'connectivity', source: features[0], target: features[features.length - 1], - intermediates: features.filter((f, index) => index !== 0 && index !== features.length - 1), + intermediates: features.filter( + (f, index) => index !== 0 && index !== features.length - 1 + ), } this.annotationEntry[0].body = body } @@ -932,12 +1019,12 @@ export default { this.mapImp.clearAnnotationFeature() } }, - forceContextLoss: function() { + forceContextLoss: function () { if (this.mapImp && !this.mapImp.contextLost && !this.loading) { this.mapImp.forceContextLoss() } }, - forceContextRestore: function() { + forceContextRestore: function () { if (this.mapImp) { this.flatmapError = null this.mapImp.forceContextRestore() @@ -979,22 +1066,35 @@ export default { commitAnnotationEvent: function (annotation) { if (this.mapImp) { if (this.offlineAnnotationEnabled) { - this.offlineAnnotations = JSON.parse(sessionStorage.getItem('anonymous-annotation')) || [] + this.offlineAnnotations = + JSON.parse(sessionStorage.getItem('anonymous-annotation')) || [] this.offlineAnnotations.push(annotation) if (this.annotationEntry[0].type === 'deleted') { - this.offlineAnnotations = this.offlineAnnotations.filter((offline) => { - return offline.resource !== this.serverURL || offline.item.id !== annotation.item.id - }) + this.offlineAnnotations = this.offlineAnnotations.filter( + (offline) => { + return ( + offline.resource !== this.serverURL || + offline.item.id !== annotation.item.id + ) + } + ) } - sessionStorage.setItem('anonymous-annotation', JSON.stringify(this.offlineAnnotations)) + sessionStorage.setItem( + 'anonymous-annotation', + JSON.stringify(this.offlineAnnotations) + ) } - if (['created', 'updated', 'deleted'].includes(this.annotationEntry[0].type)) { + if ( + ['created', 'updated', 'deleted'].includes( + this.annotationEntry[0].type + ) + ) { this.featureAnnotationSubmitted = true this.mapImp.commitAnnotationEvent(this.annotationEntry[0]) - if (annotation.body.comment === "Position Updated") { + if (annotation.body.comment === 'Position Updated') { this.annotationEntry[0].positionUpdated = false } else if (this.annotationEntry[0].type === 'deleted') { - if (this.annotationSidebar) this.$emit("annotation-close") + if (this.annotationSidebar) this.$emit('annotation-close') this.closeTooltip() // Only delete need, keep the annotation tooltip/sidebar open if created/updated this.annotationEntry = [] @@ -1009,17 +1109,29 @@ export default { * @arg {String} `userId`, * @arg {String} `participated` */ - fetchAnnotatedItemIds: async function (userId = undefined, participated = undefined) { + fetchAnnotatedItemIds: async function ( + userId = undefined, + participated = undefined + ) { let annotatedItemIds if (this.offlineAnnotationEnabled) { - this.offlineAnnotations = JSON.parse(sessionStorage.getItem('anonymous-annotation')) || [] - annotatedItemIds = this.offlineAnnotations.filter((offline) => { - return offline.resource === this.serverURL - }).map(offline => offline.item.id) + this.offlineAnnotations = + JSON.parse(sessionStorage.getItem('anonymous-annotation')) || [] + annotatedItemIds = this.offlineAnnotations + .filter((offline) => { + return offline.resource === this.serverURL + }) + .map((offline) => offline.item.id) } else { - annotatedItemIds = await this.annotator.annotatedItemIds(this.userToken, this.serverURL, userId, participated) + annotatedItemIds = await this.annotator.annotatedItemIds( + this.userToken, + this.serverURL, + userId, + participated + ) // The annotator has `resource` and `items` fields - if ('resource' in annotatedItemIds) annotatedItemIds = annotatedItemIds.itemIds + if ('resource' in annotatedItemIds) + annotatedItemIds = annotatedItemIds.itemIds } return annotatedItemIds }, @@ -1044,13 +1156,23 @@ export default { fetchDrawnFeatures: async function (userId, participated) { let drawnFeatures if (this.offlineAnnotationEnabled) { - this.offlineAnnotations = JSON.parse(sessionStorage.getItem('anonymous-annotation')) || [] - drawnFeatures = this.offlineAnnotations.filter((offline) => { - return offline.feature && offline.resource === this.serverURL - }).map(offline => offline.feature) + this.offlineAnnotations = + JSON.parse(sessionStorage.getItem('anonymous-annotation')) || [] + drawnFeatures = this.offlineAnnotations + .filter((offline) => { + return offline.feature && offline.resource === this.serverURL + }) + .map((offline) => offline.feature) } else { - const annotatedItemIds = await this.fetchAnnotatedItemIds(userId, participated) - drawnFeatures = await this.annotator.drawnFeatures(this.userToken, this.serverURL, annotatedItemIds) + const annotatedItemIds = await this.fetchAnnotatedItemIds( + userId, + participated + ) + drawnFeatures = await this.annotator.drawnFeatures( + this.userToken, + this.serverURL, + annotatedItemIds + ) // The annotator has `resource` and `features` fields if ('resource' in drawnFeatures) drawnFeatures = drawnFeatures.features } @@ -1066,13 +1188,22 @@ export default { this.clearAnnotationFeature() this.loading = true } - const userId = this.annotationFrom === 'Anyone' ? - undefined : this.authorisedUser.orcid ? - this.authorisedUser.orcid : '0000-0000-0000-0000' - const participated = this.annotationFrom === 'Anyone' ? - undefined : this.annotationFrom === 'Me' ? - true : false - const drawnFeatures = await this.fetchDrawnFeatures(userId, participated) + const userId = + this.annotationFrom === 'Anyone' + ? undefined + : this.authorisedUser.orcid + ? this.authorisedUser.orcid + : '0000-0000-0000-0000' + const participated = + this.annotationFrom === 'Anyone' + ? undefined + : this.annotationFrom === 'Me' + ? true + : false + const drawnFeatures = await this.fetchDrawnFeatures( + userId, + participated + ) this.existDrawnFeatures = drawnFeatures this.loading = false if (!this.featureAnnotationSubmitted) { @@ -1111,7 +1242,10 @@ export default { * Function to emit offline annotation enabled status */ emitOfflineAnnotationUpdate: function () { - this.$emit('update-offline-annotation-enabled', this.offlineAnnotationEnabled); + this.$emit( + 'update-offline-annotation-enabled', + this.offlineAnnotationEnabled + ) }, /** * @public @@ -1193,15 +1327,20 @@ export default { entityLabels.forEach((entityLabel) => { let enabled = true if (state) { - enabled = state.checkAll ? true : state.checked.includes(entityLabel.taxon) + enabled = state.checkAll + ? true + : state.checked.includes(entityLabel.taxon) } - this.taxonConnectivity.push({...entityLabel, enabled}); + this.taxonConnectivity.push({ ...entityLabel, enabled }) if (this.mapImp) { - this.mapImp.enableConnectivityByTaxonIds(entityLabel.taxon, enabled) + this.mapImp.enableConnectivityByTaxonIds( + entityLabel.taxon, + enabled + ) } - }); + }) } - }); + }) }, /** * @public @@ -1236,19 +1375,19 @@ export default { }, setInitMapState: function () { if (this.mapImp) { - const map = this.mapImp.map; - const bounds = this.mapImp.options.bounds; + const map = this.mapImp.map + const bounds = this.mapImp.options.bounds const initBounds = [ [bounds[0], bounds[1]], - [bounds[2], bounds[3]] - ]; + [bounds[2], bounds[3]], + ] - map.setMaxBounds(null); // override default - map.setRenderWorldCopies(false); + map.setMaxBounds(null) // override default + map.setRenderWorldCopies(false) this.initMapState = markRaw({ initBounds, - }); + }) } }, /** @@ -1259,17 +1398,17 @@ export default { resetView: function () { if (this.mapImp) { // fit to window - const map = this.mapImp.map; - const { initBounds } = this.initMapState; + const map = this.mapImp.map + const { initBounds } = this.initMapState // reset rotation map.resetNorthPitch({ animate: false, - }); + }) if (initBounds) { // reset zoom and position map.fitBounds(initBounds, { - animate: false - }); + animate: false, + }) } if (this.$refs.skcanSelection) { this.$refs.skcanSelection.reset() @@ -1306,7 +1445,7 @@ export default { } }, onSelectionsDataChanged: function (data) { - this.$emit('pathway-selection-changed', data); + this.$emit('pathway-selection-changed', data) }, /** * // Currently not in use @@ -1349,76 +1488,94 @@ export default { retrieveConnectedPaths: async function (payload, options = {}) { // query all connected paths from flatmap if (this.mapImp) { - let connectedPaths = []; - let connectedTarget = options.target?.length ? options.target : []; + let connectedPaths = [] + let connectedTarget = options.target?.length ? options.target : [] // The line below is to get the path features from the geojson ids - const nodeFeatureIds = [...this.mapImp.pathModelNodes(payload)]; - const pathsOfEntities = await this.mapImp.queryPathsForFeatures(payload); + const nodeFeatureIds = [...this.mapImp.pathModelNodes(payload)] + const pathsOfEntities = await this.mapImp.queryPathsForFeatures(payload) if (nodeFeatureIds.length) { if (!connectedTarget.length) { - const connectedType = options.type?.length ? options.type : ["all"]; - const connectivity = await this.flatmapQueries.queryForConnectivityNew(this.mapImp, payload[0]); - const originsFlat = connectivity?.ids?.dendrites.flat(Infinity); - const componentsFlat = connectivity?.ids?.components.flat(Infinity); - const destinationsFlat = connectivity?.ids?.axons.flat(Infinity); - let connected = []; - if (connectedType.includes("origins")) connected.push(...originsFlat); - if (connectedType.includes("components")) connected.push(...componentsFlat); - if (connectedType.includes("destinations")) connected.push(...destinationsFlat); - if (connectedType.includes("all")) connected.push(...originsFlat, ...componentsFlat, ...destinationsFlat); - connectedTarget = [...new Set(connected)]; + const connectedType = options.type?.length ? options.type : ['all'] + const connectivity = + await this.flatmapQueries.queryForConnectivityNew( + this.mapImp, + payload[0] + ) + const originsFlat = connectivity?.ids?.dendrites.flat(Infinity) + const componentsFlat = connectivity?.ids?.components.flat(Infinity) + const destinationsFlat = connectivity?.ids?.axons.flat(Infinity) + let connected = [] + if (connectedType.includes('origins')) + connected.push(...originsFlat) + if (connectedType.includes('components')) + connected.push(...componentsFlat) + if (connectedType.includes('destinations')) + connected.push(...destinationsFlat) + if (connectedType.includes('all')) + connected.push( + ...originsFlat, + ...componentsFlat, + ...destinationsFlat + ) + connectedTarget = [...new Set(connected)] } // Loop through the node features and check if we have certain nodes nodeFeatureIds.forEach((featureId) => { // Get the paths from each node feature - const pathsL2 = this.mapImp.nodePathModels(featureId); + const pathsL2 = this.mapImp.nodePathModels(featureId) pathsL2.forEach((path) => { // nodes of the second level path - const nodeFeatureIdsL2 = this.mapImp.pathModelNodes(path); + const nodeFeatureIdsL2 = this.mapImp.pathModelNodes(path) const nodeModelsL2 = nodeFeatureIdsL2.map((featureIdL2) => { - return this.mapImp.featureProperties(featureIdL2).models; - }); - const intersection = connectedTarget.filter(element => nodeModelsL2.includes(element)); - if (intersection.length && !connectedPaths.includes(path)) connectedPaths.push(path); - }); - }); + return this.mapImp.featureProperties(featureIdL2).models + }) + const intersection = connectedTarget.filter((element) => + nodeModelsL2.includes(element) + ) + if (intersection.length && !connectedPaths.includes(path)) + connectedPaths.push(path) + }) + }) } else if (pathsOfEntities.length) { if (connectedTarget.length) { pathsOfEntities.forEach((path) => { - const nodeFeatureIds = this.mapImp.pathModelNodes(path); + const nodeFeatureIds = this.mapImp.pathModelNodes(path) const nodeModels = nodeFeatureIds.map((featureId) => { - return this.mapImp.featureProperties(featureId).models; - }); - const intersection = connectedTarget.filter(element => nodeModels.includes(element)); - if (intersection.length && !connectedPaths.includes(path)) connectedPaths.push(path); - }); + return this.mapImp.featureProperties(featureId).models + }) + const intersection = connectedTarget.filter((element) => + nodeModels.includes(element) + ) + if (intersection.length && !connectedPaths.includes(path)) + connectedPaths.push(path) + }) } else { - connectedPaths = pathsOfEntities; + connectedPaths = pathsOfEntities } } - connectedPaths = [...new Set([...connectedPaths, ...payload])]; - return connectedPaths; + connectedPaths = [...new Set([...connectedPaths, ...payload])] + return connectedPaths } }, - resetMapFilter: function() { - const alert = this.mapFilters.alert; - let filter; - const isPathways = { 'tile-layer': 'pathways' }; - const notPathways = { NOT: isPathways }; + resetMapFilter: function () { + const alert = this.mapFilters.alert + let filter + const isPathways = { 'tile-layer': 'pathways' } + const notPathways = { NOT: isPathways } if (alert.with && !alert.without) { // Show pathways with alert filter = { - OR: [notPathways, { AND: [isPathways, { HAS: 'alert' }] }] - }; + OR: [notPathways, { AND: [isPathways, { HAS: 'alert' }] }], + } } else if (!alert.with && alert.without) { // Show pathways without alert filter = { - OR: [notPathways, { AND: [isPathways, { NOT: { HAS: 'alert' } }] }] - }; + OR: [notPathways, { AND: [isPathways, { NOT: { HAS: 'alert' } }] }], + } } else if (!alert.with && !alert.without) { // Hide all pathways - filter = notPathways; + filter = notPathways } this.setVisibilityFilter(filter) }, @@ -1431,16 +1588,17 @@ export default { alertMouseEnterEmitted: function (payload) { if (this.mapImp) { if (payload.value) { - let filter; - const isPathways = { 'tile-layer': 'pathways' }; - const notPathways = { NOT: isPathways }; + let filter + const isPathways = { 'tile-layer': 'pathways' } + const notPathways = { NOT: isPathways } - if (payload.key === "alert" || payload.key === "withoutAlert") { - const hasAlert = payload.key === "alert" ? - { HAS: 'alert' } : - { NOT: { HAS: 'alert' } }; + if (payload.key === 'alert' || payload.key === 'withoutAlert') { + const hasAlert = + payload.key === 'alert' + ? { HAS: 'alert' } + : { NOT: { HAS: 'alert' } } - filter = { OR: [notPathways, { AND: [isPathways, hasAlert] }] }; + filter = { OR: [notPathways, { AND: [isPathways, hasAlert] }] } } this.setVisibilityFilter(filter) } else { @@ -1456,13 +1614,13 @@ export default { */ alertSelected: function (payload) { if (this.mapImp) { - if (payload.key === "alert") { + if (payload.key === 'alert') { if (payload.value) { this.mapFilters.alert.with = true } else { this.mapFilters.alert.with = false } - } else if (payload.key === "withoutAlert") { + } else if (payload.key === 'withoutAlert') { if (payload.value) { this.mapFilters.alert.without = true } else { @@ -1563,7 +1721,7 @@ export default { clearTimeout(this.taxonLeaveDelay) let gid = this.mapImp.taxonFeatureIds(payload.key) this.mapImp.enableConnectivityByTaxonIds(payload.key, payload.value) // make sure path is visible - this.mapImp.zoomToGeoJSONFeatures(gid, {noZoomIn: true}) + this.mapImp.zoomToGeoJSONFeatures(gid, { noZoomIn: true }) } else { this.taxonLeaveDelay = setTimeout(() => { // reset visibility of paths @@ -1572,7 +1730,7 @@ export default { let show = payload.checked.includes(item.taxon) this.mapImp.enableConnectivityByTaxonIds(item.taxon, show) }) - }, 1000); + }, 1000) } } }, @@ -1634,15 +1792,23 @@ export default { else this.featureAnnotationSubmitted = false this.annotationEntry = [] } else if (data.type === 'modeChanged') { - if (data.feature.mode === 'direct_select') this.doubleClickedFeature = true - if (this.annotationSidebar && data.feature.mode === 'simple_select' && this.activeDrawMode === 'Deleted') { + if (data.feature.mode === 'direct_select') + this.doubleClickedFeature = true + if ( + this.annotationSidebar && + data.feature.mode === 'simple_select' && + this.activeDrawMode === 'Deleted' + ) { this.annotationEventCallback({}, { type: 'aborted' }) } } else if (data.type === 'selectionChanged') { - this.selectedDrawnFeature = data.feature.features.length === 0 ? - undefined : data.feature.features[0] + this.selectedDrawnFeature = + data.feature.features.length === 0 + ? undefined + : data.feature.features[0] payload.feature.feature = this.selectedDrawnFeature - if (!this.activeDrawTool) { // Make sure dialog content doesn't change + if (!this.activeDrawTool) { + // Make sure dialog content doesn't change this.connectionEntry = {} // For exist drawn annotation features if (this.selectedDrawnFeature) { @@ -1656,11 +1822,16 @@ export default { } this.annotationDrawModeEvent(payload) } else { - if (this.annotationSidebar && this.previousEditEvent.type === 'updated') { - this.annotationEntry = [{ - ...this.previousEditEvent, - resourceId: this.serverURL - }] + if ( + this.annotationSidebar && + this.previousEditEvent.type === 'updated' + ) { + this.annotationEntry = [ + { + ...this.previousEditEvent, + resourceId: this.serverURL, + }, + ] this.annotationEventCallback({}, { type: 'aborted' }) } this.previousEditEvent = {} @@ -1671,7 +1842,9 @@ export default { if (data.type === 'updated' && data.feature.action) { data.positionUpdated = data.feature.action === 'move' } - const feature = this.mapImp.refreshAnnotationFeatureGeometry(data.feature) + const feature = this.mapImp.refreshAnnotationFeatureGeometry( + data.feature + ) payload.feature.feature = feature // NB. this might now be `null` if user has deleted it (before OK/Submit) // so maybe then no `service.addAnnotation` ?? @@ -1725,36 +1898,39 @@ export default { const biologicalSex = this.biologicalSex const featuresAlert = data.alert const taxons = this.getTaxons(data) - let payload = [{ - dataset: data.dataset, - biologicalSex: biologicalSex, - taxonomy: taxonomy, - resource: resource, - label: label, - feature: data, - userData: args, - eventType: eventType, - provenanceTaxonomy: taxons, - alert: featuresAlert - }] + let payload = [ + { + dataset: data.dataset, + biologicalSex: biologicalSex, + taxonomy: taxonomy, + resource: resource, + label: label, + feature: data, + protocol: this.selectedSimulation, + userData: args, + eventType: eventType, + provenanceTaxonomy: taxons, + alert: featuresAlert, + }, + ] if (eventType === 'click') { // If multiple paths overlap at the click location, // `data` is an object with numeric keys for each feature (e.g., {0: {...}, 1: {...}, ..., mapUUID: '...'}). // If only one feature or path is clicked, // `data` is a single object (e.g., {featureId: '...', mapUUID: '...'}). - const singleSelection = !data[0]; + const singleSelection = !data[0] if (!singleSelection) { payload = [] const mapuuid = data.mapUUID - const seenIds = new Set(); + const seenIds = new Set() for (let [key, value] of Object.entries(data)) { if (key !== 'mapUUID') { const id = value.featureId const label = value.label const resource = [value.models] const taxons = this.getTaxons(value) - if (seenIds.has(id)) continue; - seenIds.add(id); + if (seenIds.has(id)) continue + seenIds.add(id) payload.push({ dataset: value.dataset, biologicalSex: biologicalSex, @@ -1766,13 +1942,13 @@ export default { eventType: eventType, provenanceTaxonomy: taxons, alert: value.alert, - mapUUID: mapuuid + mapUUID: mapuuid, }) } } } const clickedItem = singleSelection ? data : data[0] - this.setConnectivityDataSource(this.viewingMode, clickedItem); + this.setConnectivityDataSource(this.viewingMode, clickedItem) if (this.viewingMode === 'Neuron Connection') { // do nothing here // the method to highlight paths is moved to checkAndCreatePopups function @@ -1781,20 +1957,33 @@ export default { // This is for annotation mode - draw connectivity between features/paths if (this.activeDrawTool && !this.isValidDrawnCreated) { // Check if flatmap features or existing drawn features - const validDrawnFeature = clickedItem.featureId || this.existDrawnFeatures.find( - (feature) => feature.id === clickedItem.id - ) + const validDrawnFeature = + clickedItem.featureId || + this.existDrawnFeatures.find( + (feature) => feature.id === clickedItem.id + ) // Only the linestring will have connection if (this.activeDrawTool === 'LineString' && validDrawnFeature) { - const key = clickedItem.featureId ? clickedItem.featureId : clickedItem.id - const nodeLabel = clickedItem.label ? clickedItem.label : `Feature ${clickedItem.id}` + const key = clickedItem.featureId + ? clickedItem.featureId + : clickedItem.id + const nodeLabel = clickedItem.label + ? clickedItem.label + : `Feature ${clickedItem.id}` // Add space before key to make sure properties follows adding order this.connectionEntry[` ${key}`] = Object.assign( { label: nodeLabel }, Object.fromEntries( Object.entries(clickedItem) - .filter(([key]) => ['featureId', 'models'].includes(key)) - .map(([key, value]) => [(key === 'featureId') ? 'id' : key, value]))) + .filter(([key]) => + ['featureId', 'models'].includes(key) + ) + .map(([key, value]) => [ + key === 'featureId' ? 'id' : key, + value, + ]) + ) + ) } } } @@ -1803,7 +1992,10 @@ export default { if (data && data.type !== 'marker' && !this.activeDrawTool) { this.checkAndCreatePopups(payload) } - } else if (eventType === 'mouseenter' && this.viewingMode !== 'Neuron Connection') { + } else if ( + eventType === 'mouseenter' && + this.viewingMode !== 'Neuron Connection' + ) { this.currentHover = data.models ? data.models : '' } @@ -1823,11 +2015,13 @@ export default { setConnectivityDataSource: function (viewingMode, data) { // Exploration mode, only path click will be used as data source if (viewingMode === 'Exploration') { - this.connectivityDataSource = data.models?.startsWith('ilxtr:') ? data.models : ''; + this.connectivityDataSource = data.models?.startsWith('ilxtr:') + ? data.models + : '' } else { // Other modes, it can be anything // (annotation drawing doesn't have featureId or models) - this.connectivityDataSource = data.featureId || data.id; + this.connectivityDataSource = data.featureId || data.id } }, /** @@ -1850,12 +2044,12 @@ export default { removeActiveTooltips: function () { // Remove active tooltip/popup on map if (this.mapImp) { - this.mapImp.removePopup(); + this.mapImp.removePopup() } // Fallback: remove any existing toolitp on DOM - const tooltips = this.$el.querySelectorAll('.flatmap-tooltip-popup'); - tooltips.forEach((tooltip) => tooltip.remove()); + const tooltips = this.$el.querySelectorAll('.flatmap-tooltip-popup') + tooltips.forEach((tooltip) => tooltip.remove()) }, /** * Function to create tooltip for the provided connectivity data. @@ -1864,28 +2058,24 @@ export default { createTooltipForConnectivity: function (connectivityData, geojsonId) { // combine all labels to show together // content type must be DOM object to use HTML - const labelsContainer = document.createElement('div'); - labelsContainer.classList.add('flatmap-feature-label'); + const labelsContainer = document.createElement('div') + labelsContainer.classList.add('flatmap-feature-label') connectivityData.forEach((connectivity, i) => { - const { label } = connectivity; - labelsContainer.append(capitalise(label)); + const { label } = connectivity + labelsContainer.append(capitalise(label)) - if ((i + 1) < connectivityData.length) { - const hr = document.createElement('hr'); - labelsContainer.appendChild(hr); + if (i + 1 < connectivityData.length) { + const hr = document.createElement('hr') + labelsContainer.appendChild(hr) } - }); + }) - this.mapImp.showPopup( - geojsonId, - labelsContainer, - { - className: 'custom-popup flatmap-tooltip-popup', - positionAtLastClick: false, - preserveSelection: true, - } - ); + this.mapImp.showPopup(geojsonId, labelsContainer, { + className: 'custom-popup flatmap-tooltip-popup', + positionAtLastClick: false, + preserveSelection: true, + }) }, /** * Function to show connectivity tooltips on the map @@ -1893,129 +2083,140 @@ export default { * @arg {Object} `payload` */ showConnectivityTooltips: function (payload) { - const { connectivityInfo, data } = payload; - const featuresToHighlight = []; - const geojsonHighlights = []; - const connectivityData = []; - const errorData = []; + const { connectivityInfo, data } = payload + const featuresToHighlight = [] + const geojsonHighlights = [] + const connectivityData = [] + const errorData = [] // to keep the highlighted path on map if (connectivityInfo && connectivityInfo.featureId) { - featuresToHighlight.push(...connectivityInfo.featureId); + featuresToHighlight.push(...connectivityInfo.featureId) } if (this.mapImp) { // search the features on the map first data.forEach((connectivity) => { - const response = this.mapImp.search(connectivity.id); + const response = this.mapImp.search(connectivity.id) if (response?.results.length) { - const featureId = response?.results[0].featureId; - connectivityData.push({ featureId, ...connectivity }); + const featureId = response?.results[0].featureId + connectivityData.push({ featureId, ...connectivity }) } else { - errorData.push(connectivity); + errorData.push(connectivity) } - }); + }) if (connectivityData.length) { - let geojsonId = connectivityData[0].featureId; + let geojsonId = connectivityData[0].featureId this.mapImp.annotations.forEach((annotation) => { - const anatomicalNodes = annotation['anatomical-nodes']; + const anatomicalNodes = annotation['anatomical-nodes'] if (anatomicalNodes) { - const anatomicalNodesString = anatomicalNodes.join(''); - const foundItem = connectivityData.every((item) => - anatomicalNodesString.indexOf(item.id) !== -1 - ); + const anatomicalNodesString = anatomicalNodes.join('') + const foundItem = connectivityData.every( + (item) => anatomicalNodesString.indexOf(item.id) !== -1 + ) if (foundItem) { - geojsonId = annotation.featureId; - geojsonHighlights.push(geojsonId); + geojsonId = annotation.featureId + geojsonHighlights.push(geojsonId) } } - }); + }) - this.createTooltipForConnectivity(connectivityData, geojsonId); + this.createTooltipForConnectivity(connectivityData, geojsonId) } else { // Close all tooltips on the current flatmap element - this.removeActiveTooltips(); + this.removeActiveTooltips() } // Emit error message for connectivity - this.emitConnectivityError(errorData); + this.emitConnectivityError(errorData) // highlight all available features const connectivityFeatures = featuresToHighlight.reduce((arr, path) => { - const connectivityObj = this.mapImp.pathways.paths[path]; - const connectivities = connectivityObj ? connectivityObj.connectivity : null; + const connectivityObj = this.mapImp.pathways.paths[path] + const connectivities = connectivityObj + ? connectivityObj.connectivity + : null if (connectivities) { - const flatFeatures = connectivities.flat(Infinity); - arr.push(...flatFeatures); + const flatFeatures = connectivities.flat(Infinity) + arr.push(...flatFeatures) } - return arr; - }, []); - const uniqueConnectivityFeatures = [...new Set(connectivityFeatures)]; - const combinedFeatures = [...featuresToHighlight, ...uniqueConnectivityFeatures]; - const featureIdsToHighlight = this.mapImp.modelFeatureIdList(combinedFeatures); + return arr + }, []) + const uniqueConnectivityFeatures = [...new Set(connectivityFeatures)] + const combinedFeatures = [ + ...featuresToHighlight, + ...uniqueConnectivityFeatures, + ] + const featureIdsToHighlight = + this.mapImp.modelFeatureIdList(combinedFeatures) const allFeaturesToHighlight = [ ...featureIdsToHighlight, - ...geojsonHighlights - ]; + ...geojsonHighlights, + ] - this.mapImp.selectGeoJSONFeatures(allFeaturesToHighlight); + this.mapImp.selectGeoJSONFeatures(allFeaturesToHighlight) } }, showConnectivitiesByReference: function (resource) { this.searchConnectivitiesByReference(resource).then((featureIds) => { - this.mapImp.selectFeatures(featureIds); - }); + this.mapImp.selectFeatures(featureIds) + }) }, searchConnectivitiesByReference: async function (resource) { - const flatmapKnowledge = sessionStorage.getItem('flatmap-knowledge'); - let featureIds = []; + const flatmapKnowledge = sessionStorage.getItem('flatmap-knowledge') + let featureIds = [] if (flatmapKnowledge) { - featureIds = await getReferenceConnectivitiesFromStorage(resource); + featureIds = await getReferenceConnectivitiesFromStorage(resource) } else { - featureIds = await getReferenceConnectivitiesByAPI(this.mapImp, resource, this.flatmapQueries); - } - return featureIds; - }, - getFlatmapKnowledge: function () { - let flatmapKnowledge = []; - const flatmapKnowledgeRaw = sessionStorage.getItem('flatmap-knowledge'); - if (flatmapKnowledgeRaw) { - flatmapKnowledge = JSON.parse(flatmapKnowledgeRaw); + featureIds = await getReferenceConnectivitiesByAPI( + this.mapImp, + resource, + this.flatmapQueries + ) } - return flatmapKnowledge; + return featureIds }, emitConnectivityError: function (errorData) { this.$emit('connectivity-error', { data: { errorData: errorData, errorMessage: ERROR_MESSAGE, - } - }); + }, + }) }, - checkConnectivityTooltipEntry: function(tooltipEntry) { + checkConnectivityTooltipEntry: function (tooltipEntry) { if (tooltipEntry?.length) { - return undefined !== (tooltipEntry.find(entry => entry?.destinations?.length || entry?.components?.length)) + return ( + undefined !== + tooltipEntry.find( + (entry) => entry?.destinations?.length || entry?.components?.length + ) + ) } return false }, changeConnectivitySource: async function (payload) { - const { entry, connectivitySource } = payload; + const { entry, connectivitySource } = payload if (entry.mapId === this.mapImp.id) { - await this.flatmapQueries.queryForConnectivityNew(this.mapImp, entry.featureId[0], connectivitySource); + await this.flatmapQueries.queryForConnectivityNew( + this.mapImp, + entry.featureId[0], + connectivitySource + ) this.tooltipEntry = this.tooltipEntry.map((tooltip) => { if (tooltip.featureId[0] === entry.featureId[0]) { - return this.flatmapQueries.updateTooltipData(tooltip); + return this.flatmapQueries.updateTooltipData(tooltip) } - return tooltip; + return tooltip }) if (this.checkConnectivityTooltipEntry(this.tooltipEntry)) { - this.$emit('connectivity-info-open', this.tooltipEntry); + this.$emit('connectivity-info-open', this.tooltipEntry) } } }, @@ -2028,28 +2229,39 @@ export default { checkAndCreatePopups: async function (data, mapclick = true) { // Call flatmap database to get the connection data if (this.viewingMode === 'Annotation') { - const features = data.filter(d => d.feature).map(d => d.feature) + const features = data.filter((d) => d.feature).map((d) => d.feature) if (features.length > 0) { - if (this.annotationSidebar && this.previousDeletedEvent.type === 'deleted') { - this.annotationEntry = [{ - ...this.previousDeletedEvent, - resourceId: this.serverURL - }] + if ( + this.annotationSidebar && + this.previousDeletedEvent.type === 'deleted' + ) { + this.annotationEntry = [ + { + ...this.previousDeletedEvent, + resourceId: this.serverURL, + }, + ] this.annotationEventCallback({}, { type: 'aborted' }) } this.annotationEntry = [] - features.forEach(feature => { + features.forEach((feature) => { this.annotationEntry.push({ ...feature, resourceId: this.serverURL, - featureId: feature.featureId ? feature.featureId : feature.feature?.id, - offline: this.offlineAnnotationEnabled + featureId: feature.featureId + ? feature.featureId + : feature.feature?.id, + offline: this.offlineAnnotationEnabled, }) - }); + }) // Drawn feature annotationEntry will always have length of 1 if (features[0].feature) { // in drawing or edit/delete mode is on or valid drawn - if (this.activeDrawTool || this.activeDrawMode || this.isValidDrawnCreated) { + if ( + this.activeDrawTool || + this.activeDrawMode || + this.isValidDrawnCreated + ) { this.featureAnnotationSubmitted = false if (this.activeDrawTool) { this.createConnectivityBody() @@ -2063,8 +2275,8 @@ export default { } } else { const featureIds = this.annotationEntry - .filter(annotation => annotation.featureId && annotation.models) - .map(annotation => annotation.models) + .filter((annotation) => annotation.featureId && annotation.models) + .map((annotation) => annotation.models) if (featureIds.length > 0) { this.displayTooltip(featureIds) } @@ -2076,126 +2288,142 @@ export default { // clicking on a connectivity explorer card will be the same as exploration mode // the card should be opened without doing other functions else if (this.viewingMode === 'Neuron Connection' && mapclick) { - const resources = data.map(tooltip => tooltip.resource[0]); + const resources = data.map((tooltip) => tooltip.resource[0]) // filter out paths - const featureId = resources.find(resource => !resource.startsWith('ilxtr:')); + const featureId = resources.find( + (resource) => !resource.startsWith('ilxtr:') + ) if (featureId) { // fallback if it cannot find in anatomical nodes - const transformResources = Array.isArray(resources) ? [...resources] : [resources]; + const transformResources = Array.isArray(resources) + ? [...resources] + : [resources] if (transformResources.length === 1) { - transformResources.push([]); + transformResources.push([]) } - const featureId = data[0].feature?.featureId; - const annotation = this.mapImp.annotations.get(featureId); - const anatomicalNodes = annotation?.['anatomical-nodes']; - const annotationModels = annotation?.['models']; - let anatomicalNode; - let uniqueResource = transformResources; - const models = annotation?.['models']; + const featureId = data[0].feature?.featureId + const annotation = this.mapImp.annotations.get(featureId) + const anatomicalNodes = annotation?.['anatomical-nodes'] + const annotationModels = annotation?.['models'] + let anatomicalNode + let uniqueResource = transformResources + const models = annotation?.['models'] if (anatomicalNodes?.length) { // get the node which match the feature in a location // [feature, location] - anatomicalNode = anatomicalNodes.find((node) => - JSON.parse(node)[0] === annotationModels - ); + anatomicalNode = anatomicalNodes.find( + (node) => JSON.parse(node)[0] === annotationModels + ) } if (anatomicalNode) { - uniqueResource = JSON.parse(anatomicalNode); + uniqueResource = JSON.parse(anatomicalNode) } else if (models) { - uniqueResource = [models, []]; + uniqueResource = [models, []] } - const knowledgeSource = this.mapImp.knowledgeSource; - const terms = uniqueResource.flat(Infinity); - const uniqueTerms = [...new Set(terms)]; - const fetchResults = await fetchLabels(this.flatmapAPI, uniqueTerms); + const knowledgeSource = this.mapImp.knowledgeSource + const terms = uniqueResource.flat(Infinity) + const uniqueTerms = [...new Set(terms)] + const fetchResults = await fetchLabels(this.flatmapAPI, uniqueTerms) const objectResults = fetchResults.reduce((arr, item) => { - const id = item[0]; - const valObj = JSON.parse(item[1]); - arr.push({ id, label: valObj.label, source: valObj.source }); - return arr; - }, []); + const id = item[0] + const valObj = JSON.parse(item[1]) + arr.push({ id, label: valObj.label, source: valObj.source }) + return arr + }, []) // sort matched knowledgeSource items for same id objectResults.sort((a, b) => { if (a.id === b.id) { - if (a.source === knowledgeSource && b.source !== knowledgeSource) return -1; - if (a.source !== knowledgeSource && b.source === knowledgeSource) return 1; - return 0; + if (a.source === knowledgeSource && b.source !== knowledgeSource) + return -1 + if (a.source !== knowledgeSource && b.source === knowledgeSource) + return 1 + return 0 } - return a.id.localeCompare(b.id); - }); + return a.id.localeCompare(b.id) + }) - const labels = []; + const labels = [] for (let i = 0; i < uniqueTerms.length; i++) { - const foundObj = objectResults.find((obj) => obj.id === uniqueTerms[i]) + const foundObj = objectResults.find( + (obj) => obj.id === uniqueTerms[i] + ) if (foundObj) { - labels.push(foundObj.label); + labels.push(foundObj.label) } } - const filterItemLabel = capitalise(labels.join(', ')); + const filterItemLabel = capitalise(labels.join(', ')) const newConnectivityfilter = { facet: JSON.stringify(uniqueResource), facetPropPath: `flatmap.connectivity.source.${this.connectionType.toLowerCase()}`, tagLabel: filterItemLabel, // used tagLabel here instead of label since the label and value are different - term: this.connectionType - }; + term: this.connectionType, + } // check for existing item - const isNewFilterItemExist = this.connectivityFilters.some((connectivityfilter) => ( - connectivityfilter.facet === newConnectivityfilter.facet && - connectivityfilter.facetPropPath === newConnectivityfilter.facetPropPath - )); + const isNewFilterItemExist = this.connectivityFilters.some( + (connectivityfilter) => + connectivityfilter.facet === newConnectivityfilter.facet && + connectivityfilter.facetPropPath === + newConnectivityfilter.facetPropPath + ) if (!isNewFilterItemExist) { - this.connectivityFilters.push(newConnectivityfilter); + this.connectivityFilters.push(newConnectivityfilter) } this.$emit('neuron-connection-feature-click', { filters: this.connectivityFilters, search: '', - }); + }) } else { // clicking on paths - const searchTerms = resources.join(); + const searchTerms = resources.join() // for neuron connection mode "all" if (this.connectionType.toLowerCase() === 'all') { this.$emit('neuron-connection-feature-click', { filters: [], search: searchTerms, - }); + }) } else { // for neuron connection mode "origin", "via" and "destination" - await this.openConnectivityInfo(data); + await this.openConnectivityInfo(data) } } } else { - await this.openConnectivityInfo(data); + await this.openConnectivityInfo(data) } }, openConnectivityInfo: async function (data) { // load and store knowledge - loadAndStoreKnowledge(this.mapImp, this.flatmapQueries); + loadAndStoreKnowledge(this.mapImp, this.flatmapQueries) let prom1 = [] // Emit placeholders first. // This may contain invalid connectivity. this.tooltipEntry = data - .filter(tooltip => tooltip.resource[0] in this.mapImp.pathways.paths) + .filter((tooltip) => tooltip.resource[0] in this.mapImp.pathways.paths) .map((tooltip) => { - return { title: tooltip.label, featureId: tooltip.resource, ready: false } + return { + title: tooltip.label, + featureId: tooltip.resource, + ready: false, + } }) // this should only for flatmap paths not all features if (this.tooltipEntry.length) { - this.$emit('connectivity-info-open', this.tooltipEntry); + this.$emit('connectivity-info-open', this.tooltipEntry) // While having placeholders displayed, get details for all paths and then replace. for (let index = 0; index < data.length; index++) { prom1.push(await this.getKnowledgeTooltip(data[index])) } this.tooltipEntry = await Promise.all(prom1) - const featureIds = this.tooltipEntry.map(tooltip => tooltip.featureId[0]) + const featureIds = this.tooltipEntry.map( + (tooltip) => tooltip.featureId[0] + ) if (featureIds.length > 0) { this.displayTooltip(featureIds) } @@ -2207,79 +2435,93 @@ export default { * @param {Array} payload - The array of filter items to update. */ updateConnectivityFilters: function (payload) { - if (!payload.length) return; - this.connectivityFilters = payload.filter((filterItem) => ( - filterItem.facet.toLowerCase() !== 'show all' - )); + if (!payload.length) return + this.connectivityFilters = payload.filter( + (filterItem) => filterItem.facet.toLowerCase() !== 'show all' + ) }, resetConnectivityfilters: function (payload) { if (payload.length) { // remove not found items - this.connectivityFilters = this.connectivityFilters.filter((connectivityfilter) => - payload.some((notFoundItem) => ( - notFoundItem.facetPropPath === connectivityfilter.facetPropPath && - notFoundItem.facet !== connectivityfilter.facet - )) + this.connectivityFilters = this.connectivityFilters.filter( + (connectivityfilter) => + payload.some( + (notFoundItem) => + notFoundItem.facetPropPath === + connectivityfilter.facetPropPath && + notFoundItem.facet !== connectivityfilter.facet + ) ) } else { // full reset - this.connectivityFilters = []; + this.connectivityFilters = [] } }, getKnowledgeTooltip: async function (data) { //require data.resource && data.feature.source - const results = await this.flatmapQueries.retrieveFlatmapKnowledgeForEvent(this.mapImp, data) - let tooltip = await this.flatmapQueries.createTooltipData(this.mapImp, data) + const results = + await this.flatmapQueries.retrieveFlatmapKnowledgeForEvent( + this.mapImp, + data + ) + let tooltip = await this.flatmapQueries.createTooltipData( + this.mapImp, + data + ) + // The line below only creates the tooltip if some data was found on the path // the pubmed URLs are in knowledge response.references - if ((results && results[0]) || (data.feature.hyperlinks && data.feature.hyperlinks.length > 0)) { - tooltip['featuresAlert'] = data.alert; - tooltip['knowledgeSource'] = getKnowledgeSource(this.mapImp); + if ( + (results && results[0]) || + (data.feature.hyperlinks && data.feature.hyperlinks.length > 0) + ) { + tooltip['featuresAlert'] = data.alert + tooltip['knowledgeSource'] = getKnowledgeSource(this.mapImp) // Map id and uuid to load connectivity information from the map - tooltip['mapId'] = this.mapImp.mapMetadata.id; - tooltip['mapuuid'] = this.mapImp.mapMetadata.uuid; - // } else { - // tooltip = { - // ...tooltip, - // origins: [data.label], - // originsWithDatasets: [{ id: data.resource[0], name: data.label }], - // components: [], - // componentsWithDatasets: [], - // destinations: [], - // destinationsWithDatasets: [], - // } - // let featureIds = [] - // const pathsOfEntities = await this.mapImp.queryPathsForFeatures(data.resource) - // if (pathsOfEntities.length) { - // pathsOfEntities.forEach((path) => { - // featureIds.push(...this.mapImp.pathModelNodes(path)) - // const searchResults = this.mapImp.search(path) - // let featureId = undefined; - // for (let i = 0; i < searchResults.results.length; i++) { - // featureId = searchResults.results[i].featureId - // const annotation = this.mapImp.annotation(featureId) - // if (featureId && annotation?.label) break; - // } - // if (featureId) { - // const feature = this.mapImp.featureProperties(featureId) - // if (feature.label && !tooltip.components.includes(feature.label)) { - // tooltip.components.push(feature.label) - // tooltip.componentsWithDatasets.push({ id: feature.models, name: feature.label }) - // } - // } - // }) - // featureIds = [...new Set(featureIds)].filter(id => id !== data.feature.featureId) - // featureIds.forEach((id) => { - // const feature = this.mapImp.featureProperties(id) - // if (feature.label && !tooltip.destinations.includes(feature.label)) { - // tooltip.destinations.push(feature.label) - // tooltip.destinationsWithDatasets.push({ id: feature.models, name: feature.label }) - // } - // }) - // } - } - tooltip['ready'] = true; - return tooltip; + tooltip['mapId'] = this.mapImp.mapMetadata.id + tooltip['mapuuid'] = this.mapImp.mapMetadata.uuid + // } else { + // tooltip = { + // ...tooltip, + // origins: [data.label], + // originsWithDatasets: [{ id: data.resource[0], name: data.label }], + // components: [], + // componentsWithDatasets: [], + // destinations: [], + // destinationsWithDatasets: [], + // } + // let featureIds = [] + // const pathsOfEntities = await this.mapImp.queryPathsForFeatures(data.resource) + // if (pathsOfEntities.length) { + // pathsOfEntities.forEach((path) => { + // featureIds.push(...this.mapImp.pathModelNodes(path)) + // const searchResults = this.mapImp.search(path) + // let featureId = undefined; + // for (let i = 0; i < searchResults.results.length; i++) { + // featureId = searchResults.results[i].featureId + // const annotation = this.mapImp.annotation(featureId) + // if (featureId && annotation?.label) break; + // } + // if (featureId) { + // const feature = this.mapImp.featureProperties(featureId) + // if (feature.label && !tooltip.components.includes(feature.label)) { + // tooltip.components.push(feature.label) + // tooltip.componentsWithDatasets.push({ id: feature.models, name: feature.label }) + // } + // } + // }) + // featureIds = [...new Set(featureIds)].filter(id => id !== data.feature.featureId) + // featureIds.forEach((id) => { + // const feature = this.mapImp.featureProperties(id) + // if (feature.label && !tooltip.destinations.includes(feature.label)) { + // tooltip.destinations.push(feature.label) + // tooltip.destinationsWithDatasets.push({ id: feature.models, name: feature.label }) + // } + // }) + // } + } + tooltip['ready'] = true + return tooltip }, /** * A hack to remove flatmap tooltips while popup is open @@ -2287,7 +2529,9 @@ export default { popUpCssHacks: function () { // Below is a hack to remove flatmap tooltips while popup is open const ftooltip = document.querySelector('.flatmap-tooltip-popup') - const popupCloseButton = document.querySelector('.maplibregl-popup-close-button') + const popupCloseButton = document.querySelector( + '.maplibregl-popup-close-button' + ) if (ftooltip) ftooltip.style.display = 'none' popupCloseButton.style.display = 'block' this.$refs.tooltip.$el.style.display = 'flex' @@ -2296,7 +2540,7 @@ export default { * This event is emitted * when a connectivity info (provenance popup) is closed. */ - this.$emit('connectivity-info-close'); + this.$emit('connectivity-info-close') if (ftooltip) ftooltip.style.display = 'block' } }, @@ -2369,10 +2613,13 @@ export default { '.maplibregl-ctrl-minimap' ) if (minimapEl) { - if (this.$refs.minimapResize && - this.$refs.minimapResize.$el.parentNode) { + if ( + this.$refs.minimapResize && + this.$refs.minimapResize.$el.parentNode + ) { this.$refs.minimapResize.$el.parentNode.removeChild( - this.$refs.minimapResize.$el) + this.$refs.minimapResize.$el + ) } minimapEl.appendChild(this.$refs.minimapResize.$el) this.minimapResizeShow = true @@ -2385,52 +2632,58 @@ export default { * @arg {Boolean} `helpMode` */ setHelpMode: function (helpMode) { - const toolTipsLength = this.hoverVisibilities.length; - const lastIndex = toolTipsLength - 1; - const activePopoverObj = this.hoverVisibilities[this.helpModeActiveIndex]; + const toolTipsLength = this.hoverVisibilities.length + const lastIndex = toolTipsLength - 1 + const activePopoverObj = this.hoverVisibilities[this.helpModeActiveIndex] if (activePopoverObj) { - const popoverRefsId = activePopoverObj?.refs; - const popoverRefId = activePopoverObj?.ref; - const popoverRef = this.$refs[popoverRefsId ? popoverRefsId : popoverRefId]; + const popoverRefsId = activePopoverObj?.refs + const popoverRefId = activePopoverObj?.ref + const popoverRef = + this.$refs[popoverRefsId ? popoverRefsId : popoverRefId] if (popoverRef) { // Open pathway drawer if the tooltip is inside or beside - const { parentElement, nextElementSibling } = popoverRef.$el; + const { parentElement, nextElementSibling } = popoverRef.$el const isPathwayContainer = (element) => { - return element && ( - element.classList.contains('pathway-container') || - element.classList.contains('pathway-location') - ); - }; + return ( + element && + (element.classList.contains('pathway-container') || + element.classList.contains('pathway-location')) + ) + } if ( isPathwayContainer(parentElement) || isPathwayContainer(nextElementSibling) ) { if (this.requiresDrawer) { - this.drawerOpen = true; + this.drawerOpen = true } else { - this.helpModeActiveIndex += 1; + this.helpModeActiveIndex += 1 } } } else { // skip the unavailable tooltips - this.helpModeActiveIndex += 1; - this.setHelpMode(helpMode); + this.helpModeActiveIndex += 1 + this.setHelpMode(helpMode) } } // Skip checkbox tooltip if pathway filter is not shown - const activePopoverObjAfter = this.hoverVisibilities[this.helpModeActiveIndex]; - if (activePopoverObjAfter?.ref === 'checkBoxPopover' && !this.showPathwayFilter) { - this.helpModeActiveIndex += 1; - this.setHelpMode(helpMode); + const activePopoverObjAfter = + this.hoverVisibilities[this.helpModeActiveIndex] + if ( + activePopoverObjAfter?.ref === 'checkBoxPopover' && + !this.showPathwayFilter + ) { + this.helpModeActiveIndex += 1 + this.setHelpMode(helpMode) } if (!helpMode) { // reset to iniital state - this.helpModeActiveIndex = this.helpModeInitialIndex; + this.helpModeActiveIndex = this.helpModeInitialIndex } if (this.viewingMode !== 'Annotation' && this.helpModeActiveIndex > 9) { @@ -2441,31 +2694,34 @@ export default { /** * This event is emitted when the tooltips in help mode reach the last item. */ - this.$emit('help-mode-last-item', true); + this.$emit('help-mode-last-item', true) } if (helpMode && !this.helpModeDialog) { - this.inHelp = true; + this.inHelp = true this.hoverVisibilities.forEach((item) => { - item.value = true; - }); - } else if (helpMode && this.helpModeDialog && toolTipsLength > this.helpModeActiveIndex) { - + item.value = true + }) + } else if ( + helpMode && + this.helpModeDialog && + toolTipsLength > this.helpModeActiveIndex + ) { // Show the map tooltip as first item if (this.helpModeActiveIndex > -1) { - this.closeFlatmapHelpPopup(); + this.closeFlatmapHelpPopup() // wait for CSS transition setTimeout(() => { - this.inHelp = false; + this.inHelp = false this.hoverVisibilities.forEach((item) => { - item.value = false; - }); + item.value = false + }) - this.showTooltip(this.helpModeActiveIndex, 200); - }, 300); + this.showTooltip(this.helpModeActiveIndex, 200) + }, 300) } else if (this.helpModeActiveIndex === -1) { - this.openFlatmapHelpPopup(); + this.openFlatmapHelpPopup() } } else { this.inHelp = false @@ -2490,7 +2746,7 @@ export default { /** * This event is emitted after a tooltip in Flatmap is shown. */ - this.$emit('shown-tooltip'); + this.$emit('shown-tooltip') }, timeout) } }, @@ -2519,6 +2775,7 @@ export default { */ displayTooltip: function (feature, geometry = undefined) { let featureId = undefined + console.log('Displaying tooltip for feature:', feature) let options = { className: 'flatmapvuer-popover' } if (geometry) { featureId = feature @@ -2526,7 +2783,7 @@ export default { if (this.annotationEntry.length) { options['annotationEvent'] = { type: this.annotationEntry[0].type, - feature: this.annotationEntry[0].feature + feature: this.annotationEntry[0].feature, } } } else { @@ -2541,15 +2798,19 @@ export default { // If connectivityInfoSidebar is set to `true` // Connectivity info will show in sidebar if ( - (this.connectivityInfoSidebar && this.tooltipEntry.length) && + this.connectivityInfoSidebar && + this.tooltipEntry.length && this.viewingMode !== 'Annotation' ) { if (this.checkConnectivityTooltipEntry(this.tooltipEntry)) { - this.$emit('connectivity-info-open', this.tooltipEntry); + this.$emit('connectivity-info-open', this.tooltipEntry) } } if (this.annotationSidebar && this.viewingMode === 'Annotation') { - this.$emit('annotation-open', {annotationEntry: this.annotationEntry, commitCallback: this.commitAnnotationEvent}); + this.$emit('annotation-open', { + annotationEntry: this.annotationEntry, + commitCallback: this.commitAnnotationEvent, + }) } // If UI is not disabled, // And connectivityInfoSidebar is not set (default) or set to `false` @@ -2558,16 +2819,14 @@ export default { if ( featureId && !this.disableUI && - ( - (this.viewingMode === 'Annotation' && !this.annotationSidebar) || - (this.viewingMode === 'Exploration' && !this.connectivityInfoSidebar) - ) + ((this.viewingMode === 'Annotation' && !this.annotationSidebar) || + (this.viewingMode === 'Exploration' && !this.connectivityInfoSidebar)) ) { - this.tooltipDisplay = true; + this.tooltipDisplay = true this.$nextTick(() => { - this.mapImp.showPopup(featureId, this.$refs.tooltip.$el, options); - this.popUpCssHacks(); - }); + this.mapImp.showPopup(featureId, this.$refs.tooltip.$el, options) + this.popUpCssHacks() + }) } }, /** @@ -2576,18 +2835,18 @@ export default { * because the sidebar is opened * @arg featureIds */ - moveMap: function (featureIds, options = {}) { + moveMap: function (featureIds, options = {}) { if (this.mapImp) { - const { offsetX = 0, offsetY = 0, zoom = 4 } = options; - const Map = this.mapImp.map; - const bbox = this.mapImp.bounds.toArray(); + const { offsetX = 0, offsetY = 0, zoom = 4 } = options + const Map = this.mapImp.map + const bbox = this.mapImp.bounds.toArray() // Zoom the map to features first - this.mapImp.zoomToFeatures(featureIds, { noZoomIn: true }); + this.mapImp.zoomToFeatures(featureIds, { noZoomIn: true }) // Hide the left pathway drawer // to get more space for the map - this.showPathwaysDrawer(false); + this.showPathwaysDrawer(false) // Move the map to left side // since the sidebar is taking space on the right @@ -2596,9 +2855,9 @@ export default { Map.fitBounds(bbox, { offset: [offsetX, offsetY], zoom: zoom, - animate: true - }); - }); + animate: true, + }) + }) } } }, @@ -2618,7 +2877,7 @@ export default { /** * This event is emitted after a tooltip on Flatmap's map is shown. */ - this.$emit('shown-map-tooltip'); + this.$emit('shown-map-tooltip') } } }, @@ -2653,16 +2912,19 @@ export default { */ getVisibilityState: function (state) { const refs = ['alertSelection', 'pathwaysSelection', 'taxonSelection'] - refs.forEach(ref => { + refs.forEach((ref) => { let comp = this.$refs[ref] if (comp) { state[ref] = comp.getState() } }) if (this.$refs.treeControls) { - const checkedKeys = this.$refs.treeControls.$refs.regionTree.getCheckedKeys(); + const checkedKeys = + this.$refs.treeControls.$refs.regionTree.getCheckedKeys() //Only store first level systems (terms without .) - state['systemsSelection'] = checkedKeys.filter(term => !term.includes('.')) + state['systemsSelection'] = checkedKeys.filter( + (term) => !term.includes('.') + ) } }, /** @@ -2671,7 +2933,7 @@ export default { */ setVisibilityState: function (state) { const refs = ['alertSelection', 'pathwaysSelection', 'taxonSelection'] - refs.forEach(ref => { + refs.forEach((ref) => { const settings = state[ref] if (settings) { const comp = this.$refs[ref] @@ -2682,9 +2944,14 @@ export default { }) if ('systemsSelection' in state) { if (this.$refs.treeControls) { - this.$refs.treeControls.$refs.regionTree.setCheckedKeys(state['systemsSelection']); + this.$refs.treeControls.$refs.regionTree.setCheckedKeys( + state['systemsSelection'] + ) this.systems[0].children.forEach((item) => { - this.mapImp.enableSystem(item.key, state['systemsSelection'].includes(item.key)) + this.mapImp.enableSystem( + item.key, + state['systemsSelection'].includes(item.key) + ) }) } } @@ -2711,7 +2978,9 @@ export default { state['outlines'] = this.outlinesRadio state['background'] = this.currentBackground if (this.offlineAnnotationEnabled) { - state['offlineAnnotations'] = sessionStorage.getItem('anonymous-annotation') + state['offlineAnnotations'] = sessionStorage.getItem( + 'anonymous-annotation' + ) } this.getVisibilityState(state) return state @@ -2748,7 +3017,10 @@ export default { if (state) { if (state.viewport) this.mapImp.setState(state.viewport) if (state.offlineAnnotations) { - sessionStorage.setItem('anonymous-annotation', state.offlineAnnotations) + sessionStorage.setItem( + 'anonymous-annotation', + state.offlineAnnotations + ) } if (state.viewingMode) this.changeViewingMode(state.viewingMode) //The following three are boolean @@ -2772,7 +3044,10 @@ export default { */ setFlightPathInfo: function (mapVersion) { const mapVersionForFlightPath = 1.6 - if (mapVersion === mapVersionForFlightPath || mapVersion > mapVersionForFlightPath) { + if ( + mapVersion === mapVersionForFlightPath || + mapVersion > mapVersionForFlightPath + ) { // Show flight path option UI this.displayFlightPathOption = true // Show 2D as default on FC type @@ -2818,10 +3093,10 @@ export default { identifier.taxon = state.entry } if (state.biologicalSex) { - identifier['biologicalSex'] = state.biologicalSex; + identifier['biologicalSex'] = state.biologicalSex } else if (identifier.taxon === 'NCBITaxon:9606') { //For backward compatibility - identifier['biologicalSex'] = 'PATO:0000384'; + identifier['biologicalSex'] = 'PATO:0000384' } } else { // Set the bioloicalSex now if map is not resumed from @@ -2845,48 +3120,52 @@ export default { // tooltipDelay: 15, // new feature to delay tooltips showing } ) - promise1.then((returnedObject) => { - this.mapImp = returnedObject - this.serverURL = this.mapImp.makeServerUrl('').slice(0, -1) - let mapVersion = this.mapImp.details.version - this.setFlightPathInfo(mapVersion) - const stateToSet = this._stateToBeSet ? this._stateToBeSet : state - this.onFlatmapReady(stateToSet) - this.$nextTick(() => this.restoreMapState(stateToSet)) - }).catch((error) => { - console.error('Flatmap loading error:', error) - // prepare error object - this.flatmapError = {}; - if (error.message && error.message.indexOf('Unknown map') !== -1) { - this.flatmapError['title'] = 'Unknown Map!'; - this.flatmapError['messages'] = Object.keys(identifier).map(key => { - const keyName = key === 'uuid' ? 'UUID' : capitalise(key); - return `${keyName}: ${identifier[key]}` - }); - } else { - this.flatmapError['title'] = 'Error Loading Map!'; - this.flatmapError['messages'] = [ - error.message ? error.message : error.toString(), - 'Please try again later or contact support if the problem persists.' - ]; - } - if (this.$parent?.$refs?.multiContainer) { - // if the flatmap is in a multiflatmapvuer - // show a button to load default map - const multiFlatmapVuer = this.$parent; - this.flatmapError['button'] = { - text: 'Load Default Map', - callback: () => { - const defaultSpecies = multiFlatmapVuer.initial; - multiFlatmapVuer.setSpecies(defaultSpecies, undefined, 3); + promise1 + .then((returnedObject) => { + this.mapImp = returnedObject + this.serverURL = this.mapImp.makeServerUrl('').slice(0, -1) + let mapVersion = this.mapImp.details.version + this.setFlightPathInfo(mapVersion) + const stateToSet = this._stateToBeSet ? this._stateToBeSet : state + this.onFlatmapReady(stateToSet) + this.$nextTick(() => this.restoreMapState(stateToSet)) + }) + .catch((error) => { + console.error('Flatmap loading error:', error) + // prepare error object + this.flatmapError = {} + if (error.message && error.message.indexOf('Unknown map') !== -1) { + this.flatmapError['title'] = 'Unknown Map!' + this.flatmapError['messages'] = Object.keys(identifier).map( + (key) => { + const keyName = key === 'uuid' ? 'UUID' : capitalise(key) + return `${keyName}: ${identifier[key]}` + } + ) + } else { + this.flatmapError['title'] = 'Error Loading Map!' + this.flatmapError['messages'] = [ + error.message ? error.message : error.toString(), + 'Please try again later or contact support if the problem persists.', + ] + } + if (this.$parent?.$refs?.multiContainer) { + // if the flatmap is in a multiflatmapvuer + // show a button to load default map + const multiFlatmapVuer = this.$parent + this.flatmapError['button'] = { + text: 'Load Default Map', + callback: () => { + const defaultSpecies = multiFlatmapVuer.initial + multiFlatmapVuer.setSpecies(defaultSpecies, undefined, 3) + }, } - }; - } - this.loading = false; - }) + } + this.loading = false + }) } else if (state) { this._stateToBeSet = { - ...state + ...state, } if (this.mapImp && !this.loading) { this.restoreMapState(this._stateToBeSet) @@ -2928,7 +3207,7 @@ export default { let filterSourcesMap = new Map() for (const annotation of this.mapImp.annotations.values()) { if (annotation.source) { - if ("alert" in annotation) { + if ('alert' in annotation) { withAlert.add(annotation.source) } else { withoutAlert.add(annotation.source) @@ -2945,7 +3224,7 @@ export default { sourceMap.set(setKey, new Set()) } sourceMap.get(setKey).add(`${annotation.source}`) - }; + } if (Array.isArray(value)) { value.forEach(addToSourceMap) } else { @@ -2956,10 +3235,10 @@ export default { } } let filterSources = { - 'alert': { - 'with': [...withAlert], - 'without': [...withoutAlert] - } + alert: { + with: [...withAlert], + without: [...withoutAlert], + }, } for (const [key, value] of filterSourcesMap.entries()) { filterSources[key] = {} @@ -2970,10 +3249,15 @@ export default { return filterSources }, getFilterOptions: async function (mapImp, _providedKnowledge) { - const providedKnowledge = _providedKnowledge || this.getFlatmapKnowledge(); - const providedPathways = this.pathways; - const flatmapFilterOptions = await getFlatmapFilterOptions(this.flatmapAPI, mapImp, providedKnowledge, providedPathways); - return flatmapFilterOptions; + const providedKnowledge = _providedKnowledge || this.getFlatmapKnowledge() + const providedPathways = this.pathways + const flatmapFilterOptions = await getFlatmapFilterOptions( + this.flatmapAPI, + mapImp, + providedKnowledge, + providedPathways + ) + return flatmapFilterOptions }, /** * @public @@ -2981,33 +3265,40 @@ export default { */ onFlatmapReady: function (state) { // onFlatmapReady is used for functions that need to run immediately after the flatmap is loaded - this.sensor = markRaw(new ResizeSensor(this.$refs.display, this.mapResize)) + this.sensor = markRaw( + new ResizeSensor(this.$refs.display, this.mapResize) + ) if (this.mapImp.options?.style === 'functional') { this.isFC = true } this.mapImp.setBackgroundOpacity(1) this.backgroundChangeCallback(this.currentBackground) this.pathways = this.mapImp.pathTypes() - this.pathways = this.pathways.filter(path => { + this.pathways = this.pathways.filter((path) => { return path.enabled && path.type !== 'other' }) //Disable layers for now //this.layers = this.mapImp.getLayers(); this.processSystems(this.mapImp.getSystems()) //Async, pass the state for checking - this.processTaxon(this.mapImp.taxonIdentifiers, state ? state['taxonSelection'] : undefined) - this.containsAlert = "alert" in this.mapImp.featureFilterRanges() + this.processTaxon( + this.mapImp.taxonIdentifiers, + state ? state['taxonSelection'] : undefined + ) + + this.containsAlert = 'alert' in this.mapImp.featureFilterRanges() this.flatmapLegends = this.mapImp.flatmapLegend this.loading = false this.computePathControlsMaximumHeight() this.mapResize() - this.handleMapClick(); - this.setInitMapState(); + this.handleMapClick() + this.setInitMapState() if (this.displayMinimap) { - const minimapOptions = { position: 'top-right' }; - this.mapImp.createMinimap(minimapOptions); + const minimapOptions = { position: 'top-right' } + this.mapImp.createMinimap(minimapOptions) this.addResizeButtonToMinimap() } + this.currentFlatmapUuid = this.mapImp?.mapMetadata?.uuid /** * This is ``onFlatmapReady`` event. * @arg ``this`` (Component Vue Instance) @@ -3020,33 +3311,35 @@ export default { * after the map is loaded. */ handleMapClick: function () { - const _map = this.mapImp.map; + const _map = this.mapImp.map if (_map) { _map.on('click', (e) => { if (!this.connectivityDataSource) { - this.$emit('connectivity-info-close'); + this.$emit('connectivity-info-close') } - this.connectivityDataSource = ''; // reset - }); + this.connectivityDataSource = '' // reset + }) } }, - onContextLost: function() { + onContextLost: function () { this.lastViewport = markRaw(this.mapImp.getState()) - this.flatmapError = {}; + this.flatmapError = {} this.flatmapError['title'] = 'GL context lost!' - this.flatmapError['messages'] = [`A display issue has occurred due + this.flatmapError['messages'] = [ + `A display issue has occurred due to a limit on available WebGL contexts. You can restore the display using the Restore Context button. Please see the documentation - for more details.`] + for more details.`, + ] this.flatmapError['button'] = { text: 'Restore Context', callback: () => { this.forceContextRestore() - } - }; + }, + } }, - onContextRestored: function() { + onContextRestored: function () { if (this.mapImp) { this.handleMapClick() this.setInitMapState() @@ -3056,8 +3349,8 @@ export default { } this.restoreMapState(lostState) if (this.displayMinimap) { - const minimapOptions = { position: 'top-right' }; - this.mapImp.createMinimap(minimapOptions); + const minimapOptions = { position: 'top-right' } + this.mapImp.createMinimap(minimapOptions) this.addResizeButtonToMinimap() } if (this.filterToRestore) { @@ -3088,23 +3381,23 @@ export default { if (this.mapImp) { if (term === undefined || term === '') { this.mapImp.clearSearchResults() - if (this.viewingMode === "Exploration") { - this.$emit('connectivity-info-close'); - } else if (this.viewingMode === "Annotation") { + if (this.viewingMode === 'Exploration') { + this.$emit('connectivity-info-close') + } else if (this.viewingMode === 'Annotation') { this.manualAbortedOnClose() } - this.searchTerm = "" + this.searchTerm = '' return true } else { const searchResults = this.mapImp.search(term) if (searchResults?.results?.length) { this.mapImp.showSearchResults(searchResults) if (displayInfo) { - let featureId = undefined; + let featureId = undefined for (let i = 0; i < searchResults.results.length; i++) { featureId = searchResults.results[i].featureId const annotation = this.mapImp.annotation(featureId) - if (featureId && annotation?.label) break; + if (featureId && annotation?.label) break } if (featureId) { const feature = this.mapImp.featureProperties(featureId) @@ -3140,10 +3433,13 @@ export default { highlightConnectedPaths: function (paths) { if (paths.length) { // filter paths for this map - const filteredPaths = paths.filter(path => (path in this.mapImp.pathways.paths)) + const filteredPaths = paths.filter( + (path) => path in this.mapImp.pathways.paths + ) // this.zoomToFeatures is replaced with selectGeoJSONFeatures to highlight paths - const featureIdsToHighlight = this.mapImp.modelFeatureIdList(filteredPaths); - this.mapImp.selectGeoJSONFeatures(featureIdsToHighlight); + const featureIdsToHighlight = + this.mapImp.modelFeatureIdList(filteredPaths) + this.mapImp.selectGeoJSONFeatures(featureIdsToHighlight) } }, /** @@ -3160,7 +3456,144 @@ export default { EventBus.emit('onActionClick', data) }, setConnectionType: function (type) { - this.connectionType = type; + this.connectionType = type + }, + /** + * Main function to coordinate fetching dataset info and processing files. + */ + async fetchFlatmapProtocols(uuid) { + const cacheKey = `flatmap_dataset_${uuid}` + + // Try to get from cache first + // console.log('------- caching temporary disabled for debugging -------') + // const cachedData = null + const cachedData = this.getSessionCache(cacheKey) + if (cachedData) { + this.datasetInfo = cachedData + this.processDatasetFiles(cachedData) + return + } + + // If not in cache, call the API + const apiLocation = import.meta.env.VITE_API_LOCATION + if (!apiLocation) { + console.warn('VITE_API_LOCATION is not defined.') + return + } + + try { + console.log('Fetching dataset info from API...') + // Ensure the URL matches your backend route structure + const response = await fetch(`${apiLocation}flatmap/uuid?uuid=${uuid}`) + + if (!response.ok) + throw new Error(`API call failed: ${response.statusText}`) + + const data = await response.json() + + // Save to cache and process + this.setSessionCache(cacheKey, data) + this.datasetInfo = data + this.processDatasetFiles(data) + } catch (error) { + console.error('Error fetching flatmap protocols:', error) + } + }, + + /** + * Extract the bucket name from an S3 URI. + * + * @param s3Uri + */ + extractBucketNameFromS3Uri(s3Uri) { + try { + // Use the native URL API to parse the s3:// URI + // s3://bucket-name/path/to/key -> hostname is bucket-name + const url = new URL(s3Uri) + return url.hostname + } catch (e) { + console.error('Error converting S3 URI:', e) + return null + } + }, + /** + * Iterates through the file list, constructs full URLs, and checks for simulation content. + */ + async processDatasetFiles(data) { + if (!data || data.length === 0) return + + this.simulationInfo = [] // Reset list + + //FIXME: Currently only process the first dataset entry + const firstData = data[0] + const apiLocation = import.meta.env.VITE_API_LOCATION + // Base URL for Pennsieve public assets + const baseUrl = `${apiLocation}/s3-resource/${firstData.dataset_id}/files` + const bucketName = this.extractBucketNameFromS3Uri(firstData.s3uri) + + firstData.urls.map(async (filePath) => { + const fullUrl = `${baseUrl}/${filePath}?s3BucketName=${bucketName}` + // Add to our list of valid files + this.simulationInfo.push({ + label: firstData.title, + s3uri: firstData.s3uri, + dataset_id: firstData.dataset_id, + version: firstData.version, + path: filePath, + type: 'Simulation', + resource: fullUrl, + }) + }) + }, + /** + * Retrieve data from session storage if it hasn't expired. + */ + getSessionCache(key) { + const itemStr = sessionStorage.getItem(key) + if (!itemStr) return null + + try { + const item = JSON.parse(itemStr) + const now = new Date() + + // Check if expired (compare current time to expiry time) + if (now.getTime() > item.expiry) { + sessionStorage.removeItem(key) + return null + } + return item.value + } catch (e) { + return null + } + }, + + /** + * Save data to session storage with a 24-hour expiry. + */ + setSessionCache(key, value) { + const now = new Date() + // 24 hours in milliseconds: 24 * 60 * 60 * 1000 = 86400000 + const ttl = 86400000 + + const item = { + value: value, + expiry: now.getTime() + ttl, + } + + try { + sessionStorage.setItem(key, JSON.stringify(item)) + } catch (e) { + console.warn('Session storage full or disabled', e) + } + }, + getSimulationLabel(info) { + console.log(info.path) + return info.path.split('/').pop() + }, + openSimulation() { + if (this.selectedSimulation) { + this.$emit('open-simulation', this.selectedSimulation) + } }, }, props: { @@ -3219,7 +3652,7 @@ export default { * On default, `false`, clicking help will show all tooltips. * If `true`, clicking help will show the help-mode-dialog. */ - helpModeDialog: { + helpModeDialog: { type: Boolean, default: false, }, @@ -3353,7 +3786,7 @@ export default { /** * Flag to disable UIs on Map */ - disableUI: { + disableUI: { type: Boolean, default: false, }, @@ -3416,12 +3849,14 @@ export default { flatmapError: null, sensor: null, mapManagerRef: undefined, + currentFlatmapUuid: undefined, flatmapQueries: undefined, annotationEntry: [], //tooltip display has to be set to false until it is rendered - //for the first time, otherwise it may display an arrow at a + //for the first time, otherwise it may display an arrow at an //undesired location. tooltipDisplay: false, + tooltipTimer: null, serverURL: undefined, layers: [], pathways: [], @@ -3447,12 +3882,13 @@ export default { { value: false, ref: 'warningPopover' }, // 7 { value: false, ref: 'whatsNewPopover' }, // 8 { value: false, ref: 'featuredMarkerPopover' }, // 9 - { value: false, refs: "toolbarPopover", ref: "editPopover" }, // 10 - { value: false, refs: "toolbarPopover", ref: "deletePopover" }, // 11 - { value: false, refs: "toolbarPopover", ref: "pointPopover" }, // 12 - { value: false, refs: "toolbarPopover", ref: "lineStringPopover" }, // 13 - { value: false, refs: "toolbarPopover", ref: "polygonPopover" }, // 14 - { value: false, refs: "toolbarPopover", ref: "connectionPopover" }, // 15 + { value: false, refs: 'toolbarPopover', ref: 'editPopover' }, // 10 + { value: false, refs: 'toolbarPopover', ref: 'deletePopover' }, // 11 + { value: false, refs: 'toolbarPopover', ref: 'pointPopover' }, // 12 + { value: false, refs: 'toolbarPopover', ref: 'lineStringPopover' }, // 13 + { value: false, refs: 'toolbarPopover', ref: 'polygonPopover' }, // 14 + { value: false, refs: 'toolbarPopover', ref: 'connectionPopover' }, // 15 + { value: false, ref: 'simulationPopover' }, // 16 ], helpModeActiveIndex: this.helpModeInitialIndex, yellowstar: yellowstar, @@ -3477,9 +3913,14 @@ export default { currentHover: '', viewingMode: 'Exploration', viewingModes: { - 'Exploration': 'Find relevant research and view detail of neural pathways by selecting a pathway to view its connections and data sources', - 'Neuron Connection': 'Discover Neuron connections by selecting a neuron and viewing its associated network connections', - 'Annotation': ['View feature annotations', 'Add, comment on and view feature annotations'] + Exploration: + 'Find relevant research and view detail of neural pathways by selecting a pathway to view its connections and data sources', + 'Neuron Connection': + 'Discover Neuron connections by selecting a neuron and viewing its associated network connections', + Annotation: [ + 'View feature annotations', + 'Add, comment on and view feature annotations', + ], }, connectionType: 'All', offlineAnnotationEnabled: false, @@ -3489,12 +3930,12 @@ export default { openMapRef: undefined, backgroundIconRef: undefined, toolbarOptions: [ - "Edit", - "Delete", - "Point", - "LineString", - "Polygon", - "Connection", + 'Edit', + 'Delete', + 'Point', + 'LineString', + 'Polygon', + 'Connection', ], annotator: undefined, authorisedUser: undefined, @@ -3524,13 +3965,17 @@ export default { alert: { with: true, without: true, - } + }, }), - searchTerm: "", + searchTerm: '', taxonLeaveDelay: undefined, connectivityFilters: [], flatmapLegends: [], lastViewport: undefined, + simulationInfo: [], + datasetInfo: null, + simulationDrawerOpen: false, + selectedSimulation: null, } }, computed: { @@ -3538,16 +3983,17 @@ export default { isValidDrawnCreated: function () { return Object.keys(this.drawnCreatedEvent).length > 0 }, - requiresDrawer: function() { + requiresDrawer: function () { if (this.loading) { this.drawerOpen = false return false } - if ((this.systems?.length > 0) || + if ( + this.systems?.length > 0 || (this.containsAlert && this.alertOptions) || - (this.pathways?.length > 0) || - (this.taxonConnectivity?.length > 0) || - (this.legendEntry?.length > 0) + this.pathways?.length > 0 || + this.taxonConnectivity?.length > 0 || + this.legendEntry?.length > 0 ) { this.drawerOpen = true return true @@ -3569,7 +4015,7 @@ export default { return [...this.flatmapLegends, ...this.externalLegends] }, showDatasetMarkerTooltip: function () { - return this.hoverVisibilities[9].value; + return this.hoverVisibilities[9].value }, }, watch: { @@ -3585,11 +4031,11 @@ export default { // just take the action from helpModeActiveItem // work with local value since the indexing is different if (this.helpMode) { - this.helpModeActiveIndex += 1; - this.setHelpMode(this.helpMode); + this.helpModeActiveIndex += 1 + this.setHelpMode(this.helpMode) } }, - render: function(val) { + render: function (val) { if (val) { if (this.mapImp && this.mapImp.contextLost && !this.loading) { this.$nextTick(() => { @@ -3624,7 +4070,7 @@ export default { this.authorisedUser = undefined this.offlineAnnotationEnabled = true } - this.emitOfflineAnnotationUpdate(); + this.emitOfflineAnnotationUpdate() this.setFeatureAnnotated() this.addAnnotationFeature() this.loading = false @@ -3637,54 +4083,67 @@ export default { } }, activeDrawTool: function (tool) { - let coordinates = []; - let lastClick = { x: null, y: null }; - const canvas = this.$el.querySelector('.maplibregl-canvas'); + let coordinates = [] + let lastClick = { x: null, y: null } + const canvas = this.$el.querySelector('.maplibregl-canvas') const removeListeners = () => { - canvas.removeEventListener('keydown', handleKeyboardEvent); - canvas.removeEventListener('click', handleMouseEvent); - }; + canvas.removeEventListener('keydown', handleKeyboardEvent) + canvas.removeEventListener('click', handleMouseEvent) + } const handleKeyboardEvent = (event) => { - if (!['Escape', 'Enter'].includes(event.key)) return; + if (!['Escape', 'Enter'].includes(event.key)) return const isValidDraw = (tool === 'Point' && coordinates.length === 1) || (tool === 'LineString' && coordinates.length >= 2) || - (tool === 'Polygon' && coordinates.length >= 3); + (tool === 'Polygon' && coordinates.length >= 3) if (event.key === 'Escape' || (event.key === 'Enter' && !isValidDraw)) { - this.activeDrawTool = undefined; + this.activeDrawTool = undefined } - removeListeners(); - }; + removeListeners() + } const handleMouseEvent = (event) => { - const rect = canvas.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - const distance = Math.sqrt((x - lastClick.x) ** 2 + (y - lastClick.y) ** 2); + const rect = canvas.getBoundingClientRect() + const x = event.clientX - rect.left + const y = event.clientY - rect.top + const distance = Math.sqrt( + (x - lastClick.x) ** 2 + (y - lastClick.y) ** 2 + ) if (distance < 8) { - if (!this.isValidDrawnCreated) this.activeDrawTool = undefined; - removeListeners(); - return; + if (!this.isValidDrawnCreated) this.activeDrawTool = undefined + removeListeners() + return } - lastClick = { x, y }; - coordinates.push(lastClick); - }; + lastClick = { x, y } + coordinates.push(lastClick) + } if (tool) { - removeListeners(); - canvas.addEventListener('keydown', handleKeyboardEvent); - canvas.addEventListener('click', handleMouseEvent); + removeListeners() + canvas.addEventListener('keydown', handleKeyboardEvent) + canvas.addEventListener('click', handleMouseEvent) } - } + }, + currentFlatmapUuid: { + handler(newUuid) { + if (newUuid) { + // console.log('New map loaded with uuid:', newUuid) + this.fetchFlatmapProtocols(newUuid) + } + }, + // immediate: true, + }, }, created: function () { if (this.mapManager) { - this.mapManagerRef = this.mapManager; + this.mapManagerRef = this.mapManager } else { - this.mapManagerRef = markRaw(new flatmap.MapViewer(this.flatmapAPI, { container: undefined })); + this.mapManagerRef = markRaw( + new flatmap.MapViewer(this.flatmapAPI, { container: undefined }) + ) /** * The event emitted after a new mapManager is loaded. * This mapManager can be used to create new flatmaps. */ - this.$emit('mapmanager-loaded', this.mapManagerRef); + this.$emit('mapmanager-loaded', this.mapManagerRef) } }, mounted: function () { @@ -3701,17 +4160,16 @@ export default { } else if (this.renderAtMounted) { this.createFlatmap() } - refreshFlatmapKnowledgeCache(); + refreshFlatmapKnowledgeCache() }, } diff --git a/src/components/MultiFlatmapVuer.vue b/src/components/MultiFlatmapVuer.vue index cdfe1450..d2b66e8b 100644 --- a/src/components/MultiFlatmapVuer.vue +++ b/src/components/MultiFlatmapVuer.vue @@ -106,6 +106,7 @@ import { markRaw } from 'vue' import EventBus from './EventBus' import FlatmapVuer from './FlatmapVuer.vue' +import FlatmapError from './FlatmapError.vue' import flatmap from '../services/flatmapLoader.js' import { ElCol as Col, @@ -135,6 +136,7 @@ export default { Select, Popover, FlatmapVuer, + FlatmapError, }, created: function () { this.loadMapManager(); diff --git a/src/components/index.js b/src/components/index.js index f36c8efc..b16848ee 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,6 +1,20 @@ // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. -import FlatmapVuer from "./FlatmapVuer.vue"; -import MultiFlatmapVuer from "./MultiFlatmapVuer.vue"; +import FlatmapVuer from './FlatmapVuer.vue' +import MultiFlatmapVuer from './MultiFlatmapVuer.vue' +import { FlatmapQueries } from '../services/flatmapQueries.js' +import { + getKnowledgeSource, + getKnowledgeSourceFromProvenance, + loadAndStoreKnowledge, +} from '../services/flatmapKnowledge.js' -export { FlatmapVuer, MultiFlatmapVuer }; +export { + FlatmapQueries, + FlatmapVuer, + getKnowledgeSource, + getKnowledgeSourceFromProvenance, + loadAndStoreKnowledge, + MultiFlatmapVuer, +} +// export { FlatmapVuer, MultiFlatmapVuer }; diff --git a/src/services/flatmapKnowledge.js b/src/services/flatmapKnowledge.js index ff6c97a5..76aa7576 100644 --- a/src/services/flatmapKnowledge.js +++ b/src/services/flatmapKnowledge.js @@ -141,6 +141,7 @@ export { getReferenceConnectivitiesFromStorage, getReferenceConnectivitiesByAPI, loadAndStoreKnowledge, + getFlatmapKnowledge, getKnowledgeSource, getKnowledgeSourceFromProvenance, refreshFlatmapKnowledgeCache,