org.geotools.data.arcgisrest.GeoJSONParser.java Source code

Java tutorial

Introduction

Here is the source code for org.geotools.data.arcgisrest.GeoJSONParser.java

Source

package org.geotools.data.arcgisrest;

/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2002-2016, 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.
 *
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.MalformedJsonException;

import com.vividsolutions.jts.geom.Geometry;

import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;

import org.geotools.geometry.jts.GeometryBuilder;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Polygon;

import com.google.gson.JsonSyntaxException;

import org.geotools.data.arcgisrest.schema.catalog.Error_;

/**
 * GeoJSON parsing of simple ,mbi-dimensional features using a streaming parser
 * 
 * @author lmorandini
 *
 */
public class GeoJSONParser implements SimpleFeatureIterator {

    /**
     * GeoJSON format constants
     */
    static public final String ENCODING = "UTF-8";
    static public final String ERROR = "error";
    static public final String ERROR_CODE = "code";
    static public final String ERROR_MESSAGE = "message";
    static public final String ERROR_DETAILS = "details";
    static public final String GEOJSON_TYPE = "type";
    static public final String GEOJSON_TYPE_VALUE_FC = "FeatureCollection";
    static public final String GEOJSON_PROPERTIES = "properties";
    static public final String GEOJSON_TOTALFEATURES = "totalFeatures";
    static public final String FEATURES = "features";
    static public final String CRS = "crs";
    static public final String CRS_TYPE = "type";
    static public final String CRS_TYPE_VALUE = "name";
    static public final String CRS_PROPERTIES = "properties";
    static public final String CRS_PROPERTIES_NAME = "name";
    static public final String FEATURE_TYPE = "type";
    static public final String FEATURE_ID = "id";
    static public final String FEATURE_GEOMETRY = "geometry";
    static public final String FEATURE_GEOMETRY_TYPE = "type";
    static public final String FEATURE_TYPE_VALUE = "Feature";
    static public final String FEATURE_GEOMETRY_COORDINATES = "coordinates";
    static public final String FEATURE_PROPERTIES = "properties";

    static public final String GEOMETRY_POINT = "Point";
    static public final String GEOMETRY_MULTIPOINT = "MultiPoint";
    static public final String GEOMETRY_LINE = "LineString";
    static public final String GEOMETRY_MULTILINE = "MultiLineString";
    static public final String GEOMETRY_POLYGON = "Polygon";
    static public final String GEOMETRY_MULTIPOLYGON = "MultiPolygon";

    protected static final String ATTRIBUTES = "attributes";
    protected static final String GEOMETRY = "geometry";

    protected Logger LOGGER = null;

    // Reader from which features are read
    protected JsonReader reader;

    // Type of the features to be read
    protected SimpleFeatureType featureType;

    // Flag that shows whether the reader is in the middle of a feature collection
    protected boolean inFeatureCollection = false;

    /**
     * Constructor
     * 
     * @param iStream
     *          the stream to read features from
     * @param featureTypeIn
     *          the feature type the features conform to
     * @param loggerIn
     *          the logger to use
     * @throws UnsupportedEncodingException
     */
    public GeoJSONParser(InputStream iStream, SimpleFeatureType featureTypeIn, Logger loggerIn)
            throws UnsupportedEncodingException {
        this.reader = new JsonReader(new InputStreamReader(iStream, ENCODING));
        LOGGER = loggerIn;
        this.featureType = featureTypeIn;
    }

    /**
     * Makes sure resources are released
     */
    protected void finalize() throws Throwable {
        try {
            this.inFeatureCollection = false;
            this.close();
        } finally {
            super.finalize();
        }
    }

    /**
     * Closes associated resources (such as the inpout stream)
     */
    @Override
    public void close() {
        try {
            this.reader.close();
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }
    }

    /**
     * Checks whether there is another feature to read
     * 
     * @return true if there is another featuere to read, false otherwise
     */
    @Override
    public boolean hasNext() {

        try {
            if (this.inFeatureCollection == true && this.reader.peek() == JsonToken.BEGIN_OBJECT) {
                return true;
            }
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }

        return false;
    }

    /**
     * Returns the next feature in the feature collection (if any)
     * 
     * @return the next feature in the collection, null if at the end of it
     */
    @Override
    public SimpleFeature next() throws NoSuchElementException {

        if (this.hasNext() != true) {
            throw new NoSuchElementException();
        }

        return this.parseFeature();
    }

