/* (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.catalog.*; import org.geoserver.gwc.wmts.Tuple; import org.geoserver.util.ISO8601Formatter; import org.geoserver.wms.WMS; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.visitor.UniqueVisitor; import org.geotools.util.Converters; import org.geotools.util.Range; import org.opengis.feature.simple.SimpleFeature; import java.io.Serializable; import java.util.*; /** * Some utils methods useful to interact with dimensions. */ public final class DimensionsUtils { /** * Comparator for time domain values, ranges are taken in consideration. */ static final Comparator<Object> TEMPORAL_COMPARATOR = (objectA, objectB) -> { Date dateA = Converters.convert(objectA instanceof Range ? ((Range) objectA).getMinValue() : objectA, Date.class); Date dateB = Converters.convert(objectB instanceof Range ? ((Range) objectB).getMinValue() : objectB, Date.class); return dateA.compareTo(dateB); }; /** * Comparator for numerical domain values, ranges are taken in consideration. */ static final Comparator<Object> NUMERICAL_COMPARATOR = (objectA, objectB) -> { Double numberA = Converters.convert(objectA instanceof Range ? ((Range) objectA).getMinValue() : objectA, Double.class); Double numberB = Converters.convert(objectB instanceof Range ? ((Range) objectB).getMinValue() : objectB, Double.class); return numberA.compareTo(numberB); }; /** * Comparator for custom domain values, time values and numerical values are specially handled. */ static final Comparator<Object> CUSTOM_COMPARATOR = (objectA, objectB) -> { // make sure we are using single values Object valueA = objectA instanceof Range ? ((Range) objectA).getMinValue() : objectA; Object valueB = objectB instanceof Range ? ((Range) objectB).getMinValue() : objectB; // check if we have times or numerical values if (valueA instanceof Date && valueB instanceof Date) { return TEMPORAL_COMPARATOR.compare(objectA, objectB); } if (valueA instanceof Number && valueB instanceof Number) { return NUMERICAL_COMPARATOR.compare(objectA, objectB); } // well it seems we have custom values so let's use strings String stringA = Converters.convert(valueA, String.class); String stringB = Converters.convert(valueB, String.class); return stringA.compareTo(stringB); }; /** * Helper method that will extract a layer dimensions. */ public static List<Dimension> extractDimensions(WMS wms, LayerInfo layerInfo) { ResourceInfo resourceInfo = layerInfo.getResource(); if (resourceInfo instanceof FeatureTypeInfo) { return extractDimensions(wms, layerInfo, (FeatureTypeInfo) resourceInfo); } if (resourceInfo instanceof CoverageInfo) { return extractDimensions(wms, layerInfo, (CoverageInfo) resourceInfo); } return Collections.emptyList(); } /** * Helper method that will extract the dimensions from a feature type info. */ private static List<Dimension> extractDimensions(WMS wms, LayerInfo layerInfo, FeatureTypeInfo typeInfo) { List<Dimension> dimensions = new ArrayList<>(); DimensionInfo timeDimension = typeInfo.getMetadata().get(ResourceInfo.TIME, DimensionInfo.class); if (timeDimension != null) { checkAndAddDimension(dimensions, new VectorTimeDimension(wms, layerInfo, timeDimension)); } DimensionInfo elevationDimension = typeInfo.getMetadata().get(ResourceInfo.ELEVATION, DimensionInfo.class); if (elevationDimension != null) { checkAndAddDimension(dimensions, new VectorElevationDimension(wms, layerInfo, elevationDimension)); } return dimensions; } /** * Helper method that will extract the dimensions from a coverage type info. */ private static List<Dimension> extractDimensions(WMS wms, LayerInfo layerInfo, CoverageInfo typeInfo) { List<Dimension> dimensions = new ArrayList<>(); for (Map.Entry<String, Serializable> entry : typeInfo.getMetadata().entrySet()) { String key = entry.getKey(); Serializable value = entry.getValue(); if (key.equals(ResourceInfo.TIME)) { DimensionInfo dimensionInfo = Converters.convert(value, DimensionInfo.class); checkAndAddDimension(dimensions, new RasterTimeDimension(wms, layerInfo, dimensionInfo)); } else if (key.equals(ResourceInfo.ELEVATION)) { DimensionInfo dimensionInfo = Converters.convert(value, DimensionInfo.class); checkAndAddDimension(dimensions, new RasterElevationDimension(wms, layerInfo, dimensionInfo)); } else if (key.startsWith(ResourceInfo.CUSTOM_DIMENSION_PREFIX)) { DimensionInfo dimensionInfo = Converters.convert(value, DimensionInfo.class); String dimensionName = key.substring(ResourceInfo.CUSTOM_DIMENSION_PREFIX.length()); checkAndAddDimension(dimensions, new RasterCustomDimension(wms, layerInfo, dimensionName, dimensionInfo)); } } return dimensions; } /** * Helper method that adds a dimension to a list of dimensions if the dimension is enabled. */ private static void checkAndAddDimension(List<Dimension> dimensions, Dimension dimension) { // some layers can have a dimension configured but not enable if (dimension.getDimensionInfo().isEnabled()) { dimensions.add(dimension); } } /** * Helper method that simply returns a string representation of the values of a dimension. * Dates and ranges will have a special handling. This method will take in account the * dimension required presentation. */ static List<String> getDomainValuesAsStrings(DimensionInfo dimension, List<Object> values) { if (values == null || values.isEmpty()) { // no domain values so he just return an empty collection return Collections.emptyList(); } List<String> stringValues = new ArrayList<>(); if (DimensionPresentation.LIST == dimension.getPresentation()) { // the dimension representation for this values requires that all the values are listed for (Object value : values) { stringValues.add(formatDomainValue(value)); } } else { // the dimension representation for this values require a compact representation Object minValue = getMinValue(values); Object maxValue = getMaxValue(values); stringValues.add(formatDomainSimpleValue(minValue) + "--" + formatDomainSimpleValue(maxValue)); } return stringValues; } /** * Helper method that converts a domain value to string, range will be correctly handled. */ public static String formatDomainValue(Object value) { if (value instanceof Range) { // this domain value is a range, we use the min and max value Object minValue = ((Range) value).getMinValue(); Object maxValue = ((Range) value).getMaxValue(); return formatDomainSimpleValue(minValue) + "--" + formatDomainSimpleValue(maxValue); } return formatDomainSimpleValue(value); } /** * Helper method that converts a domain value to string. Date values are formatted using the ISO8601 format. */ public static String formatDomainSimpleValue(Object value) { if (value instanceof Date) { // FIXME: is the ISO formatter thread safe or can he be reused multiple times ? ISO8601Formatter formatter = new ISO8601Formatter(); return formatter.format(value); } return value.toString(); } /** * Helper method that return the minimum value. If the first value of the tree set * is a range the minimum value of the range is returned. */ private static Object getMinValue(List<Object> values) { Object minValue = values.get(0); if (minValue instanceof Range) { return ((Range) minValue).getMinValue(); } return minValue; } /** * Helper method that return the maximum value. If the first value of the tree set * is a range the maximum value of the range is returned. */ private static Object getMaxValue(List<Object> values) { Object maxValue = values.get(values.size() - 1); if (maxValue instanceof Range) { return ((Range) maxValue).getMaxValue(); } return maxValue; } /** * Return the min a max values of a tree set of values converted to the provided type. */ static <T> Tuple<T, T> getMinMax(List<Object> values, Class<T> type) { Object minValue = getMinValue(values); Object maxValue = getMaxValue(values); return Tuple.tuple(Converters.convert(minValue, type), Converters.convert(maxValue, type)); } /** * Helper method that simply extract from a feature collection the values of a * specific attribute removing duplicate values. */ static Set<Object> getValuesWithoutDuplicates(String attributeName, FeatureCollection featureCollection, Comparator<Object> comparator) { // using the unique visitor to remove duplicate values UniqueVisitor uniqueVisitor = new UniqueVisitor(attributeName); try { featureCollection.accepts(uniqueVisitor, null); } catch (Exception exception) { throw new RuntimeException("Error visiting collection with unique visitor."); } // make sure the values are sorted using the provided comparator Set<Object> values = new TreeSet<>(comparator); values.addAll(uniqueVisitor.getUnique()); return values; } /** * Helper method that simply extract from a feature collection the values of a * specific attribute keeping duplicate values. */ static List<Object> getValuesWithDuplicates(String attributeName, FeatureCollection featureCollection, Comparator<Object> comparator) { // full data values are returned including duplicate values List<Object> values = new ArrayList<>(); FeatureIterator featuresIterator = featureCollection.features(); while (featuresIterator.hasNext()) { // extracting the feature attribute that contain our dimension value SimpleFeature feature = (SimpleFeature) featuresIterator.next(); values.add(feature.getAttribute(attributeName)); } Collections.sort(values, comparator); return values; } }