├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── geojson-conversion.sh ├── getOriginalData.sh ├── index.html ├── setup.py ├── tests ├── requirements.txt ├── test_locations.py └── test_utilities.py └── tzwhere ├── __init__.py ├── tz_world.json.gz ├── tz_world_shortcuts.json └── tzwhere.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | *__init__* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .project 3 | .pydevproject 4 | 5 | # Packages 6 | *.egg 7 | *.egg-info 8 | dist 9 | build 10 | eggs 11 | parts 12 | bin 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | 18 | # Installer logs 19 | pip-log.txt 20 | 21 | # Unit test / coverage reports 22 | .coverage 23 | .tox 24 | 25 | #Translations 26 | *.mo 27 | 28 | #Mr Developer 29 | .mr.developer.cfg 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | # command to install dependencies 8 | install: 9 | - pip install -e . 10 | - pip install -r tests/requirements.txt 11 | # command to run tests 12 | script: 13 | - nosetests --with-coverage --cover-branches --cover-inclusive --cover-package=tzwhere 14 | after_script: 15 | coveralls 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Matt Pegler and other contributors 4 | https://github.com/pegler/pytzwhere 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include tzwhere/tz_world_shortcuts.json 4 | include tzwhere/tz_world.json.gz 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pytzwhere [![Build Status](https://travis-ci.org/pegler/pytzwhere.svg)](https://travis-ci.org/pegler/pytzwhere) [![Coverage Status](https://coveralls.io/repos/pegler/pytzwhere/badge.svg)](https://coveralls.io/r/pegler/pytzwhere) 2 | ========= 3 | 4 | pytzwhere is a Python library to lookup the timezone for a given lat/lng entirely offline. 5 | 6 | Version 3.0 fixes how `pytzwhere` deals with [holes](https://github.com/pegler/pytzwhere/issues/34) in timezones. It is recommended that you use version 3.0. 7 | 8 | It is a port from https://github.com/mattbornski/tzwhere with a few improvements. The underlying timezone data is based on work done by [Eric Muller](http://efele.net/maps/tz/world/) 9 | 10 | If used as a library, basic usage is as follows: 11 | 12 | >>> from tzwhere import tzwhere 13 | >>> tz = tzwhere.tzwhere() 14 | >>> print tz.tzNameAt(35.29, -89.66) 15 | America/Chicago 16 | 17 | The polygons used for building the timezones are based on VMAP0. Sometimes points are outside a VMAP0 polygon, but are clearly within a certain timezone (see also this [discussion](https://github.com/mattbornski/tzwhere/issues/8)). As a solution you can search for the closest timezone within a user defined radius. 18 | 19 | 20 | 21 | Dependencies: 22 | 23 | * `numpy` (optional) 24 | 25 | * `shapely` 26 | 27 | 28 | 29 | **forceTZ** 30 | 31 | If the coordinates provided are outside of the currently defined timezone boundaries, the `tzwhere` function will return `None`. If you would like to match to the closest timezone, use the forceTZ parameter. 32 | 33 | Example: 34 | 35 | >>> from tzwhere import tzwhere 36 | >>> tz = tzwhere.tzwhere() 37 | >>> print(tz.tzNameAt(53.68193999999999, -6.239169999999998)) 38 | None 39 | 40 | >>> from tzwhere import tzwhere 41 | >>> tz = tzwhere.tzwhere(forceTZ=True) 42 | >>> print(tz.tzNameAt(53.68193999999999, -6.239169999999998, forceTZ=True)) 43 | Europe/Dublin 44 | 45 | -------------------------------------------------------------------------------- /geojson-conversion.sh: -------------------------------------------------------------------------------- 1 | # Bulk convert shapefiles to geojson using ogr2ogr 2 | # For more information, see http://ben.balter.com/2013/06/26/how-to-convert-shapefiles-to-geojson-for-use-on-github/ 3 | 4 | # Note: Assumes you're in a folder with one or more zip files containing shape files 5 | # and Outputs as geojson with the crs:84 SRS (for use on GitHub or elsewhere) 6 | 7 | #geojson conversion 8 | function shp2geojson() { 9 | ogr2ogr -f GeoJSON -t_srs crs:84 "$1.json" "$1.shp" 10 | } 11 | 12 | #convert all shapefiles 13 | for var in *.shp; do shp2geojson ${var%\.*}; done 14 | -------------------------------------------------------------------------------- /getOriginalData.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Author: Christoph Stich 3 | # This is a basic script that downloads the original data and converts it to a JSON 4 | wget http://efele.net/maps/tz/world/tz_world.zip 5 | unzip tz_world 6 | ogr2ogr -f GeoJSON -t_srs crs:84 tz_world.json ./world/tz_world.shp 7 | mv tz_world.json tzwhere/ 8 | rm ./world/ -r 9 | rm tz_world.zip 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | A shapefile of the TZ timezones of the world 16 | 21 | 22 | 23 | 24 |

