en|de

Determine time zone with GeoTools from shapefiles

Karte der Zeitzonen der Welt

Time zones define the difference of the respective local time to Greenwich Mean Time (GMT) and the rules for daylight saving time changeover. With the existing possibilities of global online exchange of data and information, the exact knowledge of the time zone of the communication partner plays a major role in order to avoid misunderstandings and errors.

Time Zones

Due to the widespread use of GPS devices or geolocation based on IP addresses, it is often not a big problem to get to the geographic coordinates of a place. The determination of the time zone from these coordinates is then however not a trivial matter, because the time zone boundaries do not follow uniform rules, but are based on - temporarily changing - political and geographical circumstances. Thus, the time zone boundaries are the same as the state borders in Central Europe. Their naming follows the scheme "Europe/capital". For example, Germany belongs to the Time Zone Europe/Berlin, France to Europe/Paris and Croatia to Europe/Zagreb.

For large area countries, such as Canada or the United States, this rule no longer applies, because a subdivision of the territory into several time zones is required. Every country may handle the concrete division as it would like. In the US there are beside the great time zones such as America/Los_Angeles, America/Chicago or America/New_York some small time zones for counties, for example, America/Kentucky/Louisville or America/Indiana/Indianapolis.

On the high seas in international waters on the other hand the time zone keeps strictly within the longitude. The time zones are here 15° wide and have names like Etc/GMT for the zone around the zero meridian, Etc/GMT+1 for the zone around 15° West or Etc/GMT-1 at 15° East. The calculation of the time zone for a ship in international waters is therefore possible by means of a simple equation. Written in Java it could look like this:

public class TzDataEtc {
  /**
   * Compute the TZ offset from the longitude (valid in international waters).
   */
  public static int tzOffsetFromLon(double lon) {
    return (int) Math.floor((lon - 7.500000001) / 15) + 1;
  }

  /**
   * Compute the TZ name from the longitude (valid in international waters).
   */
  public static String tzNameFromLon(double lon) {
    String tzname;
    int tzOffset = tzOffsetFromLon(lon);
    if (tzOffset == 0) {
      tzname = "Etc/GMT";
    } else {
      tzname = String.format("Etc/GMT%+d", -tzOffset);
    }
    return tzname;
  }
}

Shapefiles: A map in Software

On land, this simple method no longer works. Here one needs an accurate map, which contains the time zone boundaries and their names. Logically, this card should not consist of paper but should be evaluated by software. An often-used format for such "electronic" maps is the ESRI shapefile. This is a vector map that allows the mapping of exact geometrical shapes by points, lines and polygons. In addition, attributes can be defined to attach geographic names and properties to the geometry.

Thankfully Eric Muller presents at efele.net/maps/tz/ shapefiles for different regions to download. The file tz_world.zip contains the time zones around the whole world as polygons. It represents a good starting point for further experiments. First you should unpack the zip file into an empty directory. The files always belong together! Although in common geographical software the file tz_world.shp is called "the shapefile", the other files (.dbf, .prj, ...) must be present in the same directory!

Java and GeoTools

A good tool for dealing with shapefiles and geographical data is the Open Source Java Toolkit GeoTools. The download and set up of the development environment are described in the Quickstart guide. I have used Ivy and Ant for this: The line

<dependency org="org.geotools" name="gt-shapefile" rev="14.1"/>

in the dependencies section of ivy.xml causes the download of all necessary JAR files of GeoTools version 14.1 to work with shapefiles. However GeoTools is not always to be found in the default Maven/Ivy repositories, so I had to add the Osgeo repository explicitely to ivysettings.xml:

<ibiblio name="osgeo" m2compatible="true" root="http://download.osgeo.org/webdav/geotools/"/> 

After these preparatory steps, it can go directly to programming. The complete source code is available on Github as Git repository tzdataservice.

Reading the shapefiles

Since the reading of the shapefiles can be a relatively lengthy operation, it makes sense to outsource this step into its own method. Responsible for loading of shapefiles are the classes ShapefileDataStoreFactory and ShapefileDataStore from the GeoTools libraries. The setting of appropriate properties before loading causes the generation of a spatial index, which speeds up the location-based search later. The result of the loading is a SimpleFeatureSource which is the starting point for all further operations:

public class TzDataShpFileReadAndLocate {

  private SimpleFeatureSource featureSource;
  private FilterFactory2 filterFactory;
  private GeometryFactory geometryFactory;

