tz_lookup_wrapper 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+
3
+ var fs = require("fs"),
4
+ path = require("path"),
5
+ COARSE_WIDTH = 48,
6
+ COARSE_HEIGHT = 24,
7
+ FINE_WIDTH = 2,
8
+ FINE_HEIGHT = 2,
9
+ MAX_DEPTH = 9,
10
+ MAX_DISTANCE_SQ = 0.16,
11
+ TIMEZONE_INTERNATIONAL_LIST = [
12
+ "Etc/GMT+12", "Etc/GMT+11", "Etc/GMT+10", "Etc/GMT+9", "Etc/GMT+8",
13
+ "Etc/GMT+7", "Etc/GMT+6", "Etc/GMT+5", "Etc/GMT+4", "Etc/GMT+3",
14
+ "Etc/GMT+2", "Etc/GMT+1", "Etc/GMT", "Etc/GMT-1", "Etc/GMT-2",
15
+ "Etc/GMT-3", "Etc/GMT-4", "Etc/GMT-5", "Etc/GMT-6", "Etc/GMT-7",
16
+ "Etc/GMT-8", "Etc/GMT-9", "Etc/GMT-10", "Etc/GMT-11", "Etc/GMT-12"
17
+ ];
18
+
19
+ function bound(poly) {
20
+ var bound = [
21
+ Number.POSITIVE_INFINITY,
22
+ Number.POSITIVE_INFINITY,
23
+ Number.NEGATIVE_INFINITY,
24
+ Number.NEGATIVE_INFINITY
25
+ ],
26
+ i;
27
+
28
+ for(i = poly.length; i--; ) {
29
+ if(poly[i][0] < bound[0]) bound[0] = poly[i][0];
30
+ if(poly[i][0] > bound[2]) bound[2] = poly[i][0];
31
+ if(poly[i][1] < bound[1]) bound[1] = poly[i][1];
32
+ if(poly[i][1] > bound[3]) bound[3] = poly[i][1];
33
+ }
34
+
35
+ return bound;
36
+ }
37
+
38
+ function area(poly) {
39
+ var area = 0.0,
40
+ a = poly[0],
41
+ i, b;
42
+
43
+ for(i = poly.length; i--; ) {
44
+ b = a;
45
+ a = poly[i];
46
+ area += a[0] * b[1] - a[1] * b[0];
47
+ }
48
+
49
+ return Math.abs(area * 0.5);
50
+ }
51
+
52
+ function contains(polygon, lat, lon) {
53
+ var a = polygon[0],
54
+ t = false,
55
+ i, b;
56
+
57
+ for(i = polygon.length; i--; ) {
58
+ b = a;
59
+ a = polygon[i];
60
+
61
+ if(((a[1] <= lat && lat < b[1]) || (b[1] <= lat && lat < a[1])) && ((lon - a[0]) < ((b[0] - a[0]) * (lat - a[1]) / (b[1] - a[1]))))
62
+ t = !t;
63
+ }
64
+
65
+ return t;
66
+ }
67
+
68
+ /* This actually returns the distance squared. :p */
69
+ function line(lat, lon, a, b) {
70
+ var x = b[0] - a[0],
71
+ y = b[1] - a[1],
72
+ u;
73
+
74
+ if(x === 0.0 && y === 0.0) {
75
+ x = a[0];
76
+ y = a[1];
77
+ }
78
+
79
+ else {
80
+ u = ((lon - a[0]) * x + (lat - a[1]) * y) / (x * x + y * y);
81
+
82
+ if(u <= 0.0) {
83
+ x = a[0];
84
+ y = a[1];
85
+ }
86
+
87
+ else if(u >= 1.0) {
88
+ x = b[0];
89
+ y = b[1];
90
+ }
91
+
92
+ else {
93
+ x = a[0] + u * x;
94
+ y = a[1] + u * y;
95
+ }
96
+ }
97
+
98
+ x -= lon;
99
+ y -= lat;
100
+ return x * x + y * y;
101
+ }
102
+
103
+ function distance(lat, lon, polygon) {
104
+ var distance = Number.POSITIVE_INFINITY,
105
+ a = polygon[0],
106
+ b, i, t;
107
+
108
+ for(i = polygon.length; i--; ) {
109
+ b = a;
110
+ a = polygon[i];
111
+ t = line(lat, lon, a, b);
112
+
113
+ if(t < distance)
114
+ distance = t;
115
+ }
116
+
117
+ return distance;
118
+ }
119
+
120
+ function Polygon(feature) {
121
+ this.tzid = feature.properties.TZID;
122
+ this.data = feature.geometry.coordinates;
123
+ this.__boundingBox();
124
+ this.__area();
125
+ }
126
+
127
+ Polygon.prototype = {
128
+ __boundingBox: function() {
129
+ this.box = bound(this.data[0]);
130
+ },
131
+ __area: function() {
132
+ var i;
133
+
134
+ this.area = area(this.data[0]);
135
+ for(i = this.data.length; --i; )
136
+ this.area -= area(this.data[i]);
137
+ },
138
+ overlap: function(that_box) {
139
+ return this.box[0] <= that_box[2] && this.box[1] <= that_box[3] &&
140
+ this.box[2] >= that_box[0] && this.box[3] >= that_box[1];
141
+ },
142
+ distance: function(lat, lon) {
143
+ var i;
144
+
145
+ /* Outside polygon: return distance to the edge. */
146
+ if(!contains(this.data[0], lat, lon))
147
+ return distance(lat, lon, this.data[0]);
148
+
149
+ /* Inside polygon hole: return distance to hole's edge. */
150
+ for(i = this.data.length; --i; )
151
+ if(contains(this.data[i], lat, lon))
152
+ return distance(lat, lon, this.data[i]);
153
+
154
+ /* Inside polygon but outside all holes. */
155
+ return 0.0;
156
+ }
157
+ };
158
+
159
+ function readGeoJSON(pathname, callback) {
160
+ fs.readFile(pathname, "ascii", function(err, data) {
161
+ var i;
162
+
163
+ if(err) {
164
+ callback(err, null);
165
+ return;
166
+ }
167
+
168
+ try {
169
+ data = JSON.parse(data.toString("ascii"));
170
+ }
171
+
172
+ catch(err) {
173
+ callback(err, null);
174
+ return;
175
+ }
176
+
177
+ data = data.features;
178
+ for(i = data.length; i--; )
179
+ if(!data[i].properties ||
180
+ !data[i].properties.TZID ||
181
+ data[i].properties.TZID === "uninhabited")
182
+ data.splice(i, 1);
183
+
184
+ else
185
+ data[i] = new Polygon(data[i]);
186
+
187
+ data.sort(function(a, b) {
188
+ return a.area - b.area;
189
+ });
190
+
191
+ callback(null, data);
192
+ });
193
+ }
194
+
195
+ function tzindex(polygons) {
196
+ var hash = {},
197
+ list = [],
198
+ i;
199
+
200
+ for(i = TIMEZONE_INTERNATIONAL_LIST.length; i--; )
201
+ hash[TIMEZONE_INTERNATIONAL_LIST[i]] = -1;
202
+
203
+ for(i = polygons.length; i--; )
204
+ hash[polygons[i].tzid] = -1;
205
+
206
+ for(i in hash)
207
+ if(hash.hasOwnProperty(i))
208
+ list.push(i);
209
+
210
+ list.sort();
211
+
212
+ for(i = list.length; i--; )
213
+ hash[list[i]] = i;
214
+
215
+ return {list: list, hash: hash};
216
+ }
217
+
218
+ function write(root, nzones) {
219
+ var queue = [],
220
+ nodes = [],
221
+ len = 0,
222
+ off = 0,
223
+ max = 0x10000 - nzones,
224
+ node, i, data, t, j;
225
+
226
+ for(queue.push(root); node = queue.shift(); ) {
227
+ node.id = nodes.length;
228
+ nodes.push(node);
229
+ len += (node.width * node.height) << 1;
230
+
231
+ for(i = 0; i < node.data.length; i++)
232
+ if(typeof node.data[i] !== "number")
233
+ queue.push(node.data[i]);
234
+ }
235
+
236
+ fs.writeFileSync("moop.json", JSON.stringify(root));
237
+ console.warn("TILE_COUNT = %d, BYTE_LENGTH = %d", nodes.length, len);
238
+ data = new Buffer(len);
239
+
240
+ for(i = 0; i < nodes.length; i++) {
241
+ node = nodes[i];
242
+
243
+ for(j = node.data.length; j--; ) {
244
+ if(typeof node.data[j] === "number")
245
+ t = node.data[j];
246
+
247
+ else {
248
+ /* We can subtract 1 since indices are unique; thus we never need to
249
+ * worry about t being zero before the subtraction. */
250
+ t = (node.data[j].id - node.id) - 1;
251
+
252
+ if(t >= max)
253
+ throw new Error("too many tiles, sorry.");
254
+ }
255
+
256
+ data.writeUInt16BE(t, off + (j << 1));
257
+ }
258
+
259
+ off += (node.width * node.height) << 1;
260
+ }
261
+
262
+ process.stdout.write(data);
263
+ }
264
+
265
+ readGeoJSON(path.join(__dirname, "tz_world.json"), function(err, polygons) {
266
+ if(err)
267
+ throw err;
268
+
269
+ var zones = tzindex(polygons);
270
+
271
+ function tile(polygons, min_lat, min_lon, max_lat, max_lon, width, height, depth) {
272
+ var box = [
273
+ min_lon - MAX_DISTANCE_SQ,
274
+ min_lat - MAX_DISTANCE_SQ,
275
+ max_lon + MAX_DISTANCE_SQ,
276
+ max_lat + MAX_DISTANCE_SQ
277
+ ],
278
+ lat = (min_lat + max_lat) * 0.5,
279
+ lon = (min_lon + max_lon) * 0.5,
280
+ list = [],
281
+ i, data, x, y, best, dist, t, flat, dlat, dlon;
282
+
283
+ for(i = polygons.length; i--; )
284
+ if(polygons[i].overlap(box))
285
+ list.push(polygons[i]);
286
+
287
+ /* No polygons cover the area? Well then, just exit early. */
288
+ if(list.length === 0)
289
+ return (0x10000 - zones.list.length) + zones.hash[TIMEZONE_INTERNATIONAL_LIST[Math.round((180.0 + lon) / 15.0)]];
290
+
291
+ /* Only one does? Then use it. (This isn't actually accurate, but our
292
+ * compression is actually lossy. Furthermore, this just means that some
293
+ * places will be considered land instead of ocean, which is actually
294
+ * helpful for our use-case.) */
295
+ if(list.length === 1)
296
+ return (0x10000 - zones.list.length) + zones.hash[list[0].tzid];
297
+
298
+ /* If we don't want to recurse any more, just return a single pixel. */
299
+ if(depth === MAX_DEPTH) {
300
+ best = null;
301
+ dist = MAX_DISTANCE_SQ;
302
+ for(i = list.length; i--; ) {
303
+ box[0] = lon - dist;
304
+ box[1] = lat - dist;
305
+ box[2] = lon + dist;
306
+ box[3] = lat + dist;
307
+ if(!list[i].overlap(box))
308
+ continue;
309
+
310
+ t = list[i].distance(lat, lon);
311
+ if(t >= dist)
312
+ continue;
313
+
314
+ best = list[i];
315
+ dist = t;
316
+ }
317
+
318
+ return (0x10000 - zones.list.length) + zones.hash[best ? best.tzid : TIMEZONE_INTERNATIONAL_LIST[Math.round((180.0 + lon) / 15.0)]];
319
+ }
320
+
321
+ /* Look up the entire tile. */
322
+ data = new Array(width * height);
323
+ flat = true;
324
+ dlat = (max_lat - min_lat) / height;
325
+ dlon = (max_lon - min_lon) / width;
326
+ for(y = height; y--; ) {
327
+ for(x = width; x--; ) {
328
+ t = tile(
329
+ list,
330
+ min_lat + (height - (y + 1)) * dlat,
331
+ min_lon + (x + 0) * dlon,
332
+ min_lat + (height - (y + 0)) * dlat,
333
+ min_lon + (x + 1) * dlon,
334
+ FINE_WIDTH,
335
+ FINE_HEIGHT,
336
+ depth + 1
337
+ );
338
+
339
+ data[y * width + x] = t;
340
+
341
+ if(typeof t !== "number")
342
+ flat = false;
343
+ }
344
+ }
345
+
346
+ /* Check to see if there's only one non-ocean color. If so, use it. If not,
347
+ * if the only color is ocean, use that. Otherwise, I guess we have to
348
+ * return the whole thing, oh well. */
349
+ if(flat) {
350
+ t = -1;
351
+
352
+ for(i = data.length; i--; ) {
353
+ if(data[i] === t)
354
+ continue;
355
+
356
+ if(zones.list[data[i] - (0x10000 - zones.list.length)].slice(0, 3) === "Etc")
357
+ continue;
358
+
359
+ if(t !== -1)
360
+ break;
361
+
362
+ t = data[i];
363
+ }
364
+
365
+ if(i === -1) {
366
+ if(t === -1)
367
+ t = (0x10000 - zones.list.length) + zones.hash[TIMEZONE_INTERNATIONAL_LIST[Math.round((180.0 + lon) / 15.0)]];
368
+
369
+ return t;
370
+ }
371
+ }
372
+
373
+ return {id: -1, width: width, height: height, data: data};
374
+ }
375
+
376
+ console.warn("TIMEZONE_LIST = %j", zones.list);
377
+ write(tile(polygons, -90.0, -180.0, +90.0, +180.0, COARSE_WIDTH, COARSE_HEIGHT, 0), zones.list.length);
378
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "tz-lookup",
3
+ "version": "6.0.1",
4
+ "description": "fast time zone lookup",
5
+ "keywords": [
6
+ "tz",
7
+ "timezone",
8
+ "time zone"
9
+ ],
10
+ "author": {
11
+ "name": "The Dark Sky Company",
12
+ "email": "devsupport@darkskyapp.com"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git://github.com/darkskyapp/tz-lookup.git"
17
+ },
18
+ "devDependencies": {
19
+ "chai": "1.9.x",
20
+ "mocha": "1.17.x"
21
+ }
22
+ }