    /**
     * Returns an iterator to navigate the features in the GeoJSON input stream.
     * Since ArcGIS ReST API may return an error message as a JSON (not a
     * GeoJSON), this case is handled by throwing an exception
     * 
     * @return A simple feature collection iterator
     * @throws IOException
     */
    public void parseFeatureCollection() throws IOException {

        this.reader.beginObject();

        while (this.reader.hasNext()) {

            switch (this.reader.nextName()) {

            case ERROR:
                // Deals with an error as reported by the ArcGIS ReST API
                throw this.parseError();

            case GEOJSON_PROPERTIES:
                // TODO: ESRI extension to GeoJSON
                this.reader.beginObject();
                while (this.reader.hasNext()) {
                    this.reader.skipValue();
                }
                this.reader.endObject();
                break;

            case GEOJSON_TOTALFEATURES:
                // TODO: ESRI extension to GeoJSON
                this.reader.skipValue();
                break;

            case CRS:
                this.reader.beginObject();
                while (this.reader.hasNext()) {
                    // TODO: this should be checked against the CRS of the feature type
                    this.reader.skipValue();
                }
                this.reader.endObject();
                break;

            case GEOJSON_TYPE:
                this.checkPropertyValue(GEOJSON_TYPE_VALUE_FC);
                break;

            case FEATURES:
                this.reader.beginArray();
                this.inFeatureCollection = true;

                // If there is the features array to read, we can leave this method
                return;

            default:
                this.LOGGER.log(Level.WARNING, "Unrecognised property");
            }
        }

        // If we come to this, the GeoJSON is not correctly formatted
        this.reader.beginObject();
        this.inFeatureCollection = false;
    }

    /**
     * Helper funciton to convert a List of double to an array of doubles
     */
    public static double[] listToArray(List<Double> coords) {

        double[] arr = new double[coords.size()];
        int i = 0;
        for (Double d : coords) {
            arr[i++] = d.doubleValue();
        }
        return arr;
    }

    /**
     * Utility methof that parses a Point GeoJSON coordinates array and adds them
     * to coords
     * 
     * @param coords
     *          List to add coordinates to
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    protected void parsePointCoordinates(List<Double> coords)
            throws JsonSyntaxException, IOException, IllegalStateException {

        this.reader.beginArray();

        // Reads the point/vertex coordinates
        while (this.reader.hasNext()) {

            // Read X and Y
            coords.add(this.reader.nextDouble());
            coords.add(this.reader.nextDouble());

            // TODO: for the time being it discards Z
            if (this.reader.peek() == JsonToken.NUMBER) {
                this.reader.skipValue();
            }
        }

        this.reader.endArray();
    }

    /**
     * Parses a GeoJSON coordinates array (it is an Array of point coordinates
     * expressed as Array) and returns it a simple double arrays
     * 
     * @return array with coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public double[] parseCoordinateArray() throws JsonSyntaxException, IOException, IllegalStateException {

        List<Double> coords = new ArrayList<Double>();

        this.reader.beginArray();

        while (this.reader.hasNext()) {
            this.parsePointCoordinates(coords);
        }

        this.reader.endArray();

        return GeoJSONParser.listToArray(coords);
    }

    /**
     * Parses a Point GeoJSON coordinates array and returns them in an array
     * 
     * @return array with coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public double[] parsePointCoordinates() throws JsonSyntaxException, IOException, IllegalStateException {

        List<Double> coords = new ArrayList<Double>();
        this.parsePointCoordinates(coords);
        return GeoJSONParser.listToArray(coords);
    }

    /**
     * Parses a MultiPoint GeoJSON coordinates array and adds them to coords
     * 
     * @return list of arrays with coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public List<double[]> parseMultiPointCoordinates()
            throws JsonSyntaxException, IOException, IllegalStateException {

        List<double[]> points = new ArrayList<double[]>();

        this.reader.beginArray();
        while (this.reader.hasNext()) {
            points.add(this.parsePointCoordinates());
        }
        this.reader.endArray();

        return points;
    }

    /**
     * Parses a Line GeoJSON coordinates array and adds them to coords
     * 
     * @return array with coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateExceptionadds them to coords
     */
    public double[] parseLineStringCoordinates() throws JsonSyntaxException, IOException, IllegalStateException {

        return this.parseCoordinateArray();
    }

