001/*
002 * Shredzone Commons - suncalc
003 *
004 * Copyright (C) 2017 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.commons.suncalc.util;
015
016import static java.lang.Math.max;
017import static java.lang.Math.toRadians;
018
019import java.time.Duration;
020import java.time.Instant;
021import java.time.LocalDate;
022import java.time.LocalDateTime;
023import java.time.LocalTime;
024import java.time.ZoneId;
025import java.time.ZonedDateTime;
026import java.time.temporal.ChronoUnit;
027import java.util.Objects;
028
029import edu.umd.cs.findbugs.annotations.Nullable;
030import org.shredzone.commons.suncalc.param.GenericParameter;
031import org.shredzone.commons.suncalc.param.LocationParameter;
032import org.shredzone.commons.suncalc.param.TimeParameter;
033import org.shredzone.commons.suncalc.param.WindowParameter;
034
035/**
036 * A base implementation of {@link LocationParameter}, {@link TimeParameter}, and
037 * {@link WindowParameter}.
038 * <p>
039 * For internal use only.
040 *
041 * @param <T>
042 *         Type of the final builder
043 */
044@SuppressWarnings("unchecked")
045public class BaseBuilder<T> implements GenericParameter<T>, LocationParameter<T>,
046        TimeParameter<T>, WindowParameter<T>, Cloneable {
047
048    private @Nullable Double lat = null;
049    private @Nullable Double lng = null;
050    private double elevation = 0.0;
051    private ZonedDateTime dateTime = ZonedDateTime.now();
052    private boolean reverse = false;
053    private Duration duration = Duration.ofDays(365L);
054
055    @Override
056    public T on(ZonedDateTime dateTime) {
057        this.dateTime = Objects.requireNonNull(dateTime, "dateTime");
058        return (T) this;
059    }
060
061    @Override
062    public T on(LocalDateTime dateTime) {
063        Objects.requireNonNull(dateTime, "dateTime");
064        return on(ZonedDateTime.of(dateTime, this.dateTime.getZone()));
065    }
066
067    @Override
068    public T on(LocalDate date) {
069        Objects.requireNonNull(date, "date");
070        return on(ZonedDateTime.of(date, LocalTime.MIDNIGHT, dateTime.getZone()));
071    }
072
073    @Override
074    public T on(Instant instant) {
075        Objects.requireNonNull(instant, "instant");
076        return on(ZonedDateTime.ofInstant(instant, dateTime.getZone()));
077    }
078
079    @Override
080    public T on(int year, int month, int date, int hour, int minute, int second) {
081        return on(ZonedDateTime.of(year, month, date, hour, minute, second, 0, dateTime.getZone()));
082    }
083
084    @Override
085    public T now() {
086        return on(ZonedDateTime.now(dateTime.getZone()));
087    }
088
089    @Override
090    public T plusDays(int days) {
091        return on(dateTime.plusDays(days));
092    }
093
094    @Override
095    public T midnight() {
096        return on(dateTime.truncatedTo(ChronoUnit.DAYS));
097    }
098
099    @Override
100    public T timezone(ZoneId tz) {
101        Objects.requireNonNull(tz, "tz");
102        on(dateTime.withZoneSameLocal(tz));
103        return (T) this;
104    }
105
106    @Override
107    public T latitude(double lat) {
108        if (lat < -90.0 || lat > 90.0) {
109            throw new IllegalArgumentException("Latitude out of range, -90.0 <= " + lat + " <= 90.0");
110        }
111        this.lat = lat;
112        return (T) this;
113    }
114
115    @Override
116    public T longitude(double lng) {
117        if (lng < -180.0 || lng > 180.0) {
118            throw new IllegalArgumentException("Longitude out of range, -180.0 <= " + lng + " <= 180.0");
119        }
120        this.lng = lng;
121        return (T) this;
122    }
123
124    @Override
125    public T elevation(double h) {
126        this.elevation = max(h, 0.0);
127        return (T) this;
128    }
129
130    public T limit(Duration duration) {
131        Objects.requireNonNull(duration, "duration");
132        this.duration = duration;
133        if (duration.isNegative()) {
134            reverse();
135        }
136        return (T) this;
137    }
138
139    public T reverse() {
140        reverse = true;
141        return (T) this;
142    }
143
144    public T forward() {
145        reverse = false;
146        return (T) this;
147    }
148
149    @Override
150    public T sameTimeAs(TimeParameter<?> t) {
151        if (! (t instanceof BaseBuilder)) {
152            throw new IllegalArgumentException("Cannot read the TimeParameter");
153        }
154        this.dateTime = ((BaseBuilder<?>) t).dateTime;
155        return (T) this;
156    }
157
158    @Override
159    public T sameLocationAs(LocationParameter<?> l) {
160        if (! (l instanceof BaseBuilder)) {
161            throw new IllegalArgumentException("Cannot read the LocationParameter");
162        }
163        BaseBuilder<?> origin = (BaseBuilder<?>) l;
164        this.lat = origin.lat;
165        this.lng = origin.lng;
166        this.elevation = origin.elevation;
167        return (T) this;
168    }
169
170    @Override
171    public T sameWindowAs(WindowParameter<?> w) {
172        if (! (w instanceof BaseBuilder)) {
173            throw new IllegalArgumentException("Cannot read the WindowParameter");
174        }
175        BaseBuilder<?> origin = (BaseBuilder<?>) w;
176        this.duration = origin.duration;
177        this.reverse = origin.reverse;
178        return (T) this;
179    }
180
181    @Override
182    public T copy() {
183        try {
184            return (T) clone();
185        } catch (CloneNotSupportedException ex) {
186            throw new RuntimeException(ex); // Should never be thrown anyway
187        }
188    }
189
190    /**
191     * Returns the longitude.
192     *
193     * @return Longitude, in degrees.
194     */
195    public double getLongitude() {
196        if (lng == null) {
197            throw new IllegalStateException("longitude is not set");
198        }
199        return lng;
200    }
201
202    /**
203     * Returns the latitude.
204     *
205     * @return Latitude, in degrees.
206     */
207    public double getLatitude() {
208        if (lat == null) {
209            throw new IllegalStateException("latitude is not set");
210        }
211        return lat;
212    }
213
214    /**
215     * Returns the longitude.
216     *
217     * @return Longitude, in radians.
218     */
219    public double getLongitudeRad() {
220        return toRadians(getLongitude());
221    }
222
223    /**
224     * Returns the latitude.
225     *
226     * @return Latitude, in radians.
227     */
228    public double getLatitudeRad() {
229        return toRadians(getLatitude());
230    }
231
232    /**
233     * Returns the elevation, in meters above sea level.
234     *
235     * @return Elevation, meters above sea level
236     */
237    public double getElevation() {
238        return elevation;
239    }
240
241    /**
242     * Returns the {@link JulianDate} to be used.
243     *
244     * @return {@link JulianDate}
245     */
246    public JulianDate getJulianDate() {
247        return new JulianDate(dateTime);
248    }
249
250    /**
251     * Returns {@code true} if a geolocation has been set.
252     *
253     * @since 3.9
254     */
255    public boolean hasLocation() {
256        return lat != null && lng != null;
257    }
258
259    /**
260     * Unset the geolocation.
261     *
262     * @since 3.9
263     */
264    public void clearLocation() {
265        lat = null;
266        lng = null;
267    }
268
269    /**
270     * Returns the duration of the time window.
271     *
272     * @since 3.11
273     */
274    public Duration getDuration() {
275        if (reverse != duration.isNegative()) {
276            return duration.negated();
277        }
278        return duration;
279    }
280
281}