Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

Wednesday, May 20, 2020

Old Phone/Tablet as an Info Board: Update 3 - The elusive sunrises and sunsets

While I was messing up with the infoboard's bus widget, I also decided to touch up on the weather forecast (see the detailed description in the earlier post) and and add a few more visual cues to it.

I wanted to distinguish day and night, so that it becomes immediately clear how the weather behaves at sunset and during the morning commute, kind of like so:



My first attempt was very simple: I added a function eachTime() that did this:

function eachTime(){
 var str = $(this).text();
 var hhmm = str.split(":");
 var sunrise = 6; var sunset = 22;
 if (hhmm[0]>=sunset || hhmm[0] < sunrise) {
  $(this).css("background","rgb(0,0,0)");
  $(this).css("color","rgb(255,255,255)");
}}

along with a single line in forecast():
$("#rowTime").find("td").each(eachTime);

However, this seemed a bit artificial to declare night as "anything from 22:00 till 6:00", so I wanted actual sunrise/sunset times to be calculated. Although, with my degree of precision of 1 hour, this seemed like a relatively simple mathematical task, there was no need to reinvent the wheel since PHP already has date_sunrise() and date_sunset() functions. So I geared up a new PHP script as follows:
<?php
$remote_dtz = new DateTimeZone('US/Eastern');
$remote_dt = new DateTime("now", $remote_dtz);
$offset = ($remote_dtz->getOffset($remote_dt))/3600;
$lat=43.6;$lon=-79.6;
echo '<html><head></head><body>';
echo '<span id="sunrise">';
echo(ceil(date_sunrise(time(),SUNFUNCS_RET_DOUBLE,$lat,$lon,90,$offset)));
echo '</span>';
echo(':'); echo '<span id="sunset">';
echo(ceil(date_sunset(time(),SUNFUNCS_RET_DOUBLE,$lat,$lon,90,$offset)));
echo '</span>';echo '</body></html>';
?> 

The only trick here is to get the correct GMT offset; I am also rounding up the result to the nearest hour. It is then very easy to load this script in another hidden iframe like so:
<iframe id="astronomy" style="visibility: hidden; height: 0px !important;" src="astro.php"> </iframe>

and amend eachTime() as follows:

function eachTime(){
 var str = $(this).text();
 var hhmm = str.split(":");
 var dom=$("iframe#astronomy").contents();
  var sunrise = 6; var sunset = 22; //fallback
  sunrise = parseInt(dom.find("#sunrise").text());
  sunset = parseInt(dom.find("#sunset").text());
 var commute = 8;
 if (hhmm[0]>=sunset || hhmm[0] < sunrise) {
  $(this).css("background","rgb(0,0,0)");
  $(this).css("color","rgb(255,255,255)");
  if (hhmm[0]==commute){ $(this).css("color","rgb(225,255,0)");$(this).css("border","1px solid yellow");}
 }
 else{  if (hhmm[0]==commute) {$(this).css("border","1px solid black"); $(this).css("background","rgb(255, 213, 171)");}}  
}


There is no need to bother about refreshing astro.php because the entire system refreshes at 2 am every day anyway, and even if we happen to be 1 day off we are well within the desired accuracy margin. In addition I have added a cue mark for the morning commute, which is (unfortunately) independent of where the Sun happens to be. (And your mileage may vary here too, distinguishing weekdays from weekends and even accounting for statutory holidays if need be.)


BONUS: As an addition, notably after a strong wind storm in our area, I wanted to colour code wind as well, along with rain and temperature. Here's what I changed about eachWind():

function windgradient(base,gust){ 
var b=(base>=80)?1.0:(base/80.0);
var g=((gust-base)>=25)?1.0:((gust-base)/25.0);
if (g<0) g=0;
b=Math.pow(b,0.75);g=Math.pow(g,1.0);
    return "rgb(" 
  + Math.round(255.0) + "," 
  + Math.round(255.0*(1.0-b*g)) + "," 
  + Math.round(255.0*(1.0-b)) + ")";
}

function eachWind(){
 var rawstr = str = $(this).text().trim(); 
 var str = rawstr.split(String.fromCharCode(160));
 var result = "rgb(255,255,255)"; var speed = 0;
 try { speed = parseInt(str[1]);} catch(ignore){speed=0;}
 var gust=speed;
 if(rawstr.indexOf("gust")!=-1) {try { gust = parseInt(str[2]);} catch(ignore){gust=speed;}}
 result=windgradient(Math.round((speed+gust)/2.0),gust);
 $(this).css("background",result);
 $(this).text(rawstr.replace("gust",">"));
}