tz_world, an efele.net/tz map

25 |

A shapefile of the TZ timezones of the world

26 |

Last data update: March 20, 2016

27 |

Last page update: March 20, 2016

28 |

TZ version: 2016b

29 | 30 |

 

31 | 32 |

The tz_world shapefile (zip, sha1) captures the boundaries of the TZ timezones across the world, as of TZ 2016b. The geometries are all POLYGONs, and a TZ timezone will sometimes have multiple polygons. There are about 28,000 rows.

33 | 34 |

The tz_world_mp shapefile (zip, sha1) captures the same boundaries. The geometries are either POLYGONs or MULTIPOLYGONs, and there is a single geometry for each TZ timezone.

35 | 36 |

There is a companion map for the TZ timezones used in Antarctica stations.

37 | 38 |

The geometries are primarily derived from the fip10s data (itself derived from the VMAP0 data), augmented with data presented in the pages for the maps of the United States, Canada, Russia and China.

39 |

Snapshot of the zones

40 |

This is a snaphsot of the zones (many of the smaller zones are not visible at this scale):

41 |

42 | 43 |

 

44 |

Timezones at sea

45 |

The tz database says: “A ship within the territorial waters of any nation uses that nation's time. In international waters, time zone boundaries are meridians 15° apart, except that UTC−12 and UTC+12 are each 7.5° wide and are separated by the 180° meridian (not by the International Date Line, which is for land and territorial waters only). A captain can change ship's clocks any time after entering a new time zone; midnight changes are common.”

46 |

While the boundaries in international waters are not difficult to construct, the boundaries of territorial waters are a completely different story, and are similar to the boundaries between countries. Unfortunately, VMAP0 does not provide geometries for the territorial waters. As a consequence, the shapefiles presented here do not cover seas and oceans.

47 |

 

48 |

Logical description of the zones

49 |

The vast majority of the tz timezones are by construction matching a country, so little needs to be said about them: we equate them with the corresponding region in our fips10s data. We also discussed the situation of the United States, Canada, Russia and China in other pages. That leaves only a few countries with multiple time zones, and again, most of them are straightforward, with timezone matching administrative divisions of the countries: the correspondance is documented in the script we use to build the shapefile (see below). The rest of this section discusses the remaining cases, where the definition of the extent of a zone is open to interpretation.

50 |

Uzbekistan

51 |

This country is covered by two tz timezones, Tashkent and Samarkand. There is not enough information in the tz data to figure out how Uzbekistan is divided between them. We use a separation along first-level administrative divisons that matches WTE.

52 |

Ukraine

53 |

This country is covered by four tz timezones. There is not enough information in the tz data to figure out how Ukraine is divided between them. We use a separation along first-level administrative divisons that matches WTE.

54 |

Australia

55 |

The extent of the Eucla zone is not clear from the tz data. The only clue is that the timezone ends just east of Caiguna. We arbitrarily make that zone the intersection of a rectangle and Western Australia: Caiguna is a 125.490E, so we use somewhat arbitrarily 125.5E as the west boundary; the north boundary is arbitrarily -31.3S; the south boundary is the ocean; the east boundary is the Western Australia/South Australia border.