  /**
   * Open the input shape file and load it into memory.
   */
  public void openInputShapefile(String inputShapefile) throws IOException {
    File file = new File(inputShapefile);

    ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
    Map<String, Serializable> params = new HashMap<>();
    params.put(ShapefileDataStoreFactory.URLP.key, file.toURI().toURL());
    params.put(ShapefileDataStoreFactory.CREATE_SPATIAL_INDEX.key, Boolean.TRUE);
    // ...

    ShapefileDataStore store = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);
    featureSource = store.getFeatureSource();

    filterFactory = CommonFactoryFinder.getFilterFactory2(GeoTools.getDefaultHints());
    geometryFactory = JTSFactoryFinder.getGeometryFactory();
  }

Analysis of the scheme

After successful loading you can print the properties of the shapefile:

  /**
   * Print info about the schema of the loaded shapefile.
   */
  public void printInputShapfileSchemaInfo() {
    SimpleFeatureType schema = featureSource.getSchema();
    System.out.println(schema.getTypeName() + ": " + DataUtilities.encodeType(schema));
  }

This function applied to tz_world.shp gives the result:

tz_world: the_geom:MultiPolygon:srid=4326,TZID:String

This means that tz_world contains two features: Once the polygon geometry the_geom and on the other the names (IDs) of the time zones as a string in TZID. The geometry has the reference system SRID 4326, that is, the points are encoded in the reference system WGS84 by means of geographical coordinates from 180° West to 180° East and 90° South to 90° North. That's exactly the same system which popular map applications like Google Maps and OpenStreetMap or the GPS system are using.

Filter by coordinates

The determination of the time zone from given geographical coordinates is performed in two steps: First, the filter function contains is applied to the the featureSource. The filter function gets the name of the geometry the_geom and the geographic coordinates (x, y) as parameters. The result of the filtering is a SimpleFeatureCollection containing the polygon in which the searched point is located. (If there is no such a polygon, then the collection is empty.) The second step determines the value of the attribute TZID from the result that contains the name of the time zone:

  /**
   * Process a single coordinate.
   * 
   * @param x Longitude in degrees.
   * @param y Latitude in degrees.
   * @return Timezone Id.
   */
  public String process(double x, double y) throws IOException {
    String result = "";

    Point point = geometryFactory.createPoint(new Coordinate(x, y));
    Filter pointInPolygon = filterFactory.contains(
      filterFactory.property("the_geom"), filterFactory.literal(point));

    SimpleFeatureCollection features = featureSource.getFeatures(pointInPolygon);

    // search in coastal waters - see below ...

    try (FeatureIterator<SimpleFeature> iterator = features.features()) {
      if (iterator.hasNext()) {
        SimpleFeature feature = iterator.next();
        String tzid = (String) feature.getAttribute("TZID");
        result = tzid;
      }
    }
    return result;
  }

Search in territorial coastal waters

With the two previously described methods it is now possible to determine the time zone on continents, islands and in international waters. What's missing are the coastal waters belonging to the state territory. Their borders are not included in tz_world.shp.

You can estimate the borders with the rule that coastal waters are said to have a maximum dimension of 12 nautical miles (about 22 km). With this information, you can use the filter dwithin which also finds points that are near a polygon. Unfortunately, the possibility to pass a unit of length, such as "km", to the filter is not implemented. GeoTools interprets the numerical value always relative to the reference system of the map. The value 0.1 used in the sample code therefore means 0.1°. That are approximately 11 km in north-south direction.

    // search in coastal waters
    if (features.size() == 0) {
      Filter dWithin = filterFactory.dwithin(
          filterFactory.property("the_geom"), filterFactory.literal(point), 0.1, "");
      features = featureSource.getFeatures(dWithin);
    }

Performance

Particularly interesting is the speed of the algorithm and its accuracy. For that purpose I used the text file cities15000.txt from geonames.org as input. It contains the coordinates and time zones of 23461 places all over the world. A computer with a Core2 Quad Q8400 processor manufactured in 2010 needed four and a half minutes to determine the time zones of all 23461 records, which are just 11 ms per location. The results differed for 407 records, that is an error of 1.7%. An analysis of the errors brought no clarity as to whether the errors are in the shapefile, in the algorithm or in the geonames database.

However, the very good performance comes about only when the the shapefile is loaded only once for all 23461 records. A repeated startup that calls openInputShapefile for each coordinate would degrade the performance dramatically.

Web service

As a finale, a REST web service should now provide the method to determine the time zone from coordinates. The service can be implemented in the way that loading and indexing of the shapefile takes place only once. The determination of a single time zone can be performed then very fast, because all the necessary data are already in the memory. The use of HTTP and REST as communication protocols enables even clients, that are not programmed in Java, to determine the time zone. For my tool GEOPosition I use for example PHP with curl as a client.

