add middle points
This commit is contained in:
20
src/assets/images/prev-waypoint-icon.svg
Normal file
20
src/assets/images/prev-waypoint-icon.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="30" height="35" viewBox="0 0 30 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.4453 1.5127C24.4711 1.08082 31.4173 11.6767 27.2842 20.9209L27.0752 21.3672C25.9614 23.6466 24.4616 25.0302 23.0117 26.2031C21.6264 27.3238 20.4204 28.1673 19.3184 29.4482L19.0986 29.71C18.9038 29.9504 18.5282 30.5355 18.0273 31.2695C17.8006 31.6018 17.5566 31.95 17.332 32.2393C17.2202 32.3833 17.0974 32.5329 16.9727 32.666C16.8723 32.7731 16.6754 32.9748 16.4141 33.124L16.4131 33.123C15.7088 33.5252 14.941 33.6027 14.2256 33.3691C13.5676 33.1543 13.0766 32.7195 12.7383 32.3145V32.3135C12.3056 31.795 11.9245 31.2168 11.5986 30.7314C11.3403 30.3467 11.107 30.0062 10.8682 29.7012L10.626 29.4072C9.78615 28.4447 8.79047 27.6829 7.65723 26.7939C6.63272 25.9903 5.49745 25.0829 4.54102 23.9053L4.35254 23.666C-2.23923 15.0669 3.27657 2.37649 13.958 1.54199L14.4453 1.5127ZM14.4834 10.8291H14.4795C11.4118 11.1538 9.65384 14.8452 11.3496 17.6201L11.5254 17.8857L11.6885 18.1016C13.3725 20.1841 16.6502 20.1828 18.334 18.1006L18.4961 17.8857C20.762 14.6868 18.2266 10.4356 14.4834 10.8291Z" stroke="white" stroke-width="3"/>
|
||||
<foreignObject x="-62" y="-62" width="154" height="159"></foreignObject><g filter="url(#filter0_d_225_89)">
|
||||
<path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z" fill="#FFC107"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_225_89" x="-62" y="-62" width="154" height="159" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_225_89"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_225_89" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="bgblur_0_225_89_clip_path" transform="translate(62 62)"><path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
21
src/assets/images/waypoint-icon.svg
Normal file
21
src/assets/images/waypoint-icon.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="42" height="66" viewBox="0 0 42 66" fill="#FFC107" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.3" d="M21 66C23.7614 66 26 64.8807 26 63.5C26 62.1193 23.7614 61 21 61C18.2386 61 16 62.1193 16 63.5C16 64.8807 18.2386 66 21 66Z" fill="#FFC107"/>
|
||||
<path clip-rule="evenodd" d="M21 0C32.598 0 42 9.40202 42 21C42 31.9234 33.6599 40.8989 23 41.906V62C23 63.1046 22.1046 64 21 64C19.8954 64 19 63.1046 19 62V41.906C8.34014 40.8989 0 31.9234 0 21C0 9.40202 9.40202 0 21 0Z" fill="#FFC107"/>
|
||||
<foreignObject x="-61" y="-61" width="164" height="164"></foreignObject><g filter="url(#filter0_d_173_69)">
|
||||
<path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z" fill="#FFC107"/>
|
||||
</g>
|
||||
<path d="M28 21C28 17.134 24.866 14 21 14C17.134 14 14 17.134 14 21C14 24.866 17.134 28 21 28C24.866 28 28 24.866 28 21Z" fill="white"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_173_69" x="-61" y="-61" width="164" height="164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_173_69"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_173_69" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="bgblur_0_173_69_clip_path" transform="translate(61 61)"><path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z"/>
|
||||
</clipPath></defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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