Files
jrshikoku/stateBox/useCurrentTrain.tsx

429 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
FC,
} from "react";
import { HeaderConfig } from "@/lib/HeaderConfig";
import useInterval from "@/lib/useInterval";
import { useStationList } from "@/stateBox/useStationList";
import { useUserPosition } from "@/stateBox/useUserPosition";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { checkDuplicateTrainData } from "@/lib/checkDuplicateTrainData";
import { getStationID } from "@/lib/eachTrainInfoCoreLib/getStationData";
import { trainDataType } from "@/lib/trainPositionTextArray";
import { MOCK_TRAIN_POSITIONS } from "@/lib/mockApi";
import { fetchMockTrainPositions } from "@/lib/mockApi/positionMasters";
import WebView from "react-native-webview";
import { StationProps } from "@/lib/CommonTypes";
import { observedFetchJson } from "@/lib/observability/network/observedFetch";
type loading = "loading" | "success" | "error";
const initialState = {
webview: undefined,
currentTrain: [],
setCurrentTrain: (e) => {},
currentTrainLoading: "loading" as loading,
setCurrentTrainLoading: (e) => {},
getCurrentTrain: () => {},
inject: (i) => {},
fixedPosition: null,
setFixedPosition: (e) => {},
nearestStationID: null,
setInjectData: (e) => {},
getCurrentStationData: ((e) => {}) as (
e: string
) => trainDataType | undefined,
getPosition: ((e) => {}) as (e: trainDataType) => string[] | undefined,
fixedPositionSize: 80,
setFixedPositionSize: (e) => {},
};
type initialStateType = {
webview: React.MutableRefObject<WebView<{}>> | null;
currentTrain: trainDataType[];
setCurrentTrain: (e: trainDataType[]) => void;
currentTrainLoading: loading;
setCurrentTrainLoading: (e: loading) => void;
getCurrentTrain: () => void;
inject: (i: string) => void;
fixedPosition: { type: "station" | "train" | "nearestStation"; value: string } | null;
setFixedPosition: (
e: { type: "station" | "train" | "nearestStation"; value: string } | null
) => void;
nearestStationID: string | null;
setInjectData: (e: {
type: "station" | "train";
value: string;
fixed: boolean;
}) => void;
getCurrentStationData: (e: string) => trainDataType | undefined;
getPosition: (e: trainDataType) => string[] | undefined;
fixedPositionSize: number;
setFixedPositionSize: (e: number) => void;
};
const CurrentTrainContext = createContext<initialStateType>(initialState);
export const useCurrentTrain = () => {
return useContext(CurrentTrainContext);
};
type props = {
children: React.ReactNode;
};
export const CurrentTrainProvider: FC<props> = ({ children }) => {
const webview = useRef<WebView>(null);
const [currentTrain, setCurrentTrain] = useState<trainDataType[]>([]); //現在在線中の全列車 { num: 列車番号, delay: 遅延時分(状態), Pos: 位置情報 }
const [currentTrainLoading, setCurrentTrainLoading] =
useState<loading>("loading");
const { mockApiFeatureEnabled, mockTrainPositions, addTrainSnapshot, lookupPosText, recorderState } = useTrainMenu();
const { getInjectJavascriptAddress, stationList, originalStationList } =
useStationList();
const { position } = useUserPosition();
const [nearestStationID, setNearestStationID] = useState<string | null>(null);
const [fixedPosition, setFixedPosition] = useState<{
type: "station" | "train" | "nearestStation";
value: string;
}>({
type: null,
value: null,
});
const [fixedPositionSize, setFixedPositionSize] = useState(80);
const [_, setIntervalState] = useInterval(() => {
if (!webview.current) return;
if (fixedPosition.type == "station") {
const script = getInjectJavascriptAddress(
fixedPosition.value,
true,
fixedPositionSize
);
inject(script);
} else if (fixedPosition.type == "train") {
const currentTrain = getCurrentStationData(fixedPosition.value);
const values = getPosition(currentTrain);
if (!values) return;
if (values[0] == "M12" || values[1] == "M12") {
const script = getInjectJavascriptAddress("M12", true);
inject(script);
return;
}
const script = getInjectJavascriptAddress(
values[values.length == 2 ? currentTrain.Direction : 0],
true,
fixedPositionSize
);
inject(script);
} else if (fixedPosition.type == "nearestStation") {
if (!position) return;
const userPos = { lat: position.coords.latitude, lng: position.coords.longitude };
const _calcDistance = (from: { lat: number; lng: number }, to: { lat: number; lng: number }) => {
const dlat = Math.abs(from.lat - to.lat);
const dlng = Math.abs(from.lng - to.lng);
return Math.sqrt(dlat * dlat + dlng * dlng);
};
let nearest: StationProps | null = null;
let nearestDist = Infinity;
Object.keys(originalStationList).forEach((lineName) => {
originalStationList[lineName]?.forEach((station) => {
const d = _calcDistance({ lat: station.lat, lng: station.lng }, userPos);
if (d < nearestDist) {
nearestDist = d;
nearest = station;
}
});
});
const stationID = nearest?.StationNumber ?? null;
setNearestStationID(stationID);
if (stationID) {
const script = getInjectJavascriptAddress(stationID, true, fixedPositionSize);
inject(script);
}
} else {
inject(`setReload()`);
}
}, 15000, true, false);
type getPositionFuncType = (
currentTrainData: trainDataType
) => string[] | undefined;
const getPosition: getPositionFuncType = (currentTrainData) => {
//currentTrainData = {Pos:"伊予若宮~伊予大洲"}; //test
if (!currentTrainData) return;
if (!currentTrainData?.Pos) return;
if (currentTrainData?.Pos.match("")) {
const pos = currentTrainData?.Pos.replace("(下り)", "")
.replace("(上り)", "")
.replace("(徳島線)", "")
.replace("(高徳線)", "")
.replace("(坂出方)", "")
.replace("(児島方)", "")
.split("");
const direction = currentTrainData?.Direction || 0;
if (pos[0] == "児島" && pos[1] == "宇多津") {
if (direction == 0) {
return ["Y09", "M12"];
} else {
return ["M12", "Y09"];
}
} else if (pos[1] == "児島" && pos[0] == "宇多津") {
return ["Y09", "M12"];
} else if (pos[0] == "伊予若宮" && pos[1] == "伊予白滝") {
return ["S18", "S14"];
} else if (pos[0] == "伊予白滝" && pos[1] == "伊予若宮") {
return ["S14", "S18"];
} else if (pos[0] == "伊予大洲" && pos[1] == "伊予若宮") {
return ["U14", "+Iyo"];
} else if (pos[0] == "伊予若宮" && pos[1] == "伊予大洲") {
return ["-Iyo", "U14"];
}
const currentPosID = Object.keys(originalStationList).map((key) => {
let firstStation = false;
let firstStationID = "";
let secondStation = false;
let secondStationID = "";
originalStationList[key].forEach((station: StationProps) => {
if (station.Station_JP === pos[0]) {
firstStation = true;
firstStationID = station.StationNumber;
}
if (station.Station_JP === pos[1]) {
secondStation = true;
secondStationID = station.StationNumber;
}
});
if (firstStation && secondStation) {
return [firstStationID, secondStationID];
} else return false;
});
const currentPos = currentPosID.filter((d) => d != false)[0];
if (currentPos) {
return direction == 0 ? currentPos.reverse() : currentPos;
} else if (direction == 0) {
return [
getStationID(pos[1], stationList),
getStationID(pos[0], stationList),
];
} else {
return [
getStationID(pos[0], stationList),
getStationID(pos[1], stationList),
];
}
} else {
return [getStationID(currentTrainData?.Pos, stationList)];
}
};
type getCurrentStationDataFunc = (e: string) => trainDataType | undefined;
const getCurrentStationData: getCurrentStationDataFunc = (e) => {
//e:trainNumber
if (!currentTrain.length) return;
return checkDuplicateTrainData(
currentTrain.filter((d) => d.num == e),
stationList
);
};
type injectDataType = {
type: "station" | "train";
value: string;
fixed: boolean;
};
type injectDataFuncType = (data: injectDataType) => void;
const setInjectData: injectDataFuncType = ({ type, value, fixed }) => {
let script;
switch (type) {
case "station":
script = getInjectJavascriptAddress(value, fixed, fixedPositionSize);
break;
case "train":
const currentTrain = getCurrentStationData(value);
const values = getPosition(currentTrain);
if (!values) return;
if (values[0] == "M12" || values[1] == "M12") {
script = getInjectJavascriptAddress("M12");
} else {
const valuesIndex = values.length == 2 ? currentTrain.Direction : 0;
script = getInjectJavascriptAddress(
values[valuesIndex],
fixed,
fixedPositionSize
);
}
break;
}
inject(script);
if (fixed) {
setFixedPosition({ type, value });
}
};
const getCurrentTrain = () => {
// 再生中: 録画データをそのまま使う(ポーリング不要)
if (recorderState === 'playing') {
const source = mockTrainPositions ?? MOCK_TRAIN_POSITIONS;
const mapped = source
.filter((x): x is import("@/lib/mockApi/webviewXhrInterceptor").TrainEntry =>
"TrainNum" in x
)
.map((x) => ({
Index: x.Index,
num: x.TrainNum,
delay: x.delay as "入線" | number,
Pos: x.Pos || lookupPosText(x.PosNum, x.Line) || '',
PosNum: x.PosNum,
Direction: x.Direction,
Type: x.Type,
Line: x.Line,
}));
setCurrentTrain(mapped);
setCurrentTrainLoading("success");
return;
}
// モックON(通常): モックAPIサーバーをポーリング
if (mockApiFeatureEnabled) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
fetchMockTrainPositions()
.then((data) => {
const entries = data.filter((x: any) => "TrainNum" in x);
const mapped = entries.map((x: any) => ({
Index: x.Index as number,
num: x.TrainNum as string,
delay: x.delay as "入線" | number,
Pos: (x.Pos || lookupPosText(x.PosNum, x.Line) || '') as string,
PosNum: x.PosNum as number,
Direction: x.Direction as number,
Type: x.Type as string,
Line: x.Line as string,
}));
setCurrentTrain(mapped);
setCurrentTrainLoading("success");
})
.catch(() => {
setCurrentTrainLoading(currentTrain.length > 0 ? "success" : "error");
})
.finally(() => clearTimeout(timeoutId));
return;
}
observedFetchJson<any>("https://n8n.haruk.in/webhook/c501550c-7d1b-4e50-927b-4429fe18931a", {
endpoint: "positions_current",
source: "n8n",
userVisible: true,
preload: false,
fetchPriority: "high",
timeoutMs: 8000,
retry: true,
urlPathTemplate: "/webhook/c501550c-7d1b-4e50-927b-4429fe18931a",
})
.then((d) => d.data)
.then((d) =>
d.map((x) => ({
Index: x.Index,
num: x.TrainNum,
delay: x.delay,
Pos: x.Pos,
PosNum: x.PosNum,
Direction: x.Direction,
Type: x.Type,
Line: x.Line,
}))
)
.then((d) => {
setCurrentTrain(d);
setCurrentTrainLoading("success");
// 録画中なら trainDataType → TrainEntry に逆変換してスナップショット追記
addTrainSnapshot(d.map((t) => ({
Index: t.Index ?? 0,
TrainNum: t.num ?? '',
Pos: t.Pos ?? '',
PosNum: t.PosNum ?? 0,
delay: t.delay ?? 0,
Direction: (t.Direction ?? 0) as 0 | 1,
Type: t.Type ?? 'normal',
Line: t.Line ?? '',
})));
})
.catch(() => {
observedFetchJson<any[]>(
"https://script.google.com/macros/s/AKfycby9Y2-Bm75J_WkbZimi7iS8v5r9wMa9wtzpdwES9sOGF4i6HIYEJOM60W6gM1gXzt1o/exec",
{
...HeaderConfig,
endpoint: "positions_gas_fallback",
source: "gas",
userVisible: true,
preload: false,
fetchPriority: "high",
timeoutMs: 8000,
retry: true,
urlPathTemplate: "/macros/s/AKfyc.../exec",
}
)
.then((d) =>
d.map((x) => ({ num: x.TrainNum, delay: x.delay, Pos: x.Pos }))
)
.then((d) => {
// フォールバックAPIはDirection等を返さないので、既存データからマージ
setCurrentTrain((prev) => {
const prevMap = new Map(prev.map((t) => [t.num, t]));
return d.map((x) => {
const existing = prevMap.get(x.num);
return existing
? { ...existing, ...x }
: x;
});
});
setCurrentTrainLoading("success");
})
.catch(() => {
setCurrentTrainLoading(currentTrain.length > 0 ? "success" : "error");
});
});
};
const inject = (i) => {
webview.current?.injectJavaScript(i);
};
useEffect(() => {
getCurrentTrain();
}, []); //初回だけ現在の全在線列車取得
// モック切替時に即座に反映
useEffect(() => {
getCurrentTrain();
}, [mockApiFeatureEnabled]);
const [_0, _1] = useInterval(() => {
getCurrentTrain();
}, 15000, true, false); //15秒毎に全在線列車取得
return (
<CurrentTrainContext.Provider
value={{
webview,
currentTrain,
setCurrentTrain,
currentTrainLoading,
setCurrentTrainLoading,
getCurrentTrain,
inject,
setInjectData,
getCurrentStationData,
getPosition,
fixedPosition,
setFixedPosition,
nearestStationID,
fixedPositionSize,
setFixedPositionSize,
}}
>
{children}
</CurrentTrainContext.Provider>
);
};