From 95e9ffe7fbef0f89ac4376e7554704f1c48b40c2 Mon Sep 17 00:00:00 2001 From: Le Li Kruczek Date: Tue, 10 Jun 2025 17:07:55 -0400 Subject: [PATCH] added export feature --- .../spectrogram/SpectrogramVizContainer.tsx | 33 +++++++ frontend/src/components/spectrogram/index.tsx | 37 ++++++-- .../components/waterfall/WaterfallPlot.tsx | 5 +- .../waterfall/WaterfallVizContainer.tsx | 91 +++++++++++++++++++ frontend/src/components/waterfall/index.tsx | 80 ++++++++-------- 5 files changed, 196 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/spectrogram/SpectrogramVizContainer.tsx b/frontend/src/components/spectrogram/SpectrogramVizContainer.tsx index 0a2aa87..b056f0e 100644 --- a/frontend/src/components/spectrogram/SpectrogramVizContainer.tsx +++ b/frontend/src/components/spectrogram/SpectrogramVizContainer.tsx @@ -91,6 +91,38 @@ const SpectrogramVizContainer = ({ } }; + const handleSaveSpectrogram = async () => { + if (!spectrogramUrl) return; + + try { + // Fetch the image blob + const response = await fetch(spectrogramUrl); + const blob = await response.blob(); + + // Create a download link + const downloadUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + + // Generate filename based on timestamp and visualization UUID + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `spectrogram-${visualizationRecord.uuid}-${timestamp}.png`; + + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(downloadUrl); + } catch (error) { + console.error('Error saving spectrogram:', error); + setJobInfo((prevStatus) => ({ + ...prevStatus, + status: 'error', + message: 'Failed to save spectrogram', + })); + } + }; + /** * Once a job is created, periodically poll the server to check its status */ @@ -184,6 +216,7 @@ const SpectrogramVizContainer = ({ diff --git a/frontend/src/components/spectrogram/index.tsx b/frontend/src/components/spectrogram/index.tsx index d868fb3..9c9f334 100644 --- a/frontend/src/components/spectrogram/index.tsx +++ b/frontend/src/components/spectrogram/index.tsx @@ -1,16 +1,19 @@ interface SpectrogramVisualizationProps { imageUrl: string | null; hasError: boolean; + onSave?: () => void; } /** * Renders a spectrogram visualization or placeholder based on provided image URL * @param imageUrl - URL of the spectrogram image to display * @param hasError - Whether there was an error generating the spectrogram + * @param onSave - Optional callback function to handle saving the spectrogram */ const SpectrogramVisualization = ({ imageUrl, hasError, + onSave, }: SpectrogramVisualizationProps) => { return (
{imageUrl ? ( - Spectrogram visualization + <> + Spectrogram visualization + {onSave && ( + + )} + ) : (

{hasError diff --git a/frontend/src/components/waterfall/WaterfallPlot.tsx b/frontend/src/components/waterfall/WaterfallPlot.tsx index 50d3901..62450cf 100644 --- a/frontend/src/components/waterfall/WaterfallPlot.tsx +++ b/frontend/src/components/waterfall/WaterfallPlot.tsx @@ -581,7 +581,10 @@ export function WaterfallPlot({ /> )}

-
+
{ + try { + // Get all canvas elements + const periodogramCanvas = document.querySelector('#chartCanvas canvas'); + const waterfallPlotCanvas = document.querySelector( + '.waterfall-plot canvas:first-child', + ); + const waterfallOverlayCanvas = document.querySelector( + '.waterfall-plot canvas:last-child', + ); + + if ( + !periodogramCanvas || + !waterfallPlotCanvas || + !waterfallOverlayCanvas + ) { + throw new Error('Could not find all visualization canvases'); + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + // Create a new canvas to combine all elements + const combinedCanvas = document.createElement('canvas'); + const ctx = combinedCanvas.getContext('2d'); + if (!ctx) throw new Error('Could not create canvas context'); + + // Get dimensions + const periodogramHeight = (periodogramCanvas as HTMLCanvasElement).height; + const waterfallHeight = (waterfallPlotCanvas as HTMLCanvasElement).height; + const waterfallWidth = (waterfallPlotCanvas as HTMLCanvasElement).width; + + // Define padding + const padding = 20; // pixels of padding on all sides + + // Set combined canvas size with padding + combinedCanvas.width = waterfallWidth + padding * 2; + combinedCanvas.height = periodogramHeight + waterfallHeight + padding * 2; + + // Fill background with white + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height); + + // Draw periodogram with padding + ctx.drawImage(periodogramCanvas as HTMLCanvasElement, padding, padding); + + // Draw waterfall plot with padding + ctx.drawImage( + waterfallPlotCanvas as HTMLCanvasElement, + padding, + periodogramHeight + padding, + ); + + // Draw overlay on top of waterfall with padding + ctx.drawImage( + waterfallOverlayCanvas as HTMLCanvasElement, + padding, + periodogramHeight + padding, + ); + + // Convert combined canvas to blob + const combinedBlob = await new Promise((resolve, reject) => { + combinedCanvas.toBlob((blob: Blob | null) => { + if (blob) resolve(blob); + else reject(new Error('Failed to create blob from combined canvas')); + }, 'image/png'); + }); + + // Helper function to trigger download + const downloadFile = (blob: Blob, filename: string) => { + const downloadUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(downloadUrl); + }; + + // Download combined image + downloadFile( + combinedBlob, + `waterfall-visualization-${visualizationRecord.uuid}-${timestamp}.png`, + ); + } catch (error) { + console.error('Error saving visualization:', error); + // You might want to show an error message to the user here + } + }; + return (
@@ -72,6 +162,7 @@ export const WaterfallVizContainer = ({ rhFiles={rhFiles} settings={settings} setSettings={setSettings} + onSave={handleSaveWaterfall} /> diff --git a/frontend/src/components/waterfall/index.tsx b/frontend/src/components/waterfall/index.tsx index 54f7670..651ee20 100644 --- a/frontend/src/components/waterfall/index.tsx +++ b/frontend/src/components/waterfall/index.tsx @@ -138,12 +138,14 @@ interface WaterfallVisualizationProps { rhFiles: RadioHoundFile[]; settings: WaterfallSettings; setSettings: React.Dispatch>; + onSave?: () => void; } const WaterfallVisualization: React.FC = ({ rhFiles, settings, setSettings, + onSave, }: WaterfallVisualizationProps) => { const currentApplication = ['PERIODOGRAM', 'WATERFALL'] as ApplicationType[]; const [displayedFileIndex, setDisplayedFileIndex] = useState( @@ -727,45 +729,45 @@ const WaterfallVisualization: React.FC = ({ return (
-
- Scan {displayedFileIndex + 1} ({rhFiles[displayedFileIndex].timestamp}) -
- - {/* Div to test left plot margins */} - {/*
*/} - {/* Div to test right plot margins */} - {/*
*/} - + {onSave && ( +
+ +
+ )} +
+
+ + +
+
); };