This commit is contained in:
Jake Kasper
2025-09-05 11:31:50 -04:00
parent da612539e6
commit b19db1d8c1

View File

@@ -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,10 +520,18 @@ 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
// 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
if (currentPoints.length >= 2) {
const [slat, slng] = currentPoints[0];
@@ -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 = () => {
/>
<span>{pendingSection?.area.toLocaleString()} sq ft</span>
</div>
{/* Nudge controls */}
<div>
<label className="label">Adjust Position (1 ft)</label>
<div className="grid grid-cols-3 gap-2 w-40">
<div></div>
<button className="btn-secondary" onClick={() => {
setPendingSection(prev => {
if (!prev) return prev;
const lat = prev.coordinates[0][0];
const metersPerDegLat = 111320;
const dLat = (1 * 0.3048) / metersPerDegLat; // 1 ft north
const coords = prev.coordinates.map(([la, ln]) => [la + dLat, ln]);
return { ...prev, coordinates };
});
}}></button>
<div></div>
<button className="btn-secondary" onClick={() => {
setPendingSection(prev => {
if (!prev) return prev;
const lat = prev.coordinates[0][0];
const metersPerDegLng = 111320 * Math.cos(lat * Math.PI / 180);
const dLng = (1 * 0.3048) / metersPerDegLng; // 1 ft west
const coords = prev.coordinates.map(([la, ln]) => [la, ln - dLng]);
return { ...prev, coordinates };
});
}}></button>
<div></div>
<button className="btn-secondary" onClick={() => {
setPendingSection(prev => {
if (!prev) return prev;
const lat = prev.coordinates[0][0];
const metersPerDegLng = 111320 * Math.cos(lat * Math.PI / 180);
const dLng = (1 * 0.3048) / metersPerDegLng; // 1 ft east
const coords = prev.coordinates.map(([la, ln]) => [la, ln + dLng]);
return { ...prev, coordinates };
});
}}></button>
<div></div>
<button className="btn-secondary" onClick={() => {
setPendingSection(prev => {
if (!prev) return prev;
const dLat = (1 * 0.3048) / 111320; // 1 ft south
const coords = prev.coordinates.map(([la, ln]) => [la - dLat, ln]);
return { ...prev, coordinates };
});
}}></button>
<div></div>
</div>
</div>
<div>
<label className="label">Grass Types</label>
<TagInput