/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2010, 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.geojson.feature;
import static org.geotools.geojson.GeoJSONUtil.array;
import static org.geotools.geojson.GeoJSONUtil.entry;
import static org.geotools.geojson.GeoJSONUtil.string;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.geojson.GeoJSONUtil;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.json.simple.JSONArray;
import org.json.simple.JSONAware;
import org.json.simple.JSONStreamAware;
import org.json.simple.parser.JSONParser;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
/**
* Reads and writes feature objects to and from geojson.
* <p>
* <pre>
* SimpleFeature feature = ...;
*
* FeatureJSON io = new FeatureJSON();
* io.writeFeature(feature, "feature.json"));
*
* Iterator<Feature> features = io.streamFeatureCollection("features.json");
* while(features.hasNext()) {
* feature = features.next();
* ...
* }
* </pre>
* </p>
* @author Justin Deoliveira, OpenGeo
*
*
*
* @source $URL: http://svn.osgeo.org/geotools/trunk/modules/unsupported/geojson/src/main/java/org/geotools/geojson/feature/FeatureJSON.java $
*/
public class FeatureJSON {
GeometryJSON gjson;
SimpleFeatureType featureType;
AttributeIO attio;
boolean encodeFeatureBounds = false;
boolean encodeFeatureCollectionBounds = false;
boolean encodeFeatureCRS = false;
boolean encodeFeatureCollectionCRS = false;
public FeatureJSON() {
this(new GeometryJSON());
}
public FeatureJSON(GeometryJSON gjson) {
this.gjson = gjson;
attio = new DefaultAttributeIO();
}
/**
* Sets the target feature type for parsing.
* <p>
* Setting the target feature type will help the geojson parser determine the type of feature
* properties during properties. When the type is not around all properties are returned as
* a string.
* </p>
*
* @param featureType The feature type. Parsed features will reference this feature type.
*/
public void setFeatureType(SimpleFeatureType featureType) {
this.featureType = featureType;
this.attio = new FeatureTypeAttributeIO(featureType);
}
/**
* Sets the flag controlling whether feature bounds are encoded.
*
* @see #isEncodeFeatureBounds()
*/
public void setEncodeFeatureBounds(boolean encodeFeatureBounds) {
this.encodeFeatureBounds = encodeFeatureBounds;
}
/**
* The flag controlling whether feature bounds are encoded.
* <p>
* When set each feature object will contain a "bbox" attribute whose value is an array
* containing the elements of the bounding box (in x1,y1,x2,y2 order) of the feature
* </p>
*/
public boolean isEncodeFeatureBounds() {
return encodeFeatureBounds;
}
/**
* Sets the flag controlling whether feature collection bounds are encoded.
*
* @see #isEncodeFeatureCollectionBounds()
*/
public void setEncodeFeatureCollectionBounds(boolean encodeFeatureCollectionBounds) {
this.encodeFeatureCollectionBounds = encodeFeatureCollectionBounds;
}
/**
* The flag controlling whether feature collection bounds are encoded.
* <p>
* When set the feature collection object will contain a "bbox" attribute whose value is an
* array containing elements of the bounding box (in x1,y1,x2,y2 order) of the feature
* collection.
* </p>
*/
public boolean isEncodeFeatureCollectionBounds() {
return encodeFeatureCollectionBounds;
}
/**
* Sets the flag controlling whether feature coordinate reference systems are encoded.
*
* @see #isEncodeFeatureCRS()
*/
public void setEncodeFeatureCRS(boolean encodeFeatureCRS) {
this.encodeFeatureCRS = encodeFeatureCRS;
}
/**
* The flag controlling whether feature coordinate reference systems are encoded.
* <p>
* When set each feature object will contain a "crs" attribute describing the
* coordinate reference system of the feature.
* </p>
*
*/
public boolean isEncodeFeatureCRS() {
return encodeFeatureCRS;
}
/**
* Sets the flag controlling whether feature collection coordinate reference systems are encoded.
*
* @see #isEncodeFeatureCollectionCRS()
*/
public void setEncodeFeatureCollectionCRS(boolean encodeFeatureCollectionCRS) {
this.encodeFeatureCollectionCRS = encodeFeatureCollectionCRS;
}
/**
* The flag controlling whether feature collection coordinate reference systems are encoded.
* <p>
* When set the feature collection object will contain a "crs" attribute describing the
* coordinate reference system of the feature collection.
* </p>
*/
public boolean isEncodeFeatureCollectionCRS() {
return encodeFeatureCollectionCRS;
}
/**
* Writes a feature as GeoJSON.
*
* @param feature The feature.
* @param output The output. See {@link GeoJSONUtil#toWriter(Object)} for details.
*/
public void writeFeature(SimpleFeature feature, Object output) throws IOException {
GeoJSONUtil.encode(new FeatureEncoder(feature).toJSONString(), output);
}
/**
* Writes a feature as GeoJSON returning the result as a string.
*
* @param geometry The geometry.
*
* @return The geometry encoded as GeoJSON
*/
public String toString(SimpleFeature feature) throws IOException {
StringWriter w = new StringWriter();
writeFeature(feature, w);
return w.toString();
}
/**
* Reads a feature from GeoJSON.
*
* @param input The input. See {@link GeoJSONUtil#toReader(Object)} for details.
* @return The feature.
*
* @throws IOException In the event of a parsing error or if the input json is invalid.
*/
public SimpleFeature readFeature(Object input) throws IOException {
return GeoJSONUtil.parse(new FeatureHandler(
featureType != null ? new SimpleFeatureBuilder(featureType): null, attio
), input, false);
}
/**
* Writes a feature collection as GeoJSON.
*
* @param features The feature collection.
* @param output The output. See {@link GeoJSONUtil#toWriter(Object)} for details.
*/
public void writeFeatureCollection(FeatureCollection features, Object output) throws IOException {
LinkedHashMap obj = new LinkedHashMap();
obj.put("type", "FeatureCollection");
if (encodeFeatureCollectionBounds || encodeFeatureCollectionCRS) {
final ReferencedEnvelope bounds = features.getBounds();
if (encodeFeatureCollectionBounds) {
obj.put("bbox", new JSONStreamAware() {
public void writeJSONString(Writer out) throws IOException {
JSONArray.writeJSONString(Arrays.asList(bounds.getMinX(),
bounds.getMinY(),bounds.getMaxX(),bounds.getMaxY()), out);
}
});
}
if (encodeFeatureCollectionCRS) {
obj.put("crs", createCRS(bounds.getCoordinateReferenceSystem()));
}
}
obj.put("features", new FeatureCollectionEncoder(features, gjson));
GeoJSONUtil.encode(obj, output);
}
/**
* Reads a feature collection from GeoJSON.
* <p>
* Warning that this method will load the entire feature collection into memory. For large
* feature collections {@link #streamFeatureCollection(Object)} should be used.
* </p>
*
* @param input The input. See {@link GeoJSONUtil#toReader(Object)} for details.
* @return The feature collection.
*
* @throws IOException In the event of a parsing error or if the input json is invalid.
*/
public FeatureCollection readFeatureCollection(Object input) throws IOException {
DefaultFeatureCollection features = new DefaultFeatureCollection(null, null);
FeatureCollectionIterator it = (FeatureCollectionIterator) streamFeatureCollection(input);
while(it.hasNext()) {
features.add(it.next());
}
return features;
}
/**
* Reads a feature collection from GeoJSON streaming back the contents via an iterator.
*
* @param input The input. See {@link GeoJSONUtil#toReader(Object)} for details.
*
* @return A feature iterator.
*
* @throws IOException In the event of a parsing error or if the input json is invalid.
*/
public FeatureIterator<SimpleFeature> streamFeatureCollection(Object input) throws IOException {
return new FeatureCollectionIterator(input);
}
/**
* Writes a feature collection as GeoJSON returning the result as a string.
*
* @param features The feature collection.
*
* @return The feature collection encoded as GeoJSON
*/
public String toString(FeatureCollection features) throws IOException {
StringWriter w = new StringWriter();
writeFeatureCollection(features, w);
return w.toString();
}
/**
* Writes a coordinate reference system as GeoJSON.
*
* @param crs The coordinate reference system.
* @param output The output. See {@link GeoJSONUtil#toWriter(Object)} for details.
*/
public void writeCRS(CoordinateReferenceSystem crs, Object output) throws IOException {
GeoJSONUtil.encode(createCRS(crs), output);
}
Map<String,Object> createCRS(CoordinateReferenceSystem crs) throws IOException {
Map<String,Object> obj = new LinkedHashMap<String,Object>();
obj.put("type", "name");
Map<String,Object> props = new LinkedHashMap<String, Object>();
try {
props.put("name", CRS.lookupIdentifier(crs, true));
}
catch (FactoryException e) {
throw (IOException) new IOException("Error looking up crs identifier").initCause(e);
}
obj.put("properties", props);
return obj;
}
/**
* Reads a coordinate reference system from GeoJSON.
* <p>
* This method only handles named coordinate reference system objects.
* </p>
*
* @param input The input. See {@link GeoJSONUtil#toReader(Object)} for details.
* @return The coordinate reference system.
*
* @throws IOException In the event of a parsing error or if the input json is invalid.
*/
public CoordinateReferenceSystem readCRS(Object input) throws IOException {
return GeoJSONUtil.parse(new CRSHandler(), input, false);
}
/**
* Writes a coordinate reference system as GeoJSON returning the result as a string.
*
* @param crs The coordinate reference system.
*
* @return The coordinate reference system encoded as GeoJSON
*/
public String toString(CoordinateReferenceSystem crs) throws IOException {
StringWriter writer = new StringWriter();
writeCRS(crs, writer);
return writer.toString();
}
class FeatureEncoder implements JSONAware {
SimpleFeatureType featureType;
SimpleFeature feature;
public FeatureEncoder(SimpleFeature feature) {
this(feature.getType());
this.feature = feature;
}
public FeatureEncoder(SimpleFeatureType featureType) {
this.featureType = featureType;
}
public String toJSONString(SimpleFeature feature) {
StringBuilder sb = new StringBuilder();
sb.append("{");
//type
entry("type", "Feature", sb);
sb.append(",");
//crs
if (encodeFeatureCRS) {
CoordinateReferenceSystem crs =
feature.getFeatureType().getCoordinateReferenceSystem();
if (crs != null) {
try {
string("crs", sb).append(":");
sb.append(FeatureJSON.this.toString(crs)).append(",");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//bounding box
if (encodeFeatureBounds) {
BoundingBox bbox = feature.getBounds();
string("bbox", sb).append(":");
sb.append(gjson.toString(bbox)).append(",");
}
//geometry
if (feature.getDefaultGeometry() != null) {
string("geometry", sb).append(":")
.append(gjson.toString((Geometry) feature.getDefaultGeometry()));
sb.append(",");
}
//properties
int gindex = featureType.getGeometryDescriptor() != null ?
featureType.indexOf(featureType.getGeometryDescriptor().getLocalName()) :
-1;
string("properties", sb).append(":").append("{");
boolean attributesWritten = false;
for (int i = 0; i < featureType.getAttributeCount(); i++) {
AttributeDescriptor ad = featureType.getDescriptor(i);
// skip the default geometry, it's already encoded
if (i == gindex) {
continue;
}
Object value = feature.getAttribute(i);
if (value == null) {
//skip
continue;
}
attributesWritten = true;
// handle special types separately, everything else as a string or literal
if (value instanceof Envelope) {
array(ad.getLocalName(), gjson.toString((Envelope)value), sb);
} else if (value instanceof BoundingBox) {
array(ad.getLocalName(), gjson.toString((BoundingBox)value), sb);
} else if (value instanceof Geometry) {
string(ad.getLocalName(), sb).append(":")
.append(gjson.toString((Geometry) value));
} else {
entry(ad.getLocalName(), value, sb);
}
sb.append(",");
}
if(attributesWritten) {
sb.setLength(sb.length()-1);
}
sb.append("},");
//id
entry("id", feature.getID(), sb);
sb.append("}");
return sb.toString();
}
public String toJSONString() {
return toJSONString(feature);
}
}
class FeatureCollectionEncoder implements JSONStreamAware {
FeatureCollection features;
GeometryJSON gjson;
public FeatureCollectionEncoder(FeatureCollection features, GeometryJSON gjson) {
this.features = features;
this.gjson = gjson;
}
public void writeJSONString(Writer out) throws IOException {
FeatureEncoder featureEncoder =
new FeatureEncoder((SimpleFeatureType) features.getSchema());
out.write("[");
FeatureIterator i = features.features();
try {
if (i.hasNext()) {
SimpleFeature f = (SimpleFeature) i.next();
out.write(featureEncoder.toJSONString(f));
while(i.hasNext()) {
out.write(",");
f = (SimpleFeature) i.next();
out.write(featureEncoder.toJSONString(f));
}
}
}
finally {
features.close(i);
}
out.write("]");
out.flush();
}
}
class FeatureCollectionIterator implements FeatureIterator<SimpleFeature> {
Reader reader;
IFeatureCollectionHandler handler;
JSONParser parser;
SimpleFeature next;
FeatureCollectionIterator(Object input) {
try {
this.reader = GeoJSONUtil.toReader(input);
}
catch (IOException e) {
throw new RuntimeException(e);
}
this.parser = new JSONParser();
}
public boolean hasNext() {
if (next != null) {
return true;
}
if (handler == null) {
handler = new FeatureCollectionHandler(featureType, attio);
//handler = GeoJSONUtil.trace(handler, IFeatureCollectionHandler.class);
}
next = readNext();
return next != null;
}
public SimpleFeature next() {
SimpleFeature feature = next;
next = null;
return feature;
}
SimpleFeature readNext() {
try {
parser.parse(reader, handler, true);
return handler.getValue();
}
catch(Exception e) {
throw new RuntimeException(e);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
public void close() {
reader = null;
parser = null;
handler = null;
}
}
}