/*
* 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();
}
}
}