From 34baedfc46605a494ef5813a23a292a7ad6b7534 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Tue, 2 Sep 2025 08:05:11 -0500 Subject: [PATCH] spreader --- .../Applications/ApplicationViewModal.js | 73 +++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/Applications/ApplicationViewModal.js b/frontend/src/components/Applications/ApplicationViewModal.js index 9c3c997..545fc86 100644 --- a/frontend/src/components/Applications/ApplicationViewModal.js +++ b/frontend/src/components/Applications/ApplicationViewModal.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import * as turf from '@turf/turf'; import { applicationsAPI } from '../../services/api'; import PropertyMap from '../Maps/PropertyMap'; import { XMarkIcon, ClockIcon, MapPinIcon, WrenchScrewdriverIcon, BeakerIcon } from '@heroicons/react/24/outline'; @@ -44,21 +45,67 @@ const ApplicationViewModal = ({ application, propertyDetails, onClose }) => { const calculateCoverage = (application, log) => { if (!log?.gpsTrack?.points || log.gpsTrack.points.length < 2) return 0; - const totalDistanceFeet = distanceFeet(log); - const plannedArea = application.totalSectionArea || 0; - if (totalDistanceFeet === 0 || plannedArea === 0) return 0; + // Determine equipment width (feet) + let widthFeet = 4; + const equipmentFromPlan = planDetails?.equipment || {}; + if (typeof equipmentFromPlan.spreadWidth === 'number' && equipmentFromPlan.spreadWidth > 0) { + widthFeet = equipmentFromPlan.spreadWidth; + } else if (typeof equipmentFromPlan.sprayWidthFeet === 'number' && equipmentFromPlan.sprayWidthFeet > 0) { + widthFeet = equipmentFromPlan.sprayWidthFeet; + } else { + const equipmentName = application.equipmentName?.toLowerCase() || ''; + if (equipmentName.includes('spreader')) widthFeet = 12; + else if (equipmentName.includes('sprayer')) widthFeet = 20; + else if (equipmentName.includes('mower')) widthFeet = 6; + } - // Estimate equipment width based on type (feet) - let equipmentWidth = 4; - const equipmentName = application.equipmentName?.toLowerCase() || ''; - if (equipmentName.includes('spreader')) equipmentWidth = 12; - else if (equipmentName.includes('sprayer')) equipmentWidth = 20; - else if (equipmentName.includes('mower')) equipmentWidth = 6; + const plannedAreaSqFt = application.totalSectionArea || 0; + if (plannedAreaSqFt <= 0) return 0; - // Distance (ft) * Width (ft) = Area (sq ft) - const theoreticalCoverageArea = totalDistanceFeet * equipmentWidth; - const coveragePercentage = Math.min((theoreticalCoverageArea / plannedArea) * 100, 100); - return Math.round(coveragePercentage); + // Build union polygon of all planned sections + try { + const polygons = (sections || []).map((section) => { + let poly = section.polygonData; + if (typeof poly === 'string') { + try { poly = JSON.parse(poly); } catch { poly = null; } + } + if (!poly?.coordinates?.[0]) return null; + // Convert [lat,lng] -> [lng,lat] + const coords = poly.coordinates[0].map(([lat, lng]) => [lng, lat]); + return turf.polygon([coords]); + }).filter(Boolean); + + if (polygons.length === 0) { + // Fallback: distance * width heuristic + const totalDistanceFeet = distanceFeet(log); + if (totalDistanceFeet === 0) return 0; + const areaFt2 = totalDistanceFeet * widthFeet; + return Math.min(Math.round((areaFt2 / plannedAreaSqFt) * 100), 100); + } + + const plannedUnion = polygons.reduce((acc, cur) => acc ? turf.union(acc, cur) : cur, null); + if (!plannedUnion) return 0; + + // Buffer the GPS line by half the width + const lineCoords = log.gpsTrack.points.map((p) => [p.lng, p.lat]); + const line = turf.lineString(lineCoords); + const bufferKm = (widthFeet / 2) * 0.3048 / 1000; // feet -> meters -> km + const swath = turf.buffer(line, bufferKm, { units: 'kilometers' }); + if (!swath) return 0; + + const overlap = turf.intersect(swath, plannedUnion); + if (!overlap) return 0; + + const overlapSqMeters = turf.area(overlap); + const plannedSqMeters = plannedAreaSqFt * 0.092903; + const pct = (overlapSqMeters / plannedSqMeters) * 100; + return Math.min(Math.round(pct), 100); + } catch (e) { + console.warn('Coverage calc fallback due to error:', e); + const totalDistanceFeet = distanceFeet(log); + const areaFt2 = totalDistanceFeet * widthFeet; + return Math.min(Math.round((areaFt2 / plannedAreaSqFt) * 100), 100); + } }; useEffect(() => {