/** This program draws a map of the world's timezones. It requires the GeoJSON file from: https://github.com/evansiroky/timezone-boundary-builder/releases specifically, you want to download the file timezones-with-oceans.geojson.zip and extract it, giving the file combined-with-oceans.json The GeoJSON format is defined in RFC 7946: https://tools.ietf.org/html/rfc7946 */ // This allows us to attach visualvm for profiling //input=input["Press start", ""] // The location you've extracted the file to: s1 = now[] s = now[] println["Reading file."] f = read["file:/home/eliasen/builds/timezones/combined-with-oceans.json"] e = now[] println["Finished in " + format[e-s, "s", 1] + "\n"] s = now[] b = parseJSON[f] println["JSON parsed."] e = now[] println["Finished in " + format[e-s, "s", 1] + "\n"] // The "type" key indicates what the top-level container is, hopefully a // FeatureCollection filetype = b@"type" println["This file contains a $filetype"] g = new graphics g.font["SansSerif", 2] glabels = new graphics glabels.font["SansSerif", 1] if filetype == "FeatureCollection" plotFeatureCollection[b, g, glabels] else if filetype == "Feature" plotFeature[b, g, glabels] g.color[0,0,0] g.add[glabels] println["Writing svg"] s=now[] g.write["timezones.svg", 1920, undef] e=now[] println["Finished in " + format[e-s, "s", 1] + "\n"] println["Writing svgz"] s=now[] g.write["timezones.svgz", 1920, undef] e=now[] println["Finished in " + format[e-s, "s", 1] + "\n"] println["Writing HTML5"] s=now[] g.write["timezones.html", 1920, undef] e=now[] println["Finished in " + format[e-s, "s", 1] + "\n"] println["Writing png"] s=now[] g.write["timezones.png", 1920, undef] e=now[] println["Finished in " + format[e-s, "s", 1] + "\n"] e1 = now[] println["Total time: " + format[e1-s1, "s", 1] + "\n"] g.show[] /** This plots a FeatureCollection, whose "features" key should contain an array of Feature objects. */ plotFeatureCollection[fc, g is graphics, glabels is graphics] := { for feature = fc@"features" plotFeature[feature, g, glabels] } /** This plots a Feature object. A Feature has a "properties" key that contains a dictionary that describes stuff (in this case, the "tzid" field may be the only member,) and a key called "geometry" which contains the object (in this case, a Polygon or MultiPolygon.) */ plotFeature[feature, g is graphics, glabels is graphics] := { for [key, value] = feature { print["$key:\t"] if key == "type" // This is a string, hopefully "Feature" print[value] // This contains a dictionary of key-value pairs. We most likely want // the "tzid" pair if key == "properties" print["tzid: " + value@"tzid"] g.color[randomFloat[0,1], randomFloat[0,1], randomFloat[0,1]] // A Feature has a key called "geometry" which describes the type if key == "geometry" { type = value@"type" print[type] gsub = undef if (type == "Polygon") { coordinates = value@"coordinates" gsub = makePolygon[coordinates] } else if (type == "MultiPolygon") { coordinates = value@"coordinates" gsub = makeMultiPolygon[coordinates] } else println["Unknown type $type"] if gsub != undef { g.add[gsub] props = feature@"properties" if props != undef { tzid = props@"tzid" if tzid != undef { tzid =~ %s/\//\n/g // Split on slash if type[gsub] == "Poly" { [cx, cy] = gsub.getCentroid[] glabels.text[tzid, cx, cy] } else { [left, top, right, bottom] = getBoundingBox[gsub] glabels.text[tzid, (right-left)/2 + left, (bottom-top)/2 + top] } } } } } println[] } println[] } /** Make a "polygon", given an object containing a GeoJSON coordinates array. In the GeoJSON specification, a "polygon" may actually be an array of concentric disconnected polygons with the first one being a surrounding polygon and the latter ones being "holes" in this object. If there are no holes, then this is returned as a Polygon object, otherwise as a GeneralPath. */ makePolygon[coordinates] := { length = length[coordinates] // If length == 1, this can be a simple polygon with no holes if length == 1 { ret = new filledPolygon for [x,y] = coordinates@0 ret.addPoint[x, -y] return ret } // Otherwise, this is a complex GeneralPath with holes ret = new filledGeneralPath outer = coordinates@0 for [x, y] = outer // Draw the outer polygon ret.addPoint[x, -y] ret.close[] for inner = slice[coordinates, 1, undef] // Draw all the inner polygons { for [x, y] = inner ret.addPoint[x, -y] ret.close[] } return ret } // This draws a set of polygons and returns it as a graphics. makeMultiPolygon[coordinates] := { g = new graphics for polygon = coordinates g.add[makePolygon[polygon]] return g }