diff --git a/frontend/src/pages/Properties/PropertyDetail.js b/frontend/src/pages/Properties/PropertyDetail.js index 5acb081..c5c0f2b 100644 --- a/frontend/src/pages/Properties/PropertyDetail.js +++ b/frontend/src/pages/Properties/PropertyDetail.js @@ -350,6 +350,7 @@ const PropertyDetail = () => { const [isTracing, setIsTracing] = useState(false); const [gpsWatchId, setGpsWatchId] = useState(null); const [gpsTracePoints, setGpsTracePoints] = useState([]); + const [gpsSamples, setGpsSamples] = useState([]); // recent raw samples for smoothing const [gpsDistance, setGpsDistance] = useState(0); const [gpsAccuracy, setGpsAccuracy] = useState(null); const [isSnapPreview, setIsSnapPreview] = useState(false); @@ -476,10 +477,39 @@ const PropertyDetail = () => { }; // GPS filtering and snapping (defaults) - const ACCURACY_MAX_METERS = 15; // Ignore points with accuracy worse than this - const MIN_MOVE_METERS = 2; // Ignore points if user hasn't moved this far from last point + const ACCURACY_MAX_METERS = 12; // Ignore points with accuracy worse than this + const MIN_MOVE_METERS = 2.5; // Ignore points if user hasn't moved this far from last point + const TURN_MIN_DEG = 12; // Minimum heading change to record when small movement const SNAP_METERS = 5; // Snap to starting point when within this distance + // Compute bearing between two lat/lng points (degrees) + const bearingDeg = (a, b) => { + const toRad = (d) => (d * Math.PI) / 180; + const toDeg = (r) => (r * 180) / Math.PI; + const lat1 = toRad(a[0]); + const lat2 = toRad(b[0]); + const dLng = toRad(b[1] - a[1]); + const y = Math.sin(dLng) * Math.cos(lat2); + const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng); + const brg = (toDeg(Math.atan2(y, x)) + 360) % 360; + return brg; + }; + + // Maintain a short buffer of samples; return averaged lat/lng of best by accuracy + const addSampleAndSmooth = (lat, lng, accuracy) => { + const now = Date.now(); + setGpsSamples((prev) => [...prev, { lat, lng, accuracy: accuracy ?? 999, t: now }].slice(-12)); + const pool = [...gpsSamples, { lat, lng, accuracy: accuracy ?? 999, t: now }].slice(-12); + const top = pool + .filter((s) => s.accuracy != null) + .sort((a, b) => a.accuracy - b.accuracy) + .slice(0, Math.min(5, pool.length)); + if (top.length === 0) return [lat, lng]; + const avgLat = top.reduce((s, p) => s + p.lat, 0) / top.length; + const avgLng = top.reduce((s, p) => s + p.lng, 0) / top.length; + return [avgLat, avgLng]; + }; + const acceptAndNormalizePoint = (lat, lng, accuracy, currentPoints) => { if (accuracy != null && accuracy > ACCURACY_MAX_METERS) { toast("GPS accuracy too low (" + Math.round(accuracy) + "m). Waiting for better fix…"); @@ -490,8 +520,16 @@ const PropertyDetail = () => { const [llat, llng] = currentPoints[currentPoints.length - 1]; const moved = haversine(llat, llng, lat, lng); if (moved < MIN_MOVE_METERS) { - // Too close to last point - return null; + // Allow small move only if heading changed sufficiently vs previous segment + if (currentPoints.length >= 2) { + const prevHead = bearingDeg(currentPoints[currentPoints.length - 2], [llat, llng]); + const candHead = bearingDeg([llat, llng], [lat, lng]); + let diff = Math.abs(prevHead - candHead); + if (diff > 180) diff = 360 - diff; + if (diff < TURN_MIN_DEG) return null; + } else { + return null; + } } } // Snap to starting point if near @@ -512,11 +550,12 @@ const PropertyDetail = () => { const { latitude, longitude, accuracy } = pos.coords; setGpsAccuracy(accuracy || null); setGpsTracePoints(prev => { - const normalized = acceptAndNormalizePoint(latitude, longitude, accuracy, prev); + const [sLat, sLng] = addSampleAndSmooth(latitude, longitude, accuracy); + const normalized = acceptAndNormalizePoint(sLat, sLng, accuracy, prev); // For preview: check proximity to start even if skipping if (prev.length >= 2) { const [slat, slng] = prev[0]; - const dStart = haversine(slat, slng, latitude, longitude); + const dStart = haversine(slat, slng, sLat, sLng); setIsSnapPreview(dStart <= SNAP_METERS); } else { setIsSnapPreview(false); } if (!normalized) return prev; // skip @@ -567,11 +606,22 @@ const PropertyDetail = () => { } try { + // Ensure closed ring and recompute area + let coords = pendingSection.coordinates || []; + if (coords.length >= 3) { + const [f0, f1] = coords[0] || []; + const [l0, l1] = coords[coords.length - 1] || []; + if (f0 !== l0 || f1 !== l1) coords = [...coords, coords[0]]; + } + if (coords.length < 4) { toast.error('Polygon invalid: need at least 3 unique points'); return; } + const recomputedArea = calculateAreaInSqFt(coords); + if (!recomputedArea || recomputedArea <= 0) { toast.error('Polygon area is zero — adjust points'); return; } + const sectionData = { name: sectionName, - area: pendingSection.area, + area: recomputedArea, polygonData: { - coordinates: [pendingSection.coordinates], + coordinates: [coords], color: pendingSection.color }, grassType: sectionGrassTypes.join(', '), @@ -611,7 +661,7 @@ const PropertyDetail = () => { setCurrentColor(SECTION_COLORS[nextIndex]); } catch (error) { console.error('Failed to save section:', error); - toast.error('Failed to save section. Please try again.'); + toast.error(error?.response?.data?.message || error?.message || 'Failed to save section. Please try again.'); } }; @@ -939,10 +989,11 @@ const PropertyDetail = () => { const { latitude, longitude, accuracy } = pos.coords; setGpsAccuracy(accuracy || null); setGpsTracePoints(prev => { - const normalized = acceptAndNormalizePoint(latitude, longitude, accuracy, prev); + const [sLat, sLng] = addSampleAndSmooth(latitude, longitude, accuracy); + const normalized = acceptAndNormalizePoint(sLat, sLng, accuracy, prev); if (prev.length >= 2) { const [slat, slng] = prev[0]; - const dStart = haversine(slat, slng, latitude, longitude); + const dStart = haversine(slat, slng, sLat, sLng); setIsSnapPreview(dStart <= SNAP_METERS); } else { setIsSnapPreview(false); } if (!normalized) return prev; @@ -1226,6 +1277,55 @@ const PropertyDetail = () => { /> {pendingSection?.area.toLocaleString()} sq ft + {/* Nudge controls */} +