From a42c0871bdfa1e9ef64c2bd3b07e4153d8750281 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sat, 7 Feb 2026 17:19:16 +0000 Subject: [PATCH] =?UTF-8?q?unyohub=E9=80=A3=E6=90=BA=E4=BB=AE=E4=BD=9C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EachTrainInfoCore/HeaderText.tsx | 15 ++- components/Settings/DataSourceSettings.tsx | 121 ++++++++++++++++++ components/Settings/SettingTopPage.tsx | 6 + components/Settings/settings.tsx | 12 ++ constants/api.ts | 3 + constants/storage.ts | 7 + lib/webViewInjectjavascript.ts | 31 ++++- stateBox/useTrainMenu.tsx | 7 +- stateBox/useUnyohub.tsx | 102 +++++++++++++++ types/unyohub.ts | 35 +++++ 10 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 components/Settings/DataSourceSettings.tsx create mode 100644 stateBox/useUnyohub.tsx create mode 100644 types/unyohub.ts diff --git a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx index 46dfa3b..da2c70d 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx @@ -5,6 +5,7 @@ import { TextStyle, TouchableOpacity, useWindowDimensions, + Alert, } from "react-native"; import { SheetManager } from "react-native-actions-sheet"; import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName"; @@ -19,6 +20,7 @@ import { getStringConfig } from "@/lib/getStringConfig"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import { getPDFViewURL } from "@/lib/getPdfViewURL"; import type { NavigateFunction } from "@/types"; +import { useUnyohub } from "@/stateBox/useUnyohub"; type Props = { data: { trainNum: string; limited: string }; @@ -57,6 +59,7 @@ export const HeaderText: FC = ({ const { allCustomTrainData, getTodayOperationByTrainId } = useAllTrainDiagram(); const { expoPushToken } = useNotification(); + const { getUnyohubByTrainNumber } = useUnyohub(); // 列車名、種別、フォントの取得 const [ @@ -143,6 +146,9 @@ export const HeaderText: FC = ({ const todayOperation = getTodayOperationByTrainId(trainNum).filter( (d) => d.state !== 100, ); + + const unyohubFormation = getUnyohubByTrainNumber(trainNum); + return ( = ({ 200 || todayOperation?.length > 0 ? "yellow" : "white" + priority > 200 || todayOperation?.length > 0 || unyohubFormation + ? "yellow" + : "white" } size={30} style={{ margin: 5 }} onPress={() => { + if (unyohubFormation) { + Alert.alert("運用情報", `運用番号: ${unyohubFormation}`, [ + { text: "OK" }, + ]); + } const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`; navigate("generalWebView", { uri, useExitButton: false }); SheetManager.hide("EachTrainInfo"); diff --git a/components/Settings/DataSourceSettings.tsx b/components/Settings/DataSourceSettings.tsx new file mode 100644 index 0000000..07e2016 --- /dev/null +++ b/components/Settings/DataSourceSettings.tsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from "react"; +import { View, Text, ScrollView, StyleSheet } from "react-native"; +import { Switch } from "react-native-elements"; +import { useNavigation } from "@react-navigation/native"; +import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem"; +import { AS } from "../../storageControl"; +import { STORAGE_KEYS } from "@/constants"; + +export const DataSourceSettings = () => { + const navigation = useNavigation(); + 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 ( + + navigation.goBack(), + }} + /> + + + 外部データソース + + + + 列車運用hub + + 列車の運用番号(車両編成番号)を表示します。 + {"\n"} + データがある列車では地図上に黄色いマークが表示され、列車情報画面のデータベースアイコンも黄色になります。 + + + + + + + + + 情報ソース設定は、外部のコミュニティデータソースとの連携を管理します。 + {"\n\n"} + データの正確性は保証されません。参考情報としてご利用ください。 + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#0099CC", + }, + content: { + flex: 1, + backgroundColor: "#f8f8fc", + }, + section: { + backgroundColor: "white", + marginTop: 20, + marginHorizontal: 10, + borderRadius: 10, + padding: 15, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "bold", + color: "#333", + marginBottom: 15, + }, + settingItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 10, + }, + settingTextContainer: { + flex: 1, + marginRight: 10, + }, + settingTitle: { + fontSize: 16, + fontWeight: "bold", + color: "#333", + marginBottom: 5, + }, + settingDescription: { + fontSize: 13, + color: "#666", + lineHeight: 18, + }, + infoSection: { + backgroundColor: "#fff3cd", + marginTop: 20, + marginHorizontal: 10, + borderRadius: 10, + padding: 15, + marginBottom: 30, + }, + infoText: { + fontSize: 13, + color: "#856404", + lineHeight: 18, + }, +}); diff --git a/components/Settings/SettingTopPage.tsx b/components/Settings/SettingTopPage.tsx index 90ae074..c815d2a 100644 --- a/components/Settings/SettingTopPage.tsx +++ b/components/Settings/SettingTopPage.tsx @@ -108,6 +108,12 @@ export const SettingTopPage = ({ navigation.navigate("setting", { screen: "LayoutSettings" }) } /> + + navigation.navigate("setting", { screen: "DataSourceSettings" }) + } + /> {Platform.OS === "android" ? ( + ); } diff --git a/constants/api.ts b/constants/api.ts index c4b0a29..6c2d654 100644 --- a/constants/api.ts +++ b/constants/api.ts @@ -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; /** diff --git a/constants/storage.ts b/constants/storage.ts index cfa640c..aca0b13 100644 --- a/constants/storage.ts +++ b/constants/storage.ts @@ -80,6 +80,13 @@ export const STORAGE_KEYS = { /** 奇妙な列車通知 */ STRANGE_TRAIN: 'strangeTrain', + + // 情報ソース設定系 + /** 列車運用hub使用設定 */ + USE_UNYOHUB: 'useUnyohub', + + /** 列車運用hubデータ */ + UNYOHUB_DATA: 'unyohubData', } as const; /** diff --git a/lib/webViewInjectjavascript.ts b/lib/webViewInjectjavascript.ts index 454dacc..8ca37a4 100644 --- a/lib/webViewInjectjavascript.ts +++ b/lib/webViewInjectjavascript.ts @@ -5,14 +5,16 @@ type InjectJavascriptData = ( b: string, c: string, d: string, - e: string + e: string, + f: string ) => string; export const injectJavascriptData: InjectJavascriptData = ( mapSwitch, iconSetting, stationMenu, trainMenu, - uiSetting + uiSetting, + unyohubData = "[]" ) => { // 一番上のメニュー非表示 地図スイッチによって切り替え const topMenu = @@ -83,6 +85,28 @@ export const injectJavascriptData: InjectJavascriptData = ( setTimeout(operationListUpdate, ${INTERVALS.DELAY_UPDATE}); } operationListUpdate(); + + // 列車運用hubデータ + let unyohubData = ${unyohubData}; + const getUnyohubFormation = (trainNumber) => { + if (!unyohubData || unyohubData.length === 0) return null; + const foundUnyos = []; + for (const unyo of unyohubData) { + 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(', '); + }; + const sortOperationalList = (a, b,targetTrainID) => { // trainIdからカンマ以降の数字を抽出する関数 const extractOrderNumber = (trainId) => { @@ -1055,7 +1079,8 @@ export const injectJavascriptData: InjectJavascriptData = ( } const optionalTextColor = optionalText.includes("最終") ? "red" : "black"; - 行き先情報.insertAdjacentHTML('beforebegin', "
" + ( isEdit ? "
" : isSeason ? "
" : "") + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + (isWanman ? "ワンマン " : "") + "

