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}