So the colour intensity and hue can independently give some information about how strong and how gusty the wind is, more or less like so (yes I did it on Wolfram Cloud for a quick illustration):



Thursday, May 14, 2020

Old Phone/Tablet as an Info Board: Update 1 - The sneaky Mississauga buses

Since I wrote my original series 2 years ago, my custom made info board has proved a (moderately) trusty friend and a time saver, especially on busy mornings. It does need a manual refresh every now and then (roughly once every 1-2 weeks), and the Playbook itself needs a reboot once every couple of months, but this is part of regular maintenance and hey, the thing is still running fine on a year-2011 Playbook and a 2013 DiskStation, which is, in and of itself, a proof that I did a decent job, and an occasion to celebrate - so cheers :)

However, nothing is forever, especially on the Internet. Over time, the board started losing components, which prompted more in-depth maintenance and code revision. So I am writing a series of update posts to list and describe that maintenance.

One of the components that bit the dust lately was the departure board for the buses at our nearby bus stop (see my earlier post on how I did it, I will be referencing it often.). Contrary to my fear that my brute-force string parsing of a JSON in PHP would give me debugging headaches, it actually proved remarkably robust. Something totally different happened: the query for the JSON was simply giving me Error 404. Apparently, the company just scrapped the service and seems to have migrated to a wholly different platform. Well, as I said, nothing on the Internet is forever, so I needed to re-implement the web scraping for the next bus departure times.

I will forgo the description of a 3-hour long troubleshooting spent in Chrome's web inspector feature (mostly because it happened too long ago for me to recover the details of it); suffice it to say that I ended up finding a new request in this format:
https://www.triplinx.ca/en/NextDeparture/NearByNextDeparture?stopId=(6-digit stop ID)
resulting in the following:


Sweet. A simple Inspect shows a clear document structure:


I wasn't inclined to modify my "bake the page directly in PHP" method, but I could now afford to parse the document somewhat more intelligently, like so:

function getElementsByClass(&$parentNode, $tagName, $className) {
    $nodes=array();
    $childNodeList = $parentNode->getElementsByTagName($tagName);
    for ($i = 0; $i < $childNodeList->length; $i++) {
        $temp = $childNodeList->item($i);
        if (stripos($temp->getAttribute('class'), $className) !== false) {
            $nodes[]=$temp;
        }
    }
    return $nodes;
}

$url = "https://www.triplinx.ca/en/NextDeparture/NearByNextDeparture?stopId=$stop";
$code = file_get_contents($url,false,$context); 

$routes=array(); $times=array();$rid=0;
$dom = new DOMDocument();
@$dom->loadHTML($code);
$list_node=$dom->getElementsByTagName("ul")->item(0);
$items=$list_node->getElementsByTagName("li");
for($i=0;$i<$items->length;$i++)
{
  $thisraw = $items->item($i)->nodeValue;
  // set route - we are in the outer LI
  $token1 = "Bus"; $pos1 = strpos($thisraw,$token1)+strlen($token1)+2;
  $token2 = "Show"; $pos2 = strpos($thisraw,$token2)-2;
  $rawroute = substr($thisraw,$pos1,$pos2-$pos1);
  if (strlen($rawroute) <= 5) 
  {$thisroute=$rawroute; }
  else // we are in the inner LI, so get time for the already set route
  {
 $thisattr=0; $thisattr += (strpos($thisraw,"real time")!==FALSE) ? 1 : 0; // real time attr
  $attr[] = $thisattr;    
  $diritem = getElementsByClass($items->item($i),"span","next-departure-label")[0];
  $direction = $diritem->nodeValue;
        $direction = str_replace("towards ","", $direction);
  $routes[] = $thisroute . substr($direction,1,1) . " ";
  $timitem = getElementsByClass($items->item($i),"span","next-departure-duration")[1];
        $thistime = $timitem->nodeValue;
        $thistime = str_replace("<", "",$thistime); // filter < 1 minute:: gone anyways 
        $thistime = str_replace("<", "",$thistime);
        $entity = htmlentities($thistime, null, 'utf-8');
        $thistime = str_replace(" ", " ", $entity); 
        $thistime = html_entity_decode($thistime);
 $times[] = $thistime;
  $rid++;
  }
}

where I borrowed the function getElementsByClass() from StackOverflow; the snippet aimed at getting rid of all &nbsp;'s was likewise borrowed from there.

