148 lines
4.5 KiB
TypeScript
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 });
|
|
},
|
|
}));
|