import { create } from 'zustand'; import { api } from '@/services/api.ts'; import { useSitesStore } from '@/store/sites.ts'; import type { CoverageResult, CoverageSettings, CoverageApiStats } from '@/types/index.ts'; import type { ApiSiteParams } from '@/services/api.ts'; interface CoverageState { result: CoverageResult | null; isCalculating: boolean; settings: CoverageSettings; heatmapVisible: boolean; error: string | null; setResult: (result: CoverageResult | null) => void; clearCoverage: () => void; setIsCalculating: (val: boolean) => void; updateSettings: (settings: Partial) => void; toggleHeatmap: () => void; setHeatmapVisible: (val: boolean) => void; setError: (error: string | null) => void; // API-driven calculation calculateCoverage: () => Promise; cancelCalculation: () => void; } export const useCoverageStore = create((set, get) => ({ result: null, isCalculating: false, settings: { radius: 10, resolution: 200, rsrpThreshold: -100, heatmapOpacity: 0.7, heatmapRadius: 400, // Propagation model defaults (standard preset) preset: 'standard', use_terrain: true, use_buildings: true, use_materials: true, use_dominant_path: false, use_street_canyon: false, use_reflections: false, }, heatmapVisible: true, error: null, setResult: (result) => set({ result }), clearCoverage: () => set({ result: null, error: null }), setIsCalculating: (val) => set({ isCalculating: val }), updateSettings: (newSettings) => set((state) => ({ settings: { ...state.settings, ...newSettings }, })), toggleHeatmap: () => set((s) => ({ heatmapVisible: !s.heatmapVisible })), setHeatmapVisible: (val) => set({ heatmapVisible: val }), setError: (error) => set({ error }), calculateCoverage: async () => { const { settings } = get(); const sites = useSitesStore.getState().sites; if (sites.length === 0) { set({ error: 'No sites to calculate coverage for' }); return; } set({ isCalculating: true, error: null }); try { // Convert sites to API format // Each site is treated as a separate sector (flat model) const apiSites: ApiSiteParams[] = sites .filter((s) => s.visible) .map((site) => ({ lat: site.lat, lon: site.lon, height: site.height, power: site.power, // Already in dBm gain: site.gain, frequency: site.frequency, azimuth: site.antennaType === 'sector' ? site.azimuth : undefined, beamwidth: site.antennaType === 'sector' ? site.beamwidth : undefined, })); if (apiSites.length === 0) { set({ isCalculating: false, error: 'No visible sites to calculate' }); return; } const response = await api.calculateCoverage({ sites: apiSites, settings: { radius: settings.radius * 1000, // km → meters resolution: settings.resolution, min_signal: settings.rsrpThreshold, preset: settings.preset, use_terrain: settings.use_terrain, use_buildings: settings.use_buildings, use_materials: settings.use_materials, use_dominant_path: settings.use_dominant_path, use_street_canyon: settings.use_street_canyon, use_reflections: settings.use_reflections, }, }); // Map API response to CoverageResult for existing heatmap/boundary components const result: CoverageResult = { points: response.points.map((p) => ({ lat: p.lat, lon: p.lon, rsrp: p.rsrp, distance: p.distance, has_los: p.has_los, terrain_loss: p.terrain_loss, building_loss: p.building_loss, reflection_gain: p.reflection_gain, })), calculationTime: response.computation_time, totalPoints: response.count, settings: settings, stats: response.stats as CoverageApiStats, modelsUsed: response.models_used, }; set({ result, isCalculating: false, error: null, }); } catch (err) { if (err instanceof Error && err.name === 'AbortError') { set({ isCalculating: false }); } else { set({ isCalculating: false, error: err instanceof Error ? err.message : 'Coverage calculation failed', }); } } }, cancelCalculation: () => { api.cancelCalculation(); set({ isCalculating: false }); }, }));