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(); const [originalStationData, setOriginalStationData] = useState(); const [originalSightData, setOriginalSightData] = useState(); const [routeData, setRouteData] = useState(); const [stationData, setStationData] = useState({ RU: [], EN: [], ZH: [], }); const [sightData, setSightData] = useState(); const [routeChanges, setRouteChanges] = useState>({}); const [stationChanges, setStationChanges] = useState( [] ); const [sightChanges, setSightChanges] = useState([]); const [isRouteLoading, setIsRouteLoading] = useState(true); const [isStationLoading, setIsStationLoading] = useState(true); const [isSightLoading, setIsSightLoading] = useState(true); const [selectedSight, setSelectedSight] = useState(); 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 ( {children} ); } ); export const useMapData = () => { const context = useContext(MapDataContext); if (!context) { throw new Error("useMapData must be used within a MapDataProvider"); } return context; };