" + viaData + "

" + optionalText + "

" + trainName + "

" + (ToData ? ToData + "行" : ToData) + "

" + trainType + "

" + (hasProblem ? "‼️停止中‼️" : "") + "

"); + const hasUnyohub = getUnyohubFormation(列番データ) !== null; + 行き先情報.insertAdjacentHTML('beforebegin', "
" + ( isEdit ? "
" : hasUnyohub ? "
" : isSeason ? "
" : "") + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + (isWanman ? "ワンマン " : "") + "

" + viaData + "

" + optionalText + "

" + trainName + "

" + (ToData ? ToData + "行" : ToData) + "

" + trainType + "

" + (hasProblem ? "‼️停止中‼️" : "") + "

"); `: ` 行き先情報.insertAdjacentHTML('beforebegin', "

" + returnText1 + "

"); 行き先情報.insertAdjacentHTML('beforebegin', "

" + (ToData ? ToData + "行 " : ToData) + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

"); diff --git a/stateBox/useTrainMenu.tsx b/stateBox/useTrainMenu.tsx index 3eae8ea..7256255 100644 --- a/stateBox/useTrainMenu.tsx +++ b/stateBox/useTrainMenu.tsx @@ -10,6 +10,7 @@ import { ASCore } from "../storageControl"; import { getStationList2 } from "../lib/getStationList"; import { injectJavascriptData } from "../lib/webViewInjectjavascript"; +import { useUnyohub } from "./useUnyohub"; import { useNotification } from "../stateBox/useNotifications"; import { STORAGE_KEYS } from "@/constants/storage"; @@ -90,6 +91,9 @@ export const TrainMenuProvider: FC = ({ children }) => { //GUIデザインベース const [uiSetting, setUiSetting] = useState("tokyo"); + + //列車運用hub + const { unyohubData } = useUnyohub(); //地図表示テキスト const injectJavascript = injectJavascriptData( @@ -97,7 +101,8 @@ export const TrainMenuProvider: FC = ({ children }) => { iconSetting, stationMenu, trainMenu, - uiSetting + uiSetting, + JSON.stringify(unyohubData) ); useEffect(() => { diff --git a/stateBox/useUnyohub.tsx b/stateBox/useUnyohub.tsx new file mode 100644 index 0000000..a350950 --- /dev/null +++ b/stateBox/useUnyohub.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from "react"; +import { AS } from "../storageControl"; +import { STORAGE_KEYS } from "@/constants"; +import { API_ENDPOINTS } from "@/constants"; +import type { UnyohubResponse } from "@/types/unyohub"; + +type UnyohubHook = { + /** 列車運用hub使用設定 */ + useUnyohub: boolean; + /** 列車運用hubデータ */ + unyohubData: UnyohubResponse; + /** 指定した列番の運用情報を取得 */ + getUnyohubByTrainNumber: (trainNumber: string) => string | null; + /** 列車運用hub使用設定を更新 */ + setUseUnyohub: (value: boolean) => void; +}; + +export const useUnyohub = (): UnyohubHook => { + const [useUnyohub, setUseUnyohubState] = useState(false); + const [unyohubData, setUnyohubData] = useState([]); + + // 初期読み込み + 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 response = await fetch(API_ENDPOINTS.UNYOHUB_DATA); + 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) { + 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(', '); + }; + + // 設定を更新 + const setUseUnyohub = (value: boolean) => { + setUseUnyohubState(value); + AS.setItem(STORAGE_KEYS.USE_UNYOHUB, value.toString()); + }; + + return { + useUnyohub, + unyohubData, + getUnyohubByTrainNumber, + setUseUnyohub, + }; +}; diff --git a/types/unyohub.ts b/types/unyohub.ts new file mode 100644 index 0000000..088e31c --- /dev/null +++ b/types/unyohub.ts @@ -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[];