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.

    No comments:

    Post a Comment