nl.b3p.geotools.data.arcgis.ArcGISFeatureReader.java Source code

Java tutorial

Introduction

Here is the source code for nl.b3p.geotools.data.arcgis.ArcGISFeatureReader.java

Source

/*
 * Copyright (C) 2012-2013 B3Partners B.V.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package nl.b3p.geotools.data.arcgis;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import java.io.IOException;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geotools.data.DataUtilities;
import org.geotools.data.Query;
import org.geotools.data.jdbc.FilterToSQLException;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureReader;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.util.Converters;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;

/**
 * 
 * @author Matthijs Laan
 */
public class ArcGISFeatureReader implements SimpleFeatureReader {
    private static final Log log = LogFactory.getLog(ArcGISFeatureReader.class);

    private static final int BATCH_SIZE = 100;

    private ArcGISFeatureSource fs;
    private String typeName;
    private Query query;
    private boolean returnGeometry;
    private String outFields;

    private String objectIdFieldName;
    private List objectIds;

    private boolean initBeforeRequestDone = false;

    private SimpleFeatureBuilder builder;
    private SimpleFeatureType featureType;
    private GeometryFactory geometryFactory = new GeometryFactory();

    private JSONArray batch;

    private int batchIndex = 0;
    private int index = 0;

    public ArcGISFeatureReader(ArcGISFeatureSource fs, Query query) {
        this.fs = fs;
        this.query = query;
        this.typeName = fs.getEntry().getTypeName();
    }

    @Override
    public SimpleFeatureType getFeatureType() {
        return fs.getSchema();
    }

    public int getCount() throws IOException {

        if (fs.getArcGISDataStore().getCurrentMajorVersion() > 9) {
            return getObjectIds().size();
        } else {
            getJSONFeaturesDirect();
            return batch.size();
        }
    }

    public List getObjectIds() throws IOException {
        if (objectIds != null) {
            return objectIds;
        }

        if (fs.getArcGISDataStore().getCurrentMajorVersion() < 10) {
            throw new UnsupportedOperationException("Object id queries not supported in ArcGIS REST version 9.x");
        }

        Map<String, String> params = null;
        try {
            params = createQueryParams();
        } catch (FilterToSQLException ex) {
            throw new IOException(ex);
        }
        params.put("returnIdsOnly", "true");

        try {
            JSONObject idsResponse = fs.getArcGISDataStore().getServerJSONResponse(typeName + "/query", params);

            if (!idsResponse.containsKey("objectIds") || !idsResponse.containsKey("objectIdFieldName")) {
                throw new Exception("Requested returnIdsOnly but no objectIds or objectIdFieldName in response");
            }

            objectIdFieldName = (String) idsResponse.get("objectIdFieldName");
            objectIds = (JSONArray) idsResponse.get("objectIds");

            if (objectIds == null) {
                return Collections.EMPTY_LIST;
            }

            // Ensure consistent startIndex by sorting the objectIds
            Collections.sort(objectIds);

            int originalSize = objectIds.size();

            if (query.getStartIndex() != null) {
                objectIds = objectIds.subList(Math.min(query.getStartIndex(), objectIds.size()), objectIds.size());
            }
            if (objectIds.size() > query.getMaxFeatures()) {
                objectIds = objectIds.subList(0, query.getMaxFeatures());
            }

            if (log.isDebugEnabled()) {
                log.debug(String.format(
                        "Object ids count for layer %s: %d; when adjusted for startIndex %s and maxFeatures %s the count is %d",
                        typeName, originalSize, query.getStartIndex() + "",
                        query.isMaxFeaturesUnlimited() ? "unlimited" : query.getMaxFeatures() + "",
                        objectIds.size()));
            }
            return objectIds;
        } catch (Exception e) {
            throw new IOException("Error retrieving feature count from ArcGIS: " + e.toString(), e);
        }
    }

