Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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<ZoneId> 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.

Expand Down Expand Up @@ -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<SameZoneSpan> 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<ZoneId> query(double latitude, double longitude)` method:

```
Expand Down
46 changes: 46 additions & 0 deletions core/src/main/java/net/iakovlev/timeshape/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -151,6 +153,7 @@ private static Stream<Polygon> getPolygons(Geojson.Feature f) {
}
}

@SuppressWarnings("SizeReplaceableByIsEmpty")
static Index build(Stream<Geojson.Feature> features, int size, Envelope boundaries, boolean accelerateGeometry) {
Envelope2D boundariesEnvelope = new Envelope2D();
boundaries.queryEnvelope2D(boundariesEnvelope);
Expand Down Expand Up @@ -192,4 +195,47 @@ static Index build(Stream<Geojson.Feature> features, int size, Envelope boundari
return new Index(quadTree, zoneIds);
}


@SuppressWarnings("SizeReplaceableByIsEmpty")
static Index build(Stream<Geojson.Feature> features, int size, Set<ZoneId> 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<Entry> zoneIds = new ArrayList<>(size);
PrimitiveIterator.OfInt indices = IntStream.iterate(0, i -> i + 1).iterator();
List<String> 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);
}
}
109 changes: 91 additions & 18 deletions core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -80,6 +83,25 @@ public boolean tryAdvance(Consumer<? super TarArchiveEntry> action) {
}
};
}


private static Stream<Geojson.Feature> spliterateInputStream(TarArchiveInputStream f) {
Spliterator<TarArchiveEntry> 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}
Expand All @@ -103,6 +125,7 @@ public List<ZoneId> queryAll(double latitude, double longitude) {
* @return {@code Optional<ZoneId>#of(ZoneId)} if input corresponds
* to some zone, or {@link Optional#empty()} otherwise.
*/
@SuppressWarnings("SizeReplaceableByIsEmpty")
public Optional<ZoneId> query(double latitude, double longitude) {
final List<ZoneId> result = index.query(latitude, longitude);
return result.size() > 0 ? Optional.of(result.get(0)) : Optional.empty();
Expand Down Expand Up @@ -138,6 +161,7 @@ public List<ZoneId> 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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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}
*/
Expand All @@ -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<TarArchiveEntry> tarArchiveEntrySpliterator = makeSpliterator(f);
Stream<Geojson.Feature> 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<Geojson.Feature> 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<ZoneId> timeZones,
boolean accelerateGeometry,
int numberOfTimeZones,
TarArchiveInputStream f) {
log.info("Initializing with list of time zones");
Stream<Geojson.Feature> 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)) {
Expand All @@ -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<ZoneId> 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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -22,6 +22,6 @@ public void testSomeZones() {
@Test
public void testWorld() {
List<ZoneId> knownZoneIds = engine.getKnownZoneIds();
assertEquals(knownZoneIds.size(), 39);
assertEquals(39, knownZoneIds.size());
}
}
Original file line number Diff line number Diff line change
@@ -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<ZoneId> 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));
}
}