import { INTERVALS, API_ENDPOINTS } from '@/constants'; import { TRAIN_TYPE_CONFIG } from './webview/trainTypeConfig'; import { TRAIN_ICON_MAP, TRAIN_ICON_REGEX } from './webview/trainIconMap'; import { STATION_DATA } from './webview/stationData'; export interface InjectJavascriptOptions { /** 地図スイッチ ("true" | "false") */ mapSwitch: string; /** 列車アイコン表示スイッチ ("true" | "false") */ iconSetting: string; /** 駅メニュースイッチ ("true" | "false") */ stationMenu: string; /** 列車メニュースイッチ ("true" | "false") */ trainMenu: string; /** UIデザイン設定 ("tokyo" | ...) */ uiSetting: string; /** 運用hub使用スイッチ ("true" | "false") */ useUnyohub: string; } export const injectJavascriptData = ({ mapSwitch, iconSetting, stationMenu, trainMenu, uiSetting, useUnyohub, }: InjectJavascriptOptions): string => { // 一番上のメニュー非表示 地図スイッチによって切り替え const topMenu = mapSwitch != "true" ? ` document.querySelector('#header a').style.display = 'none'; document.querySelector('#main').style.left = '0px'; document.querySelector('#header').style.height = '50px'; document.querySelector('#main').style.paddingTop = '54px'; document.querySelector('#headerStr').style.display = 'none'; ` : ` document.querySelector('.accordionClass').style.display = 'none'; document.querySelector('#header').style.display = 'none'; document.querySelector('#main').style.left = '0px'; document.querySelector('#main').style.paddingTop = '0px'; document.querySelector('#headerStr').style.display = 'none'; `; // 上部ヘッダーの取り扱い、自動再読み込み、setStringsの実行 const bootData = ` // 停止中 点滅アニメーション CSS を動的注入 (function(){ const s = document.createElement('style'); s.textContent = '@keyframes _jrs_blink{0%,55%{opacity:1}70%,90%{opacity:0}100%{opacity:1}}' + '@keyframes _jrs_glow{0%,55%{box-shadow:0 0 12px 3px rgba(255,0,0,0.9)}70%,90%{box-shadow:0 0 6px 2px rgba(255,0,0,0.45)}100%{box-shadow:0 0 12px 3px rgba(255,0,0,0.9)}}'; document.head.appendChild(s); })(); // Font Awesome は FA6/7 ともに CSS Custom Properties (content:var(--fa)) 依存で // 古い WebView (Chrome 49未満) では表示されないため、インライン SVG に置き換え済み。 // CDN依存・外部リソースロードも不要になった。 // 軽量な変更検出ユーティリティ (lodash_.isEqual の代替) const _hashes = {}; const hasChanged = (key, newVal) => { const h = JSON.stringify(newVal); if (_hashes[key] === h) return false; _hashes[key] = h; return true; }; // データ変数の宣言 let stationList = {}; let trainDataList = []; let operationList = []; let trainDiagramData2 = {}; let probremsData = []; let unyohubData = []; const useUnyohubSetting = ${useUnyohub === "true"}; // 列車種別設定 (typeColor / borderColor / bgColor / label / isWanman) const TRAIN_TYPE_CFG = ${JSON.stringify(TRAIN_TYPE_CONFIG)}; // operationList から列番が一致する運行情報を返すヘルパー const getOperationsByTrainId = (trainId) => { return operationList.filter(e => { const ids = [...(e.train_ids||[]),...(e.related_train_ids||[])].map(x=>x.split(",")[0]); return ids.includes(String(trainId)); }); }; // --- localStorage キャッシュヘルパー(起動直後の即時表示用)--- // データ種別ごとの有効期限(ミリ秒) const CACHE_TTL = { stationList: 60 * 60 * 1000, // 1時間(駅情報はほぼ変わらない) operationList: 2 * 60 * 1000, // 2分 probremsData: 2 * 60 * 1000, // 2分 trainDataList: 2 * 60 * 1000, // 2分 trainDiagramData2: 10 * 60 * 1000, // 10分(ダイアグラムは変化少ない) unyohubData: 10 * 60 * 1000, // 10分 }; const _wcache = { get(key) { try { const raw = localStorage.getItem('_jrs_' + key); if (!raw) return null; const { data, ts, ttl } = JSON.parse(raw); if (Date.now() - ts > ttl) return null; // 期限切れ return data; } catch(e) { return null; } }, set(key, data) { try { localStorage.setItem('_jrs_' + key, JSON.stringify({ data, ts: Date.now(), ttl: CACHE_TTL[key] ?? 60000 })); } catch(e) {} // QuotaExceededError などを無視 } }; const setReload = () => { try { document.getElementById('refreshIcon').click(); setStrings(); } catch(e) {} }; // ポーリング処理 (Phase 3 以降) const startPolling = () => { const DatalistUpdate = () => { fetch("${API_ENDPOINTS.TRAIN_DATA_API}").then(r => r.json()) .then(data => { if (hasChanged('trainDataList', data.data)) { trainDataList = data.data; _wcache.set('trainDataList', trainDataList); setReload(); } }) .catch(() => {}) .finally(() => { setTimeout(DatalistUpdate, ${INTERVALS.DELAY_UPDATE}); }); }; DatalistUpdate(); const operationListUpdate = () => { fetch("${API_ENDPOINTS.OPERATION_LOGS}").then(r => r.json()) .then(data => { if (data?.data === null) return; const filtered = (data.data || []).filter(d => d.state !== 100); if (hasChanged('operationList', filtered)) { operationList = filtered; _wcache.set('operationList', filtered); setReload(); } }) .catch(() => {}) .finally(() => { setTimeout(operationListUpdate, ${INTERVALS.DELAY_UPDATE}); }); }; operationListUpdate(); const TrainDiagramData2Update = () => { fetch("${API_ENDPOINTS.DIAGRAM_TODAY}").then(r => r.json()) .then(res => { const data = {}; res.forEach(d => { const keys = Object.keys(d); data[keys] = d[keys]; }); if (hasChanged('trainDiagramData2', data)) { trainDiagramData2 = data; _wcache.set('trainDiagramData2', data); setReload(); } }) .catch(() => {}) .finally(() => { setTimeout(TrainDiagramData2Update, ${INTERVALS.DELAY_UPDATE}); }); }; TrainDiagramData2Update(); const getProblemsData = () => { fetch("${API_ENDPOINTS.POSITION_PROBLEMS}").then(r => r.json()) .then(data => { if (hasChanged('probremsData', data?.data)) { probremsData = data.data; _wcache.set('probremsData', probremsData); setReload(); } }) .catch(() => {}) .finally(() => { setTimeout(getProblemsData, ${INTERVALS.FETCH_DIAGRAM}); }); }; getProblemsData(); // unyohub は Phase 3 で遅延スタート (他の読み込みを邪魔しない) const unyohubDataUpdate = () => { if (!useUnyohubSetting) { setTimeout(unyohubDataUpdate, ${INTERVALS.DELAY_UPDATE}); return; } fetch("${API_ENDPOINTS.UNYOHUB_DATA}" + '?_=' + Date.now()) .then(r => r.json()) .then(data => { if (hasChanged('unyohubData', data)) { unyohubData = data; _wcache.set('unyohubData', data); console.log('[UnyoHub] Data updated:', unyohubData.length, 'items'); setReload(); } }) .catch(e => console.error('[UnyoHub] Fetch error:', e)) .finally(() => { setTimeout(unyohubDataUpdate, ${INTERVALS.DELAY_UPDATE}); }); }; unyohubDataUpdate(); // バックグラウンド復帰時に全データを即時再取得(iOS WKWebView 対応) const refreshAllData = () => { // ハッシュをクリアして強制的に再描画させる Object.keys(_hashes).forEach(k => { _hashes[k] = null; }); Promise.allSettled([ fetch("${API_ENDPOINTS.TRAIN_DATA_API}").then(r => r.json()).then(data => { trainDataList = data.data ?? trainDataList; _hashes['trainDataList'] = JSON.stringify(trainDataList); }).catch(() => {}), fetch("${API_ENDPOINTS.OPERATION_LOGS}").then(r => r.json()).then(data => { if (data?.data != null) { operationList = (data.data || []).filter(d => d.state !== 100); _hashes['operationList'] = JSON.stringify(operationList); } }).catch(() => {}), fetch("${API_ENDPOINTS.POSITION_PROBLEMS}").then(r => r.json()).then(data => { probremsData = data?.data ?? probremsData; _hashes['probremsData'] = JSON.stringify(probremsData); }).catch(() => {}), ]).then(() => setReload()); }; document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { refreshAllData(); } }); // iOS Safari/WKWebView は pageshow でバックフォワードキャッシュ復帰も検出 window.addEventListener('pageshow', (e) => { if (e.persisted) refreshAllData(); }); }; // === Phase 0: localStorageキャッシュから即時ロード === // キャッシュヒット時は即座に描画することで、ネットワーク待ちの白画面を解消する { let anyHit = false; const _c0 = { stationList: _wcache.get('stationList'), operationList: _wcache.get('operationList'), probremsData: _wcache.get('probremsData'), trainDataList: _wcache.get('trainDataList'), trainDiagramData2: _wcache.get('trainDiagramData2'), unyohubData: _wcache.get('unyohubData'), }; if (_c0.stationList) { stationList = _c0.stationList; anyHit = true; } if (_c0.operationList) { operationList = _c0.operationList; _hashes['operationList'] = JSON.stringify(_c0.operationList); anyHit = true; } if (_c0.probremsData) { probremsData = _c0.probremsData; _hashes['probremsData'] = JSON.stringify(_c0.probremsData); anyHit = true; } if (_c0.trainDataList) { trainDataList = _c0.trainDataList; _hashes['trainDataList'] = JSON.stringify(_c0.trainDataList); anyHit = true; } if (_c0.trainDiagramData2) { trainDiagramData2 = _c0.trainDiagramData2; _hashes['trainDiagramData2'] = JSON.stringify(_c0.trainDiagramData2); anyHit = true; } if (_c0.unyohubData) { unyohubData = _c0.unyohubData; anyHit = true; } // setTimeout で defer することで、スクリプト全体の実行が完了し // setStrings (const) が TDZ を抜けた後に呼び出す。 // Phase 0 の setReload() はここまで const の TDZ により無効だった。 if (anyHit) setTimeout(() => setReload(), 0); } // === Phase 1: 軽量API (合計~210ms) を並列取得 → setReload 1回 === // OPERATION_LOGS: 0.07s/3KB, STATION_LIST: 0.18s/80KB, POSITION_PROBLEMS: 0.21s/11B Promise.allSettled([ fetch("${API_ENDPOINTS.STATION_LIST}").then(r => r.json()).then(data => { stationList = data; _wcache.set('stationList', data); }).catch(() => {}), fetch("${API_ENDPOINTS.OPERATION_LOGS}").then(r => r.json()).then(data => { if (data?.data != null) { const filtered = (data.data || []).filter(d => d.state !== 100); operationList = filtered; _hashes['operationList'] = JSON.stringify(filtered); _wcache.set('operationList', filtered); } }).catch(() => {}), fetch("${API_ENDPOINTS.POSITION_PROBLEMS}").then(r => r.json()).then(data => { probremsData = data?.data ?? []; _hashes['probremsData'] = JSON.stringify(probremsData); _wcache.set('probremsData', probremsData); }).catch(() => {}), ]).then(() => { setReload(); // Phase 1 完了: 1回目の描画更新 // === Phase 2: 重いAPI (合計~840ms) を並列取得 → setReload 1回 === // TRAIN_DATA_API: 0.84s/1MB, DIAGRAM_TODAY: 0.27s/522KB Promise.allSettled([ fetch("${API_ENDPOINTS.TRAIN_DATA_API}").then(r => r.json()).then(data => { trainDataList = data.data ?? []; _hashes['trainDataList'] = JSON.stringify(trainDataList); _wcache.set('trainDataList', trainDataList); }).catch(() => {}), fetch("${API_ENDPOINTS.DIAGRAM_TODAY}").then(r => r.json()).then(res => { const data = {}; res.forEach(d => { const keys = Object.keys(d); data[keys] = d[keys]; }); trainDiagramData2 = data; _hashes['trainDiagramData2'] = JSON.stringify(data); _wcache.set('trainDiagramData2', data); }).catch(() => {}), ]).then(() => { setReload(); // Phase 2 完了: 2回目の描画更新 startPolling(); // Phase 3: ポーリング開始 }); }); const getUnyohubFormation = (trainNumber) => { if (!${useUnyohub === "true"} || !unyohubData || unyohubData.length === 0) { return null; } const foundUnyos = []; 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); const result = foundUnyos.map(u => u.formations + '(' + u.position_forward + '-' + u.position_rear + ')').join(', '); return result; }; const sortOperationalList = (a, b,targetTrainID) => { // trainIdからカンマ以降の数字を抽出する関数 const extractOrderNumber = (trainId) => { 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)=> { const allTrainIds = [ ...(operation.train_ids || []), ...(operation.related_train_ids || []), ]; // data.trainNumの接頭辞と一致するものを探す for (const trainId of allTrainIds) { const prefix = trainId.split(',')[0]; // カンマ前の部分 if (prefix === targetTrainID) { 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; }; `; // 左か右かを判定してアイコンを設置する const trainIcon = ` const setStationIcon = (setIconElem,img,hasProblem,backCount = 100) =>{ const position = setIconElem.getAttribute("style").includes("left"); let marginData = ${uiSetting === "tokyo" ? `"5px"`: `"2px"`}; let backgroundColor = "transparent"; let heightData = "22px"; if(backCount == 0){ marginData = position ? ${uiSetting === "tokyo" ? `"0px 0px -10px 0px" : "-10px 0px 0px 0px"`: `"0px 2px 0px 0px" : "0px 2px 0px 0px"`}; heightData = "16px"; } setIconElem.insertAdjacentHTML('beforebegin', ""); if (backCount == 0 || backCount == 100) setIconElem.remove(); } const _TIM = ${JSON.stringify(TRAIN_ICON_MAP)}; const _TIR = ${JSON.stringify(TRAIN_ICON_REGEX)}; const _AP = "https://n8n.haruk.in/webhook/anpanman-pictures.png?trainNum="; const setTrainIcon = (列番データ) => { const _u = _TIM[列番データ]; if (_u != null) return _u === "__anpanman__" ? _AP + 列番データ : _u; for (const {pattern, url} of _TIR) { if (new RegExp(pattern).test(列番データ)) return url; } }; `; const normal_train_name = ` const nameReplace = (列車名データ,列番データ,行き先情報,hasProblem,isLeft) =>{ let isWanman = false; let trainName = ""; let trainType = ""; let trainTypeColor = "black"; let viaData = ""; let ToData = ""; let TrainNumber = 列番データ; let isEdit = false; let isSeason = false; let TrainNumberOverride; let optionalText = ""; try{ const diagram = trainDiagramData2[列番データ] || trainTimeInfo[列番データ]; if(diagram){ const diagramData = diagram.split("#"); ToData = diagramData[diagramData.length - 2].split(",")[0]; } }catch(e){} if(列車名データ.split(":")[1]){ const textBase = 列車名データ.split(":")[1].replace("\\r",""); trainName = textBase; } if(列車名データ.match("サンポート")){ const textBase = 列車名データ.split(":")[1].replace("\\r",""); trainName = textBase; } if(new RegExp(/^4[1-9]\\d\\d[DM]$/).test(列番データ) || new RegExp(/^5[1-7]\\d\\d[DM]$/).test(列番データ) || new RegExp(/^3[2-9]\\d\\d[DM]$/).test(TrainNumber) ){ flag=true; isWanman = true; } if(new RegExp(/^49[0-4]\\dD$/).test(列番データ) || new RegExp(/^9[0-4]\\dD$/).test(列番データ)){ viaData = "(海経由)"; } if(new RegExp(/^46\\d\\dD$/).test(列番データ) || new RegExp(/^6\\d\\dD$/).test(列番データ)){ viaData = "(内子経由)"; } const getThrew = num =>{ switch(num){ //牟岐線直通列車情報 //徳島線発牟岐線行き case "468D": case "478D": case "484D": viaData = "牟岐線直通"; ToData = "牟岐"; break; case "4430D": case "4472D": viaData = "牟岐線直通"; isWanman = true; ToData = "牟岐"; break; case "434D": case "474D": case "476D": case "480D": viaData = "牟岐線直通"; ToData = "阿南"; break; case "4452D": case "4466D": case "4470D": viaData = "牟岐線直通"; isWanman = true; ToData = "阿南"; break; case "4456D": viaData = "牟岐線直通"; isWanman = true; ToData = "阿波海南" break; //鳴門線発牟岐線行き case "951D": viaData = "牟岐線直通"; ToData = "桑野"; break; //牟岐線発高徳線行き case "358D": viaData = "高徳線直通"; break; case "4314D": case "4326D": case "4334D": case "4342D": case "4350D": case "4368D": viaData = "高徳線直通"; isWanman = true; break; //牟岐線発徳島線行き case "451D": case "475D": viaData = "徳島線直通"; break; case "4447D": case "4455D": case "5467D": case "5471D": case "5479D": viaData = "徳島線直通"; isWanman = true; break; //牟岐線発鳴門線行き case "952D": viaData = "鳴門線直通"; break; case "4954D": case "4978D": viaData = "鳴門線直通"; isWanman = true; break; //安芸行と併結列車を個別に表示、それ以外をdefaultで下りなら既定の行き先を、上りなら奈半利行を設定 case "5814D": case "5816D": viaData = "ごめん・なはり線[快速]"; ToData = "奈半利"; break; case "5812D": viaData = "ごめん・なはり線[快速]"; ToData = "安芸"; break; case "5874D": case "5882D": viaData = "ごめん・なはり線[各停]"; ToData = "安芸"; break; case "248D": case "250D": viaData = "ごめん・なはり線[快速]"; ToData = "(後免にて解結)\\n土佐山田/奈半利"; break; default: if(new RegExp(/^58[1-3][1,3,5,7,9][DM]$/).test(列番データ)){ viaData = "ごめん・なはり線[快速]"; break; } else if(new RegExp(/^58[4-9][1,3,5,7,9][DM]$/).test(列番データ)){ viaData = "ごめん・なはり線[各停]"; break; } else if(new RegExp(/^58[3-4][0,2,4,6,8][DM]$/).test(列番データ)){ viaData = "ごめん・なはり線[快速]"; ToData = "奈半利"; break; } else if(new RegExp(/^58[5-9][0,2,4,6,8][DM]$/).test(列番データ)){ viaData = "ごめん・なはり線[各停]"; ToData = "奈半利"; break; } } } getThrew(列番データ); if(trainDataList.find(e => e.train_id === 列番データ) !== undefined){ const data = trainDataList.find(e => e.train_id === 列番データ); const _tCfg = TRAIN_TYPE_CFG[data.type]; if (_tCfg) { trainTypeColor = _tCfg.typeColor; isWanman = _tCfg.isWanman; trainType = _tCfg.label; } isEdit = data.priority == 400; isSeason = data.priority == 300; if (getOperationsByTrainId(data.train_id).length > 0) isEdit = true; if(data.train_name != ""){ trainName = data.train_name; if(data.train_num_distance != ""){ trainName += (parseInt(列番データ.replace("M", "").replace("D", "")) - parseInt(data.train_num_distance))+"号"; } } if(data.via_data != ""){ viaData = data.via_data; } if(data.to_data != ""){ ToData = data.to_data; } if(data.train_number_override){ TrainNumberOverride = data.train_number_override; } if(data.optional_text){ optionalText = data.optional_text; } } //列番付与 const returnText1 = (isWanman ? "ワンマン " : "") + trainName + viaData; 行き先情報.innerText = ""; ${uiSetting === "tokyo" ? ` let stationIDs = []; let stationLines = []; Object.keys(stationList).forEach((key) => { const data = stationList[key].find(e => e.Station_JP === ToData )?.StationNumber; if(data){ stationIDs.push(data); stationLines.push(key); } }); let getColors = []; // to_data_colorが配列で値があればそれを使用 if(trainDataList.find(e => e.train_id === 列番データ) !== undefined){ const data = trainDataList.find(e => e.train_id === 列番データ); if(data.to_data_color && Array.isArray(data.to_data_color) && data.to_data_color.length > 0){ getColors = data.to_data_color; } } // getColorsが空の場合は既存の路線色を使用(駅番号の最初の1文字で色を取得) if(getColors.length === 0){ if(stationLines.length === 0){ getColors = ["rgba(97, 96, 96, 0.81)"]; }else{ getColors = stationLines.map(e => GetLineBarColor(e)); } } let yosan2Color = undefined; switch(viaData){ case "(内子経由)": yosan2Color = "#F5AC13"; break; case "(海経由)": yosan2Color = "#9AA7D7"; break; case "牟岐線直通": yosan2Color = "#00b8bb"; break; case "徳島線直通": yosan2Color = "#2d506e"; break; case "高徳線直通": yosan2Color = "#87CA3B"; break; case "鳴門線直通": yosan2Color = "#881F61"; break; case "予土線": yosan2Color = "#008a5a"; break; default: break; } // 複数色に対応したグラデーション生成 let gradient; if(getColors.length > 1){ const colorStops = []; const step = 100 / getColors.length; getColors.forEach((color, index) => { const start = step * index; const end = step * (index + 1); colorStops.push(color + " " + start + "%"); colorStops.push(color + " " + end + "%"); }); gradient = "linear-gradient(130deg, " + colorStops.join(", ") + ")"; }else{ gradient = getColors[0]; } const optionalTextColor = optionalText.includes("最終") ? "red" : "black"; const unyohubFormation = getUnyohubFormation(列番データ); const hasUnyohub = unyohubFormation !== null; if(hasUnyohub) { console.log('[UnyoHub] Badge shown for', 列番データ, ':', unyohubFormation); } // バッジHTMLを構築(複数のバッジを上下に配置) let badgeHtml = ""; const badgePosition = isLeft ? "right" : "left"; const badgeVerticalPos = isLeft ? "bottom" : "top"; // コミュニティバッジ(青)- fa-user-group をインライン SVG で代替 (全 WebView 対応) if(isEdit) { badgeHtml += "
"; } // 季節バッジ(青)- fa-calendar をインライン SVG で代替 (全 WebView 対応) else if(isSeason) { badgeHtml += "
"; } // 運用hubバッジ(黄色)- 他のバッジがある場合は下にずらす if(hasUnyohub) { const offsetStyle = (isEdit || isSeason) ? (badgeVerticalPos + ":20px;") : (badgeVerticalPos + ":0;"); badgeHtml += "
"; } 行き先情報.insertAdjacentHTML('beforebegin', "
" + badgeHtml + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

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

" + viaData + "

" + optionalText + "

" + trainName + "

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

" + trainType + "

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

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

" + returnText1 + "

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

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

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

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

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

"); `} } `; const textInsert = ` const setNewTrainItem = (element,hasProblem,type)=>{ var 列番データ = element.getAttribute('offclick').split('"')[1]; if(trainDataList.find(e => e.train_id === 列番データ) !== undefined){ const data = trainDataList.find(e => e.train_id === 列番データ); const _bc = TRAIN_TYPE_CFG[data.type]; element.style.borderColor = _bc ? _bc.borderColor : 'black'; element.style.backgroundColor = _bc ? _bc.bgColor : '#ffffffcc'; }else{ if(element.getAttribute('offclick').includes("express")){ element.style.borderColor = '#ff0000ff'; }else if(element.getAttribute('offclick').includes("rapid")){ element.style.borderColor = '#008cffff'; }else{ element.style.borderColor = 'black'; } } element.style.borderWidth = '2px'; element.style.borderStyle = 'solid'; element.style.borderRadius = '10%'; if(hasProblem){ element.style.boxShadow = ''; element.style.animation = '_jrs_glow 3s linear infinite'; }else{ element.style.boxShadow = '0 0 4px rgba(0, 0, 0, 0.2)'; element.style.animation = ''; } element.style.margin = '2px'; element.style.display = 'flex'; element.style.alignItems = 'center'; element.style.justifyContent = 'center'; element.style.width = '4.5em'; element.style.minHeight = '80px'; element.style.height = '100%'; element.getElementsByTagName("img")[0].style.float = 'unset'; element.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'; element.style.transition = 'transform 0.1s ease-in-out'; element.addEventListener('touchstart', () => element.style.transform = 'scale(0.8)'); element.addEventListener('touchend', () => element.style.transform = 'scale(1)'); if(element.getAttribute("style").includes("left")){ // borderを使って五角形を生成 下り element.style.borderRadius = '30px 30px 120px 120px'; element.style.flexDirection = 'column-reverse'; } else if(element.getAttribute("style").includes("right")){ // borderを使って五角形を生成 上り element.style.borderRadius = '120px 120px 30px 30px'; element.style.flexDirection = 'column'; } } //列番付与 const setStrings = () =>{ try { const elements = document.querySelectorAll('#disp > div > div > div[onclick]'); const setNewTrainItemUI = ()=>{ const aaa = (x2,pos) => { x2.style.display = 'flex'; x2.style.flexDirection = 'row'; if(pos == "right"){ x2.style.alignItems = 'flex-start'; x2.style.justifyContent = 'flex-start'; }else if(pos == "left"){ x2.style.alignItems = 'flex-end'; x2.style.justifyContent = 'flex-end'; } x2.style.flexWrap = 'wrap'; x2.style.width = '100%'; x2.style.height = "100%"; } const aaa2 = (x2) => { x2.style.display = 'flex'; x2.style.flexDirection = 'row'; x2.style.alignItems = 'center'; x2.style.justifyContent = 'center'; x2.style.flexWrap = 'wrap'; x2.style.width = '100%'; x2.style.height = "unset"; const x3 = x2.querySelectorAll(":scope > div"); x3.forEach(i=>{ i.style.position = "unset"; i.style.display = "flex"; i.style.flexDirection = "column"; i.style.alignItems = "center"; i.style.justifyContent = "center"; i.style.flex = "1"; i.style.backgroundColor = "#00000000"; i.querySelectorAll(":scope > *").forEach(j=>{ j.style.display = "flex"; j.style.flex = "1"; j.style.textAlign = "center"; j.style.margin = "5px"; j.style.padding = "5px"; }); }); } const layoutBase = (e)=>{ e.style.display = 'flex'; e.style.height = "unset"; e.style.flexDirection = 'row'; e.style.justifyContent = 'center'; } const elementBaseBase = document.querySelectorAll('[id^="stationBlock"]'); const elementNotBase = document.querySelectorAll('#disp > [id*="~"]'); elementNotBase.forEach(e=>{ layoutBase(e); const x = e.querySelectorAll(':scope > [id^="Up"], :scope > [id^="Id"], :scope > [id^="Down"]');//配下のdiv要素を選択 aaa(x[0],"left"); aaa2(x[1]); aaa(x[2],"right"); const upTrainCrossBarElement = e.querySelector(':scope > [id="upTrainCrossBar"]'); if (upTrainCrossBarElement) { upTrainCrossBarElement.style.left = '0px'; } }); elementBaseBase.forEach(e=>{ //それぞれの駅ブロック横一列 layoutBase(e); const x = e.querySelectorAll(':scope > div');//配下のdiv要素を選択 //x[0] 登りブロック x[2] 下りブロック x[1] 駅ブロック aaa(x[0],"left"); aaa2(x[1]); aaa(x[2],"right"); }); } ${uiSetting === "tokyo" ? `setNewTrainItemUI();`: ``} for (let element of elements) { if(element.getAttribute('offclick')){ continue; } element.setAttribute('offclick',element.getAttribute('onclick')) var 行き先情報 = element.getElementsByTagName("p")[0]; ${uiSetting === "tokyo" ? ` element.querySelector("img").insertAdjacentHTML('beforebegin',"
"); element.querySelector("img").insertAdjacentHTML('afterend',"
"); element.querySelector("img").style.padding = '5px'; element.style.position = 'relative'; if(element.getElementsByTagName("p")[1] != undefined){ element.getElementsByTagName("p")[1].innerText = element.getElementsByTagName("p")[1].innerText.replace("(","").replace(")",""); element.getElementsByTagName("p")[1].style.position = 'absolute'; element.getElementsByTagName("p")[1].style.backgroundColor = 'red'; element.getElementsByTagName("p")[1].style.color = 'white'; element.getElementsByTagName("p")[1].style.fontSize = '10px'; element.getElementsByTagName("p")[1].style.fontWeight = 'bold'; element.getElementsByTagName("p")[1].style.padding = '2px'; element.getElementsByTagName("p")[1].style.textAlign = 'center'; element.getElementsByTagName("p")[1].style.borderRadius = '10px'; if(element.getAttribute("style").includes("left")){ element.getElementsByTagName("p")[1].style.bottom = '0px'; element.getElementsByTagName("p")[1].style.left = '0px'; } else if(element.getAttribute("style").includes("right")){ element.getElementsByTagName("p")[1].style.right = '0px'; element.getElementsByTagName("p")[1].style.top = '0px'; } }`: ``} const isLeft = element.getAttribute("style").includes("left"); var 列番データ = element.getAttribute('offclick').split('"')[1]; var 列車名データ = element.getAttribute('offclick').split('"')[3]; const trainData = trainPositionDatas.filter(e=>!(e.Pos && e.Pos.includes("予告窓"))).find(e => e.TrainNum == 列番データ); const hasProblem = probremsData.find((e)=>{ return e.TrainNum == trainData.TrainNum && e.Pos == trainData.Pos; }); var flag=false; var TrainType = undefined; setTrainMenuDialog(element) ${iconSetting == "true" ? ` let trainIconUrl = []; operationList .sort((a,b)=>sortOperationalList(a,b,列番データ.toString())) .reverse() .forEach(e => { if (e.train_ids?.length > 0) { const trainIds = e.train_ids.map((x) => x.split(",")[0]); if (trainIds.includes(列番データ.toString())) { let iconTrainDirection = parseInt(列番データ.toString().replace(/[^\\d]/g,""))%2 == 0 ? true : false; const {directions} = trainDataList.find(e => e.train_id === 列番データ); if(directions != null && directions != undefined){ iconTrainDirection = directions ? true : false; } if(iconTrainDirection){ if(e.vehicle_img) trainIconUrl.push(e.vehicle_img); else if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right); }else{ if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right); else if(e.vehicle_img) trainIconUrl.push(e.vehicle_img); } } } else if (e.related_train_ids?.length > 0) { const trainIds = e.related_train_ids.map( (x) => x.split(",")[0] ); if (trainIds.includes(列番データ.toString())) { let iconTrainDirection = parseInt(列番データ.toString().replace(/[^\\d]/g,""))%2 === 0 ? true : false; const {directions} = trainDataList.find(e => e.train_id === 列番データ); if(directions != null && directions != undefined){ iconTrainDirection = directions ? true : false; } if(iconTrainDirection){ if(e.vehicle_img) trainIconUrl.push(e.vehicle_img); else if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right); }else{ if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right); else if(e.vehicle_img) trainIconUrl.push(e.vehicle_img); } } } }); if(trainIconUrl.length > 0){ [trainIconUrl[0], trainIconUrl[trainIconUrl.length - 1]].forEach((url,index,array) => { if(url && url != ""){ setStationIcon(element.querySelector("img"),url,hasProblem,trainIconUrl.length == 1 ? 100 : index); } }); } else{ if(trainDataList.find(e => e.train_id === 列番データ) !== undefined){ const trainIconUrl = [trainDataList.find(e => e.train_id === 列番データ).train_info_img]; if(trainIconUrl.length > 0){ trainIconUrl.forEach((url,index,array) => { if(url && url != ""){ setStationIcon(element.querySelector("img"),url,hasProblem); } }); } } else{ const trainIconUrl = [setTrainIcon(列番データ)]; if(trainIconUrl.length > 0){ if(trainIconUrl[0] && trainIconUrl[0] != ""){ setStationIcon(element.querySelector("img"),trainIconUrl[0],hasProblem); } } } } ` : ""} nameReplace(列車名データ,列番データ,行き先情報,hasProblem,isLeft); ${uiSetting === "tokyo" ? `setNewTrainItem(element,hasProblem);`: ``} } try{ for(let d of document.getElementById('disp').childNodes){ switch(d.id){ case 'pMENU_2': case 'pMENU_2_En': case 'pMENU_3': case 'pMENU_3_En': case 'pMENU_k': case 'pMENU_k_En': continue; default: break; } d.style.width = '100vw'; for(let f of d.childNodes){ try{ if(f.style.alignItems || f.style.textAlign){ f.style.width = '38vw'; } else{ if(f.id == 'upTrainCrossBar'){ f.style.width = '38vw'; } else if(f.id == 'dwTrainCrossBar'){ f.style.left = '62vw'; f.style.width = '38vw'; } else { f.style.width = '0vw'; } } if(f.style.textAlign == 'center'){ f.style.width = '24vw'; f.style.display = 'flex'; f.childNodes.forEach(i =>{ i.style.width = 'unset'; i.style.left = 'unset'; i.style.top = 'unset'; i.style.position = 'unset'; i.style.flex = '1'; i.style.margin = '5px' if(i.style.backgroundColor != 'rgb(247, 247, 247)'){ i.childNodes.forEach(m=> m.style.width = '20vw') } }) } }catch(e){} } } document.querySelector('#pMENU_2').style.borderStyle='solid'; document.querySelector('#pMENU_2').style.borderColor='#00d3e8'; document.querySelector('#pMENU_2').style.borderWidth='2px'; document.querySelector('#pMENU_2').style.borderRadius='10%'; document.querySelector('#pMENU_3').style.borderStyle='solid'; document.querySelector('#pMENU_3').style.borderColor='#00d3e8'; document.querySelector('#pMENU_3').style.borderWidth='2px'; document.querySelector('#pMENU_3').style.borderRadius='10%'; document.querySelectorAll('#pMENU_2 div').forEach((d)=>d.style.padding = '10px'); document.querySelectorAll('#pMENU_3 div').forEach((d)=>d.style.padding = '10px'); document.querySelectorAll('#topHeader div').forEach((d)=>d.style.width = '100vw'); document.querySelectorAll('#disp div')[0].style.width = '100vw'; document.getElementById('disp').style.width = '100vw'; document.getElementById('disp').style.overflowX = 'hidden'; }catch(e){ alert("本家サーバーからのデータ取得に失敗しました。"); window.ReactNativeWebView.postMessage(JSON.stringify({type:"LoadError"})); } } catch (e) {} } const textInsert = new MutationObserver( (mutations) =>{ setStrings(); const currentLines = document.querySelector('#topHeader div').innerText; window.ReactNativeWebView.postMessage(JSON.stringify({type:"currentLines",currentLines})); }); // 監視を開始 textInsert.observe(document.getElementById('disp'), { attributes: true, // 属性変化の監視 //attributeOldValue: true, // 変化前の属性値を matation.oldValue に格納する //characterData: true, // テキストノードの変化を監視 //characterDataOldValue: true, // 変化前のテキストを matation.oldValue に格納する childList: true, // 子ノードの変化を監視 //subtree: true // 子孫ノードも監視対象に含める }); `; // 列車メニュー表示の起動用スクリプト const makeTrainView = ` const makeTrainView = new MutationObserver( (mutations) => { for(let d of modal_content.getElementsByTagName("button") ){ if(d.getAttribute('data-rn-handled')) continue; d.setAttribute('data-rn-handled','1'); // getAttribute で生のHTML属性文字列を取得 (toString()はWebViewごとに書式が異なる) const raw = d.getAttribute('onclick') || ''; const data = raw.split('"')[1] || raw.split("'")[1] || ''; if(!data) continue; d.removeAttribute('onclick'); // 古いWebViewで属性が再評価されるのを防ぐ d.onclick = (e) => { e && e.stopPropagation && e.stopPropagation(); window.ReactNativeWebView.postMessage(data); }; } }); // 監視を開始 makeTrainView.observe(document.getElementById('modal_content'), { //attributes: true, // 属性変化の監視 //attributeOldValue: true, // 変化前の属性値を matation.oldValue に格納する //characterData: true, // テキストノードの変化を監視 //characterDataOldValue: true, // 変化前のテキストを matation.oldValue に格納する childList: true, // 子ノードの変化を監視 //subtree: true // 子孫ノードも監視対象に含める }); `; const makeTrainMenu = trainMenu == "true" ? ` // これの中身抽出ShowTrainTimeInfo("1228M","normal") // ShowTrainTimeInfo("142M","rapid:サンポート南風リレー") function setTrainMenuDialog(d){ try{ const offclick = d.getAttribute('offclick'); if(!offclick) return; // シングル/ダブルクォート両対応で引数を抽出 const s = offclick.replace('ShowTrainTimeInfo(','').replaceAll('"','').replaceAll("'",'').replace(')','').split(','); const returnData = {type:"ShowTrainTimeInfo",trainNum:s[0],limited:s[1]}; d.removeAttribute('onclick'); d.onclick = null; // capture=true でターゲットフェーズより先に発火 + stopImmediatePropagation で元ハンドラを封じる d.addEventListener('click', function(e){ e.stopImmediatePropagation(); e.preventDefault(); window.ReactNativeWebView.postMessage(JSON.stringify(returnData)); return false; }, true); }catch(e){ } } // Object.defineProperty でページ側JSが後から上書きできないようにロック // Chrome 81 等の古いWebViewでも onclick属性から呼ばれる関数の乗っ取りが確実になる try { Object.defineProperty(window, 'ShowTrainTimeInfo', { configurable: false, writable: false, value: function(trainNum, limited){ window.ReactNativeWebView.postMessage(JSON.stringify({type:"ShowTrainTimeInfo",trainNum:trainNum,limited:limited})); } }); } catch(e) { /* 既にnon-configurableで定義済みの場合は無視 */ } ` : `function setTrainMenuDialog(d){}`; const makeStationMenu = stationMenu == "true" ? ` //駅メニューダイアログの配置 // PopUpMenu もObject.definePropertyでロック: onclick属性から呼ばれても確実に横取りできる try { Object.defineProperty(window, 'PopUpMenu', { configurable: false, writable: false, value: function(event, id, name, pdf, map, url, chk){ window.ReactNativeWebView.postMessage(JSON.stringify({type:"PopUpMenu",event:String(event||''),id:String(id||''),name:String(name||''),pdf:String(pdf||''),map:String(map||''),url:String(url||''),chk:String(chk||'')})); } }); } catch(e) { /* 既にnon-configurableで定義済みの場合は無視 */ } const StationData = ${JSON.stringify(STATION_DATA)}; const setStationMenuDialog = new MutationObserver( (mutations) => { const data =[]; document.querySelectorAll('#disp div div').forEach(d=>d.id.indexOf("st")!= -1 && data.push(d)); for(let d of data ){ if(!d.offclick){ // getAttribute で生の属性文字列を取得 (toString() は WebView ごとに書式が異なる) d.offclick = d.getAttribute('onclick') || d.onclick.toString(); } const s = d.offclick.replace('(event)','').replaceAll("'", "").split('(')[1].split(')')[0].split(','); const stationBadge = StationData.find(e=>e.StationName === s[2])?.Feature; if(stationBadge){ const midoriStyle = JSON.parse(stationBadge).Midori.style; const IC = JSON.parse(stationBadge).IC; if(!d.childNodes[0].childNodes[0].childNodes[1]){ d.childNodes[0].childNodes[0].childNodes[0].insertAdjacentHTML('afterend',"
"+(IC ? "" : "")+(midoriStyle === "normal" ? ("") : "")+(midoriStyle === "plus" ? ("") : "")+"
"); } } d.removeAttribute('onclick'); // 古い WebView で属性が再評価されるのを防ぐ d.onclick = (e) =>{ e && e.stopPropagation && e.stopPropagation(); // これの中身抽出 PopUpMenu(event,'2','端岡','http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hashioka.pdf','https://www.google.co.jp/maps/place/34.305027,133.967643','','1') window.ReactNativeWebView.postMessage(JSON.stringify({type:"PopUpMenu",event:s[0],id:s[1],name:s[2],pdf:s[3],map:s[4],url:s[5],chk:s[6]})); return false; }; } const data2 =[]; document.querySelectorAll('#disp > div > div > div > div > font > div').forEach(d=>{ if(!!d.onclick) data2.push(d); }) for(let d of data2 ){ if(!d.offclick){ // getAttribute で生の属性文字列を取得 (toString() は WebView ごとに書式が異なる) d.offclick = d.getAttribute('onclick') || d.onclick.toString(); } const stationID = d.childNodes[0].innerText; const PDFAddress = d.offclick.split("'")[1]; const Name = d.childNodes[2].innerText.replaceAll("\\n",""); const stationBadge = StationData.find(e=>e.StationName === Name)?.Feature; if(stationBadge){ const midoriStyle = JSON.parse(stationBadge).Midori.style; const IC = JSON.parse(stationBadge).IC; if(!d.childNodes[3]) { d.childNodes[2].insertAdjacentHTML('afterend',"
"+(IC && "")+"
"); } } d.removeAttribute('onclick'); // 古い WebView で属性が再評価されるのを防ぐ d.onclick = (e) =>{ e && e.stopPropagation && e.stopPropagation(); window.ReactNativeWebView.postMessage(JSON.stringify({type:"PopUpMenu",id:stationID,name:Name,pdf:PDFAddress})); return false; }; } }); // 監視を開始 setStationMenuDialog.observe(document.querySelector('#disp'), { attributes: true, // 属性変化の監視 //attributeOldValue: true, // 変化前の属性値を matation.oldValue に格納する //characterData: true, // テキストノードの変化を監視 //characterDataOldValue: true, // 変化前のテキストを matation.oldValue に格納する childList: true, // 子ノードの変化を監視 //subtree: true // 子孫ノードも監視対象に含める }); ` : ``; return ( bootData + topMenu + trainIcon + normal_train_name + makeTrainView + makeTrainMenu + textInsert + makeStationMenu ); };