Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
983 changes: 860 additions & 123 deletions api/main.py

Large diffs are not rendered by default.

124 changes: 121 additions & 3 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import MapOverlayControls from '@/components/MapOverlayControls';
import RouteIndicatorPanel from '@/components/RouteIndicatorPanel';
import AnalysisSlidePanel from '@/components/AnalysisSlidePanel';
import { useVoyage } from '@/components/VoyageContext';
import { apiClient, Position, WindFieldData, WaveFieldData, VelocityData, OptimizationResponse, CreateZoneRequest, WaveForecastFrames, OptimizedRouteKey, AllOptimizationResults, EMPTY_ALL_RESULTS } from '@/lib/api';
import { apiClient, Position, WindFieldData, WaveFieldData, VelocityData, OptimizationResponse, CreateZoneRequest, WaveForecastFrames, IceForecastFrames, OptimizedRouteKey, AllOptimizationResults, EMPTY_ALL_RESULTS } from '@/lib/api';
import { getAnalyses, saveAnalysis, deleteAnalysis, updateAnalysisMonteCarlo, AnalysisEntry } from '@/lib/analysisStorage';
import { debugLog } from '@/lib/debugLog';
import DebugConsole from '@/components/DebugConsole';

const MapComponent = dynamic(() => import('@/components/MapComponent'), { ssr: false });

type WeatherLayer = 'wind' | 'waves' | 'currents' | 'none';
type WeatherLayer = 'wind' | 'waves' | 'currents' | 'ice' | 'visibility' | 'sst' | 'swell' | 'none';

