001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.api.model.occurrence.geo;
015
016import java.util.Objects;
017import java.util.StringJoiner;
018
019import org.apache.commons.lang3.StringUtils;
020
021import com.fasterxml.jackson.annotation.JsonCreator;
022import com.fasterxml.jackson.annotation.JsonProperty;
023
024/**
025 * The DistanceUnit enumerates several units for measuring distances. These units
026 * provide methods for converting strings and methods to convert units among each
027 * others.The default unit used within this project is <code>METERS</code>
028 * which is defined by <code>DEFAULT</code>
029 */
030public enum DistanceUnit {
031  INCH(0.0254, "in", "inch"),
032  YARD(0.9144, "yd", "yards"),
033  FEET(0.3048, "ft", "feet"),
034  KILOMETERS(1000.0, "km", "kilometers"),
035  NAUTICALMILES(1852.0, "NM", "nmi", "nauticalmiles"),
036  MILLIMETERS(0.001, "mm", "millimeters"),
037  CENTIMETERS(0.01, "cm", "centimeters"),
038
039  // 'm' is a suffix of 'nmi' so it must follow 'nmi'
040  MILES(1609.344, "mi", "miles"),
041
042  // since 'm' is suffix of other unit
043  // it must be the last entry of unit
044  // names ending with 'm'. otherwise
045  // parsing would fail
046  METERS(1, "m", "meters");
047
048  public static final DistanceUnit DEFAULT = METERS;
049
050  private double meters;
051  private final String[] names;
052
053  DistanceUnit(double meters, String...names) {
054    this.meters = meters;
055    this.names = names;
056  }
057
058  public double getMeters() {
059    return meters;
060  }
061
062  public String[] getNames() {
063    return names;
064  }
065
066  /**
067   * Convert a value to a distance string
068   *
069   * @param distance value to convert
070   * @return String representation of the distance
071   */
072  public String toString(double distance) {
073    return distance + toString();
074  }
075
076  @Override
077  public String toString() {
078    return names[0];
079  }
080
081  /**
082   * Parse a {@link Distance} from a given String
083   *
084   * @param distance String defining a {@link Distance}
085   * @return parsed {@link Distance}
086   */
087  public static Distance parseDistance(String distance) {
088    for (DistanceUnit unit: DistanceUnit.values()) {
089      for (String name : unit.getNames()) {
090        if(distance.endsWith(name)) {
091          return new Distance(Double.parseDouble(distance.substring(0, distance.length() - name.length())), unit);
092        }
093      }
094    }
095    return new Distance(Double.parseDouble(distance), DEFAULT);
096  }
097
098  /**
099   * Converts the given distance from the given DistanceUnit, to the given DistanceUnit
100   *
101   * @param distance Distance to convert
102   * @param from     Unit to convert the distance from
103   * @param to       Unit of distance to convert to
104   * @return Given distance converted to the distance in the given unit
105   */
106  public static double convert(double distance, DistanceUnit from, DistanceUnit to) {
107    if (from == to) {
108      return distance;
109    } else {
110      return distance * from.meters / to.meters;
111    }
112  }
113
114  public static class Distance implements Comparable<Distance> {
115    public final double value;
116    public final DistanceUnit unit;
117
118    @JsonCreator
119    public Distance(@JsonProperty("value") double value,
120                    @JsonProperty("unit") DistanceUnit unit) {
121      this.value = value;
122      this.unit = unit;
123    }
124
125    public double getValue() {
126      return value;
127    }
128
129    public DistanceUnit getUnit() {
130      return unit;
131    }
132
133    @Override
134    public boolean equals(Object obj) {
135      if(obj == null) {
136        return false;
137      } else if (obj instanceof Distance) {
138        Distance other = (Distance) obj;
139        return DistanceUnit.convert(value, unit, other.unit) == other.value;
140      } else {
141        return false;
142      }
143    }
144
145    @Override
146    public int hashCode() {
147      return Double.valueOf(value * unit.meters).hashCode();
148    }
149
150    @Override
151    public int compareTo(Distance o) {
152      return Double.compare(value, DistanceUnit.convert(o.value, o.unit, unit));
153    }
154
155    @Override
156    public String toString() {
157      return unit.toString(value);
158    }
159  }
160
161  public static class GeoDistance {
162
163    private final double latitude;
164
165    private final double longitude;
166
167    private final Distance distance;
168
169    @JsonCreator
170    public GeoDistance(@JsonProperty("latitude") double latitude,
171                       @JsonProperty("longitude") double longitude,
172                       @JsonProperty("distance") Distance distance) {
173      this.latitude = latitude;
174      this.longitude = longitude;
175      this.distance = distance;
176    }
177
178    public double getLatitude() {
179      return latitude;
180    }
181
182    public double getLongitude() {
183      return longitude;
184    }
185
186    public Distance getDistance() {
187      return distance;
188    }
189
190    @Override
191    public boolean equals(Object o) {
192      if (this == o) return true;
193      if (o == null || getClass() != o.getClass()) return false;
194      GeoDistance that = (GeoDistance) o;
195      return Double.compare(that.latitude, latitude) == 0
196             && Double.compare(that.longitude, longitude) == 0
197             && Objects.equals(distance, that.distance);
198    }
199
200    @Override
201    public int hashCode() {
202      return Objects.hash(latitude, longitude, distance);
203    }
204
205    @Override
206    public String toString() {
207      return new StringJoiner(", ", GeoDistance.class.getSimpleName() + "[", "]")
208        .add("latitude=" + latitude)
209        .add("longitude=" + longitude)
210        .add("distance=" + distance)
211        .toString();
212    }
213
214    public static GeoDistance parseGeoDistance(String geoDistance) {
215      if (StringUtils.isEmpty(geoDistance)) {
216        throw new IllegalArgumentException("GeoDistance cannot be null or empty");
217      }
218      String[] geoDistanceTokens = geoDistance.split(",");
219      if (geoDistanceTokens.length != 3) {
220        throw new IllegalArgumentException("GeoDistance must follow the format lat,lng,distance");
221      }
222      return parseGeoDistance(geoDistanceTokens[0].trim(), geoDistanceTokens[1].trim(), geoDistanceTokens[2].trim());
223    }
224
225    public String toGeoDistanceString() {
226      return new StringJoiner(", ")
227        .add(Double.toString(latitude))
228        .add(Double.toString(longitude))
229        .add(distance.toString())
230        .toString();
231    }
232
233    public static GeoDistance parseGeoDistance(String latitude, String longitude, String distance) {
234      return new GeoDistance(Double.parseDouble(latitude),
235                             Double.parseDouble(longitude),
236                             DistanceUnit.parseDistance(distance));
237    }
238  }
239}