89 Commits

Author SHA1 Message Date
harukin-expo-dev-env
87f1cf2b1e DataSourceAccordionCardコンポーネントを追加し、鉄道運用Hubのデータソース設定を改善 2026-03-04 14:55:18 +00:00
harukin-expo-dev-env
c49aeeb331 HUBロゴのSVGおよびPNGファイルを追加し、TrainSourcesPanelおよびTrainDataSourcesコンポーネントでの使用を更新 2026-03-04 14:54:52 +00:00
harukin-expo-dev-env
506dc7157e データ編集権限の取得URLを更新し、レスポンスからの権限情報の取得方法を修正 2026-03-04 08:59:09 +00:00
harukin-expo-dev-env
66f5744d51 鉄道運用Hubに関連するコンポーネントの名称を変更し、カスタム列車データの処理を追加 2026-03-04 07:43:49 +00:00
harukin-expo-dev-env
d4a9c4d7d8 データソース設定コンポーネントに戻るボタンの位置を設定し、条件付きレンダリングの構造を修正 2026-03-03 10:43:33 +00:00
harukin-expo-dev-env
f2d0b060b6 情報ソース設定へのアクセス権限管理機能を追加し、データソースの利用権限を実装 2026-03-03 10:37:18 +00:00
harukin-expo-dev-env
38191be0d3 UnyohubのON/OFF管理機能を追加し、追加ソースがオフの場合の挙動を修正 2026-03-03 09:22:03 +00:00
harukin-expo-dev-env
df2e4145a2 運用情報ソースの表示パネルを追加し、運用hubデータの取得機能を実装 2026-03-03 09:18:36 +00:00
harukin-expo-dev-env
d6ab19d4b1 Merge commit '7b7ec45bfa657c67bd11ccb73c6299e71b1cee6d' into develop 2026-03-03 06:58:29 +00:00
harukin-expo-dev-env
7b7ec45bfa 停止中の点滅アニメーションを動的に注入する機能を追加 2026-03-03 02:52:25 +00:00
harukin-expo-dev-env
a9bb366308 Merge commit '657ee7494bd3109f05ae73c5e8824e12820ecb9c' into develop 2026-03-02 13:33:16 +00:00
harukin-expo-dev-env
657ee7494b ScrollingDescriptionコンポーネントを追加し、テキストをスクロール表示する機能を実装 2026-03-02 12:51:10 +00:00
harukin-expo-dev-env
b60a43f25c setReloadの呼び出しをsetTimeoutで遅延させ、スクリプト実行の完了後に処理を行うように変更 2026-03-02 12:50:56 +00:00
harukin-expo-dev-env
7004eeefad Add station data, train icon mapping, and train type configuration
- Introduced `stationData.ts` to store station information including names, numbers, and features.
- Created `trainIconMap.ts` for mapping train numbers to their respective image URLs, including dynamic URLs for special trains.
- Added `trainTypeConfig.ts` to define display settings for various train types, including colors and labels for different categories.
2026-03-02 12:38:43 +00:00
harukin-expo-dev-env
413ef4acb3 不要なコメントを削除し、列番データの処理を簡素化 2026-03-02 09:37:04 +00:00
harukin-expo-dev-env
7f3a1493ef InjectJavascriptOptionsインターフェースを追加し、injectJavascriptData関数の引数をオブジェクト形式に変更 2026-03-02 09:06:25 +00:00
harukin-expo-dev-env
8e64932a01 useIntervalとwebViewInjectjavascriptでのデータ取得処理を最適化し、localStorageキャッシュを実装。バックグラウンド復帰時にデータを即時再取得する機能を追加。 2026-03-02 05:59:01 +00:00
harukin-expo-dev-env
9036e7a8c1 古いWebViewの互換性向上のため、onclick属性の処理を改善し、イベントの伝播を制御。PopUpMenuとShowTrainTimeInfo関数をObject.definePropertyでロック。 2026-03-02 04:05:48 +00:00
harukin-expo-dev-env
1bf4a6991d Font Awesomeの依存を削除し、インラインSVGに置き換え。全WebView対応のバッジ表示を実装。 2026-03-02 03:40:09 +00:00
harukin-expo-dev-env
03b9080c06 Font Awesomeの非同期読み込みを追加し、lodash依存を削除。軽量な変更検出ユーティリティを実装し、データ取得処理を最適化。 2026-03-02 03:27:22 +00:00
harukin-expo-dev-env
4952e32e65 アイコンの反転表示対応 2026-02-20 17:06:01 +00:00
harukin-expo-dev-env
ff46c6ac8f 各コンポーネントでキャッシュバスティングを実装し、運用hubデータの取得時にタイムスタンプを追加。列車情報の表示をスクロール可能な形式に変更。 2026-02-13 15:49:04 +00:00
harukin-expo-dev-env
70bbb4ed5a Merge commit '0a4c61071dfe53f8669e724d193d5a815b0a2959' into feature/add-train-hub-connection 2026-02-09 04:06:43 +00:00
harukin-expo-dev-env
0a4c61071d Merge commit '7a58a9524a60ad90011596e4c7ba73edab88a093' into develop 2026-02-09 04:06:35 +00:00
harukin-expo-dev-env
7a58a9524a Merge commit '6bcb3fcaf10c324eea7db03c42241ef5c2613294' into patch/6.x 2026-02-09 04:06:28 +00:00
harukin-expo-dev-env
6bcb3fcaf1 運休表示のテキストの簡略化 2026-02-09 04:06:12 +00:00
harukin-expo-dev-env
d921d7f8b6 行き先名の取得ロジックを修正し、表示を列車名から行き先名に変更 2026-02-09 03:55:03 +00:00
harukin-expo-dev-env
0a677c908d 列車運用hubの設定を追加し、データ取得ロジックを実装 2026-02-09 03:42:30 +00:00
harukin-expo-dev-env
a42c0871bd unyohub連携仮作成 2026-02-07 17:19:16 +00:00
harukin-expo-dev-env
4eea97ed1f LEDの行き先表示に運休表示を追加 2026-02-07 12:29:19 +00:00
harukin-expo-dev-env
5b0de88218 時刻表テキストの結合条件の整理 2026-02-07 12:28:39 +00:00
harukin-expo-dev-env
765b0d72b7 Merge commit '09c00202247c0c97f1d8c324c2cc49214eee1393' into develop 2026-02-07 09:01:56 +00:00
harukin-expo-dev-env
09c0020224 6.2.1 release 2026-02-07 09:01:48 +00:00
harukin-expo-dev-env
57278443e2 Merge commit '35810c8b8acc9624573d2b18e671a9ed3da1d92e' into patch/6.x 2026-02-07 09:00:38 +00:00
harukin-expo-dev-env
35810c8b8a to_dataのテキスト関係の微修正 2026-02-07 08:59:54 +00:00
harukin-expo-dev-env
34b83f62b0 to_dataおよびto_data_colorを優先して列車名と色を設定する機能を追加 2026-02-07 08:47:25 +00:00
harukin-expo-dev-env
ec947ab4ec optionalTextに「最終」がある場合に赤色に 2026-02-07 08:47:03 +00:00
harukin-expo-dev-env
f019725da8 休編の拡張 2026-02-07 08:20:18 +00:00
harukin-expo-dev-env
63e5e06520 数字修正 2026-02-03 04:48:59 +00:00
harukin-expo-dev-env
b653ab8b5b 複数色に対応したグラデーション生成機能を追加 2026-02-02 14:20:08 +00:00
harukin-expo-dev-env
0e9b049707 Merge commit '935b63f6cee9d84530a5e7f6e8f55bcc90f8a168' into develop 2026-02-01 13:18:43 +00:00
harukin-expo-dev-env
935b63f6ce 6.2-release 2026-02-01 13:18:35 +00:00
harukin-expo-dev-env
673bd116cd Merge commit 'c30beca2982c84604a8baa74cd3c2012e6b44502' into patch/6.x 2026-02-01 10:21:21 +00:00
harukin-expo-dev-env
c30beca298 Merge commit 'efd260ca7285cb1017c92a386c5f563730339675' into develop 2026-02-01 10:21:08 +00:00
harukin-expo-dev-env
efd260ca72 release準備 2026-02-01 10:20:51 +00:00
harukin-expo-dev-env
e5dd334552 新時刻表ボタンをアクティベート 2026-02-01 09:29:51 +00:00
harukin-expo-dev-env
76a42c66c7 公開に向けた準備 2026-02-01 08:28:34 +00:00
harukin-expo-dev-env
e7fe41d654 GeneralWebViewを戻るボタンで操作可能に 2026-01-25 16:06:25 +00:00
harukin-expo-dev-env
f16296c56f 連を増に修正 2026-01-25 15:53:11 +00:00
harukin-expo-dev-env
a760e1343e .のついた駅名が表示されていたバグを修正 2026-01-22 03:29:37 +00:00
harukin-expo-dev-env
1591819d1c Merge commit '018352daef4ff3d63a4ea63df0101b3ef357a5b5' into feature/timetable-edit 2025-12-31 16:08:21 +00:00
harukin-expo-dev-env
018352daef Merge commit 'ef81c1f4cdc325f66a55b9f06212190dbec5be24' into develop 2025-12-31 16:07:58 +00:00
harukin-expo-dev-env
ef81c1f4cd 日付を超えたときに終電前の列車が正常に表示できないバグを修正 2025-12-31 16:07:44 +00:00
harukin-expo-dev-env
224a5e2c0a Merge commit 'edf3685fc40472529c6283d98d052d895d729fd3' into feature/timetable-edit 2025-12-31 09:55:27 +00:00
harukin-expo-dev-env
edf3685fc4 Merge commit 'bb1ee2666e5c91819bc3330128a9636b5e2ad753' into develop 2025-12-31 09:52:26 +00:00
harukin-expo-dev-env
bb1ee2666e 6.1.9.4 2025-12-31 09:51:57 +00:00
harukin-expo-dev-env
5b715f2dc5 レイアウト整理 2025-12-31 09:50:22 +00:00
harukin-expo-dev-env
2888c41301 増解結の暫定的対応を作成 2025-12-31 08:57:58 +00:00
harukin-expo-dev-env
2a70a0e34b 列車の進行方向情報を修正 2025-12-31 06:46:38 +00:00
harukin-expo-dev-env
388865f898 Merge commit '4618e2e1b94c20b45a415373ee0ef51f5932f1d4' into feature/timetable-edit 2025-12-30 02:47:50 +00:00
harukin-expo-dev-env
4618e2e1b9 Merge commit 'f9c6e6dd9764cee4e2865c1f3c035fb3da08d06a' into develop 2025-12-30 02:47:45 +00:00
harukin-expo-dev-env
f9c6e6dd97 Merge commit '3b85ab9776eb449cc0fd6cd3f7162f330366da5c' into patch/6.x 2025-12-30 02:47:40 +00:00
harukin-expo-dev-env
3b85ab9776 本家モードでアイコンの表示を調整 2025-12-29 18:40:06 +00:00
harukin-expo-dev-env
58c1b93ac8 最適化 2025-12-29 07:39:03 +00:00
harukin-expo-dev-env
48f753815f レイアウト修正 2025-12-28 16:56:20 +00:00
harukin-expo-dev-env
a425a6ae46 Merge commit '56bb22247671a67f1d37a59586f2027d5d836c17' into feature/timetable-edit 2025-12-28 16:09:36 +00:00
harukin-expo-dev-env
56bb222476 Merge commit 'fa1eec45695df37f511fcd0f5eb4e88cede4adbc' into develop 2025-12-28 16:00:02 +00:00
harukin-expo-dev-env
fa1eec4569 Merge commit 'fd3e488c34d22225fb4bd52bf16c15c81ae8ccbe' into patch/6.x 2025-12-28 15:59:27 +00:00
harukin-expo-dev-env
fd3e488c34 アイコンを最初と最後だけ表示するように修正 2025-12-28 15:56:27 +00:00
harukin-expo-dev-env
ffba7a5380 ifの条件ミスを修正 2025-12-28 13:20:01 +00:00
harukin-expo-dev-env
a0bbc7c80b LEDのレイアウト大改編 2025-12-28 09:49:17 +00:00
harukin-expo-dev-env
47d020d30c テキストカラーの修正 2025-12-28 07:28:45 +00:00
harukin-expo-dev-env
696d00032e 乗り場情報を追加 2025-12-27 14:41:21 +00:00
harukin-expo-dev-env
34ac8c9a97 時刻表のsimpleGridモード作成 2025-12-27 13:49:47 +00:00
harukin-expo-dev-env
1e15f119c4 Merge commit 'cb25845e8a8e254e939e10095955e6b8475afaf2' into feature/timetable-edit 2025-12-27 11:10:06 +00:00
harukin-expo-dev-env
cb25845e8a Merge commit 'f475680665e5480cb6b8b8075139829073fd1fe1' into develop 2025-12-27 11:10:00 +00:00
harukin-expo-dev-env
f475680665 Merge commit 'b6dd05419ba07bc5ceb1a779ced0e2dd836195b2' into patch/6.x 2025-12-27 11:09:53 +00:00
harukin-expo-dev-env
b6dd05419b アイコンの本家モードでの挙動修正 2025-12-27 11:08:39 +00:00
harukin-expo-dev-env
0d7658eba1 アイコンが無いときに表示されているバグの修正(未確認) 2025-12-27 09:57:15 +00:00
harukin-expo-dev-env
07a0c6eaf6 Merge commit '02ee9fca7e180a13d2d1d41c211a5b4c7a18972d' into feature/timetable-edit 2025-12-20 10:16:20 +00:00
harukin-expo-dev-env
02ee9fca7e Merge commit '0bf345ff6ac492e66da91e71db0118adb58cb28a' into develop 2025-12-20 10:16:05 +00:00
harukin-expo-dev-env
0bf345ff6a Merge commit '342afea34c6fafa8e420501640e50f2349a9cf03' into patch/6.x 2025-12-20 10:14:47 +00:00
harukin-expo-dev-env
342afea34c 6.1.9.3 2025-12-20 10:14:40 +00:00
harukin-expo-dev-env
f8dfa77e97 ブラウザ側に複数アイコン対応 2025-12-20 10:04:30 +00:00
harukin-expo-dev-env
f261ff981a アプリ側に複数アイコン化対応 2025-12-20 10:04:14 +00:00
harukin-expo-dev-env
b40280d099 列車の運用無効化対応 2025-12-20 06:17:01 +00:00
harukin-expo-dev-env
8201573309 停車乗り場と列車走行位置アイコンの最適化 2025-12-19 14:35:59 +00:00
harukin-expo-dev-env
d4c5851ed9 Merge commit '9aed4e04e10f3ba11d23954a3ff42dda72ce0362' into feature/timetable-edit 2025-12-12 19:28:46 +00:00
harukin-expo-dev-env
14c5a66f08 クラッシュするバグを修正 2025-12-05 17:50:25 +00:00
53 changed files with 4410 additions and 1140 deletions

View File

@@ -1,8 +1,8 @@
import React, { CSSProperties } from "react";
import { View, ViewProps } from "react-native";
import { Alert, BackHandler, View, ViewProps } from "react-native";
import { WebView } from "react-native-webview";
import { BigButton } from "./components/atom/BigButton";
import { useNavigation } from "@react-navigation/native";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
export default ({ route }) => {
if (!route.params) {
return null;
@@ -10,12 +10,38 @@ export default ({ route }) => {
const { uri, useExitButton = true } = route.params;
const { goBack } = useNavigation();
const webViewRef = React.useRef<WebView>(null);
const [canGoBack, setCanGoBack] = React.useState(false);
useFocusEffect(
React.useCallback(() => {
const onHardwareBack = () => {
if (canGoBack) {
webViewRef.current?.goBack();
return true;
}
goBack();
return true;
};
BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
return () => BackHandler.removeEventListener("hardwareBackPress", onHardwareBack);
}, [canGoBack, goBack])
);
return (
<View style={styles}>
<WebView
source={{ uri }}
allowsBackForwardNavigationGestures
ref={webViewRef}
onNavigationStateChange={(navState) => {
setCanGoBack(navState.canGoBack);
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
goBack();
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
{ text: "完了" },
]);
}
}}
onMessage={(event) => {
const { data } = event.nativeEvent;
const { type } = JSON.parse(data);

BIN
assets/icons/hub_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,64 @@
import React, { FC } from "react";
import { View, Text, TouchableWithoutFeedback, Alert } from "react-native";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
export const DataConnectedButton: FC<{
i: string;
openTrainInfo: (trainNum: string) => void;
}> = ({ i, openTrainInfo }) => {
const [station, se, time] = i.split(",");
const { keyList } = useAllTrainDiagram();
// 列番が有効かどうかをチェックする関数
const isValidTrainNumber = (trainNum: string): boolean => {
return keyList.includes(trainNum);
};
return (
<TouchableWithoutFeedback
onPress={() => {
// timeの文字列が列番として有効かを検証する
if (!isValidTrainNumber(time)) {
Alert.alert(
"列番が見つかりません",
`列番「${time}」は時刻表に存在しません。`,
[{ text: "OK" }]
);
return;
}
openTrainInfo(time);
}}
key={station + time}
>
<View style={{ flexDirection: "row", backgroundColor: "#f5f5f5" }}>
<View
style={{
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
flex: 1,
}}
>
<View
style={{
width: 35,
position: "relative",
marginHorizontal: 15,
flexDirection: "row",
height: "10%",
}}
/>
<Text style={{ fontSize:16, fontFamily: "DiaPro" }}>
{se === "増" ? "⬐" : "↳"}
</Text>
<Text style={{ fontSize: 20, color: "#0000EE" }}>{time}</Text>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 18, width: 50 }}>
{se === "増" ? "増結" : "解結"}
</Text>
</View>
</View>
</TouchableWithoutFeedback>
);
};

View File

@@ -1,36 +0,0 @@
import React, { FC } from "react";
import { View, Text, TouchableWithoutFeedback } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { Linking } from "react-native";
export const DataFromButton: FC<{ i: string }> = ({ i }) => {
const [station, se, time] = i.split(",");
return (
<TouchableWithoutFeedback
onPress={() => Linking.openURL(time)}
key={station}
>
<View style={{ flexDirection: "row" }}>
<View
style={{
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
flex: 1,
}}
>
<Text style={{ fontSize: 20 }}>{station}</Text>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 18 }}>
<MaterialCommunityIcons
name={"open-in-new"}
color="black"
size={20}
/>
</Text>
</View>
</View>
</TouchableWithoutFeedback>
);
};

View File