As a result I was now getting an array of bus routes and bus times in string arrays $routes[] and $times[] respectively. However, I still needed to sort my buses by departure time rather than by route as in the screenshot above; the trick is that some of the times are given as "5 min" while some others are like "2:35 pm".

I got around the problem by determining the "number of minutes till all departures" and storing it in $nummin[], like so

date_default_timezone_set('US/Eastern');
$rawtime = new DateTime();
$rid = (($rid>6)?6:$rid);
for ($j=0; $j<$rid; $j++)
{
  if (strpos($times[$j],"min")!==false)
  {
   $nummin[$j] = intval(str_replace("min","",$times[$j]));
  }
  else
  {
   $temptime = new DateTime($times[$j]);
   $fulltime = getdate($temptime->getTimestamp());
   $temptime->setDate($fulltime['year'],$fulltime['mon'],$fulltime['mday']);
   $nummin[$j] = intval(($temptime->getTimestamp() - $rawtime->getTimestamp())/60);
   if($nummin[$j]<0)$nummin[$j]+=(24*60); // add a day if needed
  } 
}

and then using it to bubble sort all three arrays (6 elements aren't worth doing anything more sophisticated):

for ($i=0; $i<$rid; $i++)
 for ($j=0; $j<$i; $j++)
  if($nummin[$i]<$nummin[$j])
  {
   $temproute = $routes[$i]; $routes[$i] = $routes[$j]; $routes[$j] = $temproute;
   $temptime = $times[$i]; $times[$i] = $times[$j]; $times[$j] = $temptime;
   $tempmin = $nummin[$i]; $nummin[$i] = $nummin[$j]; $nummin[$j] = $tempmin;
  }

For output, we would need the reverse operation, i.e. converting "in 5 minutes" to a valid departure time. The reason is that we can't afford to pull the timetable every single minute, so labels like "in 5 min" would very soon mean "in 5 minutes, as of 3 minutes ago" which isn't very convenient to use. To get around this confusion, I have employed the following trick:

for ($j=0; $j<$rid; $j++)
 {
  if (strpos($times[$j],"min")!==false)
  {
   $live = $times[$j];
   $mins = intval(substr($live,0,strpos($live," min")));
   $newtime = (clone $rawtime);
   $newtime->modify("+{$mins} minutes");
   $bustime = date("H:i",$newtime->getTimeStamp());
   $times[$j] = $bustime . " (" . $live . ")";
  } 
 }
if (strlen($times[0])<4) $times[0]= $timestamp." (now)";

Note the line with $newtime = clone $rawtime. It is very important that $newtime is cloned, otherwise $newtime->modify(...) will modify $rawtime and our code will work, but will produce a wrong timetable! Also note that the last line is a patch to ensure that "<1 min" is captured as "now" regardless of possible parsing errors upstream (spoiler: there are errors upstream).


As an afterthought, as I have both the number of minutes and time for all departures, I decided to output them both, breaking up the elements and giving them distinct IDs for future access. Here's how:
$css= ' style="font-weight: bold; color:rgb(255,128,128);"';
for ($j=0; $j<$rid; $j++)
{
 if (strpos($times[$j],"(")!==false)
 {$times[$j] = str_replace('(','</span><span id="busreal'.$j.'" '.$css.'>(',$times[$j]); }
 else { $times[$j] =    $times[$j].'</span> <span id="busreal'.$j.'" '.$css.'>('.$nummin[$j].' min)';}
 if ($j==0) $css=str_replace(';"','; font-size: 14px;"',$css);
}
. . .
echo '<span id="bustoken" style="background: ',$color,'; color:white;">', " ".$routes[0], '</span>',  '<span id="bustime0" style="font-weight: bold; color:',$color,'">', " ", $times[0], '</span>', '  <span id="realtime" style="color: ',$color,'; ">[...]</span>';
if ($rid > 1)
{echo "<br/>";
 echo '<span style="color:rgb(255,235,235);">', $timestamp, ' </span>';
 for ($j=1; $j<$rid; $j++)
  {echo '<span id="busnum'.$j.'" style="background: rgb(255,128,128); color:white; font-size: 14px;">', " ".$routes[$j], '</span>',  '<span 
   id="bustime',$j,'"  style="font-weight: bold; color: rgb(255,128,128);font-size: 14px;">', " ", $times[$j], '</span>  ';
  };
} 

You can surely notice that the "breaking up" thing was really an afterthought and is not neat code at all. I will probably yell at myself for doing it this ugly when I decide to refactor this in another 2 years. :)


