package org.geotools.data.geojson; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import net.sf.json.JSONSerializer; import org.geotools.feature.AttributeType; import org.geotools.feature.AttributeTypeFactory; 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.IllegalAttributeException; import org.geotools.feature.SchemaException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; /** * This class is a (complementary) companion class to GeoJSONBuilder. * * @author Nicholas Bergson-Shilcock, The Open Planning Project * @version $Id$ * */ public class GeoJSONParser { private static HashMap typeMap; private static GeometryFactory gf = new GeometryFactory(); private static final int FEATURE = 0; private static final int FEATURECOLLECTION = 1; private static final int POINT = 2; private static final int LINESTRING = 3; private static final int POLYGON = 4; private static final int MULTIPOINT = 5; private static final int MULTILINESTRING = 6; private static final int MULTIPOLYGON = 7; private static final int GEOMETRYCOLLECTION = 8; // TODO: add support for CRS // TODO: add support for bbox /** * Converts any GeoJSON object into a Geometry, Feature, or FeatureCollection object. * @param jsonStr - The JSON object (as a String) to convert * @return The Geometry, Feature, or FeatureCollection with the new geometry/feature * @throws GeoJSONException if anything goes wrong */ public static Object parse(String jsonStr) throws SchemaException, IllegalAttributeException, GeoJSONException { return parse(jsonStr, null); } /** * Converts any GeoJSON object into a Geometry, Feature, or FeatureCollection object. * @param obj - The JSONObject to convert * @return The Geometry, Feature, or FeatureCollection with the new geometry/feature * @throws GeoJSONException if anything goes wrong */ public static Object parse(JSONObject obj) throws IllegalAttributeException, SchemaException, GeoJSONException { return parse(obj, null); } /** * Converts any GeoJSON object into a Geometry, Feature, or FeatureCollection object. * @param jsonStr - The JSON object (as a String) to convert * @param featureType - The FeatureType to use when creating the Feature objects * @return The Geometry, Feature, or FeatureCollection with the new geometry/feature * @throws GeoJSONException if anything goes wrong */ public static Object parse(String jsonStr, FeatureType featureType) throws SchemaException, IllegalAttributeException, GeoJSONException { JSONObject obj = (JSONObject) JSONSerializer.toJSON(jsonStr); return parse(obj, featureType); } /** * Converts any GeoJSON object into a Geometry, Feature, or FeatureCollection object. * @param obj - The JSONObject to convert * @param featureType - The FeatureType to use when creating the Feature objects * @return The Geometry, Feature, or FeatureCollection with the new geometry/feature * @throws GeoJSONException if anything goes wrong */ public static Object parse(JSONObject obj, FeatureType featureType) throws SchemaException, IllegalAttributeException, GeoJSONException { if (!obj.containsKey("type")) { throw new GeoJSONException("Missing required attribute 'type'"); } String typeStr = obj.getString("type"); int objType = type(typeStr); switch (objType) { case FEATURE: return parseFeature(obj, featureType); case FEATURECOLLECTION: return parseFeatureCollection(obj, featureType); default: return parseGeometry(obj); } } /** * Generates a new FeatureType based on the passed in Feature. * If the GeoJSON object passed is a FeatureCollection, the first * feature in the "features" property is used for the schema. * @param jsonStr - the JSON object to grab the prototypical feature from * @return The new FeatureType object * @throws SchemaException * @throws GeoJSONException */ public static FeatureType getFirstFeatureType(String jsonStr) throws SchemaException, GeoJSONException { JSONObject jsonObj = (JSONObject) JSONSerializer.toJSON(jsonStr); return getFirstFeatureType(jsonObj); } /** * Generates a new FeatureType based on the passed in Feature. * If the GeoJSON object passed is a FeatureCollection, the first * feature in the "features" property is used for the schema. * @param obj - the JSON object to grab the prototypical feature from * @return The new FeatureType object * @throws SchemaException * @throws GeoJSONException */ public static FeatureType getFirstFeatureType(JSONObject obj) throws SchemaException, GeoJSONException { if (!obj.containsKey("type")) { throw new GeoJSONException("Missing required attribute 'type'"); } JSONObject prototype = null; int objType = type(obj.getString("type")); switch (objType) { case FEATURE: prototype = obj; break; case FEATURECOLLECTION: if (!obj.containsKey("features")) { throw new GeoJSONException( "Missing required attribute 'features'"); } prototype = obj.getJSONArray("features").getJSONObject(0); break; default: throw new GeoJSONException( "Object must be feature or feature collection"); } return createType(prototype); } /** * Actually creates a new FeatureType based on the object passed in. * @param prototype The GeoJSON object to use as the basis for the schema * @return The new FeatureType object * @throws SchemaException */ private static FeatureType createType(JSONObject prototype) throws SchemaException { FeatureTypeBuilder typeBuilder = FeatureTypeBuilder.newInstance("none"); AttributeType attributeType; String name = ""; Class typeClass; for (Iterator keys = prototype.keys(); keys.hasNext();) { name = ((String) keys.next()).toLowerCase(); if (name.equals("geometry")) { typeClass = Geometry.class; } else if (name.equals("properties")) { typeClass = Object.class; } else { typeClass = String.class; } attributeType = AttributeTypeFactory.newAttributeType(name, typeClass); if (!name.equals("id") && !name.equals("type")) { typeBuilder.addType(attributeType); } } return typeBuilder.getFeatureType(); } private static Feature parseFeature(JSONObject obj, FeatureType type) throws SchemaException, IllegalAttributeException, GeoJSONException { // TODO: This method uses featureType.create() to create new features, which // seems to be the old way of doing things. What's the proper new way? if (!obj.containsKey("geometry") || !obj.containsKey("properties")) { throw new GeoJSONException("Invalid GeoJSON feature object"); } FeatureType featureType = type; // Create feature type if none is provided // Note that this is rather inefficient if all the features are of the // same type, as the FeatureType will be recreated for each feature. if (type == null) { featureType = getFirstFeatureType(obj); } // Construct a new array with the attribute values for this feature, // skipping the 'type' attribute and the 'id' attribute if it exists. String key; ArrayList values = new ArrayList(); for (Iterator iter = obj.keys(); iter.hasNext();) { key = (String) iter.next(); if (key.equals("geometry")) { values.add(parseGeometry(obj.getJSONObject(key))); } else if (!key.equals("type") && !key.equals("id")) { values.add(obj.get(key)); } } // Use id if it exists, otherwise generate one... if (obj.containsKey("id")) { String fid = obj.getString("id"); return featureType.create(values.toArray(), fid); } else { return featureType.create(values.toArray()); } } private static FeatureCollection parseFeatureCollection(JSONObject obj, FeatureType type) throws SchemaException, IllegalAttributeException, GeoJSONException { if (!obj.containsKey("features")) { throw new GeoJSONException("Missing required attribute 'features'"); } FeatureCollection featureCollection = FeatureCollections.newCollection(); JSONArray features = obj.getJSONArray("features"); for (int i = 0; i < features.size(); i++) featureCollection.add(parseFeature(features.getJSONObject(i), type)); return featureCollection; } private static Geometry parseGeometry(JSONObject obj) throws GeoJSONException { if (!obj.containsKey("type")) { throw new GeoJSONException("Missing required attribute 'type'"); } String typeStr = obj.getString("type"); int geomType = type(typeStr); if (geomType == GEOMETRYCOLLECTION) { return parseGeometryCollection(obj); } else { if (!obj.containsKey("coordinates")) { throw new GeoJSONException( "Missing required attribute 'coordinates'"); } JSONArray coords = obj.getJSONArray("coordinates"); switch (geomType) { case POINT: return parsePoint(coords); case LINESTRING: return parseLineString(coords); case POLYGON: return parsePolygon(coords); case MULTIPOINT: return parseMultiPoint(coords); case MULTILINESTRING: return parseMultiLineString(coords); case MULTIPOLYGON: return parseMultiPolygon(coords); default: throw new GeoJSONException("Invalid geometry type"); } } } private static Point parsePoint(JSONArray xy) { Coordinate coord = new Coordinate(xy.getDouble(0), xy.getDouble(1)); return gf.createPoint(coord); } private static LineString parseLineString(JSONArray points) { JSONArray xy; Coordinate[] coords = new Coordinate[points.size()]; for (int i = 0; i < coords.length; i++) { xy = points.getJSONArray(i); coords[i] = new Coordinate(xy.getDouble(0), xy.getDouble(1)); } return gf.createLineString(coords); } private static Polygon parsePolygon(JSONArray vals) { JSONArray innerPoints; JSONArray xy; JSONArray outlinePoints = vals.getJSONArray(0); // get the outline of the polygon Coordinate[] outlineCoords = new Coordinate[outlinePoints.size()]; for (int i = 0; i < outlinePoints.size(); i++) { xy = outlinePoints.getJSONArray(i); outlineCoords[i] = new Coordinate(xy.getDouble(0), xy.getDouble(1)); } LinearRing outer = gf.createLinearRing(outlineCoords); // get the holes (if any) LinearRing[] inner = null; if (vals.size() > 1) { inner = new LinearRing[vals.size() - 1]; for (int i = 1; i < vals.size(); i++) { innerPoints = vals.getJSONArray(i); Coordinate[] hole = new Coordinate[innerPoints.size()]; for (int j = 0; j < innerPoints.size(); j++) { xy = innerPoints.getJSONArray(j); hole[j] = new Coordinate(xy.getDouble(0), xy.getDouble(1)); } inner[i] = gf.createLinearRing(hole); } } return gf.createPolygon(outer, inner); } private static MultiPoint parseMultiPoint(JSONArray vals) { Point[] points = new Point[vals.size()]; for (int i = 0; i < vals.size(); i++) points[i] = parsePoint(vals.getJSONArray(i)); return gf.createMultiPoint(points); } private static MultiLineString parseMultiLineString(JSONArray vals) { LineString[] lines = new LineString[vals.size()]; for (int i = 0; i < vals.size(); i++) lines[i] = parseLineString(vals.getJSONArray(i)); return gf.createMultiLineString(lines); } private static MultiPolygon parseMultiPolygon(JSONArray vals) { Polygon[] polys = new Polygon[vals.size()]; for (int i = 0; i < vals.size(); i++) { polys[i] = parsePolygon(vals.getJSONArray(i)); } return gf.createMultiPolygon(polys); } protected static GeometryCollection parseGeometryCollection(JSONObject obj) throws GeoJSONException { if (!obj.containsKey("geometries")) { throw new GeoJSONException( "Missing required attribute 'geometries'"); } JSONArray geometries = obj.getJSONArray("geometries"); Geometry[] geomObjs = new Geometry[geometries.size()]; for (int i = 0; i < geometries.size(); i++) geomObjs[i] = parseGeometry(geometries.getJSONObject(i)); return gf.createGeometryCollection(geomObjs); } private static int type(String type) throws GeoJSONException { if (typeMap == null) { typeMap = new HashMap(); typeMap.put("feature", Integer.valueOf(FEATURE)); typeMap.put("featurecollection", Integer.valueOf(FEATURECOLLECTION)); typeMap.put("point", Integer.valueOf(POINT)); typeMap.put("linestring", Integer.valueOf(LINESTRING)); typeMap.put("polygon", Integer.valueOf(POLYGON)); typeMap.put("multipoint", Integer.valueOf(MULTIPOINT)); typeMap.put("multilinestring", Integer.valueOf(MULTILINESTRING)); typeMap.put("multipolygon", Integer.valueOf(MULTIPOLYGON)); typeMap.put("geometrycollection", Integer.valueOf(GEOMETRYCOLLECTION)); } Integer val = (Integer) typeMap.get(type.toLowerCase()); if (val == null) { throw new GeoJSONException("Unknown object type '" + type + "'"); } return val.intValue(); } }