001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.geoimage;
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 11914 (extracted from {@link CorrelateGpxWithImages})
017 */
018public final class Offset {
019
020    static final Offset ZERO = new Offset(0);
021    private final long milliseconds;
022
023    private Offset(long milliseconds) {
024        this.milliseconds = milliseconds;
025    }
026
027    static Offset milliseconds(long milliseconds) {
028        return new Offset(milliseconds);
029    }
030
031    static Offset seconds(long seconds) {
032        return new Offset(1000 * seconds);
033    }
034
035    long getMilliseconds() {
036        return milliseconds;
037    }
038
039    long getSeconds() {
040        return milliseconds / 1000;
041    }
042
043    String formatOffset() {
044        if (milliseconds % 1000 == 0) {
045            return Long.toString(milliseconds / 1000);
046        } else if (milliseconds % 100 == 0) {
047            return String.format(Locale.ENGLISH, "%.1f", milliseconds / 1000.);
048        } else {
049            return String.format(Locale.ENGLISH, "%.3f", milliseconds / 1000.);
050        }
051    }
052
053    static Offset parseOffset(String offset) throws ParseException {
054        String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
055
056        if (!offset.isEmpty()) {
057            try {
058                if (offset.startsWith("+")) {
059                    offset = offset.substring(1);
060                }
061                return Offset.milliseconds(Math.round(JosmDecimalFormatSymbolsProvider.parseDouble(offset) * 1000));
062            } catch (NumberFormatException nfe) {
063                throw (ParseException) new ParseException(error, 0).initCause(nfe);
064            }
065        } else {
066            return Offset.ZERO;
067        }
068    }
069
070    int getDayOffset() {
071        // Find day difference
072        return (int) Math.round(((double) getMilliseconds()) / TimeUnit.DAYS.toMillis(1));
073    }
074
075    Offset withoutDayOffset() {
076        return milliseconds(getMilliseconds() - TimeUnit.DAYS.toMillis(getDayOffset()));
077    }
078
079    Pair<Timezone, Offset> splitOutTimezone() {
080        // In hours
081        final double tz = ((double) withoutDayOffset().getSeconds()) / TimeUnit.HOURS.toSeconds(1);
082
083        // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
084        // -2 minutes offset. This determines the real timezone and finds offset.
085        final double timezone = (double) Math.round(tz * 2) / 2; // hours, rounded to one decimal place
086        final long delta = Math.round(getMilliseconds() - timezone * TimeUnit.HOURS.toMillis(1));
087        return Pair.create(new Timezone(timezone), Offset.milliseconds(delta));
088    }
089
090    @Override
091    public boolean equals(Object o) {
092        if (this == o) return true;
093        if (!(o instanceof Offset)) return false;
094        Offset offset = (Offset) o;
095        return milliseconds == offset.milliseconds;
096    }
097
098    @Override
099    public int hashCode() {
100        return Objects.hash(milliseconds);
101    }
102}