import React, { useState, useCallback, useRef } from 'react'; import { MapContainer, TileLayer, Polygon, Marker, Polyline, useMapEvents, useMap } from 'react-leaflet'; import { Icon } from 'leaflet'; import 'leaflet/dist/leaflet.css'; // Fix for default markers in react-leaflet delete Icon.Default.prototype._getIconUrl; Icon.Default.mergeOptions({ iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), iconUrl: require('leaflet/dist/images/marker-icon.png'), shadowUrl: require('leaflet/dist/images/marker-shadow.png'), }); // Custom GPS tracking icons const currentLocationIcon = new Icon({ iconUrl: 'data:image/svg+xml;base64,' + btoa(` `), iconSize: [20, 20], iconAnchor: [10, 10], }); const trackPointIcon = new Icon({ iconUrl: 'data:image/svg+xml;base64,' + btoa(` `), iconSize: [6, 6], iconAnchor: [3, 3], }); // Custom component to handle map clicks for drawing polygons const DrawingHandler = ({ isDrawing, onPointAdd, onDrawingComplete }) => { useMapEvents({ click: (e) => { if (isDrawing) { onPointAdd([e.latlng.lat, e.latlng.lng]); } }, keydown: (e) => { if (e.originalEvent.key === 'Escape' && isDrawing) { onDrawingComplete(); } } }); return null; }; // Keep map centered when props change const MapViewUpdater = ({ center, zoom }) => { const map = useMap(); const prev = useRef({ center: null, zoom: null }); React.useEffect(() => { const [lat, lng] = center || []; const isValid = typeof lat === 'number' && typeof lng === 'number'; const zoomToUse = typeof zoom === 'number' ? zoom : map.getZoom(); const changed = !prev.current.center || prev.current.center[0] !== lat || prev.current.center[1] !== lng || prev.current.zoom !== zoomToUse; if (isValid && changed) { map.setView(center, zoomToUse, { animate: true }); prev.current = { center, zoom: zoomToUse }; } }, [center, zoom, map]); return null; }; // Calculate polygon area in square feet const calculatePolygonArea = (coordinates) => { if (!coordinates || coordinates.length < 3) return 0; // Shoelace formula for polygon area let area = 0; const n = coordinates.length; for (let i = 0; i < n; i++) { const j = (i + 1) % n; area += coordinates[i][0] * coordinates[j][1]; area -= coordinates[j][0] * coordinates[i][1]; } area = Math.abs(area) / 2; // Convert from decimal degrees to square feet (approximate) const avgLat = coordinates.reduce((sum, coord) => sum + coord[0], 0) / n; const meterToFeet = 3.28084; const degToMeter = 111320 * Math.cos(avgLat * Math.PI / 180); return area * Math.pow(degToMeter * meterToFeet, 2); }; const PropertyMap = ({ center = [39.8283, -98.5795], // Center of USA as default zoom = 15, property, sections = [], onSectionCreate, onSectionUpdate, onSectionDelete, onPropertyUpdate, onSectionClick, selectedSections = [], editable = false, className = "h-96 w-full", // GPS tracking props mode = "view", // "view", "edit", "execution" gpsTrack = [], currentLocation = null, showTrackPoints = true }) => { // Debug logging console.log('PropertyMap render:', { center, zoom, sections: sections.length, selectedSections, mode, gpsTrack: gpsTrack.length, currentLocation }); const [isDrawing, setIsDrawing] = useState(false); const [currentPolygon, setCurrentPolygon] = useState([]); const [selectedSection, setSelectedSection] = useState(null); const [showSectionForm, setShowSectionForm] = useState(false); const [sectionName, setSectionName] = useState(''); const [grassType, setGrassType] = useState(''); const [soilType, setSoilType] = useState(''); const mapRef = useRef(null); // Handle adding points to current polygon const handlePointAdd = useCallback((point) => { setCurrentPolygon(prev => [...prev, point]); }, []); // Handle completing polygon drawing const handleDrawingComplete = useCallback(() => { if (currentPolygon.length >= 3) { const area = calculatePolygonArea(currentPolygon); setShowSectionForm(true); } else { setCurrentPolygon([]); } setIsDrawing(false); }, [currentPolygon]); // Start drawing mode const startDrawing = () => { setIsDrawing(true); setCurrentPolygon([]); setSelectedSection(null); }; // Cancel drawing const cancelDrawing = () => { setIsDrawing(false); setCurrentPolygon([]); setShowSectionForm(false); setSectionName(''); setGrassType(''); setSoilType(''); }; // Save new section const saveSection = async () => { if (!sectionName.trim() || currentPolygon.length < 3) return; const area = calculatePolygonArea(currentPolygon); const sectionData = { name: sectionName, area, polygonData: { type: 'Polygon', coordinates: [currentPolygon.map(([lat, lng]) => [lng, lat])] // GeoJSON format }, grassType: grassType || null, soilType: soilType || null }; try { if (onSectionCreate) { await onSectionCreate(sectionData); } // Reset form cancelDrawing(); } catch (error) { console.error('Error creating section:', error); } }; // Handle section click const handleSectionClick = (section) => { if (onSectionClick) { // For application planning mode - use the provided callback onSectionClick(section); } else { // For editing mode - use the internal state setSelectedSection(selectedSection?.id === section.id ? null : section); } }; // Delete selected section const deleteSelectedSection = async () => { if (!selectedSection || !onSectionDelete) return; try { await onSectionDelete(selectedSection.id); setSelectedSection(null); } catch (error) { console.error('Error deleting section:', error); } }; // Get section color based on grass type const getSectionColor = (section) => { const colors = { 'bermuda': '#10b981', 'fescue': '#059669', 'kentucky bluegrass': '#047857', 'zoysia': '#065f46', 'st augustine': '#064e3b', 'centipede': '#6ee7b7', 'default': '#3b82f6' }; return colors[section.grassType?.toLowerCase()] || colors.default; }; return (
{/* Map Controls */} {editable && (
{!isDrawing ? ( ) : (
)}
{selectedSection && (

{selectedSection.name}

{Math.round(selectedSection.area).toLocaleString()} sq ft

{selectedSection.grassType && (

Grass: {selectedSection.grassType}

)} {selectedSection.soilType && (

Soil: {selectedSection.soilType}

)}
)}
)} {/* Section Creation Form Modal */} {showSectionForm && (

New Lawn Section

setSectionName(e.target.value)} className="input" placeholder="e.g., Front Yard, Back Lawn" autoFocus />

Area: {Math.round(calculatePolygonArea(currentPolygon)).toLocaleString()} sq ft

{currentPolygon.length} points drawn

)} {/* Map */} {/* Use OpenStreetMap as fallback for better reliability */} {/* Sync map view with props */} {/* Drawing handler */} {/* Property marker */} {property && property.latitude && property.longitude && ( )} {/* Existing sections */} {sections.map((section) => { // Handle both string and object polygon data let polygonData = section.polygonData; if (typeof polygonData === 'string') { try { polygonData = JSON.parse(polygonData); } catch (e) { console.error('Failed to parse polygon data:', e); return null; } } if (!polygonData?.coordinates?.[0]) { return null; } // Coordinates are already in [lat, lng] format - no swapping needed! const coordinates = polygonData.coordinates[0]; const isInternallySelected = selectedSection?.id === section.id; const isExternallySelected = selectedSections.includes(section.id); const isSelected = isInternallySelected || isExternallySelected; return ( handleSectionClick(section) }} /> ); })} {/* GPS Tracking Elements */} {(mode === "execution" || mode === "view") && ( <> {/* GPS Track Polyline */} {gpsTrack.length > 1 && ( [point.lat, point.lng])} pathOptions={{ color: '#EF4444', weight: 4, opacity: 0.8, }} /> )} {/* Track Points (breadcrumbs) */} {showTrackPoints && gpsTrack.map((point, index) => ( index % 10 === 0 && ( // Show every 10th point to avoid clutter ) ))} {/* Current Location - only for execution mode */} {currentLocation && mode === "execution" && ( )} )} {/* Current polygon being drawn */} {currentPolygon.length > 0 && ( <> {/* Show markers for each point */} {currentPolygon.map((point, index) => ( )} )} {/* Drawing instructions */} {isDrawing && (

Drawing Mode:

Click to add points. Press Escape or click "Finish" when done.

Need at least 3 points to create a section.

)} {/* GPS Tracking stats */} {mode === "execution" && gpsTrack.length > 0 && (

GPS Tracking Active

Track Points: {gpsTrack.length}

{currentLocation && (

Accuracy: ±{Math.round(currentLocation.accuracy || 0)}m

)}
)} {/* Section stats */} {sections.length > 0 && !isDrawing && mode !== "execution" && (

{sections.length} Section{sections.length !== 1 ? 's' : ''}

Total: {sections.reduce((sum, section) => sum + (section.area || 0), 0).toLocaleString()} sq ft

)}
); }; export default PropertyMap;