{"id":573,"date":"2025-10-04T10:12:40","date_gmt":"2025-10-04T10:12:40","guid":{"rendered":"https:\/\/tomchan.hk\/?page_id=573"},"modified":"2025-10-04T11:49:28","modified_gmt":"2025-10-04T11:49:28","slug":"mtr-next-station-2","status":"publish","type":"page","link":"https:\/\/tomchan.hk\/?page_id=573","title":{"rendered":"MTR Next Station"},"content":{"rendered":"<!-- HK MTR \u2013 Next Train (unofficial) | WP-ready embed -->\r\n<div class=\"hk-mtr-widget\" data-default-line=\"TKL\" data-default-sta=\"LHP\" data-default-lang=\"EN\">\r\n  <div class=\"wrap\">\r\n    <h1>Hong Kong MTR \u2013 Next Train<\/h1>\r\n\r\n    <div class=\"card\">\r\n      <div class=\"row\">\r\n        <div>\r\n          <label for=\"hk-line\">Line<\/label>\r\n          <select id=\"hk-line\"><\/select>\r\n        <\/div>\r\n        <div>\r\n          <label for=\"hk-station\">Station<\/label>\r\n          <select id=\"hk-station\"><\/select>\r\n        <\/div>\r\n\r\n        <!-- Hidden UI (keeps auto-refresh behavior) -->\r\n        <div id=\"hk-autoWrap\">\r\n          <label><input type=\"checkbox\" id=\"hk-auto\" checked> Auto refresh<\/label>\r\n          <div class=\"muted small\">every <span id=\"hk-intervalLabel\">10<\/span>s<\/div>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div id=\"hk-alert\" class=\"card\" style=\"display:none;margin-top:16px\"><\/div>\r\n\r\n    <div class=\"grid\" style=\"margin-top:16px\">\r\n      <div class=\"card\">\r\n        <!-- dynamic title goes here -->\r\n        <div id=\"hk-upTitle\" class=\"section\" role=\"heading\" aria-level=\"3\"><strong>UP<\/strong><\/div>\r\n        <table>\r\n          <thead>\r\n            <tr>\r\n              <th>Seq<\/th>\r\n              <th>ETA<\/th>\r\n              <th class=\"desktop-only\">Countdown<\/th>\r\n              <th>Platform<\/th>\r\n              <th>Destination<\/th>\r\n            <\/tr>\r\n          <\/thead>\r\n          <tbody id=\"hk-upBody\"><\/tbody>\r\n        <\/table>\r\n      <\/div>\r\n      <div class=\"card\">\r\n        <!-- dynamic title goes here -->\r\n        <div id=\"hk-downTitle\" class=\"section\" role=\"heading\" aria-level=\"3\"><strong>DOWN<\/strong><\/div>\r\n        <table>\r\n          <thead>\r\n            <tr>\r\n              <th>Seq<\/th>\r\n              <th>ETA<\/th>\r\n              <th class=\"desktop-only\">Countdown<\/th>\r\n              <th>Platform<\/th>\r\n              <th>Destination<\/th>\r\n            <\/tr>\r\n          <\/thead>\r\n          <tbody id=\"hk-downBody\"><\/tbody>\r\n        <\/table>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div id=\"hk-meta\" class=\"footer\"><\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<style>\r\n\/* make headings bold, red, and theme-proof *\/\r\n.hk-mtr-widget .card > .section,\r\n.hk-mtr-widget .section[role=\"heading\"]{\r\n  font: 800 16px\/1.2 Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif !important;\r\n  font-variation-settings: \"wght\" 800 !important;\r\n  color: var(--brand-red) !important;\r\n}\r\n.hk-mtr-widget .section strong{\r\n  font-weight: 800 !important;\r\n  font-variation-settings: \"wght\" 800 !important;\r\n}\r\n\r\n\/* rounded first\/last cells, except for \u201cNo data\u201d *\/\r\n.hk-mtr-widget tr td:first-child:not(.nodata){\r\n  background: var(--accent) !important;\r\n  color:#fff !important;\r\n  border:none !important;\r\n  border-radius:10px 0 0 10px !important;\r\n  font-weight: 900 !important;\r\n  text-align:center !important;\r\n  min-width:48px;\r\n}\r\n.hk-mtr-widget tr td:last-child:not(.nodata){\r\n  border-radius:0 10px 10px 0 !important;\r\n}\r\n\r\n\/* \u201cNo data\u201d single cell fully rounded on all four corners *\/\r\n.hk-mtr-widget td.nodata{\r\n  background:var(--accent) !important;\r\n  color:#fff !important;\r\n  border:none !important;\r\n  border-radius:10px !important;\r\n  text-align:center !important;\r\n  font-weight:800 !important;\r\n}\r\n.hk-mtr-widget td.nodata:first-child,\r\n.hk-mtr-widget td.nodata:last-child{ border-radius:10px !important; }\r\n\r\n\/* ===== WordPress override + namespacing ===== *\/\r\n.hk-mtr-widget, .hk-mtr-widget * { box-sizing: border-box; }\r\n.hk-mtr-widget { --brand-red:#c8102e; --brand-navy:#003a70; --bg:#ffffff; --fg:#1b1b1b; --muted:#555;\r\n  --card:#f5f5f5; --accent:#92278f; --bad:#d32f2f; --ok:#388e3c; --warn:#fbc02d; color:var(--fg) !important; }\r\n.hk-mtr-widget .wrap{max-width:980px;margin:0 auto;padding:28px 20px;background:transparent !important;}\r\n@media (max-width:640px){ .hk-mtr-widget .wrap{padding:24px 12px;} }\r\n\r\n.hk-mtr-widget h1{font:700 clamp(24px,4.5vw,38px)\/1.2 Inter,Segoe UI,Roboto,Helvetica,Arial,sans-serif !important;\r\n  color:var(--brand-navy) !important; margin:0 0 12px !important;}\r\n.hk-mtr-widget .card{background:var(--card) !important; border:1px solid #ddd !important; border-radius:12px !important;\r\n  padding:16px 16px 12px !important; box-shadow:0 2px 6px rgba(0,0,0,.05) !important; overflow:hidden;}\r\n.hk-mtr-widget .row{display:flex;gap:12px;flex-wrap:wrap;align-items:end;}\r\n.hk-mtr-widget label{display:block;font-weight:700;margin:0 0 6px;color:var(--brand-navy) !important;}\r\n.hk-mtr-widget select{background:#fff !important;border:1px solid #ccc !important;color:var(--fg) !important;border-radius:8px !important;\r\n  padding:10px 12px !important; font:600 14px\/1.2 Inter,Segoe UI,Roboto,Helvetica,Arial,sans-serif !important; outline:none;}\r\n.hk-mtr-widget .grid{display:grid;gap:12px;}\r\n@media (min-width:700px){ .hk-mtr-widget .grid{grid-template-columns:1fr 1fr;} }\r\n@media (max-width:699.98px){ .hk-mtr-widget .grid{grid-template-columns:1fr;} }\r\n\r\n.hk-mtr-widget .section{margin:0 0 8px !important;color:var(--brand-red) !important;\r\n  font-weight:800 !important;font-size:16px !important;line-height:1.2 !important;}\r\n\r\n.hk-mtr-widget table{width:100% !important;border-collapse:separate !important;border-spacing:0 8px !important;\r\n  font-family:Inter,Segoe UI,Roboto,Helvetica,Arial,sans-serif !important;}\r\n.hk-mtr-widget th{font-weight:800 !important;color:#222 !important;text-align:left !important;font-size:14px !important;padding:0 8px !important;}\r\n.hk-mtr-widget td{background:#fff !important;border:1px solid #ccc !important;padding:12px 10px !important;}\r\n.hk-mtr-widget tr td:first-child{background:var(--accent) !important;color:#fff !important;border:none !important;\r\n  border-radius:10px 0 0 10px !important;font-weight:700 !important;text-align:center !important;min-width:48px;}\r\n.hk-mtr-widget tr td:last-child{border-radius:0 10px 10px 0 !important;}\r\n.hk-mtr-widget .mono{font-feature-settings:\"tnum\" 1, \"lnum\" 1;}\r\n.hk-mtr-widget .muted{color:var(--muted) !important;}\r\n.hk-mtr-widget .small{font-size:12px !important;}\r\n.hk-mtr-widget .footer{margin-top:20px !important;color:#777 !important;font-size:12px !important}\r\n.hk-mtr-widget .pill{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:800;font-size:12px}\r\n.hk-mtr-widget .pill.ok{background:rgba(56,142,60,.15);color:var(--ok)}\r\n.hk-mtr-widget .pill.warn{background:rgba(251,192,45,.15);color:var(--warn)}\r\n.hk-mtr-widget .pill.bad{background:rgba(211,47,47,.15);color:var(--bad)}\r\n\r\n.hk-mtr-widget #hk-autoWrap{display:none !important;} \/* hide auto-refresh controls *\/\r\n\r\n.hk-mtr-widget td.nodata{background:var(--accent) !important;color:#fff !important;border:none !important;border-radius:10px !important;\r\n  text-align:center !important;font-weight:800 !important}\r\n\r\n\/* Desktop: show dedicated Countdown column; hide ETA\u2019s stacked countdown *\/\r\n@media (min-width:641px){\r\n  .hk-mtr-widget .desktop-only{display:table-cell !important;}\r\n  .hk-mtr-widget .etaCd{display:none !important;}\r\n}\r\n\/* Mobile: hide separate Countdown column; show ETA\u2019s stacked countdown *\/\r\n@media (max-width:640px){\r\n  .hk-mtr-widget table{border-spacing:0 6px !important}\r\n  .hk-mtr-widget .card{padding:12px 12px 10px !important;border-radius:10px !important}\r\n  .hk-mtr-widget tr td{padding:10px 8px !important}\r\n  .hk-mtr-widget tr td:first-child{min-width:40px !important}\r\n  .hk-mtr-widget .desktop-only{display:none !important}\r\n  .hk-mtr-widget .etaTime{font-size:16px !important;font-weight:800 !important;line-height:1.1 !important}\r\n  .hk-mtr-widget .etaCd{font-size:12px !important;color:var(--muted) !important;line-height:1.1 !important;display:block !important}\r\n}\r\n\r\n.hk-mtr-widget .cd.urgent{color:var(--bad) !important;font-weight:800 !important;}\r\n<\/style>\r\n\r\n<script>\r\n(function(){\r\n  const ROOT = document.currentScript.previousElementSibling; \/\/ style tag\r\n  const WIDGET = ROOT.previousElementSibling; \/\/ widget div\r\n\r\n  \/\/ --- Config & DOM\r\n  const API='https:\/\/rt.data.gov.hk\/v1\/transport\/mtr\/getSchedule.php';\r\n  const REFRESH_MS=10000, COUNTDOWN_MS=1000;\r\n\r\n  const LINES={\r\n    AEL:{name:'Airport Express',stations:{HOK:'Hong Kong',KOW:'Kowloon',TSY:'Tsing Yi',AIR:'Airport',AWE:'AsiaWorld Expo'}},\r\n    TCL:{name:'Tung Chung Line',stations:{HOK:'Hong Kong',KOW:'Kowloon',OLY:'Olympic',NAC:'Nam Cheong',LAK:'Lai King',TSY:'Tsing Yi',SUN:'Sunny Bay',TUC:'Tung Chung'}},\r\n    TML:{name:'Tuen Ma Line',stations:{WKS:'Wu Kai Sha',MOS:'Ma On Shan',HEO:'Heng On',TSH:'Tai Shui Hang',SHM:'Shek Mun',CIO:'City One',STW:'Sha Tin Wai',CKT:'Che Kung Temple',TAW:'Tai Wai',HIK:'Hin Keng',DIH:'Diamond Hill',KAT:'Kai Tak',SUW:'Sung Wong Toi',TKW:'To Kwa Wan',HOM:'Ho Man Tin',HUH:'Hung Hom',ETS:'East Tsim Sha Tsui',AUS:'Austin',NAC:'Nam Cheong',MEF:'Mei Foo',TWW:'Tsuen Wan West',KSR:'Kam Sheung Road',YUL:'Yuen Long',LOP:'Long Ping',TIS:'Tin Shui Wai',SIH:'Siu Hong',TUM:'Tuen Mun'}},\r\n    TKL:{name:'Tseung Kwan O Line',stations:{NOP:'North Point',QUB:'Quarry Bay',YAT:'Yau Tong',TIK:'Tiu Keng Leng',TKO:'Tseung Kwan O',LHP:'LOHAS Park',HAH:'Hang Hau',POA:'Po Lam'}},\r\n    EAL:{name:'East Rail Line',stations:{ADM:'Admiralty',EXC:'Exhibition Centre',HUH:'Hung Hom',MKK:'Mong Kok East',KOT:'Kowloon Tong',TAW:'Tai Wai',SHT:'Sha Tin',FOT:'Fo Tan',RAC:'Racecourse',UNI:'University',TAP:'Tai Po Market',TWO:'Tai Wo',FAN:'Fanling',SHS:'Sheung Shui',LOW:'Lo Wu',LMC:'Lok Ma Chau'}},\r\n    SIL:{name:'South Island Line',stations:{ADM:'Admiralty',OCP:'Ocean Park',WCH:'Wong Chuk Hang',LET:'Lei Tung',SOH:'South Horizons'}},\r\n    TWL:{name:'Tsuen Wan Line',stations:{CEN:'Central',ADM:'Admiralty',TST:'Tsim Sha Tsui',JOR:'Jordan',YMT:'Yau Ma Tei',MOK:'Mong Kok',PRE:'Prince Edward',SSP:'Sham Shui Po',CSW:'Cheung Sha Wan',LCK:'Lai Chi Kok',MEF:'Mei Foo',LAK:'Lai King',KWF:'Kwai Fong',KWH:'Kwai Hing',TWH:'Tai Wo Hau',TSW:'Tsuen Wan'}},\r\n    ISL:{name:'Island Line',stations:{KET:'Kennedy Town',HKU:'HKU',SYP:'Sai Ying Pun',SHW:'Sheung Wan',CEN:'Central',ADM:'Admiralty',WAC:'Wan Chai',CAB:'Causeway Bay',TIH:'Tin Hau',FOH:'Fortress Hill',NOP:'North Point',QUB:'Quarry Bay',TAK:'Tai Koo',SWH:'Sai Wan Ho',SKW:'Shau Kei Wan',HFC:'Heng Fa Chuen',CHW:'Chai Wan'}},\r\n    KTL:{name:'Kwun Tong Line',stations:{WHA:'Whampoa',HOM:'Ho Man Tin',YMT:'Yau Ma Tei',MOK:'Mong Kok',PRE:'Prince Edward',SKM:'Shek Kip Mei',KOT:'Kowloon Tong',LOF:'Lok Fu',WTS:'Wong Tai Sin',DIH:'Diamond Hill',CHH:'Choi Hung',KOB:'Kowloon Bay',NTK:'Ngau Tau Kok',KWT:'Kwun Tong',LAT:'Lam Tin',YAT:'Yau Tong',TIK:'Tiu Keng Leng'}},\r\n    DRL:{name:'Disneyland Resort Line',stations:{SUN:'Sunny Bay',DIS:'Disneyland Resort'}}\r\n  };\r\n  const LINE_COLORS={AEL:'#007078',TCL:'#f08a00',TML:'#8d5524',TKL:'#92278f',EAL:'#00a1de',SIL:'#6bbf59',TWL:'#e60012',ISL:'#1b75bb',KTL:'#00a650',DRL:'#ea3d76'};\r\n\r\n  const qs = (sel,root=WIDGET)=>root.querySelector(sel);\r\n  const lineSel=qs('#hk-line'), staSel=qs('#hk-station'),\r\n        upBody=qs('#hk-upBody'), downBody=qs('#hk-downBody'),\r\n        upTitle=qs('#hk-upTitle'), downTitle=qs('#hk-downTitle'),\r\n        meta=qs('#hk-meta'), alertBox=qs('#hk-alert');\r\n\r\n  let fetchTimer=null, countdownTimer=null;\r\n\r\n  function opt(el,v,t){const o=document.createElement('option');o.value=v;o.textContent=t;el.appendChild(o)}\r\n  function setStations(lineCode){staSel.innerHTML='';for(const [s,name] of Object.entries(LINES[lineCode].stations)) opt(staSel,s,`${name} (${s})`);staSel.selectedIndex=0}\r\n  function applyAccent(lineCode){WIDGET.style.setProperty('--accent',LINE_COLORS[lineCode]||'#92278f')}\r\n\r\n  function initSelectors(){\r\n    for(const [code,line] of Object.entries(LINES)) opt(lineSel,code,`${line.name} (${code})`);\r\n    const dl=WIDGET.dataset.defaultLine||'TKL';\r\n    const ds=WIDGET.dataset.defaultSta||'LHP';\r\n    setStations(dl); lineSel.value=dl; staSel.value=ds; applyAccent(dl);\r\n  }\r\n\r\n  lineSel.addEventListener('change',e=>{const c=e.target.value; setStations(c); applyAccent(c); fetchNow();});\r\n  staSel.addEventListener('change',fetchNow);\r\n\r\n  function scheduleFetch(){clearInterval(fetchTimer); fetchTimer=setInterval(fetchNow,REFRESH_MS)}\r\n  function scheduleCountdown(){clearInterval(countdownTimer); countdownTimer=setInterval(updateCountdowns,COUNTDOWN_MS)}\r\n\r\n  async function fetchNow(){\r\n    const line=lineSel.value, sta=staSel.value, lang='EN';\r\n    const url=`${API}?line=${encodeURIComponent(line)}&sta=${encodeURIComponent(sta)}&lang=${encodeURIComponent(lang)}`;\r\n    meta.textContent='Fetching\u2026'; alertBox.style.display='none';\r\n    try{\r\n      const res=await fetch(url,{cache:'no-store'});\r\n      if(!res.ok) throw new Error(`HTTP ${res.status}`);\r\n      const json=await res.json();\r\n      render(json,line,sta);\r\n    }catch(err){\r\n      meta.innerHTML=`<span class=\"pill bad\">Error<\/span> ${escapeHtml(err.message)}`;\r\n    }\r\n  }\r\n\r\n  function render(j,line,sta){\r\n    if(j.status===0 && j.message){\r\n      alertBox.style.display='block';\r\n      alertBox.innerHTML=`<div class=\"pill warn\">Notice<\/div> ${escapeHtml(j.message)}${j.url?` \u2013 <a target=\"_blank\" rel=\"noopener\" href=\"${j.url}\">More<\/a>`:''}`;\r\n    }else{\r\n      alertBox.style.display='none';\r\n    }\r\n    const key=`${line}-${sta}`;\r\n    const node=j.data?.[key]||{};\r\n    const delayed=(j.isdelay||j.Isdelay)==='Y';\r\n    meta.innerHTML=delayed?'<span class=\"pill warn\">Possible delay<\/span>':'<span class=\"pill ok\">On time<\/span>';\r\n\r\n    \/\/ Fill tables\r\n    fillT(upBody,node.UP||[],line);\r\n    fillT(downBody,node.DOWN||[],line);\r\n\r\n    \/\/ Update dynamic titles\r\n    setTitle(upTitle, node.UP||[], line);\r\n    setTitle(downTitle, node.DOWN||[], line);\r\n\r\n    scheduleCountdown();\r\n  }\r\n\r\n  \/\/ Build\/assign heading text based on available destinations\r\n  function setTitle(el, arr, line){\r\n    const label = buildDestLabel(arr, line);\r\n    el.innerHTML = `<strong>${escapeHtml(label)}<\/strong>`;\r\n  }\r\n  function buildDestLabel(arr, line){\r\n    if(!arr || !arr.length) return 'Last Station';\r\n    const nameMap = LINES[line].stations || {};\r\n    const uniq = Array.from(new Set(arr.map(x => nameMap[x.dest] || x.dest).filter(Boolean)));\r\n    return 'To ' + uniq.join(' \/ ');\r\n  }\r\n\r\n  function fillT(tbody,arr,line){\r\n    tbody.innerHTML='';\r\n    const destNames=LINES[line].stations;\r\n    if(!arr || !arr.length){\r\n      const tr=document.createElement('tr');\r\n      const td=document.createElement('td');\r\n      td.colSpan=5; td.className='nodata'; td.textContent='No data';\r\n      tr.appendChild(td); tbody.appendChild(tr); return;\r\n    }\r\n    const now=Date.now();\r\n    for(const item of arr){\r\n      const tr=document.createElement('tr');\r\n      const etaDate=parseEta(item.time); const etaMs=etaDate?etaDate.getTime():NaN;\r\n      const timeOnly=item.time?.split(' ')[1]||item.time;\r\n      tr.innerHTML=`\r\n        <td class=\"mono\">${escapeHtml(item.seq)}<\/td>\r\n        <td class=\"mono\">\r\n          <div class=\"etaTime\">${escapeHtml(timeOnly)}<\/div>\r\n          <div class=\"etaCd cd\" data-eta=\"${etaMs}\">${formatRemaining(etaMs-now)}<\/div>\r\n        <\/td>\r\n        <td class=\"mono desktop-only cd\" data-eta=\"${etaMs}\">${formatRemaining(etaMs-now)}<\/td>\r\n        <td class=\"mono\">${escapeHtml(item.plat||'-')}<\/td>\r\n        <td>${escapeHtml(destNames[item.dest]||item.dest||'-')}<\/td>`;\r\n      tbody.appendChild(tr);\r\n    }\r\n  }\r\n\r\n  function updateCountdowns(){\r\n    const now=Date.now();\r\n    WIDGET.querySelectorAll('.cd').forEach(c=>{\r\n      const eta=Number(c.dataset.eta);\r\n      const diff=eta-now;\r\n      c.textContent=formatRemaining(diff);\r\n      if(isFinite(diff) && diff>0 && diff<60000){ c.classList.add('urgent'); }\r\n      else { c.classList.remove('urgent'); }\r\n    });\r\n  }\r\n\r\n  function parseEta(s){ if(!s) return null; const d=new Date(s.replace(' ','T')); return isNaN(d.getTime())?null:d; }\r\n  function formatRemaining(ms){ if(!isFinite(ms)) return '-'; ms=Math.max(0,ms); const m=Math.floor(ms\/60000), s=Math.floor((ms%60000)\/1000); return `${m}m ${s}s`; }\r\n  function escapeHtml(s){ return (s??'').toString().replace(\/[&<>\"']\/g,m=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",\"\\\"\":\"&quot;\",\"'\":\"&#39;\"}[m])); }\r\n\r\n  \/\/ Init\r\n  initSelectors();\r\n  scheduleFetch();\r\n  fetchNow();\r\n})();\r\n<\/script>\r\n\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-full.php","meta":{"footnotes":""},"tags":[],"class_list":["post-573","page","type-page","status-publish","article"],"_links":{"self":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages\/573","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tomchan.hk\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=573"}],"version-history":[{"count":13,"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages\/573\/revisions"}],"predecessor-version":[{"id":610,"href":"https:\/\/tomchan.hk\/index.php?rest_route=\/wp\/v2\/pages\/573\/revisions\/610"}],"wp:attachment":[{"href":"https:\/\/tomchan.hk\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=573"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tomchan.hk\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=573"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}