/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2012, 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.data.transform;
import java.awt.RenderingHints.Key;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.data.DataAccess;
import org.geotools.data.FeatureListener;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.data.ResourceInfo;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.sort.SortBy;
/**
* A feature source that can transform a source feature source using a set of expressions
*
* Usages:
* <ul>
* <li>hide, rename fields - compute new fields</li>
* <li>build geom from x,y (we need to add a new Point filter function and have a special treatment
* of it in simplifying filter visitor so that it turns bbox filters against it into a filter on
* x,y)</li>
* <li>on the fly simplification for WFS (just use environment variables) and in general dynamic
* processing based on params without stored queries</li>
* </ul>
*
* @author Andrea Aime - GeoSolutions
*/
public class TransformFeatureSource implements SimpleFeatureSource {
protected static final Logger LOGGER = Logging.getLogger(TransformFeatureSource.class);
protected static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
protected Transformer transformer;
protected SimpleFeatureSource source;
/**
* Creates a transformed feature source from the original source, giving it a certain name and a
* set of computed properties
*
* @param source
* @param name
* @param definitions
* @throws IOException
*/
public TransformFeatureSource(SimpleFeatureSource source, Name name, List<Definition> definitions)
throws IOException {
this.transformer = new Transformer(source, name, definitions, null);
this.source = source;
LOGGER.log(Level.FINE, "Transformed target schema for this feature source is {0}",
transformer.getSchema());
}
@Override
public Name getName() {
return transformer.getName();
}
@Override
public ResourceInfo getInfo() {
return new DefaultResourceInfo(this);
}
@Override
public DataAccess<SimpleFeatureType, SimpleFeature> getDataStore() {
return new SingleFeatureSourceDataStore(this);
}
@Override
public QueryCapabilities getQueryCapabilities() {
return new QueryCapabilities() {
@Override
public boolean isOffsetSupported() {
return true;
}
@Override
public boolean isReliableFIDSupported() {
return source.getQueryCapabilities().isReliableFIDSupported();
}
@Override
public boolean isJoiningSupported() {
return false;
}
@Override
public boolean isUseProvidedFIDSupported() {
return source.getQueryCapabilities().isUseProvidedFIDSupported();
}
@Override
public boolean isVersionSupported() {
return source.getQueryCapabilities().isVersionSupported();
}
@Override
public boolean supportsSorting(SortBy[] sortAttributes) {
// we use external sorting if any of the sort attributes is computed
for (SortBy sortBy : sortAttributes) {
if (sortBy == SortBy.NATURAL_ORDER || sortBy == SortBy.REVERSE_ORDER) {
// fine, we support this
continue;
} else {
String pn = sortBy.getPropertyName().getPropertyName();
AttributeDescriptor descriptor = transformer.getSchema().getDescriptor(pn);
if (descriptor == null) {
return false;
} else {
Class<?> binding = descriptor.getType().getBinding();
if (!Comparable.class.isAssignableFrom(binding)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(
Level.FINE,
"Can't sort on {0} because its property type {1} is not comparable",
new Object[] { descriptor.getLocalName(), binding });
}
return false;
}
}
}
}
return true;
}
};
}
@Override
public void addFeatureListener(FeatureListener listener) {
throw new UnsupportedOperationException("We don't support feature listeners at the moment");
}
@Override
public void removeFeatureListener(FeatureListener listener) {
throw new UnsupportedOperationException("We don't support feature listeners at the moment");
}
@Override
public SimpleFeatureType getSchema() {
return transformer.getSchema();
}
@Override
public ReferencedEnvelope getBounds() throws IOException {
return getBounds(Query.ALL);
}
@Override
public ReferencedEnvelope getBounds(Query query) throws IOException {
// get the geom properties, if any is really computed we return null,
// otherwise if they are just renamed we pass down the bounds request
List<String> geometryNames = transformer.getGeometryPropertyNames();
List<String> filtered = getSelectedAttributes(geometryNames, query);
// if no geometry, no bounds
if (filtered.isEmpty()) {
LOGGER.log(Level.FINE, "No geometry properties in query {0}", query);
return null;
}
// collect the original attribute names
List<String> originalNames = transformer.getOriginalNames(filtered);
if (originalNames.size() < filtered.size()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"Some of the geometry attributes is the result of a general transformation "
+ "(not a rename), can't compute the bbox quickly");
}
return null;
}
// re-shape the query
Query txQuery = transformer.transformQuery(query);
txQuery.setPropertyNames(originalNames);
// let the world know
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"The original query for bounds computation{0} has been tranformed to {1}",
new Object[] { query, txQuery });
}
return source.getBounds(txQuery);
}
/**
* Returns the set of names actually selected by the query
*
* @param attributeNames
* @param query
* @return
*/
private List<String> getSelectedAttributes(List<String> attributeNames, Query query) {
if (query.getPropertyNames() == null) {
return attributeNames;
}
List<String> pnames = Arrays.asList(query.getPropertyNames());
List<String> result = new ArrayList<String>();
for (String an : attributeNames) {
if (pnames.contains(an)) {
result.add(an);
}
}
return result;
}
@Override
public int getCount(Query query) throws IOException {
// transforming does not change count, but we have to transform the filter
Query txQuery = transformer.transformQuery(query);
txQuery.setPropertyNames(Query.ALL_NAMES);
// let the world know
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"The original query for count computation{0} has been transformed to {1}",
new Object[] { query, txQuery });
}
return source.getCount(txQuery);
}
@Override
public Set<Key> getSupportedHints() {
// set up hints
Set<Key> hints = new HashSet<Key>();
hints.addAll(source.getSupportedHints());
hints.add(Hints.FEATURE_DETACHED);
return hints;
}
@Override
public SimpleFeatureCollection getFeatures() throws IOException {
return getFeatures(Query.ALL);
}
@Override
public SimpleFeatureCollection getFeatures(Filter filter) throws IOException {
return getFeatures(new Query(transformer.getSchema().getTypeName(), filter));
}
@Override
public SimpleFeatureCollection getFeatures(Query query) throws IOException {
return new TransformFeatureCollection(this, transformer, query);
}
}