/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2016, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.feature.visitor; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.geotools.feature.DefaultFeatureCollection; import org.opengis.feature.Feature; import org.opengis.feature.FeatureVisitor; import org.opengis.feature.simple.SimpleFeature; import org.opengis.filter.expression.Expression; import org.opengis.util.ProgressListener; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.Point; /** * Group features by one or several attributes and applies an aggregator visitor to each group. */ public class GroupByVisitor implements FeatureCalc, FeatureAttributeVisitor { private final Aggregate aggregate; private final Expression expression; private final FeatureCalc visitorProtoType; private final List<Expression> groupByAttributes; private final ProgressListener progressListener; private final InMemoryGroupBy inMemoryGroupBy = new InMemoryGroupBy(); private CalcResult optimizationResult = CalcResult.NULL_RESULT; public GroupByVisitor(Aggregate aggregateVisitor, Expression expression, List<Expression> groupByAttributes, ProgressListener progressListener) { this.aggregate = aggregateVisitor; this.expression = expression; this.groupByAttributes = groupByAttributes; this.progressListener = progressListener; visitorProtoType = aggregateVisitor.create(expression); } public boolean wasOptimized() { return optimizationResult != null; } public boolean wasVisited() { return !inMemoryGroupBy.groupByIndexes.isEmpty(); } /** * This method computes and returns the group by visitor result. If the computation was optimized * the optimization result is returned otherwise the result is computed in memory. If for some * reason an optimization result exists and there are visited features, an in memory computation * is performed and is merged with the existing optimization results. * * @return group by visitor result */ @Override public CalcResult getResult() { // do a in memory computation for any visited feature Map<List<Object>, CalcResult> results = inMemoryGroupBy.visit(); // create the result, if no feature was visited this will be an empty result that can be safely merged GroupByResult result = new GroupByResult(results, aggregate, groupByAttributes); if (optimizationResult == CalcResult.NULL_RESULT) { // there is no optimization result so we just return the created one return result; } // an optimization result exists, we merge both return optimizationResult.merge(result); } @Override public void visit(Feature feature) { inMemoryGroupBy.index((SimpleFeature) feature); } public Expression getExpression() { return expression; } public FeatureVisitor getAggregateVisitor() { return visitorProtoType; } public List<Expression> getGroupByAttributes() { return groupByAttributes; } /** * Methods that allow optimizations to directly set the group by visitor result instead * of computing it visiting all the features. Aggregate visitor results are wrapped with * the appropriate feature calculation type. * * @param value the group by visitor result */ public void setValue(List<GroupByRawResult> value) { Map<List<Object>, CalcResult> results = new HashMap<>(); for (GroupByRawResult groupByRawResult : value) { // wrap the aggregate visitor result with the appropriate feature calculation type results.put(groupByRawResult.groupByValues, aggregate.wrap(expression, groupByRawResult.visitorValue)); } // create a new group by result using the raw values returned by the optimization GroupByResult newResult = new GroupByResult(results, aggregate, groupByAttributes); if (optimizationResult == CalcResult.NULL_RESULT) { // if no current result we simply return the new one optimizationResult = newResult; } else { // if a result already exists we merge it with the new one optimizationResult = optimizationResult.merge(newResult); } } /** * Helper class that should be used by optimizations to set the results. */ public static class GroupByRawResult { final List<Object> groupByValues; final Object visitorValue; public GroupByRawResult(List<Object> groupByValues, Object visitorsValue) { this.groupByValues = groupByValues; this.visitorValue = visitorsValue; } } /** * Helper class that do the computations for the group by visitor in memory. */ private class InMemoryGroupBy { // feature collections grouped by the group by attributes private final Map<List<Object>, DefaultFeatureCollection> groupByIndexes = new HashMap<>(); /** * Add a feature to the appropriate group by feature collection. * * @param feature the feature to be indexed */ void index(SimpleFeature feature) { // list of group by attributes values List<Object> groupByValues = groupByAttributes.stream() .map(expression -> expression.evaluate(feature)).collect(Collectors.toList()); // check if a feature collection already for the group by values DefaultFeatureCollection featureCollection = groupByIndexes.get(groupByValues); if (featureCollection == null) { // we create a feature collection for the group by values featureCollection = new DefaultFeatureCollection(); groupByIndexes.put(groupByValues, featureCollection); } featureCollection.add(feature); } /** * We apply a copy of the aggregation visitor to each feature collection. * * @return the result of applying the aggregation visitor to eac feature collection */ Map<List<Object>, CalcResult> visit() { Map<List<Object>, CalcResult> results = new HashMap<>(); for (Map.Entry<List<Object>, DefaultFeatureCollection> entry : groupByIndexes.entrySet()) { // creating a new aggregation visitor for the current feature collection FeatureCalc visitor = aggregate.create(expression); try { // visiting the feature collection with the aggregation visitor entry.getValue().accepts(visitor, progressListener); } catch (Exception exception) { throw new RuntimeException("Error visiting features collections.", exception); } // we add the aggregation visitor to the results results.put(entry.getKey(), visitor.getResult()); } return results; } } /** * This class implements the feature calculation result of the group by visitor. */ public static class GroupByResult implements CalcResult { private final Map<List<Object>, CalcResult> results; private final Aggregate aggregateVisitor; private final List<Expression> groupByAttributes; public GroupByResult(Map<List<Object>, CalcResult> results, Aggregate aggregateVisitor, List<Expression> groupByAttributes) { this.results = results; this.aggregateVisitor = aggregateVisitor; this.groupByAttributes = groupByAttributes; } public Map<List<Object>, CalcResult> getResults() { return results; } public Aggregate getAggregateVisitor() { return aggregateVisitor; } public List<Expression> getGroupByAttributes() { return groupByAttributes; } @Override public boolean isCompatible(CalcResult newResult) { if (newResult == CalcResult.NULL_RESULT) { // compatible with NULL result return true; } if (!(newResult instanceof GroupByResult)) { // not compatible with results that are not group by results return false; } GroupByResult groupByResult = (GroupByResult) newResult; // compatible only if the aggregation visitor is the same and the group by attributes are the same return aggregateVisitor == groupByResult.getAggregateVisitor() && groupByAttributes.equals(groupByResult.getGroupByAttributes()); } @Override public CalcResult merge(CalcResult newResult) { if (!isCompatible(newResult)) { // not compatible results throw new IllegalArgumentException(String.format( "Feature calculation result '%s' is not compatible it this result '%s'.", newResult.getClass().getSimpleName(), GroupByResult.class.getSimpleName())); } if (newResult == CalcResult.NULL_RESULT) { // if the new result is a NULL result we simply return a copy of this result return new GroupByResult(results, aggregateVisitor, groupByAttributes); } // the merged results are initialized with the content of this result Map<List<Object>, CalcResult> mergedResults = new HashMap<>(results); for (Map.Entry<List<Object>, CalcResult> entry : ((GroupByResult) newResult).getResults().entrySet()) { // check if this result contains the same aggregation result CalcResult existingResult = mergedResults.get(entry.getKey()); if (existingResult != null) { // the aggregation result exist in both results so we merge them mergedResults.put(entry.getKey(), existingResult.merge(entry.getValue())); } else { // the aggregation result only exists in the new result mergedResults.put(entry.getKey(), entry.getValue()); } } // we return a new group by result with the merged values return new GroupByResult(mergedResults, aggregateVisitor, groupByAttributes); } @Override public Object getValue() { return toArray(); } @Override public int toInt() { return 0; } @Override public double toDouble() { return 0; } @Override public String toString() { return null; } @Override public long toLong() { return 0; } @Override public float toFloat() { return 0; } @Override public Geometry toGeometry() { return null; } @Override public Envelope toEnvelope() { return null; } @Override public Point toPoint() { return null; } @Override public Set toSet() { return results.entrySet().stream().map(this::entryToArray).collect(Collectors.toSet()); } @Override public List toList() { return results.entrySet().stream().map(this::entryToArray).collect(Collectors.toList()); } @Override public Object[] toArray() { return results.entrySet().stream().map(this::entryToArray).toArray(); } /** * The keys of the map will be List instead of arrays, since arrays don't give a decent hash code. */ @Override public Map toMap() { return results.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().getValue()) ); } private Object[] entryToArray(Map.Entry<List<Object>, CalcResult> entry) { Object[] result = Arrays.copyOf(entry.getKey().toArray(), entry.getKey().size() + 1); result[entry.getKey().size()] = entry.getValue().getValue(); return result; } } @Override public List<Expression> getExpressions() { List<Expression> result = new ArrayList<>(groupByAttributes); result.add(expression); return result; } }