asdfasfd
This commit is contained in:
@@ -350,6 +350,7 @@ const PropertyDetail = () => {
|
|||||||
const [isTracing, setIsTracing] = useState(false);
|
const [isTracing, setIsTracing] = useState(false);
|
||||||
const [gpsWatchId, setGpsWatchId] = useState(null);
|
const [gpsWatchId, setGpsWatchId] = useState(null);
|
||||||
const [gpsTracePoints, setGpsTracePoints] = useState([]);
|
const [gpsTracePoints, setGpsTracePoints] = useState([]);
|
||||||
|
const [gpsSamples, setGpsSamples] = useState([]); // recent raw samples for smoothing
|
||||||
const [gpsDistance, setGpsDistance] = useState(0);
|
const [gpsDistance, setGpsDistance] = useState(0);
|
||||||
const [gpsAccuracy, setGpsAccuracy] = useState(null);
|
const [gpsAccuracy, setGpsAccuracy] = useState(null);
|
||||||
const [isSnapPreview, setIsSnapPreview] = useState(false);
|
const [isSnapPreview, setIsSnapPreview] = useState(false);
|
||||||
@@ -476,10 +477,39 @@ const PropertyDetail = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// GPS filtering and snapping (defaults)
|
// GPS filtering and snapping (defaults)
|
||||||
const ACCURACY_MAX_METERS = 15; // Ignore points with accuracy worse than this
|
const ACCURACY_MAX_METERS = 12; // 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 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
|
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) => {
|
const acceptAndNormalizePoint = (lat, lng, accuracy, currentPoints) => {
|
||||||
if (accuracy != null && accuracy > ACCURACY_MAX_METERS) {
|
if (accuracy != null && accuracy > ACCURACY_MAX_METERS) {
|
||||||
toast("GPS accuracy too low (" + Math.round(accuracy) + "m). Waiting for better fix…");
|
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 [llat, llng] = currentPoints[currentPoints.length - 1];
|
||||||
const moved = haversine(llat, llng, lat, lng);
|
const moved = haversine(llat, llng, lat, lng);
|
||||||
if (moved < MIN_MOVE_METERS) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Snap to starting point if near
|
// Snap to starting point if near
|
||||||
if (currentPoints.length >= 2) {
|
if (currentPoints.length >= 2) {
|
||||||
const [slat, slng] = currentPoints[0];
|
const [slat, slng] = currentPoints[0];
|
||||||
@@ -512,11 +550,12 @@ const PropertyDetail = () => {
|
|||||||
const { latitude, longitude, accuracy } = pos.coords;
|
const { latitude, longitude, accuracy } = pos.coords;
|
||||||
setGpsAccuracy(accuracy || null);
|
setGpsAccuracy(accuracy || null);
|
||||||
setGpsTracePoints(prev => {
|
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
|
// For preview: check proximity to start even if skipping
|
||||||
if (prev.length >= 2) {
|
if (prev.length >= 2) {
|
||||||
const [slat, slng] = prev[0];
|
const [slat, slng] = prev[0];
|
||||||
const dStart = haversine(slat, slng, latitude, longitude);
|
const dStart = haversine(slat, slng, sLat, sLng);
|
||||||
setIsSnapPreview(dStart <= SNAP_METERS);
|
setIsSnapPreview(dStart <= SNAP_METERS);
|
||||||
} else { setIsSnapPreview(false); }
|
} else { setIsSnapPreview(false); }
|
||||||
if (!normalized) return prev; // skip
|
if (!normalized) return prev; // skip
|
||||||
@@ -567,11 +606,22 @@ const PropertyDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 = {
|
const sectionData = {
|
||||||
name: sectionName,
|
name: sectionName,
|
||||||
area: pendingSection.area,
|
area: recomputedArea,
|
||||||
polygonData: {
|
polygonData: {
|
||||||
coordinates: [pendingSection.coordinates],
|
coordinates: [coords],
|
||||||
color: pendingSection.color
|
color: pendingSection.color
|
||||||
},
|
},
|
||||||
grassType: sectionGrassTypes.join(', '),
|
grassType: sectionGrassTypes.join(', '),
|
||||||
@@ -611,7 +661,7 @@ const PropertyDetail = () => {
|
|||||||
setCurrentColor(SECTION_COLORS[nextIndex]);
|
setCurrentColor(SECTION_COLORS[nextIndex]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save section:', 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;
|
const { latitude, longitude, accuracy } = pos.coords;
|
||||||
setGpsAccuracy(accuracy || null);
|
setGpsAccuracy(accuracy || null);
|
||||||
setGpsTracePoints(prev => {
|
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) {
|
if (prev.length >= 2) {
|
||||||
const [slat, slng] = prev[0];
|
const [slat, slng] = prev[0];
|
||||||
const dStart = haversine(slat, slng, latitude, longitude);
|
const dStart = haversine(slat, slng, sLat, sLng);
|
||||||
setIsSnapPreview(dStart <= SNAP_METERS);
|
setIsSnapPreview(dStart <= SNAP_METERS);
|
||||||
} else { setIsSnapPreview(false); }
|
} else { setIsSnapPreview(false); }
|
||||||
if (!normalized) return prev;
|
if (!normalized) return prev;
|
||||||
@@ -1226,6 +1277,55 @@ const PropertyDetail = () => {
|
|||||||
/>
|
/>
|
||||||
<span>{pendingSection?.area.toLocaleString()} sq ft</span>
|
<span>{pendingSection?.area.toLocaleString()} sq ft</span>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="label">Grass Types</label>
|
<label className="label">Grass Types</label>
|
||||||
<TagInput
|
<TagInput
|
||||||
|
|||||||
Reference in New Issue
Block a user