56 |

The Lindeman zone is said by tz to include the three islands of Hayman, Lindeman, Hamilton, all in Queensland. There are other islands in this area, but we don't include them, for lack of better information.

57 |

The Broken_Hill timezone is defined by law as the county of Yancowinna, New South Wales. The geometry was obtained by georeferencing and tracing of this image of the counties of New South Wales

58 |

Mongolia

59 |

It is difficult to obtain reliable and verifiable information on time zones in Mongolia: see this summary. We use the following setting:

60 | 66 |

Cuba

67 |

According to this report,

68 |
69 |

Regardless of what Cuba does, the U.S. Naval Base Guantanamo Bay will always be in the Eastern Time zone and will always observe daylight saving time as this allows the naval base to remain in synch with their headquarters in Florida. This information was confirmed with the assistance of the Public Affairs Officer at the U.S. Naval Base Guantanamo Bay.

70 |
71 |

Accordingly, we assign it the America/New_York timezone.

72 |

Marshall Islands

73 |

The Kwajalein zone is certainly no less than the 11 islands leased by the US on the Kwajalein atoll. It’s also very unlikely to be more than the Kwajalein atoll itself. We use the whole atoll as the extent of the zone.

74 | 75 |

Spain

76 |

Most of Spain is easy. The open question concerns the Plazas de soberania. Clearly, Ceuta is in Africa/Ceuta, but the timezone of Melilla, Chafarinas, Peñón de Alhucemas, and Peñón de Vélez de la Gomera is unclear. Our base map does not have areas for the last two, and we use Africa/Ceuta for Melilla and the Chafarinas.

77 | 78 |

Antarctica

79 |

In this map, we consider all of Antarctica to be uninhabited. The companion map Antarctica captures the permanent bases and the TZ timezones they use.

80 | 81 |

Greenland

82 |

The boundaries between the four zones are somewhat arbitrary.

83 |

Mexico

84 | 85 |

We use a somewhat arbitrary boundary between the America/Tijuana 86 | and America/Santa_Isabel timezones. The former is defined as the zone 87 | within 20 km of the US border, the town of Ensada and "towards the 88 | interior of the country". Similarly, we use a somewhat arbitrary boundary between America/Bahia_Banderas and America/Mazatlan.

89 | 90 |

Construction of the shapefile

91 | 92 |

The ingredients (zip, sha1) contain a script to build the map from the source, as well as a .prj file for the source map.

93 | 94 |

For the United States, Russia and China, we started from the tz maps for those countries and turned them manually into masks; the idea is to retain from them only the boundaries that are not present in our base map. Those mask shapefiles are in the ingredients.

95 | 96 |

For Mexico, we use a mask that captures the 20km zone next to the US boundary, extended to include the town of Ensanada.

97 | 98 |

For Canada, we use directly the masks that served to create the Canada map.

99 | 100 |

For Brazil, we use a mask built from 1) the great circle from Tabatinga to Porto Acre to divide east from west Amazonas, and 2) the Xigu and Javary rivers from the VMAP0 inwatera (inland water area) layer to divide east from west Para.

101 | 102 |

For the exact details of the tz assignments, see the script in the ingredients.

103 |

 

104 |
105 |

Terms of use

106 | 107 |

CC0 To the extent possible under law, Eric Muller has waived all copyright and related or neighboring rights to the efele.net/tz maps (comprising the shapefiles, the web pages describing them and the scripts and data used to build them). 108 | This work is published from the United States of America.

109 | 110 | 111 |

Note that this does not affect the rights others may have. I am not qualified to determine whether such rights exist.

112 |
113 |

Contact - Thanks

114 |

History:

115 | 235 |

 