@@ -4,11 +4,11 @@ import dayjs from "dayjs";
import lineColorList from "../../../assets/originData/lineColorList";
import { trainDataType } from "@/lib/trainPositionTextArray";
import { getStopListColors } from "./colorScheme";
import {
isCanceledSe,
isThroughSe,
import {
isCanceledSe,
isThroughSe,
isCommunitySe,
parseSeString
parseSeString,
} from "@/utils/seUtils";
import type { SeTypes } from "@/types";
@@ -20,7 +20,13 @@ type seTypes =
| "発"
| "着"
| "休編"
| "休発"
| "休着"
| "休発編"
| "休着編"
| "通休編"
| "通発休編"
| "通着休編"
| string;
type currentTrainDataType = {
@@ -57,23 +63,36 @@ export const EachStopList: FC<props> = ({
array,
isNotService = false,
}) => {
const [station, se, time] = i.split(",") as [string, seTypes, string]; // 阿波池田,発,6:21
const [station, se, time, platformNum] = i.split(",") as [
string,
seTypes,
string,
string
]; // 阿波池田,発,6:21,1
let beforeSameStationData = null;
if ((se.includes("発") || se.includes("休")) && index > 0) {
const beforeData = array[index - 1].split(",") as [string, seTypes, string];
if (beforeData[0] == station) {
beforeSameStationData = beforeData;
// 発(通常発・休発・休発編)の場合、前の着(通常着・休着・休着編)と統合する
if (se.includes("")) {
if (index > 0) {
const beforeData = array[index - 1].split(",") as [string, seTypes, string];
// 前が着(通常着でも休着でも)の場合は統合
if (beforeData[0] == station && beforeData[1].includes("着")) {
beforeSameStationData = beforeData;
}
}
}
let afterSameStationData = null;
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
if (se.includes("着")) {
const afterData = array[index + 1]?.split(",") as [string, seTypes, string];
if (afterData && afterData[0] == station) {
// 次が発(通常発でも休発でも)なら、この着を非表示にして次の発で両方表示
if (afterData && afterData[0] == station && afterData[1].includes("発")) {
afterSameStationData = afterData;
return <></>;
}
}
if(station.includes(".")){
return <></>;
}
if (!showThrew) {
if (se == "通過") return null;
if (se == "通編") return null;
@@ -96,9 +115,9 @@ export const EachStopList: FC<props> = ({
"StationNumber": "B01",
},
] */
const StationNumbers = Stations
.filter((d) => d.StationNumber != null)
.map((d) => d.StationNumber as string);
const StationNumbers = Stations.filter((d) => d.StationNumber != null).map(
(d) => d.StationNumber as string
);
// SE文字列を表示用に変換
const [seString, seType] = parseSeString(se);
@@ -106,26 +125,51 @@ export const EachStopList: FC<props> = ({
const isThrough = isThroughSe(se);
const isCanceled = isCanceledSe(se);
const isCommunity = isCommunitySe(se);
const isDelayed = currentTrainData?.delay !== undefined &&
currentTrainData?.delay !== "入線" &&
currentTrainData?.delay > 0;
const colors = getStopListColors(isThrough, isCommunity, isCanceled, isDelayed, isNotService);
const isDelayed =
currentTrainData?.delay !== undefined &&
currentTrainData?.delay !== "入線" &&
currentTrainData?.delay > 0;
const colors = getStopListColors(
isThrough,
isCommunity,
isCanceled,
isDelayed,
isNotService
);
// 打ち消し線用の通常色(遅延していない時の色)
const normalColors = getStopListColors(isThrough, isCommunity, isCanceled, false, isNotService);
const normalColors = getStopListColors(
isThrough,
isCommunity,
isCanceled,
false,
isNotService
);
// beforeSameStationData用の色設定
// 通過系と編コミュニティはbeforeのseから判定、休運休は現在のseから判定
let beforeTimeTextColor = colors.timeText;
let beforeNormalTimeTextColor = normalColors.timeText;
if (beforeSameStationData) {
const beforeSe = beforeSameStationData[1];
const beforeIsThrough = isThroughSe(beforeSe);
const beforeIsCommunity = isCommunitySe(beforeSe);
// 運休判定は現在のseを使用本体と同じ背景色なので
const beforeColors = getStopListColors(beforeIsThrough, beforeIsCommunity, isCanceled, isDelayed, isNotService);
const beforeNormalColors = getStopListColors(beforeIsThrough, beforeIsCommunity, isCanceled, false, isNotService);
const beforeColors = getStopListColors(
beforeIsThrough,
beforeIsCommunity,
isCanceled,
isDelayed,
isNotService
);
const beforeNormalColors = getStopListColors(
beforeIsThrough,
beforeIsCommunity,
isCanceled,
false,
isNotService
);
beforeTimeTextColor = beforeColors.timeText;
beforeNormalTimeTextColor = beforeNormalColors.timeText;
}
@@ -136,7 +180,7 @@ export const EachStopList: FC<props> = ({
openStationACFromEachTrainInfo &&
openStationACFromEachTrainInfo(station)
}
key={station+se+time}
key={station + se + time}
>
<View
style={{
@@ -158,6 +202,32 @@ export const EachStopList: FC<props> = ({
<StationNumbersBox stn={stn} se={se} key={index} />
))}
</View>
<View
style={{
position: "relative",
flexDirection: "column",
width: 0,
height: "100%",
alignItems: "flex-end",
}}
>
<View style={{ flex: 1 }} />
<View style={{ position: "relative", height: 25 }}>
<Text
style={{
fontSize: 20,
position: "absolute",
textAlignVertical: "center",
left: -15,
top: "auto",
}}
>
{points && "🚊"}
</Text>
</View>
<View style={{ flex: 1 }} />
</View>
<View
style={{
padding: 8,
@@ -177,21 +247,15 @@ export const EachStopList: FC<props> = ({
textAlignVertical: "center",
}}
>
{station}
{station}{platformNum &&
(parseInt(platformNum) === 0
? "⓪"
: String.fromCharCode(0x2460 + parseInt(platformNum) - 1))}
</Text>
<View style={{ flex: 1 }} />
</View>
<View style={{ flex: 1 }} />
<View style={{ position: "relative", width: 0, alignItems: "flex-end" }}>
{points && (
<Text style={{ fontSize: 20, position: "absolute", left: -70 }}>
🚊
</Text>
)}
</View>
<View
style={{ flexDirection: "column", alignItems: "flex-end" }}
>
<View style={{ flexDirection: "column", alignItems: "flex-end" }}>
{beforeSameStationData && (
<TimeText
isDouble={!!beforeSameStationData || !!afterSameStationData}
@@ -199,7 +263,7 @@ export const EachStopList: FC<props> = ({
currentTrainData={currentTrainData}
se={beforeSameStationData[1]}
time={beforeSameStationData[2]}
key={"before"+beforeSameStationData[2]}
key={"before" + beforeSameStationData[2]}
textColor={beforeTimeTextColor}
normalTextColor={beforeNormalTimeTextColor}
/>
@@ -209,7 +273,7 @@ export const EachStopList: FC<props> = ({
currentTrainData={currentTrainData}
se={se}
time={time}
key={se+time}
key={se + time}
textColor={colors.timeText}
normalTextColor={normalColors.timeText}
/>
@@ -228,9 +292,17 @@ const TimeText: FC<{
time: string;
textColor: string;
normalTextColor: string;
}> = ({ isDouble, currentTrainData, se, time, isBefore=false, textColor, normalTextColor }) => {
}> = ({
isDouble,
currentTrainData,
se,
time,
isBefore = false,
textColor,
normalTextColor,
}) => {
const [seString, seType] = parseSeString(se);
return (
<View style={{ flexDirection: "row", alignItems: "center" }}>
{!!currentTrainData?.delay &&
@@ -243,7 +315,7 @@ const TimeText: FC<{
color: normalTextColor,
width: 60,
position: "absolute",
right: isBefore ? 125:120,
right: isBefore ? 125 : 120,
textAlign: "right",
textDecorationLine: "line-through",
fontStyle: seType == "community" ? "italic" : "normal",
@@ -325,7 +397,7 @@ const StationTimeBox: FC<StationTimeBoxType> = (props) => {
.set("hour", parseInt(time.split(":")[0]))
.set("minute", parseInt(time.split(":")[1]))
.add(delay == "入線" || delay == undefined ? 0 : delay, "minute");
return (
<Text
style={{

View File

@@ -13,7 +13,7 @@ import { getTrainType } from "../../lib/getTrainType";
import { customTrainDataDetector } from "../custom-train-data";
import { useDeviceOrientationChange } from "../../stateBox/useDeviceOrientationChange";
import { EachStopList } from "./EachTrainInfo/EachStopList";
import { DataFromButton } from "./EachTrainInfo/DataFromButton";
import { DataConnectedButton } from "./EachTrainInfo/DataConnectedButton";
import { DynamicHeaderScrollView } from "../DynamicHeaderScrollView";
import { LongHeader } from "./EachTrainInfo/LongHeader";
import { ShortHeader } from "./EachTrainInfo/ShortHeader";
@@ -47,8 +47,8 @@ export const EachTrainInfoCore = ({
const { isLandscape } = useDeviceOrientationChange();
const scrollHandlers = actionSheetRef
//@ts-ignore
? useScrollHandlers("scrollview-1", actionSheetRef)
? //@ts-ignore
useScrollHandlers("scrollview-1", actionSheetRef)
: null;
// Custom hooks for data management
const { trainData, setTrainData, trueTrainID } = useTrainDiagramData(
@@ -244,8 +244,14 @@ export const EachTrainInfoCore = ({
</TouchableOpacity>
)}
{trainDataWithThrough.map((item, index, array) =>
item.split(",")[1] === "" ? (
<DataFromButton i={item} key={`${item}-data`} />
item.split(",")[1] === "増" || item.split(",")[1] === "解" ? (
<DataConnectedButton
i={item}
key={`${item}-data`}
openTrainInfo={openTrainInfo}
/>
) : item.split(",")[1].includes(".") ? (
<></>
) : (
<EachStopList
i={item}

View File

@@ -1,4 +1,4 @@
import React, { CSSProperties, FC, useEffect, useMemo, useState } from "react";
import React, { FC, useMemo } from "react";
import {
Text,
View,
@@ -9,7 +9,6 @@ import {
import { SheetManager } from "react-native-actions-sheet";
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
import { TrainIconStatus } from "./trainIconStatus";
import { TrainViewIcon } from "./trainViewIcon";
import { OneManText } from "./HeaderTextParts/OneManText";
import { customTrainDataDetector } from "@/components/custom-train-data";
import { InfogramText } from "@/components/ActionSheetComponents/EachTrainInfoCore/HeaderTextParts/InfogramText";
@@ -17,10 +16,10 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { useNotification } from "@/stateBox/useNotifications";
import { getStringConfig } from "@/lib/getStringConfig";
import { FontAwesome, MaterialCommunityIcons } from "@expo/vector-icons";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { getPDFViewURL } from "@/lib/getPdfViewURL";
import { widthPercentageToDP } from "react-native-responsive-screen";
import type { NavigateFunction } from "@/types";
import { useUnyohub } from "@/stateBox/useUnyohub";
type Props = {
data: { trainNum: string; limited: string };
@@ -59,6 +58,14 @@ export const HeaderText: FC<Props> = ({
const { allCustomTrainData, getTodayOperationByTrainId } =
useAllTrainDiagram();
const { expoPushToken } = useNotification();
const { getUnyohubByTrainNumber, getUnyohubEntriesByTrainNumber, useUnyohub: unyohubEnabled } = useUnyohub();
// 追加ソースのON/OFFをここで管理将来ソースが増えたらここに足す
const additionalSources = {
unyohub: unyohubEnabled,
// exampleSource: exampleEnabled,
};
const hasAdditionalSources = Object.values(additionalSources).some(Boolean);
// 列車名、種別、フォントの取得
const [
@@ -70,7 +77,10 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
trainInfoUrl,
directions,
customTrainData,
] = useMemo(() => {
const result = customTrainDataDetector(trainNum, allCustomTrainData);
const {
type,
train_name,
@@ -80,15 +90,14 @@ export const HeaderText: FC<Props> = ({
uwasa,
train_info_url,
to_data,
} = customTrainDataDetector(trainNum, allCustomTrainData);
directions,
} = result;
const [typeString, fontAvailable, isOneMan] = getStringConfig(
type,
trainNum
trainNum,
);
switch (true) {
case train_name !== "":
// 特急の場合は、列車名を取得
// 列番対称データがある場合はそれから列車番号を取得
return [
typeString,
train_name +
@@ -101,6 +110,8 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
case trainData[trainData.length - 1] === undefined:
return [
@@ -112,9 +123,10 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
case to_data && to_data !== "":
// 行先がある場合は、行先を取得
return [
typeString,
to_data + "行き",
@@ -124,13 +136,14 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
default:
// 行先がある場合は、行先を取得
return [
typeString,
migrateTrainName(
trainData[trainData.length - 1].split(",")[0] + "行き"
trainData[trainData.length - 1].split(",")[0] + "行き",
),
fontAvailable,
isOneMan,
@@ -138,11 +151,28 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
}
}, [trainData]);
const todayOperation = getTodayOperationByTrainId(trainNum);
const todayOperation = getTodayOperationByTrainId(trainNum).filter(
(d) => d.state !== 100,
);
let iconTrainDirection =
parseInt(trainNum.replace(/[^\d]/g, "")) % 2 == 0 ? true : false;
if (directions != undefined) {
iconTrainDirection = directions ? true : false;
}
const unyohubFormation = getUnyohubByTrainNumber(trainNum);
const unyohubEntries = getUnyohubEntriesByTrainNumber(trainNum);
const hasExtraInfo =
priority > 200 || todayOperation?.length > 0 || !!unyohubFormation;
return (
<View
style={{ padding: 10, flexDirection: "row", alignItems: "center" }}
@@ -155,6 +185,7 @@ export const HeaderText: FC<Props> = ({
navigate={navigate}
from={from}
todayOperation={todayOperation}
direction={iconTrainDirection}
/>
<TouchableOpacity
style={{
@@ -210,21 +241,6 @@ export const HeaderText: FC<Props> = ({
/>
)} */}
</TouchableOpacity>
{(priority > 200 || todayOperation?.length > 0) && (
<FontAwesome
name="commenting-o"
size={20}
color="white"
style={{ marginLeft: 5 }}
onPress={() =>
alert(
`[このアイコン、列車データはコミュニティによってリアルタイム追加されています。]\n使用車両情報:\n${todayOperation
?.map((op) => op.unit_ids)
.join("+")}\n投稿者メモ:\n${uwasa || "なし"}`
)
}
/>
)}
<View style={{ flex: 1 }} />
<TouchableOpacity
@@ -242,8 +258,37 @@ export const HeaderText: FC<Props> = ({
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
</Text>
</TouchableOpacity>
<TrainViewIcon {...{ data, navigate, from }} />
<MaterialCommunityIcons
name="database"
color={hasExtraInfo ? "yellow" : "white"}
size={30}
style={{ margin: 5 }}
onPress={() => {
if (hasAdditionalSources) {
(SheetManager.show as any)("TrainDataSources", {
payload: {
trainNum,
unyohubEntries,
todayOperation,
navigate,
expoPushToken,
priority,
direction: iconTrainDirection,
customTrainData,
typeName,
trainName,
departureStation: trainData[0]?.split(",")[0] ?? "",
destinationStation: trainData[trainData.length - 1]?.split(",")[0] ?? "",
},
});
} else {
// 追加ソースが全てオフ → 元の挙動(直接 DB ページを開く)
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
}
}}
/>
</View>
);
};

View File

@@ -0,0 +1,539 @@
import React, { FC, useMemo } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
Image,
} from "react-native";
import { MaterialCommunityIcons, Ionicons } from "@expo/vector-icons";
import { SheetManager } from "react-native-actions-sheet";
import type { NavigateFunction } from "@/types";
import type { OperationLogs } from "@/lib/CommonTypes";
import type { UnyohubData } from "@/types/unyohub";
const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png");
type Props = {
trainNum: string;
unyohubEntries: UnyohubData[];
todayOperation: OperationLogs[];
navigate: NavigateFunction;
expoPushToken: string;
onClose: () => void;
/** true = 上り (vehicle_img) / false = 下り (vehicle_img_right) */
direction: boolean;
};
/** 情報ソース別カードを並べて表示するパネル */
export const TrainSourcesPanel: FC<Props> = ({
trainNum,
unyohubEntries,
todayOperation,
navigate,
expoPushToken,
onClose,
direction,
}) => {
// 連結番号train_ids のカンマ後数値)で昇順ソート
const sortedTodayOperation = useMemo(() => {
const extractOrder = (trainId: string): number => {
const parts = trainId.split(",");
if (parts.length > 1) {
const n = parseInt(parts[1].trim(), 10);
return isNaN(n) ? Infinity : n;
}
return Infinity;
};
const findId = (op: OperationLogs): string | null => {
for (const id of [...(op.train_ids ?? []), ...(op.related_train_ids ?? [])]) {
if (id.split(",")[0] === trainNum) return id;
}
return null;
};
return [...todayOperation].sort((a, b) => {
const aId = findId(a);
const bId = findId(b);
if (!aId || !bId) return aId ? -1 : bId ? 1 : 0;
return extractOrder(aId) - extractOrder(bId);
});
}, [todayOperation, trainNum]);
return (
<View style={styles.container}>
{/* ヘッダー */}
<View style={styles.panelHeader}>
<Text style={styles.panelHeaderText}></Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Ionicons name="close" size={20} color="#0099CC" />
</TouchableOpacity>
</View>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* ──────────────────────────────────────── */}
{/* JR四国データベース常に表示 */}
{/* ──────────────────────────────────────── */}
<SourceCard
iconName="database"
iconColor="#0099CC"
tag="公式"
tagColor="#0099CC"
title="JR四国データベース"
description="コミュニティが管理する列車データ。編成・運用の詳細を確認・編集できます。"
available
onPress={() => {
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
onClose();
}}
/>
{/* ──────────────────────────────────────── */}
{/* 鉄道運用Hub */}
{/* ──────────────────────────────────────── */}
{unyohubEntries.length > 0 ? (
<>
<SectionLabel label="鉄道運用Hub" imagePng={HUB_LOGO_PNG} />
{unyohubEntries.map((entry, i) => (
<UnyohubCard key={`unyo-${i}`} entry={entry} trainNum={trainNum} navigate={navigate} onClose={onClose} />
))}
</>
) : (
<SourceCard
iconImagePng={HUB_LOGO_PNG}
iconColor="#333"
tag="運用Hub"
tagColor="#555"
title="鉄道運用Hub"
description="この列車の運用データはありません。"
available={false}
/>
)}
{/* ──────────────────────────────────────── */}
{/* 今日の運用情報 */}
{/* ──────────────────────────────────────── */}
{sortedTodayOperation.length > 0 ? (
<>
<SectionLabel label="今日の運用情報" icon="history" />
<DirectionBanner direction={direction} />
{sortedTodayOperation.map((op, i) => (
<OperationCard
key={`op-${i}`}
op={op}
index={i}
direction={direction}
navigate={navigate}
onClose={onClose}
/>
))}
</>
) : (
<SourceCard
iconName="history"
iconColor="#aaa"
tag="記録"
tagColor="#aaa"
title="今日の運用情報"
description="本日の運用記録はありません。"
available={false}
/>
)}
</ScrollView>
</View>
);
};
/* ------------------------------------------------------------------ */
/* サブコンポーネント */
/* ------------------------------------------------------------------ */
/** セクションラベル */
const SectionLabel: FC<{ label: string; icon?: string; imagePng?: any }> = ({ label, icon, imagePng }) => (
<View style={styles.sectionLabel}>
{imagePng ? (
<Image source={imagePng} style={{ width: 14, height: 14 }} />
) : (
<MaterialCommunityIcons name={(icon ?? "information") as any} size={14} color="#888" />
)}
<Text style={styles.sectionLabelText}>{label}</Text>
</View>
);
/** 汎用ソースカード */
const SourceCard: FC<{
iconName?: string;
iconImagePng?: any;
iconColor: string;
tag: string;
tagColor: string;
title: string;
description: string;
available: boolean;
onPress?: () => void;
}> = ({ iconName, iconImagePng, iconColor, tag, tagColor, title, description, available, onPress }) => (
<TouchableOpacity
style={[styles.card, !available && styles.cardDisabled]}
onPress={available ? onPress : undefined}
activeOpacity={available ? 0.7 : 1}
>
<View style={[styles.cardIconWrap, { backgroundColor: iconColor + "22" }]}>
{iconImagePng ? (
<Image source={iconImagePng} style={{ width: 24, height: 24 }} />
) : (
<MaterialCommunityIcons name={(iconName ?? "information") as any} size={22} color={iconColor} />
)}
</View>
<View style={styles.cardBody}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, !available && styles.cardTitleDisabled]}>{title}</Text>
<View style={[styles.tag, { backgroundColor: tagColor + "33" }]}>
<Text style={[styles.tagText, { color: tagColor }]}>{tag}</Text>
</View>
</View>
<Text style={styles.cardDesc}>{description}</Text>
</View>
{available && (
<MaterialCommunityIcons name="chevron-right" size={18} color="#ccc" />
)}
</TouchableOpacity>
);
/** 鉄道運用Hubカード1編成分 */
const UnyohubCard: FC<{
entry: UnyohubData;
trainNum: string;
navigate: NavigateFunction;
onClose: () => void;
}> = ({ entry, trainNum, navigate, onClose }) => {
const matchedTrain = entry.trains?.find((t) => t.train_number === trainNum);
const positionText = matchedTrain
? `位置: ${matchedTrain.position_forward}${matchedTrain.position_rear}両目`
: null;
const carInfo = `${entry.car_count}両 (${entry.min_car_count}${entry.max_car_count}両)`;
const timeInfo =
entry.starting_time && entry.ending_time
? `${entry.starting_time}${entry.ending_time}`
: null;
return (
<TouchableOpacity
style={styles.card}
activeOpacity={0.7}
onPress={() => {
// 編成番号+列番でページを開く将来的にunyohub公式ページへの対応も想定
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?formation=${encodeURIComponent(entry.formations)}&source=unyohub`;
navigate("generalWebView", { uri, useExitButton: true });
SheetManager.hide("EachTrainInfo");
onClose();
}}
>
{/* 編成色インジケーター */}
<View
style={[
styles.cardIconWrap,
{ backgroundColor: "#33333318" },
]}
>
<Image source={HUB_LOGO_PNG} style={{ width: 24, height: 24 }} />
</View>
<View style={styles.cardBody}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{entry.formations}</Text>
<View style={[styles.tag, { backgroundColor: "#FF980033" }]}>
<Text style={[styles.tagText, { color: "#FF9800" }]}>Hub</Text>
</View>
{entry.from_beginner && (
<View style={[styles.tag, { backgroundColor: "#9C27B033" }]}>
<Text style={[styles.tagText, { color: "#9C27B0" }]}></Text>
</View>
)}
</View>
{/* 詳細行 */}
<View style={styles.unyoDetailRow}>
{positionText && (
<DetailChip icon="seat" text={positionText} />
)}
<DetailChip icon="train-car" text={carInfo} />
{timeInfo && <DetailChip icon="clock-outline" text={timeInfo} />}
{entry.starting_location && (
<DetailChip icon="map-marker" text={entry.starting_location} />
)}
</View>
{entry.comment && (
<Text style={styles.unyoComment}>{entry.comment}</Text>
)}
<View style={styles.postsRow}>
<MaterialCommunityIcons name="account-group" size={12} color="#888" />
<Text style={styles.postsText}>{entry.posts_count}稿</Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={18} color="#ccc" />
</TouchableOpacity>
);
};
/** 進行方向バナー */
const DirectionBanner: FC<{ direction: boolean }> = ({ direction }) => (
<View style={styles.directionBanner}>
{direction ? (
// 上り:矢印は左向き(← 先頭)
<>
<MaterialCommunityIcons name="arrow-left" size={14} color="#0099CC" />
<Text style={styles.directionText}></Text>
</>
) : (
// 下り:矢印は右向き(先頭 →)
<>
<Text style={styles.directionText}></Text>
<MaterialCommunityIcons name="arrow-right" size={14} color="#0099CC" />
</>
)}
</View>
);
/** 今日の運用情報カード */
const OperationCard: FC<{
op: OperationLogs;
index: number;
direction: boolean;
navigate: NavigateFunction;
onClose: () => void;
}> = ({ op, index, direction, navigate, onClose }) => {
// 進行方向に応じて車両画像を選択trainIconStatus.tsx と同ロジック)
const thumbUri = direction
? (op.vehicle_img || op.vehicle_img_right)
: (op.vehicle_img_right || op.vehicle_img);
const hasUrl = !!op.vehicle_info_url;
const trainIds = [...(op.train_ids || []), ...(op.related_train_ids || [])];
return (
<TouchableOpacity
style={[styles.card, !hasUrl && styles.cardNoArrow]}
activeOpacity={hasUrl ? 0.7 : 1}
onPress={
hasUrl
? () => {
navigate("howto", { info: op.vehicle_info_url, goTo: "menu" });
SheetManager.hide("EachTrainInfo");
onClose();
}
: undefined
}
>
{/* 車両サムネイル or アイコン(進行方向考慮) */}
{thumbUri ? (
<Image
source={{ uri: thumbUri }}
style={styles.vehicleThumb}
resizeMode="contain"
/>
) : (
<View style={[styles.cardIconWrap, { backgroundColor: "#4CAF5022" }]}>
<MaterialCommunityIcons name="train" size={22} color="#4CAF50" />
</View>
)}
<View style={styles.cardBody}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}> #{index + 1}</Text>
<View style={[styles.tag, { backgroundColor: "#4CAF5033" }]}>
<Text style={[styles.tagText, { color: "#4CAF50" }]}></Text>
</View>
</View>
{trainIds.length > 0 && (
<Text style={styles.cardDesc} numberOfLines={1}>
: {trainIds.slice(0, 4).join(" / ")}
{trainIds.length > 4 ? " ..." : ""}
</Text>
)}
{op.date && (
<Text style={styles.cardDesc}>: {op.date}</Text>
)}
</View>
{hasUrl && (
<MaterialCommunityIcons name="chevron-right" size={18} color="#ccc" />
)}
</TouchableOpacity>
);
};
/** 小さい詳細チップ */
const DetailChip: FC<{ icon: string; text: string }> = ({ icon, text }) => (
<View style={styles.chip}>
<MaterialCommunityIcons name={icon as any} size={11} color="#666" />
<Text style={styles.chipText}>{text}</Text>
</View>
);
/* ------------------------------------------------------------------ */
/* スタイル */
/* ------------------------------------------------------------------ */
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
borderTopWidth: 1,
borderTopColor: "#e0e0e0",
},
panelHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 14,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
panelHeaderText: {
flex: 1,
fontSize: 14,
fontWeight: "bold",
color: "#333",
},
closeButton: {
padding: 4,
},
scroll: {
maxHeight: 360,
},
scrollContent: {
paddingHorizontal: 12,
paddingVertical: 8,
paddingBottom: 16,
},
sectionLabel: {
flexDirection: "row",
alignItems: "center",
marginTop: 10,
marginBottom: 4,
gap: 4,
},
sectionLabelText: {
fontSize: 12,
color: "#888",
fontWeight: "600",
letterSpacing: 0.5,
},
card: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f9f9f9",
borderRadius: 10,
marginBottom: 8,
padding: 10,
borderWidth: 1,
borderColor: "#eeeeee",
gap: 10,
},
cardDisabled: {
opacity: 0.5,
},
cardNoArrow: {
// no change needed
},
cardIconWrap: {
width: 40,
height: 40,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
},
vehicleThumb: {
width: 50,
height: 40,
borderRadius: 6,
flexShrink: 0,
backgroundColor: "#eee",
},
cardBody: {
flex: 1,
gap: 3,
},
cardTitleRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
flexWrap: "wrap",
},
cardTitle: {
fontSize: 14,
fontWeight: "bold",
color: "#222",
},
cardTitleDisabled: {
color: "#aaa",
},
cardDesc: {
fontSize: 12,
color: "#666",
},
tag: {
borderRadius: 4,
paddingHorizontal: 5,
paddingVertical: 1,
},
tagText: {
fontSize: 10,
fontWeight: "bold",
},
unyoDetailRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 4,
marginTop: 2,
},
unyoComment: {
fontSize: 11,
color: "#888",
fontStyle: "italic",
marginTop: 2,
},
postsRow: {
flexDirection: "row",
alignItems: "center",
gap: 3,
marginTop: 2,
},
postsText: {
fontSize: 11,
color: "#888",
},
chip: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f0f0f0",
borderRadius: 4,
paddingHorizontal: 5,
paddingVertical: 2,
gap: 3,
},
chipText: {
fontSize: 11,
color: "#555",
},
directionBanner: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: "#E3F4FB",
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 4,
marginBottom: 6,
alignSelf: "flex-start",
},
directionText: {
fontSize: 11,
fontWeight: "600",
color: "#0099CC",
},
});

View File

@@ -19,10 +19,18 @@ export const useThroughStations = (trainData) => {
const [, nextSe] = array[index + 1]?.split(',') || [];
if (nextSe) {
const isCanceled =
(se.includes('休') && nextSe.includes('休')) ||
(se.includes('着') && nextSe.includes('休')) ||
(se.includes('休') && nextSe.includes('発'));
// 運休判定ロジック:
// 1. 両方が休系(休編、休発、休着など)→ 運休区間
// 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行
// 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行
// 4. その他の休の組み合わせ → 運休区間
const bothCanceled = se.includes('休') && nextSe.includes('休');
const normalArrivalToSuspendStart =
(se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発'));
const suspendEndToNormalDeparture =
(se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編');
const isCanceled = bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture;
isCancel.push(isCanceled);
}

View File

@@ -17,31 +17,75 @@ type Props = {
navigate: NavigateFunction;
from: string;
todayOperation: OperationLogs[];
direction?: boolean;
};
type apt = {
name: GlyphNames;
color: string;
};
export const TrainIconStatus: FC<Props> = (props) => {
const { data, navigate, from, todayOperation } = props;
const { data, navigate, from, todayOperation, direction } = props;
const [anpanmanStatus, setAnpanmanStatus] = useState<apt>();
const { allCustomTrainData } = useAllTrainDiagram();
const [trainIconData, setTrainIcon] = useState<
{ vehicle_info_img: string; vehicle_info_url: string }[]
{ vehicle_info_img: string;vehicle_info_right_img: string; vehicle_info_url: string }[]
>([]);
useEffect(() => {
if (!data.trainNum) return;
const { train_info_img: vehicle_info_img, vehicle_info_url } =
customTrainDataDetector(data.trainNum, allCustomTrainData);
if (todayOperation.length !== 0) {
const data =
todayOperation.map((op) => ({
vehicle_info_img: op.vehicle_img,
vehicle_info_url: op.vehicle_info_url,
})) || [];
setTrainIcon(data);
const returnData =
todayOperation
.sort((a, b) => {
// trainIdからカンマ以降の数字を抽出する関数
const extractOrderNumber = (trainId: string): number => {
const parts = trainId.split(',');
if (parts.length > 1) {
const num = parseInt(parts[1].trim(), 10);
return isNaN(num) ? Infinity : num;
}
return Infinity; // カンマなし = 末尾に移動
};
// data.trainNumと一致するtrainIdを探す関数
const findMatchingTrainId = (operation: OperationLogs): string | null => {
const allTrainIds = [
...(operation.train_ids || []),
...(operation.related_train_ids || []),
];
// data.trainNumの接頭辞と一致するものを探す
for (const trainId of allTrainIds) {
const prefix = trainId.split(',')[0]; // カンマ前の部分
if (prefix === data.trainNum) {
return trainId;
}
}
return null;
};
const aTrainId = findMatchingTrainId(a);
const bTrainId = findMatchingTrainId(b);
// マッチしたものがない場合は元の順序を保持
if (!aTrainId || !bTrainId) {
return aTrainId ? -1 : bTrainId ? 1 : 0;
}
const aOrder = extractOrderNumber(aTrainId);
const bOrder = extractOrderNumber(bTrainId);
return aOrder - bOrder;
})
.map((op) => ({
vehicle_info_img: op.vehicle_img || vehicle_info_img,
vehicle_info_right_img: op.vehicle_img_right || op.vehicle_img || vehicle_info_img,
vehicle_info_url: op.vehicle_info_url,
})) || [];
setTrainIcon(returnData);
} else if (vehicle_info_img) {
setTrainIcon([{ vehicle_info_img, vehicle_info_url }]);
setTrainIcon([{ vehicle_info_img, vehicle_info_right_img: vehicle_info_img, vehicle_info_url }]);
}
switch (data.trainNum) {
@@ -71,30 +115,6 @@ export const TrainIconStatus: FC<Props> = (props) => {
}
});
break;
case "2074D":
case "2076D":
case "2080D":
case "2082D":
case "2071D":
case "2073D":
case "2079D":
case "2081D":
fetch(
`https://n8n.haruk.in/webhook/dosan-anpanman-first?trainNum=${
data.trainNum
}&month=${dayjs().format("M")}&day=${dayjs().format("D")}`
)
.then((d) => d.json())
.then((d) => {
if (d.trainStatus == "") {
//setAnpanmanStatus({name:"checkmark-circle-outline",color:"blue"});
} else if (d.trainStatus == "▲") {
setAnpanmanStatus({ name: "warning-outline", color: "yellow" });
} else if (d.trainStatus == "×") {
//setAnpanmanStatus({ name: "close-circle-outline", color: "red" });
}
});
break;
}
}, [data.trainNum, allCustomTrainData, todayOperation]);
const [move, setMove] = useState(true);
@@ -109,7 +129,7 @@ export const TrainIconStatus: FC<Props> = (props) => {
return (
<>
{trainIconData.map(
({ vehicle_info_img: trainIcon, vehicle_info_url: address }, index) => (
({ vehicle_info_img: trainIcon, vehicle_info_right_img: trainIconRight, vehicle_info_url: address }, index) => (
<TouchableOpacity
onPress={() => {
navigate("howto", {
@@ -122,12 +142,14 @@ export const TrainIconStatus: FC<Props> = (props) => {
>
{move ? (
<Image
source={{ uri: trainIcon }}
source={{ uri: direction ? trainIcon : trainIconRight || trainIcon }}
style={{
height: 30,
width: 24,
height: index > 0 ? 15 : 30,
width: index > 0 ? 12 : 24,
marginRight: 5,
display: index == 0 ? "flex" : "none", //暫定対応:複数アイコンがある場合は最初のアイコンのみ表示
marginLeft: index > 0 ? -10 : 0,
marginTop: index > 0 ? 10 : 0,
//display: index == 0 ? "flex" : "none", //暫定対応:複数アイコンがある場合は最初のアイコンのみ表示
}}
resizeMethod="resize"
/>

View File

@@ -29,7 +29,6 @@ export const StationDeteilView = (props) => {
const { width } = useWindowDimensions();
const { busAndTrainData } = useBusAndTrainData();
const [trainBus, setTrainBus] = useState();
const { updatePermission } = useTrainMenu();
useEffect(() => {
if (!currentStation) return () => {};
@@ -136,11 +135,11 @@ export const StationDeteilView = (props) => {
onExit={onExit}
/>
)}
{updatePermission &&<StationDiagramButton
<StationDiagramButton
navigate={navigate}
onExit={onExit}
currentStation={currentStation}
/>}
/>
{!currentStation[0].StationTimeTable || (
<StationTimeTableButton
info={info}

View File

@@ -34,7 +34,7 @@ export const StationDiagramButton: FC<Props> = (props) => {
onExit();
}}
>
v2
(β)
</TicketBox>
);
};

View File

@@ -0,0 +1,739 @@
import React, { FC } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
Platform,
Image,
} from "react-native";
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import type { NavigateFunction } from "@/types";
import type { OperationLogs, CustomTrainData } from "@/lib/CommonTypes";
import type { UnyohubData } from "@/types/unyohub";
export type TrainDataSourcesPayload = {
trainNum: string;
unyohubEntries: UnyohubData[];
todayOperation: OperationLogs[];
navigate: NavigateFunction;
expoPushToken: string;
/** customTrainDataDetector から取得した priority 値 */
priority: number;
/** 進行方向: true = 上り (vehicle_img) / false = 下り (vehicle_img_right)
* 未指定の場合は列番の奇数偶数でフォールバック */
direction?: boolean;
/** customTrainDataDetector の全データ */
customTrainData?: CustomTrainData;
/** 種別名 (e.g. "特急") */
typeName?: string;
/** 列車名・行先 (e.g. "モーニングEXP高松 高松行") */
trainName?: string;
/** 始発駅名 */
departureStation?: string;
/** 終着駅名(ダイヤから) */
destinationStation?: string;
};
const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png");
export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
payload,
}) => {
if (!payload) return null;
const { trainNum, unyohubEntries, todayOperation, navigate, expoPushToken, priority, direction: directionProp, customTrainData, typeName, trainName, departureStation, destinationStation } =
payload;
// 進行方向の確定:
// 1. payload.direction が明示されていればそれを使う
// 2. customTrainData.directions が設定されていればそれを使う
// 3. どちらもなければ列番の偶数 = 上り / 奇数 = 下り でフォールバック
const resolvedDirection: boolean = (() => {
if (directionProp !== undefined) return directionProp;
if (customTrainData?.directions !== undefined) return !!customTrainData.directions;
return parseInt(trainNum.replace(/[^\d]/g, ""), 10) % 2 === 0;
})();
const close = () => SheetManager.hide("TrainDataSources");
const openWebView = (uri: string, useExitButton: boolean) => {
SheetManager.hide("EachTrainInfo");
SheetManager.hide("TrainDataSources");
navigate("generalWebView", { uri, useExitButton });
};
/* ── 各ソースの状態 ─────────────────────────────── */
const opCount = todayOperation.length;
const unyoCount = unyohubEntries.length;
const hasTrainInfo = priority > 200;
// 運用情報: 全エントリの unit_ids をフラットに収集・重複除去
const allUnitIds = [
...new Set(todayOperation.flatMap((op) => op.unit_ids ?? [])),
];
const fallbackIds = todayOperation.flatMap((op) => op.train_ids ?? []).slice(0, 4);
const unitIdsSub = allUnitIds.length > 0
? null
: opCount > 0
? fallbackIds.join("・") || "運用記録あり"
: "本日の運用記録なし";
const operationDetail = opCount > 0 ? (
<View style={styles.operationDetailBlock}>
{allUnitIds.length > 0 ? (
<Text style={styles.unitIdText}>
{allUnitIds.slice(0, 8).join("・")}{allUnitIds.length > 8 ? `${allUnitIds.length - 8}` : ""}
</Text>
) : (
<Text style={styles.subText}>{fallbackIds.join("・") || "運用記録あり"}</Text>
)}
<DirectionBanner direction={resolvedDirection} />
</View>
) : null;
// 鉄道運用Hub: 編成名リスト
const formationNames =
unyohubEntries
.slice(0, 4)
.map((e) => e.formations)
.join("・") + (unyoCount > 4 ? `${unyoCount - 4}` : "");
const formationDetail = unyoCount > 0 ? (
<Text style={styles.unitIdText}>
{formationNames}
</Text>
) : null;
// 列車情報 subテキスト
const trainInfoSub = customTrainData?.vehicle_formation
? customTrainData.vehicle_formation
: hasTrainInfo
? "臨時情報あり / コミュニティ樓所データを確認・編集"
: "編成データ・コミュニティ独自データを確認・編集";
const trainDetail = customTrainData ? (
<TrainInfoDetail
data={customTrainData}
typeName={typeName}
trainName={trainName}
departureStation={departureStation}
destinationStation={destinationStation}
/>
) : null;
return (
<ActionSheet
gestureEnabled
CustomHeaderComponent={<></>}
isModal={Platform.OS === "ios"}
>
{/* ヘッダー */}
<View style={styles.header}>
<View style={styles.handleBar} />
<View style={styles.headerRow}>
<Text style={styles.headerTitle}></Text>
<Text style={styles.headerSub}>{trainNum}</Text>
</View>
</View>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
>
{/* ─── jr-shikoku-data-system (列車情報 + 運用情報) ─── */}
<CombinedCard
rows={[
{
icon: "database-search",
title: "列車情報",
sub: null,
badge: hasTrainInfo ? "!" : null,
detail: trainDetail,
},
{
icon: "calendar-clock",
title: "運用情報",
sub: unitIdsSub,
badge: opCount > 0 ? opCount : null,
detail: operationDetail,
},
]}
color="#0099CC"
label="jr-shikoku-data-system"
onPress={() =>
openWebView(
`https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`,
false
)
}
/>
{/* ─── 鉄道運用Hub ─────────────────────────── */}
<SourceCard
imagePng={HUB_LOGO_PNG}
color="#333"
title="鉄道運用Hub"
label="外部コミュニティデータ"
sub={unyoCount > 0 ? "" : "運用データなし / 新規投稿もここから"}
badge={unyoCount > 0 ? unyoCount : null}
badgeColor="#333"
detail={formationDetail}
onPress={() =>
openWebView(
`https://jr-shikoku-data-system.pages.dev/unyohub-connection-train-data/${trainNum}`,
true
)
}
/>
</ScrollView>
<View style={styles.footer} />
</ActionSheet>
);
};
/* ------------------------------------------------------------------ */
/* DirectionBanner: 進行方向表示 */
/* ------------------------------------------------------------------ */
const DirectionBanner: FC<{ direction: boolean }> = ({ direction }) => (
<View style={styles.directionBanner}>
{direction ? (
<>
<MaterialCommunityIcons name="arrow-left" size={13} color="#0099CC" />
<Text style={styles.directionText}></Text>
</>
) : (
<>
<Text style={styles.directionText}></Text>
<MaterialCommunityIcons name="arrow-right" size={13} color="#0099CC" />
</>
)}
</View>
);
/* ------------------------------------------------------------------ */
/* CombinedCard: 複数行を 1 枠にまとめるカード */
/* ------------------------------------------------------------------ */
type CombinedRow = {
icon: string;
title: string;
sub: string | null;
badge: number | string | null;
detail?: React.ReactNode;
};
const CombinedCard: FC<{
rows: CombinedRow[];
color: string;
label: string;
onPress: () => void;
}> = ({ rows, color, label, onPress }) => (
<TouchableOpacity style={[styles.card, styles.combinedCard]} activeOpacity={0.7} onPress={onPress}>
{/* 共通ヘッダー */}
<View style={styles.combinedHeader}>
<View style={[styles.colorBar, { backgroundColor: color }]} />
<Text style={[styles.labelText, styles.combinedLabel]}>{label}</Text>
</View>
{/* 各行 */}
{rows.map((row, i) => (
<React.Fragment key={row.title}>
{i > 0 && <View style={[styles.divider, { marginLeft: 14 }]} />}
<View style={styles.combinedRow}>
{/* 左カラーバー分のスペーサー */}
<View style={{ width: 4 }} />
<View style={[styles.iconWrap, { backgroundColor: color + "18", marginTop: 6 }]}>
<MaterialCommunityIcons name={row.icon as any} size={22} color={color} />
</View>
<View style={styles.textWrap}>
<View style={styles.titleRow}>
<Text style={styles.cardTitle}>{row.title}</Text>
{row.badge !== null && (
<View style={[styles.badge, { backgroundColor: color }]}>
<Text style={styles.badgeText}>{row.badge}</Text>
</View>
)}
</View>
{row.sub !== null && (
<Text style={styles.subText} numberOfLines={1}>{row.sub}</Text>
)}
{row.detail && (
<View style={styles.detailWrap}>{row.detail}</View>
)}
</View>
{i === rows.length - 1 && (
<MaterialCommunityIcons name="chevron-right" size={20} color="#ccc" style={{ marginRight: 10, marginTop: 16 }} />
)}
</View>
</React.Fragment>
))}
</TouchableOpacity>
);
/* ------------------------------------------------------------------ */
/* TrainInfoDetail: 列車情報の構造化表示 */
/* ------------------------------------------------------------------ */
const TrainInfoDetail: FC<{
data: CustomTrainData;
typeName?: string;
trainName?: string;
departureStation?: string;
destinationStation?: string;
}> = ({ data, typeName, trainName, departureStation, destinationStation }) => {
const { infogram, train_info, vehicle_formation, via_data, start_date, end_date, uwasa, optional_text } = data;
// to_data がなければダイヤの終着駅をフォールバック
const destDisplay = data.to_data || destinationStation || "";
const hasAny = typeName || trainName || departureStation || destDisplay || infogram || train_info || vehicle_formation || via_data || start_date || end_date || uwasa || optional_text;
if (!hasAny) return null;
return (
<View style={styles.trainDetail}>
{/* 種別・列車名・始発→行先 */}
{(typeName || trainName || departureStation || destDisplay) && (
<View style={styles.trainTitleBlock}>
<View style={styles.trainHeaderRow}>
{!!typeName && (
<View style={styles.typeTag}>
<Text style={styles.typeTagText}>{typeName}</Text>
</View>
)}
{!!trainName && (
<Text style={styles.trainNameText} numberOfLines={1}>{trainName}</Text>
)}
</View>
{!!(departureStation || destDisplay) && (
<View style={styles.routeRow}>
{!!departureStation && (
<Text style={styles.routeStation}>{departureStation}</Text>
)}
{!!(departureStation && destDisplay) && (
<MaterialCommunityIcons name="arrow-right" size={13} color="#aaa" />
)}
{!!destDisplay && (
<Text style={[styles.routeStation, styles.routeDest]}>{destDisplay}</Text>
)}
</View>
)}
</View>
)}
{/* LED インフォグラム */}
{!!infogram && (
<View style={styles.ledSection}>
<Text style={styles.detailSectionLabel}></Text>
<View style={styles.ledDisplay}>
<Text style={styles.ledText}>{infogram}</Text>
</View>
</View>
)}
{/* 列車情報テキスト */}
{!!train_info && (
<View style={styles.trainInfoSection}>
<Text style={styles.detailSectionLabel}></Text>
<Text style={styles.trainInfoText}>{train_info}</Text>
</View>
)}
{/* メタ情報チップ行 */}
{!!(vehicle_formation || via_data || start_date || end_date) && (
<View style={styles.metaChipRow}>
{!!vehicle_formation && (
<View style={styles.metaChip}>
<MaterialCommunityIcons name="train-car" size={11} color="#444" />
<Text style={styles.metaChipText}>{vehicle_formation}</Text>
</View>
)}
{!!via_data && (
<View style={styles.metaChip}>
<MaterialCommunityIcons name="map-marker-path" size={11} color="#444" />
<Text style={styles.metaChipText}>: {via_data}</Text>
</View>
)}
{!!(start_date || end_date) && (
<View style={[styles.metaChip, styles.metaChipOrange]}>
<MaterialCommunityIcons name="calendar-range" size={11} color="#bf360c" />
<Text style={[styles.metaChipText, { color: "#bf360c" }]}>
{start_date ?? ""}{end_date ?? ""}
</Text>
</View>
)}
</View>
)}
{/* うわさ / optional_text */}
{(!!uwasa || !!optional_text) && (
<View style={styles.noteSection}>
{!!uwasa && (
<View style={styles.noteRow}>
<MaterialCommunityIcons name="message-text-outline" size={12} color="#888" />
<Text style={styles.noteText}>{uwasa}</Text>
</View>
)}
{!!optional_text && (
<View style={styles.noteRow}>
<MaterialCommunityIcons name="information-outline" size={12} color="#888" />
<Text style={styles.noteText}>{optional_text}</Text>
</View>
)}
</View>
)}
</View>
);
};
/* ------------------------------------------------------------------ */
/* SourceCard */
/* ------------------------------------------------------------------ */
type SourceCardProps = {
icon?: string;
imagePng?: any;
color: string;
title: string;
label: string;
sub?: string;
badge: number | string | null;
badgeColor: string;
onPress?: () => void;
detail?: React.ReactNode;
};
const SourceCard: FC<SourceCardProps> = ({
icon,
imagePng,
color,
title,
label,
sub,
badge,
badgeColor,
onPress,
detail,
}) => (
<TouchableOpacity
style={styles.card}
activeOpacity={0.7}
onPress={onPress}
>
{/* 左カラーバー */}
<View style={[styles.colorBar, { backgroundColor: color }]} />
{/* アイコン */}
<View style={[styles.iconWrap, { backgroundColor: color + "18" }]}>
{imagePng ? (
<Image source={imagePng} style={{ width: 28, height: 28 }} />
) : (
<MaterialCommunityIcons
name={(icon ?? "information") as any}
size={24}
color={color}
/>
)}
</View>
{/* テキスト */}
<View style={styles.textWrap}>
<View style={styles.titleRow}>
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.labelText}>{label}</Text>
</View>
{sub && <Text style={styles.subText} numberOfLines={1}>{sub}</Text>}
{detail && <View style={styles.detailWrap}>{detail}</View>}
</View>
{/* バッジ + 矢印 */}
<View style={styles.rightArea}>
{badge !== null ? (
<View style={[styles.badge, { backgroundColor: badgeColor }]}>
<Text style={styles.badgeText}>{badge}</Text>
</View>
) : null}
<MaterialCommunityIcons name="chevron-right" size={20} color="#ccc" />
</View>
</TouchableOpacity>
);
/* ------------------------------------------------------------------ */
/* スタイル */
/* ------------------------------------------------------------------ */
const styles = StyleSheet.create({
header: {
paddingTop: 8,
paddingBottom: 12,
paddingHorizontal: 16,
},
handleBar: {
width: 40,
height: 5,
borderRadius: 3,
backgroundColor: "#ddd",
alignSelf: "center",
marginBottom: 12,
},
headerRow: {
flexDirection: "row",
alignItems: "baseline",
gap: 8,
},
headerTitle: {
fontSize: 17,
fontWeight: "bold",
color: "#111",
},
headerSub: {
fontSize: 14,
color: "#888",
},
trainHeaderRow: {
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
gap: 6,
},
trainTitleBlock: {
gap: 4,
paddingBottom: 4,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "#e8e8e8",
marginBottom: 4,
},
typeTag: {
backgroundColor: "#0099CC",
borderRadius: 4,
paddingHorizontal: 8,
paddingVertical: 2,
},
typeTagText: {
color: "#fff",
fontSize: 13,
fontWeight: "bold",
},
trainNameText: {
fontSize: 16,
fontWeight: "bold",
color: "#111",
flexShrink: 1,
},
routeRow: {
flexDirection: "row",
alignItems: "center",
gap: 4,
},
routeStation: {
fontSize: 13,
color: "#555",
},
routeDest: {
fontWeight: "bold",
color: "#111",
},
scroll: {},
scrollContent: {
paddingHorizontal: 14,
gap: 10,
paddingBottom: 4,
},
card: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#fff",
borderRadius: 12,
borderWidth: 1,
borderColor: "#ebebeb",
overflow: "hidden",
minHeight: 70,
},
combinedCard: {
flexDirection: "column",
alignItems: "stretch",
},
combinedHeader: {
flexDirection: "row",
alignItems: "center",
paddingTop: 8,
paddingBottom: 2,
gap: 8,
},
combinedLabel: {
color: "#888",
marginLeft: 4,
},
combinedRow: {
flexDirection: "row",
alignItems: "flex-start",
minHeight: 58,
paddingVertical: 6,
},
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: "#ebebeb",
},
colorBar: {
width: 4,
alignSelf: "stretch",
},
iconWrap: {
width: 48,
height: 48,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
marginHorizontal: 10,
flexShrink: 0,
},
textWrap: {
flex: 1,
paddingVertical: 6,
gap: 3,
},
titleRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
},
cardTitle: {
fontSize: 15,
fontWeight: "bold",
color: "#111",
},
cardTitleDisabled: {
color: "#aaa",
},
labelText: {
fontSize: 10,
color: "#aaa",
fontWeight: "500",
},
subText: {
fontSize: 12,
color: "#555",
},
subTextDisabled: {
color: "#bbb",
},
rightArea: {
flexDirection: "row",
alignItems: "center",
paddingRight: 10,
gap: 4,
},
badge: {
minWidth: 20,
height: 20,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 5,
},
badgeText: {
color: "#fff",
fontSize: 11,
fontWeight: "bold",
},
operationDetailBlock: {
//marginTop: 4,
gap: 6,
},
unitIdText: {
fontSize: 13,
fontWeight: "bold",
color: "#0077aa",
letterSpacing: 0.5,
},
footer: {
height: 20,
},
/* ── TrainInfoDetail ──────────────── */
detailWrap: {
marginTop: 4,
marginBottom: 4,
},
trainDetail: {
gap: 8,
},
ledSection: {
gap: 4,
},
detailSectionLabel: {
fontSize: 10,
color: "#999",
fontWeight: "600",
letterSpacing: 0.5,
},
ledDisplay: {
backgroundColor: "#1a1a1a",
borderRadius: 5,
paddingHorizontal: 10,
paddingVertical: 5,
alignSelf: "flex-start",
},
ledText: {
fontFamily: "JNR-font",
fontSize: 20,
color: "#ff8800",
letterSpacing: 2,
},
trainInfoSection: {
gap: 3,
},
trainInfoText: {
fontSize: 12,
color: "#333",
lineHeight: 18,
},
metaChipRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 5,
},
metaChip: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f0f0f0",
borderRadius: 5,
paddingHorizontal: 7,
paddingVertical: 3,
gap: 4,
},
metaChipOrange: {
backgroundColor: "#fff3e0",
},
metaChipText: {
fontSize: 11,
color: "#444",
},
noteSection: {
gap: 4,
borderLeftWidth: 2,
borderLeftColor: "#e0e0e0",
paddingLeft: 8,
},
noteRow: {
flexDirection: "row",
alignItems: "flex-start",
gap: 4,
},
noteText: {
fontSize: 11,
color: "#666",
lineHeight: 16,
flex: 1,
},
directionBanner: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: "#E3F4FB",
borderRadius: 5,
paddingHorizontal: 7,
paddingVertical: 3,
marginTop: 4,
alignSelf: "flex-start",
},
directionText: {
fontSize: 11,
fontWeight: "600",
color: "#0099CC",
},
});

View File

@@ -6,6 +6,7 @@ import { TrainMenuLineSelector } from "./TrainMenuLineSelector";
import { TrainIconUpdate } from "./TrainIconUpdate";
import { SpecialTrainInfo } from "./SpecialTrainInfo";
import { Social } from "./SocialMenu";
import { TrainDataSources } from "./TrainDataSources";
registerSheet("EachTrainInfo", EachTrainInfo);
registerSheet("JRSTraInfo", JRSTraInfo);
@@ -14,5 +15,6 @@ registerSheet("TrainMenuLineSelector", TrainMenuLineSelector);
registerSheet("TrainIconUpdate", TrainIconUpdate);
registerSheet("SpecialTrainInfo", SpecialTrainInfo);
registerSheet("Social", Social);
registerSheet("TrainDataSources", TrainDataSources);
export {};

View File

@@ -91,7 +91,7 @@ export const AllTrainDiagramView: FC = () => {
const Item: FC<ItemProps> = ({ id, openTrainInfo }) => {
const { train_info_img, train_name, type, train_num_distance, to_data } =
customTrainDataDetector(id, allCustomTrainData);
const todayOperation = getTodayOperationByTrainId(id);
const todayOperation = getTodayOperationByTrainId(id).filter(d=> d.state !== 100);
const [typeString, fontAvailable, isOneMan] = getStringConfig(type, id);
@@ -136,7 +136,7 @@ export const AllTrainDiagramView: FC = () => {
? todayOperation.map((operation, index) => (
<Image
key={index}
source={{ uri: operation.vehicle_img }}
source={{ uri: operation.vehicle_img || train_info_img }}
style={{
width: 20,
height: 22,

View File

@@ -12,8 +12,11 @@ import { TextBox } from "../atom/TextBox";
import { TicketBox } from "../atom/TicketBox";
import { SpecialTrainInfoBox } from "./SpecialTrainInfoBox";
import { SheetManager } from "react-native-actions-sheet";
import { useNavigation } from "@react-navigation/native";
import { useNotification } from "@/stateBox/useNotifications";
export const FixedContentBottom = (props) => {
const { expoPushToken } = useNotification();
return (
<>
{props.children}
@@ -35,7 +38,7 @@ export const FixedContentBottom = (props) => {
flex={1}
onPressButton={() =>
Linking.openURL(
"https://www.jr-shikoku.co.jp/01_trainbus/event_train/sp/"
"https://www.jr-shikoku.co.jp/01_trainbus/event_train/sp/",
)
}
>
@@ -172,9 +175,7 @@ export const FixedContentBottom = (props) => {
<TextBox
backgroundColor="#0099CC"
flex={1}
onPressButton={() =>
SheetManager.show("Social")
}
onPressButton={() => SheetManager.show("Social")}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
@@ -183,25 +184,58 @@ export const FixedContentBottom = (props) => {
JR四国のSNS一覧です
</Text>
</TextBox>
<Text style={{ fontWeight: "bold", fontSize: 20 }}></Text>
<TextBox
backgroundColor="#8c00d6"
flex={1}
onPressButton={() => props.navigate("AllTrainIDList")}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Text style={{ color: "white", fontSize: 18 }}>
</Text>
</TextBox>
<Text style={{ fontWeight: "bold", fontSize: 20 }}>
JR四国非公式列車データベース(β)
</Text>
<View style={{ flexDirection: "row" }}>
<TextBox
backgroundColor="#E67E22"
flex={1}
onPressButton={() => {
const uri = `https://jr-shikoku-data-system.pages.dev/unitList?userID=${expoPushToken}&from=eachTrainInfo`;
props.navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
}}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<MaterialCommunityIcons name="train-car" color="white" size={40} />
</TextBox>
<TextBox
backgroundColor="#9B59B6"
flex={1}
onPressButton={() => {
const uri = `https://jr-shikoku-data-system.pages.dev/operationList?userID=${expoPushToken}&from=eachTrainInfo`;
props.navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
}}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<MaterialCommunityIcons name="timetable" color="white" size={40} />
</TextBox>
<TextBox
backgroundColor="#16A085"
flex={1}
onPressButton={() => props.navigate("AllTrainIDList")}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Ionicons name="search" color="white" size={40} />
</TextBox>
</View>
<Text style={{ fontWeight: "bold", fontSize: 20 }}></Text>
<TextBox
backgroundColor="rgb(88, 101, 242)"
flex={1}
onPressButton={() => Linking.openURL("https://twitter.com/Xprocess_main/status/1955242437817012300")}
onPressButton={() =>
Linking.openURL(
"https://twitter.com/Xprocess_main/status/1955242437817012300",
)
}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
Discordのご案内

View File

@@ -0,0 +1,395 @@
import React, { useState, useEffect } from "react";
import { View, Text, ScrollView, StyleSheet, Image, TouchableOpacity, Linking } from "react-native";
import { Switch } from "react-native-elements";
import { useNavigation } from "@react-navigation/native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
import { AS } from "../../storageControl";
import { STORAGE_KEYS } from "@/constants";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png");
/* ------------------------------------------------------------------ */
/* DataSourceAccordionCard */
/* ------------------------------------------------------------------ */
type Feature = { icon: string; label: string; text: string };
type DataSourceAccordionCardProps = {
/** ロゴ画像 (require) */
logo: any;
/** アクセントカラー */
accentColor: string;
/** データソース名 */
title: string;
/** 1行サブタイトル */
tagline: string;
/** スイッチの値 */
enabled: boolean;
/** スイッチ変更ハンドラ */
onToggle: (v: boolean) => void;
/** 説明文 */
description: string;
/** 機能リスト */
features: Feature[];
/** フッターリンクラベル */
linkLabel: string;
/** フッターリンク URL */
linkUrl: string;
};
const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
logo,
accentColor,
title,
tagline,
enabled,
onToggle,
description,
features,
linkLabel,
linkUrl,
}) => {
const [expanded, setExpanded] = useState(false);
return (
<View style={[styles.accordionCard, enabled && styles.accordionCardEnabled]}>
{/* ── ヘッダー行(常時表示) ── */}
<View style={styles.accordionHeader}>
{/* 左:ロゴ */}
<Image source={logo} style={styles.accordionLogo} />
{/* 中央:タイトル+タグライン */}
<View style={styles.accordionTitles}>
<Text style={styles.accordionTitle}>{title}</Text>
<Text style={styles.accordionTagline}>{tagline}</Text>
</View>
{/* 右:スイッチ */}
<Switch
value={enabled}
onValueChange={onToggle}
color={accentColor}
style={styles.accordionSwitch}
/>
</View>
{/* スイッチ状態テキスト */}
<View style={styles.accordionStatusRow}>
<View style={[styles.statusDot, { backgroundColor: enabled ? accentColor : "#ccc" }]} />
<Text style={[styles.statusText, { color: enabled ? accentColor : "#aaa" }]}>
{enabled ? "有効 — 編成データを取得します" : "無効 — データを取得しません"}
</Text>
</View>
{/* ── 展開トリガー ── */}
<TouchableOpacity
style={styles.accordionToggleRow}
onPress={() => setExpanded((v) => !v)}
activeOpacity={0.6}
>
<Text style={styles.accordionToggleLabel}>
{expanded ? "詳細を閉じる" : "鉄道運用Hub について"}
</Text>
<MaterialCommunityIcons
name={expanded ? "chevron-up" : "chevron-down"}
size={16}
color="#888"
/>
</TouchableOpacity>
{/* ── 展開コンテンツ ── */}
{expanded && (
<View style={styles.accordionBody}>
{/* 説明文 */}
<Text style={styles.bodyDesc}>{description}</Text>
{/* 機能リスト */}
<View style={styles.bodyFeatures}>
{features.map((f) => (
<View key={f.icon} style={styles.featureRow}>
<View style={styles.featureIcon}>
<MaterialCommunityIcons name={f.icon as any} size={14} color="#444" />
</View>
<Text style={styles.featureLabel}>{f.label}</Text>
<Text style={styles.featureText}>{f.text}</Text>
</View>
))}
</View>
{/* リンク */}
<TouchableOpacity
style={styles.bodyLink}
onPress={() => Linking.openURL(linkUrl)}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="open-in-new" size={13} color="#555" />
<Text style={styles.bodyLinkText}>{linkLabel}</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
/* ------------------------------------------------------------------ */
/* 定数 */
/* ------------------------------------------------------------------ */
const UNYOHUB_FEATURES: Feature[] = [
{ icon: "map-marker-radius-outline", label: "走行位置", text: "どの編成がどの駅間を走っているかリアルタイムで確認" },
{ icon: "timetable", label: "時刻表", text: "充当編成情報付きの駅時刻表を閲覧" },
{ icon: "clipboard-list-outline", label: "運用データ", text: "一日の全運用を一覧表示・過去日付への遡り閲覧" },
{ icon: "train-car", label: "編成表", text: "車両メーカー・竣工日・運用履歴を閲覧" },
{ icon: "table-search", label: "運用表", text: "列車番号・両数・出庫場所などで運用を検索" },
];
/* ------------------------------------------------------------------ */
/* DataSourceSettings */
/* ------------------------------------------------------------------ */
export const DataSourceSettings = () => {
const navigation = useNavigation();
const { updatePermission, dataSourcePermission } = useTrainMenu();
const canAccess = updatePermission || Object.values(dataSourcePermission).some(Boolean);
const [useUnyohub, setUseUnyohub] = useState(false);
useEffect(() => {
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
setUseUnyohub(value === true || value === "true");
});
}, []);
const handleToggleUnyohub = (value: boolean) => {
setUseUnyohub(value);
AS.setItem(STORAGE_KEYS.USE_UNYOHUB, value.toString());
};
return (
<View style={styles.container}>
<SheetHeaderItem
title="情報ソース設定"
LeftItem={{
title: "戻る",
onPress: () => navigation.goBack(),
position: "left",
}}
/>
{!canAccess ? (
<View style={styles.noPermissionContainer}>
<Text style={styles.noPermissionText}></Text>
<Text style={styles.noPermissionSubText}>Hubまたはアプリ管理者の権限が必要です</Text>
</View>
) : (
<ScrollView style={styles.content} contentContainerStyle={styles.contentInner}>
<Text style={styles.sectionTitle}></Text>
<DataSourceAccordionCard
logo={HUB_LOGO_PNG}
accentColor="#0099CC"
title="鉄道運用Hub"
tagline="コミュニティによる列車運用情報サービス"
enabled={useUnyohub}
onToggle={handleToggleUnyohub}
description={
"鉄道ファン有志の目撃情報をもとに、どの編成がどの列車に充当されているかをリアルタイムで共有・確認できる無料 Web サービスです。JR 四国をはじめ全国多数の路線系統に対応しています。\n\nデータがある列車では地図上に黄色いマークが表示され、列車情報画面の編成表示も更新されます。"
}
features={UNYOHUB_FEATURES}
linkLabel="unyohub.2pd.jp を開くJR四国"
linkUrl="https://unyohub.2pd.jp/railroad_shikoku/"
/>
<View style={styles.infoSection}>
<Text style={styles.infoText}>
{"\n\n"}
</Text>
</View>
</ScrollView>
)}
</View>
);
};
const styles = StyleSheet.create({
/* ── 権限なし ── */
noPermissionContainer: {
flex: 1,
backgroundColor: "#f8f8fc",
alignItems: "center",
justifyContent: "center",
padding: 30,
gap: 10,
},
noPermissionText: {
fontSize: 16,
fontWeight: "bold",
color: "#333",
textAlign: "center",
},
noPermissionSubText: {
fontSize: 13,
color: "#666",
textAlign: "center",
},
/* ── レイアウト ── */
container: {
flex: 1,
backgroundColor: "#0099CC",
},
content: {
flex: 1,
backgroundColor: "#f8f8fc",
},
contentInner: {
paddingHorizontal: 14,
paddingBottom: 40,
gap: 12,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
color: "#888",
letterSpacing: 0.5,
marginTop: 20,
marginLeft: 4,
},
/* ── アコーディオンカード ── */
accordionCard: {
backgroundColor: "#fff",
borderRadius: 14,
borderWidth: 1,
borderColor: "#e4e4e4",
overflow: "hidden",
},
accordionCardEnabled: {
borderColor: "#0099CC44",
},
accordionHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 14,
paddingTop: 14,
paddingBottom: 6,
gap: 10,
},
accordionLogo: {
width: 40,
height: 40,
borderRadius: 8,
flexShrink: 0,
},
accordionTitles: {
flex: 1,
gap: 2,
},
accordionTitle: {
fontSize: 15,
fontWeight: "bold",
color: "#111",
},
accordionTagline: {
fontSize: 11,
color: "#888",
},
accordionSwitch: {
flexShrink: 0,
},
accordionStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingHorizontal: 14,
paddingBottom: 10,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontSize: 12,
fontWeight: "500",
},
/* ── 展開トリガー ── */
accordionToggleRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 14,
paddingVertical: 10,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#ebebeb",
},
accordionToggleLabel: {
fontSize: 12,
color: "#666",
fontWeight: "500",
},
/* ── 展開コンテンツ ── */
accordionBody: {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#ebebeb",
padding: 14,
gap: 10,
backgroundColor: "#fafafa",
},
bodyDesc: {
fontSize: 12,
color: "#444",
lineHeight: 19,
},
bodyFeatures: {
gap: 7,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#e4e4e4",
paddingTop: 8,
},
featureRow: {
flexDirection: "row",
alignItems: "flex-start",
gap: 6,
},
featureIcon: {
width: 22,
alignItems: "center",
paddingTop: 1,
flexShrink: 0,
},
featureLabel: {
fontSize: 12,
fontWeight: "bold",
color: "#333",
width: 62,
flexShrink: 0,
},
featureText: {
fontSize: 12,
color: "#555",
flex: 1,
lineHeight: 17,
},
bodyLink: {
flexDirection: "row",
alignItems: "center",
gap: 5,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#e4e4e4",
paddingTop: 8,
marginTop: 2,
},
bodyLinkText: {
fontSize: 12,
color: "#555",
},
/* ── 注意書き ── */
infoSection: {
backgroundColor: "#fff3cd",
borderRadius: 10,
padding: 14,
},
infoText: {
fontSize: 13,
color: "#856404",
lineHeight: 18,
},
});

View File

@@ -16,8 +16,9 @@ import TouchableScale from "react-native-touchable-scale";
import { SwitchArea } from "../atom/SwitchArea";
import { useNotification } from "../../stateBox/useNotifications";
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
import { useTrainMenu } from "../../stateBox/useTrainMenu";
const versionCode = "6.1.9.2"; // Update this version code as needed
const versionCode = "6.2.1"; // Update this version code as needed
export const SettingTopPage = ({
testNFC,
@@ -27,7 +28,11 @@ export const SettingTopPage = ({
}) => {
const { width } = useWindowDimensions();
const { expoPushToken } = useNotification();
const { updatePermission, dataSourcePermission } = useTrainMenu();
const navigation = useNavigation();
// admin またはいずれかのソース権限を持つ場合のみ表示
const canAccessDataSourceSettings =
updatePermission || Object.values(dataSourcePermission).some(Boolean);
return (
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<SheetHeaderItem title="アプリの設定画面" LeftItem={{
@@ -108,6 +113,14 @@ export const SettingTopPage = ({
navigation.navigate("setting", { screen: "LayoutSettings" })
}
/>
{canAccessDataSourceSettings && (
<SettingList
string="情報ソース設定"
onPress={() =>
navigation.navigate("setting", { screen: "DataSourceSettings" })
}
/>
)}
{Platform.OS === "android" ? (
<SettingList
string="ウィジェット設定"

View File

@@ -24,6 +24,7 @@ import { FavoriteSettings } from "./FavoriteSettings";
import { WidgetSettings } from "./WidgetSettings";
import { NotificationSettings } from "./NotificationSettings";
import { LauncherIconSettings } from "./LauncherIconSettings";
import { DataSourceSettings } from "./DataSourceSettings";
const Stack = createStackNavigator();
export default function Setting(props) {
@@ -167,6 +168,17 @@ export default function Setting(props) {
}}
component={FavoriteSettings}
/>
<Stack.Screen
name="DataSourceSettings"
options={{
gestureEnabled: true,
...TransitionPresets.SlideFromRightIOS,
cardOverlayEnabled: true,
headerTransparent: true,
headerShown: false,
}}
component={DataSourceSettings}
/>
</Stack.Navigator>
);
}

View File

@@ -0,0 +1,183 @@
import { FC, useRef, useState, useCallback, useEffect } from "react";
import {
View,
Text,
ScrollView,
useWindowDimensions,
Vibration,
} from "react-native";
import { ExGridViewItem } from "./ExGridViewItem";
import Animated, {
useAnimatedStyle,
useSharedValue,
runOnJS,
useAnimatedScrollHandler,
withTiming,
Easing,
FadeIn,
FadeOut,
BounceInUp,
FadeInUp,
FadeOutUp,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { ExGridViewTimePositionItem } from "./ExGridViewTimePositionItem";
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
import { logger } from "@/utils/logger";
import dayjs from "dayjs";
import { ExGridSimpleViewItem } from "./ExGridSimpleViewItem";
type hoge = {
trainNumber: string;
array: string;
name: string;
timeType: string;
time: string;
}[];
export const ExGridSimpleView: FC<{
data: hoge;
showLastStop: boolean;
}> = ({ data, showLastStop }) => {
const groupedData: {
[d: number]: {
trainNumber: string;
array: string;
name: string;
timeType: string;
time: string;
isOperating: boolean;
}[];
} = {
"4": [],
"5": [],
"6": [],
"7": [],
"8": [],
"9": [],
"10": [],
"11": [],
"12": [],
"13": [],
"14": [],
"15": [],
"16": [],
"17": [],
"18": [],
"19": [],
"20": [],
"21": [],
"22": [],
"23": [],
"0": [],
"1": [],
"2": [],
"3": [],
};
const groupKeys = [
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
"22",
"23",
"0",
"1",
"2",
"3",
];
const { currentTrain } = useCurrentTrain();
data.forEach((item) => {
let isOperating = false;
let [hour, minute] = dayjs()
.hour(parseInt(item.time.split(":")[0]))
.minute(parseInt(item.time.split(":")[1]))
.format("H:m")
.split(":");
if (currentTrain.findIndex((x) => x.num == item.trainNumber) != -1) {
const currentTrainTime = currentTrain.find(
(x) => x.num == item.trainNumber
)?.delay;
if (currentTrainTime != "入線") {
[hour, minute] = dayjs()
.hour(parseInt(hour))
.minute(parseInt(minute))
.add(currentTrainTime, "minute")
.format("H:m")
.split(":");
}
isOperating = true;
}
groupedData[hour].push({ ...item, time: `${hour}:${minute}`, isOperating });
});
// 時ヘッダーを横にスクロールしたときの処理
const scrollX = useSharedValue(0);
const stickyTextStyle = useAnimatedStyle(() => ({
transform: [{ translateX: scrollX.value }],
}));
return (
<ScrollView
stickyHeaderIndices={
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
}
style={{ backgroundColor: "#fff" }}
>
{groupKeys.map((hour) => [
<View
style={{
padding: 5,
borderBottomWidth: 0.5,
borderTopWidth: 0.5,
borderBottomColor: "#ccc",
backgroundColor: "#f0f0f0",
}}
key={hour}
>
<Animated.Text
style={[
{
fontSize: 15,
zIndex: 1,
marginLeft: 0,
},
stickyTextStyle,
]}
>
{hour}
</Animated.Text>
</View>,
<View
style={{
flexDirection: "row",
position: "relative",
flexWrap: "wrap",
}}
>
{groupedData[hour].map((d, i, array) => (
<ExGridSimpleViewItem
key={d.trainNumber + i}
d={d}
index={i}
array={array}
showLastStop={showLastStop}
/>
))}
</View>,
])}
</ScrollView>
);
};

View File

@@ -0,0 +1,249 @@
import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName";
import { getStringConfig } from "@/lib/getStringConfig";
import { getTrainType } from "@/lib/getTrainType";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { FC, useEffect, useLayoutEffect, useMemo, useState } from "react";
import {
View,
Text,
TouchableOpacity,
useWindowDimensions,
} from "react-native";
import { customTrainDataDetector } from "../custom-train-data";
import dayjs from "dayjs";
import { SheetManager } from "react-native-actions-sheet";
import { useNavigation } from "@react-navigation/native";
import { lineList } from "@/lib/getStationList";
import { useStationList } from "@/stateBox/useStationList";
import { SharedValue, useAnimatedStyle } from "react-native-reanimated";
import Animated from "react-native-reanimated";
import lineColorList from "@/assets/originData/lineColorList";
import { CustomTrainData, trainTypeID } from "@/lib/CommonTypes";
export const ExGridSimpleViewItem: FC<{
d: {
trainNumber: string;
array: string;
name: string;
timeType: string;
time: string;
isOperating: boolean;
};
index: number;
array: {
train: string;
lastStation: string;
time: string;
isThrough?: boolean;
}[];
showLastStop: boolean;
}> = ({ d, index, array, showLastStop }) => {
const { allCustomTrainData } = useAllTrainDiagram();
const { originalStationList, stationList } = useStationList();
const { navigate } = useNavigation();
const [trainData, setTrainData] = useState<CustomTrainData>();
useEffect(() => {
if (allCustomTrainData) {
allCustomTrainData.forEach((x) => {
if (x.train_id === d.trainNumber) {
setTrainData(x);
}
});
}
}, []);
const { color, data } = getTrainType({
type: trainData?.type,
whiteMode: true,
});
// 行き先(駅名)の取得
const [destinationName] = useMemo(() => {
// to_dataが設定されていればそれを優先
if (trainData?.to_data) {
return [trainData.to_data];
}
const trainDataArray = d.array.split("#").filter((d) => d !== "");
switch (true) {
case trainDataArray[trainDataArray.length - 1] === undefined:
return [""];
default:
// 通常の着は始発駅、運休着は終着駅を表示
const isArrival = (d.timeType == "着" || d.timeType == "着編") && !d.timeType.includes("休");
const trainName = isArrival
? trainDataArray[0].split(",")[0]
: trainDataArray[trainDataArray.length - 1].split(",")[0];
return [migrateTrainName(trainName)];
}
}, [d.array, trainData]);
// 列車名の取得(上部表示用)
const trainName = trainData?.train_name || "";
const timeArray = d.time.split(":").map((s) => parseInt(s));
const formattedTime = dayjs()
.set("hour", timeArray[0])
.set("minute", timeArray[1])
.format("m");
const openStationACFromEachTrainInfo = async (stationName) => {
await SheetManager.hide("EachTrainInfo");
const findStationEachLine = (selectLine) => {
let NearStation = selectLine.filter((d) => d.Station_JP == stationName);
return NearStation;
};
let returnDataBase = lineList
.map((d) => findStationEachLine(originalStationList[d]))
.filter((d) => d.length > 0)
.reduce((pre, current) => {
pre.push(...current);
return pre;
}, []);
if (returnDataBase.length) {
const payload = {
currentStation: returnDataBase,
navigate,
//@ts-ignore
useShow: () => SheetManager.show("StationDetailView", { payload }),
onExit: () => SheetManager.hide("StationDetailView"),
}; //@ts-ignore
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
} else {
SheetManager.hide("StationDetailView");
}
};
const openTrainInfo = () => {
let TrainNumber = "";
if (
trainData.train_num_distance !== "" &&
!isNaN(parseInt(trainData.train_num_distance))
) {
const timeInfo =
parseInt(trainData.train_id.replace("M", "").replace("D", "")) -
parseInt(trainData.train_num_distance);
TrainNumber = timeInfo + "号";
}
const payload = {
data: {
trainNum: trainData.train_id,
limited: `${data}:${trainData.train_name}${TrainNumber}`,
},
navigate,
openStationACFromEachTrainInfo,
from: d.isOperating ? null : "AllTrainIDList",
};
SheetManager.show("EachTrainInfo", {
//@ts-ignore
payload,
onClose: () => {
//alert(data);
},
});
};
const [stationColor, setStationColor] = useState(["gray"]);
useEffect(() => {
// to_data_colorがあればそれを優先
if (trainData?.to_data_color && trainData.to_data_color.length > 0) {
setStationColor(trainData.to_data_color);
return;
}
// to_dataがある場合は、to_dataから駅名を抽出して色を判定
const stationNameForColor = trainData?.to_data
? trainData.to_data.replace(/行き$/, "") // 「行き」を除去
: destinationName;
const Stations = stationList
.map((a) => a.filter((d) => d.StationName == stationNameForColor))
.reduce((newArray, e) => newArray.concat(e), []);
const StationNumbers =
Stations &&
Stations.filter((d) => d.StationNumber).map((d) => d.StationNumber);
if (StationNumbers && StationNumbers.length > 0) {
const stationLineColor = StationNumbers.map(
(d) => lineColorList[d.charAt(0)]
);
setStationColor(stationLineColor || ["gray"]);
}
}, [stationList, destinationName, trainData]);
// if(typeString == "回送"){
// return<></>;
// }
// 運休判定
const isCancelled = d.timeType.includes("休");
return (
<TouchableOpacity
style={{
flexDirection: "column",
//borderTopWidth: 1,
//borderBottomWidth: 0.5,
borderStyle: "solid",
borderColor: "darkgray",
opacity: d.timeType.includes("通") ? 0.5 : isCancelled ? 0.4 : 1,
//position: "absolute",
height: 60,
width: showLastStop ? 54 : 50,
marginHorizontal: showLastStop ? 2:4,
marginVertical: 3,
top: 0,
}}
onPress={() => openTrainInfo()}
>
<View style={{ position: "relative" }}>
<Text
style={{
fontSize: 8,
fontWeight: "bold",
color: isCancelled ? "gray" : "black",
textAlign: "left",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{trainName.slice(0, 4) || " "}
</Text>
<Text
style={{
fontSize: 30,
color: isCancelled ? "gray" : color,
opacity: 1,
marginVertical: -5,
fontWeight: d.isOperating ? "bold" : "thin",
fontStyle: d.isOperating ? "italic" : "normal",
textAlign: "left",
paddingRight: showLastStop ? 20 : 0,
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{formattedTime}
</Text>
{showLastStop && (
<Text
style={{
fontSize: 13,
position: "absolute",
top: 22,
left: 28,
fontWeight: "bold",
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{d.timeType}
</Text>
)}
<Text
style={{
fontSize: 12,
fontWeight: "bold",
color: isCancelled ? "gray" : stationColor[0],
textAlign: "left",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{destinationName}
</Text>
</View>
<View style={{ flex: 1 }} />
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,44 @@
import { FC } from "react";
import { View } from "react-native";
import dayjs from "dayjs";
import { SharedValue, useAnimatedStyle } from "react-native-reanimated";
import Animated from "react-native-reanimated";
export const ExGridSimpleViewTimePositionItem: FC<{
width: SharedValue<number>;
hour: string;
}> = ({ width, hour }) => {
const date = dayjs();
const formattedTime = date.format("m");
const formattedHour = date.format("H");
// if(typeString == "回送"){
// return<></>;
// }
const animatedStyle = useAnimatedStyle(() => {
const leftPosition =
((((width.value - 50) / 100) * parseInt(formattedTime)) / 60) * 100;
return {
left: leftPosition,
};
}, [formattedTime]);
if (formattedHour != hour) return <></>;
return (
<View style={{ left: 0, height: 50, width: 1 }}>
<Animated.View
style={[
{
flexDirection: "column",
borderLeftWidth: 2,
//borderBottomWidth: 0.5,
borderStyle: "solid",
borderColor: "red",
position: "absolute",
height: "100%",
},
animatedStyle,
]}
/>
</View>
);
};

View File

@@ -114,7 +114,7 @@ export const ExGridView: FC<{
[hour, minute] = dayjs()
.hour(parseInt(hour))
.minute(parseInt(minute))
.add(parseInt(currentTrainTime), "minute")
.add(currentTrainTime, "minute")
.format("H:m")
.split(":");
}
@@ -169,7 +169,7 @@ export const ExGridView: FC<{
widthX.value = calc > width ? calc : width;
})
.onEnd(() => {
logger.debug('Long press ended');
//logger.debug('Long press ended');
isChanging.value = false;
});
@@ -279,7 +279,7 @@ export const ExGridView: FC<{
{num - 5}
</Text>
);
} else return <></>;
} else return null;
})}
<Text
style={{
@@ -290,6 +290,7 @@ export const ExGridView: FC<{
fontSize: 12,
width: 50,
}}
key={"分LabelEnd"}
>
()
</Text>

View File

@@ -56,20 +56,30 @@ export const ExGridViewItem: FC<{
const [
trainName,
] = useMemo(() => {
const trainData = d.array.split("#").filter((d) => d !== "");
// to_dataが設定されていればそれを優先
if (trainData?.to_data) {
return [
trainData.to_data,
];
}
const trainDataArray = d.array.split("#").filter((d) => d !== "");
switch (true) {
case trainData[trainData.length - 1] === undefined:
case trainDataArray[trainDataArray.length - 1] === undefined:
return [
"",
];
default:
// 行先がある場合は、行先を取得
const trainName = (d.timeType == "着" || d.timeType == "着編") ? trainData[0].split(",")[0] : trainData[trainData.length - 1].split(",")[0]
// 通常の着は始発駅、運休着は終着駅を表示
const isArrival = (d.timeType == "着" || d.timeType == "着編") && !d.timeType.includes("");
const trainName = isArrival
? trainDataArray[0].split(",")[0]
: trainDataArray[trainDataArray.length - 1].split(",")[0];
return [
migrateTrainName(trainName),
];
}
}, [d.array]);
}, [d.array, trainData]);
const timeArray = d.time.split(":").map((s) => parseInt(s));
const formattedTime = dayjs()
.set("hour", timeArray[0])
@@ -139,23 +149,37 @@ export const ExGridViewItem: FC<{
};
const [stationColor, setStationColor] = useState(["gray"]);
useEffect(() => {
// to_data_colorがあればそれを優先
if (trainData?.to_data_color && trainData.to_data_color.length > 0) {
setStationColor(trainData.to_data_color);
return;
}
// to_dataがある場合は、to_dataから駅名を抽出して色を判定
const stationNameForColor = trainData?.to_data
? trainData.to_data.replace(/行き$/, "") // 「行き」を除去
: trainName;
const Stations = stationList
.map((a) => a.filter((d) => d.StationName == trainName))
.map((a) => a.filter((d) => d.StationName == stationNameForColor))
.reduce((newArray, e) => newArray.concat(e), []);
const StationNumbers =
Stations &&
Stations.filter((d) => d.StationNumber).map((d) => d.StationNumber);
if (StationNumbers) {
if (StationNumbers && StationNumbers.length > 0) {
const stationLineColor = StationNumbers.map(
(d) => lineColorList[d.charAt(0)]
);
setStationColor(stationLineColor || ["gray"]);
}
}, [stationList]);
}, [stationList, trainName, trainData]);
// if(typeString == "回送"){
// return<></>;
// }
// 運休判定
const isCancelled = d.timeType.includes("休");
const animatedStyle = useAnimatedStyle(() => {
const leftPosition =
((((width.value - 50) / 100) * parseInt(formattedTime)) / 60) * 100;
@@ -173,7 +197,7 @@ export const ExGridViewItem: FC<{
//borderBottomWidth: 0.5,
borderStyle: "solid",
borderColor: "darkgray",
opacity: d.timeType.includes("通") ? 0.5 : 1,
opacity: d.timeType.includes("通") ? 0.5 : isCancelled ? 0.4 : 1,
position: "absolute",
height: "100%",
width: 28,
@@ -184,7 +208,7 @@ export const ExGridViewItem: FC<{
>
<TouchableOpacity style={{ flex: 1 }} onPress={() => openTrainInfo()}>
<View style={{ position: "relative" }}>
<Text style={{ fontSize: 20, color: color, opacity: isSameTimeBefore ? 0 : 1, fontWeight:d.isOperating ? "bold" : "thin", fontStyle:d.isOperating? "italic" :"normal" }}>{formattedTime}</Text>
<Text style={{ fontSize: 20, color: isCancelled ? "gray" : color, opacity: isSameTimeBefore ? 0 : 1, fontWeight:d.isOperating ? "bold" : "thin", fontStyle:d.isOperating? "italic" :"normal", textDecorationLine: isCancelled ? "line-through" : "none" }}>{formattedTime}</Text>
<Text
style={{
fontSize: 10,
@@ -192,6 +216,8 @@ export const ExGridViewItem: FC<{
bottom: 0,
right: 0,
fontWeight: "bold",
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{d.timeType}
@@ -203,7 +229,8 @@ export const ExGridViewItem: FC<{
fontSize: 8,
flex: 1,
fontWeight: "bold",
color: stationColor[0],
color: isCancelled ? "gray" : stationColor[0],
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{trainName}

View File

@@ -1,6 +1,7 @@
import { FC } from "react";
import { ListViewItem } from "@/components/StationDiagram/ListViewItem";
import { View, Text, ScrollView } from "react-native";
import dayjs from "dayjs";
type hoge = {
trainNumber: string;
array: string;
@@ -14,7 +15,7 @@ export const ListView: FC<{
const groupedData: Record<string, hoge[]> = {};
const groupKeys = [];
data.forEach((item) => {
const hour = item.time.split(":")[0];
const hour = dayjs().hour(parseInt(item.time.split(":")[0])).format("H");
if (!groupedData[hour]) {
groupedData[hour] = [];
groupKeys.push(hour);

View File

@@ -61,19 +61,54 @@ export const ListViewItem: FC<{
uwasa,
vehicle_formation,
train_info_url,
to_data,
to_data_color,
} = customTrainDataDetector(d.trainNumber, allCustomTrainData);
const [typeString, fontAvailable, isOneMan] = getStringConfig(
type,
d.trainNumber
);
const trainData = d.array.split("#").filter((d) => d !== "");
const station = getStationDataFromName(trainData[trainData.length - 1].split(",")[0]);
const lineColor =
const trainDataArray = d.array.split("#").filter((d) => d !== "");
const lastStationName = trainDataArray.length > 0
? trainDataArray[trainDataArray.length - 1].split(",")[0]
: "";
// to_dataがある場合はそれから駅名を抽出、なければダイヤから
const stationNameForColor = to_data
? to_data.replace(/行き$/, "") // 「行き」を除去
: lastStationName;
const station = getStationDataFromName(stationNameForColor);
const defaultLineColor =
station.length > 0
? lineColorList[station[0]?.StationNumber.slice(0, 1)]
: "black";
// to_data_colorが設定されていればそれを最優先
const finalColor = to_data_color && to_data_color.length > 0
? to_data_color[0]
: defaultLineColor;
// to_dataが設定されていればそれを優先
if (to_data) {
// 既に「行き」が含まれていれば追加しない
const displayName = to_data.endsWith("行き") ? to_data : to_data + "行き";
return [
typeString,
displayName,
fontAvailable,
isOneMan,
infogram,
priority,
uwasa,
vehicle_formation,
train_info_url,
finalColor
];
}
switch (true) {
case trainData[trainData.length - 1] === undefined:
case trainDataArray[trainDataArray.length - 1] === undefined:
return [
typeString,
"",
@@ -83,14 +118,15 @@ export const ListViewItem: FC<{
priority,
uwasa,
vehicle_formation,
train_info_url,lineColor
train_info_url,
finalColor
];
default:
// 行先がある場合は、行先を取得
return [
typeString,
migrateTrainName(
trainData[trainData.length - 1].split(",")[0] + "行き"
trainDataArray[trainDataArray.length - 1].split(",")[0] + "行き"
),
fontAvailable,
isOneMan,
@@ -98,10 +134,11 @@ export const ListViewItem: FC<{
priority,
uwasa,
vehicle_formation,
train_info_url,lineColor
train_info_url,
finalColor
];
}
}, [d.array]);
}, [d.array, allCustomTrainData]);
const timeArray = d.time.split(":").map((s) => parseInt(s));
const formattedTime = dayjs()
.set("hour", timeArray[0])
@@ -159,6 +196,10 @@ export const ListViewItem: FC<{
},
});
};
// 運休判定
const isCancelled = d.timeType?.includes("休");
return (
<TouchableOpacity
style={{
@@ -169,12 +210,12 @@ export const ListViewItem: FC<{
borderStyle: "solid",
borderColor: "darkgray",
padding: 10,
opacity: d.timeType?.includes("通") ? 0.5 : 1,
opacity: d.timeType?.includes("通") ? 0.5 : isCancelled ? 0.4 : 1,
}}
onPress={() => openTrainInfo()}
>
<View style={{ position: "relative", flex: 3 }}>
<Text style={{ fontSize: 30, fontFamily: "DiaPro" }}>
<Text style={{ fontSize: 30, fontFamily: "DiaPro", color: isCancelled ? "gray" : "black", textDecorationLine: isCancelled ? "line-through" : "none" }}>
{formattedTime}
</Text>
<Text
@@ -184,6 +225,8 @@ export const ListViewItem: FC<{
bottom: -3,
right: 0,
fontWeight: "bold",
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{d.timeType}
@@ -198,7 +241,8 @@ export const ListViewItem: FC<{
fontWeight: !fontAvailable ? "bold" : undefined,
paddingTop: fontAvailable ? 2 : 0,
paddingLeft: 10,
color: color,
color: isCancelled ? "gray" : color,
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{typeString}
@@ -209,7 +253,8 @@ export const ListViewItem: FC<{
fontWeight: "bold",
flex: 1,
paddingLeft: 2,
color: color,
color: isCancelled ? "gray" : color,
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{(trainData?.train_name || "") +
@@ -221,6 +266,8 @@ export const ListViewItem: FC<{
style={{
fontSize: 15,
fontWeight: "bold",
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{trainData?.train_id}
@@ -233,7 +280,8 @@ export const ListViewItem: FC<{
flex: 1,
paddingHorizontal: 10,
fontWeight: "bold",
color: lineColor
color: isCancelled ? "gray" : lineColor,
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{trainName}

View File

@@ -21,6 +21,7 @@ import { customTrainDataDetector } from "../custom-train-data";
import { getTrainType } from "@/lib/getTrainType";
import { trainTypeID } from "@/lib/CommonTypes";
import { SearchInputSuggestBox } from "./SearchBox/SearchInputSuggestBox";
import { ExGridSimpleView } from "./ExGridSimpleView";
type props = {
route: {
@@ -56,7 +57,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
const { goBack } = useNavigation();
const [keyBoardVisible, setKeyBoardVisible] = useState(false);
const [input, setInput] = useState("");
const [displayMode, setDisplayMode] = useState<"list" | "grid">("list");
const [displayMode, setDisplayMode] = useState<
"list" | "grid" | "simpleGrid"
>("list");
const [selectedTypeList, setSelectedTypeList] = useState<trainTypeID[]>([
"Normal",
"OneMan",
@@ -128,6 +131,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
};
// //条件によってフィルタリング
if (!threw && timeType && timeType.includes("通")) return;
// showLastStopがfalseの時は全ての着駅を非表示運休着も含む
if (!showLastStop && timeType && timeType.includes("着")) return;
if (
selectedTypeList.findIndex((item) => item === "SPCL") === -1
@@ -210,6 +214,11 @@ export const StationDiagramView: FC<props> = ({ route }) => {
</Text>
{displayMode === "list" ? (
<ListView data={currentStationDiagram} />
) : displayMode === "simpleGrid" ? (
<ExGridSimpleView
data={currentStationDiagram}
showLastStop={showLastStop}
/>
) : (
<ExGridView data={currentStationDiagram} />
)}
@@ -403,7 +412,13 @@ export const StationDiagramView: FC<props> = ({ route }) => {
borderRadius: 100,
}}
onPress={() => {
setDisplayMode(displayMode === "list" ? "grid" : "list");
if (displayMode === "list") {
setDisplayMode("simpleGrid");
} else if (displayMode === "simpleGrid") {
setDisplayMode("grid");
} else if (displayMode === "grid") {
setDisplayMode("list");
}
}}
>
<Text
@@ -413,7 +428,11 @@ export const StationDiagramView: FC<props> = ({ route }) => {
margin: 5,
}}
>
{displayMode === "list" ? "横並びモード" : "リストモード"}
{displayMode === "list"
? "リスト"
: displayMode === "simpleGrid"
? "横並び"
: "時刻チャート"}
</Text>
</TouchableOpacity>
</ScrollView>

View File

@@ -13,21 +13,23 @@ import { TrainName } from "@/components/発車時刻表/LED_inside_Component/Tra
import { TrainPosition } from "@/components/発車時刻表/LED_inside_Component/TrainPosition";
import { StationPosPushDialog } from "@/components/発車時刻表/LED_inside_Component/TrainPositionDataPush";
import { StationPosDeleteDialog } from "@/components/発車時刻表/LED_inside_Component/TrainPositionDataDelete";
import { ScrollingDescription } from "@/components/発車時刻表/LED_inside_Component/ScrollingDescription";
import { useStationList } from "@/stateBox/useStationList";
import useInterval from "@/lib/useInterval";
import dayjs from "dayjs";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { CustomTrainData, StationProps, trainTypeID } from "@/lib/CommonTypes";
import {
CustomTrainData,
eachTrainDiagramType,
StationProps,
trainTypeID,
} from "@/lib/CommonTypes";
import { getCurrentTrainData } from "@/lib/getCurrentTrainData";
import type { NavigateFunction } from "@/types";
import { PlatformNumber } from "./LED_inside_Component/PlatformNumber";
type Props = {
d: {
train: string;
lastStation: string;
time: string;
isThrough?: boolean;
};
d: eachTrainDiagramType;
trainIDSwitch: boolean;
trainDescriptionSwitch: boolean;
station: StationProps;
@@ -52,7 +54,10 @@ export const EachData: FC<Props> = (props) => {
time: string;
}) => {
let TrainNumber = "";
if (train.train_num_distance !== "" && !isNaN(parseInt(train.train_num_distance))) {
if (
train.train_num_distance !== "" &&
!isNaN(parseInt(train.train_num_distance))
) {
const timeInfo =
parseInt(d.train.replace("M", "").replace("D", "")) -
parseInt(train.train_num_distance);
@@ -104,6 +109,24 @@ export const EachData: FC<Props> = (props) => {
const [posInput, setPosInput] = useState<string>("");
const [descInput, setDescInput] = useState<string>("");
//表示切替
const [timeDisplay, setTimeDisplay] = useState(true);
const [isDelay, setIsDelay] = useState(false);
useInterval(() => {
if (
trainDelayStatus === "当駅始発" ||
trainDelayStatus === "発車前" ||
trainDelayStatus === "定刻通り" ||
trainDelayStatus === ""
) {
if (!timeDisplay) setTimeDisplay(true);
if (isDelay) setIsDelay(false);
return;
}
if (!isDelay) setIsDelay(true);
setTimeDisplay((prev) => !prev);
}, 3000);
const [isShow, setIsShow] = useState(true);
const [isDepartureNow, setIsDepartureNow] = useState(false);
useEffect(() => {
@@ -175,14 +198,20 @@ export const EachData: FC<Props> = (props) => {
trainID={d.train}
type={train.type}
isThrew={d.isThrough}
se={d.se}
/>
<LastStation
lastStation={d.lastStation}
ToData={train.to_data}
Station_JP={station.Station_JP}
se={d.se}
/>
<DependTime time={d.time} />
<StatusAndDelay trainDelayStatus={trainDelayStatus} />
<PlatformNumber platform={d.platformNum} se={d.se} />
{timeDisplay ? (
<DependTime time={d.time} isDelay={isDelay} se={d.se} />
) : (
<StatusAndDelay trainDelayStatus={trainDelayStatus} se={d.se} />
)}
</TouchableOpacity>
{!!isDepartureNow && (
<Description
@@ -217,7 +246,20 @@ export const EachData: FC<Props> = (props) => {
/>
)}
{trainDescriptionSwitch && !!train.train_info && (
<Description info={train.train_info} key={d.train + "-description"} />
<TouchableOpacity
style={{
alignContent: "center",
alignItems: "center",
width: "94%",
marginVertical: 5,
marginHorizontal: "3%",
backgroundColor: "#000",
overflow: "hidden",
}}
key={d.train + "-description"}
>
<ScrollingDescription description={train.train_info} />
</TouchableOpacity>
)}
{trainDescriptionSwitch && !!train.uwasa && (
<Description info={train.uwasa} key={d.train + "-uwasa"} />

View File

@@ -6,9 +6,18 @@ const descriptionStyle: TextStyle = {
};
type Props = {
time: string;
isDelay?: boolean;
se?: string;
};
export const DependTime: FC<Props> = ({ time, isDelay, se }) => {
const isCanceled = se?.includes("休");
return (
<View style={{ flex: 4 }}>
<Text
style={{ ...descriptionStyle, color: isCanceled ? "#999" : isDelay ? "#ffd16fff" : "white", textDecorationLine: isCanceled ? "line-through" : "none" }}
>
{time}
</Text>
</View>
);
};
export const DependTime: FC<Props> = ({ time }) => (
<View style={{ flex: 3 }}>
<Text style={{ ...descriptionStyle, color: "white" }}>{time}</Text>
</View>
);

View File

@@ -5,17 +5,33 @@ type Props = {
lastStation: string;
ToData: string;
Station_JP: string;
se?: string;
};
export const LastStation: FC<Props> = ({ lastStation, ToData, Station_JP }) => {
export const LastStation: FC<Props> = ({ lastStation, ToData, Station_JP, se }) => {
const isEdit = !ToData ? false : ToData !== lastStation;
const string = isEdit ? ToData : lastStation;
const isCanceled = se?.includes("休");
return (
<View style={{ flex: 4, flexDirection: "row" }}>
{isCanceled && (
<Text
style={{
fontSize: parseInt("12%"),
color: "#ff6b6b",
fontWeight: "bold",
marginRight: 4,
}}
>
</Text>
)}
<Text
style={{
fontSize: lastStation?.length > 4 ? parseInt("12%") : parseInt("16%"),
color: isEdit ? "#ffd16fff" : "white",
color: isCanceled ? "#999" : isEdit ? "#ffd16fff" : "white",
fontWeight: "bold",
textDecorationLine: isCanceled ? "line-through" : "none",
}}
>
{string === Station_JP ? "当駅止" : string}

View File

@@ -0,0 +1,20 @@
import React, { FC } from "react";
import { Text, TextStyle, View } from "react-native";
const descriptionStyle: TextStyle = {
fontSize: parseInt("16%"),
fontWeight: "bold",
};
type Props = {
platform: string;
se?: string;
};
export const PlatformNumber: FC<Props> = ({ platform, se }) => {
const isCanceled = se?.includes("休");
return (
<View style={{ flex: 2 }}>
<Text style={{ ...descriptionStyle, color: isCanceled ? "#999" : "white", paddingLeft: 1, textDecorationLine: isCanceled ? "line-through" : "none" }}>
{platform}
</Text>
</View>
);
};

View File

@@ -0,0 +1,127 @@
import React, { FC, useEffect, useRef, useState } from "react";
import { Animated, Text, View, LayoutChangeEvent } from "react-native";
type Props = {
description: string;
};
export const ScrollingDescription: FC<Props> = ({ description }) => {
const scrollX = useRef(new Animated.Value(0)).current;
const [textWidth, setTextWidth] = useState(0);
const [containerWidth, setContainerWidth] = useState(0);
// 改行を削除して1行にする
const singleLineDescription = description?.replace(/\n/g, " ") || "";
useEffect(() => {
if (!singleLineDescription || textWidth === 0 || containerWidth === 0) {
return;
}
// テキストが画面幅より短い場合はスクロールしない
if (textWidth <= containerWidth) {
return;
}
// 初期位置を設定(画面の右端から開始)
scrollX.setValue(containerWidth);
const distance = textWidth + containerWidth;
const duration = distance * 10; // スクロール速度
const animation = Animated.loop(
Animated.sequence([
Animated.delay(500), // 最初に0.5秒待つ
Animated.timing(scrollX, {
toValue: -textWidth - 20,
duration: duration,
useNativeDriver: true,
}),
Animated.delay(500),
// 瞬時に右端に戻る
Animated.timing(scrollX, {
toValue: containerWidth,
duration: 0,
useNativeDriver: true,
}),
])
);
animation.start();
return () => {
animation.stop();
scrollX.setValue(containerWidth);
};
}, [singleLineDescription, textWidth, containerWidth]);
const handleTextLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
if (width > 0) {
setTextWidth(width);
}
};
const handleContainerLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
if (width > 0) {
setContainerWidth(width);
}
};
if (!singleLineDescription) {
return null;
}
return (
<View
style={{
width: "100%",
height: 20,
overflow: "hidden",
backgroundColor: "#000",
}}
onLayout={handleContainerLayout}
>
{/* 測定用の透明なテキスト(画面外、幅制限なし) */}
<View
style={{
position: "absolute",
top: -1000,
left: 0,
width: 9999, // 十分な幅を確保してテキストが折り返されないようにする
opacity: 0,
}}
>
<Text
style={{
fontSize: 16,
fontWeight: "bold",
}}
onLayout={handleTextLayout}
>
{singleLineDescription}
</Text>
</View>
{/* 実際に表示されるスクロールテキスト(幅制限なし) */}
<Animated.View
style={{
position: "absolute",
transform: [{ translateX: scrollX }],
width: 9999, // テキストが折り返されないように十分な幅を確保
}}
>
<Text
style={{
fontSize: 16,
fontWeight: "bold",
color: "#d3a203",
}}
>
{singleLineDescription}
</Text>
</Animated.View>
</View>
);
};

View File

@@ -6,11 +6,13 @@ const descriptionStyle: TextStyle = {
};
type Props = {
trainDelayStatus: string;
se?: string;
};
export const StatusAndDelay: FC<Props> = ({ trainDelayStatus }) => {
export const StatusAndDelay: FC<Props> = ({ trainDelayStatus, se }) => {
const isCanceled = se?.includes("休");
return (
<View style={{ flex: 4 }}>
<Text style={{ ...descriptionStyle, color: "white", paddingLeft: 1 }}>
<Text style={{ ...descriptionStyle, color: isCanceled ? "#999" : "#ffd16fff", paddingLeft: 1, textDecorationLine: isCanceled ? "line-through" : "none" }}>
{trainDelayStatus}
</Text>
</View>

View File

@@ -9,26 +9,35 @@ type Props = {
trainID: string;
type: trainTypeID;
isThrew: boolean;
se?: string;
};
export const TrainName: FC<Props> = (props) => {
const { trainName, trainNumDistance, trainIDSwitch, trainID, type, isThrew } = props;
const { trainName, trainNumDistance, trainIDSwitch, trainID, type, isThrew, se } =
props;
const { name, color } = getTrainType({ type });
const TrainNumber =
(trainNumDistance !== undefined && trainNumDistance !== "" && !isNaN(parseInt(trainNumDistance)))
trainNumDistance !== undefined &&
trainNumDistance !== "" &&
!isNaN(parseInt(trainNumDistance))
? `${
parseInt(trainID.replace("M", "").replace("D", "")) - parseInt(trainNumDistance)
parseInt(trainID.replace("M", "").replace("D", "")) -
parseInt(trainNumDistance)
}`
: "";
const isCanceled = se?.includes("休");
return (
<View style={{ flex: 9 }}>
<View style={{ flex: 9, flexDirection: "row", alignItems: "center" }}>
<Text
style={{
fontSize: trainName.length > 6 ? parseInt("12%") : parseInt("16%"),
color: color,
fontSize: trainName.length > 6 ? parseInt("11%") : parseInt("15%"),
color: isCanceled ? "#999" : color,
fontWeight: "bold",
textDecorationLine: isCanceled ? "line-through" : "none",
}}
>
{trainIDSwitch ? trainID : `${isThrew ? `★通過列車★` : `${name} ${trainName}${TrainNumber}`} `}
{trainIDSwitch
? trainID
: `${isThrew ? `★通過列車★` : `${name} ${trainName}${TrainNumber}`} `}
</Text>
</View>
);

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, FC } from "react";
import { View, useWindowDimensions } from "react-native";
import { View, useWindowDimensions, Text } from "react-native";
import { objectIsEmpty } from "@/lib/objectIsEmpty";
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
import { useAreaInfo } from "@/stateBox/useAreaInfo";
@@ -128,6 +128,22 @@ export const LED_vision: FC<props> = (props) => {
}}
>
<Header station={station[0]} />
<View
style={{
flexDirection: "row",
display: "flex",
justifyContent: "space-between",
paddingHorizontal: 10,
}}
>
<Text style={{ fontSize: 10, color: "white", flex: 9 }}>
/
</Text>
<Text style={{ fontSize: 10, color: "white", flex: 4 }}></Text>
<Text style={{ fontSize: 10, color: "white", flex: 2 }}></Text>
<Text style={{ fontSize: 10, color: "white", flex: 4 }}></Text>
</View>
{selectedTrain.map((d) => (
<EachData
{...{

View File

@@ -1 +1 @@
export const news = "2025-07-09";
export const news = "2026-02-01";

View File

@@ -31,6 +31,9 @@ export const API_ENDPOINTS = {
/** 位置情報問題データ */
POSITION_PROBLEMS: 'https://n8n.haruk.in/webhook/jrshikoku-position-problems',
/** 鉄道運用Hub運用データ */
UNYOHUB_DATA: 'https://jr-shikoku-api-data-storage.haruk.in/thirdparty/unyohub-unyo.json',
} as const;
/**

View File

@@ -15,7 +15,7 @@ export const INTERVALS = {
STORAGE_CHECK: 10000,
/** 遅延情報更新間隔(ミリ秒) */
DELAY_UPDATE: 60000,
DELAY_UPDATE: 30000,
/** 列車位置更新間隔(ミリ秒) */
TRAIN_POSITION_UPDATE: 5000,

View File

@@ -80,6 +80,13 @@ export const STORAGE_KEYS = {
/** 奇妙な列車通知 */
STRANGE_TRAIN: 'strangeTrain',
// 情報ソース設定系
/** 鉄道運用Hub使用設定 */
USE_UNYOHUB: 'useUnyohub',
/** 鉄道運用Hubデータ */
UNYOHUB_DATA: 'unyohubData',
} as const;
/**

View File

@@ -49,6 +49,7 @@ export type CustomTrainData = {
infogram: string;
via_data: string;
to_data: string;
to_data_color?: string[];
train_num_distance: string;
train_info: string;
train_number_override: string;
@@ -61,12 +62,15 @@ export type CustomTrainData = {
uwasa: string | null;
optional_text: string | null;
vehicle_info_url: string;
directions?: boolean;
};
export type eachTrainDiagramType = {
train: string;
time: string;
lastStation: string;
isThrough: boolean;
platformNum: string | null;
se?: string;
};
export type StationProps = {
@@ -76,6 +80,7 @@ export type CustomTrainData = {
StationMap: string;
StationNumber: string | null;
StationTimeTable: string;
StationName: string;
Station_EN: string;
Station_JP: string;
jslodApi: string;
@@ -89,6 +94,8 @@ export type OperationLogs = {
train_ids?: string[];
unit_ids?: string[];
vehicle_img: string;
vehicle_img_right: string;
vehicle_info_url: string;
related_train_ids?: string[];
state: number | null;
};

View File

@@ -4,13 +4,13 @@ export type colorString =
| "aqua"
| "#00a0bdff"
| "#007488ff"
| "red"
| "#ee424dff"
| "#297bff"
| "#ff7300ff"
| "#5f5f5fff"
| "#e000b0ff"
| "white"
| "black"
| "#333333ff"
| "pink";
type trainTypeString =
@@ -46,7 +46,7 @@ export const getTrainType: getTrainType = ({ type, id, whiteMode }) => {
switch (type) {
case "Normal":
return {
color: whiteMode ? "black" : "white",
color: whiteMode ? "#333333ff" : "white",
name: "普通列車",
shortName: "普通",
fontAvailable: true,
@@ -55,7 +55,7 @@ export const getTrainType: getTrainType = ({ type, id, whiteMode }) => {
};
case "OneMan":
return {
color: whiteMode ? "black" : "white",
color: whiteMode ? "#333333ff" : "white",
name: "普通列車(ワンマン)",
shortName: "普通",
fontAvailable: true,
@@ -82,7 +82,7 @@ export const getTrainType: getTrainType = ({ type, id, whiteMode }) => {
};
case "LTDEXP":
return {
color: "red",
color: "#ee424dff",
name: "特急",
shortName: "特急",
fontAvailable: true,
@@ -218,7 +218,7 @@ export const getTrainType: getTrainType = ({ type, id, whiteMode }) => {
}
}
return {
color: whiteMode ? "black" : "white",
color: whiteMode ? "#333333ff" : "white",
name: "その他",
shortName: "その他",
fontAvailable: false,

View File

@@ -46,9 +46,16 @@ export const trainTimeFiltering: (x: trainDataProps) => boolean = (props) => {
const delayData = currentTrain.filter((t) => t.num == d.train)[0].delay;
let delay = delayData === "入線" ? 0 : delayData;
const date = dayjs();
const IntH = parseInt(h) < 4 ? parseInt(h) + 24 : parseInt(h);
const IntH = parseInt(h);
const IntM = parseInt(m);
const targetDate = date.hour(IntH).minute(IntM + delay);
const currentHour = date.hour();
// 0時4時未満は、現在時刻が4時以上の場合のみ翌日として扱う
let targetDate = date.clone().hour(IntH).minute(IntM + delay).second(0).millisecond(0);
if (IntH < 4 && currentHour >= 4) {
targetDate = targetDate.add(1, 'day');
}
if (date.isAfter(targetDate)) return false;
if (targetDate.diff(date) < baseTime * 60 * 60 * 1000) return true;
return false;
@@ -66,14 +73,18 @@ export const getTime: getTimeProps = (stationDiagram, station) => {
lastStation: "",
isThrough: false,
train: trainNum,
platformNum: null,
se: undefined,
};
stationDiagram[trainNum].split("#").forEach((data) => {
const [stationName, type, time] = data.split(",");
const [stationName, type, time, platformNum] = data.split(",");
if (!type) return;
if (type.match("着")) {
trainData.lastStation = stationName;
}
if (stationName === station.Station_JP) {
trainData.platformNum = platformNum;
trainData.se = type;
if (type.match("発")) {
trainData.time = time;
} else if (type.match("通")) {
@@ -89,6 +100,8 @@ export const getTime: getTimeProps = (stationDiagram, station) => {
time: trainData.time,
lastStation: trainData.lastStation,
isThrough: trainData.isThrough,
platformNum: trainData.platformNum,
se: trainData.se,
};
})
.filter((d) => d.time);

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { AppState, AppStateStatus, Platform } from "react-native";
type Control = {
start: () => void;
@@ -12,10 +13,15 @@ type Fn = () => void;
export const useInterval = (fn: Fn, interval: number, autostart = true) => {
const onUpdateRef = useRef<Fn>();
const [state, setState] = useState("RUNNING");
// ユーザー操作によるSTOPAppStateによる一時停止と区別する
const userStoppedRef = useRef(!autostart);
const start = () => {
userStoppedRef.current = false;
setState("RUNNING");
};
const stop = () => {
userStoppedRef.current = true;
setState("STOPPED");
};
useEffect(() => {
@@ -23,22 +29,48 @@ export const useInterval = (fn: Fn, interval: number, autostart = true) => {
}, [fn]);
useEffect(() => {
if (autostart) {
userStoppedRef.current = false;
setState("RUNNING");
}else{
} else {
userStoppedRef.current = true;
setState("STOPPED");
}
}, [autostart]);
// バックグラウンド移行時に停止、フォアグラウンド復帰時に即時実行して再開
useEffect(() => {
let timerId;
if (Platform.OS === "web") return;
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === "active") {
if (!userStoppedRef.current) {
// 復帰直後に即時フェッチして最新データを取得
onUpdateRef.current?.();
setState("RUNNING");
}
} else if (nextAppState === "background" || nextAppState === "inactive") {
if (!userStoppedRef.current) {
// バックグラウンド中はインターバルを停止してムダなfetchエラーを防ぐ
setState("STOPPED");
}
}
};
const subscription = AppState.addEventListener("change", handleAppStateChange);
return () => {
subscription.remove();
};
}, []);
useEffect(() => {
let timerId: ReturnType<typeof setInterval> | undefined;
if (state === "RUNNING") {
timerId = setInterval(() => {
onUpdateRef.current?.();
}, interval);
} else {
timerId && clearInterval(timerId);
if (timerId) clearInterval(timerId);
}
return () => {
timerId && clearInterval(timerId);
if (timerId) clearInterval(timerId);
};
}, [interval, state]);
return [state, { start, stop }];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
/**
* 駅情報データ (みどりの窓口・ICカード対応状況)
* Feature フィールドは WebView 内で JSON.parse() して使用する。
*/
export interface StationDataItem {
StationName: string;
StationNumber: string;
/** JSON文字列: { Midori: { style: "normal"|"plus"|"none" }, IC: boolean } */
Feature: string;
}
export const STATION_DATA: StationDataItem[] = [
// ── 予讃線 ─────────────────────────────
{ StationName: "高松", StationNumber: "Y00", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "香西", StationNumber: "Y01", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "鬼無", StationNumber: "Y02", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "端岡", StationNumber: "Y03", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "国分", StationNumber: "Y04", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "讃岐府中", StationNumber: "Y05", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "鴨川", StationNumber: "Y06", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "八十場", StationNumber: "Y07", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "坂出", StationNumber: "Y08", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "宇多津", StationNumber: "Y09", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "丸亀", StationNumber: "Y10", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "讃岐塩屋", StationNumber: "Y11", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "多度津", StationNumber: "Y12", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "詫間", StationNumber: "Y14", Feature: '{"Midori":{"style":"plus"},"IC":true}' },
{ StationName: "観音寺", StationNumber: "Y19", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "川之江", StationNumber: "Y22", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "伊予三島", StationNumber: "Y23", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
{ StationName: "新居浜", StationNumber: "Y29", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
{ StationName: "伊予西条", StationNumber: "Y31", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
{ StationName: "壬生川", StationNumber: "Y36", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "今治", StationNumber: "Y40", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "伊予北条", StationNumber: "Y48", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "松山", StationNumber: "Y55", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
// ── 内子線・海線 ─────────────────────────
{ StationName: "内子", StationNumber: "U10", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "伊予大洲", StationNumber: "U14", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "伊予大洲", StationNumber: "S18", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "八幡浜", StationNumber: "U18", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
{ StationName: "宇和島", StationNumber: "U28", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
// ── 土讃線 ─────────────────────────────
{ StationName: "多度津", StationNumber: "D12", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "善通寺", StationNumber: "D14", Feature: '{"Midori":{"style":"plus"},"IC":true}' },
{ StationName: "琴平", StationNumber: "D15", Feature: '{"Midori":{"style":"plus"},"IC":true}' },
{ StationName: "阿波池田", StationNumber: "D22", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "土佐山田", StationNumber: "D37", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "後免", StationNumber: "D40", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "高知", StationNumber: "D45", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
// ── 予土線 ─────────────────────────────
{ StationName: "高知", StationNumber: "K00", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
{ StationName: "朝倉", StationNumber: "K05", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "須崎", StationNumber: "K19", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "窪川", StationNumber: "K26", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
// ── 高徳線 ─────────────────────────────
{ StationName: "高松", StationNumber: "T28", Feature: '{"Midori":{"style":"normal"},"IC":true}' },
{ StationName: "栗林公園北口", StationNumber: "T26", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "栗林", StationNumber: "T25", Feature: '{"Midori":{"style":"plus"},"IC":true}' },
{ StationName: "屋島", StationNumber: "T24", Feature: '{"Midori":{"style":"none"},"IC":true}' },
{ StationName: "志度", StationNumber: "T19", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "三本松", StationNumber: "T12", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "徳島", StationNumber: "T00", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
// ── 鳴門線 ─────────────────────────────
{ StationName: "鳴門", StationNumber: "N10", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
// ── 牟岐線 ─────────────────────────────
{ StationName: "阿南", StationNumber: "", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
{ StationName: "牟岐", StationNumber: "", Feature: '{"Midori":{"style":"normal"},"IC":false}' },
// ── 徳島線 ─────────────────────────────
{ StationName: "鴨島", StationNumber: "B09", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
{ StationName: "穴吹", StationNumber: "B16", Feature: '{"Midori":{"style":"plus"},"IC":false}' },
];

455
lib/webview/trainIconMap.ts Normal file
View File

@@ -0,0 +1,455 @@
/**
* 列番号 → 列車画像URL のマッピングデータ
*
* "__anpanman__" は動的URLのセンチネル値。
* WebView inject 側で アンパンマン列車判定 URL に変換される。
* (https://n8n.haruk.in/webhook/anpanman-pictures.png?trainNum=<列番>)
*/
const AP = "__anpanman__";
/** 完全一致マッピング (列番 → URL | "__anpanman__") */
export const TRAIN_ICON_MAP: Record<string, string> = {
// ── しおかぜ ───────────────────────────────────────
// 8000 ノーマル
"2M": "https://storage.haruk.in/s8000nr.png",
"4M": "https://storage.haruk.in/s8000nr.png",
"6M": "https://storage.haruk.in/s8000nr.png",
"14M": "https://storage.haruk.in/s8000nr.png",
"16M": "https://storage.haruk.in/s8000nr.png",
"18M": "https://storage.haruk.in/s8000nr.png",
"26M": "https://storage.haruk.in/s8000nr.png",
"28M": "https://storage.haruk.in/s8000nr.png",
"30M": "https://storage.haruk.in/s8000nr.png",
"1M": "https://storage.haruk.in/s8000nr.png",
"3M": "https://storage.haruk.in/s8000nr.png",
"5M": "https://storage.haruk.in/s8000nr.png",
"13M": "https://storage.haruk.in/s8000nr.png",
"15M": "https://storage.haruk.in/s8000nr.png",
"17M": "https://storage.haruk.in/s8000nr.png",
"25M": "https://storage.haruk.in/s8000nr.png",
"27M": "https://storage.haruk.in/s8000nr.png",
"29M": "https://storage.haruk.in/s8000nr.png",
// 8000 アンパン
"10M": AP,
"22M": AP,
"9M": AP,
"21M": AP,
// 8600
"8M": "https://storage.haruk.in/s8600.png",
"12M": "https://storage.haruk.in/s8600.png",
"20M": "https://storage.haruk.in/s8600.png",
"24M": "https://storage.haruk.in/s8600.png",
"7M": "https://storage.haruk.in/s8600.png",
"11M": "https://storage.haruk.in/s8600.png",
"19M": "https://storage.haruk.in/s8600.png",
"23M": "https://storage.haruk.in/s8600.png",
// ── いしづち ───────────────────────────────────────
// 8000 ノーマル
"1004M": "https://storage.haruk.in/s8000no.png",
"1006M": "https://storage.haruk.in/s8000no.png",
"1014M": "https://storage.haruk.in/s8000no.png",
"1016M": "https://storage.haruk.in/s8000no.png",
"1018M": "https://storage.haruk.in/s8000no.png",
"1026M": "https://storage.haruk.in/s8000no.png",
"1028M": "https://storage.haruk.in/s8000no.png",
"1030M": "https://storage.haruk.in/s8000no.png",
"1001M": "https://storage.haruk.in/s8000no.png",
"1003M": "https://storage.haruk.in/s8000no.png",
"1005M": "https://storage.haruk.in/s8000no.png",
"1013M": "https://storage.haruk.in/s8000no.png",
"1015M": "https://storage.haruk.in/s8000no.png",
"1017M": "https://storage.haruk.in/s8000no.png",
"1025M": "https://storage.haruk.in/s8000no.png",
"1027M": "https://storage.haruk.in/s8000no.png",
"1029M": "https://storage.haruk.in/s8000no.png",
// 8000 アンパン
"1010M": AP,
"1022M": AP,
"1009M": AP,
"1021M": AP,
// 8600
"1008M": "https://storage.haruk.in/s8600_isz.png",
"1012M": "https://storage.haruk.in/s8600_isz.png",
"1020M": "https://storage.haruk.in/s8600_isz.png",
"1024M": "https://storage.haruk.in/s8600_isz.png",
"1007M": "https://storage.haruk.in/s8600_isz.png",
"1011M": "https://storage.haruk.in/s8600_isz.png",
"1019M": "https://storage.haruk.in/s8600_isz.png",
"1023M": "https://storage.haruk.in/s8600_isz.png",
// MEXP
"1092M": "https://storage.haruk.in/s8000nr.png",
"1091M": "https://storage.haruk.in/s8600_isz.png",
// 三桁いしづち アンパン
"1041M": AP,
"1044M": AP,
// 三桁いしづち 8600
"1043M": "https://storage.haruk.in/s8600_isz.png",
"1042M": "https://storage.haruk.in/s8600_isz.png",
"1046M": "https://storage.haruk.in/s8600_isz.png",
// ── 南風 ───────────────────────────────────────────
// 2700 ノーマル
"34D": "https://storage.haruk.in/s2700.png",
"38D": "https://storage.haruk.in/s2700.png",
"40D": "https://storage.haruk.in/s2700.png",
"42D": "https://storage.haruk.in/s2700.png",
"46D": "https://storage.haruk.in/s2700.png",
"50D": "https://storage.haruk.in/s2700.png",
"52D": "https://storage.haruk.in/s2700.png",
"54D": "https://storage.haruk.in/s2700.png",
"58D": "https://storage.haruk.in/s2700.png",
"31D": "https://storage.haruk.in/s2700.png",
"35D": "https://storage.haruk.in/s2700.png",
"39D": "https://storage.haruk.in/s2700.png",
"41D": "https://storage.haruk.in/s2700.png",
"43D": "https://storage.haruk.in/s2700.png",
"47D": "https://storage.haruk.in/s2700.png",
"51D": "https://storage.haruk.in/s2700.png",
"53D": "https://storage.haruk.in/s2700.png",
"55D": "https://storage.haruk.in/s2700.png",
// 2700 アンパン
"32D": AP,
"36D": AP,
"44D": AP,
"48D": AP,
"56D": AP,
"33D": AP,
"37D": AP,
"45D": AP,
"49D": AP,
"57D": AP,
// ── うずしお ───────────────────────────────────────
// 2700
"3004D": "https://storage.haruk.in/s2700_uzu.png",
"3006D": "https://storage.haruk.in/s2700_uzu.png",
"3010D": "https://storage.haruk.in/s2700_uzu.png",
"3014D": "https://storage.haruk.in/s2700_uzu.png",
"3016D": "https://storage.haruk.in/s2700_uzu.png",
"3022D": "https://storage.haruk.in/s2700_uzu.png",
"3028D": "https://storage.haruk.in/s2700_uzu.png",
"3003D": "https://storage.haruk.in/s2700_uzu.png",
"3007D": "https://storage.haruk.in/s2700_uzu.png",
"3013D": "https://storage.haruk.in/s2700_uzu.png",
"3019D": "https://storage.haruk.in/s2700_uzu.png",
"3025D": "https://storage.haruk.in/s2700_uzu.png",
"3031D": "https://storage.haruk.in/s2700_uzu.png",
"3008D": "https://storage.haruk.in/s2700_uzu.png",
"3020D": "https://storage.haruk.in/s2700_uzu.png",
"3026D": "https://storage.haruk.in/s2700_uzu.png",
"3001D": "https://storage.haruk.in/s2700_uzu.png",
"3005D": "https://storage.haruk.in/s2700_uzu.png",
"3011D": "https://storage.haruk.in/s2700_uzu.png",
"3017D": "https://storage.haruk.in/s2700_uzu.png",
"3023D": "https://storage.haruk.in/s2700_uzu.png",
"3029D": "https://storage.haruk.in/s2700_uzu.png",
// 2600
"3002D": AP,
"3012D": AP,
"3018D": AP,
"3024D": AP,
"3030D": AP,
"3009D": AP,
"3015D": AP,
"3021D": AP,
"3027D": AP,
"3033D": AP,
// ── マリンライナー ─────────────────────────────────
"3104M": "https://storage.haruk.in/s5001.png",
"3106M": "https://storage.haruk.in/s5001.png",
"3108M": "https://storage.haruk.in/s5001.png",
"3110M": "https://storage.haruk.in/s5001.png",
"3112M": "https://storage.haruk.in/s5001.png",
"3114M": "https://storage.haruk.in/s5001.png",
"3116M": "https://storage.haruk.in/s5001.png",
"3118M": "https://storage.haruk.in/s5001.png",
"3120M": "https://storage.haruk.in/s5001.png",
"3122M": "https://storage.haruk.in/s5001.png",
"3124M": "https://storage.haruk.in/s5001.png",
"3126M": "https://storage.haruk.in/s5001.png",
"3128M": "https://storage.haruk.in/s5001.png",
"3130M": "https://storage.haruk.in/s5001.png",
"3132M": "https://storage.haruk.in/s5001.png",
"3134M": "https://storage.haruk.in/s5001.png",
"3136M": "https://storage.haruk.in/s5001.png",
"3138M": "https://storage.haruk.in/s5001.png",
"3140M": "https://storage.haruk.in/s5001.png",
"3142M": "https://storage.haruk.in/s5001.png",
"3144M": "https://storage.haruk.in/s5001.png",
"3146M": "https://storage.haruk.in/s5001.png",
"3148M": "https://storage.haruk.in/s5001.png",
"3150M": "https://storage.haruk.in/s5001.png",
"3152M": "https://storage.haruk.in/s5001.png",
"3154M": "https://storage.haruk.in/s5001.png",
"3156M": "https://storage.haruk.in/s5001.png",
"3158M": "https://storage.haruk.in/s5001.png",
"3160M": "https://storage.haruk.in/s5001.png",
"3162M": "https://storage.haruk.in/s5001.png",
"3164M": "https://storage.haruk.in/s5001.png",
"3166M": "https://storage.haruk.in/s5001.png",
"3168M": "https://storage.haruk.in/s5001.png",
"3170M": "https://storage.haruk.in/s5001.png",
"3105M": "https://storage.haruk.in/s5001.png",
"3107M": "https://storage.haruk.in/s5001.png",
"3109M": "https://storage.haruk.in/s5001.png",
"3111M": "https://storage.haruk.in/s5001.png",
"3113M": "https://storage.haruk.in/s5001.png",
"3115M": "https://storage.haruk.in/s5001.png",
"3117M": "https://storage.haruk.in/s5001.png",
"3119M": "https://storage.haruk.in/s5001.png",
"3121M": "https://storage.haruk.in/s5001.png",
"3123M": "https://storage.haruk.in/s5001.png",
"3125M": "https://storage.haruk.in/s5001.png",
"3127M": "https://storage.haruk.in/s5001.png",
"3129M": "https://storage.haruk.in/s5001.png",
"3131M": "https://storage.haruk.in/s5001.png",
"3133M": "https://storage.haruk.in/s5001.png",
"3135M": "https://storage.haruk.in/s5001.png",
"3137M": "https://storage.haruk.in/s5001.png",
"3139M": "https://storage.haruk.in/s5001.png",
"3141M": "https://storage.haruk.in/s5001.png",
"3143M": "https://storage.haruk.in/s5001.png",
"3145M": "https://storage.haruk.in/s5001.png",
"3147M": "https://storage.haruk.in/s5001.png",
"3149M": "https://storage.haruk.in/s5001.png",
"3151M": "https://storage.haruk.in/s5001.png",
"3153M": "https://storage.haruk.in/s5001.png",
"3155M": "https://storage.haruk.in/s5001.png",
"3157M": "https://storage.haruk.in/s5001.png",
"3159M": "https://storage.haruk.in/s5001.png",
"3161M": "https://storage.haruk.in/s5001.png",
"3163M": "https://storage.haruk.in/s5001.png",
"3165M": "https://storage.haruk.in/s5001.png",
"3167M": "https://storage.haruk.in/s5001.png",
"3169M": "https://storage.haruk.in/s5001.png",
"3175M": "https://storage.haruk.in/s5001.png",
// マリンライナー(快速)
"3102M": "https://storage.haruk.in/s5001k.png",
"3101M": "https://storage.haruk.in/s5001k.png",
"3103M": "https://storage.haruk.in/s5001k.png",
"3171M": "https://storage.haruk.in/s5001k.png",
"3173M": "https://storage.haruk.in/s5001k.png",
// ── サンライズ瀬戸 ─────────────────────────────────
"5032M": "https://storage.haruk.in/w285.png",
"5031M": "https://storage.haruk.in/w285.png",
"8041M": "https://storage.haruk.in/w285.png",
"8031M": "https://storage.haruk.in/w285.png",
// ── 宇和海 ─────────────────────────────────────────
// 2000 ノーマル
"1052D": "https://storage.haruk.in/s2000_uwa.png",
"1054D": "https://storage.haruk.in/s2000_uwa.png",
"1056D": "https://storage.haruk.in/s2000_uwa.png",
"1060D": "https://storage.haruk.in/s2000_uwa.png",
"1062D": "https://storage.haruk.in/s2000_uwa.png",
"1064D": "https://storage.haruk.in/s2000_uwa.png",
"1068D": "https://storage.haruk.in/s2000_uwa.png",
"1070D": "https://storage.haruk.in/s2000_uwa.png",
"1072D": "https://storage.haruk.in/s2000_uwa.png",
"1076D": "https://storage.haruk.in/s2000_uwa.png",
"1078D": "https://storage.haruk.in/s2000_uwa.png",
"1080D": "https://storage.haruk.in/s2000_uwa.png",
"1082D": "https://storage.haruk.in/s2000_uwa.png",
"1051D": "https://storage.haruk.in/s2000_uwa.png",
"1055D": "https://storage.haruk.in/s2000_uwa.png",
"1057D": "https://storage.haruk.in/s2000_uwa.png",
"1061D": "https://storage.haruk.in/s2000_uwa.png",
"1063D": "https://storage.haruk.in/s2000_uwa.png",
"1065D": "https://storage.haruk.in/s2000_uwa.png",
"1069D": "https://storage.haruk.in/s2000_uwa.png",
"1071D": "https://storage.haruk.in/s2000_uwa.png",
"1073D": "https://storage.haruk.in/s2000_uwa.png",
"1075D": "https://storage.haruk.in/s2000_uwa.png",
"1077D": "https://storage.haruk.in/s2000_uwa.png",
"1079D": "https://storage.haruk.in/s2000_uwa.png",
"1081D": "https://storage.haruk.in/s2000_uwa.png",
// 2000 アンパン
"1058D": AP,
"1066D": AP,
"1074D": AP,
"1053D": AP,
"1059D": AP,
"1067D": AP,
// ── しまんと ───────────────────────────────────────
"2002D": "https://storage.haruk.in/s2000_smn.png",
"2004D": "https://storage.haruk.in/s2000_smn.png",
"2001D": "https://storage.haruk.in/s2000_smn.png",
"2003D": "https://storage.haruk.in/s2000_smn.png",
// ── あしずり ───────────────────────────────────────
// 2000
"2074D": "https://storage.haruk.in/s2000_smn.png",
"2076D": "https://storage.haruk.in/s2000_smn.png",
"2080D": "https://storage.haruk.in/s2000_smn.png",
"2082D": "https://storage.haruk.in/s2000_smn.png",
"2071D": "https://storage.haruk.in/s2000_smn.png",
"2073D": "https://storage.haruk.in/s2000_smn.png",
"2079D": "https://storage.haruk.in/s2000_smn.png",
"2081D": "https://storage.haruk.in/s2000_smn.png",
// 2700
"2072D": "https://storage.haruk.in/s2700_asi.png",
"2078D": "https://storage.haruk.in/s2700_asi.png",
"2084D": "https://storage.haruk.in/s2700_asi.png",
"2075D": "https://storage.haruk.in/s2700_asi.png",
"2077D": "https://storage.haruk.in/s2700_asi.png",
"2083D": "https://storage.haruk.in/s2700_asi.png",
// ── 剣山 ───────────────────────────────────────────
"4002D": "https://storage.haruk.in/s185tu.png",
"4004D": "https://storage.haruk.in/s185tu.png",
"4006D": "https://storage.haruk.in/s185tu.png",
"4001D": "https://storage.haruk.in/s185tu.png",
"4003D": "https://storage.haruk.in/s185tu.png",
"4005D": "https://storage.haruk.in/s185tu.png",
"4007D": "https://storage.haruk.in/s185tu.png",
// ── よしのがわトロッコ ─────────────────────────────
"8452D": "https://storage.haruk.in/s185to_ai.png",
"8451D": "https://storage.haruk.in/s185to_ai.png",
// ── 岡山高松/琴平アントロ ──────────────────────────
"8176D": "https://storage.haruk.in/s32to4.png",
"8179D": "https://storage.haruk.in/s32to4.png",
"8277D": "https://storage.haruk.in/s32to4.png",
"8278D": "https://storage.haruk.in/s32to4.png",
// ── 千年ものがたり ─────────────────────────────────
"8021D": "https://storage.haruk.in/s185mm1.png",
"8022D": "https://storage.haruk.in/s185mm1.png",
// ── 夜明けものがたり ───────────────────────────────
"8082D": "https://storage.haruk.in/s185ym1.png",
"8083D": "https://storage.haruk.in/s185ym1.png",
"8073D": "https://storage.haruk.in/s185ym1.png",
"8074D": "https://storage.haruk.in/s185ym1.png",
// ── ラ・マルどこまでも ─────────────────────────────
"9253M": "https://storage.haruk.in/w213w.png",
"9256M": "https://storage.haruk.in/w213w.png",
// ── 貨物 ───────────────────────────────────────────
"74": "https://storage.haruk.in/ef210a.png",
"75": "https://storage.haruk.in/ef210a.png",
"70": "https://storage.haruk.in/ef210a.png",
"71": "https://storage.haruk.in/ef210a.png",
"73": "https://storage.haruk.in/ef210a.png",
"76": "https://storage.haruk.in/ef210a.png",
"3070": "https://storage.haruk.in/ef210a.png",
"3071": "https://storage.haruk.in/ef210a.png",
"3072": "https://storage.haruk.in/ef210a.png",
"3073": "https://storage.haruk.in/ef210a.png",
"3076": "https://storage.haruk.in/ef210a.png",
"3077": "https://storage.haruk.in/ef210a.png",
"3078": "https://storage.haruk.in/ef210a.png",
"3079": "https://storage.haruk.in/ef210a.png",
"8070": "https://storage.haruk.in/ef210a.png",
"8071": "https://storage.haruk.in/ef210a.png",
"8072": "https://storage.haruk.in/ef210a.png",
"8077": "https://storage.haruk.in/ef210a.png",
// ── 伊予灘ものがたり ───────────────────────────────
"8091D": "https://storage.haruk.in/s185iyor.png",
"8093D": "https://storage.haruk.in/s185iyor.png",
"8092D": "https://storage.haruk.in/s185iyoy.png",
"8094D": "https://storage.haruk.in/s185iyoy.png",
// ── 高徳線・徳島線・牟岐線・鳴門線 キハ40・47 ────
"4303D": "https://storage.haruk.in/s40.png",
"371D": "https://storage.haruk.in/s40.png",
"316D": "https://storage.haruk.in/s40.png",
"362D": "https://storage.haruk.in/s40.png",
"4376D": "https://storage.haruk.in/s40.png",
"951D": "https://storage.haruk.in/s40.png",
"953D": "https://storage.haruk.in/s40.png",
"955D": "https://storage.haruk.in/s40.png",
"973D": "https://storage.haruk.in/s40.png",
"975D": "https://storage.haruk.in/s40.png",
"977D": "https://storage.haruk.in/s40.png",
"979D": "https://storage.haruk.in/s40.png",
"981D": "https://storage.haruk.in/s40.png",
"950D": "https://storage.haruk.in/s40.png",
"968D": "https://storage.haruk.in/s40.png",
"970D": "https://storage.haruk.in/s40.png",
"972D": "https://storage.haruk.in/s40.png",
"974D": "https://storage.haruk.in/s40.png",
"976D": "https://storage.haruk.in/s40.png",
"980D": "https://storage.haruk.in/s40.png",
"982D": "https://storage.haruk.in/s40.png",
// ── 1000形 ─────────────────────────────────────────
"4311D": "https://storage.haruk.in/s1000.png",
"363D": "https://storage.haruk.in/s1000.png",
"356D": "https://storage.haruk.in/s1000.png",
"4374D": "https://storage.haruk.in/s1000.png",
"433D": "https://storage.haruk.in/s1000.png",
"4447D": "https://storage.haruk.in/s1000.png",
"451D": "https://storage.haruk.in/s1000.png",
"450D": "https://storage.haruk.in/s1000.png",
"4458D": "https://storage.haruk.in/s1000.png",
"474D": "https://storage.haruk.in/s1000.png",
// ── 1200形 ─────────────────────────────────────────
"4301D": "https://storage.haruk.in/s1200n.png",
"4327D": "https://storage.haruk.in/s1200n.png",
"4329D": "https://storage.haruk.in/s1200n.png",
"4343D": "https://storage.haruk.in/s1200n.png",
"353D": "https://storage.haruk.in/s1200n.png",
"355D": "https://storage.haruk.in/s1200n.png",
"367D": "https://storage.haruk.in/s1200n.png",
"310D": "https://storage.haruk.in/s1200n.png",
"4326D": "https://storage.haruk.in/s1200n.png",
"4334D": "https://storage.haruk.in/s1200n.png",
"4342D": "https://storage.haruk.in/s1200n.png",
"358D": "https://storage.haruk.in/s1200n.png",
"364D": "https://storage.haruk.in/s1200n.png",
"4453D": "https://storage.haruk.in/s1200n.png",
"4455D": "https://storage.haruk.in/s1200n.png",
"4457D": "https://storage.haruk.in/s1200n.png",
"463D": "https://storage.haruk.in/s1200n.png",
"475D": "https://storage.haruk.in/s1200n.png",
"477D": "https://storage.haruk.in/s1200n.png",
"485D": "https://storage.haruk.in/s1200n.png",
"4430D": "https://storage.haruk.in/s1200n.png",
"434D": "https://storage.haruk.in/s1200n.png",
"438D": "https://storage.haruk.in/s1200n.png",
"4460D": "https://storage.haruk.in/s1200n.png",
"4464D": "https://storage.haruk.in/s1200n.png",
"4466D": "https://storage.haruk.in/s1200n.png",
"478D": "https://storage.haruk.in/s1200n.png",
"484D": "https://storage.haruk.in/s1200n.png",
"957D": "https://storage.haruk.in/s1200n.png",
"4959D": "https://storage.haruk.in/s1200n.png",
"4963D": "https://storage.haruk.in/s1200n.png",
"4967D": "https://storage.haruk.in/s1200n.png",
"4971D": "https://storage.haruk.in/s1200n.png",
"952D": "https://storage.haruk.in/s1200n.png",
"4954D": "https://storage.haruk.in/s1200n.png",
"4958D": "https://storage.haruk.in/s1200n.png",
"4962D": "https://storage.haruk.in/s1200n.png",
"4966D": "https://storage.haruk.in/s1200n.png",
// ── 半定期臨時 ─────────────────────────────────────
"9174M": "https://storage.haruk.in/s5001.png",
"9395D": "https://storage.haruk.in/s1500.png",
};
/** 正規表現パターンマッチング (完全一致で未ヒットの場合に評価) */
export const TRAIN_ICON_REGEX: Array<{ pattern: string; url: string }> = [
// 高徳線 普通
{ pattern: "^(4|5)3\\d\\dD$", url: "https://storage.haruk.in/s1500.png" },
{ pattern: "^3\\d\\dD$", url: "https://storage.haruk.in/s1500.png" },
// 徳島線 普通
{ pattern: "^(4|5)4\\d\\dD$", url: "https://storage.haruk.in/s1500.png" },
{ pattern: "^4\\d\\dD$", url: "https://storage.haruk.in/s1500.png" },
// 鳴門線 普通
{
pattern: "^(4|5)9(5|6|7|8)\\dD$",
url: "https://storage.haruk.in/s1500.png",
},
{ pattern: "^9(5|6|7|8)\\dD$", url: "https://storage.haruk.in/s1500.png" },
];

View File

@@ -0,0 +1,35 @@
/**
* 列車種別ごとの表示設定
*
* - typeColor : nameReplace で使う種別ラベルの文字色
* - borderColor: setNewTrainItem で使う枠線色
* - bgColor : setNewTrainItem で使う背景色
* - label : 種別ラベル文字列
* - isWanman : ワンマン列車かどうか
*/
export interface TrainTypeConfig {
label: string;
typeColor: string;
borderColor: string;
bgColor: string;
isWanman: boolean;
}
export const TRAIN_TYPE_CONFIG: Record<string, TrainTypeConfig> = {
Normal: { label: "普通", typeColor: "black", borderColor: "black", bgColor: "#ffffffcc", isWanman: false },
OneMan: { label: "普通", typeColor: "black", borderColor: "black", bgColor: "#ffffffcc", isWanman: true },
Rapid: { label: "快速", typeColor: "rgba(0, 140, 255, 1)", borderColor: "rgba(0, 140, 255, 1)", bgColor: "#ffffffcc", isWanman: false },
OneManRapid: { label: "快速", typeColor: "rgba(0, 140, 255, 1)", borderColor: "rgba(0, 140, 255, 1)", bgColor: "#ffffffcc", isWanman: true },
LTDEXP: { label: "特急", typeColor: "red", borderColor: "red", bgColor: "#ffffffcc", isWanman: false },
NightLTDEXP: { label: "寝台特急", typeColor: "#d300b0ff", borderColor: "#d300b0ff", bgColor: "#ffffffcc", isWanman: false },
SPCL: { label: "臨時", typeColor: "#008d07ff", borderColor: "#008d07ff", bgColor: "#ffffffcc", isWanman: false },
SPCL_Normal: { label: "臨時", typeColor: "#008d07ff", borderColor: "#008d07ff", bgColor: "#ffffffcc", isWanman: false },
SPCL_Rapid: { label: "臨時快速", typeColor: "rgba(0, 81, 255, 1)", borderColor: "#0051ffff", bgColor: "#ffffffcc", isWanman: false },
SPCL_EXP: { label: "臨時特急", typeColor: "#a52e2eff", borderColor: "#a52e2eff", bgColor: "#ffffffcc", isWanman: false },
Party: { label: "団体臨時", typeColor: "#ff7300ff", borderColor: "#ff7300ff", bgColor: "#ffd0a9ff", isWanman: false },
Freight: { label: "貨物", typeColor: "#00869ecc", borderColor: "#00869ecc", bgColor: "#c7c7c7cc", isWanman: false },
Forwarding: { label: "回送", typeColor: "#727272cc", borderColor: "#727272cc", bgColor: "#c7c7c7cc", isWanman: false },
Trial: { label: "試運転", typeColor: "#727272cc", borderColor: "#727272cc", bgColor: "#c7c7c7cc", isWanman: false },
Construction: { label: "工事", typeColor: "#727272cc", borderColor: "#727272cc", bgColor: "#c7c7c7cc", isWanman: false },
FreightForwarding: { label: "単機回送", typeColor: "#727272cc", borderColor: "#727272cc", bgColor: "#c7c7c7cc", isWanman: false },
};

View File

@@ -2,7 +2,13 @@ import trainList from "@/assets/originData/trainList";
import { CustomTrainData, OperationLogs } from "@/lib/CommonTypes";
import useInterval from "@/lib/useInterval";
import { AS } from "@/storageControl";
import React, { createContext, FC, useContext, useEffect, useState } from "react";
import React, {
createContext,
FC,
useContext,
useEffect,
useState,
} from "react";
import { API_ENDPOINTS, STORAGE_KEYS } from "@/constants";
const initialState = {
allTrainDiagram: {},
@@ -27,9 +33,13 @@ export const useAllTrainDiagram = () => useContext(AllTrainDiagramContext);
type Props = {
children: React.ReactNode;
};
export const AllTrainDiagramProvider:FC<Props> = ({ children }) => {
const [allTrainDiagram, setAllTrainDiagram] = useState<{ [key: string]: string }>(trainList);
const [allCustomTrainData, setAllCustomTrainData] = useState<CustomTrainData[]>([]); // カスタム列車データ
export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
const [allTrainDiagram, setAllTrainDiagram] = useState<{
[key: string]: string;
}>(trainList);
const [allCustomTrainData, setAllCustomTrainData] = useState<
CustomTrainData[]
>([]); // カスタム列車データ
const [keyList, setKeyList] = useState<string[]>([]); // 第二要素
useEffect(() => {
if (allTrainDiagram && Object.keys(allTrainDiagram).length > 0)
@@ -47,7 +57,11 @@ export const AllTrainDiagramProvider:FC<Props> = ({ children }) => {
});
//dataのkeyで並び替え
const sortedData = Object.keys(data)
.sort((a, b) => parseInt(a.replace(/[D,M]/, "")) - parseInt(b.replace(/[D,M]/, "")))
.sort(
(a, b) =>
parseInt(a.replace(/[D,M]/, "")) -
parseInt(b.replace(/[D,M]/, ""))
)
.reduce((acc, key) => {
acc[key] = data[key];
return acc;
@@ -87,7 +101,7 @@ export const AllTrainDiagramProvider:FC<Props> = ({ children }) => {
getCustomTrainData();
}, []);
useInterval(getCustomTrainData, 30000); // 30秒毎にカスタム列車データ取得
const [todayOperation, setTodayOperation] = useState<OperationLogs[]>([]); // 本日の運行情報
const getTodayOperation = () => {
fetch(API_ENDPOINTS.OPERATION_LOGS)
@@ -109,15 +123,22 @@ export const AllTrainDiagramProvider:FC<Props> = ({ children }) => {
getTodayOperation();
}, []);
useInterval(getTodayOperation, 30000); // 30秒毎にカスタム列車データ取得
const getTodayOperationByTrainId = (train_id: string) => {
const returnData: OperationLogs[] = [];
todayOperation.forEach((operation) => {
if (operation.train_ids?.includes(train_id.toString())) {
returnData.push(operation);
}
else if (operation.related_train_ids?.includes(train_id.toString())) {
returnData.push(operation);
if (operation.train_ids?.length > 0) {
const trainIds = operation.train_ids.map((x) => x.split(",")[0]);
if (trainIds.includes(train_id.toString())) {
returnData.push(operation);
}
} else if (operation.related_train_ids?.length > 0) {
const trainIds = operation.related_train_ids.map(
(x) => x.split(",")[0]
);
if (trainIds.includes(train_id.toString())) {
returnData.push(operation);
}
}
});
return returnData.length > 0 ? returnData : [];
@@ -131,7 +152,7 @@ export const AllTrainDiagramProvider:FC<Props> = ({ children }) => {
allCustomTrainData,
keyList,
todayOperation,
getTodayOperationByTrainId
getTodayOperationByTrainId,
}}
>
{children}

View File

@@ -39,6 +39,8 @@ const initialState = {
setTrainMenu: (e) => {},
updatePermission: false,
setUpdatePermission: (e) => {},
/** 各情報ソースの利用権限 */
dataSourcePermission: { unyohub: false } as { unyohub: boolean },
injectJavascript: "",
};
@@ -62,20 +64,23 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
const [stationMenu, setStationMenu] = useState<boolType>(undefined);
const [LoadError, setLoadError] = useState(false);
//更新権限所有確認
//更新権限所有確認・情報ソース別利用権限(将来ロールが増えたらここに足す)
const [updatePermission, setUpdatePermission] = useState(false);
const [dataSourcePermission, setDataSourcePermission] = useState<{ unyohub: boolean }>({ unyohub: false });
useEffect(() => {
if (!expoPushToken) return;
fetch(
"https://n8n.haruk.in/webhook/data-edit-permission?token=" + expoPushToken
`https://jr-shikoku-backend-api-v1.haruk.in/check-permission?user_id=${expoPushToken}`
)
.then((res) => res.json())
.then((res) => {
if (res.data == true) {
setUpdatePermission(true);
} else {
setUpdatePermission(false);
}
});
const role: string = res.permission ?? "";
setUpdatePermission(role === "administrator");
setDataSourcePermission({
unyohub: role === "administrator" || role === "unyoHubEditor",
});
})
.catch(() => {});
}, [expoPushToken]);
//列車情報表示関連
@@ -90,15 +95,19 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
//GUIデザインベース
const [uiSetting, setUiSetting] = useState("tokyo");
// 鉄道運用Hub使用設定
const [useUnyohubSetting, setUseUnyohubSetting] = useState("false");
//地図表示テキスト
const injectJavascript = injectJavascriptData(
const injectJavascript = injectJavascriptData({
mapSwitch,
iconSetting,
stationMenu,
trainMenu,
uiSetting
);
uiSetting,
useUnyohub: useUnyohubSetting,
});
useEffect(() => {
//列車アイコンスイッチ
@@ -111,6 +120,8 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
ASCore({ k: STORAGE_KEYS.TRAIN_SWITCH, s: setTrainMenu, d: "true", u: true });
//GUIデザインベーススイッチ
ASCore({ k: STORAGE_KEYS.UI_SETTING, s: setUiSetting, d: "tokyo", u: true });
//鉄道運用Hubスイッチ
ASCore({ k: STORAGE_KEYS.USE_UNYOHUB, s: setUseUnyohubSetting, d: "false", u: true });
}, []);
return (
@@ -136,6 +147,7 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
setTrainMenu,
updatePermission,
setUpdatePermission,
dataSourcePermission,
injectJavascript,
}}
>

116
stateBox/useUnyohub.tsx Normal file
View File

@@ -0,0 +1,116 @@
import { useState, useEffect } from "react";
import { AS } from "../storageControl";
import { STORAGE_KEYS } from "@/constants";
import { API_ENDPOINTS } from "@/constants";
import type { UnyohubResponse, UnyohubData } from "@/types/unyohub";
type UnyohubHook = {
/** 鉄道運用Hub使用設定 */
useUnyohub: boolean;
/** 鉄道運用Hubデータ */
unyohubData: UnyohubResponse;
/** 指定した列番の運用情報を文字列で取得(後方互換) */
getUnyohubByTrainNumber: (trainNumber: string) => string | null;
/** 指定した列番に紐づく UnyohubData の配列を取得 */
getUnyohubEntriesByTrainNumber: (trainNumber: string) => UnyohubData[];
/** 鉄道運用Hub使用設定を更新 */
setUseUnyohub: (value: boolean) => void;
};
export const useUnyohub = (): UnyohubHook => {
const [useUnyohub, setUseUnyohubState] = useState(false);
const [unyohubData, setUnyohubData] = useState<UnyohubResponse>([]);
// 初期読み込み
useEffect(() => {
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
setUseUnyohubState(value === true || value === "true");
});
AS.getItem(STORAGE_KEYS.UNYOHUB_DATA).then((value) => {
if (value) {
try {
setUnyohubData(JSON.parse(value as string));
} catch (e) {
console.error("Failed to parse unyohub data", e);
}
}
});
}, []);
// データ更新処理
useEffect(() => {
if (!useUnyohub) return;
const fetchUnyohubData = async () => {
try {
// キャッシュバスティング用にタイムスタンプを追加
const cacheBuster = '?_=' + Date.now();
const response = await fetch(API_ENDPOINTS.UNYOHUB_DATA + cacheBuster);
const data = await response.json();
setUnyohubData(data);
await AS.setItem(STORAGE_KEYS.UNYOHUB_DATA, JSON.stringify(data));
} catch (error) {
console.error("Failed to fetch unyohub data", error);
}
};
fetchUnyohubData();
// 10分ごとにデータを更新
const interval = setInterval(fetchUnyohubData, 10 * 60 * 1000);
return () => clearInterval(interval);
}, [useUnyohub]);
// 列番から運用番号を取得複数ある場合はposition_forward順
const getUnyohubByTrainNumber = (trainNumber: string): string | null => {
if (!useUnyohub || unyohubData.length === 0) return null;
const foundUnyos: Array<{ formations: string; position_forward: number; position_rear: number }> = [];
for (const unyo of unyohubData) {
if (!unyo.trains) continue;
const found = unyo.trains.find(train => train.train_number === trainNumber);
if (found) {
foundUnyos.push({
formations: unyo.formations,
position_forward: found.position_forward,
position_rear: found.position_rear,
});
}
}
if (foundUnyos.length === 0) return null;
// position_forward順にソート
foundUnyos.sort((a, b) => a.position_forward - b.position_forward);
// 「運用番号(編成位置)」の形式で結合
return foundUnyos
.map(u => `${u.formations}(${u.position_forward}-${u.position_rear})`)
.join(', ');
};
// 列番に紐づく UnyohubData エントリをすべて取得
const getUnyohubEntriesByTrainNumber = (trainNumber: string): UnyohubData[] => {
if (!useUnyohub || unyohubData.length === 0) return [];
return unyohubData.filter(
(unyo) => unyo.trains?.some((t) => t.train_number === trainNumber)
);
};
// 設定を更新
const setUseUnyohub = (value: boolean) => {
setUseUnyohubState(value);
AS.setItem(STORAGE_KEYS.USE_UNYOHUB, value.toString());
};
return {
useUnyohub,
unyohubData,
getUnyohubByTrainNumber,
getUnyohubEntriesByTrainNumber,
setUseUnyohub,
};
};

View File

@@ -31,12 +31,16 @@ export type SeTypes =
| "頃編"
| "発"
| "着"
| "休編"
| "通休編"
| "休編" // 後方互換性のため残す(非推奨)
| "休発" // 運休の出発(本家データ)
| "休着" // 運休の到着(本家データ)
| "休発編" // 運休の出発(追加データ)
| "休着編" // 運休の到着(追加データ)
| "通休編" // 運休の通過(追加データ)
| "通発編"
| "通着編"
| "通発休編"
| "通着休編"
| "通発休編" // 運休の運転停車発(追加データ)
| "通着休編" // 運休の運転停車着(追加データ)
| string;
/**

35
types/unyohub.ts Normal file
View File

@@ -0,0 +1,35 @@
/**
* 鉄道運用Hub APIのデータ型定義
*/
export type UnyohubTrain = {
train_number: string;
line_id: string;
first_departure_time: string;
final_arrival_time: string;
starting_station: string;
terminal_station: string;
position_forward: number;
position_rear: number;
direction: string;
};
export type UnyohubData = {
formations: string;
posts_count: number;
from_beginner: boolean;
trains: UnyohubTrain[];
starting_location: string;
starting_track: string;
starting_time: string;
terminal_location: string;
terminal_track: string;
ending_time: string;
car_count: number;
min_car_count: number;
max_car_count: number;
main_color: string;
comment: string | null;
};
export type UnyohubResponse = UnyohubData[];

View File

@@ -16,10 +16,15 @@ const SE_MAPPING: Record<string, SeStringResult> = {
"通着編": ["到着", "community"],
"通編": ["通過", "community"],
"頃編": ["頃", "community"],
"休編": ["運休", "community"],
"休編": ["運休", "community"],
"通発休編": ["運休", "community"],
"通着休編": ["運休", "community"],
// 運休系
"休編": ["運休", "community"], // 後方互換性のため残す
"休発": ["出発", "normal"],
"休着": ["到着", "normal"],
"休発編": ["出発", "community"],
"休着編": ["到着", "community"],
"通休編": ["通過", "community"],
"通発休編": ["出発", "community"],
"通着休編": ["到着", "community"],
};
/**
@@ -35,7 +40,14 @@ export const parseSeString = (se: SeTypes): SeStringResult => {
* 運休系のSEかどうかを判定
*/
export const isCanceledSe = (se: SeTypes): boolean => {
return se === "休編" || se === "通休編" || se === "通発休編" || se === "通着休編";
return se === "休編" ||
se === "休発" ||
se === "休着" ||
se === "休発編" ||
se === "休着編" ||
se === "通休編" ||
se === "通発休編" ||
se === "通着休編";
};
/**