polygons
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
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 { Icon } from 'leaflet';
|
||||||
import * as turf from '@turf/turf';
|
import * as turf from '@turf/turf';
|
||||||
import {
|
import {
|
||||||
@@ -88,6 +88,141 @@ function PolygonDrawer({ isDrawing, onPolygonComplete, currentColor }) {
|
|||||||
) : null;
|
) : 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 PropertyDetail = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -212,6 +347,10 @@ const PropertyDetail = () => {
|
|||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSection = (sectionId, updatedSection) => {
|
||||||
|
setLawnSections(prev => prev.map(s => s.id === sectionId ? updatedSection : s));
|
||||||
|
};
|
||||||
|
|
||||||
const getTotalArea = () => {
|
const getTotalArea = () => {
|
||||||
return lawnSections.reduce((total, section) => total + section.area, 0);
|
return lawnSections.reduce((total, section) => total + section.area, 0);
|
||||||
};
|
};
|
||||||
@@ -290,13 +429,14 @@ const PropertyDetail = () => {
|
|||||||
<div style={{ height: '600px', width: '100%' }}>
|
<div style={{ height: '600px', width: '100%' }}>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={mapCenter}
|
center={mapCenter}
|
||||||
zoom={hasValidCoordinates ? 21 : 13}
|
zoom={hasValidCoordinates ? 18 : 13}
|
||||||
maxZoom={23}
|
maxZoom={19}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
>
|
>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
attribution='© <a href="https://www.esri.com/">Esri</a>'
|
attribution='© <a href="https://www.esri.com/">Esri</a>'
|
||||||
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
||||||
|
maxZoom={19}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasValidCoordinates && (
|
{hasValidCoordinates && (
|
||||||
@@ -306,37 +446,13 @@ const PropertyDetail = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{lawnSections.map((section) => (
|
{lawnSections.map((section) => (
|
||||||
<Polygon
|
<EditablePolygon
|
||||||
key={section.id}
|
key={section.id}
|
||||||
positions={section.coordinates}
|
section={section}
|
||||||
pathOptions={{
|
onUpdate={updateSection}
|
||||||
color: section.color.value,
|
onEdit={startEditSection}
|
||||||
fillColor: section.color.value,
|
onDelete={deleteLawnSection}
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{isDrawing && (
|
{isDrawing && (
|
||||||
@@ -360,6 +476,9 @@ const PropertyDetail = () => {
|
|||||||
<p className="text-xs text-blue-600 mt-1">
|
<p className="text-xs text-blue-600 mt-1">
|
||||||
Need at least 3 points to create a section. Press ESC to cancel.
|
Need at least 3 points to create a section. Press ESC to cancel.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-blue-500 mt-1">
|
||||||
|
💡 After creating: Click any polygon to edit its points by dragging
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user