/* (c) 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.gwc.wmts.dimensions; import org.geoserver.gwc.wmts.Tuple; import org.geoserver.util.ISO8601Formatter; import org.geotools.gce.imagemosaic.properties.time.TimeParser; import org.geotools.util.DateRange; import org.geotools.util.NumberRange; import org.geotools.util.Range; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; /** * Utilities method to produce histogram from dimension domains values. Two types of * histograms are supported numerical, times and enumerated values. */ final class HistogramUtils { private final static String HISTOGRAM_MAX_THRESHOLD_VARIABLE = "HISTORGRAM_MAX_THRESHOLD"; private final static long HISTOGRAM_MAX_THRESHOLD_DEFAULT = 10000L; private final static long HISTOGRAM_MAX_THRESHOLD = getHistogramMaxThreshold(); private final static String NUMERICAL_DEFAULT_RESOLUTION = "100"; private final static String TIME_DEFAULT_RESOLUTION = "PT1H"; private final static long MAX_ITERATIONS = 10000; private enum HistogramType { NUMERIC, TIME, ENUMERATED } private HistogramUtils() { } /** * Helper method that get the threshold value that will be used to check if the resolution is to high. */ private static long getHistogramMaxThreshold() { String value = System.getProperty(HISTOGRAM_MAX_THRESHOLD_VARIABLE); if (value == null) { // no user provided value, so let's return the default value return HISTOGRAM_MAX_THRESHOLD_DEFAULT; } // using the user provided value return Long.parseLong(value); } /** * Builds an histogram for the provided domain values. The returned tuple will contain the domain * representation and the histogram values. The domain values should be numbers, dates or strings. * Ranges are also supported, the min value will be used to discover the domain values type. */ static Tuple<String, List<Integer>> buildHistogram(List<Object> domainValues, String resolution) { if (domainValues.isEmpty()) { // FIXME: How to represent a domain with no values ? return Tuple.tuple("", Collections.emptyList()); } Tuple<String, List<Range>> buckets = computeBuckets(domainValues, resolution); ArrayList<Integer> histogramValues = new ArrayList<>(buckets.second.size()); for (int i = 0; i < buckets.second.size(); i++) { histogramValues.add(0); } for (Object value : domainValues) { int index = getBucketIndex(buckets.second, (Comparable) value); if (index >= 0) { histogramValues.set(index, histogramValues.get(index) + 1); } } return Tuple.tuple(buckets.first, histogramValues); } /** * Compute the buckets for the given domain values and resolution. */ private static Tuple<String, List<Range>> computeBuckets(List<Object> domainValues, String resolution) { switch (findHistogramType(domainValues)) { case NUMERIC: return getNumericBuckets(domainValues, resolution); case TIME: return getTimeBuckets(domainValues, resolution); default: return getEnumeratedBuckets(domainValues); } } /** * Helper method that just founds the histogram type based on domains values. */ private static HistogramType findHistogramType(List<Object> domainValues) { Object value = domainValues.get(domainValues.size() - 1); if (value instanceof Range) { // this is a range so lets use the min value value = ((Range) value).getMinValue(); } // let's try to find this histogram type if (value instanceof Number) { return HistogramType.NUMERIC; } if (value instanceof Date) { return HistogramType.TIME; } // well by default we consider the histogram to be of type enumerated return HistogramType.ENUMERATED; } /** * Helper method that creates buckets for a numeric domain based on the provided resolution. The returned tuple * will contain the domain representation and the domain buckets. */ private static Tuple<String, List<Range>> getNumericBuckets(List<Object> domainValues, String resolution) { Tuple<Double, Double> minMax = DimensionsUtils.getMinMax(domainValues, Double.class); resolution = resolution != null ? resolution : NUMERICAL_DEFAULT_RESOLUTION; double finalResolution = Double.parseDouble(resolution); double min = minMax.first; double max = Math.max(minMax.second, finalResolution); int i = 0; while ((max - min) / finalResolution >= HISTOGRAM_MAX_THRESHOLD && i < MAX_ITERATIONS) { finalResolution += 10; i++; } String domainString = min + "/" + max + "/" + finalResolution; if ((max - min) / finalResolution == 1) { return Tuple.tuple(domainString, Collections.singletonList(NumberRange.create(min, max))); } List<Range> buckets = new ArrayList<>(); for (double step = min; step < max; step += finalResolution) { double limit = step + finalResolution; if (limit > max) { buckets.add(NumberRange.create(step, max)); break; } buckets.add(NumberRange.create(step, limit)); } return Tuple.tuple(domainString, buckets); } /** * Helper method that creates buckets for a time domain based on the provided resolution. The returned tuple * will contain the domain representation and the domain buckets. */ private static Tuple<String, List<Range>> getTimeBuckets(List<Object> domainValues, String resolution) { Tuple<Date, Date> minMax = DimensionsUtils.getMinMax(domainValues, Date.class); resolution = resolution != null ? resolution : TIME_DEFAULT_RESOLUTION; Tuple<String, List<Date>> intervals = getDateIntervals(minMax, resolution); int i = 0; while (intervals.second.size() >= HISTOGRAM_MAX_THRESHOLD && i < MAX_ITERATIONS) { i++; resolution = "PT" + i + "M"; intervals = getDateIntervals(minMax, resolution); } if (intervals.second.size() == 1) { return Tuple.tuple(intervals.first, Collections.singletonList(new DateRange(minMax.first, minMax.second))); } List<Range> buckets = new ArrayList<>(); Date previous = intervals.second.get(0); for (int step = 1; step < intervals.second.size(); step++) { buckets.add(new DateRange(previous, intervals.second.get(step))); previous = intervals.second.get(step); } return Tuple.tuple(intervals.first, buckets); } /** * Helper method that computes the time intervals for a certain resolution. */ private static Tuple<String, List<Date>> getDateIntervals(Tuple<Date, Date> minMax, String resolution) { ISO8601Formatter dateFormatter = new ISO8601Formatter(); String domainString = dateFormatter.format(minMax.first); domainString += "/" + dateFormatter.format(minMax.second) + "/" + resolution; TimeParser timeParser = new TimeParser(); try { return Tuple.tuple(domainString, timeParser.parse(domainString)); } catch (ParseException exception) { throw new RuntimeException(String.format("Error parsing time resolution '%s'.", resolution), exception); } } /** * Helper method that creates buckets for an enumerated domain. The returned tuple will contain the domain * representation and the domain buckets. Note that in this case the resolution will be ignored. */ private static Tuple<String, List<Range>> getEnumeratedBuckets(List<Object> domainValues) { StringBuilder domain = new StringBuilder(); List<Range> buckets = new ArrayList<>(); for (Object value : domainValues) { String stringValue = value.toString(); domain.append(stringValue).append(','); buckets.add(new EnumeratedRange(stringValue)); } domain.delete(domain.length() - 1, domain.length()); // FIXME: we don't really have a domain range we simply enumerate all the values. return Tuple.tuple(domain.toString(), buckets); } /** * Simple helper method that founds the bucket for a value or returns -1 otherwise. */ @SuppressWarnings("unchecked") private static <T extends Comparable> int getBucketIndex(List<Range> buckets, T value) { for (int i = 0; i < buckets.size(); i++) { if (buckets.get(i).contains(value)) { return i; } } return -1; } /** * Range class used to represent enumerated values. The contains operation will * return true if the provided values is equal to the enumerated value. */ private static final class EnumeratedRange extends Range<String> { public EnumeratedRange(String value) { super(String.class, value, true, value, true); } } }