File size: 8,542 Bytes
c230052
 
 
 
 
 
de9fc43
a245764
 
c230052
aba0227
de9fc43
b069048
c230052
a245764
aba0227
de9fc43
0f22030
a245764
e269391
 
 
 
b069048
 
eb8c3f1
a245764
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e269391
 
aba0227
e269391
 
 
 
 
 
 
 
 
22bc3e0
e269391
 
 
 
 
 
 
 
aba0227
 
 
 
 
 
 
 
 
ab017df
de9fc43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f22030
de9fc43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab017df
 
 
 
 
 
de9fc43
 
 
eb8c3f1
e92d416
aba0227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb8c3f1
e92d416
aba0227
 
 
 
 
 
 
 
 
e269391
eb8c3f1
 
de9fc43
 
 
 
 
 
e92d416
e269391
 
 
 
 
 
 
 
 
 
 
1205903
 
99e0e39
eb8c3f1
589b650
eb8c3f1
cab6203
9e2c8ea
 
cab6203
eb8c3f1
e92d416
aba0227
e269391
 
 
 
aba0227
e269391
 
 
 
 
 
 
 
 
 
 
 
 
9e2c8ea
aba0227
e269391
cab6203
e269391
aba0227
 
e92d416
aba0227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cb08519
aba0227
 
cab6203
aba0227
 
eb8c3f1
 
cae34cd
de9fc43
 
 
 
 
1a8302d
de9fc43
 
 
 
 
cb08519
de9fc43
 
cab6203
de9fc43
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# coding: utf-8

# Author: Du Mingzhe ([email protected])
# Date: 2025/03/22

import os
import re
import json
import requests
import streamlit as st
from datetime import datetime
from bs4 import BeautifulSoup
from streamlit_autorefresh import st_autorefresh

nextbus_token = os.getenv("NEXTBUS_TOKEN")
datamall_token = os.getenv("DATAMALL_TOKEN")
mrt_token = os.getenv("MRT_TOKEN")
mrt_cookie = os.getenv("MRT_COOKIE")

def wide_space_default():
    st.set_page_config(layout='wide')
wide_space_default()

count = st_autorefresh(interval=30000)

def get_all_nus_stops():
    url = "https://nnextbus.nus.edu.sg/BusStops"

    payload = {}
    headers = {
        'Host': 'nnextbus.nus.edu.sg',
        'Content-Type': 'application/json',
        'Connection': 'keep-alive',
        'Accept': 'application/json',
        'User-Agent': 'nusnextbusv2/1 CFNetwork/978.0.7 Darwin/18.7.0',
        'Authorization': nextbus_token,
        'Accept-Language': 'en-us',
        'Accept-Encoding': 'br, gzip, deflate'
    }

    response = requests.request("GET", url, headers=headers, data=payload)
    return response.json()["BusStopsResult"]["busstops"]

def get_nus_bus_arrival(bus_stop_code):
    url = f"https://nnextbus.nus.edu.sg/ShuttleService?busstopname={bus_stop_code}"

    payload = {}
    headers = {
        'Host': 'nnextbus.nus.edu.sg',
        'Content-Type': 'application/json',
        'Connection': 'keep-alive',
        'Accept': 'application/json',
        'User-Agent': 'nusnextbusv2/1 CFNetwork/978.0.7 Darwin/18.7.0',
        'Authorization': nextbus_token,
        'Accept-Language': 'en-us',
        'Accept-Encoding': 'br, gzip, deflate'
    }

    response = requests.request("GET", url, headers=headers, data=payload)

    return response.json()['ShuttleServiceResult']['shuttles']

def get_lta_bus_arrival(bus_stop_code):
    url = f"https://datamall2.mytransport.sg/ltaodataservice/v3/BusArrival?BusStopCode={bus_stop_code}"
    payload = {}
    headers = {
        'AccountKey': datamall_token
    }
    response = requests.request("GET", url, headers=headers, data=payload)
    return response.json()

def get_smrt_train_arrival(station_code):
    url = "https://trainarrivalweb.smrt.com.sg/"

    payload = f"ScriptManager1=UP1%7CddlStation&stnCode=&stnName=&ddlStation={station_code}&{mrt_token}"
    headers = {
        'Accept': '*/*',
        'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh-HK;q=0.6,zh-TW;q=0.5,zh;q=0.4',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Origin': 'https://trainarrivalweb.smrt.com.sg',
        'Referer': 'https://trainarrivalweb.smrt.com.sg/',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-origin',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
        'X-MicrosoftAjax': 'Delta=true',
        'X-Requested-With': 'XMLHttpRequest',
        'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"macOS"',
        'Cookie': mrt_cookie
    }
    response = requests.request("POST", url, headers=headers, data=payload)
    raw_html = response.text    
    soup = BeautifulSoup(raw_html, 'html.parser')

    tables = soup.find_all("table", id="gvTime")
    
    trains = list()
    
    for table in tables:
        time_row = table.find_all("tr")[1]
        time_cells = [td.get_text(strip=True) for td in time_row.find_all("td")]

        direction_row = table.find_all("tr")[2]
        direction_cells = [td.get_text(strip=True) for td in direction_row.find_all("td")]
        
        for eta_text, direction in zip(time_cells, direction_cells):
            eta = re.findall(r'\d+', eta_text)
            if eta:
                trains.append({
                    "direction": direction,
                    "eta": eta[0]
                })
    
    return trains

