Good day in sunny Singapore!
Today we will embark on a little project I kept shelving for the past couple years - the Bus Arrival app! It's not a
new concept by any means - there are already plenty of apps out there that do the exact same thing... but hey, why use someone else's app and put up with their crappy ads when you can just make your own, eh?
|
Bus Stop Code. |
So here's a bit of context. This is Singapore, and every bus stop has a number.
Some bus stops have this nifty feature where they show you how many minutes it will take for your bus to arrive at the stop. But not
every stop has it, and thus we use an app to check. Singapore's Land Transport Authority released their API
back in 2011, and we will leverage upon this data. Before we begin, you will need to register for an API key.
What's going to happen after this, is that we write some HTML code that will display the results, and some PHP code to grab the results.
Let's kick this off!
Here's some HTML code. Bear in mind thiis code is not meant to be
pretty. If you want pretty, manage your own aesthetics. The body is basically set to a
white background and a bit of font styling in the CSS, for now. In the body, there is a div with id
container that houses two other divs -
stop and
bus.
<!DOCTYPE html>
<html>
<head>
<title>Singapore Bus Arrival</title>
<style>
body
{
background-color: rgb(255, 255, 255);
font-family: sans-serif;
font-size: 16px;
}
</style>
<script>
</script>
</head>
<body>
<div id="container">
<div id="stop">
</div>
<br/>
<div id="bus">
</div>
<br/>
</div>
</body>
</html>
In the
stop div, we add a header tag and an emoji.
<div id="stop">
<h1>🚏 BUS STOP</h1>
</div>
Then we have a form.
<div id="stop">
<h1>🚏 BUS STOP</h1>
<form method="POST">
</form>
</div>
In there, we have an input field that will accept only numbers. We manage this by setting the
type attribute to
number. For its
name attribute, we use
txtStop.
<div id="stop">
<h1>🚏 BUS STOP</h1>
<form method="POST">
<input type="number" name="txtStop" placeholder="e.g, 9810007" />
<button name="btnFindStop">FIND THIS STOP</button>
</form>
</div>
Bare bones look... it'll get better.
The second div,
bus, now should also have a header tag with an emoji.
<div id="bus">
<h1>🚌 BUS SERVICES</h1>
</div>
Let's style all this a little.
container will have rounded corners and a translucent
brown border, and we give it some generous padding.
body
{
background-color: rgb(255, 255, 255);
font-family: sans-serif;
font-size: 16px;
}
#container
{
border-radius: 20px;
border: 3px solid rgba(100, 0, 0, 0.8);
padding: 2em;
}
The divs within container will also boast round corners with translucent
brown borders, a less generous padding, and
black text.
#container
{
border-radius: 20px;
border: 3px solid rgba(100, 0, 0, 0.8);
padding: 2em;
}
#container div
{
border-radius: 20px;
border: 3px solid rgba(100, 0, 0, 0.2);
padding: 0.5em;
color: rgb(0, 0, 0);
}
And then we have stylings for the form inputs as well, but all this is really aesthetics and should make very little difference to functionality. Do as you will.
#container div
{
border-radius: 20px;
border: 3px solid rgba(100, 0, 0, 0.2);
padding: 0.5em;
color: rgb(0, 0, 0);
}
#stop input
{
border-radius: 5px;
padding: 5px;
width: 10em;
height: 1em;
}
#stop button
{
background-color: rgb(50, 0, 0);
color: rgb(255, 255, 255);
border-radius: 5px;
padding: 5px;
width: 10em;
}
#stop button:hover
{
background-color: rgb(50, 50, 0);
}
This is what it looks like so far.
Now, we will make the form work, using some PHP code. We start by declaring
buses as an empty array, and
busStop as an empty string.
<?php
$buses = [];
$busStop = "";
?>
<!DOCTYPE html>
<html>
The rest of the code should only work if the form has been submitted. Thus, we use an
If block to use the
isset() function and check if there exists a
POST variable called
btnFindStop, which is the name of the button in the form.
<?php
$buses = [];
$busStop = "";
if (isset($_POST["btnFindStop"]))
{
}
?>
<!DOCTYPE html>
<html>
We set the value of
busStop to the
POSTed value of
txtStop. Then we prepare to use cURL by running the
curl_init() function, and assigning the result to the object
curl.
if (isset($_POST["btnFindStop"]))
{
$busStop = $_POST["txtStop"];
$curl = curl_init();
}
We next run the
curl_setopt_array() function, passing in the
curl object as the first argument...
if (isset($_POST["btnFindStop"]))
{
$busStop = $_POST["txtStop"];
$curl = curl_init();
curl_setopt_array(
$curl,
);
}
... and an entire array of options as the next argument. Note here that this is a
GET request, and the URL has been set to the DataMall endpoint, with the
BusStopCode parameter value being
busStop.
if (isset($_POST["btnFindStop"]))
{
$busStop = $_POST["txtStop"];
$curl = curl_init();
curl_setopt_array(
$curl,
[
CURLOPT_URL => "http://datamall2.mytransport.sg/ltaodataservice/BusArrivalv2?BusStopCode=" . $busStop,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_POSTFIELDS => "",
]
);
}
We'll also need to pass in the API key like this. Replace "xxx" with the value of your own key. The
content-type property is JSON, and that's what we will parse the result as.
if (isset($_POST["btnFindStop"]))
{
$busStop = $_POST["txtStop"];
$curl = curl_init();
curl_setopt_array(
$curl,
[
CURLOPT_URL => "http://datamall2.mytransport.sg/ltaodataservice/BusArrivalv2?BusStopCode=" . $busStop,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_POSTFIELDS => "",
CURLOPT_HTTPHEADER =>
[
"Content-Type: application/json",
"accountKey: xxx"
],
]
);
}
Run the request using
curl_exec() with
curl as the argument, and assign the returned value to
response. Make a provision for errors as well, using
curl_error(), and assigning the returned value to
err. Then, as a matter of neatness, use
curl_close() on curl.
if (isset($_POST["btnFindStop"]))
{
$busStop = $_POST["txtStop"];
$curl = curl_init();
curl_setopt_array(
$curl,
[
CURLOPT_URL => "http://datamall2.mytransport.sg/ltaodataservice/BusArrivalv2?BusStopCode=" . $busStop,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_POSTFIELDS => "",
CURLOPT_HTTPHEADER =>
[
"Content-Type: application/json",
"accountKey: xx"
],
]
);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
}
If
err exists, then there is an error and we want to display it on screen. If not...
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err)
{
echo "cURL Error #:" . $err;
}
else
{
}
...declare
obj and set its value by running
json_decode() with response as an argument. Because
response will be in a JSON string. Remember
buses, the array? Well,
buses will be the
Services array within
obj.
if ($err)
{
echo "cURL Error #:" . $err;
}
else
{
$obj = json_decode($response);
$buses = $obj->Services;
}
Nothing's going to happen yet. Not until we make a few changes to the HTML. We want
busStop to be reflected in the display. Thus, if no bus stop has been searched for yet, the number just isn't going to show.
<div id="stop">
<h1>🚏 BUS STOP <?php echo $busStop;?></h1>
<form method="POST">
<input type="number" name="txtStop" placeholder="e.g, 9810007" />
<button name="btnFindStop">FIND THIS STOP</button>
</form>
</div>
For the
bus div, set it to not display if the length of
buses is 0.
<div id="bus" style="display:<?php echo (count($buses) == 0 ? "none" : "block");?>">
<h1>🚌 BUS SERVICES</h1>
</div>
Then use a
Foreach loop to iterate through
buses.
<div id="bus" style="display:<?php echo (count($buses) == 0 ? "none" : "block");?>">
<h1>🚌 BUS SERVICES</h1>
<?php
foreach($buses as $bus)
{
?>
<?php
}
?>
</div>
In here, you want a button to appear for each bus service. Display the
ServiceNo property of each element in each button. Thus, each bus service will have its own button, labelled.
<div id="bus" style="display:<?php echo (count($buses) == 0 ? "none" : "block");?>">
<h1>🚌 BUS SERVICES</h1>
<?php
foreach($buses as $bus)
{
?>
<button>
<?php
echo $bus->ServiceNo;
?>
</button>
<?php
}
?>
</div>
Let's style this a bit. I'll have each button
brown with a translucent
black border and
white text. All up to you; ultimately the functionality is not affected one way or another.
#stop button:hover
{
background-color: rgb(50, 50, 0);
}
#bus button
{
background-color: rgb(50, 0, 0);
color: rgb(255, 255, 255);
border-radius: 5px;
border: 3px solid rgba(0, 0, 0, 0.5);
padding: 5px;
width: 5em;
font-size: 20px;
font-weight: bold;
} Now try searching for a nonsense number. Try, "00000". No buses should appear.
But what if you searched for something that actually exists, such as "11111"? You'll see services 153, 165, 174, 186, 48, 855, 93 and 961!
Next we will add more divs after the bus div. Again, iterate through
buses using a
Foreach loop. In the loop, have a div with a CSS class of
arrival.
<div id="bus" style="display:<?php echo (count($buses) == 0 ? "none" : "block");?>">
<h1>🚌 BUS SERVICES</h1>
<?php
foreach($buses as $bus)
{
?>
<button>
<?php
echo $bus->ServiceNo;
?>
</button>
<?php
}
?>
</div>
<br />
<?php
foreach($buses as $bus)
{
?>
<div class="arrival">
</div>
<?php
}
?>
Inside each div, you should have another emoji with the bus service number.
<?php
foreach($buses as $bus)
{
?>
<div class="arrival">
<h1>🕐 BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>
</div>
<?php
}
?>
If the
NextBus object of the current element exists, display its
EstimatedArrival property.
<?php
foreach($buses as $bus)
{
?>
<div class="arrival">
<h1>🕐 BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>
<?php
if ($bus->NextBus)
{
echo "<h2>" . $bus->NextBus->EstimatedArrival . "</h2>";
}
?>
</div>
<?php
}
?>
Do the same for
NextBus2 and
NextBus3.
<?php
if ($bus->NextBus)
{
echo "<h2>" . $bus->NextBus->EstimatedArrival . "</h2>";
}
if ($bus->NextBus2)
{
echo "<h2>" . $bus->NextBus2->EstimatedArrival . "</h2>";
}
if ($bus->NextBus3)
{
echo "<h2>" . $bus->NextBus3->EstimatedArrival . "</h2>";
}
?>
Let's see how this looks. You should see 8 new duvs appear, each one bearing a few bus arrival timings!
Unfortunately, these timings are not
quite human-friendly. Let's fix that. Use the
formatArrivalTime() function on these values.
<?php
if ($bus->NextBus)
{
echo "<h2>" . formatArrivalTime($bus->NextBus->EstimatedArrival) . "</h2>";
}
if ($bus->NextBus2)
{
echo "<h2>" . formatArrivalTime($bus->NextBus2->EstimatedArrival) . "</h2>";
}
if ($bus->NextBus3)
{
echo "<h2>" . formatArrivalTime($bus->NextBus3->EstimatedArrival) . "</h2>";
}
?>
Now at the top of the PHP file, create this function. It has a parameter,
strTime.
else
{
$obj = json_decode($response);
$buses = $obj->Services;
}
}
function formatArrivalTime($strTime)
{
}
?>
Define
newStr, and use it to store the value of
strTime which has the "+8:00" removed using the
str_replace() function. Use the
str_replace() function on
newStr next, this time to replace the "T" with a space.
function formatArrivalTime($strTime)
{
$newStr = str_replace("+08:00", "", $strTime);
$newStr = str_replace("T", " ", $newStr);
}
Then use the
date() function to return only the time. For this, you'll also need to convert
newStr to a time object using the
strtotime() function. Here's some reference on
PHP datetime functions.
function formatArrivalTime($strTime)
{
$newStr = str_replace("+08:00", "", $strTime);
$newStr = str_replace("T", " ", $newStr);
return date("h:i A", strtotime($newStr));
}
Try it!
Next, we don't want to show all the data at one go. It's a lot of scrolling potentially, and that is
murder on a small screen. So do this, to hide the div by default.
<div class="arrival" style="display:none">
<h1>🕐 BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>
And then give each div a unique id based on the bus service number.
<div id="arrival_<?php echo $bus->ServiceNo; ?>" class="arrival" style="display:none">
<h1>🕐 BUS <?php echo $bus->ServiceNo; ?> ARRIVAL TIMINGS</h1>
For the bus div, each button should run the JavaScript function
showArrivalFor(), with the service number passed in as an argument.
<div id="bus" style="display:<?php echo (count($buses) == 0 ? "none" : "block");?>">
<h1>🚌 BUS SERVICES</h1>
<?php
foreach($buses as $bus)
{
?>
<button onclick="showArrivalFor('<?php echo $bus->ServiceNo; ?>');">
<?php
echo $bus->ServiceNo;
?>
</button>
<?php
}
?>
</div>
That function does not exist yet; we will create it.
<script>
function showArrivalFor($bus)
{
}
</script>
We begin by declaring
hide as an array of all divs styled using CSS class
arrival. (Bet you were wondering if we were ever going to use that, eh?)
<script>
function showArrivalFor(bus)
{
var hide = document.getElementsByClassName("arrival");
}
</script>
Then we use a
For loop to set all of their
display properties to
none.
<script>
function showArrivalFor(bus)
{
var hide = document.getElementsByClassName("arrival");
for (var i = 0; i < hide.length; i++)
{
hide[i].style.display = "none";
}
}
</script>
Finally, just grab the div that has the info you
do want to show, using the value of
bus. And set its
display property to
block.
<script>
function showArrivalFor(bus)
{
var hide = document.getElementsByClassName("arrival");
for (var i = 0; i < hide.length; i++)
{
hide[i].style.display = "none";
}
var show = document.getElementById("arrival_" + bus);
show.style.display = "block";
}
</script>
There should be no arrival timings when you refresh. But if you click on 48, Bus Service 48's srriving timings show.
If you click on 165, then only Bus Service 165's arrival timings show!
And that, geeks of all ages, is how you make a Singapore Bus Arrival app.
This code drives me wild (heh heh),
T___T