/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, 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.Collections;
import java.util.List;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.IllegalFilterException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
/**
* Calculates the median of an attribute in all features of a collection
*
* @author Cory Horner, Refractions
*
* @since 2.2.M2
*
* @source $URL$
*/
public class MedianVisitor implements FeatureCalc {
private Expression expr;
private List list = new ArrayList();
/**
* This var is only used to store the median for optimized functions, where
* we don't have a complete list, but just the answer instead (merging will
* be disabled until some cool code is written to handle this). Only
* setValue(median) should write to this var. If this value is not null, it
* takes priority over list.
*/
private Object median = null;
public MedianVisitor(String attributeTypeName) {
FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
expr = factory.property(attributeTypeName);
}
public MedianVisitor(int attributeTypeIndex, SimpleFeatureType type)
throws IllegalFilterException {
FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
expr = factory.property(type.getDescriptor(attributeTypeIndex).getLocalName());
}
public MedianVisitor(String attrName, SimpleFeatureType type)
throws IllegalFilterException {
FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
expr = factory.property(type.getDescriptor(attrName).getLocalName());
}
public MedianVisitor(Expression expr) throws IllegalFilterException {
this.expr = expr;
}
public void init(SimpleFeatureCollection collection) {
//do nothing
}
public void visit(SimpleFeature feature) {
visit((org.opengis.feature.Feature)feature);
}
public void visit(org.opengis.feature.Feature feature) {
/**
* Visitor function
*/
Object result = expr.evaluate(feature);
if (result instanceof Comparable) {
Comparable value = (Comparable) result;
list.add(value);
} else {
throw new IllegalStateException("Expression is not comparable!");
}
}
public Expression getExpression() {
return expr;
}
/**
* Return the median of all features in the collection
*/
public Object getMedian() {
if (median != null) {
//median was overwritten by an optimization
return median;
} else {
//we're got a list of items, determine the median...
Object newMedian = findMedian(list);
if (newMedian == null) {
throw new IllegalStateException(
"Must visit before median value is ready!");
}
return newMedian;
}
}
/**
* Reset the stored information about the median.
*/
public void reset() {
this.list.clear();
this.median = null;
}
public CalcResult getResult() {
if (median != null) {
// median was overwritten by an optimization
return new MedianResult(median);
} else if (list.size() < 1) {
// no items in the list
return CalcResult.NULL_RESULT;
} else {
// we have a list; create a CalcResult containing the list
return new MedianResult(list);
}
}
public void setValue(List list) {
reset();
this.list = list;
}
public void setValue(Comparable median) {
reset();
this.median = median;
}
public static class MedianResult extends AbstractCalcResult {
private List list;
/**
* When an optimization is used, median will have a value and list will
* not. This var takes priority over list.
*/
private Object median;
public MedianResult(List newList) {
this.list = newList;
this.median = null;
}
public MedianResult(Object median) {
this.list = null;
this.median = median;
}
public List getList() {
return list;
}
public Object getValue() {
if (median != null) {
return median;
} else {
return findMedian(list);
}
}
public boolean isCompatible(CalcResult targetResults) {
//list each calculation result which can merge with this type of result
if (targetResults instanceof MedianResult || targetResults == CalcResult.NULL_RESULT) return true;
return false;
}
public boolean isOptimized() {
if (median != null) return true;
else return false;
}
public CalcResult merge(CalcResult resultsToAdd) {
if (!isCompatible(resultsToAdd)) {
throw new IllegalArgumentException(
"Parameter is not a compatible type");
}
if(resultsToAdd == CalcResult.NULL_RESULT) {
return this;
}
if (resultsToAdd instanceof MedianResult) {
MedianResult moreResults = (MedianResult) resultsToAdd;
//ensure both MedianResults are NOT optimized
if (isOptimized() || moreResults.isOptimized()) {
throw new IllegalArgumentException("Optimized median results cannot be merged.");
}
//merge away...
List toAdd = (ArrayList) moreResults.getList();
List newList = new ArrayList();
//extract each item to an array, and convert to a common data type
int size = list.size() + toAdd.size();
Object[] values = new Object[size];
int i;
for (i = 0; i < list.size(); i++)
values[i] = list.get(i);
for (int j = 0; j < toAdd.size(); j++)
values[i+j] = toAdd.get(j);
Class bestClass = CalcUtil.bestClass(values);
for (int k = 0; k < size; k++) {
if (values[k].getClass() != bestClass)
values[k] = CalcUtil.convert(values[k], bestClass);
newList.add((Comparable) values[k]);
}
return new MedianResult(newList);
} else {
throw new IllegalArgumentException(
"The CalcResults claim to be compatible, but the appropriate merge method has not been implemented.");
}
}
}
/**
* Given a list, determines the median value and returns it. For numbers,
* the middle value is returned, or the average of the two middle numbers if
* there are an even number of elements. For non-numeric values (strings,
* etc) where the number of elements is even, a list containing the two
* middle elements is returned.
*
* @param list an arraylist which is to be sorted and its median extracted
* @return the median
*/
private static Object findMedian(List list) {
if (list.size() < 1) {
return null;
}
Object median;
Collections.sort(list);
int index = -1;
index = (int) list.size() / 2;
if ((list.size() % 2) == 0) {
// even number of elements, so we must average the 2 middle ones, or
// return a list for non-numeric elements
Object input1 = list.get(index-1);
Object input2 = list.get(index);
if ((input1 instanceof Number) && (input2 instanceof Number)) {
Number num1 = (Number) input1;
Number num2 = (Number) input2;
Number[] numbers = new Number[2];
numbers[0] = num1; numbers[1] = num2;
median = CalcUtil.average(numbers);
} else { //NaN
//return a list containing the two middle elements
List newList = new ArrayList();
newList.add(input1);
newList.add(input2);
median = newList;
}
} else {
// an odd number of elements are in the list, so we simply return
// the one in the middle, which we've already calculated the index
// for.
median = list.get(index);
}
return median;
}
}