502 lines
14 KiB
TypeScript
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;
|
|
};
|