Repository: RomanIakovlev/timeshape Branch: master Commit: 1e92bddbfa0f Files: 36 Total size: 130.0 KB Directory structure: gitextract_hhsocb8y/ ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── missing-or-unexpected-timezone.md │ └── workflows/ │ ├── dependency_graph.yml │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── CODE_LICENSE ├── DATA_LICENSE ├── Makefile ├── README.MD ├── benchmarks/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── iakovlev/ │ └── timeshape/ │ ├── AcceleratedGeometryBenchmark.java │ ├── BasicGeoOperationsBenchmark.java │ └── PolylineQueryBenchmark.java ├── builder/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── iakovlev/ │ └── timeshape/ │ ├── Builder.java │ └── Main.java ├── core/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── net/ │ │ └── iakovlev/ │ │ └── timeshape/ │ │ ├── Index.java │ │ ├── SameZoneSpan.java │ │ └── TimeZoneEngine.java │ └── test/ │ └── java/ │ └── net/ │ └── iakovlev/ │ └── timeshape/ │ ├── TimeZoneEngineBoundedTest.java │ ├── TimeZoneEngineBoundedZoneTest.java │ ├── TimeZoneEngineCoordinatesValidationTest.java │ ├── TimeZoneEngineOutfileBoundedTest.java │ ├── TimeZoneEnginePolylineTest.java │ ├── TimeZoneEngineSerializationTest.java │ └── TimeZoneEngineTest.java ├── doc/ │ └── Architecture.md ├── geojson-proto/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── protobuf/ │ └── geojson.proto ├── pom.xml ├── sonatype.key.gpg └── test-app/ ├── pom.xml └── src/ └── main/ ├── java/ │ └── net/ │ └── iakovlev/ │ └── timeshape/ │ └── testapp/ │ └── Main.java └── resources/ └── logback.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/java { "name": "sbt on Java 11", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/java:0-11", "hostRequirements": { "memory": "8gb" }, "features": { "ghcr.io/devcontainers/features/java:1": { "version": "11", "imageVariant": "11", "installMaven": "false", "installGradle": "false" }, "ghcr.io/devcontainers-contrib/features/sbt-sdkman:2": {}, "ghcr.io/devcontainers-contrib/features/scala-sdkman:2": {} } } ================================================ FILE: .github/ISSUE_TEMPLATE/missing-or-unexpected-timezone.md ================================================ --- name: Missing or unexpected timezone about: Use this if Timeshape query returns unexpected or no timezone title: Missing or unexpected timezone labels: '' assignees: '' --- # Wrong or unexpected timezone is returned 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: ## Which Timeshape version are you using? E.g. 2022g.16 ## Which Java version are you using? E.g. 8.0.361, 11.0.8 or 17.0.6. ## If you upgrade to the latest Java minor version, does it still fail? * Yes - To which Java version have you upgraded? * :tada: Not anymore! ## Which coordinates are you using? Please use `lat, lon` format, e.g. 61.237, 13.801. You may specify multiple coordinate pairs. ## Which timezone is returned? Please specify a full timezone id, such as `Europe/Berlin` or `Asia/Krasnoyarsk`. If no timezone is returned, please use `None`. ## Which timezone is expected? Please use the same format as above. ## Additional information E.g. if this used to work in some previous Timeshape version, or any other important facts. ================================================ FILE: .github/workflows/dependency_graph.yml ================================================ name: Update Dependency Graph on: push: branches: [master] jobs: dependency-graph: name: Update Dependency Graph runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' cache: 'maven' - uses: advanced-security/maven-dependency-submission-action@v4 ================================================ FILE: .github/workflows/pull_request.yml ================================================ name: Maven CI on: [pull_request] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' cache: 'maven' - name: Run tests run: make test ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: [master] permissions: contents: read jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' cache: 'maven' - name: Setup Sonatype key run: | mkdir -p /tmp/gpg chmod 700 /tmp/gpg export GNUPGHOME=/tmp/gpg gpg --batch --yes --decrypt --passphrase "${{ secrets.SONATYPE_KEY_PASSWORD }}" sonatype.key.gpg | gpg --batch --yes --import gpg --list-secret-keys --keyid-format LONG - name: Setup Maven settings run: | mkdir -p ~/.m2 cat <<-EOF > ~/.m2/settings.xml central ${{ secrets.CENTRAL_SONATYPE_COM_USERNAME }} ${{ secrets.CENTRAL_SONATYPE_COM_PASSWORD }} central true ${{ secrets.SONATYPE_KEY_ID }} EOF - name: Run tests and publish to Maven Central run: | export GNUPGHOME=/tmp/gpg export SOURCE_DATE_EPOCH=$(date +%s) make test # Deploy to Central Portal (handles both snapshots and releases) PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) echo "Project version: $PROJECT_VERSION" echo "Deploying to Sonatype Central Portal..." mvn deploy -Prelease -DskipTests -Dgpg.passphrase="" ================================================ FILE: .gitignore ================================================ .idea *.iml target output.pb.7z .bsp .vscode .cache dependency-reduced-pom.xml ================================================ FILE: CODE_LICENSE ================================================ MIT License Copyright (c) Roman Iakovlev 2018 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: DATA_LICENSE ================================================ Open Database License (ODbL) v1.0 Disclaimer Open Data Commons is not a law firm and does not provide legal services of any kind. 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. 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. Plain language summary A plain language summary of the Open Database License is available. Alternative formats: Plain Text ODC Open Database License (ODbL) Preamble The Open Database License (ODbL) is a license agreement intended to allow users to freely share, modify, and use this Database while maintaining this same freedom for others. Many databases are covered by copyright, and therefore this document licenses these rights. Some jurisdictions, mainly in the European Union, have specific rights that cover databases, and so the ODbL addresses these rights, too. Finally, the ODbL is also an agreement in contract for users of this Database to act in certain ways in return for accessing this Database. Databases can contain a wide variety of types of content (images, audiovisual material, and sounds all in the same database, for example), and so the ODbL only governs the rights over the Database, and not the contents of the Database individually. Licensors should use the ODbL together with another license for the contents, if the contents have a single set of rights that uniformly covers all of the contents. If the contents have multiple sets of different rights, Licensors should describe what rights govern what contents together in the individual record or in some other way that clarifies what rights apply. Sometimes the contents of a database, or the database itself, can be covered by other rights not addressed here (such as private contracts, trade mark over the name, or privacy rights / data protection rights over information in the contents), and so you are advised that you may have to consult other documents or clear other rights before doing activities not covered by this License. The Licensor (as defined below) and You (as defined below) agree as follows: 1.0 Definitions of Capitalised Words “Collective Database” – Means this Database in unmodified form as part of a collection of independent databases in themselves that together are assembled into a collective whole. A work that constitutes a Collective Database will not be considered a Derivative Database. “Convey” – As a verb, means Using the Database, a Derivative Database, or the Database as part of a Collective Database in any way that enables a Person to make or receive copies of the Database or a Derivative Database. Conveying does not include interaction with a user through a computer network, or creating and Using a Produced Work, where no transfer of a copy of the Database or a Derivative Database occurs. “Contents” – The contents of this Database, which includes the information, independent works, or other material collected into the Database. For example, the contents of the Database could be factual data or works such as images, audiovisual material, text, or sounds. “Database” – A collection of material (the Contents) arranged in a systematic or methodical way and individually accessible by electronic or other means offered under the terms of this License. “Database Directive” – Means Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended or succeeded. “Database Right” – Means rights resulting from the Chapter III (“sui generis”) rights in the Database Directive (as amended and as transposed by member states), which includes the Extraction and Re-utilisation of the whole or a Substantial part of the Contents, as well as any similar rights available in the relevant jurisdiction under Section 10.4. “Derivative Database” – Means a database based upon the Database, and includes any translation, adaptation, arrangement, modification, or any other alteration of the Database or of a Substantial part of the Contents. This includes, but is not limited to, Extracting or Re-utilising the whole or a Substantial part of the Contents in a new Database. “Extraction” – Means the permanent or temporary transfer of all or a Substantial part of the Contents to another medium by any means or in any form. “License” – Means this license agreement and is both a license of rights such as copyright and Database Rights and an agreement in contract. “Licensor” – Means the Person that offers the Database under the terms of this License. “Person” – Means a natural or legal person or a body of persons corporate or incorporate. “Produced Work” – a work (such as an image, audiovisual material, text, or sounds) resulting from using the whole or a Substantial part of the Contents (via a search or other query) from this Database, a Derivative Database, or this Database as part of a Collective Database. “Publicly” – means to Persons other than You or under Your control by either more than 50% ownership or by the power to direct their activities (such as contracting with an independent consultant). “Re-utilisation” – means any form of making available to the public all or a Substantial part of the Contents by the distribution of copies, by renting, by online or other forms of transmission. “Substantial” – Means substantial in terms of quantity or quality or a combination of both. The repeated and systematic Extraction or Re-utilisation of insubstantial parts of the Contents may amount to the Extraction or Re-utilisation of a Substantial part of the Contents. “Use” – As a verb, means doing any act that is restricted by copyright or Database Rights whether in the original medium or any other; and includes without limitation distributing, copying, publicly performing, publicly displaying, and preparing derivative works of the Database, as well as modifying the Database as may be technically necessary to use it in a different mode or format. “You” – Means a Person exercising rights under this License who has not previously violated the terms of this License with respect to the Database, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. Words in the singular include the plural and vice versa. 2.0 What this License covers 2.1. Legal effect of this document. This License is: a. A license of applicable copyright and neighbouring rights; b. A license of the Database Right; and c. An agreement in contract between You and the Licensor. 2.2 Legal rights covered. This License covers the legal rights in the Database, including: a. Copyright. Any copyright or neighbouring rights in the Database. The copyright licensed includes any individual elements of the Database, but does not cover the copyright over the Contents independent of this Database. See Section 2.4 for details. Copyright law varies between jurisdictions, but is likely to cover: the Database model or schema, which is the structure, arrangement, and organisation of the Database, and can also include the Database tables and table indexes; the data entry and output sheets; and the Field names of Contents stored in the Database; b. Database Rights. Database Rights only extend to the Extraction and Re-utilisation of the whole or a Substantial part of the Contents. Database Rights can apply even when there is no copyright over the Database. Database Rights can also apply when the Contents are removed from the Database and are selected and arranged in a way that would not infringe any applicable copyright; and c. Contract. This is an agreement between You and the Licensor for access to the Database. In return you agree to certain conditions of use on this access as outlined in this License. 2.3 Rights not covered. a. This License does not apply to computer programs used in the making or operation of the Database; b. This License does not cover any patents over the Contents or the Database; and c. This License does not cover any trademarks associated with the Database. 2.4 Relationship to Contents in the Database. The individual items of the Contents contained in this Database may be covered by other rights, including copyright, patent, data protection, privacy, or personality rights, and this License does not cover any rights (other than Database Rights or in contract) in individual Contents contained in the Database. For example, if used on a Database of images (the Contents), this License would not apply to copyright over individual images, which could have their own separate licenses, or one single license covering all of the rights over the images. 3.0 Rights granted 3.1 Subject to the terms and conditions of this License, the Licensor grants to You a worldwide, royalty-free, non-exclusive, terminable (but only under Section 9) license to Use the Database for the duration of any applicable copyright and Database Rights. These rights explicitly include commercial use, and do not exclude any field of endeavour. To the extent possible in the relevant jurisdiction, these rights may be exercised in all media and formats whether now known or created in the future. The rights granted cover, for example: a. Extraction and Re-utilisation of the whole or a Substantial part of the Contents; b. Creation of Derivative Databases; c. Creation of Collective Databases; d. Creation of temporary or permanent reproductions by any means and in any form, in whole or in part, including of any Derivative Databases or as a part of Collective Databases; and e. Distribution, communication, display, lending, making available, or performance to the public by any means and in any form, in whole or in part, including of any Derivative Database or as a part of Collective Databases. 3.2 Compulsory license schemes. For the avoidance of doubt: a. Non-waivable compulsory license schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; b. Waivable compulsory license schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, c. Voluntary license schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. 3.3 The right to release the Database under different terms, or to stop distributing or making available the Database, is reserved. Note that this Database may be multiple-licensed, and so You may have the choice of using alternative licenses for this Database. Subject to Section 10.4, all other rights not expressly granted by Licensor are reserved. 4.0 Conditions of Use 4.1 The rights granted in Section 3 above are expressly made subject to Your complying with the following conditions of use. These are important conditions of this License, and if You fail to follow them, You will be in material breach of its terms. 4.2 Notices. If You Publicly Convey this Database, any Derivative Database, or the Database as part of a Collective Database, then You must: a. Do so only under the terms of this License or another license permitted under Section 4.4; b. Include a copy of this License (or, as applicable, a license permitted under Section 4.4) or its Uniform Resource Identifier (URI) with the Database or Derivative Database, including both in the Database or Derivative Database and in any relevant documentation; and c. Keep intact any copyright or Database Right notices and notices that refer to this License. d. If it is not possible to put the required notices in a particular file due to its structure, then You must include the notices in a location (such as a relevant directory) where users would be likely to look for it. 4.3 Notice for using output (Contents). Creating and Using a Produced Work does not require the notice in Section 4.2. However, if you Publicly Use a Produced Work, You must include a notice associated with the Produced Work reasonably calculated to make any Person that uses, views, accesses, interacts with, or is otherwise exposed to the Produced Work aware that Content was obtained from the Database, Derivative Database, or the Database as part of a Collective Database, and that it is available under this License. a. Example notice. The following text will satisfy notice under Section 4.3: Contains information from DATABASE NAME, which is made available here under the Open Database License (ODbL). DATABASE NAME should be replaced with the name of the Database and a hyperlink to the URI of the Database. “Open Database License” should contain a hyperlink to the URI of the text of this License. If hyperlinks are not possible, You should include the plain text of the required URI’s with the above notice. 4.4 Share alike. a. Any Derivative Database that You Publicly Use must be only under the terms of: i. This License; ii. A later version of this License similar in spirit to this License; or iii. A compatible license. If You license the Derivative Database under one of the licenses mentioned in (iii), You must comply with the terms of that license. b. For the avoidance of doubt, Extraction or Re-utilisation of the whole or a Substantial part of the Contents into a new database is a Derivative Database and must comply with Section 4.4. c. Derivative Databases and Produced Works. A Derivative Database is Publicly Used and so must comply with Section 4.4. if a Produced Work created from the Derivative Database is Publicly Used. d. Share Alike and additional Contents. For the avoidance of doubt, You must not add Contents to Derivative Databases under Section 4.4 a that are incompatible with the rights granted under this License. e. Compatible licenses. Licensors may authorise a proxy to determine compatible licenses under Section 4.4 a iii. If they do so, the authorised proxy’s public statement of acceptance of a compatible license grants You permission to use the compatible license. 4.5 Limits of Share Alike. The requirements of Section 4.4 do not apply in the following: a. For the avoidance of doubt, You are not required to license Collective Databases under this License if You incorporate this Database or a Derivative Database in the collection, but this License still applies to this Database or a Derivative Database as a part of the Collective Database; b. Using this Database, a Derivative Database, or this Database as part of a Collective Database to create a Produced Work does not create a Derivative Database for purposes of Section 4.4; and c. Use of a Derivative Database internally within an organisation is not to the public and therefore does not fall under the requirements of Section 4.4. 4.6 Access to Derivative Databases. If You Publicly Use a Derivative Database or a Produced Work from a Derivative Database, You must also offer to recipients of the Derivative Database or Produced Work a copy in a machine readable form of: a. The entire Derivative Database; or b. A file containing all of the alterations made to the Database or the method of making the alterations to the Database (such as an algorithm), including any additional Contents, that make up all the differences between the Database and the Derivative Database. The Derivative Database (under a.) or alteration file (under b.) must be available at no more than a reasonable production cost for physical distributions and free of charge if distributed over the internet. 4.7 Technological measures and additional terms a. This License does not allow You to impose (except subject to Section 4.7 b.) any terms or any technological measures on the Database, a Derivative Database, or the whole or a Substantial part of the Contents that alter or restrict the terms of this License, or any rights granted under it, or have the effect or intent of restricting the ability of any person to exercise those rights. b. Parallel distribution. You may impose terms or technological measures on the Database, a Derivative Database, or the whole or a Substantial part of the Contents (a “Restricted Database”) in contravention of Section 4.74 a. only if You also make a copy of the Database or a Derivative Database available to the recipient of the Restricted Database: i. That is available without additional fee; ii. That is available in a medium that does not alter or restrict the terms of this License, or any rights granted under it, or have the effect or intent of restricting the ability of any person to exercise those rights (an “Unrestricted Database”); and iii. The Unrestricted Database is at least as accessible to the recipient as a practical matter as the Restricted Database. c. For the avoidance of doubt, You may place this Database or a Derivative Database in an authenticated environment, behind a password, or within a similar access control scheme provided that You do not alter or restrict the terms of this License or any rights granted under it or have the effect or intent of restricting the ability of any person to exercise those rights. 4.8 Licensing of others. You may not sublicense the Database. Each time You communicate the Database, the whole or Substantial part of the Contents, or any Derivative Database to anyone else in any way, the Licensor offers to the recipient a license to the Database on the same terms and conditions as this License. You are not responsible for enforcing compliance by third parties with this License, but You may enforce any rights that You have over a Derivative Database. You are solely responsible for any modifications of a Derivative Database made by You or another Person at Your direction. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. 5.0 Moral rights 5.1 Moral rights. This section covers moral rights, including any rights to be identified as the author of the Database or to object to treatment that would otherwise prejudice the author’s honour and reputation, or any other derogatory treatment: a. For jurisdictions allowing waiver of moral rights, Licensor waives all moral rights that Licensor may have in the Database to the fullest extent possible by the law of the relevant jurisdiction under Section 10.4; b. If waiver of moral rights under Section 5.1 a in the relevant jurisdiction is not possible, Licensor agrees not to assert any moral rights over the Database and waives all claims in moral rights to the fullest extent possible by the law of the relevant jurisdiction under Section 10.4; and c. For jurisdictions not allowing waiver or an agreement not to assert moral rights under Section 5.1 a and b, the author may retain their moral rights over certain aspects of the Database. Please note that some jurisdictions do not allow for the waiver of moral rights, and so moral rights may still subsist over the Database in some jurisdictions. 6.0 Fair dealing, Database exceptions, and other rights not affected 6.1 This License does not affect any rights that You or anyone else may independently have under any applicable law to make any use of this Database, including without limitation: a. Exceptions to the Database Right including: Extraction of Contents from non-electronic Databases for private purposes, Extraction for purposes of illustration for teaching or scientific research, and Extraction or Re-utilisation for public security or an administrative or judicial procedure. b. Fair dealing, fair use, or any other legally recognised limitation or exception to infringement of copyright or other applicable laws. 6.2 This License does not affect any rights of lawful users to Extract and Re-utilise insubstantial parts of the Contents, evaluated quantitatively or qualitatively, for any purposes whatsoever, including creating a Derivative Database (subject to other rights over the Contents, see Section 2.4). The repeated and systematic Extraction or Re-utilisation of insubstantial parts of the Contents may however amount to the Extraction or Re-utilisation of a Substantial part of the Contents. 7.0 Warranties and Disclaimer 7.1 The Database is licensed by the Licensor “as is” and without any warranty of any kind, either express, implied, or arising by statute, custom, course of dealing, or trade usage. Licensor specifically disclaims any and all implied warranties or conditions of title, non-infringement, accuracy or completeness, the presence or absence of errors, fitness for a particular purpose, merchantability, or otherwise. Some jurisdictions do not allow the exclusion of implied warranties, so this exclusion may not apply to You. 8.0 Limitation of liability 8.1 Subject to any liability that may not be excluded or limited by law, the Licensor is not liable for, and expressly excludes, all liability for loss or damage however and whenever caused to anyone by any use under this License, whether by You or by anyone else, and whether caused by any fault on the part of the Licensor or not. This exclusion of liability includes, but is not limited to, any special, incidental, consequential, punitive, or exemplary damages such as loss of revenue, data, anticipated profits, and lost business. This exclusion applies even if the Licensor has been advised of the possibility of such damages. 8.2 If liability may not be excluded by law, it is limited to actual and direct financial loss to the extent it is caused by proved negligence on the part of the Licensor. 9.0 Termination of Your rights under this License 9.1 Any breach by You of the terms and conditions of this License automatically terminates this License with immediate effect and without notice to You. For the avoidance of doubt, Persons who have received the Database, the whole or a Substantial part of the Contents, Derivative Databases, or the Database as part of a Collective Database from You under this License will not have their licenses terminated provided their use is in full compliance with this License or a license granted under Section 4.8 of this License. Sections 1, 2, 7, 8, 9 and 10 will survive any termination of this License. 9.2 If You are not in breach of the terms of this License, the Licensor will not terminate Your rights under it. 9.3 Unless terminated under Section 9.1, this License is granted to You for the duration of applicable rights in the Database. 9.4 Reinstatement of rights. If you cease any breach of the terms and conditions of this License, then your full rights under this License will be reinstated: a. Provisionally and subject to permanent termination until the 60th day after cessation of breach; b. Permanently on the 60th day after cessation of breach unless otherwise reasonably notified by the Licensor; or c. Permanently if reasonably notified by the Licensor of the violation, this is the first time You have received notice of violation of this License from the Licensor, and You cure the violation prior to 30 days after your receipt of the notice. Persons subject to permanent termination of rights are not eligible to be a recipient and receive a license under Section 4.8. 9.5 Notwithstanding the above, Licensor reserves the right to release the Database under different license terms or to stop distributing or making available the Database. Releasing the Database under different license terms or stopping the distribution of the Database will not withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 10.0 General 10.1 If any provision of this License is held to be invalid or unenforceable, that must not affect the validity or enforceability of the remainder of the terms and conditions of this License and each remaining provision of this License shall be valid and enforced to the fullest extent permitted by law. 10.2 This License is the entire agreement between the parties with respect to the rights granted here over the Database. It replaces any earlier understandings, agreements or representations with respect to the Database. 10.3 If You are in breach of the terms of this License, You will not be entitled to rely on the terms of this License or to complain of any breach by the Licensor. 10.4 Choice of law. This License takes effect in and will be governed by the laws of the relevant jurisdiction in which the License terms are sought to be enforced. If the standard suite of rights granted under applicable copyright law and Database Rights in the relevant jurisdiction includes additional rights not granted under this License, these additional rights are granted in this License in order to meet the terms of this License. ================================================ FILE: Makefile ================================================ # Makefile for Timeshape data processing # Handles timezone data download and resource generation for Maven build # Variables DATA_VERSION ?= 2025b BUILDER_JAR = builder/target/timeshape-builder.jar OUTPUT_DIR = core/target/classes OUTPUT_FILE = $(OUTPUT_DIR)/data.tar.zstd TIMEZONE_URL = https://github.com/evansiroky/timezone-boundary-builder/releases/download/$(DATA_VERSION)/timezones-with-oceans.geojson.zip CACHE_DIR = .cache CACHED_DATA = $(CACHE_DIR)/timezones-$(DATA_VERSION).zip # Use Maven to execute Java commands (same JVM as Maven uses) MVN = mvn # Ensure directories exist $(OUTPUT_DIR): @mkdir -p $(OUTPUT_DIR) $(CACHE_DIR): @mkdir -p $(CACHE_DIR) # Build the builder JAR if it doesn't exist or is out of date $(BUILDER_JAR): builder/src/main/java/net/iakovlev/timeshape/*.java geojson-proto/src/main/protobuf/geojson.proto @echo "Building timeshape-builder and dependencies..." @echo " - Installing parent POM..." @$(MVN) -q -N install @echo " - Compiling protobuf and installing geojson-proto..." @cd geojson-proto && $(MVN) -q compile install @echo " - Compiling builder..." @cd builder && $(MVN) -q compile @echo " - Creating builder assembly..." @cd builder && $(MVN) -q assembly:single # Download and cache timezone data $(CACHED_DATA): $(CACHE_DIR) @echo "Downloading timezone data version: $(DATA_VERSION)" @if curl -L -f -s -o $(CACHED_DATA) "$(TIMEZONE_URL)"; then \ echo "Downloaded timezone data to $(CACHED_DATA)"; \ else \ echo "Failed to download timezone data from $(TIMEZONE_URL)"; \ exit 1; \ fi # Execute builder using Maven (ensures same Java as Maven) run-builder: @if [ -f "$(CACHED_DATA)" ]; then \ echo "Using cached timezone data from $(CACHED_DATA)"; \ cd builder && $(MVN) -q exec:java -Dexec.mainClass="net.iakovlev.timeshape.Main" -Dexec.args="$(CACHED_DATA) ../$(OUTPUT_FILE)"; \ elif [ -f "/tmp/timezones-$(DATA_VERSION).zip" ]; then \ echo "Using timezone data from /tmp/timezones-$(DATA_VERSION).zip"; \ cd builder && $(MVN) -q exec:java -Dexec.mainClass="net.iakovlev.timeshape.Main" -Dexec.args="/tmp/timezones-$(DATA_VERSION).zip ../$(OUTPUT_FILE)"; \ else \ echo "Downloading and processing timezone data directly"; \ cd builder && $(MVN) -q exec:java -Dexec.mainClass="net.iakovlev.timeshape.Main" -Dexec.args="$(DATA_VERSION) ../$(OUTPUT_FILE)"; \ fi # Generate data.tar.zstd resource file generate-data: $(OUTPUT_DIR) $(BUILDER_JAR) @if [ -f "$(OUTPUT_FILE)" ]; then \ echo "Timeshape resource exists at $(OUTPUT_FILE), skipping creation"; \ else \ echo "Timeshape resource doesn't exist, creating it now"; \ echo "Generating timezone data with version: $(DATA_VERSION)"; \ $(MAKE) run-builder; \ if [ -f "$(OUTPUT_FILE)" ]; then \ echo "Successfully generated $(OUTPUT_FILE)"; \ else \ echo "Failed to generate $(OUTPUT_FILE)"; \ exit 1; \ fi; \ fi # Clean all build artifacts and generated data clean: @echo "Cleaning build artifacts and generated data..." @mvn clean @rm -f /tmp/timezones-*.zip # Clean everything including cache clean-all: clean @echo "Cleaning cache..." @rm -rf $(CACHE_DIR) # Test all modules test: generate-data @echo "Running tests..." @mvn test # Deploy to Sonatype Central Portal (handles both snapshots and releases) deploy: generate-data @echo "Deploying to Sonatype Central Portal..." @PROJECT_VERSION=$$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout); \ echo "Deploying version $$PROJECT_VERSION to Central Portal"; \ mvn deploy -Prelease -DskipTests # Force regenerate data (useful for development) force-generate-data: clean generate-data # Dry-run deployment (shows what would be deployed) deploy-dry-run: @echo "Dry-run deployment check..." @PROJECT_VERSION=$$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout); \ echo "Current version: $$PROJECT_VERSION"; \ if [[ "$$PROJECT_VERSION" == *"-SNAPSHOT" ]]; then \ echo "-> Would deploy SNAPSHOT to Sonatype Central Portal"; \ else \ echo "-> Would deploy RELEASE to Sonatype Central Portal"; \ fi; \ echo "-> Command: mvn deploy -Prelease -DskipTests"; \ echo "Modules that would be deployed:"; \ mvn help:evaluate -Dexpression=project.modules -q -DforceStdout | grep -v "maven.deploy.skip=true" || true # Show current configuration show-config: @echo "Current configuration:" @echo " DATA_VERSION: $(DATA_VERSION)" @echo " BUILDER_JAR: $(BUILDER_JAR)" @echo " OUTPUT_FILE: $(OUTPUT_FILE)" @echo " TIMEZONE_URL: $(TIMEZONE_URL)" @echo " MVN: $(MVN)" # Help target help: @echo "Available targets:" @echo " generate-data - Generate data.tar.zstd resource file" @echo " run-builder - Execute builder using Maven" @echo " test - Run all tests" @echo " deploy - Deploy to Sonatype Central Portal" @echo " deploy-dry-run - Show what would be deployed without deploying" @echo " clean - Clean build artifacts and generated data" @echo " clean-all - Clean everything including cache" @echo " force-generate-data - Force regenerate data file" @echo " show-config - Show current configuration" @echo " help - Show this help message" @echo "" @echo "Variables:" @echo " DATA_VERSION - Timezone data version (default: $(DATA_VERSION))" .PHONY: run-builder generate-data test deploy deploy-dry-run clean clean-all force-generate-data show-config help ================================================ FILE: README.MD ================================================ # Timeshape [![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.iakovlev/timeshape/badge.svg)](https://maven-badges.herokuapp.com/maven-central/net.iakovlev/timeshape/) ![Build status](https://github.com/RomanIakovlev/timeshape/actions/workflows/release.yml/badge.svg) [![Gitter](https://badges.gitter.im/timeshape/community.svg)](https://gitter.im/timeshape/community) Timeshape is a Java library that can be used to determine to which time zone a given geo coordinate belongs. It's based on data published at [https://github.com/evansiroky/timezone-boundary-builder/releases](https://github.com/evansiroky/timezone-boundary-builder/releases), which itself is inherited from the OpenStreetMap data. But what if knowing just time zone for a geo coordinate is not enough? Wouldn't it be nice to know more, like administrative area or city neighborhood? Check out [GeoBundle](https://geobundle.com), now there's a Java library for that, too! ## Quote > “Time is an illusion.” > > ― **Albert Einstein** ## Getting started Timeshape is published on Maven Central. The coordinates are the following: ```xml net.iakovlev timeshape 2025b.26 ``` Starting from release 2023b.20, Timeshape re-introduces support for Java 8. It was inadevrtently dropped in one of the previous releases, but now it's brought back. ## Android Android developers should add Timeshape to the app level gradle.build as follows: ```gradle implementation('net.iakovlev:timeshape:2025b.26') { // Exclude standard compression library exclude group: 'com.github.luben', module: 'zstd-jni' } // Import aar for native component compilation implementation 'com.github.luben:zstd-jni:1.5.5-11@aar' ``` ## Adopters Are you using Timeshape? Please consider opening a pull request to list your organization here: * Name and website of your organization * [AirPing](https://airping.app/) * [Hopper](https://hopper.com/) * [Natural Light](https://play.google.com/store/apps/details?id=com.blackholeofphotography.naturallight) ## Using the library The user API of library is in `net.iakovlev.timeshape.TimeZoneEngine` class. To use it, follow these steps: #### Initialization Initialize the class with the data for the whole world: ```java import net.iakovlev.timeshape.TimeZoneEngine; TimeZoneEngine engine = TimeZoneEngine.initialize(); ``` Or, alternatively, initialize it with some bounding box only, to reduce memory usage: ```java TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486); ``` It is important to mention that for the time zone to be loaded by the second method, it must be covered by the bounding box completely, not just intersect with it. if you have difficulty determining the bounding box, you may prefer to use a list of ZoneIds instead. ```Java Set timeZones = new HashSet<> (); timeZones.add(ZoneId.of("Europe/Berlin")); timeZones.add(ZoneId.of("America/Detroit")); TimeZoneEngine engine = TimeZoneEngine.initialize (timeZones, true); ``` During initialization, the data is read from resource and the index is built. Initialization takes a significant amount of time (approximately 1 second), so do it only once in program lifetime. Data can also be read from an external file, see overloads of `TimeZoneEngine.initialize` that accept `TarArchiveInputStream`. The file format should be same as produced by running `builder` sbt project (see [here](doc/Architecture.md#builder)). It is responsibility of caller to close input stream passed to `TimeZoneEngine.initialize`. There is an overload of `TimeZoneEngine.initialize` method that accepts a boolean parameter called `accelerateGeometry`. If that parameter is equals to `true`, Timeshape will build a bitmap index for each polygon it loads. This requires some extra memory and takes time at initialization, hence that parameter is being `false` by default, however it speeds up the typical point-in-polygon query approximately 2.5 times. See performance benchmarks in `AcceleratedGeometryBenchmark.java`, and here is the result: ``` [info] Benchmark Mode Cnt Score Error Units [info] AcceleratedGeometryBenchmark.testAcceleratedEngine thrpt 10 38.142 ± 0.448 ops/s [info] AcceleratedGeometryBenchmark.testNonAcceleratedEngine thrpt 10 15.289 ± 0.121 ops/s ``` ##### Improving start up time by using serialization Initialization on slow devices such as Android phones can take up to 20 seconds. This can be improved by serializing an instance of the `TimeZoneEngine` and deserializing on subsequent runs of your program. An example of serializing to a file can be found in `TimeZoneEngineSerializationTest.java` in the unit tests. Serialization is especially useful if you can limit `TimeZoneEngine` to a portion of the data set via a user preference. #### Query for `java.time.ZoneId`: Once initialization is completed, you can query the `ZoneId` based on latitude and longitude: ```java import java.util.Optional; import java.time.ZoneId; Optional maybeZoneId = engine.query(52.52, 13.40); ``` #### Multiple time zones for single geo point Starting from release 2019b.7, the data source from which Timeshape is built contains overlapping geometries. In other words, some geo points can simultaneously belong to multiple time zones. To accommodate this, Timeshape has made the following change. It's now possible to query all the time zones, to which given geo point belongs: ```java List allZones = engine.queryAll(52.52, 13.40); ``` The `Optional query(double latitude, double longitude)` method is still there, and it still returns maximum one `ZoneId`. If given geo point belongs to multiple time zones, only single one will be returned. Which of multiple zones will be returned is entirely arbitrary. Because of this, method `Optional query(double latitude, double longitude)` is not suitable for use cases where such choice must be deliberate. In such cases use `List queryAll(double latitude, double longitude)` method and apply further business logic to its output to choose the right time zone. Consult file https://raw.githubusercontent.com/evansiroky/timezone-boundary-builder/master/expectedZoneOverlaps.json for information about areas of the world where multiple time zones are to be expected. #### Querying polyline Timeshape supports querying of multiple sequential geo points (a polyline, e.g. a GPS trace) in an optimized way using method `List queryPolyline(double[] points)`. Performance tests (see `net.iakovlev.timeshape.PolylineQueryBenchmark`) show significant speedup of using this method for querying a polyline, comparing to separately querying each point from polyline using `Optional query(double latitude, double longitude)` method: ``` Benchmark Mode Cnt Score Error Units PolylineQueryBenchmark.testQueryPoints thrpt 5 2,188 ▒ 0,044 ops/s PolylineQueryBenchmark.testQueryPolyline thrpt 5 4,073 ▒ 0,017 ops/s ``` ### Architecture See [dedicated document](doc/Architecture.md) for description of Timeshape internals. ### Versioning Version of Timeshape consist of data version and software version, divided by a '.' symbol. Data version is as specified at [https://github.com/evansiroky/timezone-boundary-builder/releases](https://github.com/evansiroky/timezone-boundary-builder/releases). Software version is an integer, starting from 1 and incrementing for each published artifact. ## Licenses The code of the library is licensed under the [MIT License](https://opensource.org/licenses/MIT). The time zone data contained in library is licensed under the [Open Data Commons Open Database License (ODbL)](http://opendatacommons.org/licenses/odbl/). ## Endorsements Timeshape uses YourKit. YourKit supports open source projects with innovative and intelligent tools for monitoring and profiling Java and .NET applications. YourKit is the creator of YourKit Java Profiler, YourKit .NET Profiler, and YourKit YouMonitor. ================================================ FILE: benchmarks/pom.xml ================================================ 4.0.0 net.iakovlev timeshape-parent 2025b.28 timeshape-benchmarks jar Timeshape Benchmarks JMH benchmarks for Timeshape true 1.37 net.iakovlev timeshape org.openjdk.jmh jmh-core ${jmh.version} org.openjdk.jmh jmh-generator-annprocess ${jmh.version} provided org.apache.maven.plugins maven-shade-plugin 3.5.0 package shade timeshape-benchmarks org.openjdk.jmh.Main ================================================ FILE: benchmarks/src/main/java/net/iakovlev/timeshape/AcceleratedGeometryBenchmark.java ================================================ package net.iakovlev.timeshape; import net.iakovlev.timeshape.TimeZoneEngine; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import java.util.ArrayList; public class AcceleratedGeometryBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { float[] cityCoordinates = { 35.6762f, 139.6503f, // Tokyo, Japan 31.2304f, 121.4737f, // Shanghai, China 23.1291f, 113.2644f, // Guangzhou, China 28.7041f, 77.1025f, // Delhi, India 19.4326f, -99.1332f, // Mexico City, Mexico -23.551f, -46.6333f, // Sao Paulo, Brazil 34.6937f, 135.5023f, // Osaka, Japan 13.7563f, 100.5018f, // Bangkok, Thailand 39.9042f, 116.4074f, // Beijing, China 19.0760f, 72.8777f, // Mumbai, India 40.7128f, -74.0060f, // New York City, USA 22.5726f, 88.3639f, // Kolkata, India 14.5995f, 120.9842f, // Manila, Philippines 23.8103f, 90.4125f, // Dhaka, Bangladesh 3.1390f, 101.6869f, // Kuala Lumpur, Malaysia 30.0444f, 31.2357f, // Cairo, Egypt 34.0522f, -118.2437f, // Los Angeles, USA 31.5497f, 74.3436f, // Lahore, Pakistan 6.5244f, 3.3792f, // Lagos, Nigeria 55.7558f, 37.6173f // Moscow, Russia }; TimeZoneEngine engineNoAcceleration = TimeZoneEngine.initialize(false); TimeZoneEngine engineWithAcceleration = TimeZoneEngine.initialize(true); } @Benchmark public void testAcceleratedEngine(BenchmarkState state, Blackhole blackhole) { for (int i = 0; i < state.cityCoordinates.length; i += 2) { blackhole.consume( state.engineWithAcceleration.query(state.cityCoordinates[i], state.cityCoordinates[i + 1])); } } @Benchmark public void testNonAcceleratedEngine(BenchmarkState state, Blackhole blackhole) { for (int i = 0; i < state.cityCoordinates.length; i += 2) { blackhole.consume( state.engineNoAcceleration.query(state.cityCoordinates[i], state.cityCoordinates[i + 1])); } } } ================================================ FILE: benchmarks/src/main/java/net/iakovlev/timeshape/BasicGeoOperationsBenchmark.java ================================================ package net.iakovlev.timeshape; import com.esri.core.geometry.*; import net.iakovlev.timeshape.TimeZoneEngine; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; import java.lang.reflect.Field; import java.util.ArrayList; public class BasicGeoOperationsBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { TimeZoneEngine engine = TimeZoneEngine.initialize(); Index index; QuadTree quadTree; Point p = new Point(13.31, 52.52); ArrayList entries; Geometry matchingGeometry; ArrayList nonMatchingGeometries = new ArrayList<>(); SpatialReference spatialReference = SpatialReference.create(4326); @Setup public void setup() { try { Field indexField = engine.getClass().getDeclaredField("index"); indexField.setAccessible(true); index = (Index) indexField.get(engine); Field quadTreeField = index.getClass().getDeclaredField("quadTree"); quadTreeField.setAccessible(true); quadTree = (QuadTree) quadTreeField.get(index); Field zoneIdsField = index.getClass().getDeclaredField("zoneIds"); zoneIdsField.setAccessible(true); entries = (ArrayList) zoneIdsField.get(index); QuadTree.QuadTreeIterator iterator = quadTree.getIterator(p, 0); for (int i = iterator.next(); i >= 0; i = iterator.next()) { int element = quadTree.getElement(i); Index.Entry entry = entries.get(element); if (GeometryEngine.contains(entry.geometry, p, spatialReference)) { matchingGeometry = entry.geometry; } else { nonMatchingGeometries.add(entry.geometry); } } } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); throw new RuntimeException(e); } } } @Benchmark public void testQuadTree(BenchmarkState state, Blackhole blackhole) { QuadTree.QuadTreeIterator iterator = state.quadTree.getIterator(state.p, 0); for (int i = iterator.next(); i >= 0; i = iterator.next()) { blackhole.consume(state.quadTree.getElement(i)); } } @Benchmark public void testSearchInNonMatchingGeometry(BenchmarkState state, Blackhole blackhole) { for (Geometry g : state.nonMatchingGeometries) { blackhole.consume(GeometryEngine.contains(g, state.p, state.spatialReference)); } } @Benchmark public void testSearchInMatchingGeometry(BenchmarkState state, Blackhole blackhole) { blackhole.consume(GeometryEngine.contains(state.matchingGeometry, state.p, state.spatialReference)); } @Benchmark public void testIndexQuery(BenchmarkState state, Blackhole blackhole) { blackhole.consume(state.index.query(state.p.getY(), state.p.getX())); } } ================================================ FILE: benchmarks/src/main/java/net/iakovlev/timeshape/PolylineQueryBenchmark.java ================================================ package net.iakovlev.timeshape; import net.iakovlev.timeshape.TimeZoneEngine; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import java.util.ArrayList; public class PolylineQueryBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { double latStart = 52.52; double lonStart = 13.40; double latEnd = 56.52; double lonEnd = 16.40; int steps = 200; double latStep = Math.abs(latEnd - latStart) / steps; double lonStep = Math.abs(lonEnd - lonStart) / steps; double[] points; TimeZoneEngine engine = TimeZoneEngine.initialize(); ArrayList pointsList = new ArrayList<>(); @Setup public void setup() { for (int i = 0; i < steps; i++) { pointsList.add(latStart + latStep * i); pointsList.add(lonStart + lonStep * i); } points = pointsList.stream().mapToDouble(Double::doubleValue).toArray(); } } @Benchmark public void testQueryPoints(BenchmarkState state, Blackhole blackhole) { for (int i = 0; i < state.points.length - 1; i += 2) { blackhole.consume(state.engine.query(state.points[i], state.points[i+1])); } } @Benchmark public void testQueryPolyline(BenchmarkState state, Blackhole blackhole) { blackhole.consume(state.engine.queryPolyline(state.points)); } } ================================================ FILE: builder/pom.xml ================================================ 4.0.0 net.iakovlev timeshape-parent 2025b.28 timeshape-builder jar Timeshape Builder Builder utility for processing timezone data true net.iakovlev geojson-proto com.fasterxml.jackson.core jackson-core de.grundid.opendatalab geojson-jackson 1.14 org.apache.commons commons-compress com.github.luben zstd-jni org.apache.maven.plugins maven-assembly-plugin 3.6.0 net.iakovlev.timeshape.Main jar-with-dependencies timeshape-builder false make-assembly package single org.codehaus.mojo exec-maven-plugin 3.1.0 net.iakovlev.timeshape.Main ================================================ FILE: builder/src/main/java/net/iakovlev/timeshape/Builder.java ================================================ package net.iakovlev.timeshape; import net.iakovlev.timeshape.proto.Geojson; import org.geojson.*; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; final class Builder { private static Geojson.LineString geoJsonCoordinatesToProtoLineString(List geoJsonCoordinates) { return Geojson.LineString.newBuilder().addAllCoordinates(geoJsonCoordinates.stream().map(lngLatAlt -> { Geojson.Position.Builder builder = Geojson.Position.newBuilder(); builder.setLat((float) lngLatAlt.getLatitude()); builder.setLon((float) lngLatAlt.getLongitude()); return builder.build(); }).collect(Collectors.toList())).build(); } private static Geojson.Polygon geoJsonPolygonToProtoPolygon(List> poly) { List exteriorRing = poly.get(0); Geojson.LineString protoExteriorRing = geoJsonCoordinatesToProtoLineString(exteriorRing); Geojson.Polygon.Builder polygonBuilder = Geojson.Polygon.newBuilder(); polygonBuilder.addCoordinates(protoExteriorRing); poly.subList(1, poly.size()).forEach(list -> polygonBuilder.addCoordinates(geoJsonCoordinatesToProtoLineString(list))); return polygonBuilder.build(); } private static Geojson.MultiPolygon geoJsonMultiPolygonToProtoMultiPolygon(MultiPolygon multiPolygon) { Geojson.MultiPolygon.Builder builder = Geojson.MultiPolygon.newBuilder(); multiPolygon.getCoordinates().forEach(polys -> builder.addCoordinates(geoJsonPolygonToProtoPolygon(polys))); return builder.build(); } static Geojson.FeatureCollection buildProto(FeatureCollection featureCollection) { Geojson.FeatureCollection.Builder featureCollectionBuilder = Geojson.FeatureCollection.newBuilder(); for (org.geojson.Feature f : featureCollection.getFeatures()) { String tzid = f.getProperties().get("tzid").toString(); Geojson.Feature.Builder featureBuilder = Geojson.Feature.newBuilder(); featureBuilder.addProperties(Geojson.Property.newBuilder().setKey("tzid").setValueString(tzid)); GeoJsonObject geoJsonObject = f.getGeometry(); if (geoJsonObject instanceof Polygon) { Polygon poly = (Polygon) geoJsonObject; Stream.Builder> s = Stream.builder(); s.add(poly.getExteriorRing()); poly.getInteriorRings().forEach(s::add); featureCollectionBuilder.addFeatures( featureBuilder.setGeometry( Geojson.Geometry.newBuilder().setPolygon( geoJsonPolygonToProtoPolygon(s.build().collect(Collectors.toList()))))); } else if (geoJsonObject instanceof MultiPolygon) { MultiPolygon multiPolygon = (MultiPolygon) geoJsonObject; featureCollectionBuilder.addFeatures(featureBuilder.setGeometry(Geojson.Geometry.newBuilder().setMultiPolygon( geoJsonMultiPolygonToProtoMultiPolygon(multiPolygon) ))); } else { throw new RuntimeException("not implemented"); } } return featureCollectionBuilder.build(); } } ================================================ FILE: builder/src/main/java/net/iakovlev/timeshape/Main.java ================================================ package net.iakovlev.timeshape; import com.fasterxml.jackson.databind.ObjectMapper; import net.iakovlev.timeshape.proto.Geojson; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream; import org.geojson.FeatureCollection; import java.io.*; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.zip.ZipInputStream; public class Main { /** * Provides access to source GeoJSON data. If argument is a path to the file on a local computer, * it's opened for reading. If it's not a existing file, the argument is interpreted as a version number * to download from Github * @param argument either a path on a local computer or a version number to download from Github * @return Stream with contents (zip-compressed) of source GeoJSON data * @throws IOException if incorrect local path is provided or there was a problem with downloading the file */ private static InputStream createInputStream(String argument) throws IOException { if (Files.exists(Paths.get(argument))) { return new FileInputStream(argument); } else { String url = "https://github.com/evansiroky/timezone-boundary-builder/releases/download/" + argument + "/timezones-with-oceans.geojson.zip"; return new URL(url).openStream(); } } /** * Reads the zip-compressed input stream of GeoJSON, converts its data to proto and writes * each GeoJSON feature as a separate entry into a tar file, which is compressed with ZStandard * algorithm at the end * @param argument * @param outputPath */ private static void writeTarZStdProto(String argument, String outputPath) { try (ZipInputStream zipInputStream = new ZipInputStream(createInputStream(argument))) { zipInputStream.getNextEntry(); Geojson.FeatureCollection featureCollection = Builder.buildProto(new ObjectMapper().readValue(zipInputStream, FeatureCollection.class)); try (TarArchiveOutputStream out = new TarArchiveOutputStream( new ZstdCompressorOutputStream(new FileOutputStream(outputPath), 22))) { for (Geojson.Feature feature : featureCollection.getFeaturesList()) { TarArchiveEntry entry = new TarArchiveEntry(feature.getProperties(0).getValueString()); byte[] bytes = feature.toByteArray(); entry.setSize(bytes.length); out.putArchiveEntry(entry); out.write(bytes); out.closeArchiveEntry(); } } } catch (Exception e) { throw new RuntimeException("Couldn't prepare the data", e); } } public static void main(String[] args) { String argument = args[0]; String outputPath = args[1]; writeTarZStdProto(argument, outputPath); } } ================================================ FILE: core/pom.xml ================================================ 4.0.0 net.iakovlev timeshape-parent 2025b.28 timeshape jar Timeshape A Java library for mapping between coordinates and timezones com.esri.geometry esri-geometry-api 2.2.4 org.slf4j slf4j-api 1.7.30 net.iakovlev geojson-proto org.apache.commons commons-compress com.github.luben zstd-jni com.fasterxml.jackson.core jackson-core junit junit 4.13.1 test com.novocode junit-interface 0.11 test junit junit-dep org.sonatype.central central-publishing-maven-plugin true org.apache.maven.plugins maven-jar-plugin 3.3.0 net.iakovlev.timeshape org.codehaus.mojo exec-maven-plugin 3.1.0 generate-resources generate-resources exec make ${project.parent.basedir} generate-data ${data.version} ================================================ FILE: core/src/main/java/net/iakovlev/timeshape/Index.java ================================================ package net.iakovlev.timeshape; import com.esri.core.geometry.*; import net.iakovlev.timeshape.proto.Geojson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.PrimitiveIterator; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; final class Index implements Serializable { static final class Entry implements Serializable { final ZoneId zoneId; final Geometry geometry; Entry(ZoneId zoneId, Geometry geometry) { this.zoneId = zoneId; this.geometry = geometry; } } private static final int WGS84_WKID = 4326; private final ArrayList zoneIds; private static final SpatialReference spatialReference = SpatialReference.create(WGS84_WKID); private final QuadTree quadTree; private static final Logger log = LoggerFactory.getLogger(Index.class); private Index(QuadTree quadTree, ArrayList zoneIds) { log.info("Initialized index with {} time zones", zoneIds.size()); this.quadTree = quadTree; this.zoneIds = zoneIds; } List getKnownZoneIds() { return zoneIds.stream().map(e -> e.zoneId).collect(Collectors.toList()); } List query(double latitude, double longitude) { ArrayList result = new ArrayList<>(2); Point point = new Point(longitude, latitude); OperatorIntersects operator = OperatorIntersects.local(); QuadTree.QuadTreeIterator iterator = quadTree.getIterator(point, 0); for (int i = iterator.next(); i >= 0; i = iterator.next()) { int element = quadTree.getElement(i); Entry entry = zoneIds.get(element); if(operator.execute(entry.geometry, point, spatialReference, null)) { result.add(entry.zoneId); } } return result; } List queryPolyline(double[] line) { Polyline polyline = new Polyline(); ArrayList points = new ArrayList<>(line.length / 2); for (int i = 0; i < line.length - 1; i += 2) { Point p = new Point(line[i + 1], line[i]); points.add(p); } polyline.startPath(points.get(0)); for (int i = 1; i < points.size(); i += 1) { polyline.lineTo(points.get(i)); } QuadTree.QuadTreeIterator iterator = quadTree.getIterator(polyline, 0); ArrayList potentiallyMatchingEntries = new ArrayList<>(); for (int i = iterator.next(); i >= 0; i = iterator.next()) { int element = quadTree.getElement(i); Entry entry = zoneIds.get(element); potentiallyMatchingEntries.add(entry); } ArrayList sameZoneSegments = new ArrayList<>(); List currentEntry = null; // 1. find next matching geometry or geometries // 2. for every match, increase the index // 3. when it doesn't match anymore, save currentSegment to sameZoneSegments and start new one // 4. goto 1. int index = 0; boolean lastWasEmpty = false; OperatorIntersects operator = OperatorIntersects.local(); while (index < points.size()) { Point p = points.get(index); if (currentEntry == null) { currentEntry = potentiallyMatchingEntries .stream() .filter(e -> operator.execute(e.geometry, p, spatialReference, null)) .collect(Collectors.toList()); } if (currentEntry.isEmpty()) { currentEntry = null; lastWasEmpty = true; index++; } else { if (lastWasEmpty) { lastWasEmpty = false; sameZoneSegments.add(SameZoneSpan.fromIndexEntries(Collections.emptyList(), (index - 1) * 2 + 1)); continue; } if (currentEntry.stream().allMatch(e -> operator.execute(e.geometry, p, spatialReference, null))) { if (index == points.size() - 1) { sameZoneSegments.add(SameZoneSpan.fromIndexEntries(currentEntry, index * 2 + 1)); } index++; } else { sameZoneSegments.add(SameZoneSpan.fromIndexEntries(currentEntry, (index - 1) * 2 + 1)); currentEntry = null; } } } if (lastWasEmpty) { sameZoneSegments.add(SameZoneSpan.fromIndexEntries(Collections.emptyList(), index * 2 - 1)); } return sameZoneSegments; } private static Polygon buildPoly(Geojson.Polygon from) { Polygon poly = new Polygon(); from.getCoordinatesList().stream() .map(Geojson.LineString::getCoordinatesList) .forEachOrdered(lp -> { poly.startPath(lp.get(0).getLon(), lp.get(0).getLat()); lp.subList(1, lp.size()).forEach(p -> poly.lineTo(p.getLon(), p.getLat())); }); return poly; } static Index build(Stream features, int size, Envelope boundaries) { return build(features, size, boundaries, false); } private static Stream getPolygons(Geojson.Feature f) { if (f.getGeometry().hasPolygon()) { return Stream.of(buildPoly(f.getGeometry().getPolygon())); } else if (f.getGeometry().hasMultiPolygon()) { Geojson.MultiPolygon multiPolygonProto = f.getGeometry().getMultiPolygon(); return multiPolygonProto.getCoordinatesList().stream().map(Index::buildPoly); } else { throw new RuntimeException("Unknown geometry type"); } } @SuppressWarnings("SizeReplaceableByIsEmpty") static Index build(Stream features, int size, Envelope boundaries, boolean accelerateGeometry) { Envelope2D boundariesEnvelope = new Envelope2D(); boundaries.queryEnvelope2D(boundariesEnvelope); QuadTree quadTree = new QuadTree(boundariesEnvelope, 8); Envelope2D env = new Envelope2D(); ArrayList zoneIds = new ArrayList<>(size); PrimitiveIterator.OfInt indices = IntStream.iterate(0, i -> i + 1).iterator(); List unknownZones = new ArrayList<>(); OperatorIntersects operatorIntersects = OperatorIntersects.local(); features.forEach(f -> { String zoneIdName = f.getProperties(0).getValueString(); try { ZoneId zoneId = ZoneId.of(zoneIdName); getPolygons(f).forEach(polygon -> { if (GeometryEngine.contains(boundaries, polygon, spatialReference)) { log.debug("Adding zone {} to index", zoneIdName); if (accelerateGeometry) { operatorIntersects.accelerateGeometry(polygon, spatialReference, Geometry.GeometryAccelerationDegree.enumMild); } polygon.queryEnvelope2D(env); int index = indices.next(); quadTree.insert(index, env); zoneIds.add(index, new Entry(zoneId, polygon)); } else { log.debug("Not adding zone {} to index because it's out of provided boundaries", zoneIdName); } }); } catch (Exception ex) { unknownZones.add(zoneIdName); } }); if (unknownZones.size() != 0) { String allUnknownZones = String.join(", ", unknownZones); log.error( "Some of the zone ids were not recognized by the Java runtime and will be ignored. " + "The most probable reason for this is outdated Java runtime version. " + "The following zones were not recognized: " + allUnknownZones); } return new Index(quadTree, zoneIds); } @SuppressWarnings("SizeReplaceableByIsEmpty") static Index build(Stream features, int size, Set timeZones, boolean accelerateGeometry) { Envelope2D boundariesEnvelope = new Envelope2D(); Envelope boundaries = new Envelope(-180, -90, 180, +90);// (minLon, minLat, maxLon, maxLat); boundaries.queryEnvelope2D(boundariesEnvelope); QuadTree quadTree = new QuadTree(boundariesEnvelope, 8); Envelope2D env = new Envelope2D(); ArrayList zoneIds = new ArrayList<>(size); PrimitiveIterator.OfInt indices = IntStream.iterate(0, i -> i + 1).iterator(); List unknownZones = new ArrayList<>(); OperatorIntersects operatorIntersects = OperatorIntersects.local(); features.forEach(f -> { String zoneIdName = f.getProperties(0).getValueString(); try { ZoneId zoneId = ZoneId.of(zoneIdName); if (timeZones.contains (zoneId)) { getPolygons(f).forEach(polygon -> { log.debug("Adding zone {} to index", zoneIdName); if (accelerateGeometry) { operatorIntersects.accelerateGeometry(polygon, spatialReference, Geometry.GeometryAccelerationDegree.enumMild); } polygon.queryEnvelope2D(env); int index = indices.next(); quadTree.insert(index, env); zoneIds.add(index, new Entry(zoneId, polygon)); }); } else { log.debug("Not adding zone {} to index because it's out of provided boundaries", zoneIdName); } } catch (Exception ex) { unknownZones.add(zoneIdName); } }); if (unknownZones.size() != 0) { String allUnknownZones = String.join(", ", unknownZones); log.error( "Some of the zone ids were not recognized by the Java runtime and will be ignored. " + "The most probable reason for this is outdated Java runtime version. " + "The following zones were not recognized: " + allUnknownZones); } return new Index(quadTree, zoneIds); } } ================================================ FILE: core/src/main/java/net/iakovlev/timeshape/SameZoneSpan.java ================================================ package net.iakovlev.timeshape; import java.time.ZoneId; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * Represents contiguous span of points belonging to the same set of time zones */ public final class SameZoneSpan { public Set getZoneIds() { return new HashSet<>(zoneIds); } /** * Last index in the array of points (the polyline) which has the same ZoneId. * See {@link TimeZoneEngine#queryPolyline(double[])} for explanation. * @return */ public int getEndIndex() { return endIndex; } private final Set zoneIds; private final int endIndex; @Override public int hashCode() { return Objects.hash(zoneIds, endIndex); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof SameZoneSpan)) return false; SameZoneSpan other = (SameZoneSpan) obj; return other.endIndex == endIndex && other.zoneIds.equals(zoneIds); } @Override public String toString() { return String.format("%s: end index %d", zoneIds, endIndex); } SameZoneSpan(Set zoneIds, int endIndex) { this.zoneIds = new HashSet<>(zoneIds); this.endIndex = endIndex; } static SameZoneSpan fromIndexEntries(List entries, int index) { return new SameZoneSpan(entries.stream().map(e -> e.zoneId).collect(Collectors.toSet()), index); } } ================================================ FILE: core/src/main/java/net/iakovlev/timeshape/TimeZoneEngine.java ================================================ package net.iakovlev.timeshape; import com.esri.core.geometry.Envelope; import com.github.luben.zstd.ZstdInputStream; import net.iakovlev.timeshape.proto.Geojson; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.time.ZoneId; import java.util.*; import java.util.function.Consumer; import java.util.stream.Stream; import java.util.stream.StreamSupport; /** * Class {@link TimeZoneEngine} is used to lookup the instance of * {@link java.time.ZoneId} based on latitude and longitude. */ public final class TimeZoneEngine implements Serializable { private final Index index; private final static int NUMBER_OF_TIMEZONES = 449; // can't get number of entries from tar, need to set manually private final static String DATA_FILE_NAME = "/data.tar.zstd"; private final static double MIN_LAT = -90; private final static double MIN_LON = -180; private final static double MAX_LAT = 90; private final static double MAX_LON = 180; private final static Logger log = LoggerFactory.getLogger(TimeZoneEngine.class); private TimeZoneEngine(Index index) { this.index = index; } private static void validateCoordinates(double minLat, double minLon, double maxLat, double maxLon) { List errors = new ArrayList<>(); if (minLat < MIN_LAT || minLat > MAX_LAT) { errors.add(String.format(Locale.ROOT, "minimum latitude %f is out of range: must be -90 <= latitude <= 90;", minLat)); } if (maxLat < MIN_LAT || maxLat > MAX_LAT) { errors.add(String.format(Locale.ROOT, "maximum latitude %f is out of range: must be -90 <= latitude <= 90;", maxLat)); } if (minLon < MIN_LON || minLon > MAX_LON) { errors.add(String.format(Locale.ROOT, "minimum longitude %f is out of range: must be -180 <= longitude <= 180;", minLon)); } if (maxLon < MIN_LON || maxLon > MAX_LON) { errors.add(String.format(Locale.ROOT, "maximum longitude %f is out of range: must be -180 <= longitude <= 180;", maxLon)); } if (minLat > maxLat) { errors.add(String.format(Locale.ROOT, "maximum latitude %f is less than minimum latitude %f;", maxLat, minLat)); } if (minLon > maxLon) { errors.add(String.format(Locale.ROOT, "maximum longitude %f is less than minimum longitude %f;", maxLon, minLon)); } if (!errors.isEmpty()) { throw new IllegalArgumentException(String.join(" ", errors)); } } private static Spliterator makeSpliterator(TarArchiveInputStream f) { return new Spliterators.AbstractSpliterator(Long.MAX_VALUE, 0) { @Override public boolean tryAdvance(Consumer action) { try { TarArchiveEntry entry = f.getNextTarEntry(); if (entry != null) { action.accept(entry); return true; } else { return false; } } catch (IOException e) { throw new RuntimeException(e); } } }; } private static Stream spliterateInputStream(TarArchiveInputStream f) { Spliterator tarArchiveEntrySpliterator = makeSpliterator(f); return StreamSupport.stream(tarArchiveEntrySpliterator, false).map(n -> { try { if (n != null) { log.debug("Processing archive entry {}", n.getName()); byte[] e = new byte[(int) n.getSize()]; f.read(e); return Geojson.Feature.parseFrom(e); } else { throw new RuntimeException("Data entry is not found in file"); } } catch (NullPointerException | IOException ex) { throw new RuntimeException(ex); } }); } /** * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} * based on geo coordinates. * * @param latitude latitude part of query * @param longitude longitude part of query * @return List of all zones at given geo coordinate. Normally it's just * one zone, but for several places in the world there might be more. */ public List queryAll(double latitude, double longitude) { return index.query(latitude, longitude); } /** * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} * based on geo coordinates. * * @param latitude latitude part of query * @param longitude longitude part of query * @return {@code Optional#of(ZoneId)} if input corresponds * to some zone, or {@link Optional#empty()} otherwise. */ @SuppressWarnings("SizeReplaceableByIsEmpty") public Optional query(double latitude, double longitude) { final List result = index.query(latitude, longitude); return result.size() > 0 ? Optional.of(result.get(0)) : Optional.empty(); } /** * Queries the {@link TimeZoneEngine} for a {@link java.time.ZoneId} * based on sequence of geo coordinates * * @param points array of doubles representing the sequence of geo coordinates * Must have the following shape: {lat_1, lon_1, lat_2, lon_2, ..., lat_N, lon_N} * @return Sequence of {@link SameZoneSpan}, where {@link SameZoneSpan#getEndIndex()} represents the last index * in the {@param points} array, which belong to the value of {@link SameZoneSpan#getZoneIds()} * E.g. for {@param points} == {lat_1, lon_1, lat_2, lon_2, lat_3, lon_3}, that is, a polyline of * 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, * and point_3 belongs to Etc/Gmt+3, the result will be: * {SameZoneSpan(Etc/Gmt+1, 1), SameZoneSpan(Etc/Gmt+2, 3), SameZoneSpan(Etc/Gmt+3, 5)} */ public List queryPolyline(double[] points) { return index.queryPolyline(points); } /** * Returns all the time zones that can be looked up. * * @return all the time zones that can be looked up. */ public List getKnownZoneIds() { return index.getKnownZoneIds(); } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * * @param accelerateGeometry Increase query speed at the expense of memory utilization * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(boolean accelerateGeometry) { return initialize(MIN_LAT, MIN_LON, MAX_LAT, MAX_LON, accelerateGeometry); } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize() { return initialize(MIN_LAT, MIN_LON, MAX_LAT, MAX_LON, false); } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. *

* Example invocation: *

* {{{ * try (InputStream resourceAsStream = new FileInputStream("./core/target/resource_managed/main/data.tar.zstd"); * TarArchiveInputStream f = new TarArchiveInputStream(new ZstdCompressorInputStream(resourceAsStream))) { * return TimeZoneEngine.initialize(f); * } catch (NullPointerException | IOException e) { * throw new RuntimeException(e); * } * }}} * * @param f Input stream of timezone data tar archive * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(TarArchiveInputStream f) { return initialize(MIN_LAT, MIN_LON, MAX_LAT, MAX_LON, false, f); } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it from a given TarArchiveInputStream. * This is a blocking long running operation. The InputStream resource must be managed by the caller. *

* Example invocation: * {{{ * try (InputStream resourceAsStream = new FileInputStream("./core/target/resource_managed/main/data.tar.zstd"); * TarArchiveInputStream f = new TarArchiveInputStream(new ZstdCompressorInputStream(resourceAsStream))) { * return TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true, f); * } catch (NullPointerException | IOException e) { * throw new RuntimeException(e); * } * }}} * * @param minLat Minimum latitude of bounding box * @param minLon Minimum longitude of bounding box * @param maxLat Maximum latitude of bounding box * @param maxLon Maximum longitude of bounding box * @param accelerateGeometry Increase query speed at the expense of memory utilization * @param f Input stream of timezone data tar archive * * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(double minLat, double minLon, double maxLat, double maxLon, boolean accelerateGeometry, TarArchiveInputStream f) { log.info("Initializing with bounding box: {}, {}, {}, {}", minLat, minLon, maxLat, maxLon); validateCoordinates(minLat, minLon, maxLat, maxLon); Stream featureStream = spliterateInputStream (f); Envelope boundaries = new Envelope(minLon, minLat, maxLon, maxLat); return new TimeZoneEngine( Index.build( featureStream, NUMBER_OF_TIMEZONES, boundaries, accelerateGeometry)); } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * * @param timeZones List of ZoneIds to load. * @param accelerateGeometry Increase query speed at the expense of memory utilization * @param numberOfTimeZones How many timezones are in the tar archive. * @param f Input stream of timezone data tar archive * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(Set timeZones, boolean accelerateGeometry, int numberOfTimeZones, TarArchiveInputStream f) { log.info("Initializing with list of time zones"); Stream featureStream = spliterateInputStream (f); return new TimeZoneEngine( Index.build( featureStream, numberOfTimeZones, timeZones, accelerateGeometry)); } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * * @param minLat Minimum latitude of bounding box * @param minLon Minimum longitude of bounding box * @param maxLat Maximum latitude of bounding box * @param maxLon Maximum longitude of bounding box * @param accelerateGeometry Increase query speed at the expense of memory utilization * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(double minLat, double minLon, double maxLat, double maxLon, boolean accelerateGeometry) { try (InputStream resourceAsStream = TimeZoneEngine.class.getResourceAsStream(DATA_FILE_NAME)) { try (ZstdInputStream unzipStream = new ZstdInputStream(resourceAsStream)) { try (BufferedInputStream bufferedStream = new BufferedInputStream(unzipStream)) { try (TarArchiveInputStream shapeInputStream = new TarArchiveInputStream(bufferedStream)) { return initialize(minLat, minLon, maxLat, maxLon, accelerateGeometry, shapeInputStream); } } } } catch (NullPointerException | IOException e) { log.error("Unable to read resource file", e); throw new RuntimeException(e); } } /** * Creates a new instance of {@link TimeZoneEngine} and initializes it. * This is a blocking long running operation. * * @param timeZones List of ZoneIds to load. * @param accelerateGeometry Increase query speed at the expense of memory utilization * @return an initialized instance of {@link TimeZoneEngine} */ public static TimeZoneEngine initialize(Set timeZones, boolean accelerateGeometry) { try (InputStream resourceAsStream = TimeZoneEngine.class.getResourceAsStream("/data.tar.zstd")) { try (ZstdInputStream unzipStream = new ZstdInputStream(resourceAsStream)) { try (BufferedInputStream bufferedStream = new BufferedInputStream(unzipStream)) { try (TarArchiveInputStream shapeInputStream = new TarArchiveInputStream(bufferedStream)) { return initialize(timeZones, accelerateGeometry, NUMBER_OF_TIMEZONES, shapeInputStream); } } } } catch (NullPointerException | IOException e) { log.error("Unable to read resource file", e); throw new RuntimeException(e); } } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedTest.java ================================================ package net.iakovlev.timeshape; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.time.ZoneId; import java.util.List; import java.util.Optional; import static junit.framework.TestCase.assertEquals; @RunWith(JUnit4.class) public class TimeZoneEngineBoundedTest { private static final TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true); @Test public void testSomeZones() { assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); } @Test public void testWorld() { List knownZoneIds = engine.getKnownZoneIds(); assertEquals(39, knownZoneIds.size()); } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineBoundedZoneTest.java ================================================ package net.iakovlev.timeshape; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.time.ZoneId; import java.util.HashSet; import java.util.Optional; import java.util.Set; import static junit.framework.TestCase.assertEquals; @RunWith(JUnit4.class) public class TimeZoneEngineBoundedZoneTest { @Test public void testSomeZones() { Set timeZones = new HashSet<> (); timeZones.add (ZoneId.of("Europe/Berlin")); TimeZoneEngine engine = TimeZoneEngine.initialize (timeZones, true); assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); timeZones.clear (); timeZones.add(ZoneId.of("America/Detroit")); engine = TimeZoneEngine.initialize (timeZones, true); assertEquals(Optional.empty (), engine.query(52.52, 13.40)); timeZones.add(ZoneId.of("Europe/Berlin")); engine = TimeZoneEngine.initialize (timeZones, true); assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineCoordinatesValidationTest.java ================================================ package net.iakovlev.timeshape; import net.iakovlev.timeshape.TimeZoneEngine; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import static junit.framework.TestCase.assertEquals; @RunWith(JUnit4.class) public class TimeZoneEngineCoordinatesValidationTest { @Test public void testMinimumLatitudeOutOfBounds() { try { TimeZoneEngine.initialize(-100, 0, 0, 0, false); } catch (IllegalArgumentException e) { assertEquals(e.getMessage(), "minimum latitude -100.000000 is out of range: must be -90 <= latitude <= 90;"); } } @Test public void testMinimumLongitudeOutOfBounds() { try { TimeZoneEngine.initialize(0, -190, 0, 0, false); } catch (IllegalArgumentException e) { assertEquals(e.getMessage(), "minimum longitude -190.000000 is out of range: must be -180 <= longitude <= 180;"); } } @Test public void testMaximumLatitudeOutOfBounds() { try { TimeZoneEngine.initialize(0, 0, 100, 0, false); } catch (IllegalArgumentException e) { assertEquals(e.getMessage(), "maximum latitude 100.000000 is out of range: must be -90 <= latitude <= 90;"); } } @Test public void testMaximumLongitudeOutOfBounds() { try { TimeZoneEngine.initialize(0, 0, 0, 190, false); } catch (IllegalArgumentException e) { assertEquals(e.getMessage(), "maximum longitude 190.000000 is out of range: must be -180 <= longitude <= 180;"); } } @Test public void testInconsistentLatitudes() { try { TimeZoneEngine.initialize(0, 0, -1, 0, false); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "maximum latitude -1.000000 is less than minimum latitude 0.000000;"); } } @Test public void testInconsistentLongitudes() { try { TimeZoneEngine.initialize(0, 0, 0, -1, false); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), "maximum longitude -1.000000 is less than minimum longitude 0.000000;"); } } @Test public void testMultipleErrors() { try { TimeZoneEngine.initialize(0, 0, -1, -1, false); } catch (IllegalArgumentException ex) { 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;"); } } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineOutfileBoundedTest.java ================================================ package net.iakovlev.timeshape; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.time.ZoneId; import java.util.List; import java.util.Optional; import static junit.framework.TestCase.assertEquals; @RunWith(JUnit4.class) public class TimeZoneEngineOutfileBoundedTest { private static TimeZoneEngine engine = null; @BeforeClass public static void initEngine() { try (InputStream resourceAsStream = new FileInputStream("target/classes/data.tar.zstd"); TarArchiveInputStream f = new TarArchiveInputStream(new ZstdCompressorInputStream(resourceAsStream))) { engine = TimeZoneEngine.initialize(f); } catch (NullPointerException | IOException e) { throw new RuntimeException(e); } } @Test public void testSomeZones() { assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEnginePolylineTest.java ================================================ package net.iakovlev.timeshape; import net.iakovlev.timeshape.SameZoneSpan; import net.iakovlev.timeshape.TimeZoneEngine; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.time.ZoneId; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import static junit.framework.TestCase.assertEquals; @RunWith(JUnit4.class) public class TimeZoneEnginePolylineTest { private static TimeZoneEngine engine = TimeZoneEngine.initialize(true); @Test public void test2points() { assertEquals(engine.queryPolyline(new double[]{52.52, 13.40, 56.52, 16.40}), Arrays.asList( new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 1), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 3))); } @Test public void test3points() { assertEquals(engine.queryPolyline(new double[]{52.52, 13.40, 54.52, 15.40, 56.52, 16.40}), Arrays.asList( new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 1), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-1"))), 3), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 5))); } @Test public void testSpanWithMultiPoints() { 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}), Arrays.asList( new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 3), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-1"))), 5), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 9))); } @Test public void testNonMatchingPoints() { assertEquals( Arrays.asList( new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-12"))), 5), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Berlin"))), 9), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-1"))), 11), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Stockholm"))), 13)), 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})); } @Test public void testNonMatchingLastPoints() { assertEquals( Arrays.asList( new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Vilnius"))), 3), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Europe/Minsk"))), 5), new SameZoneSpan(new HashSet<>(Collections.singletonList(ZoneId.of("Etc/GMT-12"))), 9) ), engine.queryPolyline(new double[]{54.89, 23.91, 55.13, 25.57, 54.29, 28.32, 80, 180, 80, 180})); } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineSerializationTest.java ================================================ package net.iakovlev.timeshape; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.IOException; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.File; import static junit.framework.TestCase.assertEquals; @RunWith(JUnit4.class) public class TimeZoneEngineSerializationTest { private final static TimeZoneEngine engine = TimeZoneEngine.initialize(47.0599, 4.8237, 55.3300, 15.2486, true); @Test public void testSerialzation() { File f = new File ("./Engine.cache"); try { serializeTimeZoneEngine (f, engine); TimeZoneEngine engine2 = deserializeTimeZoneEngine (f); assertEquals(engine.query(52.52, 13.40), engine2.query(52.52, 13.40)); f.delete (); } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(e); } } /** * Serializes an instance of {@link TimeZoneEngine} to a file * This is a blocking long running operation. * * @param f Destination File. * @param eng Instance of TimeZoneEngine to serialize * @throws java.io.IOException */ public void serializeTimeZoneEngine (File f, TimeZoneEngine eng) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream (f, false); try (ObjectOutputStream objectOutputStream = new ObjectOutputStream (new BufferedOutputStream (fileOutputStream))) { objectOutputStream.writeObject (eng); objectOutputStream.flush (); } } /** * Creates a new instance of {@link TimeZoneEngine} from previously serialized data. * This is a blocking long running operation. * * @param f File to de-serialize from * @return an initialized instance of {@link TimeZoneEngine} * @throws java.io.IOException * @throws java.lang.ClassNotFoundException */ public static TimeZoneEngine deserializeTimeZoneEngine (File f) throws IOException, ClassNotFoundException { FileInputStream fileInputStream = new FileInputStream(f); try (ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { return (TimeZoneEngine) objectInputStream.readObject (); } } } ================================================ FILE: core/src/test/java/net/iakovlev/timeshape/TimeZoneEngineTest.java ================================================ package net.iakovlev.timeshape; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.IOException; import java.time.ZoneId; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; @RunWith(JUnit4.class) public class TimeZoneEngineTest { private static TimeZoneEngine engine = TimeZoneEngine.initialize(); @Test public void testSomeZones() { assertEquals(Optional.of(ZoneId.of("Europe/Berlin")), engine.query(52.52, 13.40)); assertEquals(Optional.of(ZoneId.of("Asia/Tomsk")), engine.query(56.49771, 84.97437)); assertEquals(Optional.of(ZoneId.of("America/Santiago")), engine.query(-33.459229, -70.645348)); assertEquals(Optional.of(ZoneId.of("Asia/Krasnoyarsk")), engine.query(56.01839, 92.86717)); assertEquals(Optional.of(ZoneId.of("Africa/Abidjan")), engine.query(5.345317, -4.024429)); assertEquals(Optional.of(ZoneId.of("America/New_York")), engine.query(40.785091, -73.968285)); assertEquals(Optional.of(ZoneId.of("Australia/Sydney")), engine.query(-33.865143, 151.215256)); assertEquals(Optional.of(ZoneId.of("Etc/GMT+1")), engine.query(38.00, -15.2814)); assertEquals(Optional.of(ZoneId.of("Asia/Shanghai")), engine.query(39.601, 79.201)); assertEquals(Optional.of(ZoneId.of("Asia/Shanghai")), engine.query(27.45, 89.05)); assertEquals(List.of(ZoneId.of("America/Ciudad_Juarez")), engine.queryAll(31.752, -106.457)); assertEquals(Optional.of(ZoneId.of("Europe/Bucharest")), engine.query(46.16799, 20.71524)); } @Test public void testBoundariesAndMultiPolygons() { assertEquals(Optional.of(ZoneId.of("Europe/Amsterdam")), engine.query(51.4457, 4.9248)); assertEquals(Optional.of(ZoneId.of("Europe/Brussels")), engine.query(51.4457, 4.9250)); assertEquals(Optional.of(ZoneId.of("Europe/Brussels")), engine.query(51.4437,4.9186)); assertEquals(Optional.of(ZoneId.of("Europe/Amsterdam")), engine.query(51.4438,4.9181)); } @Test public void testMultipleTimeZonesInResponse() throws IOException { List expected1 = new java.util.ArrayList<>(); expected1.add(ZoneId.of("Africa/Juba")); expected1.add(ZoneId.of("Africa/Khartoum")); assertEquals(expected1, engine.queryAll(9.75, 28.45)); List expected2 = new java.util.ArrayList<>(); expected2.add(ZoneId.of("America/Argentina/Rio_Gallegos")); expected2.add(ZoneId.of("America/Punta_Arenas")); assertEquals(expected2, engine.queryAll(-49.5, -73.3)); List expected3 = new java.util.ArrayList<>(); expected3.add(ZoneId.of("America/La_Paz")); expected3.add(ZoneId.of("America/Porto_Velho")); assertEquals(expected3, engine.queryAll(-10.8, -65.35)); List expected4 = new java.util.ArrayList<>(); expected4.add(ZoneId.of("America/Moncton")); expected4.add(ZoneId.of("America/New_York")); assertEquals(expected4, engine.queryAll(44.5, -67.15)); List expected5 = new java.util.ArrayList<>(); expected5.add(ZoneId.of("Asia/Hebron")); expected5.add(ZoneId.of("Asia/Jerusalem")); assertEquals(expected5, engine.queryAll(31.95, 35.2)); List expected6 = new java.util.ArrayList<>(); expected6.add(ZoneId.of("Asia/Shanghai")); expected6.add(ZoneId.of("Asia/Thimphu")); assertEquals(expected6, engine.queryAll(27.45, 89.05)); List expected7 = new java.util.ArrayList<>(); expected7.add(ZoneId.of("Europe/Bucharest")); expected7.add(ZoneId.of("Europe/Budapest")); assertEquals(expected7, engine.queryAll(46.16799, 20.71524)); List expected = new java.util.ArrayList<>(); expected.add(ZoneId.of("Asia/Shanghai")); expected.add(ZoneId.of("Asia/Urumqi")); assertEquals(expected, engine.queryAll(39.601, 79.201)); } @Test public void testWorld() { Set engineZoneIds = engine.getKnownZoneIds().stream().map(ZoneId::getId).collect(Collectors.toSet()); assertTrue(java.time.ZoneId.getAvailableZoneIds().containsAll(engineZoneIds)); } } ================================================ FILE: doc/Architecture.md ================================================ # Architecture This document describes how the Timeshape is designed, which technologies it uses, and how to build it yourself. This might be helpful if you want to contribute to it, or just to understand how it works. ## Overview Timeshape is built around the idea of taking the source data with time zones in [GeoJSON](http://geojson.org/) format, convert it into something more efficient to store and read than GeoJSON, and package converted data together with the code that can read and query it into a single artifact (JAR file). Efficiency is the key word here, because the source data is quite big, and using it as is would impose too high memory and artifact size requirements on the users of Timeshape. Timeshape currently uses compressed [protocol buffers](https://developers.google.com/protocol-buffers/) (a.k.a. protobuf) as the target data format. The protobuf data is compressed using ZStandard method, which allows to reach relatively small artifact size: 19 MB total JAR size vs 57 MB for the source data only (GeoJSON compressed with zip). The biggest win in terms of size is, however, not due to efficiency of protobuf vs GeoJSON, but due to the fact that `float` is used instead of `double` to store geo coordinates in protobuf. This means, only 4+4=8 bytes are required for each point (latitude + longitude), instead of 8+8=16 bytes for `double`. Precision of `float` is good enough for the source data. At runtime, the code reads the packaged data and build a spatial index for querying. It uses [quad tree](https://en.wikipedia.org/wiki/Quadtree) for indexing, provided by the [Esri geometry API](https://github.com/Esri/geometry-api-java) Java library. ## Build structure Timeshape uses [sbt](https://scala-sbt.org) as build system. The sbt build definition has 5 projects: * geojson-protobuf * core * builder * testApp * benchmarks Below you'll find some information about those individual projects. ### geojson-protobuf This project contains the protobuf definitions corresponding to GeoJSON format. Those definitions are in file `geojson-proto/src/main/protobuf/geojson.proto`. Java code to read and write such protobuf messages is generated during compile time by [sbt-protoc](https://github.com/thesamet/sbt-protoc) sbt plugin. Other projects (`core` and `builder`), which must read or write the protobuf, use those generated Java classes, and therefore depend on `geojson-protobuf` in classpath sense. ### core This project contains the logic to read the data into a quad tree and provide API for querying it. It's the main project with which the library users interact, and provides the main published artifact. It uses sbt feature called `resource generator` to create the protobuf file containing the time zone data. The code that actually generates the protobuf data file is in the [builder](#builder) project. The resource generator is run by sbt automatically when necessary. ### builder This project is responsible for downloading the source data from Github and converting it from GeoJSON to protobuf format. It's usually called from the resource generator of `core` project, but can be run independently (it's a standard Java application, after all). ### testApp It's a playground, more or less. It's used to experiment with the main Timeshape artifact produced by the `core` project, particularly for the purpose of estimating its memory usage (see [Memory usage](#memory-usage)), and maybe something else in the future. ### benchmarks This project contains JMH benchmarks. It was originally created to analyze performance of different ways to query the polyline, and to compare performance of optimized polyline query method with querying each point in polyline individually. ## Building If you want to build and run the Timeshape locally, follow these steps: 1. Install sbt and JDK. It's proven to work on JDK 8, but newer versions might work too. Use latest versions, because changes of time zones happen regularly in real world, and only the latest JDK build might reflect them. 2. Go to the directory where the source code is checked out and run `sbt` command there. 3. When sbt finishes to load the build definition and you see sbt console, you have several options: * run `core/test` to execute the tests, they should pass. * run `testApp/run` to run the test app. It will query the data for one time zone and print the memory usage. * run `core/publishLocal` if you've made local modifications and want to use the modified version in your program. It will publish to the “local” [Ivy repository](https://www.scala-sbt.org/1.x/docs/Publishing.html#Publishing+locally). By default, this is at `$HOME/.ivy2/local/`. * run `core/publishM2` Similar to publishLocal, publishM2 task will publish the user’s Maven local repository. This is at the location specified by `$HOME/.m2/settings.xml` or at `$HOME/.m2/repository/` by default. Another sbt build would require `Resolver.mavenLocal` to resolve out of it. Version must be set to `snapshot` for local publication to work best. ### local testing If you want to use a modified version of timeshape in your program or perform some local testing include the local repository in your build definition. With sbt/scala: ```scala resolvers += Resolver.mavenLocal ``` With gradle/java: ```groovy repositories { mavenCentral() mavenLocal() } dependencies { compile group: 'net.iakovlev', name: 'timeshape', version: '2023b.18-SNAPSHOT' } ``` ## Memory usage The `testApp` project provides memory usage estimate by using [JOL](http://openjdk.java.net/projects/code-tools/jol/). The current version's estimated footprint is roughly 128 MB of memory when the data for the whole world is loaded. It is possible to further limit the memory usage by reducing the amount of time zones loaded. This is implemented by a call to `TimeZoneEngine.initialize(double minlat, double minlon, double maxlat, double maxlon)`. ================================================ FILE: geojson-proto/pom.xml ================================================ 4.0.0 net.iakovlev timeshape-parent 2025b.28 4.31.1 geojson-proto 1.1.6 jar GeoJSON Proto Protocol Buffers definitions for GeoJSON com.google.protobuf protobuf-java ${protobuf.version} org.sonatype.central central-publishing-maven-plugin true io.github.ascopes protobuf-maven-plugin 3.10.2 ${protobuf.version} generate kr.motd.maven os-maven-plugin 1.7.1 initialize detect org.apache.maven.plugins maven-jar-plugin 3.3.0 net.iakovlev.geojson.proto ================================================ FILE: geojson-proto/src/main/protobuf/geojson.proto ================================================ syntax = "proto2"; package net.iakovlev.timeshape.proto; message FeatureCollection { repeated Feature features = 1; } message Feature { required Geometry geometry = 1; repeated Property properties = 2; } message Point { required Position coordinates = 1; } message MultiPoint { repeated Position coordinates = 1; } message LineString { repeated Position coordinates = 1; } message MultiLineString { repeated LineString coordinates = 1; } message Polygon { repeated LineString coordinates = 1; } message MultiPolygon { repeated Polygon coordinates = 1; } message GeometryCollection { repeated Geometry geometries = 1; } message Position { required float lon = 1; required float lat = 2; } message Geometry { oneof type { Point point = 1; MultiPoint multiPoint = 2; LineString lineString = 3; MultiLineString multiLineString = 4; Polygon polygon = 5; MultiPolygon multiPolygon = 6; GeometryCollection geometryCollection = 7; } } message Property { required string key = 1; oneof value { string valueString = 2; double valueNumber = 3; } } ================================================ FILE: pom.xml ================================================ 4.0.0 net.iakovlev timeshape-parent 2025b.28 pom Timeshape Parent A Java library for mapping between coordinates and timezones https://github.com/RomanIakovlev/timeshape MIT License http://opensource.org/licenses/mit-license.php scm:git:https://github.com/RomanIakovlev/timeshape.git scm:git:git@github.com:RomanIakovlev/timeshape.git https://github.com/RomanIakovlev/timeshape Roman Iakovlev Roman Iakovlev http://github.com/RomanIakovlev geojson-proto builder core test-app benchmarks 8 8 UTF-8 2025b 27 2.18.6 1.26.1 1.5.5-11 com.fasterxml.jackson.core jackson-core ${jackson.version} org.apache.commons commons-compress ${commons-compress.version} com.github.luben zstd-jni ${zstd-jni.version} net.iakovlev geojson-proto 1.1.6 net.iakovlev timeshape ${project.version} org.apache.maven.plugins maven-compiler-plugin 3.11.0 8 8 org.apache.maven.plugins maven-javadoc-plugin 3.5.0 none org.apache.maven.plugins maven-source-plugin 3.3.0 org.apache.maven.plugins maven-gpg-plugin 3.1.0 org.sonatype.central central-publishing-maven-plugin 0.8.0 true central true published true timeshape-benchmarks timeshape-testapp timeshape-builder release org.apache.maven.plugins maven-deploy-plugin true org.apache.maven.plugins maven-source-plugin attach-sources jar-no-fork org.apache.maven.plugins maven-javadoc-plugin attach-javadocs jar org.apache.maven.plugins maven-gpg-plugin sign-artifacts verify sign --pinentry-mode loopback ================================================ FILE: test-app/pom.xml ================================================ 4.0.0 net.iakovlev timeshape-parent 2025b.28 timeshape-testapp jar Timeshape Test App Test application for Timeshape true net.iakovlev timeshape org.openjdk.jol jol-core 0.9 ch.qos.logback logback-classic 1.2.13 org.apache.maven.plugins maven-jar-plugin 3.3.0 net.iakovlev.timeshape.testapp.Main ================================================ FILE: test-app/src/main/java/net/iakovlev/timeshape/testapp/Main.java ================================================ package net.iakovlev.timeshape.testapp; import net.iakovlev.timeshape.TimeZoneEngine; import org.openjdk.jol.info.GraphLayout; import org.openjdk.jol.vm.VM; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.IntStream; import static java.lang.System.out; public class Main { static public void main(String[] args) { long start = System.currentTimeMillis(); TimeZoneEngine engine = TimeZoneEngine.initialize(true); long total = System.currentTimeMillis() - start; out.println("initialization took " + total + " milliseconds"); out.println(engine.query(52.52, 13.40)); out.println(VM.current().details()); out.println(GraphLayout.parseInstance(engine).toFootprint()); double lonMin = -180.0; double lonMax = 180.0; double latMin = -90.0; double latMax = 90.0; String prev = ""; for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { double latitude = (latMax - latMin) / 100 * i; double longitude = (lonMax - lonMin) / 100 * i; final List result = engine.queryAll(latitude, longitude); if (result.size() > 1) { String report = "Found multiple zones (" + result + ") for " + latitude + ", " + longitude; if (!report.equals(prev)) { out.println("Found multiple zones (" + result + ") for " + latitude + ", " + longitude); } prev = report; } } } } } ================================================ FILE: test-app/src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n