So, here is the final result:


And this is how it looks like in an embedded form:



What's with all the different colours, you ask, what does "leave in..." mean, and why is formatting so different? I bet you guessed it: the 16:31 bus is already too soon to catch, and the 16:33 can be caught just barely, if you leave now and make haste. In my next post, I am going to describe how I achieved this on the client side.

Monday, August 6, 2018

Old Phone/Tablet as an Info Board: Table of Contents

Hi folks, now that this long overdue series of posts is complete, here's a table of contents for the ease of the reading.



Enjoy!

TL/DR: This is a series of (moderately boring) posts about how to turn your old and outdated tablet or smartphone into an information board you can use at home. Uses range from purely practical to purely aesthetic.

Thursday, August 2, 2018

Old Phone/Tablet as an Info Board Part 5: PHP capture-and-process

Finally, I describe one more method of putting information on the info board, suitable when there is a very limited amount of information you need from the target page, and when your output will have very little, if anything, to do with the target page layout. It is also the only possible method when the server outputs data in any format other than HTML.

The example is our local bus company, where I would like to get the bus departure info for a certain stop. In the browser, the result looks like this:



Now the biggest difficulty about using the previously described methods is the lack of a clearly defined URL associated with the results page - the results are dynamically generated by the parent page's javascript. After some sniffing around, it was possible to extract a working request link in the format
http://www4.mississauga.ca/PlanATrip/NextPassingTimes/RequestNextPassingTimes ?suggestionInputIdentifier=(stop_number) &suggestionInputType=Stop &stopInputIdentifier= &mustBeAccessible=false

which brings up the result in this JSON-like format:



Well, because the departure information I am after is somewhere in this JSON, I decided to parse it entirely in PHP using preg_match_all, extracting route number and arrival time for the next upcoming bus, as well as three more after it, like so:

<?php
date_default_timezone_set('EST');
$opts = array('http'=>array('header' => "User-Agent:MyAgent/1.0\r\n"));
$context = stream_context_create($opts);
$url = "http://www4.mississauga.ca/PlanATrip/NextPassingTimes/RequestNextPassingTimes?suggestionInputIdentifier=1127&suggestionInputType=Stop&stopInputIdentifier=&mustBeAccessible=false";
$timestamp = date("H:i");
$code = file_get_contents($url,false,$context); 
 
$code = strstr($code, 'NextPassingTimesList',false);
$code = strstr($code, 'NextPassingTimesLegend',true);
// now code contains only trip data
$rid = preg_match_all("/data-route-key=([^=]+)data-trip-key/",$code,$matches1);
$tid = preg_match_all("/NextPassingTimesTime ([^N]+)u003c\/div/",$code,$matches2);
for($i=0;$i<$rid;$i++)
{
$route = str_replace("\\","",$matches1[1][$i]); $route = str_replace("\"","",$route); $route = str_replace("r","",$route);$route = str_replace("n","",$route);
$routes[$i] = str_replace("~~East"," E",$route);
 
$time = $matches2[1][$i];
$id1=stripos($time,"u003e"); 
$time = substr($time,$id1+5,strlen($time)-($id1+5)-1);
$sid = stripos($time,"u003c");
if ($sid!==false) {$time = substr($time, 0, $sid-1);}
$times[$i] = $time;
}
$color = (strpos($times[0],"min")===false)?"rgb(255,128,128)":"red";
 
echo '<html><head></head><body>';
echo '<div style="color:rgb(255,128,128); font-family: Arial, Helvetica, sans-serif; font-size: 20px; position: relative; top: -5px;">';
echo '<span style="color:gray;">', $timestamp, '&nbsp;</span>';
echo '<span id="bustoken" style="background: ',$color,'; color:white;">', " ".$routes[0], '</span>',  '<span style="font-weight: bold; color:',$color,'">', "&nbsp;", $times[0], '</span>';
if ($rid > 1 && $tid > 1)
{
    echo "<br/>";
    echo '<span style="color:rgb(255,235,235);">', $timestamp, '&nbsp;</span>';
    for ($j=1; $j<(($rid>4)?4:$rid); $j++)
    {
        echo '<span style="background: rgb(255,128,128); color:white; font-size: 14px;">', " ".$routes[$j], '</span>',  '<span style="font-weight: bold; color: rgb(255,128,128);font-size: 14px;">', "&nbsp;", $times[$j], '</span> &nbsp;';
 
    };
}
echo '</div></body></html>';
?> 


