001package org.gbif.registry.search.util;
002
003import org.gbif.api.model.registry.eml.temporal.DateRange;
004import org.gbif.api.model.registry.eml.temporal.SingleDate;
005import org.gbif.api.model.registry.eml.temporal.TemporalCoverage;
006
007import java.util.Calendar;
008import java.util.List;
009import java.util.Set;
010import java.util.SortedSet;
011import java.util.TreeSet;
012
013import com.google.common.base.Preconditions;
014import com.google.common.collect.Lists;
015import com.google.common.collect.Range;
016import com.google.common.collect.Sets;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * A utility to extract decades (eg 1980, 1840) or centuries (eg 1400, 1500, 1600) from TemporalCoverages as
022 * a list of integers. Max/min bounderies for supplied values can be specific in the constructor for both a decade
023 * and a century range to avoid large list of decades for very old or future periods, mostly for bad data.
024 */
025public class TimeSeriesExtractor {
026  private static final Logger LOG = LoggerFactory.getLogger(TimeSeriesExtractor.class);
027
028  private final int minCentury;
029  private final int maxCentury;
030  private final int minDecade;
031  private final int maxDecade;
032  private final Range<Integer> decadeRange;
033
034  /**
035   * @param minCentury the minimum value ever extracted, eg 1500
036   * @param maxCentury the maximum value ever extracted, eg 2400
037   * @param minDecade the lowest decade value to be extracted, needs to be within the century range. Eg 1870
038   * @param maxDecade the largest decade value to be extracted, needs to be within the century range. Eg 2020
039   */
040  public TimeSeriesExtractor(int minCentury, int maxCentury, int minDecade, int maxDecade) {
041    Preconditions.checkArgument(minDecade <= maxDecade, "MinDecade must be below or equals maxDecade");
042    Preconditions.checkArgument(minCentury <= minDecade, "Century limits must be wider than decade boundaries");
043    Preconditions.checkArgument(maxCentury >= maxDecade, "Century limits must be wider than decade boundaries");
044    Preconditions.checkArgument(minCentury % 100 == 0, "minCentury needs to be a multiple of 100");
045    Preconditions.checkArgument(maxCentury % 100 == 0, "maxCentury needs to be a multiple of 100");
046    Preconditions.checkArgument(minDecade % 10 == 0, "minDecade needs to be a multiple of 10");
047    Preconditions.checkArgument(maxDecade % 10 == 0, "maxDecade needs to be a multiple of 10");
048    this.minDecade = minDecade;
049    this.maxDecade = maxDecade;
050    this.minCentury = minCentury;
051    this.maxCentury = maxCentury;
052    this.decadeRange = Range.closed(minDecade, maxDecade);
053  }
054
055  private Set<Integer> decadesFromInt(int start, int end) {
056    Set<Integer> decades = Sets.newHashSet();
057
058    if (start > end) {
059      LOG.warn("Potentially inverted year range: {} - {}", start, end);
060      return decades;
061    }
062
063    Range<Integer> range = Range.closed(start, end);
064    // produce centuries only if outside of decade range
065    if (!decadeRange.encloses(range)) {
066      // skip anything below min/max
067      int startC = 100 * (int) Math.floor(minmax(minCentury, maxCentury, start) / 100d);
068      int endC = 100 * (int) Math.floor(minmax(minCentury, maxCentury,  end) / 100d);
069      for (int year = startC; year <= endC; year += 100) {
070        decades.add(year);
071      }
072
073    }
074
075    // Produce decades if falling within the decade range
076    if (decadeRange.isConnected(range)) {
077      int startD = 10 * (int) Math.floor(minmax(minDecade, maxDecade, start) / 10d);
078      int endD = 10 * (int) Math.floor(minmax(minDecade, maxDecade, end) / 10d);
079      for (int year = startD; year <= endD; year += 10) {
080        decades.add(year);
081      }
082    }
083
084    return decades;
085  }
086
087  private int minmax(int min, int max, int val){
088    return val < min ? min : (val > max ? max : val);
089  }
090
091  /**
092   * Produces a list of 4 digit decades or centuries with no duplicates, following normal ordering.
093   * TODO: handle VerbatimTimePeriod?
094   * 
095   * @param temporalCoverages the various time periods
096   * @return a list of 4 digit decades with no duplicates, ordered numerically
097   */
098  public List<Integer> extractDecades(List<TemporalCoverage> temporalCoverages) {
099    SortedSet<Integer> decades = new TreeSet<Integer>();
100    if (temporalCoverages != null && !temporalCoverages.isEmpty()) {
101      for (TemporalCoverage tc : temporalCoverages) {
102        if (tc instanceof DateRange) {
103          DateRange dr = (DateRange) tc;
104          Calendar cal = Calendar.getInstance();
105          if (dr.getStart() != null && dr.getEnd() != null) {
106            cal.setTime(dr.getStart());
107            int start = cal.get(Calendar.YEAR);
108            cal.setTime(dr.getEnd());
109            int end = cal.get(Calendar.YEAR);
110            decades.addAll(decadesFromInt(start, end));
111          }
112        } else if (tc instanceof SingleDate) {
113          SingleDate sd = (SingleDate) tc;
114          if (sd.getDate() != null) {
115            Calendar cal = Calendar.getInstance();
116            cal.setTime(sd.getDate());
117            int year = cal.get(Calendar.YEAR);
118            decades.addAll(decadesFromInt(year, year));
119          }
120        }
121      }
122    }
123    return Lists.newArrayList(decades);
124  }
125
126}