File size: 12,840 Bytes
9bd422a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/**
 * GraphPathHighlighter - Highlight đường Δ‘i trong Δ‘α»“ thα»‹
 * Tìm và highlight upstream path (từ input đến node) và downstream path (từ node đến output).
 * LΓ m mờ cΓ‘c node/edge khΓ΄ng thuα»™c đường Δ‘i. XΓ³a highlight khi click vΓ o vΓΉng trα»‘ng.
 * Requirements: 18.1, 18.2, 18.3, 18.4, 18.5
 */

class GraphPathHighlighter {
    constructor() {
        /** @type {boolean} Whether a path is currently highlighted */
        this._isActive = false;

        /** @type {string|null} The node ID being traced */
        this._tracedNodeId = null;

        /** @type {Function|null} EventBus unsubscribe for path:highlight-requested */
        this._unsubHighlight = null;

        /** @type {Function|null} EventBus unsubscribe for path:cleared */
        this._unsubCleared = null;

        /** @type {Function|null} EventBus unsubscribe for model:loaded */
        this._unsubModelLoaded = null;

        this._bindEvents();
    }

    // ─── Public API ────────────────────────────────────────────────────────────

    /**
     * Highlight the full path (upstream + downstream) through a node.
     * @param {cytoscape.Core} cy - Cytoscape instance
     * @param {string} nodeId - The node to trace paths through
     */
    highlightPath(cy, nodeId) {
        if (!cy || !nodeId) return;

        const node = cy.getElementById(nodeId);
        if (!node || node.length === 0) {
            console.warn('[GraphPathHighlighter] Node not found:', nodeId);
            return;
        }

        // Find upstream and downstream paths
        const upstreamIds = this._findUpstream(cy, nodeId);
        const downstreamIds = this._findDownstream(cy, nodeId);

        console.log('[GraphPathHighlighter] upstream:', upstreamIds.size, 'downstream:', downstreamIds.size, 'total nodes:', cy.nodes().length);

        // Combine all path node IDs (include the selected node itself)
        const pathNodeIds = new Set([...upstreamIds, nodeId, ...downstreamIds]);

        // If no path found at all (isolated node with no connections)
        if (pathNodeIds.size === 1 && upstreamIds.size === 0 && downstreamIds.size === 0) {
            this._showMessage('KhΓ΄ng tΓ¬m thαΊ₯y đường Δ‘i nΓ o qua node nΓ y.');
            return;
        }

        // Collect edges on the path
        const pathEdgeIds = this._findPathEdges(cy, pathNodeIds);

        // Set traced node BEFORE applying highlight so _applyHighlight can use it
        this._isActive = true;
        this._tracedNodeId = nodeId;

        // Apply highlight classes
        this._applyHighlight(cy, pathNodeIds, pathEdgeIds);

        // Emit event
        if (typeof EventBus !== 'undefined') {
            EventBus.emit('path:highlighted', {
                nodeId: nodeId,
                upstreamCount: upstreamIds.size,
                downstreamCount: downstreamIds.size
            });
        }
    }

    /**
     * Clear all path highlighting and restore normal graph appearance.
     * @param {cytoscape.Core} cy - Cytoscape instance
     */
    clearPath(cy) {
        if (!cy) return;

        cy.startBatch();
        cy.elements().removeClass('path-highlighted path-dimmed path-source');
        cy.elements().removeStyle('opacity');
        cy.endBatch();

        this._isActive = false;
        this._tracedNodeId = null;

        if (typeof EventBus !== 'undefined') {
            EventBus.emit('path:cleared', {});
        }
    }

    /**
     * Check if path highlighting is currently active.
     * @returns {boolean}
     */
    isActive() {
        return this._isActive;
    }

    /**
     * Get the currently traced node ID.
     * @returns {string|null}
     */
    getTracedNodeId() {
        return this._tracedNodeId;
    }

    /**
     * Destroy and clean up all resources.
     */
    destroy() {
        if (this._unsubHighlight) {
            this._unsubHighlight();
            this._unsubHighlight = null;
        }
        if (this._unsubCleared) {
            this._unsubCleared();
            this._unsubCleared = null;
        }
        if (this._unsubModelLoaded) {
            this._unsubModelLoaded();
            this._unsubModelLoaded = null;
        }
        this._isActive = false;
        this._tracedNodeId = null;
    }

    // ─── Path Finding ──────────────────────────────────────────────────────────

    /**
     * Find all upstream (ancestor) nodes via BFS backwards from the given node.
     * Traverses incoming edges to find all predecessors up to input nodes.
     * @param {cytoscape.Core} cy
     * @param {string} nodeId
     * @returns {Set<string>} Set of upstream node IDs
     * @private
     */
    _findUpstream(cy, nodeId) {
        const visited = new Set();
        const queue = [nodeId];

        while (queue.length > 0) {
            const currentId = queue.shift();
            const current = cy.getElementById(currentId);
            if (!current || current.length === 0) continue;

            // Get all incoming edges (edges where this node is the target)
            const incomers = current.incomers('node');
            incomers.forEach(function (predecessor) {
                const predId = predecessor.id();
                if (!visited.has(predId)) {
                    visited.add(predId);
                    queue.push(predId);
                }
            });
        }

        return visited;
    }

    /**
     * Find all downstream (descendant) nodes via BFS forward from the given node.
     * Traverses outgoing edges to find all successors down to output nodes.
     * @param {cytoscape.Core} cy
     * @param {string} nodeId
     * @returns {Set<string>} Set of downstream node IDs
     * @private
     */
    _findDownstream(cy, nodeId) {
        const visited = new Set();
        const queue = [nodeId];

        while (queue.length > 0) {
            const currentId = queue.shift();
            const current = cy.getElementById(currentId);
            if (!current || current.length === 0) continue;

            // Get all outgoing edges (edges where this node is the source)
            const outgoers = current.outgoers('node');
            outgoers.forEach(function (successor) {
                const sucId = successor.id();
                if (!visited.has(sucId)) {
                    visited.add(sucId);
                    queue.push(sucId);
                }
            });
        }

        return visited;
    }

