@mytec: iter3.4.0 ready for testing

This commit is contained in:
2026-02-02 21:58:03 +02:00
parent 867ee3d0f4
commit 57106df5ae
8 changed files with 742 additions and 19 deletions

View File

@@ -65,6 +65,7 @@ export default function App() {
const heatmapVisible = useCoverageStore((s) => s.heatmapVisible);
const coverageError = useCoverageStore((s) => s.error);
const coverageProgress = useCoverageStore((s) => s.progress);
const partialPoints = useCoverageStore((s) => s.partialPoints);
const calculateCoverageApi = useCoverageStore((s) => s.calculateCoverage);
const cancelCalculation = useCoverageStore((s) => s.cancelCalculation);
@@ -658,20 +659,23 @@ export default function App() {
{/* Map */}
<div className="flex-1 relative">
<MapView onMapClick={handleMapClick} onEditSite={handleEditSite}>
{coverageResult && (
{/* Show partial results during tiled calculation, or final result */}
{(coverageResult || (isCalculating && partialPoints.length > 0)) && (
<>
<GeographicHeatmap
points={coverageResult.points}
points={isCalculating && partialPoints.length > 0 ? partialPoints : (coverageResult?.points ?? [])}
visible={heatmapVisible}
opacity={settings.heatmapOpacity}
radiusMeters={settings.heatmapRadius}
rsrpThreshold={settings.rsrpThreshold}
/>
<CoverageBoundary
points={coverageResult.points.filter(p => p.rsrp >= settings.rsrpThreshold)}
visible={heatmapVisible}
resolution={settings.resolution}
/>
{coverageResult && (
<CoverageBoundary
points={coverageResult.points.filter(p => p.rsrp >= settings.rsrpThreshold)}
visible={heatmapVisible}
resolution={settings.resolution}
/>
)}
</>
)}
</MapView>

View File

@@ -19,15 +19,24 @@ export interface WSProgress {
eta_seconds?: number;
}
export interface WSPartialResults {
points: Array<Record<string, unknown>>;
tile: number;
total_tiles: number;
progress: number;
}
type ProgressCallback = (progress: WSProgress) => void;
type ResultCallback = (data: CoverageResponse) => void;
type ErrorCallback = (error: string) => void;
type PartialResultsCallback = (data: WSPartialResults) => void;
type ConnectionCallback = (connected: boolean) => void;
interface PendingCalc {
onProgress?: ProgressCallback;
onResult: ResultCallback;
onError: ErrorCallback;
onPartialResults?: PartialResultsCallback;
}
class WebSocketService {
@@ -110,6 +119,16 @@ class WebSocketService {
console.warn('[WS] progress msg but no pending calc:', calcId, msg.phase, msg.progress);
}
break;
case 'partial_results':
if (pending?.onPartialResults) {
pending.onPartialResults({
points: msg.points,
tile: msg.tile,
total_tiles: msg.total_tiles,
progress: msg.progress,
});
}
break;
case 'result':
if (pending) {
pending.onResult(msg.data);
@@ -159,13 +178,14 @@ class WebSocketService {
onResult: ResultCallback,
onError: ErrorCallback,
onProgress?: ProgressCallback,
onPartialResults?: PartialResultsCallback,
): string | undefined {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return undefined;
}
const calcId = crypto.randomUUID();
this._pendingCalcs.set(calcId, { onProgress, onResult, onError });
this._pendingCalcs.set(calcId, { onProgress, onResult, onError, onPartialResults });
this.ws.send(JSON.stringify({
type: 'calculate',

View File

@@ -20,6 +20,11 @@ interface CoverageState {
progress: WSProgress | null;
activeCalcId: string | null;
// Progressive rendering — accumulates points as tiles complete
partialPoints: CoverageResult['points'];
tilesCompleted: number;
totalTiles: number;
setResult: (result: CoverageResult | null) => void;
clearCoverage: () => void;
setIsCalculating: (val: boolean) => void;
@@ -166,6 +171,9 @@ export const useCoverageStore = create<CoverageState>((set, get) => ({
error: null,
progress: null,
activeCalcId: null,
partialPoints: [],
tilesCompleted: 0,
totalTiles: 0,
setResult: (result) => set({ result }),
clearCoverage: () => set({ result: null, error: null }),
@@ -195,7 +203,7 @@ export const useCoverageStore = create<CoverageState>((set, get) => ({
const apiSettings = buildApiSettings(settings);
set({ isCalculating: true, error: null, progress: null, activeCalcId: null });
set({ isCalculating: true, error: null, progress: null, activeCalcId: null, partialPoints: [], tilesCompleted: 0, totalTiles: 0 });
// Try WebSocket first (provides real-time progress)
if (wsService.connected) {
@@ -206,7 +214,7 @@ export const useCoverageStore = create<CoverageState>((set, get) => ({
(data) => {
try {
const result = responseToResult(data, settings);
set({ result, isCalculating: false, error: null, progress: null, activeCalcId: null });
set({ result, isCalculating: false, error: null, progress: null, activeCalcId: null, partialPoints: [], tilesCompleted: 0, totalTiles: 0 });
// Show success toast for WS result
const addToast = useToastStore.getState().addToast;
if (result.points.length === 0) {
@@ -227,18 +235,40 @@ export const useCoverageStore = create<CoverageState>((set, get) => ({
}
} catch (err) {
console.error('[Coverage] Failed to process result:', err);
set({ isCalculating: false, error: 'Failed to process coverage result', progress: null, activeCalcId: null });
set({ isCalculating: false, error: 'Failed to process coverage result', progress: null, activeCalcId: null, partialPoints: [], tilesCompleted: 0, totalTiles: 0 });
}
},
// onError
(error) => {
set({ isCalculating: false, error, progress: null, activeCalcId: null });
set({ isCalculating: false, error, progress: null, activeCalcId: null, partialPoints: [], tilesCompleted: 0, totalTiles: 0 });
useToastStore.getState().addToast(`Calculation failed: ${error}`, 'error');
},
// onProgress
(progress) => {
set({ progress });
},
// onPartialResults — accumulate points as tiles complete
(data) => {
const newPoints = data.points.map((p: Record<string, unknown>) => ({
lat: p.lat as number,
lon: p.lon as number,
rsrp: p.rsrp as number,
distance: p.distance as number,
has_los: p.has_los as boolean,
terrain_loss: p.terrain_loss as number,
building_loss: p.building_loss as number,
reflection_gain: (p.reflection_gain as number) ?? 0,
vegetation_loss: (p.vegetation_loss as number) ?? 0,
rain_loss: (p.rain_loss as number) ?? 0,
indoor_loss: (p.indoor_loss as number) ?? 0,
atmospheric_loss: (p.atmospheric_loss as number) ?? 0,
}));
set((state) => ({
partialPoints: [...state.partialPoints, ...newPoints],
tilesCompleted: data.tile + 1,
totalTiles: data.total_tiles,
}));
},
);
if (calcId) {
@@ -278,6 +308,6 @@ export const useCoverageStore = create<CoverageState>((set, get) => ({
wsService.cancel(activeCalcId);
}
api.cancelCalculation();
set({ isCalculating: false, progress: null, activeCalcId: null });
set({ isCalculating: false, progress: null, activeCalcId: null, partialPoints: [], tilesCompleted: 0, totalTiles: 0 });
},
}));