Files
WhiteNightsAdminPanel/src/pages/Route/route-preview/MapDataContext.tsx
2025-11-11 03:33:26 +03:00

502 lines
14 KiB
TypeScript

import { useParams } from "react-router";
import { authInstance, languageInstance } from "@shared";
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
RouteData,
SightData,
SightPatchData,
StationData,
StationPatchData,
} from "./types";
import { observer } from "mobx-react-lite";
const MapDataContext = createContext<{
originalRouteData?: RouteData;
originalStationData?: StationData[];
originalSightData?: SightData[];
routeData?: RouteData;
stationData?: StationDataWithLanguage;
sightData?: SightData[];
isRouteLoading: boolean;
isStationLoading: boolean;
isSightLoading: boolean;
selectedSight?: SightData;
setSelectedSight: (sight?: SightData) => void;
setScaleRange: (min: number, max: number) => void;
setMapRotation: (rotation: number) => void;
setMapCenter: (x: number, y: number) => void;
setStationOffset: (stationId: number, x: number, y: number) => void;
setStationAlign: (stationId: number, align: number) => void;
setSightCoordinates: (
sightId: number,
latitude: number,
longitude: number
) => void;
setIconSize: (size: number) => void;
setFontSize: (size: number) => void;
saveChanges: () => void;
}>({
originalRouteData: undefined,
originalStationData: undefined,
originalSightData: undefined,
routeData: undefined,
stationData: undefined,
sightData: undefined,
isRouteLoading: true,
isStationLoading: true,
isSightLoading: true,
selectedSight: undefined,
setSelectedSight: () => {},
setScaleRange: () => {},
setMapRotation: () => {},
setMapCenter: () => {},
setStationOffset: () => {},
setStationAlign: () => {},
setSightCoordinates: () => {},
setIconSize: () => {},
setFontSize: () => {},
saveChanges: () => {},
});
type StationDataWithLanguage = {
[key: string]: StationData[];
};
export const MapDataProvider = observer(
({ children }: Readonly<{ children: ReactNode }>) => {
const { id: routeId } = useParams<{ id: string }>();
const [originalRouteData, setOriginalRouteData] = useState<RouteData>();
const [originalStationData, setOriginalStationData] =
useState<StationData[]>();
const [originalSightData, setOriginalSightData] = useState<SightData[]>();
const [routeData, setRouteData] = useState<RouteData>();
const [stationData, setStationData] = useState<StationDataWithLanguage>({
RU: [],
EN: [],
ZH: [],
});
const [sightData, setSightData] = useState<SightData[]>();
const [routeChanges, setRouteChanges] = useState<Partial<RouteData>>({});
const [stationChanges, setStationChanges] = useState<StationPatchData[]>(
[]
);
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
const [isRouteLoading, setIsRouteLoading] = useState(true);
const [isStationLoading, setIsStationLoading] = useState(true);
const [isSightLoading, setIsSightLoading] = useState(true);
const [selectedSight, setSelectedSight] = useState<SightData>();
useEffect(() => {
const fetchData = async () => {
try {
setIsRouteLoading(true);
setIsStationLoading(true);
setIsSightLoading(true);
const [
routeResponse,
ruStationResponse,
enStationResponse,
zhStationResponse,
sightResponse,
] = await Promise.all([
authInstance.get(`/route/${routeId}`),
languageInstance("ru").get(`/route/${routeId}/station`),
languageInstance("en").get(`/route/${routeId}/station`),
languageInstance("zh").get(`/route/${routeId}/station`),
languageInstance("ru").get(`/route/${routeId}/sight`),
]);
const routeData = routeResponse.data as RouteData;
setOriginalRouteData(routeData);
setOriginalStationData(ruStationResponse.data as StationData[]);
setStationData({
ru: ruStationResponse.data as StationData[],
en: enStationResponse.data as StationData[],
zh: zhStationResponse.data as StationData[],
});
setOriginalSightData(sightResponse.data as SightData[]);
setIsRouteLoading(false);
setIsStationLoading(false);
setIsSightLoading(false);
} catch (error) {
console.error("Error fetching data:", error);
setIsRouteLoading(false);
setIsStationLoading(false);
setIsSightLoading(false);
}
};
fetchData();
}, [routeId]);
useEffect(() => {
if (originalRouteData)
setRouteData({ ...originalRouteData, ...routeChanges });
if (originalSightData) setSightData(originalSightData);
}, [
originalRouteData,
originalSightData,
routeChanges,
stationChanges,
sightChanges,
]);
function setScaleRange(min: number, max: number) {
setRouteChanges((prev) => {
return { ...prev, scale_min: min, scale_max: max };
});
}
function setMapRotation(rotation: number) {
setRouteChanges((prev) => {
return { ...prev, rotate: rotation };
});
}
function setIconSize(size: number) {
const clamped = Math.max(50, Math.min(300, size));
setRouteChanges((prev) => {
if (prev.icon_size === clamped) {
return prev;
}
return { ...prev, icon_size: clamped };
});
}
function setFontSize(size: number) {
const clamped = Math.max(50, Math.min(300, size));
setRouteChanges((prev) => {
if (prev.font_size === clamped) {
return prev;
}
return { ...prev, font_size: clamped };
});
}
function setMapCenter(latitude: number, longitude: number) {
const epsilon = 1e-6;
setRouteChanges((prev) => {
const prevLat = prev.center_latitude;
const prevLon = prev.center_longitude;
if (
prevLat !== undefined &&
prevLon !== undefined &&
Math.abs(prevLat - latitude) < epsilon &&
Math.abs(prevLon - longitude) < epsilon
) {
return prev;
}
return {
...prev,
center_latitude: latitude,
center_longitude: longitude,
};
});
setRouteData((routePrev) => {
if (!routePrev) return routePrev;
return {
...routePrev,
center_latitude: latitude,
center_longitude: longitude,
};
});
}
async function saveChanges() {
await authInstance.patch(`/route/${routeId}`, routeData);
await saveStationChanges();
await saveSightChanges();
}
async function saveStationChanges() {
for (const station of stationChanges) {
await authInstance.patch(`/route/${routeId}/station`, station);
setStationData((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((lang) => {
updated[lang] = updated[lang].map((s) =>
s.id === station.station_id
? {
...s,
offset_x: station.offset_x,
offset_y: station.offset_y,
}
: s
);
});
return updated;
});
}
}
async function saveSightChanges() {
for (const sight of sightChanges) {
await authInstance.patch(`/route/${routeId}/sight`, sight);
setSightData((prev) =>
prev
? prev.map((s) =>
s.id === sight.sight_id
? {
...s,
latitude: sight.latitude,
longitude: sight.longitude,
}
: s
)
: prev
);
}
}
function setStationOffset(stationId: number, x: number, y: number) {
const currentStation = stationData.ru?.find(
(station) => station.id === stationId
);
if (
currentStation &&
Math.abs(currentStation.offset_x - x) < 0.01 &&
Math.abs(currentStation.offset_y - y) < 0.01
) {
return;
}
setStationChanges((prev) => {
const existingIndex = prev.findIndex(
(station) => station.station_id === stationId
);
if (existingIndex !== -1) {
const newChanges = [...prev];
newChanges[existingIndex] = {
...newChanges[existingIndex],
offset_x: x,
offset_y: y,
};
return newChanges;
} else {
const originalStation = originalStationData?.find(
(s) => s.id === stationId
);
return [
...prev,
{
station_id: stationId,
offset_x: x,
offset_y: y,
align: originalStation?.align ?? 1,
transfers: originalStation?.transfers ?? {
bus: "",
metro_blue: "",
metro_green: "",
metro_orange: "",
metro_purple: "",
metro_red: "",
train: "",
tram: "",
trolleybus: "",
},
},
];
}
});
setStationData((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((lang) => {
updated[lang] = updated[lang].map((station) => {
if (station.id === stationId) {
return { ...station, offset_x: x, offset_y: y };
}
return station;
});
});
return updated;
});
}
function setStationAlign(stationId: number, align: number) {
const currentStation = stationData.ru?.find(
(station) => station.id === stationId
);
if (currentStation && currentStation.align === align) {
return;
}
setStationChanges((prev) => {
const existingIndex = prev.findIndex(
(station) => station.station_id === stationId
);
if (existingIndex !== -1) {
const newChanges = [...prev];
newChanges[existingIndex] = {
...newChanges[existingIndex],
align: align,
};
return newChanges;
} else {
const originalStation = originalStationData?.find(
(s) => s.id === stationId
);
return [
...prev,
{
station_id: stationId,
align: align,
offset_x: originalStation?.offset_x ?? 0,
offset_y: originalStation?.offset_y ?? 0,
transfers: originalStation?.transfers ?? {
bus: "",
metro_blue: "",
metro_green: "",
metro_orange: "",
metro_purple: "",
metro_red: "",
train: "",
tram: "",
trolleybus: "",
},
},
];
}
});
setStationData((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((lang) => {
updated[lang] = updated[lang].map((station) => {
if (station.id === stationId) {
return { ...station, align: align };
}
return station;
});
});
return updated;
});
}
function setSightCoordinates(
sightId: number,
latitude: number,
longitude: number
) {
setSightData((prev) =>
prev
? prev.map((sight) =>
sight.id === sightId ? { ...sight, latitude, longitude } : sight
)
: prev
);
setSightChanges((prev) => {
const existingIndex = prev.findIndex(
(sight) => sight.sight_id === sightId
);
if (existingIndex !== -1) {
return prev.map((sight, index) => {
if (index === existingIndex) {
return {
...sight,
latitude,
longitude,
};
}
return sight;
});
} else {
const foundSight = sightData?.find((sight) => sight.id === sightId);
if (foundSight) {
return [
...prev,
{
sight_id: sightId,
latitude,
longitude,
},
];
}
return prev;
}
});
}
useEffect(() => {}, [sightChanges]);
const value = useMemo(
() => ({
originalRouteData,
originalStationData,
originalSightData,
routeData,
stationData,
sightData,
isRouteLoading,
isStationLoading,
isSightLoading,
selectedSight,
setSelectedSight,
setScaleRange,
setMapRotation,
setMapCenter,
saveChanges,
setStationOffset,
setStationAlign,
setSightCoordinates,
setIconSize,
setFontSize,
}),
[
originalRouteData,
originalStationData,
originalSightData,
routeData,
stationData,
sightData,
isRouteLoading,
isStationLoading,
isSightLoading,
selectedSight,
setIconSize,
setFontSize,
]
);
return (
<MapDataContext.Provider value={value}>
{children}
</MapDataContext.Provider>
);
}
);
export const useMapData = () => {
const context = useContext(MapDataContext);
if (!context) {
throw new Error("useMapData must be used within a MapDataProvider");
}
return context;
};