diff --git a/README.MD b/README.MD index a70a703..efafc4f 100644 --- a/README.MD +++ b/README.MD @@ -77,6 +77,15 @@ TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2 It is important to mention that for the time zone to be loaded by the second method, it must be covered by the bounding box completely, not just intersect with it. +if you have difficulty determining the bounding box, you may prefer to use a list of ZoneIds instead. +```Java +Set timeZones = new HashSet<> (); +timeZones.add(ZoneId.of("Europe/Berlin")); +timeZones.add(ZoneId.of("America/Detroit")); +TimeZoneEngine engine = TimeZoneEngine.initialize (timeZones, true); + +``` + During initialization, the data is read from resource and the index is built. Initialization takes a significant amount of time (approximately 1 second), so do it only once in program lifetime. @@ -141,7 +150,7 @@ for information about areas of the world where multiple time zones are to be exp Timeshape supports querying of multiple sequential geo points (a polyline, e.g. a GPS trace) in an optimized way using method `List queryPolyline(double[] points)`. Performance tests (see `net.iakovlev.timeshape.PolylineQueryBenchmark`) -show significant speedup of using this method for querying a polyline, comparing to separately querying each point from polyline +show significant speedup of using this method for querying a polyline, comparing to separately querying each point from polyline using `Optional query(double latitude, double longitude)` method: ``` diff --git a/core/src/main/java/net/iakovlev/timeshape/Index.java b/core/src/main/java/net/iakovlev/timeshape/Index.java index c40e728..a640edd 100644 --- a/core/src/main/java/net/iakovlev/timeshape/Index.java +++ b/core/src/main/java/net/iakovlev/timeshape/Index.java @@ -10,7 +10,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.PrimitiveIterator; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -151,6 +153,7 @@ private static Stream getPolygons(Geojson.Feature f) { } } + @SuppressWarnings("SizeReplaceableByIsEmpty") static Index build(Stream features, int size, Envelope boundaries, boolean accelerateGeometry) { Envelope2D boundariesEnvelope = new Envelope2D(); boundaries.queryEnvelope2D(boundariesEnvelope); @@ -192,4 +195,47 @@ static Index build(Stream features, int size, Envelope boundari return new Index(quadTree, zoneIds); } + + @SuppressWarnings("SizeReplaceableByIsEmpty") + static Index build(Stream features, int size, Set timeZones, boolean accelerateGeometry) { + Envelope2D boundariesEnvelope = new Envelope2D(); + Envelope boundaries = new Envelope(-180, -90, 180, +90);// (minLon, minLat, maxLon, maxLat); + boundaries.queryEnvelope2D(boundariesEnvelope); + QuadTree quadTree = new QuadTree(boundariesEnvelope, 8); + Envelope2D env = new Envelope2D(); + ArrayList zoneIds = new ArrayList<>(size); + PrimitiveIterator.OfInt indices = IntStream.iterate(0, i -> i + 1).iterator(); + List unknownZones = new ArrayList<>(); + OperatorIntersects operatorIntersects = OperatorIntersects.local(); + features.forEach(f -> { + String zoneIdName = f.getProperties(0).getValueString(); + try { + ZoneId zoneId = ZoneId.of(zoneIdName); + if (timeZones.contains (zoneId)) { + getPolygons(f).forEach(polygon -> { + log.debug("Adding zone {} to index", zoneIdName); + if (accelerateGeometry) { + operatorIntersects.accelerateGeometry(polygon, spatialReference, Geometry.GeometryAccelerationDegree.enumMild); + } + polygon.queryEnvelope2D(env); + int index = indices.next(); + quadTree.insert(index, env); + zoneIds.add(index, new Entry(zoneId, polygon)); + }); + } else { + log.debug("Not adding zone {} to index because it's out of provided boundaries", zoneIdName); + } + } catch (Exception ex) { + unknownZones.add(zoneIdName); + } + }); + if (unknownZones.size() != 0) { + String allUnknownZones = String.join(", ", unknownZones); + log.error( + "Some of the zone ids were not recognized by the Java runtime and will be ignored. " + + "The most probable reason for this is outdated Java runtime version. " + + "The following zones were not recognized: " + allUnknownZones); + } + return new Index(quadTree, zoneIds); + } } diff --git a/core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java b/core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java index adbcf61..a1668be 100644 --- a/core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java +++ b/core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java @@ -25,6 +25,9 @@ public final class TimeZoneEngine implements Serializable { private final Index index; + + private final static int NUMBER_OF_TIMEZONES = 449; // can't get number of entries from tar, need to set manually + private final static String DATA_FILE_NAME = "/data.tar.zstd"; private final static double MIN_LAT = -90; private final static double MIN_LON = -180; @@ -80,6 +83,25 @@ public boolean tryAdvance(Consumer action) { } }; } + + + private static Stream spliterateInputStream(TarArchiveInputStream f) { + Spliterator tarArchiveEntrySpliterator = makeSpliterator(f); + return StreamSupport.stream(tarArchiveEntrySpliterator, false).map(n -> { + try { + if (n != null) { + log.debug("Processing archive entry {}", n.getName()); + byte[] e = new byte[(int) n.getSize()]; + f.read(e); + return Geojson.Feature.parseFrom(e); + } else { + throw new RuntimeException("Data entry is not found in file"); + } + } catch (NullPointerException | IOException ex) { + throw new RuntimeException(ex); + } + }); + } /** * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} @@ -103,6 +125,7 @@ public List queryAll(double latitude, double longitude) { * @return {@code Optional#of(ZoneId)} if input corresponds * to some zone, or {@link Optional#empty()} otherwise. */ + @SuppressWarnings("SizeReplaceableByIsEmpty") public Optional query(double latitude, double longitude) { final List result = index.query(latitude, longitude); return result.size() > 0 ? Optional.of(result.get(0)) : Optional.empty(); @@ -138,6 +161,7 @@ public List getKnownZoneIds() { * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * + * @param accelerateGeometry Increase query speed at the expense of memory utilization * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(boolean accelerateGeometry) { @@ -169,6 +193,7 @@ public static TimeZoneEngine initialize() { * } * }}} * + * @param f Input stream of timezone data tar archive * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(TarArchiveInputStream f) { @@ -188,6 +213,13 @@ public static TimeZoneEngine initialize(TarArchiveInputStream f) { * throw new RuntimeException(e); * } * }}} + * + * @param minLat Minimum latitude of bounding box + * @param minLon Minimum longitude of bounding box + * @param maxLat Maximum latitude of bounding box + * @param maxLon Maximum longitude of bounding box + * @param accelerateGeometry Increase query speed at the expense of memory utilization + * @param f Input stream of timezone data tar archive * * @return an initialized instance of {@link TimeZoneEngine} */ @@ -199,40 +231,56 @@ public static TimeZoneEngine initialize(double minLat, TarArchiveInputStream f) { log.info("Initializing with bounding box: {}, {}, {}, {}", minLat, minLon, maxLat, maxLon); validateCoordinates(minLat, minLon, maxLat, maxLon); - Spliterator tarArchiveEntrySpliterator = makeSpliterator(f); - Stream featureStream = StreamSupport.stream(tarArchiveEntrySpliterator, false).map(n -> { - try { - if (n != null) { - log.debug("Processing archive entry {}", n.getName()); - byte[] e = new byte[(int) n.getSize()]; - f.read(e); - return Geojson.Feature.parseFrom(e); - } else { - throw new RuntimeException("Data entry is not found in file"); - } - } catch (NullPointerException | IOException ex) { - throw new RuntimeException(ex); - } - }); - int numberOfTimezones = 449; // can't get number of entries from tar, need to set manually + Stream featureStream = spliterateInputStream (f); + Envelope boundaries = new Envelope(minLon, minLat, maxLon, maxLat); return new TimeZoneEngine( Index.build( featureStream, - numberOfTimezones, + NUMBER_OF_TIMEZONES, boundaries, accelerateGeometry)); } + /** + * Creates a new instance of {@link TimeZoneEngine} and initializes it. + * This is a blocking long running operation. + * + * @param timeZones List of ZoneIds to load. + * @param accelerateGeometry Increase query speed at the expense of memory utilization + * @param numberOfTimeZones How many timezones are in the tar archive. + * @param f Input stream of timezone data tar archive + * @return an initialized instance of {@link TimeZoneEngine} + */ + + public static TimeZoneEngine initialize(Set timeZones, + boolean accelerateGeometry, + int numberOfTimeZones, + TarArchiveInputStream f) { + log.info("Initializing with list of time zones"); + Stream featureStream = spliterateInputStream (f); + + return new TimeZoneEngine( + Index.build( + featureStream, + numberOfTimeZones, + timeZones, + accelerateGeometry)); + } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * + * @param minLat Minimum latitude of bounding box + * @param minLon Minimum longitude of bounding box + * @param maxLat Maximum latitude of bounding box + * @param maxLon Maximum longitude of bounding box + * @param accelerateGeometry Increase query speed at the expense of memory utilization * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(double minLat, double minLon, double maxLat, double maxLon, boolean accelerateGeometry) { - try (InputStream resourceAsStream = TimeZoneEngine.class.getResourceAsStream("/data.tar.zstd")) { + try (InputStream resourceAsStream = TimeZoneEngine.class.getResourceAsStream(DATA_FILE_NAME)) { try (ZstdInputStream unzipStream = new ZstdInputStream(resourceAsStream)) { try (BufferedInputStream bufferedStream = new BufferedInputStream(unzipStream)) { try (TarArchiveInputStream shapeInputStream = new TarArchiveInputStream(bufferedStream)) { @@ -245,4 +293,29 @@ public static TimeZoneEngine initialize(double minLat, double minLon, double max throw new RuntimeException(e); } } + + + /** + * Creates a new instance of {@link TimeZoneEngine} and initializes it. + * This is a blocking long running operation. + * + * @param timeZones List of ZoneIds to load. + * @param accelerateGeometry Increase query speed at the expense of memory utilization + * @return an initialized instance of {@link TimeZoneEngine} + */ + public static TimeZoneEngine initialize(Set timeZones, boolean accelerateGeometry) { + try (InputStream resourceAsStream = TimeZoneEngine.class.getResourceAsStream("/data.tar.zstd")) { + try (ZstdInputStream unzipStream = new ZstdInputStream(resourceAsStream)) { + try (BufferedInputStream bufferedStream = new BufferedInputStream(unzipStream)) { + try (TarArchiveInputStream shapeInputStream = new TarArchiveInputStream(bufferedStream)) { + return initialize(timeZones, accelerateGeometry, NUMBER_OF_TIMEZONES, shapeInputStream); + } + } + } + } catch (NullPointerException | IOException e) { + log.error("Unable to read resource file", e); + throw new RuntimeException(e); + } + } + } diff --git a/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedTest.java b/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedTest.java index 0f5dc9a..14f541d 100644 --- a/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedTest.java +++ b/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedTest.java @@ -12,7 +12,7 @@ @RunWith(JUnit4.class) public class TimeZoneEngineBoundedTest { - private static TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true); + private static final TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true); @Test public void testSomeZones() { @@ -22,6 +22,6 @@ public void testSomeZones() { @Test public void testWorld() { List knownZoneIds = engine.getKnownZoneIds(); - assertEquals(knownZoneIds.size(), 39); + assertEquals(39, knownZoneIds.size()); } } diff --git a/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedZoneTest.java b/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedZoneTest.java new file mode 100644 index 0000000..84ea4d1 --- /dev/null +++ b/core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedZoneTest.java @@ -0,0 +1,33 @@ +package net.iakovlev.timeshape; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.time.ZoneId; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static junit.framework.TestCase.assertEquals; + +@RunWith(JUnit4.class) +public class TimeZoneEngineBoundedZoneTest { + + @Test + public void testSomeZones() { + Set timeZones = new HashSet<> (); + timeZones.add (ZoneId.of("Europe/Berlin")); + TimeZoneEngine engine = TimeZoneEngine.initialize (timeZones, true); + assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); + + timeZones.clear (); + timeZones.add(ZoneId.of("America/Detroit")); + engine = TimeZoneEngine.initialize (timeZones, true); + assertEquals(Optional.empty (), engine.query(52.52, 13.40)); + + timeZones.add(ZoneId.of("Europe/Berlin")); + engine = TimeZoneEngine.initialize (timeZones, true); + assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); + } +}