From d56ea98780d17f01ea58c3e619615d27f7679278 Mon Sep 17 00:00:00 2001 From: Cindy Chang Date: Wed, 21 Aug 2024 14:25:34 +0800 Subject: [PATCH] highlight path --- src/components/Discover/Map/SidebarState.vue | 37 +++++--- src/module/cytoscapeMap.js | 2 +- src/stores/cytoscapeStore.ts | 1 - src/stores/mapPathStore.ts | 92 ++++++++++++++------ 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/src/components/Discover/Map/SidebarState.vue b/src/components/Discover/Map/SidebarState.vue index 77e9465..c01cffd 100644 --- a/src/components/Discover/Map/SidebarState.vue +++ b/src/components/Discover/Map/SidebarState.vue @@ -158,14 +158,14 @@
- +

No data

    @@ -194,13 +194,13 @@
  • - import { computed, ref, } from 'vue'; import PageAdmin from '@/stores/pageAdmin'; +import MapPathStore from '@/stores/mapPathStore'; import { getTimeLabel } from '@/module/timeLabel.js'; import getMoment from 'moment'; import i18next from '@/i18n/i18n'; @@ -262,22 +263,30 @@ export default { }, setup(){ const pageAdmin = PageAdmin(); + const mapPathStore = MapPathStore(); - const active1 = ref(0); + const activeTrace = ref(0); const currentMapFile = computed(() => pageAdmin.currentMapFile); - const selectedPath = ref(0); + const clickedPathListIndex = ref(0); + const onActiveTraceClick = (clickedActiveTraceIndex) => { + mapPathStore.clearAllHighlight(); + activeTrace.value = clickedActiveTraceIndex; + mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value); + } const onPathOptionClick = (clickedPath) => { - selectedPath.value = clickedPath; + clickedPathListIndex.value = clickedPath; + mapPathStore.highlightClickedPath(activeTrace.value, clickedPathListIndex.value); }; return { currentMapFile, i18next, fieldNamesAndLabelNames, - selectedPath, + clickedPathListIndex, onPathOptionClick, - active1, + onActiveTraceClick, + activeTrace, }; }, data() { diff --git a/src/module/cytoscapeMap.js b/src/module/cytoscapeMap.js index bf80833..5098434 100644 --- a/src/module/cytoscapeMap.js +++ b/src/module/cytoscapeMap.js @@ -240,7 +240,7 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu 'overlay-color': '#0099FF', 'overlay-opacity': 0.2, 'overlay-padding': '5px', - } + }, } ], }); diff --git a/src/stores/cytoscapeStore.ts b/src/stores/cytoscapeStore.ts index 70c8251..949448c 100644 --- a/src/stores/cytoscapeStore.ts +++ b/src/stores/cytoscapeStore.ts @@ -43,7 +43,6 @@ export default defineStore('useCytoscapeStore', { if(nodeToSave) { nodeToSave.position = position; } else { - console.log('aaaa this.nodePositions[this.currentGraphId]',this.nodePositions[this.currentGraphId] ); this.nodePositions[this.currentGraphId].push({id: nodeId, position: position}); } } else { diff --git a/src/stores/mapPathStore.ts b/src/stores/mapPathStore.ts index 95da443..4860962 100644 --- a/src/stores/mapPathStore.ts +++ b/src/stores/mapPathStore.ts @@ -1,16 +1,31 @@ import { defineStore } from 'pinia'; import AllMapData from '@/stores/allMapData.js'; import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants'; +import cytoscape, { EdgeSingular, NodeSingular } from 'cytoscape'; + +interface MapPathStoreState { + clickedPath: string[]; + insights: Record; // Replace `any` with the correct type if available + cytoscape: cytoscape.Core | null; + allPaths: NodeSingular[][]; + allPathsByEdge: EdgeSingular[][]; + startNode: NodeSingular | null; + mapGraphPathToInsight: Record; + activeTrace: number; + activeListIndex: number; +} export default defineStore('useMapPathStore', { - state: () => ({ + state: (): MapPathStoreState => ({ clickedPath: [], - allMapData: AllMapData(), - insights: [], + insights: {}, cytoscape: null, allPaths: [], + allPathsByEdge: [], startNode: null, mapGraphPathToInsight: {}, + activeTrace: 0, + activeListIndex: 0, }), actions: { setCytoscape(cytoscape) { @@ -21,12 +36,12 @@ export default defineStore('useMapPathStore', { return elem.data('label').toLocaleLowerCase() === 'start'; }); // Depth First Search from the starting node - this.depthFirstSearchCreatePath(this.startNode, [this.startNode]); - const { insights } = this.allMapData; - this.insights = insights; + this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []); + const { insights } = AllMapData(); + this.insights = {...insights}; this.matchGraphPathWithInsightsPath(); for(let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) { - const curButton = insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]; + const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]; for(let listIndex = 0; listIndex < curButton.length; listIndex++) { for(let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++){ //curButton[listIndex][nodeIndex]', curButton[listIndex][nodeIndex] @@ -35,57 +50,68 @@ export default defineStore('useMapPathStore', { } }, /** 從start節點開始建立所有的path + * 於第二個參數逐漸推入節點,於第三個參數逐漸推入線段 */ - depthFirstSearchCreatePath(node, currentPath){ + depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge){ const outgoingEdges = node.outgoers('edge'); if (outgoingEdges.length === 0) { // 表示已經遇到尾聲 - this.allPaths.push([...currentPath]); + this.allPaths.push([...currentPathByNode]); + this.allPathsByEdge.push([...curPathByEdge]) } else { outgoingEdges.targets().forEach((targetNode) => { - if (!currentPath.includes(targetNode)) { + if (!currentPathByNode.includes(targetNode)) { + const connectingEdge = targetNode.edgesWith(currentPathByNode[currentPathByNode.length - 1]); // 避免loop,只有當目標節點不在當前路徑中之時才繼續 - this.depthFirstSearchCreatePath(targetNode, [...currentPath, targetNode]); + this.depthFirstSearchCreatePath(targetNode, [...currentPathByNode, targetNode], + [...curPathByEdge, connectingEdge] + ); } }); } }, /** + * 比對兩條Paths是否相等。 + * 第一條path是透過depthFirstSearchCreatePath所建立, + * 而第二條path是從後端給的insights物件而來,其資料結構是簡單的array而已。 * 在每條path沿路據節點上的label之 * 字串來匹配這個path是屬於insights物件的哪一條path, - * 其中用curButton去接住insights[INSIGHTS_FIELDS_AND_LABELS[i][0]] + * 其中用curButton去記憶住insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]內文 * 而curButton[listIndex][nodeIndex]是用來確認是否跟depthFirstSearchCreatePath內的 - * node.data('label')字串完全相等 + * node.data('label')字串完全相等,也就是 activity 節點的文字 */ matchGraphPathWithInsightsPath(){ for(let whichPath = 0; whichPath < this.allPaths.length; whichPath++) { const curPath = this.allPaths[whichPath]; + const curPathByEdge = this.allPathsByEdge[whichPath]; // 針對這個path的第一個節點,找到它在insights中是對應到哪一個起點 for(let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) { const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]; for(let listIndex = 0; listIndex < curButton.length; listIndex++) { for(let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++){ if(curPath[1].data('label') === curButton[listIndex][nodeIndex]){ + // 從 1 開始而不是從 0 開始是因為 0 的label是start字串 const matchResult = this.depthFirstSearchMatchTwoPaths(curPath, 1, curButton, listIndex, nodeIndex) if(matchResult){ - this.mapGraphPathToInsight[whichPath.toString()] = { - [INSIGHTS_FIELDS_AND_LABELS[i][0]]: listIndex, - path: [...curPath], - }; - } + this.mapGraphPathToInsight[whichPath] = { + [listIndex] : { + pathByNode: [...curPath], + pathByEdge: [...curPathByEdge] + } + } + } // end if } - } - } - } - } - console.log('this.mapGraphPathToInsight', this.mapGraphPathToInsight['1']); + } // end fourth for + } // end third for + } // end second for + } // end first for + console.log('this.mapGraphPathToInsight[0][0]', this.mapGraphPathToInsight[0][0]); }, depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex){ - // console.log('listIndex', listIndex, 'nodeIndex', nodeIndex); - if(listIndex >= curButton.length) { + if(listIndex >= curButton.length) { // 邊界條件檢查,防止超出範圍 return; } - if(nodeIndex >= curButton[listIndex]) { + if(nodeIndex >= curButton[listIndex]) { // 邊界條件檢查,防止超出範圍 return; } // 如果 `curPath` 和 `curButton[listIndex]` 完全匹配 @@ -104,14 +130,28 @@ export default defineStore('useMapPathStore', { if(nodeIndex === curButton[listIndex].length - 1) { return true; // Reach } + //從以下兩個選項選出答案可能是true的。但也可能答案都是false + // 選項一是遞增insights的第一層的指標。這裡必須遞增path的指標 if(this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) { return true; } + // 選項二是遞增insights的第一層的指標。這裡必須遞增path的指標 if(this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) { return true; } } return false; // 當前節點不匹配時返回 false }, + highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number) { + this.activeTrace = clickedActiveTraceIndex; + this.activeListIndex = clickedPathListIndex; + this.mapGraphPathToInsight[clickedActiveTraceIndex][clickedPathListIndex].pathByEdge + .forEach(pathToHighlight => { + pathToHighlight.addClass('highlight-edge'); + }); + }, + clearAllHighlight() { + this.cytoscape.edges().removeClass('highlight-edge'); + }, }, }); \ No newline at end of file