/*
* 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 static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.geotools.data.DataUtilities;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.util.NullProgressListener;
import org.junit.BeforeClass;
import org.junit.Test;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import com.vividsolutions.jts.io.WKTReader;
/**
* This class implement the test that checks the group by visitor behavior with different aggregate visitors.
* All the computations will be performed in memory. Optimization tests are implemented in the specific stores.
*/
public class GroupByVisitorTest {
private static WKTReader wktParser = new WKTReader();
private static SimpleFeatureType buildingType;
private static FeatureCollection featureCollection;
@BeforeClass
public static void setup() throws Exception {
// the building feature type that will be used during the tests
buildingType = DataUtilities.createType("buildings", "id:Integer,building_id:String," +
"building_type:String,energy_type:String,energy_consumption:Double,geo:Geometry");
// the building features that will be used during the tests
SimpleFeature[] buildingFeatures = new SimpleFeature[]{
SimpleFeatureBuilder.build(buildingType, new Object[]{1, "SCHOOL_A", "SCHOOL", "FLOWING_WATER", 50.0, wktParser.read("POINT(-5 -5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{2, "SCHOOL_A", "SCHOOL", "NUCLEAR", 10.0, wktParser.read("POINT(-5 -5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{3, "SCHOOL_A", "SCHOOL", "WIND", 20.0, wktParser.read("POINT(-5 -5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{4, "SCHOOL_B", "SCHOOL", "SOLAR", 30.0, wktParser.read("POINT(5 5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{5, "SCHOOL_B", "SCHOOL", "FUEL", 60.0, wktParser.read("POINT(5 5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{6, "SCHOOL_B", "SCHOOL", "NUCLEAR", 10.0, wktParser.read("POINT(5 5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{7, "FABRIC_A", "FABRIC", "FLOWING_WATER", 500.0, wktParser.read("POINT(-5 5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{8, "FABRIC_A", "FABRIC", "NUCLEAR", 150.0, wktParser.read("POINT(-5 5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{9, "FABRIC_B", "FABRIC", "WIND", 20.0, wktParser.read("POINT(5 -5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{10, "FABRIC_B", "FABRIC", "SOLAR", 30.0, wktParser.read("POINT(5 -5)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{11, "HOUSE_A", "HOUSE", "FUEL", 6.0, wktParser.read("POINT(0 0)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{12, "HOUSE_B", "HOUSE", "NUCLEAR", 4.0, wktParser.read("POINT(0 0)")}, null),
};
// creating the bulding featrue collection
featureCollection = DataUtilities.collection(buildingFeatures);
}
@Test
public void testGroupByWithAverageAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "Average", "building_type", new Object[][]{
new Object[]{"SCHOOL", 30.0},
new Object[]{"FABRIC", 175.0},
new Object[]{"HOUSE", 5.0}
});
}
@Test
public void testGroupByWithAverageAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "Average", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 500.0},
new Object[]{"FABRIC", "NUCLEAR", 150.0},
new Object[]{"FABRIC", "SOLAR", 30.0},
new Object[]{"FABRIC", "WIND", 20.0},
new Object[]{"HOUSE", "FUEL", 6.0},
new Object[]{"HOUSE", "NUCLEAR", 4.0},
new Object[]{"SCHOOL", "FLOWING_WATER", 50.0},
new Object[]{"SCHOOL", "FUEL", 60.0},
new Object[]{"SCHOOL", "NUCLEAR", 10.0},
new Object[]{"SCHOOL", "SOLAR", 30.0},
new Object[]{"SCHOOL", "WIND", 20.0}
});
}
@Test
public void testGroupByWithCountAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "Count", "building_type", new Object[][]{
new Object[]{"SCHOOL", 6},
new Object[]{"FABRIC", 4},
new Object[]{"HOUSE", 2}
});
}
@Test
public void testGroupByWithCountAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "Count", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 1},
new Object[]{"FABRIC", "NUCLEAR", 1},
new Object[]{"FABRIC", "SOLAR", 1},
new Object[]{"FABRIC", "WIND", 1},
new Object[]{"HOUSE", "FUEL", 1},
new Object[]{"HOUSE", "NUCLEAR", 1},
new Object[]{"SCHOOL", "FLOWING_WATER", 1},
new Object[]{"SCHOOL", "FUEL", 1},
new Object[]{"SCHOOL", "NUCLEAR", 2},
new Object[]{"SCHOOL", "SOLAR", 1},
new Object[]{"SCHOOL", "WIND", 1}
});
}
@Test
public void testGroupByWithMaxAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "Max", "building_type", new Object[][]{
new Object[]{"SCHOOL", 60.0},
new Object[]{"FABRIC", 500.0},
new Object[]{"HOUSE", 6.0}
});
}
@Test
public void testGroupByWithMaxAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "Max", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 500.0},
new Object[]{"FABRIC", "NUCLEAR", 150.0},
new Object[]{"FABRIC", "SOLAR", 30.0},
new Object[]{"FABRIC", "WIND", 20.0},
new Object[]{"HOUSE", "FUEL", 6.0},
new Object[]{"HOUSE", "NUCLEAR", 4.0},
new Object[]{"SCHOOL", "FLOWING_WATER", 50.0},
new Object[]{"SCHOOL", "FUEL", 60.0},
new Object[]{"SCHOOL", "NUCLEAR", 10.0},
new Object[]{"SCHOOL", "SOLAR", 30.0},
new Object[]{"SCHOOL", "WIND", 20.0}
});
}
@Test
public void testGroupByWithMedianAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "Median", "building_type", new Object[][]{
new Object[]{"SCHOOL", 25.0},
new Object[]{"FABRIC", 90.0},
new Object[]{"HOUSE", 5.0}
});
}
@Test
public void testGroupByWithMedianAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "Median", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 500.0},
new Object[]{"FABRIC", "NUCLEAR", 150.0},
new Object[]{"FABRIC", "SOLAR", 30.0},
new Object[]{"FABRIC", "WIND", 20.0},
new Object[]{"HOUSE", "FUEL", 6.0},
new Object[]{"HOUSE", "NUCLEAR", 4.0},
new Object[]{"SCHOOL", "FLOWING_WATER", 50.0},
new Object[]{"SCHOOL", "FUEL", 60.0},
new Object[]{"SCHOOL", "NUCLEAR", 10.0},
new Object[]{"SCHOOL", "SOLAR", 30.0},
new Object[]{"SCHOOL", "WIND", 20.0}
});
}
@Test
public void testGroupByWithMinAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "Min", "building_type", new Object[][]{
new Object[]{"SCHOOL", 10.0},
new Object[]{"FABRIC", 20.0},
new Object[]{"HOUSE", 4.0}
});
}
@Test
public void testGroupByWithMinAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "Min", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 500.0},
new Object[]{"FABRIC", "NUCLEAR", 150.0},
new Object[]{"FABRIC", "SOLAR", 30.0},
new Object[]{"FABRIC", "WIND", 20.0},
new Object[]{"HOUSE", "FUEL", 6.0},
new Object[]{"HOUSE", "NUCLEAR", 4.0},
new Object[]{"SCHOOL", "FLOWING_WATER", 50.0},
new Object[]{"SCHOOL", "FUEL", 60.0},
new Object[]{"SCHOOL", "NUCLEAR", 10.0},
new Object[]{"SCHOOL", "SOLAR", 30.0},
new Object[]{"SCHOOL", "WIND", 20.0}
});
}
@Test
public void testGroupByWithStdDevAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "StdDev", "building_type", new Object[][]{
new Object[]{"SCHOOL", 19.149},
new Object[]{"FABRIC", 194.487},
new Object[]{"HOUSE", 1.0}
});
}
@Test
public void testGroupByWithStdDevAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "StdDev", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 0.0},
new Object[]{"FABRIC", "NUCLEAR", 0.0},
new Object[]{"FABRIC", "SOLAR", 0.0},
new Object[]{"FABRIC", "WIND", 0.0},
new Object[]{"HOUSE", "FUEL", 0.0},
new Object[]{"HOUSE", "NUCLEAR", 0.0},
new Object[]{"SCHOOL", "FLOWING_WATER", 0.0},
new Object[]{"SCHOOL", "FUEL", 0.0},
new Object[]{"SCHOOL", "NUCLEAR", 0.0},
new Object[]{"SCHOOL", "SOLAR", 0.0},
new Object[]{"SCHOOL", "WIND", 0.0}
});
}
@Test
public void testGroupByWithSumDevAndSingleAttribute() throws Exception {
testVisitor("energy_consumption", "Sum", "building_type", new Object[][]{
new Object[]{"SCHOOL", 180.0},
new Object[]{"FABRIC", 700.0},
new Object[]{"HOUSE", 10.0}
});
}
@Test
public void testGroupByWithSumDevAndTwoAttributes() throws Exception {
testVisitor("energy_consumption", "Sum", "building_type", "energy_type", new Object[][]{
new Object[]{"FABRIC", "FLOWING_WATER", 500.0},
new Object[]{"FABRIC", "NUCLEAR", 150.0},
new Object[]{"FABRIC", "SOLAR", 30.0},
new Object[]{"FABRIC", "WIND", 20.0},
new Object[]{"HOUSE", "FUEL", 6.0},
new Object[]{"HOUSE", "NUCLEAR", 4.0},
new Object[]{"SCHOOL", "FLOWING_WATER", 50.0},
new Object[]{"SCHOOL", "FUEL", 60.0},
new Object[]{"SCHOOL", "NUCLEAR", 20.0},
new Object[]{"SCHOOL", "SOLAR", 30.0},
new Object[]{"SCHOOL", "WIND", 20.0}
});
}
@Test
public void testMergingGroupByResults() throws Exception {
// creating the features collections that will be used to test the merge behavior
FeatureCollection featureCollectionA = featureCollection;
FeatureCollection featureCollectionB = DataUtilities.collection(new SimpleFeature[]{
SimpleFeatureBuilder.build(buildingType, new Object[]{1, "SCHOOL_C", "SCHOOL", "NUCLEAR", 100.0, wktParser.read("POINT(-15 -15)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{1, "SCHOOL_C", "SCHOOL", "FUEL", 15.0, wktParser.read("POINT(-15 -15)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{2, "FABRIC_C", "FABRIC", "NUCLEAR", 250.0, wktParser.read("POINT(-25 -25)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{2, "FABRIC_C", "FABRIC", "WIND", 75.0, wktParser.read("POINT(-25 -25)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{2, "HOUSE_C", "HOUSE", "WIND", 10.0, wktParser.read("POINT(-35 -35)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{2, "HOUSE_C", "HOUSE", "DARK_MATTER", 850.0, wktParser.read("POINT(-35 -35)")}, null),
SimpleFeatureBuilder.build(buildingType, new Object[]{2, "THEATER_A", "THEATER", "WIND", 200.0, wktParser.read("POINT(-45 -45)")}, null)
});
// we visit the first feature collection calculating the energy consumption average by building type
GroupByVisitor visitorA = executeVisitor(featureCollectionA, "energy_consumption", "Average", new String[]{"building_type"});
checkResults(visitorA.getResult(), new Object[][]{
new Object[]{"SCHOOL", 30.0},
new Object[]{"FABRIC", 175.0},
new Object[]{"HOUSE", 5.0}
});
// we visit the second feature collection calculating the energy consumption average by building type
GroupByVisitor visitorB = executeVisitor(featureCollectionB, "energy_consumption", "Average", new String[]{"building_type"});
checkResults(visitorB.getResult(), new Object[][]{
new Object[]{"SCHOOL", 57.5},
new Object[]{"FABRIC", 162.5},
new Object[]{"HOUSE", 430.0},
new Object[]{"THEATER", 200.0}
});
// we merge the result of the two previous visitors
checkResults(visitorA.getResult().merge(visitorB.getResult()), new Object[][]{
new Object[]{"SCHOOL", 36.875},
new Object[]{"FABRIC", 170.833},
new Object[]{"HOUSE", 217.5},
new Object[]{"THEATER", 200.0}
});
}
@Test
public void testFeatureAttributeVisitor() {
GroupByVisitor visitor = buildVisitor("energy_consumption", "Average", new String[]{"building_type"});
List<Expression> expressions = visitor.getExpressions();
Set<String> names = new HashSet<>();
for (Expression expression : expressions) {
PropertyName pn = (PropertyName) expression;
names.add(pn.getPropertyName());
}
assertEquals(2, names.size());
assertTrue(names.contains("energy_consumption"));
assertTrue(names.contains("building_type"));
}
private void testVisitor(String aggregateAttribute, String aggregateVisitor,
String groupByAttribute, Object[][] expectedResults) throws Exception {
testVisitor(aggregateAttribute, aggregateVisitor, new String[]{groupByAttribute}, expectedResults);
}
private void testVisitor(String aggregateAttribute, String aggregateVisitor, String firstGroupByAttribute,
String secondGroupByAttribute, Object[][] expectedResults) throws Exception {
testVisitor(aggregateAttribute, aggregateVisitor, new String[]{firstGroupByAttribute, secondGroupByAttribute}, expectedResults);
}
private void testVisitor(String aggregateAttribute, String aggregateVisitor,
String[] groupByAttributes, Object[][] expectedResults) throws Exception {
GroupByVisitor visitor = executeVisitor(featureCollection, aggregateAttribute, aggregateVisitor, groupByAttributes);
checkResults(visitor.getResult(), expectedResults);
}
/**
* Helper method that construct the group by visitor and visit the given feature collection. The visitor result
* is also checked against the expected result.
*/
private GroupByVisitor executeVisitor(FeatureCollection featureCollection, String aggregateAttribute,
String aggregateVisitor, String[] groupByAttributes) throws Exception {
GroupByVisitor visitor = buildVisitor(aggregateAttribute, aggregateVisitor,
groupByAttributes);
featureCollection.accepts(visitor, new NullProgressListener());
return visitor;
}
private GroupByVisitor buildVisitor(String aggregateAttribute, String aggregateVisitor,
String[] groupByAttributes) {
GroupByVisitorBuilder visitorBuilder = new GroupByVisitorBuilder()
.withAggregateAttribute(aggregateAttribute, buildingType)
.withAggregateVisitor(aggregateVisitor);
for (String groupByAttribute : groupByAttributes) {
visitorBuilder.withGroupByAttribute(groupByAttribute, buildingType);
}
return visitorBuilder.build();
}
/**
* Helper method the checks if the calculation result contains the expected result.
*/
private void checkResults(CalcResult calcResult, Object[][] expectedResults) {
assertThat(calcResult, notNullValue());
Object[] results = calcResult.toArray();
assertThat(results.length, is(expectedResults.length));
for (Object[] expectedResult : expectedResults) {
assertThat(contains(results, expectedResult), is(true));
}
}
/**
* Helper method that checks if the number of results and the number of expected results
* are the same and if the expected results are contained in the results.
*/
private boolean contains(Object[] results, Object[] expectedResult) {
for (Object result : results) {
assertThat(result, instanceOf(Object[].class));
if (checkArraysAreEqual((Object[]) result, expectedResult)) {
return true;
}
}
return false;
}
/**
* Helper method that compare two arrays. This method will compare double values by difference (0.001).
*/
private boolean checkArraysAreEqual(Object[] arrayA, Object[] arrayB) {
assertThat(arrayA, notNullValue());
assertThat(arrayB, notNullValue());
if (arrayA.length != arrayB.length) {
return false;
}
for (int i = 0; i < arrayA.length; i++) {
assertThat(arrayA[i], notNullValue());
assertThat(arrayB[i], notNullValue());
if (!arrayA[i].getClass().equals(arrayB[i].getClass())) {
return false;
}
if (arrayA[i] instanceof Double) {
double difference = Math.abs((double) arrayA[i] - (double) arrayB[i]);
if (difference > 0.001) {
return false;
}
} else if (!arrayA[i].equals(arrayB[i])) {
return false;
}
}
return true;
}
}