package com.google.maps.android.geojson;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
/**
* Parses a JSONObject and places data into their appropriate GeoJsonFeature objects. Returns an
* array of
* GeoJsonFeature objects parsed from the GeoJSON file.
*/
/* package */ class GeoJsonParser {
private static final String LOG_TAG = "GeoJsonParser";
// Feature object type
private static final String FEATURE = "Feature";
// Feature object geometry member
private static final String FEATURE_GEOMETRY = "geometry";
// Feature object id member
private static final String FEATURE_ID = "id";
// FeatureCollection type
private static final String FEATURE_COLLECTION = "FeatureCollection";
// FeatureCollection features array member
private static final String FEATURE_COLLECTION_ARRAY = "features";
// Geometry coordinates member
private static final String GEOMETRY_COORDINATES_ARRAY = "coordinates";
// GeometryCollection type
private static final String GEOMETRY_COLLECTION = "GeometryCollection";
// GeometryCollection geometries array member
private static final String GEOMETRY_COLLECTION_ARRAY = "geometries";
// Coordinates for bbox
private static final String BOUNDING_BOX = "bbox";
private static final String PROPERTIES = "properties";
private static final String POINT = "Point";
private static final String MULTIPOINT = "MultiPoint";
private static final String LINESTRING = "LineString";
private static final String MULTILINESTRING = "MultiLineString";
private static final String POLYGON = "Polygon";
private static final String MULTIPOLYGON = "MultiPolygon";
private final JSONObject mGeoJsonFile;
private final ArrayList<GeoJsonFeature> mGeoJsonFeatures;
private LatLngBounds mBoundingBox;
/**
* Creates a new GeoJsonParser
*
* @param geoJsonFile GeoJSON file to parse
*/
/* package */ GeoJsonParser(JSONObject geoJsonFile) {
mGeoJsonFile = geoJsonFile;
mGeoJsonFeatures = new ArrayList<GeoJsonFeature>();
mBoundingBox = null;
parseGeoJson();
}
private static boolean isGeometry(String type) {
return type.matches(POINT + "|" + MULTIPOINT + "|" + LINESTRING + "|" + MULTILINESTRING +
"|" + POLYGON + "|" + MULTIPOLYGON + "|" + GEOMETRY_COLLECTION);
}
/**
* Parses a single GeoJSON feature which contains a geometry and properties member both of
* which can be null. Also parses the bounding box and id members of the feature if they exist.
*
* @param geoJsonFeature feature to parse
* @return GeoJsonFeature object
*/
private static GeoJsonFeature parseFeature(JSONObject geoJsonFeature) {
String id = null;
LatLngBounds boundingBox = null;
GeoJsonGeometry geometry = null;
HashMap<String, String> properties = new HashMap<String, String>();
try {
if (geoJsonFeature.has(FEATURE_ID)) {
id = geoJsonFeature.getString(FEATURE_ID);
}
if (geoJsonFeature.has(BOUNDING_BOX)) {
boundingBox = parseBoundingBox(geoJsonFeature.getJSONArray(BOUNDING_BOX));
}
if (geoJsonFeature.has(FEATURE_GEOMETRY) && !geoJsonFeature.isNull(FEATURE_GEOMETRY)) {
geometry = parseGeometry(geoJsonFeature.getJSONObject(FEATURE_GEOMETRY));
}
if (geoJsonFeature.has(PROPERTIES) && !geoJsonFeature.isNull(PROPERTIES)) {
properties = parseProperties(geoJsonFeature.getJSONObject("properties"));
}
} catch (JSONException e) {
Log.w(LOG_TAG, "Feature could not be successfully parsed " + geoJsonFeature.toString());
return null;
}
return new GeoJsonFeature(geometry, id, properties, boundingBox);
}
/**
* Parses a bounding box given as a JSONArray of 4 elements in the order of lowest values for
* all axes followed by highest values. Axes order of a bounding box follows the axes order of
* geometries.
*
* @param coordinates array of 4 coordinates
* @return LatLngBounds containing the coordinates of the bounding box
* @throws JSONException if the bounding box could not be parsed
*/
private static LatLngBounds parseBoundingBox(JSONArray coordinates) throws JSONException {
// Lowest values for all axes
LatLng southWestCorner = new LatLng(coordinates.getDouble(1), coordinates.getDouble(0));
// Highest value for all axes
LatLng northEastCorner = new LatLng(coordinates.getDouble(3), coordinates.getDouble(2));
return new LatLngBounds(southWestCorner, northEastCorner);
}
/**
* Parses a single GeoJSON geometry object containing a coordinates array or a geometries array
* if it has type GeometryCollection
*
* @param geoJsonGeometry geometry object to parse
* @return GeoJsonGeometry object
*/
private static GeoJsonGeometry parseGeometry(JSONObject geoJsonGeometry) {
try {
String geometryType = geoJsonGeometry.getString("type");
JSONArray geometryArray;
if (geometryType.equals(GEOMETRY_COLLECTION)) {
// GeometryCollection
geometryArray = geoJsonGeometry.getJSONArray(GEOMETRY_COLLECTION_ARRAY);
} else if (isGeometry(geometryType)) {
geometryArray = geoJsonGeometry.getJSONArray(GEOMETRY_COORDINATES_ARRAY);
} else {
// No geometries or coordinates array
return null;
}
return createGeometry(geometryType, geometryArray);
} catch (JSONException e) {
return null;
}
}
/**
* Converts a GeoJsonGeometry object into a GeoJsonFeature object. A geometry object has no ID,
* properties or bounding box so it is set to null.
*
* @param geoJsonGeometry Geometry object to convert into a Feature object
* @return new Feature object
*/
private static GeoJsonFeature parseGeometryToFeature(JSONObject geoJsonGeometry) {
GeoJsonGeometry geometry = parseGeometry(geoJsonGeometry);
if (geometry != null) {
return new GeoJsonFeature(geometry, null, new HashMap<String, String>(), null);
}
Log.w(LOG_TAG, "Geometry could not be parsed");
return null;
}
/**
* Parses the properties of a GeoJSON feature into a hashmap
*
* @param properties GeoJSON properties member
* @return hashmap containing property values
* @throws JSONException if the properties could not be parsed
*/
private static HashMap<String, String> parseProperties(JSONObject properties)
throws JSONException {
HashMap<String, String> propertiesMap = new HashMap<String, String>();
Iterator propertyKeys = properties.keys();
while (propertyKeys.hasNext()) {
String key = (String) propertyKeys.next();
propertiesMap.put(key, properties.isNull(key) ? null : properties.getString(key));
}
return propertiesMap;
}
/**
* Creates a GeoJsonGeometry object from the given type of geometry and its coordinates or
* geometries array
*
* @param geometryType type of geometry
* @param geometryArray coordinates or geometries of the geometry
* @return GeoJsonGeometry object
* @throws JSONException if the coordinates or geometries could be parsed
*/
private static GeoJsonGeometry createGeometry(String geometryType, JSONArray geometryArray)
throws JSONException {
if (geometryType.equals(POINT)) {
return createPoint(geometryArray);
} else if (geometryType.equals(MULTIPOINT)) {
return createMultiPoint(geometryArray);
} else if (geometryType.equals(LINESTRING)) {
return createLineString(geometryArray);
} else if (geometryType.equals(MULTILINESTRING)) {
return createMultiLineString(geometryArray);
} else if (geometryType.equals(POLYGON)) {
return createPolygon(geometryArray);
} else if (geometryType.equals(MULTIPOLYGON)) {
return createMultiPolygon(geometryArray);
} else if (geometryType.equals(GEOMETRY_COLLECTION)) {
return createGeometryCollection(geometryArray);
}
return null;
}
/**
* Creates a new GeoJsonPoint object
*
* @param coordinates array containing the coordinates for the GeoJsonPoint
* @return GeoJsonPoint object
* @throws JSONException if coordinates cannot be parsed
*/
private static GeoJsonPoint createPoint(JSONArray coordinates) throws JSONException {
return new GeoJsonPoint(parseCoordinate(coordinates));
}
/**
* Creates a new GeoJsonMultiPoint object containing an array of GeoJsonPoint objects
*
* @param coordinates array containing the coordinates for the GeoJsonMultiPoint
* @return GeoJsonMultiPoint object
* @throws JSONException if coordinates cannot be parsed
*/
private static GeoJsonMultiPoint createMultiPoint(JSONArray coordinates) throws JSONException {
ArrayList<GeoJsonPoint> geoJsonPoints = new ArrayList<GeoJsonPoint>();
for (int i = 0; i < coordinates.length(); i++) {
geoJsonPoints.add(createPoint(coordinates.getJSONArray(i)));
}
return new GeoJsonMultiPoint(geoJsonPoints);
}
/**
* Creates a new GeoJsonLineString object
*
* @param coordinates array containing the coordinates for the GeoJsonLineString
* @return GeoJsonLineString object
* @throws JSONException if coordinates cannot be parsed
*/
private static GeoJsonLineString createLineString(JSONArray coordinates) throws JSONException {
return new GeoJsonLineString(parseCoordinatesArray(coordinates));
}
/**
* Creates a new GeoJsonMultiLineString object containing an array of GeoJsonLineString objects
*
* @param coordinates array containing the coordinates for the GeoJsonMultiLineString
* @return GeoJsonMultiLineString object
* @throws JSONException if coordinates cannot be parsed
*/
private static GeoJsonMultiLineString createMultiLineString(JSONArray coordinates)
throws JSONException {
ArrayList<GeoJsonLineString> geoJsonLineStrings = new ArrayList<GeoJsonLineString>();
for (int i = 0; i < coordinates.length(); i++) {
geoJsonLineStrings.add(createLineString(coordinates.getJSONArray(i)));
}
return new GeoJsonMultiLineString(geoJsonLineStrings);
}
/**
* Creates a new GeoJsonPolygon object
*
* @param coordinates array containing the coordinates for the GeoJsonPolygon
* @return GeoJsonPolygon object
* @throws JSONException if coordinates cannot be parsed
*/
private static GeoJsonPolygon createPolygon(JSONArray coordinates) throws JSONException {
return new GeoJsonPolygon(parseCoordinatesArrays(coordinates));
}
/**
* Creates a new GeoJsonMultiPolygon object containing an array of GeoJsonPolygon objects
*
* @param coordinates array containing the coordinates for the GeoJsonMultiPolygon
* @return GeoJsonPolygon object
* @throws JSONException if coordinates cannot be parsed
*/
private static GeoJsonMultiPolygon createMultiPolygon(JSONArray coordinates)
throws JSONException {
ArrayList<GeoJsonPolygon> geoJsonPolygons = new ArrayList<GeoJsonPolygon>();
for (int i = 0; i < coordinates.length(); i++) {
geoJsonPolygons.add(createPolygon(coordinates.getJSONArray(i)));
}
return new GeoJsonMultiPolygon(geoJsonPolygons);
}
/**
* Creates a new GeoJsonGeometryCollection object containing an array of GeoJsonGeometry
* objects
*
* @param geometries array containing the geometries for the GeoJsonGeometryCollection
* @return GeoJsonGeometryCollection object
* @throws JSONException if geometries cannot be parsed
*/
private static GeoJsonGeometryCollection createGeometryCollection(JSONArray geometries)
throws JSONException {
ArrayList<GeoJsonGeometry> geometryCollectionElements
= new ArrayList<GeoJsonGeometry>();
for (int i = 0; i < geometries.length(); i++) {
JSONObject geometryElement = geometries.getJSONObject(i);
GeoJsonGeometry geometry = parseGeometry(geometryElement);
if (geometry != null) {
// Do not add geometries that could not be parsed
geometryCollectionElements.add(geometry);
}
}
return new GeoJsonGeometryCollection(geometryCollectionElements);
}
/**
* Parses an array containing a coordinate into a LatLng object
*
* @param coordinates array containing the GeoJSON coordinate
* @return LatLng object
* @throws JSONException if coordinate cannot be parsed
*/
private static LatLng parseCoordinate(JSONArray coordinates) throws JSONException {
// GeoJSON stores coordinates as Lng, Lat so we need to reverse
return new LatLng(coordinates.getDouble(1), coordinates.getDouble(0));
}
/**
* Parses an array containing coordinates into an ArrayList of LatLng objects
*
* @param coordinates array containing the GeoJSON coordinates
* @return ArrayList of LatLng objects
* @throws JSONException if coordinates cannot be parsed
*/
private static ArrayList<LatLng> parseCoordinatesArray(JSONArray coordinates)
throws JSONException {
ArrayList<LatLng> coordinatesArray = new ArrayList<LatLng>();
for (int i = 0; i < coordinates.length(); i++) {
coordinatesArray.add(parseCoordinate(coordinates.getJSONArray(i)));
}
return coordinatesArray;
}
/**
* Parses an array of arrays containing coordinates into an ArrayList of an ArrayList of LatLng
* objects
*
* @param coordinates array of an array containing the GeoJSON coordinates
* @return ArrayList of an ArrayList of LatLng objects
* @throws JSONException if coordinates cannot be parsed
*/
private static ArrayList<ArrayList<LatLng>> parseCoordinatesArrays(JSONArray coordinates)
throws JSONException {
ArrayList<ArrayList<LatLng>> coordinatesArray = new ArrayList<ArrayList<LatLng>>();
for (int i = 0; i < coordinates.length(); i++) {
coordinatesArray.add(parseCoordinatesArray(coordinates.getJSONArray(i)));
}
return coordinatesArray;
}
/**
* Parses the GeoJSON file by type and adds the generated GeoJsonFeature objects to the
* mFeatures array. Supported GeoJSON types include feature, feature collection and geometry.
*/
private void parseGeoJson() {
try {
GeoJsonFeature feature;
String type = mGeoJsonFile.getString("type");
if (type.equals(FEATURE)) {
feature = parseFeature(mGeoJsonFile);
if (feature != null) {
mGeoJsonFeatures.add(feature);
}
} else if (type.equals(FEATURE_COLLECTION)) {
mGeoJsonFeatures.addAll(parseFeatureCollection(mGeoJsonFile));
} else if (isGeometry(type)) {
feature = parseGeometryToFeature(mGeoJsonFile);
if (feature != null) {
// Don't add null features
mGeoJsonFeatures.add(feature);
}
} else {
Log.w(LOG_TAG, "GeoJSON file could not be parsed.");
}
} catch (JSONException e) {
Log.w(LOG_TAG, "GeoJSON file could not be parsed.");
}
}
/**
* Parses the array of GeoJSON features in a given GeoJSON feature collection. Also parses the
* bounding box member of the feature collection if it exists.
*
* @param geoJsonFeatureCollection feature collection to parse
* @return array of GeoJsonFeature objects
*/
private ArrayList<GeoJsonFeature> parseFeatureCollection(JSONObject geoJsonFeatureCollection) {
JSONArray geoJsonFeatures;
ArrayList<GeoJsonFeature> features = new ArrayList<GeoJsonFeature>();
try {
geoJsonFeatures = geoJsonFeatureCollection.getJSONArray(FEATURE_COLLECTION_ARRAY);
if (geoJsonFeatureCollection.has(BOUNDING_BOX)) {
mBoundingBox = parseBoundingBox(
geoJsonFeatureCollection.getJSONArray(BOUNDING_BOX));
}
} catch (JSONException e) {
Log.w(LOG_TAG, "Feature Collection could not be created.");
return features;
}
for (int i = 0; i < geoJsonFeatures.length(); i++) {
try {
JSONObject feature = geoJsonFeatures.getJSONObject(i);
if (feature.getString("type").equals(FEATURE)) {
GeoJsonFeature parsedFeature = parseFeature(feature);
if (parsedFeature != null) {
// Don't add null features
features.add(parsedFeature);
} else {
Log.w(LOG_TAG,
"Index of Feature in Feature Collection that could not be created: "
+ i);
}
}
} catch (JSONException e) {
Log.w(LOG_TAG,
"Index of Feature in Feature Collection that could not be created: " + i);
}
}
return features;
}
/**
* Gets the array of GeoJsonFeature objects
*
* @return array of GeoJsonFeatures
*/
/* package */ ArrayList<GeoJsonFeature> getFeatures() {
return mGeoJsonFeatures;
}
/**
* Gets the array containing the coordinates of the bounding box for the FeatureCollection. If
* the FeatureCollection did not have a bounding box or if the GeoJSON file did not contain a
* FeatureCollection then null will be returned.
*
* @return LatLngBounds object containing bounding box of FeatureCollection, null if no bounding
* box
*/
/* package */ LatLngBounds getBoundingBox() {
return mBoundingBox;
}
}