Repository: shred/commons-suncalc Branch: master Commit: a8778731b030 Files: 59 Total size: 272.2 KB Directory structure: gitextract_52r2ik9m/ ├── .gitignore ├── .gitlab-ci.yml ├── .project ├── LICENSE-APL.txt ├── README.md ├── pom.xml └── src/ ├── doc/ │ ├── docs/ │ │ ├── examples.md │ │ ├── faq.md │ │ ├── index.md │ │ ├── migration.md │ │ └── usage.md │ ├── mkdocs.yml │ └── theme/ │ ├── breadcrumbs.html │ ├── css/ │ │ ├── font.css │ │ ├── theme_custom.css │ │ └── theme_pygments.css │ ├── footer.html │ └── main.html ├── main/ │ ├── java/ │ │ ├── module-info.java │ │ └── org/ │ │ └── shredzone/ │ │ └── commons/ │ │ └── suncalc/ │ │ ├── MoonIllumination.java │ │ ├── MoonPhase.java │ │ ├── MoonPosition.java │ │ ├── MoonTimes.java │ │ ├── SunPosition.java │ │ ├── SunTimes.java │ │ ├── package-info.java │ │ ├── param/ │ │ │ ├── Builder.java │ │ │ ├── GenericParameter.java │ │ │ ├── LocationParameter.java │ │ │ ├── TimeParameter.java │ │ │ ├── WindowParameter.java │ │ │ └── package-info.java │ │ └── util/ │ │ ├── BaseBuilder.java │ │ ├── ExtendedMath.java │ │ ├── JulianDate.java │ │ ├── Matrix.java │ │ ├── Moon.java │ │ ├── Pegasus.java │ │ ├── QuadraticInterpolation.java │ │ ├── Sun.java │ │ ├── Vector.java │ │ └── package-info.java │ └── resources/ │ └── .gitignore └── test/ ├── java/ │ └── org/ │ └── shredzone/ │ └── commons/ │ └── suncalc/ │ ├── ExamplesTest.java │ ├── Locations.java │ ├── MoonIlluminationTest.java │ ├── MoonPhaseTest.java │ ├── MoonPositionTest.java │ ├── MoonTimesTest.java │ ├── SunPositionTest.java │ ├── SunTimesTest.java │ └── util/ │ ├── BaseBuilderTest.java │ ├── ExtendedMathTest.java │ ├── JulianDateTest.java │ ├── MatrixTest.java │ ├── PegasusTest.java │ ├── QuadraticInterpolationTest.java │ └── VectorTest.java └── resources/ └── .gitignore ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .classpath .settings target /bin/ ================================================ FILE: .gitlab-ci.yml ================================================ build: tags: - maven script: - mvn clean compile deploy: tags: - maven script: - mvn -B install javadoc:javadoc ================================================ FILE: .project ================================================ commons-suncalc org.eclipse.jdt.core.javabuilder org.eclipse.m2e.core.maven2Builder org.eclipse.jdt.core.javanature org.eclipse.m2e.core.maven2Nature ================================================ FILE: LICENSE-APL.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # 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) A Java library for calculation of sun and moon positions and phases. ## Features * Lightweight, only requires Java 8 or higher, no other dependencies * 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. * Available at [Maven Central](http://search.maven.org/#search|ga|1|a%3A%22commons-suncalc%22) * Extensive unit tests ## Accuracy Astronomical 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. This 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. If you are looking for the highest possible accuracy, you are looking for a different library. ## Quick Start This library consists of a few models, all of them are invoked the same way: ```java ZonedDateTime dateTime = // date, time and timezone of calculation double lat, lng = // geolocation SunTimes times = SunTimes.compute() .on(dateTime) // set a date .at(lat, lng) // set a location .execute(); // get the results System.out.println("Sunrise: " + times.getRise()); System.out.println("Sunset: " + times.getSet()); ``` Read the [online documentation](https://shredzone.org/maven/commons-suncalc/) for examples and API details. See the [migration guide](https://shredzone.org/maven/commons-suncalc/migration.html) for how to migrate from a previous version. ## References This library bases on: * "Astronomy on the Personal Computer", 4th edition, by Oliver Montenbruck and Thomas Pfleger * "Astronomical Algorithms" by Jean Meeus ## Contribute * Fork the [Source code at Codeberg](https://codeberg.org/shred/commons-suncalc). Feel free to send pull requests. * Found a bug? Please [file a bug report](https://codeberg.org/shred/commons-suncalc/issues). ## License _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). ================================================ FILE: pom.xml ================================================ 4.0.0 org.shredzone.commons commons-suncalc 3.12-SNAPSHOT Commons: Suncalc Compute sun and moon phases http://commons.shredzone.org 2016 Apache License Version 2.0 LICENSE-APL.txt https://codeberg.org/shred/commons-suncalc/ scm:git:git@codeberg.org:shred/commons-suncalc.git scm:git:git@codeberg.org:shred/commons-suncalc.git HEAD Codeberg Issues https://codeberg.org/shred/commons-suncalc/issues shred Richard Körber UTF-8 UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.11.0 8 default-compile 9 base-compile compile module-info.java org.apache.maven.plugins maven-jar-plugin 3.3.0 **/.gitignore com.github.spotbugs spotbugs-maven-plugin 4.7.3.5 check org.apache.maven.plugins maven-javadoc-plugin 3.2.0 syntax,reference true en org.apache.maven.plugins maven-release-plugin 2.5.3 true v@{project.version} false true org.apache.maven.plugins maven-source-plugin 3.2.1 attach-sources jar org.shredzone.maven mkdocs-maven-plugin 1.1 ${project.build.directory}/site com.github.spotbugs spotbugs-annotations 4.8.5 true com.google.code.findbugs jsr305 junit junit 4.13.2 test org.assertj assertj-core 3.26.0 test java-9 [9,10] org.apache.maven.plugins maven-javadoc-plugin --no-module-directories org.apache.maven.plugins maven-javadoc-plugin --no-module-directories java-11 [11,) org.apache.maven.plugins maven-javadoc-plugin 8 org.apache.maven.plugins maven-javadoc-plugin 8 ================================================ FILE: src/doc/docs/examples.md ================================================ # Examples In 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. I 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. ## Time Zone All calculations use your own system's local time and time zone, unless you give other parameters. This can give surprising and confusing results. Let's assume we're living in Paris, and we want to compute our sunrise and sunset time for May 1st, 2020. ```java SunTimes paris = SunTimes.compute() .on(2020, 5, 1) // May 1st, 2020, starting midnight .latitude(48, 51, 24.0) // Latitude of Paris: 48°51'24" N .longitude(2, 21, 6.0) // Longitude: 2°21'06" E .execute(); System.out.println("Sunrise in Paris: " + paris.getRise()); System.out.println("Sunset in Paris: " + paris.getSet()); ``` The result is not very surprising: ```text Sunrise in Paris: 2020-05-01T06:29:47+02:00[Europe/Paris] Sunset in Paris: 2020-05-01T21:06:45+02:00[Europe/Paris] ``` Now we want to compute the sunrise and sunset times of New York. ```java SunTimes newYork = SunTimes.compute() .on(2020, 5, 1) // May 1st, 2020, starting midnight .at(40.712778, -74.005833) // Coordinates of New York .execute(); System.out.println("Sunrise in New York: " + newYork.getRise()); System.out.println("Sunset in New York: " + newYork.getSet()); ``` The result is: ```text Sunrise in New York: 2020-05-01T11:54:05+02:00[Europe/Paris] Sunset in New York: 2020-05-01T01:51:51+02:00[Europe/Paris] ``` Huh? The sun rises at noon and sets past midnight? The sun also sets before it is rising that day? The 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. We can pass a `timezone()` parameter to tell _suncalc_ that we actually want to use a different timezone. ```java SunTimes newYorkTz = SunTimes.compute() .on(2020, 5, 1) // May 1st, 2020, starting midnight .timezone("America/New_York") // ...New York timezone .at(40.712778, -74.005833) // Coordinates of New York .execute(); System.out.println("Sunrise in New York: " + newYorkTz.getRise()); System.out.println("Sunset in New York: " + newYorkTz.getSet()); ``` Now, we finally see the actual sunrise and sunset time in New York: ```text Sunrise in New York: 2020-05-01T05:54:05-04:00[America/New_York] Sunset in New York: 2020-05-01T19:52:53-04:00[America/New_York] ``` ## Time Window [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: ```java final double[] ALERT_CANADA = new double[] { 82.5, -62.316667 }; final ZoneId ALERT_TZ = ZoneId.of("Canada/Eastern"); SunTimes march = SunTimes.compute() .on(2020, 3, 15) // March 15th, 2020, starting midnight .at(ALERT_CANADA) // Coordinates are stored in an array .timezone(ALERT_TZ) .execute(); System.out.println("Sunrise: " + march.getRise()); System.out.println("Sunset: " + march.getSet()); ``` The result is looking fine so far: ```text Sunrise: 2020-03-15T06:49:03-04:00[Canada/Eastern] Sunset: 2020-03-15T17:52:53-04:00[Canada/Eastern] ``` What about June 15th? ```java SunTimes june = SunTimes.compute() .on(2020, 6, 15) // June 15th, 2020, starting midnight .at(ALERT_CANADA) .timezone(ALERT_TZ) .execute(); System.out.println("Sunrise: " + june.getRise()); System.out.println("Sunset: " + june.getSet()); ``` The result: ```text Sunrise: 2020-09-05T00:24:03-04:00[Canada/Eastern] Sunset: 2020-09-04T23:55:46-04:00[Canada/Eastern] ``` The 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. So 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. ```java SunTimes juneReverse = SunTimes.compute() .on(2020, 6, 15) // June 15th, 2020, starting midnight .at(ALERT_CANADA) .timezone(ALERT_TZ) .reverse() // reverse the search direction! .execute(); System.out.println("Sunrise: " + juneReverse.getRise()); System.out.println("Sunset: " + juneReverse.getSet()); ``` The result: ```text Sunrise: 2020-04-06T00:35:04-04:00[Canada/Eastern] Sunset: 2020-04-05T23:44:05-04:00[Canada/Eastern] ``` Now 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: ```java SunTimes june15OnlyCycle = SunTimes.compute() .on(2020, 6, 15) // June 15th, 2020, starting midnight .at(ALERT_CANADA) .timezone(ALERT_TZ) .limit(Duration.ofHours(24)) .execute(); System.out.println("Sunset: " + june15OnlyCycle.getSet()); System.out.println("Sunrise: " + june15OnlyCycle.getRise()); ``` Instead of `limit(Duration.ofHours(24))`, we could also use `oneDay()`. Now we get a different result. There is no sunrise or sunset on June 15th: ```text Sunrise: null Sunset: null ``` But is the sun up or down all that day? ```java System.out.println("Sun is up all day: " + june15OnlyCycle.isAlwaysUp()); System.out.println("Sun is down all day: " + june15OnlyCycle.isAlwaysDown()); ``` The result confirms that the sun is up all day: ```text Sun is up all day: true Sun is down all day: false ``` ## Parameter Reuse As 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: ```java final double[] COLOGNE = new double[] { 50.938056, 6.956944 }; MoonTimes.Parameters parameters = MoonTimes.compute() .at(COLOGNE) .midnight(); MoonTimes today = parameters.execute(); System.out.println("Today, the moon rises in Cologne at " + today.getRise()); parameters.tomorrow(); MoonTimes tomorrow = parameters.execute(); System.out.println("Tomorrow, the moon will rise in Cologne at " + tomorrow.getRise()); System.out.println("But today, the moon still rises at " + today.getRise()); ``` The result is (at the time of writing): ```text Today, the moon rises in Cologne at 2020-05-24T06:40:45+02:00[Europe/Berlin] Tomorrow, the moon will rise in Cologne at 2020-05-25T07:23:06+02:00[Europe/Berlin] But today, the moon still rises at 2020-05-24T06:40:45+02:00[Europe/Berlin] ``` As you can see in the last line, the invocation of `tomorrow()` did not affect the `today` result. This 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. ```java MoonIllumination.Parameters parameters = MoonIllumination.compute() .on(2020, 1, 1); for (int i = 1; i <= 31; i++) { long percent = Math.round(parameters.execute().getFraction() * 100.0); System.out.println("On January " + i + " the moon was " + percent + "% lit."); parameters.plusDays(1); } ``` The result (excerpt): ```text On January 1 the moon was 29% lit. On January 2 the moon was 38% lit. On January 3 the moon was 48% lit. [...] On January 29 the moon was 15% lit. On January 30 the moon was 22% lit. On January 31 the moon was 30% lit. ``` ## Twilight By 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. There 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°. To learn more about the individual twilight transitions, see the [illustration of twilights](usage.md#twilight). Let's calculate the golden hour in Singapore for the next four Mondays starting June 1st, 2020: ```java SunTimes.Parameters base = SunTimes.compute() .at(1.283333, 103.833333) // Singapore .on(2020, 6, 1) .timezone("Asia/Singapore"); for (int i = 0; i < 4; i++) { SunTimes blue = base .copy() // Use a copy of base .plusDays(i * 7) .twilight(SunTimes.Twilight.BLUE_HOUR) // Blue Hour, -4° .execute(); SunTimes golden = base .copy() // Use a copy of base .plusDays(i * 7) .twilight(SunTimes.Twilight.GOLDEN_HOUR) // Golden Hour, 6° .execute(); System.out.println("Morning golden hour starts at " + blue.getRise()); System.out.println("Morning golden hour ends at " + golden.getRise()); System.out.println("Evening golden hour starts at " + golden.getSet()); System.out.println("Evening golden hour ends at " + blue.getSet()); } ``` Note 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. This is the result: ```text Morning golden hour starts at 2020-06-01T06:43:13+08:00[Asia/Singapore] Morning golden hour ends at 2020-06-01T07:26:24+08:00[Asia/Singapore] Evening golden hour starts at 2020-06-01T18:38:50+08:00[Asia/Singapore] Evening golden hour ends at 2020-06-01T19:22:02+08:00[Asia/Singapore] Morning golden hour starts at 2020-06-08T06:44:16+08:00[Asia/Singapore] Morning golden hour ends at 2020-06-08T07:27:41+08:00[Asia/Singapore] Evening golden hour starts at 2020-06-08T18:40:01+08:00[Asia/Singapore] Evening golden hour ends at 2020-06-08T19:23:27+08:00[Asia/Singapore] Morning golden hour starts at 2020-06-15T06:45:35+08:00[Asia/Singapore] Morning golden hour ends at 2020-06-15T07:29:10+08:00[Asia/Singapore] Evening golden hour starts at 2020-06-15T18:41:25+08:00[Asia/Singapore] Evening golden hour ends at 2020-06-15T19:25:00+08:00[Asia/Singapore] Morning golden hour starts at 2020-06-22T06:47:04+08:00[Asia/Singapore] Morning golden hour ends at 2020-06-22T07:30:41+08:00[Asia/Singapore] Evening golden hour starts at 2020-06-22T18:42:56+08:00[Asia/Singapore] Evening golden hour ends at 2020-06-22T19:26:32+08:00[Asia/Singapore] ``` ## Moon Phase I'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. As the visible moon phase is identical on every place on earth, we won't have to set a location here. But 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. ```java LocalDate date = LocalDate.of(2023, 1, 1); MoonPhase.Parameters parameters = MoonPhase.compute() .phase(MoonPhase.Phase.FULL_MOON); while (true) { MoonPhase moonPhase = parameters .on(date) .execute(); LocalDate nextFullMoon = moonPhase .getTime() .toLocalDate(); if (nextFullMoon.getYear() == 2024) { break; // we've reached the next year } System.out.print(nextFullMoon); if (moonPhase.isMicroMoon()) { System.out.print(" (micromoon)"); } if (moonPhase.isSuperMoon()) { System.out.print(" (supermoon)"); } System.out.println(); date = nextFullMoon.plusDays(1); } ``` The result is: ```text 2023-01-07 (micromoon) 2023-02-05 (micromoon) 2023-03-07 2023-04-06 2023-05-05 2023-06-04 2023-07-03 2023-08-01 (supermoon) 2023-08-31 (supermoon) 2023-09-29 2023-10-28 2023-11-27 2023-12-27 ``` As you can see, there are two full moons in August 2023. ## Sun and Moon Positions I'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? ```java SunPosition.Parameters sunParam = SunPosition.compute() .at(35.689722, 139.692222) // Tokyo .timezone("Asia/Tokyo") // local time .on(2018, 11, 13, 10, 3, 24); // 2018-11-13 10:03:24 MoonPosition.Parameters moonParam = MoonPosition.compute() .sameLocationAs(sunParam) .sameTimeAs(sunParam); SunPosition sun = sunParam.execute(); System.out.println(String.format( "The sun can be seen %.1f° clockwise from the North and " + "%.1f° above the horizon.\nIt is about %.0f km away right now.", sun.getAzimuth(), sun.getAltitude(), sun.getDistance() )); MoonPosition moon = moonParam.execute(); System.out.println(String.format( "The moon can be seen %.1f° clockwise from the North and " + "%.1f° above the horizon.\nIt is about %.0f km away right now.", moon.getAzimuth(), moon.getAltitude(), moon.getDistance() )); ``` Note 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`. The result is: ```text The sun can be seen 156,6° clockwise from the North and 33,0° above the horizon. It is about 148075152 km away right now. The moon can be seen 109,0° clockwise from the North and -9,5° above the horizon. It is about 404629 km away right now. ``` The 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. ================================================ FILE: src/doc/docs/faq.md ================================================ # FAQ ## There is a different result on another website/app. Which one is right? Probably 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. There 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. ## Can you enhance the precision? The 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. ## Can you add other planets or stars? No. It would add much more complexity to this library. It is not meant to be used for astronomical purposes. ## What about sea tide levels? As 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/). ================================================ FILE: src/doc/docs/index.md ================================================ # commons-suncalc A Java library for calculation of sun and moon positions and phases. The 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). ## Accuracy Astronomical 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. This 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. ## Dependencies and Requirements _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. !!! NOTE **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). ## Installation _commons-suncalc_ is available at Maven Central. Just add this snippet to your `pom.xml`: ```xml org.shredzone.commons commons-suncalc $version ``` Or use this snippet in your `build.gradle` (e.g. in Android Studio): ```groovy dependencies { compile('org.shredzone.commons:commons-suncalc:$version') } ``` Replace `$version` with your desired version. The latest version is: ![maven central](https://shredzone.org/maven-central/org.shredzone.commons/commons-suncalc/badge.svg) ## Java Module Add this line to your module descriptor: ``` requires org.shredzone.commons.suncalc; ``` ## References This library bases on: * "Astronomy on the Personal Computer", 4th edition, by Oliver Montenbruck and Thomas Pfleger * "Astronomical Algorithms" by Jean Meeus ## Contribute * Fork the [Source code at Codeberg](https://codeberg.org/shred/commons-suncalc). Feel free to send pull requests. * Found a bug? Please [file a bug report](https://codeberg.org/shred/commons-suncalc/issues). ## License _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). ================================================ FILE: src/doc/docs/migration.md ================================================ # Migration Guide This document will help you migrate your code to the latest _suncalc_ version. ## Version 3.9 * `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. * 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. * 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. ## Version 3.6 * 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. ## Version 3.3 * 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. ## Version 3.1 * 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. * `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. ## Version 3.0 * _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. * 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. * 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. * 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. ================================================ FILE: src/doc/docs/usage.md ================================================ # Usage `commons-suncalc` offers six astronomical calculations: * [SunTimes](./apidocs/org/shredzone/commons/suncalc/SunTimes.html): Sunrise, sunset, noon and nadir times. * [MoonTimes](./apidocs/org/shredzone/commons/suncalc/MoonTimes.html): Moonrise and moonset times. * [MoonPhase](./apidocs/org/shredzone/commons/suncalc/MoonPhase.html): Date and time of new moon, full moon and half moons. * [SunPosition](./apidocs/org/shredzone/commons/suncalc/SunPosition.html): Position of the sun. * [MoonPosition](./apidocs/org/shredzone/commons/suncalc/MoonPosition.html): Position of the moon. * [MoonIllumination](./apidocs/org/shredzone/commons/suncalc/MoonIllumination.html): Moon phase and angle. ## Quick Start All of the calculations mentioned above are invoked in the same way: ```java ZonedDateTime dateTime = // date, time and timezone of calculation double lat, lng = // geolocation SunTimes times = SunTimes.compute() .on(dateTime) // set a date .at(lat, lng) // set a location .execute(); // get the results System.out.println("Sunrise: " + times.getRise()); System.out.println("Sunset: " + times.getSet()); ``` You invoke `compute()`, set your parameters, invoke `execute()`, and then get the result of the calculation as an object. All 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. ```java SunPosition pos = SunPosition.compute().today().at(12.3, 45.6).execute(); ``` It is also possible to collect the parameters first, and execute the computation in a separate step: ```java SunPosition.Parameters param = SunPosition.compute(); param.today(); param.at(12.3, 45.6); SunPosition pos = param.execute(); ``` The 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. ```java param.on(2016, 12, 24); // modify the param from above SunPosition posAtChristmas = param.execute(); // pos from above is unchanged ``` ## Time-based Parameters All calculations need a date and time parameter. Some examples: ```java // Now (the default) SunPosition.compute().now(); // The same: Current time, local time zone ZonedDateTime now = ZonedDateTime.now(); SunPosition.compute().on(now); // August 21st, 2017, local midnight SunPosition.compute().on(2017, 8, 21); // Today (midnight), Berlin time zone SunPosition.compute().today().timezone("Europe/Berlin"); ``` The available time-based parameters are: * `on(int year, int month, int date)`: Midnight of the given date. Note that `month` is counted from 1 (1 = January, 2 = February, …). * `on(int year, int month, int date, int hour, int minute, int second)`: Given date and time. * `on(ZonedDateTime dateTime)`: Given date, time, and timezone. * `on(LocalDateTime dateTime)`: Given local date and time, without a timezone. * `on(LocalDate date)`: Midnight of the given local date, without a timezone. * `on(Instant instant)`: An instant without a timezone. * `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. * `on(Date date)`: Date and time as given in the old-fashioned `Date`. The `Date` is copied and can safely be modified after that. * `plusDays(int days)`: Adds the given number of days to the current date. `days` can also be negative, of course. * `now()`: The current system date and time. This is the default. * `midnight()`: Past midnight of the current date. It just truncates the time. * `today()`: Identical to `.now().midnight()`. * `tomorrow()`: Identical to `today().plusDays(1)`. * `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. * `timezone(String id)`: Same as above, but accepts a `String` for your convenience. * `timezone(TimeZone tz)`: Same as above, but accepts an old-fashioned `TimeZone` object. * `localTime()`: The system's timezone. This is the default. * `utc()`: UTC timezone. Identical to `timezone("UTC")`. * `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. If no time-based parameter is given, the current date and time, and the system's time zone is used. !!! NOTE The accuracy of the results is decreasing for dates that are far in the future, or far in the past. ## Location-based Parameters The 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. ```java // At 20.5°N, 18.3°E SunPosition.compute().at(20.5, 18.3); // The same, but more verbose SunPosition.compute().latitude(20.5).longitude(18.3); // Use arrays for coordinate constants final double[] COLOGNE = new double[] { 50.938056, 6.956944 }; SunPosition.compute().at(COLOGNE); ``` There are two exceptions: * [`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. * [`MoonPhase`](./apidocs/org/shredzone/commons/suncalc/MoonPhase.Parameters.html): The geolocation is not used here. The available location-based parameters are: * `at(double lat, double lng)`: Latitude and longitude to be used for computation. * `at(double[] coords)`: Accepts an array of 2 values (latitude, longitude) or 3 values (latitude, longitude, elevation). * `latitude(double lat)`: Verbose way to set the latitude only. * `longitude(double lng)`: Verbose way to set the longitude only. * `latitude(int d, int m, double s)`: Set the latitude in degrees, minutes, seconds and fraction of seconds. * `longitude(int d, int m, double s)`: Set the longitude in degrees, minutes, seconds and fraction of seconds. * `elevation(double h)`: Elevation above sea level, in meters. Sea level is used by default. * `elevationFt(double h)`: Elevation above sea level, in foot. Sea level is used by default. * `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. !!! NOTE `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. ## Window-based Parameters By 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. The available window-based parameters are: * `limit(Duration duration)`: Limits the time window to the given duration. A reverse time window is implicitly set if this value is negative. * `oneDay()`: Limits the time window to 24 hours. * `fullCycle()`: No limit. Calculation will find all rise, set, noon, and nadir times. * `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()`. * `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. * `sameWindowAs(WindowParameter w)`: Copies the current time window from any other parameter object. Note that subsequent changes to the other object are not adoped. If 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()`. ## Twilight Twilight Zones By 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. There 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: | Constant | Description | Angle of the Sun | Topocentric | | -------------- | ----------- | ----------------:|:-----------:| | `VISUAL` | The moment when the visual upper edge of the sun crosses the horizon. This is the default. | | yes | | `VISUAL_LOWER` | The moment when the visual lower edge of the sun crosses the horizon. | | yes | | `ASTRONOMICAL` | [Astronomical twilight](https://en.wikipedia.org/wiki/Twilight#Astronomical_twilight) | -18° | no | | `NAUTICAL` | [Nautical twilight](https://en.wikipedia.org/wiki/Twilight#Nautical_twilight) | -12° | no | | `CIVIL` | [Civil twilight](https://en.wikipedia.org/wiki/Twilight#Civil_twilight) | -6° | no | | `HORIZON` | The moment when the center of the sun crosses the horizon. | 0° | no | | `GOLDEN_HOUR` | Transition from daylight to [Golden Hour](https://en.wikipedia.org/wiki/Golden_hour_%28photography%29) | 6° | no | | `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 | | `NIGHT_HOUR` | Transition from [Blue Hour](https://en.wikipedia.org/wiki/Blue_hour) to night | -8° | no | The 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. Alternatively you can also pass any other angle (in degrees) to `twilight()`. !!! NOTE 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. All other twilights are geocentric and heliocentric. The `elevation` parameter is then ignored, and atmospheric refraction is not compensated. Example: ```java SunTimes.compute().twilight(SunTimes.Twilight.GOLDEN_HOUR); ``` ## Phase By 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: | Constant | Description | Angle | | ----------------- | ----------- | -----:| | `NEW_MOON` | Moon is not illuminated (new moon). This is the default. | 0° | | `WAXING_CRESCENT` | Waxing crescent moon. | 45° | | `FIRST_QUARTER` | Half of the waxing moon is illuminated. | 90° | | `WAXING_GIBBOUS` | Waxing gibbous moon. | 135° | | `FULL_MOON` | Moon is fully illuminated. | 180° | | `WANING_GIBBOUS` | Waning gibbous moon. | 225° | | `LAST_QUARTER` | Half of the waning moon is illuminated. | 270° | | `WANING_CRESCENT` | Waning crescent moon. | 315° | Alternatively you can also pass any other angle (in degrees) to `phase()`. Example: ```java MoonPhase.compute().phase(MoonPhase.Phase.FULL_MOON); ``` ================================================ FILE: src/doc/mkdocs.yml ================================================ site_name: commons-suncalc site_author: Richard Körber site_url: https://commons.shredzone.org/suncalc site_dir: target/site/ repo_url: https://codeberg.org/shred/commons-suncalc edit_uri: '' use_directory_urls: false theme: name: readthedocs custom_dir: theme/ highlightjs: false markdown_extensions: - admonition - codehilite - toc: permalink: true nav: - 'index.md' - 'usage.md' - 'examples.md' - 'faq.md' - 'JavaDocs': apidocs/index.html - 'migration.md' ================================================ FILE: src/doc/theme/breadcrumbs.html ================================================ ================================================ FILE: src/doc/theme/css/font.css ================================================ /* * Lato * Copyright 2010-2011 tyPoland Lukasz Dziedzic * SIL Open Font License, 1.1 */ @font-face { font-family: 'Lato'; font-style: normal; font-weight: 400; src: local('Lato Regular'), local('Lato-Regular'), url('../fonts/lato-v15-latin-regular.woff') format('woff'); } /* * Roboto Slab * Copyright 2013 Google * Apache License, version 2.0 */ @font-face { font-family: 'Roboto Slab'; font-style: normal; font-weight: 400; src: local('Roboto Slab Regular'), local('RobotoSlab-Regular'), url('../fonts/roboto-slab-v8-latin-regular.woff') format('woff'); } /* * Inconsolata * Copyright 2006 The Inconsolata Project Authors * SIL Open Font License, 1.1 */ @font-face { font-family: 'Inconsolata'; font-style: normal; font-weight: 400; src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('../fonts/inconsolata-v17-latin-regular.woff') format('woff'); } ================================================ FILE: src/doc/theme/css/theme_custom.css ================================================ .wy-nav-content { max-width: none; } .codehilite pre code { font-size: 15px; } table { margin-bottom: 2rem; } footer { margin-top: 2rem; } ================================================ FILE: src/doc/theme/css/theme_pygments.css ================================================ /* github pygments theme by @jwarby, https://github.com/jwarby/jekyll-pygments-themes */ .codehilite .hll { background-color: #ffffcc } .codehilite .c { color: #999988; font-style: italic } .codehilite .err { color: #a61717; background-color: #e3d2d2 } .codehilite .k { color: #000000; font-weight: bold } .codehilite .o { color: #000000; font-weight: bold } .codehilite .cm { color: #999988; font-style: italic } .codehilite .cp { color: #999999; font-weight: bold; font-style: italic } .codehilite .c1 { color: #999988; font-style: italic } .codehilite .cs { color: #999999; font-weight: bold; font-style: italic } .codehilite .gd { color: #000000; background-color: #ffdddd } .codehilite .ge { color: #000000; font-style: italic } .codehilite .gr { color: #aa0000 } .codehilite .gh { color: #999999 } .codehilite .gi { color: #000000; background-color: #ddffdd } .codehilite .go { color: #888888 } .codehilite .gp { color: #555555 } .codehilite .gs { font-weight: bold } .codehilite .gu { color: #aaaaaa } .codehilite .gt { color: #aa0000 } .codehilite .kc { color: #000000; font-weight: bold } .codehilite .kd { color: #000000; font-weight: bold } .codehilite .kn { color: #000000; font-weight: bold } .codehilite .kp { color: #000000; font-weight: bold } .codehilite .kr { color: #000000; font-weight: bold } .codehilite .kt { color: #445588; font-weight: bold } .codehilite .m { color: #009999 } .codehilite .s { color: #d01040 } .codehilite .na { color: #008080 } .codehilite .nb { color: #0086B3 } .codehilite .nc { color: #445588; font-weight: bold } .codehilite .no { color: #008080 } .codehilite .nd { color: #3c5d5d; font-weight: bold } .codehilite .ni { color: #800080 } .codehilite .ne { color: #990000; font-weight: bold } .codehilite .nf { color: #990000; font-weight: bold } .codehilite .nl { color: #990000; font-weight: bold } .codehilite .nn { color: #555555 } .codehilite .nt { color: #000080 } .codehilite .nv { color: #008080 } .codehilite .ow { color: #000000; font-weight: bold } .codehilite .w { color: #bbbbbb } .codehilite .mf { color: #009999 } .codehilite .mh { color: #009999 } .codehilite .mi { color: #009999 } .codehilite .mo { color: #009999 } .codehilite .sb { color: #d01040 } .codehilite .sc { color: #d01040 } .codehilite .sd { color: #d01040 } .codehilite .s2 { color: #d01040 } .codehilite .se { color: #d01040 } .codehilite .sh { color: #d01040 } .codehilite .si { color: #d01040 } .codehilite .sx { color: #d01040 } .codehilite .sr { color: #009926 } .codehilite .s1 { color: #d01040 } .codehilite .ss { color: #990073 } .codehilite .bp { color: #999999 } .codehilite .vc { color: #008080 } .codehilite .vg { color: #008080 } .codehilite .vi { color: #008080 } .codehilite .il { color: #009999 } ================================================ FILE: src/doc/theme/footer.html ================================================ ================================================ FILE: src/doc/theme/main.html ================================================ {% extends "base.html" %} {% block styles %} {%- for path in config['extra_css'] %} {%- endfor %} {% endblock %} {% block repo %} API javadocs {% endblock %} ================================================ FILE: src/main/java/module-info.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2020 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ /** * Module definition for commons-suncalc. */ module org.shredzone.commons.suncalc { requires static com.github.spotbugs.annotations; exports org.shredzone.commons.suncalc; exports org.shredzone.commons.suncalc.param; } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/MoonIllumination.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.*; import org.shredzone.commons.suncalc.param.Builder; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.LocationParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.util.BaseBuilder; import org.shredzone.commons.suncalc.util.JulianDate; import org.shredzone.commons.suncalc.util.Moon; import org.shredzone.commons.suncalc.util.Sun; import org.shredzone.commons.suncalc.util.Vector; /** * Calculates the illumination of the moon. *

