@mytec: iter10.1 ready for testing
This commit is contained in:
@@ -20,6 +20,7 @@ import ToastContainer from '@/components/ui/Toast.tsx';
|
||||
import ThemeToggle from '@/components/ui/ThemeToggle.tsx';
|
||||
import Button from '@/components/ui/Button.tsx';
|
||||
import NumberInput from '@/components/ui/NumberInput.tsx';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog.tsx';
|
||||
|
||||
const calculator = new RFCalculator();
|
||||
|
||||
@@ -57,6 +58,7 @@ export default function App() {
|
||||
} | null>(null);
|
||||
const [panelCollapsed, setPanelCollapsed] = useState(false);
|
||||
const [showShortcuts, setShowShortcuts] = useState(false);
|
||||
const [kbDeleteTarget, setKbDeleteTarget] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
// Load sites from IndexedDB on mount
|
||||
useEffect(() => {
|
||||
@@ -92,6 +94,44 @@ export default function App() {
|
||||
setPendingLocation(null);
|
||||
}, []);
|
||||
|
||||
// Keyboard delete confirmation handler
|
||||
const handleKbDeleteConfirmed = useCallback(async () => {
|
||||
if (!kbDeleteTarget) return;
|
||||
const { id, name } = kbDeleteTarget;
|
||||
const siteData = sites.find((s) => s.id === id);
|
||||
setKbDeleteTarget(null);
|
||||
|
||||
await useSitesStore.getState().deleteSite(id);
|
||||
|
||||
if (siteData) {
|
||||
addToast(`"${name}" deleted`, 'info', {
|
||||
duration: 10000,
|
||||
action: {
|
||||
label: 'Undo',
|
||||
onClick: async () => {
|
||||
await useSitesStore.getState().addSite({
|
||||
name: siteData.name,
|
||||
lat: siteData.lat,
|
||||
lon: siteData.lon,
|
||||
height: siteData.height,
|
||||
power: siteData.power,
|
||||
gain: siteData.gain,
|
||||
frequency: siteData.frequency,
|
||||
antennaType: siteData.antennaType,
|
||||
azimuth: siteData.azimuth,
|
||||
beamwidth: siteData.beamwidth,
|
||||
color: siteData.color,
|
||||
visible: siteData.visible,
|
||||
notes: siteData.notes,
|
||||
equipment: siteData.equipment,
|
||||
});
|
||||
addToast(`"${siteData.name}" restored`, 'success');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [kbDeleteTarget, sites, addToast]);
|
||||
|
||||
// Calculate coverage (with better error handling)
|
||||
const handleCalculate = useCallback(async () => {
|
||||
const currentSites = useSitesStore.getState().sites;
|
||||
@@ -186,6 +226,9 @@ export default function App() {
|
||||
onCalculate: handleCalculate,
|
||||
onCloseForm: handleCloseForm,
|
||||
onShowShortcuts: useCallback(() => setShowShortcuts(true), []),
|
||||
onDeleteRequest: useCallback((id: string, name: string) => {
|
||||
setKbDeleteTarget({ id, name });
|
||||
}, []),
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -538,6 +581,19 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keyboard delete confirmation dialog */}
|
||||
{kbDeleteTarget && (
|
||||
<ConfirmDialog
|
||||
title="Delete Site?"
|
||||
message={`Are you sure you want to delete "${kbDeleteTarget.name}"? This action can be undone for 10 seconds.`}
|
||||
confirmLabel="Delete"
|
||||
cancelLabel="Cancel"
|
||||
danger
|
||||
onConfirm={handleKbDeleteConfirmed}
|
||||
onCancel={() => setKbDeleteTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,10 +57,16 @@ export default memo(function CoverageStats({ points, resolution }: CoverageStats
|
||||
const totalArea = estimateAreaKm2(points.length, resolution);
|
||||
const total = points.length;
|
||||
|
||||
const rsrpValues = points.map((p) => p.rsrp);
|
||||
const minRSRP = Math.min(...rsrpValues);
|
||||
const maxRSRP = Math.max(...rsrpValues);
|
||||
const avgRSRP = rsrpValues.reduce((a, b) => a + b, 0) / total;
|
||||
// Use reduce instead of Math.min/max spread — spread crashes on 65k+ elements
|
||||
let minRSRP = Infinity;
|
||||
let maxRSRP = -Infinity;
|
||||
let sumRSRP = 0;
|
||||
for (const p of points) {
|
||||
if (p.rsrp < minRSRP) minRSRP = p.rsrp;
|
||||
if (p.rsrp > maxRSRP) maxRSRP = p.rsrp;
|
||||
sumRSRP += p.rsrp;
|
||||
}
|
||||
const avgRSRP = sumRSRP / total;
|
||||
|
||||
// Unique sites contributing to coverage
|
||||
const uniqueSites = new Set(points.map((p) => p.siteId)).size;
|
||||
|
||||
@@ -18,14 +18,14 @@ export function getSignalQuality(rsrp: number): SignalQuality {
|
||||
return 'no-service';
|
||||
}
|
||||
|
||||
// New color scheme: cold = weak, warm = strong
|
||||
// Warm color scheme: dark = weak, bright = strong
|
||||
export const SIGNAL_COLORS: Record<string, string> = {
|
||||
excellent: '#f44336', // Red (very strong)
|
||||
excellent: '#ffeb3b', // Yellow (excellent)
|
||||
good: '#ff9800', // Orange (strong)
|
||||
fair: '#ffeb3b', // Yellow (acceptable)
|
||||
poor: '#4caf50', // Green (weak)
|
||||
weak: '#00bcd4', // Cyan (very weak)
|
||||
'no-service': '#0d47a1', // Dark Blue (no coverage)
|
||||
fair: '#ff4444', // Bright red (fair)
|
||||
poor: '#cc0000', // Red (weak)
|
||||
weak: '#8b0000', // Dark red (very weak)
|
||||
'no-service': '#3c1428', // Deep maroon (no coverage)
|
||||
} as const;
|
||||
|
||||
export function getRSRPColor(rsrp: number): string {
|
||||
@@ -33,10 +33,10 @@ export function getRSRPColor(rsrp: number): string {
|
||||
}
|
||||
|
||||
export const RSRP_LEGEND = [
|
||||
{ label: 'Excellent', range: '> -70 dBm', color: '#f44336', description: 'Very strong signal', min: -70 },
|
||||
{ label: 'Excellent', range: '> -70 dBm', color: '#ffeb3b', description: 'Very strong signal', min: -70 },
|
||||
{ label: 'Good', range: '-70 to -85 dBm', color: '#ff9800', description: 'Strong signal', min: -85 },
|
||||
{ label: 'Fair', range: '-85 to -100 dBm', color: '#ffeb3b', description: 'Acceptable signal', min: -100 },
|
||||
{ label: 'Poor', range: '-100 to -110 dBm', color: '#4caf50', description: 'Weak signal', min: -110 },
|
||||
{ label: 'Weak', range: '-110 to -120 dBm', color: '#00bcd4', description: 'Very weak signal', min: -120 },
|
||||
{ label: 'No Service', range: '< -120 dBm', color: '#0d47a1', description: 'No coverage', min: -140 },
|
||||
{ label: 'Fair', range: '-85 to -100 dBm', color: '#ff4444', description: 'Acceptable signal', min: -100 },
|
||||
{ label: 'Poor', range: '-100 to -110 dBm', color: '#cc0000', description: 'Weak signal', min: -110 },
|
||||
{ label: 'Weak', range: '-110 to -120 dBm', color: '#8b0000', description: 'Very weak signal', min: -120 },
|
||||
{ label: 'No Service', range: '< -120 dBm', color: '#3c1428', description: 'No coverage', min: -140 },
|
||||
] as const;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface ShortcutHandlers {
|
||||
onCalculate: () => void;
|
||||
onCloseForm: () => void;
|
||||
onShowShortcuts?: () => void;
|
||||
onDeleteRequest?: (siteId: string, siteName: string) => void;
|
||||
}
|
||||
|
||||
function isInputActive(): boolean {
|
||||
@@ -21,6 +22,7 @@ export function useKeyboardShortcuts({
|
||||
onCalculate,
|
||||
onCloseForm,
|
||||
onShowShortcuts,
|
||||
onDeleteRequest,
|
||||
}: ShortcutHandlers) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -85,41 +87,15 @@ export function useKeyboardShortcuts({
|
||||
case 'f': // F: Fit to coverage — dispatch custom event for Map.tsx to handle
|
||||
window.dispatchEvent(new CustomEvent('rfcp:fit-bounds'));
|
||||
return;
|
||||
case 'delete': // Delete key: delete selected site with undo
|
||||
case 'delete': // Delete key: show confirmation dialog
|
||||
case 'backspace':
|
||||
{
|
||||
const selectedId = useSitesStore.getState().selectedSiteId;
|
||||
if (selectedId) {
|
||||
if (selectedId && onDeleteRequest) {
|
||||
const site = useSitesStore.getState().sites.find(s => s.id === selectedId);
|
||||
if (site) {
|
||||
// Snapshot for undo
|
||||
const snapshot = { ...site };
|
||||
useSitesStore.getState().deleteSite(selectedId);
|
||||
useToastStore.getState().addToast(`"${site.name}" deleted`, 'info', {
|
||||
duration: 10000,
|
||||
action: {
|
||||
label: 'Undo',
|
||||
onClick: async () => {
|
||||
await useSitesStore.getState().addSite({
|
||||
name: snapshot.name,
|
||||
lat: snapshot.lat,
|
||||
lon: snapshot.lon,
|
||||
height: snapshot.height,
|
||||
power: snapshot.power,
|
||||
gain: snapshot.gain,
|
||||
frequency: snapshot.frequency,
|
||||
antennaType: snapshot.antennaType,
|
||||
azimuth: snapshot.azimuth,
|
||||
beamwidth: snapshot.beamwidth,
|
||||
color: snapshot.color,
|
||||
visible: snapshot.visible,
|
||||
notes: snapshot.notes,
|
||||
equipment: snapshot.equipment,
|
||||
});
|
||||
useToastStore.getState().addToast(`"${snapshot.name}" restored`, 'success');
|
||||
},
|
||||
},
|
||||
});
|
||||
e.preventDefault();
|
||||
onDeleteRequest(site.id, site.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,5 +118,5 @@ export function useKeyboardShortcuts({
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onCalculate, onCloseForm, onShowShortcuts]);
|
||||
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest]);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* RSRP → color mapping with smooth gradient interpolation.
|
||||
*
|
||||
* Gradient stops are chosen to match standard RF planning tools:
|
||||
* -130 dBm = deep blue (no service)
|
||||
* -90 dBm = green (fair)
|
||||
* -50 dBm = red (excellent)
|
||||
* Warm-only palette (no cyan/green):
|
||||
* -130 dBm = dark maroon (no service)
|
||||
* -90 dBm = orange (fair)
|
||||
* -50 dBm = bright red (excellent)
|
||||
*
|
||||
* All functions are pure and allocation-free on the hot path
|
||||
* (pre-built lookup table for fast per-pixel color resolution).
|
||||
@@ -18,16 +18,14 @@ interface GradientStop {
|
||||
}
|
||||
|
||||
const GRADIENT_STOPS: GradientStop[] = [
|
||||
{ value: 0.0, r: 26, g: 35, b: 126 }, // #1a237e — deep blue
|
||||
{ value: 0.15, r: 13, g: 71, b: 161 }, // #0d47a1
|
||||
{ value: 0.25, r: 33, g: 150, b: 243 }, // #2196f3 — blue
|
||||
{ value: 0.35, r: 0, g: 188, b: 212 }, // #00bcd4 — cyan
|
||||
{ value: 0.45, r: 0, g: 137, b: 123 }, // #00897b — teal
|
||||
{ value: 0.55, r: 76, g: 175, b: 80 }, // #4caf50 — green
|
||||
{ value: 0.65, r: 139, g: 195, b: 74 }, // #8bc34a — light green
|
||||
{ value: 0.75, r: 255, g: 235, b: 59 }, // #ffeb3b — yellow
|
||||
{ value: 0.85, r: 255, g: 152, b: 0 }, // #ff9800 — orange
|
||||
{ value: 1.0, r: 244, g: 67, b: 54 }, // #f44336 — red
|
||||
{ value: 0.0, r: 60, g: 20, b: 40 }, // #3c1428 — deep maroon (no service)
|
||||
{ value: 0.15, r: 100, g: 20, b: 30 }, // #64141e — dark red-brown
|
||||
{ value: 0.30, r: 139, g: 0, b: 0 }, // #8b0000 — dark red (very weak)
|
||||
{ value: 0.45, r: 204, g: 0, b: 0 }, // #cc0000 — red (weak)
|
||||
{ value: 0.60, r: 255, g: 68, b: 68 }, // #ff4444 — bright red (fair)
|
||||
{ value: 0.75, r: 255, g: 107, b: 53 }, // #ff6b35 — orange-red (good)
|
||||
{ value: 0.85, r: 255, g: 152, b: 0 }, // #ff9800 — orange (strong)
|
||||
{ value: 1.0, r: 255, g: 235, b: 59 }, // #ffeb3b — yellow (excellent)
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user