Tuesday, November 20, 2018

Memory leak tester in Excel

In many scenarios involving complicated Excel calculations, especially those relying on extensive VBA code and/or custom-made add-ins, there is a danger that memory leaks could be created by programming errors such as careless object operations.

Most often, such memory leaks are discovered post factum, when, left unchecked, they cause slowdowns or crashes when an Excel session runs out of memory. In such a case, correcting the error becomes a major pain. How to spot a memory leak offender in a calculation intensive spreadsheet containing thousands of custom functions? 

To this end, I have written a very simple program that basically evaluates a given formula multiple times and measures how the memory consumption increased. I have used this googled snippet to determine the memory consumption:

Declare Function GetCurrentProcessId Lib "kernel32" () As Long

Function GetMemUsage()
  ' Returns the current Excel.Application memory usage in KB
  Set objSWbemServices = GetObject("winmgmts:")
  GetMemUsage = objSWbemServices.Get( _
    "Win32_Process.Handle='" & _
    GetCurrentProcessId & "'").WorkingSetSize / 1024

  Set objSWbemServices = Nothing ' We don't want to cause memory leaks here :)
    ' We don't want to cause memory leaks here :)  
End Function

and wrote a very simple wrapper: 

Sub Measure_Leak()
 Set here = ActiveSheet.Range("A2")
 template = here.Value
 i1from = here.Offset(0, 1).Value: i1to = here.Offset(0, 2).Value
 i2from = here.Offset(0, 3).Value: i2to = here.Offset(0, 4).Value
 Set there = here.Offset(0, 5)
 
 there.Offset(0, 1).Value = GetMemUsage()
 
 For i1 = i1from To i1to
 For i2 = i2from To i2to
  working = "=" & template
  On Error Resume Next
   working = Replace(working, "$1", i1)
   working = Replace(working, "$2", i2)
  On Error GoTo 0
  there.Formula = working
 Next i2, i1
 
 there.Offset(0, 2).Value = GetMemUsage()
 Set here = Nothing
 Set there = Nothing
 ActiveSheet.Calculate
End Sub

Now I put this on an even simpler spreadsheet which looks like this:



Basically, pressing the Measure button evaluates the template expression substituting $1 and $2 with values spanning two ranges, a total of (i2t-i2f+1)*(i1t-i1f+1) times. An increased memory footprint at the end of the execution means there is a memory leak.

In the screenshot we see that a built-in Excel function does not cause any memory leaks (hurra!)

To test it, let us define a really leaky VBA function using this example:

' Put this into class module Class1
Option Explicit
Private A As New Class2
Private Str As String
Private Sub Class_Initialize()
   Set A.B = Me ' Fool garbage collector
   Str = Space(1024 * 10) ' Allocate lots of memory 
End Sub

'Put this into class module Class2
Option Explicit
Public B As Class1

with two functions that look similar but one is known to be leaky:

Function vbaNoLeak()
 Dim MyObject As Class2
 Set MyObject = New Class2
 Set MyObject = Nothing
End Function

Function vbaLeak()
 Dim MyObject As Class1
 Set MyObject = New Class1
 Set MyObject = Nothing
End Function

...and...




