package org.geotoolkit.data.geojson; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.vividsolutions.jts.geom.Geometry; import org.apache.sis.referencing.CommonCRS; import org.geotoolkit.data.geojson.binding.GeoJSONGeometry; import org.geotoolkit.data.geojson.utils.GeoJSONParser; import org.geotoolkit.data.geojson.utils.GeoJSONUtils; import org.geotoolkit.data.geojson.utils.GeometryUtils; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.text.NumberFormat; import java.util.Collection; import java.util.List; import java.util.Locale; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; import org.apache.sis.feature.FeatureExt; import org.apache.sis.internal.feature.AttributeConvention; import static org.geotoolkit.data.geojson.utils.GeoJSONMembres.*; import static org.geotoolkit.data.geojson.utils.GeoJSONTypes.*; import static org.geotoolkit.data.geojson.binding.GeoJSONGeometry.*; 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.Operation; import org.opengis.feature.PropertyType; /** * @author Quentin Boileau (Geomatys) */ class GeoJSONWriter implements Closeable, Flushable { private static final NumberFormat COORD_FORMAT = NumberFormat.getInstance(Locale.US); private final static String SYS_LF; static { String lf = null; try { lf = System.getProperty("line.separator"); } catch (Throwable t) { } // access exception? SYS_LF = (lf == null) ? "\n" : lf; } private final JsonGenerator writer; private final OutputStream outputStream; private boolean first = true; private boolean prettyPrint = true; // state boolean to ensure that we can't call writeStartFeatureCollection // if we first called writeSingleFeature private boolean isFeatureCollection = false; private boolean isSingleFeature = false; private boolean isSingleGeometry = false; @Deprecated GeoJSONWriter(File file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException { this(file.toPath(), encoding, doubleAccuracy, prettyPrint); } GeoJSONWriter(Path file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException { this.prettyPrint = prettyPrint; this.outputStream = Files.newOutputStream(file, CREATE, WRITE, TRUNCATE_EXISTING); if (prettyPrint) { this.writer = GeoJSONParser.FACTORY.createGenerator(outputStream, encoding).useDefaultPrettyPrinter(); } else { this.writer = GeoJSONParser.FACTORY.createGenerator(outputStream, encoding); } COORD_FORMAT.setMaximumFractionDigits(doubleAccuracy); } GeoJSONWriter(OutputStream stream, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException { this.prettyPrint = prettyPrint; this.outputStream = null; if (prettyPrint) { this.writer = GeoJSONParser.FACTORY.createGenerator(stream, encoding).useDefaultPrettyPrinter(); } else { this.writer = GeoJSONParser.FACTORY.createGenerator(stream, encoding); } this.writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true); COORD_FORMAT.setMaximumFractionDigits(doubleAccuracy); } void writeStartFeatureCollection(CoordinateReferenceSystem crs, Envelope envelope) throws IOException { assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) : "Can't write FeatureCollection if we start a single feature or geometry GeoJSON."; isFeatureCollection = true; writer.writeStartObject(); writeNewLine(); writer.writeStringField(TYPE, FEATURE_COLLECTION); writeNewLine(); if (crs != null && org.geotoolkit.referencing.CRS.equalsApproximatively(crs, CommonCRS.defaultGeographic())) { writeCRS(crs); writeNewLine(); } if (envelope != null) { //TODO write bbox writeNewLine(); } } void writeEndFeatureCollection() throws IOException { assert(isFeatureCollection && !isSingleFeature && !isSingleGeometry) : "Can't write FeatureCollection end before writeStartFeatureCollection()."; if (!first) { writer.writeEndArray(); //close feature collection array } writer.writeEndObject(); //close root object } /** * Write GeoJSON with a single feature * @param feature * @throws IOException * @throws IllegalArgumentException */ void writeSingleFeature(Feature feature) throws IOException, IllegalArgumentException { assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) : "writeSingleFeature can called only once per GeoJSONWriter."; isSingleFeature = true; writeFeature(feature, true); } void writeFeature(Feature feature) throws IOException, IllegalArgumentException { assert(isFeatureCollection && !isSingleFeature && !isSingleGeometry) : "Can't write a Feature before writeStartFeatureCollection."; writeFeature(feature, false); } /** * Write a Feature. * @param feature * @param single * @throws IOException * @throws IllegalArgumentException */ private void writeFeature(Feature feature, boolean single) throws IOException, IllegalArgumentException { if (!single) { if (first) { writer.writeArrayFieldStart(FEATURES); writeNewLine(); first = false; } } writer.writeStartObject(); writer.writeStringField(TYPE, FEATURE); writer.writeStringField(ID, feature.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString()).toString()); //write CRS if (single) { final CoordinateReferenceSystem crs = FeatureExt.getCRS(feature.getType()); if (crs != null && !org.geotoolkit.referencing.CRS.equalsApproximatively(crs, CommonCRS.defaultGeographic())) { writeCRS(crs); } } //write geometry Object geom = feature.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString()); if (geom != null) { writer.writeFieldName(GEOMETRY); writeFeatureGeometry((Geometry) geom); } //write properties writeProperties(feature, PROPERTIES, true); writer.writeEndObject(); if (!single && !prettyPrint) writer.writeRaw(SYS_LF); } private void writeNewLine() throws IOException { if (!prettyPrint) writer.writeRaw(SYS_LF); } /** * Write CoordinateReferenceSystem * @param crs * @throws IOException */ private void writeCRS(CoordinateReferenceSystem crs) throws IOException { writer.writeObjectFieldStart(CRS); writer.writeStringField(TYPE, CRS_NAME); writer.writeObjectFieldStart(PROPERTIES); writer.writeStringField(NAME, GeoJSONUtils.toURN(crs)); writer.writeEndObject();//close properties writer.writeEndObject();//close crs } /** * Write ComplexAttribute. * @param edited * @param fieldName * @param writeFieldName * @throws IOException * @throws IllegalArgumentException */ private void writeProperties(Feature edited, String fieldName, boolean writeFieldName) throws IOException, IllegalArgumentException { if (writeFieldName) { writer.writeObjectFieldStart(fieldName); } else { writer.writeStartObject(); } FeatureType type = edited.getType(); Collection<? extends PropertyType> descriptors = type.getProperties(true); for (PropertyType propType : descriptors) { if(AttributeConvention.contains(propType.getName())) continue; if(AttributeConvention.isGeometryAttribute(propType)) continue; final String name = propType.getName().tip().toString(); final Object value = edited.getPropertyValue(propType.getName().toString()); if(propType instanceof AttributeType){ final AttributeType attType = (AttributeType) propType; if(attType.getMaximumOccurs()>1){ writer.writeArrayFieldStart(name); for(Object v : (Collection)value){ writeProperty(name, v, false); } writer.writeEndArray(); }else{ writeProperty(name, value, true); } }else if(propType instanceof FeatureAssociationRole){ final FeatureAssociationRole asso = (FeatureAssociationRole) propType; if(asso.getMaximumOccurs()>1){ writer.writeArrayFieldStart(name); for(Object v : (Collection)value){ writeProperty(name, v, false); } writer.writeEndArray(); }else{ writeProperty(name, value, true); } }else if(propType instanceof Operation){ writeProperty(name, value, true); } } writer.writeEndObject(); } /** * Write a property (Complex or Simple) * @param property * @param writeFieldName * @throws IOException */ private void writeProperty(String name, Object value, boolean writeFieldName) throws IOException, IllegalArgumentException { if (value instanceof Feature) { writeProperties((Feature) value, name, writeFieldName); } else { writeAttribute(name, value, writeFieldName); } } /** * Write an Attribute and check if attribute value is assignable to binding class. * @param property * @param writeFieldName * @throws IOException */ private void writeAttribute(String name, Object value, boolean writeFieldName) throws IOException, IllegalArgumentException { if (writeFieldName) { writer.writeFieldName(name); } GeoJSONUtils.writeValue(value, writer); } /** * Write a GeometryAttribute * @param geom * @throws IOException */ void writeSingleGeometry(Attribute geom) throws IOException { assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) : "writeSingleGeometry can called only once per GeoJSONWriter."; isSingleGeometry = true; GeoJSONGeometry jsonGeometry = GeometryUtils.toGeoJSONGeometry((Geometry) geom.getValue()); writeGeoJSONGeometry(jsonGeometry); } /** * Write a JTS Geometry * @param geom * @throws IOException */ void writeSingleGeometry(Geometry geom) throws IOException { assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) : "writeSingleGeometry can called only once per GeoJSONWriter."; isSingleGeometry = true; GeoJSONGeometry jsonGeometry = GeometryUtils.toGeoJSONGeometry(geom); writeGeoJSONGeometry(jsonGeometry); } /** * Write a GeometryAttribute * @param geom * @throws IOException */ private void writeFeatureGeometry(Geometry geom) throws IOException { writeGeoJSONGeometry(GeometryUtils.toGeoJSONGeometry(geom)); } /** * Write a GeoJSONGeometry * @param jsonGeometry * @throws IOException */ private void writeGeoJSONGeometry(GeoJSONGeometry jsonGeometry) throws IOException { writer.writeStartObject(); writer.writeStringField(TYPE, jsonGeometry.getType()); if (jsonGeometry instanceof GeoJSONGeometryCollection) { List<GeoJSONGeometry> geometries = ((GeoJSONGeometryCollection) jsonGeometry).getGeometries(); writer.writeArrayFieldStart(GEOMETRIES); // "geometries" : [ for (GeoJSONGeometry geometry : geometries) { writeGeoJSONGeometry(geometry); } writer.writeEndArray(); // "]" } else { writer.writeArrayFieldStart(COORDINATES); // "coordinates" : [ if (jsonGeometry instanceof GeoJSONPoint) { writeArray(((GeoJSONPoint) jsonGeometry).getCoordinates()); } else if (jsonGeometry instanceof GeoJSONLineString) { writeArray(((GeoJSONLineString) jsonGeometry).getCoordinates()); } else if (jsonGeometry instanceof GeoJSONPolygon) { writeArray(((GeoJSONPolygon) jsonGeometry).getCoordinates()); } else if (jsonGeometry instanceof GeoJSONMultiPoint) { writeArray(((GeoJSONMultiPoint) jsonGeometry).getCoordinates()); } else if (jsonGeometry instanceof GeoJSONMultiLineString) { writeArray(((GeoJSONMultiLineString) jsonGeometry).getCoordinates()); } else if (jsonGeometry instanceof GeoJSONMultiPolygon) { writeArray(((GeoJSONMultiPolygon) jsonGeometry).getCoordinates()); } else { throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry); } writer.writeEndArray(); // "]" } writer.writeEndObject(); } private void writeArray(double[] coordinates) throws IOException { for (double coordinate : coordinates) { writer.writeNumber(COORD_FORMAT.format(coordinate)); } } private void writeArray(double[][] coordinates) throws IOException { for (double[] coordinate : coordinates) { writer.writeStartArray(); // "[" writeArray(coordinate); writer.writeEndArray(); // "]" } } private void writeArray(double[][][] coordinates) throws IOException { for (double[][] coordinate : coordinates) { writer.writeStartArray(); // "[" writeArray(coordinate); writer.writeEndArray(); // "]" } } private void writeArray(double[][][][] coordinates) throws IOException { for (double[][][] coordinate : coordinates) { writer.writeStartArray(); // "[" writeArray(coordinate); writer.writeEndArray(); // "]" } } @Override public void flush() throws IOException { if (writer != null) { writer.flush(); } } @Override public void close() throws IOException { if (writer != null) { writer.close(); } if (outputStream != null) { outputStream.close(); } } }