watering 2

This commit is contained in:
Jake Kasper
2025-09-04 15:13:57 -04:00
parent 610131e5c2
commit 6bf68364ec
6 changed files with 239 additions and 24 deletions

View File

@@ -167,6 +167,15 @@ router.get('/', async (req, res, next) => {
material: item.material,
colorCode: item.color_code,
quantityOwned: item.quantity_owned,
// Sprinkler fields
sprinklerMount: item.sprinkler_mount,
sprinklerHeadType: item.sprinkler_head_type,
sprinklerGpm: item.sprinkler_gpm ? parseFloat(item.sprinkler_gpm) : null,
sprinklerThrowFeet: item.sprinkler_throw_feet ? parseFloat(item.sprinkler_throw_feet) : null,
sprinklerDegrees: item.sprinkler_degrees,
sprinklerLengthFeet: item.sprinkler_length_feet ? parseFloat(item.sprinkler_length_feet) : null,
sprinklerWidthFeet: item.sprinkler_width_feet ? parseFloat(item.sprinkler_width_feet) : null,
sprinklerCoverageSqft: item.sprinkler_coverage_sqft ? parseFloat(item.sprinkler_coverage_sqft) : null,
// General fields
purchaseDate: item.purchase_date,
purchasePrice: parseFloat(item.purchase_price) || null,
@@ -297,7 +306,16 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
notes: item.notes,
isActive: item.is_active,
createdAt: item.created_at,
updatedAt: item.updated_at
updatedAt: item.updated_at,
// Sprinkler fields
sprinklerMount: item.sprinkler_mount,
sprinklerHeadType: item.sprinkler_head_type,
sprinklerGpm: item.sprinkler_gpm ? parseFloat(item.sprinkler_gpm) : null,
sprinklerThrowFeet: item.sprinkler_throw_feet ? parseFloat(item.sprinkler_throw_feet) : null,
sprinklerDegrees: item.sprinkler_degrees,
sprinklerLengthFeet: item.sprinkler_length_feet ? parseFloat(item.sprinkler_length_feet) : null,
sprinklerWidthFeet: item.sprinkler_width_feet ? parseFloat(item.sprinkler_width_feet) : null,
sprinklerCoverageSqft: item.sprinkler_coverage_sqft ? parseFloat(item.sprinkler_coverage_sqft) : null
}
}
});
@@ -405,8 +423,9 @@ router.post('/', async (req, res, next) => {
tool_type, working_width_inches,
pump_type, max_gpm, max_psi, power_source,
orifice_size, spray_angle, flow_rate_gpm, droplet_size, spray_pattern, pressure_range_psi, thread_size, material, color_code, quantity_owned,
purchase_date, purchase_price, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38)
purchase_date, purchase_price, notes,
sprinkler_mount, sprinkler_head_type, sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45)
RETURNING *`,
[
req.user.id, equipmentTypeId, finalCategoryId, customName, manufacturer, model,
@@ -416,7 +435,8 @@ router.post('/', async (req, res, next) => {
toolType, workingWidthInches,
pumpType, maxGpm, maxPsi, powerSource,
orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned,
purchaseDate, purchasePrice, notes
purchaseDate, purchasePrice, notes,
sprinklerMount || null, sprinklerHeadType || null, sprinklerGpm || null, sprinklerThrowFeet || null, sprinklerDegrees || null, sprinklerLengthFeet || null, sprinklerWidthFeet || null
]
);
@@ -533,6 +553,7 @@ router.put('/:id', validateParams(idParamSchema), async (req, res, next) => {
pump_type = $21, max_gpm = $22, max_psi = $23, power_source = $24,
orifice_size = $25, spray_angle = $26, flow_rate_gpm = $27, droplet_size = $28, spray_pattern = $29, pressure_range_psi = $30, thread_size = $31, material = $32, color_code = $33, quantity_owned = $34,
purchase_date = $35, purchase_price = $36, notes = $37, is_active = $38,
sprinkler_mount = $40, sprinkler_head_type = $41, sprinkler_gpm = $42, sprinkler_throw_feet = $43, sprinkler_degrees = $44, sprinkler_length_feet = $45, sprinkler_width_feet = $46,
updated_at = CURRENT_TIMESTAMP
WHERE id = $39
RETURNING *`,
@@ -545,7 +566,8 @@ router.put('/:id', validateParams(idParamSchema), async (req, res, next) => {
pumpType, maxGpm, maxPsi, powerSource,
orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned,
purchaseDate, purchasePrice, notes, isActive !== undefined ? isActive : true,
equipmentId
equipmentId,
sprinklerMount || null, sprinklerHeadType || null, sprinklerGpm || null, sprinklerThrowFeet || null, sprinklerDegrees || null, sprinklerLengthFeet || null, sprinklerWidthFeet || null
]
);

