Files
rfcp/frontend/src/store/coverage.ts

148 lines
4.5 KiB
TypeScript

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<CoverageSettings>) => void;
toggleHeatmap: () => void;
setHeatmapVisible: (val: boolean) => void;
setError: (error: string | null) => void;
// API-driven calculation
calculateCoverage: () => Promise<void>;
cancelCalculation: () => void;
}
export const useCoverageStore = create<CoverageState>((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 });
},
}));