import React, { useEffect, useMemo, useState } from 'react'; import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, useMapEvents, useMap } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api'; import toast from 'react-hot-toast'; const SprinklerPlacement = ({ onPlace }) => { useMapEvents({ click(e) { onPlace([e.latlng.lat, e.latlng.lng]); } }); return null; }; const computeCoverage = (sprinkler) => { if (sprinkler.type === 'rotor_impact' || sprinkler.type === 'spray_fixed') { return { kind: 'circle', radius: sprinkler.throwFeet, degrees: sprinkler.degrees || 360 }; } if (sprinkler.type === 'oscillating_fan') { const L = sprinkler.lengthFeet || 0; const W = sprinkler.widthFeet || 0; return { kind: 'rect', length: L, width: W }; } return null; }; const Watering = () => { const [properties, setProperties] = useState([]); const [selectedProperty, setSelectedProperty] = useState(null); const [sections, setSections] = useState([]); const [placing, setPlacing] = useState(false); const [sprinklerForm, setSprinklerForm] = useState({ mount: 'above_ground', type: 'rotor_impact', gpm: 2.5, throwFeet: 20, degrees: 360, lengthFeet: 30, widthFeet: 20, durationMinutes: 60 }); const [plan, setPlan] = useState(null); const [points, setPoints] = useState([]); const [guiding, setGuiding] = useState(false); const [guideIndex, setGuideIndex] = useState(0); const [currentPos, setCurrentPos] = useState(null); const [watchId, setWatchId] = useState(null); const [sprinklers, setSprinklers] = useState([]); const [selectedSprinklerId, setSelectedSprinklerId] = useState(''); const [selectedPointId, setSelectedPointId] = useState(null); const selectedPoint = useMemo(()=> points.find(p=> p.id===selectedPointId), [points, selectedPointId]); const [editForm, setEditForm] = useState(null); const [satellite, setSatellite] = useState(false); useEffect(() => { (async () => { try { const [pr, eq] = await Promise.all([propertiesAPI.getAll(), equipmentAPI.getAll()]); setProperties(pr.data?.data?.properties||[]); const list = (eq.data?.data?.equipment||[]).filter(e => (e.categoryName||'').toLowerCase()==='sprinkler'); setSprinklers(list); } catch(e){ toast.error('Failed to load properties'); } })(); }, []); const loadProperty = async (pid) => { try { const r = await propertiesAPI.getById(pid); const p = r.data?.data?.property; setSelectedProperty(p); const secs = (p.sections||[]).map(s => ({ ...s, polygonData: typeof s.polygonData === 'string' ? JSON.parse(s.polygonData) : s.polygonData })); setSections(secs); } catch(e){ toast.error('Failed to load property'); } }; const ensurePlan = async () => { if (plan) return plan; if (!selectedProperty) { toast.error('Select a property first'); return null; } try { const r = await wateringAPI.createPlan({ propertyId: selectedProperty.id, name: `${selectedProperty.name} - Sprinklers` }); setPlan(r.data?.data?.plan); return r.data?.data?.plan; } catch(e){ toast.error('Failed to create plan'); return null; } }; const onPlace = async (latlng) => { const p = await ensurePlan(); if (!p) return; try { const eq = sprinklers.find(s=> s.id === (selectedSprinklerId? parseInt(selectedSprinklerId): -1)); const base = eq ? { mount: eq.sprinklerMount || sprinklerForm.mount, type: eq.sprinklerHeadType || sprinklerForm.type, gpm: eq.sprinklerGpm || sprinklerForm.gpm, throwFeet: eq.sprinklerThrowFeet || sprinklerForm.throwFeet, degrees: sprinklerForm.degrees, lengthFeet: eq.sprinklerLengthFeet || sprinklerForm.lengthFeet, widthFeet: eq.sprinklerWidthFeet || sprinklerForm.widthFeet } : sprinklerForm; const payload = { lat: latlng[0], lng: latlng[1], durationMinutes: sprinklerForm.durationMinutes, mountType: base.mount, sprinklerHeadType: base.type, gpm: base.gpm, throwFeet: base.throwFeet, degrees: base.degrees, lengthFeet: base.lengthFeet, widthFeet: base.widthFeet, headingDegrees: sprinklerForm.headingDegrees || 0 }; const r = await wateringAPI.addPlanPoint(p.id, payload); setPoints(prev => [...prev, r.data?.data?.point]); toast.success('Sprinkler location added'); setPlacing(false); } catch(e){ toast.error('Failed to add sprinkler point'); } }; const center = useMemo(() => { if (selectedProperty?.latitude && selectedProperty?.longitude) return [Number(selectedProperty.latitude), Number(selectedProperty.longitude)]; if (sections?.length){ const pts = sections.flatMap(s => (s.polygonData?.coordinates?.[0]||[])); if (pts.length){ const lat = pts.reduce((a,p)=> a + Number(p[0]), 0) / pts.length; const lng = pts.reduce((a,p)=> a + Number(p[1]), 0) / pts.length; return [lat, lng]; } } return [39.8,-98.6]; }, [selectedProperty, sections]); const CenterOnChange = ({ center }) => { const map = useMap(); useEffect(() => { if (center && Array.isArray(center)) map.setView(center); }, [center, map]); return null; }; const startGuidance = () => { if (points.length === 0) { toast.error('Add at least one point'); return; } if (!navigator.geolocation) { toast.error('GPS not available'); return; } setGuiding(true); setGuideIndex(0); const id = navigator.geolocation.watchPosition(pos => { setCurrentPos({ lat: pos.coords.latitude, lng: pos.coords.longitude }); }, err => { console.warn(err); toast.error('GPS error'); }, { enableHighAccuracy:true, maximumAge: 1000, timeout: 10000 }); setWatchId(id); }; const stopGuidance = () => { if (watchId){ navigator.geolocation.clearWatch(watchId); setWatchId(null);} setGuiding(false); }; const distanceFeet = (a,b) => { const R=6371000; const toRad=d=>d*Math.PI/180; const dLat=toRad(b.lat-a.lat); const dLng=toRad(b.lng-a.lng); const A=Math.sin(dLat/2)**2 + Math.cos(toRad(a.lat))*Math.cos(toRad(b.lat))*Math.sin(dLng/2)**2; return (2*R*Math.atan2(Math.sqrt(A),Math.sqrt(1-A)))*3.28084; }; // Build sector polygon (approx) for Leaflet const sectorPolygon = (center, radiusFeet, startDeg, endDeg, steps=60) => { const [clat, clng] = [Number(center.lat), Number(center.lng)]; const Rlat = 111320; // meters per degree lat const Rlng = Math.cos(clat*Math.PI/180)*111320; // meters per degree lng const rm = radiusFeet*0.3048; // meters const pts = []; const start = startDeg*Math.PI/180; const end = endDeg*Math.PI/180; const span = end - start; const n = Math.max(8, Math.round(steps*Math.abs(span)/(2*Math.PI))); pts.push([clat, clng]); for (let i=0;i<=n;i++){ const ang = start + (span*i/n); const dx = rm * Math.cos(ang); const dy = rm * Math.sin(ang); const lat = clat + (dy / Rlat); const lng = clng + (dx / Rlng); pts.push([lat, lng]); } pts.push([clat, clng]); return pts; }; const coverageSqft = useMemo(() => { const type = sprinklerForm.type; if (type==='rotor_impact' || type==='spray_fixed') { const r = Number(sprinklerForm.throwFeet||0); const deg=Number(sprinklerForm.degrees||360); return Math.PI * r * r * (deg/360); } if (type==='oscillating_fan') { return Number(sprinklerForm.lengthFeet||0) * Number(sprinklerForm.widthFeet||0); } return 0; }, [sprinklerForm]); const [targetInches, setTargetInches] = useState(0.5); const suggestMinutes = useMemo(() => { const gpm = Number(sprinklerForm.gpm||0); if (!gpm || !coverageSqft || !targetInches) return 0; const gallonsNeeded = coverageSqft * targetInches * 0.623; // gal per sqft-inch return Math.ceil((gallonsNeeded / gpm)); }, [sprinklerForm.gpm, coverageSqft, targetInches]); // Select a point to edit (enables popup edits too) const onSelectPoint = (pt) => { setSelectedPointId(pt.id); setEditForm({ durationMinutes: pt.duration_minutes || 0, mountType: pt.sprinkler_mount, sprinklerHeadType: pt.sprinkler_head_type, gpm: Number(pt.sprinkler_gpm||0), throwFeet: Number(pt.sprinkler_throw_feet||0), degrees: Number(pt.sprinkler_degrees||360), lengthFeet: Number(pt.sprinkler_length_feet||0), widthFeet: Number(pt.sprinkler_width_feet||0), headingDegrees: Number(pt.sprinkler_heading_degrees||0) }); }; // Persist changes to a point and refresh it in local state const updatePointField = async (id, patch) => { try { const r = await wateringAPI.updatePoint(id, patch); const np = r.data?.data?.point; setPoints(prev => prev.map(p=> p.id===id? np: p)); } catch(e){ toast.error('Failed to update point'); } }; return (