236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | README = open(os.path.join(os.path.dirname(__file__), 'README.md')).read() 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | setup( 10 | name='tzwhere', 11 | version='3.0.3', 12 | packages=['tzwhere'], 13 | package_data={ 14 | 'tzwhere': [ 15 | 'tz_world.json.gz', 16 | 'tz_world_shortcuts.json' 17 | ] 18 | }, 19 | include_package_data=True, 20 | install_requires=[ 21 | 'shapely' 22 | ], 23 | license='MIT License', 24 | description='Python library to look up timezone from lat / long offline', 25 | long_description=README, 26 | url='https://github.com/pegler/pytzwhere', 27 | author='Matt Pegler', 28 | author_email='matt@pegler.co', 29 | maintainer='Christoph Stich', 30 | maintainer_email='christoph@stich.xyz', 31 | classifiers=[ 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 3', 38 | 'Topic :: Software Development :: Localization', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==2.1.0 2 | coverage==3.7 3 | coveralls==0.3 4 | nose==1.3.0 5 | shapely 6 | numpy 7 | docopt 8 | -------------------------------------------------------------------------------- /tests/test_locations.py: -------------------------------------------------------------------------------- 1 | from tzwhere import tzwhere 2 | import datetime 3 | import unittest 4 | 5 | 6 | class LocationTestCase(unittest.TestCase): 7 | 8 | TEST_LOCATIONS = ( 9 | ( 35.295953, -89.662186, 'Arlington, TN', 'America/Chicago'), 10 | ( 33.58, -85.85, 'Memphis, TN', 'America/Chicago'), 11 | ( 61.17, -150.02, 'Anchorage, AK', 'America/Anchorage'), 12 | ( 44.12, -123.22, 'Eugene, OR', 'America/Los_Angeles'), 13 | ( 42.652647, -73.756371, 'Albany, NY', 'America/New_York'), 14 | ( 55.743749, 37.6207923, 'Moscow', 'Europe/Moscow'), 15 | ( 34.104255, -118.4055591, 'Los Angeles', 'America/Los_Angeles'), 16 | ( 55.743749, 37.6207923, 'Moscow', 'Europe/Moscow'), 17 | ( 39.194991, -106.8294024, 'Aspen, Colorado', 'America/Denver'), 18 | ( 50.438114, 30.5179595, 'Kiev', 'Europe/Kiev'), 19 | ( 12.936873, 77.6909136, 'Jogupalya', 'Asia/Kolkata'), 20 | ( 38.889144, -77.0398235, 'Washington DC', 'America/New_York'), 21 | ( 59.932490, 30.3164291, 'St Petersburg', 'Europe/Moscow'), 22 | ( 50.300624, 127.559166, 'Blagoveshchensk', 'Asia/Yakutsk'), 23 | ( 42.439370, -71.0700416, 'Boston', 'America/New_York'), 24 | ( 41.84937, -87.6611995, 'Chicago', 'America/Chicago'), 25 | ( 28.626873, -81.7584514, 'Orlando', 'America/New_York'), 26 | ( 47.610615, -122.3324847, 'Seattle', 'America/Los_Angeles'), 27 | ( 51.499990, -0.1353549, 'London', 'Europe/London'), 28 | ( 51.256241, -0.8186531, 'Church Crookham', 'Europe/London'), 29 | ( 51.292215, -0.8002638, 'Fleet', 'Europe/London'), 30 | ( 48.868743, 2.3237586, 'Paris', 'Europe/Paris'), 31 | ( 22.158114, 113.5504603, 'Macau', 'Asia/Macau'), 32 | ( 56.833123, 60.6097054, 'Russia', 'Asia/Yekaterinburg'), 33 | ( 60.887496, 26.6375756, 'Salo', 'Europe/Helsinki'), 34 | ( 52.799992, -1.8524408, 'Staffordshire', 'Europe/London'), 35 | ( 5.016666, 115.0666667, 'Muara', 'Asia/Brunei'), 36 | (-41.466666, -72.95, 'Puerto Montt seaport', 'America/Santiago'), 37 | ( 34.566666, 33.0333333, 'Akrotiri seaport', 'Asia/Nicosia'), 38 | ( 37.466666, 126.6166667, 'Inchon seaport', 'Asia/Seoul'), 39 | ( 42.8, 132.8833333, 'Nakhodka seaport', 'Asia/Vladivostok'), 40 | ( 50.26, -5.051, 'Truro', 'Europe/London'), 41 | ( 50.26, -9.051, 'Sea off Cornwall', None), 42 | ( 35.82373, -110.72144, 'Hopi Nation', 'America/Phoenix'), 43 | ( 35.751956, -110.169460, 'Deni inside Hopi Nation', 'America/Denver'), 44 | ( 68.38068073677294, -133.73396065378114, 'Upper hole in America/Yellowknife', 'America/Inuvik') 45 | ) 46 | 47 | TEST_LOCATIONS_FORCETZ = ( 48 | ( 35.295953, -89.662186, 'Arlington, TN', 'America/Chicago'), 49 | ( 33.58, -85.85, 'Memphis, TN', 'America/Chicago'), 50 | ( 61.17, -150.02, 'Anchorage, AK', 'America/Anchorage'), 51 | ( 40.7271, -73.98, 'Shore Lake Michigan', 'America/New_York'), 52 | ( 50.1536, -8.051, 'Off Cornwall', 'Europe/London'), 53 | ( 49.2698, -123.1302, 'Vancouver', 'America/Vancouver'), 54 | ( 50.26, -9.051, 'Far off Cornwall', None) 55 | ) 56 | 57 | def _test_tzwhere(self, locations, forceTZ): 58 | start = datetime.datetime.now() 59 | w = tzwhere.tzwhere(forceTZ=forceTZ) 60 | end = datetime.datetime.now() 61 | print('Initialized in: '), 62 | print(end - start) 63 | 64 | template = '{0:20s} | {1:20s} | {2:20s} | {3:2s}' 65 | print(template.format('LOCATION', 'EXPECTED', 'COMPUTED', '==')) 66 | for (lat, lon, loc, expected) in locations: 67 | computed = w.tzNameAt(float(lat), float(lon), forceTZ=forceTZ) 68 | ok = 'OK' if computed == expected else 'XX' 69 | print(template.format(loc, str(expected), str(computed), ok)) 70 | assert computed == expected 71 | 72 | def test_lookup(self): 73 | self._test_tzwhere(self.TEST_LOCATIONS,forceTZ=False) 74 | 75 | def test_forceTZ(self): 76 | self._test_tzwhere(self.TEST_LOCATIONS_FORCETZ,forceTZ=True) 77 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | ################################## 5 | # Standard library imports 6 | ################################## 7 | 8 | import os 9 | import datetime 10 | from unittest import TestCase 11 | 12 | ################################## 13 | # Custom library imports 14 | ################################## 15 | 16 | from tzwhere import tzwhere 17 | from tzwhere.tzwhere import BASE_DIR 18 | 19 | 20 | #################################### 21 | 22 | class TestTzwhereUtilities(TestCase): 23 | def test_preparemap_class(self): 24 | """ 25 | Tests the prepareMap class which writes shortcuts file. Test looks 26 | at modified date to see if that's equivalent to today's date 27 | 28 | :return: 29 | Unit test response 30 | """ 31 | 32 | a = tzwhere.prepareMap() 33 | location = os.path.join(BASE_DIR, 'tzwhere', 'tz_world_shortcuts.json') 34 | date = datetime.datetime.now().date() 35 | modified = datetime.datetime. \ 36 | fromtimestamp(os.stat(location).st_mtime).date() 37 | return self.assertEqual(date, modified) 38 | -------------------------------------------------------------------------------- /tzwhere/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pegler/pytzwhere/32d2bef9ff2d784741471fddb35fbb6732f556d5/tzwhere/__init__.py -------------------------------------------------------------------------------- /tzwhere/tz_world.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pegler/pytzwhere/32d2bef9ff2d784741471fddb35fbb6732f556d5/tzwhere/tz_world.json.gz -------------------------------------------------------------------------------- /tzwhere/tzwhere.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''tzwhere.py - time zone computation from latitude/longitude. 4 | 5 | Ordinarily this is loaded as a module and instances of the tzwhere 6 | class are instantiated and queried directly 7 | ''' 8 | 9 | import collections 10 | try: 11 | import ujson as json # loads 2 seconds faster than normal json 12 | except: 13 | try: 14 | import json 15 | except ImportError: 16 | import simplejson as json 17 | import math 18 | import gzip 19 | import os 20 | import shapely.geometry as geometry 21 | import shapely.prepared as prepared 22 | 23 | # We can save about 222MB of RAM by turning our polygon lists into 24 | # numpy arrays rather than tuples, if numpy is installed. 25 | try: 26 | import numpy 27 | WRAP = numpy.asarray 28 | COLLECTION_TYPE = numpy.ndarray 29 | except ImportError: 30 | WRAP = tuple 31 | COLLECTION_TYPE = tuple 32 | 33 | # for navigation and pulling values/files 34 | this_dir, this_filename = os.path.split(__file__) 35 | BASE_DIR = os.path.dirname(this_dir) 36 | 37 | class tzwhere(object): 38 | 39 | SHORTCUT_DEGREES_LATITUDE = 1.0 40 | SHORTCUT_DEGREES_LONGITUDE = 1.0 41 | # By default, use the data file in our package directory 42 | DEFAULT_SHORTCUTS = os.path.join(os.path.dirname(__file__), 43 | 'tz_world_shortcuts.json') 44 | DEFAULT_POLYGONS = os.path.join(os.path.dirname(__file__), 45 | 'tz_world.json.gz') 46 | 47 | def __init__(self, forceTZ=False): 48 | ''' 49 | Initializes the tzwhere class. 50 | @forceTZ: If you want to force the lookup method to a return a 51 | timezone even if the point you are looking up is slightly outside it's 52 | bounds, you need to specify this during initialization arleady 53 | ''' 54 | 55 | featureCollection = read_tzworld(tzwhere.DEFAULT_POLYGONS) 56 | pgen = feature_collection_polygons(featureCollection) 57 | self.timezoneNamesToPolygons = collections.defaultdict(list) 58 | self.unprepTimezoneNamesToPolygons = collections.defaultdict(list) 59 | for tzname, poly in pgen: 60 | self.timezoneNamesToPolygons[tzname].append(poly) 61 | for tzname, polys in self.timezoneNamesToPolygons.items(): 62 | self.timezoneNamesToPolygons[tzname] = WRAP(polys) 63 | 64 | if forceTZ: 65 | self.unprepTimezoneNamesToPolygons[tzname] = WRAP(polys) 66 | 67 | with open(tzwhere.DEFAULT_SHORTCUTS, 'r') as f: 68 | self.timezoneLongitudeShortcuts, self.timezoneLatitudeShortcuts = json.load(f) 69 | 70 | self.forceTZ = forceTZ 71 | for tzname in self.timezoneNamesToPolygons: 72 | # Convert things to tuples to save memory 73 | for degree in self.timezoneLatitudeShortcuts: 74 | for tzname in self.timezoneLatitudeShortcuts[degree].keys(): 75 | self.timezoneLatitudeShortcuts[degree][tzname] = \ 76 | tuple(self.timezoneLatitudeShortcuts[degree][tzname]) 77 | 78 | for degree in self.timezoneLongitudeShortcuts.keys(): 79 | for tzname in self.timezoneLongitudeShortcuts[degree].keys(): 80 | self.timezoneLongitudeShortcuts[degree][tzname] = \ 81 | tuple(self.timezoneLongitudeShortcuts[degree][tzname]) 82 | 83 | def tzNameAt(self, latitude, longitude, forceTZ=False): 84 | ''' 85 | Let's you lookup for a given latitude and longitude the appropriate 86 | timezone. 87 | @latitude: latitude 88 | @longitude: longitude 89 | @forceTZ: If forceTZ is true and you can't find a valid timezone return 90 | the closest timezone you can find instead. Only works if the point has 91 | the same integer value for its degree than the timezeone 92 | ''' 93 | 94 | if forceTZ: 95 | assert self.forceTZ, 'You need to initialize tzwhere with forceTZ' 96 | 97 | latTzOptions = self.timezoneLatitudeShortcuts[str( 98 | (math.floor(latitude / self.SHORTCUT_DEGREES_LATITUDE) * 99 | self.SHORTCUT_DEGREES_LATITUDE) 100 | )] 101 | latSet = set(latTzOptions.keys()) 102 | lngTzOptions = self.timezoneLongitudeShortcuts[str( 103 | (math.floor(longitude / self.SHORTCUT_DEGREES_LONGITUDE) * 104 | self.SHORTCUT_DEGREES_LONGITUDE) 105 | )] 106 | lngSet = set(lngTzOptions.keys()) 107 | possibleTimezones = lngSet.intersection(latSet) 108 | 109 | queryPoint = geometry.Point(longitude, latitude) 110 | 111 | if possibleTimezones: 112 | for tzname in possibleTimezones: 113 | if isinstance(self.timezoneNamesToPolygons[tzname], COLLECTION_TYPE): 114 | self.timezoneNamesToPolygons[tzname] = list( 115 | map(lambda p: prepared.prep( 116 | geometry.Polygon(p[0], p[1]) 117 | ), self.timezoneNamesToPolygons[tzname])) 118 | 119 | polyIndices = set(latTzOptions[tzname]).intersection(set( 120 | lngTzOptions[tzname] 121 | )) 122 | 123 | for polyIndex in polyIndices: 124 | poly = self.timezoneNamesToPolygons[tzname][polyIndex] 125 | if poly.contains_properly(queryPoint): 126 | return tzname 127 | 128 | if forceTZ: 129 | return self.__forceTZ__(possibleTimezones, latTzOptions, 130 | lngTzOptions, queryPoint) 131 | 132 | def __forceTZ__(self, possibleTimezones, latTzOptions, 133 | lngTzOptions, queryPoint): 134 | distances = [] 135 | if possibleTimezones: 136 | if len(possibleTimezones) == 1: 137 | return possibleTimezones.pop() 138 | else: 139 | for tzname in possibleTimezones: 140 | if isinstance(self.unprepTimezoneNamesToPolygons[tzname], 141 | COLLECTION_TYPE): 142 | self.unprepTimezoneNamesToPolygons[tzname] = list( 143 | map(lambda p: p.context if isinstance(p, prepared.PreparedGeometry) else geometry.Polygon(p[0], p[1]), 144 | self.timezoneNamesToPolygons[tzname])) 145 | polyIndices = set(latTzOptions[tzname]).intersection( 146 | set(lngTzOptions[tzname])) 147 | for polyIndex in polyIndices: 148 | poly = self.unprepTimezoneNamesToPolygons[ 149 | tzname][polyIndex] 150 | d = poly.distance(queryPoint) 151 | distances.append((d, tzname)) 152 | if len(distances) > 0: 153 | return sorted(distances, key=lambda x: x[0])[0][1] 154 | 155 | 156 | class prepareMap(object): 157 | 158 | def __init__(self): 159 | DEFAULT_SHORTCUTS = os.path.join(os.path.dirname(__file__), 160 | 'tz_world_shortcuts.json') 161 | DEFAULT_POLYGONS = os.path.join(os.path.dirname(__file__), 162 | 'tz_world.json.gz') 163 | featureCollection = read_tzworld(DEFAULT_POLYGONS) 164 | pgen = feature_collection_polygons(featureCollection) 165 | tzNamesToPolygons = collections.defaultdict(list) 166 | for tzname, poly in pgen: 167 | tzNamesToPolygons[tzname].append(poly) 168 | 169 | for tzname, polys in tzNamesToPolygons.items(): 170 | tzNamesToPolygons[tzname] = \ 171 | WRAP(tzNamesToPolygons[tzname]) 172 | 173 | timezoneLongitudeShortcuts,\ 174 | timezoneLatitudeShortcuts = self.construct_shortcuts( 175 | tzNamesToPolygons, tzwhere.SHORTCUT_DEGREES_LONGITUDE, 176 | tzwhere.SHORTCUT_DEGREES_LATITUDE) 177 | 178 | with open(DEFAULT_SHORTCUTS, 'w') as f: 179 | json.dump( 180 | (timezoneLongitudeShortcuts, timezoneLatitudeShortcuts), f) 181 | 182 | @staticmethod 183 | def construct_shortcuts(timezoneNamesToPolygons, 184 | shortcut_long, shortcut_lat): 185 | ''' Construct our shortcuts for looking up polygons. Much faster 186 | than using an r-tree ''' 187 | 188 | def find_min_max(ls, gridSize): 189 | minLs = (math.floor(min(ls) / gridSize) * 190 | gridSize) 191 | maxLs = (math.floor(max(ls) / gridSize) * 192 | gridSize) 193 | return minLs, maxLs 194 | 195 | timezoneLongitudeShortcuts = {} 196 | timezoneLatitudeShortcuts = {} 197 | 198 | for tzname in timezoneNamesToPolygons: 199 | tzLngs = [] 200 | tzLats = [] 201 | for polyIndex, poly in enumerate(timezoneNamesToPolygons[tzname]): 202 | lngs = [x[0] for x in poly[0]] 203 | lats = [x[1] for x in poly[0]] 204 | tzLngs.extend(lngs) 205 | tzLats.extend(lats) 206 | minLng, maxLng = find_min_max( 207 | lngs, shortcut_long) 208 | minLat, maxLat = find_min_max( 209 | lats, shortcut_lat) 210 | degree = minLng 211 | 212 | while degree <= maxLng: 213 | if degree not in timezoneLongitudeShortcuts: 214 | timezoneLongitudeShortcuts[degree] =\ 215 | collections.defaultdict(list) 216 | timezoneLongitudeShortcuts[degree][tzname].append(polyIndex) 217 | degree = degree + shortcut_long 218 | 219 | degree = minLat 220 | while degree <= maxLat: 221 | if degree not in timezoneLatitudeShortcuts: 222 | timezoneLatitudeShortcuts[degree] =\ 223 | collections.defaultdict(list) 224 | timezoneLatitudeShortcuts[degree][tzname].append(polyIndex) 225 | degree = degree + shortcut_lat 226 | return timezoneLongitudeShortcuts, timezoneLatitudeShortcuts 227 | 228 | 229 | def read_tzworld(path): 230 | reader = read_json 231 | return reader(path) 232 | 233 | 234 | def read_json(path): 235 | with gzip.open(path, "rb") as f: 236 | featureCollection = json.loads(f.read().decode("utf-8")) 237 | return featureCollection 238 | 239 | 240 | def feature_collection_polygons(featureCollection): 241 | """Turn a feature collection 242 | into an iterator over polygons. 243 | 244 | Given a featureCollection of the kind loaded from the json 245 | input, unpack it to an iterator which produces a series of 246 | (tzname, polygon) pairs, one for every polygon in the 247 | featureCollection. Here tzname is a string and polygon is a 248 | list of floats. 249 | 250 | """ 251 | for feature in featureCollection['features']: 252 | tzname = feature['properties']['TZID'] 253 | if feature['geometry']['type'] == 'Polygon': 254 | exterior = feature['geometry']['coordinates'][0] 255 | interior = feature['geometry']['coordinates'][1:] 256 | yield (tzname, (exterior, interior)) 257 | 258 | if __name__ == "__main__": 259 | prepareMap() 260 | --------------------------------------------------------------------------------