# NUS Bus Stops
nus_bus_stops = [
    {
        "caption": "COM 3",
        "name": "COM3",
        "LongName": "COM 3",
        "ShortName": "COM 3",
        "latitude": 1.294431,
        "longitude": 103.775217
    },
    {
        "caption": "Opp TCOMS",
        "name": "TCOMS-OPP",
        "LongName": "Opp TCOMS",
        "ShortName": "Opp TCOMS",
        "latitude": 1.293789,
        "longitude": 103.776715
    },
    {
        "caption": "TCOMS",
        "name": "TCOMS",
        "LongName": "TCOMS",
        "ShortName": "TCOMS",
        "latitude": 1.293654,
        "longitude": 103.776898
    },
    {
        "caption": "Prince George's Park Foyer",
        "name": "PGPR",
        "LongName": "Prince George's Park Foyer",
        "ShortName": "PGP Foyer",
        "latitude": 1.290994,
        "longitude": 103.781153
    },
]

# Public Bus Stops
public_bus_stops = [
    {
        'name': "Opp HMK",
        'code': "16061"
    },
    {
        'name': "HMK",
        'code': "16069"
    },
]

# MRT Stations
public_mrt_stations = [
    {
        'name': "KR MRT",
        'code': 'CKRG'
    }
]

# Hack the CSS to hide the delta icon
st.write(
    """
    <style>
    [data-testid="stMetricDelta"] svg {
        display: none;
    }
    </style>
    """,
    unsafe_allow_html=True,
)

# Title
st.title(":blue[IDS] Commute 🚌")

# Acknowledgement
st.write(":green[[Acknowledgement]] I would like to thank [NextBus](https://nnextbus.nus.edu.sg), [LTA](https://datamall2.mytransport.sg), and [SMRT](https://trainarrivalweb.smrt.com.sg/) for providing data, albeit perhaps unintentionally. If you plan to use their data as well, please be considerate with your network traffic to avoid disrupting their services.")

# Layout
coloumns = [2,1,1,1,1,1,1,1,1,1,1]
number_of_coloumns = len(coloumns)

# NUS Bus
for stop_info in nus_bus_stops:
    shuttle_info = get_nus_bus_arrival(stop_info['name'])
    
    buses = list()
    
    for shuttle in shuttle_info:
        if "_etas" not in shuttle: continue
        for bus in shuttle["_etas"]:
            plate = bus["plate"]
            eta = bus["eta"]
            shuttle_name = shuttle['name']
            buses.append({
                "shuttle_name": shuttle_name,
                "plate": plate,
                "eta": eta
            })
    
    buses.sort(key=lambda x: x["eta"])
    
    with st.container(border=True):
        cols = st.columns(coloumns)
        cols[0].metric("NUS Stop", stop_info['name'])
        
        for i, bus in enumerate(buses[:number_of_coloumns-1]):
            cols[i+1].metric(bus['plate'], bus["shuttle_name"], str(bus["eta"]))
            
# Public Bus
for stop_info in public_bus_stops:
    bus_info = get_lta_bus_arrival(stop_info['code'])
    buses = list()
    
    for shuttle in bus_info["Services"]:
        try:
            service_no = shuttle["ServiceNo"]
            for bus_seq in ['NextBus', 'NextBus2', 'NextBus3']:
                bus_type = shuttle[bus_seq]['Type']
                bus_load = shuttle[bus_seq]['Load']
                arrival_time = datetime.fromisoformat(shuttle[bus_seq]['EstimatedArrival'])
                now = datetime.now(arrival_time.tzinfo)
                time_diff = arrival_time - now
                eta = int(time_diff.total_seconds() / 60)
                
                buses.append({
                    "service": service_no,
                    "eta": eta,
                    "type": f'{bus_type} - {bus_load}'
                })
        except Exception as e:
            pass
    
    buses.sort(key=lambda x: x["eta"])
    
    with st.container(border=True):
        cols = st.columns(coloumns)
        cols[0].metric("Public Stop", stop_info['name'])
        
        for i, bus in enumerate(buses[:number_of_coloumns-1]):
            cols[i+1].metric(bus["type"], bus['service'], bus["eta"])

# SMRT Train
for station in public_mrt_stations:
    smrt_data = get_smrt_train_arrival(station['code'])
    trains = list()
    
    for train in smrt_data:
        trains.append({
            "direction": train["direction"],
            "eta": int(train["eta"])
        })
        
    trains.sort(key=lambda x: x["eta"])
    
with st.container(border=True):
    cols = st.columns(coloumns)
    cols[0].metric("MRT Station", station['name'])
    
    for i, train in enumerate(trains[:number_of_coloumns-1]):
        cols[i+1].metric(train['direction'], 'CC', str(train['eta']))