    /**
     * Parses a MultiLine GeoJSON coordinates array and adds them to coords
     * 
     * @return list of arrays with coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public List<double[]> parseMultiLineStringCoordinates()
            throws JsonSyntaxException, IOException, IllegalStateException {

        List<double[]> lines = new ArrayList<double[]>();

        this.reader.beginArray();
        while (this.reader.hasNext()) {
            lines.add(this.parseLineStringCoordinates());
        }
        this.reader.endArray();
        return lines;
    }

    /**
     * Parses a Polygon GeoJSON coordinates array and adds them to coords
     * 
     * @return list of arrays with coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public List<double[]> parsePolygonCoordinates() throws JsonSyntaxException, IOException, IllegalStateException {

        List<double[]> rings = new ArrayList<double[]>();

        this.reader.beginArray();
        while (this.reader.hasNext()) {
            rings.add(this.parseLineStringCoordinates());
        }
        this.reader.endArray();
        return rings;
    }

    /**
     * Parses a MultiPolygon GeoJSON coordinates array and adds them to coords
     * 
     * @return list of arrays with ring coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public List<List<double[]>> parseMultiPolygonCoordinates()
            throws JsonSyntaxException, IOException, IllegalStateException {

        List<List<double[]>> polys = new ArrayList<List<double[]>>();

        this.reader.beginArray();
        while (this.reader.hasNext()) {
            polys.add(this.parsePolygonCoordinates());
        }
        this.reader.endArray();
        return polys;
    }

    /**
     * Parses a Geometry in GeoJSON format
     * 
     * @return list of arrays with ring coordinates
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public Geometry parseGeometry() throws JsonSyntaxException, IOException, IllegalStateException {

        double[] coords;
        GeometryBuilder builder = new GeometryBuilder();
        GeometryFactory geomFactory = new GeometryFactory();

        // If geometry is null, returns a null point
        try {
            if (this.reader.peek() == JsonToken.NULL) {
                this.reader.nextNull();
                throw (new MalformedJsonException("just here to avoid repeating the return statement"));
            }
        } catch (IllegalStateException | MalformedJsonException e) {
            return builder.point();
        }

        this.reader.beginObject();

        // Check the presence of feature type
        if (!reader.nextName().equals(FEATURE_TYPE)) {
            throw (new JsonSyntaxException("Geometry type expected"));
        }

        switch (reader.nextString()) {

        case GEOMETRY_POINT:
            this.checkPropertyName(FEATURE_GEOMETRY_COORDINATES);
            coords = this.parsePointCoordinates();
            this.reader.endObject();
            return (Geometry) builder.point(coords[0], coords[1]);

        case GEOMETRY_MULTIPOINT:
            this.checkPropertyName(FEATURE_GEOMETRY_COORDINATES);
            List<double[]> pointCoords = this.parseMultiPointCoordinates();
            ;
            Point[] points = new Point[pointCoords.size()];
            for (int i = 0; i < pointCoords.size(); i++) {
                points[i] = (Point) builder.point(pointCoords.get(i)[0], pointCoords.get(i)[1]);
            }
            this.reader.endObject();
            return (Geometry) new MultiPoint(points, geomFactory);

        case GEOMETRY_LINE:
            this.checkPropertyName(FEATURE_GEOMETRY_COORDINATES);
            coords = this.parseLineStringCoordinates();
            this.reader.endObject();
            return (Geometry) builder.lineString(coords);

        case GEOMETRY_MULTILINE:
            this.checkPropertyName(FEATURE_GEOMETRY_COORDINATES);
            List<double[]> lineArrays = this.parseMultiLineStringCoordinates();
            LineString[] lines = new LineString[lineArrays.size()];
            int i = 0;
            for (double[] array : lineArrays) {
                lines[i++] = builder.lineString(array);
            }
            this.reader.endObject();
            return (Geometry) builder.multiLineString(lines);

        case GEOMETRY_POLYGON:
            this.checkPropertyName(FEATURE_GEOMETRY_COORDINATES);
            List<double[]> rings = this.parsePolygonCoordinates();
            this.reader.endObject();
            return (Geometry) builder.polygon(rings.get(0)); // FIXME: what about
                                                             // holes?

        case GEOMETRY_MULTIPOLYGON:
            this.checkPropertyName(FEATURE_GEOMETRY_COORDINATES);
            List<List<double[]>> polyArrays = this.parseMultiPolygonCoordinates();
            Polygon[] polys = new Polygon[polyArrays.size()];
            int j = 0;
            for (List<double[]> array : polyArrays) {
                polys[j++] = builder.polygon(array.get(0)); // FIXME: what about holes?
            }
            this.reader.endObject();
            return (Geometry) builder.multiPolygon(polys);

        default:
            throw (new JsonSyntaxException("Unrecognized geometry type"));
        }

    }

    /**
     * Parses a GeoJSON feature properties. The values returned in a map is a
     * Boolean, a String, or a Double (for every numeric values)
     * 
     * @return A map with property names as keys, and property values as values
     * 
     * @throws IOException,
     *           JsonSyntaxException, IllegalStateException
     */
    public Map<String, Object> parseProperties() throws JsonSyntaxException, IOException, IllegalStateException {

        Map<String, Object> props = new HashMap<String, Object>();
        String name;

        // If properties is null, returns a null point
        // If geometry is null, returns a null point
        try {
            if (this.reader.peek() == JsonToken.NULL) {
                this.reader.nextNull();
                throw (new MalformedJsonException("just here to avoid repeating the return statement"));
            }
        } catch (IllegalStateException | MalformedJsonException e) {
            return props;
        }

        this.reader.beginObject();

        try {
            while (this.reader.hasNext()) {
                name = this.reader.nextName();

                switch (this.reader.peek()) {

                case BOOLEAN:
                    props.put(name, this.reader.nextBoolean());
                    break;

                case NUMBER:
                    props.put(name, this.reader.nextDouble());
                    break;

                case STRING:
                    props.put(name, this.reader.nextString());
                    break;

                case NULL:
                    this.reader.nextNull();
                    props.put(name, null);
                    break;

                default:
                    throw (new JsonSyntaxException("Value expected"));
                }
            }
        } catch (IOException | IllegalStateException e) {
            throw (new NoSuchElementException(e.getMessage()));
        }

        this.reader.endObject();

        return props;
    }

