polygons
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polygon, useMapEvents } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polygon, useMapEvents, useMap } from 'react-leaflet';
|
||||
import { Icon } from 'leaflet';
|
||||
import * as turf from '@turf/turf';
|
||||
import {
|
||||
@@ -88,6 +88,141 @@ function PolygonDrawer({ isDrawing, onPolygonComplete, currentColor }) {
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Component for editable polygons
|
||||
function EditablePolygon({ section, onUpdate, onEdit, onDelete }) {
|
||||
// Import toast function from the module scope
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedCoordinates, setEditedCoordinates] = useState(section.coordinates);
|
||||
const map = useMap();
|
||||
|
||||
const handleMarkerDrag = (index, newLatLng) => {
|
||||
const newCoords = [...editedCoordinates];
|
||||
newCoords[index] = [newLatLng.lat, newLatLng.lng];
|
||||
setEditedCoordinates(newCoords);
|
||||
|
||||
// Recalculate area
|
||||
const area = calculateAreaInSqFt([...newCoords, newCoords[0]]);
|
||||
onUpdate(section.id, { ...section, coordinates: newCoords, area });
|
||||
};
|
||||
|
||||
const addPointAtIndex = (index, e) => {
|
||||
e.originalEvent.stopPropagation();
|
||||
const newCoords = [...editedCoordinates];
|
||||
const newPoint = [e.latlng.lat, e.latlng.lng];
|
||||
newCoords.splice(index + 1, 0, newPoint);
|
||||
setEditedCoordinates(newCoords);
|
||||
|
||||
const area = calculateAreaInSqFt([...newCoords, newCoords[0]]);
|
||||
onUpdate(section.id, { ...section, coordinates: newCoords, area });
|
||||
};
|
||||
|
||||
const removePoint = (index) => {
|
||||
if (editedCoordinates.length <= 3) {
|
||||
toast.error('Polygon must have at least 3 points');
|
||||
return;
|
||||
}
|
||||
const newCoords = editedCoordinates.filter((_, i) => i !== index);
|
||||
setEditedCoordinates(newCoords);
|
||||
|
||||
const area = calculateAreaInSqFt([...newCoords, newCoords[0]]);
|
||||
onUpdate(section.id, { ...section, coordinates: newCoords, area });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Polygon
|
||||
positions={editedCoordinates}
|
||||
pathOptions={{
|
||||
color: section.color.value,
|
||||
fillColor: section.color.value,
|
||||
fillOpacity: isEditing ? 0.3 : 0.4,
|
||||
weight: isEditing ? 3 : 2
|
||||
}}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
toast.info('Edit mode: Drag points to move, right-click to add/remove points');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<div className="text-center">
|
||||
<strong>{section.name}</strong><br />
|
||||
{section.area.toLocaleString()} sq ft<br />
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditing(!isEditing);
|
||||
toast.info(isEditing ? 'Edit mode disabled' : 'Edit mode enabled');
|
||||
}}
|
||||
className={`text-sm ${isEditing ? 'text-green-600' : 'text-blue-600'}`}
|
||||
>
|
||||
{isEditing ? 'Done' : 'Edit Points'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onEdit(section)}
|
||||
className="text-blue-600 text-sm"
|
||||
>
|
||||
Edit Name
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(section.id)}
|
||||
className="text-red-600 text-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Polygon>
|
||||
|
||||
{/* Editable markers for each point */}
|
||||
{isEditing && editedCoordinates.map((coord, index) => (
|
||||
<Marker
|
||||
key={`${section.id}-${index}`}
|
||||
position={coord}
|
||||
draggable={true}
|
||||
eventHandlers={{
|
||||
dragend: (e) => {
|
||||
handleMarkerDrag(index, e.target.getLatLng());
|
||||
},
|
||||
contextmenu: (e) => {
|
||||
e.originalEvent.preventDefault();
|
||||
if (editedCoordinates.length > 3) {
|
||||
removePoint(index);
|
||||
}
|
||||
}
|
||||
}}
|
||||
icon={new Icon({
|
||||
iconUrl: 'data:image/svg+xml;base64,' + btoa(`
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="6" cy="6" r="5" fill="${section.color.value}" stroke="white" stroke-width="2"/>
|
||||
</svg>
|
||||
`),
|
||||
iconSize: [12, 12],
|
||||
iconAnchor: [6, 6]
|
||||
})}
|
||||
>
|
||||
<Popup>
|
||||
<div className="text-center">
|
||||
<p className="text-xs">Point {index + 1}</p>
|
||||
<button
|
||||
onClick={() => removePoint(index)}
|
||||
className="text-red-600 text-xs"
|
||||
disabled={editedCoordinates.length <= 3}
|
||||
>
|
||||
Remove Point
|
||||
</button>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PropertyDetail = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -212,6 +347,10 @@ const PropertyDetail = () => {
|
||||
setShowEditModal(false);
|
||||
};
|
||||
|
||||
const updateSection = (sectionId, updatedSection) => {
|
||||
setLawnSections(prev => prev.map(s => s.id === sectionId ? updatedSection : s));
|
||||
};
|
||||
|
||||
const getTotalArea = () => {
|
||||
return lawnSections.reduce((total, section) => total + section.area, 0);
|
||||
};
|
||||
@@ -290,13 +429,14 @@ const PropertyDetail = () => {
|
||||
<div style={{ height: '600px', width: '100%' }}>
|
||||
<MapContainer
|
||||
center={mapCenter}
|
||||
zoom={hasValidCoordinates ? 21 : 13}
|
||||
maxZoom={23}
|
||||
zoom={hasValidCoordinates ? 18 : 13}
|
||||
maxZoom={19}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.esri.com/">Esri</a>'
|
||||
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
||||
maxZoom={19}
|
||||
/>
|
||||
|
||||
{hasValidCoordinates && (
|
||||
@@ -306,37 +446,13 @@ const PropertyDetail = () => {
|
||||
)}
|
||||
|
||||
{lawnSections.map((section) => (
|
||||
<Polygon
|
||||
<EditablePolygon
|
||||
key={section.id}
|
||||
positions={section.coordinates}
|
||||
pathOptions={{
|
||||
color: section.color.value,
|
||||
fillColor: section.color.value,
|
||||
fillOpacity: 0.4,
|
||||
weight: 2
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<div className="text-center">
|
||||
<strong>{section.name}</strong><br />
|
||||
{section.area.toLocaleString()} sq ft<br />
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={() => startEditSection(section)}
|
||||
className="text-blue-600 text-sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteLawnSection(section.id)}
|
||||
className="text-red-600 text-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Polygon>
|
||||
section={section}
|
||||
onUpdate={updateSection}
|
||||
onEdit={startEditSection}
|
||||
onDelete={deleteLawnSection}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isDrawing && (
|
||||
@@ -360,6 +476,9 @@ const PropertyDetail = () => {
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
Need at least 3 points to create a section. Press ESC to cancel.
|
||||
</p>
|
||||
<p className="text-xs text-blue-500 mt-1">
|
||||
💡 After creating: Click any polygon to edit its points by dragging
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user