278 lines
8.4 KiB
TypeScript
278 lines
8.4 KiB
TypeScript
import dosan from "@/assets/originData/dosan";
|
|
import dosan2 from "@/assets/originData/dosan2";
|
|
import koutoku from "@/assets/originData/koutoku";
|
|
import naruto from "@/assets/originData/naruto";
|
|
import seto from "@/assets/originData/seto";
|
|
import tokushima from "@/assets/originData/tokushima";
|
|
import trainList from "@/assets/originData/trainList";
|
|
import uwajima from "@/assets/originData/uwajima";
|
|
import uwajima2 from "@/assets/originData/uwajima2";
|
|
import yosan from "@/assets/originData/yosan";
|
|
import type { ElesiteData } from "@/types/unyohub";
|
|
|
|
type HeadingDirection = "left" | "right";
|
|
|
|
export type ElesiteLineGroup = {
|
|
key: string;
|
|
lineCode: string | null;
|
|
lineLabel: string | null;
|
|
leftStation: string | null;
|
|
rightStation: string | null;
|
|
timetableUrl: string | null;
|
|
lastReportedAt: string | null;
|
|
entries: ElesiteData[];
|
|
formations: string[];
|
|
formationText: string | null;
|
|
hasFormations: boolean;
|
|
};
|
|
|
|
// マリンライナー(3xxxM)用: JR西日本区間の駅は originData にないのでここで定義
|
|
const MARINE_STATION_SEQUENCE = [
|
|
"岡山", "大元", "備前西市", "妹尾", "早島", "茶屋町", "植松", "木見", "上の町",
|
|
"児島", "坂出", "鴨川", "国分", "端岡", "鬼無", "高松",
|
|
];
|
|
|
|
const STATION_SEQUENCES: string[][] = [
|
|
...[yosan, uwajima, uwajima2, dosan, dosan2, koutoku, tokushima, naruto, seto].map(
|
|
(stations) => stations.map((station) => station.Station_JP),
|
|
),
|
|
MARINE_STATION_SEQUENCE,
|
|
];
|
|
|
|
const uniqueNonEmpty = (values: Array<string | null | undefined>): string[] => {
|
|
const seen = new Set<string>();
|
|
|
|
return values.filter((value): value is string => {
|
|
const normalized = value?.trim();
|
|
if (!normalized || seen.has(normalized)) return false;
|
|
seen.add(normalized);
|
|
return true;
|
|
});
|
|
};
|
|
|
|
const getElesiteQueryParam = (url: string, key: string): string | null => {
|
|
if (!url) return null;
|
|
|
|
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const match = url.match(new RegExp(`[?&]${escapedKey}=([^&#]+)`));
|
|
if (!match) return null;
|
|
|
|
try {
|
|
return decodeURIComponent(match[1].replace(/\+/g, " "));
|
|
} catch {
|
|
return match[1];
|
|
}
|
|
};
|
|
|
|
const getElesiteMatchedTrain = (
|
|
entry: ElesiteData,
|
|
trainNumber: string,
|
|
) =>
|
|
entry.trains?.find(
|
|
(train) => train.train_number.trim() === trainNumber.trim(),
|
|
);
|
|
|
|
const getElesiteLineMeta = (
|
|
entry: ElesiteData,
|
|
trainNumber: string,
|
|
): Omit<ElesiteLineGroup, "entries" | "formations" | "formationText" | "hasFormations" | "lastReportedAt"> => {
|
|
const matchedTrain = getElesiteMatchedTrain(entry, trainNumber);
|
|
const timetableUrl = matchedTrain?.timetable_url?.trim() || null;
|
|
const lineCode = timetableUrl
|
|
? getElesiteQueryParam(timetableUrl, "rosen_code")
|
|
: null;
|
|
const lineLabel = timetableUrl
|
|
? getElesiteQueryParam(timetableUrl, "rosen_name")
|
|
: null;
|
|
const leftStation = entry.formation_config?.left_station?.trim() || null;
|
|
const rightStation = entry.formation_config?.right_station?.trim() || null;
|
|
const fallbackKey = [lineLabel, leftStation, rightStation]
|
|
.filter(Boolean)
|
|
.join("::") || "unknown";
|
|
|
|
return {
|
|
key: lineCode || fallbackKey,
|
|
lineCode,
|
|
lineLabel,
|
|
leftStation,
|
|
rightStation,
|
|
timetableUrl,
|
|
};
|
|
};
|
|
|
|
const getElesiteEntryFormations = (entry: ElesiteData): string[] =>
|
|
uniqueNonEmpty(entry.formation_config?.units?.map((unit) => unit.formation) ?? []);
|
|
|
|
const getRouteEndpoints = (
|
|
trainNumber: string,
|
|
): { firstStation: string; lastStation: string } | null => {
|
|
const diagram = trainList[trainNumber.trim()];
|
|
if (!diagram) return null;
|
|
|
|
const stations = diagram
|
|
.split("#")
|
|
.map((stop) => stop.split(",")[0]?.trim())
|
|
.filter((station): station is string => !!station);
|
|
|
|
if (stations.length < 2) return null;
|
|
|
|
return {
|
|
firstStation: stations[0],
|
|
lastStation: stations[stations.length - 1],
|
|
};
|
|
};
|
|
|
|
const getMatchingStationSequence = (
|
|
leftStation: string,
|
|
rightStation: string,
|
|
firstStation: string,
|
|
lastStation: string,
|
|
): string[] | null =>
|
|
STATION_SEQUENCES.find(
|
|
(sequence) =>
|
|
sequence.includes(leftStation) &&
|
|
sequence.includes(rightStation) &&
|
|
sequence.includes(firstStation) &&
|
|
sequence.includes(lastStation),
|
|
) ??
|
|
STATION_SEQUENCES.find(
|
|
(sequence) =>
|
|
sequence.includes(leftStation) && sequence.includes(rightStation),
|
|
) ??
|
|
null;
|
|
|
|
export const inferElesiteHeadingDirection = (
|
|
entry: ElesiteData,
|
|
trainNumber: string,
|
|
): HeadingDirection | null => {
|
|
const matchedTrain = entry.trains?.find(
|
|
(train) => train.train_number.trim() === trainNumber.trim(),
|
|
);
|
|
const headingTo = matchedTrain?.nav?.heading_to;
|
|
|
|
if (headingTo === "left" || headingTo === "right") {
|
|
return headingTo;
|
|
}
|
|
|
|
const route = getRouteEndpoints(trainNumber);
|
|
if (!route) return null;
|
|
|
|
const leftStation = entry.formation_config?.left_station;
|
|
const rightStation = entry.formation_config?.right_station;
|
|
if (!leftStation || !rightStation) return null;
|
|
|
|
const sequence = getMatchingStationSequence(
|
|
leftStation,
|
|
rightStation,
|
|
route.firstStation,
|
|
route.lastStation,
|
|
);
|
|
if (!sequence) return null;
|
|
|
|
const firstIndex = sequence.indexOf(route.firstStation);
|
|
const lastIndex = sequence.indexOf(route.lastStation);
|
|
const leftIndex = sequence.indexOf(leftStation);
|
|
const rightIndex = sequence.indexOf(rightStation);
|
|
if ([firstIndex, lastIndex, leftIndex, rightIndex].some((index) => index < 0)) {
|
|
return null;
|
|
}
|
|
|
|
const trainDelta = lastIndex - firstIndex;
|
|
const formationDelta = rightIndex - leftIndex;
|
|
if (trainDelta === 0 || formationDelta === 0) return null;
|
|
|
|
return Math.sign(trainDelta) === Math.sign(formationDelta)
|
|
? "right"
|
|
: "left";
|
|
};
|
|
|
|
export const getElesiteLeftSideRank = (
|
|
entry: ElesiteData,
|
|
trainNumber: string,
|
|
): number => {
|
|
const matchedTrain = entry.trains?.find(
|
|
(train) => train.train_number.trim() === trainNumber.trim(),
|
|
);
|
|
if (!matchedTrain?.nav) return 2;
|
|
|
|
const headingDirection = inferElesiteHeadingDirection(entry, trainNumber);
|
|
if (!headingDirection) {
|
|
const isLeftSide =
|
|
(matchedTrain.nav.heading_to === "left") ===
|
|
(matchedTrain.nav.is_leading === true);
|
|
return isLeftSide ? 0 : 1;
|
|
}
|
|
|
|
const isLeftSide =
|
|
(headingDirection === "left") === (matchedTrain.nav.is_leading === true);
|
|
|
|
return isLeftSide ? 0 : 1;
|
|
};
|
|
|
|
export const sortElesiteEntriesByTrainNumber = (
|
|
entries: ElesiteData[],
|
|
trainNumber: string,
|
|
): ElesiteData[] =>
|
|
[...entries].sort(
|
|
(a, b) =>
|
|
getElesiteLeftSideRank(a, trainNumber) -
|
|
getElesiteLeftSideRank(b, trainNumber),
|
|
);
|
|
|
|
export const buildElesiteLineGroups = (
|
|
entries: ElesiteData[],
|
|
trainNumber: string,
|
|
): ElesiteLineGroup[] => {
|
|
const groups = new Map<string, ElesiteData[]>();
|
|
const lineOrder: string[] = [];
|
|
|
|
for (const entry of entries) {
|
|
const key = getElesiteLineMeta(entry, trainNumber).key;
|
|
if (!groups.has(key)) {
|
|
groups.set(key, []);
|
|
lineOrder.push(key);
|
|
}
|
|
groups.get(key)?.push(entry);
|
|
}
|
|
|
|
return lineOrder.map((key) => {
|
|
const groupedEntries = groups.get(key) ?? [];
|
|
const sortedEntries = sortElesiteEntriesByTrainNumber(groupedEntries, trainNumber);
|
|
const lineMetas = sortedEntries.map((entry) => getElesiteLineMeta(entry, trainNumber));
|
|
const formations = uniqueNonEmpty(
|
|
sortedEntries.flatMap((entry) => getElesiteEntryFormations(entry)),
|
|
);
|
|
|
|
return {
|
|
key,
|
|
lineCode: lineMetas.map((meta) => meta.lineCode).find(Boolean) ?? null,
|
|
lineLabel: lineMetas.map((meta) => meta.lineLabel).find(Boolean) ?? null,
|
|
leftStation: lineMetas.map((meta) => meta.leftStation).find(Boolean) ?? null,
|
|
rightStation: lineMetas.map((meta) => meta.rightStation).find(Boolean) ?? null,
|
|
timetableUrl:
|
|
lineMetas.map((meta) => meta.timetableUrl).find(Boolean) ?? null,
|
|
lastReportedAt:
|
|
sortedEntries
|
|
.map((entry) => entry.report_info?.last_reported_at)
|
|
.filter((value): value is string => !!value)
|
|
.sort()
|
|
.at(-1) ?? null,
|
|
entries: sortedEntries,
|
|
formations,
|
|
formationText: formations.length > 0 ? formations.join("+") : null,
|
|
hasFormations: formations.length > 0,
|
|
};
|
|
});
|
|
};
|
|
|
|
export const getElesiteSummaryByTrainNumber = (
|
|
entries: ElesiteData[],
|
|
trainNumber: string,
|
|
): string | null => {
|
|
const lineGroups = buildElesiteLineGroups(entries, trainNumber).filter(
|
|
(group) => group.hasFormations,
|
|
);
|
|
|
|
return lineGroups[0]?.formationText ?? null;
|
|
};
|