├── sonatype.key.gpg ├── .gitignore ├── test-app ├── src │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── java │ │ └── net │ │ └── iakovlev │ │ └── timeshape │ │ └── testapp │ │ └── Main.java └── pom.xml ├── .github ├── workflows │ ├── pull_request.yml │ ├── dependency_graph.yml │ └── release.yml └── ISSUE_TEMPLATE │ └── missing-or-unexpected-timezone.md ├── .devcontainer └── devcontainer.json ├── core ├── src │ ├── test │ │ └── java │ │ │ └── net │ │ │ └── iakovlev │ │ │ └── timeshape │ │ │ ├── TimeZoneEngineBoundedTest.java │ │ │ ├── TimeZoneEngineOutfileBoundedTest.java │ │ │ ├── TimeZoneEngineSerializationTest.java │ │ │ ├── TimeZoneEngineCoordinatesValidationTest.java │ │ │ ├── TimeZoneEnginePolylineTest.java │ │ │ └── TimeZoneEngineTest.java │ └── main │ │ └── java │ │ └── net │ │ └── iakovlev │ │ └── timeshape │ │ ├── SameZoneSpan.java │ │ ├── Index.java │ │ └── TimeZoneEngine.java └── pom.xml ├── CODE_LICENSE ├── geojson-proto ├── src │ └── main │ │ └── protobuf │ │ └── geojson.proto └── pom.xml ├── benchmarks ├── src │ └── main │ │ └── java │ │ └── net │ │ └── iakovlev │ │ └── timeshape │ │ ├── PolylineQueryBenchmark.java │ │ ├── AcceleratedGeometryBenchmark.java │ │ └── BasicGeoOperationsBenchmark.java └── pom.xml ├── Makefile ├── doc └── Architecture.md ├── pom.xml ├── README.MD └── DATA_LICENSE /sonatype.key.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanIakovlev/timeshape/HEAD/sonatype.key.gpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | output.pb.7z 5 | .bsp 6 | .vscode 7 | .cache 8 | dependency-reduced-pom.xml 9 | -------------------------------------------------------------------------------- /test-app/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Maven CI 2 | 3 | on: [pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up JDK 11 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: '11' 19 | distribution: 'temurin' 20 | cache: 'maven' 21 | - name: Run tests 22 | run: make test 23 | -------------------------------------------------------------------------------- /.github/workflows/dependency_graph.yml: -------------------------------------------------------------------------------- 1 | name: Update Dependency Graph 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | dependency-graph: 7 | name: Update Dependency Graph 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-java@v4 12 | with: 13 | java-version: '11' 14 | distribution: 'temurin' 15 | cache: 'maven' 16 | - uses: advanced-security/maven-dependency-submission-action@v4 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/java 3 | { 4 | "name": "sbt on Java 11", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/java:0-11", 7 | 8 | "hostRequirements": { 9 | "memory": "8gb" 10 | }, 11 | 12 | "features": { 13 | "ghcr.io/devcontainers/features/java:1": { 14 | "version": "11", 15 | "imageVariant": "11", 16 | "installMaven": "false", 17 | "installGradle": "false" 18 | }, 19 | "ghcr.io/devcontainers-contrib/features/sbt-sdkman:2": {}, 20 | "ghcr.io/devcontainers-contrib/features/scala-sdkman:2": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedTest.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | 7 | import java.time.ZoneId; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | import static junit.framework.TestCase.assertEquals; 12 | 13 | @RunWith(JUnit4.class) 14 | public class TimeZoneEngineBoundedTest { 15 | private static TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true); 16 | 17 | @Test 18 | public void testSomeZones() { 19 | assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); 20 | } 21 | 22 | @Test 23 | public void testWorld() { 24 | List knownZoneIds = engine.getKnownZoneIds(); 25 | assertEquals(knownZoneIds.size(), 39); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CODE_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Roman Iakovlev 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/missing-or-unexpected-timezone.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Missing or unexpected timezone 3 | about: Use this if Timeshape query returns unexpected or no timezone 4 | title: Missing or unexpected timezone 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Wrong or unexpected timezone is returned 11 | 12 | The most frequent reason for wrong or missing timezone id in Timeshape response is an outdated version of timezone database (`tzdb`) in Java distribution. To help you figure out why it doesn't work, please answer the following questions: 13 | 14 | ## Which Timeshape version are you using? 15 | E.g. 2022g.16 16 | 17 | ## Which Java version are you using? 18 | E.g. 8.0.361, 11.0.8 or 17.0.6. 19 | 20 | ## If you upgrade to the latest Java minor version, does it still fail? 21 | * Yes 22 | - To which Java version have you upgraded? 23 | * :tada: Not anymore! 24 | 25 | ## Which coordinates are you using? 26 | Please use `lat, lon` format, e.g. 61.237, 13.801. You may specify multiple coordinate pairs. 27 | 28 | ## Which timezone is returned? 29 | Please specify a full timezone id, such as `Europe/Berlin` or `Asia/Krasnoyarsk`. If no timezone is returned, please use `None`. 30 | 31 | ## Which timezone is expected? 32 | Please use the same format as above. 33 | 34 | ## Additional information 35 | E.g. if this used to work in some previous Timeshape version, or any other important facts. 36 | -------------------------------------------------------------------------------- /core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineOutfileBoundedTest.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; 4 | import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream; 5 | import org.junit.BeforeClass; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.JUnit4; 9 | 10 | import java.io.FileInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.time.ZoneId; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | import static junit.framework.TestCase.assertEquals; 18 | 19 | @RunWith(JUnit4.class) 20 | public class TimeZoneEngineOutfileBoundedTest { 21 | private static TimeZoneEngine engine = null; 22 | 23 | @BeforeClass 24 | public static void initEngine() { 25 | try (InputStream resourceAsStream = new FileInputStream("target/classes/data.tar.zstd"); 26 | TarArchiveInputStream f = new TarArchiveInputStream(new ZstdCompressorInputStream(resourceAsStream))) { 27 | engine = TimeZoneEngine.initialize(f); 28 | } catch (NullPointerException | IOException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | @Test 34 | public void testSomeZones() { 35 | assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /geojson-proto/src/main/protobuf/geojson.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package net.iakovlev.timeshape.proto; 4 | 5 | message FeatureCollection { 6 | repeated Feature features = 1; 7 | } 8 | 9 | message Feature { 10 | required Geometry geometry = 1; 11 | repeated Property properties = 2; 12 | } 13 | 14 | message Point { 15 | required Position coordinates = 1; 16 | } 17 | 18 | message MultiPoint { 19 | repeated Position coordinates = 1; 20 | } 21 | 22 | message LineString { 23 | repeated Position coordinates = 1; 24 | } 25 | 26 | message MultiLineString { 27 | repeated LineString coordinates = 1; 28 | } 29 | 30 | message Polygon { 31 | repeated LineString coordinates = 1; 32 | } 33 | 34 | message MultiPolygon { 35 | repeated Polygon coordinates = 1; 36 | } 37 | 38 | message GeometryCollection { 39 | repeated Geometry geometries = 1; 40 | } 41 | 42 | message Position { 43 | required float lon = 1; 44 | required float lat = 2; 45 | } 46 | 47 | message Geometry { 48 | oneof type { 49 | Point point = 1; 50 | MultiPoint multiPoint = 2; 51 | LineString lineString = 3; 52 | MultiLineString multiLineString = 4; 53 | Polygon polygon = 5; 54 | MultiPolygon multiPolygon = 6; 55 | GeometryCollection geometryCollection = 7; 56 | } 57 | } 58 | 59 | message Property { 60 | required string key = 1; 61 | oneof value { 62 | string valueString = 2; 63 | double valueNumber = 3; 64 | } 65 | } -------------------------------------------------------------------------------- /benchmarks/src/main/java/net/iakovlev/timeshape/PolylineQueryBenchmark.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import net.iakovlev.timeshape.TimeZoneEngine; 4 | import org.openjdk.jmh.annotations.*; 5 | import org.openjdk.jmh.infra.Blackhole; 6 | 7 | import java.util.ArrayList; 8 | 9 | public class PolylineQueryBenchmark { 10 | @State(Scope.Benchmark) 11 | public static class BenchmarkState { 12 | double latStart = 52.52; 13 | double lonStart = 13.40; 14 | double latEnd = 56.52; 15 | double lonEnd = 16.40; 16 | 17 | int steps = 200; 18 | 19 | double latStep = Math.abs(latEnd - latStart) / steps; 20 | double lonStep = Math.abs(lonEnd - lonStart) / steps; 21 | double[] points; 22 | TimeZoneEngine engine = TimeZoneEngine.initialize(); 23 | ArrayList pointsList = new ArrayList<>(); 24 | 25 | @Setup 26 | public void setup() { 27 | for (int i = 0; i < steps; i++) { 28 | pointsList.add(latStart + latStep * i); 29 | pointsList.add(lonStart + lonStep * i); 30 | } 31 | 32 | points = pointsList.stream().mapToDouble(Double::doubleValue).toArray(); 33 | } 34 | } 35 | 36 | @Benchmark 37 | public void testQueryPoints(BenchmarkState state, Blackhole blackhole) { 38 | for (int i = 0; i < state.points.length - 1; i += 2) { 39 | blackhole.consume(state.engine.query(state.points[i], state.points[i+1])); 40 | } 41 | } 42 | 43 | @Benchmark 44 | public void testQueryPolyline(BenchmarkState state, Blackhole blackhole) { 45 | blackhole.consume(state.engine.queryPolyline(state.points)); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/java/net/iakovlev/timeshape/SameZoneSpan.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import java.time.ZoneId; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Represents contiguous span of points belonging to the same set of time zones 12 | */ 13 | public final class SameZoneSpan { 14 | public Set getZoneIds() { 15 | return new HashSet<>(zoneIds); 16 | } 17 | 18 | /** 19 | * Last index in the array of points (the polyline) which has the same ZoneId. 20 | * See {@link TimeZoneEngine#queryPolyline(double[])} for explanation. 21 | * @return 22 | */ 23 | public int getEndIndex() { 24 | return endIndex; 25 | } 26 | 27 | private final Set zoneIds; 28 | private final int endIndex; 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(zoneIds, endIndex); 33 | } 34 | 35 | @Override 36 | public boolean equals(Object obj) { 37 | if (this == obj) 38 | return true; 39 | if (!(obj instanceof SameZoneSpan)) 40 | return false; 41 | SameZoneSpan other = (SameZoneSpan) obj; 42 | return other.endIndex == endIndex && other.zoneIds.equals(zoneIds); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return String.format("%s: end index %d", zoneIds, endIndex); 48 | } 49 | 50 | SameZoneSpan(Set zoneIds, int endIndex) { 51 | this.zoneIds = new HashSet<>(zoneIds); 52 | this.endIndex = endIndex; 53 | } 54 | 55 | static SameZoneSpan fromIndexEntries(List entries, int index) { 56 | return new SameZoneSpan(entries.stream().map(e -> e.zoneId).collect(Collectors.toSet()), index); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test-app/src/main/java/net/iakovlev/timeshape/testapp/Main.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape.testapp; 2 | 3 | import net.iakovlev.timeshape.TimeZoneEngine; 4 | import org.openjdk.jol.info.GraphLayout; 5 | import org.openjdk.jol.vm.VM; 6 | 7 | import java.time.ZoneId; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.concurrent.ConcurrentMap; 11 | import java.util.stream.Collector; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.IntStream; 14 | 15 | import static java.lang.System.out; 16 | 17 | public class Main { 18 | static public void main(String[] args) { 19 | long start = System.currentTimeMillis(); 20 | TimeZoneEngine engine = TimeZoneEngine.initialize(true); 21 | long total = System.currentTimeMillis() - start; 22 | out.println("initialization took " + total + " milliseconds"); 23 | out.println(engine.query(52.52, 13.40)); 24 | out.println(VM.current().details()); 25 | out.println(GraphLayout.parseInstance(engine).toFootprint()); 26 | double lonMin = -180.0; 27 | double lonMax = 180.0; 28 | double latMin = -90.0; 29 | double latMax = 90.0; 30 | String prev = ""; 31 | for (int i = 0; i < 100; i++) { 32 | for (int j = 0; j < 100; j++) { 33 | double latitude = (latMax - latMin) / 100 * i; 34 | double longitude = (lonMax - lonMin) / 100 * i; 35 | final List result = engine.queryAll(latitude, longitude); 36 | if (result.size() > 1) { 37 | String report = "Found multiple zones (" + result + ") for " + latitude + ", " + longitude; 38 | if (!report.equals(prev)) { 39 | out.println("Found multiple zones (" + result + ") for " + latitude + ", " + longitude); 40 | } 41 | prev = report; 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.iakovlev 9 | timeshape-parent 10 | 2025b.28 11 | 12 | 13 | timeshape-testapp 14 | jar 15 | 16 | Timeshape Test App 17 | Test application for Timeshape 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 25 | net.iakovlev 26 | timeshape 27 | 28 | 29 | org.openjdk.jol 30 | jol-core 31 | 0.9 32 | 33 | 34 | ch.qos.logback 35 | logback-classic 36 | 1.2.13 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-jar-plugin 45 | 3.3.0 46 | 47 | 48 | 49 | net.iakovlev.timeshape.testapp.Main 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [master] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: 'maven' 23 | 24 | - name: Setup Sonatype key 25 | run: | 26 | mkdir -p /tmp/gpg 27 | chmod 700 /tmp/gpg 28 | export GNUPGHOME=/tmp/gpg 29 | gpg --batch --yes --decrypt --passphrase "${{ secrets.SONATYPE_KEY_PASSWORD }}" sonatype.key.gpg | gpg --batch --yes --import 30 | gpg --list-secret-keys --keyid-format LONG 31 | 32 | - name: Setup Maven settings 33 | run: | 34 | mkdir -p ~/.m2 35 | cat <<-EOF > ~/.m2/settings.xml 36 | 37 | 38 | 39 | central 40 | ${{ secrets.CENTRAL_SONATYPE_COM_USERNAME }} 41 | ${{ secrets.CENTRAL_SONATYPE_COM_PASSWORD }} 42 | 43 | 44 | 45 | 46 | central 47 | 48 | true 49 | 50 | 51 | ${{ secrets.SONATYPE_KEY_ID }} 52 | 53 | 54 | 55 | 56 | EOF 57 | 58 | - name: Run tests and publish to Maven Central 59 | run: | 60 | export GNUPGHOME=/tmp/gpg 61 | export SOURCE_DATE_EPOCH=$(date +%s) 62 | make test 63 | 64 | # Deploy to Central Portal (handles both snapshots and releases) 65 | PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) 66 | echo "Project version: $PROJECT_VERSION" 67 | echo "Deploying to Sonatype Central Portal..." 68 | mvn deploy -Prelease -DskipTests -Dgpg.passphrase="" -------------------------------------------------------------------------------- /benchmarks/src/main/java/net/iakovlev/timeshape/AcceleratedGeometryBenchmark.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import net.iakovlev.timeshape.TimeZoneEngine; 4 | import org.openjdk.jmh.annotations.*; 5 | import org.openjdk.jmh.infra.Blackhole; 6 | 7 | import java.util.ArrayList; 8 | 9 | public class AcceleratedGeometryBenchmark { 10 | @State(Scope.Benchmark) 11 | public static class BenchmarkState { 12 | 13 | float[] cityCoordinates = { 14 | 35.6762f, 139.6503f, // Tokyo, Japan 15 | 31.2304f, 121.4737f, // Shanghai, China 16 | 23.1291f, 113.2644f, // Guangzhou, China 17 | 28.7041f, 77.1025f, // Delhi, India 18 | 19.4326f, -99.1332f, // Mexico City, Mexico 19 | -23.551f, -46.6333f, // Sao Paulo, Brazil 20 | 34.6937f, 135.5023f, // Osaka, Japan 21 | 13.7563f, 100.5018f, // Bangkok, Thailand 22 | 39.9042f, 116.4074f, // Beijing, China 23 | 19.0760f, 72.8777f, // Mumbai, India 24 | 40.7128f, -74.0060f, // New York City, USA 25 | 22.5726f, 88.3639f, // Kolkata, India 26 | 14.5995f, 120.9842f, // Manila, Philippines 27 | 23.8103f, 90.4125f, // Dhaka, Bangladesh 28 | 3.1390f, 101.6869f, // Kuala Lumpur, Malaysia 29 | 30.0444f, 31.2357f, // Cairo, Egypt 30 | 34.0522f, -118.2437f, // Los Angeles, USA 31 | 31.5497f, 74.3436f, // Lahore, Pakistan 32 | 6.5244f, 3.3792f, // Lagos, Nigeria 33 | 55.7558f, 37.6173f // Moscow, Russia 34 | }; 35 | 36 | TimeZoneEngine engineNoAcceleration = TimeZoneEngine.initialize(false); 37 | TimeZoneEngine engineWithAcceleration = TimeZoneEngine.initialize(true); 38 | } 39 | 40 | @Benchmark 41 | public void testAcceleratedEngine(BenchmarkState state, Blackhole blackhole) { 42 | for (int i = 0; i < state.cityCoordinates.length; i += 2) { 43 | blackhole.consume( 44 | state.engineWithAcceleration.query(state.cityCoordinates[i], state.cityCoordinates[i + 1])); 45 | } 46 | 47 | } 48 | 49 | @Benchmark 50 | public void testNonAcceleratedEngine(BenchmarkState state, Blackhole blackhole) { 51 | for (int i = 0; i < state.cityCoordinates.length; i += 2) { 52 | blackhole.consume( 53 | state.engineNoAcceleration.query(state.cityCoordinates[i], state.cityCoordinates[i + 1])); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /benchmarks/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.iakovlev 9 | timeshape-parent 10 | 2025b.28 11 | 12 | 13 | timeshape-benchmarks 14 | jar 15 | 16 | Timeshape Benchmarks 17 | JMH benchmarks for Timeshape 18 | 19 | 20 | true 21 | 1.37 22 | 23 | 24 | 25 | 26 | net.iakovlev 27 | timeshape 28 | 29 | 30 | org.openjdk.jmh 31 | jmh-core 32 | ${jmh.version} 33 | 34 | 35 | org.openjdk.jmh 36 | jmh-generator-annprocess 37 | ${jmh.version} 38 | provided 39 | 40 | 41 | 42 | 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-shade-plugin 47 | 3.5.0 48 | 49 | 50 | package 51 | 52 | shade 53 | 54 | 55 | timeshape-benchmarks 56 | 57 | 58 | org.openjdk.jmh.Main 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineSerializationTest.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | 7 | import java.io.IOException; 8 | import java.io.BufferedOutputStream; 9 | import java.io.FileInputStream; 10 | import java.io.FileOutputStream; 11 | import java.io.ObjectInputStream; 12 | import java.io.ObjectOutputStream; 13 | 14 | import java.time.ZoneId; 15 | import java.util.List; 16 | import java.util.Optional; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | import java.io.File; 20 | 21 | import static junit.framework.TestCase.assertEquals; 22 | import static junit.framework.TestCase.assertTrue; 23 | 24 | @RunWith(JUnit4.class) 25 | public class TimeZoneEngineSerializationTest { 26 | private static TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true); 27 | 28 | @Test 29 | public void testSerialzation() { 30 | File f = new File ("./Engine.cache"); 31 | try { 32 | serializeTimeZoneEngine (f, engine); 33 | TimeZoneEngine engine2 = deserializeTimeZoneEngine (f); 34 | assertEquals(engine.query(52.52, 13.40), engine2.query(52.52, 13.40)); 35 | 36 | f.delete (); 37 | } 38 | catch (IOException | ClassNotFoundException e) { 39 | throw new RuntimeException(e); 40 | } 41 | 42 | } 43 | 44 | 45 | /** 46 | * Serializes an instance of {@link TimeZoneEngine} to a file 47 | * This is a blocking long running operation. 48 | * 49 | * @param f Destination File. 50 | * @param eng Instance of TimeZoneEngine to serialize 51 | */ 52 | 53 | public void serializeTimeZoneEngine (File f, TimeZoneEngine eng) throws IOException 54 | { 55 | FileOutputStream fileOutputStream = new FileOutputStream (f, false); 56 | try (ObjectOutputStream objectOutputStream = new ObjectOutputStream (new BufferedOutputStream (fileOutputStream))) { 57 | objectOutputStream.writeObject (eng); 58 | objectOutputStream.flush (); 59 | } 60 | } 61 | 62 | 63 | /** 64 | * Creates a new instance of {@link TimeZoneEngine} from previously serialized data. 65 | * This is a blocking long running operation. 66 | * 67 | * @return an initialized instance of {@link TimeZoneEngine} 68 | */ 69 | public static TimeZoneEngine deserializeTimeZoneEngine (File f) throws IOException, ClassNotFoundException 70 | { 71 | FileInputStream fileInputStream = new FileInputStream(f); 72 | try (ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { 73 | return (TimeZoneEngine) objectInputStream.readObject (); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineCoordinatesValidationTest.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import net.iakovlev.timeshape.TimeZoneEngine; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.junit.runners.JUnit4; 7 | 8 | import static junit.framework.TestCase.assertEquals; 9 | 10 | @RunWith(JUnit4.class) 11 | public class TimeZoneEngineCoordinatesValidationTest { 12 | @Test 13 | public void testMinimumLatitudeOutOfBounds() { 14 | try { 15 | TimeZoneEngine.initialize(-100, 0, 0, 0, false); 16 | } catch (IllegalArgumentException e) { 17 | assertEquals(e.getMessage(), "minimum latitude -100.000000 is out of range: must be -90 <= latitude <= 90;"); 18 | } 19 | } 20 | @Test 21 | public void testMinimumLongitudeOutOfBounds() { 22 | try { 23 | TimeZoneEngine.initialize(0, -190, 0, 0, false); 24 | } catch (IllegalArgumentException e) { 25 | assertEquals(e.getMessage(), "minimum longitude -190.000000 is out of range: must be -180 <= longitude <= 180;"); 26 | } 27 | } 28 | @Test 29 | public void testMaximumLatitudeOutOfBounds() { 30 | try { 31 | TimeZoneEngine.initialize(0, 0, 100, 0, false); 32 | } catch (IllegalArgumentException e) { 33 | assertEquals(e.getMessage(), "maximum latitude 100.000000 is out of range: must be -90 <= latitude <= 90;"); 34 | } 35 | } 36 | @Test 37 | public void testMaximumLongitudeOutOfBounds() { 38 | try { 39 | TimeZoneEngine.initialize(0, 0, 0, 190, false); 40 | } catch (IllegalArgumentException e) { 41 | assertEquals(e.getMessage(), "maximum longitude 190.000000 is out of range: must be -180 <= longitude <= 180;"); 42 | } 43 | } 44 | @Test 45 | public void testInconsistentLatitudes() { 46 | try { 47 | TimeZoneEngine.initialize(0, 0, -1, 0, false); 48 | } catch (IllegalArgumentException ex) { 49 | assertEquals(ex.getMessage(), "maximum latitude -1.000000 is less than minimum latitude 0.000000;"); 50 | } 51 | } 52 | @Test 53 | public void testInconsistentLongitudes() { 54 | try { 55 | TimeZoneEngine.initialize(0, 0, 0, -1, false); 56 | } catch (IllegalArgumentException ex) { 57 | assertEquals(ex.getMessage(), "maximum longitude -1.000000 is less than minimum longitude 0.000000;"); 58 | } 59 | } 60 | @Test 61 | public void testMultipleErrors() { 62 | try { 63 | TimeZoneEngine.initialize(0, 0, -1, -1, false); 64 | } catch (IllegalArgumentException ex) { 65 | assertEquals(ex.getMessage(), "maximum latitude -1.000000 is less than minimum latitude 0.000000; maximum longitude -1.000000 is less than minimum longitude 0.000000;"); 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /geojson-proto/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.iakovlev 9 | timeshape-parent 10 | 2025b.28 11 | 12 | 13 | 14 | 4.31.1 15 | 16 | 17 | geojson-proto 18 | 1.1.6 19 | jar 20 | 21 | GeoJSON Proto 22 | Protocol Buffers definitions for GeoJSON 23 | 24 | 25 | 26 | com.google.protobuf 27 | protobuf-java 28 | ${protobuf.version} 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.sonatype.central 36 | central-publishing-maven-plugin 37 | 38 | true 39 | 40 | 41 | 42 | io.github.ascopes 43 | protobuf-maven-plugin 44 | 3.10.2 45 | 46 | ${protobuf.version} 47 | 48 | 49 | 50 | 51 | generate 52 | 53 | 54 | 55 | 56 | 57 | kr.motd.maven 58 | os-maven-plugin 59 | 1.7.1 60 | 61 | 62 | initialize 63 | 64 | detect 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-jar-plugin 72 | 3.3.0 73 | 74 | 75 | 76 | net.iakovlev.geojson.proto 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /core/src/test/java/net/iakovlev/timeshape/TimeZoneEnginePolylineTest.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import net.iakovlev.timeshape.SameZoneSpan; 4 | import net.iakovlev.timeshape.TimeZoneEngine; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.JUnit4; 8 | 9 | import java.time.ZoneId; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.HashSet; 13 | 14 | import static junit.framework.TestCase.assertEquals; 15 | 16 | @RunWith(JUnit4.class) 17 | public class TimeZoneEnginePolylineTest { 18 | private static TimeZoneEngine engine = TimeZoneEngine.initialize(true); 19 | 20 | @Test 21 | public void test2points() { 22 | assertEquals(engine.queryPolyline(new double[]{52.52, 13.40, 56.52, 16.40}), 23 | Arrays.asList( 24 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 1), 25 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 3))); 26 | } 27 | 28 | @Test 29 | public void test3points() { 30 | assertEquals(engine.queryPolyline(new double[]{52.52, 13.40, 54.52, 15.40, 56.52, 16.40}), 31 | Arrays.asList( 32 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 1), 33 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-1"))), 3), 34 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 5))); 35 | } 36 | 37 | @Test 38 | public void testSpanWithMultiPoints() { 39 | assertEquals(engine.queryPolyline(new double[]{52.52, 13.40, 52.53, 13.30, 54.52, 15.40, 56.52, 16.40, 56.52, 16.40}), 40 | Arrays.asList( 41 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 3), 42 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-1"))), 5), 43 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 9))); 44 | } 45 | 46 | @Test 47 | public void testNonMatchingPoints() { 48 | assertEquals( 49 | Arrays.asList( 50 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-12"))), 5), 51 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 9), 52 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-1"))), 11), 53 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 13)), 54 | engine.queryPolyline(new double[]{80, 180, 80, 180, 80, 180, 52.52, 13.40, 52.53, 13.30, 54.52, 15.40, 56.52, 16.40})); 55 | } 56 | 57 | @Test 58 | public void testNonMatchingLastPoints() { 59 | assertEquals( 60 | Arrays.asList( 61 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Vilnius"))), 3), 62 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Minsk"))), 5), 63 | new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-12"))), 9) 64 | ), 65 | engine.queryPolyline(new double[]{54.89, 23.91, 55.13, 25.57, 54.29, 28.32, 80, 180, 80, 180})); 66 | } 67 | } -------------------------------------------------------------------------------- /benchmarks/src/main/java/net/iakovlev/timeshape/BasicGeoOperationsBenchmark.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import com.esri.core.geometry.*; 4 | import net.iakovlev.timeshape.TimeZoneEngine; 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.Scope; 7 | import org.openjdk.jmh.annotations.Setup; 8 | import org.openjdk.jmh.annotations.State; 9 | import org.openjdk.jmh.infra.Blackhole; 10 | 11 | import java.lang.reflect.Field; 12 | import java.util.ArrayList; 13 | 14 | public class BasicGeoOperationsBenchmark { 15 | @State(Scope.Benchmark) 16 | public static class BenchmarkState { 17 | TimeZoneEngine engine = TimeZoneEngine.initialize(); 18 | Index index; 19 | QuadTree quadTree; 20 | Point p = new Point(13.31, 52.52); 21 | ArrayList entries; 22 | Geometry matchingGeometry; 23 | ArrayList nonMatchingGeometries = new ArrayList<>(); 24 | SpatialReference spatialReference = SpatialReference.create(4326); 25 | @Setup 26 | public void setup() { 27 | try { 28 | Field indexField = engine.getClass().getDeclaredField("index"); 29 | indexField.setAccessible(true); 30 | index = (Index) indexField.get(engine); 31 | Field quadTreeField = index.getClass().getDeclaredField("quadTree"); 32 | quadTreeField.setAccessible(true); 33 | quadTree = (QuadTree) quadTreeField.get(index); 34 | Field zoneIdsField = index.getClass().getDeclaredField("zoneIds"); 35 | zoneIdsField.setAccessible(true); 36 | entries = (ArrayList) zoneIdsField.get(index); 37 | QuadTree.QuadTreeIterator iterator = quadTree.getIterator(p, 0); 38 | for (int i = iterator.next(); i >= 0; i = iterator.next()) { 39 | int element = quadTree.getElement(i); 40 | Index.Entry entry = entries.get(element); 41 | if (GeometryEngine.contains(entry.geometry, p, spatialReference)) { 42 | matchingGeometry = entry.geometry; 43 | } else { 44 | nonMatchingGeometries.add(entry.geometry); 45 | } 46 | 47 | } 48 | } catch (NoSuchFieldException | IllegalAccessException e) { 49 | e.printStackTrace(); 50 | throw new RuntimeException(e); 51 | } 52 | } 53 | 54 | } 55 | 56 | 57 | @Benchmark 58 | public void testQuadTree(BenchmarkState state, Blackhole blackhole) { 59 | QuadTree.QuadTreeIterator iterator = state.quadTree.getIterator(state.p, 0); 60 | for (int i = iterator.next(); i >= 0; i = iterator.next()) { 61 | blackhole.consume(state.quadTree.getElement(i)); 62 | } 63 | } 64 | 65 | @Benchmark 66 | public void testSearchInNonMatchingGeometry(BenchmarkState state, Blackhole blackhole) { 67 | for (Geometry g : state.nonMatchingGeometries) { 68 | blackhole.consume(GeometryEngine.contains(g, state.p, state.spatialReference)); 69 | } 70 | } 71 | 72 | @Benchmark 73 | public void testSearchInMatchingGeometry(BenchmarkState state, Blackhole blackhole) { 74 | blackhole.consume(GeometryEngine.contains(state.matchingGeometry, state.p, state.spatialReference)); 75 | } 76 | 77 | @Benchmark 78 | public void testIndexQuery(BenchmarkState state, Blackhole blackhole) { 79 | blackhole.consume(state.index.query(state.p.getY(), state.p.getX())); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineTest.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.JUnit4; 6 | 7 | import java.io.IOException; 8 | import java.time.ZoneId; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | 14 | import static junit.framework.TestCase.assertEquals; 15 | import static junit.framework.TestCase.assertTrue; 16 | 17 | @RunWith(JUnit4.class) 18 | public class TimeZoneEngineTest { 19 | private static TimeZoneEngine engine = TimeZoneEngine.initialize(); 20 | 21 | @Test 22 | public void testSomeZones() { 23 | assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); 24 | assertEquals(Optional.of(ZoneId.of("Asia/Tomsk")), engine.query(56.49771, 84.97437)); 25 | assertEquals(Optional.of(ZoneId.of("America/Santiago")), engine.query(-33.459229, -70.645348)); 26 | assertEquals(Optional.of(ZoneId.of("Asia/Krasnoyarsk")), engine.query(56.01839, 92.86717)); 27 | assertEquals(Optional.of(ZoneId.of("Africa/Abidjan")), engine.query(5.345317, -4.024429)); 28 | assertEquals(Optional.of(ZoneId.of("America/New_York")), engine.query(40.785091, -73.968285)); 29 | assertEquals(Optional.of(ZoneId.of("Australia/Sydney")), engine.query(-33.865143, 151.215256)); 30 | assertEquals(Optional.of(ZoneId.of("Etc/GMT+1")), engine.query(38.00, -15.2814)); 31 | assertEquals(Optional.of(ZoneId.of("Asia/Shanghai")), engine.query(39.601, 79.201)); 32 | assertEquals(Optional.of(ZoneId.of("Asia/Shanghai")), engine.query(27.45, 89.05)); 33 | assertEquals(List.of(ZoneId.of("America/Ciudad_Juarez")), engine.queryAll(31.752, -106.457)); 34 | assertEquals(Optional.of(ZoneId.of("Europe/Bucharest")), engine.query(46.16799, 20.71524)); 35 | } 36 | 37 | @Test 38 | public void testBoundariesAndMultiPolygons() { 39 | assertEquals(Optional.of(ZoneId.of("Europe/Amsterdam")), engine.query(51.4457, 4.9248)); 40 | assertEquals(Optional.of(ZoneId.of("Europe/Brussels")), engine.query(51.4457, 4.9250)); 41 | assertEquals(Optional.of(ZoneId.of("Europe/Brussels")), engine.query(51.4437,4.9186)); 42 | assertEquals(Optional.of(ZoneId.of("Europe/Amsterdam")), engine.query(51.4438,4.9181)); 43 | } 44 | 45 | @Test 46 | public void testMultipleTimeZonesInResponse() throws IOException { 47 | 48 | List expected1 = new java.util.ArrayList<>(); 49 | expected1.add(ZoneId.of("Africa/Juba")); 50 | expected1.add(ZoneId.of("Africa/Khartoum")); 51 | assertEquals(expected1, engine.queryAll(9.75, 28.45)); 52 | 53 | List expected2 = new java.util.ArrayList<>(); 54 | expected2.add(ZoneId.of("America/Argentina/Rio_Gallegos")); 55 | expected2.add(ZoneId.of("America/Punta_Arenas")); 56 | assertEquals(expected2, engine.queryAll(-49.5, -73.3)); 57 | 58 | List expected3 = new java.util.ArrayList<>(); 59 | expected3.add(ZoneId.of("America/La_Paz")); 60 | expected3.add(ZoneId.of("America/Porto_Velho")); 61 | assertEquals(expected3, engine.queryAll(-10.8, -65.35)); 62 | 63 | List expected4 = new java.util.ArrayList<>(); 64 | expected4.add(ZoneId.of("America/Moncton")); 65 | expected4.add(ZoneId.of("America/New_York")); 66 | assertEquals(expected4, engine.queryAll(44.5, -67.15)); 67 | 68 | List expected5 = new java.util.ArrayList<>(); 69 | expected5.add(ZoneId.of("Asia/Hebron")); 70 | expected5.add(ZoneId.of("Asia/Jerusalem")); 71 | assertEquals(expected5, engine.queryAll(31.95, 35.2)); 72 | 73 | List expected6 = new java.util.ArrayList<>(); 74 | expected6.add(ZoneId.of("Asia/Shanghai")); 75 | expected6.add(ZoneId.of("Asia/Thimphu")); 76 | assertEquals(expected6, engine.queryAll(27.45, 89.05)); 77 | 78 | List expected7 = new java.util.ArrayList<>(); 79 | expected7.add(ZoneId.of("Europe/Bucharest")); 80 | expected7.add(ZoneId.of("Europe/Budapest")); 81 | assertEquals(expected7, engine.queryAll(46.16799, 20.71524)); 82 | 83 | List expected = new java.util.ArrayList<>(); 84 | expected.add(ZoneId.of("Asia/Shanghai")); 85 | expected.add(ZoneId.of("Asia/Urumqi")); 86 | assertEquals(expected, engine.queryAll(39.601, 79.201)); 87 | 88 | } 89 | 90 | @Test 91 | public void testWorld() { 92 | Set engineZoneIds = engine.getKnownZoneIds().stream().map(ZoneId::getId).collect(Collectors.toSet()); 93 | assertTrue(java.time.ZoneId.getAvailableZoneIds().containsAll(engineZoneIds)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.iakovlev 9 | timeshape-parent 10 | 2025b.28 11 | 12 | 13 | timeshape 14 | jar 15 | 16 | Timeshape 17 | A Java library for mapping between coordinates and timezones 18 | 19 | 20 | 21 | com.esri.geometry 22 | esri-geometry-api 23 | 2.2.4 24 | 25 | 26 | org.slf4j 27 | slf4j-api 28 | 1.7.30 29 | 30 | 31 | net.iakovlev 32 | geojson-proto 33 | 34 | 35 | org.apache.commons 36 | commons-compress 37 | 38 | 39 | com.github.luben 40 | zstd-jni 41 | 42 | 43 | com.fasterxml.jackson.core 44 | jackson-core 45 | 46 | 47 | 48 | 49 | junit 50 | junit 51 | 4.13.1 52 | test 53 | 54 | 55 | com.novocode 56 | junit-interface 57 | 0.11 58 | test 59 | 60 | 61 | junit 62 | junit-dep 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.sonatype.central 72 | central-publishing-maven-plugin 73 | 74 | true 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-jar-plugin 80 | 3.3.0 81 | 82 | 83 | 84 | net.iakovlev.timeshape 85 | 86 | 87 | 88 | 89 | 90 | org.codehaus.mojo 91 | exec-maven-plugin 92 | 3.1.0 93 | 94 | 95 | generate-resources 96 | generate-resources 97 | 98 | exec 99 | 100 | 101 | make 102 | ${project.parent.basedir} 103 | 104 | generate-data 105 | 106 | 107 | ${data.version} 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Timeshape data processing 2 | # Handles timezone data download and resource generation for Maven build 3 | 4 | # Variables 5 | DATA_VERSION ?= 2025b 6 | BUILDER_JAR = builder/target/timeshape-builder.jar 7 | OUTPUT_DIR = core/target/classes 8 | OUTPUT_FILE = $(OUTPUT_DIR)/data.tar.zstd 9 | TIMEZONE_URL = https://github.com/evansiroky/timezone-boundary-builder/releases/download/$(DATA_VERSION)/timezones-with-oceans.geojson.zip 10 | CACHE_DIR = .cache 11 | CACHED_DATA = $(CACHE_DIR)/timezones-$(DATA_VERSION).zip 12 | 13 | # Use Maven to execute Java commands (same JVM as Maven uses) 14 | MVN = mvn 15 | 16 | # Ensure directories exist 17 | $(OUTPUT_DIR): 18 | @mkdir -p $(OUTPUT_DIR) 19 | 20 | $(CACHE_DIR): 21 | @mkdir -p $(CACHE_DIR) 22 | 23 | # Build the builder JAR if it doesn't exist or is out of date 24 | $(BUILDER_JAR): builder/src/main/java/net/iakovlev/timeshape/*.java geojson-proto/src/main/protobuf/geojson.proto 25 | @echo "Building timeshape-builder and dependencies..." 26 | @echo " - Installing parent POM..." 27 | @$(MVN) -q -N install 28 | @echo " - Compiling protobuf and installing geojson-proto..." 29 | @cd geojson-proto && $(MVN) -q compile install 30 | @echo " - Compiling builder..." 31 | @cd builder && $(MVN) -q compile 32 | @echo " - Creating builder assembly..." 33 | @cd builder && $(MVN) -q assembly:single 34 | 35 | # Download and cache timezone data 36 | $(CACHED_DATA): $(CACHE_DIR) 37 | @echo "Downloading timezone data version: $(DATA_VERSION)" 38 | @if curl -L -f -s -o $(CACHED_DATA) "$(TIMEZONE_URL)"; then \ 39 | echo "Downloaded timezone data to $(CACHED_DATA)"; \ 40 | else \ 41 | echo "Failed to download timezone data from $(TIMEZONE_URL)"; \ 42 | exit 1; \ 43 | fi 44 | 45 | # Execute builder using Maven (ensures same Java as Maven) 46 | run-builder: 47 | @if [ -f "$(CACHED_DATA)" ]; then \ 48 | echo "Using cached timezone data from $(CACHED_DATA)"; \ 49 | cd builder && $(MVN) -q exec:java -Dexec.mainClass="net.iakovlev.timeshape.Main" -Dexec.args="$(CACHED_DATA) ../$(OUTPUT_FILE)"; \ 50 | elif [ -f "/tmp/timezones-$(DATA_VERSION).zip" ]; then \ 51 | echo "Using timezone data from /tmp/timezones-$(DATA_VERSION).zip"; \ 52 | cd builder && $(MVN) -q exec:java -Dexec.mainClass="net.iakovlev.timeshape.Main" -Dexec.args="/tmp/timezones-$(DATA_VERSION).zip ../$(OUTPUT_FILE)"; \ 53 | else \ 54 | echo "Downloading and processing timezone data directly"; \ 55 | cd builder && $(MVN) -q exec:java -Dexec.mainClass="net.iakovlev.timeshape.Main" -Dexec.args="$(DATA_VERSION) ../$(OUTPUT_FILE)"; \ 56 | fi 57 | 58 | # Generate data.tar.zstd resource file 59 | generate-data: $(OUTPUT_DIR) $(BUILDER_JAR) 60 | @if [ -f "$(OUTPUT_FILE)" ]; then \ 61 | echo "Timeshape resource exists at $(OUTPUT_FILE), skipping creation"; \ 62 | else \ 63 | echo "Timeshape resource doesn't exist, creating it now"; \ 64 | echo "Generating timezone data with version: $(DATA_VERSION)"; \ 65 | $(MAKE) run-builder; \ 66 | if [ -f "$(OUTPUT_FILE)" ]; then \ 67 | echo "Successfully generated $(OUTPUT_FILE)"; \ 68 | else \ 69 | echo "Failed to generate $(OUTPUT_FILE)"; \ 70 | exit 1; \ 71 | fi; \ 72 | fi 73 | 74 | # Clean all build artifacts and generated data 75 | clean: 76 | @echo "Cleaning build artifacts and generated data..." 77 | @mvn clean 78 | @rm -f /tmp/timezones-*.zip 79 | 80 | # Clean everything including cache 81 | clean-all: clean 82 | @echo "Cleaning cache..." 83 | @rm -rf $(CACHE_DIR) 84 | 85 | # Test all modules 86 | test: generate-data 87 | @echo "Running tests..." 88 | @mvn test 89 | 90 | # Deploy to Sonatype Central Portal (handles both snapshots and releases) 91 | deploy: generate-data 92 | @echo "Deploying to Sonatype Central Portal..." 93 | @PROJECT_VERSION=$$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout); \ 94 | echo "Deploying version $$PROJECT_VERSION to Central Portal"; \ 95 | mvn deploy -Prelease -DskipTests 96 | 97 | # Force regenerate data (useful for development) 98 | force-generate-data: clean generate-data 99 | 100 | # Dry-run deployment (shows what would be deployed) 101 | deploy-dry-run: 102 | @echo "Dry-run deployment check..." 103 | @PROJECT_VERSION=$$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout); \ 104 | echo "Current version: $$PROJECT_VERSION"; \ 105 | if [[ "$$PROJECT_VERSION" == *"-SNAPSHOT" ]]; then \ 106 | echo "-> Would deploy SNAPSHOT to Sonatype Central Portal"; \ 107 | else \ 108 | echo "-> Would deploy RELEASE to Sonatype Central Portal"; \ 109 | fi; \ 110 | echo "-> Command: mvn deploy -Prelease -DskipTests"; \ 111 | echo "Modules that would be deployed:"; \ 112 | mvn help:evaluate -Dexpression=project.modules -q -DforceStdout | grep -v "maven.deploy.skip=true" || true 113 | 114 | # Show current configuration 115 | show-config: 116 | @echo "Current configuration:" 117 | @echo " DATA_VERSION: $(DATA_VERSION)" 118 | @echo " BUILDER_JAR: $(BUILDER_JAR)" 119 | @echo " OUTPUT_FILE: $(OUTPUT_FILE)" 120 | @echo " TIMEZONE_URL: $(TIMEZONE_URL)" 121 | @echo " MVN: $(MVN)" 122 | 123 | # Help target 124 | help: 125 | @echo "Available targets:" 126 | @echo " generate-data - Generate data.tar.zstd resource file" 127 | @echo " run-builder - Execute builder using Maven" 128 | @echo " test - Run all tests" 129 | @echo " deploy - Deploy to Sonatype Central Portal" 130 | @echo " deploy-dry-run - Show what would be deployed without deploying" 131 | @echo " clean - Clean build artifacts and generated data" 132 | @echo " clean-all - Clean everything including cache" 133 | @echo " force-generate-data - Force regenerate data file" 134 | @echo " show-config - Show current configuration" 135 | @echo " help - Show this help message" 136 | @echo "" 137 | @echo "Variables:" 138 | @echo " DATA_VERSION - Timezone data version (default: $(DATA_VERSION))" 139 | 140 | .PHONY: run-builder generate-data test deploy deploy-dry-run clean clean-all force-generate-data show-config help -------------------------------------------------------------------------------- /doc/Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | This document describes how the Timeshape is designed, which technologies it uses, and how to build it yourself. 3 | This might be helpful if you want to contribute to it, or just to understand how it works. 4 | 5 | ## Overview 6 | Timeshape is built around the idea of taking the source data with time zones in [GeoJSON](http://geojson.org/) format, 7 | convert it into something more efficient to store and read than GeoJSON, and package converted data together with 8 | the code that can read and query it into a single artifact (JAR file). Efficiency is the key word here, because the 9 | source data is quite big, and using it as is would impose too high memory and artifact size requirements on the users 10 | of Timeshape. 11 | 12 | Timeshape currently uses compressed [protocol buffers](https://developers.google.com/protocol-buffers/) 13 | (a.k.a. protobuf) as the target data format. The protobuf data is compressed using ZStandard method, which allows to reach 14 | relatively small artifact size: 19 MB total JAR size vs 57 MB for the source data only (GeoJSON compressed with zip). 15 | The biggest win in terms of size is, however, not due to efficiency of protobuf vs GeoJSON, but due to the fact that `float` 16 | is used instead of `double` to store geo coordinates in protobuf. 17 | This means, only 4+4=8 bytes are required for each point (latitude + longitude), instead of 8+8=16 bytes for `double`. 18 | Precision of `float` is good enough for the source data. 19 | 20 | At runtime, the code reads the packaged data and build a spatial index for querying. It uses 21 | [quad tree](https://en.wikipedia.org/wiki/Quadtree) for indexing, provided by the 22 | [Esri geometry API](https://github.com/Esri/geometry-api-java) Java library. 23 | 24 | ## Build structure 25 | Timeshape uses [sbt](https://scala-sbt.org) as build system. The sbt build definition has 5 projects: 26 | * geojson-protobuf 27 | * core 28 | * builder 29 | * testApp 30 | * benchmarks 31 | 32 | Below you'll find some information about those individual projects. 33 | ### geojson-protobuf 34 | This project contains the protobuf definitions corresponding to GeoJSON format. 35 | Those definitions are in file `geojson-proto/src/main/protobuf/geojson.proto`. Java code to read and write such protobuf 36 | messages is generated during compile time by [sbt-protoc](https://github.com/thesamet/sbt-protoc) sbt plugin. 37 | Other projects (`core` and `builder`), which must read or write the protobuf, use those generated Java classes, and therefore 38 | depend on `geojson-protobuf` in classpath sense. 39 | 40 | ### core 41 | This project contains the logic to read the data into a quad tree and provide API for querying it. It's the main project 42 | with which the library users interact, and provides the main published artifact. It uses sbt feature called 43 | `resource generator` to create the protobuf file containing the time zone data. The code that actually generates the 44 | protobuf data file is in the [builder](#builder) project. The resource generator is run by sbt automatically when necessary. 45 | 46 | ### builder 47 | This project is responsible for downloading the source data from Github and converting it from GeoJSON to protobuf format. 48 | It's usually called from the resource generator of `core` project, but can be run independently 49 | (it's a standard Java application, after all). 50 | 51 | ### testApp 52 | It's a playground, more or less. It's used to experiment with the main Timeshape artifact produced 53 | by the `core` project, particularly for the purpose of estimating its memory usage (see [Memory usage](#memory-usage)), 54 | and maybe something else in the future. 55 | 56 | ### benchmarks 57 | This project contains JMH benchmarks. It was originally created to analyze performance of different ways to query 58 | the polyline, and to compare performance of optimized polyline query method with querying each point in polyline 59 | individually. 60 | 61 | ## Building 62 | If you want to build and run the Timeshape locally, follow these steps: 63 | 64 | 1. Install sbt and JDK. It's proven to work on JDK 8, but newer versions might work too. Use latest versions, because 65 | changes of time zones happen regularly in real world, and only the latest JDK build might reflect them. 66 | 2. Go to the directory where the source code is checked out and run `sbt` command there. 67 | 3. When sbt finishes to load the build definition and you see sbt console, you have several options: 68 | * run `core/test` to execute the tests, they should pass. 69 | * run `testApp/run` to run the test app. It will query the data for one time zone and print the memory usage. 70 | * run `core/publishLocal` if you've made local modifications and want to use the modified version in your program. 71 | It will publish to the “local” [Ivy repository](https://www.scala-sbt.org/1.x/docs/Publishing.html#Publishing+locally). 72 | By default, this is at `$HOME/.ivy2/local/`. 73 | * run `core/publishM2` Similar to publishLocal, publishM2 task will publish the user’s Maven local repository. 74 | This is at the location specified by `$HOME/.m2/settings.xml` or at `$HOME/.m2/repository/` by default. 75 | Another sbt build would require `Resolver.mavenLocal` to resolve out of it. 76 | 77 | Version must be set to `snapshot` for local publication to work best. 78 | 79 | ### local testing 80 | 81 | If you want to use a modified version of timeshape in your program or perform some local testing include the local 82 | repository in your build definition. 83 | 84 | With sbt/scala: 85 | 86 | ```scala 87 | resolvers += Resolver.mavenLocal 88 | ``` 89 | 90 | With gradle/java: 91 | 92 | ```groovy 93 | repositories { 94 | mavenCentral() 95 | mavenLocal() 96 | } 97 | 98 | dependencies { 99 | compile group: 'net.iakovlev', name: 'timeshape', version: '2023b.18-SNAPSHOT' 100 | } 101 | ``` 102 | 103 | ## Memory usage 104 | 105 | The `testApp` project provides memory usage estimate by using [JOL](http://openjdk.java.net/projects/code-tools/jol/). 106 | The current version's estimated footprint is roughly 128 MB of memory when the data for the whole world is loaded. 107 | It is possible to further limit the memory usage by reducing the amount of time zones loaded. This is implemented by a call to 108 | `TimeZoneEngine.initialize(double minlat, double minlon, double maxlat, double maxlon)`. 109 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | net.iakovlev 8 | timeshape-parent 9 | 2025b.28 10 | pom 11 | 12 | Timeshape Parent 13 | A Java library for mapping between coordinates and timezones 14 | https://github.com/RomanIakovlev/timeshape 15 | 16 | 17 | 18 | MIT License 19 | http://opensource.org/licenses/mit-license.php 20 | 21 | 22 | 23 | 24 | scm:git:https://github.com/RomanIakovlev/timeshape.git 25 | scm:git:git@github.com:RomanIakovlev/timeshape.git 26 | https://github.com/RomanIakovlev/timeshape 27 | 28 | 29 | 30 | 31 | Roman Iakovlev 32 | Roman Iakovlev 33 | http://github.com/RomanIakovlev 34 | 35 | 36 | 37 | 38 | geojson-proto 39 | builder 40 | core 41 | test-app 42 | benchmarks 43 | 44 | 45 | 46 | 8 47 | 8 48 | UTF-8 49 | 2025b 50 | 27 51 | 2.17.2 52 | 1.26.1 53 | 1.5.5-11 54 | 55 | 56 | 57 | 58 | 59 | com.fasterxml.jackson.core 60 | jackson-core 61 | ${jackson.version} 62 | 63 | 64 | org.apache.commons 65 | commons-compress 66 | ${commons-compress.version} 67 | 68 | 69 | com.github.luben 70 | zstd-jni 71 | ${zstd-jni.version} 72 | 73 | 74 | net.iakovlev 75 | geojson-proto 76 | 1.1.6 77 | 78 | 79 | net.iakovlev 80 | timeshape 81 | ${project.version} 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-compiler-plugin 92 | 3.11.0 93 | 94 | 8 95 | 8 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-javadoc-plugin 101 | 3.5.0 102 | 103 | none 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-source-plugin 109 | 3.3.0 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-gpg-plugin 114 | 3.1.0 115 | 116 | 117 | 118 | 119 | 120 | org.sonatype.central 121 | central-publishing-maven-plugin 122 | 0.8.0 123 | true 124 | 125 | central 126 | true 127 | published 128 | true 129 | 130 | timeshape-benchmarks 131 | timeshape-testapp 132 | timeshape-builder 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | release 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-deploy-plugin 148 | 149 | true 150 | 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-source-plugin 155 | 156 | 157 | attach-sources 158 | 159 | jar-no-fork 160 | 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-javadoc-plugin 167 | 168 | 169 | attach-javadocs 170 | 171 | jar 172 | 173 | 174 | 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-gpg-plugin 179 | 180 | 181 | sign-artifacts 182 | verify 183 | 184 | sign 185 | 186 | 187 | 188 | --pinentry-mode 189 | loopback 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Timeshape 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.iakovlev/timeshape/badge.svg)](https://maven-badges.herokuapp.com/maven-central/net.iakovlev/timeshape/) 4 | ![Build status](https://github.com/RomanIakovlev/timeshape/actions/workflows/release.yml/badge.svg) 5 | [![Gitter](https://badges.gitter.im/timeshape/community.svg)](https://gitter.im/timeshape/community) 6 | 7 | Timeshape is a Java library that can be used to determine to which time zone a given geo coordinate belongs. 8 | It's based on data published at 9 | [https://github.com/evansiroky/timezone-boundary-builder/releases](https://github.com/evansiroky/timezone-boundary-builder/releases), 10 | which itself is inherited from the OpenStreetMap data. 11 | 12 | But what if knowing just time zone for a geo coordinate is not enough? Wouldn't it be nice to know more, like 13 | administrative area or city neighborhood? Check out [GeoBundle](https://geobundle.com), now there's a Java library for that, too! 14 | 15 | ## Quote 16 | 17 | > “Time is an illusion.” 18 | > 19 | > ― **Albert Einstein** 20 | 21 | ## Getting started 22 | 23 | Timeshape is published on Maven Central. The coordinates are the following: 24 | 25 | ```xml 26 | 27 | net.iakovlev 28 | timeshape 29 | 2025b.26/version> 30 | 31 | ``` 32 | 33 | Starting from release 2023b.20, Timeshape re-introduces support for Java 8. It was inadevrtently dropped in one of the previous releases, 34 | but now it's brought back. 35 | 36 | ## Android 37 | 38 | Android developers should add Timeshape to the app level gradle.build as follows: 39 | 40 | ```gradle 41 | implementation('net.iakovlev:timeshape:2025b.26') { 42 | // Exclude standard compression library 43 | exclude group: 'com.github.luben', module: 'zstd-jni' 44 | } 45 | // Import aar for native component compilation 46 | implementation 'com.github.luben:zstd-jni:1.5.5-11@aar' 47 | ``` 48 | 49 | ## Adopters 50 | 51 | Are you using Timeshape? Please consider opening a pull request to list your organization here: 52 | 53 | * Name and website of your organization 54 | * [AirPing](https://airping.app/) 55 | * [Hopper](https://hopper.com/) 56 | * [Natural Light](https://play.google.com/store/apps/details?id=com.blackholeofphotography.naturallight) 57 | 58 | ## Using the library 59 | 60 | The user API of library is in `net.iakovlev.timeshape.TimeZoneEngine` class. To use it, follow these steps: 61 | 62 | #### Initialization 63 | 64 | Initialize the class with the data for the whole world: 65 | 66 | ```java 67 | import net.iakovlev.timeshape.TimeZoneEngine; 68 | TimeZoneEngine engine = TimeZoneEngine.initialize(); 69 | ``` 70 | 71 | Or, alternatively, initialize it with some bounding box only, to reduce memory usage: 72 | 73 | ```java 74 | TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486); 75 | ``` 76 | 77 | It is important to mention that for the time zone to be loaded by the second method, 78 | it must be covered by the bounding box completely, not just intersect with it. 79 | 80 | During initialization, the data is read from resource and the index is built. 81 | Initialization takes a significant amount of time (approximately 1 second), so do it only once in program lifetime. 82 | 83 | Data can also be read from an external file, see overloads of `TimeZoneEngine.initialize` that accept 84 | `TarArchiveInputStream`. The file format should be same as produced by running `builder` sbt project 85 | (see [here](doc/Architecture.md#builder)). 86 | It is responsibility of caller to close input stream passed to `TimeZoneEngine.initialize`. 87 | 88 | There is an overload of `TimeZoneEngine.initialize` method that accepts a boolean parameter called 89 | `accelerateGeometry`. If that parameter is equals to `true`, Timeshape will build a bitmap index 90 | for each polygon it loads. This requires some extra memory and takes time at initialization, hence 91 | that parameter is being `false` by default, however it speeds up the typical point-in-polygon query 92 | approximately 2.5 times. See performance benchmarks in `AcceleratedGeometryBenchmark.java`, and 93 | here is the result: 94 | ``` 95 | [info] Benchmark Mode Cnt Score Error Units 96 | [info] AcceleratedGeometryBenchmark.testAcceleratedEngine thrpt 10 38.142 ± 0.448 ops/s 97 | [info] AcceleratedGeometryBenchmark.testNonAcceleratedEngine thrpt 10 15.289 ± 0.121 ops/s 98 | ``` 99 | 100 | 101 | ##### Improving start up time by using serialization 102 | 103 | Initialization on slow devices such as Android phones can take up to 20 seconds. This can be improved by 104 | serializing an instance of the `TimeZoneEngine` and deserializing on subsequent runs of your program. 105 | An example of serializing to a file can be found in `TimeZoneEngineSerializationTest.java` in the 106 | unit tests. Serialization is especially useful if you can limit `TimeZoneEngine` to a portion of the 107 | data set via a user preference. 108 | 109 | 110 | #### Query for `java.time.ZoneId`: 111 | 112 | Once initialization is completed, you can query the `ZoneId` based on latitude and longitude: 113 | 114 | ```java 115 | import java.util.Optional; 116 | import java.time.ZoneId; 117 | Optional maybeZoneId = engine.query(52.52, 13.40); 118 | ``` 119 | 120 | #### Multiple time zones for single geo point 121 | 122 | Starting from release 2019b.7, the data source from which Timeshape is built contains overlapping geometries. 123 | In other words, some geo points can simultaneously belong to multiple time zones. To accommodate this, 124 | Timeshape has made the following change. It's now possible to query all the time zones, to which given 125 | geo point belongs: 126 | 127 | ```java 128 | List allZones = engine.queryAll(52.52, 13.40); 129 | ``` 130 | 131 | The `Optional query(double latitude, double longitude)` method is still there, and it still returns 132 | maximum one `ZoneId`. If given geo point belongs to multiple time zones, only single one will be returned. 133 | Which of multiple zones will be returned is entirely arbitrary. Because of this, method 134 | `Optional query(double latitude, double longitude)` is not suitable for use cases where such choice must 135 | be deliberate. In such cases use `List queryAll(double latitude, double longitude)` method and apply further 136 | business logic to its output to choose the right time zone. Consult file 137 | https://raw.githubusercontent.com/evansiroky/timezone-boundary-builder/master/expectedZoneOverlaps.json 138 | for information about areas of the world where multiple time zones are to be expected. 139 | 140 | #### Querying polyline 141 | 142 | Timeshape supports querying of multiple sequential geo points (a polyline, e.g. a GPS trace) in an optimized way using method 143 | `List queryPolyline(double[] points)`. Performance tests (see `net.iakovlev.timeshape.PolylineQueryBenchmark`) 144 | show significant speedup of using this method for querying a polyline, comparing to separately querying each point from polyline 145 | using `Optional query(double latitude, double longitude)` method: 146 | 147 | ``` 148 | Benchmark Mode Cnt Score Error Units 149 | PolylineQueryBenchmark.testQueryPoints thrpt 5 2,188 ▒ 0,044 ops/s 150 | PolylineQueryBenchmark.testQueryPolyline thrpt 5 4,073 ▒ 0,017 ops/s 151 | ``` 152 | 153 | ### Architecture 154 | 155 | See [dedicated document](doc/Architecture.md) for description of Timeshape internals. 156 | 157 | ### Versioning 158 | 159 | Version of Timeshape consist of data version and software version, divided by a '.' symbol. 160 | Data version is as specified at [https://github.com/evansiroky/timezone-boundary-builder/releases](https://github.com/evansiroky/timezone-boundary-builder/releases). 161 | Software version is an integer, starting from 1 and incrementing for each published artifact. 162 | 163 | ## Licenses 164 | 165 | The code of the library is licensed under the [MIT License](https://opensource.org/licenses/MIT). 166 | 167 | The time zone data contained in library is licensed under the [Open Data Commons Open Database License (ODbL)](http://opendatacommons.org/licenses/odbl/). 168 | 169 | ## Endorsements 170 | 171 | Timeshape uses YourKit. 172 | 173 | 174 | 175 | YourKit supports open source projects with innovative and intelligent tools 176 | for monitoring and profiling Java and .NET applications. 177 | YourKit is the creator of YourKit Java Profiler, 178 | YourKit .NET Profiler, 179 | and YourKit YouMonitor. 180 | -------------------------------------------------------------------------------- /core/src/main/java/net/iakovlev/timeshape/Index.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import com.esri.core.geometry.*; 4 | import net.iakovlev.timeshape.proto.Geojson; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.Serializable; 9 | import java.time.ZoneId; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.PrimitiveIterator; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.IntStream; 16 | import java.util.stream.Stream; 17 | 18 | final class Index implements Serializable { 19 | static final class Entry implements Serializable { 20 | final ZoneId zoneId; 21 | final Geometry geometry; 22 | 23 | Entry(ZoneId zoneId, Geometry geometry) { 24 | this.zoneId = zoneId; 25 | this.geometry = geometry; 26 | } 27 | } 28 | 29 | private static final int WGS84_WKID = 4326; 30 | private final ArrayList zoneIds; 31 | private static final SpatialReference spatialReference = SpatialReference.create(WGS84_WKID); 32 | private final QuadTree quadTree; 33 | private static final Logger log = LoggerFactory.getLogger(Index.class); 34 | 35 | private Index(QuadTree quadTree, ArrayList zoneIds) { 36 | log.info("Initialized index with {} time zones", zoneIds.size()); 37 | this.quadTree = quadTree; 38 | this.zoneIds = zoneIds; 39 | } 40 | 41 | List getKnownZoneIds() { 42 | return zoneIds.stream().map(e -> e.zoneId).collect(Collectors.toList()); 43 | } 44 | 45 | List query(double latitude, double longitude) { 46 | ArrayList result = new ArrayList<>(2); 47 | Point point = new Point(longitude, latitude); 48 | OperatorIntersects operator = OperatorIntersects.local(); 49 | QuadTree.QuadTreeIterator iterator = quadTree.getIterator(point, 0); 50 | for (int i = iterator.next(); i >= 0; i = iterator.next()) { 51 | int element = quadTree.getElement(i); 52 | Entry entry = zoneIds.get(element); 53 | if(operator.execute(entry.geometry, point, spatialReference, null)) { 54 | result.add(entry.zoneId); 55 | } 56 | } 57 | return result; 58 | } 59 | 60 | List queryPolyline(double[] line) { 61 | 62 | Polyline polyline = new Polyline(); 63 | ArrayList points = new ArrayList<>(line.length / 2); 64 | for (int i = 0; i < line.length - 1; i += 2) { 65 | Point p = new Point(line[i + 1], line[i]); 66 | points.add(p); 67 | } 68 | polyline.startPath(points.get(0)); 69 | for (int i = 1; i < points.size(); i += 1) { 70 | polyline.lineTo(points.get(i)); 71 | } 72 | QuadTree.QuadTreeIterator iterator = quadTree.getIterator(polyline, 0); 73 | 74 | ArrayList potentiallyMatchingEntries = new ArrayList<>(); 75 | 76 | for (int i = iterator.next(); i >= 0; i = iterator.next()) { 77 | int element = quadTree.getElement(i); 78 | Entry entry = zoneIds.get(element); 79 | potentiallyMatchingEntries.add(entry); 80 | } 81 | 82 | ArrayList sameZoneSegments = new ArrayList<>(); 83 | List currentEntry = null; 84 | // 1. find next matching geometry or geometries 85 | // 2. for every match, increase the index 86 | // 3. when it doesn't match anymore, save currentSegment to sameZoneSegments and start new one 87 | // 4. goto 1. 88 | int index = 0; 89 | boolean lastWasEmpty = false; 90 | OperatorIntersects operator = OperatorIntersects.local(); 91 | while (index < points.size()) { 92 | Point p = points.get(index); 93 | if (currentEntry == null) { 94 | currentEntry = potentiallyMatchingEntries 95 | .stream() 96 | .filter(e -> operator.execute(e.geometry, p, spatialReference, null)) 97 | .collect(Collectors.toList()); 98 | } 99 | if (currentEntry.isEmpty()) { 100 | currentEntry = null; 101 | lastWasEmpty = true; 102 | index++; 103 | } else { 104 | if (lastWasEmpty) { 105 | lastWasEmpty = false; 106 | sameZoneSegments.add(SameZoneSpan.fromIndexEntries(Collections.emptyList(), (index - 1) * 2 + 1)); 107 | continue; 108 | } 109 | if (currentEntry.stream().allMatch(e -> operator.execute(e.geometry, p, spatialReference, null))) { 110 | if (index == points.size() - 1) { 111 | sameZoneSegments.add(SameZoneSpan.fromIndexEntries(currentEntry, index * 2 + 1)); 112 | } 113 | index++; 114 | } else { 115 | sameZoneSegments.add(SameZoneSpan.fromIndexEntries(currentEntry, (index - 1) * 2 + 1)); 116 | currentEntry = null; 117 | } 118 | } 119 | } 120 | 121 | if (lastWasEmpty) { 122 | sameZoneSegments.add(SameZoneSpan.fromIndexEntries(Collections.emptyList(), index * 2 - 1)); 123 | } 124 | 125 | return sameZoneSegments; 126 | } 127 | 128 | private static Polygon buildPoly(Geojson.Polygon from) { 129 | Polygon poly = new Polygon(); 130 | from.getCoordinatesList().stream() 131 | .map(Geojson.LineString::getCoordinatesList) 132 | .forEachOrdered(lp -> { 133 | poly.startPath(lp.get(0).getLon(), lp.get(0).getLat()); 134 | lp.subList(1, lp.size()).forEach(p -> poly.lineTo(p.getLon(), p.getLat())); 135 | }); 136 | return poly; 137 | } 138 | 139 | static Index build(Stream features, int size, Envelope boundaries) { 140 | return build(features, size, boundaries, false); 141 | } 142 | 143 | private static Stream getPolygons(Geojson.Feature f) { 144 | if (f.getGeometry().hasPolygon()) { 145 | return Stream.of(buildPoly(f.getGeometry().getPolygon())); 146 | } else if (f.getGeometry().hasMultiPolygon()) { 147 | Geojson.MultiPolygon multiPolygonProto = f.getGeometry().getMultiPolygon(); 148 | return multiPolygonProto.getCoordinatesList().stream().map(Index::buildPoly); 149 | } else { 150 | throw new RuntimeException("Unknown geometry type"); 151 | } 152 | } 153 | 154 | static Index build(Stream features, int size, Envelope boundaries, boolean accelerateGeometry) { 155 | Envelope2D boundariesEnvelope = new Envelope2D(); 156 | boundaries.queryEnvelope2D(boundariesEnvelope); 157 | QuadTree quadTree = new QuadTree(boundariesEnvelope, 8); 158 | Envelope2D env = new Envelope2D(); 159 | ArrayList zoneIds = new ArrayList<>(size); 160 | PrimitiveIterator.OfInt indices = IntStream.iterate(0, i -> i + 1).iterator(); 161 | List unknownZones = new ArrayList<>(); 162 | OperatorIntersects operatorIntersects = OperatorIntersects.local(); 163 | features.forEach(f -> { 164 | String zoneIdName = f.getProperties(0).getValueString(); 165 | try { 166 | ZoneId zoneId = ZoneId.of(zoneIdName); 167 | getPolygons(f).forEach(polygon -> { 168 | if (GeometryEngine.contains(boundaries, polygon, spatialReference)) { 169 | log.debug("Adding zone {} to index", zoneIdName); 170 | if (accelerateGeometry) { 171 | operatorIntersects.accelerateGeometry(polygon, spatialReference, Geometry.GeometryAccelerationDegree.enumMild); 172 | } 173 | polygon.queryEnvelope2D(env); 174 | int index = indices.next(); 175 | quadTree.insert(index, env); 176 | zoneIds.add(index, new Entry(zoneId, polygon)); 177 | } else { 178 | log.debug("Not adding zone {} to index because it's out of provided boundaries", zoneIdName); 179 | } 180 | }); 181 | } catch (Exception ex) { 182 | unknownZones.add(zoneIdName); 183 | } 184 | }); 185 | if (unknownZones.size() != 0) { 186 | String allUnknownZones = String.join(", ", unknownZones); 187 | log.error( 188 | "Some of the zone ids were not recognized by the Java runtime and will be ignored. " + 189 | "The most probable reason for this is outdated Java runtime version. " + 190 | "The following zones were not recognized: " + allUnknownZones); 191 | } 192 | return new Index(quadTree, zoneIds); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java: -------------------------------------------------------------------------------- 1 | package net.iakovlev.timeshape; 2 | 3 | import com.esri.core.geometry.Envelope; 4 | import com.github.luben.zstd.ZstdInputStream; 5 | import net.iakovlev.timeshape.proto.Geojson; 6 | import org.apache.commons.compress.archivers.tar.TarArchiveEntry; 7 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.io.BufferedInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.Serializable; 15 | import java.time.ZoneId; 16 | import java.util.*; 17 | import java.util.function.Consumer; 18 | import java.util.stream.Stream; 19 | import java.util.stream.StreamSupport; 20 | 21 | /** 22 | * Class {@link TimeZoneEngine} is used to lookup the instance of 23 | * {@link java.time.ZoneId} based on latitude and longitude. 24 | */ 25 | public final class TimeZoneEngine implements Serializable { 26 | 27 | private final Index index; 28 | 29 | private final static double MIN_LAT = -90; 30 | private final static double MIN_LON = -180; 31 | private final static double MAX_LAT = 90; 32 | private final static double MAX_LON = 180; 33 | 34 | private final static Logger log = LoggerFactory.getLogger(TimeZoneEngine.class); 35 | 36 | private TimeZoneEngine(Index index) { 37 | this.index = index; 38 | } 39 | 40 | private static void validateCoordinates(double minLat, double minLon, double maxLat, double maxLon) { 41 | List errors = new ArrayList<>(); 42 | if (minLat < MIN_LAT || minLat > MAX_LAT) { 43 | errors.add(String.format(Locale.ROOT, "minimum latitude %f is out of range: must be -90 <= latitude <= 90;", minLat)); 44 | } 45 | if (maxLat < MIN_LAT || maxLat > MAX_LAT) { 46 | errors.add(String.format(Locale.ROOT, "maximum latitude %f is out of range: must be -90 <= latitude <= 90;", maxLat)); 47 | } 48 | if (minLon < MIN_LON || minLon > MAX_LON) { 49 | errors.add(String.format(Locale.ROOT, "minimum longitude %f is out of range: must be -180 <= longitude <= 180;", minLon)); 50 | } 51 | if (maxLon < MIN_LON || maxLon > MAX_LON) { 52 | errors.add(String.format(Locale.ROOT, "maximum longitude %f is out of range: must be -180 <= longitude <= 180;", maxLon)); 53 | } 54 | if (minLat > maxLat) { 55 | errors.add(String.format(Locale.ROOT, "maximum latitude %f is less than minimum latitude %f;", maxLat, minLat)); 56 | } 57 | if (minLon > maxLon) { 58 | errors.add(String.format(Locale.ROOT, "maximum longitude %f is less than minimum longitude %f;", maxLon, minLon)); 59 | } 60 | if (!errors.isEmpty()) { 61 | throw new IllegalArgumentException(String.join(" ", errors)); 62 | } 63 | } 64 | 65 | private static Spliterator makeSpliterator(TarArchiveInputStream f) { 66 | return new Spliterators.AbstractSpliterator(Long.MAX_VALUE, 0) { 67 | @Override 68 | public boolean tryAdvance(Consumer action) { 69 | try { 70 | TarArchiveEntry entry = f.getNextTarEntry(); 71 | if (entry != null) { 72 | action.accept(entry); 73 | return true; 74 | } else { 75 | return false; 76 | } 77 | } catch (IOException e) { 78 | throw new RuntimeException(e); 79 | } 80 | } 81 | }; 82 | } 83 | 84 | /** 85 | * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} 86 | * based on geo coordinates. 87 | * 88 | * @param latitude latitude part of query 89 | * @param longitude longitude part of query 90 | * @return List of all zones at given geo coordinate. Normally it's just 91 | * one zone, but for several places in the world there might be more. 92 | */ 93 | public List queryAll(double latitude, double longitude) { 94 | return index.query(latitude, longitude); 95 | } 96 | 97 | /** 98 | * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} 99 | * based on geo coordinates. 100 | * 101 | * @param latitude latitude part of query 102 | * @param longitude longitude part of query 103 | * @return {@code Optional#of(ZoneId)} if input corresponds 104 | * to some zone, or {@link Optional#empty()} otherwise. 105 | */ 106 | public Optional query(double latitude, double longitude) { 107 | final List result = index.query(latitude, longitude); 108 | return result.size() > 0 ? Optional.of(result.get(0)) : Optional.empty(); 109 | } 110 | 111 | /** 112 | * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} 113 | * based on sequence of geo coordinates 114 | * 115 | * @param points array of doubles representing the sequence of geo coordinates 116 | * Must have the following shape: {lat_1, lon_1, lat_2, lon_2, ..., lat_N, lon_N} 117 | * @return Sequence of {@link SameZoneSpan}, where {@link SameZoneSpan#getEndIndex()} represents the last index 118 | * in the {@param points} array, which belong to the value of {@link SameZoneSpan#getZoneIds()} 119 | * E.g. for {@param points} == {lat_1, lon_1, lat_2, lon_2, lat_3, lon_3}, that is, a polyline of 120 | * 3 points: point_1, point_2, and point_3, and presuming point_1 belongs to Etc/GMT+1, point_2 belongs to Etc/GMT+2, 121 | * and point_3 belongs to Etc/Gmt+3, the result will be: 122 | * {SameZoneSpan(Etc/Gmt+1, 1), SameZoneSpan(Etc/Gmt+2, 3), SameZoneSpan(Etc/Gmt+3, 5)} 123 | */ 124 | public List queryPolyline(double[] points) { 125 | return index.queryPolyline(points); 126 | } 127 | 128 | /** 129 | * Returns all the time zones that can be looked up. 130 | * 131 | * @return all the time zones that can be looked up. 132 | */ 133 | public List getKnownZoneIds() { 134 | return index.getKnownZoneIds(); 135 | } 136 | 137 | /** 138 | * Creates a new instance of {@link TimeZoneEngine} and initializes it. 139 | * This is a blocking long running operation. 140 | * 141 | * @return an initialized instance of {@link TimeZoneEngine} 142 | */ 143 | public static TimeZoneEngine initialize(boolean accelerateGeometry) { 144 | return initialize(MIN_LAT, MIN_LON, MAX_LAT, MAX_LON, accelerateGeometry); 145 | } 146 | 147 | /** 148 | * Creates a new instance of {@link TimeZoneEngine} and initializes it. 149 | * This is a blocking long running operation. 150 | * 151 | * @return an initialized instance of {@link TimeZoneEngine} 152 | */ 153 | public static TimeZoneEngine initialize() { 154 | return initialize(MIN_LAT, MIN_LON, MAX_LAT, MAX_LON, false); 155 | } 156 | 157 | /** 158 | * Creates a new instance of {@link TimeZoneEngine} and initializes it. 159 | * This is a blocking long running operation. 160 | *

161 | * Example invocation: 162 | *

163 | * {{{ 164 | * try (InputStream resourceAsStream = new FileInputStream("./core/target/resource_managed/main/data.tar.zstd"); 165 | * TarArchiveInputStream f = new TarArchiveInputStream(new ZstdCompressorInputStream(resourceAsStream))) { 166 | * return TimeZoneEngine.initialize(f); 167 | * } catch (NullPointerException | IOException e) { 168 | * throw new RuntimeException(e); 169 | * } 170 | * }}} 171 | * 172 | * @return an initialized instance of {@link TimeZoneEngine} 173 | */ 174 | public static TimeZoneEngine initialize(TarArchiveInputStream f) { 175 | return initialize(MIN_LAT, MIN_LON, MAX_LAT, MAX_LON, false, f); 176 | } 177 | 178 | /** 179 | * Creates a new instance of {@link TimeZoneEngine} and initializes it from a given TarArchiveInputStream. 180 | * This is a blocking long running operation. The InputStream resource must be managed by the caller. 181 | *

182 | * Example invocation: 183 | * {{{ 184 | * try (InputStream resourceAsStream = new FileInputStream("./core/target/resource_managed/main/data.tar.zstd"); 185 | * TarArchiveInputStream f = new TarArchiveInputStream(new ZstdCompressorInputStream(resourceAsStream))) { 186 | * return TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true, f); 187 | * } catch (NullPointerException | IOException e) { 188 | * throw new RuntimeException(e); 189 | * } 190 | * }}} 191 | * 192 | * @return an initialized instance of {@link TimeZoneEngine} 193 | */ 194 | public static TimeZoneEngine initialize(double minLat, 195 | double minLon, 196 | double maxLat, 197 | double maxLon, 198 | boolean accelerateGeometry, 199 | TarArchiveInputStream f) { 200 | log.info("Initializing with bounding box: {}, {}, {}, {}", minLat, minLon, maxLat, maxLon); 201 | validateCoordinates(minLat, minLon, maxLat, maxLon); 202 | Spliterator tarArchiveEntrySpliterator = makeSpliterator(f); 203 | Stream featureStream = StreamSupport.stream(tarArchiveEntrySpliterator, false).map(n -> { 204 | try { 205 | if (n != null) { 206 | log.debug("Processing archive entry {}", n.getName()); 207 | byte[] e = new byte[(int) n.getSize()]; 208 | f.read(e); 209 | return Geojson.Feature.parseFrom(e); 210 | } else { 211 | throw new RuntimeException("Data entry is not found in file"); 212 | } 213 | } catch (NullPointerException | IOException ex) { 214 | throw new RuntimeException(ex); 215 | } 216 | }); 217 | int numberOfTimezones = 449; // can't get number of entries from tar, need to set manually 218 | Envelope boundaries = new Envelope(minLon, minLat, maxLon, maxLat); 219 | return new TimeZoneEngine( 220 | Index.build( 221 | featureStream, 222 | numberOfTimezones, 223 | boundaries, 224 | accelerateGeometry)); 225 | } 226 | 227 | 228 | /** 229 | * Creates a new instance of {@link TimeZoneEngine} and initializes it. 230 | * This is a blocking long running operation. 231 | * 232 | * @return an initialized instance of {@link TimeZoneEngine} 233 | */ 234 | public static TimeZoneEngine initialize(double minLat, double minLon, double maxLat, double maxLon, boolean accelerateGeometry) { 235 | try (InputStream resourceAsStream = TimeZoneEngine.class.getResourceAsStream("/data.tar.zstd")) { 236 | try (ZstdInputStream unzipStream = new ZstdInputStream(resourceAsStream)) { 237 | try (BufferedInputStream bufferedStream = new BufferedInputStream(unzipStream)) { 238 | try (TarArchiveInputStream shapeInputStream = new TarArchiveInputStream(bufferedStream)) { 239 | return initialize(minLat, minLon, maxLat, maxLon, accelerateGeometry, shapeInputStream); 240 | } 241 | } 242 | } 243 | } catch (NullPointerException | IOException e) { 244 | log.error("Unable to read resource file", e); 245 | throw new RuntimeException(e); 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /DATA_LICENSE: -------------------------------------------------------------------------------- 1 | Open Database License (ODbL) v1.0 2 | Disclaimer 3 | Open Data Commons is not a law firm and does not provide legal services of any kind. 4 | 5 | Open Data Commons has no formal relationship with you. Your receipt of this document does not create any kind of agent-client relationship. Please seek the advice of a suitably qualified legal professional licensed to practice in your jurisdiction before using this document. 6 | 7 | No warranties and disclaimer of any damages. This information is provided ‘as is‘, and this site makes no warranties on the information provided. Any damages resulting from its use are disclaimed. 8 | 9 | Plain language summary 10 | A plain language summary of the Open Database License is available. 11 | 12 | Alternative formats: 13 | Plain Text 14 | 15 | ODC Open Database License (ODbL) 16 | Preamble 17 | The Open Database License (ODbL) is a license agreement intended to 18 | allow users to freely share, modify, and use this Database while 19 | maintaining this same freedom for others. Many databases are covered by 20 | copyright, and therefore this document licenses these rights. Some 21 | jurisdictions, mainly in the European Union, have specific rights that 22 | cover databases, and so the ODbL addresses these rights, too. Finally, 23 | the ODbL is also an agreement in contract for users of this Database to 24 | act in certain ways in return for accessing this Database. 25 | 26 | Databases can contain a wide variety of types of content (images, 27 | audiovisual material, and sounds all in the same database, for example), 28 | and so the ODbL only governs the rights over the Database, and not the 29 | contents of the Database individually. Licensors should use the ODbL 30 | together with another license for the contents, if the contents have a 31 | single set of rights that uniformly covers all of the contents. If the 32 | contents have multiple sets of different rights, Licensors should 33 | describe what rights govern what contents together in the individual 34 | record or in some other way that clarifies what rights apply. 35 | 36 | Sometimes the contents of a database, or the database itself, can be 37 | covered by other rights not addressed here (such as private contracts, 38 | trade mark over the name, or privacy rights / data protection rights 39 | over information in the contents), and so you are advised that you may 40 | have to consult other documents or clear other rights before doing 41 | activities not covered by this License. 42 | 43 | The Licensor (as defined below) 44 | 45 | and 46 | 47 | You (as defined below) 48 | 49 | agree as follows: 50 | 51 | 1.0 Definitions of Capitalised Words 52 | “Collective Database” – Means this Database in unmodified form as part 53 | of a collection of independent databases in themselves that together are 54 | assembled into a collective whole. A work that constitutes a Collective 55 | Database will not be considered a Derivative Database. 56 | 57 | “Convey” – As a verb, means Using the Database, a Derivative Database, 58 | or the Database as part of a Collective Database in any way that enables 59 | a Person to make or receive copies of the Database or a Derivative 60 | Database. Conveying does not include interaction with a user through a 61 | computer network, or creating and Using a Produced Work, where no 62 | transfer of a copy of the Database or a Derivative Database occurs. 63 | “Contents” – The contents of this Database, which includes the 64 | information, independent works, or other material collected into the 65 | Database. For example, the contents of the Database could be factual 66 | data or works such as images, audiovisual material, text, or sounds. 67 | 68 | “Database” – A collection of material (the Contents) arranged in a 69 | systematic or methodical way and individually accessible by electronic 70 | or other means offered under the terms of this License. 71 | 72 | “Database Directive” – Means Directive 96/9/EC of the European 73 | Parliament and of the Council of 11 March 1996 on the legal protection 74 | of databases, as amended or succeeded. 75 | 76 | “Database Right” – Means rights resulting from the Chapter III (“sui 77 | generis”) rights in the Database Directive (as amended and as transposed 78 | by member states), which includes the Extraction and Re-utilisation of 79 | the whole or a Substantial part of the Contents, as well as any similar 80 | rights available in the relevant jurisdiction under Section 10.4. 81 | 82 | “Derivative Database” – Means a database based upon the Database, and 83 | includes any translation, adaptation, arrangement, modification, or any 84 | other alteration of the Database or of a Substantial part of the 85 | Contents. This includes, but is not limited to, Extracting or 86 | Re-utilising the whole or a Substantial part of the Contents in a new 87 | Database. 88 | 89 | “Extraction” – Means the permanent or temporary transfer of all or a 90 | Substantial part of the Contents to another medium by any means or in 91 | any form. 92 | 93 | “License” – Means this license agreement and is both a license of rights 94 | such as copyright and Database Rights and an agreement in contract. 95 | 96 | “Licensor” – Means the Person that offers the Database under the terms 97 | of this License. 98 | 99 | “Person” – Means a natural or legal person or a body of persons 100 | corporate or incorporate. 101 | 102 | “Produced Work” – a work (such as an image, audiovisual material, text, 103 | or sounds) resulting from using the whole or a Substantial part of the 104 | Contents (via a search or other query) from this Database, a Derivative 105 | Database, or this Database as part of a Collective Database. 106 | 107 | “Publicly” – means to Persons other than You or under Your control by 108 | either more than 50% ownership or by the power to direct their 109 | activities (such as contracting with an independent consultant). 110 | 111 | “Re-utilisation” – means any form of making available to the public all 112 | or a Substantial part of the Contents by the distribution of copies, by 113 | renting, by online or other forms of transmission. 114 | 115 | “Substantial” – Means substantial in terms of quantity or quality or a 116 | combination of both. The repeated and systematic Extraction or 117 | Re-utilisation of insubstantial parts of the Contents may amount to the 118 | Extraction or Re-utilisation of a Substantial part of the Contents. 119 | 120 | “Use” – As a verb, means doing any act that is restricted by copyright 121 | or Database Rights whether in the original medium or any other; and 122 | includes without limitation distributing, copying, publicly performing, 123 | publicly displaying, and preparing derivative works of the Database, as 124 | well as modifying the Database as may be technically necessary to use it 125 | in a different mode or format. 126 | 127 | “You” – Means a Person exercising rights under this License who has not 128 | previously violated the terms of this License with respect to the 129 | Database, or who has received express permission from the Licensor to 130 | exercise rights under this License despite a previous violation. 131 | 132 | Words in the singular include the plural and vice versa. 133 | 134 | 2.0 What this License covers 135 | 2.1. Legal effect of this document. This License is: 136 | 137 | a. A license of applicable copyright and neighbouring rights; 138 | 139 | b. A license of the Database Right; and 140 | 141 | c. An agreement in contract between You and the Licensor. 142 | 143 | 2.2 Legal rights covered. This License covers the legal rights in the 144 | Database, including: 145 | 146 | a. Copyright. Any copyright or neighbouring rights in the Database. 147 | The copyright licensed includes any individual elements of the 148 | Database, but does not cover the copyright over the Contents 149 | independent of this Database. See Section 2.4 for details. Copyright 150 | law varies between jurisdictions, but is likely to cover: the Database 151 | model or schema, which is the structure, arrangement, and organisation 152 | of the Database, and can also include the Database tables and table 153 | indexes; the data entry and output sheets; and the Field names of 154 | Contents stored in the Database; 155 | 156 | b. Database Rights. Database Rights only extend to the Extraction and 157 | Re-utilisation of the whole or a Substantial part of the Contents. 158 | Database Rights can apply even when there is no copyright over the 159 | Database. Database Rights can also apply when the Contents are removed 160 | from the Database and are selected and arranged in a way that would 161 | not infringe any applicable copyright; and 162 | 163 | c. Contract. This is an agreement between You and the Licensor for 164 | access to the Database. In return you agree to certain conditions of 165 | use on this access as outlined in this License. 166 | 167 | 2.3 Rights not covered. 168 | 169 | a. This License does not apply to computer programs used in the making 170 | or operation of the Database; 171 | 172 | b. This License does not cover any patents over the Contents or the 173 | Database; and 174 | 175 | c. This License does not cover any trademarks associated with the 176 | Database. 177 | 178 | 2.4 Relationship to Contents in the Database. The individual items of 179 | the Contents contained in this Database may be covered by other rights, 180 | including copyright, patent, data protection, privacy, or personality 181 | rights, and this License does not cover any rights (other than Database 182 | Rights or in contract) in individual Contents contained in the Database. 183 | For example, if used on a Database of images (the Contents), this 184 | License would not apply to copyright over individual images, which could 185 | have their own separate licenses, or one single license covering all of 186 | the rights over the images. 187 | 188 | 3.0 Rights granted 189 | 3.1 Subject to the terms and conditions of this License, the Licensor 190 | grants to You a worldwide, royalty-free, non-exclusive, terminable (but 191 | only under Section 9) license to Use the Database for the duration of 192 | any applicable copyright and Database Rights. These rights explicitly 193 | include commercial use, and do not exclude any field of endeavour. To 194 | the extent possible in the relevant jurisdiction, these rights may be 195 | exercised in all media and formats whether now known or created in the 196 | future. 197 | 198 | The rights granted cover, for example: 199 | 200 | a. Extraction and Re-utilisation of the whole or a Substantial part of 201 | the Contents; 202 | 203 | b. Creation of Derivative Databases; 204 | 205 | c. Creation of Collective Databases; 206 | 207 | d. Creation of temporary or permanent reproductions by any means and 208 | in any form, in whole or in part, including of any Derivative 209 | Databases or as a part of Collective Databases; and 210 | 211 | e. Distribution, communication, display, lending, making available, or 212 | performance to the public by any means and in any form, in whole or in 213 | part, including of any Derivative Database or as a part of Collective 214 | Databases. 215 | 216 | 3.2 Compulsory license schemes. For the avoidance of doubt: 217 | 218 | a. Non-waivable compulsory license schemes. In those jurisdictions in 219 | which the right to collect royalties through any statutory or 220 | compulsory licensing scheme cannot be waived, the Licensor reserves 221 | the exclusive right to collect such royalties for any exercise by You 222 | of the rights granted under this License; 223 | 224 | b. Waivable compulsory license schemes. In those jurisdictions in 225 | which the right to collect royalties through any statutory or 226 | compulsory licensing scheme can be waived, the Licensor waives the 227 | exclusive right to collect such royalties for any exercise by You of 228 | the rights granted under this License; and, 229 | 230 | c. Voluntary license schemes. The Licensor waives the right to collect 231 | royalties, whether individually or, in the event that the Licensor is 232 | a member of a collecting society that administers voluntary licensing 233 | schemes, via that society, from any exercise by You of the rights 234 | granted under this License. 235 | 236 | 3.3 The right to release the Database under different terms, or to stop 237 | distributing or making available the Database, is reserved. Note that 238 | this Database may be multiple-licensed, and so You may have the choice 239 | of using alternative licenses for this Database. Subject to Section 240 | 10.4, all other rights not expressly granted by Licensor are reserved. 241 | 242 | 4.0 Conditions of Use 243 | 4.1 The rights granted in Section 3 above are expressly made subject to 244 | Your complying with the following conditions of use. These are important 245 | conditions of this License, and if You fail to follow them, You will be 246 | in material breach of its terms. 247 | 248 | 4.2 Notices. If You Publicly Convey this Database, any Derivative 249 | Database, or the Database as part of a Collective Database, then You 250 | must: 251 | 252 | a. Do so only under the terms of this License or another license 253 | permitted under Section 4.4; 254 | 255 | b. Include a copy of this License (or, as applicable, a license 256 | permitted under Section 4.4) or its Uniform Resource Identifier (URI) 257 | with the Database or Derivative Database, including both in the 258 | Database or Derivative Database and in any relevant documentation; and 259 | 260 | c. Keep intact any copyright or Database Right notices and notices 261 | that refer to this License. 262 | 263 | d. If it is not possible to put the required notices in a particular 264 | file due to its structure, then You must include the notices in a 265 | location (such as a relevant directory) where users would be likely to 266 | look for it. 267 | 268 | 4.3 Notice for using output (Contents). Creating and Using a Produced 269 | Work does not require the notice in Section 4.2. However, if you 270 | Publicly Use a Produced Work, You must include a notice associated with 271 | the Produced Work reasonably calculated to make any Person that uses, 272 | views, accesses, interacts with, or is otherwise exposed to the Produced 273 | Work aware that Content was obtained from the Database, Derivative 274 | Database, or the Database as part of a Collective Database, and that it 275 | is available under this License. 276 | 277 | a. Example notice. The following text will satisfy notice under 278 | Section 4.3: 279 | 280 | Contains information from DATABASE NAME, which is made available 281 | here under the Open Database License (ODbL). 282 | DATABASE NAME should be replaced with the name of the Database and a 283 | hyperlink to the URI of the Database. “Open Database License” should 284 | contain a hyperlink to the URI of the text of this License. If 285 | hyperlinks are not possible, You should include the plain text of the 286 | required URI’s with the above notice. 287 | 288 | 4.4 Share alike. 289 | 290 | a. Any Derivative Database that You Publicly Use must be only under 291 | the terms of: 292 | 293 | i. This License; 294 | 295 | ii. A later version of this License similar in spirit to this 296 | License; or 297 | 298 | iii. A compatible license. 299 | 300 | If You license the Derivative Database under one of the licenses 301 | mentioned in (iii), You must comply with the terms of that license. 302 | 303 | b. For the avoidance of doubt, Extraction or Re-utilisation of the 304 | whole or a Substantial part of the Contents into a new database is a 305 | Derivative Database and must comply with Section 4.4. 306 | 307 | c. Derivative Databases and Produced Works. A Derivative Database is 308 | Publicly Used and so must comply with Section 4.4. if a Produced Work 309 | created from the Derivative Database is Publicly Used. 310 | 311 | d. Share Alike and additional Contents. For the avoidance of doubt, 312 | You must not add Contents to Derivative Databases under Section 4.4 a 313 | that are incompatible with the rights granted under this License. 314 | 315 | e. Compatible licenses. Licensors may authorise a proxy to determine 316 | compatible licenses under Section 4.4 a iii. If they do so, the 317 | authorised proxy’s public statement of acceptance of a compatible 318 | license grants You permission to use the compatible license. 319 | 320 | 4.5 Limits of Share Alike. The requirements of Section 4.4 do not apply 321 | in the following: 322 | 323 | a. For the avoidance of doubt, You are not required to license 324 | Collective Databases under this License if You incorporate this 325 | Database or a Derivative Database in the collection, but this License 326 | still applies to this Database or a Derivative Database as a part of 327 | the Collective Database; 328 | 329 | b. Using this Database, a Derivative Database, or this Database as 330 | part of a Collective Database to create a Produced Work does not 331 | create a Derivative Database for purposes of Section 4.4; and 332 | 333 | c. Use of a Derivative Database internally within an organisation is 334 | not to the public and therefore does not fall under the requirements 335 | of Section 4.4. 336 | 337 | 4.6 Access to Derivative Databases. If You Publicly Use a Derivative 338 | Database or a Produced Work from a Derivative Database, You must also 339 | offer to recipients of the Derivative Database or Produced Work a copy 340 | in a machine readable form of: 341 | 342 | a. The entire Derivative Database; or 343 | 344 | b. A file containing all of the alterations made to the Database or 345 | the method of making the alterations to the Database (such as an 346 | algorithm), including any additional Contents, that make up all the 347 | differences between the Database and the Derivative Database. 348 | 349 | The Derivative Database (under a.) or alteration file (under b.) must be 350 | available at no more than a reasonable production cost for physical 351 | distributions and free of charge if distributed over the internet. 352 | 353 | 4.7 Technological measures and additional terms 354 | 355 | a. This License does not allow You to impose (except subject to 356 | Section 4.7 b.) any terms or any technological measures on the 357 | Database, a Derivative Database, or the whole or a Substantial part of 358 | the Contents that alter or restrict the terms of this License, or any 359 | rights granted under it, or have the effect or intent of restricting 360 | the ability of any person to exercise those rights. 361 | 362 | b. Parallel distribution. You may impose terms or technological 363 | measures on the Database, a Derivative Database, or the whole or a 364 | Substantial part of the Contents (a “Restricted Database”) in 365 | contravention of Section 4.74 a. only if You also make a copy of the 366 | Database or a Derivative Database available to the recipient of the 367 | Restricted Database: 368 | 369 | i. That is available without additional fee; 370 | 371 | ii. That is available in a medium that does not alter or restrict 372 | the terms of this License, or any rights granted under it, or have 373 | the effect or intent of restricting the ability of any person to 374 | exercise those rights (an “Unrestricted Database”); and 375 | 376 | iii. The Unrestricted Database is at least as accessible to the 377 | recipient as a practical matter as the Restricted Database. 378 | 379 | c. For the avoidance of doubt, You may place this Database or a 380 | Derivative Database in an authenticated environment, behind a 381 | password, or within a similar access control scheme provided that You 382 | do not alter or restrict the terms of this License or any rights 383 | granted under it or have the effect or intent of restricting the 384 | ability of any person to exercise those rights. 385 | 386 | 4.8 Licensing of others. You may not sublicense the Database. Each time 387 | You communicate the Database, the whole or Substantial part of the 388 | Contents, or any Derivative Database to anyone else in any way, the 389 | Licensor offers to the recipient a license to the Database on the same 390 | terms and conditions as this License. You are not responsible for 391 | enforcing compliance by third parties with this License, but You may 392 | enforce any rights that You have over a Derivative Database. You are 393 | solely responsible for any modifications of a Derivative Database made 394 | by You or another Person at Your direction. You may not impose any 395 | further restrictions on the exercise of the rights granted or affirmed 396 | under this License. 397 | 398 | 5.0 Moral rights 399 | 5.1 Moral rights. This section covers moral rights, including any rights 400 | to be identified as the author of the Database or to object to treatment 401 | that would otherwise prejudice the author’s honour and reputation, or 402 | any other derogatory treatment: 403 | 404 | a. For jurisdictions allowing waiver of moral rights, Licensor waives 405 | all moral rights that Licensor may have in the Database to the fullest 406 | extent possible by the law of the relevant jurisdiction under Section 407 | 10.4; 408 | 409 | b. If waiver of moral rights under Section 5.1 a in the relevant 410 | jurisdiction is not possible, Licensor agrees not to assert any moral 411 | rights over the Database and waives all claims in moral rights to the 412 | fullest extent possible by the law of the relevant jurisdiction under 413 | Section 10.4; and 414 | 415 | c. For jurisdictions not allowing waiver or an agreement not to assert 416 | moral rights under Section 5.1 a and b, the author may retain their 417 | moral rights over certain aspects of the Database. 418 | 419 | Please note that some jurisdictions do not allow for the waiver of moral 420 | rights, and so moral rights may still subsist over the Database in some 421 | jurisdictions. 422 | 423 | 6.0 Fair dealing, Database exceptions, and other rights not affected 424 | 6.1 This License does not affect any rights that You or anyone else may 425 | independently have under any applicable law to make any use of this 426 | Database, including without limitation: 427 | 428 | a. Exceptions to the Database Right including: Extraction of Contents 429 | from non-electronic Databases for private purposes, Extraction for 430 | purposes of illustration for teaching or scientific research, and 431 | Extraction or Re-utilisation for public security or an administrative 432 | or judicial procedure. 433 | 434 | b. Fair dealing, fair use, or any other legally recognised limitation 435 | or exception to infringement of copyright or other applicable laws. 436 | 437 | 6.2 This License does not affect any rights of lawful users to Extract 438 | and Re-utilise insubstantial parts of the Contents, evaluated 439 | quantitatively or qualitatively, for any purposes whatsoever, including 440 | creating a Derivative Database (subject to other rights over the 441 | Contents, see Section 2.4). The repeated and systematic Extraction or 442 | Re-utilisation of insubstantial parts of the Contents may however amount 443 | to the Extraction or Re-utilisation of a Substantial part of the 444 | Contents. 445 | 446 | 7.0 Warranties and Disclaimer 447 | 7.1 The Database is licensed by the Licensor “as is” and without any 448 | warranty of any kind, either express, implied, or arising by statute, 449 | custom, course of dealing, or trade usage. Licensor specifically 450 | disclaims any and all implied warranties or conditions of title, 451 | non-infringement, accuracy or completeness, the presence or absence of 452 | errors, fitness for a particular purpose, merchantability, or otherwise. 453 | Some jurisdictions do not allow the exclusion of implied warranties, so 454 | this exclusion may not apply to You. 455 | 456 | 8.0 Limitation of liability 457 | 8.1 Subject to any liability that may not be excluded or limited by law, 458 | the Licensor is not liable for, and expressly excludes, all liability 459 | for loss or damage however and whenever caused to anyone by any use 460 | under this License, whether by You or by anyone else, and whether caused 461 | by any fault on the part of the Licensor or not. This exclusion of 462 | liability includes, but is not limited to, any special, incidental, 463 | consequential, punitive, or exemplary damages such as loss of revenue, 464 | data, anticipated profits, and lost business. This exclusion applies 465 | even if the Licensor has been advised of the possibility of such 466 | damages. 467 | 468 | 8.2 If liability may not be excluded by law, it is limited to actual and 469 | direct financial loss to the extent it is caused by proved negligence on 470 | the part of the Licensor. 471 | 472 | 9.0 Termination of Your rights under this License 473 | 9.1 Any breach by You of the terms and conditions of this License 474 | automatically terminates this License with immediate effect and without 475 | notice to You. For the avoidance of doubt, Persons who have received the 476 | Database, the whole or a Substantial part of the Contents, Derivative 477 | Databases, or the Database as part of a Collective Database from You 478 | under this License will not have their licenses terminated provided 479 | their use is in full compliance with this License or a license granted 480 | under Section 4.8 of this License. Sections 1, 2, 7, 8, 9 and 10 will 481 | survive any termination of this License. 482 | 483 | 9.2 If You are not in breach of the terms of this License, the Licensor 484 | will not terminate Your rights under it. 485 | 486 | 9.3 Unless terminated under Section 9.1, this License is granted to You 487 | for the duration of applicable rights in the Database. 488 | 489 | 9.4 Reinstatement of rights. If you cease any breach of the terms and 490 | conditions of this License, then your full rights under this License 491 | will be reinstated: 492 | 493 | a. Provisionally and subject to permanent termination until the 60th 494 | day after cessation of breach; 495 | 496 | b. Permanently on the 60th day after cessation of breach unless 497 | otherwise reasonably notified by the Licensor; or 498 | 499 | c. Permanently if reasonably notified by the Licensor of the 500 | violation, this is the first time You have received notice of 501 | violation of this License from the Licensor, and You cure the 502 | violation prior to 30 days after your receipt of the notice. 503 | 504 | Persons subject to permanent termination of rights are not eligible to 505 | be a recipient and receive a license under Section 4.8. 506 | 507 | 9.5 Notwithstanding the above, Licensor reserves the right to release 508 | the Database under different license terms or to stop distributing or 509 | making available the Database. Releasing the Database under different 510 | license terms or stopping the distribution of the Database will not 511 | withdraw this License (or any other license that has been, or is 512 | required to be, granted under the terms of this License), and this 513 | License will continue in full force and effect unless terminated as 514 | stated above. 515 | 516 | 10.0 General 517 | 10.1 If any provision of this License is held to be invalid or 518 | unenforceable, that must not affect the validity or enforceability of 519 | the remainder of the terms and conditions of this License and each 520 | remaining provision of this License shall be valid and enforced to the 521 | fullest extent permitted by law. 522 | 523 | 10.2 This License is the entire agreement between the parties with 524 | respect to the rights granted here over the Database. It replaces any 525 | earlier understandings, agreements or representations with respect to 526 | the Database. 527 | 528 | 10.3 If You are in breach of the terms of this License, You will not be 529 | entitled to rely on the terms of this License or to complain of any 530 | breach by the Licensor. 531 | 532 | 10.4 Choice of law. This License takes effect in and will be governed by 533 | the laws of the relevant jurisdiction in which the License terms are 534 | sought to be enforced. If the standard suite of rights granted under 535 | applicable copyright law and Database Rights in the relevant 536 | jurisdiction includes additional rights not granted under this License, 537 | these additional rights are granted in this License in order to meet the 538 | terms of this License. 539 | --------------------------------------------------------------------------------