2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.astro.internal.calc;
15 import java.util.Calendar;
17 import org.openhab.binding.astro.internal.model.Season;
18 import org.openhab.binding.astro.internal.model.SeasonName;
19 import org.openhab.binding.astro.internal.util.DateTimeUtils;
22 * Calculates the seasons of the year.
24 * @author Gerhard Riegler - Initial contribution
25 * @implNote based on the calculations of http://stellafane.org/misc/equinox.html
27 public class SeasonCalc {
28 private int currentYear;
29 private Season currentSeason;
32 * Returns the seasons of the year of the specified calendar.
34 public Season getSeason(Calendar calendar, double latitude, boolean useMeteorologicalSeason) {
35 int year = calendar.get(Calendar.YEAR);
36 boolean isSouthernHemisphere = latitude < 0.0;
37 Season season = currentSeason;
38 if (currentYear != year) {
39 season = new Season();
40 if (!isSouthernHemisphere) {
41 season.setSpring(calcEquiSol(0, year));
42 season.setSummer(calcEquiSol(1, year));
43 season.setAutumn(calcEquiSol(2, year));
44 season.setWinter(calcEquiSol(3, year));
46 season.setSpring(calcEquiSol(2, year));
47 season.setSummer(calcEquiSol(3, year));
48 season.setAutumn(calcEquiSol(0, year));
49 season.setWinter(calcEquiSol(1, year));
51 currentSeason = season;
55 if (useMeteorologicalSeason) {
56 atMidnightOfFirstMonthDay(season.getSpring());
57 atMidnightOfFirstMonthDay(season.getSummer());
58 atMidnightOfFirstMonthDay(season.getAutumn());
59 atMidnightOfFirstMonthDay(season.getWinter());
62 season.setName(!isSouthernHemisphere ? getCurrentSeasonNameNorthern(calendar)
63 : getCurrentSeasonNameSouthern(calendar));
67 private void atMidnightOfFirstMonthDay(Calendar calendar) {
68 calendar.set(Calendar.DAY_OF_MONTH, 1);
69 calendar.set(Calendar.HOUR_OF_DAY, 0);
70 calendar.set(Calendar.MINUTE, 0);
71 calendar.set(Calendar.SECOND, 0);
72 calendar.set(Calendar.MILLISECOND, 0);
76 * Returns the current season name for the northern hemisphere.
78 private SeasonName getCurrentSeasonNameNorthern(Calendar calendar) {
79 long currentMillis = calendar.getTimeInMillis();
80 if (currentMillis < currentSeason.getSpring().getTimeInMillis()
81 || currentMillis >= currentSeason.getWinter().getTimeInMillis()) {
82 return SeasonName.WINTER;
83 } else if (currentMillis >= currentSeason.getSpring().getTimeInMillis()
84 && currentMillis < currentSeason.getSummer().getTimeInMillis()) {
85 return SeasonName.SPRING;
86 } else if (currentMillis >= currentSeason.getSummer().getTimeInMillis()
87 && currentMillis < currentSeason.getAutumn().getTimeInMillis()) {
88 return SeasonName.SUMMER;
89 } else if (currentMillis >= currentSeason.getAutumn().getTimeInMillis()
90 && currentMillis < currentSeason.getWinter().getTimeInMillis()) {
91 return SeasonName.AUTUMN;
97 * Returns the current season name for the southern hemisphere.
99 private SeasonName getCurrentSeasonNameSouthern(Calendar calendar) {
100 long currentMillis = calendar.getTimeInMillis();
101 if (currentMillis < currentSeason.getAutumn().getTimeInMillis()
102 || currentMillis >= currentSeason.getSummer().getTimeInMillis()) {
103 return SeasonName.SUMMER;
104 } else if (currentMillis >= currentSeason.getAutumn().getTimeInMillis()
105 && currentMillis < currentSeason.getWinter().getTimeInMillis()) {
106 return SeasonName.AUTUMN;
107 } else if (currentMillis >= currentSeason.getWinter().getTimeInMillis()
108 && currentMillis < currentSeason.getSpring().getTimeInMillis()) {
109 return SeasonName.WINTER;
110 } else if (currentMillis >= currentSeason.getSpring().getTimeInMillis()
111 && currentMillis < currentSeason.getSummer().getTimeInMillis()) {
112 return SeasonName.SPRING;
118 * Calculates the date of the season.
120 private Calendar calcEquiSol(int season, int year) {
121 double estimate = calcInitial(season, year);
122 double t = (estimate - 2451545.0) / 36525;
123 double w = 35999.373 * t - 2.47;
124 double dl = 1 + 0.0334 * cosDeg(w) + 0.0007 * cosDeg(2 * w);
125 double s = periodic24(t);
126 double julianDate = estimate + ((0.00001 * s) / dl);
127 return DateTimeUtils.toCalendar(julianDate);
131 * Calculate an initial guess of the Equinox or Solstice of a given year.
133 private double calcInitial(int season, int year) {
134 double y = (year - 2000) / 1000d;
137 return 2451623.80984 + 365242.37404 * y + 0.05169 * Math.pow(y, 2) - 0.00411 * Math.pow(y, 3)
138 - 0.00057 * Math.pow(y, 4);
140 return 2451716.56767 + 365241.62603 * y + 0.00325 * Math.pow(y, 2) + 0.00888 * Math.pow(y, 3)
141 - 0.00030 * Math.pow(y, 4);
143 return 2451810.21715 + 365242.01767 * y - 0.11575 * Math.pow(y, 2) + 0.00337 * Math.pow(y, 3)
144 + 0.00078 * Math.pow(y, 4);
146 return 2451900.05952 + 365242.74049 * y - 0.06223 * Math.pow(y, 2) - 0.00823 * Math.pow(y, 3)
147 + 0.00032 * Math.pow(y, 4);
153 * Calculate 24 periodic terms
155 private double periodic24(double T) {
156 int[] a = new int[] { 485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12,
158 double[] b = new double[] { 324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81,
159 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73,
161 double[] c = new double[] { 1934.136, 32964.467, 20.186, 445267.112, 45036.886, 22518.443, 65928.934, 3034.906,
162 9037.513, 33718.147, 150.678, 2281.226, 29929.562, 31555.956, 4443.417, 67555.328, 4562.452, 62894.029,
163 31436.921, 14577.848, 31931.756, 34777.259, 1222.114, 16859.074 };
166 for (int i = 0; i < 24; i++) {
167 result += a[i] * cosDeg(b[i] + (c[i] * T));
173 * Cosinus of a degree value.
175 private double cosDeg(double deg) {
176 return Math.cos(deg * Math.PI / 180);