in What time is it? I started describing a clock-building project. I'm midway (I hope!) through the project, and it is a little different from where it began. Previously the emphasis was on getting the time from the internet, which it might still do as a backup if GPS doesn't get a signal, but the primary time source will probably be a GPS.
But, in either case, time arrives as UTC, not local time. How to convert UTC to local is not a problem on a computer running a real operating system. When a real OS is installed, a timezone is supplied, like America/New_York, etc. That's all the OS needs (along with a table of data about names, offsets from UTC, and when daylight time starts and ends, which is built in to Windows, Mac, and Linux OSs.)
The ESP32 based clock only has about 4MB memory total, and runs a version of FreeRTOS , a real-time OS for tiny computers. It does not include a table to convert from a name of a timezone to the rule for timezone, but it does understand the rules. A rule is a short string that indicates the offset from UTC, whether daylight (summer) time is observed, and when daylight time starts and stops.
A clock with a GPS should be able to set itself to local time if it has a complete map of lat/lon data to timezone rules. Making that map, and packing it into <2MB is what this JSL does. The comments tell the story of using JSL to build a binary blob of run-length encoded data to flash onto a ESP32 SoC (System On a Chip.) The if(0) sections are needed the first time the code runs; some of them take a long time and did not need to be rerun as the code was developed. Various sections of the JSL needs to run about three times to reach the best-compressed state because the order of the zone drawing changes, which changes the usage count of the run codes, which affects how pixels are obscured in the graph because the least used are drawn last to make sure they show up.
Also: if you use this, the resolution is worse than 1 mile in places. Do your own testing!
dtTrans = open("$desktop/timezoneNameConversion.jmp");
if( 0,
za = Open( "z:/timezones.geojson.zip", "zip" );
blob = za << read( "combined.json", Format( blob ) );
p = Parse JSON( Blob To Char( blob ) );
Save Text File( "$desktop/rlejson.jsl", char(p) );
);
P = Eval( Parse( Load Text File( "$desktop/rlejson.jsl" ) ) );
featureList = P["features"];
rle = Eval( Parse( Load Text File( "$desktop/rletext.jsl" ) ) );
freqCodeToName = Eval( Parse( Load Text File( "$desktop/rlename.jsl" ) ) );
iFeatureToName = {};
for( global:iFeature = 1, global:iFeature <= N Items( featureList ), global:iFeature += 1,
tzdata = featureList[global:iFeature];
tzproperties = tzdata["properties"];
tzname=tzproperties["tzid"];
insertinto(iFeatureToName,tzname);
if(!contains(freqCodeToName,tzname),
write("\!nadd ",tzname);
insertinto(freqCodeToName, tzname);
);
);
if( freqCodeToName[1] != "water" | freqCodeToName[2] != "Asia/Shanghai" | freqCodeToName[426] != "Europe/Busingen" | freqCodeToName[427] != "Europe/Vatican",
Throw( "freqCodeToName" )
);
dopoly = function( {tzName, tzcoords},
{i, m, xs,ys},
for( i = 1, i <= N Items( tzcoords ), i += 1,
m = Matrix( tzcoords[i] );
xs = m[0, 1];
ys = m[0, 2];
polygon(xs, ys);
)
);
drawpolys = function( {},
{tzdata, tzgeometry, tzproperties, tztype, tzGeometryType, tzcoordinates, code, greenbits, bluebits, redbits, ipoly},
tzdata = featureList[global:iFeature];
tzgeometry = tzdata["geometry"];
tzproperties = tzdata["properties"];
write("\!n drawing ",global:iFeature,": ", tzproperties["tzid"] );
tztype = tzdata["type"];
tzGeometryType = tzgeometry["type"];
tzcoordinates = tzgeometry["coordinates"];
Fill Color( "white" );
if(
tzGeometryType == "MultiPolygon",
for( ipoly = 1, ipoly <= N Items( tzcoordinates ), ipoly += 1,
dopoly( tzproperties["tzid"], tzcoordinates[ipoly] )
);
, tzGeometryType == "Polygon",
dopoly( tzproperties["tzid"], tzcoordinates );
,
Throw( "tzGeometryType" || Char( tzGeometryType ) )
);
);
composition = 0;
composition = J( 16000, 30000, 0 );42;
if(0,
for( ifreq = 2, ifreq <= N Items( freqCodeToName ), ifreq += 1,
thisname=freqCodeToName[ifreq];
global:iFeature = loc(iFeatureToName,thisname)[1];
gb = Graph Box(
X Scale( -180, 180 ),
Y Scale( -90, 90 ),
framesize( 30000 + 1, 16000 + 1 ),
<<backgroundcolor( "black" ),
drawpolys();
);
pixels = gb[framebox( 1 )] << getpicture;
gb = 0;
pixels = pixels << getpixels;
pixels = pixels[2 :: (N Rows( pixels ) - 5), 2 :: (N Cols( pixels ) - 5)];
if( N Rows( composition ) == N Rows( pixels ) & N Cols( composition ) == N Cols( pixels ),
composition[Loc( pixels )] = global:iFeature;
,
Throw( "composition" )
);
write("\!n freq=", ifreq," composition code=",global:iFeature," for ",thisname );
if(thisname == "America/New_York", write(" ***"));
Wait( .1 );
);
try(deletefile("$desktop/rlecomp.jsl"));
bcomp = matrixtoblob(composition[1::4000,0],"float",8,"little");
file=Save Text File( "$desktop/rlecomp.jsl", bcomp, mode("replace") );
show(filesize(file));
bcomp = matrixtoblob(composition[4001::8000,0],"float",8,"little");
file=Save Text File( "$desktop/rlecomp.jsl", bcomp, mode("append") );
show(filesize(file));
bcomp = matrixtoblob(composition[8001::12000,0],"float",8,"little");
file=Save Text File( "$desktop/rlecomp.jsl", bcomp, mode("append") );
show(filesize(file));
bcomp = matrixtoblob(composition[12001::16000,0],"float",8,"little");
file=Save Text File( "$desktop/rlecomp.jsl", bcomp, mode("append") );
show(filesize(file));
);
composition = 0;
composition = J( 16000, 30000, 0 );
composition[1::4000,0] = blobtomatrix(loadtextfile("$desktop/rlecomp.jsl",blob(readOffsetFromBegin(0),readlength(960000000))),"float",8,"little",30000);
composition[4001::8000,0] = blobtomatrix(loadtextfile("$desktop/rlecomp.jsl",blob(readOffsetFromBegin(1*960000000),readlength(960000000))),"float",8,"little",30000);
composition[8001::12000,0] = blobtomatrix(loadtextfile("$desktop/rlecomp.jsl",blob(readOffsetFromBegin(2*960000000),readlength(960000000))),"float",8,"little",30000);
composition[12001::16000,0] = blobtomatrix(loadtextfile("$desktop/rlecomp.jsl",blob(readOffsetFromBegin(3*960000000),readlength(960000000))),"float",8,"little",30000);
img = 0;
img = New Image( Heat Color( composition / Max( composition ), "spectral" ) );
img << scale( 1 / 16 );
New Window( "x", img );
if(0,
codecounts = [=>0];
rle = {};
for( irow = 1, irow <= N Rows( composition ), irow += 1,
if(mod(irow,100)==0,write(irow," ");wait(.01));
row = {};
code = -1;
length = 0;
for( icol = 1, icol <= N Cols( composition ), icol += 1,
if( composition[irow, icol] != code,
if( length > 0,
run = {};
codecounts[code]+=1;
run[1] = code;
run[2] = length;
row[N Items( row ) + 1] = run;
);
length = 1;
code = composition[irow, icol];
,
length += 1
)
);
run = {};
codecounts[code]+=1;
run[1] = code;
run[2] = length;
row[N Items( row ) + 1] = run;
rle[N Items( rle ) + 1] = Matrix( row );
);
countkeys = codecounts<<getkeys;
countvals = codecounts<<getvalues;
rnk = rank(countvals);
countvals = reverse(countvals[rnk]);
countkeys = reverse(countkeys[rnk]);
if(countkeys[1]!=0,throw("countkeys"));
freqCodeToName = iFeatureToName[countkeys[2::nitems(countkeys)]];
insertinto(freqCodeToName,"water",1);
Save Text File( "$desktop/rletext.jsl", Char( rle ) );
Save Text File( "$desktop/rlename.jsl", char(freqCodeToName) );
);
RleTableIndex = J( N Items( rle ), 1, . );
RleTableDataBlob = Char To Blob( "" );
address = 0;
irun = 0;
partBlob = Char To Blob( "" );
for( i = 1, i <= N Items( rle ), i += 1,
if(mod(i,100)==0,write(i," ");wait(0););
RleTableIndex[i] = address;
k = 0;
for( j = 1, j <= N Items( rle[i] ) / 2, j += 1,
irun += 1;
k += 1;
code = rle[i][k];
k += 1;
length = rle[i][k];
if(!(0<=code<=426),throw("code"||char(code)));
if(!(1<=length<=30000),throw("length"||char(length)));
partBlob = partBlob || (if( code <= 127,
address += 1;
Matrix To Blob( Matrix( code ), "int", 1, "big" );
,
address += 2;
Matrix To Blob( Matrix( -(code) ), "int", 2, "big" );
) || if( length <= 127,
address += 1;
Matrix To Blob( Matrix( length ), "int", 1, "big" );
,
address += 2;
Matrix To Blob( Matrix( -length ), "int", 2, "big" );
));
);
if(mod(i,100)==0,RleTableDataBlob = RleTableDataBlob || partBlob; partBlob = Char To Blob( "" ););
);
show(length(RleTableDataBlob));
StringTableIndex = J( Nitems( iFeatureToName )+1, 1, . );
null = Hex To Blob( "00" );
StringTableDataBlob = null;
StringTableIndex[1] = 0;
xlate=associativearray(dtTrans:from<<getvalues,dtTrans:to<<getvalues);
for( i = 1, i <= Nitems( iFeatureToName ), i += 1,
StringTableIndex[i+1] = Length( StringTableDataBlob );
tznamet=xlate[iFeatureToName[i]];
StringTableDataBlob = StringTableDataBlob || (chartoblob(tznamet) || null);
);
Length( StringTableDataBlob );
loc(iFeatureToName,"America/New_York");
stiLength = 4*nitems(StringTableIndex);
rtiLength = 4*nitems(RleTableIndex);
stdLength = length(StringTableDataBlob);
n=12;
header = n
||((4+n*4)+0)
||((4+n*4)+stiLength)
||((4+n*4)+stiLength+stdLength)
||((4+n*4)+stiLength+stdLength+rtiLength)
||((4+n*4)+stiLength+stdLength+rtiLength+length(RleTableDataBlob))
||nrows(composition)
||ncols(composition)
||-90||90
||-180||180
||hextonumber(hex(reverse("END.")));
if(n!=nitems(header)-1,throw("header"));
headerBlob = matrixtoblob(header,"int",4,"little");
hbLength = length(headerBlob);
stiBlob = matrixToBlob(StringTableIndex+0*(hbLength+stiLength),"int",4,"little");
if(length(stiBlob)!=stiLength,throw("stiBlob"));
rtiBlob = matrixtoblob(RleTableIndex+0*(hbLength+stiLength+rtiLength),"int",4,"little");
if(length(rtiBlob)!=rtiLength,throw("rtiBlob"));
finalblob = headerBlob
|| stiBlob
|| StringTableDataBlob
|| rtiBlob
|| RleTableDataBlob
|| chartoblob("TZ lookup built "||char(asdate(today()))) || null;
write("\!nfinalblob size=",length(finalblob));
write(
"\!nstring NY=",
blobpeek(finalblob,
blobtomatrix(
Blob Peek( finalblob,
Blob To Matrix( Blob Peek( finalblob, 1 * 4, 4 ), "int", 4, "little" )[1] + 4 * 152
, 4
),
"int",4,"little"
)
+
Blob To Matrix( Blob Peek( finalblob, 2 * 4, 4 ), "int", 4, "little" )[1]
,
17
)
);
write("\!nsignature=",
blobpeek(
finalblob,
Blob To Matrix( Blob Peek( finalblob, 5 * 4, 4 ), "int", 4, "little" )[1],
300
)
);
/
lookup = function({lat=41.91,lon},{row,col,r,c,code},
row=floor(interpolate(lat,-90,N Rows( composition ),90,1+0))-0;
r=RleTableIndex[row];
col=round(interpolate(lon,-180,1+0,180,N Cols( composition )))-0;
c=0;
while(c<col,
code = blobtomatrix(blobpeek(RleTableDataBlob,r,1),"int",1,"big")[1];
if(code<0,
code = -blobtomatrix(blobpeek(RleTableDataBlob,r,2),"int",2,"big")[1];
r+=2;
,
r+=1;
);
length = blobtomatrix(blobpeek(RleTableDataBlob,r,1),"int",1,"big")[1];
if(length<0,
length = -blobtomatrix(blobpeek(RleTableDataBlob,r,2),"int",2,"big")[1];
r+=2;
,
r+=1;
);
c += length;
);
if(code>0,
iFeatureToName[code];
,
"water"
)
);
markersOver = function({clat,clon,name,grid=1},
dtVat=New Table( name,
New Column( "tz", Character, "Nominal" ),
New Column( "lat", Numeric, "Continuous", Format( "Best", 12 ) ),
New Column( "lon", Numeric, "Continuous", Format( "Best", 12 ) )
);
for(ylat=clat-.5*grid, ylat<=clat+.5*grid,ylat+=.004*grid,
for(xlon=clon-.5*grid,xlon<=clon+.5*grid,xlon+=.006*grid,
dtVat<<addrows(1);
dtVat:tz=lookup(ylat,xlon);
dtVat:lat=ylat;
dtVat:lon=xlon;
)
);
dtVat:tz<<label(1);
dtVat<< Color or Mark by Column(dtVat:tz);
dtVat<<setdirty(0);
dtVat = Graph Builder(
Size( 1843, 976 ),
Show Control Panel( 0 ),
Variables( X( :lon ), Y( :lat ) ),
Elements( Points( X, Y, Legend( 3 ) ) ),
SendToReport(
Dispatch(
{},
"Graph Builder",
FrameBox,
{Background Map(
Images(
"Web Map Service", "https://ows.terrestris.de/osm-gray/service?",
"OSM-WMS"
)
), Marker Size( 1 ), Marker Drawing Mode( "Normal" )}
)
)
);
);
markersover(41.905,12.445,"vatican city");
markersover(38.76012940186213, -85.07358907248562, "America/Indiana/Vevay") ;
markersover(38, -85, "Kentucky",20) ;
markersover(4, -51.5,"French Guiana/Brazil");
markersover(26.528054926367076, 88.5814945236749,"Bangladesh");
markersover(19.413200799245335, 166.6283736471554,"wake island");
markersover(18.925476569622884, -71.7749456680967, "Haiti");
markersover(-54.7, -68.79215603902576, "Tierra del Fuego");
markersover(81.3, 62.4,"Rudolf Island");
markersover(65.84924591016264, -169,"alaska");
markersover(6.297049492023935, 1.7798964984249592,"origin");
markersover(35, -78,"home",100);
markersover(38.5, -101.5,"kansas",5);
Does Indiana really have eleven time zones?
Part of the testing to see if the blob works to convert lat/lon to a timezone involved making some maps. The JSL for this map makes a grid of data points over a region. Indiana is a state in the middle of the United States; it is on the edge of the change from Eastern to Central time. The wikipedia article (interesting read) suggests how daylight saving time is a pain, far from the center of a time zone, causing counties within the state to make different choices over many years. The zone names may have had subtle differences in the past with when or if daylight time started and stopped, and not-so-subtle choices between Eastern and Central time. These zone names (from column) appear in Indiana:
New_York, Chicago, and Kentucky zones extend way beyond Indiana.
There are only two distinct rules (the to column) for the time in Indiana. The parts of Indiana that use Central time are close to large cities outside of Indiana that use Central time.
Eleven zones in one state.
Kansas time zones
Kansas is 15 degrees (a whole timezone) west of Indiana and has a similar issue on it's western border. Four counties choose to use Denver (Mountain) time rather than Chicago (Central) time. This makes a good test with the county outlines to see not only the left-right alignment but the top-bottom alignment as well. State outline shown here. Wikipedia's Kansas story is not as interesting as Indiana (good!)
The blue Denver time is used by four counties on the western side of Kansas. The Red Chicago time is used by most of the state.
Flashing the data to the ESP32:
Linux shell window. I should probably rename my computer. I dropped that subscription years ago after I got to the end.
Finally, a tip-of-the-hat to IANA for their part in making the internet work.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.