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}