    private Map<String, String> createQueryParams() throws FilterToSQLException {
        Map<String, String> params = new HashMap<String, String>();
        params.put("f", "json");

        String where;
        if (query.getFilter() != null) {
            FilterToArcGISSQL visitor = new FilterToArcGISSQL();
            visitor.setFeatureType(getFeatureType());

            where = visitor.encodeToString(query.getFilter());
            params.putAll(visitor.getSpatialParams());
        } else {
            where = "1 = 1"; // where parameter is required
        }

        params.put("where", where);
        return params;
    }

    private String getOutFields() {
        if (outFields != null) {
            return outFields;
        }

        Set<String> s = new HashSet<String>();
        // We always need the id
        if (objectIdFieldName != null) {
            // No objectID in 9.x
            s.add(objectIdFieldName);
        }
        if (query.getPropertyNames() == Query.ALL_NAMES) {
            // Undocumented shortcut!
            s.clear();
            s.add("*");
            //for(AttributeDescriptor ad: getFeatureType().getAttributeDescriptors()) {
            //    s.add(ad.getLocalName());
            //}
            returnGeometry = true;
        } else if (query.getPropertyNames().length > 0) {
            s.addAll(Arrays.asList(query.getPropertyNames()));
            returnGeometry = s.contains(getFeatureType().getGeometryDescriptor().getLocalName());
            s.remove(ArcGISFeatureSource.DEFAULT_GEOMETRY_ATTRIBUTE_NAME);
        }
        StringBuilder sb = new StringBuilder();
        for (String name : s) {
            if (sb.length() > 0) {
                sb.append(",");
            }
            sb.append(name);
        }
        outFields = sb.toString();

        return outFields;
    }

    private void initBeforeRequest() throws IOException {
        if (initBeforeRequestDone) {
            return;
        }

        // Call to getFeatureType() will request layer info from server
        featureType = getFeatureType();

        builder = new SimpleFeatureBuilder(featureType);

        outFields = getOutFields();

        initBeforeRequestDone = true;
    }

    protected void getNextBatch() throws IOException {
        initBeforeRequest();

        if (batch != null) {
            batchIndex += batch.size();
        }
        int end = Math.min(batchIndex + BATCH_SIZE, objectIds.size());
        batch = getJSONFeaturesByObjectIds(objectIds.subList(batchIndex, end));
    }

    private JSONArray getJSONFeaturesByObjectIds(List ids) throws IOException {
        Map<String, String> params = new HashMap<String, String>();
        params.put("f", "json");
        params.put("outFields", getOutFields());
        params.put("returnGeometry", returnGeometry + "");

        StringBuilder sb = new StringBuilder();
        for (Object id : ids) {
            if (sb.length() > 0) {
                sb.append(",");
            }
            sb.append(id);
        }
        params.put("objectIds", sb.toString());

        // XXX should use POST for large number of ids
        JSONObject response = fs.getArcGISDataStore().getServerJSONResponse(typeName + "/query", params);

        JSONArray features = (JSONArray) response.get("features");
        if (features == null) {
            throw new IOException("No features returned in ArcGIS server response");
        }

        // Sort not strictly necessary, but neater
        final String idField = objectIdFieldName;
        Collections.sort(features, new Comparator() {

            @Override
            public int compare(Object o1, Object o2) {
                JSONObject lhs = (JSONObject) o1;
                JSONObject rhs = (JSONObject) o2;
                JSONObject lhsAtts = (JSONObject) lhs.get("attributes");
                JSONObject rhsAtts = (JSONObject) rhs.get("attributes");
                return ((Comparable) lhsAtts.get(idField)).compareTo(rhsAtts.get(idField));
            }
        });

        return features;
    }