    /**
     * Parses a GeoJSON feature that conforms to the given FeatureType
     * 
     * @return the parsed feature
     */
    public SimpleFeature parseFeature() {

        Geometry geom = null;
        String id = SimpleFeatureBuilder.createDefaultFeatureIdentifier(FEATURES).getID();
        Map<String, Object> props = new HashMap<String, Object>();
        List<Object> values = new ArrayList();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(this.featureType);

        // Parses the feature
        try {
            this.reader.beginObject();

            while (this.reader.hasNext()) {

                switch (this.reader.nextName()) {

                case FEATURE_TYPE:
                    this.checkPropertyValue(FEATURE_TYPE_VALUE);
                    break;

                case FEATURE_GEOMETRY:
                    geom = this.parseGeometry();
                    break;

                case FEATURE_PROPERTIES:
                    props = this.parseProperties();
                    break;

                case FEATURE_ID:
                    id = this.reader.nextString();
                    break;

                default:
                    this.LOGGER.log(Level.WARNING, "Unrecognized feature format");
                    this.reader.skipValue();
                }
            }

            this.reader.endObject();

        } catch (IOException | IllegalStateException e) {
            throw (new NoSuchElementException(e.getMessage()));
        }

        // Builds the feature, inserting the properties in the same
        // order of the atterbiutes in the feature type
        for (AttributeDescriptor attr : this.featureType.getAttributeDescriptors()) {

            if (this.featureType.getGeometryDescriptor().getLocalName().equals(attr.getLocalName())) {
                builder.add(geom);
            } else {
                builder.add(props.get(attr.getLocalName()));
            }
        }

        return builder.buildFeature(id);
    }

    /**
     * Parses an ArcGIS ReST API error message
     * 
     * @return the exception reflecting the error
     * @throws IOException
     */
    public IOException parseError() throws IOException {

        Error_ err = new Error_();

        try {
            this.reader.beginObject();

            while (this.reader.hasNext()) {

                switch (this.reader.nextName()) {

                case ERROR_CODE:
                    err.setCode(this.reader.nextInt());
                    break;

                case ERROR_MESSAGE:
                    err.setMessage(this.reader.nextString());
                    break;

                case ERROR_DETAILS:
                    List<String> details = new ArrayList<String>();
                    this.reader.beginArray();
                    while (this.reader.hasNext()) {
                        details.add(this.reader.nextString());
                    }
                    this.reader.endArray();
                    err.setDetails(details);
                }
            }

            this.reader.endObject();

        } catch (IOException | IllegalStateException e) {
            throw (new NoSuchElementException(e.getMessage()));
        }

        this.close();
        return new IOException("ArcGIS ReST API Error: " + err.getCode() + " " + err.getMessage() + " "
                + String.join(",", err.getDetails()));
    }

    /**
     * Checks the next token is expProp, trows an exception if not
     * 
     * @param expProp
     *          expected property name
     * @throws JsonSyntaxException
     *           ,IoException
     */
    protected void checkPropertyName(String expProp) throws JsonSyntaxException, IOException {

        if (!expProp.equals(this.reader.nextName())) {
            throw (new JsonSyntaxException("'" + expProp + "' property expected"));
        }
    }

    /**
     * Checks the next strig value is expValue, trows an exception if not
     * 
     * @param expValue
     *          expected property value
     * @throws JsonSyntaxException
     *           ,IoException
     */
    protected void checkPropertyValue(String expValue) throws JsonSyntaxException, IOException {

        if (!expValue.equals(this.reader.nextString())) {
            throw (new JsonSyntaxException("'" + expValue + "' value expected"));
        }
    }
}