+
{formatAge(station.age)}
{station.speed > 0 && {station.speed} kt }
diff --git a/src/components/ActivateFilterManager.jsx b/src/components/ActivateFilterManager.jsx
new file mode 100644
index 00000000..fdc1598d
--- /dev/null
+++ b/src/components/ActivateFilterManager.jsx
@@ -0,0 +1,491 @@
+/**
+ * ActivateFilterManager Component
+ * Filter modal for ActivatePanel type spots - Bands, Grids, Modes
+ */
+import React, { useState } from 'react';
+
+const BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m', '70cm'];
+const MODES = [
+ '-FT8',
+ 'CONTESTI',
+ 'CW',
+ 'CWU',
+ 'DFCW-90',
+ 'DOMINO',
+ 'ECHO',
+ 'FREEDV',
+ 'FSK441',
+ 'FSQ',
+ 'FST4',
+ 'FST4W',
+ 'FST4W-90',
+ 'FT4',
+ 'FT8',
+ 'HELL',
+ 'HFDL',
+ 'JT',
+ 'JT4',
+ 'JT65',
+ 'JT65B',
+ 'JT9',
+ 'JTMS',
+ 'JS8',
+ 'LZ3CB',
+ 'MFSK16',
+ 'MFSK22',
+ 'MFSK32',
+ 'MSK144',
+ 'NULL',
+ 'OLIVIA',
+ 'OLIVIA 1',
+ 'OLIVIA 3',
+ 'OLIVIA 4',
+ 'OLIVIA 8',
+ 'OPERA',
+ 'PI4',
+ 'PKT',
+ 'POCSAG',
+ 'PSK',
+ 'PSK31',
+ 'PSK32',
+ 'PSK63',
+ 'Q65',
+ 'Q65-30A',
+ 'Q65A',
+ 'Q65B',
+ 'Q65D',
+ 'ROS',
+ 'RTTY',
+ 'RTTY 45',
+ 'SIM31',
+ 'SIM63',
+ 'SSB',
+ 'SSTV',
+ 'THOR-M',
+ 'THOR11',
+ 'THOR22',
+ 'THOR32',
+ 'THRB',
+ 'VARAC',
+ 'WSPR',
+];
+
+// Common grid field prefixes by region
+const GRID_REGIONS = [
+ { name: 'North America East', grids: ['FN', 'FM', 'EN', 'EM', 'DN', 'DM'] },
+ { name: 'North America West', grids: ['CN', 'CM', 'DM', 'DN', 'BN', 'BM'] },
+ { name: 'Europe', grids: ['JO', 'JN', 'IO', 'IN', 'KO', 'KN', 'LO', 'LN'] },
+ { name: 'South America', grids: ['GG', 'GH', 'GI', 'FG', 'FH', 'FI', 'FF', 'FE'] },
+ { name: 'Asia', grids: ['PM', 'PL', 'OM', 'OL', 'QL', 'QM', 'NM', 'NL'] },
+ { name: 'Oceania', grids: ['QF', 'QG', 'PF', 'PG', 'RF', 'RG', 'OF', 'OG'] },
+ { name: 'Africa', grids: ['KH', 'KG', 'JH', 'JG', 'IH', 'IG'] },
+];
+
+export const ActivateFilterManager = ({ filters, onFilterChange, isOpen, onClose, name }) => {
+ const [activeTab, setActiveTab] = useState('bands');
+ const [customGrid, setCustomGrid] = useState('');
+
+ if (!isOpen) return null;
+
+ const toggleArrayItem = (key, item) => {
+ const current = filters[key] || [];
+ const newArray = current.includes(item) ? current.filter((x) => x !== item) : [...current, item];
+ onFilterChange({ ...filters, [key]: newArray.length ? newArray : undefined });
+ };
+
+ const selectAll = (key, items) => {
+ onFilterChange({ ...filters, [key]: [...items] });
+ };
+
+ const clearFilter = (key) => {
+ const newFilters = { ...filters };
+ delete newFilters[key];
+ onFilterChange(newFilters);
+ };
+
+ const clearAllFilters = () => {
+ onFilterChange({});
+ };
+
+ const addCustomGrid = () => {
+ if (customGrid.trim() && customGrid.length >= 2) {
+ const grid = customGrid.toUpperCase().substring(0, 2);
+ const current = filters?.grids || [];
+ if (!current.includes(grid)) {
+ onFilterChange({ ...filters, grids: [...current, grid] });
+ }
+ setCustomGrid('');
+ }
+ };
+
+ const getActiveFilterCount = () => {
+ let count = 0;
+ if (filters?.bands?.length) count += filters.bands.length;
+ if (filters?.grids?.length) count += filters.grids.length;
+ if (filters?.modes?.length) count += filters.modes.length;
+ if (filters?.direction && filters.direction !== 'both') count += 1;
+ return count;
+ };
+
+ const tabStyle = (active) => ({
+ padding: '8px 16px',
+ background: active ? 'var(--accent-amber)' : 'transparent',
+ border: 'none',
+ borderBottom: active ? '2px solid var(--accent-amber)' : '2px solid transparent',
+ color: active ? '#000' : 'var(--text-muted)',
+ fontSize: '13px',
+ cursor: 'pointer',
+ fontFamily: 'inherit',
+ fontWeight: active ? '600' : '400',
+ });
+
+ const chipStyle = (selected) => ({
+ padding: '6px 12px',
+ background: selected ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
+ border: `1px solid ${selected ? 'var(--accent-amber)' : 'var(--border-color)'}`,
+ borderRadius: '4px',
+ color: selected ? '#000' : 'var(--text-secondary)',
+ fontSize: '12px',
+ cursor: 'pointer',
+ fontFamily: 'JetBrains Mono, monospace',
+ fontWeight: selected ? '600' : '400',
+ });
+
+ const renderBandsTab = () => (
+
+
+
Filter by Band
+
+ selectAll('bands', BANDS)}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'var(--accent-cyan)',
+ fontSize: '12px',
+ cursor: 'pointer',
+ }}
+ >
+ Select All
+
+ clearFilter('bands')}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'var(--accent-red)',
+ fontSize: '12px',
+ cursor: 'pointer',
+ }}
+ >
+ Clear
+
+
+
+
+ {BANDS.map((band) => (
+ toggleArrayItem('bands', band)}
+ style={chipStyle(filters?.bands?.includes(band))}
+ >
+ {band}
+
+ ))}
+
+
+ {filters?.bands?.length ? `Showing only: ${filters.bands.join(', ')}` : 'Showing all bands (no filter)'}
+
+
+ );
+
+ const renderGridsTab = () => (
+
+
+ Filter by Grid Square
+ clearFilter('grids')}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'var(--accent-red)',
+ fontSize: '12px',
+ cursor: 'pointer',
+ }}
+ >
+ Clear All
+
+
+
+ {/* Custom grid input */}
+
+ setCustomGrid(e.target.value.toUpperCase())}
+ maxLength={2}
+ onKeyPress={(e) => e.key === 'Enter' && addCustomGrid()}
+ style={{
+ flex: 1,
+ padding: '8px 12px',
+ background: 'var(--bg-tertiary)',
+ border: '1px solid var(--border-color)',
+ borderRadius: '4px',
+ color: 'var(--text-primary)',
+ fontSize: '13px',
+ fontFamily: 'JetBrains Mono',
+ }}
+ />
+
+ Add
+
+
+
+ {/* Selected grids */}
+ {filters?.grids?.length > 0 && (
+
+
Active Grid Filters:
+
+ {filters.grids.map((grid) => (
+ toggleArrayItem('grids', grid)}
+ style={{
+ ...chipStyle(true),
+ display: 'flex',
+ alignItems: 'center',
+ gap: '6px',
+ }}
+ >
+ {grid}
+ ×
+
+ ))}
+
+
+ )}
+
+ {/* Quick select by region */}
+
Quick Select by Region:
+ {GRID_REGIONS.map((region) => (
+
+
{region.name}
+
+ {region.grids.map((grid) => (
+ toggleArrayItem('grids', grid)}
+ style={{
+ ...chipStyle(filters?.grids?.includes(grid)),
+ padding: '4px 8px',
+ fontSize: '11px',
+ }}
+ >
+ {grid}
+
+ ))}
+
+
+ ))}
+
+ );
+
+ const renderModesTab = () => (
+
+
+
Filter by Mode
+
+ selectAll('modes', MODES)}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'var(--accent-cyan)',
+ fontSize: '12px',
+ cursor: 'pointer',
+ }}
+ >
+ Select All
+
+ clearFilter('modes')}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'var(--accent-red)',
+ fontSize: '12px',
+ cursor: 'pointer',
+ }}
+ >
+ Clear
+
+
+
+
+ {MODES.map((mode) => (
+ toggleArrayItem('modes', mode)}
+ style={chipStyle(filters?.modes?.includes(mode))}
+ >
+ {mode}
+
+ ))}
+
+
+ {filters?.modes?.length ? `Showing only: ${filters.modes.join(', ')}` : 'Showing all modes (no filter)'}
+
+
+ );
+
+ return (
+
e.target === e.currentTarget && onClose()}
+ >
+
+ {/* Header */}
+
+
+
⌇ {name} Filters
+
+ {getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active
+
+
+
+ ×
+
+
+
+ {/* Tabs */}
+
+ setActiveTab('bands')} style={tabStyle(activeTab === 'bands')}>
+ Bands {filters?.bands?.length ? `(${filters.bands.length})` : ''}
+
+ setActiveTab('grids')} style={tabStyle(activeTab === 'grids')}>
+ Grids {filters?.grids?.length ? `(${filters.grids.length})` : ''}
+
+ setActiveTab('modes')} style={tabStyle(activeTab === 'modes')}>
+ Modes {filters?.modes?.length ? `(${filters.modes.length})` : ''}
+
+
+
+ {/* Tab Content */}
+
+ {activeTab === 'bands' && renderBandsTab()}
+ {activeTab === 'grids' && renderGridsTab()}
+ {activeTab === 'modes' && renderModesTab()}
+
+
+ {/* Footer */}
+
+
+ Clear All Filters
+
+
+ Done
+
+
+
+
+ );
+};
+
+export default ActivateFilterManager;
diff --git a/src/components/ActivatePanel.jsx b/src/components/ActivatePanel.jsx
index b35e308c..df622745 100644
--- a/src/components/ActivatePanel.jsx
+++ b/src/components/ActivatePanel.jsx
@@ -4,6 +4,7 @@
*/
import React from 'react';
import CallsignLink from './CallsignLink.jsx';
+import { IconSearch, IconRefresh, IconMap, IconTag } from './Icons.jsx';
export const ActivatePanel = ({
name,
@@ -19,10 +20,20 @@ export const ActivatePanel = ({
onToggleLabelsOnMap,
onSpotClick,
onHoverSpot,
+ filters,
+ onOpenFilters,
+ filteredData,
}) => {
const staleMinutes = lastUpdated ? Math.floor((Date.now() - lastUpdated) / 60000) : null;
const isStale = staleMinutes !== null && staleMinutes >= 5;
const checkedTime = lastChecked ? new Date(lastChecked).toISOString().substr(11, 5) + 'z' : '';
+ const filterActiveColor = '#ffaa00';
+ const spots = filteredData ? filteredData : data;
+
+ let filterCount = 0;
+ if (filters?.bands?.length) filterCount += filters.bands.length;
+ if (filters?.grids?.length) filterCount += filters.grids.length;
+ if (filters?.modes?.length) filterCount += filters.modes.length;
return (
@@ -61,24 +72,27 @@ export const ActivatePanel = ({
)}
+
-
- ⊞ Map {showOnMap ? 'ON' : 'OFF'}
-
+ {typeof onOpenFilters === 'function' && (
+ 0 ? `${filterActiveColor}30` : 'rgba(100,100,100,0.3)',
+ border: `1px solid ${filterCount > 0 ? filterActiveColor : '#555'}`,
+ color: filterCount > 0 ? filterActiveColor : '#777',
+ padding: '2px 6px',
+ borderRadius: '3px',
+ fontSize: '10px',
+ cursor: 'pointer',
+ lineHeight: 1,
+ }}
+ >
+
+ {filterCount > 0 ? filterCount : ''}
+
+ )}
{typeof onToggleLabelsOnMap === 'function' && (
- ⊞ Calls {showLabelsOnMap ? 'ON' : 'OFF'}
+
)}
+
+
+
+
@@ -106,9 +138,9 @@ export const ActivatePanel = ({
- ) : data && data.length > 0 ? (
+ ) : spots && spots.length > 0 ? (
- {data.map((spot, i) => (
+ {spots.map((spot, i) => (
{
return (
);
};
diff --git a/src/components/PSKFilterManager.jsx b/src/components/PSKFilterManager.jsx
index 6ecec5d4..db089b37 100644
--- a/src/components/PSKFilterManager.jsx
+++ b/src/components/PSKFilterManager.jsx
@@ -138,7 +138,9 @@ export const PSKFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =
return (
-
Map Direction Filter
+
+ Map Direction Filter
+
Useful when WSJT-X is also enabled — avoids duplicate plots for the same stations.
@@ -161,14 +163,30 @@ export const PSKFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =
textAlign: 'left',
}}
>
-
- {direction === opt.value ? '● ' : '○ '}{opt.label}
+
+ {direction === opt.value ? '● ' : '○ '}
+ {opt.label}
{opt.desc}
))}
-
+
Map Legend:
━━ TX (solid line, ● circle)
diff --git a/src/components/PotaSotaPanel.jsx b/src/components/PotaSotaPanel.jsx
index 8411a077..cc399866 100644
--- a/src/components/PotaSotaPanel.jsx
+++ b/src/components/PotaSotaPanel.jsx
@@ -18,28 +18,51 @@ export const PotaSotaPanel = ({
potaLastChecked,
showPOTA,
onTogglePOTA,
+ showPOTALabels,
+ togglePOTALabels,
+ onPOTASpotClick,
+ potaFilters,
+ setShowPotaFilters,
+ filteredPotaSpots,
+
wwffData,
wwffLoading,
wwffLastUpdated,
wwffLastChecked,
showWWFF,
onToggleWWFF,
+ showWWFFLabels,
+ toggleWWFFLabels,
+ onWWFFSpotClick,
+ wwffFilters,
+ setShowWwffFilters,
+ filteredWwffSpots,
+
sotaData,
sotaLoading,
sotaLastUpdated,
sotaLastChecked,
showSOTA,
onToggleSOTA,
+ showSOTALabels,
+ toggleSOTALabels,
+ onSOTASpotClick,
+ sotaFilters,
+ setShowSotaFilters,
+ filteredSotaSpots,
+
wwbotaData,
wwbotaLoading,
wwbotaLastUpdated,
wwbotaConnected,
showWWBOTA,
onToggleWWBOTA,
- onPOTASpotClick,
- onWWFFSpotClick,
- onSOTASpotClick,
+ showWWBOTALabels,
+ toggleWWBOTALabels,
onWWBOTASpotClick,
+ wwbotaFilters,
+ setShowWwbotaFilters,
+ filteredWwbotaSpots,
}) => {
const [activeTab, setActiveTab] = useState(() => {
try {
@@ -127,6 +150,11 @@ export const PotaSotaPanel = ({
showOnMap={showPOTA}
onToggleMap={onTogglePOTA}
onSpotClick={onPOTASpotClick}
+ showLabelsOnMap={showPOTALabels}
+ onToggleLabelsOnMap={togglePOTALabels}
+ filters={potaFilters}
+ onOpenFilters={setShowPotaFilters}
+ filteredData={filteredPotaSpots}
/>
) : activeTab === 'sota' ? (
) : activeTab === 'wwbota' ? (
) : (
)}
diff --git a/src/components/SOTAPanel.jsx b/src/components/SOTAPanel.jsx
index 48a94dbf..39e8dec0 100644
--- a/src/components/SOTAPanel.jsx
+++ b/src/components/SOTAPanel.jsx
@@ -15,6 +15,9 @@ export const SOTAPanel = ({
onToggleLabelsOnMap = true,
onSpotClick,
onHoverSpot,
+ filters,
+ onOpenFilters,
+ filteredData,
}) => {
return (
);
};
diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx
index 1a046b74..a0b73b9d 100644
--- a/src/components/SettingsPanel.jsx
+++ b/src/components/SettingsPanel.jsx
@@ -2357,7 +2357,14 @@ export const SettingsPanel = ({
marginBottom: '12px',
}}
>
-
+
{layer.icon}
-
+
{layer.name.startsWith('plugins.') ? t(layer.name) : layer.name}
{layer.description && (
@@ -2381,7 +2395,16 @@ export const SettingsPanel = ({
{layer.enabled && (
-
+
{t('station.settings.layers.opacity')}: {Math.round(layer.opacity * 100)}%
{ctrlPressed &&
- ['lightning', 'wspr', 'rbn', 'grayline', 'n3fjp_logged_qsos', 'voacap-heatmap'].includes(layer.id) && (
+ ['lightning', 'wspr', 'rbn', 'grayline', 'n3fjp_logged_qsos', 'voacap-heatmap'].includes(
+ layer.id,
+ ) && (
resetPopupPositions(layer.id)}
- style={{ marginTop: '12px', padding: '8px 12px', background: 'var(--accent-red)', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '11px', fontWeight: '600', textTransform: 'uppercase', width: '100%' }}
+ style={{
+ marginTop: '12px',
+ padding: '8px 12px',
+ background: 'var(--accent-red)',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontSize: '11px',
+ fontWeight: '600',
+ textTransform: 'uppercase',
+ width: '100%',
+ }}
>
🔄 RESET POPUPS
@@ -2433,9 +2470,11 @@ export const SettingsPanel = ({
});
});
// Any uncategorized leftovers
- nonSatLayers.filter((l) => !rendered.has(l.id)).forEach((layer) => {
- result.push(renderLayerCard(layer));
- });
+ nonSatLayers
+ .filter((l) => !rendered.has(l.id))
+ .forEach((layer) => {
+ result.push(renderLayerCard(layer));
+ });
return result;
})()
) : (
diff --git a/src/components/WWBOTAPanel.jsx b/src/components/WWBOTAPanel.jsx
index 1059c913..12f69c5a 100644
--- a/src/components/WWBOTAPanel.jsx
+++ b/src/components/WWBOTAPanel.jsx
@@ -15,6 +15,9 @@ export const WWBOTAPanel = ({
onToggleLabelsOnMap,
onSpotClick,
onHoverSpot,
+ filters,
+ onOpenFilters,
+ filteredData,
}) => {
return (
);
};
diff --git a/src/components/WWFFPanel.jsx b/src/components/WWFFPanel.jsx
index d7fa1d46..20ecb40b 100644
--- a/src/components/WWFFPanel.jsx
+++ b/src/components/WWFFPanel.jsx
@@ -15,6 +15,9 @@ export const WWFFPanel = ({
onToggleLabelsOnMap,
onSpotClick,
onHoverSpot,
+ filters,
+ onOpenFilters,
+ filteredData,
}) => {
return (
);
};
diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx
index 7998edc7..bbf36f11 100644
--- a/src/components/WorldMap.jsx
+++ b/src/components/WorldMap.jsx
@@ -2431,4 +2431,4 @@ export const WorldMap = ({
);
};
-export default WorldMap;
\ No newline at end of file
+export default WorldMap;
diff --git a/src/components/index.js b/src/components/index.js
index a04c1214..788375c8 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -19,6 +19,7 @@ export { LocationPanel } from './LocationPanel.jsx';
export { SettingsPanel } from './SettingsPanel.jsx';
export { DXFilterManager } from './DXFilterManager.jsx';
export { PSKFilterManager } from './PSKFilterManager.jsx';
+export { ActivateFilterManager } from './ActivateFilterManager.jsx';
export { SolarPanel } from './SolarPanel.jsx';
export { PropagationPanel } from './PropagationPanel.jsx';
export { DXpeditionPanel } from './DXpeditionPanel.jsx';
diff --git a/src/hooks/app/useFilters.js b/src/hooks/app/useFilters.js
index cdf4c70b..0d13d099 100644
--- a/src/hooks/app/useFilters.js
+++ b/src/hooks/app/useFilters.js
@@ -36,6 +36,70 @@ export default function useFilters() {
} catch (e) {}
}, [pskFilters]);
+ // POTA Filters
+ const [potaFilters, setPotaFilters] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_potaFilters');
+ return stored ? JSON.parse(stored) : {};
+ } catch (e) {
+ return {};
+ }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_potaFilters', JSON.stringify(potaFilters));
+ syncAllSettingsToServer();
+ } catch (e) {}
+ }, [potaFilters]);
+
+ // SOTA Filters
+ const [sotaFilters, setSotaFilters] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_sotaFilters');
+ return stored ? JSON.parse(stored) : {};
+ } catch (e) {
+ return {};
+ }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_sotaFilters', JSON.stringify(sotaFilters));
+ syncAllSettingsToServer();
+ } catch (e) {}
+ }, [sotaFilters]);
+
+ // WWFF Filters
+ const [wwffFilters, setWwffFilters] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_wwffFilters');
+ return stored ? JSON.parse(stored) : {};
+ } catch (e) {
+ return {};
+ }
+ });
+
+ useEffect(() => {
+ localStorage.setItem('openhamclock_wwffFilters', JSON.stringify(wwffFilters));
+ syncAllSettingsToServer();
+ }, [wwffFilters]);
+
+ // WWBOTA Filters
+ const [wwbotaFilters, setWwbotaFilters] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_wwbotaFilters');
+ return stored ? JSON.parse(stored) : {};
+ } catch (e) {
+ return {};
+ }
+ });
+
+ useEffect(() => {
+ localStorage.setItem('openhamclock_wwbotaFilters', JSON.stringify(wwbotaFilters));
+ syncAllSettingsToServer();
+ }, [wwffFilters]);
+
const [mapBandFilter, setMapBandFilter] = useState(() => {
try {
const stored = localStorage.getItem('openhamclock_mapBandFilter');
@@ -57,7 +121,15 @@ export default function useFilters() {
dxFilters,
setDxFilters,
pskFilters,
+ potaFilters,
+ sotaFilters,
+ wwffFilters,
+ wwbotaFilters,
setPskFilters,
+ setPotaFilters,
+ setSotaFilters,
+ setWwffFilters,
+ setWwbotaFilters,
mapBandFilter,
setMapBandFilter,
};
diff --git a/src/hooks/usePOTASpots.js b/src/hooks/usePOTASpots.js
index 95bb07a2..d92494cd 100644
--- a/src/hooks/usePOTASpots.js
+++ b/src/hooks/usePOTASpots.js
@@ -5,6 +5,8 @@
import { useState, useEffect, useRef } from 'react';
import { useVisibilityRefresh } from './useVisibilityRefresh';
import { apiFetch } from '../utils/apiFetch';
+import { WGS84ToMaidenhead } from '@hamset/maidenhead-locator';
+import { getBandFromFreq } from '../utils/callsign';
// Convert grid square to lat/lon
function gridToLatLon(grid) {
@@ -122,6 +124,7 @@ export const usePOTASpots = () => {
call: s.activator,
ref: s.reference,
freq: freqMhz ? freqMhz.toString() : s.frequency, // Convert to MHz string
+ band: getBandFromFreq(s.frequency),
mode: s.mode,
name: s.name || s.locationDesc,
locationDesc: s.locationDesc,
@@ -137,6 +140,7 @@ export const usePOTASpots = () => {
})()
: '',
expire: s.expire || 0,
+ grid: s.grid6 ? s.grid6 : s.grid4 ? s.grid4 : WGS84ToMaidenhead({ lat: lat, lng: lon }),
};
}),
);
diff --git a/src/hooks/useSOTASpots.js b/src/hooks/useSOTASpots.js
index c5c77457..6c320e61 100644
--- a/src/hooks/useSOTASpots.js
+++ b/src/hooks/useSOTASpots.js
@@ -5,6 +5,8 @@
import { useState, useEffect, useRef } from 'react';
import { useVisibilityRefresh } from './useVisibilityRefresh';
import { apiFetch } from '../utils/apiFetch';
+import { WGS84ToMaidenhead } from '@hamset/maidenhead-locator';
+import { getBandFromFreq } from '../utils/callsign';
export const useSOTASpots = () => {
const [data, setData] = useState([]);
@@ -70,6 +72,7 @@ export const useSOTASpots = () => {
altM: details.altM || details.altitude || null,
points: details.points || s.points || null,
freq,
+ band: getBandFromFreq(s.frequency),
mode: s.mode || '',
comments: s.comments || '',
lat,
@@ -83,6 +86,7 @@ export const useSOTASpots = () => {
return new Date(ts).toISOString().substr(11, 5) + 'z';
})()
: '',
+ grid: WGS84ToMaidenhead({ lat: lat, lng: lon }),
};
});
diff --git a/src/hooks/useWWBOTASpots.js b/src/hooks/useWWBOTASpots.js
index 07227d77..7ff986e1 100644
--- a/src/hooks/useWWBOTASpots.js
+++ b/src/hooks/useWWBOTASpots.js
@@ -6,6 +6,8 @@
* Handles spot data with frequency, callsign, bunker reference, and coordinates
*/
import { useState, useEffect, useRef } from 'react';
+import { WGS84ToMaidenhead } from '@hamset/maidenhead-locator';
+import { getBandFromFreq } from '../utils';
export const useWWBOTASpots = () => {
const [data, setData] = useState([]);
@@ -112,6 +114,7 @@ export const useWWBOTASpots = () => {
call,
ref: refs,
freq,
+ band: getBandFromFreq(freq),
mode,
spotter,
name: name,
@@ -127,6 +130,7 @@ export const useWWBOTASpots = () => {
lon,
time: time ? time.substring(11, 16) + 'z' : '', // Extract HH:MM from ISO string
type: spot.type || 'Live', // Live, QRT, or Test
+ grid: WGS84ToMaidenhead({ lat: lat, lng: lon }),
};
// Skip QRT spots
diff --git a/src/hooks/useWWFFSpots.js b/src/hooks/useWWFFSpots.js
index bd903c86..c71b6019 100644
--- a/src/hooks/useWWFFSpots.js
+++ b/src/hooks/useWWFFSpots.js
@@ -5,6 +5,8 @@
import { useState, useEffect, useRef } from 'react';
import { useVisibilityRefresh } from './useVisibilityRefresh';
import { apiFetch } from '../utils/apiFetch';
+import { WGS84ToMaidenhead } from '@hamset/maidenhead-locator';
+import { getBandFromFreq } from '../utils';
export const useWWFFSpots = () => {
const [data, setData] = useState([]);
@@ -72,6 +74,7 @@ export const useWWFFSpots = () => {
call: s.activator,
ref: s.reference,
freq: freqMhz ? freqMhz.toString() : s.frequency_khz, // Convert to MHz string
+ band: getBandFromFreq(s.frequency_khz),
mode: s.mode,
name: s.reference_name,
remarks: s.remarks,
@@ -79,6 +82,7 @@ export const useWWFFSpots = () => {
lon,
time: s.spot_time ? s.spot_time_formatted.substr(11, 5) + 'z' : '',
expire: 0,
+ grid: WGS84ToMaidenhead({ lat: lat, lng: lon }),
};
}),
);
diff --git a/src/layouts/ModernLayout.jsx b/src/layouts/ModernLayout.jsx
index abff1fb6..5f387dc2 100644
--- a/src/layouts/ModernLayout.jsx
+++ b/src/layouts/ModernLayout.jsx
@@ -64,9 +64,13 @@ export default function ModernLayout(props) {
propagation,
dxClusterData,
potaSpots,
+ filteredPotaSpots,
wwffSpots,
+ filteredWwffSpots,
sotaSpots,
+ filteredSotaSpots,
wwbotaSpots,
+ filteredWwbotaSpots,
mySpots,
dxpeditions,
contests,
@@ -81,13 +85,25 @@ export default function ModernLayout(props) {
pskFilters,
setShowDXFilters,
setShowPSKFilters,
+ potaFilters,
+ setShowPotaFilters,
+ sotaFilters,
+ setShowSotaFilters,
+ wwffFilters,
+ setShowWwffFilters,
+ wwbotaFilters,
+ setShowWwbotaFilters,
mapLayers,
toggleDXPaths,
toggleDXLabels,
togglePOTA,
+ togglePOTALabels,
toggleWWFF,
+ toggleWWFFLabels,
toggleSOTA,
+ toggleSOTALabels,
toggleWWBOTA,
+ toggleWWBOTALabels,
toggleSatellites,
togglePSKReporter,
toggleWSJTX,
@@ -118,10 +134,10 @@ export default function ModernLayout(props) {
dxLocation={dxLocation}
onDXChange={handleDXChange}
dxLocked={dxLocked}
- potaSpots={potaSpots.data}
- wwffSpots={wwffSpots.data}
- sotaSpots={sotaSpots.data}
- wwbotaSpots={wwbotaSpots.data}
+ potaSpots={filteredPotaSpots ? filteredPotaSpots : potaSpots.data}
+ wwffSpots={filteredWwffSpots ? filteredWwffSpots : wwffSpots.data}
+ sotaSpots={filteredSotaSpots ? filteredSotaSpots : sotaSpots.data}
+ wwbotaSpots={filteredWwbotaSpots ? filteredWwbotaSpots : wwbotaSpots.data}
mySpots={mySpots.data}
dxPaths={dxClusterData.paths}
dxFilters={dxFilters}
@@ -133,8 +149,11 @@ export default function ModernLayout(props) {
showDXLabels={mapLayers.showDXLabels}
onToggleDXLabels={toggleDXLabels}
showPOTA={mapLayers.showPOTA}
+ showPOTALabels={mapLayers.showPOTALabels}
showWWFF={mapLayers.showWWFF}
+ showWWFFLabels={mapLayers.showWWFFLabels}
showSOTA={mapLayers.showSOTA}
+ showSOTALabels={mapLayers.showSOTALabels}
showWWBOTA={mapLayers.showWWBOTA}
showSatellites={mapLayers.showSatellites}
showPSKReporter={mapLayers.showPSKReporter}
@@ -308,28 +327,48 @@ export default function ModernLayout(props) {
potaLastChecked={potaSpots.lastChecked}
showPOTA={mapLayers.showPOTA}
onTogglePOTA={togglePOTA}
+ showPOTALabels={mapLayers.showPOTALabels}
+ togglePOTALabels={togglePOTALabels}
+ onPOTASpotClick={handleParkSpotClick}
+ potaFilters={potaFilters}
+ setShowPotaFilters={setShowPotaFilters}
+ filteredPotaSpots={filteredPotaSpots}
sotaData={sotaSpots.data}
sotaLoading={sotaSpots.loading}
sotaLastUpdated={sotaSpots.lastUpdated}
sotaLastChecked={sotaSpots.lastChecked}
showSOTA={mapLayers.showSOTA}
onToggleSOTA={toggleSOTA}
+ showSOTALabels={mapLayers.showSOTALabels}
+ toggleSOTALabels={toggleSOTALabels}
+ onSOTASpotClick={handleParkSpotClick}
+ sotaFilters={sotaFilters}
+ setShowSotaFilters={setShowSotaFilters}
+ filteredSotaSpots={filteredSotaSpots}
wwffData={wwffSpots.data}
wwffLoading={wwffSpots.loading}
wwffLastUpdated={wwffSpots.lastUpdated}
wwffLastChecked={wwffSpots.lastChecked}
showWWFF={mapLayers.showWWFF}
onToggleWWFF={toggleWWFF}
+ showWWFFLabels={mapLayers.showWWFFLabels}
+ toggleWWFFLabels={toggleWWFFLabels}
+ onWWFFSpotClick={handleParkSpotClick}
+ wwffFilters={wwffFilters}
+ setShowWwffFilters={setShowWwffFilters}
+ filteredWwffSpots={filteredWwffSpots}
wwbotaData={wwbotaSpots.data}
wwbotaLoading={wwbotaSpots.loading}
wwbotaLastUpdated={wwbotaSpots.lastUpdated}
wwbotaConnected={wwbotaSpots.connected}
showWWBOTA={mapLayers.showWWBOTA}
onToggleWWBOTA={toggleWWBOTA}
- onPOTASpotClick={handleParkSpotClick}
- onWWFFSpotClick={handleParkSpotClick}
- onSOTASpotClick={handleParkSpotClick}
+ showWWBOTALabels={mapLayers.showWWBOTALabels}
+ toggleWWBOTALabels={toggleWWBOTALabels}
onWWBOTASpotClick={handleParkSpotClick}
+ wwbotaFilters={wwbotaFilters}
+ setShowWwbotaFilters={setShowWwbotaFilters}
+ filteredWwbotaSpots={filteredWwbotaSpots}
/>
);
diff --git a/src/plugins/layers/useFloods.js b/src/plugins/layers/useFloods.js
index 890254a7..16e3f340 100644
--- a/src/plugins/layers/useFloods.js
+++ b/src/plugins/layers/useFloods.js
@@ -34,7 +34,9 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory
// Fetch both floods and severe storms in parallel
const [floodsRes, stormsRes] = await Promise.all([
fetch(`https://eonet.gsfc.nasa.gov/api/v3/events?category=floods&status=open&limit=${MAX_EVENTS}`),
- fetch(`https://eonet.gsfc.nasa.gov/api/v3/events?category=severeStorms&status=open&limit=${Math.floor(MAX_EVENTS / 2)}`),
+ fetch(
+ `https://eonet.gsfc.nasa.gov/api/v3/events?category=severeStorms&status=open&limit=${Math.floor(MAX_EVENTS / 2)}`,
+ ),
]);
const [floodsData, stormsData] = await Promise.all([floodsRes.json(), stormsRes.json()]);
@@ -177,7 +179,9 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory
: `${Math.floor(ageHours / 24)} days ago`;
const sources = (event.sources || [])
- .map((s) => `${s.id} `)
+ .map(
+ (s) => `${s.id} `,
+ )
.join(' · ');
marker.bindPopup(`
diff --git a/src/plugins/layers/useWildfires.js b/src/plugins/layers/useWildfires.js
index 3a05dfa6..5466d1a2 100644
--- a/src/plugins/layers/useWildfires.js
+++ b/src/plugins/layers/useWildfires.js
@@ -150,7 +150,9 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory
// Source links
const sources = (event.sources || [])
- .map((s) => `${s.id} `)
+ .map(
+ (s) => `${s.id} `,
+ )
.join(' · ');
marker.bindPopup(`
diff --git a/src/utils/config.js b/src/utils/config.js
index a8e6eb29..4bdcd6e1 100644
--- a/src/utils/config.js
+++ b/src/utils/config.js
@@ -188,6 +188,10 @@ const SYNC_KEYS = [
'openhamclock_panelZoom',
'openhamclock_pskActiveTab',
'openhamclock_pskFilters',
+ 'openhamclock_potaFilters',
+ 'openhamclock_sotaFilters',
+ 'openhamclock_wwffFilters',
+ 'openhamclock_wwbotaFilters',
'openhamclock_pskPanelMode',
'openhamclock_bandColors',
'openhamclock_satelliteFilters',
diff --git a/src/utils/profiles.js b/src/utils/profiles.js
index a5071f1c..dbcde821 100644
--- a/src/utils/profiles.js
+++ b/src/utils/profiles.js
@@ -22,6 +22,10 @@ const SNAPSHOT_KEYS = [
'openhamclock_mapSettings',
'openhamclock_pskActiveTab',
'openhamclock_pskFilters',
+ 'openhamclock_potaFilters',
+ 'openhamclock_sotaFilters',
+ 'openhamclock_wwffFilters',
+ 'openhamclock_wwbotaFilters',
'openhamclock_pskPanelMode',
'openhamclock_bandColors',
'openhamclock_satelliteFilters',