add middle points
This commit is contained in:
@@ -1,50 +1,135 @@
|
||||
import { Box, Button, TextField } from "@mui/material";
|
||||
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => {
|
||||
const ROW_COLORS = {
|
||||
source: "#0EA37A", // سبز
|
||||
waypoint: "#F5A623", // زرد/نارنجی
|
||||
dest: "#E53935", // قرمز
|
||||
};
|
||||
|
||||
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
|
||||
|
||||
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 12,
|
||||
color: value ? "#1f1f1f" : "#bdbdbd",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{value || placeholder}
|
||||
</Typography>
|
||||
</Box>
|
||||
{onEdit && (
|
||||
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
|
||||
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{onRemove && (
|
||||
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
|
||||
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const DataBox = ({
|
||||
isSource,
|
||||
isDest,
|
||||
sourcePosition,
|
||||
destPosition,
|
||||
editingTarget,
|
||||
onEditSource,
|
||||
onEditDest,
|
||||
waypoints = [],
|
||||
onEditWaypoint,
|
||||
onAddWaypoint,
|
||||
onRemoveWaypoint,
|
||||
canAddWaypoint,
|
||||
}) => {
|
||||
if (!isSource) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
zIndex: 1000,
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e1e1e1",
|
||||
padding: 1,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
backdropFilter: "blur(6px)",
|
||||
border: "1px solid #ececec",
|
||||
borderRadius: 2,
|
||||
opacity: 0.8,
|
||||
gap: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
width: 230,
|
||||
maxWidth: "70vw",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
readOnly
|
||||
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
|
||||
<Row
|
||||
color={ROW_COLORS.source}
|
||||
label="مبدا"
|
||||
value={formatCoord(sourcePosition)}
|
||||
onEdit={isSource && isDest ? onEditSource : null}
|
||||
/>
|
||||
|
||||
{waypoints.map((wp, index) => (
|
||||
<Row
|
||||
key={`waypoint-${index}`}
|
||||
color={ROW_COLORS.waypoint}
|
||||
label={`نقطه میانی ${index + 1}`}
|
||||
value={formatCoord(wp.position)}
|
||||
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
|
||||
onRemove={() => onRemoveWaypoint(index)}
|
||||
/>
|
||||
{isSource && isDest && (
|
||||
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
|
||||
ویرایش مبدا
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isDest && (
|
||||
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
readOnly
|
||||
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
|
||||
/>
|
||||
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
|
||||
ویرایش مقصد
|
||||
</Button>
|
||||
</div>
|
||||
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
|
||||
)}
|
||||
|
||||
{canAddWaypoint && (
|
||||
<>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
onClick={onAddWaypoint}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: ROW_COLORS.waypoint,
|
||||
py: 0.25,
|
||||
"&:hover": { opacity: 0.75 },
|
||||
}}
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 16 }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||
افزودن نقطه میانی
|
||||
</Typography>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -55,3 +55,78 @@ const ShowRoute = ({ area }) => {
|
||||
};
|
||||
|
||||
export default ShowRoute;
|
||||
|
||||
|
||||
|
||||
|
||||
// import { useEffect, useState } from "react";
|
||||
// import { Marker, Polyline, useMap } from "react-leaflet";
|
||||
// import L from "leaflet";
|
||||
// import polyline from "@mapbox/polyline";
|
||||
// import SourceIcon from "@/assets/images/source-icon.svg";
|
||||
// import DestIcon from "@/assets/images/destination-icon.svg";
|
||||
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||
//
|
||||
// const defaultIconSize = [35, 35];
|
||||
//
|
||||
// const createCustomIcon = (size, iconUrl) => {
|
||||
// return L.icon({
|
||||
// iconUrl: iconUrl,
|
||||
// iconSize: size,
|
||||
// iconAnchor: [size[0] / 2, size[1]],
|
||||
// popupAnchor: [0, -size[1]],
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
|
||||
//
|
||||
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
|
||||
// const ShowRoute = ({ area, locations = [] }) => {
|
||||
// const map = useMap();
|
||||
// const [showRoute, setShowRoute] = useState(false);
|
||||
//
|
||||
// const coords = area ? polyline.decode(area, 6) : [];
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (!coords.length) return;
|
||||
//
|
||||
// setShowRoute(false);
|
||||
//
|
||||
// map.flyToBounds(coords, {
|
||||
// padding: [50, 50],
|
||||
// duration: 1.5,
|
||||
// });
|
||||
//
|
||||
// map.once("moveend", () => {
|
||||
// setShowRoute(true);
|
||||
// });
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [area]);
|
||||
//
|
||||
// if (!area || coords.length === 0) return null;
|
||||
//
|
||||
// const sourcePosition = coords[0];
|
||||
// const destPosition = coords[coords.length - 1];
|
||||
//
|
||||
// // نقاط میانی فقط از روی locations قابل استخراجان (بین اولی و آخری)
|
||||
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
|
||||
//
|
||||
// return (
|
||||
// <>
|
||||
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||
//
|
||||
// <Marker position={sourcePosition} icon={sourceIcon} />
|
||||
//
|
||||
// {middleWaypoints.map((wp, index) => (
|
||||
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
|
||||
// ))}
|
||||
//
|
||||
// <Marker position={destPosition} icon={destIcon} />
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// export default ShowRoute;
|
||||
|
||||
|
||||
@@ -5,19 +5,29 @@ import SourceIcon from "@/assets/images/source-icon.svg";
|
||||
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
|
||||
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
|
||||
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
|
||||
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import DataBox from "./DataBox";
|
||||
import AlarmText from "./AlarmText";
|
||||
|
||||
const MAX_WAYPOINTS = 1;
|
||||
|
||||
const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
const mapPrevSourceMarker = useRef();
|
||||
const mapPrevDestMarker = useRef();
|
||||
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی بهازای هر waypoint
|
||||
const mapRef = useRef(null);
|
||||
const defaultIconSize = [35, 35];
|
||||
|
||||
const [isSource, setIsSource] = useState(false);
|
||||
const [sourcePosition, setSourcePosition] = useState(null);
|
||||
|
||||
const [isDest, setIsDest] = useState(false);
|
||||
const [destPosition, setDestPosition] = useState(null);
|
||||
|
||||
const [waypoints, setWaypoints] = useState([]);
|
||||
|
||||
const [routeCoords, setRouteCoords] = useState([]);
|
||||
const [alternateCoords, setAlternateCoords] = useState([]);
|
||||
const [editingTarget, setEditingTarget] = useState(null);
|
||||
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
|
||||
);
|
||||
|
||||
const isMovingSource = !isSource || editingTarget === "source";
|
||||
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
|
||||
const getWaypointIcon = (index) => {
|
||||
const wp = waypoints[index];
|
||||
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
|
||||
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
|
||||
};
|
||||
|
||||
const getActiveTarget = () => {
|
||||
if (editingTarget) return editingTarget;
|
||||
if (!isSource) return "source";
|
||||
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
|
||||
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
|
||||
if (!isDest) return "dest";
|
||||
return null;
|
||||
};
|
||||
|
||||
const map = useMapEvents({
|
||||
move(e) {
|
||||
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
|
||||
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
|
||||
const target = getActiveTarget();
|
||||
const center = e.target.getCenter();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setLatLng(center);
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setLatLng(center);
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
|
||||
}
|
||||
},
|
||||
movestart() {
|
||||
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||
if (isMovingDest && mapPrevDestMarker.current)
|
||||
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||
const target = getActiveTarget();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
|
||||
}
|
||||
},
|
||||
moveend() {
|
||||
if (isMovingSource)
|
||||
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||
if (isMovingDest && mapPrevDestMarker.current)
|
||||
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||
const target = getActiveTarget();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (locations.length < 2) return;
|
||||
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
|
||||
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
|
||||
setSourcePosition(restoredSource);
|
||||
setDestPosition(restoredDest);
|
||||
const first = { lat: locations[0].lat, lng: locations[0].lon };
|
||||
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
|
||||
const middle = locations.slice(1, -1).map((loc) => ({
|
||||
position: { lat: loc.lat, lng: loc.lon },
|
||||
confirmed: true,
|
||||
}));
|
||||
|
||||
setSourcePosition(first);
|
||||
setDestPosition(last);
|
||||
setWaypoints(middle);
|
||||
setIsSource(true);
|
||||
setIsDest(true);
|
||||
|
||||
if (area) {
|
||||
const decoded = polyline.decode(area, 6);
|
||||
setRouteCoords(decoded);
|
||||
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||
}, 100);
|
||||
} else {
|
||||
fetchRoute(restoredSource, restoredDest);
|
||||
fetchRoute(first, last, middle);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchRoute = async (source, dest) => {
|
||||
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
|
||||
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
|
||||
const locs = [
|
||||
{ lat: source.lat, lon: source.lng },
|
||||
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
|
||||
{ lat: dest.lat, lon: dest.lng },
|
||||
];
|
||||
try {
|
||||
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetRoute = () => {
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
};
|
||||
|
||||
const handleMarkerClick = async () => {
|
||||
if (!isSource || editingTarget === "source") {
|
||||
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
|
||||
setSourcePosition(clickedPosition);
|
||||
setIsSource(true);
|
||||
const wasEditing = editingTarget === "source";
|
||||
setEditingTarget(null);
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
|
||||
if (editingTarget === "source" && destPosition) {
|
||||
await fetchRoute(clickedPosition, destPosition);
|
||||
if (wasEditing && destPosition) {
|
||||
await fetchRoute(clickedPosition, destPosition, waypoints);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
setDestPosition(clickedPosition);
|
||||
setIsDest(true);
|
||||
setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
await fetchRoute(sourcePosition, clickedPosition);
|
||||
await fetchRoute(sourcePosition, clickedPosition, waypoints);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWaypointMarkerClick = async (index) => {
|
||||
const wp = waypoints[index];
|
||||
if (!wp || wp.confirmed) return; // فقط روی نقطهی تأییدنشده کلیک معتبره (یا حالت ادیت)
|
||||
|
||||
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
|
||||
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
|
||||
setWaypoints(updated);
|
||||
const wasEditing = editingTarget === `waypoint-${index}`;
|
||||
setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
if (sourcePosition && destPosition) {
|
||||
await fetchRoute(sourcePosition, destPosition, updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSource = () => {
|
||||
setEditingTarget("source");
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
const handleEditDest = () => {
|
||||
setEditingTarget("dest");
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
const handleEditWaypoint = (index) => {
|
||||
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed میکنیم تا با حرکت نقشه جابهجا بشه
|
||||
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
|
||||
setEditingTarget(`waypoint-${index}`);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
|
||||
const handleAddWaypoint = () => {
|
||||
if (waypoints.length >= MAX_WAYPOINTS) return;
|
||||
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
|
||||
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
|
||||
setEditingTarget(`waypoint-${waypoints.length}`);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
// حذف یک نقطه میانی
|
||||
const handleRemoveWaypoint = async (index) => {
|
||||
const updated = waypoints.filter((_, i) => i !== index);
|
||||
setWaypoints(updated);
|
||||
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
|
||||
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
if (sourcePosition && destPosition) {
|
||||
await fetchRoute(sourcePosition, destPosition, updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRoute = (index, coords) => {
|
||||
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
editingTarget={editingTarget}
|
||||
onEditSource={handleEditSource}
|
||||
onEditDest={handleEditDest}
|
||||
waypoints={waypoints}
|
||||
onEditWaypoint={handleEditWaypoint}
|
||||
onAddWaypoint={handleAddWaypoint}
|
||||
onRemoveWaypoint={handleRemoveWaypoint}
|
||||
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
|
||||
/>
|
||||
<AlarmText isSource={isSource} />
|
||||
|
||||
<Marker
|
||||
position={sourcePosition || map.getCenter()}
|
||||
ref={mapPrevSourceMarker}
|
||||
eventHandlers={{ click: handleMarkerClick }}
|
||||
icon={defaultIcon}
|
||||
/>
|
||||
|
||||
{waypoints.map((wp, index) => (
|
||||
<Marker
|
||||
key={`waypoint-${index}`}
|
||||
position={wp.position || map.getCenter()}
|
||||
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
|
||||
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
|
||||
icon={getWaypointIcon(index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isSource && (
|
||||
<Marker
|
||||
position={destPosition || map.getCenter()}
|
||||
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
icon={prevDestIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{alternateCoords.map((coords, index) => (
|
||||
<Polyline
|
||||
key={`alternate-${index}-${selectedRoute?.index === index}`}
|
||||
|
||||
@@ -1,50 +1,135 @@
|
||||
import { Box, Button, TextField } from "@mui/material";
|
||||
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => {
|
||||
const ROW_COLORS = {
|
||||
source: "#0EA37A", // سبز
|
||||
waypoint: "#F5A623", // زرد/نارنجی
|
||||
dest: "#E53935", // قرمز
|
||||
};
|
||||
|
||||
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
|
||||
|
||||
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 12,
|
||||
color: value ? "#1f1f1f" : "#bdbdbd",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{value || placeholder}
|
||||
</Typography>
|
||||
</Box>
|
||||
{onEdit && (
|
||||
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
|
||||
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{onRemove && (
|
||||
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
|
||||
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const DataBox = ({
|
||||
isSource,
|
||||
isDest,
|
||||
sourcePosition,
|
||||
destPosition,
|
||||
editingTarget,
|
||||
onEditSource,
|
||||
onEditDest,
|
||||
waypoints = [],
|
||||
onEditWaypoint,
|
||||
onAddWaypoint,
|
||||
onRemoveWaypoint,
|
||||
canAddWaypoint,
|
||||
}) => {
|
||||
if (!isSource) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
zIndex: 1000,
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e1e1e1",
|
||||
padding: 1,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
backdropFilter: "blur(6px)",
|
||||
border: "1px solid #ececec",
|
||||
borderRadius: 2,
|
||||
opacity: 0.8,
|
||||
gap: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
width: 230,
|
||||
maxWidth: "70vw",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
readOnly
|
||||
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
|
||||
<Row
|
||||
color={ROW_COLORS.source}
|
||||
label="مبدا"
|
||||
value={formatCoord(sourcePosition)}
|
||||
onEdit={isSource && isDest ? onEditSource : null}
|
||||
/>
|
||||
|
||||
{waypoints.map((wp, index) => (
|
||||
<Row
|
||||
key={`waypoint-${index}`}
|
||||
color={ROW_COLORS.waypoint}
|
||||
label={`نقطه میانی ${index + 1}`}
|
||||
value={formatCoord(wp.position)}
|
||||
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
|
||||
onRemove={() => onRemoveWaypoint(index)}
|
||||
/>
|
||||
{isSource && isDest && (
|
||||
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
|
||||
ویرایش مبدا
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isDest && (
|
||||
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
readOnly
|
||||
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
|
||||
/>
|
||||
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
|
||||
ویرایش مقصد
|
||||
</Button>
|
||||
</div>
|
||||
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
|
||||
)}
|
||||
|
||||
{canAddWaypoint && (
|
||||
<>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
onClick={onAddWaypoint}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: ROW_COLORS.waypoint,
|
||||
py: 0.25,
|
||||
"&:hover": { opacity: 0.75 },
|
||||
}}
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 16 }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||
افزودن نقطه میانی
|
||||
</Typography>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||
import L from "leaflet";
|
||||
import polyline from "@mapbox/polyline";
|
||||
@@ -20,12 +20,22 @@ const ShowRoute = ({ area }) => {
|
||||
|
||||
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||
useEffect(() => {
|
||||
if (coords.length > 0) {
|
||||
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
|
||||
}
|
||||
}, [area]);
|
||||
const [showRoute, setShowRoute] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coords.length) return;
|
||||
|
||||
setShowRoute(false);
|
||||
|
||||
map.flyToBounds(coords, {
|
||||
padding: [50, 50],
|
||||
duration: 1.5,
|
||||
});
|
||||
|
||||
map.once("moveend", () => {
|
||||
setShowRoute(true);
|
||||
});
|
||||
}, [area]);
|
||||
if (!area) return null;
|
||||
const coords = polyline.decode(area, 6);
|
||||
|
||||
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Polyline positions={coords} color="#015688" weight={7} opacity={1} />
|
||||
{showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||
|
||||
<Marker position={sourcePosition} icon={sourceIcon} />
|
||||
<Marker position={destPosition} icon={destIcon} />
|
||||
</>
|
||||
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
|
||||
};
|
||||
|
||||
export default ShowRoute;
|
||||
|
||||
// import { useEffect, useState } from "react";
|
||||
// import { Marker, Polyline, useMap } from "react-leaflet";
|
||||
// import L from "leaflet";
|
||||
// import polyline from "@mapbox/polyline";
|
||||
// import SourceIcon from "@/assets/images/source-icon.svg";
|
||||
// import DestIcon from "@/assets/images/destination-icon.svg";
|
||||
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||
//
|
||||
// const defaultIconSize = [35, 35];
|
||||
//
|
||||
// const createCustomIcon = (size, iconUrl) => {
|
||||
// return L.icon({
|
||||
// iconUrl: iconUrl,
|
||||
// iconSize: size,
|
||||
// iconAnchor: [size[0] / 2, size[1]],
|
||||
// popupAnchor: [0, -size[1]],
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
|
||||
//
|
||||
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
|
||||
// const ShowRoute = ({ area, locations = [] }) => {
|
||||
// const map = useMap();
|
||||
// const [showRoute, setShowRoute] = useState(false);
|
||||
//
|
||||
// const coords = area ? polyline.decode(area, 6) : [];
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (!coords.length) return;
|
||||
//
|
||||
// setShowRoute(false);
|
||||
//
|
||||
// map.flyToBounds(coords, {
|
||||
// padding: [50, 50],
|
||||
// duration: 1.5,
|
||||
// });
|
||||
//
|
||||
// map.once("moveend", () => {
|
||||
// setShowRoute(true);
|
||||
// });
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [area]);
|
||||
//
|
||||
// if (!area || coords.length === 0) return null;
|
||||
//
|
||||
// const sourcePosition = coords[0];
|
||||
// const destPosition = coords[coords.length - 1];
|
||||
//
|
||||
// // نقاط میانی فقط از روی locations قابل استخراجان (بین اولی و آخری)
|
||||
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
|
||||
//
|
||||
// return (
|
||||
// <>
|
||||
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||
//
|
||||
// <Marker position={sourcePosition} icon={sourceIcon} />
|
||||
//
|
||||
// {middleWaypoints.map((wp, index) => (
|
||||
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
|
||||
// ))}
|
||||
//
|
||||
// <Marker position={destPosition} icon={destIcon} />
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// export default ShowRoute;
|
||||
|
||||
@@ -5,19 +5,29 @@ import SourceIcon from "@/assets/images/source-icon.svg";
|
||||
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
|
||||
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
|
||||
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
|
||||
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import DataBox from "./DataBox";
|
||||
import AlarmText from "./AlarmText";
|
||||
|
||||
const MAX_WAYPOINTS = 1;
|
||||
|
||||
const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
const mapPrevSourceMarker = useRef();
|
||||
const mapPrevDestMarker = useRef();
|
||||
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی بهازای هر waypoint
|
||||
const mapRef = useRef(null);
|
||||
const defaultIconSize = [35, 35];
|
||||
|
||||
const [isSource, setIsSource] = useState(false);
|
||||
const [sourcePosition, setSourcePosition] = useState(null);
|
||||
|
||||
const [isDest, setIsDest] = useState(false);
|
||||
const [destPosition, setDestPosition] = useState(null);
|
||||
|
||||
const [waypoints, setWaypoints] = useState([]);
|
||||
|
||||
const [routeCoords, setRouteCoords] = useState([]);
|
||||
const [alternateCoords, setAlternateCoords] = useState([]);
|
||||
const [editingTarget, setEditingTarget] = useState(null);
|
||||
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
|
||||
);
|
||||
|
||||
const isMovingSource = !isSource || editingTarget === "source";
|
||||
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
|
||||
const getWaypointIcon = (index) => {
|
||||
const wp = waypoints[index];
|
||||
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
|
||||
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
|
||||
};
|
||||
|
||||
const getActiveTarget = () => {
|
||||
if (editingTarget) return editingTarget;
|
||||
if (!isSource) return "source";
|
||||
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
|
||||
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
|
||||
if (!isDest) return "dest";
|
||||
return null;
|
||||
};
|
||||
|
||||
const map = useMapEvents({
|
||||
move(e) {
|
||||
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
|
||||
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
|
||||
const target = getActiveTarget();
|
||||
const center = e.target.getCenter();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setLatLng(center);
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setLatLng(center);
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
|
||||
}
|
||||
},
|
||||
movestart() {
|
||||
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||
if (isMovingDest && mapPrevDestMarker.current)
|
||||
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||
const target = getActiveTarget();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
|
||||
}
|
||||
},
|
||||
moveend() {
|
||||
if (isMovingSource)
|
||||
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||
if (isMovingDest && mapPrevDestMarker.current)
|
||||
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||
const target = getActiveTarget();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (locations.length < 2) return;
|
||||
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
|
||||
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
|
||||
setSourcePosition(restoredSource);
|
||||
setDestPosition(restoredDest);
|
||||
const first = { lat: locations[0].lat, lng: locations[0].lon };
|
||||
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
|
||||
const middle = locations.slice(1, -1).map((loc) => ({
|
||||
position: { lat: loc.lat, lng: loc.lon },
|
||||
confirmed: true,
|
||||
}));
|
||||
|
||||
setSourcePosition(first);
|
||||
setDestPosition(last);
|
||||
setWaypoints(middle);
|
||||
setIsSource(true);
|
||||
setIsDest(true);
|
||||
|
||||
if (area) {
|
||||
const decoded = polyline.decode(area, 6);
|
||||
setRouteCoords(decoded);
|
||||
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||
}, 100);
|
||||
} else {
|
||||
fetchRoute(restoredSource, restoredDest);
|
||||
fetchRoute(first, last, middle);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchRoute = async (source, dest) => {
|
||||
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
|
||||
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
|
||||
const locs = [
|
||||
{ lat: source.lat, lon: source.lng },
|
||||
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
|
||||
{ lat: dest.lat, lon: dest.lng },
|
||||
];
|
||||
try {
|
||||
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetRoute = () => {
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
};
|
||||
|
||||
const handleMarkerClick = async () => {
|
||||
if (!isSource || editingTarget === "source") {
|
||||
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
|
||||
setSourcePosition(clickedPosition);
|
||||
setIsSource(true);
|
||||
const wasEditing = editingTarget === "source";
|
||||
setEditingTarget(null);
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
|
||||
if (editingTarget === "source" && destPosition) {
|
||||
await fetchRoute(clickedPosition, destPosition);
|
||||
if (wasEditing && destPosition) {
|
||||
await fetchRoute(clickedPosition, destPosition, waypoints);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
setDestPosition(clickedPosition);
|
||||
setIsDest(true);
|
||||
setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
await fetchRoute(sourcePosition, clickedPosition);
|
||||
await fetchRoute(sourcePosition, clickedPosition, waypoints);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWaypointMarkerClick = async (index) => {
|
||||
const wp = waypoints[index];
|
||||
if (!wp || wp.confirmed) return; // فقط روی نقطهی تأییدنشده کلیک معتبره (یا حالت ادیت)
|
||||
|
||||
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
|
||||
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
|
||||
setWaypoints(updated);
|
||||
const wasEditing = editingTarget === `waypoint-${index}`;
|
||||
setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
if (sourcePosition && destPosition) {
|
||||
await fetchRoute(sourcePosition, destPosition, updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSource = () => {
|
||||
setEditingTarget("source");
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
const handleEditDest = () => {
|
||||
setEditingTarget("dest");
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
const handleEditWaypoint = (index) => {
|
||||
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed میکنیم تا با حرکت نقشه جابهجا بشه
|
||||
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
|
||||
setEditingTarget(`waypoint-${index}`);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
|
||||
const handleAddWaypoint = () => {
|
||||
if (waypoints.length >= MAX_WAYPOINTS) return;
|
||||
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
|
||||
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
|
||||
setEditingTarget(`waypoint-${waypoints.length}`);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
// حذف یک نقطه میانی
|
||||
const handleRemoveWaypoint = async (index) => {
|
||||
const updated = waypoints.filter((_, i) => i !== index);
|
||||
setWaypoints(updated);
|
||||
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
|
||||
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
if (sourcePosition && destPosition) {
|
||||
await fetchRoute(sourcePosition, destPosition, updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRoute = (index, coords) => {
|
||||
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
editingTarget={editingTarget}
|
||||
onEditSource={handleEditSource}
|
||||
onEditDest={handleEditDest}
|
||||
waypoints={waypoints}
|
||||
onEditWaypoint={handleEditWaypoint}
|
||||
onAddWaypoint={handleAddWaypoint}
|
||||
onRemoveWaypoint={handleRemoveWaypoint}
|
||||
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
|
||||
/>
|
||||
<AlarmText isSource={isSource} />
|
||||
|
||||
<Marker
|
||||
position={sourcePosition || map.getCenter()}
|
||||
ref={mapPrevSourceMarker}
|
||||
eventHandlers={{ click: handleMarkerClick }}
|
||||
icon={defaultIcon}
|
||||
/>
|
||||
|
||||
{waypoints.map((wp, index) => (
|
||||
<Marker
|
||||
key={`waypoint-${index}`}
|
||||
position={wp.position || map.getCenter()}
|
||||
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
|
||||
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
|
||||
icon={getWaypointIcon(index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isSource && (
|
||||
<Marker
|
||||
position={destPosition || map.getCenter()}
|
||||
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
icon={prevDestIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{alternateCoords.map((coords, index) => (
|
||||
<Polyline
|
||||
key={`alternate-${index}-${selectedRoute?.index === index}`}
|
||||
|
||||
@@ -1,50 +1,135 @@
|
||||
import { Box, Button, TextField } from "@mui/material";
|
||||
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => {
|
||||
const ROW_COLORS = {
|
||||
source: "#0EA37A", // سبز
|
||||
waypoint: "#F5A623", // زرد/نارنجی
|
||||
dest: "#E53935", // قرمز
|
||||
};
|
||||
|
||||
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
|
||||
|
||||
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 12,
|
||||
color: value ? "#1f1f1f" : "#bdbdbd",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{value || placeholder}
|
||||
</Typography>
|
||||
</Box>
|
||||
{onEdit && (
|
||||
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
|
||||
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{onRemove && (
|
||||
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
|
||||
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const DataBox = ({
|
||||
isSource,
|
||||
isDest,
|
||||
sourcePosition,
|
||||
destPosition,
|
||||
editingTarget,
|
||||
onEditSource,
|
||||
onEditDest,
|
||||
waypoints = [],
|
||||
onEditWaypoint,
|
||||
onAddWaypoint,
|
||||
onRemoveWaypoint,
|
||||
canAddWaypoint,
|
||||
}) => {
|
||||
if (!isSource) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
zIndex: 1000,
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #e1e1e1",
|
||||
padding: 1,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
backdropFilter: "blur(6px)",
|
||||
border: "1px solid #ececec",
|
||||
borderRadius: 2,
|
||||
opacity: 0.8,
|
||||
gap: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
width: 230,
|
||||
maxWidth: "70vw",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
readOnly
|
||||
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
|
||||
<Row
|
||||
color={ROW_COLORS.source}
|
||||
label="مبدا"
|
||||
value={formatCoord(sourcePosition)}
|
||||
onEdit={isSource && isDest ? onEditSource : null}
|
||||
/>
|
||||
|
||||
{waypoints.map((wp, index) => (
|
||||
<Row
|
||||
key={`waypoint-${index}`}
|
||||
color={ROW_COLORS.waypoint}
|
||||
label={`نقطه میانی ${index + 1}`}
|
||||
value={formatCoord(wp.position)}
|
||||
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
|
||||
onRemove={() => onRemoveWaypoint(index)}
|
||||
/>
|
||||
{isSource && isDest && (
|
||||
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
|
||||
ویرایش مبدا
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isDest && (
|
||||
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
readOnly
|
||||
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
|
||||
/>
|
||||
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
|
||||
ویرایش مقصد
|
||||
</Button>
|
||||
</div>
|
||||
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
|
||||
)}
|
||||
|
||||
{canAddWaypoint && (
|
||||
<>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
onClick={onAddWaypoint}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: ROW_COLORS.waypoint,
|
||||
py: 0.25,
|
||||
"&:hover": { opacity: 0.75 },
|
||||
}}
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 16 }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||
افزودن نقطه میانی
|
||||
</Typography>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||
import L from "leaflet";
|
||||
import polyline from "@mapbox/polyline";
|
||||
@@ -18,14 +18,24 @@ const ShowRoute = ({ area }) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (coords.length > 0) {
|
||||
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
|
||||
}
|
||||
}, [area]);
|
||||
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||
const [showRoute, setShowRoute] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coords.length) return;
|
||||
|
||||
setShowRoute(false);
|
||||
|
||||
map.flyToBounds(coords, {
|
||||
padding: [50, 50],
|
||||
duration: 1.5,
|
||||
});
|
||||
|
||||
map.once("moveend", () => {
|
||||
setShowRoute(true);
|
||||
});
|
||||
}, [area]);
|
||||
if (!area) return null;
|
||||
const coords = polyline.decode(area, 6);
|
||||
|
||||
@@ -36,7 +46,8 @@ const ShowRoute = ({ area }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Polyline positions={coords} color="#015688" weight={7} opacity={1} />
|
||||
{showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||
|
||||
<Marker position={sourcePosition} icon={sourceIcon} />
|
||||
<Marker position={destPosition} icon={destIcon} />
|
||||
</>
|
||||
@@ -44,3 +55,74 @@ const ShowRoute = ({ area }) => {
|
||||
};
|
||||
|
||||
export default ShowRoute;
|
||||
|
||||
// import { useEffect, useState } from "react";
|
||||
// import { Marker, Polyline, useMap } from "react-leaflet";
|
||||
// import L from "leaflet";
|
||||
// import polyline from "@mapbox/polyline";
|
||||
// import SourceIcon from "@/assets/images/source-icon.svg";
|
||||
// import DestIcon from "@/assets/images/destination-icon.svg";
|
||||
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||
//
|
||||
// const defaultIconSize = [35, 35];
|
||||
//
|
||||
// const createCustomIcon = (size, iconUrl) => {
|
||||
// return L.icon({
|
||||
// iconUrl: iconUrl,
|
||||
// iconSize: size,
|
||||
// iconAnchor: [size[0] / 2, size[1]],
|
||||
// popupAnchor: [0, -size[1]],
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
|
||||
//
|
||||
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
|
||||
// const ShowRoute = ({ area, locations = [] }) => {
|
||||
// const map = useMap();
|
||||
// const [showRoute, setShowRoute] = useState(false);
|
||||
//
|
||||
// const coords = area ? polyline.decode(area, 6) : [];
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (!coords.length) return;
|
||||
//
|
||||
// setShowRoute(false);
|
||||
//
|
||||
// map.flyToBounds(coords, {
|
||||
// padding: [50, 50],
|
||||
// duration: 1.5,
|
||||
// });
|
||||
//
|
||||
// map.once("moveend", () => {
|
||||
// setShowRoute(true);
|
||||
// });
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [area]);
|
||||
//
|
||||
// if (!area || coords.length === 0) return null;
|
||||
//
|
||||
// const sourcePosition = coords[0];
|
||||
// const destPosition = coords[coords.length - 1];
|
||||
//
|
||||
// // نقاط میانی فقط از روی locations قابل استخراجان (بین اولی و آخری)
|
||||
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
|
||||
//
|
||||
// return (
|
||||
// <>
|
||||
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||
//
|
||||
// <Marker position={sourcePosition} icon={sourceIcon} />
|
||||
//
|
||||
// {middleWaypoints.map((wp, index) => (
|
||||
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
|
||||
// ))}
|
||||
//
|
||||
// <Marker position={destPosition} icon={destIcon} />
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// export default ShowRoute;
|
||||
|
||||
@@ -5,19 +5,29 @@ import SourceIcon from "@/assets/images/source-icon.svg";
|
||||
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
|
||||
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
|
||||
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
|
||||
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import DataBox from "./DataBox";
|
||||
import AlarmText from "./AlarmText";
|
||||
|
||||
const MAX_WAYPOINTS = 1;
|
||||
|
||||
const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
const mapPrevSourceMarker = useRef();
|
||||
const mapPrevDestMarker = useRef();
|
||||
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی بهازای هر waypoint
|
||||
const mapRef = useRef(null);
|
||||
const defaultIconSize = [35, 35];
|
||||
|
||||
const [isSource, setIsSource] = useState(false);
|
||||
const [sourcePosition, setSourcePosition] = useState(null);
|
||||
|
||||
const [isDest, setIsDest] = useState(false);
|
||||
const [destPosition, setDestPosition] = useState(null);
|
||||
|
||||
const [waypoints, setWaypoints] = useState([]);
|
||||
|
||||
const [routeCoords, setRouteCoords] = useState([]);
|
||||
const [alternateCoords, setAlternateCoords] = useState([]);
|
||||
const [editingTarget, setEditingTarget] = useState(null);
|
||||
@@ -42,24 +52,55 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
|
||||
);
|
||||
|
||||
const isMovingSource = !isSource || editingTarget === "source";
|
||||
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
|
||||
const getWaypointIcon = (index) => {
|
||||
const wp = waypoints[index];
|
||||
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
|
||||
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
|
||||
};
|
||||
|
||||
const getActiveTarget = () => {
|
||||
if (editingTarget) return editingTarget;
|
||||
if (!isSource) return "source";
|
||||
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
|
||||
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
|
||||
if (!isDest) return "dest";
|
||||
return null;
|
||||
};
|
||||
|
||||
const map = useMapEvents({
|
||||
move(e) {
|
||||
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
|
||||
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
|
||||
const target = getActiveTarget();
|
||||
const center = e.target.getCenter();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setLatLng(center);
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setLatLng(center);
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
|
||||
}
|
||||
},
|
||||
movestart() {
|
||||
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||
if (isMovingDest && mapPrevDestMarker.current)
|
||||
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||
const target = getActiveTarget();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
|
||||
}
|
||||
},
|
||||
moveend() {
|
||||
if (isMovingSource)
|
||||
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||
if (isMovingDest && mapPrevDestMarker.current)
|
||||
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||
const target = getActiveTarget();
|
||||
if (target === "source") {
|
||||
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||
} else if (target === "dest") {
|
||||
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||
} else if (target?.startsWith("waypoint-")) {
|
||||
const idx = Number(target.split("-")[1]);
|
||||
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,12 +110,19 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (locations.length < 2) return;
|
||||
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
|
||||
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
|
||||
setSourcePosition(restoredSource);
|
||||
setDestPosition(restoredDest);
|
||||
const first = { lat: locations[0].lat, lng: locations[0].lon };
|
||||
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
|
||||
const middle = locations.slice(1, -1).map((loc) => ({
|
||||
position: { lat: loc.lat, lng: loc.lon },
|
||||
confirmed: true,
|
||||
}));
|
||||
|
||||
setSourcePosition(first);
|
||||
setDestPosition(last);
|
||||
setWaypoints(middle);
|
||||
setIsSource(true);
|
||||
setIsDest(true);
|
||||
|
||||
if (area) {
|
||||
const decoded = polyline.decode(area, 6);
|
||||
setRouteCoords(decoded);
|
||||
@@ -83,13 +131,15 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||
}, 100);
|
||||
} else {
|
||||
fetchRoute(restoredSource, restoredDest);
|
||||
fetchRoute(first, last, middle);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchRoute = async (source, dest) => {
|
||||
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
|
||||
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
|
||||
const locs = [
|
||||
{ lat: source.lat, lon: source.lng },
|
||||
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
|
||||
{ lat: dest.lat, lon: dest.lng },
|
||||
];
|
||||
try {
|
||||
@@ -118,18 +168,23 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetRoute = () => {
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
};
|
||||
|
||||
const handleMarkerClick = async () => {
|
||||
if (!isSource || editingTarget === "source") {
|
||||
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
|
||||
setSourcePosition(clickedPosition);
|
||||
setIsSource(true);
|
||||
const wasEditing = editingTarget === "source";
|
||||
setEditingTarget(null);
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
|
||||
if (editingTarget === "source" && destPosition) {
|
||||
await fetchRoute(clickedPosition, destPosition);
|
||||
if (wasEditing && destPosition) {
|
||||
await fetchRoute(clickedPosition, destPosition, waypoints);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -140,23 +195,65 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
setDestPosition(clickedPosition);
|
||||
setIsDest(true);
|
||||
setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
await fetchRoute(sourcePosition, clickedPosition);
|
||||
await fetchRoute(sourcePosition, clickedPosition, waypoints);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWaypointMarkerClick = async (index) => {
|
||||
const wp = waypoints[index];
|
||||
if (!wp || wp.confirmed) return; // فقط روی نقطهی تأییدنشده کلیک معتبره (یا حالت ادیت)
|
||||
|
||||
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
|
||||
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
|
||||
setWaypoints(updated);
|
||||
const wasEditing = editingTarget === `waypoint-${index}`;
|
||||
setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
if (sourcePosition && destPosition) {
|
||||
await fetchRoute(sourcePosition, destPosition, updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSource = () => {
|
||||
setEditingTarget("source");
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
const handleEditDest = () => {
|
||||
setEditingTarget("dest");
|
||||
setRouteCoords([]);
|
||||
setAlternateCoords([]);
|
||||
setSelectedRoute(null);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
const handleEditWaypoint = (index) => {
|
||||
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed میکنیم تا با حرکت نقشه جابهجا بشه
|
||||
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
|
||||
setEditingTarget(`waypoint-${index}`);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
|
||||
const handleAddWaypoint = () => {
|
||||
if (waypoints.length >= MAX_WAYPOINTS) return;
|
||||
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
|
||||
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
|
||||
setEditingTarget(`waypoint-${waypoints.length}`);
|
||||
resetRoute();
|
||||
};
|
||||
|
||||
// حذف یک نقطه میانی
|
||||
const handleRemoveWaypoint = async (index) => {
|
||||
const updated = waypoints.filter((_, i) => i !== index);
|
||||
setWaypoints(updated);
|
||||
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
|
||||
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
|
||||
resetRoute();
|
||||
|
||||
if (sourcePosition && destPosition) {
|
||||
await fetchRoute(sourcePosition, destPosition, updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRoute = (index, coords) => {
|
||||
@@ -176,14 +273,31 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
editingTarget={editingTarget}
|
||||
onEditSource={handleEditSource}
|
||||
onEditDest={handleEditDest}
|
||||
waypoints={waypoints}
|
||||
onEditWaypoint={handleEditWaypoint}
|
||||
onAddWaypoint={handleAddWaypoint}
|
||||
onRemoveWaypoint={handleRemoveWaypoint}
|
||||
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
|
||||
/>
|
||||
<AlarmText isSource={isSource} />
|
||||
|
||||
<Marker
|
||||
position={sourcePosition || map.getCenter()}
|
||||
ref={mapPrevSourceMarker}
|
||||
eventHandlers={{ click: handleMarkerClick }}
|
||||
icon={defaultIcon}
|
||||
/>
|
||||
|
||||
{waypoints.map((wp, index) => (
|
||||
<Marker
|
||||
key={`waypoint-${index}`}
|
||||
position={wp.position || map.getCenter()}
|
||||
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
|
||||
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
|
||||
icon={getWaypointIcon(index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isSource && (
|
||||
<Marker
|
||||
position={destPosition || map.getCenter()}
|
||||
@@ -192,6 +306,7 @@ const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||
icon={prevDestIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{alternateCoords.map((coords, index) => (
|
||||
<Polyline
|
||||
key={`alternate-${index}-${selectedRoute?.index === index}`}
|
||||
|
||||
Reference in New Issue
Block a user