├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── gmaps_example.html ├── jquery.timezone-picker.js ├── openlayers_example.html ├── run.sh ├── scripts ├── dbfUtils.py ├── gen_json.py └── shpUtils.py ├── setup_example.sh └── tz_json.tgz /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | example_site/ 3 | tz_json/ 4 | .project 5 | .pydevproject 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "openlayers"] 2 | path = openlayers 3 | url = git@github.com:/openlayers/openlayers 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (C) 2011-2012 Andrew Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Live demos 2 | ---------- 3 | - Google Maps: http://scratch.andrewl.in/timezone-picker/example_site/gmaps_example.html 4 | - OpenLayers: http://scratch.andrewl.in/timezone-picker/example_site/openlayers_example.html 5 | 6 | Prerequisites 7 | ------------- 8 | - jQuery 9 | - Google Maps Javascript API v3 or OpenLayers 10 | 11 | Usage 12 | ----- 13 | See gmaps_example.html and openlayers_example.html 14 | 15 | ./setup_example.sh 16 | ./run.sh 17 | http://localhost:8000/gmaps_example.html 18 | http://localhost:8000/openlayers_example.html 19 | 20 | Setup 21 | ----- 22 | To use in your site, extract tz_json.tgz to a web-accessible location on your 23 | server and pass in the path as the jsonRootUrl option 24 | 25 | Options 26 | ------- 27 | - fillColor: the color of the fill of the rendered timezones (default '#ffcccc') 28 | - fillOpacity: the opacity of the outline of rendered timezones (default 0.5) 29 | - initialLat: the initial latitude for the center point of the map (default 0) 30 | - initialLng: the initial longitude for the center point of the map (default 0) 31 | - initialZoom: the initial zoom level of the map (1-20, default to 2, 1 is most zoomed out) 32 | - jsonRootUrl: the default root URL for the JSON data files (default '/tz_json/') 33 | - strokeColor: the color of the outline of rendered timezones (default '#ff0000') 34 | - strokeWeight: the width of the outline of rendered timezones (default 2) 35 | - strokeOpacity: the opacity of the outline of rendered timezones (default 0.7) 36 | - onHover: callback when a timezone is hovered. Parameters: utcOffset (in minutes), tzNames (array of strings) 37 | - onReady: callback when all the data files are loaded 38 | - onSelected: callback when a timezone is selected via the infowindow. Parameters: olsonName, utcOffset (in minutes), tzName (eg. EST, GMT) 39 | - useOpenLayers: use OpenLayers instead of Google maps 40 | 41 | Methods 42 | ------- 43 | - showInfoWindow(htmlString): show an infoWindow for the current selected region. Takes an HTML string as a parameter 44 | - hideInfoWindow(): hide it 45 | - setDate(Date): set the "relative" date for the picker for proper timezone names (eg. EST vs EDT) 46 | - selectZone(olsonName): programmatically select a timezone 47 | 48 | CSS Classes 49 | ----------- 50 | - timezone-picker-infowindow: the main content container for the infowindow 51 | - NOTE: if you are using Twitter Bootstrap and Google maps or OpenLayers, you may need something like 52 | ``` 53 | #zonepicker img { 54 | max-width: none; 55 | } 56 | ``` 57 | to prevent distortion of your map controls (thanks michaelahlers!) 58 | 59 | For Data File Generation 60 | ------------------------ 61 | You do not need to do all of the steps mentioned below if you're going to use `tz_json.tgz` (the timezone data JSON files are in there already). 62 | 63 | This plugin uses a bunch of timezone data files on a web server. 64 | 65 | - bounding_boxes.json: an array of bounding boxes for each timezone (for hit testing) 66 | - hover_regions.json: an array of polygons representing hover regions for each timezone 67 | - polygons/*.json: polygon definitions for each timezone 68 | 69 | Requires: 70 | 71 | * Python 2.6+ 72 | * libgeos-dev (sudo apt-get install libgeos-dev) (for Mac OS, brew install geos) 73 | * pip (sudo easy_install pip) 74 | * shapely (sudo pip install shapely) 75 | * pytz (sudo pip install pytz) 76 | * simplejson (sudo pip install simplejson) 77 | 78 | To Generate all timezone data JSON files 79 | 80 | - Download and extract the tz_world file from http://efele.net/maps/tz/world/tz_world.zip 81 | - `python script/gen_json.py ` 82 | - Be very patient... 83 | 84 | Acknowledgments 85 | ---------------- 86 | - Thanks to Eric Muller for creating the timezone shape files (http://efele.net/maps/tz/world) 87 | - Thanks to Zachary Forest Johnson for the shp file loading python scripts (http://indiemaps.com/blog/2008/03/easy-shapefile-loading-in-python/) 88 | -------------------------------------------------------------------------------- /gmaps_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 30 | 31 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /jquery.timezone-picker.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | var _options; 3 | var _self; 4 | 5 | var _boundingBoxes; 6 | var _zoneCentroids = {}; 7 | var _selectedRegionKey; 8 | var _selectedPolygon; 9 | var _mapper; 10 | var _mapZones = {}; 11 | var _transitions = {}; 12 | 13 | var _currentHoverRegion; 14 | var _hoverRegions = {}; 15 | var _hoverPolygons = []; 16 | 17 | var _loader; 18 | var _loaderGif; 19 | var _maskPng; 20 | var _needsLoader = 0; 21 | 22 | var GoogleMapsMapper = function(el, mouseClickHandler, mouseMoveHandler, mapOptions) { 23 | var gmaps = google.maps; 24 | var _map; 25 | 26 | // Create the maps instance 27 | _map = new gmaps.Map(el, $.extend({ 28 | mapTypeId: gmaps.MapTypeId.ROADMAP, 29 | center: new gmaps.LatLng(mapOptions.centerLat, mapOptions.centerLng) 30 | }, mapOptions)); 31 | gmaps.event.addListener(_map, 'click', mouseClickHandler); 32 | if (mouseMoveHandler) { 33 | gmaps.event.addListener(_map, 'mousemove', mouseMoveHandler); 34 | } 35 | 36 | var addPolygon = function(coords, stroke, fill, clickHandler, mouseMoveHandler) { 37 | var mapPolygon = new gmaps.Polygon({ 38 | paths: coords, 39 | strokeColor: stroke.color, 40 | strokeOpacity: stroke.opacity, 41 | strokeWeight: stroke.width, 42 | fillColor: fill.color, 43 | fillOpacity: fill.opacity 44 | }); 45 | mapPolygon.setMap(_map); 46 | 47 | gmaps.event.addListener(mapPolygon, 'click', clickHandler); 48 | 49 | if (mouseMoveHandler) { 50 | gmaps.event.addListener(mapPolygon, 'mousemove', mouseMoveHandler); 51 | } 52 | 53 | return mapPolygon; 54 | }; 55 | 56 | var createPoint = function(lat, lng) { 57 | return new gmaps.LatLng(lat, lng); 58 | }; 59 | 60 | var hideInfoWindow = function() { 61 | if (_map.lastInfoWindow) { 62 | _map.lastInfoWindow.close(); 63 | } 64 | }; 65 | 66 | var removePolygon = function(mapPolygon) { 67 | mapPolygon.setMap(null); 68 | }; 69 | 70 | var showInfoWindow = function(pos, content, callback) { 71 | var infowindow = new gmaps.InfoWindow({ 72 | content: '
' + 73 | content + 74 | '
' 75 | }); 76 | 77 | gmaps.event.addListener(infowindow, 'domready', function() { 78 | // HACK: Put rounded corners on the infowindow 79 | $('#timezone_picker_infowindow').parent().parent().parent().prev().css('border-radius', 80 | '5px'); 81 | 82 | if (callback) { 83 | callback.apply($('#timezone_picker_infowindow')); 84 | } 85 | }); 86 | infowindow.setPosition(pos); 87 | infowindow.open(_map); 88 | 89 | _map.lastInfoWindow = infowindow; 90 | }; 91 | 92 | return { 93 | addPolygon: addPolygon, 94 | createPoint: createPoint, 95 | hideInfoWindow: hideInfoWindow, 96 | removePolygon: removePolygon, 97 | showInfoWindow: showInfoWindow 98 | }; 99 | }; 100 | 101 | var OpenLayersMapper = function(el, mouseClickHandler, mouseMoveHandler, mapOptions) { 102 | var infoWindow; 103 | 104 | // Create the maps instance 105 | var map = new OpenLayers.Map(OpenLayers.Util.extend({ 106 | div: el, 107 | projection: "EPSG:900913", 108 | displayProjection: "EPSG:4326", 109 | numZoomLevels: 18, 110 | controls: [ 111 | new OpenLayers.Control.Attribution(), 112 | new OpenLayers.Control.DragPan(), 113 | new OpenLayers.Control.Navigation({ 114 | mouseWheelOptions: { 115 | cumulative: false, 116 | maxDelta: 6, 117 | interval: 50 118 | }, 119 | zoomWheelEnabled: true 120 | }), 121 | new OpenLayers.Control.Zoom(), 122 | new OpenLayers.Control.ZoomBox() 123 | ] 124 | }, mapOptions)); 125 | 126 | var newLayer = new OpenLayers.Layer.OSM( 127 | "OSM Layer", 128 | "http://a.tile.openstreetmap.org/${z}/${x}/${y}.png" 129 | ); 130 | map.addLayer(newLayer); 131 | 132 | var vectors = new OpenLayers.Layer.Vector("vector"); 133 | map.addLayer(vectors); 134 | 135 | OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, { 136 | defaultHandlerOptions: { 137 | 'single': true, 138 | 'double': false, 139 | 'pixelTolerance': 0, 140 | 'stopSingle': false, 141 | 'stopDouble': false 142 | }, 143 | 144 | initialize: function() { 145 | this.handlerOptions = OpenLayers.Util.extend( 146 | {}, this.defaultHandlerOptions 147 | ); 148 | OpenLayers.Control.prototype.initialize.apply( 149 | this, arguments 150 | ); 151 | this.handler = new OpenLayers.Handler.Click( 152 | this, { 153 | click: this.trigger 154 | }, this.handlerOptions 155 | ); 156 | }, 157 | 158 | trigger: function(e) { 159 | var position = map.getLonLatFromViewPortPx(e.xy); 160 | position.transform( 161 | map.getProjectionObject(), 162 | new OpenLayers.Projection("EPSG:4326") 163 | ); 164 | mapClickHandler({ 165 | latLng: { 166 | lat: function() { 167 | return position.lat; 168 | }, 169 | lng: function() { 170 | return position.lon; 171 | } 172 | } 173 | }); 174 | } 175 | }); 176 | var click = new OpenLayers.Control.Click(); 177 | map.addControl(click); 178 | click.activate(); 179 | 180 | if (mouseMoveHandler) { 181 | map.events.register("mousemove", map, function(e) { 182 | var position = map.getLonLatFromViewPortPx(e.xy); 183 | position.transform( 184 | map.getProjectionObject(), 185 | new OpenLayers.Projection("EPSG:4326") 186 | ); 187 | mouseMoveHandler({ 188 | latLng: { 189 | lat: function() { 190 | return position.lat; 191 | }, 192 | lng: function() { 193 | return position.lon; 194 | } 195 | } 196 | }); 197 | }); 198 | } 199 | 200 | var onPolygonSelect = function(feature) { 201 | if (feature.clickHandler) { 202 | var position = map.getLonLatFromPixel(new OpenLayers.Pixel( 203 | polygonSelect.handlers.feature.evt.layerX, 204 | polygonSelect.handlers.feature.evt.layerY 205 | )); 206 | position.transform( 207 | map.getProjectionObject(), 208 | new OpenLayers.Projection("EPSG:4326") 209 | ); 210 | feature.clickHandler({ 211 | latLng: { 212 | lat: function() { 213 | return position.lat; 214 | }, 215 | lng: function() { 216 | return position.lon; 217 | } 218 | } 219 | }); 220 | } 221 | }; 222 | 223 | var onPolygonHighlight = function(e) { 224 | if (e.feature.hoverHandler) { 225 | e.feature.hoverHandler(); 226 | } 227 | }; 228 | 229 | var polygonHover = new OpenLayers.Control.SelectFeature(vectors, { 230 | hover: true, 231 | highlightOnly: true, 232 | renderIntent: "temporary", 233 | eventListeners: { 234 | beforefeaturehighlighted:onPolygonHighlight 235 | } 236 | }); 237 | 238 | var polygonSelect = new OpenLayers.Control.SelectFeature(vectors, { 239 | onSelect: onPolygonSelect 240 | }); 241 | 242 | map.addControl(polygonHover); 243 | map.addControl(polygonSelect); 244 | polygonHover.activate(); 245 | polygonSelect.activate(); 246 | 247 | map.setCenter(new OpenLayers.LonLat(0, 0), mapOptions.zoom); 248 | 249 | var addPolygon = function(coords, stroke, fill, clickHandler, mouseMoveHandler) { 250 | for (var i = 0; i < coords.length; i++) { 251 | coords[i].transform( 252 | new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984 253 | map.getProjectionObject() // to Spherical Mercator Projection 254 | ); 255 | } 256 | 257 | var style = { 258 | strokeColor: stroke.color, 259 | strokeOpacity: stroke.opacity, 260 | strokeWidth: stroke.width, 261 | fillColor: fill.color, 262 | fillOpacity: fill.opacity 263 | }; 264 | var linearRing = new OpenLayers.Geometry.LinearRing(coords); 265 | var feature = new OpenLayers.Feature.Vector( 266 | new OpenLayers.Geometry.Polygon(linearRing), null, style 267 | ); 268 | 269 | vectors.addFeatures([ feature ]); 270 | 271 | // NOTE: Stuff our click/mousemove handlers on the object for use in onPolygonSelect 272 | feature.clickHandler = clickHandler; 273 | feature.hoverHandler = mouseMoveHandler; 274 | 275 | return feature; 276 | }; 277 | 278 | var createPoint = function(lat, lng) { 279 | return new OpenLayers.Geometry.Point(lng, lat); 280 | }; 281 | 282 | var hideInfoWindow = function() { 283 | if (infoWindow) { 284 | map.removePopup(infoWindow); 285 | infoWindow.destroy(); 286 | infoWindow = null; 287 | } 288 | }; 289 | 290 | var removePolygon = function(mapPolygon) { 291 | vectors.removeFeatures([ mapPolygon ]); 292 | }; 293 | 294 | var showInfoWindow = function(pos, content, callback) { 295 | if (infoWindow) { 296 | hideInfoWindow(infoWindow); 297 | } 298 | 299 | pos = new OpenLayers.LonLat(pos.x, pos.y); 300 | pos.transform( 301 | new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984 302 | map.getProjectionObject() // to Spherical Mercator Projection 303 | ); 304 | 305 | infoWindow = new OpenLayers.Popup.FramedCloud('timezone_picker_infowindow', 306 | pos, 307 | new OpenLayers.Size(100,100), 308 | content, 309 | null, true, null); 310 | map.addPopup(infoWindow); 311 | 312 | // HACK: callback for popup using a set timeout 313 | if (callback) { 314 | setTimeout(function() { 315 | callback.apply($('#timezone_picker_infowindow')); 316 | }, 100); 317 | } 318 | }; 319 | 320 | return { 321 | addPolygon: addPolygon, 322 | createPoint: createPoint, 323 | hideInfoWindow: hideInfoWindow, 324 | removePolygon: removePolygon, 325 | showInfoWindow: showInfoWindow 326 | }; 327 | }; 328 | 329 | // Forward declarations to satisfy jshint 330 | var hideLoader, hitTestAndConvert, selectPolygonZone, 331 | showInfoWindow, slugifyName; 332 | 333 | var clearHover = function() { 334 | $.each(_hoverPolygons, function(i, p) { 335 | _mapper.removePolygon(p); 336 | }); 337 | 338 | _hoverPolygons = []; 339 | }; 340 | 341 | var clearZones = function() { 342 | $.each(_mapZones, function(i, zone) { 343 | $.each(zone, function(j, polygon) { 344 | _mapper.removePolygon(polygon); 345 | }); 346 | }); 347 | 348 | _mapZones = {}; 349 | }; 350 | 351 | var drawZone = function(name, lat, lng, callback) { 352 | if (_mapZones[name]) { 353 | return; 354 | } 355 | 356 | $.get(_options.jsonRootUrl + 'polygons/' + name + '.json', function(data) { 357 | _needsLoader--; 358 | if (_needsLoader === 0 && _loader) { 359 | hideLoader(); 360 | } 361 | 362 | if (callback) { 363 | callback(); 364 | } 365 | 366 | data = typeof data === 'string' ? JSON.parse(data) : data; 367 | 368 | _mapZones[name] = []; 369 | $.extend(_transitions, data.transitions); 370 | 371 | var result = hitTestAndConvert(data.polygons, lat, lng); 372 | 373 | if (result.inZone) { 374 | _selectedRegionKey = name; 375 | $.each(result.allPolygons, function(i, polygonInfo) { 376 | var mapPolygon = _mapper.addPolygon(polygonInfo.coords, { 377 | color: '#ff0000', 378 | opacity: 0.7, 379 | width: 1 380 | }, { 381 | color: '#ffcccc', 382 | opacity: 0.5 383 | }, function() { 384 | selectPolygonZone(polygonInfo.polygon); 385 | }, clearHover); 386 | 387 | _mapZones[name].push(mapPolygon); 388 | }); 389 | 390 | selectPolygonZone(result.selectedPolygon); 391 | } 392 | }).fail(function() { 393 | console.warn(arguments); 394 | }); 395 | }; 396 | 397 | var getCurrentTransition = function(transitions) { 398 | if (transitions.length === 1) { 399 | return transitions[0]; 400 | } 401 | 402 | var now = _options.date.getTime() / 1000; 403 | var selected = null; 404 | $.each(transitions, function(i, transition) { 405 | if (transition[0] < now && i < transitions.length - 1 && 406 | transitions[i + 1][0] > now) { 407 | selected = transition; 408 | } 409 | }); 410 | 411 | // If we couldn't find a matching transition, just use the first one 412 | // NOTE: This will sometimes be wrong for events in the past 413 | if (!selected) { 414 | selected = transitions[0]; 415 | } 416 | 417 | return selected; 418 | }; 419 | 420 | var hideInfoWindow = function() { 421 | _mapper.hideInfoWindow(); 422 | }; 423 | 424 | hideLoader = function() { 425 | _loader.remove(); 426 | _loader = null; 427 | }; 428 | 429 | hitTestAndConvert = function(polygons, lat, lng) { 430 | var allPolygons = []; 431 | var inZone = false; 432 | var selectedPolygon; 433 | $.each(polygons, function(i, polygon) { 434 | // Ray casting counter for hit testing. 435 | var rayTest = 0; 436 | var lastPoint = polygon.points.slice(-2); 437 | 438 | var coords = []; 439 | var j = 0; 440 | for (j = 0; j < polygon.points.length; j += 2) { 441 | var point = polygon.points.slice(j, j + 2); 442 | 443 | coords.push(_mapper.createPoint(point[0], point[1])); 444 | 445 | // Ray casting test 446 | if ((lastPoint[0] <= lat && point[0] >= lat) || 447 | (lastPoint[0] > lat && point[0] < lat)) { 448 | var slope = (point[1] - lastPoint[1]) / (point[0] - lastPoint[0]); 449 | var testPoint = slope * (lat - lastPoint[0]) + lastPoint[1]; 450 | if (testPoint < lng) { 451 | rayTest++; 452 | } 453 | } 454 | 455 | lastPoint = point; 456 | } 457 | 458 | allPolygons.push({ 459 | polygon: polygon, 460 | coords: coords 461 | }); 462 | 463 | // If the count is odd, we are in the polygon 464 | var odd = (rayTest % 2 === 1); 465 | inZone = inZone || odd; 466 | if (odd) { 467 | selectedPolygon = polygon; 468 | } 469 | }); 470 | 471 | return { 472 | allPolygons: allPolygons, 473 | inZone: inZone, 474 | selectedPolygon: selectedPolygon 475 | }; 476 | }; 477 | 478 | var mapClickHandler = function(e) { 479 | if (_needsLoader > 0) { 480 | return; 481 | } 482 | 483 | hideInfoWindow(); 484 | 485 | var lat = e.latLng.lat(); 486 | var lng = e.latLng.lng(); 487 | 488 | var candidates = []; 489 | $.each(_boundingBoxes, function(i, v) { 490 | var bb = v.boundingBox; 491 | if (lat > bb.ymin && lat < bb.ymax && 492 | lng > bb.xmin && 493 | lng < bb.xmax) { 494 | candidates.push(slugifyName(v.name)); 495 | } 496 | }); 497 | 498 | _needsLoader = candidates.length; 499 | setTimeout(function() { 500 | if (_needsLoader > 0) { 501 | showLoader(); 502 | } 503 | }, 500); 504 | 505 | clearZones(); 506 | $.each(candidates, function(i, v) { 507 | drawZone(v, lat, lng, function() { 508 | $.each(_hoverPolygons, function(i, p) { 509 | _mapper.removePolygon(p); 510 | }); 511 | _hoverPolygons = []; 512 | _currentHoverRegion = null; 513 | }); 514 | }); 515 | }; 516 | 517 | var mouseMoveHandler = function(e) { 518 | var lat = e.latLng.lat(); 519 | var lng = e.latLng.lng(); 520 | 521 | $.each(_boundingBoxes, function(i, v) { 522 | var bb = v.boundingBox; 523 | if (lat > bb.ymin && lat < bb.ymax && 524 | lng > bb.xmin && 525 | lng < bb.xmax) { 526 | var hoverRegion = _hoverRegions[v.name]; 527 | if (!hoverRegion) { 528 | return; 529 | } 530 | 531 | var result = hitTestAndConvert(hoverRegion.hoverRegion, lat, lng); 532 | var slugName = slugifyName(v.name); 533 | if (result.inZone && slugName !== _currentHoverRegion && 534 | slugName !== _selectedRegionKey) { 535 | clearHover(); 536 | _currentHoverRegion = slugName; 537 | 538 | $.each(result.allPolygons, function(i, polygonInfo) { 539 | var mapPolygon = _mapper.addPolygon(polygonInfo.coords, { 540 | color: '#444444', 541 | opacity: 0.7, 542 | width: 1 543 | }, { 544 | color: '#888888', 545 | opacity: 0.5 546 | }, mapClickHandler, null); 547 | 548 | _hoverPolygons.push(mapPolygon); 549 | }); 550 | 551 | if (_options.onHover) { 552 | var transition = getCurrentTransition(hoverRegion.transitions); 553 | _options.onHover(transition[1], transition[2]); 554 | } 555 | } 556 | } 557 | }); 558 | }; 559 | 560 | selectPolygonZone = function(polygon) { 561 | _selectedPolygon = polygon; 562 | 563 | var transition = getCurrentTransition( 564 | _transitions[polygon.name]); 565 | 566 | var olsonName = polygon.name; 567 | var utcOffset = transition[1]; 568 | var tzName = transition[2]; 569 | 570 | if (_options.onSelected) { 571 | _options.onSelected(olsonName, utcOffset, tzName); 572 | } 573 | else { 574 | var pad = function(d) { 575 | if (d < 10) { 576 | return '0' + d; 577 | } 578 | return d.toString(); 579 | }; 580 | 581 | var now = new Date(); 582 | var adjusted = new Date(); 583 | adjusted.setTime(adjusted.getTime() + 584 | (adjusted.getTimezoneOffset() + utcOffset) * 60 * 1000); 585 | 586 | showInfoWindow('

