Saturday 23 March 2024

Web Tutorial: Bus Arrival App

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>&#128655; BUS STOP</h1>
</div>


Then we have a form.
<div id="stop">
    <h1>&#128655; 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>&#128655; 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>&#128652; 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>&#128655; 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>&#128652; 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>&#128652; 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>&#128652; 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>&#128652; 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>&#128336; 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>&#128336; 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>&#128336; 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>&#128336; 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>&#128652; 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

No comments:

Post a Comment