/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wfs.json;
import com.vividsolutions.jts.geom.Geometry;
import net.sf.json.JSONException;
import org.geoserver.config.GeoServer;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.Operation;
import org.geoserver.platform.ServiceException;
import org.geoserver.wfs.WFSGetFeatureOutputFormat;
import org.geoserver.wfs.WFSInfo;
import org.geoserver.wfs.request.FeatureCollectionResponse;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.gml2.SrsSyntax;
import org.geotools.referencing.CRS;
import org.geotools.referencing.NamedIdentifier;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A GetFeatureInfo response handler specialized in producing Json and JsonP data for a GetFeatureInfo request.
*
* @author Simone Giannecchini, GeoSolutions
* @author Carlo Cancellieri - GeoSolutions
*
*/
public class GeoJSONGetFeatureResponse extends WFSGetFeatureOutputFormat {
private final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(this.getClass());
// store the response type
private final boolean jsonp;
public GeoJSONGetFeatureResponse(GeoServer gs, String format) {
super(gs, format);
if (JSONType.isJsonMimeType(format)) {
jsonp = false;
} else if (JSONType.isJsonpMimeType(format)) {
jsonp = true;
} else {
throw new IllegalArgumentException(
"Unable to create the JSON Response handler using format: " + format
+ " supported mymetype are: "
+ Arrays.toString(JSONType.getSupportedTypes()));
}
}
/**
* capabilities output format string.
*/
public String getCapabilitiesElementName() {
return JSONType.getJSONType(getOutputFormat()).toString();
}
/**
* Returns the mime type
*/
public String getMimeType(Object value, Operation operation) throws ServiceException {
if(jsonp) {
return JSONType.JSONP.getMimeType();
} else {
return JSONType.JSON.getMimeType();
}
}
/**
* Helper method that checks if the results feature collections contain complex features.
*/
private static boolean isComplexFeature(FeatureCollectionResponse results) {
for (FeatureCollection featureCollection : results.getFeatures()) {
if (!(featureCollection.getSchema() instanceof SimpleFeatureType)) {
// this feature collection contains complex features
return true;
}
}
// all features collections contain only simple features
return false;
}
@Override
protected void write(FeatureCollectionResponse featureCollection, OutputStream output,
Operation describeFeatureType) throws IOException {
int numDecimals = getNumDecimals(featureCollection.getFeature(), gs, gs.getCatalog());
if (LOGGER.isLoggable(Level.INFO))
LOGGER.info("about to encode JSON");
// Generate bounds for every feature?
WFSInfo wfs = getInfo();
boolean featureBounding = wfs.isFeatureBounding();
// include fid?
String id_option = null; // null - default, "" - none, or "property"
//GetFeatureRequest request = GetFeatureRequest.adapt(describeFeatureType.getParameters()[0]);
Request request = Dispatcher.REQUEST.get();
if (request != null) {
id_option = JSONType.getIdPolicy( request.getKvp() );
}
// prepare to write out
OutputStreamWriter osw = null;
Writer outWriter = null;
// get feature count for request
BigInteger totalNumberOfFeatures = featureCollection.getTotalNumberOfFeatures();
BigInteger featureCount = (totalNumberOfFeatures != null && totalNumberOfFeatures.longValue() < 0)
? null : totalNumberOfFeatures;
try {
osw = new OutputStreamWriter(output, gs.getGlobal().getSettings().getCharset());
outWriter = new BufferedWriter(osw);
if (jsonp) {
outWriter.write(getCallbackFunction() + "(");
}
// currently complex features count always return zero
boolean isComplex = isComplexFeature(featureCollection);
if (featureCount != null && isComplex && featureCount.equals(BigInteger.ZERO)) {
// a zero count when dealing with complex features means that features count is not supported
featureCount = null;
}
final GeoJSONBuilder jsonWriter = new GeoJSONBuilder(outWriter);
jsonWriter.setNumberOfDecimals(numDecimals);
jsonWriter.object().key("type").value("FeatureCollection");
if(featureCount != null) {
jsonWriter.key("totalFeatures").value(featureCount);
} else {
jsonWriter.key("totalFeatures").value("unknown");
}
jsonWriter.key("features");
jsonWriter.array();
// execute should of set all the header information
// including the lockID
//
// execute should also fail if all of the locks could not be acquired
List<FeatureCollection> resultsList = featureCollection.getFeature();
// encode the features and extract information about the CRS and if geometry exists
boolean hasGeom = false;
CoordinateReferenceSystem crs;
if (!isComplex) {
FeaturesInfo featuresInfo = encodeSimpleFeatures(jsonWriter, resultsList, id_option, featureBounding);
hasGeom = featuresInfo.hasGeometry;
crs = featuresInfo.crs;
} else {
// encode collection with complex features
ComplexGeoJsonWriter complexWriter = new ComplexGeoJsonWriter(jsonWriter);
complexWriter.write(resultsList);
hasGeom = complexWriter.geometryFound();
crs = complexWriter.foundCrs();
}
jsonWriter.endArray(); // end features
// Coordinate Reference System
try {
if ("true".equals(GeoServerExtensions.getProperty("GEOSERVER_GEOJSON_LEGACY_CRS"))){
// This is wrong, but GeoServer used to do it this way.
writeCrsLegacy(jsonWriter, crs);
} else {
writeCrs(jsonWriter, crs);
}
} catch (FactoryException e) {
throw (IOException) new IOException("Error looking up crs identifier").initCause(e);
}
// Bounding box for featurecollection
if (hasGeom && featureBounding) {
ReferencedEnvelope e = null;
for (int i = 0; i < resultsList.size(); i++) {
FeatureCollection collection = resultsList.get(i);
if (e == null) {
e = collection.getBounds();
} else {
e.expandToInclude(collection.getBounds());
}
}
if (e != null) {
jsonWriter.setAxisOrder(CRS.getAxisOrder(e.getCoordinateReferenceSystem()));
jsonWriter.writeBoundingBox(e);
}
}
jsonWriter.endObject(); // end featurecollection
if (jsonp) {
outWriter.write(")");
}
outWriter.flush();
} catch (JSONException jsonException) {
ServiceException serviceException = new ServiceException("Error: "
+ jsonException.getMessage());
serviceException.initCause(jsonException);
throw serviceException;
}
}
/**
* Container class for information related with a group of features.
*/
private class FeaturesInfo {
final CoordinateReferenceSystem crs;
final boolean hasGeometry;
private FeaturesInfo(CoordinateReferenceSystem crs, boolean hasGeometry) {
this.crs = crs;
this.hasGeometry = hasGeometry;
}
}
private FeaturesInfo encodeSimpleFeatures(GeoJSONBuilder jsonWriter, List<FeatureCollection> resultsList,
String id_option, boolean featureBounding) {
CoordinateReferenceSystem crs = null;
boolean hasGeom = false;
for (FeatureCollection collection : resultsList) {
try (FeatureIterator iterator = collection.features()) {
SimpleFeatureType fType;
List<AttributeDescriptor> types;
// encode each simple feature
while (iterator.hasNext()) {
// get next simple feature
SimpleFeature simpleFeature = (SimpleFeature) iterator.next();
// start writing the JSON feature object
jsonWriter.object();
jsonWriter.key("type").value("Feature");
fType = simpleFeature.getFeatureType();
types = fType.getAttributeDescriptors();
// write the simple feature id
if (id_option == null) {
// no specific attribute nominated, use the simple feature id
jsonWriter.key("id").value(simpleFeature.getID());
} else if (id_option.length() != 0) {
// a specific attribute was nominated to be used as id
Object value = simpleFeature.getAttribute(id_option);
jsonWriter.key("id").value(value);
}
// set that axis order that should be used to write geometries
GeometryDescriptor defaultGeomType = fType.getGeometryDescriptor();
if (defaultGeomType != null) {
CoordinateReferenceSystem featureCrs =
defaultGeomType.getCoordinateReferenceSystem();
jsonWriter.setAxisOrder(CRS.getAxisOrder(featureCrs));
if (crs == null) {
crs = featureCrs;
}
} else {
// If we don't know, assume EAST_NORTH so that no swapping occurs
jsonWriter.setAxisOrder(CRS.AxisOrder.EAST_NORTH);
}
// start writing the simple feature geometry JSON object
jsonWriter.key("geometry");
Geometry aGeom = (Geometry) simpleFeature.getDefaultGeometry();
// Write the geometry, whether it is a null or not
if (aGeom != null) {
jsonWriter.writeGeom(aGeom);
hasGeom = true;
} else {
jsonWriter.value(null);
}
if (defaultGeomType != null) {
jsonWriter.key("geometry_name").value(defaultGeomType.getLocalName());
}
// start writing feature properties JSON object
jsonWriter.key("properties");
jsonWriter.object();
for (int j = 0; j < types.size(); j++) {
Object value = simpleFeature.getAttribute(j);
AttributeDescriptor ad = types.get(j);
if (id_option != null && id_option.equals(ad.getLocalName())) {
continue; // skip this value as it is used as the id
}
if (ad instanceof GeometryDescriptor) {
// This is an area of the spec where they
// decided to 'let convention evolve',
// that is how to handle multiple
// geometries. My take is to print the
// geometry here if it's not the default.
// If it's the default that you already
// printed above, so you don't need it here.
if (ad.equals(defaultGeomType)) {
// Do nothing, we wrote it above
// jsonWriter.value("geometry_name");
} else if (value == null) {
jsonWriter.key(ad.getLocalName());
jsonWriter.value(null);
} else {
jsonWriter.key(ad.getLocalName());
jsonWriter.writeGeom((Geometry) value);
}
} else {
jsonWriter.key(ad.getLocalName());
jsonWriter.value(value);
}
}
// Bounding box for feature in properties
ReferencedEnvelope refenv = ReferencedEnvelope.reference(simpleFeature.getBounds());
if (featureBounding && !refenv.isEmpty())
jsonWriter.writeBoundingBox(refenv);
jsonWriter.endObject(); // end the properties
jsonWriter.endObject(); // end the feature
}
}
}
return new FeaturesInfo(crs, hasGeom);
}
private void writeCrs(final GeoJSONBuilder jsonWriter,
CoordinateReferenceSystem crs) throws FactoryException {
if (crs != null) {
String identifier = null;
Integer code = CRS.lookupEpsgCode(crs, true);
if(code != null) {
if (code != null) {
identifier = SrsSyntax.OGC_URN.getPrefix() + code;
}
} else {
identifier = CRS.lookupIdentifier(crs, true);
}
jsonWriter.key("crs");
jsonWriter.object();
jsonWriter.key("type").value("name");
jsonWriter.key("properties");
jsonWriter.object();
jsonWriter.key("name");
jsonWriter.value(identifier);
jsonWriter.endObject(); // end properties
jsonWriter.endObject(); // end crs
} else {
jsonWriter.key("crs");
jsonWriter.value(null);
}
}
// Doesn't follow spec, but GeoServer used to do this.
private void writeCrsLegacy(final GeoJSONBuilder jsonWriter,
CoordinateReferenceSystem crs) {
// Coordinate Reference System, currently only if the namespace is
// EPSG
if (crs != null) {
Set<ReferenceIdentifier> ids = crs.getIdentifiers();
// WKT defined crs might not have identifiers at all
if (ids != null && ids.size() > 0) {
NamedIdentifier namedIdent = (NamedIdentifier) ids.iterator().next();
String csStr = namedIdent.getCodeSpace().toUpperCase();
if (csStr.equals("EPSG")) {
jsonWriter.key("crs");
jsonWriter.object();
jsonWriter.key("type").value(csStr);
jsonWriter.key("properties");
jsonWriter.object();
jsonWriter.key("code");
jsonWriter.value(namedIdent.getCode());
jsonWriter.endObject(); // end properties
jsonWriter.endObject(); // end crs
}
}
}
}
private String getCallbackFunction() {
Request request = Dispatcher.REQUEST.get();
if (request == null) {
return JSONType.CALLBACK_FUNCTION;
}
return JSONType.getCallbackFunction(request.getKvp());
}
@Override
public String getCharset(Operation operation){
return gs.getGlobal().getSettings().getCharset();
}
}