001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.gpx;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.text.ParseException;
007import java.util.Locale;
008import java.util.Objects;
009import java.util.concurrent.TimeUnit;
010
011import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
012import org.openstreetmap.josm.tools.Pair;
013
014/**
015 * Time offset of GPX correlation.
016 * @since 14205 (extracted from {@code CorrelateGpxWithImages})
017 */
018public final class GpxTimeOffset {
019
020    /**
021     * The time offset 0.
022     */
023    public static final GpxTimeOffset ZERO = new GpxTimeOffset(0);
024    private final long milliseconds;
025
026    private GpxTimeOffset(long milliseconds) {
027        this.milliseconds = milliseconds;
028    }
029
030    /**
031     * Constructs a new {@code GpxTimeOffset} from milliseconds.
032     * @param milliseconds time offset in milliseconds.
033     * @return new {@code GpxTimeOffset}
034     */
035    public static GpxTimeOffset milliseconds(long milliseconds) {
036        return new GpxTimeOffset(milliseconds);
037    }
038
039    /**
040     * Constructs a new {@code GpxTimeOffset} from seconds.
041     * @param seconds time offset in seconds.
042     * @return new {@code GpxTimeOffset}
043     */
044    public static GpxTimeOffset seconds(long seconds) {
045        return new GpxTimeOffset(1000 * seconds);
046    }
047
048    /**
049     * Get time offset in milliseconds.
050     * @return time offset in milliseconds
051     */
052    public long getMilliseconds() {
053        return milliseconds;
054    }
055
056    /**
057     * Get time offset in seconds.
058     * @return time offset in seconds
059     */
060    public long getSeconds() {
061        return milliseconds / 1000;
062    }
063
064    /**
065     * Formats time offset.
066     * @return formatted time offset. Format: decimal number
067     */
068    public String formatOffset() {
069        if (milliseconds % 1000 == 0) {
070            return Long.toString(milliseconds / 1000);
071        } else if (milliseconds % 100 == 0) {
072            return String.format(Locale.ENGLISH, "%.1f", milliseconds / 1000.);
073        } else {
074            return String.format(Locale.ENGLISH, "%.3f", milliseconds / 1000.);
075        }
076    }
077
078    /**
079     * Parses time offset.
080     * @param offset time offset. Format: decimal number
081     * @return time offset
082     * @throws ParseException if time offset can't be parsed
083     */
084    public static GpxTimeOffset parseOffset(String offset) throws ParseException {
085        String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
086
087        if (!offset.isEmpty()) {
088            try {
089                if (offset.startsWith("+")) {
090                    offset = offset.substring(1);
091                }
092                return GpxTimeOffset.milliseconds(Math.round(JosmDecimalFormatSymbolsProvider.parseDouble(offset) * 1000));
093            } catch (NumberFormatException nfe) {
094                throw (ParseException) new ParseException(error, 0).initCause(nfe);
095            }
096        } else {
097            return GpxTimeOffset.ZERO;
098        }
099    }
100
101    /**
102     * Returns the day difference.
103     * @return the day difference
104     */
105    public int getDayOffset() {
106        // Find day difference
107        return (int) Math.round(((double) getMilliseconds()) / TimeUnit.DAYS.toMillis(1));
108    }
109
110    /**
111     * Returns offset without day difference.
112     * @return offset without day difference
113     */
114    public GpxTimeOffset withoutDayOffset() {
115        return milliseconds(getMilliseconds() - TimeUnit.DAYS.toMillis(getDayOffset()));
116    }
117
118    /**
119     * Split out timezone and offset.
120     * @return pair of timezone and offset
121     */
122    public Pair<GpxTimezone, GpxTimeOffset> splitOutTimezone() {
123        // In hours
124        final double tz = ((double) withoutDayOffset().getSeconds()) / TimeUnit.HOURS.toSeconds(1);
125
126        // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
127        // -2 minutes offset. This determines the real timezone and finds offset.
128        final double timezone = (double) Math.round(tz * 2) / 2; // hours, rounded to one decimal place
129        final long delta = Math.round(getMilliseconds() - timezone * TimeUnit.HOURS.toMillis(1));
130        return Pair.create(new GpxTimezone(timezone), GpxTimeOffset.milliseconds(delta));
131    }
132
133    @Override
134    public boolean equals(Object o) {
135        if (this == o) return true;
136        if (!(o instanceof GpxTimeOffset)) return false;
137        GpxTimeOffset offset = (GpxTimeOffset) o;
138        return milliseconds == offset.milliseconds;
139    }
140
141    @Override
142    public int hashCode() {
143        return Objects.hash(milliseconds);
144    }
145}