* Starting with v3.9, a geolocation can be set optionally. If set, the results will be * topocentric, relative to the given location. If not set, the result is geocentric, * which was the standard behavior before v3.9. */ public class MoonIllumination { private final double fraction; private final double phase; private final double angle; private final double elongation; private final double radius; private final double crescentWidth; private MoonIllumination(double fraction, double phase, double angle, double elongation, double radius, double crescentWidth) { this.fraction = fraction; this.phase = phase; this.angle = angle; this.elongation = elongation; this.radius = radius; this.crescentWidth = crescentWidth; } /** * Starts the computation of {@link MoonIllumination}. * * @return {@link Parameters} to set. */ public static Parameters compute() { return new MoonIlluminationBuilder(); } /** * Collects all parameters for {@link MoonIllumination}. */ public interface Parameters extends GenericParameter, TimeParameter, LocationParameter, Builder { /** * Clears the geolocation, so the result will be geocentric. * * @return itself * @since 3.9 */ Parameters geocentric(); } /** * Builder for {@link MoonIllumination}. Performs the computations based on the * parameters, and creates a {@link MoonIllumination} object that holds the result. */ private static class MoonIlluminationBuilder extends BaseBuilder implements Parameters { @Override public Parameters geocentric() { clearLocation(); return this; } @Override public MoonIllumination execute() { JulianDate t = getJulianDate(); Vector s = Sun.position(t); Vector m = Moon.position(t); double phi = PI - acos(m.dot(s) / (m.getR() * s.getR())); Vector sunMoon = m.cross(s); double angle = atan2( cos(s.getTheta()) * sin(s.getPhi() - m.getPhi()), sin(s.getTheta()) * cos(m.getTheta()) - cos(s.getTheta()) * sin(m.getTheta()) * cos(s.getPhi() - m.getPhi()) ); Vector sTopo, mTopo; if (hasLocation()) { sTopo = Sun.positionTopocentric(t, getLatitudeRad(), getLongitudeRad(), getElevation()); mTopo = Moon.positionTopocentric(t, getLatitudeRad(), getLongitudeRad(), getElevation()); } else { sTopo = s; mTopo = m; } double r = mTopo.subtract(sTopo).norm(); double re = sTopo.norm(); double d = mTopo.norm(); double elongation = acos((d*d + re*re - r*r) / (2.0*d*re)); double moonRadius = Moon.angularRadius(mTopo.getR()); double crescentWidth = moonRadius * (1 - cos(elongation)); return new MoonIllumination( (1 + cos(phi)) / 2, toDegrees(phi * signum(sunMoon.getTheta())), toDegrees(angle), toDegrees(elongation), toDegrees(moonRadius), toDegrees(crescentWidth)); } } /** * Illuminated fraction. {@code 0.0} indicates new moon, {@code 1.0} indicates full * moon. */ public double getFraction() { return fraction; } /** * Moon phase. Starts at {@code -180.0} (new moon, waxing), passes {@code 0.0} (full * moon) and moves toward {@code 180.0} (waning, new moon). *

* Note that for historical reasons, the range of this phase is different to the * moon phase angle used in {@link MoonPhase}. */ public double getPhase() { return phase; } /** * The angle of the moon illumination relative to earth. The moon is waxing if the * angle is negative, and waning if positive. *

* By subtracting {@link MoonPosition#getParallacticAngle()} from {@link #getAngle()}, * one can get the zenith angle of the moons bright limb (anticlockwise). The zenith * angle can be used do draw the moon shape from the observer's perspective (e.g. the * moon lying on its back). */ public double getAngle() { return angle; } /** * The closest {@link MoonPhase.Phase} that is matching the moon's angle. * * @return Closest {@link MoonPhase.Phase} * @since 3.5 */ public MoonPhase.Phase getClosestPhase() { return MoonPhase.Phase.toPhase(phase + 180.0); } /** * The elongation, which is the angular distance between the moon and the sun as * observed from a specific location on earth. * * @return Elongation between moon and sun, in degrees. * @since 3.9 */ public double getElongation() { return elongation; } /** * The radius of the moon disk, as observed from a specific location on earth. * * @return Moon radius, in degrees. * @since 3.9 */ public double getRadius() { return radius; } /** * The width of the moon crescent, as observed from a specific location on earth. * * @return Crescent width, in degrees. * @since 3.9 */ public double getCrescentWidth() { return crescentWidth; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MoonIllumination[fraction=").append(fraction); sb.append(", phase=").append(phase); sb.append("°, angle=").append(angle); sb.append("°, elongation=").append(elongation); sb.append("°, radius=").append(radius); sb.append("°, crescentWidth=").append(crescentWidth); sb.append("°]"); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/MoonPhase.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2018 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.PI; import static java.lang.Math.toRadians; import static org.shredzone.commons.suncalc.util.ExtendedMath.PI2; import java.time.ZonedDateTime; import org.shredzone.commons.suncalc.param.Builder; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.util.BaseBuilder; import org.shredzone.commons.suncalc.util.JulianDate; import org.shredzone.commons.suncalc.util.Moon; import org.shredzone.commons.suncalc.util.Pegasus; import org.shredzone.commons.suncalc.util.Sun; import org.shredzone.commons.suncalc.util.Vector; /** * Calculates the date and time when the moon reaches the desired phase. *

* Note: Due to the simplified formulas used in suncalc, the returned time can have an * error of several minutes. */ public class MoonPhase { private final ZonedDateTime time; private final double distance; private MoonPhase(ZonedDateTime time, double distance) { this.time = time; this.distance = distance; } /** * Starts the computation of {@link MoonPhase}. * * @return {@link Parameters} to set. */ public static Parameters compute() { return new MoonPhaseBuilder(); } /** * Collects all parameters for {@link MoonPhase}. */ public interface Parameters extends GenericParameter, TimeParameter, Builder { /** * Sets the desired {@link Phase}. *

* Defaults to {@link Phase#NEW_MOON}. * * @param phase * {@link Phase} to be used. * @return itself */ Parameters phase(Phase phase); /** * Sets a free phase to be used. * * @param phase * Desired phase, in degrees. 0 = new moon, 90 = first quarter, 180 = * full moon, 270 = third quarter. * @return itself */ Parameters phase(double phase); } /** * Enumeration of moon phases. */ public enum Phase { /** * New moon. */ NEW_MOON(0.0), /** * Waxing crescent moon. * * @since 3.5 */ WAXING_CRESCENT(45.0), /** * Waxing half moon. */ FIRST_QUARTER(90.0), /** * Waxing gibbous moon. * * @since 3.5 */ WAXING_GIBBOUS(135.0), /** * Full moon. */ FULL_MOON(180.0), /** * Waning gibbous moon. * * @since 3.5 */ WANING_GIBBOUS(225.0), /** * Waning half moon. */ LAST_QUARTER(270.0), /** * Waning crescent moon. * * @since 3.5 */ WANING_CRESCENT(315.0); /** * Converts an angle to the closest matching moon phase. * * @param angle * Moon phase angle, in degrees. 0 = New Moon, 180 = Full Moon. Angles * outside the [0,360) range are normalized into that range. * @return Closest Phase that is matching that angle. * @since 3.5 */ public static Phase toPhase(double angle) { // bring into range 0.0 .. 360.0 double normalized = angle % 360.0; if (normalized < 0.0) { normalized += 360.0; } if (normalized < 22.5) { return NEW_MOON; } if (normalized < 67.5) { return WAXING_CRESCENT; } if (normalized < 112.5) { return FIRST_QUARTER; } if (normalized < 157.5) { return WAXING_GIBBOUS; } if (normalized < 202.5) { return FULL_MOON; } if (normalized < 247.5) { return WANING_GIBBOUS; } if (normalized < 292.5) { return LAST_QUARTER; } if (normalized < 337.5) { return WANING_CRESCENT; } return NEW_MOON; } private final double angle; private final double angleRad; Phase(double angle) { this.angle = angle; this.angleRad = toRadians(angle); } /** * Returns the moons's angle in reference to the sun, in degrees. */ public double getAngle() { return angle; } /** * Returns the moons's angle in reference to the sun, in radians. */ public double getAngleRad() { return angleRad; } } /** * Builder for {@link MoonPhase}. Performs the computations based on the parameters, * and creates a {@link MoonPhase} object that holds the result. */ private static class MoonPhaseBuilder extends BaseBuilder implements Parameters { private static final double SUN_LIGHT_TIME_TAU = 8.32 / (1440.0 * 36525.0); private double phase = Phase.NEW_MOON.getAngleRad(); @Override public Parameters phase(Phase phase) { this.phase = phase.getAngleRad(); return this; } @Override public Parameters phase(double phase) { this.phase = toRadians(phase); return this; } @Override public MoonPhase execute() { final JulianDate jd = getJulianDate(); double dT = 7.0 / 36525.0; // step rate: 1 week double accuracy = (0.5 / 1440.0) / 36525.0; // accuracy: 30 seconds double t0 = jd.getJulianCentury(); double t1 = t0 + dT; double d0 = moonphase(jd, t0); double d1 = moonphase(jd, t1); while (d0 * d1 > 0.0 || d1 < d0) { t0 = t1; d0 = d1; t1 += dT; d1 = moonphase(jd, t1); } double tphase = Pegasus.calculate(t0, t1, accuracy, x -> moonphase(jd, x)); JulianDate tjd = jd.atJulianCentury(tphase); return new MoonPhase(tjd.getDateTime(), Moon.positionEquatorial(tjd).getR()); } /** * Calculates the position of the moon at the given phase. * * @param jd * Base Julian date * @param t * Ephemeris time * @return difference angle of the sun's and moon's position */ private double moonphase(JulianDate jd, double t) { Vector sun = Sun.positionEquatorial(jd.atJulianCentury(t - SUN_LIGHT_TIME_TAU)); Vector moon = Moon.positionEquatorial(jd.atJulianCentury(t)); double diff = moon.getPhi() - sun.getPhi() - phase; //NOSONAR: false positive while (diff < 0.0) { diff += PI2; } return ((diff + PI) % PI2) - PI; } } /** * Date and time of the desired moon phase. The time is rounded to full minutes. */ public ZonedDateTime getTime() { return time; } /** * Geocentric distance of the moon at the given phase, in kilometers. * * @since 3.4 */ public double getDistance() { return distance; } /** * Checks if the moon is in a SuperMoon position. *

* Note that there is no official definition of supermoon. Suncalc will assume a * supermoon if the center of the moon is closer than 360,000 km to the center of * Earth. Usually only full moons or new moons are candidates for supermoons. * * @since 3.4 */ public boolean isSuperMoon() { return distance < 360000.0; } /** * Checks if the moon is in a MicroMoon position. *

* Note that there is no official definition of micromoon. Suncalc will assume a * micromoon if the center of the moon is farther than 405,000 km from the center of * Earth. Usually only full moons or new moons are candidates for micromoons. * * @since 3.4 */ public boolean isMicroMoon() { return distance > 405000.0; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MoonPhase[time=").append(time); sb.append(", distance=").append(distance); sb.append(" km]"); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/MoonPosition.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.*; import static org.shredzone.commons.suncalc.util.ExtendedMath.equatorialToHorizontal; import static org.shredzone.commons.suncalc.util.ExtendedMath.refraction; import org.shredzone.commons.suncalc.param.Builder; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.LocationParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.util.BaseBuilder; import org.shredzone.commons.suncalc.util.JulianDate; import org.shredzone.commons.suncalc.util.Moon; import org.shredzone.commons.suncalc.util.Vector; /** * Calculates the position of the moon. */ public class MoonPosition { private final double azimuth; private final double altitude; private final double trueAltitude; private final double distance; private final double parallacticAngle; private MoonPosition(double azimuth, double altitude, double trueAltitude, double distance, double parallacticAngle) { this.azimuth = (toDegrees(azimuth) + 180.0) % 360.0; this.altitude = toDegrees(altitude); this.trueAltitude = toDegrees(trueAltitude); this.distance = distance; this.parallacticAngle = toDegrees(parallacticAngle); } /** * Starts the computation of {@link MoonPosition}. * * @return {@link Parameters} to set. */ public static Parameters compute() { return new MoonPositionBuilder(); } /** * Collects all parameters for {@link MoonPosition}. */ public interface Parameters extends GenericParameter, LocationParameter, TimeParameter, Builder { } /** * Builder for {@link MoonPosition}. Performs the computations based on the * parameters, and creates a {@link MoonPosition} object that holds the result. */ private static class MoonPositionBuilder extends BaseBuilder implements Parameters { @Override public MoonPosition execute() { if (!hasLocation()) { throw new IllegalArgumentException("Geolocation is missing."); } JulianDate t = getJulianDate(); double phi = getLatitudeRad(); double lambda = getLongitudeRad(); Vector mc = Moon.position(t); double h = t.getGreenwichMeanSiderealTime() + lambda - mc.getPhi(); Vector horizontal = equatorialToHorizontal(h, mc.getTheta(), mc.getR(), phi); double hRef = refraction(horizontal.getTheta()); double pa = atan2(sin(h), tan(phi) * cos(mc.getTheta()) - sin(mc.getTheta()) * cos(h)); return new MoonPosition( horizontal.getPhi(), horizontal.getTheta() + hRef, horizontal.getTheta(), mc.getR(), pa); } } /** * Moon altitude above the horizon, in degrees. *

* {@code 0.0} means the moon's center is at the horizon, {@code 90.0} at the zenith * (straight over your head). Atmospheric refraction is taken into account. * * @see #getTrueAltitude() */ public double getAltitude() { return altitude; } /** * The true moon altitude above the horizon, in degrees. *

* {@code 0.0} means the moon's center is at the horizon, {@code 90.0} at the zenith * (straight over your head). * * @see #getAltitude() * @since 3.8 */ public double getTrueAltitude() { return trueAltitude; } /** * Moon azimuth, in degrees, north-based. *

* This is the direction along the horizon, measured from north to east. For example, * {@code 0.0} means north, {@code 135.0} means southeast, {@code 270.0} means west. */ public double getAzimuth() { return azimuth; } /** * Distance to the moon in kilometers. */ public double getDistance() { return distance; } /** * Parallactic angle of the moon, in degrees. */ public double getParallacticAngle() { return parallacticAngle; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MoonPosition[azimuth=").append(azimuth); sb.append("°, altitude=").append(altitude); sb.append("°, true altitude=").append(trueAltitude); sb.append("°, distance=").append(distance); sb.append(" km, parallacticAngle=").append(parallacticAngle); sb.append("°]"); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/MoonTimes.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.ceil; import static java.lang.Math.floor; import static org.shredzone.commons.suncalc.util.ExtendedMath.apparentRefraction; import static org.shredzone.commons.suncalc.util.ExtendedMath.parallax; import java.time.Duration; import java.time.ZonedDateTime; import edu.umd.cs.findbugs.annotations.Nullable; import org.shredzone.commons.suncalc.param.Builder; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.LocationParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.param.WindowParameter; import org.shredzone.commons.suncalc.util.BaseBuilder; import org.shredzone.commons.suncalc.util.JulianDate; import org.shredzone.commons.suncalc.util.Moon; import org.shredzone.commons.suncalc.util.QuadraticInterpolation; import org.shredzone.commons.suncalc.util.Vector; /** * Calculates the times of the moon. */ public final class MoonTimes { private final @Nullable ZonedDateTime rise; private final @Nullable ZonedDateTime set; private final boolean alwaysUp; private final boolean alwaysDown; private MoonTimes(@Nullable ZonedDateTime rise, @Nullable ZonedDateTime set, boolean alwaysUp, boolean alwaysDown) { this.rise = rise; this.set = set; this.alwaysUp = alwaysUp; this.alwaysDown = alwaysDown; } /** * Starts the computation of {@link MoonTimes}. * * @return {@link Parameters} to set. */ public static Parameters compute() { return new MoonTimesBuilder(); } /** * Collects all parameters for {@link MoonTimes}. */ public static interface Parameters extends GenericParameter, LocationParameter, TimeParameter, WindowParameter, Builder { } /** * Builder for {@link MoonTimes}. Performs the computations based on the parameters, * and creates a {@link MoonTimes} object that holds the result. */ private static class MoonTimesBuilder extends BaseBuilder implements Parameters { private double refraction = apparentRefraction(0.0); @Override public MoonTimes execute() { if (!hasLocation()) { throw new IllegalArgumentException("Geolocation is missing."); } JulianDate jd = getJulianDate(); Double rise = null; Double set = null; boolean alwaysUp = false; boolean alwaysDown = false; double ye; int hourStep; double lowerLimitHours, upperLimitHours; if (getDuration().isNegative()) { hourStep = -1; lowerLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0); upperLimitHours = 0.0; } else { hourStep = 1; lowerLimitHours = 0.0; upperLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);; } int hour = 0; int minHours = (int) floor(lowerLimitHours); int maxHours = (int) ceil(upperLimitHours); double y_minus = correctedMoonHeight(jd.atHour(hour - 1.0)); double y_0 = correctedMoonHeight(jd.atHour(hour)); double y_plus = correctedMoonHeight(jd.atHour(hour + 1.0)); if (y_0 > 0.0) { alwaysUp = true; } else { alwaysDown = true; } while (hour <= maxHours && hour >= minHours) { QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus); ye = qi.getYe(); if (qi.getNumberOfRoots() == 1) { double rt = qi.getRoot1() + hour; if (y_minus < 0.0) { if (rise == null && rt >= lowerLimitHours && rt < upperLimitHours) { rise = rt; alwaysDown = false; } } else { if (set == null && rt >= lowerLimitHours && rt < upperLimitHours) { set = rt; alwaysUp = false; } } } else if (qi.getNumberOfRoots() == 2) { if (rise == null) { double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1()); if (rt >= lowerLimitHours && rt < upperLimitHours) { rise = rt; alwaysDown = false; } } if (set == null) { double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2()); if (rt >= lowerLimitHours && rt < upperLimitHours) { set = rt; alwaysUp = false; } } } if (rise != null && set != null) { break; } hour += hourStep; if (hourStep > 0) { y_minus = y_0; y_0 = y_plus; y_plus = correctedMoonHeight(jd.atHour(hour + 1.0)); } else { y_plus = y_0; y_0 = y_minus; y_minus = correctedMoonHeight(jd.atHour(hour - 1.0)); } } return new MoonTimes( rise != null ? jd.atHour(rise).getDateTime() : null, set != null ? jd.atHour(set).getDateTime() : null, alwaysUp, alwaysDown); } /** * Computes the moon height at the given date and position. * * @param jd {@link JulianDate} to use * @return height, in radians */ private double correctedMoonHeight(JulianDate jd) { Vector pos = Moon.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad()); double hc = parallax(getElevation(), pos.getR()) - refraction - Moon.angularRadius(pos.getR()); return pos.getTheta() - hc; } } /** * Moonrise time. {@code null} if the moon does not rise that day. */ @Nullable public ZonedDateTime getRise() { return rise; } /** * Moonset time. {@code null} if the moon does not set that day. */ @Nullable public ZonedDateTime getSet() { return set; } /** * {@code true} if the moon never rises/sets, but is always above the horizon. */ public boolean isAlwaysUp() { return alwaysUp; } /** * {@code true} if the moon never rises/sets, but is always below the horizon. */ public boolean isAlwaysDown() { return alwaysDown; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MoonTimes[rise=").append(rise); sb.append(", set=").append(set); sb.append(", alwaysUp=").append(alwaysUp); sb.append(", alwaysDown=").append(alwaysDown); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/SunPosition.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.toDegrees; import static org.shredzone.commons.suncalc.util.ExtendedMath.refraction; import org.shredzone.commons.suncalc.param.Builder; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.LocationParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.util.BaseBuilder; import org.shredzone.commons.suncalc.util.JulianDate; import org.shredzone.commons.suncalc.util.Sun; import org.shredzone.commons.suncalc.util.Vector; /** * Calculates the position of the sun. */ public class SunPosition { private final double azimuth; private final double altitude; private final double trueAltitude; private final double distance; private SunPosition(double azimuth, double altitude, double trueAltitude, double distance) { this.azimuth = (toDegrees(azimuth) + 180.0) % 360.0; this.altitude = toDegrees(altitude); this.trueAltitude = toDegrees(trueAltitude); this.distance = distance; } /** * Starts the computation of {@link SunPosition}. * * @return {@link Parameters} to set. */ public static Parameters compute() { return new SunPositionBuilder(); } /** * Collects all parameters for {@link SunPosition}. */ public interface Parameters extends GenericParameter, LocationParameter, TimeParameter, Builder { } /** * Builder for {@link SunPosition}. Performs the computations based on the parameters, * and creates a {@link SunPosition} object that holds the result. */ private static class SunPositionBuilder extends BaseBuilder implements Parameters { @Override public SunPosition execute() { if (!hasLocation()) { throw new IllegalArgumentException("Geolocation is missing."); } JulianDate t = getJulianDate(); Vector horizontal = Sun.positionHorizontal(t, getLatitudeRad(), getLongitudeRad()); double hRef = refraction(horizontal.getTheta()); return new SunPosition(horizontal.getPhi(), horizontal.getTheta() + hRef, horizontal.getTheta(), horizontal.getR()); } } /** * The visible sun altitude above the horizon, in degrees. *

* {@code 0.0} means the sun's center is at the horizon, {@code 90.0} at the zenith * (straight over your head). Atmospheric refraction is taken into account. * * @see #getTrueAltitude() */ public double getAltitude() { return altitude; } /** * The true sun altitude above the horizon, in degrees. *

* {@code 0.0} means the sun's center is at the horizon, {@code 90.0} at the zenith * (straight over your head). * * @see #getAltitude() */ public double getTrueAltitude() { return trueAltitude; } /** * Sun azimuth, in degrees, north-based. *

* This is the direction along the horizon, measured from north to east. For example, * {@code 0.0} means north, {@code 135.0} means southeast, {@code 270.0} means west. */ public double getAzimuth() { return azimuth; } /** * Sun's distance, in kilometers. */ public double getDistance() { return distance; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("SunPosition[azimuth=").append(azimuth); sb.append("°, altitude=").append(altitude); sb.append("°, true altitude=").append(trueAltitude); sb.append("°, distance=").append(distance).append(" km]"); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/SunTimes.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.*; import static org.shredzone.commons.suncalc.util.ExtendedMath.*; import java.time.Duration; import java.time.ZonedDateTime; import edu.umd.cs.findbugs.annotations.Nullable; import org.shredzone.commons.suncalc.param.Builder; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.LocationParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.param.WindowParameter; import org.shredzone.commons.suncalc.util.BaseBuilder; import org.shredzone.commons.suncalc.util.JulianDate; import org.shredzone.commons.suncalc.util.QuadraticInterpolation; import org.shredzone.commons.suncalc.util.Sun; import org.shredzone.commons.suncalc.util.Vector; /** * Calculates the rise and set times of the sun. */ public class SunTimes { private final @Nullable ZonedDateTime rise; private final @Nullable ZonedDateTime set; private final @Nullable ZonedDateTime noon; private final @Nullable ZonedDateTime nadir; private final boolean alwaysUp; private final boolean alwaysDown; private SunTimes(@Nullable ZonedDateTime rise, @Nullable ZonedDateTime set, @Nullable ZonedDateTime noon, @Nullable ZonedDateTime nadir, boolean alwaysUp, boolean alwaysDown) { this.rise = rise; this.set = set; this.noon = noon; this.nadir = nadir; this.alwaysUp = alwaysUp; this.alwaysDown = alwaysDown; } /** * Starts the computation of {@link SunTimes}. * * @return {@link Parameters} to set. */ public static Parameters compute() { return new SunTimesBuilder(); } /** * Collects all parameters for {@link SunTimes}. */ public interface Parameters extends GenericParameter, LocationParameter, TimeParameter, WindowParameter, Builder { /** * Sets the {@link Twilight} mode. *

* Defaults to {@link Twilight#VISUAL}. * * @param twilight * {@link Twilight} mode to be used. * @return itself */ Parameters twilight(Twilight twilight); /** * Sets the desired elevation angle of the sun. The sunrise and sunset times are * referring to the moment when the center of the sun passes this angle. * * @param angle * Geocentric elevation angle, in degrees. * @return itself */ Parameters twilight(double angle); } /** * Enumeration of predefined twilights. *

* The twilight angles use a geocentric reference, by definition. However, * {@link #VISUAL} and {@link #VISUAL_LOWER} are topocentric, and take the spectator's * elevation and the atmospheric refraction into account. * * @see Wikipedia: Twilight */ public enum Twilight { /** * The moment when the visual upper edge of the sun crosses the horizon. This is * commonly referred to as "sunrise" and "sunset". Atmospheric refraction is taken * into account. *

* This is the default. */ VISUAL(0.0, 1.0), /** * The moment when the visual lower edge of the sun crosses the horizon. This is * the ending of the sunrise and the starting of the sunset. Atmospheric * refraction is taken into account. */ VISUAL_LOWER(0.0, -1.0), /** * The moment when the center of the sun crosses the horizon (0°). */ HORIZON(0.0), /** * Civil twilight (-6°). */ CIVIL(-6.0), /** * Nautical twilight (-12°). */ NAUTICAL(-12.0), /** * Astronomical twilight (-18°). */ ASTRONOMICAL(-18.0), /** * Golden hour (6°). The Golden hour is between {@link #GOLDEN_HOUR} and * {@link #BLUE_HOUR}. The Magic hour is between {@link #GOLDEN_HOUR} and * {@link #CIVIL}. * * @see Wikipedia: * Golden hour */ GOLDEN_HOUR(6.0), /** * Blue hour (-4°). The Blue hour is between {@link #NIGHT_HOUR} and * {@link #BLUE_HOUR}. * * @see Wikipedia: Blue hour */ BLUE_HOUR(-4.0), /** * End of Blue hour (-8°). *

* "Night Hour" is not an official term, but just a name that is marking the * beginning/end of the Blue hour. */ NIGHT_HOUR(-8.0); private final double angle; private final double angleRad; private final @Nullable Double position; Twilight(double angle) { this(angle, null); } Twilight(double angle, @Nullable Double position) { this.angle = angle; this.angleRad = toRadians(angle); this.position = position; } /** * Returns the sun's angle at the twilight position, in degrees. */ public double getAngle() { return angle; } /** * Returns the sun's angle at the twilight position, in radians. */ public double getAngleRad() { return angleRad; } /** * Returns {@code true} if this twilight position is topocentric. Then the * parallax and the atmospheric refraction is taken into account. */ public boolean isTopocentric() { return position != null; } /** * Returns the angular position. {@code 0.0} means center of the sun. {@code 1.0} * means upper edge of the sun. {@code -1.0} means lower edge of the sun. * {@code null} means the angular position is not topocentric. */ @Nullable private Double getAngularPosition() { return position; } } /** * Builder for {@link SunTimes}. Performs the computations based on the parameters, * and creates a {@link SunTimes} object that holds the result. */ private static class SunTimesBuilder extends BaseBuilder implements Parameters { private double angle = Twilight.VISUAL.getAngleRad(); private @Nullable Double position = Twilight.VISUAL.getAngularPosition(); @Override public Parameters twilight(Twilight twilight) { this.angle = twilight.getAngleRad(); this.position = twilight.getAngularPosition(); return this; } @Override public Parameters twilight(double angle) { this.angle = toRadians(angle); this.position = null; return this; } @Override public SunTimes execute() { if (!hasLocation()) { throw new IllegalArgumentException("Geolocation is missing."); } JulianDate jd = getJulianDate(); Double rise = null; Double set = null; Double noon = null; Double nadir = null; boolean alwaysUp = false; boolean alwaysDown = false; double ye; int hourStep; double lowerLimitHours, upperLimitHours; if (getDuration().isNegative()) { hourStep = -1; lowerLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0); upperLimitHours = 0.0; } else { hourStep = 1; lowerLimitHours = 0.0; upperLimitHours = getDuration().toMillis() / (60 * 60 * 1000.0);; } int hour = 0; int minHours = (int) floor(lowerLimitHours); int maxHours = (int) ceil(upperLimitHours); double y_minus = correctedSunHeight(jd.atHour(hour - 1.0)); double y_0 = correctedSunHeight(jd.atHour(hour)); double y_plus = correctedSunHeight(jd.atHour(hour + 1.0)); if (y_0 > 0.0) { alwaysUp = true; } else { alwaysDown = true; } while (hour <= maxHours && hour >= minHours) { QuadraticInterpolation qi = new QuadraticInterpolation(y_minus, y_0, y_plus); ye = qi.getYe(); if (qi.getNumberOfRoots() == 1) { double rt = qi.getRoot1() + hour; if (y_minus < 0.0) { if (rise == null && rt >= lowerLimitHours && rt < upperLimitHours) { rise = rt; alwaysDown = false; } } else { if (set == null && rt >= lowerLimitHours && rt < upperLimitHours) { set = rt; alwaysUp = false; } } } else if (qi.getNumberOfRoots() == 2) { if (rise == null) { double rt = hour + (ye < 0.0 ? qi.getRoot2() : qi.getRoot1()); if (rt >= lowerLimitHours && rt < upperLimitHours) { rise = rt; alwaysDown = false; } } if (set == null) { double rt = hour + (ye < 0.0 ? qi.getRoot1() : qi.getRoot2()); if (rt >= lowerLimitHours && rt < upperLimitHours) { set = rt; alwaysUp = false; } } } double xeAbs = abs(qi.getXe()); if (xeAbs <= 1.0) { double xeHour = qi.getXe() + hour; if (hourStep > 0 ? xeHour >= 0.0 : xeHour <= 0.0) { if (qi.isMaximum()) { if (noon == null) { noon = xeHour; } } else { if (nadir == null) { nadir = xeHour; } } } } if (rise != null && set != null && noon != null && nadir != null) { break; } hour += hourStep; if (hourStep > 0) { y_minus = y_0; y_0 = y_plus; y_plus = correctedSunHeight(jd.atHour(hour + 1.0)); } else { y_plus = y_0; y_0 = y_minus; y_minus = correctedSunHeight(jd.atHour(hour - 1.0)); } } if (noon != null) { noon = readjustMax(noon, 2.0, 14, t -> correctedSunHeight(jd.atHour(t))); if (noon < lowerLimitHours || noon >= upperLimitHours) { noon = null; } } if (nadir != null) { nadir = readjustMin(nadir, 2.0, 14, t -> correctedSunHeight(jd.atHour(t))); if (nadir < lowerLimitHours || nadir >= upperLimitHours) { nadir = null; } } return new SunTimes( rise != null ? jd.atHour(rise).getDateTime() : null, set != null ? jd.atHour(set).getDateTime() : null, noon != null ? jd.atHour(noon).getDateTime() : null, nadir != null ? jd.atHour(nadir).getDateTime() : null, alwaysUp, alwaysDown ); } /** * Computes the sun height at the given date and position. * * @param jd {@link JulianDate} to use * @return height, in radians */ private double correctedSunHeight(JulianDate jd) { Vector pos = Sun.positionHorizontal(jd, getLatitudeRad(), getLongitudeRad()); double hc = angle; if (position != null) { hc -= apparentRefraction(hc); hc += parallax(getElevation(), pos.getR()); hc -= position * Sun.angularRadius(pos.getR()); } return pos.getTheta() - hc; } } /** * Sunrise time. {@code null} if the sun does not rise that day. *

* Always returns a sunrise time if {@link Parameters#fullCycle()} was set. */ @Nullable public ZonedDateTime getRise() { return rise; } /** * Sunset time. {@code null} if the sun does not set that day. *

* Always returns a sunset time if {@link Parameters#fullCycle()} was set. */ @Nullable public ZonedDateTime getSet() { return set; } /** * The time when the sun reaches its highest point. *

* Use {@link #isAlwaysDown()} to find out if the highest point is still below the * twilight angle. */ @Nullable public ZonedDateTime getNoon() { return noon; } /** * The time when the sun reaches its lowest point. *

* Use {@link #isAlwaysUp()} to find out if the lowest point is still above the * twilight angle. */ @Nullable public ZonedDateTime getNadir() { return nadir; } /** * {@code true} if the sun never rises/sets, but is always above the twilight angle. */ public boolean isAlwaysUp() { return alwaysUp; } /** * {@code true} if the sun never rises/sets, but is always below the twilight angle. */ public boolean isAlwaysDown() { return alwaysDown; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("SunTimes[rise=").append(rise); sb.append(", set=").append(set); sb.append(", noon=").append(noon); sb.append(", nadir=").append(nadir); sb.append(", alwaysUp=").append(alwaysUp); sb.append(", alwaysDown=").append(alwaysDown); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/package-info.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ /** * This is the main package. It contains classes for calculating sun and moon data. */ @ReturnValuesAreNonnullByDefault @DefaultAnnotationForParameters(NonNull.class) @DefaultAnnotationForFields(NonNull.class) package org.shredzone.commons.suncalc; import edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields; import edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/param/Builder.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.param; /** * An interface for the method that eventually executes the calculation. * * @param * Result type */ public interface Builder { /** * Executes the calculation and returns the desired result. *

* The resulting object is immutable. You can change parameters, and then invoke * {@link #execute()} again, to get a new object with new results. * * @return Result of the calculation. */ T execute(); } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/param/GenericParameter.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2020 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.param; /** * Generic parameters and options. * * @param * Type of the final builder */ public interface GenericParameter { /** * Creates a copy of the current parameters. The copy can be changed independently. */ T copy(); } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/param/LocationParameter.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.param; import static org.shredzone.commons.suncalc.util.ExtendedMath.dms; /** * Location based parameters. *

* Use them to give information about the geolocation of the observer. If ommitted, the * coordinates of Null Island are * used. * * @param * Type of the final builder */ @SuppressWarnings("unchecked") public interface LocationParameter { /** * Sets the latitude. * * @param lat * Latitude, in degrees. * @return itself */ T latitude(double lat); /** * Sets the longitude. * * @param lng * Longitude, in degrees. * @return itself */ T longitude(double lng); /** * Sets the elevation. * * @param h * Elevation, in meters above sea level. Default: 0.0 m. Negative values * are silently changed to the acceptable minimum of 0.0 m. * @return itself * @see #elevationFt(double) * @since 3.9 */ T elevation(double h); /** * Sets the elevation, in foot. * * @param ft * Elevation, in foot above sea level. Default: 0.0 ft. Negative values are * silently changed to the acceptable minimum of 0.0 ft. * @return itself * @see #elevation(double) * @since 3.9 */ default T elevationFt(double ft) { return elevation(ft * 0.3048); } /** * Sets the height. * * @param h * Height, in meters above sea level. Default: 0.0 m. Negative values are * silently changed to the acceptable minimum of 0.0 m. * @return itself * @deprecated Use {@link #elevation(double)} instead. */ @Deprecated default T height(double h) { return elevation(h); } /** * Sets the height, in foot. * * @param ft * Height, in foot above sea level. Default: 0.0 ft. Negative values are * silently changed to the acceptable minimum of 0.0 ft. * @return itself * @since 3.8 * @deprecated Use {@link #elevationFt(double)} instead. */ @Deprecated default T heightFt(double ft) { return elevationFt(ft); } /** * Sets the geolocation. * * @param lat * Latitude, in degrees. * @param lng * Longitude, in degrees. * @return itself */ default T at(double lat, double lng) { latitude(lat); longitude(lng); return (T) this; } /** * Sets the geolocation. In the given array, index 0 must contain the latitude, and * index 1 must contain the longitude. An optional index 2 may contain the elevation, * in meters. *

* This call is meant to be used for coordinates stored in constants. * * @param coords * Array containing the latitude and longitude, in degrees. * @return itself */ default T at(double[] coords) { if (coords.length != 2 && coords.length != 3) { throw new IllegalArgumentException("Array must contain 2 or 3 doubles"); } if (coords.length == 3) { elevation(coords[2]); } return at(coords[0], coords[1]); } /** * Sets the latitude. * * @param d * Degrees * @param m * Minutes * @param s * Seconds (and fraction of seconds) * @return itself */ default T latitude(int d, int m, double s) { return latitude(dms(d, m, s)); } /** * Sets the longitude. * * @param d * Degrees * @param m * Minutes * @param s * Seconds (and fraction of seconds) * @return itself */ default T longitude(int d, int m, double s) { return longitude(dms(d, m, s)); } /** * Uses the same location as given in the {@link LocationParameter} at this moment. *

* Changes to the source parameter will not affect this parameter, though. * * @param l {@link LocationParameter} to be used. * @return itself */ T sameLocationAs(LocationParameter l); } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/param/TimeParameter.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.param; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Date; import java.util.Objects; import java.util.TimeZone; /** * Time based parameters. *

* Use them to give information about the desired time. If ommitted, the current time and * the system's time zone is used. * * @param * Type of the final builder */ @SuppressWarnings("unchecked") public interface TimeParameter { /** * Sets date and time. Note that also seconds can be passed in for convenience, but * the results are not that accurate. * * @param year * Year * @param month * Month (1 = January, 2 = February, ...) * @param date * Day of month * @param hour * Hour of day * @param minute * Minute * @param second * Second * @return itself */ T on(int year, int month, int date, int hour, int minute, int second); /** * Sets midnight of the year, month and date. * * @param year * Year * @param month * Month (1 = January, 2 = February, ...) * @param date * Day of month * @return itself */ default T on(int year, int month, int date) { return on(year, month, date, 0, 0, 0); } /** * Uses the given {@link ZonedDateTime} instance. * * @param dateTime * {@link ZonedDateTime} to be used. * @return itself */ T on(ZonedDateTime dateTime); /** * Uses the given {@link LocalDateTime} instance. * * @param dateTime * {@link LocalDateTime} to be used. * @return itself */ T on(LocalDateTime dateTime); /** * Uses the given {@link LocalDate} instance, and assumes midnight. * * @param date * {@link LocalDate} to be used. * @return itself */ T on(LocalDate date); /** * Uses the given {@link Instant} instance. * * @param instant * {@link Instant} to be used. * @return itself */ T on(Instant instant); /** * Uses the given {@link Date} instance. * * @param date * {@link Date} to be used. * @return itself */ default T on(Date date) { Objects.requireNonNull(date, "date"); return on(date.toInstant()); } /** * Uses the given {@link Calendar} instance. * * @param cal * {@link Calendar} to be used * @return itself */ default T on(Calendar cal) { Objects.requireNonNull(cal, "cal"); return on(ZonedDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId())); } /** * Sets the current date and time. This is the default. * * @return itself */ T now(); /** * Sets the time to the start of the current date ("last midnight"). * * @return itself */ T midnight(); /** * Adds a number of days to the current date. * * @param days * Number of days to add * @return itself */ T plusDays(int days); /** * Sets today, midnight. *

* It is the same as now().midnight(). * * @return itself */ default T today() { now(); midnight(); return (T) this; } /** * Sets tomorrow, midnight. *

* It is the same as now().midnight().plusDays(1). * * @return itself */ default T tomorrow() { today(); plusDays(1); return (T) this; } /** * Sets the given {@link ZoneId}. The local time is retained, so the parameter order * is not important. * * @param tz * {@link ZoneId} to be used. * @return itself */ T timezone(ZoneId tz); /** * Sets the given timezone. This is a convenience method that just invokes * {@link ZoneId#of(String)}. * * @param id * ID of the time zone. * @return itself * @see ZoneId#of(String) */ default T timezone(String id) { return timezone(ZoneId.of(id)); } /** * Sets the system's timezone. This is the default. * * @return itself */ default T localTime() { return timezone(ZoneId.systemDefault()); } /** * Sets the time zone to UTC. * * @return itself */ default T utc() { return timezone("UTC"); } /** * Sets the {@link TimeZone}. * * @param tz {@link TimeZone} to be used * @return itself */ default T timezone(TimeZone tz) { Objects.requireNonNull(tz, "tz"); return timezone(tz.toZoneId()); } /** * Uses the same time as given in the {@link TimeParameter}. *

* Changes to the source parameter will not affect this parameter, though. * * @param t {@link TimeParameter} to be used. * @return itself */ T sameTimeAs(TimeParameter t); } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/param/WindowParameter.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2024 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.param; import java.time.Duration; /** * Time window based parameters. *

* Use them to give information about the desired time window. If ommitted, a forward * window of 365 days is assumed. * * @since 3.11 * @param * Type of the final builder */ public interface WindowParameter { /** * Limits the calculation window to the given {@link Duration}. * * @param duration * Duration of the calculation window. A negative duration sets a reverse time * window, giving result times in the past. * @return itself */ T limit(Duration duration); /** * Limits the time window to the next 24 hours. * * @return itself */ default T oneDay() { return limit(Duration.ofDays(1L)); } /** * Computes until all times are found. *

* This is the default. * * @return itself */ default T fullCycle() { return limit(Duration.ofDays(365L)); } /** * Sets a reverse calculation window. It will end at the given date. * * @return itself * @since 3.11 */ T reverse(); /** * Sets a forward calculation window. It will start at the given date. *

* This is the default. * * @return itself * @since 3.11 */ T forward(); /** * Uses the same window as given in the {@link WindowParameter}. *

* Changes to the source parameter will not affect this parameter, though. * * @param w * {@link WindowParameter} to be used. * @return itself * @since 3.11 */ T sameWindowAs(WindowParameter w); } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/param/package-info.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ /** * This package contains interfaces for setting common calculation parameters. */ @ReturnValuesAreNonnullByDefault @DefaultAnnotationForParameters(NonNull.class) @DefaultAnnotationForFields(NonNull.class) package org.shredzone.commons.suncalc.param; import edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields; import edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/BaseBuilder.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.max; import static java.lang.Math.toRadians; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Objects; import edu.umd.cs.findbugs.annotations.Nullable; import org.shredzone.commons.suncalc.param.GenericParameter; import org.shredzone.commons.suncalc.param.LocationParameter; import org.shredzone.commons.suncalc.param.TimeParameter; import org.shredzone.commons.suncalc.param.WindowParameter; /** * A base implementation of {@link LocationParameter}, {@link TimeParameter}, and * {@link WindowParameter}. *

* For internal use only. * * @param * Type of the final builder */ @SuppressWarnings("unchecked") public class BaseBuilder implements GenericParameter, LocationParameter, TimeParameter, WindowParameter, Cloneable { private @Nullable Double lat = null; private @Nullable Double lng = null; private double elevation = 0.0; private ZonedDateTime dateTime = ZonedDateTime.now(); private boolean reverse = false; private Duration duration = Duration.ofDays(365L); @Override public T on(ZonedDateTime dateTime) { this.dateTime = Objects.requireNonNull(dateTime, "dateTime"); return (T) this; } @Override public T on(LocalDateTime dateTime) { Objects.requireNonNull(dateTime, "dateTime"); return on(ZonedDateTime.of(dateTime, this.dateTime.getZone())); } @Override public T on(LocalDate date) { Objects.requireNonNull(date, "date"); return on(ZonedDateTime.of(date, LocalTime.MIDNIGHT, dateTime.getZone())); } @Override public T on(Instant instant) { Objects.requireNonNull(instant, "instant"); return on(ZonedDateTime.ofInstant(instant, dateTime.getZone())); } @Override public T on(int year, int month, int date, int hour, int minute, int second) { return on(ZonedDateTime.of(year, month, date, hour, minute, second, 0, dateTime.getZone())); } @Override public T now() { return on(ZonedDateTime.now(dateTime.getZone())); } @Override public T plusDays(int days) { return on(dateTime.plusDays(days)); } @Override public T midnight() { return on(dateTime.truncatedTo(ChronoUnit.DAYS)); } @Override public T timezone(ZoneId tz) { Objects.requireNonNull(tz, "tz"); on(dateTime.withZoneSameLocal(tz)); return (T) this; } @Override public T latitude(double lat) { if (lat < -90.0 || lat > 90.0) { throw new IllegalArgumentException("Latitude out of range, -90.0 <= " + lat + " <= 90.0"); } this.lat = lat; return (T) this; } @Override public T longitude(double lng) { if (lng < -180.0 || lng > 180.0) { throw new IllegalArgumentException("Longitude out of range, -180.0 <= " + lng + " <= 180.0"); } this.lng = lng; return (T) this; } @Override public T elevation(double h) { this.elevation = max(h, 0.0); return (T) this; } public T limit(Duration duration) { Objects.requireNonNull(duration, "duration"); this.duration = duration; if (duration.isNegative()) { reverse(); } return (T) this; } public T reverse() { reverse = true; return (T) this; } public T forward() { reverse = false; return (T) this; } @Override public T sameTimeAs(TimeParameter t) { if (! (t instanceof BaseBuilder)) { throw new IllegalArgumentException("Cannot read the TimeParameter"); } this.dateTime = ((BaseBuilder) t).dateTime; return (T) this; } @Override public T sameLocationAs(LocationParameter l) { if (! (l instanceof BaseBuilder)) { throw new IllegalArgumentException("Cannot read the LocationParameter"); } BaseBuilder origin = (BaseBuilder) l; this.lat = origin.lat; this.lng = origin.lng; this.elevation = origin.elevation; return (T) this; } @Override public T sameWindowAs(WindowParameter w) { if (! (w instanceof BaseBuilder)) { throw new IllegalArgumentException("Cannot read the WindowParameter"); } BaseBuilder origin = (BaseBuilder) w; this.duration = origin.duration; this.reverse = origin.reverse; return (T) this; } @Override public T copy() { try { return (T) clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); // Should never be thrown anyway } } /** * Returns the longitude. * * @return Longitude, in degrees. */ public double getLongitude() { if (lng == null) { throw new IllegalStateException("longitude is not set"); } return lng; } /** * Returns the latitude. * * @return Latitude, in degrees. */ public double getLatitude() { if (lat == null) { throw new IllegalStateException("latitude is not set"); } return lat; } /** * Returns the longitude. * * @return Longitude, in radians. */ public double getLongitudeRad() { return toRadians(getLongitude()); } /** * Returns the latitude. * * @return Latitude, in radians. */ public double getLatitudeRad() { return toRadians(getLatitude()); } /** * Returns the elevation, in meters above sea level. * * @return Elevation, meters above sea level */ public double getElevation() { return elevation; } /** * Returns the {@link JulianDate} to be used. * * @return {@link JulianDate} */ public JulianDate getJulianDate() { return new JulianDate(dateTime); } /** * Returns {@code true} if a geolocation has been set. * * @since 3.9 */ public boolean hasLocation() { return lat != null && lng != null; } /** * Unset the geolocation. * * @since 3.9 */ public void clearLocation() { lat = null; lng = null; } /** * Returns the duration of the time window. * * @since 3.11 */ public Duration getDuration() { if (reverse != duration.isNegative()) { return duration.negated(); } return duration; } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/ExtendedMath.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.*; import java.util.Comparator; import java.util.function.Function; /** * Contains constants and mathematical operations that are not available in {@link Math}. */ public final class ExtendedMath { /** * PI * 2 */ public static final double PI2 = PI * 2.0; /** * Arc-Seconds per Radian. */ public static final double ARCS = toDegrees(3600.0); /** * Mean radius of the earth, in kilometers. */ public static final double EARTH_MEAN_RADIUS = 6371.0; /** * Refraction at the horizon, in radians. */ public static final double REFRACTION_AT_HORIZON = PI / (tan(toRadians(7.31 / 4.4)) * 10800.0); private ExtendedMath() { // utility class without constructor } /** * Returns the decimal part of a value. * * @param a * Value * @return Fraction of that value. It has the same sign as the input value. */ public static double frac(double a) { return a % 1.0; } /** * Performs a safe check if the given double is actually zero (0.0). *

* Note that "almost zero" returns {@code false}, so this method should not be used * for comparing calculation results to zero. * * @param d * double to check for zero. * @return {@code true} if the value was zero, or negative zero. */ public static boolean isZero(double d) { // This should keep squid:S1244 silent... return !Double.isNaN(d) && round(signum(d)) == 0L; } /** * Converts equatorial coordinates to horizontal coordinates. * * @param tau * Hour angle (radians) * @param dec * Declination (radians) * @param dist * Distance of the object * @param lat * Latitude of the observer (radians) * @return {@link Vector} containing the horizontal coordinates */ public static Vector equatorialToHorizontal(double tau, double dec, double dist, double lat) { return Matrix.rotateY(PI / 2.0 - lat).multiply(Vector.ofPolar(tau, dec, dist)); } /** * Creates a rotational {@link Matrix} for converting equatorial to ecliptical * coordinates. * * @param t * {@link JulianDate} to use * @return {@link Matrix} for converting equatorial to ecliptical coordinates */ public static Matrix equatorialToEcliptical(JulianDate t) { double jc = t.getJulianCentury(); double eps = toRadians(23.43929111 - (46.8150 + (0.00059 - 0.001813 * jc) * jc) * jc / 3600.0); return Matrix.rotateX(eps); } /** * Returns the parallax for objects at the horizon. * * @param elevation * Observer's elevation, in meters above sea level. Must not be negative. * @param distance * Distance of the object, in kilometers. * @return parallax, in radians */ public static double parallax(double elevation, double distance) { return asin(EARTH_MEAN_RADIUS / distance) - acos(EARTH_MEAN_RADIUS / (EARTH_MEAN_RADIUS + (elevation / 1000.0))); } /** * Calculates the atmospheric refraction of an object at the given apparent altitude. *

* The result is only valid for positive altitude angles. If negative, 0.0 is * returned. *

* Assumes an atmospheric pressure of 1010 hPa and a temperature of 10 °C. * * @param ha * Apparent altitude, in radians. * @return Refraction at this altitude * @see Wikipedia: * Atmospheric Refraction */ public static double apparentRefraction(double ha) { if (ha < 0.0) { return 0.0; } if (isZero(ha)) { return REFRACTION_AT_HORIZON; } return PI / (tan(toRadians(ha + (7.31 / (ha + 4.4)))) * 10800.0); } /** * Calculates the atmospheric refraction of an object at the given altitude. *

* The result is only valid for positive altitude angles. If negative, 0.0 is * returned. *

* Assumes an atmospheric pressure of 1010 hPa and a temperature of 10 °C. * * @param h * True altitude, in radians. * @return Refraction at this altitude * @see Wikipedia: * Atmospheric Refraction */ public static double refraction(double h) { if (h < 0.0) { return 0.0; } // refraction formula, converted to radians return 0.000296706 / tan(h + 0.00312537 / (h + 0.0890118)); } /** * Converts dms to double. * * @param d * Degrees. Sign is used for result. * @param m * Minutes. Sign is ignored. * @param s * Seconds and fractions. Sign is ignored. * @return angle, in degrees */ public static double dms(int d, int m, double s) { double sig = d < 0 ? -1.0 : 1.0; return sig * ((abs(s) / 60.0 + abs(m)) / 60.0 + abs(d)); } /** * Locates the true maximum within the given time frame. * * @param time * Base time * @param frame * Time frame, which is added to and subtracted from the base time for the * interval * @param depth * Maximum recursion depth. For each recursion, the function is invoked once. * @param f * Function to be used for calculation * @return time of the true maximum */ public static double readjustMax(double time, double frame, int depth, Function f) { double left = time - frame; double right = time + frame; double leftY = f.apply(left); double rightY = f.apply(right); return readjustInterval(left, right, leftY, rightY, depth, f, Double::compare); } /** * Locates the true minimum within the given time frame. * * @param time * Base time * @param frame * Time frame, which is added to and subtracted from the base time for the * interval * @param depth * Maximum recursion depth. For each recursion, the function is invoked once. * @param f * Function to be used for calculation * @return time of the true minimum */ public static double readjustMin(double time, double frame, int depth, Function f) { double left = time - frame; double right = time + frame; double leftY = f.apply(left); double rightY = f.apply(right); return readjustInterval(left, right, leftY, rightY, depth, f, (yl, yr) -> Double.compare(yr, yl)); } /** * Recursively find the true maximum/minimum within the given time frame. * * @param left * Left interval border * @param right * Right interval border * @param yl * Function result at the left interval * @param yr * Function result at the right interval * @param depth * Maximum recursion depth. For each recursion, the function is invoked once. * @param f * Function to invoke * @param cmp * Comparator to decide whether the left or right side of the interval half is * to be used * @return Position of the approximated minimum/maximum */ private static double readjustInterval(double left, double right, double yl, double yr, int depth, Function f, Comparator cmp) { if (depth <= 0) { return (cmp.compare(yl, yr) < 0) ? right : left; } double middle = (left + right) / 2.0; double ym = f.apply(middle); if (cmp.compare(yl, yr) < 0) { return readjustInterval(middle, right, ym, yr, depth - 1, f, cmp); } else { return readjustInterval(left, middle, yl, ym, depth - 1, f, cmp); } } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/JulianDate.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.floor; import static java.lang.Math.round; import static org.shredzone.commons.suncalc.util.ExtendedMath.PI2; import static org.shredzone.commons.suncalc.util.ExtendedMath.frac; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Objects; /** * This class contains a Julian Date representation of a date. *

* Objects are immutable and threadsafe. */ public class JulianDate { private final ZonedDateTime dateTime; private final double mjd; /** * Creates a new {@link JulianDate}. * * @param time * {@link ZonedDateTime} to use for the date. */ public JulianDate(ZonedDateTime time) { dateTime = Objects.requireNonNull(time, "time"); mjd = dateTime.toInstant().toEpochMilli() / 86400000.0 + 40587.0; } /** * Returns a {@link JulianDate} of the current date and the given hour. * * @param hour * Hour of this date. This is a floating point value. Fractions are used * for minutes and seconds. * @return {@link JulianDate} instance. */ public JulianDate atHour(double hour) { return new JulianDate(dateTime.plusSeconds(round(hour * 60.0 * 60.0))); } /** * Returns a {@link JulianDate} of the given modified Julian date. * * @param mjd * Modified Julian Date * @return {@link JulianDate} instance. */ public JulianDate atModifiedJulianDate(double mjd) { Instant mjdi = Instant.ofEpochMilli(Math.round((mjd - 40587.0) * 86400000.0)); return new JulianDate(ZonedDateTime.ofInstant(mjdi, dateTime.getZone())); } /** * Returns a {@link JulianDate} of the given Julian century. * * @param jc * Julian Century * @return {@link JulianDate} instance. */ public JulianDate atJulianCentury(double jc) { return atModifiedJulianDate(jc * 36525.0 + 51544.5); } /** * Returns this {@link JulianDate} as {@link ZonedDateTime} object. * * @return {@link ZonedDateTime} of this {@link JulianDate}. */ public ZonedDateTime getDateTime() { return dateTime; } /** * Returns the Modified Julian Date. * * @return Modified Julian Date, UTC. */ public double getModifiedJulianDate() { return mjd; } /** * Returns the Julian Centuries. * * @return Julian Centuries, based on J2000 epoch, UTC. */ public double getJulianCentury() { return (mjd - 51544.5) / 36525.0; } /** * Returns the Greenwich Mean Sidereal Time of this Julian Date. * * @return GMST */ public double getGreenwichMeanSiderealTime() { final double secs = 86400.0; double mjd0 = floor(mjd); double ut = (mjd - mjd0) * secs; double t0 = (mjd0 - 51544.5) / 36525.0; double t = (mjd - 51544.5) / 36525.0; double gmst = 24110.54841 + 8640184.812866 * t0 + 1.0027379093 * ut + (0.093104 - 6.2e-6 * t) * t * t; return (PI2 / secs) * (gmst % secs); } /** * Returns the earth's true anomaly of the current date. *

* A simple approximation is used here. * * @return True anomaly, in radians */ public double getTrueAnomaly() { return PI2 * frac((dateTime.getDayOfYear() - 5.0) / 365.256363); } @Override public String toString() { return String.format("%dd %02dh %02dm %02ds", (long) mjd, (long) (mjd * 24 % 24), (long) (mjd * 24 * 60 % 60), (long) (mjd * 24 * 60 * 60 % 60)); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/Matrix.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.cos; import static java.lang.Math.sin; import java.util.Arrays; /** * A three dimensional matrix. *

* Objects are immutable and threadsafe. */ public class Matrix { private final double[] mx; private Matrix() { mx = new double[9]; } private Matrix(double... values) { if (values == null || values.length != 9) { throw new IllegalArgumentException("requires 9 values"); } mx = values; } /** * Creates an identity matrix. * * @return Identity {@link Matrix} */ public static Matrix identity() { return new Matrix( 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); } /** * Creates a matrix that rotates a vector by the given angle at the X axis. * * @param angle * angle, in radians * @return Rotation {@link Matrix} */ public static Matrix rotateX(double angle) { double s = sin(angle); double c = cos(angle); return new Matrix( 1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c ); } /** * Creates a matrix that rotates a vector by the given angle at the Y axis. * * @param angle * angle, in radians * @return Rotation {@link Matrix} */ public static Matrix rotateY(double angle) { double s = sin(angle); double c = cos(angle); return new Matrix( c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c ); } /** * Creates a matrix that rotates a vector by the given angle at the Z axis. * * @param angle * angle, in radians * @return Rotation {@link Matrix} */ public static Matrix rotateZ(double angle) { double s = sin(angle); double c = cos(angle); return new Matrix( c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0 ); } /** * Transposes this matrix. * * @return {@link Matrix} that is a transposition of this matrix. */ public Matrix transpose() { Matrix result = new Matrix(); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { result.set(i, j, get(j, i)); } } return result; } /** * Negates this matrix. * * @return {@link Matrix} that is a negation of this matrix. */ public Matrix negate() { Matrix result = new Matrix(); for (int i = 0; i < 9; i++) { result.mx[i] = -mx[i]; } return result; } /** * Adds a matrix to this matrix. * * @param right * {@link Matrix} to add * @return {@link Matrix} that is a sum of both matrices */ public Matrix add(Matrix right) { Matrix result = new Matrix(); for (int i = 0; i < 9; i++) { result.mx[i] = mx[i] + right.mx[i]; } return result; } /** * Subtracts a matrix from this matrix. * * @param right * {@link Matrix} to subtract * @return {@link Matrix} that is the difference of both matrices */ public Matrix subtract(Matrix right) { Matrix result = new Matrix(); for (int i = 0; i < 9; i++) { result.mx[i] = mx[i] - right.mx[i]; } return result; } /** * Multiplies two matrices. * * @param right * {@link Matrix} to multiply with * @return {@link Matrix} that is the product of both matrices */ public Matrix multiply(Matrix right) { Matrix result = new Matrix(); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { double scalp = 0.0; for (int k = 0; k < 3; k++) { scalp += get(i, k) * right.get(k, j); } result.set(i, j, scalp); } } return result; } /** * Performs a scalar multiplication. * * @param scalar * Scalar to multiply with * @return {@link Matrix} that is the scalar product */ public Matrix multiply(double scalar) { Matrix result = new Matrix(); for (int i = 0; i < 9; i++) { result.mx[i] = mx[i] * scalar; } return result; } /** * Applies this matrix to a {@link Vector}. * * @param right * {@link Vector} to multiply with * @return {@link Vector} that is the product of this matrix and the given vector */ public Vector multiply(Vector right) { double[] vec = new double[] {right.getX(), right.getY(), right.getZ()}; double[] result = new double[3]; for (int i = 0; i < 3; i++) { double scalp = 0.0; for (int j = 0; j < 3; j++) { scalp += get(i, j) * vec[j]; } result[i] = scalp; } return new Vector(result); } /** * Gets a value from the matrix. * * @param r * Row number (0..2) * @param c * Column number (0..2) * @return Value at that position */ public double get(int r, int c) { if (r < 0 || r > 2 || c < 0 || c > 2) { throw new IllegalArgumentException("row/column out of range: " + r + ":" + c); } return mx[r * 3 + c]; } /** * Changes a value in the matrix. As a {@link Matrix} object is immutable from the * outside, this method is private. * * @param r * Row number (0..2) * @param c * Column number (0..2) * @param v * New value */ private void set(int r, int c, double v) { if (r < 0 || r > 2 || c < 0 || c > 2) { throw new IllegalArgumentException("row/column out of range: " + r + ":" + c); } mx[r * 3 + c] = v; } @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof Matrix)) { return false; } return Arrays.equals(mx, ((Matrix) obj).mx); } @Override public int hashCode() { return Arrays.hashCode(mx); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append('['); for (int ix = 0; ix < 9; ix++) { if (ix % 3 == 0) { sb.append('['); } sb.append(mx[ix]); if (ix % 3 == 2) { sb.append(']'); } if (ix < 8) { sb.append(", "); } } sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/Moon.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.*; import static org.shredzone.commons.suncalc.util.ExtendedMath.*; /** * Calculations and constants for the Moon. * * @see "Astronomy on the Personal Computer, 4th edition * (Oliver Montenbruck, Thomas Pfleger) - * ISBN 978-3-540-67221-0" */ public final class Moon { private static final double MOON_MEAN_RADIUS = 1737.1; private Moon() { // Utility class without constructor } /** * Calculates the equatorial position of the moon. * * @param date * {@link JulianDate} to be used * @return {@link Vector} of equatorial moon position */ public static Vector positionEquatorial(JulianDate date) { double T = date.getJulianCentury(); double L0 = frac(0.606433 + 1336.855225 * T); double l = PI2 * frac(0.374897 + 1325.552410 * T); double ls = PI2 * frac(0.993133 + 99.997361 * T); double D = PI2 * frac(0.827361 + 1236.853086 * T); double F = PI2 * frac(0.259086 + 1342.227825 * T); double D2 = 2.0 * D; double l2 = 2.0 * l; double F2 = 2.0 * F; double dL = 22640.0 * sin(l) - 4586.0 * sin(l - D2) + 2370.0 * sin(D2) + 769.0 * sin(l2) - 668.0 * sin(ls) - 412.0 * sin(F2) - 212.0 * sin(l2 - D2) - 206.0 * sin(l + ls - D2) + 192.0 * sin(l + D2) - 165.0 * sin(ls - D2) - 125.0 * sin(D) - 110.0 * sin(l + ls) + 148.0 * sin(l - ls) - 55.0 * sin(F2 - D2); double S = F + (dL + 412.0 * sin(F2) + 541.0 * sin(ls)) / ARCS; double h = F - D2; double N = -526.0 * sin(h) + 44.0 * sin(l + h) - 31.0 * sin(-l + h) - 23.0 * sin(ls + h) + 11.0 * sin(-ls + h) - 25.0 * sin(-l2 + F) + 21.0 * sin(-l + F); double l_Moon = PI2 * frac(L0 + dL / 1296.0e3); double b_Moon = (18520.0 * sin(S) + N) / ARCS; double dt = 385000.5584 - 20905.3550 * cos(l) - 3699.1109 * cos(D2 - l) - 2955.9676 * cos(D2) - 569.9251 * cos(l2); return Vector.ofPolar(l_Moon, b_Moon, dt); } /** * Calculates the geocentric position of the moon. * * @param date * {@link JulianDate} to be used * @return {@link Vector} of geocentric moon position */ public static Vector position(JulianDate date) { Matrix rotateMatrix = equatorialToEcliptical(date).transpose(); return rotateMatrix.multiply(positionEquatorial(date)); } /** * Calculates the horizontal position of the moon. * * @param date * {@link JulianDate} to be used * @param lat * Latitude, in radians * @param lng * Longitute, in radians * @return {@link Vector} of horizontal moon position */ public static Vector positionHorizontal(JulianDate date, double lat, double lng) { Vector mc = position(date); double h = date.getGreenwichMeanSiderealTime() + lng - mc.getPhi(); return equatorialToHorizontal(h, mc.getTheta(), mc.getR(), lat); } /** * Calculates the topocentric position of the moon. *

* Atmospheric refraction is not taken into account. * * @param date * {@link JulianDate} to be used * @param lat * Latitude, in radians * @param lng * Longitute, in radians * @param elev * Elevation, in meters * @return {@link Vector} of topocentric moon position * @since 3.9 */ public static Vector positionTopocentric(JulianDate date, double lat, double lng, double elev) { Vector pos = positionHorizontal(date, lat, lng); return Vector.ofPolar( pos.getPhi(), pos.getTheta() - parallax(elev, pos.getR()), pos.getR() ); } /** * Returns the angular radius of the moon. * * @param distance * Distance of the moon, in kilometers. * @return Angular radius of the moon, in radians. * @see Wikipedia: Angular * Diameter */ public static double angularRadius(double distance) { return asin(MOON_MEAN_RADIUS / distance); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/Pegasus.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2018 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.abs; import java.util.function.Function; /** * Finds the root of a function by using the Pegasus method. * * @see regula falsi */ public class Pegasus { private static final int MAX_ITERATIONS = 30; /** * Find the root of the given function within the boundaries. * * @param lower * Lower boundary * @param upper * Upper boundary * @param accuracy * Desired accuracy * @param f * Function to be used for calculation * @return root that was found * @throws ArithmeticException * if the root could not be found in the given accuracy within * {@value #MAX_ITERATIONS} iterations. */ public static Double calculate(double lower, double upper, double accuracy, Function f) { double x1 = lower; double x2 = upper; double f1 = f.apply(x1); double f2 = f.apply(x2); if (f1 * f2 >= 0.0) { throw new ArithmeticException("No root within the given boundaries"); } int i = MAX_ITERATIONS; while (i-- > 0) { double x3 = x2 - f2 / ((f2 - f1) / (x2 - x1)); double f3 = f.apply(x3); if (f3 * f2 <= 0.0) { x1 = x2; f1 = f2; x2 = x3; f2 = f3; } else { f1 = f1 * f2 / (f2 + f3); x2 = x3; f2 = f3; } if (abs(x2 - x1) <= accuracy) { return abs(f1) < abs(f2) ? x1 : x2; } } throw new ArithmeticException("Maximum number of iterations exceeded"); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/QuadraticInterpolation.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.abs; import static java.lang.Math.sqrt; /** * Calculates the roots and extremum of a quadratic equation. */ public class QuadraticInterpolation { private final double xe; private final double ye; private final double root1; private final double root2; private final int nRoot; private final boolean maximum; /** * Creates a new quadratic equation. * * @param yMinus * y at x == -1 * @param y0 * y at x == 0 * @param yPlus * y at x == 1 */ public QuadraticInterpolation(double yMinus, double y0, double yPlus) { double a = 0.5 * (yPlus + yMinus) - y0; double b = 0.5 * (yPlus - yMinus); double c = y0; xe = -b / (2.0 * a); ye = (a * xe + b) * xe + c; maximum = a < 0.0; double dis = b * b - 4.0 * a * c; int rootCount = 0; if (dis >= 0.0) { double dx = 0.5 * sqrt(dis) / abs(a); root1 = xe - dx; root2 = xe + dx; if (abs(root1) <= 1.0) { rootCount++; } if (abs(root2) <= 1.0) { rootCount++; } } else { root1 = Double.NaN; root2 = Double.NaN; } nRoot = rootCount; } /** * Returns X of extremum. Can be outside [-1 .. 1]. * * @return X */ public double getXe() { return xe; } /** * Returns the Y value at the extremum. * * @return Y */ public double getYe() { return ye; } /** * Returns the first root that was found. * * @return X of first root */ public double getRoot1() { return root1 < -1.0 ? root2 : root1; } /** * Returns the second root that was found. * * @return X of second root */ public double getRoot2() { return root2; } /** * Returns the number of roots found in [-1 .. 1]. * * @return Number of roots */ public int getNumberOfRoots() { return nRoot; } /** * Returns whether the extremum is a minimum or a maximum. * * @return {@code true}: Extremum at xe is a maximum. {@code false}: Extremum at xe is * a minimum. */ public boolean isMaximum() { return maximum; } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/Sun.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.*; import static org.shredzone.commons.suncalc.util.ExtendedMath.*; /** * Calculations and constants for the Sun. * * @see "Astronomy on the Personal Computer, 4th edition * (Oliver Montenbruck, Thomas Pfleger) - * ISBN 978-3-540-67221-0" */ public final class Sun { private static final double SUN_DISTANCE = 149598000.0; private static final double SUN_MEAN_RADIUS = 695700.0; private Sun() { // Utility class without constructor } /** * Calculates the equatorial position of the sun. * * @param date * {@link JulianDate} to be used * @return {@link Vector} containing the sun position */ public static Vector positionEquatorial(JulianDate date) { double T = date.getJulianCentury(); double M = PI2 * frac(0.993133 + 99.997361 * T); double L = PI2 * frac(0.7859453 + M / PI2 + (6893.0 * sin(M) + 72.0 * sin(2.0 * M) + 6191.2 * T) / 1296.0e3); double d = SUN_DISTANCE * (1 - 0.016718 * cos(date.getTrueAnomaly())); return Vector.ofPolar(L, 0.0, d); } /** * Calculates the geocentric position of the sun. * * @param date * {@link JulianDate} to be used * @return {@link Vector} containing the sun position */ public static Vector position(JulianDate date) { Matrix rotateMatrix = equatorialToEcliptical(date).transpose(); return rotateMatrix.multiply(positionEquatorial(date)); } /** * Calculates the horizontal position of the sun. * * @param date * {@link JulianDate} to be used * @param lat * Latitude, in radians * @param lng * Longitute, in radians * @return {@link Vector} of horizontal sun position */ public static Vector positionHorizontal(JulianDate date, double lat, double lng) { Vector mc = position(date); double h = date.getGreenwichMeanSiderealTime() + lng - mc.getPhi(); return equatorialToHorizontal(h, mc.getTheta(), mc.getR(), lat); } /** * Calculates the topocentric position of the sun. *

* Atmospheric refraction is not taken into account. * * @param date * {@link JulianDate} to be used * @param lat * Latitude, in radians * @param lng * Longitute, in radians * @param elev * Elevation, in meters * @return {@link Vector} of topocentric sun position * @since 3.9 */ public static Vector positionTopocentric(JulianDate date, double lat, double lng, double elev) { Vector pos = positionHorizontal(date, lat, lng); return Vector.ofPolar( pos.getPhi(), pos.getTheta() - parallax(elev, pos.getR()), pos.getR() ); } /** * Returns the angular radius of the sun. * * @param distance * Distance of the sun, in kilometers. * @return Angular radius of the sun, in radians. * @see Wikipedia: Angular * Diameter */ public static double angularRadius(double distance) { return asin(SUN_MEAN_RADIUS / distance); } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/Vector.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.*; import static org.shredzone.commons.suncalc.util.ExtendedMath.PI2; import static org.shredzone.commons.suncalc.util.ExtendedMath.isZero; import edu.umd.cs.findbugs.annotations.Nullable; /** * A three dimensional vector. *

* Objects are is immutable and threadsafe. */ public class Vector { private final double x; private final double y; private final double z; private final Polar polar = new Polar(); /** * Creates a new {@link Vector} of the given cartesian coordinates. * * @param x * X coordinate * @param y * Y coordinate * @param z * Z coordinate */ public Vector(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } /** * Creates a new {@link Vector} of the given cartesian coordinates. * * @param d * Array of coordinates, must have 3 elements */ public Vector(double[] d) { if (d.length != 3) { throw new IllegalArgumentException("invalid vector length"); } this.x = d[0]; this.y = d[1]; this.z = d[2]; } /** * Creates a new {@link Vector} of the given polar coordinates, with a radial distance * of 1. * * @param φ * Azimuthal Angle * @param θ * Polar Angle * @return Created {@link Vector} */ public static Vector ofPolar(double φ, double θ) { return ofPolar(φ, θ, 1.0); } /** * Creates a new {@link Vector} of the given polar coordinates. * * @param φ * Azimuthal Angle * @param θ * Polar Angle * @param r * Radial Distance * @return Created {@link Vector} */ public static Vector ofPolar(double φ, double θ, double r) { double cosθ = cos(θ); Vector result = new Vector( r * cos(φ) * cosθ, r * sin(φ) * cosθ, r * sin(θ) ); result.polar.setPolar(φ, θ, r); return result; } /** * Returns the cartesian X coordinate. */ public double getX() { return x; } /** * Returns the cartesian Y coordinate. */ public double getY() { return y; } /** * Returns the cartesian Z coordinate. */ public double getZ() { return z; } /** * Returns the azimuthal angle (φ) in radians. */ public double getPhi() { return polar.getPhi(); } /** * Returns the polar angle (θ) in radians. */ public double getTheta() { return polar.getTheta(); } /** * Returns the polar radial distance (r). */ public double getR() { return polar.getR(); } /** * Returns a {@link Vector} that is the sum of this {@link Vector} and the given * {@link Vector}. * * @param vec * {@link Vector} to add * @return Resulting {@link Vector} */ public Vector add(Vector vec) { return new Vector( x + vec.x, y + vec.y, z + vec.z ); } /** * Returns a {@link Vector} that is the difference of this {@link Vector} and the * given {@link Vector}. * * @param vec * {@link Vector} to subtract * @return Resulting {@link Vector} */ public Vector subtract(Vector vec) { return new Vector( x - vec.x, y - vec.y, z - vec.z ); } /** * Returns a {@link Vector} that is the scalar product of this {@link Vector} and the * given scalar. * * @param scalar * Scalar to multiply * @return Resulting {@link Vector} */ public Vector multiply(double scalar) { return new Vector( x * scalar, y * scalar, z * scalar ); } /** * Returns the negation of this {@link Vector}. * * @return Resulting {@link Vector} */ public Vector negate() { return new Vector( -x, -y, -z ); } /** * Returns a {@link Vector} that is the cross product of this {@link Vector} and the * given {@link Vector}. * * @param right * {@link Vector} to multiply * @return Resulting {@link Vector} */ public Vector cross(Vector right) { return new Vector( y * right.z - z * right.y, z * right.x - x * right.z, x * right.y - y * right.x ); } /** * Returns the dot product of this {@link Vector} and the given {@link Vector}. * * @param right * {@link Vector} to multiply * @return Resulting dot product */ public double dot(Vector right) { return x * right.x + y * right.y + z * right.z; } /** * Returns the norm of this {@link Vector}. * * @return Norm of this vector */ public double norm() { return sqrt(dot(this)); } @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof Vector)) { return false; } Vector vec = (Vector) obj; return Double.compare(x, vec.x) == 0 && Double.compare(y, vec.y) == 0 && Double.compare(z, vec.z) == 0; } @Override public int hashCode() { return Double.valueOf(x).hashCode() ^ Double.valueOf(y).hashCode() ^ Double.valueOf(z).hashCode(); } @Override public String toString() { return "(x=" + x + ", y=" + y + ", z=" + z + ")"; } /** * Helper class for lazily computing the polar coordinates in an immutable Vector * object. */ private class Polar { private @Nullable Double φ = null; private @Nullable Double θ = null; private @Nullable Double r = null; /** * Sets polar coordinates. * * @param φ * Phi * @param θ * Theta * @param r * R */ public synchronized void setPolar(double φ, double θ, double r) { this.φ = φ; this.θ = θ; this.r = r; } public synchronized double getPhi() { if (φ == null) { if (isZero(x) && isZero(y)) { φ = 0.0; } else { φ = atan2(y, x); } if (φ < 0.0) { φ += PI2; } } return φ; } public synchronized double getTheta() { if (θ == null) { double ρSqr = x * x + y * y; if (isZero(z) && isZero(ρSqr)) { θ = 0.0; } else { θ = atan2(z, sqrt(ρSqr)); } } return θ; } public synchronized double getR() { if (r == null) { r = sqrt(x * x + y * y + z * z); } return r; } } } ================================================ FILE: src/main/java/org/shredzone/commons/suncalc/util/package-info.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ /** * This package contains internal utility methods. *

* Do not use in your code! This package will not be exported in a Java module. */ @ReturnValuesAreNonnullByDefault @DefaultAnnotationForParameters(NonNull.class) @DefaultAnnotationForFields(NonNull.class) package org.shredzone.commons.suncalc.util; import edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields; import edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; ================================================ FILE: src/main/resources/.gitignore ================================================ ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/ExamplesTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2020 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; import java.util.TimeZone; import org.junit.Ignore; import org.junit.Test; /** * These are some examples that are meant to be executed manually. * * @see Example * chapter of the Documentation */ @Ignore // No real unit tests, but meant to be run manually public class ExamplesTest { @Test public void testTimezone() { // Our example takes place in Paris, so set the timezone accordingly. TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); SunTimes paris = SunTimes.compute() .on(2020, 5, 1) // May 1st, 2020, starting midnight .latitude(48, 51, 24.0) // Latitude of Paris: 48°51'24" N .longitude(2, 21, 6.0) // Longitude: 2°21'06" E .execute(); System.out.println("Sunrise in Paris: " + paris.getRise()); System.out.println("Sunset in Paris: " + paris.getSet()); SunTimes newYork = SunTimes.compute() .on(2020, 5, 1) // May 1st, 2020, starting midnight .at(40.712778, -74.005833) // Coordinates of New York .execute(); System.out.println("Sunrise in New York: " + newYork.getRise()); System.out.println("Sunset in New York: " + newYork.getSet()); SunTimes newYorkTz = SunTimes.compute() .on(2020, 5, 1) // May 1st, 2020, starting midnight .timezone("America/New_York") // ...New York timezone .at(40.712778, -74.005833) // Coordinates of New York .execute(); System.out.println("Sunrise in New York: " + newYorkTz.getRise()); System.out.println("Sunset in New York: " + newYorkTz.getSet()); } @Test public void testTimeWindow() { final double[] ALERT_CANADA = new double[] { 82.5, -62.316667 }; final ZoneId ALERT_TZ = ZoneId.of("Canada/Eastern"); SunTimes march = SunTimes.compute() .on(2020, 3, 15) // March 15th, 2020, starting midnight .at(ALERT_CANADA) // Coordinates are stored in an array .timezone(ALERT_TZ) .execute(); System.out.println("Sunrise: " + march.getRise()); System.out.println("Sunset: " + march.getSet()); SunTimes june = SunTimes.compute() .on(2020, 6, 15) // June 15th, 2020, starting midnight .at(ALERT_CANADA) .timezone(ALERT_TZ) .execute(); System.out.println("Sunrise: " + june.getRise()); System.out.println("Sunset: " + june.getSet()); SunTimes juneReverse = SunTimes.compute() .on(2020, 6, 15) // June 15th, 2020, starting midnight .at(ALERT_CANADA) .timezone(ALERT_TZ) .reverse() .execute(); System.out.println("Sunrise: " + juneReverse.getRise()); System.out.println("Sunset: " + juneReverse.getSet()); SunTimes june15OnlyCycle = SunTimes.compute() .on(2020, 6, 15) // June 15th, 2020, starting midnight .at(ALERT_CANADA) .timezone(ALERT_TZ) .limit(Duration.ofHours(24)) .execute(); System.out.println("Sunset: " + june15OnlyCycle.getSet()); System.out.println("Sunrise: " + june15OnlyCycle.getRise()); System.out.println("Sun is up all day: " + june15OnlyCycle.isAlwaysUp()); System.out.println("Sun is down all day: " + june15OnlyCycle.isAlwaysDown()); } @Test public void testParameterRecycling() { final double[] COLOGNE = new double[] { 50.938056, 6.956944 }; MoonTimes.Parameters parameters = MoonTimes.compute() .at(COLOGNE) .midnight(); MoonTimes today = parameters.execute(); System.out.println("Today, the moon rises in Cologne at " + today.getRise()); parameters.tomorrow(); MoonTimes tomorrow = parameters.execute(); System.out.println("Tomorrow, the moon will rise in Cologne at " + tomorrow.getRise()); System.out.println("But today, the moon still rises at " + today.getRise()); } @Test public void testParameterRecyclingLoop() { MoonIllumination.Parameters parameters = MoonIllumination.compute() .on(2020, 1, 1); for (int i = 1; i <= 31; i++) { long percent = Math.round(parameters.execute().getFraction() * 100.0); System.out.println("On January " + i + " the moon was " + percent + "% lit."); parameters.plusDays(1); } } @Test public void testGoldenHour() { SunTimes.Parameters base = SunTimes.compute() .at(1.283333, 103.833333) // Singapore .on(2020, 6, 1) .timezone("Asia/Singapore"); for (int i = 0; i < 4; i++) { SunTimes blue = base .copy() // Use a copy of base .plusDays(i * 7) .twilight(SunTimes.Twilight.BLUE_HOUR) // Blue Hour .execute(); SunTimes golden = base .copy() // Use a copy of base .plusDays(i * 7) .twilight(SunTimes.Twilight.GOLDEN_HOUR) // Golden Hour .execute(); System.out.println("Morning golden hour starts at " + blue.getRise()); System.out.println("Morning golden hour ends at " + golden.getRise()); System.out.println("Evening golden hour starts at " + golden.getSet()); System.out.println("Evening golden hour ends at " + blue.getSet()); } } @Test public void testMoonPhase() { LocalDate date = LocalDate.of(2023, 1, 1); MoonPhase.Parameters parameters = MoonPhase.compute() .phase(MoonPhase.Phase.FULL_MOON); while (true) { MoonPhase moonPhase = parameters .on(date) .execute(); LocalDate nextFullMoon = moonPhase .getTime() .toLocalDate(); if (nextFullMoon.getYear() == 2024) { break; // we've reached the next year } System.out.print(nextFullMoon); if (moonPhase.isMicroMoon()) { System.out.print(" (micromoon)"); } if (moonPhase.isSuperMoon()) { System.out.print(" (supermoon)"); } System.out.println(); date = nextFullMoon.plusDays(1); } } @Test public void testPositions() { SunPosition.Parameters sunParam = SunPosition.compute() .at(35.689722, 139.692222) // Tokyo .timezone("Asia/Tokyo") // local time .on(2018, 11, 13, 10, 3, 24); // 2018-11-13 10:03:24 MoonPosition.Parameters moonParam = MoonPosition.compute() .sameLocationAs(sunParam) .sameTimeAs(sunParam); SunPosition sun = sunParam.execute(); System.out.println(String.format( "The sun can be seen %.1f° clockwise from the North and " + "%.1f° above the horizon.\nIt is about %.0f km away right now.", sun.getAzimuth(), sun.getAltitude(), sun.getDistance() )); MoonPosition moon = moonParam.execute(); System.out.println(String.format( "The moon can be seen %.1f° clockwise from the North and " + "%.1f° above the horizon.\nIt is about %.0f km away right now.", moon.getAzimuth(), moon.getAltitude(), moon.getDistance() )); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/Locations.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import java.time.ZoneId; /** * Geocoordinates of some test locations. */ public final class Locations { /** * Cologne, Germany. A random city on the northern hemisphere. */ public static final double[] COLOGNE = new double[] { 50.938056, 6.956944 }; public static final ZoneId COLOGNE_TZ = ZoneId.of("Europe/Berlin"); /** * Alert, Nunavut, Canada. The northernmost place in the world with a permanent * population. */ public static final double[] ALERT = new double[] { 82.5, -62.316667 }; public static final ZoneId ALERT_TZ = ZoneId.of("Canada/Eastern"); /** * Wellington, New Zealand. A random city on the southern hemisphere, close to the * international date line. */ public static final double[] WELLINGTON = new double[] { -41.2875, 174.776111 }; public static final ZoneId WELLINGTON_TZ = ZoneId.of("Pacific/Auckland"); /** * Puerto Williams, Chile. The southernmost town in the world. */ public static final double[] PUERTO_WILLIAMS = new double[] { -54.933333, -67.616667 }; public static final ZoneId PUERTO_WILLIAMS_TZ = ZoneId.of("America/Punta_Arenas"); /** * Singapore. A random city close to the equator. */ public static final double[] SINGAPORE = new double[] { 1.283333, 103.833333 }; public static final ZoneId SINGAPORE_TZ = ZoneId.of("Asia/Singapore"); /** * Martinique. To test a fix for issue #13. */ public static final double[] MARTINIQUE = new double[] { 14.640725, -61.0112 }; public static final ZoneId MARTINIQUE_TZ = ZoneId.of("America/Martinique"); /** * Sydney. To test a fix for issue #14. */ public static final double[] SYDNEY = new double[] { -33.744272, 151.231291 }; public static final ZoneId SYDNEY_TZ = ZoneId.of("Australia/Sydney"); /** * Santa Monica, CA. To test a fix for issue #18. */ public static final double[] SANTA_MONICA = new double[] { 34.0, -118.5 }; public static final ZoneId SANTA_MONICA_TZ = ZoneId.of("America/Los_Angeles"); /** * Baghdad, Iraq. To test topocentric moon illumination. */ public static final double[] BAGHDAD = new double[] { 33.338611, 44.393888 }; public static final ZoneId BAGHDAD_TZ = ZoneId.of("Asia/Baghdad"); /** * Cape Town, South Africa. To test topocentric moon illumination. */ public static final double[] CAPETOWN = new double[] { -33.966666, 18.6 }; public static final ZoneId CAPETOWN_TZ = ZoneId.of("Africa/Johannesburg"); } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/MoonIlluminationTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static org.assertj.core.api.Assertions.assertThat; import static org.shredzone.commons.suncalc.Locations.*; import org.assertj.core.data.Offset; import org.junit.Test; import org.shredzone.commons.suncalc.MoonPhase.Phase; /** * Unit tests for {@link MoonIllumination}. */ public class MoonIlluminationTest { private static final Offset ERROR = Offset.offset(0.1); private static final Offset FINE_ERROR = Offset.offset(0.001); @Test public void testNewMoon() { MoonIllumination mi = MoonIllumination.compute() .on(2017, 6, 24, 4, 30, 0) .timezone(COLOGNE_TZ) .execute(); assertThat(mi.getFraction()).as("fraction").isCloseTo(0.0, ERROR); assertThat(mi.getPhase()).as("phase").isCloseTo(176.0, ERROR); // -180.0 assertThat(mi.getAngle()).as("angle").isCloseTo(2.0, ERROR); assertThat(mi.getClosestPhase()).as("MoonPhase.Phase").isEqualTo(Phase.NEW_MOON); } @Test public void testWaxingHalfMoon() { MoonIllumination mi = MoonIllumination.compute() .on(2017, 7, 1, 2, 51, 0) .timezone(COLOGNE_TZ) .execute(); assertThat(mi.getFraction()).as("fraction").isCloseTo(0.5, ERROR); assertThat(mi.getPhase()).as("phase").isCloseTo(-90.0, ERROR); assertThat(mi.getAngle()).as("angle").isCloseTo(-66.9, ERROR); assertThat(mi.getClosestPhase()).as("MoonPhase.Phase").isEqualTo(Phase.FIRST_QUARTER); } @Test public void testFullMoon() { MoonIllumination mi = MoonIllumination.compute() .on(2017, 7, 9, 6, 6, 0) .timezone(COLOGNE_TZ) .execute(); assertThat(mi.getFraction()).as("fraction").isCloseTo(1.0, ERROR); assertThat(mi.getPhase()).as("phase").isCloseTo(-3.2, ERROR); // 0.0 assertThat(mi.getAngle()).as("angle").isCloseTo(-7.4, ERROR); assertThat(mi.getClosestPhase()).as("MoonPhase.Phase").isEqualTo(Phase.FULL_MOON); } @Test public void testWaningHalfMoon() { MoonIllumination mi = MoonIllumination.compute() .on(2017, 7, 16, 21, 25, 0) .timezone(COLOGNE_TZ) .execute(); assertThat(mi.getFraction()).as("fraction").isCloseTo(0.5, ERROR); assertThat(mi.getPhase()).as("phase").isCloseTo(90.0, ERROR); assertThat(mi.getAngle()).as("angle").isCloseTo(68.7, ERROR); assertThat(mi.getClosestPhase()).as("MoonPhase.Phase").isEqualTo(Phase.LAST_QUARTER); } @Test public void testBaghdad1() { MoonIllumination tmi = MoonIllumination.compute() .on(2002, 12, 6, 17, 49, 0) .timezone(BAGHDAD_TZ) .at(BAGHDAD) .elevation(46.0) .execute(); assertThat(tmi.getElongation()).as("elongation").isCloseTo(29.8, ERROR); assertThat(tmi.getRadius()).as("moonRadius").isCloseTo(0.2661, FINE_ERROR); assertThat(tmi.getCrescentWidth()).as("crescentWidth").isCloseTo(0.036, FINE_ERROR); } @Test public void testBaghdad2() { MoonIllumination tmi = MoonIllumination.compute() .on(2002, 9, 8, 17, 49, 0) .timezone(BAGHDAD_TZ) .at(BAGHDAD) .elevation(46.0) .execute(); assertThat(tmi.getElongation()).as("elongation").isCloseTo(20.2, ERROR); assertThat(tmi.getRadius()).as("moonRadius").isCloseTo(0.278, FINE_ERROR); assertThat(tmi.getCrescentWidth()).as("crescentWidth").isCloseTo(0.017, FINE_ERROR); } @Test public void testCapeTown1() { MoonIllumination tmi = MoonIllumination.compute() .on(2010, 7, 13, 18, 51, 0) .timezone(CAPETOWN_TZ) .at(CAPETOWN) .execute(); assertThat(tmi.getElongation()).as("elongation").isCloseTo(25.3, ERROR); assertThat(tmi.getRadius()).as("moonRadius").isCloseTo(0.276, FINE_ERROR); assertThat(tmi.getCrescentWidth()).as("crescentWidth").isCloseTo(0.027, FINE_ERROR); } @Test public void testCapeTown2() { MoonIllumination tmi = MoonIllumination.compute() .on(2010, 2, 16, 20, 1, 0) .timezone(CAPETOWN_TZ) .at(CAPETOWN) .execute(); assertThat(tmi.getElongation()).as("elongation").isCloseTo(28.7, ERROR); assertThat(tmi.getRadius()).as("moonRadius").isCloseTo(0.248, FINE_ERROR); assertThat(tmi.getCrescentWidth()).as("crescentWidth").isCloseTo(0.031, FINE_ERROR); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/MoonPhaseTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2018 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static org.assertj.core.api.Assertions.assertThat; import java.time.temporal.ChronoUnit; import org.assertj.core.api.AbstractDateAssert; import org.assertj.core.data.Offset; import org.junit.BeforeClass; import org.junit.Test; import org.shredzone.commons.suncalc.MoonPhase.Phase; /** * Unit tests for {@link MoonPhase}. */ public class MoonPhaseTest { private static final Offset ERROR = Offset.offset(500.0); @BeforeClass public static void init() { AbstractDateAssert.registerCustomDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); } @Test public void testNewMoon() { MoonPhase mp = MoonPhase.compute() .on(2017, 9, 1) .utc() .phase(Phase.NEW_MOON) .execute(); assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS)) .isEqualTo("2017-09-20T05:29:30Z"); assertThat(mp.getDistance()).isCloseTo(382740.0, ERROR); } @Test public void testFirstQuarterMoon() { MoonPhase mp = MoonPhase.compute() .on(2017, 9, 1) .utc() .phase(Phase.FIRST_QUARTER) .execute(); assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS)) .isEqualTo("2017-09-28T02:52:40Z"); assertThat(mp.getDistance()).isCloseTo(403894.0, ERROR); } @Test public void testFullMoon() { MoonPhase mp = MoonPhase.compute() .on(2017, 9, 1) .utc() .phase(Phase.FULL_MOON) .execute(); assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS)) .isEqualTo("2017-09-06T07:07:44Z"); assertThat(mp.getDistance()).isCloseTo(384364.0, ERROR); } @Test public void testLastQuarterMoon() { MoonPhase mp = MoonPhase.compute() .on(2017, 9, 1) .utc() .phase(Phase.LAST_QUARTER) .execute(); assertThat(mp.getTime().truncatedTo(ChronoUnit.SECONDS)) .isEqualTo("2017-09-13T06:28:34Z"); assertThat(mp.getDistance()).isCloseTo(369899.0, ERROR); } @Test public void testToPhase() { // exact angles assertThat(Phase.toPhase( 0.0)).isEqualTo(Phase.NEW_MOON); assertThat(Phase.toPhase( 45.0)).isEqualTo(Phase.WAXING_CRESCENT); assertThat(Phase.toPhase( 90.0)).isEqualTo(Phase.FIRST_QUARTER); assertThat(Phase.toPhase(135.0)).isEqualTo(Phase.WAXING_GIBBOUS); assertThat(Phase.toPhase(180.0)).isEqualTo(Phase.FULL_MOON); assertThat(Phase.toPhase(225.0)).isEqualTo(Phase.WANING_GIBBOUS); assertThat(Phase.toPhase(270.0)).isEqualTo(Phase.LAST_QUARTER); assertThat(Phase.toPhase(315.0)).isEqualTo(Phase.WANING_CRESCENT); // out of range angles (normalization test) assertThat(Phase.toPhase( 360.0)).isEqualTo(Phase.NEW_MOON); assertThat(Phase.toPhase( 720.0)).isEqualTo(Phase.NEW_MOON); assertThat(Phase.toPhase(-360.0)).isEqualTo(Phase.NEW_MOON); assertThat(Phase.toPhase(-720.0)).isEqualTo(Phase.NEW_MOON); assertThat(Phase.toPhase( 855.0)).isEqualTo(Phase.WAXING_GIBBOUS); assertThat(Phase.toPhase(-585.0)).isEqualTo(Phase.WAXING_GIBBOUS); assertThat(Phase.toPhase(-945.0)).isEqualTo(Phase.WAXING_GIBBOUS); // close to boundary assertThat(Phase.toPhase( 22.4)).isEqualTo(Phase.NEW_MOON); assertThat(Phase.toPhase( 67.4)).isEqualTo(Phase.WAXING_CRESCENT); assertThat(Phase.toPhase(112.4)).isEqualTo(Phase.FIRST_QUARTER); assertThat(Phase.toPhase(157.4)).isEqualTo(Phase.WAXING_GIBBOUS); assertThat(Phase.toPhase(202.4)).isEqualTo(Phase.FULL_MOON); assertThat(Phase.toPhase(247.4)).isEqualTo(Phase.WANING_GIBBOUS); assertThat(Phase.toPhase(292.4)).isEqualTo(Phase.LAST_QUARTER); assertThat(Phase.toPhase(337.4)).isEqualTo(Phase.WANING_CRESCENT); assertThat(Phase.toPhase(382.4)).isEqualTo(Phase.NEW_MOON); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/MoonPositionTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static org.assertj.core.api.Assertions.assertThat; import static org.shredzone.commons.suncalc.Locations.*; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link MoonPosition}. */ public class MoonPositionTest { private static final Offset ERROR = Offset.offset(0.1); private static final Offset DISTANCE_ERROR = Offset.offset(800.0); @Test public void testCologne() { MoonPosition mp1 = MoonPosition.compute() .on(2017, 7, 12, 13, 28, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(mp1.getAzimuth()).as("azimuth").isCloseTo(304.8, ERROR); assertThat(mp1.getAltitude()).as("altitude").isCloseTo(-39.6, ERROR); assertThat(mp1.getTrueAltitude()).as("trueAltitude").isCloseTo(-39.6, ERROR); assertThat(mp1.getParallacticAngle()).as("pa").isCloseTo(32.0, ERROR); MoonPosition mp2 = MoonPosition.compute() .on(2017, 7, 12, 3, 51, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(mp2.getAzimuth()).as("azimuth").isCloseTo(179.9, ERROR); assertThat(mp2.getAltitude()).as("altitude").isCloseTo(25.3, ERROR); assertThat(mp2.getTrueAltitude()).as("trueAltitude").isCloseTo(25.3, ERROR); assertThat(mp2.getDistance()).as("distance").isCloseTo(394709.0, DISTANCE_ERROR); assertThat(mp2.getParallacticAngle()).as("pa").isCloseTo(0.0, ERROR); } @Test public void testAlert() { MoonPosition mp1 = MoonPosition.compute() .on(2017, 7, 12, 8, 4, 0) .at(ALERT) .timezone(ALERT_TZ) .execute(); assertThat(mp1.getAzimuth()).as("azimuth").isCloseTo(257.5, ERROR); assertThat(mp1.getAltitude()).as("altitude").isCloseTo(-10.9, ERROR); assertThat(mp1.getTrueAltitude()).as("trueAltitude").isCloseTo(-10.9, ERROR); assertThat(mp1.getParallacticAngle()).as("pa").isCloseTo(7.5, ERROR); MoonPosition mp2 = MoonPosition.compute() .on(2017, 7, 12, 2, 37, 0) .at(ALERT) .timezone(ALERT_TZ) .execute(); assertThat(mp2.getAzimuth()).as("azimuth").isCloseTo(179.8, ERROR); assertThat(mp2.getAltitude()).as("altitude").isCloseTo(-5.7, ERROR); assertThat(mp2.getTrueAltitude()).as("trueAltitude").isCloseTo(-5.7, ERROR); assertThat(mp2.getDistance()).as("distance").isCloseTo(393609.0, DISTANCE_ERROR); assertThat(mp2.getParallacticAngle()).as("pa").isCloseTo(0.0, ERROR); } @Test public void testWellington() { MoonPosition mp1 = MoonPosition.compute() .on(2017, 7, 12, 4, 7, 0) .at(WELLINGTON) .timezone(WELLINGTON_TZ) .execute(); assertThat(mp1.getAzimuth()).as("azimuth").isCloseTo(311.3, ERROR); assertThat(mp1.getAltitude()).as("altitude").isCloseTo(55.1, ERROR); assertThat(mp1.getTrueAltitude()).as("trueAltitude").isCloseTo(55.1, ERROR); assertThat(mp1.getParallacticAngle()).as("pa").isCloseTo(144.2, ERROR); MoonPosition mp2 = MoonPosition.compute() .on(2017, 7, 12, 2, 17, 0) .at(WELLINGTON) .timezone(WELLINGTON_TZ) .execute(); assertThat(mp2.getAzimuth()).as("azimuth").isCloseTo(0.5, ERROR); assertThat(mp2.getAltitude()).as("altitude").isCloseTo(63.9, ERROR); assertThat(mp2.getTrueAltitude()).as("trueAltitude").isCloseTo(63.9, ERROR); assertThat(mp2.getDistance()).as("distance").isCloseTo(396272.0, DISTANCE_ERROR); assertThat(mp2.getParallacticAngle()).as("pa").isCloseTo(-179.6, ERROR); } @Test public void testPuertoWilliams() { MoonPosition mp1 = MoonPosition.compute() .on(2017, 2, 7, 9, 44, 0) .at(PUERTO_WILLIAMS) .timezone(PUERTO_WILLIAMS_TZ) .execute(); assertThat(mp1.getAzimuth()).as("azimuth").isCloseTo(199.4, ERROR); assertThat(mp1.getAltitude()).as("altitude").isCloseTo(-52.7, ERROR); assertThat(mp1.getTrueAltitude()).as("trueAltitude").isCloseTo(-52.7, ERROR); assertThat(mp1.getParallacticAngle()).as("pa").isCloseTo(168.3, ERROR); MoonPosition mp2 = MoonPosition.compute() .on(2017, 2, 7, 23, 4, 0) .at(PUERTO_WILLIAMS) .timezone(PUERTO_WILLIAMS_TZ) .execute(); assertThat(mp2.getAzimuth()).as("azimuth").isCloseTo(0.1, ERROR); assertThat(mp2.getAltitude()).as("altitude").isCloseTo(16.3, ERROR); assertThat(mp2.getTrueAltitude()).as("trueAltitude").isCloseTo(16.3, ERROR); assertThat(mp2.getDistance()).as("distance").isCloseTo(369731.0, DISTANCE_ERROR); assertThat(mp2.getParallacticAngle()).as("pa").isCloseTo(-179.9, ERROR); } @Test public void testSingapore() { MoonPosition mp1 = MoonPosition.compute() .on(2017, 7, 12, 5, 12, 0) .at(SINGAPORE) .timezone(SINGAPORE_TZ) .execute(); assertThat(mp1.getAzimuth()).as("azimuth").isCloseTo(240.6, ERROR); assertThat(mp1.getAltitude()).as("altitude").isCloseTo(57.1, ERROR); assertThat(mp1.getTrueAltitude()).as("trueAltitude").isCloseTo(57.1, ERROR); assertThat(mp1.getParallacticAngle()).as("pa").isCloseTo(64.0, ERROR); MoonPosition mp2 = MoonPosition.compute() .on(2017, 7, 12, 3, 11, 0) .at(SINGAPORE) .timezone(SINGAPORE_TZ) .execute(); assertThat(mp2.getAzimuth()).as("azimuth").isCloseTo(180.0, ERROR); assertThat(mp2.getAltitude()).as("altitude").isCloseTo(74.1, ERROR); assertThat(mp2.getTrueAltitude()).as("trueAltitude").isCloseTo(74.1, ERROR); assertThat(mp2.getDistance()).as("distance").isCloseTo(395621.0, DISTANCE_ERROR); assertThat(mp2.getParallacticAngle()).as("pa").isCloseTo(0.0, ERROR); } @Test public void testBaghdad() { MoonPosition mp1 = MoonPosition.compute() .on(2002, 12, 4, 16, 57, 0) .latitude(33, 20, 0.0) .longitude(44, 25, 0.0) .elevationFt(100.0) .timezone("Asia/Baghdad") .execute(); assertThat(mp1.getAzimuth()).as("azimuth").isCloseTo(241.1, ERROR); assertThat(mp1.getAltitude()).as("altitude").isCloseTo(1.2, ERROR); assertThat(mp1.getTrueAltitude()).as("trueAltitude").isCloseTo(0.8, ERROR); assertThat(mp1.getParallacticAngle()).as("pa").isCloseTo(52.8, ERROR); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/MoonTimesTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static org.assertj.core.api.Assertions.assertThat; import static org.shredzone.commons.suncalc.Locations.*; import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import org.assertj.core.api.AbstractDateAssert; import org.junit.BeforeClass; import org.junit.Test; /** * Unit tests for {@link MoonTimes}. */ public class MoonTimesTest { @BeforeClass public static void init() { AbstractDateAssert.registerCustomDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); } @Test public void testCologne() { MoonTimes mt = MoonTimes.compute().on(2017, 7, 12).utc().at(COLOGNE).execute(); assertThat(mt.getRise()).as("rise").isEqualTo("2017-07-12T21:25:55Z"); assertThat(mt.getSet()).as("set").isEqualTo("2017-07-12T06:53:30Z"); assertThat(mt.isAlwaysUp()).as("alwaysup").isFalse(); assertThat(mt.isAlwaysDown()).as("alwaysdown").isFalse(); MoonTimes mtr = MoonTimes.compute().on(2017, 7, 12).utc().at(COLOGNE).reverse().execute(); assertThat(mtr.getRise()).as("rise").isEqualTo("2017-07-11T20:56:23Z"); assertThat(mtr.getSet()).as("set").isEqualTo("2017-07-11T05:49:00Z"); assertThat(mtr.isAlwaysUp()).as("alwaysup").isFalse(); assertThat(mtr.isAlwaysDown()).as("alwaysdown").isFalse(); } @Test public void testAlert() { MoonTimes mt1 = MoonTimes.compute().on(2017, 7, 12).utc().at(ALERT).oneDay().execute(); assertThat(mt1.isAlwaysUp()).as("alwaysup").isFalse(); assertThat(mt1.isAlwaysDown()).as("alwaysdown").isTrue(); MoonTimes mt2 = MoonTimes.compute().on(2017, 7, 12).utc().at(ALERT).execute(); assertThat(mt2.getRise()).as("rise").isEqualTo("2017-07-14T05:45:05Z"); assertThat(mt2.getSet()).as("set").isEqualTo("2017-07-14T11:26:43Z"); assertThat(mt2.isAlwaysUp()).as("alwaysup").isFalse(); assertThat(mt2.isAlwaysDown()).as("alwaysdown").isFalse(); MoonTimes mt3 = MoonTimes.compute().on(2017, 7, 14).utc().at(ALERT).limit(Duration.ofDays(1)).execute(); assertThat(mt3.getRise()).as("rise").isEqualTo("2017-07-14T05:45:05Z"); assertThat(mt3.getSet()).as("set").isEqualTo("2017-07-14T11:26:43Z"); assertThat(mt3.isAlwaysUp()).as("alwaysup").isFalse(); assertThat(mt3.isAlwaysDown()).as("alwaysdown").isFalse(); MoonTimes mt4 = MoonTimes.compute().on(2017, 7, 18).utc().at(ALERT).oneDay().execute(); assertThat(mt4.isAlwaysUp()).as("alwaysup").isTrue(); assertThat(mt4.isAlwaysDown()).as("alwaysdown").isFalse(); MoonTimes mt5 = MoonTimes.compute().on(2017, 7, 18).utc().at(ALERT).fullCycle().execute(); assertThat(mt5.getRise()).as("rise").isEqualTo("2017-07-27T11:59:20Z"); assertThat(mt5.getSet()).as("set").isEqualTo("2017-07-27T04:07:10Z"); assertThat(mt5.isAlwaysUp()).as("alwaysup").isFalse(); assertThat(mt5.isAlwaysDown()).as("alwaysdown").isFalse(); } @Test public void testWellington() { MoonTimes mt1 = MoonTimes.compute().on(2017, 7, 12).utc().at(WELLINGTON).execute(); assertThat(mt1.getRise()).as("rise").isEqualTo("2017-07-12T08:05:53Z"); assertThat(mt1.getSet()).as("set").isEqualTo("2017-07-12T21:57:38Z"); MoonTimes mt2 = MoonTimes.compute().on(2017, 7, 12).timezone("NZ").at(WELLINGTON).execute(); assertThat(mt2.getRise()).as("rise").isEqualTo("2017-07-12T20:05:53+12:00"); assertThat(mt2.getSet()).as("set").isEqualTo("2017-07-12T09:23:00+12:00"); } @Test public void testPuertoWilliams() { MoonTimes mt = MoonTimes.compute().on(2017, 7, 13).utc().at(PUERTO_WILLIAMS) .execute(); assertThat(mt.getRise()).as("rise").isEqualTo("2017-07-13T00:31:29Z"); assertThat(mt.getSet()).as("set").isEqualTo("2017-07-13T14:48:37Z"); } @Test public void testSingapore() { MoonTimes mt = MoonTimes.compute().on(2017, 7, 13).utc().at(SINGAPORE).execute(); assertThat(mt.getRise()).as("rise").isEqualTo("2017-07-13T14:35:09Z"); assertThat(mt.getSet()).as("set").isEqualTo("2017-07-13T02:08:57Z"); } @Test public void testSequence() { long acceptableError = 60 * 1000L; ZonedDateTime riseBefore = createDate(2017, 11, 25, 12, 0); ZonedDateTime riseAfter = createDate(2017, 11, 26, 12, 29); ZonedDateTime setBefore = createDate(2017, 11, 25, 21, 49); ZonedDateTime setAfter = createDate(2017, 11, 26, 22, 55); for (int hour = 0; hour < 24; hour++) { for (int minute = 0; minute < 60; minute++) { MoonTimes times = MoonTimes.compute() .at(COLOGNE) .on(2017, 11, 25, hour, minute, 0).utc() .fullCycle() .execute(); ZonedDateTime rise = times.getRise(); ZonedDateTime set = times.getSet(); assertThat(rise).isNotNull(); assertThat(set).isNotNull(); if (hour < 12 || (hour == 12 && minute == 0)) { long diff = Duration.between(rise, riseBefore).abs().toMillis(); assertThat(diff).as("rise @%02d:%02d", hour, minute).isLessThan(acceptableError); } else { long diff = Duration.between(rise, riseAfter).abs().toMillis(); assertThat(diff).as("rise @%02d:%02d", hour, minute).isLessThan(acceptableError); } if (hour < 21 || (hour == 21 && minute <= 49)) { long diff = Duration.between(set, setBefore).abs().toMillis(); assertThat(diff).as("set @%02d:%02d", hour, minute).isLessThan(acceptableError); } else { long diff = Duration.between(set, setAfter).abs().toMillis(); assertThat(diff).as("set @%02d:%02d", hour, minute).isLessThan(acceptableError); } } } } private ZonedDateTime createDate(int year, int month, int day, int hour, int minute) { return ZonedDateTime.of(year, month, day, hour, minute, 0, 0, ZoneId.of("UTC")); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/SunPositionTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static org.assertj.core.api.Assertions.assertThat; import static org.shredzone.commons.suncalc.Locations.*; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link SunPosition}. */ public class SunPositionTest { private static final Offset ERROR = Offset.offset(0.1); @Test public void testCologne() { SunPosition sp1 = SunPosition.compute() .on(2017, 7, 12, 16, 10, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(sp1.getAzimuth()).as("azimuth").isCloseTo(239.8, ERROR); assertThat(sp1.getAltitude()).as("altitude").isCloseTo(48.6, ERROR); assertThat(sp1.getTrueAltitude()).as("trueAltitude").isCloseTo(48.6, ERROR); SunPosition sp2 = SunPosition.compute() .on(2017, 7, 12, 13, 37, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(sp2.getAzimuth()).as("azimuth").isCloseTo(179.6, ERROR); assertThat(sp2.getAltitude()).as("altitude").isCloseTo(61.0, ERROR); assertThat(sp2.getTrueAltitude()).as("trueAltitude").isCloseTo(61.0, ERROR); } @Test public void testAlert() { SunPosition sp1 = SunPosition.compute() .on(2017, 7, 12, 6, 17, 0) .at(ALERT) .timezone(ALERT_TZ) .execute(); assertThat(sp1.getAzimuth()).as("azimuth").isCloseTo(87.5, ERROR); assertThat(sp1.getAltitude()).as("altitude").isCloseTo(21.8, ERROR); assertThat(sp1.getTrueAltitude()).as("trueAltitude").isCloseTo(21.8, ERROR); SunPosition sp2 = SunPosition.compute() .on(2017, 7, 12, 12, 14, 0) .at(ALERT) .timezone(ALERT_TZ) .execute(); assertThat(sp2.getAzimuth()).as("azimuth").isCloseTo(179.7, ERROR); assertThat(sp2.getAltitude()).as("altitude").isCloseTo(29.4, ERROR); assertThat(sp2.getTrueAltitude()).as("trueAltitude").isCloseTo(29.4, ERROR); } @Test public void testWellington() { SunPosition sp1 = SunPosition.compute() .on(2017, 7, 12, 3, 7, 0) .at(WELLINGTON) .timezone(WELLINGTON_TZ) .execute(); assertThat(sp1.getAzimuth()).as("azimuth").isCloseTo(107.3, ERROR); assertThat(sp1.getAltitude()).as("altitude").isCloseTo(-51.3, ERROR); assertThat(sp1.getTrueAltitude()).as("trueAltitude").isCloseTo(-51.3, ERROR); SunPosition sp2 = SunPosition.compute() .on(2017, 7, 12, 12, 26, 0) .at(WELLINGTON) .timezone(WELLINGTON_TZ) .execute(); assertThat(sp2.getAzimuth()).as("azimuth").isCloseTo(0.1, ERROR); assertThat(sp2.getAltitude()).as("altitude").isCloseTo(26.8, ERROR); assertThat(sp2.getTrueAltitude()).as("trueAltitude").isCloseTo(26.8, ERROR); SunPosition sp3 = SunPosition.compute() .on(2017, 7, 12, 7, 50, 0) .at(WELLINGTON) .timezone(WELLINGTON_TZ) .execute(); assertThat(sp3.getAzimuth()).as("azimuth").isCloseTo(60.0, ERROR); assertThat(sp3.getAltitude()).as("altitude").isCloseTo(0.6, ERROR); assertThat(sp3.getTrueAltitude()).as("trueAltitude").isCloseTo(0.1, ERROR); } @Test public void testPuertoWilliams() { SunPosition sp1 = SunPosition.compute() .on(2017, 2, 7, 18, 13, 0) .at(PUERTO_WILLIAMS) .timezone(PUERTO_WILLIAMS_TZ) .execute(); assertThat(sp1.getAzimuth()).as("azimuth").isCloseTo(280.1, ERROR); assertThat(sp1.getAltitude()).as("altitude").isCloseTo(25.4, ERROR); assertThat(sp1.getTrueAltitude()).as("trueAltitude").isCloseTo(25.4, ERROR); SunPosition sp2 = SunPosition.compute() .on(2017, 2, 7, 13, 44, 0) .at(PUERTO_WILLIAMS) .timezone(PUERTO_WILLIAMS_TZ) .execute(); assertThat(sp2.getAzimuth()).as("azimuth").isCloseTo(0.2, ERROR); assertThat(sp2.getAltitude()).as("altitude").isCloseTo(50.2, ERROR); assertThat(sp2.getTrueAltitude()).as("trueAltitude").isCloseTo(50.2, ERROR); } @Test public void testSingapore() { SunPosition sp1 = SunPosition.compute() .on(2017, 7, 12, 10, 19, 0) .at(SINGAPORE) .timezone(SINGAPORE_TZ) .execute(); assertThat(sp1.getAzimuth()).as("azimuth").isCloseTo(60.4, ERROR); assertThat(sp1.getAltitude()).as("altitude").isCloseTo(43.5, ERROR); assertThat(sp1.getTrueAltitude()).as("trueAltitude").isCloseTo(43.5, ERROR); SunPosition sp2 = SunPosition.compute() .on(2017, 7, 12, 13, 10, 0) .at(SINGAPORE) .timezone(SINGAPORE_TZ) .execute(); assertThat(sp2.getAzimuth()).as("azimuth").isCloseTo(0.2, ERROR); assertThat(sp2.getAltitude()).as("altitude").isCloseTo(69.4, ERROR); assertThat(sp2.getTrueAltitude()).as("trueAltitude").isCloseTo(69.4, ERROR); } @Test public void testDistance() { SunPosition sp1 = SunPosition.compute() .on(2017, 1, 4, 12, 37, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(sp1.getDistance()).as("distance").isCloseTo(147097390.6, ERROR); SunPosition sp2 = SunPosition.compute() .on(2017, 4, 20, 13, 31, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(sp2.getDistance()).as("distance").isCloseTo(150181373.3, ERROR); SunPosition sp3 = SunPosition.compute() .on(2017, 7, 12, 13, 37, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(sp3.getDistance()).as("distance").isCloseTo(152088309.0, ERROR); SunPosition sp4 = SunPosition.compute() .on(2017, 10, 11, 13, 18, 0) .at(COLOGNE) .timezone(COLOGNE_TZ) .execute(); assertThat(sp4.getDistance()).as("distance").isCloseTo(149380680.0, ERROR); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/SunTimesTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc; import static java.lang.Math.abs; import static org.assertj.core.api.Assertions.assertThat; import static org.shredzone.commons.suncalc.Locations.*; import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.EnumMap; import java.util.Map; import org.assertj.core.api.AbstractDateAssert; import org.junit.BeforeClass; import org.junit.Test; import org.shredzone.commons.suncalc.SunTimes.Twilight; /** * Unit tests for {@link SunTimes}. */ public class SunTimesTest { @BeforeClass public static void init() { AbstractDateAssert.registerCustomDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); } @Test public void testCologne() { Map riseTimes = new EnumMap<>(Twilight.class); riseTimes.put(Twilight.ASTRONOMICAL, "2017-08-10T01:44:18Z"); riseTimes.put(Twilight.NAUTICAL, "2017-08-10T02:44:57Z"); riseTimes.put(Twilight.NIGHT_HOUR, "2017-08-10T03:18:22Z"); riseTimes.put(Twilight.CIVIL, "2017-08-10T03:34:01Z"); riseTimes.put(Twilight.BLUE_HOUR, "2017-08-10T03:48:59Z"); riseTimes.put(Twilight.VISUAL, "2017-08-10T04:11:49Z"); riseTimes.put(Twilight.VISUAL_LOWER, "2017-08-10T04:15:33Z"); riseTimes.put(Twilight.HORIZON, "2017-08-10T04:17:44Z"); riseTimes.put(Twilight.GOLDEN_HOUR, "2017-08-10T04:58:33Z"); Map setTimes = new EnumMap<>(Twilight.class); setTimes.put(Twilight.GOLDEN_HOUR, "2017-08-10T18:15:49Z"); setTimes.put(Twilight.HORIZON, "2017-08-10T18:56:30Z"); setTimes.put(Twilight.VISUAL_LOWER, "2017-08-10T18:58:39Z"); setTimes.put(Twilight.VISUAL, "2017-08-10T19:02:20Z"); setTimes.put(Twilight.BLUE_HOUR, "2017-08-10T19:25:16Z"); setTimes.put(Twilight.CIVIL, "2017-08-10T19:40:13Z"); setTimes.put(Twilight.NIGHT_HOUR, "2017-08-10T19:55:35Z"); setTimes.put(Twilight.NAUTICAL, "2017-08-10T20:28:56Z"); setTimes.put(Twilight.ASTRONOMICAL, "2017-08-10T21:28:43Z"); for (Twilight angle : Twilight.values()) { SunTimes times = SunTimes.compute().at(COLOGNE).on(2017, 8, 10).utc() .twilight(angle) .execute(); assertThat(times.getRise()).as("%s-rise", angle.name()).isEqualTo(riseTimes.get(angle)); assertThat(times.getSet()).as("%s-set", angle.name()).isEqualTo(setTimes.get(angle)); assertThat(times.getNoon()).as("%s-noon", angle.name()).isEqualTo("2017-08-10T11:37:22Z"); assertThat(times.getNadir()).as("%s-nadir", angle.name()).isEqualTo("2017-08-10T23:37:45Z"); assertThat(times.isAlwaysDown()).as("%s-always-down", angle.name()).isFalse(); assertThat(times.isAlwaysUp()).as("%s-always-up", angle.name()).isFalse(); } SunTimes times = SunTimes.compute().at(COLOGNE).on(2017, 8, 10).utc() .twilight(-4.0) .execute(); assertThat(times.getRise()).as("rise").isEqualTo("2017-08-10T03:48:59Z"); assertThat(times.getSet()).as("set").isEqualTo("2017-08-10T19:25:16Z"); assertThat(times.getNoon()).as("noon").isEqualTo("2017-08-10T11:37:22Z"); assertThat(times.getNadir()).as("nadir").isEqualTo("2017-08-10T23:37:45Z"); assertThat(times.isAlwaysDown()).as("always-down").isFalse(); assertThat(times.isAlwaysUp()).as("always-up").isFalse(); } @Test public void testAlert() { SunTimes t1 = SunTimes.compute().at(ALERT).on(2017, 8, 10).utc() .oneDay() .execute(); assertTimes(t1, null, null, "2017-08-10T16:13:14Z", true); SunTimes t2 = SunTimes.compute().at(ALERT).on(2017, 9, 24).utc() .execute(); assertTimes(t2, "2017-09-24T09:54:29Z", "2017-09-24T22:02:01Z", "2017-09-24T15:59:16Z"); SunTimes t3 = SunTimes.compute().at(ALERT).on(2017, 2, 10).utc() .oneDay() .execute(); assertTimes(t3, null, null, "2017-02-10T16:25:09Z", false); SunTimes t4 = SunTimes.compute().at(ALERT).on(2017, 8, 10).utc() .execute(); assertTimes(t4, "2017-09-06T05:13:15Z", "2017-09-06T03:06:02Z", "2017-08-10T16:13:14Z"); SunTimes t5 = SunTimes.compute().at(ALERT).on(2017, 2, 10).utc() .execute(); assertTimes(t5, "2017-02-27T15:24:18Z", "2017-02-27T17:23:46Z", "2017-02-10T16:25:09Z"); SunTimes t6 = SunTimes.compute().at(ALERT).on(2017, 9, 6).utc() .execute(); assertTimes(t6, "2017-09-06T05:13:15Z", "2017-09-06T03:06:02Z", "2017-09-06T16:05:41Z"); // Summer solstice is the worst case for noon calculation SunTimes t7 = SunTimes.compute().at(ALERT).on(2020, 6, 20).utc() .limit(Duration.ofDays(2L)) .execute(); assertTimes(t7, null, null, "2020-06-20T16:11:02Z", true); // Reverse check SunTimes t8 = SunTimes.compute().at(ALERT).on(2017, 2, 28).utc() .reverse() .execute(); assertTimes(t8, "2017-02-27T15:23:52Z", "2017-02-27T17:23:14Z", "2017-02-27T16:23:47Z"); } @Test public void testWellington() { SunTimes t1 = SunTimes.compute().at(WELLINGTON).on(2017, 8, 10).timezone(WELLINGTON_TZ) .execute(); assertTimes(t1, "2017-08-09T19:18:33Z", "2017-08-10T05:34:50Z", "2017-08-10T00:26:33Z"); } @Test public void testWellingtonReverse() { SunTimes t1 = SunTimes.compute().at(WELLINGTON).on(2017, 8, 10).timezone(WELLINGTON_TZ) .reverse() .execute(); assertTimes(t1, "2017-08-08T19:19:35Z", "2017-08-09T05:33:36Z", "2017-08-09T00:26:42Z"); } @Test public void testPuertoWilliams() { SunTimes t1 = SunTimes.compute().at(PUERTO_WILLIAMS).on(2017, 8, 10).timezone(PUERTO_WILLIAMS_TZ) .execute(); assertTimes(t1, "2017-08-10T12:01:51Z", "2017-08-10T21:10:36Z", "2017-08-10T16:36:07Z"); } @Test public void testSingapore() { SunTimes t1 = SunTimes.compute().at(SINGAPORE).on(2017, 8, 10).timezone(SINGAPORE_TZ) .execute(); assertTimes(t1, "2017-08-09T23:05:13Z", "2017-08-10T11:14:56Z", "2017-08-10T05:10:07Z"); } @Test public void testMartinique() { SunTimes t1 = SunTimes.compute().at(MARTINIQUE).on(2019, 7, 1).timezone(MARTINIQUE_TZ) .execute(); assertTimes(t1, "2019-07-01T09:38:35Z", "2019-07-01T22:37:23Z", "2019-07-01T16:07:57Z"); } @Test public void testSydney() { SunTimes t1 = SunTimes.compute().at(SYDNEY).on(2019, 7, 3).timezone(SYDNEY_TZ) .execute(); assertTimes(t1, "2019-07-02T21:00:35Z", "2019-07-03T06:58:02Z", "2019-07-03T01:59:18Z"); } @Test public void testElevation() { // At the top of the Tokyo Skytree SunTimes skytree = SunTimes.compute().at(35.710046, 139.810718).on(2020, 6, 25).timezone("Asia/Tokyo") .elevation(634.0).execute(); assertTimes(skytree, "2020-06-24T19:21:46Z", "2020-06-25T10:05:17Z", "2020-06-25T02:43:28Z"); // In an airplane at 38,000 feet SunTimes airplane = SunTimes.compute().at(46.58, -6.3).on(2020, 6, 25).utc() .elevation(11582.4).execute(); assertTimes(airplane, "2020-06-25T04:07:33Z", "2020-06-25T20:48:32Z", "2020-06-25T12:28:00Z"); } @Test public void testJustBeforeJustAfter() { // Thanks to @isomeme for providing the test cases for issue #18. long shortDuration = 2; long longDuration = 30; SunTimes.Parameters param = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ) .on(2020, 5, 3); ZonedDateTime noon = param.execute().getNoon(); ZonedDateTime noonNextDay = param.plusDays(1).execute().getNoon(); long acceptableError = 65 * 1000L; ZonedDateTime wellBeforeNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ) .on(noon.minusMinutes(longDuration)) .execute().getNoon(); assertThat(Duration.between(wellBeforeNoon, noon).abs().toMillis()) .as("wellBeforeNoon").isLessThan(acceptableError); ZonedDateTime justBeforeNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ) .on(noon.minusMinutes(shortDuration)) .execute().getNoon(); assertThat(Duration.between(justBeforeNoon, noon).abs().toMillis()) .as("justBeforeNoon").isLessThan(acceptableError); ZonedDateTime justAfterNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ) .on(noon.plusMinutes(shortDuration)) .execute().getNoon(); assertThat(Duration.between(justAfterNoon, noonNextDay).abs().toMillis()) .as("justAfterNoon").isLessThan(acceptableError); ZonedDateTime wellAfterNoon = SunTimes.compute().at(SANTA_MONICA).timezone(SANTA_MONICA_TZ) .on(noon.plusMinutes(longDuration)) .execute().getNoon(); assertThat(Duration.between(wellAfterNoon, noonNextDay).abs().toMillis()) .as("wellAfterNoon").isLessThan(acceptableError); ZonedDateTime nadirWellAfterNoon = SunTimes.compute().on(wellAfterNoon).timezone(SANTA_MONICA_TZ) .at(SANTA_MONICA).execute().getNadir(); ZonedDateTime nadirJustBeforeNadir = SunTimes.compute() .on(nadirWellAfterNoon.minusMinutes(shortDuration)) .at(SANTA_MONICA).timezone(SANTA_MONICA_TZ).execute().getNadir(); assertThat(Duration.between(nadirWellAfterNoon, nadirJustBeforeNadir).abs().toMillis()) .as("nadir").isLessThan(acceptableError); } @Test public void testNoonNadirAzimuth() { // Thanks to @isomeme for providing the test cases for issue #20. assertNoonNadirPrecision(ZonedDateTime.of(2020, 6, 2, 3, 30, 0, 0, SANTA_MONICA_TZ), SANTA_MONICA); assertNoonNadirPrecision(ZonedDateTime.of(2020, 6, 16, 4, 11, 0, 0, SANTA_MONICA_TZ), SANTA_MONICA); } @Test public void testSequence() { long acceptableError = 62 * 1000L; ZonedDateTime riseBefore = createDate(2017, 11, 25, 7, 4); ZonedDateTime riseAfter = createDate(2017, 11, 26, 7, 6); ZonedDateTime setBefore = createDate(2017, 11, 25, 15, 33); ZonedDateTime setAfter = createDate(2017, 11, 26, 15, 32); for (int hour = 0; hour < 24; hour++) { for (int minute = 0; minute < 60; minute++) { SunTimes times = SunTimes.compute() .at(COLOGNE) .on(2017, 11, 25, hour, minute, 0).utc() .fullCycle() .execute(); ZonedDateTime rise = times.getRise(); ZonedDateTime set = times.getSet(); assertThat(rise).isNotNull(); assertThat(set).isNotNull(); if (hour < 7 || (hour == 7 && minute <= 4)) { long diff = Duration.between(rise, riseBefore).abs().toMillis(); assertThat(diff).as("rise @%02d:%02d", hour, minute).isLessThan(acceptableError); } else { long diff = Duration.between(rise, riseAfter).abs().toMillis(); assertThat(diff).as("rise @%02d:%02d", hour, minute).isLessThan(acceptableError); } if (hour < 15 || (hour == 15 && minute <= 33)) { long diff = Duration.between(set, setBefore).abs().toMillis(); assertThat(diff).as("set @%02d:%02d", hour, minute).isLessThan(acceptableError); } else { long diff = Duration.between(set, setAfter).abs().toMillis(); assertThat(diff).as("set @%02d:%02d", hour, minute).isLessThan(acceptableError); } } } } private ZonedDateTime createDate(int year, int month, int day, int hour, int minute) { return ZonedDateTime.of(year, month, day, hour, minute, 0, 0, ZoneId.of("UTC")); } private void assertNoonNadirPrecision(ZonedDateTime time, double[] location) { SunTimes sunTimes = SunTimes.compute() .at(location) .on(time) .execute(); SunPosition sunPositionAtNoon = SunPosition.compute() .at(location) .on(sunTimes.getNoon()) .execute(); SunPosition sunPositionAtNadir = SunPosition.compute() .at(location) .on(sunTimes.getNadir()) .execute(); assertThat(abs(sunPositionAtNoon.getAzimuth() - 180.0)).isLessThan(0.1); assertThat(abs(sunPositionAtNadir.getAzimuth() - 360.0)).isLessThan(0.1); } private void assertTimes(SunTimes t, String rise, String set, String noon) { assertTimes(t, rise, set, noon, null); } private void assertTimes(SunTimes t, String rise, String set, String noon, Boolean alwaysUp) { if (rise != null) { assertThat(t.getRise()).as("sunrise").isEqualTo(rise); } else { assertThat(t.getRise()).as("sunrise").isNull(); } if (set != null) { assertThat(t.getSet()).as("sunset").isEqualTo(set); } else { assertThat(t.getSet()).as("sunset").isNull(); } assertThat(t.getNoon()).as("noon").isEqualTo(noon); if (alwaysUp != null) { assertThat(t.isAlwaysDown()).as("always-down").isNotEqualTo(alwaysUp); assertThat(t.isAlwaysUp()).as("always-up").isEqualTo(alwaysUp); } else { assertThat(t.isAlwaysDown()).as("always-down").isFalse(); assertThat(t.isAlwaysUp()).as("always-up").isFalse(); } } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/BaseBuilderTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.toRadians; import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.TimeZone; import org.assertj.core.api.AbstractDateAssert; import org.assertj.core.data.Offset; import org.junit.BeforeClass; import org.junit.Test; import org.shredzone.commons.suncalc.Locations; /** * Unit tests for {@link BaseBuilder}. * * @author Richard "Shred" Körber */ public class BaseBuilderTest { private static final Offset ERROR = Offset.offset(0.001); private static final ZonedDateTime NOW = ZonedDateTime.now(); @BeforeClass public static void init() { AbstractDateAssert.registerCustomDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); } @Test public void testLocationParameters() { TestBuilder p = new TestBuilder(); TestBuilder r; assertThatIllegalStateException().isThrownBy(p::getLatitude); assertThatIllegalStateException().isThrownBy(p::getLongitude); assertThatNoException().isThrownBy(p::getElevation); assertThat(p.hasLocation()).isFalse(); p.latitude(0.0); assertThatNoException().isThrownBy(p::getLatitude); assertThatIllegalStateException().isThrownBy(p::getLongitude); assertThat(p.hasLocation()).isFalse(); p.longitude(0.0); assertThatNoException().isThrownBy(p::getLatitude); assertThatNoException().isThrownBy(p::getLongitude); assertThat(p.hasLocation()).isTrue(); p.clearLocation(); assertThatIllegalStateException().isThrownBy(p::getLatitude); assertThatIllegalStateException().isThrownBy(p::getLongitude); assertThat(p.hasLocation()).isFalse(); r = p.at(12.34, 34.56); assertLatLng(p, 12.34, 34.56, 0.0); assertThat(r).isSameAs(p); r = p.at(new double[] { 13.43, 51.23 }); assertLatLng(p, 13.43, 51.23, 0.0); assertThat(r).isSameAs(p); r = p.latitude(-11.22); assertLatLng(p, -11.22, 51.23, 0.0); assertThat(r).isSameAs(p); r = p.longitude(-8.23); assertLatLng(p, -11.22, -8.23, 0.0); assertThat(r).isSameAs(p); r = p.latitude(5, 7, 37.2); assertLatLng(p, 5.127, -8.23, 0.0); assertThat(r).isSameAs(p); r = p.longitude(-12, 43, 22.8); assertLatLng(p, 5.127, -12.723, 0.0); assertThat(r).isSameAs(p); r = p.elevation(18267.3); assertLatLng(p, 5.127, -12.723, 18267.3); assertThat(r).isSameAs(p); // Negative elevations are always changed to 0.0 r = p.elevation(-10.2); assertLatLng(p, 5.127, -12.723, 0.0); assertThat(r).isSameAs(p); r = p.elevationFt(12000.0); assertLatLng(p, 5.127, -12.723, 3657.6); assertThat(r).isSameAs(p); r = p.at(new double[] { 1.22, -3.44, 323.0 }); assertLatLng(p, 1.22, -3.44, 323.0); assertThat(r).isSameAs(p); TestBuilder s = new TestBuilder(); s.sameLocationAs(p); assertLatLng(s, 1.22, -3.44, 323.0); } @Test public void testBadLocations() { TestBuilder p = new TestBuilder(); // At least two array records are required assertThatIllegalArgumentException() .isThrownBy(() -> p.at(new double[] { 12.0 })); // No more than three array records are permitted assertThatIllegalArgumentException() .isThrownBy(() -> p.at(new double[] { 12.0, 34.0, 56.0, 78.0 })); // Latitude out of range (negative) assertThatIllegalArgumentException() .isThrownBy(() -> p.latitude(-90.1)); // Latitude out of range (positive) assertThatIllegalArgumentException() .isThrownBy(() -> p.latitude(90.1)); // Longitude out of range (negative) assertThatIllegalArgumentException() .isThrownBy(() -> p.longitude(-180.1)); // Longitude out of range (positive) assertThatIllegalArgumentException() .isThrownBy(() -> p.longitude(180.1)); } @Test public void testTimeParameters() { TestBuilder p = new TestBuilder(); TestBuilder r; assertThat(p.getJulianDate().getDateTime()).isNotNull(); p.on(NOW); assertDate(p, NOW.getYear(), NOW.getMonthValue(), NOW.getDayOfMonth(), NOW.getHour(), NOW.getMinute(), NOW.getSecond(), NOW.getZone()); r = p.on(2017, 8, 12); assertDate(p, 2017, 8, 12, 0, 0, 0, ZoneId.systemDefault()); assertThat(r).isSameAs(p); r = p.on(2016, 4, 10, 14, 11, 59); assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.systemDefault()); assertThat(r).isSameAs(p); r = p.timezone("Europe/Berlin"); assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of("Europe/Berlin")); assertThat(r).isSameAs(p); r = p.timezone(ZoneId.of("Asia/Tokyo")); assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of("Asia/Tokyo")); assertThat(r).isSameAs(p); r = p.timezone(TimeZone.getTimeZone("America/New_York")); assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of("America/New_York")); assertThat(r).isSameAs(p); r = p.utc(); assertDate(p, 2016, 4, 10, 14, 11, 59, ZoneId.of("UTC")); assertThat(r).isSameAs(p); r = p.on(LocalDate.of(2020, 3, 12)); assertDate(p, 2020, 3, 12, 0, 0, 0, ZoneId.of("UTC")); assertThat(r).isSameAs(p); r = p.on(ZonedDateTime.of(2020, 7, 11, 2, 44, 12, 0, ZoneId.of("UTC")).toInstant()); assertDate(p, 2020, 7, 11, 2, 44, 12, ZoneId.of("UTC")); assertThat(r).isSameAs(p); r = p.on(LocalDateTime.of(2020, 4, 1, 12, 45, 33)); assertDate(p, 2020, 4, 1, 12, 45, 33, ZoneId.of("UTC")); assertThat(r).isSameAs(p); Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Europe/Berlin")); cal.set(2018, Calendar.AUGUST, 12, 3, 23, 11); r = p.on(cal); assertDate(p, 2018, 8, 12, 3, 23, 11, ZoneId.of("Europe/Berlin")); assertThat(r).isSameAs(p); cal.set(Calendar.YEAR, 2019); r = p.on(cal.getTime()); assertDate(p, 2019, 8, 12, 3, 23, 11, ZoneId.of("Europe/Berlin")); assertThat(r).isSameAs(p); r = p.localTime(); assertDate(p, 2019, 8, 12, 3, 23, 11, ZoneId.systemDefault()); assertThat(r).isSameAs(p); r = p.on(2012, 3, 11, 8, 1, 12).midnight(); assertDate(p, 2012, 3, 11, 0, 0, 0, ZoneId.systemDefault()); assertThat(r).isSameAs(p); r = p.on(2012, 1, 24, 1, 33, 12).plusDays(100); assertDate(p, 2012, 5, 3, 1, 33, 12, ZoneId.systemDefault()); assertThat(r).isSameAs(p); r = p.on(2000, 1, 1, 2, 3, 4).now(); assertDate(p, NOW.getYear(), NOW.getMonthValue(), NOW.getDayOfMonth(), NOW.getHour(), NOW.getMinute(), NOW.getSecond(), NOW.getZone()); assertThat(r).isSameAs(p); r = p.on(2000, 2, 2, 3, 4, 5).today(); assertDate(p, NOW.getYear(), NOW.getMonthValue(), NOW.getDayOfMonth(), 0, 0, 0, NOW.getZone()); assertThat(r).isSameAs(p); r = p.on(2000, 2, 2, 3, 4, 5).tomorrow(); ZonedDateTime tomorrow = NOW.plusDays(1); assertDate(p, tomorrow.getYear(), tomorrow.getMonthValue(), tomorrow.getDayOfMonth(), 0, 0, 0, tomorrow.getZone()); assertThat(r).isSameAs(p); r = p.on(2000, 3, 3, 4, 5, 6).on(NOW); assertDate(p, NOW.getYear(), NOW.getMonthValue(), NOW.getDayOfMonth(), NOW.getHour(), NOW.getMinute(), NOW.getSecond(), NOW.getZone()); assertThat(r).isSameAs(p); TestBuilder s = new TestBuilder(); s.sameTimeAs(p); assertDate(s, NOW.getYear(), NOW.getMonthValue(), NOW.getDayOfMonth(), NOW.getHour(), NOW.getMinute(), NOW.getSecond(), NOW.getZone()); } @Test public void testWindowParameters() { TestBuilder p = new TestBuilder(); TestBuilder r; assertThat(p.getDuration()).isNotNull().isEqualTo(Duration.ofDays(365L)); r = p.oneDay(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(1L)); assertThat(r).isSameAs(p); r = p.fullCycle(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(365L)); assertThat(r).isSameAs(p); r = p.limit(Duration.ofDays(14L).negated()); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L).negated()); assertThat(r).isSameAs(p); r = p.forward(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L)); assertThat(r).isSameAs(p); // Second forward won't negate again! r = p.forward(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L)); assertThat(r).isSameAs(p); r = p.reverse(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L).negated()); assertThat(r).isSameAs(p); // Second reverse won't negate again! r = p.reverse(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L).negated()); assertThat(r).isSameAs(p); r = p.forward(); assertThat(p.getDuration()).isEqualTo(Duration.ofDays(14L)); assertThat(r).isSameAs(p); r = p.limit(Duration.ofHours(12L)); assertThat(p.getDuration()).isEqualTo(Duration.ofHours(12L)); assertThat(r).isSameAs(p); TestBuilder s = new TestBuilder(); s.sameWindowAs(p); assertThat(s.getDuration()).isEqualTo(Duration.ofHours(12L)); p.reverse(); TestBuilder s2 = new TestBuilder(); s2.sameWindowAs(p); assertThat(s2.getDuration()).isEqualTo(Duration.ofHours(12L).negated()); assertThatNullPointerException().isThrownBy(() -> { p.limit(null); }); } @Test public void testCopy() { ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime tomorrow = now.plusDays(1); ZonedDateTime yesterday = now.minusDays(1); Duration duration = Duration.ofDays(60L); // Set test parameters TestBuilder p1 = new TestBuilder(); p1.at(Locations.COLOGNE); p1.on(now); p1.elevation(123.0); p1.limit(duration); // Make sure copy has identical values TestBuilder p2 = p1.copy(); assertThat(p2.getLatitude()).isEqualTo(Locations.COLOGNE[0]); assertThat(p2.getLongitude()).isEqualTo(Locations.COLOGNE[1]); assertThat(p2.getJulianDate().getDateTime()).isEqualTo(now); assertThat(p2.getElevation()).isEqualTo(123.0); assertThat(p2.getDuration()).isEqualTo(Duration.ofDays(60L)); // Make sure changes to p1 won't affect p2 p1.at(Locations.SINGAPORE); p1.on(tomorrow); p1.oneDay(); assertThat(p1.getLatitude()).isEqualTo(Locations.SINGAPORE[0]); assertThat(p1.getLongitude()).isEqualTo(Locations.SINGAPORE[1]); assertThat(p1.getJulianDate().getDateTime()).isEqualTo(tomorrow); assertThat(p1.getDuration()).isEqualTo(Duration.ofDays(1L)); assertThat(p2.getLatitude()).isEqualTo(Locations.COLOGNE[0]); assertThat(p2.getLongitude()).isEqualTo(Locations.COLOGNE[1]); assertThat(p2.getJulianDate().getDateTime()).isEqualTo(now); assertThat(p2.getDuration()).isEqualTo(Duration.ofDays(60L)); // Make sure changes to p2 won't affect p1 p2.at(Locations.WELLINGTON); p2.on(yesterday); p2.fullCycle(); assertThat(p1.getLatitude()).isEqualTo(Locations.SINGAPORE[0]); assertThat(p1.getLongitude()).isEqualTo(Locations.SINGAPORE[1]); assertThat(p1.getJulianDate().getDateTime()).isEqualTo(tomorrow); assertThat(p1.getDuration()).isEqualTo(Duration.ofDays(1L)); assertThat(p2.getLatitude()).isEqualTo(Locations.WELLINGTON[0]); assertThat(p2.getLongitude()).isEqualTo(Locations.WELLINGTON[1]); assertThat(p2.getJulianDate().getDateTime()).isEqualTo(yesterday); assertThat(p2.getDuration()).isEqualTo(Duration.ofDays(365L)); } private void assertLatLng(TestBuilder p, double lat, double lng, double elev) { assertThat(p.getLatitude()).as("latitude").isCloseTo(lat, ERROR); assertThat(p.getLongitude()).as("longitude").isCloseTo(lng, ERROR); assertThat(p.getLatitudeRad()).as("latitude-rad").isCloseTo(toRadians(lat), ERROR); assertThat(p.getLongitudeRad()).as("longitude-rad").isCloseTo(toRadians(lng), ERROR); assertThat(p.getElevation()).as("elevation").isCloseTo(elev, ERROR); } private void assertDate(TestBuilder p, int year, int month, int day, int hour, int minute, int second, ZoneId tz) { ZonedDateTime cal = p.getJulianDate().getDateTime(); assertThat(cal.getYear()).as("year").isEqualTo(year); assertThat(cal.getMonthValue()).as("month").isEqualTo(month); assertThat(cal.getDayOfMonth()).as("day").isEqualTo(day); assertThat(cal.getHour()).as("hour").isEqualTo(hour); assertThat(cal.getMinute()).as("minute").isEqualTo(minute); assertThat(cal.getSecond()).as("second").isEqualTo(second); assertThat(cal.getZone()).as("timezone").isEqualTo(tz); } private static class TestBuilder extends BaseBuilder { @Override public TestBuilder now() { return on(NOW); } } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/ExtendedMathTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static org.assertj.core.api.Assertions.assertThat; import static org.shredzone.commons.suncalc.util.ExtendedMath.*; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link ExtendedMath}. */ public class ExtendedMathTest { private static final Offset ERROR = Offset.offset(0.001); @Test public void testFrac() { assertThat(frac( 1.0 )).isCloseTo( 0.0 , ERROR); assertThat(frac( 0.5 )).isCloseTo( 0.5 , ERROR); assertThat(frac( 123.25)).isCloseTo( 0.25, ERROR); assertThat(frac( 0.0 )).isCloseTo( 0.0 , ERROR); assertThat(frac( -1.0 )).isCloseTo( 0.0 , ERROR); assertThat(frac( -0.5 )).isCloseTo(-0.5 , ERROR); assertThat(frac(-123.25)).isCloseTo(-0.25, ERROR); } @Test public void testIsZero() { assertThat(isZero( 1.0 )).isFalse(); assertThat(isZero( 0.0001)).isFalse(); assertThat(isZero( 0.0 )).isTrue(); assertThat(isZero(-0.0 )).isTrue(); assertThat(isZero(-0.0001)).isFalse(); assertThat(isZero(-1.0 )).isFalse(); assertThat(isZero( Double.NaN)).isFalse(); assertThat(isZero(-Double.NaN)).isFalse(); assertThat(isZero( Double.POSITIVE_INFINITY)).isFalse(); assertThat(isZero( Double.NEGATIVE_INFINITY)).isFalse(); } @Test public void testDms() { // Valid parameters assertThat(dms( 0, 0, 0.0 )).isEqualTo(0.0); assertThat(dms( 13, 27, 4.32)).isEqualTo(13.4512); assertThat(dms(-88, 39, 8.28)).isEqualTo(-88.6523); // Sign at wrong position is ignored assertThat(dms( 14, -14, 2.4)).isEqualTo(14.234); assertThat(dms( 66, 12, -46.8)).isEqualTo(66.213); // Out of range values are carried to the next position assertThat(dms( 0, 0, 72.0)).isEqualTo(0.02); // 0° 1' 12.0" assertThat(dms( 1, 80, 132.0)).isEqualTo(2.37); // 2° 22' 12.0" } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/JulianDateTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.PI; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.Offset.offset; import java.time.ZoneId; import java.time.ZonedDateTime; import org.assertj.core.api.AbstractDateAssert; import org.assertj.core.data.Offset; import org.junit.BeforeClass; import org.junit.Test; /** * Unit tests for {@link JulianDate}. */ public class JulianDateTest { private static final Offset ERROR = Offset.offset(0.001); @BeforeClass public static void init() { AbstractDateAssert.registerCustomDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); } @Test public void testAtHour() { JulianDate jd = new JulianDate(of(2017, 8, 19, 0, 0, 0, "UTC")); assertDate(jd, "2017-08-19T00:00:00Z"); JulianDate jd2 = jd.atHour(8.5); assertDate(jd2, "2017-08-19T08:30:00Z"); JulianDate jd3 = new JulianDate(of(2017, 8, 19, 0, 0, 0, "Europe/Berlin")); assertDate(jd3, "2017-08-19T00:00:00+02:00"); JulianDate jd4 = jd3.atHour(8.5); assertDate(jd4, "2017-08-19T08:30:00+02:00"); } @Test public void testModifiedJulianDate() { // MJD epoch is midnight of November 17th, 1858. JulianDate jd1 = new JulianDate(of(1858, 11, 17, 0, 0, 0, "UTC")); assertThat(jd1.getModifiedJulianDate()).isZero(); assertThat(jd1.toString()).isEqualTo("0d 00h 00m 00s"); JulianDate jd2 = new JulianDate(of(2017, 8, 19, 15, 6, 16, "UTC")); assertThat(jd2.getModifiedJulianDate()).isCloseTo(57984.629, ERROR); assertThat(jd2.toString()).isEqualTo("57984d 15h 06m 16s"); JulianDate jd3 = new JulianDate(of(2017, 8, 19, 15, 6, 16, "GMT+2")); assertThat(jd3.getModifiedJulianDate()).isCloseTo(57984.546, ERROR); assertThat(jd3.toString()).isEqualTo("57984d 13h 06m 16s"); } @Test public void testJulianCentury() { JulianDate jd1 = new JulianDate(of(2000, 1, 1, 0, 0, 0, "UTC")); assertThat(jd1.getJulianCentury()).isCloseTo(0.0, ERROR); JulianDate jd2 = new JulianDate(of(2017, 1, 1, 0, 0, 0, "UTC")); assertThat(jd2.getJulianCentury()).isCloseTo(0.17, ERROR); JulianDate jd3 = new JulianDate(of(2050, 7, 1, 0, 0, 0, "UTC")); assertThat(jd3.getJulianCentury()).isCloseTo(0.505, ERROR); } @Test public void testGreenwichMeanSiderealTime() { JulianDate jd1 = new JulianDate(of(2017, 9, 3, 19, 5, 15, "UTC")); assertThat(jd1.getGreenwichMeanSiderealTime()).isCloseTo(4.702, ERROR); } @Test public void testTrueAnomaly() { JulianDate jd1 = new JulianDate(of(2017, 1, 4, 0, 0, 0, "UTC")); assertThat(jd1.getTrueAnomaly()).isCloseTo(0.0, offset(0.1)); JulianDate jd2 = new JulianDate(of(2017, 7, 4, 0, 0, 0, "UTC")); assertThat(jd2.getTrueAnomaly()).isCloseTo(PI, offset(0.1)); } @Test public void testAtModifiedJulianDate() { JulianDate jd1 = new JulianDate(of(2017, 8, 19, 15, 6, 16, "UTC")); JulianDate jd2 = new JulianDate(ZonedDateTime.now(ZoneId.of("UTC"))) .atModifiedJulianDate(jd1.getModifiedJulianDate()); assertThat(jd2.getDateTime()).isEqualTo(jd1.getDateTime()); } @Test public void testAtJulianCentury() { JulianDate jd1 = new JulianDate(of(2017, 1, 1, 0, 0, 0, "UTC")); JulianDate jd2 = new JulianDate(ZonedDateTime.now(ZoneId.of("UTC"))) .atJulianCentury(jd1.getJulianCentury()); assertThat(jd2.getDateTime()).isEqualTo(jd1.getDateTime()); } private ZonedDateTime of(int year, int month, int day, int hour, int minute, int second, String zone) { return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.of(zone)); } private void assertDate(JulianDate jd, String date) { assertThat(jd.getDateTime()).isEqualTo(date); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/MatrixTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.PI; import static org.assertj.core.api.Assertions.assertThat; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link Matrix}. */ public class MatrixTest { private static final Offset ERROR = Offset.offset(0.001); private static final double PI_HALF = PI / 2.0; @Test public void testIdentity() { Matrix mx = Matrix.identity(); assertValues(mx, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); } @Test public void testRotateX() { Matrix mx = Matrix.rotateX(PI_HALF); assertValues(mx, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0); } @Test public void testRotateY() { Matrix mx = Matrix.rotateY(PI_HALF); assertValues(mx, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0); } @Test public void testRotateZ() { Matrix mx = Matrix.rotateZ(PI_HALF); assertValues(mx, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0); } @Test public void testTranspose() { Matrix mx = Matrix.rotateX(PI_HALF).transpose(); assertValues(mx, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0); } @Test public void testNegate() { Matrix mx = Matrix.identity().negate(); assertValues(mx, -1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, -1.0); } @Test public void testAdd() { Matrix mx1 = Matrix.rotateX(PI_HALF); Matrix mx2 = Matrix.rotateY(PI_HALF); assertValues(mx1.add(mx2), 1.0, 0.0, -1.0, 0.0, 1.0, 1.0, 1.0, -1.0, 0.0); assertValues(mx2.add(mx1), 1.0, 0.0, -1.0, 0.0, 1.0, 1.0, 1.0, -1.0, 0.0); } @Test public void testSubtract() { Matrix mx1 = Matrix.rotateX(PI_HALF); Matrix mx2 = Matrix.rotateY(PI_HALF); assertValues(mx1.subtract(mx2), 1.0, 0.0, 1.0, 0.0, -1.0, 1.0, -1.0, -1.0, 0.0); assertValues(mx2.subtract(mx1), -1.0, 0.0, -1.0, 0.0, 1.0, -1.0, 1.0, 1.0, 0.0); } @Test public void testMultiply() { Matrix mx1 = Matrix.rotateX(PI_HALF); Matrix mx2 = Matrix.rotateY(PI_HALF); assertValues(mx1.multiply(mx2), 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0); assertValues(mx2.multiply(mx1), 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0); } @Test public void testScalarMultiply() { Matrix mx = Matrix.identity().multiply(5.0); assertValues(mx, 5.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 5.0); } @Test public void testVectorMultiply() { Matrix mx = Matrix.rotateX(PI_HALF); Vector vc = new Vector(5.0, 8.0, -3.0); Vector result = mx.multiply(vc); assertThat(result.getX()).isCloseTo( 5.0, ERROR); assertThat(result.getY()).isCloseTo(-3.0, ERROR); assertThat(result.getZ()).isCloseTo(-8.0, ERROR); } @Test public void testEquals() { Matrix mx1 = Matrix.identity(); Matrix mx2 = Matrix.rotateX(PI_HALF); Matrix mx3 = Matrix.identity(); assertThat(mx1.equals(mx2)).isFalse(); assertThat(mx1.equals(mx3)).isTrue(); assertThat(mx2.equals(mx3)).isFalse(); assertThat(mx3.equals(mx1)).isTrue(); assertThat(mx1.equals(null)).isFalse(); assertThat(mx1.equals(new Object())).isFalse(); } @Test public void testHashCode() { int mx1 = Matrix.identity().hashCode(); int mx2 = Matrix.rotateX(PI_HALF).hashCode(); int mx3 = Matrix.identity().hashCode(); assertThat(mx1).isNotEqualTo(0); assertThat(mx2).isNotEqualTo(0); assertThat(mx3).isNotEqualTo(0); assertThat(mx1).isNotEqualTo(mx2); assertThat(mx1).isEqualTo(mx3); } @Test public void testToString() { Matrix mx = Matrix.identity(); assertThat(mx.toString()).isEqualTo("[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]"); } private void assertValues(Matrix mx, double... values) { for (int ix = 0; ix < values.length; ix++) { int r = ix / 3; int c = ix % 3; assertThat(mx.get(r, c)) .as("r=%d, c=%d", r, c) .isCloseTo(values[ix], ERROR); } } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/PegasusTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2018 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import java.util.function.Function; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link Pegasus}. */ public class PegasusTest { private static final Offset ERROR = Offset.offset(0.001); @Test public void testParabola() { // f(x) = x^2 + 2x - 3 // Roots at x = -3 and x = 1 Function parabola = x -> x * x + 2 * x - 3; double r1 = Pegasus.calculate(0.0, 3.0, 0.1, parabola); assertThat(r1).isCloseTo(1.0, ERROR); double r2 = Pegasus.calculate(-5.0, 0.0, 0.1, parabola); assertThat(r2).isCloseTo(-3.0, ERROR); try { Pegasus.calculate(-2.0, 0.5, 0.1, parabola); fail("Found a non-existing root"); } catch (ArithmeticException ex) { // expected } } @Test(expected = ArithmeticException.class) public void testParabola2() { // f(x) = x^2 + 3 // No roots Pegasus.calculate(-5.0, 5.0, 0.1, x -> x * x + 3); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/QuadraticInterpolationTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static org.assertj.core.api.Assertions.assertThat; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link QuadraticInterpolation}. */ public class QuadraticInterpolationTest { private static final Offset ERROR = Offset.offset(0.001); @Test public void testTwoRootsAndMinimum() { QuadraticInterpolation qi = new QuadraticInterpolation(1.0, -1.0, 1.0); assertThat(qi.getNumberOfRoots()).isEqualTo(2); assertThat(qi.getRoot1()).isCloseTo(-0.707, ERROR); assertThat(qi.getRoot2()).isCloseTo( 0.707, ERROR); assertThat(qi.getXe()).isCloseTo( 0.0, ERROR); assertThat(qi.getYe()).isCloseTo(-1.0, ERROR); assertThat(qi.isMaximum()).isFalse(); } @Test public void testTwoRootsAndMaximum() { QuadraticInterpolation qi = new QuadraticInterpolation(-1.0, 1.0, -1.0); assertThat(qi.getNumberOfRoots()).isEqualTo(2); assertThat(qi.getRoot1()).isCloseTo(-0.707, ERROR); assertThat(qi.getRoot2()).isCloseTo( 0.707, ERROR); assertThat(qi.getXe()).isCloseTo(0.0, ERROR); assertThat(qi.getYe()).isCloseTo(1.0, ERROR); assertThat(qi.isMaximum()).isTrue(); } @Test public void testOneRoot() { QuadraticInterpolation qi = new QuadraticInterpolation(2.0, 0.0, -1.0); assertThat(qi.getNumberOfRoots()).isEqualTo(1); assertThat(qi.getRoot1()).isCloseTo( 0.0, ERROR); assertThat(qi.getXe()).isCloseTo( 1.5, ERROR); assertThat(qi.getYe()).isCloseTo(-1.125, ERROR); assertThat(qi.isMaximum()).isFalse(); } @Test public void testNoRoot() { QuadraticInterpolation qi = new QuadraticInterpolation(3.0, 2.0, 1.0); assertThat(qi.getNumberOfRoots()).isZero(); } } ================================================ FILE: src/test/java/org/shredzone/commons/suncalc/util/VectorTest.java ================================================ /* * Shredzone Commons - suncalc * * Copyright (C) 2017 Richard "Shred" Körber * http://commons.shredzone.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ package org.shredzone.commons.suncalc.util; import static java.lang.Math.PI; import static org.assertj.core.api.Assertions.assertThat; import org.assertj.core.data.Offset; import org.junit.Test; /** * Unit tests for {@link Vector}. */ public class VectorTest { private static final Offset ERROR = Offset.offset(0.001); private static final double PI_HALF = PI / 2.0; @Test public void testConstructors() { Vector v1 = new Vector(20.0, 10.0, 5.0); assertThat(v1.getX()).isEqualTo(20.0); assertThat(v1.getY()).isEqualTo(10.0); assertThat(v1.getZ()).isEqualTo(5.0); Vector v2 = new Vector(new double[] { 20.0, 10.0, 5.0 }); assertThat(v2.getX()).isEqualTo(20.0); assertThat(v2.getY()).isEqualTo(10.0); assertThat(v2.getZ()).isEqualTo(5.0); Vector v3 = Vector.ofPolar(0.5, 0.25); assertThat(v3.getPhi()).isEqualTo(0.5); assertThat(v3.getTheta()).isEqualTo(0.25); assertThat(v3.getR()).isEqualTo(1.0); Vector v4 = Vector.ofPolar(0.5, 0.25, 50.0); assertThat(v4.getPhi()).isEqualTo(0.5); assertThat(v4.getTheta()).isEqualTo(0.25); assertThat(v4.getR()).isEqualTo(50.0); } @Test(expected = IllegalArgumentException.class) public void testBadConstructor() { new Vector(new double[] { 20.0, 10.0 }); } @Test public void testAdd() { Vector v1 = new Vector(20.0, 10.0, 5.0); Vector v2 = new Vector(10.0, 25.0, 15.0); Vector r1 = v1.add(v2); assertThat(r1.getX()).isEqualTo(30.0); assertThat(r1.getY()).isEqualTo(35.0); assertThat(r1.getZ()).isEqualTo(20.0); Vector r2 = v2.add(v1); assertThat(r2.getX()).isEqualTo(30.0); assertThat(r2.getY()).isEqualTo(35.0); assertThat(r2.getZ()).isEqualTo(20.0); } @Test public void testSubtract() { Vector v1 = new Vector(20.0, 10.0, 5.0); Vector v2 = new Vector(10.0, 25.0, 15.0); Vector r1 = v1.subtract(v2); assertThat(r1.getX()).isEqualTo(10.0); assertThat(r1.getY()).isEqualTo(-15.0); assertThat(r1.getZ()).isEqualTo(-10.0); Vector r2 = v2.subtract(v1); assertThat(r2.getX()).isEqualTo(-10.0); assertThat(r2.getY()).isEqualTo(15.0); assertThat(r2.getZ()).isEqualTo(10.0); } @Test public void testMultiply() { Vector v1 = new Vector(20.0, 10.0, 5.0); Vector r1 = v1.multiply(5.0); assertThat(r1.getX()).isEqualTo(100.0); assertThat(r1.getY()).isEqualTo(50.0); assertThat(r1.getZ()).isEqualTo(25.0); } @Test public void testNegate() { Vector v1 = new Vector(20.0, 10.0, 5.0); Vector r1 = v1.negate(); assertThat(r1.getX()).isEqualTo(-20.0); assertThat(r1.getY()).isEqualTo(-10.0); assertThat(r1.getZ()).isEqualTo(-5.0); } @Test public void testCross() { Vector v1 = new Vector(3.0, -3.0, 1.0); Vector v2 = new Vector(4.0, 9.0, 2.0); Vector r1 = v1.cross(v2); assertThat(r1.getX()).isEqualTo(-15.0); assertThat(r1.getY()).isEqualTo(-2.0); assertThat(r1.getZ()).isEqualTo(39.0); } @Test public void testDot() { Vector v1 = new Vector(1.0, 2.0, 3.0); Vector v2 = new Vector(4.0, -5.0, 6.0); double r1 = v1.dot(v2); assertThat(r1).isCloseTo(12.0, ERROR); } @Test public void testNorm() { Vector v1 = new Vector(5.0, -6.0, 7.0); double r1 = v1.norm(); assertThat(r1).isCloseTo(10.488, ERROR); } @Test public void testEquals() { Vector v1 = new Vector(3.0, -3.0, 1.0); Vector v2 = new Vector(4.0, 9.0, 2.0); Vector v3 = new Vector(3.0, -3.0, 1.0); assertThat(v1.equals(v2)).isFalse(); assertThat(v1.equals(v3)).isTrue(); assertThat(v2.equals(v3)).isFalse(); assertThat(v3.equals(v1)).isTrue(); assertThat(v1.equals(null)).isFalse(); assertThat(v1.equals(new Object())).isFalse(); } @Test public void testHashCode() { int h1 = new Vector(3.0, -3.0, 1.0).hashCode(); int h2 = new Vector(4.0, 9.0, 2.0).hashCode(); int h3 = new Vector(3.0, -3.0, 1.0).hashCode(); assertThat(h1).isNotZero(); assertThat(h2).isNotZero(); assertThat(h3).isNotZero(); assertThat(h1).isNotEqualTo(h2); assertThat(h1).isEqualTo(h3); } @Test public void testToString() { Vector v1 = new Vector(3.0, -3.0, 1.0); assertThat(v1.toString()).isEqualTo("(x=3.0, y=-3.0, z=1.0)"); } @Test public void testToCartesian() { Vector v1 = Vector.ofPolar(0.0, 0.0); assertThat(v1.getX()).isCloseTo(1.0, ERROR); assertThat(v1.getY()).isCloseTo(0.0, ERROR); assertThat(v1.getZ()).isCloseTo(0.0, ERROR); Vector v2 = Vector.ofPolar(PI_HALF, 0.0); assertThat(v2.getX()).isCloseTo(0.0, ERROR); assertThat(v2.getY()).isCloseTo(1.0, ERROR); assertThat(v2.getZ()).isCloseTo(0.0, ERROR); Vector v3 = Vector.ofPolar(0.0, PI_HALF); assertThat(v3.getX()).isCloseTo(0.0, ERROR); assertThat(v3.getY()).isCloseTo(0.0, ERROR); assertThat(v3.getZ()).isCloseTo(1.0, ERROR); Vector v4 = Vector.ofPolar(PI_HALF, PI_HALF); assertThat(v4.getX()).isCloseTo(0.0, ERROR); assertThat(v4.getY()).isCloseTo(0.0, ERROR); assertThat(v4.getZ()).isCloseTo(1.0, ERROR); Vector v5 = Vector.ofPolar(PI_HALF, -PI_HALF); assertThat(v5.getX()).isCloseTo(0.0, ERROR); assertThat(v5.getY()).isCloseTo(0.0, ERROR); assertThat(v5.getZ()).isCloseTo(-1.0, ERROR); Vector v6 = Vector.ofPolar(0.0, 0.0, 5.0); assertThat(v6.getX()).isCloseTo(5.0, ERROR); assertThat(v6.getY()).isCloseTo(0.0, ERROR); assertThat(v6.getZ()).isCloseTo(0.0, ERROR); Vector v7 = Vector.ofPolar(PI_HALF, 0.0, 5.0); assertThat(v7.getX()).isCloseTo(0.0, ERROR); assertThat(v7.getY()).isCloseTo(5.0, ERROR); assertThat(v7.getZ()).isCloseTo(0.0, ERROR); Vector v8 = Vector.ofPolar(0.0, PI_HALF, 5.0); assertThat(v8.getX()).isCloseTo(0.0, ERROR); assertThat(v8.getY()).isCloseTo(0.0, ERROR); assertThat(v8.getZ()).isCloseTo(5.0, ERROR); Vector v9 = Vector.ofPolar(PI_HALF, PI_HALF, 5.0); assertThat(v9.getX()).isCloseTo(0.0, ERROR); assertThat(v9.getY()).isCloseTo(0.0, ERROR); assertThat(v9.getZ()).isCloseTo(5.0, ERROR); Vector v10 = Vector.ofPolar(PI_HALF, -PI_HALF, 5.0); assertThat(v10.getX()).isCloseTo(0.0, ERROR); assertThat(v10.getY()).isCloseTo(0.0, ERROR); assertThat(v10.getZ()).isCloseTo(-5.0, ERROR); } @Test public void testToPolar() { Vector v1 = new Vector(20.0, 0.0, 0.0); assertThat(v1.getPhi()).isEqualTo(0.0); assertThat(v1.getTheta()).isEqualTo(0.0); assertThat(v1.getR()).isEqualTo(20.0); Vector v2 = new Vector(0.0, 20.0, 0.0); assertThat(v2.getPhi()).isEqualTo(PI_HALF); assertThat(v2.getTheta()).isEqualTo(0.0); assertThat(v2.getR()).isEqualTo(20.0); Vector v3 = new Vector(0.0, 0.0, 20.0); assertThat(v3.getPhi()).isEqualTo(0.0); assertThat(v3.getTheta()).isEqualTo(PI_HALF); assertThat(v3.getR()).isEqualTo(20.0); Vector v4 = new Vector(-20.0, 0.0, 0.0); assertThat(v4.getPhi()).isEqualTo(PI); assertThat(v4.getTheta()).isEqualTo(0.0); assertThat(v4.getR()).isEqualTo(20.0); Vector v5 = new Vector(0.0, -20.0, 0.0); assertThat(v5.getPhi()).isEqualTo(PI + PI_HALF); assertThat(v5.getTheta()).isEqualTo(0.0); assertThat(v5.getR()).isEqualTo(20.0); Vector v6 = new Vector(0.0, 0.0, -20.0); assertThat(v6.getPhi()).isEqualTo(0.0); assertThat(v6.getTheta()).isEqualTo(-PI_HALF); assertThat(v6.getR()).isEqualTo(20.0); Vector v7 = new Vector(0.0, 0.0, 0.0); assertThat(v7.getPhi()).isEqualTo(0.0); assertThat(v7.getTheta()).isEqualTo(0.0); assertThat(v7.getR()).isEqualTo(0.0); } } ================================================ FILE: src/test/resources/.gitignore ================================================