@@ -10,29 +10,22 @@ import {
1010 drawState ,
1111 searchState ,
1212 cacheState ,
13- updateMapState ,
14- updateDrawState ,
15- updateFilterState ,
16- updateSearchState ,
17- updateCacheState ,
18- clearSearchState ,
19- clearDrawState
13+ updateFilterState
2014} from './state.js' ;
2115
2216import {
2317 SEVERITY_COLORS ,
24- CRASH_TYPE_PALETTE ,
25- MARKER_CONFIG
18+ CRASH_TYPE_PALETTE
2619} from './config.js' ;
2720
2821import {
29- convertCoordinates ,
3022 escapeHtml ,
3123 normalizeLGAName ,
3224 getLGAName ,
3325 showLoading ,
3426 hideLoading ,
35- updateLoadingMessage
27+ updateLoadingMessage ,
28+ getSearchRadiusKm
3629} from './utils.js' ;
3730
3831import { updateStatistics } from './analytics.js' ;
@@ -264,6 +257,22 @@ export function initMap() {
264257 }
265258 ` ;
266259 document . head . appendChild ( style ) ;
260+
261+ // Re-run GPS filter when the radius selector changes (only when GPS mode is active).
262+ // Guard against double-binding if init ever runs twice (dataset flag).
263+ const radiusSelect = document . getElementById ( 'searchRadius' ) ;
264+ if ( radiusSelect && radiusSelect . dataset . gpsBound !== '1' ) {
265+ radiusSelect . dataset . gpsBound = '1' ;
266+ radiusSelect . addEventListener ( 'change' , ( ) => {
267+ if ( searchState . gpsLocation ) {
268+ const { lat, lng } = searchState . gpsLocation ;
269+ updateGpsCircle ( lat , lng , false ) ;
270+ if ( typeof window . applyFilters === 'function' ) {
271+ window . applyFilters ( ) ;
272+ }
273+ }
274+ } ) ;
275+ }
267276}
268277
269278// ============================================================================
@@ -842,15 +851,15 @@ export function addChoropleth() {
842851 ` ) ;
843852
844853 // Hover effects
845- layer . on ( 'mouseover' , function ( e ) {
854+ layer . on ( 'mouseover' , function ( ) {
846855 this . setStyle ( {
847856 weight : 3 ,
848857 opacity : 1 ,
849858 fillOpacity : 0.8
850859 } ) ;
851860 } ) ;
852861
853- layer . on ( 'mouseout' , function ( e ) {
862+ layer . on ( 'mouseout' , function ( ) {
854863 mapState . choroplethLayer . resetStyle ( this ) ;
855864 } ) ;
856865 }
@@ -964,15 +973,15 @@ export function addChoroplethBySuburb() {
964973 ` ) ;
965974
966975 // Hover effects
967- layer . on ( 'mouseover' , function ( e ) {
976+ layer . on ( 'mouseover' , function ( ) {
968977 this . setStyle ( {
969978 weight : 2 ,
970979 opacity : 1 ,
971980 fillOpacity : 0.8
972981 } ) ;
973982 } ) ;
974983
975- layer . on ( 'mouseout' , function ( e ) {
984+ layer . on ( 'mouseout' , function ( ) {
976985 mapState . choroplethLayer . resetStyle ( this ) ;
977986 } ) ;
978987 }
@@ -1409,15 +1418,23 @@ export function clearLocationSearch() {
14091418 searchState . searchCircle = null ;
14101419 }
14111420
1421+ // Clear GPS state so the radius listener stops re-filtering
1422+ searchState . gpsLocation = null ;
1423+ document . getElementById ( 'gpsBtn' ) ?. classList . remove ( 'gps-btn-active' ) ;
1424+
14121425 // Clear input and results
14131426 const input = document . getElementById ( 'locationSearch' ) ;
14141427 const results = document . getElementById ( 'searchResults' ) ;
14151428 if ( input ) input . value = '' ;
1416- if ( results ) results . style . display = 'none' ;
1429+ if ( results ) {
1430+ results . textContent = '' ;
1431+ delete results . dataset . source ;
1432+ results . classList . add ( 'hidden' ) ;
1433+ }
14171434
1418- // Mark filters as changed to trigger proper state tracking
1419- if ( typeof window . markFiltersChanged === 'function' ) {
1420- window . markFiltersChanged ( ) ;
1435+ // Re-run filters so markers reflect the remaining active filters
1436+ if ( typeof window . applyFilters === 'function' ) {
1437+ window . applyFilters ( ) ;
14211438 }
14221439}
14231440
@@ -1460,9 +1477,13 @@ export async function searchByLocation() {
14601477 const lng = parseFloat ( location . lon ) ;
14611478
14621479 // Get search radius
1463- const radiusKm = parseFloat ( document . getElementById ( 'searchRadius' ) ?. value || 5 ) ;
1480+ const radiusKm = getSearchRadiusKm ( ) ;
14641481 const radiusMeters = radiusKm * 1000 ;
14651482
1483+ // Entering text search clears GPS mode
1484+ searchState . gpsLocation = null ;
1485+ document . getElementById ( 'gpsBtn' ) ?. classList . remove ( 'gps-btn-active' ) ;
1486+
14661487 // Clear previous search marker/circle
14671488 if ( searchState . searchMarker ) {
14681489 mapState . map . removeLayer ( searchState . searchMarker ) ;
@@ -1493,14 +1514,20 @@ export async function searchByLocation() {
14931514 // Zoom to search area
14941515 mapState . map . fitBounds ( searchState . searchCircle . getBounds ( ) , { padding : [ 50 , 50 ] } ) ;
14951516
1496- // Filter crashes within radius
1517+ // Filter crashes within radius using pre-cached coords.
1518+ // Bounding-box prefilter avoids an expensive distance call for far-field points.
1519+ const latDelta = radiusMeters / 111320 ;
1520+ const lngDelta = radiusMeters / ( 111320 * Math . cos ( lat * Math . PI / 180 ) ) ;
1521+ const latMin = lat - latDelta ;
1522+ const latMax = lat + latDelta ;
1523+ const lngMin = lng - lngDelta ;
1524+ const lngMax = lng + lngDelta ;
14971525 const nearbyCrashes = dataState . crashData . filter ( crash => {
1498- const coords = convertCoordinates ( crash . ACCLOC_X , crash . ACCLOC_Y ) ;
1526+ const coords = crash . _coords ;
14991527 if ( ! coords ) return false ;
1500-
1501- const [ crashLat , crashLng ] = coords ;
1502- const distance = mapState . map . distance ( [ lat , lng ] , [ crashLat , crashLng ] ) ;
1503- return distance <= radiusMeters ;
1528+ const [ cLat , cLng ] = coords ;
1529+ if ( cLat < latMin || cLat > latMax || cLng < lngMin || cLng > lngMax ) return false ;
1530+ return mapState . map . distance ( [ lat , lng ] , coords ) <= radiusMeters ;
15041531 } ) ;
15051532
15061533 hideLoading ( ) ;
@@ -1510,7 +1537,8 @@ export async function searchByLocation() {
15101537 const resultsEl = document . getElementById ( 'searchResults' ) ;
15111538 if ( resultsEl ) {
15121539 resultsEl . textContent = resultMsg ;
1513- resultsEl . style . display = 'block' ;
1540+ resultsEl . dataset . source = 'search' ;
1541+ resultsEl . classList . remove ( 'hidden' ) ;
15141542 }
15151543
15161544 // Apply filter to show only nearby crashes
@@ -1525,6 +1553,126 @@ export async function searchByLocation() {
15251553 }
15261554}
15271555
1556+ /**
1557+ * Draw/update the GPS radius circle. Visual only — filtering is handled by applyFilters().
1558+ */
1559+ function updateGpsCircle ( lat , lng , fitBounds ) {
1560+ const radiusMeters = getSearchRadiusKm ( ) * 1000 ;
1561+
1562+ if ( searchState . searchCircle ) {
1563+ mapState . map . removeLayer ( searchState . searchCircle ) ;
1564+ }
1565+ searchState . searchCircle = L . circle ( [ lat , lng ] , {
1566+ radius : radiusMeters ,
1567+ color : '#22c55e' ,
1568+ fillColor : '#22c55e' ,
1569+ fillOpacity : 0.08 ,
1570+ weight : 2
1571+ } ) . addTo ( mapState . map ) ;
1572+
1573+ if ( fitBounds ) {
1574+ mapState . map . fitBounds ( searchState . searchCircle . getBounds ( ) , { padding : [ 50 , 50 ] } ) ;
1575+ }
1576+ }
1577+
1578+ /**
1579+ * Use the device GPS to centre the map on the user and filter nearby crashes
1580+ */
1581+ export function useMyLocation ( ) {
1582+ // Modern browsers require a secure context for geolocation. Without this
1583+ // check the call can fail silently or surface a generic permission error.
1584+ if ( ! window . isSecureContext ) {
1585+ showNotification ( 'GPS requires a secure (HTTPS) connection.' , 'warning' ) ;
1586+ return ;
1587+ }
1588+ if ( ! navigator . geolocation ) {
1589+ showNotification ( 'Geolocation is not supported by your browser.' , 'warning' ) ;
1590+ return ;
1591+ }
1592+
1593+ const btn = document . getElementById ( 'gpsBtn' ) ;
1594+ // Re-entrancy guard: a second click while a request is in flight would
1595+ // race two callbacks that each overwrite the marker/circle and re-trigger
1596+ // applyFilters with divergent positions.
1597+ if ( btn ?. dataset . pending === '1' ) return ;
1598+ if ( btn ) {
1599+ btn . dataset . pending = '1' ;
1600+ btn . setAttribute ( 'disabled' , '' ) ;
1601+ }
1602+
1603+ showLoading ( 'Getting your location...' ) ;
1604+
1605+ const finish = ( ) => {
1606+ hideLoading ( ) ;
1607+ if ( btn ) {
1608+ btn . dataset . pending = '0' ;
1609+ btn . removeAttribute ( 'disabled' ) ;
1610+ }
1611+ } ;
1612+
1613+ navigator . geolocation . getCurrentPosition (
1614+ ( position ) => {
1615+ const lat = position . coords . latitude ;
1616+ const lng = position . coords . longitude ;
1617+ const accuracy = position . coords . accuracy ;
1618+
1619+ searchState . gpsLocation = { lat, lng } ;
1620+
1621+ if ( searchState . searchMarker ) {
1622+ mapState . map . removeLayer ( searchState . searchMarker ) ;
1623+ }
1624+ if ( searchState . searchCircle ) {
1625+ mapState . map . removeLayer ( searchState . searchCircle ) ;
1626+ }
1627+
1628+ const radiusMeters = getSearchRadiusKm ( ) * 1000 ;
1629+ const accuracyText = Number . isFinite ( accuracy )
1630+ ? `<br><small>Accuracy: ±${ Math . round ( accuracy ) } m</small>`
1631+ : '' ;
1632+ const popupHtml = `<strong>📍 Your Location</strong>${ accuracyText } ` ;
1633+
1634+ searchState . searchMarker = L . marker ( [ lat , lng ] , {
1635+ icon : L . divIcon ( {
1636+ className : '' ,
1637+ html : '<div class="gps-marker-dot"><div class="gps-marker-pulse"></div></div>' ,
1638+ iconSize : [ 20 , 20 ] ,
1639+ iconAnchor : [ 10 , 10 ]
1640+ } )
1641+ } ) . addTo ( mapState . map ) . bindPopup ( popupHtml ) . openPopup ( ) ;
1642+
1643+ updateGpsCircle ( lat , lng , true ) ;
1644+ btn ?. classList . add ( 'gps-btn-active' ) ;
1645+
1646+ // Warn when the reported accuracy is larger than the active radius —
1647+ // the "nearby crashes" result is unlikely to be meaningful in that case.
1648+ if ( Number . isFinite ( accuracy ) && accuracy > radiusMeters ) {
1649+ showNotification (
1650+ `Your location accuracy (±${ Math . round ( accuracy ) } m) is larger than the ${ radiusMeters / 1000 } km search radius. Results may be inaccurate.` ,
1651+ 'warning'
1652+ ) ;
1653+ }
1654+
1655+ finish ( ) ;
1656+
1657+ // Delegate filtering to applyFilters so GPS stacks on top of active panel filters
1658+ if ( typeof window . applyFilters === 'function' ) {
1659+ window . applyFilters ( ) ;
1660+ }
1661+ } ,
1662+ ( error ) => {
1663+ finish ( ) ;
1664+ console . error ( 'Geolocation error:' , error ) ;
1665+ const messages = {
1666+ 1 : 'Location access denied. Please allow location access in your browser settings.' ,
1667+ 2 : 'Unable to determine your location. Please try again.' ,
1668+ 3 : 'Location request timed out. Please try again.'
1669+ } ;
1670+ showNotification ( messages [ error . code ] || 'Could not get your location.' , 'warning' ) ;
1671+ } ,
1672+ { enableHighAccuracy : true , timeout : 10000 , maximumAge : 0 }
1673+ ) ;
1674+ }
1675+
15281676/**
15291677 * Toggle location search collapse/expand
15301678 */
0 commit comments