Java 7 defines with JAX-RS a standard for declaring RESTful web services. To bring up and run such a service with Java SE 7 you will also need a JAX-RS implementation. The obvious is to use the reference implementation Jersey. You may instruct Ivy to download all necessary Jersey JARs by adding the lines

<dependency org="com.sun.jersey" name="jersey-bundle" rev="1.19" conf="master"/>
<dependency org="javax.ws.rs" name="jsr311-api" rev="1.1.1" conf="master"/>

to the dependencies section of ivy.xml. After that, it can go directly to the implementation of the web service method. The JAX-RS annotations @GET, @Path, and @Produces define the HTTP method to call the service (GET), the relative URL path (bylonlat/{lon}/{lat}) and the type of the result (plain text). The implementation first tries to determine the time zone from the shapefile. If the result is empty, then it calculates the time zone from the longitude (TzDataEtc):

  /**
   * REST method to compute the timezone id.
   * 
   * @param lon Latitude (deg)
   * @param lat Longitude (deg)
   * @return Timezone id.
   */
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  @Path("bylonlat/{lon}/{lat}")
  public String bylonlat(@PathParam("lon") String lon, @PathParam("lat") String lat) {
    try {
      double x = Double.parseDouble(lon);
      double y = Double.parseDouble(lat);
      String tzid = tzdata.process(x, y);
      if (tzid.length() == 0) {
        tzid = TzDataEtc.tzNameFromLon(x);
      }
      return tzid;
    } catch (NumberFormatException e) {
      throw new WebApplicationException(Status.BAD_REQUEST);
    } catch (IOException e) {
      throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
    }
  }

To complete the program a main function is required. It performs the one-time loading of the shapefile and starts the built-in Java 7 HTTP server on port 28100. You may use other ports of course. The server is bound only to the loopback interface with the IP address 127.0.0.1 for security reasons. The class definition has the JAX-RS annotation @Path. When the HTTP server starts, Jersey searches all classes for this annotation and registers them as a web service at the specified path.

/**
 * REST service to compute the timezone id from latitude and longitude.
 */
@Path("/tz")
public class TzDataService {

  private static TzDataShpFileReadAndLocate tzdata;

  /**
   * MAIN program.
   * Starts the rest service and runs forever.
   * 
   * @param args path to the tz_world.shp file
   */
  public static void main(String[] args) throws IOException {
    if (args.length != 1) {
      System.err.println("Usage: java " + TzDataService.class.getName() + " path/to/tz_world.shp");
      System.exit(1);
    }
    tzdata = new TzDataShpFileReadAndLocate();
    tzdata.openInputShapefile(args[0]);

    HttpServer server = createHttpServer(28100, "/");
    server.start();
  }

  /**
   * Create HTTP server that is bound to the loopback address only.
   */
  private static HttpServer createHttpServer(int port, String path) throws IOException {
    // bind server to loopback interface only:
    InetSocketAddress bindAddr = new InetSocketAddress(InetAddress.getLoopbackAddress(), port);
    // bind server to any interface (may be a security risk!):
    //InetSocketAddress bindAddr = new InetSocketAddress(port);
    HttpServer server = HttpServer.create(bindAddr, 0);
    server.setExecutor(Executors.newCachedThreadPool());
    HttpHandler handler = ContainerFactory.createContainer(HttpHandler.class);
    server.createContext(path, handler);      
    return server;
  }

The server program requires the path to the shapefile as command line argument:

java de.kompf.tzdata.rest.TzDataService world/tz_world.shp

Now you can enter a URL like http://localhost:28100/tz/bylonlat/9/50 into the browser. It should then answer the time zone for the coordinates longitude 9° East and latitude 50° North. Or you can use curl, the "Swiss army knife" for the web developer:

curl http://localhost:28100/tz/bylonlat/9/50
Europe/Berlin

Conclusion

A new test with cities15000.txt demonstrates the function of the web service. As expected the performance is worse than the direct call, but is still quite good with about 60 ms per query. In addition, the server is able to process simultaneous requests in multiple threads in parallel.

The web service has been running for several weeks stable on my website. It provides time zone information for the tool GEOPosition. It shows - in addition to the time zone - the coordinates, the altitude above sea level and the times for sunrise and sunset for any location on Earth. To be able to display the latter in local time, the knowledge of the exact time zone is required as well.