diff --git a/frontend/src/components/HUDleftPoints.jsx b/frontend/src/components/HUDleftPoints.jsx index 8902e64..abc2016 100644 --- a/frontend/src/components/HUDleftPoints.jsx +++ b/frontend/src/components/HUDleftPoints.jsx @@ -26,7 +26,11 @@ function LogoBlock() { const [showSortMenu, setShowSortMenu] = useState(false); const [unit, setUnit] = useState('C'); const [showFilterModal, setShowFilterModal] = useState(false); - const [tempFilter, setTempFilter] = useState({ min: '', max: '' }); + const [tempFilter, setTempFilter] = useState({ + min: '', max: '', + distanceMin: '', distanceMax: '', + maxAge: '' + }); // Add new state for user location const [userLocation, setUserLocation] = useState(null); @@ -303,19 +307,30 @@ function LogoBlock() { // NEW: dispatch filterchange event const handleApplyFilter = () => { - const min = parseFloat(tempFilter.min); - const max = parseFloat(tempFilter.max); + const min = parseFloat(tempFilter.min); + const max = parseFloat(tempFilter.max); + const distanceMax = parseFloat(tempFilter.distanceMax); + const maxAge = parseFloat(tempFilter.maxAge); window.dispatchEvent(new CustomEvent('filterchange', { - detail: { min: isNaN(min) ? NaN : min, max: isNaN(max) ? NaN : max } + detail: { + min: isNaN(min) ? NaN : min, + max: isNaN(max) ? NaN : max, + distanceMax:isNaN(distanceMax)? NaN : distanceMax, + maxAge: isNaN(maxAge) ? NaN : maxAge + } })); setShowFilterModal(false); }; // NEW: reset filter AND dispatch const handleResetFilter = () => { - setTempFilter({ min: '', max: '' }); + setTempFilter({ min:'', max:'', distanceMax:'', maxAge:'' }); window.dispatchEvent(new CustomEvent('filterchange', { - detail: { min: NaN, max: NaN } + detail: { + min: NaN, max: NaN, + distanceMax: NaN, + maxAge: NaN + } })); setShowFilterModal(false); }; @@ -323,19 +338,35 @@ function LogoBlock() { // Listen for filterchange events and update the side‐panel list useEffect(() => { function handleFilterChange(e) { - const { min, max } = e.detail; - setFilteredList( - locaList.filter(item => - (isNaN(min) || item.temp >= min) && - (isNaN(max) || item.temp <= max) - ) - ); + const { min, max, distanceMax, maxAge } = e.detail; + const now = Date.now(); + setFilteredList(locaList.filter(item => { + const tempOK = (isNaN(min) || item.temp >= min) && + (isNaN(max) || item.temp <= max); + + let distOK = true; + if (!isNaN(distanceMax) && userLocation) { + const d = calculateDistance( + userLocation.latitude, + userLocation.longitude, + item.lat, item.lng || item.lon + ); + if (d > distanceMax) distOK = false; + } + + let ageOK = true; + if (!isNaN(maxAge) && item.timestamp) { + const ageDays = (now - new Date(item.timestamp).getTime()) + / (1000*60*60*24); + if (ageDays > maxAge) ageOK = false; + } + + return tempOK && distOK && ageOK; + })); } window.addEventListener('filterchange', handleFilterChange); - return () => { - window.removeEventListener('filterchange', handleFilterChange); - }; - }, [locaList]); + return () => window.removeEventListener('filterchange', handleFilterChange); + }, [locaList, userLocation]); return (
UnitManager.getUnit()); + const userLocationRef = useRef(null); // Temperature color function - works with both Celsius and Fahrenheit function getTemperatureColor(temp, unit = 'C', mode = 'light') { @@ -724,7 +725,7 @@ export default function MapComponent() { }, 100); }); - markersRef.current.push({ marker, tempC: t, name, lat, lon }); + markersRef.current.push({ marker, tempC: t, name, lat, lon, timestamp: ts }); } } @@ -1122,6 +1123,79 @@ export default function MapComponent() { }; }, []); + // new single useEffect to handle ALL filters + useEffect(() => { + function calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371; + const dLat = (lat2 - lat1)*Math.PI/180; + const dLon = (lon2 - lon1)*Math.PI/180; + const a = Math.sin(dLat/2)**2 + + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180) * + Math.sin(dLon/2)**2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + } + + const handleFilter = e => { + const { min, max, distanceMax, maxAge } = e.detail; + const now = Date.now(); + + // If distance filter is requested but no location, try to get it + if (!isNaN(distanceMax) && !userLocationRef.current) { + console.log('🔄 Distance filter requested, attempting to get location...'); + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + userLocationRef.current = { + lat: position.coords.latitude, + lon: position.coords.longitude + }; + console.log('📍 Got user location for filtering:', userLocationRef.current); + // Re-trigger the filter with location now available + handleFilter(e); + }, + (error) => { + console.warn('❌ Could not get location for distance filter:', error.message); + alert('Location access is required for distance filtering. Please enable location permissions and try again.'); + } + ); + return; // Exit early, will re-run once location is obtained + } else { + alert('Geolocation is not supported by this browser.'); + return; + } + } + + markersRef.current.forEach(({ marker, tempC, lat, lon, timestamp }) => { + let keep = true; + if (!isNaN(min) && tempC < min) keep = false; + if (!isNaN(max) && tempC > max) keep = false; + + if (!isNaN(distanceMax) && userLocationRef.current) { + const d = calculateDistance( + userLocationRef.current.lat, + userLocationRef.current.lon, + lat, lon + ); + if (d > distanceMax) keep = false; + } + + if (!isNaN(maxAge) && timestamp) { + const ageDays = (now - timestamp)/(1000*60*60*24); + if (ageDays > maxAge) keep = false; + } + + if (keep) { + marker.addTo(mapInstanceRef.current); + } else { + mapInstanceRef.current.removeLayer(marker); + } + }); + }; + + window.addEventListener('filterchange', handleFilter); + return () => window.removeEventListener('filterchange', handleFilter); + }, []); + return (
{/* Map container */} diff --git a/frontend/src/components/TempFilterModal.jsx b/frontend/src/components/TempFilterModal.jsx index 712267d..643d39f 100644 --- a/frontend/src/components/TempFilterModal.jsx +++ b/frontend/src/components/TempFilterModal.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { createPortal } from 'react-dom'; -function TempFilterModal({ +export default function TempFilterModal({ show, onClose, theme, @@ -80,7 +80,7 @@ function TempFilterModal({
+ + {/* only Max Distance now */} +
+ + setTempFilter(f => ({ ...f, distanceMax: e.target.value }))} + style={{ + width: '100%', + padding: '0.5rem', + borderRadius: '0.5rem', + border: theme === 'light' + ? '1px solid rgba(0,0,0,0.1)' + : '1px solid rgba(255,255,255,0.2)', + backgroundColor: theme === 'light' ? '#fff' : 'rgba(255,255,255,0.1)', + color: theme === 'light' ? '#000' : '#fff', + fontSize: '1rem', + outline: 'none', + transition: 'border-color 0.2s ease', + boxSizing: 'border-box' + }} + onFocus={e => e.target.style.borderColor = theme === 'light' ? '#007AFF' : '#0A84FF'} + onBlur={e => e.target.style.borderColor = theme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.2)'} + /> +
+ +
+ + setTempFilter(f => ({ ...f, maxAge: e.target.value }))} + style={{ + width: '100%', + padding: '0.5rem', + borderRadius: '0.5rem', + border: theme === 'light' + ? '1px solid rgba(0,0,0,0.1)' + : '1px solid rgba(255,255,255,0.2)', + backgroundColor: theme === 'light' + ? '#fff' + : 'rgba(255,255,255,0.1)', + color: theme === 'light' ? '#000' : '#fff', + fontSize: '1rem', + outline: 'none', + transition: 'border-color 0.2s ease', + boxSizing: 'border-box' + }} + onFocus={e => e.target.style.borderColor = theme === 'light' ? '#007AFF' : '#0A84FF'} + onBlur={e => e.target.style.borderColor = theme === 'light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.2)'} + /> +
, document.body ) : null; -} - -export default TempFilterModal; \ No newline at end of file +} \ No newline at end of file