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';
import { generateXhrInterceptorJs, MockApiConfig } from './mockApi/webviewXhrInterceptor';
import {
isHubIconDisplayMode,
normalizeIconDisplayMode,
usesCustomTrainIcons,
} from './iconDisplayMode';
import dosan from '@/assets/originData/dosan';
import dosan2 from '@/assets/originData/dosan2';
import koutoku from '@/assets/originData/koutoku';
import naruto from '@/assets/originData/naruto';
import seto from '@/assets/originData/seto';
import tokushima from '@/assets/originData/tokushima';
import uwajima from '@/assets/originData/uwajima';
import uwajima2 from '@/assets/originData/uwajima2';
import yosan from '@/assets/originData/yosan';
export interface InjectJavascriptOptions {
/** 地図スイッチ ("true" | "false") */
mapSwitch: string;
/** 列車アイコン表示モード ("default" | "original" | "hub") */
iconSetting: string;
/** 駅メニュースイッチ ("true" | "false") */
stationMenu: string;
/** 列車メニュースイッチ ("true" | "false") */
trainMenu: string;
/** UIデザイン設定 ("tokyo" | ...) */
uiSetting: string;
/** 運用hub使用スイッチ ("true" | "false") */
useUnyohub: string;
/** えれサイト使用スイッチ ("true" | "false") */
useElesite: string;
/** ダークモードかどうか */
isDark: boolean;
/**
* モックAPIの設定。指定した場合、WebView内の公式サイトAPIをインターセプトして
* モックデータを返すXHRインターセプターが注入されます。
* 未指定または null の場合、インターセプターは無効で通常通り公式APIに接続します。
*/
mockApiConfig?: MockApiConfig | null;
/**
* バックエンドAPIのベースURL。省略時は本番URL。
* experimental 環境では https://jr-shikoku-backend-api-v1-beta.haruk.in を渡す。
*/
backendApiBaseUrl?: string;
/**
* ダイアグラムJSONのURL。省略時は本番URL。
* experimental 環境では diagram-today-beta.json のURLを渡す。
*/
diagramTodayUrl?: string;
}
const PRODUCTION_BACKEND_BASE = 'https://jr-shikoku-backend-api-v1.haruk.in';
export const injectJavascriptData = ({
mapSwitch,
iconSetting,
stationMenu,
trainMenu,
uiSetting,
useUnyohub,
useElesite,
isDark,
backendApiBaseUrl,
diagramTodayUrl,
mockApiConfig,
}: InjectJavascriptOptions): string => {
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
const preferHubTrainIcon = isHubIconDisplayMode(iconDisplayMode);
const useVehicleIcons = usesCustomTrainIcons(iconDisplayMode);
const MARINE_STATION_SEQUENCE = [
"岡山", "大元", "備前西市", "妹尾", "早島", "茶屋町", "植松", "木見", "上の町",
"児島", "坂出", "鴨川", "国分", "端岡", "鬼無", "高松",
];
const elesiteStationSequences: string[][] = [
...[yosan, uwajima, uwajima2, dosan, dosan2, koutoku, tokushima, naruto, seto].map(
(stations) => stations.map((station) => station.Station_JP),
),
MARINE_STATION_SEQUENCE,
];
// 一番上のメニュー非表示 地図スイッチによって切り替え
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 = `
// ダークモードフラグ & テーマ色定数
const _isDark = ${isDark};
const _t = _isDark
? { bg:'#0d1117', bgSec:'#161b22', bgTer:'#21262d', text:'#e6edf3', textSec:'#8b949e', border:'#30363d', cardBg:'#48505acc', shadow:'rgba(0,0,0,0.5)' }
: { bg:'#ffffff', bgSec:'#f8f8fc', bgTer:'#f0f0f0', text:'#000000', textSec:'#555555', border:'#cccccc', cardBg:'#ffffffcc', shadow:'rgba(0,0,0,0.2)' };
// ダークモード用: インライン背景色をHSL変換で暗くするヘルパー
// 彩度を維持しつつ明度(L)を指定値に設定
const _darkenBg = _isDark ? function(el, targetL) {
try {
var bg = el.style.backgroundColor;
if (!bg || bg === 'transparent') return;
var m = bg.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);
if (!m) return;
var r = +m[1]/255, g = +m[2]/255, b = +m[3]/255;
var mx = Math.max(r,g,b), mn = Math.min(r,g,b);
var l = (mx+mn)/2;
if (l < 0.12) return;
var h = 0, s = 0, d = mx - mn;
if (d > 0) {
s = l > 0.5 ? d/(2-mx-mn) : d/(mx+mn);
if (mx===r) h = ((g-b)/d + (g1)t-=1;
if(t<1/6) return p+(q-p)*6*t;
if(t<1/2) return q;
if(t<2/3) return p+(q-p)*(2/3-t)*6;
return p;
};
var q2 = nl < 0.5 ? nl*(1+ns) : nl+ns-nl*ns;
var p2 = 2*nl - q2;
var nr = hue2rgb(p2,q2,h+1/3);
var ng = hue2rgb(p2,q2,h);
var nb = hue2rgb(p2,q2,h-1/3);
el.style.backgroundColor = 'rgb('+(nr*255|0)+','+(ng*255|0)+','+(nb*255|0)+')';
} catch(e) {}
} : function(){};
// 停止中 点滅アニメーション CSS + ダークモード 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)}}'
+ '@keyframes _jrs_type_primary{0%,49.999%{opacity:1}50%,100%{opacity:0}}'
+ '@keyframes _jrs_type_secondary{0%,49.999%{opacity:0}50%,99.999%{opacity:1}100%{opacity:0}}'
+ (_isDark ? (
'html,body{background-color:#3a3f47!important;}'
+ '#main{background-color:#3a3f47!important;}'
+ '#disp{background-color:transparent!important;}'
+ '#header{background-color:#454b54!important;color:#e6edf3!important;border-bottom:1px solid #555d66!important;}'
+ '#headerStr{color:#e6edf3!important;}'
+ '#topHeader{background-color:#454b54!important;border-bottom:1px solid #555d66!important;}'
+ '#topHeader div{color:#000000!important;}'
+ '#topHeader .accordion{background-color:#4d545d!important;color:#000000!important;}'
+ '${uiSetting === "tokyo" ? '[id^="stationBlock"]{background-color:rgba(0,0,0,0.45)!important;}#disp>[id*="~"]{background-color:rgba(0,0,0,0.25)!important;}' : ''}'
+ '#pMENU_2,#pMENU_3,#pMENU_k{background-color:#454b54!important;border-color:#555d66!important;}'
+ '#pMENU_2 div,#pMENU_3 div,#pMENU_k div{color:#e6edf3!important;}'
+ '#modal_content{background-color:#454b54!important;color:#e6edf3!important;}'
+ '#modal_content button{background-color:#4d545d!important;color:#e6edf3!important;border-color:#555d66!important;}'
+ '.accordionClass{background-color:#454b54!important;color:#e6edf3!important;}'
+ 'select{background-color:#4d545d!important;color:#e6edf3!important;border-color:#555d66!important;}'
+ '#upTrainCrossBar,#dwTrainCrossBar{opacity:0.85!important;}'
) : (
'#topHeader div{color:#000000!important;}'
+ '#topHeader .accordion{color:#000000!important;}'
));
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 trainDataListReady = false;
let operationList = [];
let trainDiagramData2 = {};
let probremsData = [];
let unyohubData = [];
const useUnyohubSetting = ${useUnyohub === "true"};
let elesiteData = [];
const useEleSiteSetting = ${useElesite === "true"};
// 列車種別設定 (typeColor / borderColor / bgColor / label)
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: 7 * 24 * 60 * 60 * 1000, // 一週間(駅情報はほぼ変わらない)
operationList: 24 * 60 * 60 * 1000, // 一日
probremsData: 1 * 60 * 1000, // 一分
trainDataList: 24 * 60 * 60 * 1000, // 一日
trainDiagramData2: 60 * 60 * 1000, // 1時間(ダイアグラムは変化少ない)
unyohubData: 60 * 60 * 1000, // 1時間
elesiteData: 60 * 60 * 1000, // 1時間
};
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 などを無視
}
};
window.setReload = () => {
try {
document.getElementById('refreshIcon').click();
setStrings();
// Also run after async XHR responses settle.
// When mock mode is active, the XHR interceptor responds via setTimeout(0),
// which means the site re-renders trains AFTER this synchronous call.
// The delayed call ensures Tokyo UX is re-applied after the DOM update.
setTimeout(() => { try { setStrings(); } catch(e) {} }, 200);
} catch(e) {}
};
const setReload = window.setReload;
// ポーリング処理 (Phase 3 以降)
const startPolling = () => {
const DatalistUpdate = () => {
fetch("${API_ENDPOINTS.TRAIN_DATA_API}").then(r => r.json())
.then(data => {
const nextTrainDataList = Array.isArray(data?.data) ? data.data : null;
if (!nextTrainDataList) return;
trainDataListReady = true;
if (!hasChanged('trainDataList', nextTrainDataList)) return;
trainDataList = nextTrainDataList;
_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 => {
const source = Array.isArray(data?.data) ? data.data : [];
const filtered = source.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();
// elesiteは Phase 3 で遅延スタート
const elesiteDataUpdate = () => {
if (!useEleSiteSetting) {
setTimeout(elesiteDataUpdate, ${INTERVALS.DELAY_UPDATE});
return;
}
fetch("${API_ENDPOINTS.ELESITE_DATA}" + '?_=' + Date.now())
.then(r => r.json())
.then(data => {
elesiteData = data;
_wcache.set('elesiteData', data);
console.log('[Elesite] Data updated:', elesiteData.length, 'items');
setReload();
})
.catch(e => console.error('[Elesite] Fetch error:', e))
.finally(() => { setTimeout(elesiteDataUpdate, ${INTERVALS.DELAY_UPDATE}); });
};
elesiteDataUpdate();
// バックグラウンド復帰時に全データを即時再取得(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 => {
if (Array.isArray(data?.data)) {
trainDataList = data.data;
trainDataListReady = true;
_hashes['trainDataList'] = JSON.stringify(trainDataList);
_wcache.set('trainDataList', trainDataList);
}
}).catch(() => {}),
fetch("${API_ENDPOINTS.OPERATION_LOGS}").then(r => r.json()).then(data => {
const source = Array.isArray(data?.data) ? data.data : [];
operationList = source.filter(d => d.state !== 100);
_hashes['operationList'] = JSON.stringify(operationList);
_wcache.set('operationList', 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'),
elesiteData: _wcache.get('elesiteData'),
};
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.trainDataList) trainDataListReady = true;
if (_c0.trainDiagramData2) { trainDiagramData2 = _c0.trainDiagramData2; _hashes['trainDiagramData2'] = JSON.stringify(_c0.trainDiagramData2); anyHit = true; }
if (_c0.unyohubData) { unyohubData = _c0.unyohubData; anyHit = true; }
if (_c0.elesiteData) { elesiteData = _c0.elesiteData; 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 => {
const source = Array.isArray(data?.data) ? data.data : [];
const filtered = source.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 => {
if (!Array.isArray(data?.data)) return;
trainDataList = data.data;
trainDataListReady = true;
_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);
// formations が空のエントリはバッジ対象外(TrainDataSources と同じ判定)
if (found && unyo.formations && unyo.formations.trim() !== "") {
foundUnyos.push({
formations: unyo.formations,
position_forward: found.position_forward,
position_rear: found.position_rear,
direction: found.direction,
});
}
}
if (foundUnyos.length === 0) return null;
// outbound=pos_fwd昇順, inbound=pos_fwd降順(TrainDataSources と同じ判定)
const dir = foundUnyos[0].direction;
foundUnyos.sort((a, b) => dir === 'inbound'
? b.position_forward - a.position_forward
: a.position_forward - b.position_forward
);
return foundUnyos.map(u => u.formations + '(' + u.position_forward + '-' + u.position_rear + ')').join(', ');
};
/** 今日付の投稿があるか判定("YYYY-MM-DD ..." 形式の last_posted_datetime) */
const isUnyohubStale = (trainNumber) => {
if (!unyohubData || unyohubData.length === 0) return true;
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, '0');
const dd = String(today.getDate()).padStart(2, '0');
const todayStr = yyyy + '-' + mm + '-' + dd;
const matchedEntries = unyohubData.filter(
u => u.trains && u.trains.some(t => t.train_number === trainNumber)
&& u.formations && u.formations.trim() !== ""
);
if (matchedEntries.length === 0) return true;
return !matchedEntries.some(u => u.last_posted_datetime && u.last_posted_datetime.startsWith(todayStr));
};
const getElesiteFormation = (trainNumber) => {
if (!${useElesite === "true"} || !elesiteData || elesiteData.length === 0) {
return null;
}
const results = [];
const elesiteStationSequences = ${JSON.stringify(elesiteStationSequences)};
const getElesiteRouteEndpoints = (targetTrainNumber) => {
const diagram = trainDiagramData2[targetTrainNumber] || trainTimeInfo[targetTrainNumber];
if (!diagram) return null;
const stations = diagram
.split('#')
.map((stop) => stop.split(',')[0] && stop.split(',')[0].trim())
.filter(Boolean);
if (stations.length < 2) return null;
return {
firstStation: stations[0],
lastStation: stations[stations.length - 1],
};
};
const getElesiteHeadingDirection = (entry, targetTrainNumber) => {
const matchedTrain = entry.trains && entry.trains.find((train) => train.train_number === targetTrainNumber);
const nav = matchedTrain && matchedTrain.nav;
if (!nav) return null;
if (nav.heading_to === 'left' || nav.heading_to === 'right') {
return nav.heading_to;
}
const route = getElesiteRouteEndpoints(targetTrainNumber);
const formationConfig = entry.formation_config;
if (!route || !formationConfig || !formationConfig.left_station || !formationConfig.right_station) {
return null;
}
const sequence = elesiteStationSequences.find((stations) =>
stations.includes(formationConfig.left_station) &&
stations.includes(formationConfig.right_station) &&
stations.includes(route.firstStation) &&
stations.includes(route.lastStation)
) || elesiteStationSequences.find((stations) =>
stations.includes(formationConfig.left_station) &&
stations.includes(formationConfig.right_station)
);
if (!sequence) return null;
const firstIndex = sequence.indexOf(route.firstStation);
const lastIndex = sequence.indexOf(route.lastStation);
const leftIndex = sequence.indexOf(formationConfig.left_station);
const rightIndex = sequence.indexOf(formationConfig.right_station);
if ([firstIndex, lastIndex, leftIndex, rightIndex].some((index) => index < 0)) {
return null;
}
const trainDelta = lastIndex - firstIndex;
const formationDelta = rightIndex - leftIndex;
if (trainDelta === 0 || formationDelta === 0) {
return null;
}
return Math.sign(trainDelta) === Math.sign(formationDelta) ? 'right' : 'left';
};
const getElesiteSortRank = (entry, targetTrainNumber) => {
const matchedTrain = entry.trains && entry.trains.find((train) => train.train_number === targetTrainNumber);
const nav = matchedTrain && matchedTrain.nav;
if (!nav) return 2;
const headingDirection = getElesiteHeadingDirection(entry, targetTrainNumber);
if (!headingDirection) {
const isLeftSide = (nav.heading_to === 'left') === (nav.is_leading === true);
return isLeftSide ? 0 : 1;
}
const isLeftSide = (headingDirection === 'left') === (nav.is_leading === true);
return isLeftSide ? 0 : 1;
};
// 高松(left_station)側のユニットを先に表示
const sortedElesiteData = elesiteData.slice().sort((a, b) => {
return getElesiteSortRank(a, trainNumber) - getElesiteSortRank(b, trainNumber);
});
for (const entry of sortedElesiteData) {
if (!entry.trains) continue;
const found = entry.trains.find(train => train.train_number === trainNumber);
if (!found) continue;
// units が1件以上ある場合のみ編成名を返す(空 units は報告なし扱い)
const units = entry.formation_config && entry.formation_config.units;
const formText = (units && units.length)
? units.map(u => u.formation).join('+')
: null;
if (formText) results.push(formText);
}
return results.length > 0 ? results.join(', ') : null;
};
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 _preferHubTrainIcon = ${preferHubTrainIcon};
const setStationIcon = (setIconElem,img,hasProblem,backCount = 100) =>{
const position = setIconElem.getAttribute("style").includes("left");
const isHubDataset = _preferHubTrainIcon && typeof img === "string" && img.includes("unyohub-icon-dataset/");
let marginData = ${uiSetting === "tokyo" ? `"5px"`: `"2px"`};
let backgroundColor = "transparent";
let heightData = "22px";
let widthData = isHubDataset ? "18px" : "auto";
let transformData = isHubDataset ? "scale(1.15)" : "none";
let filterData = isHubDataset
? "drop-shadow(0 0 0.8px rgba(255,255,255,0.75)) drop-shadow(0 1px 1px rgba(0,0,0,0.38)) drop-shadow(0 1px 1.5px rgba(0,0,0,0.18))"
: "drop-shadow(0 1px 1.5px rgba(0,0,0,0.35))";
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";
widthData = isHubDataset ? "13px" : "auto";
if (isHubDataset) {
filterData = "drop-shadow(0 0 0.6px rgba(255,255,255,0.7)) drop-shadow(0 1px 0.8px rgba(0,0,0,0.36)) drop-shadow(0 1px 1.2px rgba(0,0,0,0.16))";
}
}
setIconElem.insertAdjacentHTML('beforebegin', " " + 列番データ + " " + (hasProblem ? "‼️停止中‼️" : "") + "");
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 pickOperationTrainIcon = (operation, direction) => {
const forward = _preferHubTrainIcon
? (operation.vehicle_img_hub || operation.vehicle_img)
: operation.vehicle_img;
const rear = _preferHubTrainIcon
? (operation.vehicle_img_right_hub || operation.vehicle_img_hub || operation.vehicle_img_right || operation.vehicle_img)
: (operation.vehicle_img_right || operation.vehicle_img);
return direction ? (forward || rear) : (rear || forward);
};
`;
const normal_train_name = `
const nameReplace = (列車名データ,列番データ,行き先情報,hasProblem,isLeft) =>{
let trainName = "";
let trainType = "";
let trainTypeColor = "black";
let resolvedType = "";
let viaData = "";
let ToData = "";
let TrainNumber = 列番データ;
let isEdit = false;
let isSeason = false;
let TrainNumberOverride;
let optionalText = "";
const registeredTrainData = trainDataList.find(e => String(e.train_id) === String(列番データ));
// 列車マスタに未登録でも在線カード自体は残す。
// 推測した種別・行先等は表示せず、列番と車両アイコンだけの最小表示にする。
if(trainDataListReady && !registeredTrainData){
行き先情報.innerText = "";
${uiSetting === "tokyo" ? `
行き先情報.insertAdjacentHTML('beforebegin', "
" + 列番データ + "
"); `} 行き先情報.remove(); return; } 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(/^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 = "牟岐線直通"; ToData = "牟岐"; break; case "434D": case "474D": case "476D": case "480D": viaData = "牟岐線直通"; ToData = "阿南"; break; case "4452D": case "4466D": case "4470D": viaData = "牟岐線直通"; ToData = "阿南"; break; case "4456D": viaData = "牟岐線直通"; ToData = "阿波海南" break; //鳴門線発牟岐線行き case "951D": viaData = "牟岐線直通"; ToData = "桑野"; break; //牟岐線発高徳線行き case "358D": viaData = "高徳線直通"; break; case "4314D": case "4326D": case "4334D": case "4342D": case "4350D": case "4368D": viaData = "高徳線直通"; break; //牟岐線発徳島線行き case "451D": case "475D": viaData = "徳島線直通"; break; case "4447D": case "4455D": case "5467D": case "5471D": case "5479D": viaData = "徳島線直通"; break; //牟岐線発鳴門線行き case "952D": viaData = "鳴門線直通"; break; case "4954D": case "4978D": viaData = "鳴門線直通"; 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 === 列番データ); resolvedType = data.type; const _tCfg = TRAIN_TYPE_CFG[data.type]; if (_tCfg) { trainTypeColor = _tCfg.typeColor; 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 isOneManType = resolvedType === "OneMan" || resolvedType === "OneManRapid"; //列番付与 const returnText1 = 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 hasViaData = !!viaData && viaData.trim() !== ""; const hasOptionalText = !!optionalText && optionalText.trim() !== ""; const hasTrainName = !!trainName && trainName.trim() !== ""; const hasToData = !!ToData && ToData.trim() !== ""; const trainTypeTextStyle = "font-size:10px;font-weight:bold;font-style:italic;padding:0;margin:0;color:#ffffff;text-align:center;line-height:1;"; const trainTypeFrameStyle = "position:relative;height:10px;width:100%;overflow:hidden;display:flex;align-items:center;justify-content:center;"; const trainTypeMainStyle = trainTypeTextStyle + "display:flex;align-items:center;justify-content:center;width:100%;height:100%;"; const trainTypeWanmanStyle = trainTypeMainStyle.replace("font-size:10px;", "font-size:9px;"); const trainTypeHtml = isOneManType ? "" + trainType + "
ワンマン
" + trainType + "
" + viaData + "
" : ""; const optionalTextHtml = hasOptionalText ? "" + optionalText + "
" : ""; const trainNameHtml = hasTrainName ? "" + trainName + "
" : ""; const toDataHtml = hasToData ? "" + ToData + "行
‼️停止中‼️
" : ""; 行き先情報.insertAdjacentHTML('beforebegin', "" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "
" + viaDataHtml + optionalTextHtml + trainNameHtml + toDataHtml + "" + problemHtml + "" + returnText1 + "
"); } 行き先情報.insertAdjacentHTML('beforebegin', "" + (ToData ? ToData + "行 " : "") + "
" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "
‼️停止中‼️
"); } `} } `; 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'; var _bgc = _bc ? _bc.bgColor : '#ffffffff'; if (_isDark && _bgc.endsWith('cc')) { _bgc = _bgc.slice(0, -2) + 'ff'; } element.style.backgroundColor = _bgc; }else{ element.style.backgroundColor = '#ffffffff'; element.style.borderColor = _isDark ? '#30363d' : 'black'; } element.style.borderWidth = _isDark ? '1px' : '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 = _isDark ? '0 2px 8px rgba(0,0,0,0.6)' : '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 = '56px'; element.style.maxWidth = '56px'; element.style.flexShrink = '0'; 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 { // viewport meta tagを強制設定(iPad Stage Manager等でのズーム防止) var _vpMeta = document.querySelector('meta[name="viewport"]'); if (!_vpMeta) { _vpMeta = document.createElement('meta'); _vpMeta.name = 'viewport'; document.head.appendChild(_vpMeta); } _vpMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; const elements = document.querySelectorAll('#disp > div > div > div[onclick]'); const setNewTrainItemUI = ()=>{ const aaa = (x2,pos) => { if (!x2) return; 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) => { if (!x2) return; 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) ${useVehicleIcons ? ` 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; } const iconUrl = pickOperationTrainIcon(e, iconTrainDirection); if(iconUrl) trainIconUrl.push(iconUrl); } } 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; } const iconUrl = pickOperationTrainIcon(e, iconTrainDirection); if(iconUrl) trainIconUrl.push(iconUrl); } } }); 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 trainData = trainDataList.find(e => e.train_id === 列番データ); const trainIconUrl = [_preferHubTrainIcon ? (trainData.train_info_img_hub || trainData.train_info_img) : trainData.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_2').style.backgroundColor=_t.bgSec; 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.querySelector('#pMENU_3').style.backgroundColor=_t.bgSec; 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'; // ダークモード: 左右ゾーンの縦グラデーション if (_isDark) { document.querySelectorAll('[id^="stationBlock"]').forEach(function(block) { // 左右ゾーン:元の縦グラデーションを継承する微妙な明暗 if (block.children[0]) block.children[0].style.backgroundImage = 'linear-gradient(to bottom, rgba(200,210,230,0.04), transparent 30%, transparent 70%, rgba(200,210,230,0.04))'; if (block.children[2]) block.children[2].style.backgroundImage = 'linear-gradient(to bottom, rgba(200,210,230,0.04), transparent 30%, transparent 70%, rgba(200,210,230,0.04))'; }); // ~区間の左右にもグラデーション document.querySelectorAll('#disp > [id*="~"]').forEach(function(row) { var up = row.querySelector(':scope > [id^="Up"]'); var down = row.querySelector(':scope > [id^="Down"]'); if (up) up.style.backgroundImage = 'linear-gradient(to bottom, rgba(200,210,230,0.04), transparent 30%, transparent 70%, rgba(200,210,230,0.04))'; if (down) down.style.backgroundImage = 'linear-gradient(to bottom, rgba(200,210,230,0.04), transparent 30%, transparent 70%, rgba(200,210,230,0.04))'; }); } }catch(e){ alert("本家サーバーからのデータ取得に失敗しました。"); window.ReactNativeWebView.postMessage(JSON.stringify({type:"LoadError"})); } } catch (e) {} } let _ssDebounce = null; const textInsert = new MutationObserver( (mutations) =>{ // Only send currentLines when direct children of #disp change (line selection change) const disp = document.getElementById('disp'); if (mutations.some(m => m.target === disp)) { const currentLines = document.querySelector('#topHeader div').innerText; window.ReactNativeWebView.postMessage(JSON.stringify({type:"currentLines",currentLines})); } // Apply Tokyo UX immediately (prevents visible flash on normal renders). try { setStrings(); } catch(e) {} // Also schedule a follow-up after 250ms to catch async re-renders // triggered by mock XHR setTimeout(0) responses that settle later. clearTimeout(_ssDebounce); _ssDebounce = setTimeout(() => { try { setStrings(); } catch(e) {} }, 250); }); // 監視を開始 (subtree: true で深い子孫の変化も捕捉する) textInsert.observe(document.getElementById('disp'), { childList: true, // 子ノードの変化を監視 subtree: true, // 子孫ノードも監視対象に含める (train elements are 3 levels deep) }); `; // 列車メニュー表示の起動用スクリプト 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',"