/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014, 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.Date; import org.geotools.factory.CommonFactoryFinder; import org.geotools.util.Converters; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.filter.FilterFactory; import org.opengis.filter.expression.Expression; import com.vividsolutions.jts.geom.Geometry; /** * Finds the nearest value to the provided one in the attribute domain. * * @author Andrea Aime - GeoSolutions * @author Ilkka Rinne / Spatineo Inc for the Finnish Meteorological Institute * * @param <T> */ public class NearestVisitor implements FeatureCalc { private Expression expr; private Class attributeClass; private NearestAccumulator accumulator; boolean visited = false; double shortestDistance = Double.MAX_VALUE; private Object valueToMatch; private Object nearest; /** * Creates a NearestVisitor instance for the given attribute and a value to match. * * @param expression * @param valueToMatch The target value to match */ public NearestVisitor(Expression expression, Object valueToMatch) { this.expr = expression; this.valueToMatch = valueToMatch; } /** * Visitor function, which looks at each feature and finds the value of the attribute given * attribute nearest to the given comparison value. * * @param feature the feature to be visited */ @SuppressWarnings("unchecked") public void visit(org.opengis.feature.Feature feature) { // bail out immediately if we have already found an exact match if (visited) { return; } if(attributeClass == null) { PropertyDescriptor desc = (PropertyDescriptor) expr.evaluate(feature.getType()); attributeClass = desc.getType().getBinding(); if(accumulator == null) { accumulator = getAccumulator(attributeClass); } } // extract the value Object attribValue = expr.evaluate(feature); if (attribValue == null) { return; } else { visited |= accumulator.visit(attribValue); } } private NearestAccumulator getAccumulator(Class attributeClass) { if(Number.class.isAssignableFrom(attributeClass)) { Double convertedTarget = Converters.convert(valueToMatch, Double.class); return new NumberAccumulator(convertedTarget); } else if(Date.class.isAssignableFrom(attributeClass)) { Date convertedTarget = Converters.convert(valueToMatch, Date.class); return new DateAccumulator(convertedTarget); } else if(Geometry.class.isAssignableFrom(attributeClass)) { Geometry convertedTarget = Converters.convert(valueToMatch, Geometry.class); return new GeometryAccumulator(convertedTarget); } else if(Comparable.class.isAssignableFrom(attributeClass)) { Comparable convertedTarget = (Comparable) Converters.convert(valueToMatch, attributeClass); return new ComparableAccumulator(convertedTarget); } // TODO: we should probably create a custom one for strings, there are various // string distance algorithms described on the net throw new IllegalArgumentException("Don't know how to compute nearest for target class " + attributeClass); } public void reset() { visited = false; accumulator = null; attributeClass = null; nearest = null; } public void setValue(Object nearest) { this.nearest = nearest; this.visited = true; } public void setValue(Object maxBelow, Object minAbove) { if(maxBelow == null) { this.nearest = minAbove; } else if(minAbove == null) { this.nearest = maxBelow; } else { NearestAccumulator accumulator = getAccumulator(maxBelow.getClass()); accumulator.visit(maxBelow); accumulator.visit(minAbove); nearest = accumulator.getNearest(); } } /** * Returns the match after {@link #visit}. * * @return * @throws IllegalStateException */ public Object getNearestMatch() throws IllegalStateException { if(nearest == null) { if(accumulator != null) { this.nearest = accumulator.getNearest(); } } return nearest; } @Override public CalcResult getResult() { return new AbstractCalcResult() { @Override public Object getValue() { return NearestVisitor.this.getNearestMatch(); } }; } /** * Expression used to access collection content. * * @return expr used to access collection */ public Expression getExpression() { return expr; } /** * Provided value to match against. * * @return value to match against. */ public Object getValueToMatch() { return valueToMatch; } static interface NearestAccumulator<T> { public boolean visit(T value); public T getNearest(); } static class ComparableAccumulator implements NearestAccumulator<Comparable> { private Comparable minAbove; private Comparable maxBelow; private Comparable targetValue; public ComparableAccumulator(Comparable targetValue) { this.targetValue = targetValue; } @Override public boolean visit(Comparable value) { // compare to find the two values that are right below, and right above, the target // number int aboveBelow = value.compareTo(targetValue); boolean exact = false; if (aboveBelow == 0) { // equality, bail out now minAbove = maxBelow = value; exact = true; } else if (aboveBelow > 0) { if (minAbove == null || minAbove.compareTo(value) > 0) { minAbove = value; } } else if (aboveBelow < 0) { if (maxBelow == null || maxBelow.compareTo(value) < 0) { maxBelow = value; } } return exact; } public Comparable getNearest() { if(maxBelow == null) { return minAbove; } else if(minAbove == null) { return maxBelow; } else { // No real guarantee this will return the closest, but this is the best we can do // not knowing anything else about the target class int diffAbove = Math.abs(targetValue.compareTo(minAbove)); int diffBelow = Math.abs(targetValue.compareTo(maxBelow)); if (diffAbove < diffBelow) { return minAbove; } else { return maxBelow; } } } } class NumberAccumulator implements NearestAccumulator<Number> { double targetValue; double difference = Double.MAX_VALUE; Number nearest; public NumberAccumulator(Number targetValue) { this.targetValue = targetValue.doubleValue(); } @Override public boolean visit(Number value) { double v = value.doubleValue(); double d = Math.abs(v - targetValue); if (d < difference) { difference = d; nearest = value; } return d == 0; } @Override public Number getNearest() { return nearest; } } class DateAccumulator implements NearestAccumulator<Date> { long targetValue; long difference = Long.MAX_VALUE; Date nearest; public DateAccumulator(Date targetValue) { this.targetValue = targetValue.getTime(); } @Override public boolean visit(Date value) { long v = value.getTime(); long d = Math.abs(v - targetValue); if (d < difference) { difference = d; nearest = value; } return d == 0; } @Override public Date getNearest() { return nearest; } } class GeometryAccumulator implements NearestAccumulator<Geometry> { Geometry targetValue; double distance = Double.MAX_VALUE; Geometry nearest; public GeometryAccumulator(Geometry targetValue) { this.targetValue = targetValue; } @Override public boolean visit(Geometry value) { double d = targetValue.distance(value); if (d < distance) { distance = d; nearest = value; } return d == 0; } @Override public Geometry getNearest() { return nearest; } } }