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 += "
" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "
" + (isWanman ? "ワンマン " : "") + "
" + viaData + "
" + optionalText + "
" + trainName + "
" + (ToData ? ToData + "行" : ToData) + "
" + trainType + "
" + (hasProblem ? "‼️停止中‼️" : "") + "
" + returnText1 + "
"); 行き先情報.insertAdjacentHTML('beforebegin', "" + (ToData ? ToData + "行 " : ToData) + "
" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "
" + (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',"