Spaces:
Sleeping
Sleeping
| // Copyright (c) 2025-2026, RTE (https://www.rte-france.com) | |
| // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. | |
| // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, | |
| // you can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. | |
| import { useEffect } from 'react'; | |
| import { api } from '../api'; | |
| import { cloneBaseSvg, applyPatchToClone } from '../utils/svgPatch'; | |
| import { processSvg } from '../utils/svgUtils'; | |
| import type { DiagramsState } from './useDiagrams'; | |
| import type { ConfirmDialogState } from '../components/modals/ConfirmationDialog'; | |
| interface UseContingencyFetchArgs { | |
| /** Ordered list of element IDs disconnected for the current contingency. */ | |
| selectedContingency: string[]; | |
| branches: string[]; | |
| voltageLevelsLength: number; | |
| diagrams: DiagramsState; | |
| analysisLoading: boolean; | |
| hasAnalysisState: () => boolean; | |
| clearContingencyState: () => void; | |
| setSelectedContingency: (v: string[]) => void; | |
| setConfirmDialog: (v: ConfirmDialogState) => void; | |
| setError: (v: string) => void; | |
| } | |
| const sameElements = (a: string[], b: string[]): boolean => { | |
| if (a.length !== b.length) return false; | |
| for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; | |
| return true; | |
| }; | |
| /** | |
| * Drives the contingency diagram fetch whenever the user commits a | |
| * contingency (or when a session reload rehydrates | |
| * ``selectedContingency`` under an analysis result that pre-exists the | |
| * diagram). Owns two paths: | |
| * | |
| * 1. Fast svgPatch DOM-recycling path — clone the N-state SVG and | |
| * mutate only what changed (dashed disconnected lines, flow labels, | |
| * overload halos). Avoids a fresh ~20 MB NAD fetch on large grids. | |
| * See docs/performance/history/svg-dom-recycling.md. | |
| * | |
| * 2. Full ``/api/contingency-diagram`` fallback — used when the base | |
| * N SVG is not yet mounted, the backend rejects the patch | |
| * (``patchable: false``), the patch throws, or we're restoring a | |
| * saved session (the reload contract mandates a full-fetch | |
| * round-trip). | |
| * | |
| * The effect also routes through the contingency-change confirmation | |
| * dialog when a study is already loaded, and short-circuits when the | |
| * fetch for the current ``selectedContingency`` is already done / in | |
| * flight — the EXCEPT clause that keeps session-restore from being | |
| * silently skipped is ``diagrams.restoringSessionRef.current``. | |
| */ | |
| export function useContingencyFetch({ | |
| selectedContingency, | |
| branches, | |
| voltageLevelsLength, | |
| diagrams, | |
| analysisLoading, | |
| hasAnalysisState, | |
| clearContingencyState, | |
| setSelectedContingency, | |
| setConfirmDialog, | |
| setError, | |
| }: UseContingencyFetchArgs): void { | |
| const { n1Diagram, n1Loading, nDiagram } = diagrams; | |
| useEffect(() => { | |
| if (selectedContingency.length === 0) { | |
| diagrams.setN1Diagram(null); | |
| if (!hasAnalysisState()) { | |
| diagrams.committedBranchRef.current = []; | |
| } | |
| return; | |
| } | |
| if (branches.length > 0) { | |
| // Reject if any element is unknown (e.g. stale typing partial). | |
| for (const elt of selectedContingency) { | |
| if (!branches.includes(elt)) return; | |
| } | |
| } | |
| const committed = diagrams.committedBranchRef.current; | |
| if (sameElements(selectedContingency, committed) | |
| && !diagrams.restoringSessionRef.current | |
| && (n1Diagram || hasAnalysisState() || n1Loading || analysisLoading)) return; | |
| if (!sameElements(selectedContingency, committed) | |
| && hasAnalysisState() | |
| && !diagrams.restoringSessionRef.current) { | |
| setConfirmDialog({ type: 'contingency', pendingBranch: selectedContingency.join('+') }); | |
| setSelectedContingency([...committed]); | |
| return; | |
| } | |
| // Capture BEFORE we reset the ref so the rest of this effect can | |
| // distinguish "user changed contingency" from "session was just | |
| // restored". Without the local capture, any later branch in | |
| // this effect would see ``restoringSessionRef.current === false`` | |
| // and incorrectly wipe the just-restored analysis state via | |
| // ``clearContingencyState()``. | |
| const isRestoring = diagrams.restoringSessionRef.current; | |
| diagrams.restoringSessionRef.current = false; | |
| diagrams.committedBranchRef.current = [...selectedContingency]; | |
| if (!isRestoring) { | |
| clearContingencyState(); | |
| diagrams.setN1Diagram(null); | |
| } | |
| const fetchContingency = async () => { | |
| diagrams.setN1Loading(true); | |
| // On user-initiated contingency change, switch to the contingency | |
| // tab so the fetched diagram is visible. On session restore, leave | |
| // the active tab alone so the user lands on whichever tab the | |
| // VisualizationPanel default (Action / Overflow) resolves to given | |
| // the restored state. | |
| if (!isRestoring) { | |
| diagrams.setActiveTab('contingency'); | |
| } | |
| // Fast path — svgPatch DOM-recycling. Clone the N-state SVG and | |
| // mutate only what changed (the dashed disconnected lines, flow | |
| // labels, overload halos) instead of fetching a fresh ~20 MB NAD. | |
| // Falls back to the full /api/contingency-diagram endpoint on any | |
| // error or when the base N DOM is not yet mounted. Session reload | |
| // always uses the full-fetch path to preserve the reload contract. | |
| // See docs/performance/history/svg-dom-recycling.md. | |
| const baseSvgEl = diagrams.nSvgContainerRef.current?.querySelector('svg') as SVGSVGElement | null; | |
| const baseMeta = diagrams.nMetaIndex; | |
| const canPatch = !!baseSvgEl && !!baseMeta && !isRestoring; | |
| if (canPatch) { | |
| console.log('[svgPatch] contingency patch PATH — calling /api/contingency-diagram-patch', selectedContingency); | |
| try { | |
| const patch = await api.getContingencyDiagramPatch(selectedContingency); | |
| if (patch.patchable) { | |
| console.log('[svgPatch] contingency patch applied (patchable=true)', selectedContingency); | |
| const cloned = cloneBaseSvg(baseSvgEl!); | |
| applyPatchToClone(cloned, baseMeta!, patch); | |
| diagrams.setN1Diagram({ | |
| svg: cloned, | |
| metadata: nDiagram?.metadata ?? null, | |
| originalViewBox: nDiagram?.originalViewBox ? { ...nDiagram.originalViewBox } : null, | |
| lines_overloaded: patch.lines_overloaded, | |
| lines_overloaded_rho: patch.lines_overloaded_rho, | |
| flow_deltas: patch.flow_deltas, | |
| reactive_flow_deltas: patch.reactive_flow_deltas, | |
| asset_deltas: patch.asset_deltas, | |
| lf_converged: patch.lf_converged, | |
| lf_status: patch.lf_status, | |
| }); | |
| diagrams.lastZoomState.current = { query: '', branch: '' }; | |
| diagrams.setN1Loading(false); | |
| return; | |
| } | |
| } catch (e) { | |
| console.warn('[svgPatch] contingency patch threw — falling back to full fetch', e); | |
| } | |
| } else { | |
| console.log('[svgPatch] contingency patch SKIPPED — no base N SVG/meta yet. baseSvgEl:', !!baseSvgEl, 'baseMeta:', !!baseMeta, 'restoring:', isRestoring); | |
| } | |
| try { | |
| const res = await api.getContingencyDiagram(selectedContingency); | |
| const { svg, viewBox } = processSvg(res.svg, voltageLevelsLength); | |
| diagrams.setN1Diagram({ ...res, svg, originalViewBox: viewBox }); | |
| // Ensure auto-zoom fires after the new contingency diagram is | |
| // ready. Reset lastZoomState so the auto-zoom effect sees a | |
| // "branch change" on the render that has both | |
| // ``activeTab='contingency'`` and the new SVG in DOM. | |
| diagrams.lastZoomState.current = { query: '', branch: '' }; | |
| } catch (err) { | |
| console.error('Failed to fetch contingency diagram', err); | |
| setError(`Failed to fetch contingency diagram for ${selectedContingency.join('+')}`); | |
| } finally { | |
| diagrams.setN1Loading(false); | |
| } | |
| }; | |
| fetchContingency(); | |
| }, [ | |
| selectedContingency, branches, voltageLevelsLength, hasAnalysisState, | |
| clearContingencyState, analysisLoading, n1Diagram, n1Loading, setError, | |
| diagrams, nDiagram, setConfirmDialog, setSelectedContingency, | |
| ]); | |
| } | |