Note that using this method to troubleshoot parts of your VBA macro won't always work because many functions are prohibited inside VBA functions (they abort immediately, basically ensuring that VBA functions have no side effects). But it is very easy to modify the code above to be callable as a procedure from within a VBA macro.


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.

    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.

    Friday, March 16, 2018

    Old Phone/Tablet as an Info Board Part 2: Direct API queries

    In the previous post, we discussed using iframe for an information widget on our info board, noting that it would be too heavy on your browser if your target web page is feature-rich. This means you can forget anything like a Google maps snippet.

    However, precisely with this in mind, many large websites have developed APIs that other websites (read: we) can use. Google does a great job providing its maps API, so incorporating local traffic can be done as simple as

     
    <div id="map" class="basic" style="border: none; position: absolute; right: 10px; top: 10px; width: 415px; height: 350px;">
    </div>
    
    <script async defer
        src="https://maps.googleapis.com/maps/api/js?key=(get_your_own_key)&callback=initMap">
    </script>
     
     <script>
      function initMap() {
         map = new google.maps.Map(document.getElementById('map'), {
           zoom: 13,
            center: {lat: 44.444, lng: -77.777} // Change these parameters to your location
            });
    
            var trafficLayer = new google.maps.TrafficLayer();
            trafficLayer.setMap(map);
          }
    </script>
    


    Obviously to make this work, you will need to get you own Google API key, and change the map location (ans possibly, zoom) to where you would like your map to display. The easiest way to find it would be to open your Google Maps view and then look at the URL to find the values for latitude and longitude.

    Now let us take a step further and provide a widget to monitor the duration in traffic of a given trip in real time. For added flavor, we will compute both the travel time in both directions, where the outbound trip is to start now and the return trip would start in the future.

    For this, we define the interface

     
    <div id="travel" class="basic info" style="position: absolute; left: 150px; bottom: 190px; width: 415px; height: 60px; background:rgb(255,255,225);">
    <span id="log1" style="font-size: 10px; color:rgb(200,200,200); position: relative; top: 0px;">Not loading</span><br/>
    <span id="log2" style="font-size: 10px; color:rgb(200,200,200); position: relative; top: 2px;">Not loading</span><br/>
    <span id="results" style="position: relative; top: 10px;"><span id="result1" style="font-weight: bold;">. . .</span> / <span id="result2" style="font-weight: bold;">. . .</span> <span id="resultkm">...</span></span>
    </div>
    


    and poll the Directions API like this:

     
    function navigate(){
     $("#log1").text("load...");$("#log2").text("load...");
     var origin = "44.44444, -77.77777";
     var destination = "43.33333,-78.88888";
     //Change these to your origin and destination
    
     var directionsService = new google.maps.DirectionsService();
     // var directionsDisplay = new google.maps.DirectionsRenderer();
     // directionsDisplay.setMap(map)
        var request1 = {
     origin: origin, destination: destination, 
     travelMode: google.maps.DirectionsTravelMode.DRIVING,
     drivingOptions: { departureTime: new Date(Date.now()+0), trafficModel: 'bestguess'}
     };
        var request2 = {
     origin: destination, destination: origin, // return trip
     travelMode: google.maps.DirectionsTravelMode.DRIVING,
     drivingOptions: { departureTime: new Date(Date.now()+1000*60*30), trafficModel: 'bestguess'}
            // trip stars 30 minutes in the future
     };
       var fAlert = "rgb(200, 0, 200)";
       var fWarn = "rgb(255, 0, 0)";
       var bOK = "rgb(225,255,225)";
       var bAlert = "rgb(255,255,200)";
       var bWarn = "rgb(255,180,180)";
         
       directionsService.route( request1, function( response, status ) {
        if ( status === 'OK' ) {
            // directionsDisplay.setDirections(response);
            var point = response.routes[ 0 ].legs[ 0 ];
     var d=new Date();
     $( '#log1' ).html(d.toString());
            $( '#result1' ).html(point.duration_in_traffic.text);
        $( '#resultkm' ).html(' (' + point.distance.text + ')' );
     if (point.duration_in_traffic.value > 16*60) {$("#result1").css("color",fAlert);};
     if (point.duration_in_traffic.value > 25*60) {$("#result1").css("color",fWarn);};
     $("#travel").css("background-color",bOK);
     if ($("#result1").css("color")==fAlert) {$("#travel").css("background-color",bAlert);};
     if ($("#result1").css("color")==fWarn || $("#result2").css("color")==fWarn ) {$("#travel").css("background-color",bWarn);};
        }
    } ); 
       directionsService.route( request2, function( response, status ) {
        if ( status === 'OK' ) {
            var point = response.routes[ 0 ].legs[ 0 ];
     var d=new Date();
     $( '#log2' ).html(d.toString());
            $( '#result2' ).html(point.duration_in_traffic.text);
     if (point.duration_in_traffic.value > 16*60) {$("#result2").css("color",fAlert);};
     if (point.duration_in_traffic.value > 25*60) {$("#result2").css("color",fWarn);};
        }
    } ); 
    }
    
    Note that as per Google's use policies, you will need to display the directions and route on a map if you use this function on a website that other people can see - just uncomment the corresponding DirectionsRenderer code lines. For your own use, and at your own risk, I believe you can forego this for the sake of readability if you know what you are doing. There is a caveat, though, in that you won't know the exact route that the time was calculated for.

    Here are the examples, showing three possible traffic situations:
    As usual, your mileage may vary:
    • You can implement more sophisticated logic of coloring and alerts - an obvious candidate would be analyzing the difference between .duration and .duration_in_traffic
    • You can analyze the suggested best route and alert if it happens to be different from the default route (which you can determine by making a request when there is as little traffic as possible, i.e. in the middle of the night) - this will indicate a major congestion.
    • You can compare travel time along a predetermined route (by setting many waypoints in the most congested portion) and the chosen preferred route;
    • You can even log / graph the travel time gathering your own statistics and storing it in an SQL database on the DiskStation. This way, should Google's API become unavailable, you will have a fallback method to determine travel times based on historical values. 
    As far as the billing goes... Even if we poll the server every minute round the clock (which is totally overkill), we still won't exceed Google free usage quotas. And even if we did, once debugged and calibrated, this is such a useful service that you may seriously consider paying for it.

    Thursday, January 18, 2018

    Old Phone/Tablet as an Info Board Part 1: IFRAME's and their limitations

    This continues our series on making an info screen. Last time, we created a skeleton layout, so let us begin filling it with contents.

    As an example, let us try to display current and hourly weather using this webpage:
    https://www.theweathernetwork.com/ca/hourly-weather-forecast/ontario/mississauga

    Highlighted are regions I'd like to put on the info board.

    The most straightforward way of putting something from the third-party website into your own would be an IFRAME tag, of this global format:
    <IFRAME scrolling="no" src="..." style="..."></IFRAME>

    Now usually you would only need some portion of the website displayed on your screen. Unfortunately, if the contents of your IFRAME is from the third-party website, you cannot interact with its content (with a very few exceptions) due to the commonly accepted same-origin policy. (Annoying as it is in our case, this limitation is what prevents a fair amount of malicious attacks.)

    However, the desired portion of the website can be extracted via re-positioning the iframe using this negative margin trick:

    style="margin-left: -(XXX)px; margin-top: -(YYY)px;"

    To zoom in or out on the corresponding website, the only way is to use CSS transform property, like so:

    <div id="weather1" class="basic" style="position: relative; left: 5px; top: 10px; width: 500px; height: 180px;">
    <iframe scrolling="no" src="https://www.theweathernetwork.com/ca/hourly-weather-forecast/ontario/mississauga" 
    style=" -webkit-transform: scale(0.72);  -webkit-transform-origin: 0 0;
     transform: scale(0.72);  transform-origin: 0 0; 
            margin-left: -20px; margin-top: -200px; 
     border: 0px none; height: 8120px; width: 750px;"> 
    </iframe></div>
    
    <div id="weather2" class="basic" style="position: relative; left: 5px; top: 20px; width: 500px; height: 200px;">
    <iframe scrolling="no" src="https://www.theweathernetwork.com/ca/hourly-weather-forecast/ontario/mississauga" 
    style=" -webkit-transform: scale(0.72);  -webkit-transform-origin: 0 0;
     transform: scale(0.72);  transform-origin: 0 0; 
            margin-left: -30px; margin-top: -690px; 
     border: 0px none; height: 8120px; width: 750px;"> 
    </iframe></div>
    
    (The -webkit- prefix is needed for browsers like the PlayBook's (or Safari); some other browsers may need other prefixes.)
    This gives us something like:


    As you can see, this method is very easy to code and there is no need (and no possibility for that matter) to do any rearrangement of the displayed information. The target website takes care of that for you.


    Two major pitfalls (besides the above mentioned inability to interact with the iframe contents) are:
    1. Manual adjustment of the margin is very unreliable. Granted, other methods are prone to failing once the target web site undergoes a redesign, but here even a minor change of the layout would screw the placement of your desired content and require re-adjustment. What is worse, this may happen even without a website redesign proper, due to some external content (e.g. ads) affecting the elements' location and sizing. So your screen can intermittently show the wrong content, and may need frequent readjustments.
    2. Even though most of the iframe's target website will be hidden, it will still be loaded and processed (all its scripts, embedded videos, plugins, and ads included), which makes it very heavy on the client browser (especially on older hardware such as the PlayBook). Using transformation makes matters much worse (I guess it may even force the browser to have to "invisibly" render the entire page - how else would the browser know how to scale it?). In my testing, the above example rendered my PlayBook rather unresponsive. 
    So, this method would be prohibitively slow for many practically relevant cases. Still, it is a simple and viable method so long as the target URL is rather lightweight and not too loaded with dynamic HTML or embedded media. A good candidate is a mobile webpage or a page specially designed to be embedded, like this: 

    <div id="weather1" class="basic" style="position: relative; left: 10px; top: 10px; width: 300; height: 185px; background: white;">
    <iframe scrolling="no" src="http://weather.gc.ca/wxlink/wxlink.html?cityCode=on-24&lang=e" allowtransparency="true" 
     style="border: 0px none; height: 185px; width: 300px;"></iframe>
    </div>
    



    Friday, January 12, 2018

    Old Phone/Tablet as an Info Board Intro: Backstory and Basics

    Some time in the past, my loving wife gave me a Blackberry PlayBook for my birthday. As I don't use it much these days (my smartphone has become more versatile and powerful), and would loathe to part with it (it is barely worn and beautiful, and has sentimental value), I would like to give it a second life. 

    So in a series of posts I am going to log how I turn the Playbook into an info board: an always-on, always up to date information screen next to the front door, showing some information such as the status of my commute and today's weather. 

    Why an information screen in favor of other alternatives? For the same reasons they use departure boards at airports and transit stations: it is the fastest and the least disruptive way to get the important information. You can use it with your hands full (unlike your phone), and you don't have to stand there listening for a robotic voice (unlike Hey Google / Alexa / Siri).

    Like so: (and yes, this is a sneak peek into the beta version of the end result):



    The DiskStation server I have at home.
    Rather than getting the SDK and writing an app (long!!!), I decided to make a web page and host it on my DiskStation (a network attached storage which has a web server function). This approach is much more versatile since it is not limited to the PlayBook or Blackberry. In fact, there will be very few PlayBook-specific points here (mostly limitations: PlayBook uses a rather old implementation of Webkit, and its processor is, by the modern web standards, not the fastest).

    On the contrary, the procedures given here should be helpful for many other devices - old iPads, old Android tables, old smartphones (even though a smartphone has a smaller screen and there will be less info that it can meaningfully display) and perhaps something even more exotic like old monitors hooked to something like a Raspberry Pi.

    In fact, when the screen is ready, anyone at home can access it from their desktop or phone if they need the info but don't feel like physically going downstairs.


    So, to get the job done, we will be combining server-side programming (PHP) and client-side programming (JavaScript / jQuery).

    Let's get started by designing an interface:

    <html>
    <head>
        <title>Info Screen</title>
      <style>
       div.background {background: black; fallback: linear-gradient(180deg, rgb(0,0,0), rgb(25,25,25)); position: absolute; left:0; top:0; height: 560px; width: 1024px; }
       div.basic { background-color: white; border: 1px solid rgb(255,255,255); border-radius: 15px; overflow: hidden; padding: 5px;}
       div.saver {filter:invert(100%);-webkit-filter:invert(100%);}
       div.info { text-align: center; vertical-align: middle;  font-family: "Arial", Helvetica, Sans-Serif; font-size:22px}
      .element {font-size: 12px; text-align: center;}
      .large{background-color: #ffffee;width:650px;height:400px;float:left;}
      </style>
    </head>
    
    <body> <div class="background">
    <div id="status" class="basic info" style="position: absolute; left: 10px; top: 10px; width: 120px; height: 90px; background: #EEFFF8; color: gray; font-size: 12px;"></div>
    
    <div id="weather" class="basic" style="position: absolute; left: 10px; bottom: 190px; width: 120px; height: 240px; background: #EEEEFF;"></div>
    <div id="hourly" class="basic info" style="position: absolute; left: 10px; bottom: 10px; width: 990px; height: 160px; background: white;"></div>
    
    <div id="map" class="basic" style="border: none; position: absolute; right: 10px; top: 10px; width: 415px; height: 350px;"></div>
    <div id="travel" class="basic info" style="position: absolute; left: 150px; bottom: 190px; width: 415px; height: 60px; background:rgb(255,255,225);"> </div>
    
    <div id="bus" class="basic info" style="position: absolute; left: 150px; bottom: 270px; width: 415px; height: 50px; background:rgb(255,235,235);"> </div>
    <div id="train1" class="basic" style="position: absolute; left: 150px; top: 10px; width: 200px; height: 195px; "></div>
    <div id="train2" class="basic" style="position: absolute; left: 365px; top: 10px; width: 200px; height: 195px; "></div>
    
    </div>
    </body> </html>
    

    Note that the code above describes only the layout of the elements. In the upcoming posts (there will be 3-4 of them) I am going to discuss in detail how to actually fill the interface with contents, as well as how to script its automatic updates.

    (On a side note, it looks like we've lived through a shift of paradigm about what good stuff is.

    For centuries - and indeed, persisting all the way into the end of the 20th century - the basic idea of all goods was "the good stuff is the stuff that lasts".

    Indeed, in a world where changes are slow and manufacturing is scarce, or expensive, or both, this made total sense. Having to replace anything (from your tools of the trade to the pair of shoes you wear) was an extra burden to be avoided if at all possible; ideally, good stuff would last a lifetime and sometimes even outlast its owners.

    But nowadays - at least the "modernized countries" world - this has changed. And the biggest change has been the change of pace. Stuff improves much faster than it wears out. Not only that, but also - with the digital stuff in particular - your device is only as good as the software it works with, and if that software outruns your hardware, bad luck.

    To summarize this in a metaphor, we became kids again - the digital kids who outgrow their digital clothes before they have any chance of wearing out. We may happen to have the best smartphone in the world today. But in a few years it will be bugged with so many "apps-that-won't update" and "sites-that-take-forever-to-load" and "services-that-no-longer-work" that using this once-perfect gadget would feel like dressing your five-year-old in clothes that were her favorite when she was three.