    private JSONArray getJSONFeaturesDirect() throws IOException {
        initBeforeRequest();

        Map<String, String> params = new HashMap<String, String>();

        try {
            params.putAll(createQueryParams());
        } catch (FilterToSQLException ex) {
            throw new IOException(ex);
        }

        params.put("f", "json");
        params.put("outFields", getOutFields());
        params.put("returnGeometry", returnGeometry + "");

        try {
            JSONObject response = fs.getArcGISDataStore().getServerJSONResponse(typeName + "/query", params);

            batch = (JSONArray) response.get("features");

            int originalSize = batch.size();

            // XXX do not copy when startIndex = 0
            // XXX do only one subList() call for startIndex and maxFeatures

            if (query.getStartIndex() != null) {
                JSONArray subBatch = new JSONArray();
                subBatch.addAll(batch.subList(Math.min(query.getStartIndex(), batch.size()), batch.size()));
                batch = subBatch;
            }
            if (batch.size() > query.getMaxFeatures()) {
                JSONArray subBatch = new JSONArray();
                subBatch.addAll(batch.subList(0, query.getMaxFeatures()));
                batch = subBatch;
            }

            if (log.isDebugEnabled()) {
                log.debug(String.format(
                        "Features received for layer %s: %d; when adjusted for startIndex %s and maxFeatures %s the count is %d",
                        typeName, originalSize, query.getStartIndex() + "",
                        query.isMaxFeaturesUnlimited() ? "unlimited" : query.getMaxFeatures() + "", batch.size()));
            }
            return batch;
        } catch (Exception e) {
            throw new IOException("Error retrieving features from ArcGIS: " + e.toString(), e);
        }
    }

    public SimpleFeatureCollection getFeaturesByObjectIds(List ids) throws IOException {
        JSONArray jfeatures = getJSONFeaturesByObjectIds(ids);
        List<SimpleFeature> features = new ArrayList<SimpleFeature>();
        for (Object o : jfeatures) {
            features.add(buildFeature((JSONObject) o));
        }
        return DataUtilities.collection(features);
    }

    protected SimpleFeature buildFeature(JSONObject jf) throws IOException {
        String id = null;

        JSONObject attributes = (JSONObject) jf.get("attributes");

        for (String attribute : (Set<String>) attributes.keySet()) {

            if (objectIdFieldName != null && objectIdFieldName.equals(attribute)) {
                Object o = attributes.get(objectIdFieldName);
                id = o == null ? null : o.toString();
            }

            Class binding = null;
            try {
                binding = featureType.getType(attribute).getBinding();
                if (binding == null) {
                    continue;
                }
                builder.set(attribute, Converters.convert(attributes.get(attribute), binding));
            } catch (Exception e) {
                throw new IOException(String.format("Error converting field \"%s\" value \"%s\" to type %s: %s",
                        attribute, attributes.get(attribute), binding, e.toString()), e);
            }
        }

        JSONObject jg = (JSONObject) jf.get("geometry");
        if (jg != null) {
            AttributeDescriptor ad = featureType.getGeometryDescriptor();

            Class binding = ad.getType().getBinding();
            if (binding.isAssignableFrom(Geometry.class)) {
                // Geometry type in layer JSON was invalid, try the geometryType
                // in the json feature
                binding = ArcGISUtils.getGeometryBinding((String) jf.get("geometryType"));
            }
            Geometry g = ArcGISUtils.convertToJTSGeometry(jg, binding, geometryFactory);
            builder.set(ad.getName(), g);
        }

        return builder.buildFeature(id);
    }

    @Override
    public SimpleFeature next() throws IOException, IllegalArgumentException, NoSuchElementException {

        if (fs.getArcGISDataStore().getCurrentMajorVersion() > 9) {
            if (batch == null || index >= batchIndex + batch.size()) {
                getNextBatch();
            }
        } else {
            // 9.x does not support paging and has a max number of features (500)
            // no method to retrieve all features
            if (batch == null) {
                getJSONFeaturesDirect();
            }
        }

        try {
            return buildFeature((JSONObject) batch.get(index++ - batchIndex));
        } catch (IndexOutOfBoundsException e) {
            throw new NoSuchElementException();
        }
    }

    @Override
    public boolean hasNext() throws IOException {

        if (fs.getArcGISDataStore().getCurrentMajorVersion() > 9) {
            return index < getObjectIds().size();
        } else {
            if (batch == null) {
                getJSONFeaturesDirect();
            }

            return index < batch.size();
        }
    }

    @Override
    public void close() throws IOException {

    }

    public String getObjectIdFieldName() {
        return objectIdFieldName;
    }
}