    /**
     * Find all edges that connect nodes within the path set.
     * @param {cytoscape.Core} cy
     * @param {Set<string>} pathNodeIds
     * @returns {Set<string>} Set of edge IDs on the path
     * @private
     */
    _findPathEdges(cy, pathNodeIds) {
        const pathEdgeIds = new Set();

        cy.edges().forEach(function (edge) {
            var srcId = edge.source().id();
            var tgtId = edge.target().id();
            if (pathNodeIds.has(srcId) && pathNodeIds.has(tgtId)) {
                pathEdgeIds.add(edge.id());
            }
        });

        return pathEdgeIds;
    }

    // ─── Highlight Application ─────────────────────────────────────────────────

    /**
     * Apply visual highlight to path elements and dim non-path elements.
     * @param {cytoscape.Core} cy
     * @param {Set<string>} pathNodeIds
     * @param {Set<string>} pathEdgeIds
     * @private
     */
    _applyHighlight(cy, pathNodeIds, pathEdgeIds) {
        // First clear any existing path highlight
        cy.elements().removeClass('path-highlighted path-dimmed path-source');
        cy.elements().removeStyle('opacity');

        // Batch style changes for performance
        cy.startBatch();

        // Dim ALL elements first using inline style (more reliable than class)
        cy.elements().style('opacity', 0.15);

        // Un-dim and highlight path nodes
        pathNodeIds.forEach(function (id) {
            var el = cy.getElementById(id);
            if (el && el.length > 0) {
                el.style('opacity', 1);
                el.addClass('path-highlighted');
            }
        });

        // Un-dim and highlight path edges
        pathEdgeIds.forEach(function (id) {
            var el = cy.getElementById(id);
            if (el && el.length > 0) {
                el.style('opacity', 1);
                el.addClass('path-highlighted');
            }
        });

        // Mark the source node distinctly
        var sourceId = this._tracedNodeId;
        if (sourceId) {
            var sourceNode = cy.getElementById(sourceId);
            if (sourceNode && sourceNode.length > 0) {
                sourceNode.addClass('path-source');
            }
        }

        cy.endBatch();
    }

    // ─── Event Handling ────────────────────────────────────────────────────────

    /**
     * Bind EventBus listeners for path highlight requests and clearing.
     * @private
     */
    _bindEvents() {
        if (typeof EventBus === 'undefined' || typeof CONFIG === 'undefined') return;

        // Listen for path highlight requests (Shift+click or Trace Path button)
        this._unsubHighlight = EventBus.on('path:highlight-requested', (data) => {
            console.log('[GraphPathHighlighter] path:highlight-requested received:', data);
            if (!data || !data.nodeId) return;
            var cy = this._getCytoscapeInstance();
            if (!cy) {
                console.warn('[GraphPathHighlighter] No Cytoscape instance available');
                return;
            }
            console.log('[GraphPathHighlighter] Highlighting path for node:', data.nodeId);
            this.highlightPath(cy, data.nodeId);
            if (cy) {
                this.highlightPath(cy, data.nodeId);
            }
        });

        // Listen for path clear requests
        this._unsubCleared = EventBus.on('path:clear-requested', () => {
            var cy = this._getCytoscapeInstance();
            if (cy) {
                this.clearPath(cy);
            }
        });

        // Clear path when a new model is loaded
        this._unsubModelLoaded = EventBus.on(CONFIG.EVENTS.MODEL_LOADED, () => {
            var cy = this._getCytoscapeInstance();
            if (cy) {
                this.clearPath(cy);
            }
        });
    }

    /**
     * Attach Shift+click and background-click handlers to a Cytoscape instance.
     * Call this after the graph is initialized/rendered.
     * @param {cytoscape.Core} cy
     */
    attachCytoscapeHandlers(cy) {
        if (!cy) return;

        // Remove previous handlers to avoid duplicates
        cy.off('tap.pathHighlighter');

        // Shift+click on a node β†’ trace path
        cy.on('tap.pathHighlighter', 'node', (evt) => {
            if (evt.originalEvent && evt.originalEvent.shiftKey) {
                var nodeId = evt.target.id();
                this.highlightPath(cy, nodeId);
            }
        });

        // Click on background β†’ clear path
        cy.on('tap.pathHighlighter', (evt) => {
            if (evt.target === cy && this._isActive) {
                this.clearPath(cy);
            }
        });
    }

    // ─── Helpers ───────────────────────────────────────────────────────────────

    /**
     * Get the Cytoscape instance from the global app interface.
     * @returns {cytoscape.Core|null}
     * @private
     */
    _getCytoscapeInstance() {
        if (window._onnxApp && typeof window._onnxApp.getGraphVisualizer === 'function') {
            var gv = window._onnxApp.getGraphVisualizer();
            if (gv && gv._cy) {
                return gv._cy;
            }
        }
        return null;
    }

    /**
     * Show a temporary message to the user (e.g., "no path found").
     * @param {string} message
     * @private
     */
    _showMessage(message) {
        // Use EventBus error display if available
        if (typeof EventBus !== 'undefined' && typeof CONFIG !== 'undefined') {
            EventBus.emit(CONFIG.EVENTS.ERROR_OCCURRED, {
                message: message,
                type: 'info'
            });
            return;
        }
        console.info('[GraphPathHighlighter]', message);
    }
}

// Export as global for browser usage
window.GraphPathHighlighter = GraphPathHighlighter;