diff --git a/frontend/src/components/MapComponent.jsx b/frontend/src/components/MapComponent.jsx index fad9cc3..4e38383 100644 --- a/frontend/src/components/MapComponent.jsx +++ b/frontend/src/components/MapComponent.jsx @@ -185,7 +185,98 @@ export default function MapComponent() { // Update the addMarkers function function addMarkers(items) { - items.forEach((item, i) => { + // First, separate user points from API points + const apiPoints = []; + const userPoints = []; + + items.forEach(item => { + if (item.isUserPoint || (!item.siteName && !item.Label)) { + userPoints.push(item); + } else { + apiPoints.push(item); + } + }); + + // Helper function to calculate distance between two points in kilometers (Haversine formula) + function calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371; // Radius of the earth in km + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = + Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; // Distance in km + } + + // Group user points that are within 1km of each other + const groupedUserPoints = []; + + userPoints.forEach(point => { + const lon = point.lng || point.lon || point.Longitude; + const lat = point.lat || point.Latitude; + const timestamp = new Date(point.timestamp || point.createdAt || point.created_at).getTime(); + + // Find if this point belongs to any existing group + let foundGroup = false; + + for (const group of groupedUserPoints) { + const distance = calculateDistance(lat, lon, group.lat, group.lon); + + if (distance <= 1) { // Within 1km + foundGroup = true; + // Add this point to the group's historical points collection + if (!group.historicalPoints) { + group.historicalPoints = []; + } + + // Add this point to the historical collection + group.historicalPoints.push({ + temp: point.temp || point.Result, + timestamp: point.timestamp || point.createdAt || point.created_at + }); + + // Update the main display with the most recent point + const groupTimestamp = new Date(group.timestamp || group.createdAt || group.created_at).getTime(); + + if (!isNaN(timestamp) && !isNaN(groupTimestamp) && timestamp > groupTimestamp) { + // Update the group with this point's data but keep the group's position + group.temp = point.temp || point.Result; + group.timestamp = point.timestamp || point.createdAt || point.created_at; + } + + // Increase the count regardless + group.pointCount = (group.pointCount || 1) + 1; + break; + } + } + + // If no matching group was found, create a new one + if (!foundGroup) { + groupedUserPoints.push({ + ...point, + pointCount: 1, + historicalPoints: [{ + temp: point.temp || point.Result, + timestamp: point.timestamp || point.createdAt || point.created_at + }] + }); + } + }); + + // Add API points (these don't get grouped) + apiPoints.forEach((item, i) => { + addMarkerForItem(item, i); + }); + + // Add the grouped user points + groupedUserPoints.forEach((item, i) => { + addMarkerForItem(item, i, true); + }); + + // Function to add a marker for a single item + function addMarkerForItem(item, i, isGrouped = false) { const lon = item.lng || item.lon || item.Longitude; const lat = item.lat || item.Latitude; const t = item.temp || item.Result; @@ -210,23 +301,34 @@ export default function MapComponent() { const staleFilter = isStale ? 'backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);' : ''; + + // If this is a grouped user point, modify the marker appearance + const groupLabel = isGrouped && item.pointCount > 1 + ? `${item.pointCount}` + : ''; + + const groupStyle = isGrouped && item.pointCount > 1 + ? 'border: 2px solid #fff; transform: scale(1.1);' + : ''; const icon = L.divIcon({ className: 'custom-temp-marker', html: `
${formattedTemp} + ${groupLabel}
`, iconSize: [50, 40], @@ -235,6 +337,12 @@ export default function MapComponent() { const marker = L.marker([lat, lon], { icon }).addTo(mapInstanceRef.current); + // Set group information for popup display + if (isGrouped && item.pointCount > 1) { + marker.isGrouped = true; + marker.pointCount = item.pointCount; + } + // bump this marker to the top on hover marker.on('mouseover', () => { marker.setZIndexOffset(1000); @@ -266,36 +374,67 @@ export default function MapComponent() { marker.on('click', async () => { // add prefix for grey (stale) points const labelPrefix = isStale ? 'OLD: ' : ''; + + // add prefix for grouped points + const groupPrefix = isGrouped && item.pointCount > 1 + ? `GROUP: ${item.pointCount} points within 1km • ` + : ''; - const historicalData = await fetchHistoricalData(name); + let historicalData = []; + + // For grouped user points, use the collected historical points + if (isGrouped && item.historicalPoints && item.historicalPoints.length > 0) { + historicalData = item.historicalPoints; + } else { + // For non-grouped points, fetch data from API as usual + historicalData = await fetchHistoricalData(name); + } + const popupOffset = [17, -32]; // no historical data if (historicalData.length === 0) { + // Ensure popup is rebinding so it can reopen + marker.closePopup(); + marker.unbindPopup(); marker.bindPopup(`
- +

No historical data available

-

Current temperature: ${formattedTemp} (${tempCategory})

+ ${ + isStale + ? `

Last updated at: ${new Date(rawTime).toLocaleString()}

` + : `

Current temperature: ${formattedTemp} (${tempCategory})

` + } + ${isGrouped && item.pointCount > 1 ? '

This is a group of multiple user points within 1km radius. The most recent temperature is shown.

' : ''}
`, { offset: popupOffset, className: 'custom-popup' - }).openPopup(); + }); + marker.openPopup(); return; } if (historicalData.length < 2) { - marker.bindPopup( - `${name}
Not enough data to generate a graph`, - { - offset: popupOffset, - className: 'custom-popup' - } - ).openPopup(); + // Rebind popup to guarantee it opens on every click + marker.closePopup(); + marker.unbindPopup(); + marker.bindPopup(` +
+ +

Not enough data to generate a graph.

+

Last updated at: ${new Date(rawTime).toLocaleString()}

+
+ `, { + offset: popupOffset, + className: 'custom-popup' + }); + marker.openPopup(); return; } + // Sort the historical data by timestamp const sortedData = historicalData.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); // Declare currentUnit BEFORE using it @@ -389,7 +528,9 @@ export default function MapComponent() { plugins: { title: { display: true, - text: `Historical Data for ${name}`, + text: isGrouped && item.pointCount > 1 + ? `Historical Data for Group (${item.pointCount} points within 1km)` + : `Historical Data for ${name}`, font: { size: isSmallMobile ? 14 : (isMobile ? 16 : 18), weight: 'bold' @@ -417,10 +558,19 @@ export default function MapComponent() { }, afterBody: function(context) { const dataIndex = context[0].dataIndex; + let info = []; + + // Add gap information if (dataIndex > 0 && timeDifferences[dataIndex - 1]) { - return [`Gap from previous: ${timeDifferences[dataIndex - 1]}`]; + info.push(`Gap from previous: ${timeDifferences[dataIndex - 1]}`); + } + + // For grouped points, add additional information + if (isGrouped && item.pointCount > 1) { + info.push(`From group of ${item.pointCount} user points within 1km`); } - return []; + + return info; } } } @@ -575,7 +725,7 @@ export default function MapComponent() { }); markersRef.current.push({ marker, tempC: t, name, lat, lon }); - }); + } } // Function to add water temperature markers diff --git a/frontend/src/styles/MapView.css b/frontend/src/styles/MapView.css index ee27e5c..d212fc2 100644 --- a/frontend/src/styles/MapView.css +++ b/frontend/src/styles/MapView.css @@ -285,3 +285,25 @@ .temp-label.stale { border-left: none !important; } + +/* Grouped user points styling */ +.temp-label.grouped { + position: relative; + border: 2px solid white !important; + box-shadow: 0 0 0 1px rgba(0,0,0,0.3), 0 4px 12px rgba(0,0,0,0.4) !important; +} + +.temp-label .group-count { + display: none !important; +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + .temp-label.grouped { + border: 2px solid rgba(255,255,255,0.8) !important; + } + + .temp-label .group-count { + border: 1px solid rgba(0,0,0,0.8); + } +}