@@ -27,6 +27,51 @@ const MESHMAPPER_API_URL = "https://yow.meshmapper.net/wardriving-api.php";
2727const MESHMAPPER_API_KEY = "59C7754DABDF5C11CA5F5D8368F89" ;
2828const MESHMAPPER_DEFAULT_WHO = "GOME-WarDriver" ; // Default identifier
2929
30+ // GPS Enhancement Configuration
31+ const MIN_PING_DISTANCE_M = 25 ; // Minimum distance between pings in meters
32+ const OTTAWA_CENTER_LAT = 45.4215 ; // Ottawa center (Parliament Hill) latitude
33+ const OTTAWA_CENTER_LON = - 75.6972 ; // Ottawa center (Parliament Hill) longitude
34+ const OTTAWA_GEOFENCE_RADIUS_KM = 150 ; // Service area radius in kilometers (covers greater Ottawa region)
35+
36+ // ---- GPS Distance Calculation (Haversine Formula) ----
37+ const EARTH_RADIUS_METERS = 6371000 ; // Earth's mean radius in meters
38+ const DEG_TO_RAD = Math . PI / 180 ; // Conversion factor from degrees to radians
39+
40+ /**
41+ * Calculate the great-circle distance between two GPS coordinates using the Haversine formula.
42+ * @param {number } lat1 - Latitude of first point in degrees
43+ * @param {number } lon1 - Longitude of first point in degrees
44+ * @param {number } lat2 - Latitude of second point in degrees
45+ * @param {number } lon2 - Longitude of second point in degrees
46+ * @returns {number } Distance in meters
47+ */
48+ function calculateDistance ( lat1 , lon1 , lat2 , lon2 ) {
49+ const φ1 = lat1 * DEG_TO_RAD ;
50+ const φ2 = lat2 * DEG_TO_RAD ;
51+ const Δφ = ( lat2 - lat1 ) * DEG_TO_RAD ;
52+ const Δλ = ( lon2 - lon1 ) * DEG_TO_RAD ;
53+
54+ const a = Math . sin ( Δφ / 2 ) * Math . sin ( Δφ / 2 ) +
55+ Math . cos ( φ1 ) * Math . cos ( φ2 ) *
56+ Math . sin ( Δλ / 2 ) * Math . sin ( Δλ / 2 ) ;
57+ const c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ;
58+
59+ return EARTH_RADIUS_METERS * c ; // Distance in meters
60+ }
61+
62+ /**
63+ * Check if current location is within the Ottawa geofence.
64+ * @param {number } lat - Current latitude
65+ * @param {number } lon - Current longitude
66+ * @returns {object } { inBounds: boolean, distanceKm: number }
67+ */
68+ function checkGeofence ( lat , lon ) {
69+ const distanceM = calculateDistance ( lat , lon , OTTAWA_CENTER_LAT , OTTAWA_CENTER_LON ) ;
70+ const distanceKm = distanceM / 1000 ;
71+ const inBounds = distanceKm <= OTTAWA_GEOFENCE_RADIUS_KM ;
72+ return { inBounds, distanceKm } ;
73+ }
74+
3075// ---- DOM refs (from index.html; unchanged except the two new selectors) ----
3176const $ = ( id ) => document . getElementById ( id ) ;
3277const statusEl = $ ( "status" ) ;
@@ -64,7 +109,8 @@ const state = {
64109 autoCountdownTimer : null , // Timer for auto-ping countdown display
65110 nextAutoPingTime : null , // Timestamp when next auto-ping will occur
66111 apiCountdownTimer : null , // Timer for API post countdown display
67- apiPostTime : null // Timestamp when API post will occur
112+ apiPostTime : null , // Timestamp when API post will occur
113+ lastPingLocation : null // { lat, lon, tsMs } - location of last successful ping for distance filtering
68114} ;
69115
70116// ---- UI helpers ----
@@ -555,11 +601,56 @@ async function sendPing(manual = false) {
555601 }
556602 }
557603
604+ // Check geofence: ensure we're within the Ottawa service area
605+ const geofenceCheck = checkGeofence ( lat , lon ) ;
606+ if ( ! geofenceCheck . inBounds ) {
607+ const msg = `Location outside service area (${ geofenceCheck . distanceKm . toFixed ( 1 ) } km from Ottawa)` ;
608+ console . log ( `Geofence check failed: ${ msg } ` ) ;
609+ setStatus ( msg , "text-red-300" ) ;
610+
611+ // In auto mode, schedule next ping to keep checking location
612+ if ( ! manual && state . running ) {
613+ scheduleNextAutoPing ( ) ;
614+ }
615+ return ;
616+ }
617+
618+ // Check distance from last ping: skip if too close (within 25m)
619+ if ( state . lastPingLocation ) {
620+ const distanceFromLastPing = calculateDistance (
621+ lat , lon ,
622+ state . lastPingLocation . lat , state . lastPingLocation . lon
623+ ) ;
624+
625+ console . log ( `Distance from last ping: ${ distanceFromLastPing . toFixed ( 1 ) } m` ) ;
626+
627+ if ( distanceFromLastPing < MIN_PING_DISTANCE_M ) {
628+ const remainingDistance = MIN_PING_DISTANCE_M - distanceFromLastPing ;
629+ const msg = `Ping skipped, too close to last ping (${ distanceFromLastPing . toFixed ( 1 ) } m away). Need ${ remainingDistance . toFixed ( 1 ) } m more to ping` ;
630+ console . log ( msg ) ;
631+ setStatus ( msg , "text-amber-300" ) ;
632+
633+ // In auto mode, schedule next ping to keep checking distance
634+ if ( ! manual && state . running ) {
635+ scheduleNextAutoPing ( ) ;
636+ }
637+ return ;
638+ }
639+ }
640+
558641 const payload = buildPayload ( lat , lon ) ;
559642
560643 const ch = await ensureChannel ( ) ;
561644 await state . connection . sendChannelTextMessage ( ch . channelIdx , payload ) ;
562645
646+ // Store this location as the last successful ping location for distance filtering
647+ state . lastPingLocation = {
648+ lat,
649+ lon,
650+ tsMs : Date . now ( )
651+ } ;
652+ console . log ( `Last ping location updated: ${ lat . toFixed ( 5 ) } , ${ lon . toFixed ( 5 ) } ` ) ;
653+
563654 // Start cooldown period after successful ping
564655 startCooldown ( ) ;
565656
@@ -765,6 +856,7 @@ async function connect() {
765856 state . cooldownEndTime = null ;
766857
767858 state . lastFix = null ;
859+ state . lastPingLocation = null ; // Clear last ping location on disconnect
768860 state . gpsState = "idle" ;
769861 updateGpsUi ( ) ;
770862 } ) ;
0 commit comments