' + 587 | olsonName + ' ' + 588 | '(' + tzName + ')

' + 589 | ''); 601 | } 602 | }; 603 | 604 | showInfoWindow = function(content, callback) { 605 | // Hack to get the centroid of the largest polygon - we just check 606 | // which has the most edges 607 | var centroid; 608 | var maxPoints = 0; 609 | if (_selectedPolygon.points.length > maxPoints) { 610 | centroid = _selectedPolygon.centroid; 611 | maxPoints = _selectedPolygon.points.length; 612 | } 613 | 614 | hideInfoWindow(); 615 | 616 | _mapper.showInfoWindow(_mapper.createPoint(centroid[1], centroid[0]), content, 617 | callback); 618 | }; 619 | 620 | var showLoader = function() { 621 | _loader = $('
' + 623 | '
'); 628 | _loader.height(_self.height()).width(_self.width()); 629 | _self.append(_loader); 630 | }; 631 | 632 | slugifyName = function(name) { 633 | return name.toLowerCase().replace(/[^a-z0-9]/g, '-'); 634 | }; 635 | 636 | var methods = { 637 | init: function(options) { 638 | _self = this; 639 | 640 | // Populate the options and set defaults 641 | _options = options || {}; 642 | _options.initialZoom = _options.initialZoom || 2; 643 | _options.initialLat = _options.initialLat || 0; 644 | _options.initialLng = _options.initialLng || 0; 645 | _options.strokeColor = _options.strokeColor || '#ff0000'; 646 | _options.strokeWeight = _options.strokeWeight || 2; 647 | _options.strokeOpacity = _options.strokeOpacity || 0.7; 648 | _options.fillColor = _options.fillColor || '#ffcccc'; 649 | _options.fillOpacity = _options.fillOpacity || 0.5; 650 | _options.jsonRootUrl = _options.jsonRootUrl || 'tz_json/'; 651 | _options.date = _options.date || new Date(); 652 | 653 | _options.mapOptions = $.extend({ 654 | zoom: _options.initialZoom, 655 | centerLat: _options.initialLat, 656 | centerLng: _options.initialLng 657 | }, _options.mapOptions); 658 | 659 | if (typeof _options.hoverRegions === 'undefined') { 660 | _options.hoverRegions = true; 661 | } 662 | 663 | if (_options.useOpenLayers) { 664 | _mapper = new OpenLayersMapper(_self.get(0), 665 | mapClickHandler, 666 | _options.hoverRegions ? mouseMoveHandler : null, 667 | _options.mapOptions); 668 | } 669 | else { 670 | _mapper = new GoogleMapsMapper(_self.get(0), 671 | mapClickHandler, 672 | _options.hoverRegions ? mouseMoveHandler : null, 673 | _options.mapOptions); 674 | } 675 | 676 | // Load the necessary data files 677 | var loadCount = _options.hoverRegions ? 2 : 1; 678 | var checkLoading = function() { 679 | loadCount--; 680 | if (loadCount === 0) { 681 | hideLoader(); 682 | 683 | if (_options.onReady) { 684 | _options.onReady(); 685 | } 686 | } 687 | }; 688 | 689 | showLoader(); 690 | $.get(_options.jsonRootUrl + 'bounding_boxes.json', function(data) { 691 | _boundingBoxes = typeof data === 'string' ? JSON.parse(data) : data; 692 | $.each(_boundingBoxes, function(i, bb) { 693 | $.extend(_zoneCentroids, bb.zoneCentroids); 694 | }); 695 | checkLoading(); 696 | }); 697 | 698 | if (_options.hoverRegions) { 699 | $.get(_options.jsonRootUrl + 'hover_regions.json', function(data) { 700 | var hoverData = typeof data === 'string' ? JSON.parse(data) : data; 701 | $.each(hoverData, function(i, v) { 702 | _hoverRegions[v.name] = v; 703 | }); 704 | checkLoading(); 705 | }); 706 | } 707 | }, 708 | setDate: function(date) { 709 | hideInfoWindow(); 710 | _options.date = date; 711 | }, 712 | hideInfoWindow: hideInfoWindow, 713 | showInfoWindow: function(content, callback) { 714 | showInfoWindow(content, callback); 715 | }, 716 | selectZone: function(olsonName) { 717 | var centroid = _zoneCentroids[olsonName]; 718 | if (centroid) { 719 | mapClickHandler({ 720 | latLng: { 721 | lat: function() { 722 | return centroid[1]; 723 | }, 724 | lng: function() { 725 | return centroid[0]; 726 | } 727 | } 728 | }); 729 | } 730 | } 731 | }; 732 | 733 | $.fn.timezonePicker = function(method) { 734 | 735 | if (methods[method]) { 736 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 737 | } 738 | else if (typeof method === 'object' || !method) { 739 | return methods.init.apply(this, arguments); 740 | } 741 | else { 742 | $.error('Method ' + method + ' does not exist on jQuery.timezonePicker.'); 743 | } 744 | }; 745 | 746 | _loaderGif = "data:image/gif;base64,R0lGODlhEAAQAPIAAKqqqv///729vejo6P///93d3dPT083NzSH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA=="; 747 | _maskPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDgA6CHKQBUUAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGDwAQAAUQBNbrgEdAAAAABJRU5ErkJggg=="; 748 | })(jQuery); 749 | -------------------------------------------------------------------------------- /openlayers_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 29 | 30 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | (cd example_site;python -m SimpleHTTPServer) 3 | -------------------------------------------------------------------------------- /scripts/dbfUtils.py: -------------------------------------------------------------------------------- 1 | import struct, datetime, decimal, itertools 2 | 3 | def dbfreader(f): 4 | """Returns an iterator over records in a Xbase DBF file. 5 | 6 | The first row returned contains the field names. 7 | The second row contains field specs: (type, size, decimal places). 8 | Subsequent rows contain the data records. 9 | If a record is marked as deleted, it is skipped. 10 | 11 | File should be opened for binary reads. 12 | 13 | """ 14 | # See DBF format spec at: 15 | # http://www.pgts.com.au/download/public/xbase.htm#DBF_STRUCT 16 | 17 | numrec, lenheader = struct.unpack('= collation_now: 70 | collation_key += "%d>%d," % (t[0], t[1]) 71 | 72 | # for non-daylight savings regions, just use the utc_offset 73 | if len(collation_key) == 0: 74 | collation_key = "0>%d" % transition_info[-1][1] 75 | 76 | zones[collation_key] = zones.get(collation_key, { 77 | "bounding_box": { 78 | "xmin": sys.maxint, 79 | "ymin": sys.maxint, 80 | "xmax":-sys.maxint - 1, 81 | "ymax":-sys.maxint - 1 82 | }, 83 | "polygons": [], 84 | "transitions": {}, 85 | "name": name 86 | }) 87 | 88 | zones[collation_key]["transitions"][name] = transition_info 89 | 90 | polygons = reduce_polygons(shp_data, 0.1, 0.01, 4, 5000, 0, 0.05) 91 | 92 | for part in polygons: 93 | polygonInfo = simplify(part["points"]) 94 | polygonInfo["name"] = name 95 | zones[collation_key]["polygons"].append(polygonInfo) 96 | 97 | b = zones[collation_key]["bounding_box"] 98 | b["xmin"] = min(b["xmin"], polygonInfo["bounds"][0]) 99 | b["ymin"] = min(b["ymin"], polygonInfo["bounds"][1]) 100 | b["xmax"] = max(b["xmax"], polygonInfo["bounds"][2]) 101 | b["ymax"] = max(b["ymax"], polygonInfo["bounds"][3]) 102 | del polygonInfo["bounds"] 103 | 104 | return zones 105 | 106 | def convert_points(polygons): 107 | # Convert {x,y} to [lat,lng], for more compact JSON 108 | for polygon in polygons: 109 | polygon["points"] = reduce(lambda x, y: x + [y["y"], y["x"]], 110 | polygon["points"], []) 111 | return polygons 112 | 113 | def reduce_json(jsonString, maxPrecision=6): 114 | reduced_precision = re.sub( 115 | r'(\d)\.(\d{' + str(maxPrecision) + r'})(\d+)', r'\1.\2', 116 | jsonString 117 | ) 118 | 119 | return re.sub(r'\s', '', reduced_precision) 120 | 121 | def reduce_polygons(polygonData, hullAreaThreshold, bufferDistance, 122 | bufferResolution, numThreshold, areaThreshold, 123 | simplifyThreshold): 124 | polygons = [] 125 | for p in polygonData: 126 | polygon = Polygon(map(lambda x: (x["x"], x["y"]), 127 | p["points"])) 128 | 129 | # For very small regions, use a convex hull 130 | if polygon.area < hullAreaThreshold: 131 | polygon = polygon.convex_hull 132 | # Also buffer by a small distance to aid the cascaded union 133 | polygon = polygon.buffer(bufferDistance, bufferResolution) 134 | 135 | polygons.append(polygon) 136 | 137 | # Try to merge some polygons 138 | polygons = cascaded_union(polygons) 139 | 140 | # Normalize the Polygon or MultiPolygon into an array 141 | if "exterior" in dir(polygons): 142 | polygons = [polygons] 143 | else: 144 | polygons = [p for p in polygons] 145 | 146 | region = [] 147 | # Sort from largest to smallest to faciliate dropping of small regions 148 | polygons.sort(key=lambda x:-x.area) 149 | for p in polygons: 150 | # Try to include regions that are big enough, once we have a 151 | # few representative regions 152 | if len(region) > numThreshold and p.area < areaThreshold: 153 | break 154 | 155 | p = p.simplify(simplifyThreshold) 156 | region.append({ 157 | "points": map(lambda x: {"x": x[0], "y": x[1]}, 158 | p.exterior.coords) 159 | }) 160 | 161 | return region 162 | 163 | def simplify(points): 164 | polygon = Polygon(map(lambda x: (x["x"], x["y"]), points)) 165 | polygon = polygon.simplify(0.05) 166 | 167 | return { 168 | "points": map(lambda x: {"x": x[0], "y": x[1]}, 169 | polygon.exterior.coords), 170 | "centroid": (polygon.centroid.x, polygon.centroid.y), 171 | "bounds": polygon.bounds, 172 | "area": polygon.area 173 | } 174 | 175 | def timedelta_to_minutes(td): 176 | return td.days * 24 * 60 + td.seconds / 60 177 | 178 | if __name__ == '__main__': 179 | if len(sys.argv) < 3: 180 | print 'Usage: python gen_json.py ' 181 | sys.exit(1) 182 | 183 | zones = collate_zones(sys.argv[1]) 184 | boxes = [] 185 | hovers = [] 186 | 187 | output_dir = sys.argv[2] 188 | os.mkdir(os.path.join(output_dir, "polygons")) 189 | for key, zone in zones.iteritems(): 190 | # calculate a hover region 191 | sys.stderr.write('Calculating hover region for %s\n' % zone["name"]) 192 | hover_region = reduce_polygons(zone["polygons"], 1, 0.1, 4, 3, 0.5, 193 | 0.05) 194 | 195 | # Merge transitions information for all contained timezones 196 | hoverTransitions = [] 197 | zone_transitions = zone["transitions"].values() 198 | for i, transition in enumerate(zone_transitions[0]): 199 | tzNames = {} 200 | for zone_transition in zone_transitions: 201 | tzNames[zone_transition[i][2]] = tzNames.get( 202 | zone_transition[i][2], 0) + 1 203 | 204 | hoverTransitions.append([ 205 | transition[0], 206 | transition[1], 207 | map(lambda x: x[0], 208 | sorted(tzNames.iteritems(), key=lambda x:-x[1])) 209 | ]) 210 | 211 | hovers.append({ 212 | "name": zone["name"], 213 | "hoverRegion": convert_points(hover_region), 214 | "transitions": hoverTransitions 215 | }) 216 | 217 | # Get a centroid for the largest polygon in each zone 218 | zone_centroids = {} 219 | for polygon in zone["polygons"]: 220 | zone_centroid = zone_centroids.get(polygon["name"], { 221 | "centroid": (0, 0), 222 | "area": 0 223 | }) 224 | 225 | if polygon["area"] > zone_centroid["area"]: 226 | zone_centroids[polygon["name"]] = { 227 | "centroid": polygon["centroid"], 228 | "area": polygon["area"] 229 | } 230 | 231 | # Don't need this anymore, so purge it to save some JSON space 232 | del polygon["area"] 233 | 234 | boxes.append({ 235 | "name": zone["name"], 236 | "boundingBox": zone["bounding_box"], 237 | "zoneCentroids": dict(map( 238 | lambda x: (x[0], x[1]["centroid"]), zone_centroids.iteritems() 239 | )) 240 | }) 241 | 242 | filename = re.sub(r'[^a-z0-9]+', '-', zone["name"].lower()) 243 | out_file = os.path.join(output_dir, "polygons", "%s.json" % filename) 244 | open(out_file, "w").write( 245 | reduce_json(simplejson.dumps({ 246 | "name": zone["name"], 247 | "polygons": convert_points(zone["polygons"]), 248 | "transitions": zone["transitions"] 249 | }), 5)) 250 | 251 | open(os.path.join(output_dir, "bounding_boxes.json"), "w").write( 252 | reduce_json(simplejson.dumps(boxes), 2) 253 | ) 254 | open(os.path.join(output_dir, "hover_regions.json"), "w").write( 255 | reduce_json(simplejson.dumps(hovers), 2) 256 | ) 257 | -------------------------------------------------------------------------------- /scripts/shpUtils.py: -------------------------------------------------------------------------------- 1 | from struct import unpack 2 | import dbfUtils, math 3 | XY_POINT_RECORD_LENGTH = 16 4 | db = [] 5 | 6 | def loadShapefile(file_name): 7 | global db 8 | shp_bounding_box = [] 9 | shp_type = 0 10 | file_name = file_name 11 | records = [] 12 | # open dbf file and get records as a list 13 | dbf_file = file_name[0:-4] + '.dbf' 14 | dbf = open(dbf_file, 'rb') 15 | db = list(dbfUtils.dbfreader(dbf)) 16 | dbf.close() 17 | fp = open(file_name, 'rb') 18 | 19 | # get basic shapefile configuration 20 | fp.seek(32) 21 | shp_type = readAndUnpack('i', fp.read(4)) 22 | shp_bounding_box = readBoundingBox(fp) 23 | 24 | # fetch Records 25 | fp.seek(100) 26 | while True: 27 | shp_record = createRecord(fp) 28 | if shp_record == False: 29 | break 30 | records.append(shp_record) 31 | 32 | return records 33 | 34 | record_class = {0:'RecordNull', 1:'RecordPoint', 8:'RecordMultiPoint', 3:'RecordPolyLine', 5:'RecordPolygon'} 35 | 36 | def createRecord(fp): 37 | # read header 38 | record_number = readAndUnpack('>L', fp.read(4)) 39 | if record_number == '': 40 | print 'doner' 41 | return False 42 | content_length = readAndUnpack('>L', fp.read(4)) 43 | record_shape_type = readAndUnpack(' maxarea: 177 | maxarea = ringArea 178 | biggest = ring 179 | #now get the true centroid 180 | tempPoint = {'x':0, 'y':0} 181 | if biggest[points][0] != biggest[points][len(biggest[points])-1]: 182 | print "mug", biggest[points][0], biggest[points][len(biggest[points])-1] 183 | for i in range(0, len(biggest[points])-1): 184 | j = (i + 1) % (len(biggest[points])-1) 185 | tempPoint['x'] -= (biggest[points][i]['x'] + biggest[points][j]['x']) * ((biggest[points][i]['x'] * biggest[points][j]['y']) - (biggest[points][j]['x'] * biggest[points][i]['y'])) 186 | tempPoint['y'] -= (biggest[points][i]['y'] + biggest[points][j]['y']) * ((biggest[points][i]['x'] * biggest[points][j]['y']) - (biggest[points][j]['x'] * biggest[points][i]['y'])) 187 | 188 | tempPoint['x'] = tempPoint['x'] / ((6) * maxarea) 189 | tempPoint['y'] = tempPoint['y'] / ((6) * maxarea) 190 | feature['shp_data']['truecentroid'] = tempPoint 191 | 192 | 193 | def getArea(ring, points): 194 | #returns the area of a polygon 195 | #needs to be spherical area, but isn't 196 | area = 0 197 | for i in range(0,len(ring[points])-1): 198 | j = (i + 1) % (len(ring[points])-1) 199 | area += ring[points][i]['x'] * ring[points][j]['y'] 200 | area -= ring[points][i]['y'] * ring[points][j]['x'] 201 | 202 | return math.fabs(area/2) 203 | 204 | 205 | def getNeighbors(records): 206 | 207 | #for each feature 208 | for i in range(len(records)): 209 | #print i, records[i]['dbf_data']['ADMIN_NAME'] 210 | if not 'neighbors' in records[i]['shp_data']: 211 | records[i]['shp_data']['neighbors'] = [] 212 | 213 | #for each other feature 214 | for j in range(i+1, len(records)): 215 | numcommon = 0 216 | #first check to see if the bounding boxes overlap 217 | if overlap(records[i], records[j]): 218 | #if so, check every single point in this feature to see if it matches a point in the other feature 219 | 220 | #for each part: 221 | for part in records[i]['shp_data']['parts']: 222 | 223 | #for each point: 224 | for point in part['points']: 225 | 226 | for otherPart in records[j]['shp_data']['parts']: 227 | if point in otherPart['points']: 228 | numcommon += 1 229 | if numcommon == 2: 230 | if not 'neighbors' in records[j]['shp_data']: 231 | records[j]['shp_data']['neighbors'] = [] 232 | records[i]['shp_data']['neighbors'].append(j) 233 | records[j]['shp_data']['neighbors'].append(i) 234 | #now break out to the next j 235 | break 236 | if numcommon == 2: 237 | break 238 | if numcommon == 2: 239 | break 240 | 241 | 242 | 243 | 244 | def projectShapefile(records, whatProjection, lonCenter=0, latCenter=0): 245 | print 'projecting to ', whatProjection 246 | for feature in records: 247 | for part in feature['shp_data']['parts']: 248 | part['projectedPoints'] = [] 249 | for point in part['points']: 250 | tempPoint = projectPoint(point, whatProjection, lonCenter, latCenter) 251 | part['projectedPoints'].append(tempPoint) 252 | 253 | def projectPoint(fromPoint, whatProjection, lonCenter, latCenter): 254 | latRadians = fromPoint['y'] * math.pi/180 255 | if latRadians > 1.5: latRadians = 1.5 256 | if latRadians < -1.5: latRadians = -1.5 257 | lonRadians = fromPoint['x'] * math.pi/180 258 | lonCenter = lonCenter * math.pi/180 259 | latCenter = latCenter * math.pi/180 260 | newPoint = {} 261 | if whatProjection == "MERCATOR": 262 | newPoint['x'] = (180/math.pi) * (lonRadians - lonCenter) 263 | newPoint['y'] = (180/math.pi) * math.log(math.tan(latRadians) + (1/math.cos(latRadians))) 264 | if newPoint['y'] > 200: 265 | newPoint['y'] = 200 266 | if newPoint['y'] < -200: 267 | newPoint['y'] = 200 268 | return newPoint 269 | if whatProjection == "EQUALAREA": 270 | newPoint['x'] = 0 271 | newPoint['y'] = 0 272 | return newPoint 273 | 274 | 275 | def overlap(feature1, feature2): 276 | if (feature1['shp_data']['xmax'] > feature2['shp_data']['xmin'] and feature1['shp_data']['ymax'] > feature2['shp_data']['ymin'] and feature1['shp_data']['xmin'] < feature2['shp_data']['xmax'] and feature1['shp_data']['ymin'] < feature2['shp_data']['ymax']): 277 | return True 278 | else: 279 | return False 280 | -------------------------------------------------------------------------------- /setup_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p example_site 3 | cd openlayers/build 4 | ./build.py light.cfg 5 | cd ../.. 6 | cp gmaps_example.html example_site 7 | cp openlayers_example.html example_site 8 | cp jquery.timezone-picker.js example_site 9 | cp -R openlayers/build/OpenLayers.js example_site 10 | cp -R openlayers/img example_site 11 | cp -R openlayers/theme example_site 12 | cp tz_json.tgz example_site 13 | cd example_site 14 | tar xvf tz_json.tgz 15 | cd .. 16 | -------------------------------------------------------------------------------- /tz_json.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dosx/timezone-picker/f2aee40744ba81dfa991774516d37651e21c40c6/tz_json.tgz --------------------------------------------------------------------------------