tz_lookup_wrapper 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }