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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.gitmodules +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -0
- data/Rakefile +7 -0
- data/lib/tz_lookup_wrapper/active_support.rb +413 -0
- data/lib/tz_lookup_wrapper/detect_node.rb +35 -0
- data/lib/tz_lookup_wrapper/exception.rb +11 -0
- data/lib/tz_lookup_wrapper/version.rb +3 -0
- data/lib/tz_lookup_wrapper.rb +50 -0
- data/lib/tz_lookup_wrapper.rb~ +32 -0
- data/spec/lib/tz_lookup_wrapper/detect_node_spec.rb +5 -0
- data/spec/lib/tz_lookup_wrapper_spec.rb +11 -0
- data/spec/spec_helper.rb +1 -0
- data/tz_lookup_wrapper.gemspec +38 -0
- data/vendor/tz-lookup/.gitignore +2 -0
- data/vendor/tz-lookup/README.md +62 -0
- data/vendor/tz-lookup/index.js +169 -0
- data/vendor/tz-lookup/json2bin +378 -0
- data/vendor/tz-lookup/package.json +22 -0
- data/vendor/tz-lookup/test.js +2115 -0
- data/vendor/tz-lookup/tz.bin +0 -0
- metadata +112 -0
@@ -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
|
+
}
|