@@ -185,7 +185,98 @@ export default function MapComponent() {
185185
186186 // Update the addMarkers function
187187 function addMarkers ( items ) {
188- items . forEach ( ( item , i ) => {
188+ // First, separate user points from API points
189+ const apiPoints = [ ] ;
190+ const userPoints = [ ] ;
191+
192+ items . forEach ( item => {
193+ if ( item . isUserPoint || ( ! item . siteName && ! item . Label ) ) {
194+ userPoints . push ( item ) ;
195+ } else {
196+ apiPoints . push ( item ) ;
197+ }
198+ } ) ;
199+
200+ // Helper function to calculate distance between two points in kilometers (Haversine formula)
201+ function calculateDistance ( lat1 , lon1 , lat2 , lon2 ) {
202+ const R = 6371 ; // Radius of the earth in km
203+ const dLat = ( lat2 - lat1 ) * Math . PI / 180 ;
204+ const dLon = ( lon2 - lon1 ) * Math . PI / 180 ;
205+ const a =
206+ Math . sin ( dLat / 2 ) * Math . sin ( dLat / 2 ) +
207+ Math . cos ( lat1 * Math . PI / 180 ) * Math . cos ( lat2 * Math . PI / 180 ) *
208+ Math . sin ( dLon / 2 ) * Math . sin ( dLon / 2 ) ;
209+ const c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ;
210+ return R * c ; // Distance in km
211+ }
212+
213+ // Group user points that are within 1km of each other
214+ const groupedUserPoints = [ ] ;
215+
216+ userPoints . forEach ( point => {
217+ const lon = point . lng || point . lon || point . Longitude ;
218+ const lat = point . lat || point . Latitude ;
219+ const timestamp = new Date ( point . timestamp || point . createdAt || point . created_at ) . getTime ( ) ;
220+
221+ // Find if this point belongs to any existing group
222+ let foundGroup = false ;
223+
224+ for ( const group of groupedUserPoints ) {
225+ const distance = calculateDistance ( lat , lon , group . lat , group . lon ) ;
226+
227+ if ( distance <= 1 ) { // Within 1km
228+ foundGroup = true ;
229+ // Add this point to the group's historical points collection
230+ if ( ! group . historicalPoints ) {
231+ group . historicalPoints = [ ] ;
232+ }
233+
234+ // Add this point to the historical collection
235+ group . historicalPoints . push ( {
236+ temp : point . temp || point . Result ,
237+ timestamp : point . timestamp || point . createdAt || point . created_at
238+ } ) ;
239+
240+ // Update the main display with the most recent point
241+ const groupTimestamp = new Date ( group . timestamp || group . createdAt || group . created_at ) . getTime ( ) ;
242+
243+ if ( ! isNaN ( timestamp ) && ! isNaN ( groupTimestamp ) && timestamp > groupTimestamp ) {
244+ // Update the group with this point's data but keep the group's position
245+ group . temp = point . temp || point . Result ;
246+ group . timestamp = point . timestamp || point . createdAt || point . created_at ;
247+ }
248+
249+ // Increase the count regardless
250+ group . pointCount = ( group . pointCount || 1 ) + 1 ;
251+ break ;
252+ }
253+ }
254+
255+ // If no matching group was found, create a new one
256+ if ( ! foundGroup ) {
257+ groupedUserPoints . push ( {
258+ ...point ,
259+ pointCount : 1 ,
260+ historicalPoints : [ {
261+ temp : point . temp || point . Result ,
262+ timestamp : point . timestamp || point . createdAt || point . created_at
263+ } ]
264+ } ) ;
265+ }
266+ } ) ;
267+
268+ // Add API points (these don't get grouped)
269+ apiPoints . forEach ( ( item , i ) => {
270+ addMarkerForItem ( item , i ) ;
271+ } ) ;
272+
273+ // Add the grouped user points
274+ groupedUserPoints . forEach ( ( item , i ) => {
275+ addMarkerForItem ( item , i , true ) ;
276+ } ) ;
277+
278+ // Function to add a marker for a single item
279+ function addMarkerForItem ( item , i , isGrouped = false ) {
189280 const lon = item . lng || item . lon || item . Longitude ;
190281 const lat = item . lat || item . Latitude ;
191282 const t = item . temp || item . Result ;
@@ -210,23 +301,34 @@ export default function MapComponent() {
210301 const staleFilter = isStale
211302 ? 'backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);'
212303 : '' ;
304+
305+ // If this is a grouped user point, modify the marker appearance
306+ const groupLabel = isGrouped && item . pointCount > 1
307+ ? `<span class="group-count">${ item . pointCount } </span>`
308+ : '' ;
309+
310+ const groupStyle = isGrouped && item . pointCount > 1
311+ ? 'border: 2px solid #fff; transform: scale(1.1);'
312+ : '' ;
213313
214314 const icon = L . divIcon ( {
215315 className : 'custom-temp-marker' ,
216316 html : `
217317 <div
218- class="temp-label accessible-marker${ isStale ? ' stale' : '' } "
318+ class="temp-label accessible-marker${ isStale ? ' stale' : '' } ${ isGrouped && item . pointCount > 1 ? ' grouped' : '' } "
219319 style="
220320 background-color: ${ bgColor } ;
221321 ${ outlineStyle }
222322 ${ staleFilter }
323+ ${ groupStyle }
223324 "
224325 role="button"
225326 tabindex="0"
226- aria-label="Water temperature ${ formattedTemp } at ${ name } . ${ isStale ? 'Data older than 2 days.' : `Category: ${ tempCategory } . Press Enter or Space to view details.` } "
327+ aria-label="Water temperature ${ formattedTemp } at ${ name } . ${ isGrouped && item . pointCount > 1 ? `Group of ${ item . pointCount } user points. ` : '' } ${ isStale ? 'Data older than 2 days.' : `Category: ${ tempCategory } . Press Enter or Space to view details.` } "
227328 data-temp-category="${ tempCategory . toLowerCase ( ) . replace ( / / g, '-' ) } "
228329 >
229330 <span class="temp-value" style="${ valueColor } ">${ formattedTemp } </span>
331+ ${ groupLabel }
230332 </div>
231333 ` ,
232334 iconSize : [ 50 , 40 ] ,
@@ -235,6 +337,12 @@ export default function MapComponent() {
235337
236338 const marker = L . marker ( [ lat , lon ] , { icon } ) . addTo ( mapInstanceRef . current ) ;
237339
340+ // Set group information for popup display
341+ if ( isGrouped && item . pointCount > 1 ) {
342+ marker . isGrouped = true ;
343+ marker . pointCount = item . pointCount ;
344+ }
345+
238346 // bump this marker to the top on hover
239347 marker . on ( 'mouseover' , ( ) => {
240348 marker . setZIndexOffset ( 1000 ) ;
@@ -266,36 +374,67 @@ export default function MapComponent() {
266374 marker . on ( 'click' , async ( ) => {
267375 // add prefix for grey (stale) points
268376 const labelPrefix = isStale ? '<strong>OLD:</strong> ' : '' ;
377+
378+ // add prefix for grouped points
379+ const groupPrefix = isGrouped && item . pointCount > 1
380+ ? `<strong>GROUP:</strong> ${ item . pointCount } points within 1km • `
381+ : '' ;
269382
270- const historicalData = await fetchHistoricalData ( name ) ;
383+ let historicalData = [ ] ;
384+
385+ // For grouped user points, use the collected historical points
386+ if ( isGrouped && item . historicalPoints && item . historicalPoints . length > 0 ) {
387+ historicalData = item . historicalPoints ;
388+ } else {
389+ // For non-grouped points, fetch data from API as usual
390+ historicalData = await fetchHistoricalData ( name ) ;
391+ }
392+
271393 const popupOffset = [ 17 , - 32 ] ;
272394
273395 // no historical data
274396 if ( historicalData . length === 0 ) {
397+ // Ensure popup is rebinding so it can reopen
398+ marker . closePopup ( ) ;
399+ marker . unbindPopup ( ) ;
275400 marker . bindPopup ( `
276401 <div role="dialog" aria-labelledby="popup-title-${ i } ">
277- <h3 id="popup-title-${ i } ">${ labelPrefix } ${ name } </h3>
402+ <h3 id="popup-title-${ i } ">${ labelPrefix } ${ groupPrefix } ${ name } </h3>
278403 <p>No historical data available</p>
279- <p>Current temperature: ${ formattedTemp } (${ tempCategory } )</p>
404+ ${
405+ isStale
406+ ? `<p>Last updated at: ${ new Date ( rawTime ) . toLocaleString ( ) } </p>`
407+ : `<p>Current temperature: ${ formattedTemp } (${ tempCategory } )</p>`
408+ }
409+ ${ isGrouped && item . pointCount > 1 ? '<p>This is a group of multiple user points within 1km radius. The most recent temperature is shown.</p>' : '' }
280410 </div>
281411 ` , {
282412 offset : popupOffset ,
283413 className : 'custom-popup'
284- } ) . openPopup ( ) ;
414+ } ) ;
415+ marker . openPopup ( ) ;
285416 return ;
286417 }
287418
288419 if ( historicalData . length < 2 ) {
289- marker . bindPopup (
290- `<strong>${ name } </strong><br/>Not enough data to generate a graph` ,
291- {
292- offset : popupOffset ,
293- className : 'custom-popup'
294- }
295- ) . openPopup ( ) ;
420+ // Rebind popup to guarantee it opens on every click
421+ marker . closePopup ( ) ;
422+ marker . unbindPopup ( ) ;
423+ marker . bindPopup ( `
424+ <div role="dialog" aria-labelledby="popup-title-${ i } ">
425+ <h3 id="popup-title-${ i } ">${ labelPrefix } ${ groupPrefix } ${ name } </h3>
426+ <p>Not enough data to generate a graph.</p>
427+ <p>Last updated at: ${ new Date ( rawTime ) . toLocaleString ( ) } </p>
428+ </div>
429+ ` , {
430+ offset : popupOffset ,
431+ className : 'custom-popup'
432+ } ) ;
433+ marker . openPopup ( ) ;
296434 return ;
297435 }
298436
437+ // Sort the historical data by timestamp
299438 const sortedData = historicalData . sort ( ( a , b ) => new Date ( a . timestamp ) - new Date ( b . timestamp ) ) ;
300439
301440 // Declare currentUnit BEFORE using it
@@ -389,7 +528,9 @@ export default function MapComponent() {
389528 plugins : {
390529 title : {
391530 display : true ,
392- text : `Historical Data for ${ name } ` ,
531+ text : isGrouped && item . pointCount > 1
532+ ? `Historical Data for Group (${ item . pointCount } points within 1km)`
533+ : `Historical Data for ${ name } ` ,
393534 font : {
394535 size : isSmallMobile ? 14 : ( isMobile ? 16 : 18 ) ,
395536 weight : 'bold'
@@ -417,10 +558,19 @@ export default function MapComponent() {
417558 } ,
418559 afterBody : function ( context ) {
419560 const dataIndex = context [ 0 ] . dataIndex ;
561+ let info = [ ] ;
562+
563+ // Add gap information
420564 if ( dataIndex > 0 && timeDifferences [ dataIndex - 1 ] ) {
421- return [ `Gap from previous: ${ timeDifferences [ dataIndex - 1 ] } ` ] ;
565+ info . push ( `Gap from previous: ${ timeDifferences [ dataIndex - 1 ] } ` ) ;
566+ }
567+
568+ // For grouped points, add additional information
569+ if ( isGrouped && item . pointCount > 1 ) {
570+ info . push ( `From group of ${ item . pointCount } user points within 1km` ) ;
422571 }
423- return [ ] ;
572+
573+ return info ;
424574 }
425575 }
426576 }
@@ -575,7 +725,7 @@ export default function MapComponent() {
575725 } ) ;
576726
577727 markersRef . current . push ( { marker, tempC : t , name, lat, lon } ) ;
578- } ) ;
728+ }
579729 }
580730
581731 // Function to add water temperature markers
0 commit comments