export default function HomePage() {
// Voyage context (shared with header dropdowns, persisted across navigation)
Expand All @@ -35,9 +35,13 @@ export default function HomePage() {
// Weather visualization
const [weatherLayer, setWeatherLayer] = useState<WeatherLayer>('none');
const [windData, setWindData] = useState<WindFieldData | null>(null);
const windDataBaseRef = useRef<WindFieldData | null>(null); // preserve ocean mask for forecast
const windFieldCacheRef = useRef<Record<string, WindFieldData>>({}); // per-frame cache
const windFieldCacheVersionRef = useRef<string>(''); // invalidated on new GFS run
const [waveData, setWaveData] = useState<WaveFieldData | null>(null);
const [windVelocityData, setWindVelocityData] = useState<VelocityData[] | null>(null);
const [currentVelocityData, setCurrentVelocityData] = useState<VelocityData[] | null>(null);
const [extendedWeatherData, setExtendedWeatherData] = useState<any>(null);
const [isLoadingWeather, setIsLoadingWeather] = useState(false);

// Viewport state
Expand Down Expand Up @@ -136,6 +140,11 @@ export default function HomePage() {
resolution: getResolutionForZoom(v.zoom),
};

// Clear stale extended data immediately when switching layers
if (activeLayer === 'ice' || activeLayer === 'visibility' || activeLayer === 'sst' || activeLayer === 'swell') {
setExtendedWeatherData(null);
}

setIsLoadingWeather(true);
const t0 = performance.now();
debugLog('info', 'API', `Loading ${activeLayer} weather: zoom=${v.zoom}, bbox=[${params.lat_min.toFixed(1)},${params.lat_max.toFixed(1)},${params.lon_min.toFixed(1)},${params.lon_max.toFixed(1)}]`);
Expand All @@ -148,6 +157,7 @@ export default function HomePage() {
const dt = (performance.now() - t0).toFixed(0);
debugLog('info', 'API', `Wind loaded in ${dt}ms: grid=${wind?.ny}x${wind?.nx}`);
setWindData(wind);
windDataBaseRef.current = wind; // stash for forecast frame reconstruction
setWindVelocityData(windVel);
} else if (activeLayer === 'waves') {
const waves = await apiClient.getWaveField(params);
Expand All @@ -159,6 +169,26 @@ export default function HomePage() {
const dt = (performance.now() - t0).toFixed(0);
debugLog('info', 'API', `Currents loaded in ${dt}ms: ${currentVel ? 'yes' : 'no data'}`);
setCurrentVelocityData(currentVel);
} else if (activeLayer === 'ice') {
const data = await apiClient.getIceField(params);
const dt = (performance.now() - t0).toFixed(0);
debugLog('info', 'API', `Ice loaded in ${dt}ms: grid=${data?.ny}x${data?.nx}`);
setExtendedWeatherData(data);
} else if (activeLayer === 'visibility') {
const data = await apiClient.getVisibilityField(params);
const dt = (performance.now() - t0).toFixed(0);
debugLog('info', 'API', `Visibility loaded in ${dt}ms: grid=${data?.ny}x${data?.nx}`);
setExtendedWeatherData(data);
} else if (activeLayer === 'sst') {
const data = await apiClient.getSstField(params);
const dt = (performance.now() - t0).toFixed(0);
debugLog('info', 'API', `SST loaded in ${dt}ms: grid=${data?.ny}x${data?.nx}`);
setExtendedWeatherData(data);
} else if (activeLayer === 'swell') {
const data = await apiClient.getSwellField(params);
const dt = (performance.now() - t0).toFixed(0);
debugLog('info', 'API', `Swell loaded in ${dt}ms: grid=${data?.ny}x${data?.nx}`);
setExtendedWeatherData(data);
}
} catch (error) {
debugLog('error', 'API', `Weather load failed: ${error}`);
Expand All @@ -177,8 +207,53 @@ export default function HomePage() {
// Handle wind forecast hour change from timeline
const handleForecastHourChange = useCallback((hour: number, data: VelocityData[] | null) => {
setForecastHour(hour);
if (data) {
if (data && data.length >= 2) {
setWindVelocityData(data);

// Lazy-cache: build WindFieldData once per (run, hour), reuse on loops
const hdr = data[0].header;
const version = hdr.refTime || '';
if (version !== windFieldCacheVersionRef.current) {
windFieldCacheRef.current = {};
windFieldCacheVersionRef.current = version;
}
const key = String(hour);
let field = windFieldCacheRef.current[key];
if (!field) {
const nx = hdr.nx;
const ny = hdr.ny;
const lats = Array.from({ length: ny }, (_, j) => hdr.la1 - j * hdr.dy);
const lons = Array.from({ length: nx }, (_, i) => hdr.lo1 + i * hdr.dx);
const flatU = data[0].data;
const flatV = data[1].data;
const u2d = Array.from({ length: ny }, (_, j) => {
const off = j * nx;
return Array.from({ length: nx }, (_, i) => flatU[off + i] ?? 0);
});
const v2d = Array.from({ length: ny }, (_, j) => {
const off = j * nx;
return Array.from({ length: nx }, (_, i) => flatV[off + i] ?? 0);
});
const base = windDataBaseRef.current;
field = {
parameter: 'wind',
time: version,
bbox: {
lat_min: Math.min(hdr.la1, hdr.la2),
lat_max: Math.max(hdr.la1, hdr.la2),
lon_min: hdr.lo1,
lon_max: hdr.lo2,
},
resolution: hdr.dx,
nx, ny, lats, lons,
u: u2d, v: v2d,
ocean_mask: base?.ocean_mask,
ocean_mask_lats: base?.ocean_mask_lats,
ocean_mask_lons: base?.ocean_mask_lons,
};
windFieldCacheRef.current[key] = field;
}
setWindData(field);
} else if (hour === 0) {
loadWeatherData();
}
Expand Down Expand Up @@ -230,6 +305,43 @@ export default function HomePage() {
setWaveData(synth);
}, [loadWeatherData]);

// Handle ice forecast hour change
const handleIceForecastHourChange = useCallback((hour: number, allFrames: IceForecastFrames | null) => {
setForecastHour(hour);
if (!allFrames) {
debugLog('warn', 'ICE', `Hour ${hour}: no frame data available`);
if (hour === 0) loadWeatherData();
return;
}
const frame = allFrames.frames?.[String(hour)];
if (!frame || !frame.data) {
debugLog('warn', 'ICE', `Hour ${hour}: frame not found in ${Object.keys(allFrames.frames).length} frames`);
return;
}
debugLog('info', 'ICE', `Frame Day ${hour / 24}: grid=${allFrames.ny}x${allFrames.nx}`);

setExtendedWeatherData({
parameter: 'ice_concentration',
time: allFrames.run_time,
bbox: {
lat_min: allFrames.lats[0],
lat_max: allFrames.lats[allFrames.lats.length - 1],
lon_min: allFrames.lons[0],
lon_max: allFrames.lons[allFrames.lons.length - 1],
},
resolution: allFrames.lats.length > 1 ? Math.abs(allFrames.lats[1] - allFrames.lats[0]) : 1,
nx: allFrames.nx,
ny: allFrames.ny,
lats: allFrames.lats,
lons: allFrames.lons,
data: frame.data,
unit: 'fraction',
ocean_mask: allFrames.ocean_mask,
ocean_mask_lats: allFrames.ocean_mask_lats,
ocean_mask_lons: allFrames.ocean_mask_lons,
});
}, [loadWeatherData]);

// Handle current forecast hour change
const handleCurrentForecastHourChange = useCallback((hour: number, allFrames: any | null) => {
setForecastHour(hour);
Expand Down Expand Up @@ -497,6 +609,10 @@ export default function HomePage() {
if (weatherLayer === 'wind') return 'NOAA GFS 0.25\u00B0';
if (weatherLayer === 'waves') return 'CMEMS WAV 1/12\u00B0';
if (weatherLayer === 'currents') return 'CMEMS PHY 1/12\u00B0';
if (weatherLayer === 'ice') return 'CMEMS ICE';
if (weatherLayer === 'visibility') return 'NOAA GFS';
if (weatherLayer === 'sst') return 'CMEMS PHY';
if (weatherLayer === 'swell') return 'CMEMS WAV';
return undefined;
}, [weatherLayer]);

Expand Down Expand Up @@ -529,9 +645,11 @@ export default function HomePage() {
onForecastHourChange={handleForecastHourChange}
onWaveForecastHourChange={handleWaveForecastHourChange}
onCurrentForecastHourChange={handleCurrentForecastHourChange}
onIceForecastHourChange={handleIceForecastHourChange}
onViewportChange={setViewport}
viewportBounds={viewport?.bounds ?? null}
weatherModelLabel={weatherModelLabel}
extendedWeatherData={extendedWeatherData}
>
<MapOverlayControls
weatherLayer={weatherLayer}
Expand Down
Loading