/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2014, Geomatys * * 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.geotoolkit.data.geojson; import com.vividsolutions.jts.geom.Geometry; import org.apache.sis.util.logging.Logging; import org.geotoolkit.data.FeatureReader; import org.geotoolkit.data.FeatureStoreRuntimeException; import org.geotoolkit.data.geojson.binding.GeoJSONFeature; import org.geotoolkit.data.geojson.binding.GeoJSONFeatureCollection; import org.geotoolkit.data.geojson.binding.GeoJSONGeometry; import org.geotoolkit.data.geojson.binding.GeoJSONObject; import org.geotoolkit.data.geojson.utils.GeoJSONParser; import org.geotoolkit.data.geojson.utils.GeometryUtils; import org.apache.sis.util.ObjectConverters; import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.util.ObjectConverter; import org.opengis.referencing.crs.CoordinateReferenceSystem; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Array; import java.nio.file.Path; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.sis.feature.FeatureExt; import org.apache.sis.internal.feature.AttributeConvention; import org.opengis.feature.Attribute; import org.opengis.feature.AttributeType; import org.opengis.feature.Feature; import org.opengis.feature.FeatureAssociationRole; import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyNotFoundException; import org.opengis.feature.PropertyType; /** * @author Quentin Boileau (Geomatys) */ public class GeoJSONReader implements FeatureReader { private final static Logger LOGGER = Logging.getLogger("org.geotoolkit.data.geojson"); private final Map<Map.Entry<Class, Class>, ObjectConverter> convertersCache = new HashMap<>(); private GeoJSONObject jsonObj = null; private Boolean toRead = true; protected final ReadWriteLock rwlock; protected final FeatureType featureType; protected final Path jsonFile; protected Feature current = null; protected int currentFeatureIdx = 0; public GeoJSONReader(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock) { try{ featureType.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString()); }catch(PropertyNotFoundException ex){ throw new RuntimeException("Missing identifier field in feature type"); } this.jsonFile = jsonFile; this.featureType = featureType; this.rwlock = rwLock; rwlock.readLock().lock(); } @Override public FeatureType getFeatureType() { return featureType; } @Override public boolean hasNext() throws FeatureStoreRuntimeException { read(); return current != null; } @Override public Feature next() throws FeatureStoreRuntimeException { read(); final Feature ob = current; current = null; if(ob == null){ throw new FeatureStoreRuntimeException("No more records."); } return ob; } private void read() throws FeatureStoreRuntimeException { if(current != null) return; //first call if (toRead) { try { jsonObj = GeoJSONParser.parse(jsonFile, true); } catch (IOException e) { throw new FeatureStoreRuntimeException(e); } finally { toRead = false; } } current = null; if (jsonObj instanceof GeoJSONFeatureCollection && ((GeoJSONFeatureCollection)jsonObj).hasNext()) { GeoJSONFeature feature = ((GeoJSONFeatureCollection)jsonObj).next(); String id = "id-"+currentFeatureIdx; if (feature.getId() != null) { id = feature.getId(); } current = toFeature(feature, id); currentFeatureIdx++; return; } if (jsonObj instanceof GeoJSONFeature) { GeoJSONFeature feature = (GeoJSONFeature)jsonObj; String id = "id-0"; if (feature.getId() != null) { id = feature.getId(); } current = toFeature(feature, id); jsonObj = null; return; } if (jsonObj instanceof GeoJSONGeometry) { current = toFeature((GeoJSONGeometry)jsonObj, "id-0"); jsonObj = null; } } /** * Convert a GeoJSONFeature to geotk Feature. * @param jsonFeature * @param featureId * @return */ protected Feature toFeature(GeoJSONFeature jsonFeature, String featureId) throws FeatureStoreRuntimeException { //Build geometry final CoordinateReferenceSystem crs = FeatureExt.getCRS(featureType); final Geometry geom = GeometryUtils.toJTS(jsonFeature.getGeometry(), crs); //empty feature final Feature feature = featureType.newInstance(); feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), featureId); feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), geom); //recursively fill other properties final Map<String, Object> properties = jsonFeature.getProperties(); fillFeature(feature, properties); return feature; } /** * Recursively fill a ComplexAttribute with properties map * @param feature * @param properties */ private void fillFeature(Feature feature, Map<String, Object> properties) throws FeatureStoreRuntimeException { final FeatureType featureType = feature.getType(); for(final PropertyType type : featureType.getProperties(true)) { final String attName = type.getName().toString(); final Object value = properties.get(attName); if(value==null) continue; if (type instanceof FeatureAssociationRole ) { final FeatureAssociationRole asso = (FeatureAssociationRole) type; final FeatureType assoType = asso.getValueType(); final Class valueClass = value.getClass(); if (valueClass.isArray()) { Class base = value.getClass().getComponentType(); if (!Map.class.isAssignableFrom(base)) { LOGGER.log(Level.WARNING, "Invalid complex property value " + value); } final int size = Array.getLength(value); if (size > 0) { //list of objects final List<Feature> subs = new ArrayList<>(); for (int i = 0; i < size; i++) { final Feature subComplexAttribute = assoType.newInstance(); fillFeature(subComplexAttribute, (Map) Array.get(value, i)); subs.add(subComplexAttribute); } feature.setPropertyValue(attName,subs); } } else if (value instanceof Map) { final Feature subComplexAttribute = assoType.newInstance(); fillFeature(subComplexAttribute, (Map) value); feature.setPropertyValue(attName, subComplexAttribute); } } else if(type instanceof AttributeType) { final Attribute property = (Attribute) feature.getProperty( type.getName().toString()); fillProperty(property, value); } } } /** * Try to convert value as expected in PropertyType description. * @param prop * @param value */ private void fillProperty(Attribute prop, Object value) throws FeatureStoreRuntimeException { Object convertValue = null; try { if (value != null) { final AttributeType<?> propertyType = prop.getType(); final Class binding = propertyType.getValueClass(); if (value.getClass().isArray() && binding.isArray()) { int nbdim = 1; Class base = value.getClass().getComponentType(); while (base.isArray()) { base = base.getComponentType(); nbdim++; } convertValue = rebuildArray(value, base, nbdim); } else { convertValue = convert(value, binding); } } } catch (UnconvertibleObjectException e1) { throw new FeatureStoreRuntimeException(String.format("Inconvertible property %s : %s", prop.getName().tip().toString(), e1.getMessage()), e1); } prop.setValue(convertValue); } /** * Rebuild nDim arrays recursively * @param candidate * @param componentType * @param depth * @return Array object * @throws UnconvertibleObjectException */ private Object rebuildArray(Object candidate, Class componentType, int depth) throws UnconvertibleObjectException { if(candidate==null) return null; if(candidate.getClass().isArray()){ final int size = Array.getLength(candidate); final int[] dims = new int[depth]; dims[0] = size; final Object rarray = Array.newInstance(componentType, dims); depth--; for(int k=0; k<size; k++){ Array.set(rarray, k, rebuildArray(Array.get(candidate, k), componentType, depth)); } return rarray; }else{ return convert(candidate, componentType); } } /** * Convert value object into binding class * @param value * @param binding * @return * @throws UnconvertibleObjectException */ private Object convert(Object value, Class binding) throws UnconvertibleObjectException { AbstractMap.SimpleEntry<Class, Class> key = new AbstractMap.SimpleEntry<Class, Class>(value.getClass(), binding); ObjectConverter converter = convertersCache.get(key); if (converter == null) { converter = ObjectConverters.find(value.getClass(), binding); convertersCache.put(key, converter); } return converter.apply(value); } /** * Convert a GeoJSONGeometry to Feature. * @param jsonGeometry * @param featureId * @return */ protected Feature toFeature(GeoJSONGeometry jsonGeometry, String featureId) { final Feature feature = featureType.newInstance(); feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), featureId); final CoordinateReferenceSystem crs = FeatureExt.getCRS(featureType); final Geometry geom = GeometryUtils.toJTS(jsonGeometry, crs); feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), geom); return feature; } @Override public void remove() { throw new FeatureStoreRuntimeException("Not supported on reader."); } @Override public void close() { try { // If our object is a feature collection, it could get an opened connexion to a file. We must dispose it. if (jsonObj instanceof Closeable) { ((Closeable) jsonObj).close(); } } catch (IOException e) { e.printStackTrace(); } finally { rwlock.readLock().unlock(); } } }