Showing posts with label HTML. Show all posts
Showing posts with label HTML. 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):



Friday, November 8, 2019

House hunting in the cloud

Let us continue the tradition of following a low-level language coding post with really high-level. This time, with a cloud to boot.

Anyone who was apartment and house hunting knows that it is time consuming and hard to go through individual listings. Extracting trends from a large number of them can be very time-consuming and involving a lot of manual input.

It would be really helpful to have a took that would extract the features you need from a listing feed, and visualize it in your preferred way, like so:




We can actually do this with a little bit of coding entirely in Wolfram Cloud. Let's get started with a sample Toronto MLS listing selection (the kind you will be receiving from a real estate agent), looking like so (sorry for a tall image).



So the task is (1) to web-scrape the listing web page for information, and (2) to visualize it in human digestible form.

Using this nice Wolfram Language scraping guide, we can get started by simply grabbing the table data in one line, like so:


url = "http://v3.torontomls.net/Live/Pages/Public/Link.aspx?Key=...&App=TREB";
structured = Import[url,"Data"];
mlstable = structured[[1,2]];
addresses = Transpose[mlstable][[2]];
rawprices = Transpose[mlstable][[4]];
prices = ToExpression[StringReplace[#,{"$"->"",","->""}]]&/@rawprices;


Now we have the list of street addresses and prices. To geo-locate the addresses, the easiest way is to complete each address with city and country info, ans then use Interpreter:


locations = Map[Interpreter["StreetAddress"][#<>", Mississauga ON Canada"]&, addresses];


We can then convert the list of prices into a list of colored pins where color is determined by the house price, using a slightly modified example from GeoMarker documentation:


pin[color_]:= Graphics[GraphicsGroup[{FaceForm[color],EdgeForm[Black],
   FilledCurve[{{Line[Join[{{0, 0}}, ({Cos[#1], 3 + Sin[#1]} &) /@
   Range[-((2 Pi)/20), Pi + (2 Pi)/20, Pi/20], {{0, 0}}]]}, {Line[(0.5 {Cos[#1], 6 + Sin[#1]} &) /@
   Range[0, 2 Pi, Pi/20]]}}]}]]
rcfun[sc_] :=Blend[{{0,Green},{0.5,RGBColor[0.75,0.75,0]},{1,Red}},sc]
pins = Map[pin[rcfun[(#-900000)/(1300000-900000)]]&,prices];


It only remains to convert the list of geographical coordinates and pins to GeoMarker and filter out failed address lookups (as well as missed lookups, defined as those more than say 10 miles away from the arbitrarily chosen city center), like so


markertable = MapThread[GeoMarker[#1,#2]&,{locations,pins}];
home = Interpreter["StreetAddress"]["Square One, Mississauga ON Canada"];
goodmarkers = Select[markertable,(!FailureQ[#[[1]]] && GeoDistance[#[[1]],home][[1]]<10)&];


In my example, 94 markers out of 99 remain as "good". Then, we simply plot the markers on a map using GeoGraphics:


gp = GeoGraphics[{"Mississauga",Append[goodmarkers,GeoMarker[home,pin[White]]]}];
gins=DensityPlot[(x*1000-900000)/(1300000-900000),{y,0,1},{x,900,1300},ColorFunction->rcfun,AspectRatio->5,FrameTicks->{None,Automatic},Background->RGBColor[1,1,1,0.5],FrameStyle->Directive[Thick],LabelStyle->Normal];
Show[gp,Epilog->Inset[gins,Scaled[{1,0}],Scaled[{1,0}],0.028]]






In the second example let us color code the markers using price per square foot. Note that the square footage of the houses is not in the table, so we need to parse individual listings. So we need to do a more complicated web scraping:


url = "real_estate.html"; xml = Import[url,"XMLObject"];
formitems=Cases[xml,XMLElement["span",{"class"->"formitem formfield"},x_]->x,Infinity];
sqfeet=ToExpression[Last[StringSplit[#,"-"]]]&/@((If[#[[3]]=={},"0-0",#[[3,1]]])&/@Extract[formitems,(#+{0,1})&/@Position[formitems,XMLElement["label",{},{"Apx Sqft:"}]]] )


Note several things about this code:
  • The specific criteria to supply to Cases have been determined by inspecting the page code in the browser; in this case all the information bearing fields conveniently are <span> tags with classes formitem formfield. Your particular case will be different.
  • In the last line, Extract basically retrieves "every element following a label saying Apx Sqft:". Again your case will be different, and I admit that this is not the only way to get to the right info.
  • Local HTML file is used instead of a live URL. This is a trick dome to work around asynchronous deferred loading of listings on Toronto MSL website; if live URL were used, the scraper would only retrieve some 25 listings. The HTML file is obtaied by loading the MLS link, scrolling all the way down (not too fat so that all listings have a chance to load), then saving the webpage as a complete package and loading its HTML file into Wolfram Cloud (or creating a text file in the cloud and copy-pasting, or hosting it locally and making it accessible to Wolfram Cloud)
  • The last portion, involving Last[StringSplit[...]] is needed to convert approximate designations like "1500-2000" into a number 2000.

After this, the code is familiar, except that marker filtering should now include filtering out the listings without square feet information:



pricesperfoot = Quiet[prices/cfeet];
score=Quiet[(pricesperfoot-250)/(800-250)];
scorepins=Map[pin[rcfun[#]]&,score];
smarkertable = MapThread[GeoMarker[#1,#2]&,{locations,scorepins}];
sgoodmarkers=Select[smarkertable,(!FailureQ[#[[1]]] && GeoDistance[#[[1]],home][[1]]<10 
                    && NumberQ[#[[2,1,1,1,-1,-1]]])&];
gs=GeoGraphics[{"Mississauga",Append[sgoodmarkers,GeoMarker[home,pin[White]]]}];
Show[gs,Epilog->Inset[ginss,Scaled[{1,0}],Scaled[{1,0}],0.028]]


Here's the final result:



This is only an example I spent about an hour coding, another 1-2 polishing and another 1-2 hours writing about. Your mileage may vary. By the same token you can easily visualize houses according to any score you compute (such as "price per score determined by adding the number of bedrooms and half the number of washrooms"). You can also add multidimensional visualization, where a pin's size, or border color, or shape, or all of these, would convey different information. You can use geo-location and scrape some other website to score neighborhoods and show the "best bang for the buck" according to that score. You can build a linear regression machine-learning house pricing service. If you are dragged into the boredom of house hunting, there are always ways to make some colorful fun out of it :) .

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.

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.