Putting it into our standard PHP iframe like this:
<div id="bus" class="basic info" style="position: absolute; left: 150px; bottom: 270px; width: 415px; height: 50px; background:rgb(255,235,235);">
<iframe id="miway" scrolling="no" src="bus.php" style="height: 100px; width: 415px; border: none;"> </iframe>
</div>


The result looks like:


Note the following tricks:
  • Whenever the real-time information is available ("the bus is leaving in XX minutes"), I am accenting the color to highlight it.
  • I am also outputting the time of the query, so that the "XX minutes" format remains meaningful without having to query every single minute.
  • The line with date_default_timezone_set serves to ensure correct DST for $timestamp.
  • As a future development, I plan to supplement the "XX minutes" format with actual projected time, replacing it with "HH:MM (XX min)" and retaining accentuation.
  • As another exercise, I plan to change color accentuation depending on the remaining time - highlighting buses that are still reachable given the walking/running distance to the stop, and dimming the buses that aren't.

I admit that this method, as implemented, is quick-and-dirty and extremely brute-force, and is rather vulnerable to the underlying data format changes. A much more robust way would be to have the PHP parse the JSON "the proper way" and either process it using dedicated JSON functions, or simply echo the HTML result in its entirety, using jQuery to interact with it. However, my approach has worked surprisingly well for over 6 months already - and "if it ain't broken, won't fix it".

Wednesday, August 1, 2018

Old Phone/Tablet as an Info Board Part 4: IFRAME with PHP capture-and-rearrange

As we can see from the previous post, the capture-and-restyle method is ill-suited for larger target pages from which you only need a limited portion of data, and/or when you want to rearrange that data significantly. The reason is that it will take an enormous amount of analysis and restyling to get things look the way you want - on par with the effort needed to design a web site yourself.

Here I describe another, complementary approach, which I dub capture and rearrange, that you can use in exactly the opposite scenario:

  • your target page is not very lightweight,
  • you only need a small portion of the target's contents,
  • you need to rearrange the layout significantly. 


As an example, we will use Environment Canada's hourly forecast page to display a limited portion of the page (the hourly forecast) in a totally different format - horizontal rather than vertical layout, a much more condensed presentation, and adding visual aids and highlighting according to the weather conditions. 

Or in an example of a picture that's worth a thousand words, we would like to make this

from this (never mind the difference in the actual content; you get the idea)



