/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package com.orci.geoserver.wfs.getnearest;
import java.io.IOException;
import java.io.OutputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Logger;
import javax.units.Converter;
import javax.units.SI;
import javax.units.Unit;
import javax.units.UnitFormat;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureSource;
import org.geotools.factory.FactoryFinder;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureCollections;
import org.geotools.feature.FeatureType;
import org.geotools.feature.FeatureTypeBuilder;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.feature.SchemaException;
import org.geotools.feature.type.NumericAttributeType;
import org.geotools.filter.AttributeExpression;
import org.geotools.filter.FidFilter;
import org.geotools.filter.Filter;
import org.geotools.filter.FilterFactory;
import org.geotools.filter.FilterFactoryFinder;
import org.geotools.filter.LiteralExpression;
import org.geotools.measure.Measure;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.opengis.filter.And;
import org.opengis.filter.spatial.DWithin;
import org.vfny.geoserver.Request;
import org.vfny.geoserver.Response;
import org.vfny.geoserver.ServiceException;
import org.vfny.geoserver.global.AttributeTypeInfo;
import org.vfny.geoserver.global.Data;
import org.vfny.geoserver.global.FeatureTypeInfo;
import org.vfny.geoserver.global.GeoServer;
import org.vfny.geoserver.global.NameSpaceInfo;
import org.vfny.geoserver.global.Service;
import org.vfny.geoserver.global.WFS;
import org.vfny.geoserver.wfs.Query;
import org.vfny.geoserver.wfs.WfsException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.operation.distance.DistanceOp;
/**
* Handles a Get Feature request and creates a Get Feature response GML
* string.
*
* @author Chris Holmes, TOPP
* @author Jody Garnett, Refractions Research
* @version $Id: GetNearestResponse.java,v 1.24 2004/04/05 12:03:19 cholmesny Exp $
*/
public class GetNearestResponse implements Response {
/** Standard logging instance for class */
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.vfny.geoserver.responses");
GetNearestResponseDelegate delegate;
String featureTypeName;
/**
* This is the request provided to the execute( Request ) method.<p>We
* save it so we can access the handle provided by the user for error
* reporting during the writeTo( OutputStream ) opperation.</p>
* <p>This value will be <code>null</code> until execute is
* called.</p>
*/
private GetNearestRequest request;
/**
* Empty constructor
*/
public GetNearestResponse() {
request = null;
}
/**
* Returns any extra headers that this service might want to set in
* the HTTP response object.
*
* @see org.vfny.geoserver.Response#getResponseHeaders()
*/
public HashMap getResponseHeaders() {
return null;
}
/**
* DOCUMENT ME!
*
* @param gs DOCUMENT ME!
*
* @return DOCUMENT ME!
*/
public String getContentType(GeoServer gs) {
return delegate.getContentType(gs);
}
public String getContentEncoding() {
return delegate.getContentEncoding();
}
/**
* Jody here with one pass replacement for writeTo.<p>This code is
* a discussion point, when everyone has had there input we will try and
* set things up properly.</p>
* <p>I am providing a mirror of the existing desing: - execute
* gathers the resultList - sets up the header</p>
*
* @param out DOCUMENT ME!
*
* @throws ServiceException DOCUMENT ME!
* @throws IOException DOCUMENT ME!
* @throws IllegalStateException DOCUMENT ME!
*/
public void writeTo(OutputStream out) throws ServiceException, IOException {
if ((request == null) || (delegate == null)) {
throw new IllegalStateException("execute has not been called prior to writeTo");
}
delegate.encode(out);
}
/**
* Executes GetNearestRequest.<p>Willing to execute a FetureRequest,
* or GetNearestRequestWith Lock.</p>
*
* @param req DOCUMENT ME!
*
* @throws ServiceException DOCUMENT ME!
*/
public void execute(Request req) throws ServiceException {
execute((GetNearestRequest) req);
}
/**
* use the SPI mechanism to get a GetNearestResponseDelegate for the
* specified output format.
*
* @param outputFormat
*
* @return
*
* @throws NoSuchElementException DOCUMENT ME!
*/
public static GetNearestResponseDelegate getDelegate(String outputFormat)
throws NoSuchElementException {
GetNearestResponseDelegateProducerSpi spi;
Iterator spi_it = FactoryFinder.factories(GetNearestResponseDelegateProducerSpi.class);
while (spi_it.hasNext()) {
spi = (GetNearestResponseDelegateProducerSpi) spi_it.next();
if (spi.canProduce(outputFormat)) {
return spi.createFeatureDelegateProducer(outputFormat);
}
}
throw new NoSuchElementException();
}
/**
* Performs a getFeatures, or getFeaturesWithLock (using gt2
* locking ).<p>The idea is to grab the FeatureResulsts during
* execute, and use them during writeTo.</p>
*
* @param request
*
* @throws ServiceException
* @throws WfsException DOCUMENT ME!
*
* @task TODO: split this up a bit more? Also get the proper namespace
* declrations and schema locations. Right now we're back up to
* where we were with 1.0., as we can return two FeatureTypes in the
* same namespace. CITE didn't check for two in different
* namespaces, and gml builder just couldn't deal. Now we should be
* able to, we just need to get the reporting right, use the
* AllSameType function as Describe does.
*/
public void execute(GetNearestRequest request) throws ServiceException {
LOGGER.finest("execute GetNearestRequest response. Called request is: " + request);
this.request = request;
this.featureTypeName = null;
String outputFormat = request.getOutputFormat();
try {
delegate = GetNearestResponse.getDelegate(outputFormat);
} catch (NoSuchElementException ex) {
throw new WfsException("output format: " + outputFormat + " not "
+ "supported by geoserver", ex);
}
GetNearestResults results = new GetNearestResults(request);
//
// Optimization Idea
//
// We should be able to reduce this to a two pass opperations.
//
// Pass #1 execute
// - Attempt to Locks Fids during the first pass
// - Also collect Bounds information during the first pass
//
// Pass #2 writeTo
// - Using the Bounds to describe our FeatureCollections
// - Iterate through FeatureResults producing GML
//
// And allways remember to release locks if we are failing:
// - if we fail to aquire all the locks we will need to fail and
// itterate through the the FeatureSources to release the locks
//
WFS wfs = request.getWFS();
GeoServer config = wfs.getGeoServer();
Data catalog = wfs.getData();
FeatureTypeInfo meta = null;
NameSpaceInfo namespace;
Query query;
FeatureSource source;
Feature feature;
String fid;
FilterFactory filterFactory = FilterFactoryFinder.createFilterFactory();
FidFilter fidFilter;
try {
for (Iterator it = request.getQueries().iterator(); it.hasNext();) {
query = (Query) it.next();
// the feature type name used in the content disposition response will match
// the first feature type
if (featureTypeName == null) {
featureTypeName = query.getTypeName();
}
meta = catalog.getFeatureTypeInfo(query.getTypeName());
namespace = meta.getDataStoreInfo().getNameSpace();
source = meta.getFeatureSource();
List attrs = meta.getAttributes();
List propNames = query.getPropertyNames(); // REAL LIST: be careful here :)
List attributeNames = meta.getAttributeNames();
for (Iterator iter = propNames.iterator(); iter.hasNext();) {
String propName = (String) iter.next();
if (!attributeNames.contains(propName)) {
String mesg = "Requested property: " + propName + " is "
+ "not available for " + query.getTypeName() + ". "
+ "The possible propertyName values are: " + attributeNames;
throw new WfsException(mesg);
}
}
List extraGeometries = new ArrayList();
List properties = new ArrayList();
if (propNames.size() != 0) {
Iterator ii = attrs.iterator();
while (ii.hasNext()) {
AttributeTypeInfo ati = (AttributeTypeInfo) ii.next();
//String attName = (String) ii.next();
LOGGER.finer("checking to see if " + propNames + " contains" + ati);
if (((ati.getMinOccurs() > 0) && (ati.getMaxOccurs() != 0))
|| propNames.contains(ati.getName())) {
properties.add(ati.getName());
}
//if(wfs.isFeatureBounding() && meta.getFeatureType().getAttributeType(ati.getName()) instanceof GeometryAttributeType
// && !properties.contains(ati.getName())) {
// properties.add(ati.getName());
// extraGeometries.add(ati.getName());
//}
if(meta.getFeatureType().getAttributeType(ati.getName()) instanceof GeometryAttributeType
&& !properties.contains(ati.getName())) {
properties.add(ati.getName());
extraGeometries.add(ati.getName());
}
}
query.setPropertyNames(properties);
}
// Add range to filter
AttributeExpression geomAttb = filterFactory.createAttributeExpression(meta.getFeatureType(), meta.getFeatureType().getDefaultGeometry().getName());
LiteralExpression pointExpr = filterFactory.createLiteralExpression(request.getPoint());
DWithin dWithin = filterFactory.dwithin(geomAttb, pointExpr, request.getMaxRange(), request.getUnits());
if (query.getFilter() == null) {
query.addFilter((Filter)dWithin);
} else {
And andFilter = filterFactory.and(Arrays.asList(new Filter[] { (Filter)dWithin, query.getFilter() }));
query.addFilter((Filter)andFilter);
}
LOGGER.fine("Query is " + query + "\n To gt2: " + query.toDataQuery(Integer.MAX_VALUE));
//DJB: note if maxFeatures gets to 0 the while loop above takes care of this! (this is a subtle situation)
FeatureCollection featuresCheck = source.getFeatures(query.toDataQuery(Integer.MAX_VALUE));
// find nearest feature
Unit fromUnit = SI.METER;
Unit toUnit = UnitFormat.getInstance().parseUnit(request.getUnits());
Converter unitConvert = fromUnit.getConverterTo(toUnit);
Feature nearestFeature = null;
double nearestDistance = 9e9;
double nearestBearing = 0;
for (Iterator sItr = featuresCheck.iterator(); sItr.hasNext();) {
Feature f = (Feature)sItr.next();
if (f.getDefaultGeometry() == null) continue;
DistanceOp op = new DistanceOp(request.getPoint(), f.getDefaultGeometry());
Coordinate[] co = op.closestPoints();
Measure m = DefaultGeographicCRS.WGS84.distance(new double[] { co[0].x, co[0].y, }, new double[] { co[1].x, co[1].y, });
if (m.doubleValue() > nearestDistance) continue;
nearestFeature = f;
nearestDistance = m.doubleValue();
nearestBearing = calcBearing(co);
}
//GR: I don't know if the featuresults should be added here for later
//encoding if it was a lock request. may be after ensuring the lock
//succeed?
FeatureCollection features = FeatureCollections.newCollection();
if (nearestFeature != null) features.add(superFeature(nearestFeature, unitConvert.convert(nearestDistance), nearestBearing));
// we may need to shave off geometries we did load only to make bounds
// computation happy
if(extraGeometries.size() > 0) {
List residualProperties = new ArrayList(properties);
residualProperties.removeAll(extraGeometries);
residualProperties.add("nearest_distance");
residualProperties.add("nearest_bearing");
String[] residualNames = (String[]) residualProperties.toArray(new String[residualProperties.size()]);
FeatureType targetType = DataUtilities.createSubType(superFeatureType(meta.getFeatureType()), residualNames);
features = new FeatureBoundsFeatureCollection(features, targetType);
}
results.addFeatures(meta, features);
}
//end for
//prepare to encode in the desired output format
delegate.prepare(outputFormat, results);
} catch (IOException e) {
throw new ServiceException(e, "problem with FeatureResults", request.getHandle());
} catch (NoSuchElementException e) {
throw new ServiceException(e, "problem with FeatureResults", request.getHandle());
} catch (SchemaException e) {
throw new ServiceException(e, "problem with FeatureResults", request.getHandle());
} catch (ParseException e) {
throw new ServiceException(e, "problem with FeatureResults", request.getHandle());
}
}
private FeatureType superFeatureType(FeatureType oldType) {
FeatureType featureType = null;
try {
FeatureTypeBuilder typeBuilder = FeatureTypeBuilder.newInstance(oldType.getTypeName());
typeBuilder.setNamespace(oldType.getNamespace());
//typeBuilder.setDefaultGeometry(oldType.getDefaultGeometry());
for (int i = 0; i < oldType.getAttributeCount(); i++) {
//if (oldType.getAttributeType(i).equals(oldType.getDefaultGeometry())) continue;
typeBuilder.addType(oldType.getAttributeType(i));
}
typeBuilder.addType(new NumericAttributeType("nearest_distance", Double.class, false, 0, 999, null, null));
typeBuilder.addType(new NumericAttributeType("nearest_bearing", Double.class, false, 0, 999, null, null));
featureType = typeBuilder.getFeatureType();
} catch (Exception e) {
LOGGER.severe("Error createing super feature type: " + e); e.printStackTrace();
}
return featureType;
}
private Feature superFeature(Feature oldFeature, Double distance, Double bearing) {
FeatureType featureType = superFeatureType(oldFeature.getFeatureType());
Object[] attbs = new Object[featureType.getAttributeCount()];
for (int i = 0; i < oldFeature.getFeatureType().getAttributeCount(); i++) {
attbs[i] = oldFeature.getAttribute(i);
}
attbs[attbs.length - 2] = distance;
attbs[attbs.length - 1] = bearing;
try {
return featureType.create(attbs, oldFeature.getID());
} catch (Exception e) {
LOGGER.severe("Error createing super feature: " + e); e.printStackTrace();
}
return null;
}
private double calcBearing(Coordinate[] coords) {
double y = Math.sin(coords[0].x - coords[1].x) * Math.cos(coords[1].y);
double x = Math.cos(coords[0].y)*Math.sin(coords[1].y) - Math.sin(coords[0].y)*Math.cos(coords[1].y)*Math.cos(coords[0].x - coords[1].x);
double brng = ((Math.atan2(y, x) * 180.0 / Math.PI) + 360) % 360;
return brng;
}
/**
* Release locks if we are into that sort of thing.
*
* @see org.vfny.geoserver.Response#abort()
*/
public void abort(Service gs) {
}
public String getContentDisposition() {
if ((featureTypeName != null) && (featureTypeName.indexOf(':') != -1)) {
featureTypeName = featureTypeName.substring(featureTypeName.indexOf(':') + 1);
}
return delegate.getContentDisposition(featureTypeName);
}
}