1448 lines
46 KiB
TypeScript
1448 lines
46 KiB
TypeScript
import lineColorList from "@/assets/originData/lineColorList";
|
||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||
import { useStationList } from "@/stateBox/useStationList";
|
||
import { StationProps } from "@/lib/CommonTypes";
|
||
import { FC, useEffect, useRef, useState, useMemo } from "react";
|
||
import {
|
||
Text,
|
||
TouchableOpacity,
|
||
View,
|
||
Image,
|
||
LayoutAnimation,
|
||
ScrollView,
|
||
Platform,
|
||
PermissionsAndroid,
|
||
AppState,
|
||
} from "react-native";
|
||
import { getTrainType } from "@/lib/getTrainType";
|
||
import { trainDataType, trainPosition } from "@/lib/trainPositionTextArray";
|
||
import { StationNumberMaker } from "@/components/駅名表/StationNumberMaker";
|
||
import { lineListPair, stationIDPair } from "@/lib/getStationList";
|
||
import { findReversalPoints } from "@/lib/eachTrainInfoCoreLib/findReversalPoints";
|
||
import { CustomTrainData, trainTypeID } from "@/lib/CommonTypes";
|
||
import { getCurrentTrainData } from "@/lib/getCurrentTrainData";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import dayjs from "dayjs";
|
||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||
import { useThemeColors } from "@/lib/theme";
|
||
import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
|
||
import { resolveTrainDataIcon } from "@/lib/trainDataIcon";
|
||
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
||
import {
|
||
startTrainFollowActivity,
|
||
updateTrainFollowActivity,
|
||
endTrainFollowActivity,
|
||
isAvailable as isLiveActivityAvailable,
|
||
cancelLocationAnnouncements,
|
||
} from "expo-live-activity";
|
||
import {
|
||
DEFAULT_BACKGROUND_RIKKA_TRIGGER_SOURCE,
|
||
prepareBackgroundRikkaAnnouncements,
|
||
sendTrainPositionRikkaAnnouncement,
|
||
} from "@/lib/backgroundRikkaAnnouncements";
|
||
import { AS } from "@/storageControl";
|
||
import { STORAGE_KEYS } from "@/constants";
|
||
|
||
type props = {
|
||
trainID: string;
|
||
};
|
||
|
||
type TrainPathEntry = {
|
||
raw: string;
|
||
station: string;
|
||
se: string;
|
||
time: string;
|
||
index: number;
|
||
isThrough: boolean;
|
||
hasTime: boolean;
|
||
};
|
||
|
||
const normalizeTrainPosLabel = (value: string) =>
|
||
value
|
||
.replace(
|
||
/(下り)|(上り)|\(下り\)|\(上り\)|(徳島線)|(高徳線)|(坂出方)|(児島方)/g,
|
||
""
|
||
)
|
||
.trim();
|
||
|
||
const isHiddenThroughPointEntry = (raw: string) => {
|
||
const [station = "", se = ""] = (raw || "").split(",");
|
||
return station.startsWith(".") && se.includes("通");
|
||
};
|
||
|
||
const calcDistanceMinute = (
|
||
time: string,
|
||
delayTime: number,
|
||
playbackCurrentTimeIso?: string | null
|
||
) => {
|
||
if (!time || time === "") return null;
|
||
const now = playbackCurrentTimeIso ? dayjs(playbackCurrentTimeIso) : dayjs();
|
||
const hour = parseInt(time.split(":")[0], 10);
|
||
const target = now
|
||
.hour(hour < 4 ? hour + 24 : hour)
|
||
.minute(parseInt(time.split(":")[1], 10));
|
||
let diff = target.diff(now, "minute") + delayTime;
|
||
if (now.hour() < 4 && hour < 4) diff -= 1440;
|
||
return diff;
|
||
};
|
||
|
||
export const FixedTrain: FC<props> = ({ trainID }) => {
|
||
const { colors, fixed } = useThemeColors();
|
||
const {
|
||
setFixedPosition,
|
||
currentTrain,
|
||
getCurrentStationData,
|
||
getPosition,
|
||
fixedPositionSize,
|
||
setFixedPositionSize,
|
||
} = useCurrentTrain();
|
||
|
||
const { mapSwitch, iconSetting, playbackCurrentTimeIso } = useTrainMenu();
|
||
const { allCustomTrainData, allTrainDiagram, getTodayOperationByTrainId } =
|
||
useAllTrainDiagram();
|
||
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
|
||
|
||
const [liveNotifyId, setLiveNotifyId] = useState<string | null>(null);
|
||
const liveNotifyIdRef = useRef<string | null>(null);
|
||
const hasStartedRef = useRef(false);
|
||
const backgroundRikkaSignatureRef = useRef("");
|
||
const lastTrainPositionAnnouncementRef = useRef("");
|
||
const backgroundRikkaTrackingId = `train-${trainID}`;
|
||
|
||
const [train, setTrain] = useState<trainDataType>(null);
|
||
const [customData, setCustomData] = useState<CustomTrainData>(
|
||
getCurrentTrainData(trainID, currentTrain, allCustomTrainData)
|
||
);
|
||
const todayOperation = useMemo(
|
||
() => (getTodayOperationByTrainId(trainID) ?? []).filter((d) => d.state !== 100),
|
||
[getTodayOperationByTrainId, trainID]
|
||
);
|
||
const customTrainIcon = useMemo(() => {
|
||
const iconEntry = resolveTrainIconEntries({
|
||
trainNum: trainID,
|
||
customTrainData: customData,
|
||
todayOperation,
|
||
iconDisplayMode,
|
||
})[0];
|
||
|
||
return (
|
||
iconEntry?.vehicle_info_img ||
|
||
resolveTrainDataIcon(customData, iconDisplayMode)
|
||
);
|
||
}, [customData, iconDisplayMode, todayOperation, trainID]);
|
||
useEffect(() => {
|
||
setCustomData(
|
||
getCurrentTrainData(trainID, currentTrain, allCustomTrainData)
|
||
);
|
||
}, [currentTrain, trainID, allCustomTrainData]);
|
||
useEffect(() => {
|
||
const stationData = getCurrentStationData(trainID);
|
||
if (stationData) {
|
||
setTrain(stationData);
|
||
} else {
|
||
// バックグラウンドでは一時的にデータが消えることがある→フォアグラウンド時のみ終了
|
||
if (AppState.currentState === "active") {
|
||
alert("追跡していた列車が消えました。追跡を終了します。");
|
||
setFixedPosition({ type: null, value: null });
|
||
}
|
||
}
|
||
}, [trainID, currentTrain]);
|
||
|
||
const { getStationDataFromName, stationList, originalStationList } =
|
||
useStationList();
|
||
|
||
const [trainDataWidhThrough, setTrainDataWithThrough] = useState<string[]>(
|
||
[]
|
||
);
|
||
|
||
const computedTrainDataWithThrough = useMemo(() => {
|
||
const trainData =
|
||
allTrainDiagram[trainID]
|
||
?.split("#")
|
||
.filter((entry) => !isHiddenThroughPointEntry(entry)) ?? [];
|
||
if (trainData.length === 0) return [];
|
||
|
||
// 駅名ごとに駅情報を集約するマップを構築
|
||
const stationByNameMap = new Map();
|
||
stationList.forEach((lineStations) => {
|
||
lineStations.forEach((station) => {
|
||
if (!stationByNameMap.has(station.StationName)) {
|
||
stationByNameMap.set(station.StationName, []);
|
||
}
|
||
stationByNameMap.get(station.StationName).push(station);
|
||
});
|
||
});
|
||
|
||
const stopStationList = trainData.map((i) => {
|
||
const [station, se, time] = i.split(",");
|
||
const targetStations = stationByNameMap.get(station) || [];
|
||
// 各路線での該当駅をフィルタ(元のロジック維持)
|
||
return stationList.map((a) =>
|
||
targetStations.filter((d) => a.findIndex((s) => s.StationNumber === d.StationNumber) !== -1)
|
||
);
|
||
});
|
||
|
||
const allThroughStationList = stopStationList.map((i, index, array) => {
|
||
let allThroughStation = [];
|
||
if (index == array.length - 1) return;
|
||
|
||
const firstItem = array[index];
|
||
const secondItem = array[index + 1];
|
||
let betweenStationLine = "";
|
||
let baseStationNumberFirst = "";
|
||
let baseStationNumberSecond = "";
|
||
Object.keys(stationIDPair).forEach((d, index2) => {
|
||
if (!d) return;
|
||
const haveFirst = firstItem[index2];
|
||
const haveSecond = secondItem[index2];
|
||
if (haveFirst.length && haveSecond.length) {
|
||
betweenStationLine = d;
|
||
baseStationNumberFirst = haveFirst[0].StationNumber;
|
||
baseStationNumberSecond = haveSecond[0].StationNumber;
|
||
}
|
||
});
|
||
if (!betweenStationLine) return;
|
||
let reverse = false;
|
||
originalStationList[
|
||
lineListPair[stationIDPair[betweenStationLine]]
|
||
].forEach((d) => {
|
||
const isHiddenPassPoint = d.Station_JP?.startsWith(".");
|
||
if (isHiddenPassPoint) return;
|
||
if (
|
||
d.StationNumber > baseStationNumberFirst &&
|
||
d.StationNumber < baseStationNumberSecond
|
||
) {
|
||
allThroughStation.push(`${d.Station_JP},通過,`);
|
||
reverse = false;
|
||
} else {
|
||
if (
|
||
d.StationNumber < baseStationNumberFirst &&
|
||
d.StationNumber > baseStationNumberSecond
|
||
) {
|
||
allThroughStation.push(`${d.Station_JP},通過,`);
|
||
reverse = true;
|
||
}
|
||
}
|
||
});
|
||
if (reverse) allThroughStation.reverse();
|
||
return allThroughStation;
|
||
});
|
||
|
||
let mainArray = [...trainData];
|
||
let indexs = 0;
|
||
trainData.forEach((d, index) => {
|
||
indexs = indexs + 1;
|
||
if (!allThroughStationList[index]) return;
|
||
if (allThroughStationList[index].length == 0) return;
|
||
mainArray.splice(indexs, 0, ...allThroughStationList[index]);
|
||
indexs = indexs + allThroughStationList[index].length;
|
||
});
|
||
|
||
return mainArray;
|
||
}, [allTrainDiagram, stationList, trainID, originalStationList]);
|
||
|
||
useEffect(() => {
|
||
setTrainDataWithThrough(computedTrainDataWithThrough);
|
||
}, [computedTrainDataWithThrough]);
|
||
const [stopStationIDList, setStopStationList] = useState([]);
|
||
|
||
const computedStopStationIDList = useMemo(() => {
|
||
return trainDataWidhThrough.map((i) => {
|
||
const [station, se, time] = i.split(",");
|
||
const Stations = stationList.map((a) =>
|
||
a.filter((d) => d.StationName == station)
|
||
);
|
||
const StationNumbers =
|
||
Stations &&
|
||
Stations.reduce((newArray, e) => {
|
||
return newArray.concat(e);
|
||
}, []).map((d) => d.StationNumber);
|
||
return StationNumbers;
|
||
});
|
||
}, [trainDataWidhThrough, stationList]);
|
||
|
||
useEffect(() => {
|
||
setStopStationList(computedStopStationIDList);
|
||
}, [computedStopStationIDList]);
|
||
const [currentPosition, setCurrentPosition] = useState<string[]>([]);
|
||
const parsedTrainPath = useMemo<TrainPathEntry[]>(
|
||
() =>
|
||
trainDataWidhThrough.map((raw, index) => {
|
||
const [station = "", se = "", time = ""] = (raw || "").split(",");
|
||
return {
|
||
raw,
|
||
station,
|
||
se,
|
||
time,
|
||
index,
|
||
isThrough: se.includes("通"),
|
||
hasTime: time !== "",
|
||
};
|
||
}),
|
||
[trainDataWidhThrough]
|
||
);
|
||
|
||
useEffect(() => {
|
||
let position = getPosition(train);
|
||
if (stopStationIDList.length == 0) return;
|
||
if (position) {
|
||
if (position.length > 1) {
|
||
if (position[0] == "-Iyo") {
|
||
position[0] =
|
||
stopStationIDList[
|
||
stopStationIDList.findIndex((d) => d.includes("U14")) - 1
|
||
][0];
|
||
} else if (position[0] == "+Iyo") {
|
||
position[0] =
|
||
stopStationIDList[
|
||
stopStationIDList.findIndex((d) => d.includes("U14")) + 1
|
||
][0];
|
||
}
|
||
if (position[1] == "+Iyo") {
|
||
position[1] =
|
||
stopStationIDList[
|
||
stopStationIDList.findIndex((d) => d.includes("U14")) + 1
|
||
][0];
|
||
} else if (position[1] == "-Iyo") {
|
||
position[1] =
|
||
stopStationIDList[
|
||
stopStationIDList.findIndex((d) => d.includes("U14")) - 1
|
||
][0];
|
||
}
|
||
}
|
||
|
||
setCurrentPosition(position);
|
||
}
|
||
}, [train, stopStationIDList]);
|
||
|
||
const [currentPointData, setCurrentPointData] = useState<StationProps[]>([]);
|
||
const [nextStopStationData, setNextStopStationData] = useState<StationProps[]>([]);
|
||
const [untilStationData, setUntilStationData] = useState<string[]>([]);
|
||
const [probably, setProbably] = useState(false);
|
||
const [isCurrentPointDisplay, setIsCurrentPointDisplay] = useState(false);
|
||
const [currentDisplayIndex, setCurrentDisplayIndex] = useState(0);
|
||
const [tick, setTick] = useState(0);
|
||
|
||
useEffect(() => {
|
||
if (playbackCurrentTimeIso) return;
|
||
const interval = setInterval(() => setTick((value) => value + 1), 15000);
|
||
return () => clearInterval(interval);
|
||
}, [playbackCurrentTimeIso]);
|
||
|
||
useEffect(() => {
|
||
const points = findReversalPoints(currentPosition, stopStationIDList);
|
||
if (!points || points.length === 0) return;
|
||
|
||
const pointIndexes = points.reduce(
|
||
(acc: number[], isCurrent, index) => {
|
||
if (isCurrent) acc.push(index);
|
||
return acc;
|
||
},
|
||
[] as number[]
|
||
);
|
||
if (!pointIndexes.length) return;
|
||
|
||
const firstMatchedIndex = pointIndexes[0];
|
||
const isCurrentStationCluster = currentPosition.length <= 1;
|
||
const delayTime =
|
||
train?.delay == "入線" ? 0 : parseInt(String(train?.delay), 10) || 0;
|
||
|
||
let anchorIndex = firstMatchedIndex;
|
||
let usedTimeEstimation = false;
|
||
const shouldShowCurrentPoint = isCurrentStationCluster;
|
||
const hasIntermediateCurrentPoints = pointIndexes.length > 1;
|
||
|
||
if (!isCurrentStationCluster && hasIntermediateCurrentPoints) {
|
||
let upcomingTimedIndex = -1;
|
||
let lastPassedTimedIndex = -1;
|
||
|
||
for (
|
||
let searchCount = firstMatchedIndex;
|
||
searchCount < parsedTrainPath.length;
|
||
searchCount++
|
||
) {
|
||
const entry = parsedTrainPath[searchCount];
|
||
if (!entry?.raw || !entry.hasTime) continue;
|
||
|
||
const distanceMinute = calcDistanceMinute(
|
||
entry.time,
|
||
delayTime,
|
||
playbackCurrentTimeIso
|
||
);
|
||
if (distanceMinute == null) continue;
|
||
|
||
if (distanceMinute < 0) {
|
||
lastPassedTimedIndex = searchCount;
|
||
continue;
|
||
}
|
||
|
||
upcomingTimedIndex = searchCount;
|
||
break;
|
||
}
|
||
|
||
const baseIndex =
|
||
lastPassedTimedIndex >= 0 ? lastPassedTimedIndex + 1 : firstMatchedIndex;
|
||
|
||
if (upcomingTimedIndex >= 0) {
|
||
const hasUntimedGapBeforeUpcoming = parsedTrainPath
|
||
.slice(baseIndex, upcomingTimedIndex)
|
||
.some((entry) => entry?.raw && !entry.hasTime);
|
||
|
||
anchorIndex = hasUntimedGapBeforeUpcoming ? baseIndex : upcomingTimedIndex;
|
||
} else if (lastPassedTimedIndex >= 0) {
|
||
const lastPassedEntry = parsedTrainPath[lastPassedTimedIndex];
|
||
anchorIndex = lastPassedEntry?.isThrough
|
||
? Math.min(lastPassedTimedIndex + 1, parsedTrainPath.length - 1)
|
||
: lastPassedTimedIndex;
|
||
}
|
||
|
||
usedTimeEstimation = anchorIndex !== firstMatchedIndex;
|
||
}
|
||
|
||
const anchorEntry = parsedTrainPath[anchorIndex];
|
||
const currentPointName = anchorEntry?.station || "";
|
||
setCurrentPointData(
|
||
currentPointName ? getStationDataFromName(currentPointName) : []
|
||
);
|
||
|
||
const nextStopSearchStart = shouldShowCurrentPoint
|
||
? anchorIndex + 1
|
||
: anchorIndex;
|
||
const nextStopEntry = parsedTrainPath.find(
|
||
(entry, index) =>
|
||
index >= nextStopSearchStart && !!entry?.station && !entry.isThrough
|
||
);
|
||
const nextStopName = nextStopEntry?.station || "";
|
||
setNextStopStationData(
|
||
nextStopName ? getStationDataFromName(nextStopName) : []
|
||
);
|
||
|
||
const visibleStart = Math.max(anchorIndex - 1, 0);
|
||
const trainList = parsedTrainPath
|
||
.slice(visibleStart)
|
||
.map((entry) => entry.raw)
|
||
.filter((entry) => !!entry);
|
||
|
||
setProbably(usedTimeEstimation);
|
||
setIsCurrentPointDisplay(shouldShowCurrentPoint);
|
||
setCurrentDisplayIndex(Math.max(anchorIndex - visibleStart, 0));
|
||
setUntilStationData(trainList);
|
||
}, [
|
||
currentPosition,
|
||
parsedTrainPath,
|
||
playbackCurrentTimeIso,
|
||
stopStationIDList,
|
||
tick,
|
||
train?.delay,
|
||
getStationDataFromName,
|
||
]);
|
||
const [ToData, setToData] = useState("");
|
||
useEffect(() => {
|
||
if (customData.to_data && customData.to_data != "") {
|
||
setToData(customData.to_data);
|
||
} else {
|
||
if (trainDataWidhThrough.length == 0) return;
|
||
setToData(
|
||
trainDataWidhThrough[trainDataWidhThrough.length - 2].split(",")[0]
|
||
);
|
||
}
|
||
}, [customData, trainDataWidhThrough]);
|
||
|
||
const [station, setStation] = useState<StationProps[]>([]);
|
||
useEffect(() => {
|
||
const data = getStationDataFromName(ToData);
|
||
setStation(data);
|
||
}, [ToData]);
|
||
const lineColor =
|
||
customData.to_data_color && customData.to_data_color.length > 0
|
||
? customData.to_data_color[0]
|
||
: station.length > 0
|
||
? lineColorList[station[0]?.StationNumber?.slice(0, 1)]
|
||
: "black";
|
||
//const lineColor = "red";
|
||
const customTrainType = getTrainType({
|
||
type: customData.type,
|
||
whiteMode: true,
|
||
});
|
||
const trainNameText = `${customData.train_name}${
|
||
(customData.train_num_distance !== "" && !isNaN(parseInt(customData.train_num_distance)))
|
||
? ` ${parseInt(customData.train_id) - parseInt(customData.train_num_distance)}号`
|
||
: ""
|
||
}`;
|
||
|
||
// ── Station Progress for Live Notification ──
|
||
// 着のみエントリを除外(終着駅は着を許可、発・通編・通発編は保持)
|
||
const lastValidIdx = trainDataWidhThrough.reduce(
|
||
(last: number, d: string, i: number) => (d ? i : last), -1
|
||
);
|
||
const filteredTrainData = trainDataWidhThrough.filter((d, idx) => {
|
||
if (!d || isHiddenThroughPointEntry(d)) return false;
|
||
const [, se] = d.split(",");
|
||
if (!se) return true;
|
||
// 着を含み発を含まないエントリは終着駅のみ許可
|
||
if (se.includes("着") && !se.includes("発")) {
|
||
return idx === lastValidIdx;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
const stationStops = filteredTrainData
|
||
.filter((d) => !d.split(",")[1]?.includes("通"))
|
||
.map((d) => d.split(",")[0]);
|
||
|
||
// 全駅リスト(通過駅含む、停車/通過フラグ+乗換色付き)
|
||
// 駅名→所属路線コードのマップ構築
|
||
const stationToLineCodes: Record<string, string[]> = {};
|
||
if (originalStationList) {
|
||
Object.keys(lineListPair).forEach((lineCode: string) => {
|
||
const lineName = lineListPair[lineCode];
|
||
const stations = originalStationList[lineName];
|
||
if (!stations) return;
|
||
stations.forEach((s: StationProps) => {
|
||
if (!stationToLineCodes[s.Station_JP]) stationToLineCodes[s.Station_JP] = [];
|
||
if (!stationToLineCodes[s.Station_JP].includes(lineCode)) {
|
||
stationToLineCodes[s.Station_JP].push(lineCode);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
// 現在走行中の路線コード
|
||
const runningLineCode = station.length > 0
|
||
? station[0]?.StationNumber?.slice(0, 1) || ""
|
||
: "";
|
||
|
||
const allStations = filteredTrainData
|
||
.map((d) => {
|
||
const [name, se] = d.split(",");
|
||
const isStop = !se?.includes("通");
|
||
const lineCodes = stationToLineCodes[name] || [];
|
||
// 乗換色: 走行路線以外の路線色
|
||
const transferColors = lineCodes
|
||
.filter((c) => c !== runningLineCode)
|
||
.map((c) => lineColorList[c])
|
||
.filter(Boolean);
|
||
return {
|
||
name,
|
||
isStop,
|
||
...(transferColors.length > 0 ? { transferColors } : {}),
|
||
};
|
||
});
|
||
|
||
const shouldShowCurrentLabel =
|
||
isCurrentPointDisplay || currentPointData[0]?.Station_JP === train?.Pos;
|
||
const currentStationLabel = shouldShowCurrentLabel
|
||
? currentPointData[0]?.Station_JP || train?.Pos || ""
|
||
: train?.Pos || "";
|
||
const bannerStationData = shouldShowCurrentLabel
|
||
? currentPointData
|
||
: nextStopStationData;
|
||
|
||
// 全駅リスト中の現在地インデックス
|
||
const currentStationIndex = (() => {
|
||
const pos = currentStationLabel;
|
||
if (!pos) return 0;
|
||
// Pos は "駅名" (駅にいる時) or "駅A~駅B" (走行中) の形式
|
||
const posStations = pos.split("~").map((s: string) =>
|
||
normalizeTrainPosLabel(s)
|
||
);
|
||
// 完全一致
|
||
const firstIdx = allStations.findIndex((s) => s.name === posStations[0]);
|
||
if (firstIdx >= 0) return firstIdx;
|
||
// 部分一致フォールバック
|
||
const partialIdx = allStations.findIndex((s) =>
|
||
posStations[0].includes(s.name) || s.name.includes(posStations[0])
|
||
);
|
||
if (partialIdx >= 0) return partialIdx;
|
||
return 0;
|
||
})();
|
||
|
||
const nextStationIndex = (() => {
|
||
const name = nextStopStationData[0]?.Station_JP;
|
||
if (!name) return -1;
|
||
const idx = stationStops.indexOf(name);
|
||
if (idx >= 0) return idx;
|
||
// 部分一致フォールバック
|
||
return stationStops.findIndex((s) => s === name || name.includes(s) || s.includes(name));
|
||
})();
|
||
|
||
// ── Live Notification ──
|
||
useEffect(() => {
|
||
liveNotifyIdRef.current = liveNotifyId;
|
||
}, [liveNotifyId]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (liveNotifyIdRef.current) {
|
||
endTrainFollowActivity(liveNotifyIdRef.current).catch(() => {});
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!liveNotifyId || !train) return;
|
||
const delayNum = train.delay === "入線" ? 0 : parseInt(String(train.delay)) || 0;
|
||
const delayStatus = delayNum > 0 ? `${delayNum}分遅れ` : "定刻";
|
||
const positionStatus = shouldShowCurrentLabel ? "ただいま" : "次は";
|
||
updateTrainFollowActivity(liveNotifyId, {
|
||
currentStation: currentStationLabel,
|
||
nextStation: nextStopStationData[0]?.Station_JP || "",
|
||
delayMinutes: delayNum,
|
||
scheduledArrival: "",
|
||
trainNumber: trainID,
|
||
trainType: customTrainType.shortName,
|
||
trainName: trainNameText,
|
||
trainTypeColor: customTrainType.color,
|
||
lineColor,
|
||
destination: ToData,
|
||
positionStatus,
|
||
delayStatus,
|
||
stationStops,
|
||
nextStationIndex: nextStationIndex >= 0 ? nextStationIndex : undefined,
|
||
allStations,
|
||
currentStationIndex,
|
||
}).catch(() => {});
|
||
}, [
|
||
train,
|
||
nextStopStationData,
|
||
liveNotifyId,
|
||
stationStops,
|
||
nextStationIndex,
|
||
currentStationIndex,
|
||
currentStationLabel,
|
||
shouldShowCurrentLabel,
|
||
]);
|
||
|
||
// バナー表示と同時にLive Activityを自動開始
|
||
useEffect(() => {
|
||
if (
|
||
!isLiveActivityAvailable() ||
|
||
hasStartedRef.current ||
|
||
!train ||
|
||
!nextStopStationData[0]
|
||
) {
|
||
return;
|
||
}
|
||
hasStartedRef.current = true;
|
||
const startActivity = async () => {
|
||
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
||
const granted = await PermissionsAndroid.request(
|
||
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
|
||
);
|
||
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
|
||
}
|
||
const delayNum = train?.delay === "入線" ? 0 : parseInt(String(train?.delay)) || 0;
|
||
const delayStatus = delayNum > 0 ? `${delayNum}分遅れ` : "定刻";
|
||
const positionStatus = shouldShowCurrentLabel ? "ただいま" : "次は";
|
||
try {
|
||
const id = await startTrainFollowActivity({
|
||
trainNumber: trainID,
|
||
lineName: "",
|
||
destination: ToData,
|
||
currentStation: currentStationLabel,
|
||
nextStation: nextStopStationData[0]?.Station_JP || "",
|
||
delayMinutes: delayNum,
|
||
scheduledArrival: "",
|
||
trainType: customTrainType.shortName,
|
||
trainName: trainNameText,
|
||
trainTypeColor: customTrainType.color,
|
||
lineColor,
|
||
positionStatus,
|
||
delayStatus,
|
||
stationStops,
|
||
nextStationIndex: nextStationIndex >= 0 ? nextStationIndex : undefined,
|
||
allStations,
|
||
currentStationIndex,
|
||
});
|
||
setLiveNotifyId(id);
|
||
} catch (e) {
|
||
console.warn('[LiveNotify] start error:', e);
|
||
hasStartedRef.current = false;
|
||
}
|
||
};
|
||
startActivity();
|
||
}, [
|
||
train,
|
||
nextStopStationData,
|
||
currentStationLabel,
|
||
shouldShowCurrentLabel,
|
||
ToData,
|
||
trainID,
|
||
customTrainType.shortName,
|
||
trainNameText,
|
||
customTrainType.color,
|
||
lineColor,
|
||
stationStops,
|
||
nextStationIndex,
|
||
allStations,
|
||
currentStationIndex,
|
||
]);
|
||
|
||
// iOSへ残り停車駅の位置トリガーとりっかちゃん通知音を登録する。
|
||
useEffect(() => {
|
||
if (Platform.OS !== "ios" || playbackCurrentTimeIso) return;
|
||
|
||
const upcomingStations = allStations
|
||
.slice(currentStationIndex + 1)
|
||
.map((entry, offset) => ({ entry, offset }))
|
||
.filter(({ entry }) => entry.isStop)
|
||
.map(({ entry, offset }) => {
|
||
const stationData = getStationDataFromName(entry.name)[0];
|
||
if (
|
||
!stationData ||
|
||
!Number.isFinite(stationData.lat) ||
|
||
!Number.isFinite(stationData.lng)
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
identifier: `${currentStationIndex + 1 + offset}-${stationData.StationNumber || entry.name}`,
|
||
stationName: entry.name,
|
||
latitude: stationData.lat,
|
||
longitude: stationData.lng,
|
||
};
|
||
})
|
||
.filter((station): station is NonNullable<typeof station> => station != null)
|
||
.slice(0, 20);
|
||
|
||
if (upcomingStations.length === 0) return;
|
||
|
||
const signature = [
|
||
backgroundRikkaTrackingId,
|
||
currentStationIndex,
|
||
...upcomingStations.map((station) => station.identifier),
|
||
].join(":");
|
||
if (backgroundRikkaSignatureRef.current === signature) return;
|
||
|
||
const controller = new AbortController();
|
||
Promise.all([
|
||
AS.getItem(STORAGE_KEYS.BACKGROUND_RIKKA_ANNOUNCEMENT).catch(() => "false"),
|
||
AS.getItem(STORAGE_KEYS.BACKGROUND_RIKKA_TRIGGER_SOURCE).catch(
|
||
() => DEFAULT_BACKGROUND_RIKKA_TRIGGER_SOURCE
|
||
),
|
||
])
|
||
.then(async ([value, source]) => {
|
||
const enabled = value === true || value === "true";
|
||
if (
|
||
!enabled ||
|
||
source === "trainPosition" ||
|
||
controller.signal.aborted
|
||
) {
|
||
backgroundRikkaSignatureRef.current = "";
|
||
await cancelLocationAnnouncements(backgroundRikkaTrackingId);
|
||
return;
|
||
}
|
||
|
||
backgroundRikkaSignatureRef.current = signature;
|
||
const result = await prepareBackgroundRikkaAnnouncements({
|
||
trackingId: backgroundRikkaTrackingId,
|
||
stations: upcomingStations,
|
||
signal: controller.signal,
|
||
});
|
||
console.info(
|
||
`[BackgroundRikka] scheduled=${result.scheduled} failed=${result.failedStations.length}`
|
||
);
|
||
})
|
||
.catch((error) => {
|
||
backgroundRikkaSignatureRef.current = "";
|
||
if (!controller.signal.aborted) {
|
||
console.warn("[BackgroundRikka] Failed to schedule announcements", error);
|
||
}
|
||
});
|
||
|
||
return () => controller.abort();
|
||
}, [
|
||
allStations,
|
||
backgroundRikkaTrackingId,
|
||
currentStationIndex,
|
||
getStationDataFromName,
|
||
playbackCurrentTimeIso,
|
||
]);
|
||
|
||
// 検証用: 列車走行位置から算出した次駅が変わった瞬間に通知する。
|
||
useEffect(() => {
|
||
if (Platform.OS !== "ios") return;
|
||
|
||
const stationName = nextStopStationData[0]?.Station_JP;
|
||
if (!stationName) return;
|
||
|
||
const controller = new AbortController();
|
||
Promise.all([
|
||
AS.getItem(STORAGE_KEYS.BACKGROUND_RIKKA_ANNOUNCEMENT).catch(() => "false"),
|
||
AS.getItem(STORAGE_KEYS.BACKGROUND_RIKKA_TRIGGER_SOURCE).catch(
|
||
() => DEFAULT_BACKGROUND_RIKKA_TRIGGER_SOURCE
|
||
),
|
||
])
|
||
.then(async ([enabledValue, source]) => {
|
||
const enabled =
|
||
enabledValue === true || enabledValue === "true";
|
||
if (
|
||
!enabled ||
|
||
source !== "trainPosition" ||
|
||
controller.signal.aborted
|
||
) {
|
||
lastTrainPositionAnnouncementRef.current = "";
|
||
return;
|
||
}
|
||
|
||
const announcementKey = `${trainID}:${stationName}`;
|
||
if (
|
||
lastTrainPositionAnnouncementRef.current === announcementKey
|
||
) {
|
||
return;
|
||
}
|
||
|
||
lastTrainPositionAnnouncementRef.current = announcementKey;
|
||
await cancelLocationAnnouncements(backgroundRikkaTrackingId);
|
||
if (controller.signal.aborted) return;
|
||
|
||
await sendTrainPositionRikkaAnnouncement({
|
||
stationName,
|
||
trainId: trainID,
|
||
signal: controller.signal,
|
||
});
|
||
})
|
||
.catch((error) => {
|
||
lastTrainPositionAnnouncementRef.current = "";
|
||
if (!controller.signal.aborted) {
|
||
console.warn(
|
||
"[BackgroundRikka] Train-position announcement failed",
|
||
error
|
||
);
|
||
}
|
||
});
|
||
|
||
return () => controller.abort();
|
||
}, [
|
||
backgroundRikkaTrackingId,
|
||
nextStopStationData,
|
||
train?.Pos,
|
||
trainID,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
cancelLocationAnnouncements(backgroundRikkaTrackingId).catch(() => {});
|
||
};
|
||
}, [backgroundRikkaTrackingId]);
|
||
|
||
return (
|
||
<View
|
||
style={{ display: "flex", flexDirection: "column", flex: 1 }}
|
||
pointerEvents="box-none"
|
||
>
|
||
<View
|
||
style={{
|
||
flex: 1,
|
||
flexDirection: fixedPositionSize === 226 ? "column" : "row",
|
||
backgroundColor: "black",
|
||
//borderBottomColor: "black",
|
||
//borderBottomWidth: 2,
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
flexDirection: fixedPositionSize === 226 ? "row" : "column",
|
||
flex: 1,
|
||
backgroundColor: colors.background,
|
||
height: fixedPositionSize === 226 ? 200 : 50,
|
||
overflow: "hidden",
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
flex: fixedPositionSize === 226 ? 5 : 1,
|
||
flexDirection: "row",
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
backgroundColor: customTrainType.color,
|
||
flexDirection: "row",
|
||
alignContent: "center",
|
||
alignSelf: "center",
|
||
alignItems: "center",
|
||
height: "100%",
|
||
}}
|
||
>
|
||
<Image
|
||
source={{ uri: customTrainIcon }}
|
||
width={fixedPositionSize === 226 ? 23 : 14}
|
||
height={fixedPositionSize === 226 ? 26 : 17}
|
||
style={{ margin: 5 }}
|
||
/>
|
||
<View
|
||
style={{
|
||
flexDirection: fixedPositionSize === 226 ? "column" : "row",
|
||
alignContent: "center",
|
||
alignSelf: "center",
|
||
alignItems: "center",
|
||
maxWidth: fixedPositionSize === 226 ? 80 : 100,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
fontSize: trainNameText.length > 4 ? 12 : 14,
|
||
fontFamily: customTrainType.fontFamily,
|
||
fontWeight: customTrainType.fontFamily ? undefined : "bold",
|
||
marginTop: customTrainType.fontFamily ? 3 : 0,
|
||
color: fixed.textOnPrimary,
|
||
textAlignVertical: "center",
|
||
textAlign: "left",
|
||
}}
|
||
>
|
||
{customTrainType.shortName}
|
||
{customTrainType.fontFamily === "JR-WEST-PLUS" ? (
|
||
<Text style={{ fontFamily: "DiaPro", fontSize: 7 }}>{"\u00A0"}</Text>
|
||
) : null}
|
||
</Text>
|
||
{customData.train_name && (
|
||
<Text
|
||
style={{
|
||
fontSize: trainNameText.length > 4 ? 8 : 14,
|
||
color: fixed.textOnPrimary,
|
||
maxWidth: fixedPositionSize === 226 ? 200 : 60,
|
||
textAlignVertical: "center",
|
||
}}
|
||
>
|
||
{trainNameText}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
<View
|
||
style={{
|
||
backgroundColor: customTrainType.color,
|
||
width: 10,
|
||
borderLeftColor: customTrainType.color,
|
||
borderTopColor: lineColor,
|
||
borderBottomColor: lineColor,
|
||
borderTopWidth: fixedPositionSize === 226 ? 50 : 14,
|
||
borderBottomWidth: fixedPositionSize === 226 ? 50 : 14,
|
||
borderLeftWidth: fixedPositionSize === 226 ? 30 : 10,
|
||
borderRightWidth: 0,
|
||
//height: fixedPositionSize === 226 ? 20 : 100,
|
||
height: "100%",
|
||
}}
|
||
></View>
|
||
</View>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignContent: "center",
|
||
alignSelf: "center",
|
||
height: "100%",
|
||
backgroundColor: lineColor,
|
||
flex: 1,
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignContent: "center",
|
||
alignSelf: "center",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<StationNumberMaker
|
||
currentStation={station}
|
||
singleSize={18}
|
||
useEach={true}
|
||
/>
|
||
<Text
|
||
style={{
|
||
fontSize: customData?.to_data?.length > 4 ? 9 : 12,
|
||
color: fixed.textOnPrimary,
|
||
fontWeight: "bold",
|
||
textAlignVertical: "center",
|
||
margin: 0,
|
||
padding: 0,
|
||
height: "100%",
|
||
}}
|
||
>
|
||
{ToData}行
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
{fixedPositionSize === 226 && (
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.background,
|
||
width: 10,
|
||
borderLeftColor: "black",
|
||
borderTopColor: lineColor,
|
||
borderBottomColor: colors.background,
|
||
borderRightColor: "black",
|
||
borderTopWidth: 50,
|
||
borderBottomWidth: 0,
|
||
borderLeftWidth: 0,
|
||
borderRightWidth: 20,
|
||
}}
|
||
></View>
|
||
)}
|
||
<View
|
||
style={{
|
||
backgroundColor: "black",
|
||
flex: fixedPositionSize === 226 ? 4 : 1,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<View style={{ flexDirection: "column" }}>
|
||
<Text
|
||
style={{
|
||
fontSize: 10,
|
||
fontWeight: "bold",
|
||
color: "white",
|
||
marginHorizontal: 5,
|
||
paddingVertical: 0,
|
||
marginVertical: -1,
|
||
}}
|
||
>
|
||
{shouldShowCurrentLabel ? "ただいま" : "次は"}
|
||
</Text>
|
||
{probably && (
|
||
<Text
|
||
style={{
|
||
fontSize: 5,
|
||
color: "white",
|
||
fontWeight: "bold",
|
||
marginHorizontal: 5,
|
||
paddingVertical: 0,
|
||
marginVertical: -1,
|
||
}}
|
||
>
|
||
(時刻推定)
|
||
</Text>
|
||
)}
|
||
</View>
|
||
<StationNumberMaker
|
||
currentStation={bannerStationData}
|
||
singleSize={20}
|
||
useEach={true}
|
||
/>
|
||
<Text
|
||
style={{
|
||
fontSize: 18,
|
||
fontWeight: "bold",
|
||
color: "white",
|
||
flex: 1,
|
||
}}
|
||
>
|
||
{bannerStationData[0]?.Station_JP || "不明"}
|
||
</Text>
|
||
{fixedPositionSize !== 226 && (
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.background,
|
||
width: 10,
|
||
borderLeftColor: "black",
|
||
borderTopColor: "black",
|
||
borderBottomColor: colors.background,
|
||
borderRightColor: colors.background,
|
||
borderTopWidth: 21,
|
||
borderBottomWidth: 0,
|
||
borderLeftWidth: 0,
|
||
borderRightWidth: 7,
|
||
}}
|
||
></View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
<CurrentPositionBox
|
||
train={train}
|
||
lineColor={lineColor}
|
||
trainDataWithThrough={untilStationData}
|
||
currentDisplayIndex={currentDisplayIndex}
|
||
isSmall={fixedPositionSize !== 226}
|
||
/>
|
||
</View>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
borderTopColor: "black",
|
||
borderTopWidth: 2,
|
||
}}
|
||
pointerEvents="box-none"
|
||
>
|
||
<TouchableOpacity
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
}}
|
||
onPress={() => {
|
||
setFixedPosition({ type: null, value: null });
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
backgroundColor: "black",
|
||
paddingHorizontal: 5,
|
||
height: 26,
|
||
}}
|
||
>
|
||
<Ionicons name="lock-closed" size={15} color={fixed.textOnPrimary} />
|
||
<Text
|
||
style={{
|
||
color: fixed.textOnPrimary,
|
||
fontSize: 15,
|
||
paddingRight: 5,
|
||
}}
|
||
>
|
||
列車追跡中
|
||
</Text>
|
||
<Ionicons name="close" size={15} color={fixed.textOnPrimary} />
|
||
</View>
|
||
|
||
<View
|
||
style={{
|
||
backgroundColor: "#0000",
|
||
width: 6,
|
||
borderLeftColor: "black",
|
||
borderTopColor: "black",
|
||
borderBottomColor: "#0000",
|
||
borderRightColor: "#0000",
|
||
borderBottomWidth: 26,
|
||
borderLeftWidth: 10,
|
||
borderRightWidth: 0,
|
||
borderTopWidth: 0,
|
||
height: 26,
|
||
}}
|
||
/>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
}}
|
||
onPress={() => {
|
||
LayoutAnimation.configureNext({
|
||
duration: 200,
|
||
update: { type: "easeInEaseOut", springDamping: 0.4 },
|
||
});
|
||
if (fixedPositionSize === 226) {
|
||
setFixedPositionSize(mapSwitch == "true" ? 76 : 80);
|
||
} else {
|
||
setFixedPositionSize(226);
|
||
}
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
backgroundColor: "#0000",
|
||
width: 6,
|
||
borderLeftColor: "#0000",
|
||
borderTopColor: "black",
|
||
borderBottomColor: "#0000",
|
||
borderRightColor: "black",
|
||
borderBottomWidth: 26,
|
||
borderLeftWidth: 0,
|
||
borderRightWidth: 10,
|
||
borderTopWidth: 0,
|
||
height: 26,
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
backgroundColor: "black",
|
||
paddingHorizontal: 5,
|
||
height: 26,
|
||
}}
|
||
>
|
||
<Ionicons
|
||
name={fixedPositionSize == 226 ? "chevron-up" : "chevron-down"}
|
||
size={15}
|
||
color="white"
|
||
/>
|
||
<Text
|
||
style={{
|
||
color: "white",
|
||
paddingRight: 5,
|
||
backgroundColor: "black",
|
||
fontSize: 15,
|
||
}}
|
||
>
|
||
{fixedPositionSize == 226 ? "列車情報縮小" : "列車情報展開"}
|
||
</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
const CurrentPositionBox = ({
|
||
train,
|
||
lineColor,
|
||
trainDataWithThrough,
|
||
currentDisplayIndex,
|
||
isSmall,
|
||
}) => {
|
||
const { colors } = useThemeColors();
|
||
let firstText = "";
|
||
let secondText = "";
|
||
let marginText = "";
|
||
const { isBetween, Pos: PosData } = trainPosition(train);
|
||
if (isBetween === true) {
|
||
const { from, to } = PosData;
|
||
firstText = normalizeTrainPosLabel(from);
|
||
secondText = normalizeTrainPosLabel(to);
|
||
marginText = "→";
|
||
} else {
|
||
const { Pos } = PosData;
|
||
if (Pos !== "") {
|
||
firstText = normalizeTrainPosLabel(Pos);
|
||
}
|
||
}
|
||
const delayTime = train?.delay == "入線" ? 0 : parseInt(train?.delay);
|
||
return (
|
||
<View
|
||
style={{
|
||
flex: isSmall ? 1 : 3,
|
||
backgroundColor: colors.background,
|
||
flexDirection: "row",
|
||
}}
|
||
>
|
||
{isSmall && (
|
||
<View style={{ flexDirection: "column" }}>
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.background,
|
||
width: 10,
|
||
borderLeftColor: lineColor,
|
||
borderTopColor: lineColor,
|
||
borderBottomColor: colors.background,
|
||
borderRightColor: colors.background,
|
||
borderTopWidth: 28,
|
||
borderBottomWidth: 0,
|
||
borderLeftWidth: 0,
|
||
borderRightWidth: 10,
|
||
}}
|
||
></View>
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.background,
|
||
width: 10,
|
||
borderLeftColor: colors.background,
|
||
borderTopColor: colors.background,
|
||
borderBottomColor: colors.background,
|
||
borderRightColor: colors.background,
|
||
borderTopWidth: 18,
|
||
borderBottomWidth: 0,
|
||
borderLeftWidth: 0,
|
||
borderRightWidth: 10,
|
||
}}
|
||
></View>
|
||
</View>
|
||
)}
|
||
<ScrollView
|
||
style={{ flex: 1, flexDirection: "row" }}
|
||
horizontal
|
||
overScrollMode="always"
|
||
>
|
||
{trainDataWithThrough.length > 0 &&
|
||
(() => {
|
||
// 着→発ペアを同一駅で統合(EachStopListと同様)
|
||
const merged: { d: string; arrivalTime: string | null }[] = [];
|
||
let mergedCurrentDisplayIndex = Math.max(currentDisplayIndex, 0);
|
||
for (let i = 0; i < trainDataWithThrough.length; i++) {
|
||
const d = trainDataWithThrough[i];
|
||
if (!d) continue;
|
||
const [st, se] = d.split(",");
|
||
if (se?.includes("着") && !se?.includes("発")) {
|
||
const next = trainDataWithThrough[i + 1];
|
||
if (next) {
|
||
const [nextSt, nextSe] = next.split(",");
|
||
if (nextSt === st && nextSe?.includes("発")) {
|
||
// この着エントリは次の発エントリで統合するためスキップ
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
if (se?.includes("発") && i > 0) {
|
||
const prev = trainDataWithThrough[i - 1];
|
||
if (prev) {
|
||
const [prevSt, prevSe, prevTime] = prev.split(",");
|
||
if (prevSt === st && prevSe?.includes("着")) {
|
||
merged.push({ d, arrivalTime: prevTime });
|
||
if (i === currentDisplayIndex || i - 1 === currentDisplayIndex) {
|
||
mergedCurrentDisplayIndex = merged.length - 1;
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
merged.push({ d, arrivalTime: null });
|
||
if (i === currentDisplayIndex) {
|
||
mergedCurrentDisplayIndex = merged.length - 1;
|
||
}
|
||
}
|
||
return merged.map(({ d, arrivalTime }, index) => (
|
||
<EachStopData
|
||
d={d}
|
||
index={index}
|
||
key={d + "FixedTrainBoxEachStopData"}
|
||
delayTime={delayTime}
|
||
isSmall={isSmall}
|
||
secondText={secondText}
|
||
currentDisplayIndex={mergedCurrentDisplayIndex}
|
||
arrivalTime={arrivalTime}
|
||
/>
|
||
));
|
||
})()}
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
type eachStopType = {
|
||
d: string;
|
||
delayTime: number;
|
||
isSmall: boolean;
|
||
index: number;
|
||
secondText: string;
|
||
currentDisplayIndex: number;
|
||
arrivalTime?: string | null;
|
||
};
|
||
|
||
const EachStopData: FC<eachStopType> = (props) => {
|
||
const { colors } = useThemeColors();
|
||
const { playbackCurrentTimeIso } = useTrainMenu();
|
||
const {
|
||
d,
|
||
delayTime,
|
||
isSmall,
|
||
index,
|
||
secondText,
|
||
currentDisplayIndex,
|
||
arrivalTime,
|
||
} = props;
|
||
if (!d) return null;
|
||
if (d == "") return null;
|
||
const [station, se, time] = d.split(",");
|
||
const calcMinute = (t: string) => {
|
||
if (!t || t === "") return null;
|
||
const now = playbackCurrentTimeIso ? dayjs(playbackCurrentTimeIso) : dayjs();
|
||
const hour = parseInt(t.split(":")[0]);
|
||
const dt = now
|
||
.hour(hour < 4 ? hour + 24 : hour)
|
||
.minute(parseInt(t.split(":")[1]));
|
||
let diff = dt.diff(now, "minute") + delayTime;
|
||
if (now.hour() < 4 && hour < 4) diff -= 1440;
|
||
return diff;
|
||
};
|
||
const distanceMinute = calcMinute(time) ?? 0;
|
||
const arrivalMinute = arrivalTime ? calcMinute(arrivalTime) : null;
|
||
return (
|
||
<>
|
||
<View
|
||
style={{
|
||
flexDirection: "column",
|
||
backgroundColor: se.includes("通") ? "#6e6e6e77" : "#6e6e6eff",
|
||
borderRadius: 30,
|
||
marginHorizontal: isSmall ? 2 : 4,
|
||
marginVertical: isSmall ? 0 : 2,
|
||
padding: isSmall ? 2 : 4,
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
overflow: "hidden",
|
||
}}
|
||
key={d + "CurrentPositionBox"}
|
||
>
|
||
{station.split("").map((i, index, array) => {
|
||
return (
|
||
<Text
|
||
key={i + index}
|
||
style={{
|
||
fontSize:
|
||
array.length < 5 ? (isSmall ? 5 : 12) : isSmall ? 3 : 10,
|
||
color: "white",
|
||
margin: 0,
|
||
padding: 0,
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
{i}
|
||
</Text>
|
||
);
|
||
})}
|
||
<View style={{ flex: 1 }} />
|
||
{!isSmall && arrivalMinute != null && (
|
||
<Text
|
||
style={{
|
||
fontSize: 9,
|
||
color: colors.text,
|
||
backgroundColor: colors.background,
|
||
fontWeight: "bold",
|
||
opacity: 0.5,
|
||
}}
|
||
>
|
||
{arrivalMinute}
|
||
</Text>
|
||
)}
|
||
{isSmall ||
|
||
(time != "" && (
|
||
<Text
|
||
style={{
|
||
fontSize: isSmall ? 8 : 12,
|
||
color: colors.text,
|
||
backgroundColor: colors.background,
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
{distanceMinute}
|
||
</Text>
|
||
))}
|
||
<Text
|
||
style={{
|
||
fontSize: isSmall ? 8 : 14,
|
||
color:
|
||
index === currentDisplayIndex && secondText === ""
|
||
? "#ffe852ff"
|
||
: se.includes("通")
|
||
? "#020202ff"
|
||
: "white",
|
||
marginTop: isSmall ? 0 : 3,
|
||
height: isSmall ? "auto" : 17,
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
{index === currentDisplayIndex && secondText === ""
|
||
? "→"
|
||
: se.includes("通")
|
||
? null
|
||
: "●"}
|
||
</Text>
|
||
</View>
|
||
{index === 0 && secondText !== "" && (
|
||
<View
|
||
style={{
|
||
flexDirection: "column",
|
||
backgroundColor: "#0000",
|
||
borderRadius: 10,
|
||
marginHorizontal: isSmall ? 2 : 4,
|
||
padding: isSmall ? 2 : 4,
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
overflow: "hidden",
|
||
}}
|
||
>
|
||
<View style={{ flex: 1 }} />
|
||
<Ionicons
|
||
name="arrow-forward"
|
||
size={isSmall ? 8 : 14}
|
||
color={colors.icon}
|
||
style={{ marginTop: isSmall ? 0 : 3 }}
|
||
/>
|
||
</View>
|
||
)}
|
||
</>
|
||
);
|
||
};
|