View File

@@ -91,15 +91,14 @@ router.post('/plans/:id/points', async (req,res,next)=>{
const ins = await pool.query(
`INSERT INTO watering_plan_points
(plan_id, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type,
sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`,
sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft, sprinkler_heading_degrees)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`,
[planId, sequence, payload.lat, payload.lng, payload.durationMinutes||0, payload.mountType||null,
payload.sprinklerHeadType||null, payload.gpm||null, payload.throwFeet||null, payload.degrees||null,
payload.lengthFeet||null, payload.widthFeet||null, coverage]
payload.lengthFeet||null, payload.widthFeet||null, coverage, payload.headingDegrees||null]
);
res.status(201).json({ success:true, data:{ point: ins.rows[0] }});
} catch (e) { next(e); }
});
module.exports = router;

View File

@@ -0,0 +1,6 @@
-- Add heading for sprinkler sectors on watering plan points
ALTER TABLE watering_plan_points
ADD COLUMN IF NOT EXISTS sprinkler_heading_degrees INTEGER;
SELECT 'Added sprinkler_heading_degrees to watering_plan_points' as migration_status;

View File

@@ -66,6 +66,12 @@ const Layout = ({ children }) => {
icon: CalendarDaysIcon,
iconSolid: CalendarIconSolid,
},
{
name: 'Watering',
href: '/watering',
icon: CloudIcon,
iconSolid: CloudIconSolid,
},
{
name: 'Mowing',
href: '/mowing',
@@ -84,6 +90,12 @@ const Layout = ({ children }) => {
icon: CloudIcon,
iconSolid: CloudIconSolid,
},
{
name: 'Watering',
href: '/watering',
icon: CloudIcon,
iconSolid: CloudIconSolid,
},
];
const adminNavigation = [

View File

@@ -246,6 +246,18 @@ const Equipment = () => {
{item.boomSections && <p><strong>Boom Sections:</strong> {item.boomSections}</p>}
</>
);
case 'sprinkler':
return (
<>
{item.sprinklerMount && <p><strong>Mount:</strong> {item.sprinklerMount.replace('_',' ')}</p>}
{item.sprinklerHeadType && <p><strong>Head:</strong> {item.sprinklerHeadType.replace('_',' ')}</p>}
{item.sprinklerGpm && <p><strong>GPM:</strong> {item.sprinklerGpm}</p>}
{item.sprinklerThrowFeet && <p><strong>Throw:</strong> {item.sprinklerThrowFeet} ft</p>}
{item.sprinklerDegrees && <p><strong>Degrees:</strong> {item.sprinklerDegrees}°</p>}
{item.sprinklerLengthFeet && <p><strong>Length:</strong> {item.sprinklerLengthFeet} ft</p>}
{item.sprinklerWidthFeet && <p><strong>Width:</strong> {item.sprinklerWidthFeet} ft</p>}
</>
);
case 'pump':
return (
<>
@@ -281,6 +293,7 @@ const Equipment = () => {
'Mower': 'bg-green-100 text-green-800',
'Spreader': 'bg-orange-100 text-orange-800',
'Sprayer': 'bg-blue-100 text-blue-800',
'Sprinkler': 'bg-cyan-100 text-cyan-800',
'Nozzle': 'bg-teal-100 text-teal-800',
'Pump': 'bg-purple-100 text-purple-800',
'Aerator': 'bg-yellow-100 text-yellow-800',
@@ -529,6 +542,14 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
material: equipment?.material || '',
colorCode: equipment?.colorCode || '',
quantityOwned: equipment?.quantityOwned || 1,
// Sprinkler fields
sprinklerMount: equipment?.sprinklerMount || 'above_ground',
sprinklerHeadType: equipment?.sprinklerHeadType || 'rotor_impact',
sprinklerGpm: equipment?.sprinklerGpm || '',
sprinklerThrowFeet: equipment?.sprinklerThrowFeet || '',
sprinklerDegrees: equipment?.sprinklerDegrees || 360,
sprinklerLengthFeet: equipment?.sprinklerLengthFeet || '',
sprinklerWidthFeet: equipment?.sprinklerWidthFeet || '',
// General fields
purchaseDate: equipment?.purchaseDate || '',
purchasePrice: equipment?.purchasePrice || '',
@@ -587,6 +608,14 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
material: formData.material || null,
colorCode: formData.colorCode || null,
quantityOwned: formData.quantityOwned ? parseInt(formData.quantityOwned) : null,
// Sprinkler fields
sprinklerMount: formData.sprinklerMount || null,
sprinklerHeadType: formData.sprinklerHeadType || null,
sprinklerGpm: formData.sprinklerGpm ? parseFloat(formData.sprinklerGpm) : null,
sprinklerThrowFeet: formData.sprinklerThrowFeet ? parseFloat(formData.sprinklerThrowFeet) : null,
sprinklerDegrees: formData.sprinklerDegrees ? parseInt(formData.sprinklerDegrees) : null,
sprinklerLengthFeet: formData.sprinklerLengthFeet ? parseFloat(formData.sprinklerLengthFeet) : null,
sprinklerWidthFeet: formData.sprinklerWidthFeet ? parseFloat(formData.sprinklerWidthFeet) : null,
purchaseDate: formData.purchaseDate || null,
purchasePrice: formData.purchasePrice ? parseFloat(formData.purchasePrice) : null,
notes: formData.notes || null,
@@ -782,6 +811,56 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
</div>
</>
);
case 'sprinkler':
return (
<>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Mount *</label>
<select className="input" value={formData.sprinklerMount} onChange={(e)=> setFormData({ ...formData, sprinklerMount: e.target.value })}>
<option value="in_ground">InGround</option>
<option value="above_ground">AboveGround</option>
</select>
</div>
<div>
<label className="label">Head Type *</label>
<select className="input" value={formData.sprinklerHeadType} onChange={(e)=> setFormData({ ...formData, sprinklerHeadType: e.target.value })}>
<option value="rotor_impact">Rotor/Impact</option>
<option value="oscillating_fan">Oscillating/Fan</option>
<option value="spray_fixed">Spray (Fixed)</option>
</select>
</div>
</div>
{(formData.sprinklerHeadType === 'rotor_impact' || formData.sprinklerHeadType === 'spray_fixed') && (
<div className="grid grid-cols-3 gap-4">
<div>
<label className="label">GPM</label>
<input type="number" step="0.1" className="input" value={formData.sprinklerGpm} onChange={(e)=> setFormData({ ...formData, sprinklerGpm: e.target.value })} />
</div>
<div>
<label className="label">Throw (ft)</label>
<input type="number" step="0.1" className="input" value={formData.sprinklerThrowFeet} onChange={(e)=> setFormData({ ...formData, sprinklerThrowFeet: e.target.value })} />
</div>
<div>
<label className="label">Degrees</label>
<input type="number" className="input" value={formData.sprinklerDegrees} onChange={(e)=> setFormData({ ...formData, sprinklerDegrees: e.target.value })} />
</div>
</div>
)}
{formData.sprinklerHeadType === 'oscillating_fan' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Length (ft)</label>
<input type="number" step="0.1" className="input" value={formData.sprinklerLengthFeet} onChange={(e)=> setFormData({ ...formData, sprinklerLengthFeet: e.target.value })} />
</div>
<div>
<label className="label">Width (ft)</label>
<input type="number" step="0.1" className="input" value={formData.sprinklerWidthFeet} onChange={(e)=> setFormData({ ...formData, sprinklerWidthFeet: e.target.value })} />
</div>
</div>
)}
</>
);
case 'pump':
return (

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, useMapEvents } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import { propertiesAPI, wateringAPI } from '../../services/api';
import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api';
import toast from 'react-hot-toast';
const SprinklerPlacement = ({ onPlace }) => {
@@ -43,14 +43,25 @@ const Watering = () => {
const [guideIndex, setGuideIndex] = useState(0);
const [currentPos, setCurrentPos] = useState(null);
const [watchId, setWatchId] = useState(null);
const [sprinklers, setSprinklers] = useState([]);
const [selectedSprinklerId, setSelectedSprinklerId] = useState('');
useEffect(() => { (async () => {
try { const r = await propertiesAPI.getAll(); setProperties(r.data?.data?.properties||[]); }
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); setSections(p.sections||[]); }
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'); }
};
@@ -64,15 +75,26 @@ const Watering = () => {
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: sprinklerForm.mount,
sprinklerHeadType: sprinklerForm.type,
gpm: sprinklerForm.gpm,
throwFeet: sprinklerForm.throwFeet,
degrees: sprinklerForm.degrees,
lengthFeet: sprinklerForm.lengthFeet,
widthFeet: sprinklerForm.widthFeet
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]);
@@ -82,8 +104,11 @@ const Watering = () => {
};
const center = useMemo(() => {
if (selectedProperty?.latitude && selectedProperty?.longitude) return [selectedProperty.latitude, selectedProperty.longitude];
if (sections?.length){ const s=sections[0]; const c=s.polygonData?.coordinates?.[0]?.[0]; if (c) return [c[0], c[1]]; }
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]);
@@ -106,6 +131,48 @@ const Watering = () => {
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]);
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Watering - Sprinklers</h1>
@@ -120,6 +187,26 @@ const Watering = () => {
</div>
<div className="card space-y-2">
<div className="font-medium">Sprinkler Settings</div>
<div className="text-sm">Choose Saved Sprinkler</div>
<select className="input" value={selectedSprinklerId} onChange={(e)=> {
const val=e.target.value; setSelectedSprinklerId(val);
const eq = sprinklers.find(s=> s.id === parseInt(val));
if (eq) {
setSprinklerForm(sf=> ({...sf,
mount: eq.sprinklerMount || sf.mount,
type: eq.sprinklerHeadType || sf.type,
gpm: eq.sprinklerGpm || sf.gpm,
throwFeet: eq.sprinklerThrowFeet || sf.throwFeet,
lengthFeet: eq.sprinklerLengthFeet || sf.lengthFeet,
widthFeet: eq.sprinklerWidthFeet || sf.widthFeet
}));
}
}}>
<option value="">-- none --</option>
{sprinklers.map(s=> (
<option key={s.id} value={s.id}>{s.customName || `${s.manufacturer||''} ${s.model||''}`.trim()}</option>
))}
</select>
<div className="text-sm">Mount</div>
<select className="input" value={sprinklerForm.mount} onChange={e=> setSprinklerForm({...sprinklerForm, mount:e.target.value})}>
<option value="in_ground">InGround</option>
@@ -139,6 +226,8 @@ const Watering = () => {
<input type="number" step="0.1" className="input" value={sprinklerForm.throwFeet} onChange={e=> setSprinklerForm({...sprinklerForm, throwFeet: parseFloat(e.target.value)})} />
<div className="text-sm">Degrees (0360)</div>
<input type="number" className="input" value={sprinklerForm.degrees} onChange={e=> setSprinklerForm({...sprinklerForm, degrees: parseInt(e.target.value||'0',10)})} />
<div className="text-sm">Heading (0359, 0 = East)</div>
<input type="number" className="input" value={sprinklerForm.headingDegrees||0} onChange={e=> setSprinklerForm({...sprinklerForm, headingDegrees: (parseInt(e.target.value||'0',10)||0)%360})} />
</>
) : (
<>
@@ -148,6 +237,10 @@ const Watering = () => {
<input type="number" step="0.1" className="input" value={sprinklerForm.widthFeet} onChange={e=> setSprinklerForm({...sprinklerForm, widthFeet: parseFloat(e.target.value)})} />
</>
)}
<div className="text-sm">Target Depth (inches)</div>
<input type="number" step="0.1" className="input" value={targetInches} onChange={e=> setTargetInches(parseFloat(e.target.value||'0'))} />
<div className="text-xs text-gray-600">Suggested runtime: {suggestMinutes} minutes {sprinklerForm.gpm? '' : '(enter GPM to calculate)'}
</div>
<div className="text-sm">Run Duration (minutes)</div>
<input type="number" className="input" value={sprinklerForm.durationMinutes} onChange={e=> setSprinklerForm({...sprinklerForm, durationMinutes: parseInt(e.target.value||'0',10)})} />
<button className="btn-primary w-full" disabled={!selectedProperty} onClick={()=> setPlacing(true)}>Place Sprinkler on Map</button>
@@ -198,7 +291,11 @@ const Watering = () => {
<React.Fragment key={pt.id}>
<Marker position={[pt.lat, pt.lng]} />
{cov?.kind==='circle' && (
<Circle center={[pt.lat, pt.lng]} radius={cov.radius*0.3048} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
cov.degrees && cov.degrees < 360 ? (
<Polygon positions={sectorPolygon({lat:Number(pt.lat),lng:Number(pt.lng)}, cov.radius, (pt.sprinkler_heading_degrees||0), (pt.sprinkler_heading_degrees||0)+cov.degrees)} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
) : (
<Circle center={[pt.lat, pt.lng]} radius={cov.radius*0.3048} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
)
)}
{cov?.kind==='rect' && (
<Rectangle bounds={[[pt.lat - (cov.width/2)*0.00000274, pt.lng - (cov.length/2)*0.0000036], [pt.lat + (cov.width/2)*0.00000274, pt.lng + (cov.length/2)*0.0000036]]} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />