Tuesday, August 7, 2018

Laurel / Yanny Hands-On

Ever since the Laurel/Yanny auditory illusion went viral, I had a suspicion that we are dealing with a bifurcation-type illusion similar to the "figure or ground" type:


That guess seemed correct with the publishing of a NY Times article where you can "move a slider" to augment the clip in either direction, causing either "Laurel" or "Yanny" to be more pronounced.

I admit that this article is quite revealing and educating (it taught me how I can tweak my brain into hearing one or the other). But moving a slider still seems rather artificial. For all I know they could have been cheating, actually having two separate recordings and the slider mixing them in different proportions.

Can we prove this wasn't cheating? Hands on?

Yes we can.

Some snooping around reveals that here is a working version of the original recording:
https://ia802800.us.archive.org/28/items/YannyVsLaurelVideoWhichNameDoYouHear-Audio/Yanny%20vs%20Laurel%20video%20which%20name%20do%20you%20hear%20%E2%80%93%20audio.mp3

Let's go to Wolfram Cloud Computing and run this simple program:

url="(the url above)"
audio=AudioTrim[Audio[url],{0,4}]
CloudExport[AudioPitchShift[audio,1.1],"wav"] (*Laurel*)
CloudExport[AudioPitchShift[audio,0.85],"wav"] (*Yanny*)

We see that at the heart of it is AudioPitchShift which simply shifts the pitch of the recording by the desired amount. Looks like lower frequencies emphasize "Yanny" while higher frequencies bring out "Laurel", in agreement with the above mentioned Wikipedia article.

My conjecture, based on the observation that "Laurel" is lower-frequency than "Yanny", is that we tend to hear whatever is closer to the maximum frequency sensitivity of our ears, so whoever initially hears "Yanny" likely has an ear for higher-pitched tunes than the "Laurel" guy.



It could have been a bit easier if Wolfram Cloud could actually play audio from within the interface without resorting to CloudExport. Still, it opens nearly endless possibilities for experiments. Enjoy!

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.

Old Phone/Tablet as an Info Board Final Part: Flow Control and Asynchronous Dynamic Data Refresh

This is the final post about how to make an information board out of your old tablet or smartphone.

Let's assume you have all your elements that deliver your information as you want it, beautifully designed and tested, again using my board as an example:




OK, the information is up to date when the page has loaded. Now, there comes a question: how to keep all this information reasonably up to date, ready for you at any moment in case you need it? On-demand refresh of any kind is not an option - an info board you'd have to stand in front of, waiting, is a lousy info board.

It would seem easy at first - "just do a META REFRESH", - but there are several pitfalls to think through:

  1. It is obvious that you need to refresh all the elements with some frequency. But how to choose this frequency? Too seldom, your info is out of date half the time. Too frequently, and your board will spend more time refreshing than actually showing the info, and you risk being kicked out by your data sources for DDOS'ing them. 
  2. Further elaborating on the idea, it is clear that some elements need a more frequent refresh than some others: you are quite OK with a weather forecast obtained half an hour ago, but train departures from half an hour ago are totally useless to you. 
  3. Furthermore, the refresh frequency should be time dependent. You don't need frequent traffic updates in the middle of the night, but you do need them during commute hours.
  4. What to do if an element fails to refresh, or gets stuck? Should we perhaps retry sooner than its normal refresh cycle? Then again, what would be the reasonable timeout? Set it too short and you'll mistake normal wait times for "getting stuck".
What it all means that you will need to refresh asynchronously (i.e. some elements but not others), and dynamically (using time intervals that are element and time dependent), and you'll have to have a mechanism to check if the refresh was actually successful or needs to be retried. Also note that some elements (notably those that use jQuery on a hidden iframe) will need a two-step refresh: some method to reload the iframe, and another to process its contents once it has loaded and rendered in DOM (but no sooner).



So, let us begin by introducing a "refresh handler" like so:

var dummydate = new Date();
var rExample = {freq: 30, // refresh every 30 minutes by default
    prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}, // between 7:30 and 8:30 refresh every 5 minutes
            {freq: 15, start:{h:15, m:30}, end:{h:20, m:30}}], // between 15:30 and 20:30 refresh every 15 minutes
    last: dummydate, next: dummydate, fresh: true,  overdue: 0, //internal variables
    lag: 5, retry: 45, //internal constants
    handleRefresh: function(){...}, // refresh function 
    handleReady: function(){...}, // "is ready" function
    handlePost: function(){...} // post-refresh function for 2-stage refresh
    };  


We see that this handler is a self-contained object that handles all refresh code for "something on the board". Then, given an array of these handlers, we define an event poller to be called periodically:

const scale = 1; //seconds per minute: 1 for debugging; 60 for actual use
var REFRESH = [rWeather, rTrain1, rTrain2, rBus, rHourly, rMap, rTravel];

function pollEvent()
{
var now= new Date();
// add force refresh @ 2AM, ONCE

for (var i=0, len=REFRESH.length; i<len; i++)
{
 var handle = REFRESH[i];
 if(now > handle.next) {
 try{
  if (handle.handleReady()) {
    handle.overdue=0;
 var behind = parseInt((now.getTime() - handle.last.getTime())/1000/scale);
 $("#ref"+(i+1)).text(behind);$("#ref"+(i+1)).css("color","green");
  }
  else { console.log(i+" not ready")
        if (handle.overdue > 5) {console.log("***FORCE REFRESH***"); /*location.reload();*/};
  handle.overdue++;
  $("#ref"+(i+1)).text("x");$("#ref"+(i+1)).css("color","darkred");
  };
  
   console.log("refresh "+i); 
   handle.handleRefresh(); handle.fresh = true;
   postprocess(handle);
   handle.last = now;
   handle.next = new Date(now.getTime() + 1000*((handle.overdue==0)?handle.retry:scale*timespan(handle,now)));
   } 
catch(ignore) {handle.overdue++; $("#ref"+(i+1)).text("X");$("#ref"+(i+1)).css("color","red"); handle.next = new Date(now.getTime() + 1000*handle.retry);} 
 }
}
setTimeout(pollEvent,100*scale); // call again, every 6 seconds in production 
};


Here timespan() defines the refresh frequency for a given handler at a given time:


 function timespan(handle,stamp)
{
 var ts = handle.freq;
 for (var j=0; j<handle.prime.length; j++)
 {
  var thisprime = handle.prime[j];
  if (stamp.getHours()>= thisprime.start.h && stamp.getHours()<= thisprime.end.h 
  && ( stamp.getHours()>thisprime.start.h || 
     (stamp.getHours() == thisprime.start.h && stamp.getMinutes()>= thisprime.start.m))
  && ( stamp.getHours()<thisprime.end.h || 
     (stamp.getHours() == thisprime.end.h && stamp.getMinutes()<= thisprime.end.m))
   ) {ts = thisprime.freq;}
 }
 return ts;
}


Now define another function to ensure two-step refresh happens as fast as possible:


function postprocess(handle){ 
 try{
  if(handle.handleReady) {console.log("++"); handle.handlePost();}
  else {console.log("--");setTimeout(function(){postprocess(handle);}, 1000*((handle.fresh)?1:handle.lag)); handle.fresh=false; };
}
 catch(ignore) {}
}


It only remains to define some auxiliary routines


function frameload(frameid){$(frameid).attr("src",$(frameid).attr("src"));}
function nop(){return true;}
function clock(){
var now = new Date(); var h=now.getHours(); var m=now.getMinutes(); var s = now.getSeconds();
$("#nowclock").text(((h>=10)?h:("0"+h)) + ":" + ((m>=10)?m:("0"+m)) + ":" + ((s>=10)?s:("0"+s)));} 


and add an initial invocation upon document's DOM ready:


$( document ).ready(function() 
{
var now = new Date(); var h=now.getHours(); var m=now.getMinutes();
$("#loadclock").text(((h>=10)?h:("0"+h)) + ":" + ((m>=10)?m:("0"+m)));
setInterval(clock, 1000);
setTimeout(pollEvent,3000);
});



That's all. For reference here are the refresh handlers for all the elements:


 var rWeather = {freq: 30, 
 prime: [{freq: 15, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 15, start:{h:15, m:30}, end:{h:20, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 5, retry: 45,
 handleRefresh: function(){f(document, 'script', 'plmxbtn');},
 handleReady: function(){return $("#weather").find(".city").length>0;},
 handlePost:nop };
 
 var rTrain1 = {freq: 120, 
 prime: [{freq: 10, start:{h:5, m:30}, end:{h:9, m:0}}, 
   {freq: 2, start:{h:7, m:45}, end:{h:8, m:20}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 1, retry: 10,
 handleRefresh: function(){frameload("#go1");},
 handleReady: function(){return $("iframe#go1").contents().find(".currentDateMain").length>0;},
 handlePost:trainhide };
 
 var rTrain2 = {freq: 20, 
 prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 1, retry: 10,
 handleRefresh: function(){frameload("#go2");},
 handleReady: function(){return $("iframe#go2").contents().find(".currentDateMain").length>0;},
 handlePost:trainhide };
 
 var rBus = {freq: 20, 
 prime: [{freq: 5, start:{h:6, m:15}, end:{h:9, m:0}}, 
   {freq: 1, start:{h:7, m:45}, end:{h:8, m:15}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 1, retry: 10,
 handleRefresh: function(){frameload("#miway");},
 handleReady: function(){return $("iframe#miway").contents().find("#bustoken").length>0;}, 
 handlePost:nop };
 
 var rHourly = {freq: 60, 
 prime: [{freq: 15, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 15, start:{h:15, m:30}, end:{h:20, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 5, retry: 45,
 handleRefresh: function(){frameload("#forecast");},
 handleReady: function(){return $("iframe#forecast").contents().find("table.wxo-media")
   .find("[headers='header1']").length>0;},
 handlePost:forecast }; // *** add clear if exists
 
 var rMap = {freq: 30, 
 prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 5, start:{h:15, m:30}, end:{h:18, m:00}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 10, retry: 45,
 handleRefresh: refreshMap,
 handleReady: function(){return true;}, 
 handlePost:nop }; // *** add clear if exists?
 
 var rTravel = {freq: 20, 
 prime: [{freq: 5, start:{h:7, m:30}, end:{h:8, m:30}}, 
   {freq: 5, start:{h:15, m:30}, end:{h:18, m:30}}, 
   {freq: 1, start:{h:7, m:50}, end:{h:8, m:10}}, 
   {freq: 1, start:{h:17, m:00}, end:{h:17, m:30}}],
 last: dummydate, next: dummydate, fresh: true,  overdue: 0, lag: 5, retry: 45,
 handleRefresh: navigate, 
 handleReady: function(){return ( parseInt($("#result1").text()) > 0 
   && parseInt($("#result2").text()) > 0);}, 
 handlePost:nop }; 




The code also includes forced refresh once daily at approximately 2:00 AM. This is done to prevent an occasional memory leak to screw our browser (we'll be running this 24/7 for months on end, remember?). I am leaving this part as an exercise for the reader - the flow chart is basically, "if the current time is between 2:00 and 3:00, and if the startup day is not the same as the current day, do location.reload()".

Still, I can see that the Playbook browser (or to be exact, an app called Backlight Override, which is just a browser wrapper that additionally prevents the backlight from ever going off) does crash - once every 3-4 weeks. Well, for me, this is a fairly acceptable "mean time between failures", even though anything more frequent than once a week would already border on annoying.


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.