@ -1,7 +1,7 @@
import { useState , useEffect , useRef } from 'react' ;
import { useState , useEffect , useRef } from 'react' ;
/ * *
/ * *
* WSPR Propagation Heatmap Plugin v1 . 4. 2
* WSPR Propagation Heatmap Plugin v1 . 4. 3
*
*
* Advanced Features :
* Advanced Features :
* - Great circle curved path lines between transmitters and receivers
* - Great circle curved path lines between transmitters and receivers
@ -19,6 +19,7 @@ import { useState, useEffect, useRef } from 'react';
* - Proper cleanup on disable ( v1 . 4.1 )
* - Proper cleanup on disable ( v1 . 4.1 )
* - Fixed duplicate control creation ( v1 . 4.2 )
* - Fixed duplicate control creation ( v1 . 4.2 )
* - Performance optimizations ( v1 . 4.2 )
* - Performance optimizations ( v1 . 4.2 )
* - Separate opacity controls for paths and heatmap ( v1 . 4.3 )
* - Statistics display ( total stations , spots )
* - Statistics display ( total stations , spots )
* - Signal strength legend
* - Signal strength legend
*
*
@ -34,7 +35,7 @@ export const metadata = {
category : 'propagation' ,
category : 'propagation' ,
defaultEnabled : false ,
defaultEnabled : false ,
defaultOpacity : 0.7 ,
defaultOpacity : 0.7 ,
version : '1.4. 2 '
version : '1.4. 3 '
} ;
} ;
// Convert grid square to lat/lon
// Convert grid square to lat/lon
@ -264,6 +265,10 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
const [ showAnimation , setShowAnimation ] = useState ( true ) ;
const [ showAnimation , setShowAnimation ] = useState ( true ) ;
const [ showHeatmap , setShowHeatmap ] = useState ( false ) ;
const [ showHeatmap , setShowHeatmap ] = useState ( false ) ;
// v1.4.3 - Separate opacity controls
const [ pathOpacity , setPathOpacity ] = useState ( 0.7 ) ;
const [ heatmapOpacity , setHeatmapOpacity ] = useState ( 0.6 ) ;
// UI Controls (refs to avoid recreation)
// UI Controls (refs to avoid recreation)
const legendControlRef = useRef ( null ) ;
const legendControlRef = useRef ( null ) ;
const statsControlRef = useRef ( null ) ;
const statsControlRef = useRef ( null ) ;
@ -358,7 +363,19 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
style = "width: 100%;" / >
style = "width: 100%;" / >
< / d i v >
< / d i v >
< div style = "margin-bottom: 8px; padding-top: 8px; border-top: 1px solid #555;" >
< label style = "display: block; margin-bottom: 3px;" > Path Opacity : < span id = "path-opacity-value" > 70 < / s p a n > % < / l a b e l >
< input type = "range" id = "wspr-path-opacity" min = "10" max = "100" value = "70" step = "5"
style = "width: 100%;" / >
< / d i v >
< div style = "margin-bottom: 8px;" >
< div style = "margin-bottom: 8px;" >
< label style = "display: block; margin-bottom: 3px;" > Heatmap Opacity : < span id = "heatmap-opacity-value" > 60 < / s p a n > % < / l a b e l >
< input type = "range" id = "wspr-heatmap-opacity" min = "10" max = "100" value = "60" step = "5"
style = "width: 100%;" / >
< / d i v >
< div style = "margin-bottom: 8px; padding-top: 8px; border-top: 1px solid #555;" >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< label style = "display: flex; align-items: center; cursor: pointer;" >
< input type = "checkbox" id = "wspr-animation" checked style = "margin-right: 5px;" / >
< input type = "checkbox" id = "wspr-animation" checked style = "margin-right: 5px;" / >
< span > Animate Paths < / s p a n >
< span > Animate Paths < / s p a n >
@ -400,6 +417,10 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
const timeSelect = document . getElementById ( 'wspr-time-filter' ) ;
const timeSelect = document . getElementById ( 'wspr-time-filter' ) ;
const snrSlider = document . getElementById ( 'wspr-snr-filter' ) ;
const snrSlider = document . getElementById ( 'wspr-snr-filter' ) ;
const snrValue = document . getElementById ( 'snr-value' ) ;
const snrValue = document . getElementById ( 'snr-value' ) ;
const pathOpacitySlider = document . getElementById ( 'wspr-path-opacity' ) ;
const pathOpacityValue = document . getElementById ( 'path-opacity-value' ) ;
const heatmapOpacitySlider = document . getElementById ( 'wspr-heatmap-opacity' ) ;
const heatmapOpacityValue = document . getElementById ( 'heatmap-opacity-value' ) ;
const animCheck = document . getElementById ( 'wspr-animation' ) ;
const animCheck = document . getElementById ( 'wspr-animation' ) ;
const heatCheck = document . getElementById ( 'wspr-heatmap' ) ;
const heatCheck = document . getElementById ( 'wspr-heatmap' ) ;
@ -411,6 +432,20 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
if ( snrValue ) snrValue . textContent = e . target . value ;
if ( snrValue ) snrValue . textContent = e . target . value ;
} ) ;
} ) ;
}
}
if ( pathOpacitySlider ) {
pathOpacitySlider . addEventListener ( 'input' , ( e ) => {
const value = parseInt ( e . target . value ) / 100 ;
setPathOpacity ( value ) ;
if ( pathOpacityValue ) pathOpacityValue . textContent = e . target . value ;
} ) ;
}
if ( heatmapOpacitySlider ) {
heatmapOpacitySlider . addEventListener ( 'input' , ( e ) => {
const value = parseInt ( e . target . value ) / 100 ;
setHeatmapOpacity ( value ) ;
if ( heatmapOpacityValue ) heatmapOpacityValue . textContent = e . target . value ;
} ) ;
}
if ( animCheck ) animCheck . addEventListener ( 'change' , ( e ) => setShowAnimation ( e . target . checked ) ) ;
if ( animCheck ) animCheck . addEventListener ( 'change' , ( e ) => setShowAnimation ( e . target . checked ) ) ;
if ( heatCheck ) heatCheck . addEventListener ( 'change' , ( e ) => {
if ( heatCheck ) heatCheck . addEventListener ( 'change' , ( e ) => {
console . log ( '[WSPR] Heatmap toggle:' , e . target . checked ) ;
console . log ( '[WSPR] Heatmap toggle:' , e . target . checked ) ;
@ -598,7 +633,7 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
const path = L . polyline ( pathCoords , {
const path = L . polyline ( pathCoords , {
color : isBestPath ? '#00ffff' : getSNRColor ( spot . snr ) ,
color : isBestPath ? '#00ffff' : getSNRColor ( spot . snr ) ,
weight : isBestPath ? 4 : getLineWeight ( spot . snr ) ,
weight : isBestPath ? 4 : getLineWeight ( spot . snr ) ,
opacity : o pacity * ( isBestPath ? 0.9 : 0.6 ) ,
opacity : pathO pacity * ( isBestPath ? 0.9 : 0.6 ) ,
smoothFactor : 1 ,
smoothFactor : 1 ,
className : showAnimation ? 'wspr-animated-path' : ''
className : showAnimation ? 'wspr-animated-path' : ''
} ) ;
} ) ;
@ -633,8 +668,8 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
fillColor : '#ff6600' ,
fillColor : '#ff6600' ,
color : '#ffffff' ,
color : '#ffffff' ,
weight : 1 ,
weight : 1 ,
fillOpacity : o pacity * 0.8 ,
fillOpacity : pathO pacity * 0.8 ,
opacity : o pacity
opacity : pathO pacity
} ) ;
} ) ;
txMarker . bindTooltip ( ` TX: ${ spot . sender } ` , { permanent : false , direction : 'top' } ) ;
txMarker . bindTooltip ( ` TX: ${ spot . sender } ` , { permanent : false , direction : 'top' } ) ;
txMarker . addTo ( map ) ;
txMarker . addTo ( map ) ;
@ -649,8 +684,8 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
fillColor : '#0088ff' ,
fillColor : '#0088ff' ,
color : '#ffffff' ,
color : '#ffffff' ,
weight : 1 ,
weight : 1 ,
fillOpacity : o pacity * 0.8 ,
fillOpacity : pathO pacity * 0.8 ,
opacity : o pacity
opacity : pathO pacity
} ) ;
} ) ;
rxMarker . bindTooltip ( ` RX: ${ spot . receiver } ` , { permanent : false , direction : 'top' } ) ;
rxMarker . bindTooltip ( ` RX: ${ spot . receiver } ` , { permanent : false , direction : 'top' } ) ;
rxMarker . addTo ( map ) ;
rxMarker . addTo ( map ) ;
@ -730,7 +765,7 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
try { map . removeLayer ( layer ) ; } catch ( e ) { }
try { map . removeLayer ( layer ) ; } catch ( e ) { }
} ) ;
} ) ;
} ;
} ;
} , [ enabled , wsprData , map , snrThreshold, showAnimation , timeWindow ] ) ;
} , [ enabled , wsprData , map , pathOpacity, snrThreshold, showAnimation , timeWindow ] ) ;
// Render heatmap overlay (v1.4.0)
// Render heatmap overlay (v1.4.0)
useEffect ( ( ) => {
useEffect ( ( ) => {
@ -801,7 +836,7 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
const circle = L . circle ( [ point . lat , point . lon ] , {
const circle = L . circle ( [ point . lat , point . lon ] , {
radius : radius * 50000 , // Convert to meters for Leaflet
radius : radius * 50000 , // Convert to meters for Leaflet
fillColor : color ,
fillColor : color ,
fillOpacity : fillOpacity * o pacity,
fillOpacity : fillOpacity * heatmapO pacity,
color : color ,
color : color ,
weight : 0 ,
weight : 0 ,
opacity : 0
opacity : 0
@ -833,7 +868,7 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
} catch ( e ) { }
} catch ( e ) { }
} ) ;
} ) ;
} ;
} ;
} , [ enabled , showHeatmap , wsprData , map , o pacity, snrThreshold , heatmapLayer ] ) ;
} , [ enabled , showHeatmap , wsprData , map , heatmapO pacity, snrThreshold , heatmapLayer ] ) ;
// Cleanup controls on disable - FIX: properly remove all controls and layers
// Cleanup controls on disable - FIX: properly remove all controls and layers
useEffect ( ( ) => {
useEffect ( ( ) => {
@ -911,28 +946,11 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null }) {
}
}
} , [ enabled , map , heatmapLayer , pathLayers , markerLayers ] ) ;
} , [ enabled , map , heatmapLayer , pathLayers , markerLayers ] ) ;
// Update opacity
useEffect ( ( ) => {
pathLayers . forEach ( layer => {
if ( layer . setStyle ) {
layer . setStyle ( { opacity : opacity * 0.6 } ) ;
}
} ) ;
markerLayers . forEach ( layer => {
if ( layer . setStyle ) {
layer . setStyle ( {
fillOpacity : opacity * 0.8 ,
opacity : opacity
} ) ;
}
} ) ;
} , [ opacity , pathLayers , markerLayers ] ) ;
return {
return {
paths : pathLayers ,
paths : pathLayers ,
markers : markerLayers ,
markers : markerLayers ,
spotCount : wsprData . length ,
spotCount : wsprData . length ,
filteredCount : wsprData . filter ( s => ( s . snr || - 30 ) >= snrThreshold ) . length ,
filteredCount : wsprData . filter ( s => ( s . snr || - 30 ) >= snrThreshold ) . length ,
filters : { bandFilter , timeWindow , snrThreshold , showAnimation , showHeatmap }
filters : { bandFilter , timeWindow , snrThreshold , showAnimation , showHeatmap , pathOpacity , heatmapOpacity }
} ;
} ;
}
}