[
  {
    "path": ".gitignore",
    "content": ".classpath\n.settings\ntarget\n/bin/\n"
  },
  {
    "path": ".gitlab-ci.yml",
    "content": "build:\n  tags:\n    - maven\n  script:\n    - mvn clean compile\n\ndeploy:\n  tags:\n    - maven\n  script:\n    - mvn -B install javadoc:javadoc\n"
  },
  {
    "path": ".project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>commons-suncalc</name>\n\t<comment></comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.m2e.core.maven2Builder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n\t\t<nature>org.eclipse.m2e.core.maven2Nature</nature>\n\t</natures>\n</projectDescription>\n"
  },
  {
    "path": "LICENSE-APL.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# commons-suncalc ![build status](https://shredzone.org/badge/commons-suncalc.svg) ![maven central](https://shredzone.org/maven-central/org.shredzone.commons/commons-suncalc/badge.svg)\n\nA Java library for calculation of sun and moon positions and phases.\n\n## Features\n\n* Lightweight, only requires Java 8 or higher, no other dependencies\n* Android compatible, requires API level 26 (Android 8.0 \"Oreo\") or higher. For older Android versions, use [commons-suncalc v2](https://codeberg.org/shred/commons-suncalc/tree/v2), which is similar to this version, but does not use the Java Date/Time API.\n* Available at [Maven Central](http://search.maven.org/#search|ga|1|a%3A%22commons-suncalc%22)\n* Extensive unit tests\n\n## Accuracy\n\nAstronomical calculations are far more complex than throwing a few numbers into an obscure formula and then getting a fully accurate result. There is always a tradeoff between accuracy and computing time.\n\nThis library has its focus on getting _acceptable_ results at low cost, so it can also run on mobile devices, or devices with a low computing power. The results have an accuracy of about a minute, which should be good enough for common applications (like sunrise/sunset timers), but is probably not sufficient for astronomical purposes.\n\nIf you are looking for the highest possible accuracy, you are looking for a different library.\n\n## Quick Start\n\nThis library consists of a few models, all of them are invoked the same way:\n\n```java\nZonedDateTime dateTime = // date, time and timezone of calculation\ndouble lat, lng = // geolocation\nSunTimes times = SunTimes.compute()\n        .on(dateTime)   // set a date\n        .at(lat, lng)   // set a location\n        .execute();     // get the results\nSystem.out.println(\"Sunrise: \" + times.getRise());\nSystem.out.println(\"Sunset: \" + times.getSet());\n```\n\nRead the [online documentation](https://shredzone.org/maven/commons-suncalc/) for examples and API details.\n\nSee the [migration guide](https://shredzone.org/maven/commons-suncalc/migration.html) for how to migrate from a previous version.\n\n## References\n\nThis library bases on:\n\n* \"Astronomy on the Personal Computer\", 4th edition, by Oliver Montenbruck and Thomas Pfleger\n* \"Astronomical Algorithms\" by Jean Meeus\n\n## Contribute\n\n* Fork the [Source code at Codeberg](https://codeberg.org/shred/commons-suncalc). Feel free to send pull requests.\n* Found a bug? Please [file a bug report](https://codeberg.org/shred/commons-suncalc/issues).\n\n## License\n\n_commons-suncalc_ is open source software. The source code is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>org.shredzone.commons</groupId>\n    <artifactId>commons-suncalc</artifactId>\n    <version>3.12-SNAPSHOT</version>\n    <name>Commons: Suncalc</name>\n    <description>Compute sun and moon phases</description>\n    <url>http://commons.shredzone.org</url>\n    <inceptionYear>2016</inceptionYear>\n    <licenses>\n        <license>\n            <name>Apache License Version 2.0</name>\n            <url>LICENSE-APL.txt</url>\n        </license>\n    </licenses>\n    <scm>\n        <url>https://codeberg.org/shred/commons-suncalc/</url>\n        <connection>scm:git:git@codeberg.org:shred/commons-suncalc.git</connection>\n        <developerConnection>scm:git:git@codeberg.org:shred/commons-suncalc.git</developerConnection>\n      <tag>HEAD</tag>\n  </scm>\n    <issueManagement>\n        <system>Codeberg Issues</system>\n        <url>https://codeberg.org/shred/commons-suncalc/issues</url>\n    </issueManagement>\n    <developers>\n        <developer>\n            <id>shred</id>\n            <name>Richard Körber</name>\n        </developer>\n    </developers>\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n    </properties>\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.11.0</version>\n                <configuration>\n                    <release>8</release>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>default-compile</id>\n                        <configuration>\n                            <release>9</release>\n                        </configuration>\n                    </execution>\n                    <execution>\n                        <id>base-compile</id>\n                        <goals>\n                            <goal>compile</goal>\n                        </goals>\n                        <configuration>\n                            <excludes>\n                                <exclude>module-info.java</exclude>\n                            </excludes>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-jar-plugin</artifactId>\n                <version>3.3.0</version>\n                <configuration>\n                    <excludes>\n                        <exclude>**/.gitignore</exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>com.github.spotbugs</groupId>\n                <artifactId>spotbugs-maven-plugin</artifactId>\n                <version>4.7.3.5</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>3.2.0</version>\n                <configuration>\n                    <doclint>syntax,reference</doclint>\n                    <linksource>true</linksource>\n                    <locale>en</locale>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-release-plugin</artifactId>\n                <version>2.5.3</version>\n                <configuration>\n                    <autoVersionSubmodules>true</autoVersionSubmodules>\n                    <tagNameFormat>v@{project.version}</tagNameFormat>\n                    <pushChanges>false</pushChanges>\n                    <localCheckout>true</localCheckout>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>3.2.1</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.shredzone.maven</groupId>\n                <artifactId>mkdocs-maven-plugin</artifactId>\n                <version>1.1</version>\n                <configuration>\n                    <outputDirectory>${project.build.directory}/site</outputDirectory>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n    <dependencies>\n        <dependency>\n            <groupId>com.github.spotbugs</groupId>\n            <artifactId>spotbugs-annotations</artifactId>\n            <version>4.8.5</version>\n            <optional>true</optional>\n            <exclusions>\n                <exclusion>\n                    <groupId>com.google.code.findbugs</groupId>\n                    <artifactId>jsr305</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>4.13.2</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.assertj</groupId>\n            <artifactId>assertj-core</artifactId>\n            <version>3.26.0</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <!-- Workaround: Java 9's javadoc search is broken if no module is defined -->\n    <profiles>\n        <profile>\n            <id>java-9</id>\n            <activation>\n                <jdk>[9,10]</jdk>\n            </activation>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <configuration>\n                            <additionalJOption combine.self=\"override\">--no-module-directories</additionalJOption>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n            <reporting>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <configuration>\n                            <additionalJOption combine.self=\"override\">--no-module-directories</additionalJOption>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </reporting>\n        </profile>\n        <profile>\n            <id>java-11</id>\n            <activation>\n                <jdk>[11,)</jdk>\n            </activation>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <configuration>\n                            <source>8</source>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n            <reporting>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <configuration>\n                            <source>8</source>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </reporting>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "src/doc/docs/examples.md",
    "content": "# Examples\n\nIn this chapter, you will find code examples that demonstrate the use and the possibilities of the API. You can also find (and run) these examples in the [ExamplesTest](https://codeberg.org/shred/commons-suncalc/blob/master/src/test/java/org/shredzone/commons/suncalc/ExamplesTest.java) unit test.\n\nI know this is a long chapter. It is because _suncalc_ offers a lot of functionality. I still recommend to read it, or at least skim it, to get an idea of what is possible or best practice.\n\n## Time Zone\n\nAll calculations use your own system's local time and time zone, unless you give other parameters. This can give surprising and confusing results.\n\nLet's assume we're living in Paris, and we want to compute our sunrise and sunset time for May 1st, 2020.\n\n```java\nSunTimes paris = SunTimes.compute()\n        .on(2020, 5, 1)             // May 1st, 2020, starting midnight\n        .latitude(48, 51, 24.0)     // Latitude of Paris: 48°51'24\" N\n        .longitude(2, 21, 6.0)      // Longitude:          2°21'06\" E\n        .execute();\nSystem.out.println(\"Sunrise in Paris: \" + paris.getRise());\nSystem.out.println(\"Sunset in Paris:  \" + paris.getSet());\n```\n\nThe result is not very surprising:\n\n```text\nSunrise in Paris: 2020-05-01T06:29:47+02:00[Europe/Paris]\nSunset in Paris:  2020-05-01T21:06:45+02:00[Europe/Paris]\n```\n\nNow we want to compute the sunrise and sunset times of New York.\n\n```java\nSunTimes newYork = SunTimes.compute()\n        .on(2020, 5, 1)             // May 1st, 2020, starting midnight\n        .at(40.712778, -74.005833)  // Coordinates of New York\n        .execute();\nSystem.out.println(\"Sunrise in New York: \" + newYork.getRise());\nSystem.out.println(\"Sunset in New York:  \" + newYork.getSet());\n```\n\nThe result is:\n\n```text\nSunrise in New York: 2020-05-01T11:54:05+02:00[Europe/Paris]\nSunset in New York:  2020-05-01T01:51:51+02:00[Europe/Paris]\n```\n\nHuh? The sun rises at noon and sets past midnight? The sun also sets before it is rising that day?\n\nThe reason is that we're still using the Paris timezone. On May 1st, midnight **Paris time**, the sun is still up in New York. It sets in New York when it's 1:52 in Paris, and raises again when it's 11:54 in Paris.\n\nWe can pass a `timezone()` parameter to tell _suncalc_ that we actually want to use a different timezone.\n\n```java\nSunTimes newYorkTz = SunTimes.compute()\n        .on(2020, 5, 1)             // May 1st, 2020, starting midnight\n        .timezone(\"America/New_York\") // ...New York timezone\n        .at(40.712778, -74.005833)  // Coordinates of New York\n        .execute();\nSystem.out.println(\"Sunrise in New York: \" + newYorkTz.getRise());\nSystem.out.println(\"Sunset in New York:  \" + newYorkTz.getSet());\n```\n\nNow, we finally see the actual sunrise and sunset time in New York:\n\n```text\nSunrise in New York: 2020-05-01T05:54:05-04:00[America/New_York]\nSunset in New York:  2020-05-01T19:52:53-04:00[America/New_York]\n```\n\n## Time Window\n\n[Alert, Nunavut, Canada](https://en.wikipedia.org/wiki/Alert,_Nunavut) is the northernmost place in the world with a permanent population. Let's find out when the sun rises and sets there on March 15th, 2020:\n\n```java\nfinal double[] ALERT_CANADA = new double[] { 82.5, -62.316667 };\nfinal ZoneId ALERT_TZ = ZoneId.of(\"Canada/Eastern\");\n\nSunTimes march = SunTimes.compute()\n        .on(2020, 3, 15)            // March 15th, 2020, starting midnight\n        .at(ALERT_CANADA)           // Coordinates are stored in an array\n        .timezone(ALERT_TZ)\n        .execute();\nSystem.out.println(\"Sunrise: \" + march.getRise());\nSystem.out.println(\"Sunset:  \" + march.getSet());\n```\n\nThe result is looking fine so far:\n\n```text\nSunrise: 2020-03-15T06:49:03-04:00[Canada/Eastern]\nSunset:  2020-03-15T17:52:53-04:00[Canada/Eastern]\n```\n\nWhat about June 15th?\n\n```java\nSunTimes june = SunTimes.compute()\n        .on(2020, 6, 15)            // June 15th, 2020, starting midnight\n        .at(ALERT_CANADA)\n        .timezone(ALERT_TZ)\n        .execute();\nSystem.out.println(\"Sunrise: \" + june.getRise());\nSystem.out.println(\"Sunset:  \" + june.getSet());\n```\n\nThe result:\n\n```text\nSunrise: 2020-09-05T00:24:03-04:00[Canada/Eastern]\nSunset:  2020-09-04T23:55:46-04:00[Canada/Eastern]\n```\n\nThe sun will set on September 4th, and will rise again about 30 minutes later. This is technically correct, because Alert is above the Arctic Circle, where the sun never sets all summer.\n\nSo the sun will set about three months later, that's interesting! When did it raise for the last time before Northern summer? We can use `.reverse()` for that. It changes the search direction, so the given date is the end date of our search window.\n\n```java\nSunTimes juneReverse = SunTimes.compute()\n        .on(2020, 6, 15)            // June 15th, 2020, starting midnight\n        .at(ALERT_CANADA)\n        .timezone(ALERT_TZ)\n        .reverse()                  // reverse the search direction!\n        .execute();\nSystem.out.println(\"Sunrise: \" + juneReverse.getRise());\nSystem.out.println(\"Sunset:  \" + juneReverse.getSet());\n```\n\nThe result:\n\n```text\nSunrise: 2020-04-06T00:35:04-04:00[Canada/Eastern]\nSunset:  2020-04-05T23:44:05-04:00[Canada/Eastern]\n```\n\nNow we know that the sun was rising for the last time on April 6th, and will set again on September 4th. However, we initially wanted to get a result for June 15th, so we limit the window to 24 hours:\n\n```java\nSunTimes june15OnlyCycle = SunTimes.compute()\n        .on(2020, 6, 15)            // June 15th, 2020, starting midnight\n        .at(ALERT_CANADA)\n        .timezone(ALERT_TZ)\n        .limit(Duration.ofHours(24))\n        .execute();\nSystem.out.println(\"Sunset:  \" + june15OnlyCycle.getSet());\nSystem.out.println(\"Sunrise: \" + june15OnlyCycle.getRise());\n```\n\nInstead of `limit(Duration.ofHours(24))`, we could also use `oneDay()`.\n\nNow we get a different result. There is no sunrise or sunset on June 15th:\n\n```text\nSunrise: null\nSunset:  null\n```\n\nBut is the sun up or down all that day?\n\n```java\nSystem.out.println(\"Sun is up all day:   \" + june15OnlyCycle.isAlwaysUp());\nSystem.out.println(\"Sun is down all day: \" + june15OnlyCycle.isAlwaysDown());\n```\n\nThe result confirms that the sun is up all day:\n\n```text\nSun is up all day:   true\nSun is down all day: false\n```\n\n## Parameter Reuse\n\nAs soon as `execute()` is invoked, _suncalc_ performs the calculations according to the given parameters, and creates a result object which is immutable. The parameters can be reused after that:\n\n```java\nfinal double[] COLOGNE = new double[] { 50.938056, 6.956944 };\n\nMoonTimes.Parameters parameters = MoonTimes.compute()\n        .at(COLOGNE)\n        .midnight();\n\nMoonTimes today = parameters.execute();\nSystem.out.println(\"Today, the moon rises in Cologne at \" + today.getRise());\n\nparameters.tomorrow();\nMoonTimes tomorrow = parameters.execute();\nSystem.out.println(\"Tomorrow, the moon will rise in Cologne at \" + tomorrow.getRise());\nSystem.out.println(\"But today, the moon still rises at \" + today.getRise());\n```\n\nThe result is (at the time of writing):\n\n```text\nToday, the moon rises in Cologne at 2020-05-24T06:40:45+02:00[Europe/Berlin]\nTomorrow, the moon will rise in Cologne at 2020-05-25T07:23:06+02:00[Europe/Berlin]\nBut today, the moon still rises at 2020-05-24T06:40:45+02:00[Europe/Berlin]\n```\n\nAs you can see in the last line, the invocation of `tomorrow()` did not affect the `today` result.\n\nThis can be useful for loops. Let's find out how much of the visible moon surface is lit by the sun on each day of January 2020.\n\n```java\nMoonIllumination.Parameters parameters = MoonIllumination.compute()\n        .on(2020, 1, 1);\n\nfor (int i = 1; i <= 31; i++) {\n    long percent = Math.round(parameters.execute().getFraction() * 100.0);\n    System.out.println(\"On January \" + i + \" the moon was \" + percent + \"% lit.\");\n    parameters.plusDays(1);\n}\n```\n\nThe result (excerpt):\n\n```text\nOn January 1 the moon was 29% lit.\nOn January 2 the moon was 38% lit.\nOn January 3 the moon was 48% lit.\n [...]\nOn January 29 the moon was 15% lit.\nOn January 30 the moon was 22% lit.\nOn January 31 the moon was 30% lit.\n```\n\n## Twilight\n\nBy default `SunTimes` computes the sunrise and sunset times as we would expect it. The sun rises when the upper part of the sun disc just appears on the horizon, and it sets when the upper part just vanishes. Because of our atmosphere, the sun is actually deeper on the horizon as it appears to be. This effect is called [atmospheric refraction](https://en.wikipedia.org/wiki/Atmospheric_refraction), and it is factored into the calculation.\n\nThere are other [twilights](https://en.wikipedia.org/wiki/Twilight) that may be interesting. Photographers are especially interested in the [Golden hour](https://en.wikipedia.org/wiki/Golden_hour_(photography)), which gives a warm and soft sunlight. In the morning, Golden hour starts at an angle of -4° (which is the end of the Blue hour), and ends when the sun reaches an angle of 6°. In the evening, the golden hour starts when the sun reaches an angle of 6°, and ends at an angle of -4°.\n\nTo learn more about the individual twilight transitions, see the [illustration of twilights](usage.md#twilight).\n\nLet's calculate the golden hour in Singapore for the next four Mondays starting June 1st, 2020:\n\n```java\nSunTimes.Parameters base = SunTimes.compute()\n        .at(1.283333, 103.833333)            // Singapore\n        .on(2020, 6, 1)\n        .timezone(\"Asia/Singapore\");\n\nfor (int i = 0; i < 4; i++) {\n    SunTimes blue = base\n            .copy()                          // Use a copy of base\n            .plusDays(i * 7)\n            .twilight(SunTimes.Twilight.BLUE_HOUR)      // Blue Hour, -4°\n            .execute();\n    SunTimes golden = base\n            .copy()                          // Use a copy of base\n            .plusDays(i * 7)\n            .twilight(SunTimes.Twilight.GOLDEN_HOUR)    // Golden Hour, 6°\n            .execute();\n\n    System.out.println(\"Morning golden hour starts at \" + blue.getRise());\n    System.out.println(\"Morning golden hour ends at   \" + golden.getRise());\n    System.out.println(\"Evening golden hour starts at \" + golden.getSet());\n    System.out.println(\"Evening golden hour ends at   \" + blue.getSet());\n}\n```\n\nNote the `copy()` method! It copies the current set of parameters into a new parameter object. Both objects can then be changed independently of each other. This is very useful when you need to have different parameters in loops.\n\nThis is the result:\n\n```text\nMorning golden hour starts at 2020-06-01T06:43:13+08:00[Asia/Singapore]\nMorning golden hour ends at   2020-06-01T07:26:24+08:00[Asia/Singapore]\nEvening golden hour starts at 2020-06-01T18:38:50+08:00[Asia/Singapore]\nEvening golden hour ends at   2020-06-01T19:22:02+08:00[Asia/Singapore]\nMorning golden hour starts at 2020-06-08T06:44:16+08:00[Asia/Singapore]\nMorning golden hour ends at   2020-06-08T07:27:41+08:00[Asia/Singapore]\nEvening golden hour starts at 2020-06-08T18:40:01+08:00[Asia/Singapore]\nEvening golden hour ends at   2020-06-08T19:23:27+08:00[Asia/Singapore]\nMorning golden hour starts at 2020-06-15T06:45:35+08:00[Asia/Singapore]\nMorning golden hour ends at   2020-06-15T07:29:10+08:00[Asia/Singapore]\nEvening golden hour starts at 2020-06-15T18:41:25+08:00[Asia/Singapore]\nEvening golden hour ends at   2020-06-15T19:25:00+08:00[Asia/Singapore]\nMorning golden hour starts at 2020-06-22T06:47:04+08:00[Asia/Singapore]\nMorning golden hour ends at   2020-06-22T07:30:41+08:00[Asia/Singapore]\nEvening golden hour starts at 2020-06-22T18:42:56+08:00[Asia/Singapore]\nEvening golden hour ends at   2020-06-22T19:26:32+08:00[Asia/Singapore]\n```\n\n## Moon Phase\n\nI'd like to print a calendar of 2023, and mark all the days having a full moon. As I print a calendar, I'm only interested in the day of full moon, but I won't care for the concrete time. Besides that, I also want to know if a full moon qualifies as [supermoon](https://en.wikipedia.org/wiki/Supermoon) or micromoon.\n\nAs the visible moon phase is identical on every place on earth, we won't have to set a location here.\n\nBut we have to be careful! Since there are about 29.5 days between two full moons, a month might actually have two full moons. For this reason, we cannot simply iterate over the months. Instead we take the previous full moon, add one day so we won't find the same full moon again, and use this date as a base for the next iteration.\n\n```java\nLocalDate date = LocalDate.of(2023, 1, 1);\n\nMoonPhase.Parameters parameters = MoonPhase.compute()\n        .phase(MoonPhase.Phase.FULL_MOON);\n\nwhile (true) {\n    MoonPhase moonPhase = parameters\n            .on(date)\n            .execute();\n    LocalDate nextFullMoon = moonPhase\n            .getTime()\n            .toLocalDate();\n    if (nextFullMoon.getYear() == 2024) {\n        break;      // we've reached the next year\n    }\n\n    System.out.print(nextFullMoon);\n    if (moonPhase.isMicroMoon()) {\n        System.out.print(\" (micromoon)\");\n    }\n    if (moonPhase.isSuperMoon()) {\n        System.out.print(\" (supermoon)\");\n    }\n    System.out.println();\n\n    date = nextFullMoon.plusDays(1);\n}\n```\n\nThe result is:\n\n```text\n2023-01-07 (micromoon)\n2023-02-05 (micromoon)\n2023-03-07\n2023-04-06\n2023-05-05\n2023-06-04\n2023-07-03\n2023-08-01 (supermoon)\n2023-08-31 (supermoon)\n2023-09-29\n2023-10-28\n2023-11-27\n2023-12-27\n```\n\nAs you can see, there are two full moons in August 2023.\n\n## Sun and Moon Positions\n\nI'm in Tokyo. It's November 13th 2018, 10:03:24. In what direction do I have to look in order to see the sun and the moon?\n\n```java\nSunPosition.Parameters sunParam = SunPosition.compute()\n        .at(35.689722, 139.692222)      // Tokyo\n        .timezone(\"Asia/Tokyo\")         // local time\n        .on(2018, 11, 13, 10, 3, 24);   // 2018-11-13 10:03:24\n\nMoonPosition.Parameters moonParam = MoonPosition.compute()\n        .sameLocationAs(sunParam)\n        .sameTimeAs(sunParam);\n\nSunPosition sun = sunParam.execute();\nSystem.out.println(String.format(\n        \"The sun can be seen %.1f° clockwise from the North and \"\n        + \"%.1f° above the horizon.\\nIt is about %.0f km away right now.\",\n        sun.getAzimuth(),\n        sun.getAltitude(),\n        sun.getDistance()\n));\n\nMoonPosition moon = moonParam.execute();\nSystem.out.println(String.format(\n        \"The moon can be seen %.1f° clockwise from the North and \"\n        + \"%.1f° above the horizon.\\nIt is about %.0f km away right now.\",\n        moon.getAzimuth(),\n        moon.getAltitude(),\n        moon.getDistance()\n));\n```\n\nNote the invocations of `sameLocationAs()` and `sameTimeAs()`. Both methods are useful to copy the location and time parameter from other pararameter objects. The other parameter object won't need to be of the same type, so the `MoonPosition` can just \"steal\" the location and time from the `SunPosition`.\n\nThe result is:\n\n```text\nThe sun can be seen 156,6° clockwise from the North and 33,0° above the horizon.\nIt is about 148075152 km away right now.\nThe moon can be seen 109,0° clockwise from the North and -9,5° above the horizon.\nIt is about 404629 km away right now.\n```\n\nThe sun is in the southeast and about 33° above the horizon. The moon is to the east, but below the horizon, so it is not visible right now.\n\n"
  },
  {
    "path": "src/doc/docs/faq.md",
    "content": "# FAQ\n\n## There is a different result on another website/app. Which one is right?\n\nProbably both. Calculating sun and moon positions is rather complex, there is no universal formula that can be used. Other tools might also include the topology or other factors. If the difference is less than two minutes, it is within the acceptable tolerance.\n\nThere is no official definition of supermoon and micromoon, so the results may differ from other sources. _suncalc_ assumes a supermoon if the moon is closer than 360,000 km from Earth, and a micromoon if the moon is farther than 405,000 km from Earth.\n\n## Can you enhance the precision?\n\nThe positions of the sun and moon are approximated by a rather simple set of formulae. The results have an accuracy of about a minute, which should be good enough for common applications (like sunrise/sunset timers). This library is targeted for mobile devices, or devices with low computing power, and the precision is acceptable for that target. A higher precision would involve perturbation tables of all planets, and would multiply the necessary computing load.\n\n## Can you add other planets or stars?\n\nNo. It would add much more complexity to this library. It is not meant to be used for astronomical purposes.\n\n## What about sea tide levels?\n\nAs the tide directly depends on the position of the sun and moon, there could be some kind of `Tide` calculator class. But it's not as easy as that. The tide at a location also depends on geological conditions and currents. This library could only give a very rough estimation. At best it would have no practical use, at worst it would even be life threatening if someone relied on the results. There are sophisitcated tools that have been made just for this purpose, e.g. [JTides](https://arachnoid.com/JTides/).\n"
  },
  {
    "path": "src/doc/docs/index.md",
    "content": "# commons-suncalc\n\nA Java library for calculation of sun and moon positions and phases.\n\nThe source code can be found at [Codeberg](https://codeberg.org/shred/commons-suncalc) and is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n\n## Accuracy\n\nAstronomical calculations are far more complex than throwing a few numbers into an obscure formula and then getting a fully accurate result. There is always a tradeoff between accuracy and computing time.\n\nThis library has its focus on getting _acceptable_ results at low cost, so it can also run on mobile devices, or devices with a low computing power. The results have an accuracy of about a minute, which should be good enough for common applications (like sunrise/sunset timers), but is probably not sufficient for astronomical purposes.\n\n## Dependencies and Requirements\n\n_commons-suncalc_ requires at least Java 8, but has no other runtime dependencies. It can also be used on Android, API level 26 (Android 8.0 \"Oreo\") or higher.\n\n!!! NOTE\n    **For Android versions before API level 26**, please use [commons-suncalc v2](https://codeberg.org/shred/commons-suncalc/tree/v2). It is similar to this version, but does not use the Java Date/Time API. Also see the [commons-suncalc v2 documentation](https://shredzone.org/maven/commons-suncalc-v2/index.html).\n\n## Installation\n\n_commons-suncalc_ is available at Maven Central. Just add this snippet to your `pom.xml`:\n\n```xml\n<dependency>\n  <groupId>org.shredzone.commons</groupId>\n  <artifactId>commons-suncalc</artifactId>\n  <version>$version</version>\n</dependency>\n```\n\nOr use this snippet in your `build.gradle` (e.g. in Android Studio):\n\n```groovy\ndependencies {\n    compile('org.shredzone.commons:commons-suncalc:$version')\n}\n```\n\nReplace `$version` with your desired version. The latest version is: ![maven central](https://shredzone.org/maven-central/org.shredzone.commons/commons-suncalc/badge.svg)\n\n## Java Module\n\nAdd this line to your module descriptor:\n\n```\nrequires org.shredzone.commons.suncalc;\n```\n\n## References\n\nThis library bases on:\n\n* \"Astronomy on the Personal Computer\", 4th edition, by Oliver Montenbruck and Thomas Pfleger\n* \"Astronomical Algorithms\" by Jean Meeus\n\n## Contribute\n\n* Fork the [Source code at Codeberg](https://codeberg.org/shred/commons-suncalc). Feel free to send pull requests.\n* Found a bug? Please [file a bug report](https://codeberg.org/shred/commons-suncalc/issues).\n\n## License\n\n_commons-suncalc_ is open source software. The source code is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).\n"
  },
  {
    "path": "src/doc/docs/migration.md",
    "content": "# Migration Guide\n\nThis document will help you migrate your code to the latest _suncalc_ version.\n\n## Version 3.9\n* `MoonIllumination` now permits to set an optional geolocation. If set, the results are topocentric. If not set, the result is geocentric, which was the default behavior. For this reason, no migration is necessary here.\n* In previous versions, the geolocation was assumed to be 0°N 0°E ([Null Island](https://en.wikipedia.org/wiki/Null_Island)) if not set. This was not very useful in practice. Starting now, `execute()` will throw an exception if latitude or longitude is not set. To emulate the old behavior, use `at(0.0, 0.0)` as parameter. However, if you get an exception now, it rather means that you have used suncalc wrong.\n* For the spectator's altitude above sea level, `elevation()` and `elevationFt()` is now used instead of `height()` and `heightFt()`. The old methods are marked as deprecated, but are still functional. Please change to the new methods, they are drop-in replacements.\n\n## Version 3.6\n* Due to a very old bug, `MoonPosition.getParallacticAngle()` gave completely wrong results. If you used that method in your code or tests, prepare to get totally different values now.\n\n## Version 3.3\n* Due to a very old bug, setting the `height()` had almost no impact on the result, which is obviously wrong (on sunset, a plane in the sky is still illuminated while the sun has just gone at the viewer's position). This bug has been fixed. If you are using `height()`, you will get correct results now. They may deviate by several minutes from the results of earlier versions.\n\n## Version 3.1\n* As it was an eternal source of confusion, `SunTime` and `MoonTime` now default to `fullCycle()`, so they always compute until all times have been found. To revert to the previous behavior, use the `oneDay()` parameter. There is also a new `limit()` parameter which limits the window to any end date.\n* `isAlwaysUp()`, `isAlwaysDown()`, `getNoon()` and `getNadir()` now also use the given time window instead of the next 24 hours. Use `oneDay()` to revert to the previous behavior.\n\n## Version 3.0\n* _suncalc_ now requires at least Java 8 or Android 8.0 \"Oreo\" (API level 26). You can still use _suncalc_ v2 for Java 7 or Android API level 19 compatibility. The v2 branch is not discontinued and will still receive bugfixes.\n* The outdated `Date` and `Calendar` classes have been replaced by the Java Date/Time API. All results are now returned as `ZonedDateTime` instances, and now also carry the timezone that was used for calculation. It is now much easier to use the result.\n* Result rounding has been removed from this library, as it can easily be done by `ZoneDateTime.truncateTo()`. Note that even though the results now contain milliseconds, the precision is still only up to about a minute.\n* The JSR305 null-safe annotations have been replaced by SpotBugs annotations. This _should_ have no impact on your code, as the method signatures themselves are unchanged. However, the compiler could now complain about some `null` dereferences that have been undetected before. Reason is that JSR305 uses the `javax.annotations` package, which leads to split packages in a Java 9 modular environment.\n"
  },
  {
    "path": "src/doc/docs/usage.md",
    "content": "# Usage\n\n`commons-suncalc` offers six astronomical calculations:\n\n* [SunTimes](./apidocs/org/shredzone/commons/suncalc/SunTimes.html): Sunrise, sunset, noon and nadir times.\n* [MoonTimes](./apidocs/org/shredzone/commons/suncalc/MoonTimes.html): Moonrise and moonset times.\n* [MoonPhase](./apidocs/org/shredzone/commons/suncalc/MoonPhase.html): Date and time of new moon, full moon and half moons.\n* [SunPosition](./apidocs/org/shredzone/commons/suncalc/SunPosition.html): Position of the sun.\n* [MoonPosition](./apidocs/org/shredzone/commons/suncalc/MoonPosition.html): Position of the moon.\n* [MoonIllumination](./apidocs/org/shredzone/commons/suncalc/MoonIllumination.html): Moon phase and angle.\n\n## Quick Start\n\nAll of the calculations mentioned above are invoked in the same way:\n\n```java\nZonedDateTime dateTime =    // date, time and timezone of calculation\ndouble lat, lng =           // geolocation\nSunTimes times = SunTimes.compute()\n            .on(dateTime)   // set a date\n            .at(lat, lng)   // set a location\n            .execute();     // get the results\nSystem.out.println(\"Sunrise: \" + times.getRise());\nSystem.out.println(\"Sunset: \" + times.getSet());\n```\n\nYou invoke `compute()`, set your parameters, invoke `execute()`, and then get the result of the calculation as an object.\n\nAll parameters are passed in by a fluent builder-style interface. After retrieving the builder from `compute()`, you can chain the parameter setters, and finally call `execute()` to perform the computation.\n\n```java\nSunPosition pos = SunPosition.compute().today().at(12.3, 45.6).execute();\n```\n\nIt is also possible to collect the parameters first, and execute the computation in a separate step:\n\n```java\nSunPosition.Parameters param = SunPosition.compute();\nparam.today();\nparam.at(12.3, 45.6);\n\nSunPosition pos = param.execute();\n```\n\nThe instance that is returned by `execute()` is immutable and only holds the calculation result of the current set of parameters. You can modify the parameters without changing the first result, then call `execute()` again for a second result.\n\n```java\nparam.on(2016, 12, 24); // modify the param from above\n\nSunPosition posAtChristmas = param.execute();\n// pos from above is unchanged\n```\n\n## Time-based Parameters\n\nAll calculations need a date and time parameter. Some examples:\n\n```java\n// Now (the default)\nSunPosition.compute().now();\n\n// The same: Current time, local time zone\nZonedDateTime now = ZonedDateTime.now();\nSunPosition.compute().on(now);\n\n// August 21st, 2017, local midnight\nSunPosition.compute().on(2017, 8, 21);\n\n// Today (midnight), Berlin time zone\nSunPosition.compute().today().timezone(\"Europe/Berlin\");\n```\n\nThe available time-based parameters are:\n\n* `on(int year, int month, int date)`: Midnight of the given date. Note that `month` is counted from 1 (1 = January, 2 = February, …).\n* `on(int year, int month, int date, int hour, int minute, int second)`: Given date and time.\n* `on(ZonedDateTime dateTime)`: Given date, time, and timezone.\n* `on(LocalDateTime dateTime)`: Given local date and time, without a timezone.\n* `on(LocalDate date)`: Midnight of the given local date, without a timezone.\n* `on(Instant instant)`: An instant without a timezone.\n* `on(Calendar cal)`: Date, time and timezone as given in the old-fashioned `Calendar`. The `Calender` is copied and can safely be modified after that.\n* `on(Date date)`: Date and time as given in the old-fashioned `Date`. The `Date` is copied and can safely be modified after that.\n* `plusDays(int days)`: Adds the given number of days to the current date. `days` can also be negative, of course.\n* `now()`: The current system date and time. This is the default.\n* `midnight()`: Past midnight of the current date. It just truncates the time.\n* `today()`: Identical to `.now().midnight()`.\n* `tomorrow()`: Identical to `today().plusDays(1)`.\n* `timezone(ZoneId tz)`: Use the given `ZoneId` as timezone. The current local time is unchanged (this is, it is not converted to the new timezone), so the order of parameters is not important.\n* `timezone(String id)`: Same as above, but accepts a `String` for your convenience.\n* `timezone(TimeZone tz)`: Same as above, but accepts an old-fashioned `TimeZone` object.\n* `localTime()`: The system's timezone. This is the default.\n* `utc()`: UTC timezone. Identical to `timezone(\"UTC\")`.\n* `sameTimeAs(TimeParameter<?> t)`: Copies the current date, time, and timezone from any other parameter object. Note that subsequent changes to the other object are not adopted.\n\nIf no time-based parameter is given, the current date and time, and the system's time zone is used.\n\n!!! NOTE\n    The accuracy of the results is decreasing for dates that are far in the future, or far in the past.\n\n## Location-based Parameters\n\nThe geolocation is required, and `execute()` will throw an exception if the latitude or longitude is missing. The elevation is optional, and is 0 meters (sea level) if not set.\n\n```java\n// At 20.5°N, 18.3°E\nSunPosition.compute().at(20.5, 18.3);\n\n// The same, but more verbose\nSunPosition.compute().latitude(20.5).longitude(18.3);\n\n// Use arrays for coordinate constants\nfinal double[] COLOGNE = new double[] { 50.938056, 6.956944 };\nSunPosition.compute().at(COLOGNE);\n```\n\nThere are two exceptions:\n\n* [`MoonIllumination`](./apidocs/org/shredzone/commons/suncalc/MoonIllumination.Parameters.html):  If the geolocation is set, the result is topocentric. If the geolocation is unset, the result is geocentric.\n* [`MoonPhase`](./apidocs/org/shredzone/commons/suncalc/MoonPhase.Parameters.html): The geolocation is not used here.\n\nThe available location-based parameters are:\n\n* `at(double lat, double lng)`: Latitude and longitude to be used for computation.\n* `at(double[] coords)`: Accepts an array of 2 values (latitude, longitude) or 3 values (latitude, longitude, elevation).\n* `latitude(double lat)`: Verbose way to set the latitude only.\n* `longitude(double lng)`: Verbose way to set the longitude only.\n* `latitude(int d, int m, double s)`: Set the latitude in degrees, minutes, seconds and fraction of seconds.\n* `longitude(int d, int m, double s)`: Set the longitude in degrees, minutes, seconds and fraction of seconds.\n* `elevation(double h)`: Elevation above sea level, in meters. Sea level is used by default.\n* `elevationFt(double h)`: Elevation above sea level, in foot. Sea level is used by default.\n* `sameLocationAs(LocationParameter<?> l)`: Copies the current location and elevation from any other parameter object. Note that subsequent changes to the other object are not adoped.\n\n!!! NOTE\n    `elevation` cannot be negative. If you pass in a negative elevation, it is silently changed to the accepted minimum of 0 meters. For this reason, it is safe to pass coordinates from satellite-based navigation systems without range checking.\n\n## Window-based Parameters\n\nBy default, [`SunTimes`](./apidocs/org/shredzone/commons/suncalc/SunTimes.Parameters.html) and [`MoonTimes`](./apidocs/org/shredzone/commons/suncalc/MoonTimes.Parameters.html) will calculate a full cycle of the sun or moon. This means that rise, set, noon, and nadir times may be more than 24 hours ahead. You can limit this time window.\n\nThe available window-based parameters are:\n\n* `limit(Duration duration)`: Limits the time window to the given duration. A reverse time window is implicitly set if this value is negative.\n* `oneDay()`: Limits the time window to 24 hours.\n* `fullCycle()`: No limit. Calculation will find all rise, set, noon, and nadir times.\n* `reverse()`: Sets a reverse time window. It will end at the given time. The rise, set, noon, and nadir times will be in the past. You can also pass a negative duration as `limit()`.\n* `forward()`: Sets a forward time window. It will start at the given time. The rise, set, noon, and nadir times will be in the future. This is the default.\n* `sameWindowAs(WindowParameter<?> w)`: Copies the current time window from any other parameter object. Note that subsequent changes to the other object are not adoped.\n\nIf the sun or moon does not rise or set within the given window, the appropriate getters return `null`. You can check if the sun or moon is always above or below the horizon, by checking `isAlwaysUp()` and `isAlwaysDown()`.\n\n## Twilight\n\n<img src=\"twilights.svg\" alt=\"Twilight Zones\" align=\"right\" width=\"550px\"/>\n\nBy default, [`SunTimes`](./apidocs/org/shredzone/commons/suncalc/SunTimes.Parameters.html) calculates the time of the visual sunrise and sunset. This means that `getRise()` returns the instant when the sun just starts to rise above the horizon, and `getSet()` returns the instant when the sun just disappeared from the horizon. [Atmospheric refraction](https://en.wikipedia.org/wiki/Atmospheric_refraction) is taken into account.\n\nThere are other interesting [twilight](https://en.wikipedia.org/wiki/Twilight) angles available. You can set them via the `twilight()` parameter, by using one of the [`SunTimes.Twilight`](./apidocs/org/shredzone/commons/suncalc/SunTimes.Twilight.html) constants:\n\n| Constant       | Description | Angle of the Sun | Topocentric |\n| -------------- | ----------- | ----------------:|:-----------:|\n| `VISUAL`       | The moment when the visual upper edge of the sun crosses the horizon. This is the default. | | yes |\n| `VISUAL_LOWER` | The moment when the visual lower edge of the sun crosses the horizon. | | yes |\n| `ASTRONOMICAL` | [Astronomical twilight](https://en.wikipedia.org/wiki/Twilight#Astronomical_twilight) | -18° | no |\n| `NAUTICAL`     | [Nautical twilight](https://en.wikipedia.org/wiki/Twilight#Nautical_twilight) | -12° | no |\n| `CIVIL`        | [Civil twilight](https://en.wikipedia.org/wiki/Twilight#Civil_twilight) | -6° | no |\n| `HORIZON`      | The moment when the center of the sun crosses the horizon. | 0° | no |\n| `GOLDEN_HOUR`  | Transition from daylight to [Golden Hour](https://en.wikipedia.org/wiki/Golden_hour_%28photography%29) | 6° | no |\n| `BLUE_HOUR`    | Transition from [Golden Hour](https://en.wikipedia.org/wiki/Golden_hour_%28photography%29) to [Blue Hour](https://en.wikipedia.org/wiki/Blue_hour) | -4° | no |\n| `NIGHT_HOUR`   | Transition from [Blue Hour](https://en.wikipedia.org/wiki/Blue_hour) to night | -8° | no |\n\nThe illustration shows the transitions of each twilight constant. If you want to get the duration of a twilight, you need to calculate the times of both transitions of the twilight. For example, to get the beginning and ending of the civil twilight, you need to calculate both the `VISUAL` and the `CIVIL` twilight transition times.\n\nAlternatively you can also pass any other angle (in degrees) to `twilight()`.\n\n!!! NOTE\n    Only `VISUAL` and `VISUAL_LOWER` are topocentric. They refer to the visual edge of the sun, take account of the `elevation` parameter, and compensate atmospheric refraction.\n    \n    All other twilights are geocentric and heliocentric. The `elevation` parameter is then ignored, and atmospheric refraction is not compensated.\n\nExample:\n\n```java\nSunTimes.compute().twilight(SunTimes.Twilight.GOLDEN_HOUR);\n```\n\n## Phase\n\nBy default, [`MoonPhase`](./apidocs/org/shredzone/commons/suncalc/MoonPhase.Parameters.html) calculates the date of the next new moon. If you want to compute the date of another phase, you can set it via the `phase()` parameter, by using one of the [`MoonPhase.Phase`](./apidocs/org/shredzone/commons/suncalc/MoonPhase.Phase.html) constants:\n\n| Constant          | Description | Angle |\n| ----------------- | ----------- | -----:|\n| `NEW_MOON`        | Moon is not illuminated (new moon). This is the default. | 0° |\n| `WAXING_CRESCENT` | Waxing crescent moon. | 45° |\n| `FIRST_QUARTER`   | Half of the waxing moon is illuminated. | 90° |\n| `WAXING_GIBBOUS`  | Waxing gibbous moon. | 135° |\n| `FULL_MOON`       | Moon is fully illuminated. | 180° |\n| `WANING_GIBBOUS`  | Waning gibbous moon. | 225° |\n| `LAST_QUARTER`    | Half of the waning moon is illuminated. | 270° |\n| `WANING_CRESCENT` | Waning crescent moon. | 315° |\n\nAlternatively you can also pass any other angle (in degrees) to `phase()`.\n\nExample:\n\n```java\nMoonPhase.compute().phase(MoonPhase.Phase.FULL_MOON);\n```\n"
  },
  {
    "path": "src/doc/mkdocs.yml",
    "content": "site_name: commons-suncalc\nsite_author: Richard Körber\nsite_url: https://commons.shredzone.org/suncalc\nsite_dir: target/site/\nrepo_url: https://codeberg.org/shred/commons-suncalc\nedit_uri: ''\nuse_directory_urls: false\ntheme:\n    name: readthedocs\n    custom_dir: theme/\n    highlightjs: false\nmarkdown_extensions:\n  - admonition\n  - codehilite\n  - toc:\n      permalink: true\nnav:\n  - 'index.md'\n  - 'usage.md'\n  - 'examples.md'\n  - 'faq.md'\n  - 'JavaDocs': apidocs/index.html\n  - 'migration.md'\n"
  },
  {
    "path": "src/doc/theme/breadcrumbs.html",
    "content": ""
  },
  {
    "path": "src/doc/theme/css/font.css",
    "content": "/*\n * Lato\n * Copyright 2010-2011 tyPoland Lukasz Dziedzic\n * SIL Open Font License, 1.1\n */\n@font-face {\n  font-family: 'Lato';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Lato Regular'), local('Lato-Regular'), url('../fonts/lato-v15-latin-regular.woff') format('woff');\n}\n\n/*\n * Roboto Slab\n * Copyright 2013 Google\n * Apache License, version 2.0\n */\n@font-face {\n  font-family: 'Roboto Slab';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Roboto Slab Regular'), local('RobotoSlab-Regular'), url('../fonts/roboto-slab-v8-latin-regular.woff') format('woff');\n}\n\n/*\n * Inconsolata\n * Copyright 2006 The Inconsolata Project Authors\n * SIL Open Font License, 1.1\n */\n@font-face {\n  font-family: 'Inconsolata';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('../fonts/inconsolata-v17-latin-regular.woff') format('woff');\n}\n"
  },
  {
    "path": "src/doc/theme/css/theme_custom.css",
    "content": "\n.wy-nav-content {\n    max-width: none;\n}\n\n.codehilite pre code {\n    font-size: 15px;\n}\n\ntable {\n    margin-bottom: 2rem;\n}\n\nfooter {\n    margin-top: 2rem;\n}\n"
  },
  {
    "path": "src/doc/theme/css/theme_pygments.css",
    "content": "\n/* github pygments theme by @jwarby, https://github.com/jwarby/jekyll-pygments-themes */\n.codehilite .hll { background-color: #ffffcc }\n.codehilite .c { color: #999988; font-style: italic }\n.codehilite .err { color: #a61717; background-color: #e3d2d2 }\n.codehilite .k { color: #000000; font-weight: bold }\n.codehilite .o { color: #000000; font-weight: bold }\n.codehilite .cm { color: #999988; font-style: italic }\n.codehilite .cp { color: #999999; font-weight: bold; font-style: italic }\n.codehilite .c1 { color: #999988; font-style: italic }\n.codehilite .cs { color: #999999; font-weight: bold; font-style: italic }\n.codehilite .gd { color: #000000; background-color: #ffdddd }\n.codehilite .ge { color: #000000; font-style: italic }\n.codehilite .gr { color: #aa0000 }\n.codehilite .gh { color: #999999 }\n.codehilite .gi { color: #000000; background-color: #ddffdd }\n.codehilite .go { color: #888888 }\n.codehilite .gp { color: #555555 }\n.codehilite .gs { font-weight: bold }\n.codehilite .gu { color: #aaaaaa }\n.codehilite .gt { color: #aa0000 }\n.codehilite .kc { color: #000000; font-weight: bold }\n.codehilite .kd { color: #000000; font-weight: bold }\n.codehilite .kn { color: #000000; font-weight: bold }\n.codehilite .kp { color: #000000; font-weight: bold }\n.codehilite .kr { color: #000000; font-weight: bold }\n.codehilite .kt { color: #445588; font-weight: bold }\n.codehilite .m { color: #009999 }\n.codehilite .s { color: #d01040 }\n.codehilite .na { color: #008080 }\n.codehilite .nb { color: #0086B3 }\n.codehilite .nc { color: #445588; font-weight: bold }\n.codehilite .no { color: #008080 }\n.codehilite .nd { color: #3c5d5d; font-weight: bold }\n.codehilite .ni { color: #800080 }\n.codehilite .ne { color: #990000; font-weight: bold }\n.codehilite .nf { color: #990000; font-weight: bold }\n.codehilite .nl { color: #990000; font-weight: bold }\n.codehilite .nn { color: #555555 }\n.codehilite .nt { color: #000080 }\n.codehilite .nv { color: #008080 }\n.codehilite .ow { color: #000000; font-weight: bold }\n.codehilite .w { color: #bbbbbb }\n.codehilite .mf { color: #009999 }\n.codehilite .mh { color: #009999 }\n.codehilite .mi { color: #009999 }\n.codehilite .mo { color: #009999 }\n.codehilite .sb { color: #d01040 }\n.codehilite .sc { color: #d01040 }\n.codehilite .sd { color: #d01040 }\n.codehilite .s2 { color: #d01040 }\n.codehilite .se { color: #d01040 }\n.codehilite .sh { color: #d01040 }\n.codehilite .si { color: #d01040 }\n.codehilite .sx { color: #d01040 }\n.codehilite .sr { color: #009926 }\n.codehilite .s1 { color: #d01040 }\n.codehilite .ss { color: #990073 }\n.codehilite .bp { color: #999999 }\n.codehilite .vc { color: #008080 }\n.codehilite .vg { color: #008080 }\n.codehilite .vi { color: #008080 }\n.codehilite .il { color: #009999 }\n"
  },
  {
    "path": "src/doc/theme/footer.html",
    "content": "<footer>\n  <div class=\"rst-footer-buttons\" role=\"navigation\" aria-label=\"footer navigation\">\n    {% if page.next_page %}\n      <a href=\"{{ page.next_page.url|url }}\" class=\"btn btn-neutral float-right\" title=\"{{ page.next_page.title }}\">Next <span class=\"icon icon-circle-arrow-right\"></span></a>\n    {% endif %}\n    {% if page.previous_page %}\n      <a href=\"{{ page.previous_page.url|url }}\" class=\"btn btn-neutral\" title=\"{{ page.previous_page.title }}\"><span class=\"icon icon-circle-arrow-left\"></span> Previous</a>\n    {% endif %}\n  </div>\n</footer>\n"
  },
  {
    "path": "src/doc/theme/main.html",
    "content": "{% extends \"base.html\" %}\n\n{% block styles %}\n<link rel=\"stylesheet\" href=\"{{ 'css/font.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme_extra.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme_custom.css'|url }}\" />\n<link rel=\"stylesheet\" href=\"{{ 'css/theme_pygments.css'|url }}\" />\n{%- for path in config['extra_css'] %}\n<link href=\"{{ path|url }}\" rel=\"stylesheet\" />\n{%- endfor %}\n{% endblock %}\n\n{% block repo %}\n<a href=\"https://shredzone.org/maven/commons-suncalc/apidocs/index.html\">API javadoc</a>s\n{% endblock %}\n"
  },
  {
    "path": "src/main/java/module-info.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * Module definition for commons-suncalc.\n */\nmodule org.shredzone.commons.suncalc {\n    requires static com.github.spotbugs.annotations;\n\n    exports org.shredzone.commons.suncalc;\n    exports org.shredzone.commons.suncalc.param;\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/MoonIllumination.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.*;\n\nimport org.shredzone.commons.suncalc.param.Builder;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.LocationParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.util.BaseBuilder;\nimport org.shredzone.commons.suncalc.util.JulianDate;\nimport org.shredzone.commons.suncalc.util.Moon;\nimport org.shredzone.commons.suncalc.util.Sun;\nimport org.shredzone.commons.suncalc.util.Vector;\n\n/**\n * Calculates the illumination of the moon.\n * <p>\n * Starting with v3.9, a geolocation can be set optionally. If set, the results will be\n * topocentric, relative to the given location. If not set, the result is geocentric,\n * which was the standard behavior before v3.9.\n */\npublic class MoonIllumination {\n\n    private final double fraction;\n    private final double phase;\n    private final double angle;\n    private final double elongation;\n    private final double radius;\n    private final double crescentWidth;\n\n    private MoonIllumination(double fraction, double phase, double angle,\n                             double elongation, double radius, double crescentWidth) {\n        this.fraction = fraction;\n        this.phase = phase;\n        this.angle = angle;\n        this.elongation = elongation;\n        this.radius = radius;\n        this.crescentWidth = crescentWidth;\n    }\n\n    /**\n     * Starts the computation of {@link MoonIllumination}.\n     *\n     * @return {@link Parameters} to set.\n     */\n    public static Parameters compute() {\n        return new MoonIlluminationBuilder();\n    }\n\n    /**\n     * Collects all parameters for {@link MoonIllumination}.\n     */\n    public interface Parameters extends\n            GenericParameter<Parameters>,\n            TimeParameter<Parameters>,\n            LocationParameter<Parameters>,\n            Builder<MoonIllumination> {\n\n        /**\n         * Clears the geolocation, so the result will be geocentric.\n         *\n         * @return itself\n         * @since 3.9\n         */\n        Parameters geocentric();\n\n    }\n\n    /**\n     * Builder for {@link MoonIllumination}. Performs the computations based on the\n     * parameters, and creates a {@link MoonIllumination} object that holds the result.\n     */\n    private static class MoonIlluminationBuilder extends BaseBuilder<Parameters> implements Parameters {\n        @Override\n        public Parameters geocentric() {\n            clearLocation();\n            return this;\n        }\n\n        @Override\n        public MoonIllumination execute() {\n            JulianDate t = getJulianDate();\n            Vector s = Sun.position(t);\n            Vector m = Moon.position(t);\n\n            double phi = PI - acos(m.dot(s) / (m.getR() * s.getR()));\n            Vector sunMoon = m.cross(s);\n            double angle = atan2(\n                    cos(s.getTheta()) * sin(s.getPhi() - m.getPhi()),\n                    sin(s.getTheta()) * cos(m.getTheta()) - cos(s.getTheta()) * sin(m.getTheta()) * cos(s.getPhi() - m.getPhi())\n            );\n\n            Vector sTopo, mTopo;\n            if (hasLocation()) {\n                sTopo = Sun.positionTopocentric(t, getLatitudeRad(), getLongitudeRad(), getElevation());\n                mTopo = Moon.positionTopocentric(t, getLatitudeRad(), getLongitudeRad(), getElevation());\n            } else {\n                sTopo = s;\n                mTopo = m;\n            }\n\n            double r = mTopo.subtract(sTopo).norm();\n            double re = sTopo.norm();\n            double d = mTopo.norm();\n            double elongation = acos((d*d + re*re - r*r) / (2.0*d*re));\n            double moonRadius = Moon.angularRadius(mTopo.getR());\n            double crescentWidth = moonRadius * (1 - cos(elongation));\n\n            return new MoonIllumination(\n                            (1 + cos(phi)) / 2,\n                            toDegrees(phi * signum(sunMoon.getTheta())),\n                            toDegrees(angle),\n                            toDegrees(elongation),\n                            toDegrees(moonRadius),\n                            toDegrees(crescentWidth));\n        }\n    }\n\n    /**\n     * Illuminated fraction. {@code 0.0} indicates new moon, {@code 1.0} indicates full\n     * moon.\n     */\n    public double getFraction() {\n        return fraction;\n    }\n\n    /**\n     * Moon phase. Starts at {@code -180.0} (new moon, waxing), passes {@code 0.0} (full\n     * moon) and moves toward {@code 180.0} (waning, new moon).\n     * <p>\n     * Note that for historical reasons, the range of this phase is different to the\n     * moon phase angle used in {@link MoonPhase}.\n     */\n    public double getPhase() {\n        return phase;\n    }\n\n    /**\n     * The angle of the moon illumination relative to earth. The moon is waxing if the\n     * angle is negative, and waning if positive.\n     * <p>\n     * By subtracting {@link MoonPosition#getParallacticAngle()} from {@link #getAngle()},\n     * one can get the zenith angle of the moons bright limb (anticlockwise). The zenith\n     * angle can be used do draw the moon shape from the observer's perspective (e.g. the\n     * moon lying on its back).\n     */\n    public double getAngle() {\n        return angle;\n    }\n\n    /**\n     * The closest {@link MoonPhase.Phase} that is matching the moon's angle.\n     *\n     * @return Closest {@link MoonPhase.Phase}\n     * @since 3.5\n     */\n    public MoonPhase.Phase getClosestPhase() {\n        return MoonPhase.Phase.toPhase(phase + 180.0);\n    }\n\n    /**\n     * The elongation, which is the angular distance between the moon and the sun as\n     * observed from a specific location on earth.\n     *\n     * @return Elongation between moon and sun, in degrees.\n     * @since 3.9\n     */\n    public double getElongation() {\n        return elongation;\n    }\n\n    /**\n     * The radius of the moon disk, as observed from a specific location on earth.\n     *\n     * @return Moon radius, in degrees.\n     * @since 3.9\n     */\n    public double getRadius() {\n        return radius;\n    }\n\n    /**\n     * The width of the moon crescent, as observed from a specific location on earth.\n     *\n     * @return Crescent width, in degrees.\n     * @since 3.9\n     */\n    public double getCrescentWidth() {\n        return crescentWidth;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"MoonIllumination[fraction=\").append(fraction);\n        sb.append(\", phase=\").append(phase);\n        sb.append(\"°, angle=\").append(angle);\n        sb.append(\"°, elongation=\").append(elongation);\n        sb.append(\"°, radius=\").append(radius);\n        sb.append(\"°, crescentWidth=\").append(crescentWidth);\n        sb.append(\"°]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/MoonPhase.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.PI;\nimport static java.lang.Math.toRadians;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.PI2;\n\nimport java.time.ZonedDateTime;\n\nimport org.shredzone.commons.suncalc.param.Builder;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.util.BaseBuilder;\nimport org.shredzone.commons.suncalc.util.JulianDate;\nimport org.shredzone.commons.suncalc.util.Moon;\nimport org.shredzone.commons.suncalc.util.Pegasus;\nimport org.shredzone.commons.suncalc.util.Sun;\nimport org.shredzone.commons.suncalc.util.Vector;\n\n/**\n * Calculates the date and time when the moon reaches the desired phase.\n * <p>\n * Note: Due to the simplified formulas used in suncalc, the returned time can have an\n * error of several minutes.\n */\npublic class MoonPhase {\n\n    private final ZonedDateTime time;\n    private final double distance;\n\n    private MoonPhase(ZonedDateTime time, double distance) {\n        this.time = time;\n        this.distance = distance;\n    }\n\n    /**\n     * Starts the computation of {@link MoonPhase}.\n     *\n     * @return {@link Parameters} to set.\n     */\n    public static Parameters compute() {\n        return new MoonPhaseBuilder();\n    }\n\n    /**\n     * Collects all parameters for {@link MoonPhase}.\n     */\n    public interface Parameters extends\n            GenericParameter<Parameters>,\n            TimeParameter<Parameters>,\n            Builder<MoonPhase> {\n\n        /**\n         * Sets the desired {@link Phase}.\n         * <p>\n         * Defaults to {@link Phase#NEW_MOON}.\n         *\n         * @param phase\n         *            {@link Phase} to be used.\n         * @return itself\n         */\n        Parameters phase(Phase phase);\n\n        /**\n         * Sets a free phase to be used.\n         *\n         * @param phase\n         *            Desired phase, in degrees. 0 = new moon, 90 = first quarter, 180 =\n         *            full moon, 270 = third quarter.\n         * @return itself\n         */\n        Parameters phase(double phase);\n    }\n\n    /**\n     * Enumeration of moon phases.\n     */\n    public enum Phase {\n\n        /**\n         * New moon.\n         */\n        NEW_MOON(0.0),\n\n        /**\n         * Waxing crescent moon.\n         *\n         * @since 3.5\n         */\n        WAXING_CRESCENT(45.0),\n\n        /**\n         * Waxing half moon.\n         */\n        FIRST_QUARTER(90.0),\n\n        /**\n         * Waxing gibbous moon.\n         *\n         * @since 3.5\n         */\n        WAXING_GIBBOUS(135.0),\n\n        /**\n         * Full moon.\n         */\n        FULL_MOON(180.0),\n\n        /**\n         * Waning gibbous moon.\n         *\n         * @since 3.5\n         */\n        WANING_GIBBOUS(225.0),\n\n        /**\n         * Waning half moon.\n         */\n        LAST_QUARTER(270.0),\n\n        /**\n         * Waning crescent moon.\n         *\n         * @since 3.5\n         */\n        WANING_CRESCENT(315.0);\n\n        /**\n         * Converts an angle to the closest matching moon phase.\n         *\n         * @param angle\n         *         Moon phase angle, in degrees. 0 = New Moon, 180 = Full Moon. Angles\n         *         outside the [0,360) range are normalized into that range.\n         * @return Closest Phase that is matching that angle.\n         * @since 3.5\n         */\n        public static Phase toPhase(double angle) {\n            // bring into range 0.0 .. 360.0\n            double normalized = angle % 360.0;\n            if (normalized < 0.0) {\n                normalized += 360.0;\n            }\n\n            if (normalized < 22.5) {\n                return NEW_MOON;\n            }\n            if (normalized < 67.5) {\n                return WAXING_CRESCENT;\n            }\n            if (normalized < 112.5) {\n                return FIRST_QUARTER;\n            }\n            if (normalized < 157.5) {\n                return WAXING_GIBBOUS;\n            }\n            if (normalized < 202.5) {\n                return FULL_MOON;\n            }\n            if (normalized < 247.5) {\n                return WANING_GIBBOUS;\n            }\n            if (normalized < 292.5) {\n                return LAST_QUARTER;\n            }\n            if (normalized < 337.5) {\n                return WANING_CRESCENT;\n            }\n            return NEW_MOON;\n        }\n\n        private final double angle;\n        private final double angleRad;\n\n        Phase(double angle) {\n            this.angle = angle;\n            this.angleRad = toRadians(angle);\n        }\n\n        /**\n         * Returns the moons's angle in reference to the sun, in degrees.\n         */\n        public double getAngle() {\n            return angle;\n        }\n\n        /**\n         * Returns the moons's angle in reference to the sun, in radians.\n         */\n        public double getAngleRad() {\n            return angleRad;\n        }\n    }\n\n    /**\n     * Builder for {@link MoonPhase}. Performs the computations based on the parameters,\n     * and creates a {@link MoonPhase} object that holds the result.\n     */\n    private static class MoonPhaseBuilder extends BaseBuilder<Parameters> implements Parameters {\n        private static final double SUN_LIGHT_TIME_TAU = 8.32 / (1440.0 * 36525.0);\n\n        private double phase = Phase.NEW_MOON.getAngleRad();\n\n        @Override\n        public Parameters phase(Phase phase) {\n            this.phase = phase.getAngleRad();\n            return this;\n        }\n\n        @Override\n        public Parameters phase(double phase) {\n            this.phase = toRadians(phase);\n            return this;\n        }\n\n        @Override\n        public MoonPhase execute() {\n            final JulianDate jd = getJulianDate();\n\n            double dT = 7.0 / 36525.0;                      // step rate: 1 week\n            double accuracy = (0.5 / 1440.0) / 36525.0;     // accuracy: 30 seconds\n\n            double t0 = jd.getJulianCentury();\n            double t1 = t0 + dT;\n\n            double d0 = moonphase(jd, t0);\n            double d1 = moonphase(jd, t1);\n\n            while (d0 * d1 > 0.0 || d1 < d0) {\n                t0 = t1;\n                d0 = d1;\n                t1 += dT;\n                d1 = moonphase(jd, t1);\n            }\n\n            double tphase = Pegasus.calculate(t0, t1, accuracy, x -> moonphase(jd, x));\n            JulianDate tjd = jd.atJulianCentury(tphase);\n            return new MoonPhase(tjd.getDateTime(), Moon.positionEquatorial(tjd).getR());\n        }\n\n        /**\n         * Calculates the position of the moon at the given phase.\n         *\n         * @param jd\n         *            Base Julian date\n         * @param t\n         *            Ephemeris time\n         * @return difference angle of the sun's and moon's position\n         */\n        private double moonphase(JulianDate jd, double t) {\n            Vector sun = Sun.positionEquatorial(jd.atJulianCentury(t - SUN_LIGHT_TIME_TAU));\n            Vector moon = Moon.positionEquatorial(jd.atJulianCentury(t));\n            double diff = moon.getPhi() - sun.getPhi() - phase; //NOSONAR: false positive\n            while (diff < 0.0) {\n                diff += PI2;\n            }\n            return ((diff + PI) % PI2) - PI;\n        }\n\n    }\n\n    /**\n     * Date and time of the desired moon phase. The time is rounded to full minutes.\n     */\n    public ZonedDateTime getTime() {\n        return time;\n    }\n\n    /**\n     * Geocentric distance of the moon at the given phase, in kilometers.\n     *\n     * @since 3.4\n     */\n    public double getDistance() { return distance; }\n\n    /**\n     * Checks if the moon is in a SuperMoon position.\n     * <p>\n     * Note that there is no official definition of supermoon. Suncalc will assume a\n     * supermoon if the center of the moon is closer than 360,000 km to the center of\n     * Earth. Usually only full moons or new moons are candidates for supermoons.\n     *\n     * @since 3.4\n     */\n    public boolean isSuperMoon() {\n        return distance < 360000.0;\n    }\n\n    /**\n     * Checks if the moon is in a MicroMoon position.\n     * <p>\n     * Note that there is no official definition of micromoon. Suncalc will assume a\n     * micromoon if the center of the moon is farther than 405,000 km from the center of\n     * Earth. Usually only full moons or new moons are candidates for micromoons.\n     *\n     * @since 3.4\n     */\n    public boolean isMicroMoon() {\n        return distance > 405000.0;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"MoonPhase[time=\").append(time);\n        sb.append(\", distance=\").append(distance);\n        sb.append(\" km]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/MoonPosition.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.*;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.equatorialToHorizontal;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.refraction;\n\nimport org.shredzone.commons.suncalc.param.Builder;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.LocationParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.util.BaseBuilder;\nimport org.shredzone.commons.suncalc.util.JulianDate;\nimport org.shredzone.commons.suncalc.util.Moon;\nimport org.shredzone.commons.suncalc.util.Vector;\n\n/**\n * Calculates the position of the moon.\n */\npublic class MoonPosition {\n\n    private final double azimuth;\n    private final double altitude;\n    private final double trueAltitude;\n    private final double distance;\n    private final double parallacticAngle;\n\n    private MoonPosition(double azimuth, double altitude, double trueAltitude, double distance, double parallacticAngle) {\n        this.azimuth = (toDegrees(azimuth) + 180.0) % 360.0;\n        this.altitude = toDegrees(altitude);\n        this.trueAltitude = toDegrees(trueAltitude);\n        this.distance = distance;\n        this.parallacticAngle = toDegrees(parallacticAngle);\n    }\n\n    /**\n     * Starts the computation of {@link MoonPosition}.\n     *\n     * @return {@link Parameters} to set.\n     */\n    public static Parameters compute() {\n        return new MoonPositionBuilder();\n    }\n\n    /**\n     * Collects all parameters for {@link MoonPosition}.\n     */\n    public interface Parameters extends\n            GenericParameter<Parameters>,\n            LocationParameter<Parameters>,\n            TimeParameter<Parameters>,\n            Builder<MoonPosition> {\n    }\n\n    /**\n     * Builder for {@link MoonPosition}. Performs the computations based on the\n     * parameters, and creates a {@link MoonPosition} object that holds the result.\n     */\n    private static class MoonPositionBuilder extends BaseBuilder<Parameters> implements Parameters {\n        @Override\n        public MoonPosition execute() {\n            if (!hasLocation()) {\n                throw new IllegalArgumentException(\"Geolocation is missing.\");\n            }\n\n            JulianDate t = getJulianDate();\n\n            double phi = getLatitudeRad();\n            double lambda = getLongitudeRad();\n\n            Vector mc = Moon.position(t);\n            double h = t.getGreenwichMeanSiderealTime() + lambda - mc.getPhi();\n\n            Vector horizontal = equatorialToHorizontal(h, mc.getTheta(), mc.getR(), phi);\n\n            double hRef = refraction(horizontal.getTheta());\n\n            double pa = atan2(sin(h), tan(phi) * cos(mc.getTheta()) - sin(mc.getTheta()) * cos(h));\n\n            return new MoonPosition(\n                    horizontal.getPhi(),\n                    horizontal.getTheta() + hRef,\n                    horizontal.getTheta(),\n                    mc.getR(),\n                    pa);\n        }\n    }\n\n    /**\n     * Moon altitude above the horizon, in degrees.\n     * <p>\n     * {@code 0.0} means the moon's center is at the horizon, {@code 90.0} at the zenith\n     * (straight over your head). Atmospheric refraction is taken into account.\n     *\n     * @see #getTrueAltitude()\n     */\n    public double getAltitude() {\n        return altitude;\n    }\n\n    /**\n     * The true moon altitude above the horizon, in degrees.\n     * <p>\n     * {@code 0.0} means the moon's center is at the horizon, {@code 90.0} at the zenith\n     * (straight over your head).\n     *\n     * @see #getAltitude()\n     * @since 3.8\n     */\n    public double getTrueAltitude() {\n        return trueAltitude;\n    }\n\n    /**\n     * Moon azimuth, in degrees, north-based.\n     * <p>\n     * This is the direction along the horizon, measured from north to east. For example,\n     * {@code 0.0} means north, {@code 135.0} means southeast, {@code 270.0} means west.\n     */\n    public double getAzimuth() {\n        return azimuth;\n    }\n\n    /**\n     * Distance to the moon in kilometers.\n     */\n    public double getDistance() {\n        return distance;\n    }\n\n    /**\n     * Parallactic angle of the moon, in degrees.\n     */\n    public double getParallacticAngle() {\n        return parallacticAngle;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"MoonPosition[azimuth=\").append(azimuth);\n        sb.append(\"°, altitude=\").append(altitude);\n        sb.append(\"°, true altitude=\").append(trueAltitude);\n        sb.append(\"°, distance=\").append(distance);\n        sb.append(\" km, parallacticAngle=\").append(parallacticAngle);\n        sb.append(\"°]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/MoonTimes.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.ceil;\nimport static java.lang.Math.floor;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.apparentRefraction;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.parallax;\n\nimport java.time.Duration;\nimport java.time.ZonedDateTime;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.commons.suncalc.param.Builder;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.LocationParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.param.WindowParameter;\nimport org.shredzone.commons.suncalc.util.BaseBuilder;\nimport org.shredzone.commons.suncalc.util.JulianDate;\nimport org.shredzone.commons.suncalc.util.Moon;\nimport org.shredzone.commons.suncalc.util.QuadraticInterpolation;\nimport org.shredzone.commons.suncalc.util.Vector;\n\n/**\n * Calculates the times of the moon.\n */\npublic final class MoonTimes {\n\n    private final @Nullable ZonedDateTime rise;\n    private final @Nullable ZonedDateTime set;\n    private final boolean alwaysUp;\n    private final boolean alwaysDown;\n\n    private MoonTimes(@Nullable ZonedDateTime rise, @Nullable ZonedDateTime set,\n              boolean alwaysUp, boolean alwaysDown) {\n        this.rise = rise;\n        this.set = set;\n        this.alwaysUp = alwaysUp;\n        this.alwaysDown = alwaysDown;\n    }\n\n    /**\n     * Starts the computation of {@link MoonTimes}.\n     *\n     * @return {@link Parameters} to set.\n     */\n    public static Parameters compute() {\n        return new MoonTimesBuilder();\n    }\n\n    /**\n     * Collects all parameters for {@link MoonTimes}.\n     */\n    public static interface Parameters extends\n            GenericParameter<Parameters>,\n            LocationParameter<Parameters>,\n            TimeParameter<Parameters>,\n            WindowParameter<Parameters>,\n            Builder<MoonTimes> {\n    }\n\n    /**\n     * Builder for {@link MoonTimes}. Performs the computations based on the parameters,\n     * and creates a {@link MoonTimes} object that holds the result.\n     */\n    private static class MoonTimesBuilder extends BaseBuilder<Parameters> implements Parameters {\n        private double refraction = apparentRefraction(0.0);\n\n        @Override\n        public MoonTimes execute() {\n            if (!hasLocation()) {\n                throw new IllegalArgumentException(\"Geolocation is missing.\");\n            }\n\n            JulianDate jd = getJulianDate();\n\n            Double rise = null;\n            Double set = null;\n            boolean alwaysUp = false;\n            boolean alwaysDown = false;\n            double ye;\n\n            int hourStep;\n            double lowerLimitHours, upperLimitHours;\n            if (getDuration().isNegative()) {\n                hourStep = -1;\n                lowerLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);\n                upperLimitHours = 0.0;\n            } else {\n                hourStep = 1;\n                lowerLimitHours = 0.0;\n                upperLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);;\n            }\n\n            int hour = 0;\n            int minHours = (int) floor(lowerLimitHours);\n            int maxHours = (int) ceil(upperLimitHours);\n\n            double y_minus = correctedMoonHeight(jd.atHour(hour - 1.0));\n            double y_0 = correctedMoonHeight(jd.atHour(hour));\n            double y_plus = correctedMoonHeight(jd.atHour(hour + 1.0));\n\n            if (y_0 > 0.0) {\n                alwaysUp = true;\n            } else {\n                alwaysDown = true;\n            }\n\n            while (hour <= maxHours && hour >= minHours) {\n                QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus);\n                ye = qi.getYe();\n\n                if (qi.getNumberOfRoots() == 1) {\n                    double rt = qi.getRoot1() + hour;\n                    if (y_minus < 0.0) {\n                        if (rise == null && rt >= lowerLimitHours && rt < upperLimitHours) {\n                            rise = rt;\n                            alwaysDown = false;\n                        }\n                    } else {\n                        if (set == null && rt >= lowerLimitHours && rt < upperLimitHours) {\n                            set = rt;\n                            alwaysUp = false;\n                        }\n                    }\n                } else if (qi.getNumberOfRoots() == 2) {\n                    if (rise == null) {\n                        double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1());\n                        if (rt >= lowerLimitHours && rt < upperLimitHours) {\n                            rise = rt;\n                            alwaysDown = false;\n                        }\n                    }\n                    if (set == null) {\n                        double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2());\n                        if (rt >= lowerLimitHours && rt < upperLimitHours) {\n                            set = rt;\n                            alwaysUp = false;\n                        }\n                    }\n                }\n\n                if (rise != null && set != null) {\n                    break;\n                }\n\n                hour += hourStep;\n                if (hourStep > 0) {\n                    y_minus = y_0;\n                    y_0 = y_plus;\n                    y_plus = correctedMoonHeight(jd.atHour(hour + 1.0));\n                } else {\n                    y_plus = y_0;\n                    y_0 = y_minus;\n                    y_minus = correctedMoonHeight(jd.atHour(hour - 1.0));\n                }\n            }\n\n            return new MoonTimes(\n                    rise != null ? jd.atHour(rise).getDateTime() : null,\n                    set != null ? jd.atHour(set).getDateTime() : null,\n                    alwaysUp,\n                    alwaysDown);\n        }\n\n        /**\n         * Computes the moon height at the given date and position.\n         *\n         * @param jd {@link JulianDate} to use\n         * @return height, in radians\n         */\n        private double correctedMoonHeight(JulianDate jd) {\n            Vector pos = Moon.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad());\n            double hc = parallax(getElevation(), pos.getR())\n                            - refraction\n                            - Moon.angularRadius(pos.getR());\n            return pos.getTheta() - hc;\n        }\n    }\n\n    /**\n     * Moonrise time. {@code null} if the moon does not rise that day.\n     */\n    @Nullable\n    public ZonedDateTime getRise() {\n        return rise;\n    }\n\n    /**\n     * Moonset time. {@code null} if the moon does not set that day.\n     */\n    @Nullable\n    public ZonedDateTime getSet() {\n        return set;\n    }\n\n    /**\n     * {@code true} if the moon never rises/sets, but is always above the horizon.\n     */\n    public boolean isAlwaysUp() {\n        return alwaysUp;\n    }\n\n    /**\n     * {@code true} if the moon never rises/sets, but is always below the horizon.\n     */\n    public boolean isAlwaysDown() {\n        return alwaysDown;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"MoonTimes[rise=\").append(rise);\n        sb.append(\", set=\").append(set);\n        sb.append(\", alwaysUp=\").append(alwaysUp);\n        sb.append(\", alwaysDown=\").append(alwaysDown);\n        sb.append(']');\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/SunPosition.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.toDegrees;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.refraction;\n\nimport org.shredzone.commons.suncalc.param.Builder;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.LocationParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.util.BaseBuilder;\nimport org.shredzone.commons.suncalc.util.JulianDate;\nimport org.shredzone.commons.suncalc.util.Sun;\nimport org.shredzone.commons.suncalc.util.Vector;\n\n/**\n * Calculates the position of the sun.\n */\npublic class SunPosition {\n\n    private final double azimuth;\n    private final double altitude;\n    private final double trueAltitude;\n    private final double distance;\n\n    private SunPosition(double azimuth, double altitude, double trueAltitude, double distance) {\n        this.azimuth = (toDegrees(azimuth) + 180.0) % 360.0;\n        this.altitude = toDegrees(altitude);\n        this.trueAltitude = toDegrees(trueAltitude);\n        this.distance = distance;\n    }\n\n    /**\n     * Starts the computation of {@link SunPosition}.\n     *\n     * @return {@link Parameters} to set.\n     */\n    public static Parameters compute() {\n        return new SunPositionBuilder();\n    }\n\n    /**\n     * Collects all parameters for {@link SunPosition}.\n     */\n    public interface Parameters extends\n            GenericParameter<Parameters>,\n            LocationParameter<Parameters>,\n            TimeParameter<Parameters>,\n            Builder<SunPosition> {\n    }\n\n    /**\n     * Builder for {@link SunPosition}. Performs the computations based on the parameters,\n     * and creates a {@link SunPosition} object that holds the result.\n     */\n    private static class SunPositionBuilder extends BaseBuilder<Parameters> implements Parameters {\n        @Override\n        public SunPosition execute() {\n            if (!hasLocation()) {\n                throw new IllegalArgumentException(\"Geolocation is missing.\");\n            }\n\n            JulianDate t = getJulianDate();\n\n            Vector horizontal = Sun.positionHorizontal(t, getLatitudeRad(), getLongitudeRad());\n            double hRef = refraction(horizontal.getTheta());\n\n            return new SunPosition(horizontal.getPhi(),\n                            horizontal.getTheta() + hRef,\n                            horizontal.getTheta(),\n                            horizontal.getR());\n        }\n    }\n\n    /**\n     * The visible sun altitude above the horizon, in degrees.\n     * <p>\n     * {@code 0.0} means the sun's center is at the horizon, {@code 90.0} at the zenith\n     * (straight over your head). Atmospheric refraction is taken into account.\n     *\n     * @see #getTrueAltitude()\n     */\n    public double getAltitude() {\n        return altitude;\n    }\n\n    /**\n     * The true sun altitude above the horizon, in degrees.\n     * <p>\n     * {@code 0.0} means the sun's center is at the horizon, {@code 90.0} at the zenith\n     * (straight over your head).\n     *\n     * @see #getAltitude()\n     */\n    public double getTrueAltitude() {\n        return trueAltitude;\n    }\n\n    /**\n     * Sun azimuth, in degrees, north-based.\n     * <p>\n     * This is the direction along the horizon, measured from north to east. For example,\n     * {@code 0.0} means north, {@code 135.0} means southeast, {@code 270.0} means west.\n     */\n    public double getAzimuth() {\n        return azimuth;\n    }\n\n    /**\n     * Sun's distance, in kilometers.\n     */\n    public double getDistance() {\n        return distance;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"SunPosition[azimuth=\").append(azimuth);\n        sb.append(\"°, altitude=\").append(altitude);\n        sb.append(\"°, true altitude=\").append(trueAltitude);\n        sb.append(\"°, distance=\").append(distance).append(\" km]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/SunTimes.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.*;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.*;\n\nimport java.time.Duration;\nimport java.time.ZonedDateTime;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.commons.suncalc.param.Builder;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.LocationParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.param.WindowParameter;\nimport org.shredzone.commons.suncalc.util.BaseBuilder;\nimport org.shredzone.commons.suncalc.util.JulianDate;\nimport org.shredzone.commons.suncalc.util.QuadraticInterpolation;\nimport org.shredzone.commons.suncalc.util.Sun;\nimport org.shredzone.commons.suncalc.util.Vector;\n\n/**\n * Calculates the rise and set times of the sun.\n */\npublic class SunTimes {\n\n    private final @Nullable ZonedDateTime rise;\n    private final @Nullable ZonedDateTime set;\n    private final @Nullable ZonedDateTime noon;\n    private final @Nullable ZonedDateTime nadir;\n    private final boolean alwaysUp;\n    private final boolean alwaysDown;\n\n    private SunTimes(@Nullable ZonedDateTime rise, @Nullable ZonedDateTime set,\n                     @Nullable ZonedDateTime noon, @Nullable ZonedDateTime nadir,\n                     boolean alwaysUp, boolean alwaysDown) {\n        this.rise = rise;\n        this.set = set;\n        this.noon = noon;\n        this.nadir = nadir;\n        this.alwaysUp = alwaysUp;\n        this.alwaysDown = alwaysDown;\n    }\n\n    /**\n     * Starts the computation of {@link SunTimes}.\n     *\n     * @return {@link Parameters} to set.\n     */\n    public static Parameters compute() {\n        return new SunTimesBuilder();\n    }\n\n    /**\n     * Collects all parameters for {@link SunTimes}.\n     */\n    public interface Parameters extends\n            GenericParameter<Parameters>,\n            LocationParameter<Parameters>,\n            TimeParameter<Parameters>,\n            WindowParameter<Parameters>,\n            Builder<SunTimes> {\n\n        /**\n         * Sets the {@link Twilight} mode.\n         * <p>\n         * Defaults to {@link Twilight#VISUAL}.\n         *\n         * @param twilight\n         *            {@link Twilight} mode to be used.\n         * @return itself\n         */\n        Parameters twilight(Twilight twilight);\n\n        /**\n         * Sets the desired elevation angle of the sun. The sunrise and sunset times are\n         * referring to the moment when the center of the sun passes this angle.\n         *\n         * @param angle\n         *            Geocentric elevation angle, in degrees.\n         * @return itself\n         */\n        Parameters twilight(double angle);\n    }\n\n    /**\n     * Enumeration of predefined twilights.\n     * <p>\n     * The twilight angles use a geocentric reference, by definition. However,\n     * {@link #VISUAL} and {@link #VISUAL_LOWER} are topocentric, and take the spectator's\n     * elevation and the atmospheric refraction into account.\n     *\n     * @see <a href=\"https://en.wikipedia.org/wiki/Twilight\">Wikipedia: Twilight</a>\n     */\n    public enum Twilight {\n\n        /**\n         * The moment when the visual upper edge of the sun crosses the horizon. This is\n         * commonly referred to as \"sunrise\" and \"sunset\". Atmospheric refraction is taken\n         * into account.\n         * <p>\n         * This is the default.\n         */\n        VISUAL(0.0, 1.0),\n\n        /**\n         * The moment when the visual lower edge of the sun crosses the horizon. This is\n         * the ending of the sunrise and the starting of the sunset. Atmospheric\n         * refraction is taken into account.\n         */\n        VISUAL_LOWER(0.0, -1.0),\n\n        /**\n         * The moment when the center of the sun crosses the horizon (0°).\n         */\n        HORIZON(0.0),\n\n        /**\n         * Civil twilight (-6°).\n         */\n        CIVIL(-6.0),\n\n        /**\n         * Nautical twilight (-12°).\n         */\n        NAUTICAL(-12.0),\n\n        /**\n         * Astronomical twilight (-18°).\n         */\n        ASTRONOMICAL(-18.0),\n\n        /**\n         * Golden hour (6°). The Golden hour is between {@link #GOLDEN_HOUR} and\n         * {@link #BLUE_HOUR}. The Magic hour is between {@link #GOLDEN_HOUR} and\n         * {@link #CIVIL}.\n         *\n         * @see <a href=\n         *      \"https://en.wikipedia.org/wiki/Golden_hour_(photography)\">Wikipedia:\n         *      Golden hour</a>\n         */\n        GOLDEN_HOUR(6.0),\n\n        /**\n         * Blue hour (-4°). The Blue hour is between {@link #NIGHT_HOUR} and\n         * {@link #BLUE_HOUR}.\n         *\n         * @see <a href=\"https://en.wikipedia.org/wiki/Blue_hour\">Wikipedia: Blue hour</a>\n         */\n        BLUE_HOUR(-4.0),\n\n        /**\n         * End of Blue hour (-8°).\n         * <p>\n         * \"Night Hour\" is not an official term, but just a name that is marking the\n         * beginning/end of the Blue hour.\n         */\n        NIGHT_HOUR(-8.0);\n\n        private final double angle;\n        private final double angleRad;\n        private final @Nullable Double position;\n\n        Twilight(double angle) {\n            this(angle, null);\n        }\n\n        Twilight(double angle, @Nullable Double position) {\n            this.angle = angle;\n            this.angleRad = toRadians(angle);\n            this.position = position;\n        }\n\n        /**\n         * Returns the sun's angle at the twilight position, in degrees.\n         */\n        public double getAngle() {\n            return angle;\n        }\n\n        /**\n         * Returns the sun's angle at the twilight position, in radians.\n         */\n        public double getAngleRad() {\n            return angleRad;\n        }\n\n        /**\n         * Returns {@code true} if this twilight position is topocentric. Then the\n         * parallax and the atmospheric refraction is taken into account.\n         */\n        public boolean isTopocentric() {\n            return position != null;\n        }\n\n        /**\n         * Returns the angular position. {@code 0.0} means center of the sun. {@code 1.0}\n         * means upper edge of the sun. {@code -1.0} means lower edge of the sun.\n         * {@code null} means the angular position is not topocentric.\n         */\n        @Nullable\n        private Double getAngularPosition() {\n            return position;\n        }\n    }\n\n    /**\n     * Builder for {@link SunTimes}. Performs the computations based on the parameters,\n     * and creates a {@link SunTimes} object that holds the result.\n     */\n    private static class SunTimesBuilder extends BaseBuilder<Parameters> implements Parameters {\n        private double angle = Twilight.VISUAL.getAngleRad();\n        private @Nullable Double position = Twilight.VISUAL.getAngularPosition();\n\n        @Override\n        public Parameters twilight(Twilight twilight) {\n            this.angle = twilight.getAngleRad();\n            this.position = twilight.getAngularPosition();\n            return this;\n        }\n\n        @Override\n        public Parameters twilight(double angle) {\n            this.angle = toRadians(angle);\n            this.position = null;\n            return this;\n        }\n\n        @Override\n        public SunTimes execute() {\n            if (!hasLocation()) {\n                throw new IllegalArgumentException(\"Geolocation is missing.\");\n            }\n\n            JulianDate jd = getJulianDate();\n\n            Double rise = null;\n            Double set = null;\n            Double noon = null;\n            Double nadir = null;\n            boolean alwaysUp = false;\n            boolean alwaysDown = false;\n            double ye;\n\n            int hourStep;\n            double lowerLimitHours, upperLimitHours;\n            if (getDuration().isNegative()) {\n                hourStep = -1;\n                lowerLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);\n                upperLimitHours = 0.0;\n            } else {\n                hourStep = 1;\n                lowerLimitHours = 0.0;\n                upperLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);;\n            }\n\n            int hour = 0;\n            int minHours = (int) floor(lowerLimitHours);\n            int maxHours = (int) ceil(upperLimitHours);\n\n            double y_minus = correctedSunHeight(jd.atHour(hour - 1.0));\n            double y_0 = correctedSunHeight(jd.atHour(hour));\n            double y_plus = correctedSunHeight(jd.atHour(hour + 1.0));\n\n            if (y_0 > 0.0) {\n                alwaysUp = true;\n            } else {\n                alwaysDown = true;\n            }\n\n            while (hour <= maxHours && hour >= minHours) {\n                QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus);\n                ye = qi.getYe();\n\n                if (qi.getNumberOfRoots() == 1) {\n                    double rt = qi.getRoot1() + hour;\n                    if (y_minus < 0.0) {\n                        if (rise == null && rt >= lowerLimitHours && rt < upperLimitHours) {\n                            rise = rt;\n                            alwaysDown = false;\n                        }\n                    } else {\n                        if (set == null && rt >= lowerLimitHours && rt < upperLimitHours) {\n                            set = rt;\n                            alwaysUp = false;\n                        }\n                    }\n                } else if (qi.getNumberOfRoots() == 2) {\n                    if (rise == null) {\n                        double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1());\n                        if (rt >= lowerLimitHours && rt < upperLimitHours) {\n                            rise = rt;\n                            alwaysDown = false;\n                        }\n                    }\n                    if (set == null) {\n                        double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2());\n                        if (rt >= lowerLimitHours && rt < upperLimitHours) {\n                            set = rt;\n                            alwaysUp = false;\n                        }\n                    }\n                }\n\n                double xeAbs = abs(qi.getXe());\n                if (xeAbs <= 1.0) {\n                    double xeHour = qi.getXe() + hour;\n                    if (hourStep > 0 ? xeHour >= 0.0 : xeHour <= 0.0) {\n                        if (qi.isMaximum()) {\n                            if (noon == null) {\n                                noon = xeHour;\n                            }\n                        } else {\n                            if (nadir == null) {\n                                nadir = xeHour;\n                            }\n                        }\n                    }\n                }\n\n                if (rise != null && set != null && noon != null && nadir != null) {\n                    break;\n                }\n\n                hour += hourStep;\n                if (hourStep > 0) {\n                    y_minus = y_0;\n                    y_0 = y_plus;\n                    y_plus = correctedSunHeight(jd.atHour(hour + 1.0));\n                } else {\n                    y_plus = y_0;\n                    y_0 = y_minus;\n                    y_minus = correctedSunHeight(jd.atHour(hour - 1.0));\n                }\n            }\n\n            if (noon != null) {\n                noon = readjustMax(noon, 2.0, 14, t -> correctedSunHeight(jd.atHour(t)));\n                if (noon < lowerLimitHours || noon >= upperLimitHours) {\n                    noon = null;\n                }\n            }\n\n            if (nadir != null) {\n                nadir = readjustMin(nadir, 2.0, 14, t -> correctedSunHeight(jd.atHour(t)));\n                if (nadir < lowerLimitHours || nadir >= upperLimitHours) {\n                    nadir = null;\n                }\n            }\n\n            return new SunTimes(\n                    rise != null ? jd.atHour(rise).getDateTime() : null,\n                    set != null ? jd.atHour(set).getDateTime() : null,\n                    noon != null ? jd.atHour(noon).getDateTime() : null,\n                    nadir != null ? jd.atHour(nadir).getDateTime() : null,\n                    alwaysUp,\n                    alwaysDown\n                );\n        }\n\n        /**\n         * Computes the sun height at the given date and position.\n         *\n         * @param jd {@link JulianDate} to use\n         * @return height, in radians\n         */\n        private double correctedSunHeight(JulianDate jd) {\n            Vector pos = Sun.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad());\n\n            double hc = angle;\n            if (position != null) {\n                hc -= apparentRefraction(hc);\n                hc += parallax(getElevation(), pos.getR());\n                hc -= position * Sun.angularRadius(pos.getR());\n            }\n\n            return pos.getTheta() - hc;\n        }\n    }\n\n    /**\n     * Sunrise time. {@code null} if the sun does not rise that day.\n     * <p>\n     * Always returns a sunrise time if {@link Parameters#fullCycle()} was set.\n     */\n    @Nullable\n    public ZonedDateTime getRise() {\n        return rise;\n    }\n\n    /**\n     * Sunset time. {@code null} if the sun does not set that day.\n     * <p>\n     * Always returns a sunset time if {@link Parameters#fullCycle()} was set.\n     */\n    @Nullable\n    public ZonedDateTime getSet() {\n        return set;\n    }\n\n    /**\n     * The time when the sun reaches its highest point.\n     * <p>\n     * Use {@link #isAlwaysDown()} to find out if the highest point is still below the\n     * twilight angle.\n     */\n    @Nullable\n    public ZonedDateTime getNoon() {\n        return noon;\n    }\n\n    /**\n     * The time when the sun reaches its lowest point.\n     * <p>\n     * Use {@link #isAlwaysUp()} to find out if the lowest point is still above the\n     * twilight angle.\n     */\n    @Nullable\n    public ZonedDateTime getNadir() {\n        return nadir;\n    }\n\n    /**\n     * {@code true} if the sun never rises/sets, but is always above the twilight angle.\n     */\n    public boolean isAlwaysUp() {\n        return alwaysUp;\n    }\n\n    /**\n     * {@code true} if the sun never rises/sets, but is always below the twilight angle.\n     */\n    public boolean isAlwaysDown() {\n        return alwaysDown;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"SunTimes[rise=\").append(rise);\n        sb.append(\", set=\").append(set);\n        sb.append(\", noon=\").append(noon);\n        sb.append(\", nadir=\").append(nadir);\n        sb.append(\", alwaysUp=\").append(alwaysUp);\n        sb.append(\", alwaysDown=\").append(alwaysDown);\n        sb.append(']');\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/package-info.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This is the main package. It contains classes for calculating sun and moon data.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.commons.suncalc;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/param/Builder.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.param;\n\n/**\n * An interface for the method that eventually executes the calculation.\n *\n * @param <T>\n *            Result type\n */\npublic interface Builder<T> {\n\n    /**\n     * Executes the calculation and returns the desired result.\n     * <p>\n     * The resulting object is immutable. You can change parameters, and then invoke\n     * {@link #execute()} again, to get a new object with new results.\n     *\n     * @return Result of the calculation.\n     */\n    T execute();\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/param/GenericParameter.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.param;\n\n/**\n * Generic parameters and options.\n *\n * @param <T>\n *            Type of the final builder\n */\npublic interface GenericParameter<T> {\n\n    /**\n     * Creates a copy of the current parameters. The copy can be changed independently.\n     */\n    T copy();\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/param/LocationParameter.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.param;\n\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.dms;\n\n/**\n * Location based parameters.\n * <p>\n * Use them to give information about the geolocation of the observer. If ommitted, the\n * coordinates of <a href=\"https://en.wikipedia.org/wiki/Null_Island\">Null Island</a> are\n * used.\n *\n * @param <T>\n *            Type of the final builder\n */\n@SuppressWarnings(\"unchecked\")\npublic interface LocationParameter<T> {\n\n    /**\n     * Sets the latitude.\n     *\n     * @param lat\n     *            Latitude, in degrees.\n     * @return itself\n     */\n    T latitude(double lat);\n\n    /**\n     * Sets the longitude.\n     *\n     * @param lng\n     *            Longitude, in degrees.\n     * @return itself\n     */\n    T longitude(double lng);\n\n    /**\n     * Sets the elevation.\n     *\n     * @param h\n     *            Elevation, in meters above sea level. Default: 0.0 m. Negative values\n     *            are silently changed to the acceptable minimum of 0.0 m.\n     * @return itself\n     * @see #elevationFt(double)\n     * @since 3.9\n     */\n    T elevation(double h);\n\n    /**\n     * Sets the elevation, in foot.\n     *\n     * @param ft\n     *            Elevation, in foot above sea level. Default: 0.0 ft. Negative values are\n     *            silently changed to the acceptable minimum of 0.0 ft.\n     * @return itself\n     * @see #elevation(double)\n     * @since 3.9\n     */\n    default T elevationFt(double ft) {\n        return elevation(ft * 0.3048);\n    }\n\n    /**\n     * Sets the height.\n     *\n     * @param h\n     *            Height, in meters above sea level. Default: 0.0 m. Negative values are\n     *            silently changed to the acceptable minimum of 0.0 m.\n     * @return itself\n     * @deprecated Use {@link #elevation(double)} instead.\n     */\n    @Deprecated\n    default T height(double h) {\n        return elevation(h);\n    }\n\n    /**\n     * Sets the height, in foot.\n     *\n     * @param ft\n     *            Height, in foot above sea level. Default: 0.0 ft. Negative values are\n     *            silently changed to the acceptable minimum of 0.0 ft.\n     * @return itself\n     * @since 3.8\n     * @deprecated Use {@link #elevationFt(double)} instead.\n     */\n    @Deprecated\n    default T heightFt(double ft) {\n        return elevationFt(ft);\n    }\n\n    /**\n     * Sets the geolocation.\n     *\n     * @param lat\n     *            Latitude, in degrees.\n     * @param lng\n     *            Longitude, in degrees.\n     * @return itself\n     */\n    default T at(double lat, double lng) {\n        latitude(lat);\n        longitude(lng);\n        return (T) this;\n    }\n\n    /**\n     * Sets the geolocation. In the given array, index 0 must contain the latitude, and\n     * index 1 must contain the longitude. An optional index 2 may contain the elevation,\n     * in meters.\n     * <p>\n     * This call is meant to be used for coordinates stored in constants.\n     *\n     * @param coords\n     *            Array containing the latitude and longitude, in degrees.\n     * @return itself\n     */\n    default T at(double[] coords) {\n        if (coords.length != 2 && coords.length != 3) {\n            throw new IllegalArgumentException(\"Array must contain 2 or 3 doubles\");\n        }\n        if (coords.length == 3) {\n            elevation(coords[2]);\n        }\n        return at(coords[0], coords[1]);\n    }\n\n    /**\n     * Sets the latitude.\n     *\n     * @param d\n     *            Degrees\n     * @param m\n     *            Minutes\n     * @param s\n     *            Seconds (and fraction of seconds)\n     * @return itself\n     */\n    default T latitude(int d, int m, double s) {\n        return latitude(dms(d, m, s));\n    }\n\n    /**\n     * Sets the longitude.\n     *\n     * @param d\n     *            Degrees\n     * @param m\n     *            Minutes\n     * @param s\n     *            Seconds (and fraction of seconds)\n     * @return itself\n     */\n    default T longitude(int d, int m, double s) {\n        return longitude(dms(d, m, s));\n    }\n\n    /**\n     * Uses the same location as given in the {@link LocationParameter} at this moment.\n     * <p>\n     * Changes to the source parameter will not affect this parameter, though.\n     *\n     * @param l  {@link LocationParameter} to be used.\n     * @return itself\n     */\n    T sameLocationAs(LocationParameter<?> l);\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/param/TimeParameter.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.param;\n\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Objects;\nimport java.util.TimeZone;\n\n/**\n * Time based parameters.\n * <p>\n * Use them to give information about the desired time. If ommitted, the current time and\n * the system's time zone is used.\n *\n * @param <T>\n *            Type of the final builder\n */\n@SuppressWarnings(\"unchecked\")\npublic interface TimeParameter<T> {\n\n    /**\n     * Sets date and time. Note that also seconds can be passed in for convenience, but\n     * the results are not that accurate.\n     *\n     * @param year\n     *            Year\n     * @param month\n     *            Month (1 = January, 2 = February, ...)\n     * @param date\n     *            Day of month\n     * @param hour\n     *            Hour of day\n     * @param minute\n     *            Minute\n     * @param second\n     *            Second\n     * @return itself\n     */\n    T on(int year, int month, int date, int hour, int minute, int second);\n\n    /**\n     * Sets midnight of the year, month and date.\n     *\n     * @param year\n     *            Year\n     * @param month\n     *            Month (1 = January, 2 = February, ...)\n     * @param date\n     *            Day of month\n     * @return itself\n     */\n    default T on(int year, int month, int date) {\n        return on(year, month, date, 0, 0, 0);\n    }\n\n    /**\n     * Uses the given {@link ZonedDateTime} instance.\n     *\n     * @param dateTime\n     *            {@link ZonedDateTime} to be used.\n     * @return itself\n     */\n    T on(ZonedDateTime dateTime);\n\n    /**\n     * Uses the given {@link LocalDateTime} instance.\n     *\n     * @param dateTime\n     *         {@link LocalDateTime} to be used.\n     * @return itself\n     */\n    T on(LocalDateTime dateTime);\n\n    /**\n     * Uses the given {@link LocalDate} instance, and assumes midnight.\n     *\n     * @param date\n     *         {@link LocalDate} to be used.\n     * @return itself\n     */\n    T on(LocalDate date);\n\n    /**\n     * Uses the given {@link Instant} instance.\n     *\n     * @param instant\n     *            {@link Instant} to be used.\n     * @return itself\n     */\n    T on(Instant instant);\n\n    /**\n     * Uses the given {@link Date} instance.\n     *\n     * @param date\n     *         {@link Date} to be used.\n     * @return itself\n     */\n    default T on(Date date) {\n        Objects.requireNonNull(date, \"date\");\n        return on(date.toInstant());\n    }\n\n    /**\n     * Uses the given {@link Calendar} instance.\n     *\n     * @param cal\n     *         {@link Calendar} to be used\n     * @return itself\n     */\n    default T on(Calendar cal) {\n        Objects.requireNonNull(cal, \"cal\");\n        return on(ZonedDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId()));\n    }\n\n    /**\n     * Sets the current date and time. This is the default.\n     *\n     * @return itself\n     */\n    T now();\n\n    /**\n     * Sets the time to the start of the current date (\"last midnight\").\n     *\n     * @return itself\n     */\n    T midnight();\n\n    /**\n     * Adds a number of days to the current date.\n     *\n     * @param days\n     *            Number of days to add\n     * @return itself\n     */\n    T plusDays(int days);\n\n    /**\n     * Sets today, midnight.\n     * <p>\n     * It is the same as <code>now().midnight()</code>.\n     *\n     * @return itself\n     */\n    default T today() {\n        now();\n        midnight();\n        return (T) this;\n    }\n\n    /**\n     * Sets tomorrow, midnight.\n     * <p>\n     * It is the same as <code>now().midnight().plusDays(1)</code>.\n     *\n     * @return itself\n     */\n    default T tomorrow() {\n        today();\n        plusDays(1);\n        return (T) this;\n    }\n\n    /**\n     * Sets the given {@link ZoneId}. The local time is retained, so the parameter order\n     * is not important.\n     *\n     * @param tz\n     *            {@link ZoneId} to be used.\n     * @return itself\n     */\n    T timezone(ZoneId tz);\n\n    /**\n     * Sets the given timezone. This is a convenience method that just invokes\n     * {@link ZoneId#of(String)}.\n     *\n     * @param id\n     *            ID of the time zone.\n     * @return itself\n     * @see ZoneId#of(String)\n     */\n    default T timezone(String id) {\n        return timezone(ZoneId.of(id));\n    }\n\n    /**\n     * Sets the system's timezone. This is the default.\n     *\n     * @return itself\n     */\n    default T localTime() {\n        return timezone(ZoneId.systemDefault());\n    }\n\n    /**\n     * Sets the time zone to UTC.\n     *\n     * @return itself\n     */\n    default T utc() {\n        return timezone(\"UTC\");\n    }\n\n    /**\n     * Sets the {@link TimeZone}.\n     *\n     * @param tz {@link TimeZone} to be used\n     * @return itself\n     */\n    default T timezone(TimeZone tz) {\n        Objects.requireNonNull(tz, \"tz\");\n        return timezone(tz.toZoneId());\n    }\n\n    /**\n     * Uses the same time as given in the {@link TimeParameter}.\n     * <p>\n     * Changes to the source parameter will not affect this parameter, though.\n     *\n     * @param t  {@link TimeParameter} to be used.\n     * @return itself\n     */\n    T sameTimeAs(TimeParameter<?> t);\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/param/WindowParameter.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2024 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.param;\n\nimport java.time.Duration;\n\n/**\n * Time window based parameters.\n * <p>\n * Use them to give information about the desired time window. If ommitted, a forward\n * window of 365 days is assumed.\n *\n * @since 3.11\n * @param <T>\n *            Type of the final builder\n */\npublic interface WindowParameter<T> {\n\n    /**\n     * Limits the calculation window to the given {@link Duration}.\n     *\n     * @param duration\n     *         Duration of the calculation window. A negative duration sets a reverse time\n     *         window, giving result times in the past.\n     * @return itself\n     */\n    T limit(Duration duration);\n\n    /**\n     * Limits the time window to the next 24 hours.\n     *\n     * @return itself\n     */\n    default T oneDay() {\n        return limit(Duration.ofDays(1L));\n    }\n\n    /**\n     * Computes until all times are found.\n     * <p>\n     * This is the default.\n     *\n     * @return itself\n     */\n    default T fullCycle() {\n        return limit(Duration.ofDays(365L));\n    }\n\n    /**\n     * Sets a reverse calculation window. It will end at the given date.\n     *\n     * @return itself\n     * @since 3.11\n     */\n    T reverse();\n\n    /**\n     * Sets a forward calculation window. It will start at the given date.\n     * <p>\n     * This is the default.\n     *\n     * @return itself\n     * @since 3.11\n     */\n    T forward();\n\n    /**\n     * Uses the same window as given in the {@link WindowParameter}.\n     * <p>\n     * Changes to the source parameter will not affect this parameter, though.\n     *\n     * @param w\n     *         {@link WindowParameter} to be used.\n     * @return itself\n     * @since 3.11\n     */\n    T sameWindowAs(WindowParameter<?> w);\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/param/package-info.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains interfaces for setting common calculation parameters.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.commons.suncalc.param;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/BaseBuilder.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.toRadians;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Objects;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport org.shredzone.commons.suncalc.param.GenericParameter;\nimport org.shredzone.commons.suncalc.param.LocationParameter;\nimport org.shredzone.commons.suncalc.param.TimeParameter;\nimport org.shredzone.commons.suncalc.param.WindowParameter;\n\n/**\n * A base implementation of {@link LocationParameter}, {@link TimeParameter}, and\n * {@link WindowParameter}.\n * <p>\n * For internal use only.\n *\n * @param <T>\n *         Type of the final builder\n */\n@SuppressWarnings(\"unchecked\")\npublic class BaseBuilder<T> implements GenericParameter<T>, LocationParameter<T>,\n        TimeParameter<T>, WindowParameter<T>, Cloneable {\n\n    private @Nullable Double lat = null;\n    private @Nullable Double lng = null;\n    private double elevation = 0.0;\n    private ZonedDateTime dateTime = ZonedDateTime.now();\n    private boolean reverse = false;\n    private Duration duration = Duration.ofDays(365L);\n\n    @Override\n    public T on(ZonedDateTime dateTime) {\n        this.dateTime = Objects.requireNonNull(dateTime, \"dateTime\");\n        return (T) this;\n    }\n\n    @Override\n    public T on(LocalDateTime dateTime) {\n        Objects.requireNonNull(dateTime, \"dateTime\");\n        return on(ZonedDateTime.of(dateTime, this.dateTime.getZone()));\n    }\n\n    @Override\n    public T on(LocalDate date) {\n        Objects.requireNonNull(date, \"date\");\n        return on(ZonedDateTime.of(date, LocalTime.MIDNIGHT, dateTime.getZone()));\n    }\n\n    @Override\n    public T on(Instant instant) {\n        Objects.requireNonNull(instant, \"instant\");\n        return on(ZonedDateTime.ofInstant(instant, dateTime.getZone()));\n    }\n\n    @Override\n    public T on(int year, int month, int date, int hour, int minute, int second) {\n        return on(ZonedDateTime.of(year, month, date, hour, minute, second, 0, dateTime.getZone()));\n    }\n\n    @Override\n    public T now() {\n        return on(ZonedDateTime.now(dateTime.getZone()));\n    }\n\n    @Override\n    public T plusDays(int days) {\n        return on(dateTime.plusDays(days));\n    }\n\n    @Override\n    public T midnight() {\n        return on(dateTime.truncatedTo(ChronoUnit.DAYS));\n    }\n\n    @Override\n    public T timezone(ZoneId tz) {\n        Objects.requireNonNull(tz, \"tz\");\n        on(dateTime.withZoneSameLocal(tz));\n        return (T) this;\n    }\n\n    @Override\n    public T latitude(double lat) {\n        if (lat < -90.0 || lat > 90.0) {\n            throw new IllegalArgumentException(\"Latitude out of range, -90.0 <= \" + lat + \" <= 90.0\");\n        }\n        this.lat = lat;\n        return (T) this;\n    }\n\n    @Override\n    public T longitude(double lng) {\n        if (lng < -180.0 || lng > 180.0) {\n            throw new IllegalArgumentException(\"Longitude out of range, -180.0 <= \" + lng + \" <= 180.0\");\n        }\n        this.lng = lng;\n        return (T) this;\n    }\n\n    @Override\n    public T elevation(double h) {\n        this.elevation = max(h, 0.0);\n        return (T) this;\n    }\n\n    public T limit(Duration duration) {\n        Objects.requireNonNull(duration, \"duration\");\n        this.duration = duration;\n        if (duration.isNegative()) {\n            reverse();\n        }\n        return (T) this;\n    }\n\n    public T reverse() {\n        reverse = true;\n        return (T) this;\n    }\n\n    public T forward() {\n        reverse = false;\n        return (T) this;\n    }\n\n    @Override\n    public T sameTimeAs(TimeParameter<?> t) {\n        if (! (t instanceof BaseBuilder)) {\n            throw new IllegalArgumentException(\"Cannot read the TimeParameter\");\n        }\n        this.dateTime = ((BaseBuilder<?>) t).dateTime;\n        return (T) this;\n    }\n\n    @Override\n    public T sameLocationAs(LocationParameter<?> l) {\n        if (! (l instanceof BaseBuilder)) {\n            throw new IllegalArgumentException(\"Cannot read the LocationParameter\");\n        }\n        BaseBuilder<?> origin = (BaseBuilder<?>) l;\n        this.lat = origin.lat;\n        this.lng = origin.lng;\n        this.elevation = origin.elevation;\n        return (T) this;\n    }\n\n    @Override\n    public T sameWindowAs(WindowParameter<?> w) {\n        if (! (w instanceof BaseBuilder)) {\n            throw new IllegalArgumentException(\"Cannot read the WindowParameter\");\n        }\n        BaseBuilder<?> origin = (BaseBuilder<?>) w;\n        this.duration = origin.duration;\n        this.reverse = origin.reverse;\n        return (T) this;\n    }\n\n    @Override\n    public T copy() {\n        try {\n            return (T) clone();\n        } catch (CloneNotSupportedException ex) {\n            throw new RuntimeException(ex); // Should never be thrown anyway\n        }\n    }\n\n    /**\n     * Returns the longitude.\n     *\n     * @return Longitude, in degrees.\n     */\n    public double getLongitude() {\n        if (lng == null) {\n            throw new IllegalStateException(\"longitude is not set\");\n        }\n        return lng;\n    }\n\n    /**\n     * Returns the latitude.\n     *\n     * @return Latitude, in degrees.\n     */\n    public double getLatitude() {\n        if (lat == null) {\n            throw new IllegalStateException(\"latitude is not set\");\n        }\n        return lat;\n    }\n\n    /**\n     * Returns the longitude.\n     *\n     * @return Longitude, in radians.\n     */\n    public double getLongitudeRad() {\n        return toRadians(getLongitude());\n    }\n\n    /**\n     * Returns the latitude.\n     *\n     * @return Latitude, in radians.\n     */\n    public double getLatitudeRad() {\n        return toRadians(getLatitude());\n    }\n\n    /**\n     * Returns the elevation, in meters above sea level.\n     *\n     * @return Elevation, meters above sea level\n     */\n    public double getElevation() {\n        return elevation;\n    }\n\n    /**\n     * Returns the {@link JulianDate} to be used.\n     *\n     * @return {@link JulianDate}\n     */\n    public JulianDate getJulianDate() {\n        return new JulianDate(dateTime);\n    }\n\n    /**\n     * Returns {@code true} if a geolocation has been set.\n     *\n     * @since 3.9\n     */\n    public boolean hasLocation() {\n        return lat != null && lng != null;\n    }\n\n    /**\n     * Unset the geolocation.\n     *\n     * @since 3.9\n     */\n    public void clearLocation() {\n        lat = null;\n        lng = null;\n    }\n\n    /**\n     * Returns the duration of the time window.\n     *\n     * @since 3.11\n     */\n    public Duration getDuration() {\n        if (reverse != duration.isNegative()) {\n            return duration.negated();\n        }\n        return duration;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/ExtendedMath.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.*;\n\nimport java.util.Comparator;\nimport java.util.function.Function;\n\n/**\n * Contains constants and mathematical operations that are not available in {@link Math}.\n */\npublic final class ExtendedMath {\n\n    /**\n     * PI * 2\n     */\n    public static final double PI2 = PI * 2.0;\n\n    /**\n     * Arc-Seconds per Radian.\n     */\n    public static final double ARCS = toDegrees(3600.0);\n\n    /**\n     * Mean radius of the earth, in kilometers.\n     */\n    public static final double EARTH_MEAN_RADIUS = 6371.0;\n\n    /**\n     * Refraction at the horizon, in radians.\n     */\n    public static final double REFRACTION_AT_HORIZON = PI / (tan(toRadians(7.31 / 4.4)) * 10800.0);\n\n    private ExtendedMath() {\n        // utility class without constructor\n    }\n\n    /**\n     * Returns the decimal part of a value.\n     *\n     * @param a\n     *            Value\n     * @return Fraction of that value. It has the same sign as the input value.\n     */\n    public static double frac(double a) {\n        return a % 1.0;\n    }\n\n    /**\n     * Performs a safe check if the given double is actually zero (0.0).\n     * <p>\n     * Note that \"almost zero\" returns {@code false}, so this method should not be used\n     * for comparing calculation results to zero.\n     *\n     * @param d\n     *            double to check for zero.\n     * @return {@code true} if the value was zero, or negative zero.\n     */\n    public static boolean isZero(double d) {\n        // This should keep squid:S1244 silent...\n        return !Double.isNaN(d) && round(signum(d)) == 0L;\n    }\n\n    /**\n     * Converts equatorial coordinates to horizontal coordinates.\n     *\n     * @param tau\n     *            Hour angle (radians)\n     * @param dec\n     *            Declination (radians)\n     * @param dist\n     *            Distance of the object\n     * @param lat\n     *            Latitude of the observer (radians)\n     * @return {@link Vector} containing the horizontal coordinates\n     */\n    public static Vector equatorialToHorizontal(double tau, double dec, double dist, double lat) {\n        return Matrix.rotateY(PI / 2.0 - lat).multiply(Vector.ofPolar(tau, dec, dist));\n    }\n\n    /**\n     * Creates a rotational {@link Matrix} for converting equatorial to ecliptical\n     * coordinates.\n     *\n     * @param t\n     *            {@link JulianDate} to use\n     * @return {@link Matrix} for converting equatorial to ecliptical coordinates\n     */\n    public static Matrix equatorialToEcliptical(JulianDate t) {\n        double jc = t.getJulianCentury();\n        double eps = toRadians(23.43929111 - (46.8150 + (0.00059 - 0.001813 * jc) * jc) * jc / 3600.0);\n        return Matrix.rotateX(eps);\n    }\n\n    /**\n     * Returns the parallax for objects at the horizon.\n     *\n     * @param elevation\n     *            Observer's elevation, in meters above sea level. Must not be negative.\n     * @param distance\n     *            Distance of the object, in kilometers.\n     * @return parallax, in radians\n     */\n    public static double parallax(double elevation, double distance) {\n        return asin(EARTH_MEAN_RADIUS / distance)\n             - acos(EARTH_MEAN_RADIUS / (EARTH_MEAN_RADIUS + (elevation / 1000.0)));\n    }\n\n    /**\n     * Calculates the atmospheric refraction of an object at the given apparent altitude.\n     * <p>\n     * The result is only valid for positive altitude angles. If negative, 0.0 is\n     * returned.\n     * <p>\n     * Assumes an atmospheric pressure of 1010 hPa and a temperature of 10 °C.\n     *\n     * @param ha\n     *            Apparent altitude, in radians.\n     * @return Refraction at this altitude\n     * @see <a href=\"https://en.wikipedia.org/wiki/Atmospheric_refraction\">Wikipedia:\n     *      Atmospheric Refraction</a>\n     */\n    public static double apparentRefraction(double ha) {\n        if (ha < 0.0) {\n            return 0.0;\n        }\n\n        if (isZero(ha)) {\n            return REFRACTION_AT_HORIZON;\n        }\n\n        return PI / (tan(toRadians(ha + (7.31 / (ha + 4.4)))) * 10800.0);\n    }\n\n    /**\n     * Calculates the atmospheric refraction of an object at the given altitude.\n     * <p>\n     * The result is only valid for positive altitude angles. If negative, 0.0 is\n     * returned.\n     * <p>\n     * Assumes an atmospheric pressure of 1010 hPa and a temperature of 10 °C.\n     *\n     * @param h\n     *            True altitude, in radians.\n     * @return Refraction at this altitude\n     * @see <a href=\"https://en.wikipedia.org/wiki/Atmospheric_refraction\">Wikipedia:\n     *      Atmospheric Refraction</a>\n     */\n    public static double refraction(double h) {\n        if (h < 0.0) {\n            return 0.0;\n        }\n\n        // refraction formula, converted to radians\n        return 0.000296706 / tan(h + 0.00312537 / (h + 0.0890118));\n    }\n\n    /**\n     * Converts dms to double.\n     *\n     * @param d\n     *            Degrees. Sign is used for result.\n     * @param m\n     *            Minutes. Sign is ignored.\n     * @param s\n     *            Seconds and fractions. Sign is ignored.\n     * @return angle, in degrees\n     */\n    public static double dms(int d, int m, double s) {\n        double sig = d < 0 ? -1.0 : 1.0;\n        return sig * ((abs(s) / 60.0 + abs(m)) / 60.0 + abs(d));\n    }\n\n    /**\n     * Locates the true maximum within the given time frame.\n     *\n     * @param time\n     *         Base time\n     * @param frame\n     *         Time frame, which is added to and subtracted from the base time for the\n     *         interval\n     * @param depth\n     *         Maximum recursion depth. For each recursion, the function is invoked once.\n     * @param f\n     *         Function to be used for calculation\n     * @return time of the true maximum\n     */\n    public static double readjustMax(double time, double frame, int depth, Function<Double, Double> f) {\n        double left = time - frame;\n        double right = time + frame;\n        double leftY = f.apply(left);\n        double rightY = f.apply(right);\n\n        return readjustInterval(left, right, leftY, rightY, depth, f, Double::compare);\n    }\n\n    /**\n     * Locates the true minimum within the given time frame.\n     *\n     * @param time\n     *         Base time\n     * @param frame\n     *         Time frame, which is added to and subtracted from the base time for the\n     *         interval\n     * @param depth\n     *         Maximum recursion depth. For each recursion, the function is invoked once.\n     * @param f\n     *         Function to be used for calculation\n     * @return time of the true minimum\n     */\n    public static double readjustMin(double time, double frame, int depth, Function<Double, Double> f) {\n        double left = time - frame;\n        double right = time + frame;\n        double leftY = f.apply(left);\n        double rightY = f.apply(right);\n\n        return readjustInterval(left, right, leftY, rightY, depth, f, (yl, yr) -> Double.compare(yr, yl));\n    }\n\n    /**\n     * Recursively find the true maximum/minimum within the given time frame.\n     *\n     * @param left\n     *         Left interval border\n     * @param right\n     *         Right interval border\n     * @param yl\n     *         Function result at the left interval\n     * @param yr\n     *         Function result at the right interval\n     * @param depth\n     *         Maximum recursion depth. For each recursion, the function is invoked once.\n     * @param f\n     *         Function to invoke\n     * @param cmp\n     *         Comparator to decide whether the left or right side of the interval half is\n     *         to be used\n     * @return Position of the approximated minimum/maximum\n     */\n    private static double readjustInterval(double left, double right, double yl, double yr, int depth,\n                                           Function<Double, Double> f, Comparator<Double> cmp) {\n        if (depth <= 0) {\n            return (cmp.compare(yl, yr) < 0) ? right : left;\n        }\n\n        double middle = (left + right) / 2.0;\n        double ym = f.apply(middle);\n        if (cmp.compare(yl, yr) < 0) {\n            return readjustInterval(middle, right, ym, yr, depth - 1, f, cmp);\n        } else {\n            return readjustInterval(left, middle, yl, ym, depth - 1, f, cmp);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/JulianDate.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.floor;\nimport static java.lang.Math.round;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.PI2;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.frac;\n\nimport java.time.Instant;\nimport java.time.ZonedDateTime;\nimport java.util.Objects;\n\n/**\n * This class contains a Julian Date representation of a date.\n * <p>\n * Objects are immutable and threadsafe.\n */\npublic class JulianDate {\n\n    private final ZonedDateTime dateTime;\n    private final double mjd;\n\n    /**\n     * Creates a new {@link JulianDate}.\n     *\n     * @param time\n     *            {@link ZonedDateTime} to use for the date.\n     */\n    public JulianDate(ZonedDateTime time) {\n        dateTime = Objects.requireNonNull(time, \"time\");\n        mjd = dateTime.toInstant().toEpochMilli() / 86400000.0 + 40587.0;\n    }\n\n    /**\n     * Returns a {@link JulianDate} of the current date and the given hour.\n     *\n     * @param hour\n     *            Hour of this date. This is a floating point value. Fractions are used\n     *            for minutes and seconds.\n     * @return {@link JulianDate} instance.\n     */\n    public JulianDate atHour(double hour) {\n        return new JulianDate(dateTime.plusSeconds(round(hour * 60.0 * 60.0)));\n    }\n\n    /**\n     * Returns a {@link JulianDate} of the given modified Julian date.\n     *\n     * @param mjd\n     *            Modified Julian Date\n     * @return {@link JulianDate} instance.\n     */\n    public JulianDate atModifiedJulianDate(double mjd) {\n        Instant mjdi = Instant.ofEpochMilli(Math.round((mjd - 40587.0) * 86400000.0));\n        return new JulianDate(ZonedDateTime.ofInstant(mjdi, dateTime.getZone()));\n    }\n\n    /**\n     * Returns a {@link JulianDate} of the given Julian century.\n     *\n     * @param jc\n     *            Julian Century\n     * @return {@link JulianDate} instance.\n     */\n    public JulianDate atJulianCentury(double jc) {\n        return atModifiedJulianDate(jc * 36525.0 + 51544.5);\n    }\n\n    /**\n     * Returns this {@link JulianDate} as {@link ZonedDateTime} object.\n     *\n     * @return {@link ZonedDateTime} of this {@link JulianDate}.\n     */\n    public ZonedDateTime getDateTime() {\n        return dateTime;\n    }\n\n    /**\n     * Returns the Modified Julian Date.\n     *\n     * @return Modified Julian Date, UTC.\n     */\n    public double getModifiedJulianDate() {\n        return mjd;\n    }\n\n    /**\n     * Returns the Julian Centuries.\n     *\n     * @return Julian Centuries, based on J2000 epoch, UTC.\n     */\n    public double getJulianCentury() {\n        return (mjd - 51544.5) / 36525.0;\n    }\n\n    /**\n     * Returns the Greenwich Mean Sidereal Time of this Julian Date.\n     *\n     * @return GMST\n     */\n    public double getGreenwichMeanSiderealTime() {\n        final double secs = 86400.0;\n\n        double mjd0 = floor(mjd);\n        double ut = (mjd - mjd0) * secs;\n        double t0 = (mjd0 - 51544.5) / 36525.0;\n        double t = (mjd - 51544.5) / 36525.0;\n\n        double gmst = 24110.54841\n                + 8640184.812866 * t0\n                + 1.0027379093 * ut\n                + (0.093104 - 6.2e-6 * t) * t * t;\n\n        return (PI2 / secs) * (gmst % secs);\n    }\n\n    /**\n     * Returns the earth's true anomaly of the current date.\n     * <p>\n     * A simple approximation is used here.\n     *\n     * @return True anomaly, in radians\n     */\n    public double getTrueAnomaly() {\n        return PI2 * frac((dateTime.getDayOfYear() - 5.0) / 365.256363);\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"%dd %02dh %02dm %02ds\",\n                (long) mjd,\n                (long) (mjd * 24 % 24),\n                (long) (mjd * 24 * 60 % 60),\n                (long) (mjd * 24 * 60 * 60 % 60));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/Matrix.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.cos;\nimport static java.lang.Math.sin;\n\nimport java.util.Arrays;\n\n/**\n * A three dimensional matrix.\n * <p>\n * Objects are immutable and threadsafe.\n */\npublic class Matrix {\n\n    private final double[] mx;\n\n    private Matrix() {\n        mx = new double[9];\n    }\n\n    private Matrix(double... values) {\n        if (values == null || values.length != 9) {\n            throw new IllegalArgumentException(\"requires 9 values\");\n        }\n        mx = values;\n    }\n\n    /**\n     * Creates an identity matrix.\n     *\n     * @return Identity {@link Matrix}\n     */\n    public static Matrix identity() {\n        return new Matrix(\n            1.0, 0.0, 0.0,\n            0.0, 1.0, 0.0,\n            0.0, 0.0, 1.0);\n    }\n\n    /**\n     * Creates a matrix that rotates a vector by the given angle at the X axis.\n     *\n     * @param angle\n     *            angle, in radians\n     * @return Rotation {@link Matrix}\n     */\n    public static Matrix rotateX(double angle) {\n        double s = sin(angle);\n        double c = cos(angle);\n        return new Matrix(\n            1.0, 0.0, 0.0,\n            0.0,   c,   s,\n            0.0,  -s,   c\n        );\n    }\n\n    /**\n     * Creates a matrix that rotates a vector by the given angle at the Y axis.\n     *\n     * @param angle\n     *            angle, in radians\n     * @return Rotation {@link Matrix}\n     */\n    public static Matrix rotateY(double angle) {\n        double s = sin(angle);\n        double c = cos(angle);\n        return new Matrix(\n              c, 0.0,  -s,\n            0.0, 1.0, 0.0,\n              s, 0.0,   c\n        );\n    }\n\n    /**\n     * Creates a matrix that rotates a vector by the given angle at the Z axis.\n     *\n     * @param angle\n     *            angle, in radians\n     * @return Rotation {@link Matrix}\n     */\n    public static Matrix rotateZ(double angle) {\n        double s = sin(angle);\n        double c = cos(angle);\n        return new Matrix(\n              c,   s, 0.0,\n             -s,   c, 0.0,\n            0.0, 0.0, 1.0\n        );\n    }\n\n    /**\n     * Transposes this matrix.\n     *\n     * @return {@link Matrix} that is a transposition of this matrix.\n     */\n    public Matrix transpose() {\n        Matrix result = new Matrix();\n        for (int i = 0; i < 3; i++) {\n            for (int j = 0; j < 3; j++) {\n                result.set(i, j, get(j, i));\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Negates this matrix.\n     *\n     * @return {@link Matrix} that is a negation of this matrix.\n     */\n    public Matrix negate() {\n        Matrix result = new Matrix();\n        for (int i = 0; i < 9; i++) {\n            result.mx[i] = -mx[i];\n        }\n        return result;\n    }\n\n    /**\n     * Adds a matrix to this matrix.\n     *\n     * @param right\n     *            {@link Matrix} to add\n     * @return {@link Matrix} that is a sum of both matrices\n     */\n    public Matrix add(Matrix right) {\n        Matrix result = new Matrix();\n        for (int i = 0; i < 9; i++) {\n            result.mx[i] = mx[i] + right.mx[i];\n        }\n        return result;\n    }\n\n    /**\n     * Subtracts a matrix from this matrix.\n     *\n     * @param right\n     *            {@link Matrix} to subtract\n     * @return {@link Matrix} that is the difference of both matrices\n     */\n    public Matrix subtract(Matrix right) {\n        Matrix result = new Matrix();\n        for (int i = 0; i < 9; i++) {\n            result.mx[i] = mx[i] - right.mx[i];\n        }\n        return result;\n    }\n\n    /**\n     * Multiplies two matrices.\n     *\n     * @param right\n     *            {@link Matrix} to multiply with\n     * @return {@link Matrix} that is the product of both matrices\n     */\n    public Matrix multiply(Matrix right) {\n        Matrix result = new Matrix();\n        for (int i = 0; i < 3; i++) {\n            for (int j = 0; j < 3; j++) {\n                double scalp = 0.0;\n                for (int k = 0; k < 3; k++) {\n                    scalp += get(i, k) * right.get(k, j);\n                }\n                result.set(i, j, scalp);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Performs a scalar multiplication.\n     *\n     * @param scalar\n     *            Scalar to multiply with\n     * @return {@link Matrix} that is the scalar product\n     */\n    public Matrix multiply(double scalar) {\n        Matrix result = new Matrix();\n        for (int i = 0; i < 9; i++) {\n            result.mx[i] = mx[i] * scalar;\n        }\n        return result;\n    }\n\n    /**\n     * Applies this matrix to a {@link Vector}.\n     *\n     * @param right\n     *            {@link Vector} to multiply with\n     * @return {@link Vector} that is the product of this matrix and the given vector\n     */\n    public Vector multiply(Vector right) {\n        double[] vec = new double[] {right.getX(), right.getY(), right.getZ()};\n        double[] result = new double[3];\n\n        for (int i = 0; i < 3; i++) {\n            double scalp = 0.0;\n            for (int j = 0; j < 3; j++) {\n                scalp += get(i, j) * vec[j];\n            }\n            result[i] = scalp;\n        }\n\n        return new Vector(result);\n    }\n\n    /**\n     * Gets a value from the matrix.\n     *\n     * @param r\n     *            Row number (0..2)\n     * @param c\n     *            Column number (0..2)\n     * @return Value at that position\n     */\n    public double get(int r, int c) {\n        if (r < 0 || r > 2 || c < 0 || c > 2) {\n            throw new IllegalArgumentException(\"row/column out of range: \" + r + \":\" + c);\n        }\n        return mx[r * 3 + c];\n    }\n\n    /**\n     * Changes a value in the matrix. As a {@link Matrix} object is immutable from the\n     * outside, this method is private.\n     *\n     * @param r\n     *            Row number (0..2)\n     * @param c\n     *            Column number (0..2)\n     * @param v\n     *            New value\n     */\n    private void set(int r, int c, double v) {\n        if (r < 0 || r > 2 || c < 0 || c > 2) {\n            throw new IllegalArgumentException(\"row/column out of range: \" + r + \":\" + c);\n        }\n        mx[r * 3 + c] = v;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj == null || !(obj instanceof Matrix)) {\n            return false;\n        }\n        return Arrays.equals(mx, ((Matrix) obj).mx);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(mx);\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append('[');\n        for (int ix = 0; ix < 9; ix++) {\n            if (ix % 3 == 0) {\n                sb.append('[');\n            }\n            sb.append(mx[ix]);\n            if (ix % 3 == 2) {\n                sb.append(']');\n            }\n            if (ix < 8) {\n                sb.append(\", \");\n            }\n        }\n        sb.append(']');\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/Moon.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.*;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.*;\n\n/**\n * Calculations and constants for the Moon.\n *\n * @see \"Astronomy on the Personal Computer, 4th edition\n *      (Oliver Montenbruck, Thomas Pfleger) -\n *      ISBN 978-3-540-67221-0\"\n */\npublic final class Moon {\n\n    private static final double MOON_MEAN_RADIUS = 1737.1;\n\n    private Moon() {\n        // Utility class without constructor\n    }\n\n    /**\n     * Calculates the equatorial position of the moon.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @return {@link Vector} of equatorial moon position\n     */\n    public static Vector positionEquatorial(JulianDate date) {\n        double T  = date.getJulianCentury();\n        double L0 =       frac(0.606433 + 1336.855225 * T);\n        double l  = PI2 * frac(0.374897 + 1325.552410 * T);\n        double ls = PI2 * frac(0.993133 +   99.997361 * T);\n        double D  = PI2 * frac(0.827361 + 1236.853086 * T);\n        double F  = PI2 * frac(0.259086 + 1342.227825 * T);\n        double D2 = 2.0 * D;\n        double l2 = 2.0 * l;\n        double F2 = 2.0 * F;\n\n        double dL = 22640.0 * sin(l)\n                  -  4586.0 * sin(l - D2)\n                  +  2370.0 * sin(D2)\n                  +   769.0 * sin(l2)\n                  -   668.0 * sin(ls)\n                  -   412.0 * sin(F2)\n                  -   212.0 * sin(l2 - D2)\n                  -   206.0 * sin(l + ls - D2)\n                  +   192.0 * sin(l + D2)\n                  -   165.0 * sin(ls - D2)\n                  -   125.0 * sin(D)\n                  -   110.0 * sin(l + ls)\n                  +   148.0 * sin(l - ls)\n                  -    55.0 * sin(F2 - D2);\n\n        double S  = F + (dL + 412.0 * sin(F2) + 541.0 * sin(ls)) / ARCS;\n        double h  = F - D2;\n        double N  =  -526.0 * sin(h)\n                  +    44.0 * sin(l + h)\n                  -    31.0 * sin(-l + h)\n                  -    23.0 * sin(ls + h)\n                  +    11.0 * sin(-ls + h)\n                  -    25.0 * sin(-l2 + F)\n                  +    21.0 * sin(-l + F);\n\n        double l_Moon = PI2 * frac(L0 + dL / 1296.0e3);\n        double b_Moon = (18520.0 * sin(S) + N) / ARCS;\n\n        double dt = 385000.5584\n                  -  20905.3550 * cos(l)\n                  -   3699.1109 * cos(D2 - l)\n                  -   2955.9676 * cos(D2)\n                  -    569.9251 * cos(l2);\n\n        return Vector.ofPolar(l_Moon, b_Moon, dt);\n    }\n\n    /**\n     * Calculates the geocentric position of the moon.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @return {@link Vector} of geocentric moon position\n     */\n    public static Vector position(JulianDate date) {\n        Matrix rotateMatrix = equatorialToEcliptical(date).transpose();\n        return rotateMatrix.multiply(positionEquatorial(date));\n    }\n\n    /**\n     * Calculates the horizontal position of the moon.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @param lat\n     *            Latitude, in radians\n     * @param lng\n     *            Longitute, in radians\n     * @return {@link Vector} of horizontal moon position\n     */\n    public static Vector positionHorizontal(JulianDate date, double lat, double lng) {\n        Vector mc = position(date);\n        double h = date.getGreenwichMeanSiderealTime() + lng - mc.getPhi();\n        return equatorialToHorizontal(h, mc.getTheta(), mc.getR(), lat);\n    }\n\n    /**\n     * Calculates the topocentric position of the moon.\n     * <p>\n     * Atmospheric refraction is <em>not</em> taken into account.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @param lat\n     *            Latitude, in radians\n     * @param lng\n     *            Longitute, in radians\n     * @param elev\n     *            Elevation, in meters\n     * @return {@link Vector} of topocentric moon position\n     * @since 3.9\n     */\n    public static Vector positionTopocentric(JulianDate date, double lat, double lng, double elev) {\n        Vector pos = positionHorizontal(date, lat, lng);\n        return Vector.ofPolar(\n                pos.getPhi(),\n                pos.getTheta() - parallax(elev, pos.getR()),\n                pos.getR()\n        );\n    }\n\n    /**\n     * Returns the angular radius of the moon.\n     *\n     * @param distance\n     *            Distance of the moon, in kilometers.\n     * @return Angular radius of the moon, in radians.\n     * @see <a href=\"https://en.wikipedia.org/wiki/Angular_diameter\">Wikipedia: Angular\n     *      Diameter</a>\n     */\n    public static double angularRadius(double distance) {\n        return asin(MOON_MEAN_RADIUS / distance);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/Pegasus.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.abs;\n\nimport java.util.function.Function;\n\n/**\n * Finds the root of a function by using the Pegasus method.\n *\n * @see <a href=\"https://en.wikipedia.org/wiki/False_position_method\">regula falsi</a>\n */\npublic class Pegasus {\n\n    private static final int MAX_ITERATIONS = 30;\n\n    /**\n     * Find the root of the given function within the boundaries.\n     *\n     * @param lower\n     *            Lower boundary\n     * @param upper\n     *            Upper boundary\n     * @param accuracy\n     *            Desired accuracy\n     * @param f\n     *            Function to be used for calculation\n     * @return root that was found\n     * @throws ArithmeticException\n     *             if the root could not be found in the given accuracy within\n     *             {@value #MAX_ITERATIONS} iterations.\n     */\n    public static Double calculate(double lower, double upper, double accuracy, Function<Double, Double> f) {\n        double x1 = lower;\n        double x2 = upper;\n\n        double f1 = f.apply(x1);\n        double f2 = f.apply(x2);\n\n        if (f1 * f2 >= 0.0) {\n            throw new ArithmeticException(\"No root within the given boundaries\");\n        }\n\n        int i = MAX_ITERATIONS;\n\n        while (i-- > 0) {\n            double x3 = x2 - f2 / ((f2 - f1) / (x2 - x1));\n            double f3 = f.apply(x3);\n\n            if (f3 * f2 <= 0.0) {\n                x1 = x2;\n                f1 = f2;\n                x2 = x3;\n                f2 = f3;\n            } else {\n                f1 = f1 * f2 / (f2 + f3);\n                x2 = x3;\n                f2 = f3;\n            }\n\n            if (abs(x2 - x1) <= accuracy) {\n                return abs(f1) < abs(f2) ? x1 : x2;\n            }\n        }\n\n        throw new ArithmeticException(\"Maximum number of iterations exceeded\");\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/QuadraticInterpolation.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.abs;\nimport static java.lang.Math.sqrt;\n\n/**\n * Calculates the roots and extremum of a quadratic equation.\n */\npublic class QuadraticInterpolation {\n\n    private final double xe;\n    private final double ye;\n    private final double root1;\n    private final double root2;\n    private final int nRoot;\n    private final boolean maximum;\n\n    /**\n     * Creates a new quadratic equation.\n     *\n     * @param yMinus\n     *            y at x == -1\n     * @param y0\n     *            y at x == 0\n     * @param yPlus\n     *            y at x == 1\n     */\n    public QuadraticInterpolation(double yMinus, double y0, double yPlus) {\n        double a = 0.5 * (yPlus + yMinus) - y0;\n        double b = 0.5 * (yPlus - yMinus);\n        double c = y0;\n\n        xe = -b / (2.0 * a);\n        ye = (a * xe + b) * xe + c;\n        maximum = a < 0.0;\n        double dis = b * b - 4.0 * a * c;\n\n        int rootCount = 0;\n\n        if (dis >= 0.0) {\n            double dx = 0.5 * sqrt(dis) / abs(a);\n            root1 = xe - dx;\n            root2 = xe + dx;\n\n            if (abs(root1) <= 1.0) {\n                rootCount++;\n            }\n\n            if (abs(root2) <= 1.0) {\n                rootCount++;\n            }\n        } else {\n            root1 = Double.NaN;\n            root2 = Double.NaN;\n        }\n\n        nRoot = rootCount;\n    }\n\n    /**\n     * Returns X of extremum. Can be outside [-1 .. 1].\n     *\n     * @return X\n     */\n    public double getXe() {\n        return xe;\n    }\n\n    /**\n     * Returns the Y value at the extremum.\n     *\n     * @return Y\n     */\n    public double getYe() {\n        return ye;\n    }\n\n    /**\n     * Returns the first root that was found.\n     *\n     * @return X of first root\n     */\n    public double getRoot1() {\n        return root1 < -1.0 ? root2 : root1;\n    }\n\n    /**\n     * Returns the second root that was found.\n     *\n     * @return X of second root\n     */\n    public double getRoot2() {\n        return root2;\n    }\n\n    /**\n     * Returns the number of roots found in [-1 .. 1].\n     *\n     * @return Number of roots\n     */\n    public int getNumberOfRoots() {\n        return nRoot;\n    }\n\n    /**\n     * Returns whether the extremum is a minimum or a maximum.\n     *\n     * @return {@code true}: Extremum at xe is a maximum. {@code false}: Extremum at xe is\n     *         a minimum.\n     */\n    public boolean isMaximum() {\n        return maximum;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/Sun.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.*;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.*;\n\n/**\n * Calculations and constants for the Sun.\n *\n * @see \"Astronomy on the Personal Computer, 4th edition\n *      (Oliver Montenbruck, Thomas Pfleger) -\n *      ISBN 978-3-540-67221-0\"\n */\npublic final class Sun {\n\n    private static final double SUN_DISTANCE = 149598000.0;\n    private static final double SUN_MEAN_RADIUS = 695700.0;\n\n    private Sun() {\n        // Utility class without constructor\n    }\n\n    /**\n     * Calculates the equatorial position of the sun.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @return {@link Vector} containing the sun position\n     */\n    public static Vector positionEquatorial(JulianDate date) {\n        double T = date.getJulianCentury();\n        double M = PI2 * frac(0.993133 + 99.997361 * T);\n        double L = PI2 * frac(0.7859453 + M / PI2\n            + (6893.0 * sin(M) + 72.0 * sin(2.0 * M) + 6191.2 * T) / 1296.0e3);\n\n        double d = SUN_DISTANCE\n            * (1 - 0.016718 * cos(date.getTrueAnomaly()));\n\n        return Vector.ofPolar(L, 0.0, d);\n    }\n\n    /**\n     * Calculates the geocentric position of the sun.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @return {@link Vector} containing the sun position\n     */\n    public static Vector position(JulianDate date) {\n        Matrix rotateMatrix = equatorialToEcliptical(date).transpose();\n        return rotateMatrix.multiply(positionEquatorial(date));\n    }\n\n    /**\n     * Calculates the horizontal position of the sun.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @param lat\n     *            Latitude, in radians\n     * @param lng\n     *            Longitute, in radians\n     * @return {@link Vector} of horizontal sun position\n     */\n    public static Vector positionHorizontal(JulianDate date, double lat, double lng) {\n        Vector mc = position(date);\n        double h = date.getGreenwichMeanSiderealTime() + lng - mc.getPhi();\n        return equatorialToHorizontal(h, mc.getTheta(), mc.getR(), lat);\n    }\n\n    /**\n     * Calculates the topocentric position of the sun.\n     * <p>\n     * Atmospheric refraction is <em>not</em> taken into account.\n     *\n     * @param date\n     *            {@link JulianDate} to be used\n     * @param lat\n     *            Latitude, in radians\n     * @param lng\n     *            Longitute, in radians\n     * @param elev\n     *            Elevation, in meters\n     * @return {@link Vector} of topocentric sun position\n     * @since 3.9\n     */\n    public static Vector positionTopocentric(JulianDate date, double lat, double lng, double elev) {\n        Vector pos = positionHorizontal(date, lat, lng);\n        return Vector.ofPolar(\n                pos.getPhi(),\n                pos.getTheta() - parallax(elev, pos.getR()),\n                pos.getR()\n        );\n    }\n\n    /**\n     * Returns the angular radius of the sun.\n     *\n     * @param distance\n     *            Distance of the sun, in kilometers.\n     * @return Angular radius of the sun, in radians.\n     * @see <a href=\"https://en.wikipedia.org/wiki/Angular_diameter\">Wikipedia: Angular\n     *      Diameter</a>\n     */\n    public static double angularRadius(double distance) {\n        return asin(SUN_MEAN_RADIUS / distance);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/Vector.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.*;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.PI2;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.isZero;\n\nimport edu.umd.cs.findbugs.annotations.Nullable;\n\n/**\n * A three dimensional vector.\n * <p>\n * Objects are is immutable and threadsafe.\n */\npublic class Vector {\n\n    private final double x;\n    private final double y;\n    private final double z;\n    private final Polar polar = new Polar();\n\n    /**\n     * Creates a new {@link Vector} of the given cartesian coordinates.\n     *\n     * @param x\n     *            X coordinate\n     * @param y\n     *            Y coordinate\n     * @param z\n     *            Z coordinate\n     */\n    public Vector(double x, double y, double z) {\n        this.x = x;\n        this.y = y;\n        this.z = z;\n    }\n\n    /**\n     * Creates a new {@link Vector} of the given cartesian coordinates.\n     *\n     * @param d\n     *            Array of coordinates, must have 3 elements\n     */\n    public Vector(double[] d) {\n        if (d.length != 3) {\n            throw new IllegalArgumentException(\"invalid vector length\");\n        }\n        this.x = d[0];\n        this.y = d[1];\n        this.z = d[2];\n    }\n\n    /**\n     * Creates a new {@link Vector} of the given polar coordinates, with a radial distance\n     * of 1.\n     *\n     * @param φ\n     *            Azimuthal Angle\n     * @param θ\n     *            Polar Angle\n     * @return Created {@link Vector}\n     */\n    public static Vector ofPolar(double φ, double θ) {\n        return ofPolar(φ, θ, 1.0);\n    }\n\n    /**\n     * Creates a new {@link Vector} of the given polar coordinates.\n     *\n     * @param φ\n     *            Azimuthal Angle\n     * @param θ\n     *            Polar Angle\n     * @param r\n     *            Radial Distance\n     * @return Created {@link Vector}\n     */\n    public static Vector ofPolar(double φ, double θ, double r) {\n        double cosθ = cos(θ);\n        Vector result = new Vector(\n            r * cos(φ) * cosθ,\n            r * sin(φ) * cosθ,\n            r *          sin(θ)\n        );\n        result.polar.setPolar(φ, θ, r);\n        return result;\n    }\n\n    /**\n     * Returns the cartesian X coordinate.\n     */\n    public double getX() {\n        return x;\n    }\n\n    /**\n     * Returns the cartesian Y coordinate.\n     */\n    public double getY() {\n        return y;\n    }\n\n    /**\n     * Returns the cartesian Z coordinate.\n     */\n    public double getZ() {\n        return z;\n    }\n\n    /**\n     * Returns the azimuthal angle (φ) in radians.\n     */\n    public double getPhi() {\n        return polar.getPhi();\n    }\n\n    /**\n     * Returns the polar angle (θ) in radians.\n     */\n    public double getTheta() {\n        return polar.getTheta();\n    }\n\n    /**\n     * Returns the polar radial distance (r).\n     */\n    public double getR() {\n        return polar.getR();\n    }\n\n    /**\n     * Returns a {@link Vector} that is the sum of this {@link Vector} and the given\n     * {@link Vector}.\n     *\n     * @param vec\n     *            {@link Vector} to add\n     * @return Resulting {@link Vector}\n     */\n    public Vector add(Vector vec) {\n        return new Vector(\n            x + vec.x,\n            y + vec.y,\n            z + vec.z\n        );\n    }\n\n    /**\n     * Returns a {@link Vector} that is the difference of this {@link Vector} and the\n     * given {@link Vector}.\n     *\n     * @param vec\n     *            {@link Vector} to subtract\n     * @return Resulting {@link Vector}\n     */\n    public Vector subtract(Vector vec) {\n        return new Vector(\n            x - vec.x,\n            y - vec.y,\n            z - vec.z\n        );\n    }\n\n    /**\n     * Returns a {@link Vector} that is the scalar product of this {@link Vector} and the\n     * given scalar.\n     *\n     * @param scalar\n     *            Scalar to multiply\n     * @return Resulting {@link Vector}\n     */\n    public Vector multiply(double scalar) {\n        return new Vector(\n            x * scalar,\n            y * scalar,\n            z * scalar\n        );\n    }\n\n    /**\n     * Returns the negation of this {@link Vector}.\n     *\n     * @return Resulting {@link Vector}\n     */\n    public Vector negate() {\n        return new Vector(\n            -x,\n            -y,\n            -z\n        );\n    }\n\n    /**\n     * Returns a {@link Vector} that is the cross product of this {@link Vector} and the\n     * given {@link Vector}.\n     *\n     * @param right\n     *            {@link Vector} to multiply\n     * @return Resulting {@link Vector}\n     */\n    public Vector cross(Vector right) {\n        return new Vector(\n            y * right.z - z * right.y,\n            z * right.x - x * right.z,\n            x * right.y - y * right.x\n        );\n    }\n\n    /**\n     * Returns the dot product of this {@link Vector} and the given {@link Vector}.\n     *\n     * @param right\n     *            {@link Vector} to multiply\n     * @return Resulting dot product\n     */\n    public double dot(Vector right) {\n        return x * right.x + y * right.y + z * right.z;\n    }\n\n    /**\n     * Returns the norm of this {@link Vector}.\n     *\n     * @return Norm of this vector\n     */\n    public double norm() {\n        return sqrt(dot(this));\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj == null || !(obj instanceof Vector)) {\n            return false;\n        }\n\n        Vector vec = (Vector) obj;\n        return Double.compare(x, vec.x) == 0\n            && Double.compare(y, vec.y) == 0\n            && Double.compare(z, vec.z) == 0;\n    }\n\n    @Override\n    public int hashCode() {\n        return Double.valueOf(x).hashCode()\n            ^  Double.valueOf(y).hashCode()\n            ^  Double.valueOf(z).hashCode();\n    }\n\n    @Override\n    public String toString() {\n        return \"(x=\" + x + \", y=\" + y + \", z=\" + z + \")\";\n    }\n\n    /**\n     * Helper class for lazily computing the polar coordinates in an immutable Vector\n     * object.\n     */\n    private class Polar {\n        private @Nullable Double φ = null;\n        private @Nullable Double θ = null;\n        private @Nullable Double r = null;\n\n        /**\n         * Sets polar coordinates.\n         *\n         * @param φ\n         *            Phi\n         * @param θ\n         *            Theta\n         * @param r\n         *            R\n         */\n        public synchronized void setPolar(double φ, double θ, double r) {\n            this.φ = φ;\n            this.θ = θ;\n            this.r = r;\n        }\n\n        public synchronized double getPhi() {\n            if (φ == null) {\n                if (isZero(x) && isZero(y)) {\n                    φ = 0.0;\n                } else {\n                    φ = atan2(y, x);\n                }\n\n                if (φ < 0.0) {\n                    φ += PI2;\n                }\n            }\n            return φ;\n        }\n\n        public synchronized double getTheta() {\n            if (θ == null) {\n                double ρSqr = x * x + y * y;\n\n                if (isZero(z) && isZero(ρSqr)) {\n                    θ = 0.0;\n                } else {\n                    θ = atan2(z, sqrt(ρSqr));\n                }\n            }\n            return θ;\n        }\n\n        public synchronized double getR() {\n            if (r == null) {\n                r = sqrt(x * x + y * y + z * z);\n            }\n            return r;\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/org/shredzone/commons/suncalc/util/package-info.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\n\n/**\n * This package contains internal utility methods.\n * <p>\n * Do not use in your code! This package will not be exported in a Java module.\n */\n@ReturnValuesAreNonnullByDefault\n@DefaultAnnotationForParameters(NonNull.class)\n@DefaultAnnotationForFields(NonNull.class)\npackage org.shredzone.commons.suncalc.util;\n\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;\nimport edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;\n"
  },
  {
    "path": "src/main/resources/.gitignore",
    "content": ""
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/ExamplesTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2020 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport java.time.Duration;\nimport java.time.LocalDate;\nimport java.time.ZoneId;\nimport java.util.TimeZone;\n\nimport org.junit.Ignore;\nimport org.junit.Test;\n\n/**\n * These are some examples that are meant to be executed manually.\n *\n * @see <a href=\"https://shredzone.org/maven/commons-suncalc/examples.html\">Example\n * chapter of the Documentation</a>\n */\n@Ignore // No real unit tests, but meant to be run manually\npublic class ExamplesTest {\n\n    @Test\n    public void testTimezone() {\n        // Our example takes place in Paris, so set the timezone accordingly.\n        TimeZone.setDefault(TimeZone.getTimeZone(\"Europe/Paris\"));\n\n        SunTimes paris = SunTimes.compute()\n                .on(2020, 5, 1)             // May 1st, 2020, starting midnight\n                .latitude(48, 51, 24.0)     // Latitude of Paris: 48°51'24\" N\n                .longitude(2, 21, 6.0)      // Longitude:          2°21'06\" E\n                .execute();\n        System.out.println(\"Sunrise in Paris: \" + paris.getRise());\n        System.out.println(\"Sunset in Paris:  \" + paris.getSet());\n\n        SunTimes newYork = SunTimes.compute()\n                .on(2020, 5, 1)             // May 1st, 2020, starting midnight\n                .at(40.712778, -74.005833)  // Coordinates of New York\n                .execute();\n        System.out.println(\"Sunrise in New York: \" + newYork.getRise());\n        System.out.println(\"Sunset in New York:  \" + newYork.getSet());\n\n        SunTimes newYorkTz = SunTimes.compute()\n                .on(2020, 5, 1)             // May 1st, 2020, starting midnight\n                .timezone(\"America/New_York\")   // ...New York timezone\n                .at(40.712778, -74.005833)  // Coordinates of New York\n                .execute();\n        System.out.println(\"Sunrise in New York: \" + newYorkTz.getRise());\n        System.out.println(\"Sunset in New York:  \" + newYorkTz.getSet());\n    }\n\n    @Test\n    public void testTimeWindow() {\n        final double[] ALERT_CANADA = new double[] { 82.5, -62.316667 };\n        final ZoneId ALERT_TZ = ZoneId.of(\"Canada/Eastern\");\n\n        SunTimes march = SunTimes.compute()\n                .on(2020, 3, 15)            // March 15th, 2020, starting midnight\n                .at(ALERT_CANADA)           // Coordinates are stored in an array\n                .timezone(ALERT_TZ)\n                .execute();\n        System.out.println(\"Sunrise: \" + march.getRise());\n        System.out.println(\"Sunset:  \" + march.getSet());\n\n        SunTimes june = SunTimes.compute()\n                .on(2020, 6, 15)            // June 15th, 2020, starting midnight\n                .at(ALERT_CANADA)\n                .timezone(ALERT_TZ)\n                .execute();\n        System.out.println(\"Sunrise: \" + june.getRise());\n        System.out.println(\"Sunset:  \" + june.getSet());\n\n        SunTimes juneReverse = SunTimes.compute()\n                .on(2020, 6, 15)            // June 15th, 2020, starting midnight\n                .at(ALERT_CANADA)\n                .timezone(ALERT_TZ)\n                .reverse()\n                .execute();\n        System.out.println(\"Sunrise: \" + juneReverse.getRise());\n        System.out.println(\"Sunset:  \" + juneReverse.getSet());\n\n        SunTimes june15OnlyCycle = SunTimes.compute()\n                .on(2020, 6, 15)            // June 15th, 2020, starting midnight\n                .at(ALERT_CANADA)\n                .timezone(ALERT_TZ)\n                .limit(Duration.ofHours(24))\n                .execute();\n        System.out.println(\"Sunset:  \" + june15OnlyCycle.getSet());\n        System.out.println(\"Sunrise: \" + june15OnlyCycle.getRise());\n\n        System.out.println(\"Sun is up all day:   \" + june15OnlyCycle.isAlwaysUp());\n        System.out.println(\"Sun is down all day: \" + june15OnlyCycle.isAlwaysDown());\n    }\n\n    @Test\n    public void testParameterRecycling() {\n        final double[] COLOGNE = new double[] { 50.938056, 6.956944 };\n\n        MoonTimes.Parameters parameters = MoonTimes.compute()\n                .at(COLOGNE)\n                .midnight();\n\n        MoonTimes today = parameters.execute();\n        System.out.println(\"Today, the moon rises in Cologne at \" + today.getRise());\n\n        parameters.tomorrow();\n        MoonTimes tomorrow = parameters.execute();\n        System.out.println(\"Tomorrow, the moon will rise in Cologne at \" + tomorrow.getRise());\n        System.out.println(\"But today, the moon still rises at \" + today.getRise());\n    }\n\n    @Test\n    public void testParameterRecyclingLoop() {\n        MoonIllumination.Parameters parameters = MoonIllumination.compute()\n                .on(2020, 1, 1);\n\n        for (int i = 1; i <= 31; i++) {\n            long percent = Math.round(parameters.execute().getFraction() * 100.0);\n            System.out.println(\"On January \" + i + \" the moon was \" + percent + \"% lit.\");\n            parameters.plusDays(1);\n        }\n    }\n\n    @Test\n    public void testGoldenHour() {\n        SunTimes.Parameters base = SunTimes.compute()\n                .at(1.283333, 103.833333)            // Singapore\n                .on(2020, 6, 1)\n                .timezone(\"Asia/Singapore\");\n\n        for (int i = 0; i < 4; i++) {\n            SunTimes blue = base\n                    .copy()                          // Use a copy of base\n                    .plusDays(i * 7)\n                    .twilight(SunTimes.Twilight.BLUE_HOUR)      // Blue Hour\n                    .execute();\n            SunTimes golden = base\n                    .copy()                          // Use a copy of base\n                    .plusDays(i * 7)\n                    .twilight(SunTimes.Twilight.GOLDEN_HOUR)    // Golden Hour\n                    .execute();\n\n            System.out.println(\"Morning golden hour starts at \" + blue.getRise());\n            System.out.println(\"Morning golden hour ends at   \" + golden.getRise());\n            System.out.println(\"Evening golden hour starts at \" + golden.getSet());\n            System.out.println(\"Evening golden hour ends at   \" + blue.getSet());\n        }\n    }\n\n    @Test\n    public void testMoonPhase() {\n        LocalDate date = LocalDate.of(2023, 1, 1);\n\n        MoonPhase.Parameters parameters = MoonPhase.compute()\n                .phase(MoonPhase.Phase.FULL_MOON);\n\n        while (true) {\n            MoonPhase moonPhase = parameters\n                    .on(date)\n                    .execute();\n            LocalDate nextFullMoon = moonPhase\n                    .getTime()\n                    .toLocalDate();\n            if (nextFullMoon.getYear() == 2024) {\n                break;      // we've reached the next year\n            }\n\n            System.out.print(nextFullMoon);\n            if (moonPhase.isMicroMoon()) {\n                System.out.print(\" (micromoon)\");\n            }\n            if (moonPhase.isSuperMoon()) {\n                System.out.print(\" (supermoon)\");\n            }\n            System.out.println();\n\n            date = nextFullMoon.plusDays(1);\n        }\n    }\n\n    @Test\n    public void testPositions() {\n        SunPosition.Parameters sunParam = SunPosition.compute()\n                .at(35.689722, 139.692222)      // Tokyo\n                .timezone(\"Asia/Tokyo\")         // local time\n                .on(2018, 11, 13, 10, 3, 24);   // 2018-11-13 10:03:24\n\n        MoonPosition.Parameters moonParam = MoonPosition.compute()\n                .sameLocationAs(sunParam)\n                .sameTimeAs(sunParam);\n\n        SunPosition sun = sunParam.execute();\n        System.out.println(String.format(\n                \"The sun can be seen %.1f° clockwise from the North and \"\n                + \"%.1f° above the horizon.\\nIt is about %.0f km away right now.\",\n                sun.getAzimuth(),\n                sun.getAltitude(),\n                sun.getDistance()\n        ));\n\n        MoonPosition moon = moonParam.execute();\n        System.out.println(String.format(\n                \"The moon can be seen %.1f° clockwise from the North and \"\n                + \"%.1f° above the horizon.\\nIt is about %.0f km away right now.\",\n                moon.getAzimuth(),\n                moon.getAltitude(),\n                moon.getDistance()\n        ));\n    }\n\n}"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/Locations.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport java.time.ZoneId;\n\n/**\n * Geocoordinates of some test locations.\n */\npublic final class Locations {\n\n    /**\n     * Cologne, Germany. A random city on the northern hemisphere.\n     */\n    public static final double[] COLOGNE = new double[] { 50.938056, 6.956944 };\n    public static final ZoneId COLOGNE_TZ = ZoneId.of(\"Europe/Berlin\");\n\n    /**\n     * Alert, Nunavut, Canada. The northernmost place in the world with a permanent\n     * population.\n     */\n    public static final double[] ALERT = new double[] { 82.5, -62.316667 };\n    public static final ZoneId ALERT_TZ = ZoneId.of(\"Canada/Eastern\");\n\n    /**\n     * Wellington, New Zealand. A random city on the southern hemisphere, close to the\n     * international date line.\n     */\n    public static final double[] WELLINGTON = new double[] { -41.2875, 174.776111 };\n    public static final ZoneId WELLINGTON_TZ = ZoneId.of(\"Pacific/Auckland\");\n\n    /**\n     * Puerto Williams, Chile. The southernmost town in the world.\n     */\n    public static final double[] PUERTO_WILLIAMS = new double[] { -54.933333, -67.616667 };\n    public static final ZoneId PUERTO_WILLIAMS_TZ = ZoneId.of(\"America/Punta_Arenas\");\n\n    /**\n     * Singapore. A random city close to the equator.\n     */\n    public static final double[] SINGAPORE = new double[] { 1.283333, 103.833333 };\n    public static final ZoneId SINGAPORE_TZ = ZoneId.of(\"Asia/Singapore\");\n\n    /**\n     * Martinique. To test a fix for issue #13.\n     */\n    public static final double[] MARTINIQUE = new double[] { 14.640725, -61.0112 };\n    public static final ZoneId MARTINIQUE_TZ = ZoneId.of(\"America/Martinique\");\n\n    /**\n     * Sydney. To test a fix for issue #14.\n     */\n    public static final double[] SYDNEY = new double[] { -33.744272, 151.231291 };\n    public static final ZoneId SYDNEY_TZ = ZoneId.of(\"Australia/Sydney\");\n\n    /**\n     * Santa Monica, CA. To test a fix for issue #18.\n     */\n    public static final double[] SANTA_MONICA = new double[] { 34.0, -118.5 };\n    public static final ZoneId SANTA_MONICA_TZ = ZoneId.of(\"America/Los_Angeles\");\n\n    /**\n     * Baghdad, Iraq. To test topocentric moon illumination.\n     */\n    public static final double[] BAGHDAD = new double[] { 33.338611, 44.393888 };\n    public static final ZoneId BAGHDAD_TZ = ZoneId.of(\"Asia/Baghdad\");\n\n    /**\n     * Cape Town, South Africa. To test topocentric moon illumination.\n     */\n    public static final double[] CAPETOWN = new double[] { -33.966666, 18.6 };\n    public static final ZoneId CAPETOWN_TZ = ZoneId.of(\"Africa/Johannesburg\");\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/MoonIlluminationTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.commons.suncalc.Locations.*;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\nimport org.shredzone.commons.suncalc.MoonPhase.Phase;\n\n/**\n * Unit tests for {@link MoonIllumination}.\n */\npublic class MoonIlluminationTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.1);\n    private static final Offset<Double> FINE_ERROR = Offset.offset(0.001);\n\n    @Test\n    public void testNewMoon() {\n        MoonIllumination mi = MoonIllumination.compute()\n                        .on(2017, 6, 24, 4, 30, 0)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(mi.getFraction()).as(\"fraction\").isCloseTo(0.0, ERROR);\n        assertThat(mi.getPhase()).as(\"phase\").isCloseTo(176.0, ERROR); // -180.0\n        assertThat(mi.getAngle()).as(\"angle\").isCloseTo(2.0, ERROR);\n        assertThat(mi.getClosestPhase()).as(\"MoonPhase.Phase\").isEqualTo(Phase.NEW_MOON);\n    }\n\n    @Test\n    public void testWaxingHalfMoon() {\n        MoonIllumination mi = MoonIllumination.compute()\n                        .on(2017, 7, 1, 2, 51, 0)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(mi.getFraction()).as(\"fraction\").isCloseTo(0.5, ERROR);\n        assertThat(mi.getPhase()).as(\"phase\").isCloseTo(-90.0, ERROR);\n        assertThat(mi.getAngle()).as(\"angle\").isCloseTo(-66.9, ERROR);\n        assertThat(mi.getClosestPhase()).as(\"MoonPhase.Phase\").isEqualTo(Phase.FIRST_QUARTER);\n    }\n\n    @Test\n    public void testFullMoon() {\n        MoonIllumination mi = MoonIllumination.compute()\n                        .on(2017, 7, 9, 6, 6, 0)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(mi.getFraction()).as(\"fraction\").isCloseTo(1.0, ERROR);\n        assertThat(mi.getPhase()).as(\"phase\").isCloseTo(-3.2, ERROR); // 0.0\n        assertThat(mi.getAngle()).as(\"angle\").isCloseTo(-7.4, ERROR);\n        assertThat(mi.getClosestPhase()).as(\"MoonPhase.Phase\").isEqualTo(Phase.FULL_MOON);\n    }\n\n    @Test\n    public void testWaningHalfMoon() {\n        MoonIllumination mi = MoonIllumination.compute()\n                        .on(2017, 7, 16, 21, 25, 0)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(mi.getFraction()).as(\"fraction\").isCloseTo(0.5, ERROR);\n        assertThat(mi.getPhase()).as(\"phase\").isCloseTo(90.0, ERROR);\n        assertThat(mi.getAngle()).as(\"angle\").isCloseTo(68.7, ERROR);\n        assertThat(mi.getClosestPhase()).as(\"MoonPhase.Phase\").isEqualTo(Phase.LAST_QUARTER);\n    }\n\n    @Test\n    public void testBaghdad1() {\n        MoonIllumination tmi = MoonIllumination.compute()\n                .on(2002, 12, 6, 17, 49, 0)\n                .timezone(BAGHDAD_TZ)\n                .at(BAGHDAD)\n                .elevation(46.0)\n                .execute();\n        assertThat(tmi.getElongation()).as(\"elongation\").isCloseTo(29.8, ERROR);\n        assertThat(tmi.getRadius()).as(\"moonRadius\").isCloseTo(0.2661, FINE_ERROR);\n        assertThat(tmi.getCrescentWidth()).as(\"crescentWidth\").isCloseTo(0.036, FINE_ERROR);\n    }\n\n    @Test\n    public void testBaghdad2() {\n        MoonIllumination tmi = MoonIllumination.compute()\n                .on(2002, 9, 8, 17, 49, 0)\n                .timezone(BAGHDAD_TZ)\n                .at(BAGHDAD)\n                .elevation(46.0)\n                .execute();\n        assertThat(tmi.getElongation()).as(\"elongation\").isCloseTo(20.2, ERROR);\n        assertThat(tmi.getRadius()).as(\"moonRadius\").isCloseTo(0.278, FINE_ERROR);\n        assertThat(tmi.getCrescentWidth()).as(\"crescentWidth\").isCloseTo(0.017, FINE_ERROR);\n    }\n\n    @Test\n    public void testCapeTown1() {\n        MoonIllumination tmi = MoonIllumination.compute()\n                .on(2010, 7, 13, 18, 51, 0)\n                .timezone(CAPETOWN_TZ)\n                .at(CAPETOWN)\n                .execute();\n        assertThat(tmi.getElongation()).as(\"elongation\").isCloseTo(25.3, ERROR);\n        assertThat(tmi.getRadius()).as(\"moonRadius\").isCloseTo(0.276, FINE_ERROR);\n        assertThat(tmi.getCrescentWidth()).as(\"crescentWidth\").isCloseTo(0.027, FINE_ERROR);\n    }\n\n    @Test\n    public void testCapeTown2() {\n        MoonIllumination tmi = MoonIllumination.compute()\n                .on(2010, 2, 16, 20, 1, 0)\n                .timezone(CAPETOWN_TZ)\n                .at(CAPETOWN)\n                .execute();\n        assertThat(tmi.getElongation()).as(\"elongation\").isCloseTo(28.7, ERROR);\n        assertThat(tmi.getRadius()).as(\"moonRadius\").isCloseTo(0.248, FINE_ERROR);\n        assertThat(tmi.getCrescentWidth()).as(\"crescentWidth\").isCloseTo(0.031, FINE_ERROR);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/MoonPhaseTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.time.temporal.ChronoUnit;\n\nimport org.assertj.core.api.AbstractDateAssert;\nimport org.assertj.core.data.Offset;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.shredzone.commons.suncalc.MoonPhase.Phase;\n\n/**\n * Unit tests for {@link MoonPhase}.\n */\npublic class MoonPhaseTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(500.0);\n\n    @BeforeClass\n    public static void init() {\n        AbstractDateAssert.registerCustomDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n    }\n\n    @Test\n    public void testNewMoon() {\n        MoonPhase mp = MoonPhase.compute()\n                        .on(2017, 9, 1)\n                        .utc()\n                        .phase(Phase.NEW_MOON)\n                        .execute();\n\n        assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS))\n                .isEqualTo(\"2017-09-20T05:29:30Z\");\n        assertThat(mp.getDistance()).isCloseTo(382740.0, ERROR);\n    }\n\n    @Test\n    public void testFirstQuarterMoon() {\n        MoonPhase mp = MoonPhase.compute()\n                        .on(2017, 9, 1)\n                        .utc()\n                        .phase(Phase.FIRST_QUARTER)\n                        .execute();\n\n        assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS))\n                .isEqualTo(\"2017-09-28T02:52:40Z\");\n        assertThat(mp.getDistance()).isCloseTo(403894.0, ERROR);\n    }\n\n    @Test\n    public void testFullMoon() {\n        MoonPhase mp = MoonPhase.compute()\n                        .on(2017, 9, 1)\n                        .utc()\n                        .phase(Phase.FULL_MOON)\n                        .execute();\n\n        assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS))\n                .isEqualTo(\"2017-09-06T07:07:44Z\");\n        assertThat(mp.getDistance()).isCloseTo(384364.0, ERROR);\n    }\n\n    @Test\n    public void testLastQuarterMoon() {\n        MoonPhase mp = MoonPhase.compute()\n                        .on(2017, 9, 1)\n                        .utc()\n                        .phase(Phase.LAST_QUARTER)\n                        .execute();\n\n        assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS))\n                .isEqualTo(\"2017-09-13T06:28:34Z\");\n        assertThat(mp.getDistance()).isCloseTo(369899.0, ERROR);\n    }\n\n    @Test\n    public void testToPhase() {\n        // exact angles\n        assertThat(Phase.toPhase(  0.0)).isEqualTo(Phase.NEW_MOON);\n        assertThat(Phase.toPhase( 45.0)).isEqualTo(Phase.WAXING_CRESCENT);\n        assertThat(Phase.toPhase( 90.0)).isEqualTo(Phase.FIRST_QUARTER);\n        assertThat(Phase.toPhase(135.0)).isEqualTo(Phase.WAXING_GIBBOUS);\n        assertThat(Phase.toPhase(180.0)).isEqualTo(Phase.FULL_MOON);\n        assertThat(Phase.toPhase(225.0)).isEqualTo(Phase.WANING_GIBBOUS);\n        assertThat(Phase.toPhase(270.0)).isEqualTo(Phase.LAST_QUARTER);\n        assertThat(Phase.toPhase(315.0)).isEqualTo(Phase.WANING_CRESCENT);\n\n        // out of range angles (normalization test)\n        assertThat(Phase.toPhase( 360.0)).isEqualTo(Phase.NEW_MOON);\n        assertThat(Phase.toPhase( 720.0)).isEqualTo(Phase.NEW_MOON);\n        assertThat(Phase.toPhase(-360.0)).isEqualTo(Phase.NEW_MOON);\n        assertThat(Phase.toPhase(-720.0)).isEqualTo(Phase.NEW_MOON);\n        assertThat(Phase.toPhase( 855.0)).isEqualTo(Phase.WAXING_GIBBOUS);\n        assertThat(Phase.toPhase(-585.0)).isEqualTo(Phase.WAXING_GIBBOUS);\n        assertThat(Phase.toPhase(-945.0)).isEqualTo(Phase.WAXING_GIBBOUS);\n\n        // close to boundary\n        assertThat(Phase.toPhase( 22.4)).isEqualTo(Phase.NEW_MOON);\n        assertThat(Phase.toPhase( 67.4)).isEqualTo(Phase.WAXING_CRESCENT);\n        assertThat(Phase.toPhase(112.4)).isEqualTo(Phase.FIRST_QUARTER);\n        assertThat(Phase.toPhase(157.4)).isEqualTo(Phase.WAXING_GIBBOUS);\n        assertThat(Phase.toPhase(202.4)).isEqualTo(Phase.FULL_MOON);\n        assertThat(Phase.toPhase(247.4)).isEqualTo(Phase.WANING_GIBBOUS);\n        assertThat(Phase.toPhase(292.4)).isEqualTo(Phase.LAST_QUARTER);\n        assertThat(Phase.toPhase(337.4)).isEqualTo(Phase.WANING_CRESCENT);\n        assertThat(Phase.toPhase(382.4)).isEqualTo(Phase.NEW_MOON);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/MoonPositionTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.commons.suncalc.Locations.*;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link MoonPosition}.\n */\npublic class MoonPositionTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.1);\n    private static final Offset<Double> DISTANCE_ERROR = Offset.offset(800.0);\n\n    @Test\n    public void testCologne() {\n        MoonPosition mp1 = MoonPosition.compute()\n                        .on(2017, 7, 12, 13, 28, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(mp1.getAzimuth()).as(\"azimuth\").isCloseTo(304.8, ERROR);\n        assertThat(mp1.getAltitude()).as(\"altitude\").isCloseTo(-39.6, ERROR);\n        assertThat(mp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(-39.6, ERROR);\n        assertThat(mp1.getParallacticAngle()).as(\"pa\").isCloseTo(32.0, ERROR);\n\n        MoonPosition mp2 = MoonPosition.compute()\n                        .on(2017, 7, 12, 3, 51, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(mp2.getAzimuth()).as(\"azimuth\").isCloseTo(179.9, ERROR);\n        assertThat(mp2.getAltitude()).as(\"altitude\").isCloseTo(25.3, ERROR);\n        assertThat(mp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(25.3, ERROR);\n        assertThat(mp2.getDistance()).as(\"distance\").isCloseTo(394709.0, DISTANCE_ERROR);\n        assertThat(mp2.getParallacticAngle()).as(\"pa\").isCloseTo(0.0, ERROR);\n    }\n\n    @Test\n    public void testAlert() {\n        MoonPosition mp1 = MoonPosition.compute()\n                        .on(2017, 7, 12, 8, 4, 0)\n                        .at(ALERT)\n                        .timezone(ALERT_TZ)\n                        .execute();\n        assertThat(mp1.getAzimuth()).as(\"azimuth\").isCloseTo(257.5, ERROR);\n        assertThat(mp1.getAltitude()).as(\"altitude\").isCloseTo(-10.9, ERROR);\n        assertThat(mp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(-10.9, ERROR);\n        assertThat(mp1.getParallacticAngle()).as(\"pa\").isCloseTo(7.5, ERROR);\n\n        MoonPosition mp2 = MoonPosition.compute()\n                        .on(2017, 7, 12, 2, 37, 0)\n                        .at(ALERT)\n                        .timezone(ALERT_TZ)\n                        .execute();\n        assertThat(mp2.getAzimuth()).as(\"azimuth\").isCloseTo(179.8, ERROR);\n        assertThat(mp2.getAltitude()).as(\"altitude\").isCloseTo(-5.7, ERROR);\n        assertThat(mp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(-5.7, ERROR);\n        assertThat(mp2.getDistance()).as(\"distance\").isCloseTo(393609.0, DISTANCE_ERROR);\n        assertThat(mp2.getParallacticAngle()).as(\"pa\").isCloseTo(0.0, ERROR);\n    }\n\n    @Test\n    public void testWellington() {\n        MoonPosition mp1 = MoonPosition.compute()\n                        .on(2017, 7, 12, 4, 7, 0)\n                        .at(WELLINGTON)\n                        .timezone(WELLINGTON_TZ)\n                        .execute();\n        assertThat(mp1.getAzimuth()).as(\"azimuth\").isCloseTo(311.3, ERROR);\n        assertThat(mp1.getAltitude()).as(\"altitude\").isCloseTo(55.1, ERROR);\n        assertThat(mp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(55.1, ERROR);\n        assertThat(mp1.getParallacticAngle()).as(\"pa\").isCloseTo(144.2, ERROR);\n\n        MoonPosition mp2 = MoonPosition.compute()\n                        .on(2017, 7, 12, 2, 17, 0)\n                        .at(WELLINGTON)\n                        .timezone(WELLINGTON_TZ)\n                        .execute();\n        assertThat(mp2.getAzimuth()).as(\"azimuth\").isCloseTo(0.5, ERROR);\n        assertThat(mp2.getAltitude()).as(\"altitude\").isCloseTo(63.9, ERROR);\n        assertThat(mp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(63.9, ERROR);\n        assertThat(mp2.getDistance()).as(\"distance\").isCloseTo(396272.0, DISTANCE_ERROR);\n        assertThat(mp2.getParallacticAngle()).as(\"pa\").isCloseTo(-179.6, ERROR);\n    }\n\n    @Test\n    public void testPuertoWilliams() {\n        MoonPosition mp1 = MoonPosition.compute()\n                        .on(2017, 2, 7, 9, 44, 0)\n                        .at(PUERTO_WILLIAMS)\n                        .timezone(PUERTO_WILLIAMS_TZ)\n                        .execute();\n        assertThat(mp1.getAzimuth()).as(\"azimuth\").isCloseTo(199.4, ERROR);\n        assertThat(mp1.getAltitude()).as(\"altitude\").isCloseTo(-52.7, ERROR);\n        assertThat(mp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(-52.7, ERROR);\n        assertThat(mp1.getParallacticAngle()).as(\"pa\").isCloseTo(168.3, ERROR);\n\n        MoonPosition mp2 = MoonPosition.compute()\n                        .on(2017, 2, 7, 23, 4, 0)\n                        .at(PUERTO_WILLIAMS)\n                        .timezone(PUERTO_WILLIAMS_TZ)\n                        .execute();\n        assertThat(mp2.getAzimuth()).as(\"azimuth\").isCloseTo(0.1, ERROR);\n        assertThat(mp2.getAltitude()).as(\"altitude\").isCloseTo(16.3, ERROR);\n        assertThat(mp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(16.3, ERROR);\n        assertThat(mp2.getDistance()).as(\"distance\").isCloseTo(369731.0, DISTANCE_ERROR);\n        assertThat(mp2.getParallacticAngle()).as(\"pa\").isCloseTo(-179.9, ERROR);\n    }\n\n    @Test\n    public void testSingapore() {\n        MoonPosition mp1 = MoonPosition.compute()\n                        .on(2017, 7, 12, 5, 12, 0)\n                        .at(SINGAPORE)\n                        .timezone(SINGAPORE_TZ)\n                        .execute();\n        assertThat(mp1.getAzimuth()).as(\"azimuth\").isCloseTo(240.6, ERROR);\n        assertThat(mp1.getAltitude()).as(\"altitude\").isCloseTo(57.1, ERROR);\n        assertThat(mp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(57.1, ERROR);\n        assertThat(mp1.getParallacticAngle()).as(\"pa\").isCloseTo(64.0, ERROR);\n\n        MoonPosition mp2 = MoonPosition.compute()\n                        .on(2017, 7, 12, 3, 11, 0)\n                        .at(SINGAPORE)\n                        .timezone(SINGAPORE_TZ)\n                        .execute();\n        assertThat(mp2.getAzimuth()).as(\"azimuth\").isCloseTo(180.0, ERROR);\n        assertThat(mp2.getAltitude()).as(\"altitude\").isCloseTo(74.1, ERROR);\n        assertThat(mp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(74.1, ERROR);\n        assertThat(mp2.getDistance()).as(\"distance\").isCloseTo(395621.0, DISTANCE_ERROR);\n        assertThat(mp2.getParallacticAngle()).as(\"pa\").isCloseTo(0.0, ERROR);\n    }\n\n    @Test\n    public void testBaghdad() {\n        MoonPosition mp1 = MoonPosition.compute()\n                .on(2002, 12, 4, 16, 57, 0)\n                .latitude(33, 20, 0.0)\n                .longitude(44, 25, 0.0)\n                .elevationFt(100.0)\n                .timezone(\"Asia/Baghdad\")\n                .execute();\n        assertThat(mp1.getAzimuth()).as(\"azimuth\").isCloseTo(241.1, ERROR);\n        assertThat(mp1.getAltitude()).as(\"altitude\").isCloseTo(1.2, ERROR);\n        assertThat(mp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(0.8, ERROR);\n        assertThat(mp1.getParallacticAngle()).as(\"pa\").isCloseTo(52.8, ERROR);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/MoonTimesTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.commons.suncalc.Locations.*;\n\nimport java.time.Duration;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\n\nimport org.assertj.core.api.AbstractDateAssert;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link MoonTimes}.\n */\npublic class MoonTimesTest {\n\n    @BeforeClass\n    public static void init() {\n        AbstractDateAssert.registerCustomDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n    }\n\n    @Test\n    public void testCologne() {\n        MoonTimes mt = MoonTimes.compute().on(2017, 7, 12).utc().at(COLOGNE).execute();\n        assertThat(mt.getRise()).as(\"rise\").isEqualTo(\"2017-07-12T21:25:55Z\");\n        assertThat(mt.getSet()).as(\"set\").isEqualTo(\"2017-07-12T06:53:30Z\");\n        assertThat(mt.isAlwaysUp()).as(\"alwaysup\").isFalse();\n        assertThat(mt.isAlwaysDown()).as(\"alwaysdown\").isFalse();\n\n        MoonTimes mtr = MoonTimes.compute().on(2017, 7, 12).utc().at(COLOGNE).reverse().execute();\n        assertThat(mtr.getRise()).as(\"rise\").isEqualTo(\"2017-07-11T20:56:23Z\");\n        assertThat(mtr.getSet()).as(\"set\").isEqualTo(\"2017-07-11T05:49:00Z\");\n        assertThat(mtr.isAlwaysUp()).as(\"alwaysup\").isFalse();\n        assertThat(mtr.isAlwaysDown()).as(\"alwaysdown\").isFalse();\n    }\n\n    @Test\n    public void testAlert() {\n        MoonTimes mt1 = MoonTimes.compute().on(2017, 7, 12).utc().at(ALERT).oneDay().execute();\n        assertThat(mt1.isAlwaysUp()).as(\"alwaysup\").isFalse();\n        assertThat(mt1.isAlwaysDown()).as(\"alwaysdown\").isTrue();\n\n        MoonTimes mt2 = MoonTimes.compute().on(2017, 7, 12).utc().at(ALERT).execute();\n        assertThat(mt2.getRise()).as(\"rise\").isEqualTo(\"2017-07-14T05:45:05Z\");\n        assertThat(mt2.getSet()).as(\"set\").isEqualTo(\"2017-07-14T11:26:43Z\");\n        assertThat(mt2.isAlwaysUp()).as(\"alwaysup\").isFalse();\n        assertThat(mt2.isAlwaysDown()).as(\"alwaysdown\").isFalse();\n\n        MoonTimes mt3 = MoonTimes.compute().on(2017, 7, 14).utc().at(ALERT).limit(Duration.ofDays(1)).execute();\n        assertThat(mt3.getRise()).as(\"rise\").isEqualTo(\"2017-07-14T05:45:05Z\");\n        assertThat(mt3.getSet()).as(\"set\").isEqualTo(\"2017-07-14T11:26:43Z\");\n        assertThat(mt3.isAlwaysUp()).as(\"alwaysup\").isFalse();\n        assertThat(mt3.isAlwaysDown()).as(\"alwaysdown\").isFalse();\n\n        MoonTimes mt4 = MoonTimes.compute().on(2017, 7, 18).utc().at(ALERT).oneDay().execute();\n        assertThat(mt4.isAlwaysUp()).as(\"alwaysup\").isTrue();\n        assertThat(mt4.isAlwaysDown()).as(\"alwaysdown\").isFalse();\n\n        MoonTimes mt5 = MoonTimes.compute().on(2017, 7, 18).utc().at(ALERT).fullCycle().execute();\n        assertThat(mt5.getRise()).as(\"rise\").isEqualTo(\"2017-07-27T11:59:20Z\");\n        assertThat(mt5.getSet()).as(\"set\").isEqualTo(\"2017-07-27T04:07:10Z\");\n        assertThat(mt5.isAlwaysUp()).as(\"alwaysup\").isFalse();\n        assertThat(mt5.isAlwaysDown()).as(\"alwaysdown\").isFalse();\n    }\n\n    @Test\n    public void testWellington() {\n        MoonTimes mt1 = MoonTimes.compute().on(2017, 7, 12).utc().at(WELLINGTON).execute();\n        assertThat(mt1.getRise()).as(\"rise\").isEqualTo(\"2017-07-12T08:05:53Z\");\n        assertThat(mt1.getSet()).as(\"set\").isEqualTo(\"2017-07-12T21:57:38Z\");\n\n        MoonTimes mt2 = MoonTimes.compute().on(2017, 7, 12).timezone(\"NZ\").at(WELLINGTON).execute();\n        assertThat(mt2.getRise()).as(\"rise\").isEqualTo(\"2017-07-12T20:05:53+12:00\");\n        assertThat(mt2.getSet()).as(\"set\").isEqualTo(\"2017-07-12T09:23:00+12:00\");\n    }\n\n    @Test\n    public void testPuertoWilliams() {\n        MoonTimes mt = MoonTimes.compute().on(2017, 7, 13).utc().at(PUERTO_WILLIAMS)\n                        .execute();\n        assertThat(mt.getRise()).as(\"rise\").isEqualTo(\"2017-07-13T00:31:29Z\");\n        assertThat(mt.getSet()).as(\"set\").isEqualTo(\"2017-07-13T14:48:37Z\");\n    }\n\n    @Test\n    public void testSingapore() {\n        MoonTimes mt = MoonTimes.compute().on(2017, 7, 13).utc().at(SINGAPORE).execute();\n        assertThat(mt.getRise()).as(\"rise\").isEqualTo(\"2017-07-13T14:35:09Z\");\n        assertThat(mt.getSet()).as(\"set\").isEqualTo(\"2017-07-13T02:08:57Z\");\n    }\n\n    @Test\n    public void testSequence() {\n        long acceptableError = 60 * 1000L;\n\n        ZonedDateTime riseBefore = createDate(2017, 11, 25, 12, 0);\n        ZonedDateTime riseAfter = createDate(2017, 11, 26, 12, 29);\n        ZonedDateTime setBefore = createDate(2017, 11, 25, 21, 49);\n        ZonedDateTime setAfter = createDate(2017, 11, 26, 22, 55);\n\n        for (int hour = 0; hour < 24; hour++) {\n            for (int minute = 0; minute < 60; minute++) {\n                MoonTimes times = MoonTimes.compute()\n                            .at(COLOGNE)\n                            .on(2017, 11, 25, hour, minute, 0).utc()\n                            .fullCycle()\n                            .execute();\n\n                ZonedDateTime rise = times.getRise();\n                ZonedDateTime set = times.getSet();\n\n                assertThat(rise).isNotNull();\n                assertThat(set).isNotNull();\n\n                if (hour < 12 || (hour == 12 && minute == 0)) {\n                    long diff = Duration.between(rise, riseBefore).abs().toMillis();\n                    assertThat(diff).as(\"rise @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                } else {\n                    long diff = Duration.between(rise, riseAfter).abs().toMillis();\n                    assertThat(diff).as(\"rise @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                }\n\n                if (hour < 21 || (hour == 21 && minute <= 49)) {\n                    long diff = Duration.between(set, setBefore).abs().toMillis();\n                    assertThat(diff).as(\"set @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                } else {\n                    long diff = Duration.between(set, setAfter).abs().toMillis();\n                    assertThat(diff).as(\"set @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                }\n            }\n        }\n    }\n\n    private ZonedDateTime createDate(int year, int month, int day, int hour, int minute) {\n        return ZonedDateTime.of(year, month, day, hour, minute, 0, 0, ZoneId.of(\"UTC\"));\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/SunPositionTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.commons.suncalc.Locations.*;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link SunPosition}.\n */\npublic class SunPositionTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.1);\n\n    @Test\n    public void testCologne() {\n        SunPosition sp1 = SunPosition.compute()\n                        .on(2017, 7, 12, 16, 10, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(sp1.getAzimuth()).as(\"azimuth\").isCloseTo(239.8, ERROR);\n        assertThat(sp1.getAltitude()).as(\"altitude\").isCloseTo(48.6, ERROR);\n        assertThat(sp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(48.6, ERROR);\n\n        SunPosition sp2 = SunPosition.compute()\n                        .on(2017, 7, 12, 13, 37, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(sp2.getAzimuth()).as(\"azimuth\").isCloseTo(179.6, ERROR);\n        assertThat(sp2.getAltitude()).as(\"altitude\").isCloseTo(61.0, ERROR);\n        assertThat(sp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(61.0, ERROR);\n    }\n\n    @Test\n    public void testAlert() {\n        SunPosition sp1 = SunPosition.compute()\n                        .on(2017, 7, 12, 6, 17, 0)\n                        .at(ALERT)\n                        .timezone(ALERT_TZ)\n                        .execute();\n        assertThat(sp1.getAzimuth()).as(\"azimuth\").isCloseTo(87.5, ERROR);\n        assertThat(sp1.getAltitude()).as(\"altitude\").isCloseTo(21.8, ERROR);\n        assertThat(sp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(21.8, ERROR);\n\n        SunPosition sp2 = SunPosition.compute()\n                        .on(2017, 7, 12, 12, 14, 0)\n                        .at(ALERT)\n                        .timezone(ALERT_TZ)\n                        .execute();\n        assertThat(sp2.getAzimuth()).as(\"azimuth\").isCloseTo(179.7, ERROR);\n        assertThat(sp2.getAltitude()).as(\"altitude\").isCloseTo(29.4, ERROR);\n        assertThat(sp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(29.4, ERROR);\n    }\n\n    @Test\n    public void testWellington() {\n        SunPosition sp1 = SunPosition.compute()\n                        .on(2017, 7, 12, 3, 7, 0)\n                        .at(WELLINGTON)\n                        .timezone(WELLINGTON_TZ)\n                        .execute();\n        assertThat(sp1.getAzimuth()).as(\"azimuth\").isCloseTo(107.3, ERROR);\n        assertThat(sp1.getAltitude()).as(\"altitude\").isCloseTo(-51.3, ERROR);\n        assertThat(sp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(-51.3, ERROR);\n\n        SunPosition sp2 = SunPosition.compute()\n                        .on(2017, 7, 12, 12, 26, 0)\n                        .at(WELLINGTON)\n                        .timezone(WELLINGTON_TZ)\n                        .execute();\n        assertThat(sp2.getAzimuth()).as(\"azimuth\").isCloseTo(0.1, ERROR);\n        assertThat(sp2.getAltitude()).as(\"altitude\").isCloseTo(26.8, ERROR);\n        assertThat(sp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(26.8, ERROR);\n\n        SunPosition sp3 = SunPosition.compute()\n                        .on(2017, 7, 12, 7, 50, 0)\n                        .at(WELLINGTON)\n                        .timezone(WELLINGTON_TZ)\n                        .execute();\n        assertThat(sp3.getAzimuth()).as(\"azimuth\").isCloseTo(60.0, ERROR);\n        assertThat(sp3.getAltitude()).as(\"altitude\").isCloseTo(0.6, ERROR);\n        assertThat(sp3.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(0.1, ERROR);\n    }\n\n    @Test\n    public void testPuertoWilliams() {\n        SunPosition sp1 = SunPosition.compute()\n                        .on(2017, 2, 7, 18, 13, 0)\n                        .at(PUERTO_WILLIAMS)\n                        .timezone(PUERTO_WILLIAMS_TZ)\n                        .execute();\n        assertThat(sp1.getAzimuth()).as(\"azimuth\").isCloseTo(280.1, ERROR);\n        assertThat(sp1.getAltitude()).as(\"altitude\").isCloseTo(25.4, ERROR);\n        assertThat(sp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(25.4, ERROR);\n\n        SunPosition sp2 = SunPosition.compute()\n                        .on(2017, 2, 7, 13, 44, 0)\n                        .at(PUERTO_WILLIAMS)\n                        .timezone(PUERTO_WILLIAMS_TZ)\n                        .execute();\n        assertThat(sp2.getAzimuth()).as(\"azimuth\").isCloseTo(0.2, ERROR);\n        assertThat(sp2.getAltitude()).as(\"altitude\").isCloseTo(50.2, ERROR);\n        assertThat(sp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(50.2, ERROR);\n    }\n\n    @Test\n    public void testSingapore() {\n        SunPosition sp1 = SunPosition.compute()\n                        .on(2017, 7, 12, 10, 19, 0)\n                        .at(SINGAPORE)\n                        .timezone(SINGAPORE_TZ)\n                        .execute();\n        assertThat(sp1.getAzimuth()).as(\"azimuth\").isCloseTo(60.4, ERROR);\n        assertThat(sp1.getAltitude()).as(\"altitude\").isCloseTo(43.5, ERROR);\n        assertThat(sp1.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(43.5, ERROR);\n\n        SunPosition sp2 = SunPosition.compute()\n                        .on(2017, 7, 12, 13, 10, 0)\n                        .at(SINGAPORE)\n                        .timezone(SINGAPORE_TZ)\n                        .execute();\n        assertThat(sp2.getAzimuth()).as(\"azimuth\").isCloseTo(0.2, ERROR);\n        assertThat(sp2.getAltitude()).as(\"altitude\").isCloseTo(69.4, ERROR);\n        assertThat(sp2.getTrueAltitude()).as(\"trueAltitude\").isCloseTo(69.4, ERROR);\n    }\n\n    @Test\n    public void testDistance() {\n        SunPosition sp1 = SunPosition.compute()\n                        .on(2017, 1, 4, 12, 37, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(sp1.getDistance()).as(\"distance\").isCloseTo(147097390.6, ERROR);\n\n        SunPosition sp2 = SunPosition.compute()\n                        .on(2017, 4, 20, 13, 31, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(sp2.getDistance()).as(\"distance\").isCloseTo(150181373.3, ERROR);\n\n        SunPosition sp3 = SunPosition.compute()\n                        .on(2017, 7, 12, 13, 37, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(sp3.getDistance()).as(\"distance\").isCloseTo(152088309.0, ERROR);\n\n        SunPosition sp4 = SunPosition.compute()\n                        .on(2017, 10, 11, 13, 18, 0)\n                        .at(COLOGNE)\n                        .timezone(COLOGNE_TZ)\n                        .execute();\n        assertThat(sp4.getDistance()).as(\"distance\").isCloseTo(149380680.0, ERROR);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/SunTimesTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc;\n\nimport static java.lang.Math.abs;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.commons.suncalc.Locations.*;\n\nimport java.time.Duration;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.EnumMap;\nimport java.util.Map;\n\nimport org.assertj.core.api.AbstractDateAssert;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.shredzone.commons.suncalc.SunTimes.Twilight;\n\n/**\n * Unit tests for {@link SunTimes}.\n */\npublic class SunTimesTest {\n\n    @BeforeClass\n    public static void init() {\n        AbstractDateAssert.registerCustomDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n    }\n\n    @Test\n    public void testCologne() {\n        Map<Twilight, String> riseTimes = new EnumMap<>(Twilight.class);\n        riseTimes.put(Twilight.ASTRONOMICAL, \"2017-08-10T01:44:18Z\");\n        riseTimes.put(Twilight.NAUTICAL,     \"2017-08-10T02:44:57Z\");\n        riseTimes.put(Twilight.NIGHT_HOUR,   \"2017-08-10T03:18:22Z\");\n        riseTimes.put(Twilight.CIVIL,        \"2017-08-10T03:34:01Z\");\n        riseTimes.put(Twilight.BLUE_HOUR,    \"2017-08-10T03:48:59Z\");\n        riseTimes.put(Twilight.VISUAL,       \"2017-08-10T04:11:49Z\");\n        riseTimes.put(Twilight.VISUAL_LOWER, \"2017-08-10T04:15:33Z\");\n        riseTimes.put(Twilight.HORIZON,      \"2017-08-10T04:17:44Z\");\n        riseTimes.put(Twilight.GOLDEN_HOUR,  \"2017-08-10T04:58:33Z\");\n\n        Map<Twilight, String> setTimes = new EnumMap<>(Twilight.class);\n        setTimes.put(Twilight.GOLDEN_HOUR,   \"2017-08-10T18:15:49Z\");\n        setTimes.put(Twilight.HORIZON,       \"2017-08-10T18:56:30Z\");\n        setTimes.put(Twilight.VISUAL_LOWER,  \"2017-08-10T18:58:39Z\");\n        setTimes.put(Twilight.VISUAL,        \"2017-08-10T19:02:20Z\");\n        setTimes.put(Twilight.BLUE_HOUR,     \"2017-08-10T19:25:16Z\");\n        setTimes.put(Twilight.CIVIL,         \"2017-08-10T19:40:13Z\");\n        setTimes.put(Twilight.NIGHT_HOUR,    \"2017-08-10T19:55:35Z\");\n        setTimes.put(Twilight.NAUTICAL,      \"2017-08-10T20:28:56Z\");\n        setTimes.put(Twilight.ASTRONOMICAL,  \"2017-08-10T21:28:43Z\");\n\n        for (Twilight angle : Twilight.values()) {\n            SunTimes times = SunTimes.compute().at(COLOGNE).on(2017, 8, 10).utc()\n                            .twilight(angle)\n                            .execute();\n            assertThat(times.getRise()).as(\"%s-rise\", angle.name()).isEqualTo(riseTimes.get(angle));\n            assertThat(times.getSet()).as(\"%s-set\", angle.name()).isEqualTo(setTimes.get(angle));\n            assertThat(times.getNoon()).as(\"%s-noon\", angle.name()).isEqualTo(\"2017-08-10T11:37:22Z\");\n            assertThat(times.getNadir()).as(\"%s-nadir\", angle.name()).isEqualTo(\"2017-08-10T23:37:45Z\");\n            assertThat(times.isAlwaysDown()).as(\"%s-always-down\", angle.name()).isFalse();\n            assertThat(times.isAlwaysUp()).as(\"%s-always-up\", angle.name()).isFalse();\n        }\n\n        SunTimes times = SunTimes.compute().at(COLOGNE).on(2017, 8, 10).utc()\n                        .twilight(-4.0)\n                        .execute();\n        assertThat(times.getRise()).as(\"rise\").isEqualTo(\"2017-08-10T03:48:59Z\");\n        assertThat(times.getSet()).as(\"set\").isEqualTo(\"2017-08-10T19:25:16Z\");\n        assertThat(times.getNoon()).as(\"noon\").isEqualTo(\"2017-08-10T11:37:22Z\");\n        assertThat(times.getNadir()).as(\"nadir\").isEqualTo(\"2017-08-10T23:37:45Z\");\n        assertThat(times.isAlwaysDown()).as(\"always-down\").isFalse();\n        assertThat(times.isAlwaysUp()).as(\"always-up\").isFalse();\n    }\n\n    @Test\n    public void testAlert() {\n        SunTimes t1 = SunTimes.compute().at(ALERT).on(2017, 8, 10).utc()\n                        .oneDay()\n                        .execute();\n        assertTimes(t1, null, null, \"2017-08-10T16:13:14Z\", true);\n\n        SunTimes t2 = SunTimes.compute().at(ALERT).on(2017, 9, 24).utc()\n                        .execute();\n        assertTimes(t2, \"2017-09-24T09:54:29Z\", \"2017-09-24T22:02:01Z\", \"2017-09-24T15:59:16Z\");\n\n        SunTimes t3 = SunTimes.compute().at(ALERT).on(2017, 2, 10).utc()\n                        .oneDay()\n                        .execute();\n        assertTimes(t3, null, null, \"2017-02-10T16:25:09Z\", false);\n\n        SunTimes t4 = SunTimes.compute().at(ALERT).on(2017, 8, 10).utc()\n                        .execute();\n        assertTimes(t4, \"2017-09-06T05:13:15Z\", \"2017-09-06T03:06:02Z\", \"2017-08-10T16:13:14Z\");\n\n        SunTimes t5 = SunTimes.compute().at(ALERT).on(2017, 2, 10).utc()\n                        .execute();\n        assertTimes(t5, \"2017-02-27T15:24:18Z\", \"2017-02-27T17:23:46Z\", \"2017-02-10T16:25:09Z\");\n\n        SunTimes t6 = SunTimes.compute().at(ALERT).on(2017, 9, 6).utc()\n                        .execute();\n        assertTimes(t6, \"2017-09-06T05:13:15Z\", \"2017-09-06T03:06:02Z\", \"2017-09-06T16:05:41Z\");\n\n        // Summer solstice is the worst case for noon calculation\n        SunTimes t7 = SunTimes.compute().at(ALERT).on(2020, 6, 20).utc()\n                        .limit(Duration.ofDays(2L))\n                        .execute();\n        assertTimes(t7, null, null, \"2020-06-20T16:11:02Z\", true);\n\n        // Reverse check\n        SunTimes t8 = SunTimes.compute().at(ALERT).on(2017, 2, 28).utc()\n                .reverse()\n                .execute();\n        assertTimes(t8, \"2017-02-27T15:23:52Z\", \"2017-02-27T17:23:14Z\", \"2017-02-27T16:23:47Z\");\n    }\n\n    @Test\n    public void testWellington() {\n        SunTimes t1 = SunTimes.compute().at(WELLINGTON).on(2017, 8, 10).timezone(WELLINGTON_TZ)\n                        .execute();\n        assertTimes(t1, \"2017-08-09T19:18:33Z\", \"2017-08-10T05:34:50Z\", \"2017-08-10T00:26:33Z\");\n    }\n\n    @Test\n    public void testWellingtonReverse() {\n        SunTimes t1 = SunTimes.compute().at(WELLINGTON).on(2017, 8, 10).timezone(WELLINGTON_TZ)\n                .reverse()\n                .execute();\n        assertTimes(t1, \"2017-08-08T19:19:35Z\", \"2017-08-09T05:33:36Z\", \"2017-08-09T00:26:42Z\");\n    }\n\n    @Test\n    public void testPuertoWilliams() {\n        SunTimes t1 = SunTimes.compute().at(PUERTO_WILLIAMS).on(2017, 8, 10).timezone(PUERTO_WILLIAMS_TZ)\n                        .execute();\n        assertTimes(t1, \"2017-08-10T12:01:51Z\", \"2017-08-10T21:10:36Z\", \"2017-08-10T16:36:07Z\");\n    }\n\n    @Test\n    public void testSingapore() {\n        SunTimes t1 = SunTimes.compute().at(SINGAPORE).on(2017, 8, 10).timezone(SINGAPORE_TZ)\n                        .execute();\n        assertTimes(t1, \"2017-08-09T23:05:13Z\", \"2017-08-10T11:14:56Z\", \"2017-08-10T05:10:07Z\");\n    }\n\n    @Test\n    public void testMartinique() {\n        SunTimes t1 = SunTimes.compute().at(MARTINIQUE).on(2019, 7, 1).timezone(MARTINIQUE_TZ)\n                        .execute();\n        assertTimes(t1, \"2019-07-01T09:38:35Z\", \"2019-07-01T22:37:23Z\", \"2019-07-01T16:07:57Z\");\n    }\n\n    @Test\n    public void testSydney() {\n        SunTimes t1 = SunTimes.compute().at(SYDNEY).on(2019, 7, 3).timezone(SYDNEY_TZ)\n                        .execute();\n        assertTimes(t1, \"2019-07-02T21:00:35Z\", \"2019-07-03T06:58:02Z\", \"2019-07-03T01:59:18Z\");\n    }\n\n    @Test\n    public void testElevation() {\n        // At the top of the Tokyo Skytree\n        SunTimes skytree = SunTimes.compute().at(35.710046, 139.810718).on(2020, 6, 25).timezone(\"Asia/Tokyo\")\n                .elevation(634.0).execute();\n        assertTimes(skytree, \"2020-06-24T19:21:46Z\", \"2020-06-25T10:05:17Z\", \"2020-06-25T02:43:28Z\");\n\n        // In an airplane at 38,000 feet\n        SunTimes airplane = SunTimes.compute().at(46.58, -6.3).on(2020, 6, 25).utc()\n                .elevation(11582.4).execute();\n        assertTimes(airplane, \"2020-06-25T04:07:33Z\", \"2020-06-25T20:48:32Z\", \"2020-06-25T12:28:00Z\");\n    }\n\n    @Test\n    public void testJustBeforeJustAfter() {\n        // Thanks to @isomeme for providing the test cases for issue #18.\n\n        long shortDuration = 2;\n        long longDuration = 30;\n        SunTimes.Parameters param = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ)\n                .on(2020, 5, 3);\n        ZonedDateTime noon = param.execute().getNoon();\n        ZonedDateTime noonNextDay = param.plusDays(1).execute().getNoon();\n        long acceptableError = 65 * 1000L;\n\n        ZonedDateTime wellBeforeNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ)\n                .on(noon.minusMinutes(longDuration))\n                .execute().getNoon();\n        assertThat(Duration.between(wellBeforeNoon, noon).abs().toMillis())\n                .as(\"wellBeforeNoon\").isLessThan(acceptableError);\n\n        ZonedDateTime justBeforeNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ)\n                .on(noon.minusMinutes(shortDuration))\n                .execute().getNoon();\n        assertThat(Duration.between(justBeforeNoon, noon).abs().toMillis())\n                .as(\"justBeforeNoon\").isLessThan(acceptableError);\n\n        ZonedDateTime justAfterNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ)\n                .on(noon.plusMinutes(shortDuration))\n                .execute().getNoon();\n        assertThat(Duration.between(justAfterNoon, noonNextDay).abs().toMillis())\n                .as(\"justAfterNoon\").isLessThan(acceptableError);\n\n        ZonedDateTime wellAfterNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ)\n                .on(noon.plusMinutes(longDuration))\n                .execute().getNoon();\n        assertThat(Duration.between(wellAfterNoon, noonNextDay).abs().toMillis())\n                .as(\"wellAfterNoon\").isLessThan(acceptableError);\n\n        ZonedDateTime nadirWellAfterNoon = SunTimes.compute().on(wellAfterNoon).timezone(SANTA_MONICA_TZ)\n                .at(SANTA_MONICA).execute().getNadir();\n        ZonedDateTime nadirJustBeforeNadir = SunTimes.compute()\n                .on(nadirWellAfterNoon.minusMinutes(shortDuration))\n                .at(SANTA_MONICA).timezone(SANTA_MONICA_TZ).execute().getNadir();\n        assertThat(Duration.between(nadirWellAfterNoon, nadirJustBeforeNadir).abs().toMillis())\n                .as(\"nadir\").isLessThan(acceptableError);\n    }\n\n    @Test\n    public void testNoonNadirAzimuth() {\n        // Thanks to @isomeme for providing the test cases for issue #20.\n\n        assertNoonNadirPrecision(ZonedDateTime.of(2020, 6, 2, 3, 30, 0, 0, SANTA_MONICA_TZ), SANTA_MONICA);\n        assertNoonNadirPrecision(ZonedDateTime.of(2020, 6, 16, 4, 11, 0, 0, SANTA_MONICA_TZ), SANTA_MONICA);\n    }\n\n    @Test\n    public void testSequence() {\n        long acceptableError = 62 * 1000L;\n\n        ZonedDateTime riseBefore = createDate(2017, 11, 25, 7, 4);\n        ZonedDateTime riseAfter = createDate(2017, 11, 26, 7, 6);\n        ZonedDateTime setBefore = createDate(2017, 11, 25, 15, 33);\n        ZonedDateTime setAfter = createDate(2017, 11, 26, 15, 32);\n\n        for (int hour = 0; hour < 24; hour++) {\n            for (int minute = 0; minute < 60; minute++) {\n                SunTimes times = SunTimes.compute()\n                            .at(COLOGNE)\n                            .on(2017, 11, 25, hour, minute, 0).utc()\n                            .fullCycle()\n                            .execute();\n\n                ZonedDateTime rise = times.getRise();\n                ZonedDateTime set = times.getSet();\n\n                assertThat(rise).isNotNull();\n                assertThat(set).isNotNull();\n\n                if (hour < 7 || (hour == 7 && minute <= 4)) {\n                    long diff = Duration.between(rise, riseBefore).abs().toMillis();\n                    assertThat(diff).as(\"rise @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                } else {\n                    long diff = Duration.between(rise, riseAfter).abs().toMillis();\n                    assertThat(diff).as(\"rise @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                }\n\n                if (hour < 15 || (hour == 15 && minute <= 33)) {\n                    long diff = Duration.between(set, setBefore).abs().toMillis();\n                    assertThat(diff).as(\"set @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                } else {\n                    long diff = Duration.between(set, setAfter).abs().toMillis();\n                    assertThat(diff).as(\"set @%02d:%02d\", hour, minute).isLessThan(acceptableError);\n                }\n            }\n        }\n    }\n\n    private ZonedDateTime createDate(int year, int month, int day, int hour, int minute) {\n        return ZonedDateTime.of(year, month, day, hour, minute, 0, 0, ZoneId.of(\"UTC\"));\n    }\n\n    private void assertNoonNadirPrecision(ZonedDateTime time, double[] location) {\n        SunTimes sunTimes = SunTimes.compute()\n                .at(location)\n                .on(time)\n                .execute();\n\n        SunPosition sunPositionAtNoon = SunPosition.compute()\n                .at(location)\n                .on(sunTimes.getNoon())\n                .execute();\n\n        SunPosition sunPositionAtNadir = SunPosition.compute()\n                .at(location)\n                .on(sunTimes.getNadir())\n                .execute();\n\n        assertThat(abs(sunPositionAtNoon.getAzimuth() - 180.0)).isLessThan(0.1);\n        assertThat(abs(sunPositionAtNadir.getAzimuth() - 360.0)).isLessThan(0.1);\n    }\n\n    private void assertTimes(SunTimes t, String rise, String set, String noon) {\n        assertTimes(t, rise, set, noon, null);\n    }\n\n    private void assertTimes(SunTimes t, String rise, String set, String noon, Boolean alwaysUp) {\n        if (rise != null) {\n            assertThat(t.getRise()).as(\"sunrise\").isEqualTo(rise);\n        } else {\n            assertThat(t.getRise()).as(\"sunrise\").isNull();\n        }\n\n        if (set != null) {\n            assertThat(t.getSet()).as(\"sunset\").isEqualTo(set);\n        } else {\n            assertThat(t.getSet()).as(\"sunset\").isNull();\n        }\n\n        assertThat(t.getNoon()).as(\"noon\").isEqualTo(noon);\n\n        if (alwaysUp != null) {\n            assertThat(t.isAlwaysDown()).as(\"always-down\").isNotEqualTo(alwaysUp);\n            assertThat(t.isAlwaysUp()).as(\"always-up\").isEqualTo(alwaysUp);\n        } else {\n            assertThat(t.isAlwaysDown()).as(\"always-down\").isFalse();\n            assertThat(t.isAlwaysUp()).as(\"always-up\").isFalse();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/BaseBuilderTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.toRadians;\n\nimport static org.assertj.core.api.Assertions.*;\n\nimport java.time.Duration;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.Calendar;\nimport java.util.TimeZone;\n\nimport org.assertj.core.api.AbstractDateAssert;\nimport org.assertj.core.data.Offset;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.shredzone.commons.suncalc.Locations;\n\n/**\n * Unit tests for {@link BaseBuilder}.\n *\n * @author Richard \"Shred\" Körber\n */\npublic class BaseBuilderTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n    private static final ZonedDateTime NOW = ZonedDateTime.now();\n\n    @BeforeClass\n    public static void init() {\n        AbstractDateAssert.registerCustomDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n    }\n\n    @Test\n    public void testLocationParameters() {\n        TestBuilder p = new TestBuilder();\n        TestBuilder r;\n\n        assertThatIllegalStateException().isThrownBy(p::getLatitude);\n        assertThatIllegalStateException().isThrownBy(p::getLongitude);\n        assertThatNoException().isThrownBy(p::getElevation);\n        assertThat(p.hasLocation()).isFalse();\n\n        p.latitude(0.0);\n        assertThatNoException().isThrownBy(p::getLatitude);\n        assertThatIllegalStateException().isThrownBy(p::getLongitude);\n        assertThat(p.hasLocation()).isFalse();\n\n        p.longitude(0.0);\n        assertThatNoException().isThrownBy(p::getLatitude);\n        assertThatNoException().isThrownBy(p::getLongitude);\n        assertThat(p.hasLocation()).isTrue();\n\n        p.clearLocation();\n        assertThatIllegalStateException().isThrownBy(p::getLatitude);\n        assertThatIllegalStateException().isThrownBy(p::getLongitude);\n        assertThat(p.hasLocation()).isFalse();\n\n        r = p.at(12.34, 34.56);\n        assertLatLng(p, 12.34, 34.56, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.at(new double[] { 13.43, 51.23 });\n        assertLatLng(p, 13.43, 51.23, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.latitude(-11.22);\n        assertLatLng(p, -11.22, 51.23, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.longitude(-8.23);\n        assertLatLng(p, -11.22, -8.23, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.latitude(5, 7, 37.2);\n        assertLatLng(p, 5.127, -8.23, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.longitude(-12, 43, 22.8);\n        assertLatLng(p, 5.127, -12.723, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.elevation(18267.3);\n        assertLatLng(p, 5.127, -12.723, 18267.3);\n        assertThat(r).isSameAs(p);\n\n        // Negative elevations are always changed to 0.0\n        r = p.elevation(-10.2);\n        assertLatLng(p, 5.127, -12.723, 0.0);\n        assertThat(r).isSameAs(p);\n\n        r = p.elevationFt(12000.0);\n        assertLatLng(p, 5.127, -12.723, 3657.6);\n        assertThat(r).isSameAs(p);\n\n        r = p.at(new double[] { 1.22, -3.44, 323.0 });\n        assertLatLng(p, 1.22, -3.44, 323.0);\n        assertThat(r).isSameAs(p);\n\n        TestBuilder s = new TestBuilder();\n        s.sameLocationAs(p);\n        assertLatLng(s, 1.22, -3.44, 323.0);\n    }\n\n    @Test\n    public void testBadLocations() {\n        TestBuilder p = new TestBuilder();\n\n        // At least two array records are required\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> p.at(new double[] { 12.0 }));\n\n        // No more than three array records are permitted\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> p.at(new double[] { 12.0, 34.0, 56.0, 78.0 }));\n\n        // Latitude out of range (negative)\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> p.latitude(-90.1));\n\n        // Latitude out of range (positive)\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> p.latitude(90.1));\n\n        // Longitude out of range (negative)\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> p.longitude(-180.1));\n\n        // Longitude out of range (positive)\n        assertThatIllegalArgumentException()\n                .isThrownBy(() -> p.longitude(180.1));\n    }\n\n    @Test\n    public void testTimeParameters() {\n        TestBuilder p = new TestBuilder();\n        TestBuilder r;\n\n        assertThat(p.getJulianDate().getDateTime()).isNotNull();\n\n        p.on(NOW);\n        assertDate(p,\n                NOW.getYear(),\n                NOW.getMonthValue(),\n                NOW.getDayOfMonth(),\n                NOW.getHour(),\n                NOW.getMinute(),\n                NOW.getSecond(),\n                NOW.getZone());\n\n        r = p.on(2017, 8, 12);\n        assertDate(p, 2017, 8, 12, 0, 0, 0, ZoneId.systemDefault());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2016, 4, 10, 14, 11, 59);\n        assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.systemDefault());\n        assertThat(r).isSameAs(p);\n\n        r = p.timezone(\"Europe/Berlin\");\n        assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of(\"Europe/Berlin\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.timezone(ZoneId.of(\"Asia/Tokyo\"));\n        assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of(\"Asia/Tokyo\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.timezone(TimeZone.getTimeZone(\"America/New_York\"));\n        assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of(\"America/New_York\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.utc();\n        assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of(\"UTC\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.on(LocalDate.of(2020, 3, 12));\n        assertDate(p, 2020, 3, 12, 0, 0, 0, ZoneId.of(\"UTC\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.on(ZonedDateTime.of(2020, 7, 11, 2, 44, 12, 0, ZoneId.of(\"UTC\")).toInstant());\n        assertDate(p, 2020, 7, 11, 2, 44, 12, ZoneId.of(\"UTC\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.on(LocalDateTime.of(2020, 4, 1, 12, 45, 33));\n        assertDate(p, 2020, 4, 1, 12, 45, 33, ZoneId.of(\"UTC\"));\n        assertThat(r).isSameAs(p);\n\n        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(\"Europe/Berlin\"));\n        cal.set(2018, Calendar.AUGUST, 12, 3, 23, 11);\n        r = p.on(cal);\n        assertDate(p, 2018, 8, 12, 3, 23, 11, ZoneId.of(\"Europe/Berlin\"));\n        assertThat(r).isSameAs(p);\n\n        cal.set(Calendar.YEAR, 2019);\n        r = p.on(cal.getTime());\n        assertDate(p, 2019, 8, 12, 3, 23, 11, ZoneId.of(\"Europe/Berlin\"));\n        assertThat(r).isSameAs(p);\n\n        r = p.localTime();\n        assertDate(p, 2019, 8, 12, 3, 23, 11, ZoneId.systemDefault());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2012, 3, 11, 8, 1, 12).midnight();\n        assertDate(p, 2012, 3, 11, 0, 0, 0, ZoneId.systemDefault());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2012, 1, 24, 1, 33, 12).plusDays(100);\n        assertDate(p, 2012, 5, 3, 1, 33, 12, ZoneId.systemDefault());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2000, 1, 1, 2, 3, 4).now();\n        assertDate(p,\n                NOW.getYear(),\n                NOW.getMonthValue(),\n                NOW.getDayOfMonth(),\n                NOW.getHour(),\n                NOW.getMinute(),\n                NOW.getSecond(),\n                NOW.getZone());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2000, 2, 2, 3, 4, 5).today();\n        assertDate(p,\n                NOW.getYear(),\n                NOW.getMonthValue(),\n                NOW.getDayOfMonth(),\n                0, 0, 0,\n                NOW.getZone());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2000, 2, 2, 3, 4, 5).tomorrow();\n        ZonedDateTime tomorrow = NOW.plusDays(1);\n        assertDate(p,\n                tomorrow.getYear(),\n                tomorrow.getMonthValue(),\n                tomorrow.getDayOfMonth(),\n                0, 0, 0,\n                tomorrow.getZone());\n        assertThat(r).isSameAs(p);\n\n        r = p.on(2000, 3, 3, 4, 5, 6).on(NOW);\n        assertDate(p,\n                NOW.getYear(),\n                NOW.getMonthValue(),\n                NOW.getDayOfMonth(),\n                NOW.getHour(),\n                NOW.getMinute(),\n                NOW.getSecond(),\n                NOW.getZone());\n        assertThat(r).isSameAs(p);\n\n        TestBuilder s = new TestBuilder();\n        s.sameTimeAs(p);\n        assertDate(s,\n                NOW.getYear(),\n                NOW.getMonthValue(),\n                NOW.getDayOfMonth(),\n                NOW.getHour(),\n                NOW.getMinute(),\n                NOW.getSecond(),\n                NOW.getZone());\n    }\n\n    @Test\n    public void testWindowParameters() {\n        TestBuilder p = new TestBuilder();\n        TestBuilder r;\n\n        assertThat(p.getDuration()).isNotNull().isEqualTo(Duration.ofDays(365L));\n\n        r = p.oneDay();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(1L));\n        assertThat(r).isSameAs(p);\n\n        r = p.fullCycle();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(365L));\n        assertThat(r).isSameAs(p);\n\n        r = p.limit(Duration.ofDays(14L).negated());\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L).negated());\n        assertThat(r).isSameAs(p);\n\n        r = p.forward();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L));\n        assertThat(r).isSameAs(p);\n\n        // Second forward won't negate again!\n        r = p.forward();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L));\n        assertThat(r).isSameAs(p);\n\n        r = p.reverse();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L).negated());\n        assertThat(r).isSameAs(p);\n\n        // Second reverse won't negate again!\n        r = p.reverse();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L).negated());\n        assertThat(r).isSameAs(p);\n\n        r = p.forward();\n        assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L));\n        assertThat(r).isSameAs(p);\n\n        r = p.limit(Duration.ofHours(12L));\n        assertThat(p.getDuration()).isEqualTo(Duration.ofHours(12L));\n        assertThat(r).isSameAs(p);\n\n        TestBuilder s = new TestBuilder();\n        s.sameWindowAs(p);\n        assertThat(s.getDuration()).isEqualTo(Duration.ofHours(12L));\n\n        p.reverse();\n        TestBuilder s2 = new TestBuilder();\n        s2.sameWindowAs(p);\n        assertThat(s2.getDuration()).isEqualTo(Duration.ofHours(12L).negated());\n\n        assertThatNullPointerException().isThrownBy(() -> {\n            p.limit(null);\n        });\n    }\n\n    @Test\n    public void testCopy() {\n        ZonedDateTime now = ZonedDateTime.now();\n        ZonedDateTime tomorrow = now.plusDays(1);\n        ZonedDateTime yesterday = now.minusDays(1);\n        Duration duration = Duration.ofDays(60L);\n\n        // Set test parameters\n        TestBuilder p1 = new TestBuilder();\n        p1.at(Locations.COLOGNE);\n        p1.on(now);\n        p1.elevation(123.0);\n        p1.limit(duration);\n\n        // Make sure copy has identical values\n        TestBuilder p2 = p1.copy();\n        assertThat(p2.getLatitude()).isEqualTo(Locations.COLOGNE[0]);\n        assertThat(p2.getLongitude()).isEqualTo(Locations.COLOGNE[1]);\n        assertThat(p2.getJulianDate().getDateTime()).isEqualTo(now);\n        assertThat(p2.getElevation()).isEqualTo(123.0);\n        assertThat(p2.getDuration()).isEqualTo(Duration.ofDays(60L));\n\n        // Make sure changes to p1 won't affect p2\n        p1.at(Locations.SINGAPORE);\n        p1.on(tomorrow);\n        p1.oneDay();\n        assertThat(p1.getLatitude()).isEqualTo(Locations.SINGAPORE[0]);\n        assertThat(p1.getLongitude()).isEqualTo(Locations.SINGAPORE[1]);\n        assertThat(p1.getJulianDate().getDateTime()).isEqualTo(tomorrow);\n        assertThat(p1.getDuration()).isEqualTo(Duration.ofDays(1L));\n        assertThat(p2.getLatitude()).isEqualTo(Locations.COLOGNE[0]);\n        assertThat(p2.getLongitude()).isEqualTo(Locations.COLOGNE[1]);\n        assertThat(p2.getJulianDate().getDateTime()).isEqualTo(now);\n        assertThat(p2.getDuration()).isEqualTo(Duration.ofDays(60L));\n\n        // Make sure changes to p2 won't affect p1\n        p2.at(Locations.WELLINGTON);\n        p2.on(yesterday);\n        p2.fullCycle();\n        assertThat(p1.getLatitude()).isEqualTo(Locations.SINGAPORE[0]);\n        assertThat(p1.getLongitude()).isEqualTo(Locations.SINGAPORE[1]);\n        assertThat(p1.getJulianDate().getDateTime()).isEqualTo(tomorrow);\n        assertThat(p1.getDuration()).isEqualTo(Duration.ofDays(1L));\n        assertThat(p2.getLatitude()).isEqualTo(Locations.WELLINGTON[0]);\n        assertThat(p2.getLongitude()).isEqualTo(Locations.WELLINGTON[1]);\n        assertThat(p2.getJulianDate().getDateTime()).isEqualTo(yesterday);\n        assertThat(p2.getDuration()).isEqualTo(Duration.ofDays(365L));\n    }\n\n    private void assertLatLng(TestBuilder p, double lat, double lng, double elev) {\n        assertThat(p.getLatitude()).as(\"latitude\").isCloseTo(lat, ERROR);\n        assertThat(p.getLongitude()).as(\"longitude\").isCloseTo(lng, ERROR);\n        assertThat(p.getLatitudeRad()).as(\"latitude-rad\").isCloseTo(toRadians(lat), ERROR);\n        assertThat(p.getLongitudeRad()).as(\"longitude-rad\").isCloseTo(toRadians(lng), ERROR);\n        assertThat(p.getElevation()).as(\"elevation\").isCloseTo(elev, ERROR);\n    }\n\n    private void assertDate(TestBuilder p, int year, int month, int day,\n            int hour, int minute, int second, ZoneId tz) {\n        ZonedDateTime cal = p.getJulianDate().getDateTime();\n\n        assertThat(cal.getYear()).as(\"year\").isEqualTo(year);\n        assertThat(cal.getMonthValue()).as(\"month\").isEqualTo(month);\n        assertThat(cal.getDayOfMonth()).as(\"day\").isEqualTo(day);\n        assertThat(cal.getHour()).as(\"hour\").isEqualTo(hour);\n        assertThat(cal.getMinute()).as(\"minute\").isEqualTo(minute);\n        assertThat(cal.getSecond()).as(\"second\").isEqualTo(second);\n        assertThat(cal.getZone()).as(\"timezone\").isEqualTo(tz);\n    }\n\n    private static class TestBuilder extends BaseBuilder<TestBuilder> {\n        @Override\n        public TestBuilder now() {\n            return on(NOW);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/ExtendedMathTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.shredzone.commons.suncalc.util.ExtendedMath.*;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link ExtendedMath}.\n */\npublic class ExtendedMathTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n\n    @Test\n    public void testFrac() {\n        assertThat(frac(   1.0 )).isCloseTo( 0.0 , ERROR);\n        assertThat(frac(   0.5 )).isCloseTo( 0.5 , ERROR);\n        assertThat(frac( 123.25)).isCloseTo( 0.25, ERROR);\n        assertThat(frac(   0.0 )).isCloseTo( 0.0 , ERROR);\n        assertThat(frac(  -1.0 )).isCloseTo( 0.0 , ERROR);\n        assertThat(frac(  -0.5 )).isCloseTo(-0.5 , ERROR);\n        assertThat(frac(-123.25)).isCloseTo(-0.25, ERROR);\n    }\n\n    @Test\n    public void testIsZero() {\n        assertThat(isZero( 1.0   )).isFalse();\n        assertThat(isZero( 0.0001)).isFalse();\n        assertThat(isZero( 0.0   )).isTrue();\n        assertThat(isZero(-0.0   )).isTrue();\n        assertThat(isZero(-0.0001)).isFalse();\n        assertThat(isZero(-1.0   )).isFalse();\n        assertThat(isZero( Double.NaN)).isFalse();\n        assertThat(isZero(-Double.NaN)).isFalse();\n        assertThat(isZero( Double.POSITIVE_INFINITY)).isFalse();\n        assertThat(isZero( Double.NEGATIVE_INFINITY)).isFalse();\n    }\n\n    @Test\n    public void testDms() {\n        // Valid parameters\n        assertThat(dms(  0,   0,   0.0 )).isEqualTo(0.0);\n        assertThat(dms( 13,  27,   4.32)).isEqualTo(13.4512);\n        assertThat(dms(-88,  39,   8.28)).isEqualTo(-88.6523);\n\n        // Sign at wrong position is ignored\n        assertThat(dms( 14, -14,   2.4)).isEqualTo(14.234);\n        assertThat(dms( 66,  12, -46.8)).isEqualTo(66.213);\n\n        // Out of range values are carried to the next position\n        assertThat(dms(  0,   0,  72.0)).isEqualTo(0.02);   // 0°  1' 12.0\"\n        assertThat(dms(  1,  80, 132.0)).isEqualTo(2.37);   // 2° 22' 12.0\"\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/JulianDateTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.PI;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.data.Offset.offset;\n\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\n\nimport org.assertj.core.api.AbstractDateAssert;\nimport org.assertj.core.data.Offset;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link JulianDate}.\n */\npublic class JulianDateTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n\n    @BeforeClass\n    public static void init() {\n        AbstractDateAssert.registerCustomDateFormat(\"yyyy-MM-dd'T'HH:mm:ssX\");\n    }\n\n    @Test\n    public void testAtHour() {\n        JulianDate jd = new JulianDate(of(2017, 8, 19, 0, 0, 0, \"UTC\"));\n        assertDate(jd, \"2017-08-19T00:00:00Z\");\n\n        JulianDate jd2 = jd.atHour(8.5);\n        assertDate(jd2, \"2017-08-19T08:30:00Z\");\n\n        JulianDate jd3 = new JulianDate(of(2017, 8, 19, 0, 0, 0, \"Europe/Berlin\"));\n        assertDate(jd3, \"2017-08-19T00:00:00+02:00\");\n\n        JulianDate jd4 = jd3.atHour(8.5);\n        assertDate(jd4, \"2017-08-19T08:30:00+02:00\");\n    }\n\n    @Test\n    public void testModifiedJulianDate() {\n        // MJD epoch is midnight of November 17th, 1858.\n        JulianDate jd1 = new JulianDate(of(1858, 11, 17, 0, 0, 0, \"UTC\"));\n        assertThat(jd1.getModifiedJulianDate()).isZero();\n        assertThat(jd1.toString()).isEqualTo(\"0d 00h 00m 00s\");\n\n        JulianDate jd2 = new JulianDate(of(2017, 8, 19, 15, 6, 16, \"UTC\"));\n        assertThat(jd2.getModifiedJulianDate()).isCloseTo(57984.629, ERROR);\n        assertThat(jd2.toString()).isEqualTo(\"57984d 15h 06m 16s\");\n\n        JulianDate jd3 = new JulianDate(of(2017, 8, 19, 15, 6, 16, \"GMT+2\"));\n        assertThat(jd3.getModifiedJulianDate()).isCloseTo(57984.546, ERROR);\n        assertThat(jd3.toString()).isEqualTo(\"57984d 13h 06m 16s\");\n    }\n\n    @Test\n    public void testJulianCentury() {\n        JulianDate jd1 = new JulianDate(of(2000, 1, 1, 0, 0, 0, \"UTC\"));\n        assertThat(jd1.getJulianCentury()).isCloseTo(0.0, ERROR);\n\n        JulianDate jd2 = new JulianDate(of(2017, 1, 1, 0, 0, 0, \"UTC\"));\n        assertThat(jd2.getJulianCentury()).isCloseTo(0.17, ERROR);\n\n        JulianDate jd3 = new JulianDate(of(2050, 7, 1, 0, 0, 0, \"UTC\"));\n        assertThat(jd3.getJulianCentury()).isCloseTo(0.505, ERROR);\n    }\n\n    @Test\n    public void testGreenwichMeanSiderealTime() {\n        JulianDate jd1 = new JulianDate(of(2017, 9, 3, 19, 5, 15, \"UTC\"));\n        assertThat(jd1.getGreenwichMeanSiderealTime()).isCloseTo(4.702, ERROR);\n\n    }\n\n    @Test\n    public void testTrueAnomaly() {\n        JulianDate jd1 = new JulianDate(of(2017, 1, 4, 0, 0, 0, \"UTC\"));\n        assertThat(jd1.getTrueAnomaly()).isCloseTo(0.0, offset(0.1));\n\n        JulianDate jd2 = new JulianDate(of(2017, 7, 4, 0, 0, 0, \"UTC\"));\n        assertThat(jd2.getTrueAnomaly()).isCloseTo(PI, offset(0.1));\n    }\n\n    @Test\n    public void testAtModifiedJulianDate() {\n        JulianDate jd1 = new JulianDate(of(2017, 8, 19, 15, 6, 16, \"UTC\"));\n\n        JulianDate jd2 = new JulianDate(ZonedDateTime.now(ZoneId.of(\"UTC\")))\n                        .atModifiedJulianDate(jd1.getModifiedJulianDate());\n\n        assertThat(jd2.getDateTime()).isEqualTo(jd1.getDateTime());\n    }\n\n    @Test\n    public void testAtJulianCentury() {\n        JulianDate jd1 = new JulianDate(of(2017, 1, 1, 0, 0, 0, \"UTC\"));\n\n        JulianDate jd2 = new JulianDate(ZonedDateTime.now(ZoneId.of(\"UTC\")))\n                        .atJulianCentury(jd1.getJulianCentury());\n\n        assertThat(jd2.getDateTime()).isEqualTo(jd1.getDateTime());\n    }\n\n    private ZonedDateTime of(int year, int month, int day, int hour, int minute, int second, String zone) {\n        return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.of(zone));\n    }\n\n    private void assertDate(JulianDate jd, String date) {\n        assertThat(jd.getDateTime()).isEqualTo(date);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/MatrixTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.PI;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link Matrix}.\n */\npublic class MatrixTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n    private static final double PI_HALF = PI / 2.0;\n\n    @Test\n    public void testIdentity() {\n        Matrix mx = Matrix.identity();\n        assertValues(mx,\n                1.0, 0.0, 0.0,\n                0.0, 1.0, 0.0,\n                0.0, 0.0, 1.0);\n    }\n\n    @Test\n    public void testRotateX() {\n        Matrix mx = Matrix.rotateX(PI_HALF);\n        assertValues(mx,\n                1.0,  0.0,  0.0,\n                0.0,  0.0,  1.0,\n                0.0, -1.0,  0.0);\n    }\n\n    @Test\n    public void testRotateY() {\n        Matrix mx = Matrix.rotateY(PI_HALF);\n        assertValues(mx,\n                0.0,  0.0, -1.0,\n                0.0,  1.0,  0.0,\n                1.0,  0.0,  0.0);\n    }\n\n    @Test\n    public void testRotateZ() {\n        Matrix mx = Matrix.rotateZ(PI_HALF);\n        assertValues(mx,\n                0.0,  1.0,  0.0,\n               -1.0,  0.0,  0.0,\n                0.0,  0.0,  1.0);\n    }\n\n    @Test\n    public void testTranspose() {\n        Matrix mx = Matrix.rotateX(PI_HALF).transpose();\n        assertValues(mx,\n                1.0,  0.0,  0.0,\n                0.0,  0.0, -1.0,\n                0.0,  1.0,  0.0);\n    }\n\n    @Test\n    public void testNegate() {\n        Matrix mx = Matrix.identity().negate();\n        assertValues(mx,\n               -1.0,  0.0,  0.0,\n                0.0, -1.0,  0.0,\n                0.0,  0.0, -1.0);\n    }\n\n    @Test\n    public void testAdd() {\n        Matrix mx1 = Matrix.rotateX(PI_HALF);\n        Matrix mx2 = Matrix.rotateY(PI_HALF);\n\n        assertValues(mx1.add(mx2),\n                1.0,  0.0, -1.0,\n                0.0,  1.0,  1.0,\n                1.0, -1.0,  0.0);\n\n        assertValues(mx2.add(mx1),\n                1.0,  0.0, -1.0,\n                0.0,  1.0,  1.0,\n                1.0, -1.0,  0.0);\n    }\n\n    @Test\n    public void testSubtract() {\n        Matrix mx1 = Matrix.rotateX(PI_HALF);\n        Matrix mx2 = Matrix.rotateY(PI_HALF);\n\n        assertValues(mx1.subtract(mx2),\n                1.0,  0.0,  1.0,\n                0.0, -1.0,  1.0,\n               -1.0, -1.0,  0.0);\n\n        assertValues(mx2.subtract(mx1),\n               -1.0,  0.0, -1.0,\n                0.0,  1.0, -1.0,\n                1.0,  1.0,  0.0);\n    }\n\n    @Test\n    public void testMultiply() {\n        Matrix mx1 = Matrix.rotateX(PI_HALF);\n        Matrix mx2 = Matrix.rotateY(PI_HALF);\n\n        assertValues(mx1.multiply(mx2),\n                0.0,  0.0, -1.0,\n                1.0,  0.0,  0.0,\n                0.0, -1.0,  0.0);\n\n        assertValues(mx2.multiply(mx1),\n                0.0,  1.0,  0.0,\n                0.0,  0.0,  1.0,\n                1.0,  0.0,  0.0);\n    }\n\n    @Test\n    public void testScalarMultiply() {\n        Matrix mx = Matrix.identity().multiply(5.0);\n        assertValues(mx,\n                5.0, 0.0, 0.0,\n                0.0, 5.0, 0.0,\n                0.0, 0.0, 5.0);\n    }\n\n    @Test\n    public void testVectorMultiply() {\n        Matrix mx = Matrix.rotateX(PI_HALF);\n        Vector vc = new Vector(5.0, 8.0, -3.0);\n        Vector result = mx.multiply(vc);\n        assertThat(result.getX()).isCloseTo( 5.0, ERROR);\n        assertThat(result.getY()).isCloseTo(-3.0, ERROR);\n        assertThat(result.getZ()).isCloseTo(-8.0, ERROR);\n    }\n\n    @Test\n    public void testEquals() {\n        Matrix mx1 = Matrix.identity();\n        Matrix mx2 = Matrix.rotateX(PI_HALF);\n        Matrix mx3 = Matrix.identity();\n\n        assertThat(mx1.equals(mx2)).isFalse();\n        assertThat(mx1.equals(mx3)).isTrue();\n        assertThat(mx2.equals(mx3)).isFalse();\n        assertThat(mx3.equals(mx1)).isTrue();\n        assertThat(mx1.equals(null)).isFalse();\n        assertThat(mx1.equals(new Object())).isFalse();\n    }\n\n    @Test\n    public void testHashCode() {\n        int mx1 = Matrix.identity().hashCode();\n        int mx2 = Matrix.rotateX(PI_HALF).hashCode();\n        int mx3 = Matrix.identity().hashCode();\n\n        assertThat(mx1).isNotEqualTo(0);\n        assertThat(mx2).isNotEqualTo(0);\n        assertThat(mx3).isNotEqualTo(0);\n        assertThat(mx1).isNotEqualTo(mx2);\n        assertThat(mx1).isEqualTo(mx3);\n    }\n\n    @Test\n    public void testToString() {\n        Matrix mx = Matrix.identity();\n\n        assertThat(mx.toString()).isEqualTo(\"[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]\");\n    }\n\n    private void assertValues(Matrix mx, double... values) {\n        for (int ix = 0; ix < values.length; ix++) {\n            int r = ix / 3;\n            int c = ix % 3;\n            assertThat(mx.get(r, c))\n                .as(\"r=%d, c=%d\", r, c)\n                .isCloseTo(values[ix], ERROR);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/PegasusTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2018 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.Assert.fail;\n\nimport java.util.function.Function;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link Pegasus}.\n */\npublic class PegasusTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n\n    @Test\n    public void testParabola() {\n        // f(x) = x^2 + 2x - 3\n        // Roots at x = -3 and x = 1\n        Function<Double, Double> parabola = x -> x * x + 2 * x - 3;\n\n        double r1 = Pegasus.calculate(0.0, 3.0, 0.1, parabola);\n        assertThat(r1).isCloseTo(1.0, ERROR);\n\n        double r2 = Pegasus.calculate(-5.0, 0.0, 0.1, parabola);\n        assertThat(r2).isCloseTo(-3.0, ERROR);\n\n        try {\n            Pegasus.calculate(-2.0, 0.5, 0.1, parabola);\n            fail(\"Found a non-existing root\");\n        } catch (ArithmeticException ex) {\n            // expected\n        }\n    }\n\n    @Test(expected = ArithmeticException.class)\n    public void testParabola2() {\n        // f(x) = x^2 + 3\n        // No roots\n        Pegasus.calculate(-5.0, 5.0, 0.1, x -> x * x + 3);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/QuadraticInterpolationTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link QuadraticInterpolation}.\n */\npublic class QuadraticInterpolationTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n\n    @Test\n    public void testTwoRootsAndMinimum() {\n        QuadraticInterpolation qi = new QuadraticInterpolation(1.0, -1.0, 1.0);\n\n        assertThat(qi.getNumberOfRoots()).isEqualTo(2);\n        assertThat(qi.getRoot1()).isCloseTo(-0.707, ERROR);\n        assertThat(qi.getRoot2()).isCloseTo( 0.707, ERROR);\n        assertThat(qi.getXe()).isCloseTo( 0.0, ERROR);\n        assertThat(qi.getYe()).isCloseTo(-1.0, ERROR);\n        assertThat(qi.isMaximum()).isFalse();\n    }\n\n    @Test\n    public void testTwoRootsAndMaximum() {\n        QuadraticInterpolation qi = new QuadraticInterpolation(-1.0, 1.0, -1.0);\n\n        assertThat(qi.getNumberOfRoots()).isEqualTo(2);\n        assertThat(qi.getRoot1()).isCloseTo(-0.707, ERROR);\n        assertThat(qi.getRoot2()).isCloseTo( 0.707, ERROR);\n        assertThat(qi.getXe()).isCloseTo(0.0, ERROR);\n        assertThat(qi.getYe()).isCloseTo(1.0, ERROR);\n        assertThat(qi.isMaximum()).isTrue();\n    }\n\n    @Test\n    public void testOneRoot() {\n        QuadraticInterpolation qi = new QuadraticInterpolation(2.0, 0.0, -1.0);\n\n        assertThat(qi.getNumberOfRoots()).isEqualTo(1);\n        assertThat(qi.getRoot1()).isCloseTo( 0.0, ERROR);\n        assertThat(qi.getXe()).isCloseTo( 1.5, ERROR);\n        assertThat(qi.getYe()).isCloseTo(-1.125, ERROR);\n        assertThat(qi.isMaximum()).isFalse();\n    }\n\n    @Test\n    public void testNoRoot() {\n        QuadraticInterpolation qi = new QuadraticInterpolation(3.0, 2.0, 1.0);\n\n        assertThat(qi.getNumberOfRoots()).isZero();\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/org/shredzone/commons/suncalc/util/VectorTest.java",
    "content": "/*\n * Shredzone Commons - suncalc\n *\n * Copyright (C) 2017 Richard \"Shred\" Körber\n *   http://commons.shredzone.org\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n */\npackage org.shredzone.commons.suncalc.util;\n\nimport static java.lang.Math.PI;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.assertj.core.data.Offset;\nimport org.junit.Test;\n\n/**\n * Unit tests for {@link Vector}.\n */\npublic class VectorTest {\n\n    private static final Offset<Double> ERROR = Offset.offset(0.001);\n    private static final double PI_HALF = PI / 2.0;\n\n    @Test\n    public void testConstructors() {\n        Vector v1 = new Vector(20.0, 10.0, 5.0);\n        assertThat(v1.getX()).isEqualTo(20.0);\n        assertThat(v1.getY()).isEqualTo(10.0);\n        assertThat(v1.getZ()).isEqualTo(5.0);\n\n        Vector v2 = new Vector(new double[] { 20.0, 10.0, 5.0 });\n        assertThat(v2.getX()).isEqualTo(20.0);\n        assertThat(v2.getY()).isEqualTo(10.0);\n        assertThat(v2.getZ()).isEqualTo(5.0);\n\n        Vector v3 = Vector.ofPolar(0.5, 0.25);\n        assertThat(v3.getPhi()).isEqualTo(0.5);\n        assertThat(v3.getTheta()).isEqualTo(0.25);\n        assertThat(v3.getR()).isEqualTo(1.0);\n\n        Vector v4 = Vector.ofPolar(0.5, 0.25, 50.0);\n        assertThat(v4.getPhi()).isEqualTo(0.5);\n        assertThat(v4.getTheta()).isEqualTo(0.25);\n        assertThat(v4.getR()).isEqualTo(50.0);\n    }\n\n    @Test(expected = IllegalArgumentException.class)\n    public void testBadConstructor() {\n        new Vector(new double[] { 20.0, 10.0 });\n    }\n\n    @Test\n    public void testAdd() {\n        Vector v1 = new Vector(20.0, 10.0, 5.0);\n        Vector v2 = new Vector(10.0, 25.0, 15.0);\n\n        Vector r1 = v1.add(v2);\n        assertThat(r1.getX()).isEqualTo(30.0);\n        assertThat(r1.getY()).isEqualTo(35.0);\n        assertThat(r1.getZ()).isEqualTo(20.0);\n\n        Vector r2 = v2.add(v1);\n        assertThat(r2.getX()).isEqualTo(30.0);\n        assertThat(r2.getY()).isEqualTo(35.0);\n        assertThat(r2.getZ()).isEqualTo(20.0);\n    }\n\n    @Test\n    public void testSubtract() {\n        Vector v1 = new Vector(20.0, 10.0, 5.0);\n        Vector v2 = new Vector(10.0, 25.0, 15.0);\n\n        Vector r1 = v1.subtract(v2);\n        assertThat(r1.getX()).isEqualTo(10.0);\n        assertThat(r1.getY()).isEqualTo(-15.0);\n        assertThat(r1.getZ()).isEqualTo(-10.0);\n\n        Vector r2 = v2.subtract(v1);\n        assertThat(r2.getX()).isEqualTo(-10.0);\n        assertThat(r2.getY()).isEqualTo(15.0);\n        assertThat(r2.getZ()).isEqualTo(10.0);\n    }\n\n    @Test\n    public void testMultiply() {\n        Vector v1 = new Vector(20.0, 10.0, 5.0);\n\n        Vector r1 = v1.multiply(5.0);\n        assertThat(r1.getX()).isEqualTo(100.0);\n        assertThat(r1.getY()).isEqualTo(50.0);\n        assertThat(r1.getZ()).isEqualTo(25.0);\n    }\n\n    @Test\n    public void testNegate() {\n        Vector v1 = new Vector(20.0, 10.0, 5.0);\n\n        Vector r1 = v1.negate();\n        assertThat(r1.getX()).isEqualTo(-20.0);\n        assertThat(r1.getY()).isEqualTo(-10.0);\n        assertThat(r1.getZ()).isEqualTo(-5.0);\n    }\n\n    @Test\n    public void testCross() {\n        Vector v1 = new Vector(3.0, -3.0, 1.0);\n        Vector v2 = new Vector(4.0, 9.0, 2.0);\n\n        Vector r1 = v1.cross(v2);\n        assertThat(r1.getX()).isEqualTo(-15.0);\n        assertThat(r1.getY()).isEqualTo(-2.0);\n        assertThat(r1.getZ()).isEqualTo(39.0);\n    }\n\n    @Test\n    public void testDot() {\n        Vector v1 = new Vector(1.0, 2.0, 3.0);\n        Vector v2 = new Vector(4.0, -5.0, 6.0);\n\n        double r1 = v1.dot(v2);\n        assertThat(r1).isCloseTo(12.0, ERROR);\n    }\n\n    @Test\n    public void testNorm() {\n        Vector v1 = new Vector(5.0, -6.0, 7.0);\n\n        double r1 = v1.norm();\n        assertThat(r1).isCloseTo(10.488, ERROR);\n    }\n\n    @Test\n    public void testEquals() {\n        Vector v1 = new Vector(3.0, -3.0, 1.0);\n        Vector v2 = new Vector(4.0, 9.0, 2.0);\n        Vector v3 = new Vector(3.0, -3.0, 1.0);\n\n        assertThat(v1.equals(v2)).isFalse();\n        assertThat(v1.equals(v3)).isTrue();\n        assertThat(v2.equals(v3)).isFalse();\n        assertThat(v3.equals(v1)).isTrue();\n        assertThat(v1.equals(null)).isFalse();\n        assertThat(v1.equals(new Object())).isFalse();\n    }\n\n    @Test\n    public void testHashCode() {\n        int h1 = new Vector(3.0, -3.0, 1.0).hashCode();\n        int h2 = new Vector(4.0, 9.0, 2.0).hashCode();\n        int h3 = new Vector(3.0, -3.0, 1.0).hashCode();\n\n        assertThat(h1).isNotZero();\n        assertThat(h2).isNotZero();\n        assertThat(h3).isNotZero();\n        assertThat(h1).isNotEqualTo(h2);\n        assertThat(h1).isEqualTo(h3);\n    }\n\n    @Test\n    public void testToString() {\n        Vector v1 = new Vector(3.0, -3.0, 1.0);\n\n        assertThat(v1.toString()).isEqualTo(\"(x=3.0, y=-3.0, z=1.0)\");\n    }\n\n    @Test\n    public void testToCartesian() {\n        Vector v1 = Vector.ofPolar(0.0, 0.0);\n        assertThat(v1.getX()).isCloseTo(1.0, ERROR);\n        assertThat(v1.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v1.getZ()).isCloseTo(0.0, ERROR);\n\n        Vector v2 = Vector.ofPolar(PI_HALF, 0.0);\n        assertThat(v2.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v2.getY()).isCloseTo(1.0, ERROR);\n        assertThat(v2.getZ()).isCloseTo(0.0, ERROR);\n\n        Vector v3 = Vector.ofPolar(0.0, PI_HALF);\n        assertThat(v3.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v3.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v3.getZ()).isCloseTo(1.0, ERROR);\n\n        Vector v4 = Vector.ofPolar(PI_HALF, PI_HALF);\n        assertThat(v4.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v4.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v4.getZ()).isCloseTo(1.0, ERROR);\n\n        Vector v5 = Vector.ofPolar(PI_HALF, -PI_HALF);\n        assertThat(v5.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v5.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v5.getZ()).isCloseTo(-1.0, ERROR);\n\n        Vector v6 = Vector.ofPolar(0.0, 0.0, 5.0);\n        assertThat(v6.getX()).isCloseTo(5.0, ERROR);\n        assertThat(v6.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v6.getZ()).isCloseTo(0.0, ERROR);\n\n        Vector v7 = Vector.ofPolar(PI_HALF, 0.0, 5.0);\n        assertThat(v7.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v7.getY()).isCloseTo(5.0, ERROR);\n        assertThat(v7.getZ()).isCloseTo(0.0, ERROR);\n\n        Vector v8 = Vector.ofPolar(0.0, PI_HALF, 5.0);\n        assertThat(v8.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v8.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v8.getZ()).isCloseTo(5.0, ERROR);\n\n        Vector v9 = Vector.ofPolar(PI_HALF, PI_HALF, 5.0);\n        assertThat(v9.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v9.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v9.getZ()).isCloseTo(5.0, ERROR);\n\n        Vector v10 = Vector.ofPolar(PI_HALF, -PI_HALF, 5.0);\n        assertThat(v10.getX()).isCloseTo(0.0, ERROR);\n        assertThat(v10.getY()).isCloseTo(0.0, ERROR);\n        assertThat(v10.getZ()).isCloseTo(-5.0, ERROR);\n    }\n\n    @Test\n    public void testToPolar() {\n        Vector v1 = new Vector(20.0, 0.0, 0.0);\n        assertThat(v1.getPhi()).isEqualTo(0.0);\n        assertThat(v1.getTheta()).isEqualTo(0.0);\n        assertThat(v1.getR()).isEqualTo(20.0);\n\n        Vector v2 = new Vector(0.0, 20.0, 0.0);\n        assertThat(v2.getPhi()).isEqualTo(PI_HALF);\n        assertThat(v2.getTheta()).isEqualTo(0.0);\n        assertThat(v2.getR()).isEqualTo(20.0);\n\n        Vector v3 = new Vector(0.0, 0.0, 20.0);\n        assertThat(v3.getPhi()).isEqualTo(0.0);\n        assertThat(v3.getTheta()).isEqualTo(PI_HALF);\n        assertThat(v3.getR()).isEqualTo(20.0);\n\n        Vector v4 = new Vector(-20.0, 0.0, 0.0);\n        assertThat(v4.getPhi()).isEqualTo(PI);\n        assertThat(v4.getTheta()).isEqualTo(0.0);\n        assertThat(v4.getR()).isEqualTo(20.0);\n\n        Vector v5 = new Vector(0.0, -20.0, 0.0);\n        assertThat(v5.getPhi()).isEqualTo(PI + PI_HALF);\n        assertThat(v5.getTheta()).isEqualTo(0.0);\n        assertThat(v5.getR()).isEqualTo(20.0);\n\n        Vector v6 = new Vector(0.0, 0.0, -20.0);\n        assertThat(v6.getPhi()).isEqualTo(0.0);\n        assertThat(v6.getTheta()).isEqualTo(-PI_HALF);\n        assertThat(v6.getR()).isEqualTo(20.0);\n\n        Vector v7 = new Vector(0.0, 0.0, 0.0);\n        assertThat(v7.getPhi()).isEqualTo(0.0);\n        assertThat(v7.getTheta()).isEqualTo(0.0);\n        assertThat(v7.getR()).isEqualTo(0.0);\n    }\n\n}\n"
  },
  {
    "path": "src/test/resources/.gitignore",
    "content": ""
  }
]