The workflow is as follows:
  1. Capture the page via PHP in the previously described way, like so:
    <?php
    $opts = array('http'=>array('header' => "User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.59.10 (KHTML, like Gecko) Version/5.1.9 Safari/534.59.10\r\n"));
    $context = stream_context_create($opts);
    $code = file_get_contents("http://weather.gc.ca/forecast/hourly/on-24_metric_e.html",false,$context); 
    $code = str_replace("/weathericons/small/","http://weather.gc.ca/weathericons/small/",$code);
    $code = str_replace("Medium","Med",$code);
     
    echo $code;
    ?>
    


    Note that we have redirected the images directly to the server to spare the effort of saving locally.
    However, in my particular case, I found out that the Playbook browser (unlike newer browsers) had trouble accessing the server version of the images. Well, if you have read the previous post, you know the workaround: just create a local copy of weathericons/small/ and do this in any bash compatible terminal (Mac/Linux/Cygwin) - do not forget to set the execute attribute on the DiskStation:
    for $i in $(seq 0 9); do wget https://weather.gc.ca/weathericons/small/0$i.png; done
    for $i in $(seq 10 99); do wget https://weather.gc.ca/weathericons/small/$i.png; done
    


  • Then define a placeholder interface for our forecast, displaying our PHP captured page in a hidden iframe:
    <div id="hourly" class="basic info" style="position: absolute; left: 10px; bottom: 10px; width: 990px; height: 160px; background: white;">
    <iframe id="forecast" style="visibility: hidden; height: 0px !important;" src="hourly.php"> </iframe>
    <table > <tbody>
    <tr id="rowTime" class="element">
    </tr>
    <tr id="rowTemp" class="element" style="font-weight: bold; font-size: 16px;">
    </tr>
    <tr id="rowIcon" class="element">
    </tr>
    <tr id="rowText" class="element" style="font-size: 7px;">
    </tr>
    <tr id="rowRain" class="element">
    </tr>
    <tr id="rowWind" class="element">
    </tr>
    <tr id="rowChill" class="element">
    </tr>
    </tbody></table>
    </div>
    
  • Inspect the HTML of our source page to uniquely identify elements you want to use in your widget. This is the trickiest but the most creative part - I will detail what I did in my example, but it will be entirely dependent on the source website. As on many other occasions, the Inspect Element functionality in your browser is the tool of choice here. Fortunately, most websites nowadays are designed in such a way that all their elements are addressable by some combination of their IDs, class names and styles, which is what we need.
  • Once the frame is loaded, and once we know which elements to look for, we move those elements from the hidden iframe to our placeholder using jQuery's capabilities:
    Like so:
    function forecast()
    {
      $(".element").empty();
      var iFrameDOM = $("iframe#forecast").contents().find("table.wxo-media");
      $("#rowTime").append(iFrameDOM.find("[headers='header1']"));
      $("#rowTemp").append(iFrameDOM.find("[headers='header2']"));
      $("#rowIcon").append(iFrameDOM.find("img.media-object")); $("#rowIcon").find("img.media-object").wrap("<td> </td>");
      $("#rowText").append(iFrameDOM.find("div.media-body")); $("#rowText").find("div.media-body").wrap("<td> </td>");
      $("#rowRain").append(iFrameDOM.find("[headers='header4']"));
      $("#rowWind").append(iFrameDOM.find("[headers='header5']"));
      $("#rowChill").append(iFrameDOM.find("[headers='header7']"));
      
      $("#rowTemp").find("td").each(eachTemp);
      $("#rowRain").find("td").each(eachRain);
      $("#rowWind").find("td").each(eachWind);
      $("#rowChill").find("td").each(eachChill);
    }
    


  • The final four lines in the above snippet introduce content-dependent formatting of the newly added elements - I want to be able to see if there's rain/wind/heat/freeze from across the room. For this, we write a few auxiliary functions.

    Temperature gradient - this is just a linear interpolation between colors:
    function gradient(deg)
    {
    var colors = [ 
      [-15, 255,0,255],
      [-10, 255,128,255],
      [0, 128,128,255],
      [12, 128,255,255],
      [17, 150,255,128],
      [25, 255,255,128],
      [30, 255,128,0],
      [35, 255,0,0]
       ];
    // determine extrema
    if (deg <= colors[0][0]) {return "rgb(" + colors[0][1] + "," + colors[0][2] + "," + colors[0][3] + ")";}
    if (deg >= colors[7][0]) {return "rgb(" + colors[7][1] + "," + colors[7][2] + "," + colors[7][3] + ")";}
    //otherwise, interpolate
    for(i=1;i<=7;i++)
    {
       if(deg > colors[i-1][0] && deg <= colors[i][0])
       {
         var p = (deg-colors[i-1][0]) / (colors[i][0]-colors[i-1][0]);
         return "rgb(" 
      + Math.round(colors[i][1]*p + colors[i-1][1]*(1.0-p)) + "," 
      + Math.round(colors[i][2]*p + colors[i-1][2]*(1.0-p)) + "," 
      + Math.round(colors[i][3]*p + colors[i-1][3]*(1.0-p)) + ")";
       };
    }
    //error
    return "rgb(255,255,0)"; 
    }
    function eachTemp(){$(this).css("background",gradient(parseInt($(this).text())));}
    

    Rain highlighting:
    function eachRain(){
     var str = $(this).text();
     var result = "rgb(255,255,255)";var result2 = "rgb(0,0,0)";
     if (str=="Low") {result = "rgb(225,255,255)";}
     if (str=="Med") {result = "rgb(0,255,255)";}
     if (str=="High") {result = "rgb(0,0,255)";result2 = "rgb(255,255,255)";}
     $(this).css("background",result);
     $(this).css("color",result2);
    }
    

    Wind highlighting:
    function eachWind(){
     var str = $(this).text().trim().split(String.fromCharCode(160));
     var result = "rgb(255,255,255)"; var speed = 0; 
     console.log(str);
     try { speed = parseInt(str[1]);} catch(ignore){speed=0;}
     if (speed >=20 ) {result = "rgb(255,255,128)";}
     if (speed >=40 ) {result = "rgb(255,255,0)";}
     if (speed >=60 ) {result = "rgb(255,0,0)";}
     $(this).css("background",result);
    }
    

    Wind chill highlighting - note that it will depend on other element's content (hence the need to look beyond $(this) and therefore pass the index parameter.)
    function eachChill(index){
     var str = $(this).text()
     var result = "rgb(255,255,255)"; var chill = 0; var temp=0; var diff=0;
     try { chill = parseInt(str); temp = parseInt($($("#rowTemp").find("td")[index]).text()); diff = chill-temp; } 
     catch(ignore){diff=0;}
     if (diff<= -5) {result = "rgb(128,128,255)";}
     if (diff >= 5 ) {result = "rgb(255,128,128)";}
     $(this).css("background",result);
    }
    

    The final result looks more or less like in the picture above. All that remains is to call forecast() at some point after the hidden iframe has finished loading. I'll describe this in more detail in the final post that has details on flow control (which seems simple, but ended up being rather sophisticated for the sake of usability).


    Note that this design is not completely fool-proof. Table columns will change with depending on the content, some of the info may be clipped in some rare cases, and I have not yet extended the functionality to include both wind chill and humidex. All of these fixes may be considered exercises for the reader. After all, as a father of two with hardly any extended family support I had to become a firm believer in the Pareto principle.

    Monday, June 4, 2018

    Old Phone/Tablet as an Info Board Part 3: IFRAME with PHP capture-and-restyle

    In an earlier post, we discussed that using iframe for an information widget on our screen is perhaps the most universal, but the least versatile because without the ability to interact with the content of the iframe, you cannot customize anything at all about the way your information is presented. So if you happen to need to reorganize your information to fit your needs, bad luck.

    Luckily I have a Synology DiskStation to host my screen, and it can function as a PHP web server. So the idea becomes to fetch the target web page by the server and echo it back to the client, using code like this :

    <?php
    $opts = array('http'=>array('header' => "User-Agent:MyAgent/1.0\r\n"));
    $context = stream_context_create($opts);
    $code = file_get_contents("http://your_URL_goes_here",false,$context); 
    echo $code;
    ?> 
    

    Here $opts and $context are needed to appear to the target host as a legitimate HTTP request so it won't respond with something like Error 403. Now, if you put the above into a file called, e.g., frame.php, you can then embed it into your interface using our old <iframe src="frame.php"...> tag.

    As an immediate benefit, PHP get_file_contents is not a browser, so it will retrieve only the basic HTML (hopefully containing the information you are after). This makes the resulting iframe a whole lot, big time easier on the client browser!

    On the flipside, this also means that you will need to do the styling and formatting yourself.

    The first method that we cover is something I called "capture-and-restyle" or "CSS injection". In a nutshell, the idea is to save a local copy of the target site resources (mainly, CSS styles and images), which you can then edit to suit your needs. This method is best suitable if:
    • Most of the target page contents will be used;
    • The target page, as formatted, looks more or less the way you want it to look on the board;
    • The target page is relatively simple - you don't want to sift through hundreds of styles manually.
    As an example, we use the GO transit (suburban commuter trains in the Toronto area) mobile departure board page:
    http://gotracker.ca/GoTracker/mobile/StationStatus/Service/01/Station/7

    It is already "kind of" optimized for viewing on small screens, so we will only need to make a few minor adjustments. So it makes sense to reuse as much of the original styling as we can.


    The workflow is as follows:
    1. Save local copies of styles and images that you want to display in your page. (The simplest way of doing this is to save the complete webpage in Chrome and look for items). Upload these on the DiskStation in a subfolder (e.g. go/) to the same folder where your index.html resides:
      Important! Make sure that you grant Execute permissions to the images! (To do this, right click the items in the File Station browser and choose Properties.)
    2. Add the following to the PHP script right before echo $code, redirecting requests to styles and images to local copies:
      $code = str_replace('/GOTracker/mobile/', 'go/', $code);
      $code = str_replace('/GoTracker/mobile/', 'go/', $code);
      $code = str_replace('../../../../', 'go/', $code);
      
      At this point, your local PHP, entered in your local browser (e.g. http://192.168.0.xxx/frame.php) should look (more or less) exactly as the target page typed in the same browser. If it does not, look for the missing elements / check permissions. Your web page inspector in Chrome or Safari is your friend here.
    3. Edit the locally saved styles/images to augment the way the widget looks. Basically, in this case, we want to hide the elements that are irrelevant to us, such as the logos; shrink the sign of the direction banners (they are obvious to us); and adjust the size of the information bearing elements so that they format nicely in a small-sized widget. I ended up adding a chunk of CSS code to the end of the stylesheet that loads last (GOGrid.css), more or less like so:
      * {border: none !important; font-family: Arial, Helvetica, sans-serif !important; font-stretch: semi-condensed;}
      #frontImg {visibility: hidden !important; height: 0px !important;}
      .imageButtonLink {visibility: hidden !important; height: 0px !important;}
       
      .sTbl {table-layout: fixed;}
      .SecondTitle > td:nth-child(1), .SecondTitle > td:nth-child(3) {width:0px ;}
      .SecondTitle > td:nth-child(2) { text-align: left !important; font-stretch: none; font-weight: bold;}
       
      .headerTR {visibility: hidden !important; height: 0px !important; font-size:0px;}
      .directionHeaderTH {font-size:6px ;}
      .bottomDoubleRowTR * {font-weight: normal; font-size: 10px;}
      .oddRowTR:nth-child(even) {background:rgb(225,255,225);}
      .feedbackLink {visibility: hidden !important; height: 0px !important; font-size:0px;}
       
      .currentDateMain {position: fixed !important; visibility: visible !important; top: 5px !important; right: 0px;}
      #lblCurrentDateMain {color: rgb(128,150,128); text-shadow: none !important; } 
      #lblCurrentTimeMain {color: rgb(0,128,0); font-weight: bold; text-shadow: none !important; }
      

      (Note the line with nth-child(even): this introduces the striped table style for ease of readability. Apparently this was originally in mind of the website programmers, since the class name, .oddRowTR, kind of suggests that there should also be .evenRowTR with different styling; however in practice all rows are of the .oddRowTR class, so, well, we fixed this.:)
    4. Done - it ended up looking like so:



    So here is the final version of the PHP
    <?php
    $opts = array('http'=>array('header' => "User-Agent:MyAgent/1.0\r\n"));
    $context = stream_context_create($opts);
    $line=$_GET["line"];
    $stn=$_GET["station"];
    $code = file_get_contents("http://gotracker.ca/GoTracker/mobile/StationStatus/Service/".$line."/Station/".$stn,false,$context); 
    $code = str_replace('/GOTracker/mobile/', 'go/', $code);
    $code = str_replace('/GoTracker/mobile/', 'go/', $code);
    $code = str_replace('../../../../', 'go/', $code);
     
    $code = str_replace('Union Station', 'Union Stn', $code);
    $code = str_replace('Clarkson GO', 'Clarkson', $code);
    $code = str_replace('Erindale GO', 'Erindale', $code);
    $code = str_replace('On Time', 'OK', $code);
     
    echo $code;
    ?> 
    
    Notice that I added some parametric functionality to reuse the same PHP for two stations as in the original layout. The last four str_replace are for cosmetic purposes - to make the individual departure lines fit on a single line as often as possible. There will still be occasional ugly misses, but (1) they can be corrected upon discovery, and (2) nothing is perfect, so why bother.

    and final HTML
    <div id="train1" class="basic" style="position: absolute; left: 150px; top: 10px; width: 200px; height: 195px; ">
    <iframe id="go1" scrolling="no" src="go.php?line=21&station=554" style="height: 190px; width: 200px; border: none;"> </iframe>
    </div>
    <div id="train2" class="basic" style="position: absolute; left: 365px; top: 10px; width: 200px; height: 195px; ">
    <iframe id="go2" scrolling="no" src="go.php?line=01&station=7" style="height: 190px; width: 200px;  border: none;"> </iframe>
    </div>
    

    As another big benefit, the source of the iframe now has the same origin as your main page. This means that you are now fully in control of its contents, which can now be accessed and manipulated from the script. We will make extensive use of this feature in the next post where we describe another PHP+iframe combo to make our hourly weather forecast widget. Here, we limit the use of this feature to a simple example: dim the widget if it has no relevant information. Let us do it like this:
    <div id="mask1" class="basic" style="opacity: 0.75; visibility: hidden; position: absolute; left: 150px; top: 10px; width: 200px; height: 195px; "></div>
    <div id="mask2" class="basic" style="opacity: 0.75; visibility: hidden; position: absolute; left: 365px; top: 10px; width: 200px; height: 195px; "></div>
    
    function trainhide()
    {
    var dom = $("iframe#go1").contents().find(".oddRowTR");
    $("#mask1").css("visibility",(dom.size()==0)?"visible":"hidden");
    dom = $("iframe#go2").contents().find(".oddRowTR");
    $("#mask2").css("visibility",(dom.size()==0)?"visible":"hidden");
    }
    

    Interlude: I apologize that it takes me so long to write up this series of posts; however the need to meticulously sort though all the necessary snippets and screenshots is taking more time than I previously thought. Bear with me - there are "only" 3 posts left. In the meantime, the info board continues to work - for nearly 6 months already.