add middle points

This commit is contained in:
2026-06-21 13:28:02 +03:30
parent b5c7501192
commit 69677a0a63
11 changed files with 1083 additions and 203 deletions

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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}`}

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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}`}

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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}`}