/* * 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.utils; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import org.apache.sis.util.logging.Logging; import org.geotoolkit.data.geojson.binding.*; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import static org.geotoolkit.data.geojson.utils.GeoJSONMembres.*; import static org.geotoolkit.data.geojson.utils.GeoJSONTypes.*; import static org.geotoolkit.data.geojson.binding.GeoJSONGeometry.*; /** * Efficient GeoJSONParsing using jackson {@link JsonParser} * @author Quentin Boileau (Geomatys) */ public final class GeoJSONParser { public static final JsonFactory FACTORY = new JsonFactory(); public final static Logger LOGGER = Logging.getLogger("org.geotoolkit.data.geojson.utils"); private GeoJSONParser() {} /** * Parse a json file and return a GeoJSONObject. * If parser was construct with lazyParsing as {@code true} and root object * is a FeatureCollection, returned GeoJSONFeatureCollection will only have * start and end feature array location. * Otherwise, all Feature will be parsed and add to GeoJSONFeatureCollection. * * @param jsonFile file to parse * @return GeoJSONObject * @throws IOException */ @Deprecated public static GeoJSONObject parse(File jsonFile) throws IOException { return parse(jsonFile.toPath()); } /** * Parse a json file and return a GeoJSONObject. * If parser was construct with lazyParsing as {@code true} and root object * is a FeatureCollection, returned GeoJSONFeatureCollection will only have * start and end feature array location. * Otherwise, all Feature will be parsed and add to GeoJSONFeatureCollection. * * @param jsonFile file to parse * @return GeoJSONObject * @throws IOException */ public static GeoJSONObject parse(Path jsonFile) throws IOException { return parse(jsonFile, Boolean.FALSE); } /** * Parse a json file and return a GeoJSONObject. * If parser was construct with lazyParsing as {@code true} and root object * is a FeatureCollection, returned GeoJSONFeatureCollection will only have * start and end feature array location. * Otherwise, all Feature will be parsed and add to GeoJSONFeatureCollection. * * @param jsonFile file to parse * @param lazy lazy mode flag * @return GeoJSONObject * @throws IOException */ public static GeoJSONObject parse(Path jsonFile, boolean lazy) throws IOException { try (InputStream reader = Files.newInputStream(jsonFile); JsonParser p = FACTORY.createParser(reader)) { JsonToken startToken = p.nextToken(); assert (startToken == JsonToken.START_OBJECT) : "Input File is not a JSON file " + jsonFile.toAbsolutePath().toString(); return parseGeoJSONObject(p, lazy, jsonFile); } } /** * Parse a json InputStream and return a GeoJSONObject. * In InputStream case, lazy loading of FeatureCollection is * disabled. * * @param inputStream stream to parse * @return GeoJSONObject * @throws IOException */ public static GeoJSONObject parse(InputStream inputStream) throws IOException { try (JsonParser p = FACTORY.createParser(inputStream)) { JsonToken startToken = p.nextToken(); assert (startToken == JsonToken.START_OBJECT) : "Input stream is not a valid JSON "; return parseGeoJSONObject(p); } } /** * Parse a GeoJSONObject (FeatureCollection, Feature or a Geometry) * JsonParser location MUST be on a START_OBJECT token. * @param p parser jackson parser with current token on a START_OBJECT. * @return GeoJSONObject (FeatureCollection, Feature or a Geometry) * @throws IOException */ public static GeoJSONObject parseGeoJSONObject(JsonParser p) throws IOException { return parseGeoJSONObject(p, Boolean.FALSE, null); } /** * Parse a GeoJSONObject (FeatureCollection, Feature or a Geometry) * JsonParser location MUST be on a START_OBJECT token. * @param p parser jackson parser with current token on a START_OBJECT. * @param lazy lazy mode flag * @return GeoJSONObject (FeatureCollection, Feature or a Geometry) * @throws IOException */ private static GeoJSONObject parseGeoJSONObject(JsonParser p, Boolean lazy, Path source) throws IOException { assert(p.getCurrentToken() == JsonToken.START_OBJECT); GeoJSONObject object = new GeoJSONObject(); while (p.nextToken() != JsonToken.END_OBJECT) { String fieldname = p.getCurrentName(); if (fieldname == null) { throw new IOException("Parsing error, expect object field name value but got null"); } switch (fieldname) { case ID: p.nextToken(); String id = p.getValueAsString(); if(object instanceof GeoJSONFeature){ ((GeoJSONFeature)object).setId(id); } break; case TYPE: p.nextToken(); String value = p.getValueAsString(); object = getOrCreateFromType(object, value, lazy); break; case BBOX: //array p.nextToken(); // "[" object.setBbox(parseBBoxArray(p)); break; case CRS: //object p.nextToken(); // "{" object.setCrs(parseCRS(p)); break; case FEATURES: object = getOrCreateFromType(object, FEATURE_COLLECTION, lazy); //array of GeoJSONFeature if (Boolean.TRUE.equals(lazy)) { lazyParseFeatureCollection((GeoJSONFeatureCollection) object, p, source); } else { parseFeatureCollection((GeoJSONFeatureCollection) object, p); } break; case PROPERTIES: object = getOrCreateFromType(object, FEATURE); //object parseProperties((GeoJSONFeature) object, p); break; case GEOMETRY: object = getOrCreateFromType(object, FEATURE); //object parseFeatureGeometry((GeoJSONFeature) object, p); break; case COORDINATES: //array if (object instanceof GeoJSONGeometry) { parseGeometry((GeoJSONGeometry) object, p); } else { LOGGER.log(Level.WARNING, "Error need type before coordinates"); } break; case GEOMETRIES: if (object instanceof GeoJSONGeometryCollection) { object = getOrCreateFromType(object, GEOMETRY_COLLECTION); //array of GeoJSONGeometry parseGeometryCollection((GeoJSONGeometryCollection) object, p); } else { LOGGER.log(Level.WARNING, "Error need type before coordinates"); } break; default : if(p.getCurrentToken()==JsonToken.START_OBJECT){ //skip any unknown properties parseGeoJSONObject(p,lazy,source); } break; } } return object; } /** * Parse properties map and add to given Feature * @param feature Feature to attach properties * @param p parser * @throws IOException */ private static void parseProperties(GeoJSONFeature feature, JsonParser p) throws IOException { p.nextToken(); // "{" feature.getProperties().putAll(parseMap(p)); } /** * Parse a map of String, Object. * JsonParser location MUST be on a START_OBJECT token. * @param p parser jackson parser with current token on a START_OBJECT. * @return Map of String - Object * @throws IOException */ private static Map<String, Object> parseMap(JsonParser p) throws IOException { Map<String, Object> map = new HashMap<>(); final JsonToken currentToken = p.getCurrentToken(); if (currentToken == JsonToken.VALUE_NULL) { return map; } if (currentToken != JsonToken.START_OBJECT) { LOGGER.log(Level.WARNING, "Expect START_OBJECT token but got "+currentToken+" for "+p.getCurrentName()); return map; } assert(currentToken == JsonToken.START_OBJECT); while (p.nextToken() != JsonToken.END_OBJECT) { String key = p.getCurrentName(); JsonToken next = p.nextToken(); map.put(key, getValue(next, p)); } return map; } /** * Parse a List of Objects. * JsonParser location MUST be on a START_ARRAY token. * @param p parser jackson parser with current token on a START_ARRAY. * @return List of Objects * @throws IOException */ private static List<Object> parseArray(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); List<Object> list = new ArrayList<>(); while (p.nextToken() != JsonToken.END_ARRAY) { list.add(getValue(p.getCurrentToken(), p)); } return list; } /** * Parse a List of Objects. * JsonParser location MUST be on a START_ARRAY token. * @param p parser jackson parser with current token on a START_ARRAY. * @return array object typed after first element class * @throws IOException */ private static Object parseArray2(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); List<Object> list = new ArrayList<>(); while (p.nextToken() != JsonToken.END_ARRAY) { list.add(getValue(p.getCurrentToken(), p)); } if (list.isEmpty()) { return new Object[0]; } Class binding = list.get(0).getClass(); Object newArray = Array.newInstance(binding, list.size()); for (int i = 0; i < list.size(); i++) { Array.set(newArray, i, list.get(i)); } return newArray; } /** * Convert the current token to appropriate object. * Supported (String, Integer, Float, Boolean, Null, Array, Map) * * @param token current token * @param p parser * @return current token value String or Integer or Float or Boolean or null or an array or a map. * @throws IOException */ static Object getValue(JsonToken token, JsonParser p) throws IOException { if (token == JsonToken.VALUE_STRING) { return p.getValueAsString(); } else if (token == JsonToken.VALUE_NUMBER_INT) { return p.getValueAsInt(); } else if (token == JsonToken.VALUE_NUMBER_FLOAT) { return p.getValueAsDouble(); } else if (token == JsonToken.VALUE_TRUE || token == JsonToken.VALUE_FALSE) { return token == JsonToken.VALUE_TRUE; } else if (token == JsonToken.VALUE_NULL) { return null; } else if (token == JsonToken.START_ARRAY) { return parseArray2(p); } else if (token == JsonToken.START_OBJECT) { return parseMap(p); } else { throw new UnsupportedOperationException("Unsupported JSON token : "+token+ ", value : "+p.getText()); } } /** * Parse a coordinates array(s) and add to given GeoJSONGeometry. * @param geom GeoJSONGeometry * @param p parser * @throws IOException */ private static void parseGeometry(GeoJSONGeometry geom, JsonParser p) throws IOException { p.nextToken(); // "[" if (geom instanceof GeoJSONPoint) { ((GeoJSONPoint) geom).setCoordinates(parsePoint(p)); } else if (geom instanceof GeoJSONLineString) { ((GeoJSONLineString) geom).setCoordinates(parseLineString(p)); } else if (geom instanceof GeoJSONPolygon) { ((GeoJSONPolygon) geom).setCoordinates(parseMultiLineString(p)); } else if (geom instanceof GeoJSONMultiPoint) { ((GeoJSONMultiPoint) geom).setCoordinates(parseLineString(p)); } else if (geom instanceof GeoJSONMultiLineString) { ((GeoJSONMultiLineString) geom).setCoordinates(parseMultiLineString(p)); } else if (geom instanceof GeoJSONMultiPolygon) { ((GeoJSONMultiPolygon) geom).setCoordinates(parseMultiPolygon(p)); } } /** * Full parse of GeoJSONFeatures for a GeoJSONFeatureCollection * @param coll GeoJSONFeatureCollection * @param p parser * @throws IOException */ private static void parseFeatureCollection(GeoJSONFeatureCollection coll, JsonParser p) throws IOException { p.nextToken(); // "{" // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { GeoJSONObject obj = parseGeoJSONObject(p, false, null); if (obj instanceof GeoJSONFeature) { coll.getFeatures().add((GeoJSONFeature)obj); } else { LOGGER.log(Level.WARNING, "ERROR feature collection"); } } } /** * Lazy parse of GeoJSONFeatures for a GeoJSONFeatureCollection. * Only find an set START_ARRAY and END_ARRAY TokenLocation of the features array to * the GeoJSONFeatureCollection object. * * @param coll GeoJSONFeatureCollection * @param p parser * @param source * @throws IOException */ private static void lazyParseFeatureCollection(GeoJSONFeatureCollection coll, JsonParser p, Path source) throws IOException { p.nextToken(); coll.setSourceInput(source); coll.setStartPosition(p.getCurrentLocation()); int startArray = 1; int endArray = 0; //loop to the right "]" while (startArray != endArray) { JsonToken token = p.nextToken(); if (token == JsonToken.START_ARRAY) startArray ++; if (token == JsonToken.END_ARRAY) endArray ++; } coll.setEndPosition(p.getCurrentLocation()); } /** * Parse GeoJSONGeometry for GeoJSONFeature. * @param feature GeoJSONFeature * @param p parser * @throws IOException */ private static void parseFeatureGeometry(GeoJSONFeature feature, JsonParser p) throws IOException { p.nextToken(); // "{" GeoJSONObject obj = parseGeoJSONObject(p); assert(obj != null) : "Un-parsable GeoJSONGeometry."; assert(obj instanceof GeoJSONGeometry) : "Unexpected GeoJSONObject : " + obj.getType()+" expected : GeoJSONGeometry"; feature.setGeometry((GeoJSONGeometry) obj); } /** * Parse GeoJSONGeometry for GeoJSONGeometryCollection. * @param geom GeoJSONGeometryCollection * @param p parser * @throws IOException */ private static void parseGeometryCollection(GeoJSONGeometryCollection geom, JsonParser p) throws IOException { p.nextToken(); // "[" // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { GeoJSONObject obj = parseGeoJSONObject(p); assert(obj != null) : "Un-parsable GeoJSONGeometry."; assert(obj instanceof GeoJSONGeometry) : "Unexpected GeoJSONObject : " + obj.getType()+" expected : GeoJSONGeometry"; geom.getGeometries().add((GeoJSONGeometry)obj); } } /** * Create GeoJSONObject using type. * If previous object is not null, forward bbox and crs parameter to the new GeoJSONObject. * * @param object previous object. * @param type see {@link org.geotoolkit.data.geojson.utils.GeoJSONTypes} * @return GeoJSONObject */ private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type) { return getOrCreateFromType(object, type, Boolean.FALSE); } /** * Create GeoJSONObject using type. * If previous object is not null, forward bbox and crs parameter to the new GeoJSONObject. * * @param object previous object. * @param type see {@link org.geotoolkit.data.geojson.utils.GeoJSONTypes} * @param lazy lazy mode flag * @return GeoJSONObject */ private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type, Boolean lazy) { GeoJSONObject result = object; if (type != null) { switch (type) { case FEATURE_COLLECTION: if (result instanceof GeoJSONFeatureCollection) { return result; } else { result = new GeoJSONFeatureCollection(lazy); } break; case FEATURE: if (result instanceof GeoJSONFeature) { return result; } else { result = new GeoJSONFeature(); } break; case POINT: if (result instanceof GeoJSONPoint) { return result; } else { result = new GeoJSONPoint(); } break; case LINESTRING: if (result instanceof GeoJSONLineString) { return result; } else { result = new GeoJSONLineString(); } break; case POLYGON: if (result instanceof GeoJSONPolygon) { return result; } else { result = new GeoJSONPolygon(); } break; case MULTI_POINT: if (result instanceof GeoJSONMultiPoint) { return result; } else { result = new GeoJSONMultiPoint(); } break; case MULTI_LINESTRING: if (result instanceof GeoJSONMultiLineString) { return result; } else { result = new GeoJSONMultiLineString(); } break; case MULTI_POLYGON: if (result instanceof GeoJSONMultiPolygon) { return result; } else { result = new GeoJSONMultiPolygon(); } break; case GEOMETRY_COLLECTION: if (result instanceof GeoJSONGeometryCollection) { return result; } else { result = new GeoJSONGeometryCollection(); } break; default: throw new IllegalArgumentException("Unknown type " + type); } if (object != null) { result.setBbox(object.getBbox()); result.setCrs(object.getCrs()); } } return result; } /** * Parse the bbox array. * JsonParser location MUST be on a START_ARRAY token. * @param p JsonParser location MUST be on a START_ARRAY token. * @return an array of double with a length of 4 or 6. * @throws IOException */ private static double[] parseBBoxArray(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); double[] bbox = new double[4]; int idx = 0; // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { if (idx == 4) { bbox = Arrays.copyOf(bbox, 6); } bbox[idx++] = p.getDoubleValue(); } return bbox; } /** * Parse a CRS Object. * JsonParser location MUST be on a START_OBJECT token. * @param p JsonParser location MUST be on a START_OBJECT token. * @return GeoJSONCRS * @throws IOException */ private static GeoJSONCRS parseCRS(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_OBJECT); GeoJSONCRS crs = new GeoJSONCRS(); while (p.nextToken() != JsonToken.END_OBJECT) { String fieldName = p.getCurrentName(); if (TYPE.equals(fieldName)) { crs.setType(p.nextTextValue()); } else if (PROPERTIES.equals(fieldName)) { //object p.nextToken(); while (p.nextToken() != JsonToken.END_OBJECT) { crs.getProperties().put(p.getCurrentName(), p.getValueAsString()); } } } return crs; } /** * Parse a Coordinate. * JsonParser location MUST be on a START_ARRAY token. * @param p JsonParser location MUST be on a START_ARRAY token. * @return an array of double like [X,Y,(Z)] * @throws IOException */ private static double[] parsePoint(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); double[] pt = new double[2]; int idx = 0; // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { if (idx == 2) { pt = Arrays.copyOf(pt, 3); } pt[idx++] = p.getDoubleValue(); } return pt; } /** * Parse a LineString/MultiPoint. * JsonParser location MUST be on a START_ARRAY token. * @param p JsonParser location MUST be on a START_ARRAY token. * @return an array of double like [[X0,Y0,(Z0)], [X1,Y1,(Z1)]] * @throws IOException */ private static double[][] parseLineString(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); List<double[]> line = new ArrayList<>(); // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { line.add(parsePoint(p)); } return line.toArray(new double[line.size()][]); } /** * Parse a List of LineString or Polygon. * JsonParser location MUST be on a START_ARRAY token. * @param p JsonParser location MUST be on a START_ARRAY token. * @return an array of double like * [ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], ... * ] * @throws IOException */ private static double[][][] parseMultiLineString(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); List<double[][]> lines = new ArrayList<>(); // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { lines.add(parseLineString(p)); } return lines.toArray(new double[lines.size()][][]); } /** * Parse a List of Polygons (list of list of LineString). * JsonParser location MUST be on a START_ARRAY token. * @param p JsonParser location MUST be on a START_ARRAY token. * @return an array of double like * [[ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]] * ],[ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]] * ], ... ] * @throws IOException */ private static double[][][][] parseMultiPolygon(JsonParser p) throws IOException { assert(p.getCurrentToken() == JsonToken.START_ARRAY); List<double[][][]> polygons = new ArrayList<>(); // messages is array, loop until token equal to "]" while (p.nextToken() != JsonToken.END_ARRAY) { polygons.add(parseMultiLineString(p)); } return polygons.toArray(new double[polygons.size()][][][]); } }