{"id":663,"date":"2026-10-09T04:00:04","date_gmt":"2026-10-09T04:00:04","guid":{"rendered":"https:\/\/webmotiontech.com\/?page_id=663"},"modified":"2026-10-09T06:28:49","modified_gmt":"2026-10-09T06:28:49","slug":"ktmb-trains","status":"publish","type":"page","link":"https:\/\/webmotiontech.com\/index.php\/ktmb-trains\/","title":{"rendered":"KTMB Trains"},"content":{"rendered":"\n<p>This is our contribution to the KTMB schedule planner.  I think it&#8217;s better than the PDF provided on KTMB&#8217;s website.  Please feel free to use it.  I&#8217;m improving it and should have a web app and mobile app soon.  I value your comments, thank you. Please e-mail them to ravi@webmotiontech.com<\/p>\n\n\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>KTM Komuter Schedule & Trip Planner<\/title>\r\n    <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\r\n    <link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.4.0\/css\/all.min.css\">\r\n    <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@300;400;500;600;700;800&display=swap\" rel=\"stylesheet\">\r\n    <style>\r\n        body {\r\n            font-family: 'Inter', sans-serif;\r\n            background-color: #f1f5f9;\r\n        }\r\n        .custom-scrollbar::-webkit-scrollbar {\r\n            width: 6px;\r\n            height: 6px;\r\n        }\r\n        .custom-scrollbar::-webkit-scrollbar-track {\r\n            background: #f1f5f9;\r\n        }\r\n        .custom-scrollbar::-webkit-scrollbar-thumb {\r\n            color: #ffffff;\r\n            box-shadow: 0 4px 6px -1px rgba(185, 28, 28, 0.3);\r\n        }\r\n        .inactive-tab {\r\n            background-color: #ffffff;\r\n            color: #475569;\r\n        }\r\n        .inactive-tab:hover {\r\n            background-color: #f8fafc;\r\n            color: #0f172a;\r\n        }\r\n    <\/style>\r\n<\/head>\r\n<body class=\"bg-slate-100 min-h-screen flex flex-col text-slate-800\">\r\n\r\n    <header class=\"bg-gradient-to-r from-red-800 via-red-700 to-red-900 text-white shadow-lg sticky top-0 z-50\">\r\n        <div class=\"max-w-5xl mx-auto px-4 py-4 flex flex-col sm:flex-row items-center justify-between gap-3\">\r\n            <div class=\"flex items-center space-x-3\">\r\n                <div class=\"bg-white\/10 p-2.5 rounded-xl backdrop-blur-md border border-white\/20\">\r\n                    <i class=\"fa-solid fa-train-subway text-2xl text-yellow-400\"><\/i>\r\n                <\/div>\r\n                <div>\r\n                    <h1 class=\"text-xl font-extrabold tracking-tight text-white\">KTM Komuter Planner<\/h1>\r\n                    <p class=\"text-xs text-red-200 font-medium\">Tanjung Malim <i class=\"fa-solid fa-arrow-right-arrow-left text-[10px] mx-1\"><\/i> Pelabuhan Klang<\/p>\r\n                <\/div>\r\n            <\/div>\r\n            <div class=\"flex items-center space-x-2 bg-black\/20 px-3 py-1.5 rounded-lg border border-white\/10 text-xs font-semibold\">\r\n                <span class=\"inline-block w-2 h-2 rounded-full bg-emerald-400 animate-pulse\"><\/span>\r\n                <span>Weekday Schedule Active<\/span>\r\n            <\/div>\r\n        <\/div>\r\n    <\/header>\r\n\r\n    <main class=\"flex-grow max-w-5xl w-full mx-auto px-4 py-6\">\r\n        \r\n        <!-- Tab Controls -->\r\n        <div class=\"grid grid-cols-2 gap-2 bg-slate-200 p-1.5 rounded-2xl mb-6 shadow-inner\">\r\n            <button id=\"tab-planner-btn\" onclick=\"switchTab('planner')\" class=\"active-tab py-3 px-4 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all duration-200\">\r\n                <i class=\"fa-solid fa-route text-base\"><\/i>\r\n                <span>Trip Planner<\/span>\r\n            <\/button>\r\n            <button id=\"tab-timetable-btn\" onclick=\"switchTab('timetable')\" class=\"inactive-tab py-3 px-4 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all duration-200\">\r\n                <i class=\"fa-solid fa-clock text-base\"><\/i>\r\n                <span>Station Timetable<\/span>\r\n            <\/button>\r\n        <\/div>\r\n\r\n        <!-- VIEW 1: TRIP PLANNER -->\r\n        <div id=\"view-planner\" class=\"space-y-6\">\r\n            <div class=\"bg-white rounded-2xl p-5 md:p-6 shadow-sm border border-slate-200\">\r\n                <div class=\"flex items-center justify-between mb-4 border-b border-slate-100 pb-3\">\r\n                    <h2 class=\"text-base font-bold text-slate-800 flex items-center gap-2\">\r\n                        <i class=\"fa-solid fa-sliders text-red-600\"><\/i> Find Train Connections\r\n                    <\/h2>\r\n                    <button onclick=\"resetPlanner()\" class=\"text-xs font-semibold text-slate-500 hover:text-red-600 transition flex items-center gap-1\">\r\n                        <i class=\"fa-solid fa-rotate-left\"><\/i> Reset Defaults\r\n                    <\/button>\r\n                <\/div>\r\n\r\n                <div class=\"grid grid-cols-1 md:grid-cols-12 gap-4 items-center\">\r\n                    <!-- Origin -->\r\n                    <div class=\"md:col-span-5\">\r\n                        <label class=\"block text-xs font-bold uppercase tracking-wider text-slate-500 mb-1.5\">Origin Station<\/label>\r\n                        <div class=\"relative\">\r\n                            <i class=\"fa-solid fa-location-dot absolute left-3.5 top-1\/2 -translate-y-1\/2 text-slate-400\"><\/i>\r\n                            <select id=\"origin-select\" onchange=\"calculateRoute()\" class=\"w-full bg-slate-50 border border-slate-300 text-slate-800 text-sm font-semibold rounded-xl pl-10 pr-8 py-3 appearance-none focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition\">\r\n                            <\/select>\r\n                            <i class=\"fa-solid fa-chevron-down absolute right-3.5 top-1\/2 -translate-y-1\/2 text-slate-400 pointer-events-none text-xs\"><\/i>\r\n                        <\/div>\r\n                    <\/div>\r\n\r\n                    <!-- Swap Button -->\r\n                    <div class=\"md:col-span-2 flex justify-center py-1 md:py-0\">\r\n                        <button onclick=\"swapStations()\" title=\"Swap Stations\" class=\"bg-slate-100 hover:bg-red-50 hover:text-red-600 text-slate-600 p-3 rounded-full border border-slate-200 shadow-sm transition active:scale-95\">\r\n                            <i class=\"fa-solid fa-arrow-right-arrow-left text-sm md:rotate-0\"><\/i>\r\n                        <\/button>\r\n                    <\/div>\r\n\r\n                    <!-- Destination -->\r\n                    <div class=\"md:col-span-5\">\r\n                        <label class=\"block text-xs font-bold uppercase tracking-wider text-slate-500 mb-1.5\">Destination Station<\/label>\r\n                        <div class=\"relative\">\r\n                            <i class=\"fa-solid fa-flag-checkered absolute left-3.5 top-1\/2 -translate-y-1\/2 text-slate-400\"><\/i>\r\n                            <select id=\"destination-select\" onchange=\"calculateRoute()\" class=\"w-full bg-slate-50 border border-slate-300 text-slate-800 text-sm font-semibold rounded-xl pl-10 pr-8 py-3 appearance-none focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition\">\r\n                            <\/select>\r\n                            <i class=\"fa-solid fa-chevron-down absolute right-3.5 top-1\/2 -translate-y-1\/2 text-slate-400 pointer-events-none text-xs\"><\/i>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <!-- Departing After Time Input -->\r\n                <div class=\"mt-4 pt-4 border-t border-slate-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3\">\r\n                    <div class=\"flex items-center space-x-3 w-full sm:w-auto\">\r\n                        <label for=\"time-filter\" class=\"text-xs font-bold uppercase tracking-wider text-slate-500 shrink-0\">Departing After:<\/label>\r\n                        <input type=\"time\" id=\"time-filter\" value=\"18:00\" onchange=\"calculateRoute()\" class=\"bg-slate-50 border border-slate-300 text-slate-800 text-sm font-bold rounded-xl px-3 py-2 focus:outline-none focus:ring-2 focus:ring-red-500\">\r\n                    <\/div>\r\n                    <div class=\"text-xs text-slate-500 italic\">\r\n                        All times display exact calculated departures.\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- Results Container -->\r\n            <div>\r\n                <div class=\"flex items-center justify-between mb-3 px-1\">\r\n                    <h3 class=\"font-bold text-slate-700 text-sm flex items-center gap-2\">\r\n                        <span id=\"results-count\" class=\"bg-red-100 text-red-700 px-2 py-0.5 rounded-md text-xs font-extrabold\">0 Available<\/span>\r\n                        Direct Trips\r\n                    <\/h3>\r\n                    <span class=\"text-xs text-slate-400 font-medium\"><i class=\"fa-solid fa-arrow-down-short-wide text-xs mr-1\"><\/i>Sorted Chronologically<\/span>\r\n                <\/div>\r\n\r\n                <div id=\"results-list\" class=\"space-y-3\">\r\n                    <!-- Dynamic Trip Cards Render Here -->\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <!-- VIEW 2: STATION TIMETABLE -->\r\n        <div id=\"view-timetable\" class=\"hidden space-y-6\">\r\n            <div class=\"bg-white rounded-2xl p-5 md:p-6 shadow-sm border border-slate-200\">\r\n                <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n                    <div>\r\n                        <label class=\"block text-xs font-bold uppercase tracking-wider text-slate-500 mb-1.5\">Select Station<\/label>\r\n                        <div class=\"relative\">\r\n                            <i class=\"fa-solid fa-train-tram absolute left-3.5 top-1\/2 -translate-y-1\/2 text-slate-400\"><\/i>\r\n                            <select id=\"tt-station-select\" onchange=\"renderTimetable()\" class=\"w-full bg-slate-50 border border-slate-300 text-slate-800 text-sm font-semibold rounded-xl pl-10 pr-8 py-3 appearance-none focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition\">\r\n                            <\/select>\r\n                            <i class=\"fa-solid fa-chevron-down absolute right-3.5 top-1\/2 -translate-y-1\/2 text-slate-400 pointer-events-none text-xs\"><\/i>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div>\r\n                        <label class=\"block text-xs font-bold uppercase tracking-wider text-slate-500 mb-1.5\">Filter Direction<\/label>\r\n                        <select id=\"tt-direction-select\" onchange=\"renderTimetable()\" class=\"w-full bg-slate-50 border border-slate-300 text-slate-800 text-sm font-semibold rounded-xl px-4 py-3 appearance-none focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition\">\r\n                            <option value=\"ALL\">All Directions<\/option>\r\n                            <option value=\"NORTH\">Northbound (to Tanjung Malim)<\/option>\r\n                            <option value=\"SOUTH\">Southbound (to Pelabuhan Klang)<\/option>\r\n                        <\/select>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <div class=\"bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden\">\r\n                <div class=\"px-5 py-4 border-b border-slate-100 flex items-center justify-between\">\r\n                    <h3 class=\"font-bold text-slate-800 text-base\" id=\"tt-title\">Full Station Departure Board<\/h3>\r\n                    <span class=\"text-xs bg-slate-100 text-slate-600 px-2.5 py-1 rounded-full font-semibold\" id=\"tt-count\">0 Departures<\/span>\r\n                <\/div>\r\n                <div class=\"overflow-x-auto\">\r\n                    <table class=\"w-full text-left border-collapse\">\r\n                        <thead>\r\n                            <tr class=\"bg-slate-50 text-xs font-bold text-slate-500 uppercase tracking-wider border-b border-slate-200\">\r\n                                <th class=\"py-3 px-4\">Train No.<\/th>\r\n                                <th class=\"py-3 px-4\">Departure Time<\/th>\r\n                                <th class=\"py-3 px-4\">Direction \/ Destination<\/th>\r\n                                <th class=\"py-3 px-4\">Origin Hub<\/th>\r\n                            <\/tr>\r\n                        <\/thead>\r\n                        <tbody id=\"tt-table-body\" class=\"divide-y divide-slate-100 text-sm\">\r\n                            <!-- Dynamic Timetable Rows Render Here -->\r\n                        <\/tbody>\r\n                    <\/table>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n    <\/main>\r\n\r\n    <!-- FOOTER -->\r\n    <footer class=\"bg-slate-900 text-slate-400 text-center text-xs py-6 px-4 mt-auto border-t border-slate-800\">\r\n        <div class=\"max-w-4xl mx-auto space-y-2\">\r\n            <p class=\"font-medium text-slate-300\">Schedule Effective Date: 06\/07\/2026 | Built for convenience & trip planning by Ravishanker @ WebMotion Tech, \u00a9 2026<\/p>\r\n\t\t\t\t\t<p class=\"font-medium text-slate-300\"> Dedicated to my wife, Jane Garimbao <\/p>\r\n            <p class=\"text-[11px] text-slate-500\">Official KTM Komuter Klang Valley Weekday Timetable Data Model. All schedules subject to KTMB updates.<\/p>\r\n        <\/div>\r\n    <\/footer>\r\n\r\n    <script>\r\n        \/\/ --- 1. STATIONS ARRAY IN STRICT ORDER (Pelabuhan Klang -> Tanjung Malim) ---\r\n        const STATIONS = [\r\n            \"Pelabuhan Klang\", \"Jalan Kastam\", \"Kg Raja Uda\", \"Teluk Gadong\", \"Teluk Pulai\", \r\n            \"Klang\", \"Bukit Badak\", \"Padang Jawa\", \"Shah Alam\", \"Batu Tiga\", \r\n            \"Subang Jaya\", \"Setia Jaya\", \"Seri Setia\", \"Kg Dato Harun\", \"Jalan Templer\", \r\n            \"Petaling\", \"Pantai Dalam\", \"Angkasapuri\", \"Abdullah Hukum\", \"KL Sentral\", \r\n            \"Kuala Lumpur\", \"Bank Negara\", \"Putra\", \"Segambut\", \"Segambut Utara\", \r\n            \"Kepong\", \"Kepong Sentral\", \"Sungai Buloh\", \"Kuang\", \"Rawang\", \r\n            \"Serendah\", \"Batang Kali\", \"Rasa\", \"Kuala Kubu Bharu\", \"Tanjung Malim\"\r\n        ];\r\n\r\n        \/\/ Cumulative minutes travel offset from index 0 (\"Pelabuhan Klang\")\r\n        const CUMULATIVE_MINUTES = [\r\n            0,   3,   6,   9,  11, \r\n            16,  21,  27,  33,  39, \r\n            48,  53,  56,  59,  63, \r\n            67,  70,  73,  76,  81, \r\n            86,  90,  94,  99, 103, \r\n            107, 110, 115, 122, 131, \r\n            139, 148, 155, 163, 178\r\n        ];\r\n\r\n        \/\/ Utility: convert \"HH:MM\" (24h string) to total minutes from midnight\r\n        function timeToMinutes(timeStr) {\r\n            if (!timeStr) return null;\r\n            const [h, m] = timeStr.split(':').map(Number);\r\n            return h * 60 + m;\r\n        }\r\n\r\n        \/\/ Utility: convert total minutes from midnight to 24h & 12h format strings\r\n        function minutesToTimeString(totalMinutes) {\r\n            let mins = totalMinutes % (24 * 60);\r\n            if (mins < 0) mins += 24 * 60;\r\n            const hours = Math.floor(mins \/ 60);\r\n            const m = mins % 60;\r\n            const hh = hours.toString().padStart(2, '0');\r\n            const mm = m.toString().padStart(2, '0');\r\n            \r\n            \/\/ 12h format string\r\n            const period = hours >= 12 ? 'PM' : 'AM';\r\n            const h12 = hours % 12 === 0 ? 12 : hours % 12;\r\n            const display12 = `${h12.toString().padStart(2, '0')}:${mm} ${period}`;\r\n\r\n            return { time24: `${hh}:${mm}`, time12: display12 };\r\n        }\r\n\r\n        \/\/ --- 2. RAW TRAIN BASE SCHEDULE DATA (EXPLICIT TIME ANCHORS) ---\r\n        \/\/ Anchor maps allow calculating explicit exact departure time at ANY station along the route.\r\n        const RAW_TRAINS = [\r\n            \/\/ NORTHBOUND TRAINS (Pelabuhan Klang \/ KL Sentral -> Tanjung Malim)\r\n            { id: \"2102\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"05:25\", \"KL Sentral\": \"06:46\", \"Sungai Buloh\": \"07:20\", \"Rawang\": \"07:36\", \"Serendah\": \"07:44\", \"Tanjung Malim\": \"08:23\" } },\r\n            { id: \"2104\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"06:10\", \"Sungai Buloh\": \"06:44\", \"Rawang\": \"07:00\", \"Serendah\": \"07:08\", \"Tanjung Malim\": \"07:47\" } },\r\n            { id: \"2108\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"06:15\", \"KL Sentral\": \"07:36\", \"Sungai Buloh\": \"08:10\", \"Rawang\": \"08:26\", \"Serendah\": \"08:34\", \"Tanjung Malim\": \"09:13\" } },\r\n            { id: \"2112\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"08:05\", \"Sungai Buloh\": \"08:39\", \"Rawang\": \"08:55\", \"Serendah\": \"09:03\", \"Tanjung Malim\": \"09:42\" } },\r\n            { id: \"2116\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"07:40\", \"KL Sentral\": \"09:01\", \"Sungai Buloh\": \"09:35\", \"Rawang\": \"09:51\", \"Serendah\": \"09:59\", \"Tanjung Malim\": \"10:38\" } },\r\n            { id: \"2120\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"10:05\", \"Sungai Buloh\": \"10:39\", \"Rawang\": \"10:55\", \"Serendah\": \"11:03\", \"Tanjung Malim\": \"11:42\" } },\r\n            { id: \"2124\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"09:50\", \"KL Sentral\": \"11:11\", \"Sungai Buloh\": \"11:45\", \"Rawang\": \"12:01\", \"Serendah\": \"12:09\", \"Tanjung Malim\": \"12:48\" } },\r\n            { id: \"2128\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"12:15\", \"Sungai Buloh\": \"12:49\", \"Rawang\": \"13:05\", \"Serendah\": \"13:13\", \"Tanjung Malim\": \"13:52\" } },\r\n            { id: \"2132\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"11:50\", \"KL Sentral\": \"13:11\", \"Sungai Buloh\": \"13:45\", \"Rawang\": \"14:01\", \"Serendah\": \"14:09\", \"Tanjung Malim\": \"14:48\" } },\r\n            { id: \"2136\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"14:15\", \"Sungai Buloh\": \"14:49\", \"Rawang\": \"15:05\", \"Serendah\": \"15:13\", \"Tanjung Malim\": \"15:52\" } },\r\n            { id: \"2140\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"13:50\", \"KL Sentral\": \"15:11\", \"Sungai Buloh\": \"15:45\", \"Rawang\": \"16:01\", \"Serendah\": \"16:09\", \"Tanjung Malim\": \"16:48\" } },\r\n            { id: \"2144\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"16:05\", \"Sungai Buloh\": \"16:39\", \"Rawang\": \"16:55\", \"Serendah\": \"17:03\", \"Tanjung Malim\": \"17:42\" } },\r\n            { id: \"2148\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"15:45\", \"KL Sentral\": \"17:06\", \"Sungai Buloh\": \"17:40\", \"Rawang\": \"17:56\", \"Serendah\": \"18:04\", \"Tanjung Malim\": \"18:43\" } },\r\n            { id: \"2154\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"17:05\", \"KL Sentral\": \"18:26\", \"Sungai Buloh\": \"19:00\", \"Rawang\": \"19:16\", \"Serendah\": \"19:24\", \"Tanjung Malim\": \"20:03\" } },\r\n            { id: \"2158\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"17:25\", \"Sungai Buloh\": \"17:59\", \"Rawang\": \"18:15\", \"Serendah\": \"18:23\", \"Tanjung Malim\": \"19:02\" } },\r\n            { id: \"2160\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"18:40\", \"Sungai Buloh\": \"19:14\", \"Rawang\": \"19:30\", \"Serendah\": \"19:38\", \"Tanjung Malim\": \"20:17\" } },\r\n            { id: \"2162\", dir: \"NORTH\", startStation: \"KL Sentral\", termStation: \"Tanjung Malim\", anchors: { \"KL Sentral\": \"19:10\", \"Sungai Buloh\": \"19:44\", \"Rawang\": \"20:00\", \"Serendah\": \"20:08\", \"Tanjung Malim\": \"20:47\" } },\r\n            { id: \"2166\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"19:30\", \"KL Sentral\": \"20:51\", \"Sungai Buloh\": \"21:25\", \"Rawang\": \"21:41\", \"Serendah\": \"21:49\", \"Tanjung Malim\": \"22:28\" } },\r\n            { id: \"2168\", dir: \"NORTH\", startStation: \"Pelabuhan Klang\", termStation: \"Tanjung Malim\", anchors: { \"Pelabuhan Klang\": \"21:05\", \"KL Sentral\": \"22:26\", \"Sungai Buloh\": \"23:00\", \"Rawang\": \"23:16\", \"Serendah\": \"23:24\", \"Tanjung Malim\": \"00:03\" } },\r\n\r\n            \/\/ SOUTHBOUND TRAINS (Tanjung Malim -> Pelabuhan Klang)\r\n            { id: \"2105\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"05:35\", \"Serendah\": \"06:14\", \"Rawang\": \"06:22\", \"Sungai Buloh\": \"06:38\", \"KL Sentral\": \"07:12\", \"Pelabuhan Klang\": \"08:33\" } },\r\n            { id: \"2109\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"KL Sentral\", anchors: { \"Tanjung Malim\": \"06:05\", \"Serendah\": \"06:44\", \"Rawang\": \"06:52\", \"Sungai Buloh\": \"07:08\", \"KL Sentral\": \"07:42\" } },\r\n            { id: \"2113\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"06:45\", \"Serendah\": \"07:24\", \"Rawang\": \"07:32\", \"Sungai Buloh\": \"07:48\", \"KL Sentral\": \"08:22\", \"Pelabuhan Klang\": \"09:43\" } },\r\n            { id: \"2117\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"08:15\", \"Serendah\": \"08:54\", \"Rawang\": \"09:02\", \"Sungai Buloh\": \"09:18\", \"KL Sentral\": \"09:52\", \"Pelabuhan Klang\": \"11:13\" } },\r\n            { id: \"2121\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"09:45\", \"Serendah\": \"10:24\", \"Rawang\": \"10:32\", \"Sungai Buloh\": \"10:48\", \"KL Sentral\": \"11:22\", \"Pelabuhan Klang\": \"12:43\" } },\r\n            { id: \"2125\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"11:05\", \"Serendah\": \"11:44\", \"Rawang\": \"11:52\", \"Sungai Buloh\": \"12:08\", \"KL Sentral\": \"12:42\", \"Pelabuhan Klang\": \"14:03\" } },\r\n            { id: \"2129\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"13:05\", \"Serendah\": \"13:44\", \"Rawang\": \"13:52\", \"Sungai Buloh\": \"14:08\", \"KL Sentral\": \"14:42\", \"Pelabuhan Klang\": \"16:03\" } },\r\n            { id: \"2133\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"15:05\", \"Serendah\": \"15:44\", \"Rawang\": \"15:52\", \"Sungai Buloh\": \"16:08\", \"KL Sentral\": \"16:42\", \"Pelabuhan Klang\": \"18:03\" } },\r\n            { id: \"2137\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"16:05\", \"Serendah\": \"16:44\", \"Rawang\": \"16:52\", \"Sungai Buloh\": \"17:08\", \"KL Sentral\": \"17:42\", \"Pelabuhan Klang\": \"19:03\" } },\r\n            { id: \"2141\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"17:10\", \"Serendah\": \"17:49\", \"Rawang\": \"17:57\", \"Sungai Buloh\": \"18:13\", \"KL Sentral\": \"18:47\", \"Pelabuhan Klang\": \"20:08\" } },\r\n            { id: \"2145\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"18:15\", \"Serendah\": \"18:54\", \"Rawang\": \"19:02\", \"Sungai Buloh\": \"19:18\", \"KL Sentral\": \"19:52\", \"Pelabuhan Klang\": \"21:13\" } },\r\n            { id: \"2149\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"19:35\", \"Serendah\": \"20:14\", \"Rawang\": \"20:22\", \"Sungai Buloh\": \"20:38\", \"KL Sentral\": \"21:12\", \"Pelabuhan Klang\": \"22:33\" } },\r\n            { id: \"2153\", dir: \"SOUTH\", startStation: \"Tanjung Malim\", termStation: \"Pelabuhan Klang\", anchors: { \"Tanjung Malim\": \"20:40\", \"Serendah\": \"21:19\", \"Rawang\": \"21:27\", \"Sungai Buloh\": \"21:43\", \"KL Sentral\": \"22:17\", \"Pelabuhan Klang\": \"23:38\" } }\r\n        ];\r\n\r\n        \/\/ --- STREAMING_CHUNK:Interpolation function calculating exact station departure minutes... ---\r\n        \/\/ Get absolute departure time in minutes for ANY station for a given train\r\n        function getStationTimeInMinutes(train, stationName) {\r\n            const stIndex = STATIONS.indexOf(stationName);\r\n            const startIndex = STATIONS.indexOf(train.startStation);\r\n            const termIndex = STATIONS.indexOf(train.termStation);\r\n\r\n            \/\/ Verify station is served by this trip\r\n            if (train.dir === \"NORTH\") {\r\n                if (stIndex < startIndex || stIndex > termIndex) return null;\r\n            } else { \/\/ SOUTH\r\n                if (stIndex > startIndex || stIndex < termIndex) return null;\r\n            }\r\n\r\n            \/\/ Check direct exact anchor first\r\n            if (train.anchors[stationName]) {\r\n                return timeToMinutes(train.anchors[stationName]);\r\n            }\r\n\r\n            \/\/ Find nearest prior anchor to calculate minute offset\r\n            let nearestAnchorStation = null;\r\n            let nearestAnchorMin = null;\r\n\r\n            if (train.dir === \"NORTH\") {\r\n                for (let i = stIndex; i >= 0; i--) {\r\n                    const candidate = STATIONS[i];\r\n                    if (train.anchors[candidate]) {\r\n                        nearestAnchorStation = candidate;\r\n                        nearestAnchorMin = timeToMinutes(train.anchors[candidate]);\r\n                        break;\r\n                    }\r\n                }\r\n            } else {\r\n                for (let i = stIndex; i < STATIONS.length; i++) {\r\n                    const candidate = STATIONS[i];\r\n                    if (train.anchors[candidate]) {\r\n                        nearestAnchorStation = candidate;\r\n                        nearestAnchorMin = timeToMinutes(train.anchors[candidate]);\r\n                        break;\r\n                    }\r\n                }\r\n            }\r\n\r\n            if (nearestAnchorStation !== null && nearestAnchorMin !== null) {\r\n                const anchorIdx = STATIONS.indexOf(nearestAnchorStation);\r\n                const minuteDiff = train.dir === \"NORTH\" \r\n                    ? CUMULATIVE_MINUTES[stIndex] - CUMULATIVE_MINUTES[anchorIdx]\r\n                    : CUMULATIVE_MINUTES[anchorIdx] - CUMULATIVE_MINUTES[stIndex];\r\n\r\n                return nearestAnchorMin + minuteDiff;\r\n            }\r\n\r\n            return null;\r\n        }\r\n\r\n        \/\/ --- 3. UI INITIALIZATION & EVENT HANDLERS ---\r\n        window.onload = function () {\r\n            populateStationDropdowns();\r\n            resetPlanner();\r\n        };\r\n\r\n        function populateStationDropdowns() {\r\n            const originSel = document.getElementById('origin-select');\r\n            const destSel = document.getElementById('destination-select');\r\n            const ttSel = document.getElementById('tt-station-select');\r\n\r\n            let optionsHtml = '';\r\n            STATIONS.forEach(st => {\r\n                optionsHtml += `<option value=\"${st}\">${st}<\/option>`;\r\n            });\r\n\r\n            originSel.innerHTML = optionsHtml;\r\n            destSel.innerHTML = optionsHtml;\r\n            ttSel.innerHTML = optionsHtml;\r\n        }\r\n\r\n        function resetPlanner() {\r\n            document.getElementById('origin-select').value = \"KL Sentral\";\r\n            document.getElementById('destination-select').value = \"Tanjung Malim\";\r\n            document.getElementById('time-filter').value = \"18:00\";\r\n            calculateRoute();\r\n        }\r\n\r\n        function swapStations() {\r\n            const orig = document.getElementById('origin-select').value;\r\n            const dest = document.getElementById('destination-select').value;\r\n            document.getElementById('origin-select').value = dest;\r\n            document.getElementById('destination-select').value = orig;\r\n            calculateRoute();\r\n        }\r\n\r\n        function switchTab(tabName) {\r\n            const plannerBtn = document.getElementById('tab-planner-btn');\r\n            const timetableBtn = document.getElementById('tab-timetable-btn');\r\n            const plannerView = document.getElementById('view-planner');\r\n            const timetableView = document.getElementById('view-timetable');\r\n\r\n            if (tabName === 'planner') {\r\n                plannerBtn.className = 'active-tab py-3 px-4 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all duration-200';\r\n                timetableBtn.className = 'inactive-tab py-3 px-4 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all duration-200';\r\n                plannerView.classList.remove('hidden');\r\n                timetableView.classList.add('hidden');\r\n                calculateRoute();\r\n            } else {\r\n                timetableBtn.className = 'active-tab py-3 px-4 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all duration-200';\r\n                plannerBtn.className = 'inactive-tab py-3 px-4 rounded-xl font-bold text-sm flex items-center justify-center space-x-2 transition-all duration-200';\r\n                timetableView.classList.remove('hidden');\r\n                plannerView.classList.add('hidden');\r\n                renderTimetable();\r\n            }\r\n        }\r\n\r\n        \/\/ --- STREAMING_CHUNK:Core Search Engine logic for Trip Calculation and Sorting... ---\r\n        \/\/ --- 4. ROUTE CALCULATOR ENGINE ---\r\n        function calculateRoute() {\r\n            const origin = document.getElementById('origin-select').value;\r\n            const dest = document.getElementById('destination-select').value;\r\n            const timeFilterVal = document.getElementById('time-filter').value;\r\n            const filterMin = timeToMinutes(timeFilterVal) || 0;\r\n\r\n            const resultsContainer = document.getElementById('results-list');\r\n            const countBadge = document.getElementById('results-count');\r\n\r\n            if (origin === dest) {\r\n                resultsContainer.innerHTML = `\r\n                    <div class=\"bg-amber-50 border border-amber-200 text-amber-800 rounded-2xl p-6 text-center\">\r\n                        <i class=\"fa-solid fa-triangle-exclamation text-2xl text-amber-500 mb-2\"><\/i>\r\n                        <p class=\"font-bold\">Same Station Selected<\/p>\r\n                        <p class=\"text-xs text-amber-600 mt-1\">Please select different origin and destination stations to plan your trip.<\/p>\r\n                    <\/div>\r\n                `;\r\n                countBadge.innerText = `0 Available`;\r\n                return;\r\n            }\r\n\r\n            const origIndex = STATIONS.indexOf(origin);\r\n            const destIndex = STATIONS.indexOf(dest);\r\n            const requiredDirection = origIndex < destIndex ? \"NORTH\" : \"SOUTH\";\r\n\r\n            const matchedTrips = [];\r\n\r\n            RAW_TRAINS.forEach(train => {\r\n                if (train.dir !== requiredDirection) return;\r\n\r\n                const origDepMin = getStationTimeInMinutes(train, origin);\r\n                const destArrMin = getStationTimeInMinutes(train, dest);\r\n\r\n                \/\/ Both stations must be served and origin must come before destination chronologically\r\n                if (origDepMin !== null && destArrMin !== null && origDepMin < destArrMin) {\r\n                    \/\/ Filter: departure must be >= selected filter time\r\n                    if (origDepMin >= filterMin) {\r\n                        const duration = destArrMin - origDepMin;\r\n                        matchedTrips.push({\r\n                            trainNo: train.id,\r\n                            direction: train.dir,\r\n                            origDepMin: origDepMin,\r\n                            destArrMin: destArrMin,\r\n                            duration: duration,\r\n                            startStation: train.startStation,\r\n                            termStation: train.termStation,\r\n                            origDepTime: minutesToTimeString(origDepMin),\r\n                            destArrTime: minutesToTimeString(destArrMin)\r\n                        });\r\n                    }\r\n                }\r\n            });\r\n\r\n            \/\/ STRICT ASCENDING SORT BY DEPARTURE TIME\r\n            matchedTrips.sort((a, b) => a.origDepMin - b.origDepMin);\r\n\r\n            countBadge.innerText = `${matchedTrips.length} Available`;\r\n\r\n            if (matchedTrips.length === 0) {\r\n                resultsContainer.innerHTML = `\r\n                    <div class=\"bg-white border border-slate-200 rounded-2xl p-8 text-center shadow-sm\">\r\n                        <div class=\"w-12 h-12 bg-red-50 text-red-500 rounded-full flex items-center justify-center mx-auto mb-3\">\r\n                            <i class=\"fa-solid fa-calendar-xmark text-xl\"><\/i>\r\n                        <\/div>\r\n                        <h4 class=\"font-bold text-slate-800\">No Direct Trains Found<\/h4>\r\n                        <p class=\"text-xs text-slate-500 mt-1 max-w-sm mx-auto\">No scheduled departures found after ${minutesToTimeString(filterMin).time12} from ${origin} to ${dest}. Try setting an earlier departure time.<\/p>\r\n                    <\/div>\r\n                `;\r\n                return;\r\n            }\r\n\r\n            let cardsHtml = '';\r\n            matchedTrips.forEach(trip => {\r\n                cardsHtml += `\r\n                    <div class=\"bg-white rounded-2xl border border-slate-200 p-4 md:p-5 shadow-sm hover:shadow-md transition group\">\r\n                        <div class=\"flex flex-col md:flex-row md:items-center justify-between gap-4\">\r\n                            \r\n                            <!-- Train Meta -->\r\n                            <div class=\"flex items-center space-x-3\">\r\n                                <div class=\"bg-slate-100 group-hover:bg-red-50 text-slate-700 group-hover:text-red-600 p-3 rounded-xl border border-slate-200 transition\">\r\n                                    <i class=\"fa-solid fa-train text-lg\"><\/i>\r\n                                <\/div>\r\n                                <div>\r\n                                    <div class=\"flex items-center gap-2\">\r\n                                        <span class=\"font-extrabold text-slate-900 text-sm\">Train ${trip.trainNo}<\/span>\r\n                                        <span class=\"text-[10px] font-bold bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full uppercase\">${trip.direction}BOUND<\/span>\r\n                                    <\/div>\r\n                                    <p class=\"text-xs text-slate-500\">${trip.startStation} <i class=\"fa-solid fa-arrow-right text-[10px] mx-0.5\"><\/i> ${trip.termStation}<\/p>\r\n                                <\/div>\r\n                            <\/div>\r\n\r\n                            <!-- Route & Time Timeline -->\r\n                            <div class=\"flex items-center justify-between md:justify-end gap-4 sm:gap-6 border-t md:border-t-0 pt-3 md:pt-0 border-slate-100\">\r\n                                <!-- Origin Departure -->\r\n                                <div class=\"text-left\">\r\n                                    <p class=\"text-xs text-slate-400 font-semibold uppercase\">Departs<\/p>\r\n                                    <p class=\"text-base font-extrabold text-slate-900\">${trip.origDepTime.time12}<\/p>\r\n                                    <p class=\"text-xs text-red-600 font-bold truncate max-w-[100px]\">${origin}<\/p>\r\n                                <\/div>\r\n\r\n                                <!-- Travel Duration Graphic -->\r\n                                <div class=\"flex flex-col items-center px-2\">\r\n                                    <span class=\"text-[11px] font-bold text-slate-500 bg-slate-100 px-2 py-0.5 rounded-full mb-1\">${trip.duration} mins<\/span>\r\n                                    <div class=\"flex items-center space-x-1\">\r\n                                        <span class=\"w-2 h-2 rounded-full bg-red-600\"><\/span>\r\n                                        <div class=\"w-12 sm:w-20 h-0.5 bg-slate-300\"><\/div>\r\n                                        <i class=\"fa-solid fa-chevron-right text-xs text-slate-400\"><\/i>\r\n                                    <\/div>\r\n                                <\/div>\r\n\r\n                                <!-- Destination Arrival -->\r\n                                <div class=\"text-right\">\r\n                                    <p class=\"text-xs text-slate-400 font-semibold uppercase\">Arrives<\/p>\r\n                                    <p class=\"text-base font-extrabold text-slate-900\">${trip.destArrTime.time12}<\/p>\r\n                                    <p class=\"text-xs text-emerald-600 font-bold truncate max-w-[100px]\">${dest}<\/p>\r\n                                <\/div>\r\n                            <\/div>\r\n\r\n                        <\/div>\r\n                    <\/div>\r\n                `;\r\n            });\r\n\r\n            resultsContainer.innerHTML = cardsHtml;\r\n        }\r\n\r\n        \/\/ --- STREAMING_CHUNK:Station Timetable view rendering logic... ---\r\n        \/\/ --- 5. TIMETABLE VIEW ENGINE ---\r\n        function renderTimetable() {\r\n            const station = document.getElementById('tt-station-select').value;\r\n            const dirFilter = document.getElementById('tt-direction-select').value;\r\n            const tbody = document.getElementById('tt-table-body');\r\n            const countEl = document.getElementById('tt-count');\r\n            const titleEl = document.getElementById('tt-title');\r\n\r\n            titleEl.innerText = `${station} - Station Timetable`;\r\n\r\n            const departures = [];\r\n\r\n            RAW_TRAINS.forEach(train => {\r\n                if (dirFilter !== \"ALL\" && train.dir !== dirFilter) return;\r\n\r\n                const depMin = getStationTimeInMinutes(train, station);\r\n                if (depMin !== null) {\r\n                    departures.push({\r\n                        trainNo: train.id,\r\n                        direction: train.dir,\r\n                        depMin: depMin,\r\n                        depTime: minutesToTimeString(depMin),\r\n                        startStation: train.startStation,\r\n                        termStation: train.termStation\r\n                    });\r\n                }\r\n            });\r\n\r\n            departures.sort((a, b) => a.depMin - b.depMin);\r\n            countEl.innerText = `${departures.length} Departures`;\r\n\r\n            if (departures.length === 0) {\r\n                tbody.innerHTML = `\r\n                    <tr>\r\n                        <td colspan=\"4\" class=\"py-8 text-center text-slate-400 font-medium\">No train departures scheduled for ${station} under selected filter.<\/td>\r\n                    <\/tr>\r\n                `;\r\n                return;\r\n            }\r\n\r\n            let rowsHtml = '';\r\n            departures.forEach(dep => {\r\n                rowsHtml += `\r\n                    <tr class=\"hover:bg-slate-50 transition\">\r\n                        <td class=\"py-3.5 px-4 font-bold text-slate-800\">\r\n                            <span class=\"bg-slate-100 text-slate-700 px-2 py-1 rounded-md text-xs\">Train ${dep.trainNo}<\/span>\r\n                        <\/td>\r\n                        <td class=\"py-3.5 px-4 font-extrabold text-red-600 text-base\">\r\n                            ${dep.depTime.time12}\r\n                        <\/td>\r\n                        <td class=\"py-3.5 px-4\">\r\n                            <div class=\"font-semibold text-slate-800 text-xs\">To ${dep.termStation}<\/div>\r\n                            <div class=\"text-[11px] text-slate-400 uppercase font-bold\">${dep.direction}BOUND<\/div>\r\n                        <\/td>\r\n                        <td class=\"py-3.5 px-4 text-xs font-medium text-slate-500\">\r\n                            ${dep.startStation}\r\n                        <\/td>\r\n                    <\/tr>\r\n                `;\r\n            });\r\n\r\n            tbody.innerHTML = rowsHtml;\r\n        }\r\n    <\/script>\r\n<\/body>\r\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>This is our contribution to the KTMB schedule planner. I think it&#8217;s better than the PDF provided on KTMB&#8217;s website. Please feel free to use it. I&#8217;m improving it and should have a web app and mobile app soon. I value your comments, thank you. Please e-mail them to ravi@webmotiontech.com<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-663","page","type-page","status-publish"],"_links":{"self":[{"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/pages\/663","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/comments?post=663"}],"version-history":[{"count":5,"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/pages\/663\/revisions"}],"predecessor-version":[{"id":671,"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/pages\/663\/revisions\/671"}],"wp:attachment":[{"href":"https:\/\/webmotiontech.com\/index.php\/wp-json\/wp\/v2\/media?parent=663"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}