From commits-return-13518-apmail-sis-commits-archive=sis.apache.org@sis.apache.org Tue Apr 28 08:19:43 2020 Return-Path: X-Original-To: apmail-sis-commits-archive@www.apache.org Delivered-To: apmail-sis-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [207.244.88.153]) by minotaur.apache.org (Postfix) with SMTP id 7740F19748 for ; Tue, 28 Apr 2020 08:19:42 +0000 (UTC) Received: (qmail 31226 invoked by uid 500); 28 Apr 2020 08:19:42 -0000 Delivered-To: apmail-sis-commits-archive@sis.apache.org Received: (qmail 31201 invoked by uid 500); 28 Apr 2020 08:19:41 -0000 Mailing-List: contact commits-help@sis.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: sis-dev@sis.apache.org Delivered-To: mailing list commits@sis.apache.org Received: (qmail 31192 invoked by uid 99); 28 Apr 2020 08:19:41 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 28 Apr 2020 08:19:41 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 6EF1F890A8; Tue, 28 Apr 2020 08:19:41 +0000 (UTC) Date: Tue, 28 Apr 2020 08:19:41 +0000 To: "commits@sis.apache.org" Subject: [sis] 01/01: GeoJson : add GeoJson DataStore MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: jsorel@apache.org In-Reply-To: <158806198042.1948.5653027646928130524@gitbox.apache.org> References: <158806198042.1948.5653027646928130524@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: sis X-Git-Refname: refs/heads/feat/geojson X-Git-Reftype: branch X-Git-Rev: d80be716b96503eb2d35fefcbb9cfcd454da9678 X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20200428081941.6EF1F890A8@gitbox.apache.org> This is an automated email from the ASF dual-hosted git repository. jsorel pushed a commit to branch feat/geojson in repository https://gitbox.apache.org/repos/asf/sis.git commit d80be716b96503eb2d35fefcbb9cfcd454da9678 Author: jsorel AuthorDate: Tue Oct 29 17:07:57 2019 +0100 GeoJson : add GeoJson DataStore --- storage/pom.xml | 1 + storage/{ => sis-geojson}/pom.xml | 98 +-- .../sis/internal/geojson/FeatureTypeUtils.java | 597 +++++++++++++++ .../apache/sis/internal/geojson/GeoJSONParser.java | 691 +++++++++++++++++ .../apache/sis/internal/geojson/GeoJSONUtils.java | 580 +++++++++++++++ .../sis/internal/geojson/LiteJsonLocation.java | 112 +++ .../sis/internal/geojson/binding/GeoJSONCRS.java | 93 +++ .../internal/geojson/binding/GeoJSONFeature.java | 68 ++ .../geojson/binding/GeoJSONFeatureCollection.java | 196 +++++ .../internal/geojson/binding/GeoJSONGeometry.java | 504 +++++++++++++ .../internal/geojson/binding/GeoJSONObject.java | 77 ++ .../org/apache/sis/storage/geojson/Bundle.java | 212 ++++++ .../apache/sis/storage/geojson/Bundle.properties | 5 + .../sis/storage/geojson/Bundle_en.properties | 5 + .../sis/storage/geojson/Bundle_fr.properties | 5 + .../sis/storage/geojson/GeoJSONConstants.java | 59 ++ .../sis/storage/geojson/GeoJSONFileWriter.java | 131 ++++ .../sis/storage/geojson/GeoJSONProvider.java | 183 +++++ .../apache/sis/storage/geojson/GeoJSONReader.java | 356 +++++++++ .../apache/sis/storage/geojson/GeoJSONStore.java | 459 ++++++++++++ .../sis/storage/geojson/GeoJSONStreamWriter.java | 220 ++++++ .../apache/sis/storage/geojson/GeoJSONWriter.java | 469 ++++++++++++ .../org.apache.sis.storage.DataStoreProvider | 1 + .../storage/geojson/FeatureTypeUtilsTest.java | 182 +++++ .../internal/storage/geojson/GeoJSONReadTest.java | 344 +++++++++ .../internal/storage/geojson/GeoJSONWriteTest.java | 581 +++++++++++++++ .../storage/geojson/LiteJsonLocationTest.java | 112 +++ .../apache/sis/test/suite/GeoJSONTestSuite.java | 34 + .../sis/internal/storage/geojson/f_prop_array.json | 7 + .../sis/internal/storage/geojson/feature.json | 69 ++ .../storage/geojson/featurecollection.json | 158 ++++ .../internal/storage/geojson/geometries.properties | 8 + .../storage/geojson/geometrycollection.json | 188 +++++ .../sis/internal/storage/geojson/linestring.json | 109 +++ .../sis/internal/storage/geojson/longValue.json | 24 + .../internal/storage/geojson/multilinestring.json | 85 +++ .../sis/internal/storage/geojson/multipoint.json | 13 + .../sis/internal/storage/geojson/multipolygon.json | 815 +++++++++++++++++++++ .../apache/sis/internal/storage/geojson/point.json | 7 + .../sis/internal/storage/geojson/polygon.json | 789 ++++++++++++++++++++ .../geojson/sample_with_null_properties.json | 502 +++++++++++++ 41 files changed, 9079 insertions(+), 70 deletions(-) diff --git a/storage/pom.xml b/storage/pom.xml index fe99bdb..081c953 100644 --- a/storage/pom.xml +++ b/storage/pom.xml @@ -169,6 +169,7 @@ sis-sqlstore sis-netcdf sis-geotiff + sis-geojson sis-earth-observation sis-gdal diff --git a/storage/pom.xml b/storage/sis-geojson/pom.xml similarity index 57% copy from storage/pom.xml copy to storage/sis-geojson/pom.xml index fe99bdb..ce96135 100644 --- a/storage/pom.xml +++ b/storage/sis-geojson/pom.xml @@ -27,7 +27,7 @@ org.apache.sis - parent + storage 2.0-SNAPSHOT @@ -35,12 +35,11 @@ - storage - pom - Apache SIS storage + org.apache.sis.storage + sis-geojson + Apache SIS GeoJSON storage - Group of modules for reading and writing data from/to various storages. - Storages are typically file formats or a database schemas. + Geojson DataStore implementation. @@ -72,26 +71,6 @@ - - Thi Phuong Hao Nguyen - nguyenthiphuonghao243@gmail.com - VNSC - http://vnsc.org.vn - +7 - - developer - - - - Minh Chinh Vu - chinhvm.uet.1995@gmail.com - VNSC - http://vnsc.org.vn - +7 - - developer - - @@ -100,21 +79,18 @@ =========================================================== --> - - - org.apache.sis.core - sis-build-helper - ${sis.plugin.version} - - - - compile-resources - collect-jars - - - + org.apache.maven.plugins + maven-jar-plugin + + + + + org.apache.sis.storage.geojson + + + + @@ -125,52 +101,34 @@ =========================================================== --> - org.apache.sis.core - sis-metadata + org.apache.sis.storage + sis-storage ${project.version} org.apache.sis.core - sis-referencing + sis-cql ${project.version} - org.opengis - geoapi-pending - - - - - org.opengis - geoapi-conformance + org.locationtech.jts + jts-core - org.apache.derby - derby - test + com.fasterxml.jackson.core + jackson-databind + 2.10.0 + org.apache.sis.core - sis-utility + sis-feature ${project.version} test-jar - test - - - - sis-storage - sis-shapefile - sis-xmlstore - sis-sqlstore - sis-netcdf - sis-geotiff - sis-earth-observation - sis-gdal - + + diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/FeatureTypeUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/FeatureTypeUtils.java new file mode 100644 index 0000000..d890837 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/FeatureTypeUtils.java @@ -0,0 +1,597 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import static java.nio.file.StandardOpenOption.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.sis.feature.builder.AttributeRole; +import org.apache.sis.feature.builder.AttributeTypeBuilder; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.internal.system.DefaultFactories; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.Static; +import org.apache.sis.util.iso.Names; +import org.apache.sis.util.iso.SimpleInternationalString; +import org.locationtech.jts.geom.Geometry; +import org.opengis.feature.AttributeType; +import org.opengis.feature.FeatureAssociationRole; +import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyType; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.FactoryException; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; +import org.opengis.util.NameFactory; + +/** + * An utility class to handle read/write of FeatureType into a JSON schema file. + * + * Theses schema are inspired from JSON-Schema specification. + * Changes are : + * - introducing of a {@code javatype} that define Java class used. + * - introducing of a {@code nillable} property for nullable attributes + * - introducing of a {@code userdata} Map property that contain previous user data information. + * - introducing of a {@code geometry} property to describe a geometry + * - introducing of a {@code crs} property to describe a geometry crs + * + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public final class FeatureTypeUtils extends Static { + + private static final String TITLE = "title"; + private static final String TYPE = "type"; + private static final String JAVA_TYPE = "javatype"; + private static final String DESCRIPTION = "description"; + private static final String PROPERTIES = "properties"; + private static final String PRIMARY_KEY = "primaryKey"; + private static final String RESTRICTION = "restriction"; + private static final String REQUIRED = "required"; + private static final String MIN_ITEMS = "minItems"; + private static final String MAX_ITEMS = "maxItems"; + private static final String USER_DATA = "userdata"; + private static final String GEOMETRY = "geometry"; + private static final String GEOMETRY_ATT_NAME = "geometryName"; + private static final String CRS = "crs"; + + private static final String OBJECT = "object"; + private static final String ARRAY = "array"; + private static final String INTEGER = "integer"; + private static final String NUMBER = "number"; + private static final String STRING = "string"; + private static final String BOOLEAN = "boolean"; + + /** + * Write a FeatureType in output File. + * + * @param ft + * @param output + * @throws IOException + */ + public static void writeFeatureType(FeatureType ft, Path output) throws IOException, DataStoreException { + ArgumentChecks.ensureNonNull("FeatureType", ft); + ArgumentChecks.ensureNonNull("outputFile", output); + + final AttributeType geom = GeoJSONUtils + .castOrUnwrap(GeoJSONUtils.getDefaultGeometry(ft)) + .orElseThrow(() -> new DataStoreException("No default Geometry in given FeatureType : " + ft)); + + try (OutputStream outStream = Files.newOutputStream(output, CREATE, WRITE, TRUNCATE_EXISTING); + JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) { + + writer.useDefaultPrettyPrinter(); + //start write feature collection. + writer.writeStartObject(); + writer.writeStringField(TITLE, ft.getName().tip().toString()); + writer.writeStringField(TYPE, OBJECT); + writer.writeStringField(JAVA_TYPE, "FeatureType"); + if (ft.getDescription() != null) { + writer.writeStringField(DESCRIPTION, ft.getDescription().toString()); + } + + writeGeometryType(geom, writer); + writeProperties(ft, writer); + + writer.writeEndObject(); + writer.flush(); + } + } + + public static void writeFeatureTypes(List fts, OutputStream output) throws IOException, DataStoreException { + ArgumentChecks.ensureNonNull("FeatureType", fts); + ArgumentChecks.ensureNonNull("outputStream", output); + + if (fts.isEmpty()) { + return; + } + + if (fts.size() > 1) { + JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output, JsonEncoding.UTF8).useDefaultPrettyPrinter(); + writer.writeStartArray(); + for (FeatureType ft : fts) { + writeFeatureType(ft, output, writer); + } + writer.writeEndArray(); + writer.flush(); + writer.close(); + } else { + writeFeatureType(fts.get(0), output); + } + } + + /** + * Write a FeatureType in output File. + * + * @param ft + * @param output + * @throws IOException + */ + public static void writeFeatureType(FeatureType ft, OutputStream output) throws IOException, DataStoreException { + JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output, JsonEncoding.UTF8).useDefaultPrettyPrinter(); + writeFeatureType(ft, output, writer); + writer.flush(); + writer.close(); + } + + private static void writeFeatureType(FeatureType ft, OutputStream output, JsonGenerator writer) throws IOException, DataStoreException { + ArgumentChecks.ensureNonNull("FeatureType", ft); + ArgumentChecks.ensureNonNull("outputStream", output); + + if (GeoJSONUtils.getDefaultGeometry(ft) == null) { + throw new DataStoreException("No default Geometry in given FeatureType : " + ft); + } + + //start write feature collection. + writer.writeStartObject(); + writer.writeStringField(TITLE, ft.getName().tip().toString()); + writer.writeStringField(TYPE, OBJECT); + writer.writeStringField(JAVA_TYPE, "FeatureType"); + if (ft.getDescription() != null) { + writer.writeStringField(DESCRIPTION, ft.getDescription().toString()); + } + + final Optional> geom = GeoJSONUtils.castOrUnwrap( + GeoJSONUtils.getDefaultGeometry(ft) + ); + if (geom.isPresent()) { + writeGeometryType(geom.get(), writer); + } + + writeProperties(ft, writer); + + writer.writeEndObject(); + } + + private static void writeProperties(FeatureType ft, JsonGenerator writer) throws IOException { + writer.writeObjectFieldStart(PROPERTIES); + + Collection descriptors = ft.getProperties(true); + List required = new ArrayList<>(); + + for (PropertyType type : descriptors) { + boolean isRequired = false; + + if (type instanceof FeatureAssociationRole) { + isRequired = writeComplexType((FeatureAssociationRole) type, ((FeatureAssociationRole) type).getValueType(), writer); + } else if (type instanceof AttributeType) { + if (Geometry.class.isAssignableFrom(((AttributeType) type).getValueClass())) { +// GeometryType geometryType = (GeometryType) type; +// isRequired = writeGeometryType(descriptor, geometryType, writer); + } else { + isRequired = writeAttributeType(ft, (AttributeType) type, writer); + } + } + if (isRequired) { + required.add(type.getName().tip().toString()); + } + } + + if (!required.isEmpty()) { + writer.writeArrayFieldStart(REQUIRED); + for (String req : required) { + writer.writeString(req); + } + writer.writeEndArray(); + } + writer.writeEndObject(); + } + + private static boolean writeComplexType(FeatureAssociationRole descriptor, FeatureType complex, JsonGenerator writer) + throws IOException { + + writer.writeObjectFieldStart(descriptor.getName().tip().toString()); + writer.writeStringField(TYPE, OBJECT); + writer.writeStringField(JAVA_TYPE, "ComplexType"); + if (complex.getDescription() != null) { + writer.writeStringField(DESCRIPTION, complex.getDescription().toString()); + } + writer.writeNumberField(MIN_ITEMS, descriptor.getMinimumOccurs()); + writer.writeNumberField(MAX_ITEMS, descriptor.getMaximumOccurs()); + writeProperties(complex, writer); + + writer.writeEndObject(); + + return descriptor.getMinimumOccurs() > 0; + } + + private static boolean writeAttributeType(FeatureType featureType, AttributeType att, JsonGenerator writer) + throws IOException { + + writer.writeObjectFieldStart(att.getName().tip().toString()); + Class binding = att.getValueClass(); + + writer.writeStringField(TYPE, findType(binding)); + writer.writeStringField(JAVA_TYPE, binding.getName()); + if (att.getDescription() != null) { + writer.writeStringField(DESCRIPTION, att.getDescription().toString()); + } + if (GeoJSONUtils.isPartOfPrimaryKey(featureType, att.getName().toString())) { + writer.writeBooleanField(PRIMARY_KEY, true); + } + writer.writeNumberField(MIN_ITEMS, att.getMinimumOccurs()); + writer.writeNumberField(MAX_ITEMS, att.getMaximumOccurs()); +// List restrictions = att.getRestrictions(); +// if (restrictions != null && !restrictions.isEmpty()) { +// final Filter merged = FF.and(restrictions); +// writer.writeStringField(RESTRICTION, CQL.write(merged)); +// } + writer.writeEndObject(); + + return att.getMinimumOccurs() > 0; + } + + private static String findType(Class binding) { + + if (Integer.class.isAssignableFrom(binding)) { + return INTEGER; + } else if (Number.class.isAssignableFrom(binding)) { + return NUMBER; + } else if (Boolean.class.isAssignableFrom(binding)) { + return BOOLEAN; + } else if (binding.isArray()) { + return ARRAY; + } else { + //fallback + return STRING; + } + } + + private static boolean writeGeometryType(AttributeType geometryType, JsonGenerator writer) + throws IOException { + writer.writeObjectFieldStart(GEOMETRY); + writer.writeStringField(TYPE, OBJECT); + if (geometryType.getDescription() != null) { + writer.writeStringField(DESCRIPTION, geometryType.getDescription().toString()); + } + writer.writeStringField(JAVA_TYPE, geometryType.getValueClass().getCanonicalName()); + CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(geometryType); + if (crs != null) { + final Optional urn = GeoJSONUtils.toURN(crs); + if (urn.isPresent()) { + writer.writeStringField(CRS, urn.get()); + } + } + writer.writeStringField(GEOMETRY_ATT_NAME, geometryType.getName().tip().toString()); + writer.writeEndObject(); + return true; + } + + /** + * Read a FeatureType from an input File. + * + * @param input file to read + * @return FeatureType + * @throws IOException + */ + public static FeatureType readFeatureType(Path input) throws IOException, DataStoreException { + + try (InputStream stream = Files.newInputStream(input); + JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) { + + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + parser.nextToken(); + + while (parser.nextToken() != JsonToken.END_OBJECT) { + + final String currName = parser.getCurrentName(); + switch (currName) { + case TITLE: + ftb.setName(parser.nextTextValue()); + break; + case JAVA_TYPE: + String type = parser.nextTextValue(); + if (!"FeatureType".equals(type)) { + throw new DataStoreException("Invalid JSON schema : " + input.getFileName().toString()); + } + break; + case PROPERTIES: + readProperties(ftb, parser); + break; + case GEOMETRY: + readGeometry(ftb, parser); + break; + case DESCRIPTION: + ftb.setDescription(parser.nextTextValue()); + break; + } + } + + try { + return ftb.build(); + } catch (IllegalStateException ex) { + throw new DataStoreException("FeatureType name or default geometry not found in JSON schema\n" + ex.getMessage(), ex); + } + } + } + + private static void readGeometry(FeatureTypeBuilder ftb, JsonParser parser) + throws IOException, DataStoreException { + + Class binding = null; + CoordinateReferenceSystem crs = null; + InternationalString description = null; + String geometryName = null; + + parser.nextToken(); // { + while (parser.nextToken() != JsonToken.END_OBJECT) { // -> } + final String currName = parser.getCurrentName(); + switch (currName) { + case JAVA_TYPE: + String javaTypeValue = parser.nextTextValue(); + if (!"ComplexType".equals(javaTypeValue)) { + try { + binding = Class.forName(javaTypeValue); + } catch (ClassNotFoundException e) { + throw new DataStoreException("Geometry javatype " + javaTypeValue + " invalid : " + e.getMessage(), e); + } + } + break; + + case CRS: + String crsCode = parser.nextTextValue(); + try { + crs = org.apache.sis.referencing.CRS.forCode(crsCode); + } catch (FactoryException e) { + throw new DataStoreException("Geometry crs " + crsCode + " invalid : " + e.getMessage(), e); + } + break; + + case DESCRIPTION: + description = new SimpleInternationalString(parser.nextTextValue()); + break; + case GEOMETRY_ATT_NAME: + geometryName = parser.nextTextValue(); + } + } + + if (binding == null) { + throw new DataStoreException("Binding class not found."); + } + + final GenericName name = geometryName != null ? Names.createLocalName(null, null, geometryName) : Names.createLocalName(null, null, "geometry"); + + final AttributeTypeBuilder atb = ftb.addAttribute(binding); + atb.setName(name); + atb.setDescription(description); + if (crs != null) { + atb.setCRS(crs); + } + atb.setMinimumOccurs(1); + atb.setMaximumOccurs(1); + atb.addRole(AttributeRole.DEFAULT_GEOMETRY); + } + + private static void readProperties(FeatureTypeBuilder ftb, JsonParser parser) + throws IOException, DataStoreException { + parser.nextToken(); // { + + List requiredList = null; + while (parser.nextToken() != JsonToken.END_OBJECT) { // -> } + final JsonToken currToken = parser.getCurrentToken(); + + if (currToken == JsonToken.FIELD_NAME) { + final String currName = parser.getCurrentName(); + + if (REQUIRED.equals(currName)) { + requiredList = parseRequiredArray(parser); + } else { + parseProperty(ftb, parser); + } + } + } + } + + private static void parseProperty(FeatureTypeBuilder ftb, JsonParser parser) + throws IOException, DataStoreException { + + final String attributeName = parser.getCurrentName(); + Class binding = String.class; + boolean primaryKey = false; + int minOccurs = 0; + int maxOccurs = 1; + CharSequence description = null; + String restrictionCQL = null; + Map userData = null; + FeatureTypeBuilder subftb = null; + + parser.nextToken(); + while (parser.nextToken() != JsonToken.END_OBJECT) { + + final String currName = parser.getCurrentName(); + switch (currName) { + case JAVA_TYPE: + String javaTypeValue = parser.nextTextValue(); + if (!"ComplexType".equals(javaTypeValue)) { + try { + binding = Class.forName(javaTypeValue); + } catch (ClassNotFoundException e) { + throw new DataStoreException("Attribute " + attributeName + " invalid : " + e.getMessage(), e); + } + } + break; + case MIN_ITEMS: + minOccurs = parser.nextIntValue(0); + break; + case MAX_ITEMS: + maxOccurs = parser.nextIntValue(1); + break; + case PRIMARY_KEY: + primaryKey = parser.nextBooleanValue(); + break; + case RESTRICTION: + restrictionCQL = parser.nextTextValue(); + break; + case USER_DATA: + userData = parseUserDataMap(parser); + break; + case PROPERTIES: + subftb = new FeatureTypeBuilder(); + readProperties(subftb, parser); + break; + case DESCRIPTION: + description = parser.nextTextValue(); + break; + } + } + + GenericName name = nameValueOf(attributeName); + if (subftb == null) { + //build AttributeDescriptor + if (binding == null) { + throw new DataStoreException("Empty javatype for attribute " + attributeName); + } + + AttributeTypeBuilder atb = ftb.addAttribute(binding) + .setName(name) + .setDescription(description) + .setMinimumOccurs(minOccurs) + .setMaximumOccurs(maxOccurs); + + if (primaryKey) { + atb.addRole(AttributeRole.IDENTIFIER_COMPONENT); + } + + } else { + //build ComplexType + subftb.setName(name); + subftb.setDescription(description); + final FeatureType complexType = subftb.build(); + + ftb.addAssociation(complexType) + .setName(name) + .setMinimumOccurs(minOccurs) + .setMaximumOccurs(maxOccurs); + } + } + + private static Map parseUserDataMap(JsonParser parser) throws IOException { + + Map map = new HashMap<>(); + parser.nextToken(); // { + while (parser.nextToken() != JsonToken.END_OBJECT) { + Object key = parser.getCurrentName(); + JsonToken next = parser.nextToken(); + map.put(key, GeoJSONParser.getValue(next, parser)); + } + return map; + + } + + private static List parseRequiredArray(JsonParser parser) throws IOException { + List requiredList = new ArrayList<>(); + parser.nextToken(); // [ + + while (parser.nextToken() != JsonToken.END_ARRAY) { // -> ] + requiredList.add(parser.getValueAsString()); + } + + return requiredList; + } + + /** + * Parse a string value that can be expressed in 2 different forms : + * JSR-283 extended form : {uri}localpart + * Separator form : uri:localpart + * + * if the given string do not match any, then a Name with no namespace will + * be created and the localpart will be the given string. + */ + public static GenericName nameValueOf(final String candidate) { + + if (candidate.startsWith("{")) { + //name is in extended form + return toSessionNamespaceFromExtended(candidate); + } + + int index = candidate.lastIndexOf(':'); + + if (index <= 0) { + return createName(null, candidate); + } else { + final String uri = candidate.substring(0, index); + final String name = candidate.substring(index + 1, candidate.length()); + return createName(uri, name); + } + + } + + private static GenericName toSessionNamespaceFromExtended(final String candidate) { + final int index = candidate.indexOf('}'); + + if (index == -1) { + throw new IllegalArgumentException("Invalide extended form : " + candidate); + } + + final String uri = candidate.substring(1, index); + final String name = candidate.substring(index + 1, candidate.length()); + + return createName(uri, name); + } + + /** + * + * @param namespace if null or empty will not be used for the name + * @param local mandatory + */ + public static GenericName createName(final String namespace, final String local) { + + // WARNING: DefaultFactories.NAMES is not a public API and may change in any future SIS version. + if (namespace == null || namespace.isEmpty()) { + return DefaultFactories.forBuildin(NameFactory.class).createGenericName(null, local); + } else { + return DefaultFactories.forBuildin(NameFactory.class).createGenericName(null, namespace, local); + } + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONParser.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONParser.java new file mode 100644 index 0000000..b7eecae --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONParser.java @@ -0,0 +1,691 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson; + +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry; +import org.apache.sis.internal.geojson.binding.GeoJSONCRS; +import org.apache.sis.internal.geojson.binding.GeoJSONObject; +import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONFeature; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +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 org.apache.sis.util.logging.Logging; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONLineString; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPoint; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPolygon; +import static org.apache.sis.storage.geojson.GeoJSONConstants.*; + +/** + * Efficient GeoJSONParsing using jackson {@link JsonParser} + * + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public final class GeoJSONParser { + + public static final JsonFactory FACTORY = new JsonFactory(); + public static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.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 + */ + 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 parseMap(JsonParser p) throws IOException { + Map 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 parseArray(JsonParser p) throws IOException { + assert (p.getCurrentToken() == JsonToken.START_ARRAY); + List 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 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) { + final long value = p.getValueAsLong(); + if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) { + return (int) value; + } + return value; + } 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.apache.sis.storage.geojson.GeoJSONConstants} + * @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.apache.sis.storage.geojson.GeoJSONConstants} + * @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 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 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 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()][][][]); + } + +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONUtils.java new file mode 100644 index 0000000..01246e0 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONUtils.java @@ -0,0 +1,580 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonLocation; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.io.wkt.Convention; +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.internal.geojson.binding.GeoJSONObject; +import org.opengis.geometry.Envelope; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.FactoryException; + +import java.io.*; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.ParseException; +import java.util.Collection; +import java.util.logging.Level; + +import org.apache.sis.util.Utilities; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.TimeZone; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.apache.sis.feature.AbstractOperation; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.internal.storage.io.IOUtilities; +import org.apache.sis.io.wkt.WKTFormat; +import org.apache.sis.metadata.iso.citation.Citations; +import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.util.Numbers; +import static org.apache.sis.storage.geojson.GeoJSONConstants.*; +import org.apache.sis.util.Static; +import org.opengis.feature.AttributeType; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; +import org.opengis.feature.IdentifiedType; +import org.opengis.feature.Operation; +import org.opengis.feature.PropertyNotFoundException; +import org.opengis.feature.PropertyType; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public final class GeoJSONUtils extends Static { + + /** + * Fallback CRS + */ + private static final CoordinateReferenceSystem DEFAULT_CRS = CommonCRS.WGS84.normalizedGeographic(); + + /** + * A test to know if a given property is an SIS convention or not. Return true if + * the property is NOT marked as an SIS convention, false otherwise. + */ + public static final Predicate IS_NOT_CONVENTION = p -> !AttributeConvention.contains(p.getName()); + + /** + * Extract the coordinate reference system associated to the primary geometry + * of input data type. + * + * @implNote + * Primary geometry is determined using {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }. + * + * @param type The data type to extract reference system from. + * @return The CRS associated to the default geometry of this data type, or + * a null value if we cannot determine what is the primary geometry of the + * data type. Note that a null value is also returned if a geometry property + * is found, but no CRS characteristics is associated with it. + */ + public static CoordinateReferenceSystem getCRS(FeatureType type){ + try { + return getCRS(getDefaultGeometry(type)); + } catch (IllegalArgumentException | IllegalStateException ex) { + //no default geometry property + return null; + } + } + + /** + * Extract CRS characteristic if it exist. + * + * @param type + * @return CoordinateReferenceSystem or null + */ + public static CoordinateReferenceSystem getCRS(PropertyType type){ + return getCharacteristicValue(type, AttributeConvention.CRS_CHARACTERISTIC.toString(), null); + } + + /** + * Extract characteristic value if it exist. + * + * @param expected value class + * @param type base type to search in + * @param charName characteristic name + * @param defaulValue default value if characteristic is missing or null. + * @return characteristic value or default value is not found + */ + public static T getCharacteristicValue(PropertyType type, String charName, T defaulValue){ + while (type instanceof Operation) { + type = (PropertyType) ((Operation) type).getResult(); + } + if (type instanceof AttributeType) { + final AttributeType at = (AttributeType) ((AttributeType) type).characteristics().get(charName); + if (at != null) { + T val = (T) at.getDefaultValue(); + return (val == null) ? defaulValue : val; + } + } + return defaulValue; + } + + /** + * Search for the main geometric property in the given type. We'll search + * for an SIS convention first (see + * {@link AttributeConvention#GEOMETRY_PROPERTY}. If no convention is set on + * the input type, we'll check if it contains a single geometric property. + * If it's the case, we return it. Otherwise (no or multiple geometries), we + * throw an exception. + * + * @param type The data type to search into. + * @return The main geometric property we've found. + * @throws PropertyNotFoundException If no geometric property is available + * in the given type. + * @throws IllegalStateException If no convention is set (see + * {@link AttributeConvention#GEOMETRY_PROPERTY}), and we've found more than + * one geometry. + */ + public static PropertyType getDefaultGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException { + PropertyType geometry; + try { + geometry = type.getProperty(AttributeConvention.GEOMETRY_PROPERTY.toString()); + } catch (PropertyNotFoundException e) { + try { + geometry = searchForGeometry(type); + } catch (RuntimeException e2) { + e2.addSuppressed(e); + throw e2; + } + } + + return geometry; + } + + /** + * Search for a geometric attribute outside SIS conventions. More accurately, + * we expect the given type to have a single geometry attribute. If many are + * found, an exception is thrown. + * + * @param type The data type to search into. + * @return The only geometric property we've found. + * @throws PropertyNotFoundException If no geometric property is available in + * the given type. + * @throws IllegalStateException If we've found more than one geometry. + */ + private static PropertyType searchForGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException { + final List geometries = type.getProperties(true).stream() + .filter(IS_NOT_CONVENTION) + .filter(AttributeConvention::isGeometryAttribute) + .collect(Collectors.toList()); + + if (geometries.size() < 1) { + throw new PropertyNotFoundException("No geometric property can be found outside of sis convention."); + } else if (geometries.size() > 1) { + throw new IllegalStateException("Multiple geometries found. We don't know which one to select."); + } else { + return geometries.get(0); + } + } + + /** + * Get main geometry property value. The ways this method determines default + * geometry property are the same as {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }. + * + * @param input the feature to extract geometry from. + * @return Value of the main geometric property of the given feature. The returned + * optional will be empty only if the feature defines a geometric property, but has + * no value for it. + * @throws PropertyNotFoundException If no geometric property is available in + * the given feature. + * @throws IllegalStateException If we've found more than one geometry. + */ + public static Optional getDefaultGeometryValue(Feature input) throws PropertyNotFoundException, IllegalStateException { + Object geometry; + try { + geometry = input.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString()); + } catch (PropertyNotFoundException ex) { + try { + final PropertyType geomType = getDefaultGeometry(input.getType()); + geometry = input.getPropertyValue(geomType.getName().toString()); + } catch (RuntimeException e) { + e.addSuppressed(ex); + throw e; + } + } + + return Optional.ofNullable(geometry); + } + + /** + * Parse LinkedCRS (href + type). + * @param href + * @param crsType + * @return CoordinateReferenceSystem or null. + */ + public static CoordinateReferenceSystem parseCRS(String href, String crsType) { + String wkt = null; + try (InputStream stream = new URL(href).openStream()) { + wkt = IOUtilities.toString(stream); + } catch (IOException e) { + GeoJSONParser.LOGGER.log(Level.WARNING, "Can't access to linked CRS "+href, e); + } + + if (wkt != null) { + WKTFormat format = new WKTFormat(Locale.ENGLISH, TimeZone.getTimeZone("GMT")); + if (crsType.equals(CRS_TYPE_OGCWKT)) { + format.setConvention(Convention.WKT1); + } else if (crsType.equals(CRS_TYPE_ESRIWKT)) { + format.setConvention(Convention.WKT1_COMMON_UNITS); + } + try { + Object obj = format.parseObject(wkt); + if (obj instanceof CoordinateReferenceSystem) { + return (CoordinateReferenceSystem) obj; + } else { + GeoJSONParser.LOGGER.log(Level.WARNING, "Parsed WKT is not a CRS "+wkt); + } + } catch (ParseException e) { + GeoJSONParser.LOGGER.log(Level.WARNING, "Can't parse CRS WKT " + crsType+ " : "+wkt, e); + } + } + + return null; + } + + /** + * Test if given data type is an attribute as defined by {@link AttributeType}, + * or if it depends on an attribute, and return it (the attribute) if possible. + * @param input the data type to unravel the attribute from. + * @return The found attribute or an empty shell if we cannot find any. + */ + public static Optional> castOrUnwrap(IdentifiedType input) { + // In case an operation also implements attribute type, we check it first. + // TODO : cycle detection ? + while (!(input instanceof AttributeType) && input instanceof Operation) { + input = ((Operation) input).getResult(); + } + + if (input instanceof AttributeType) { + return Optional.of((AttributeType) input); + } + + return Optional.empty(); + } + + /** + * Returns true if property is a component of the feature type primary key. + */ + public static boolean isPartOfPrimaryKey(FeatureType type, String propertyName) { + PropertyType property; + try { + property = type.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString()); + } catch (PropertyNotFoundException ex) { + //no identifier property + return false; + } + if (property instanceof AbstractOperation) { + final Set dependencies = ((AbstractOperation) property).getDependencies(); + return dependencies.contains(propertyName); + } + return false; + } + + /** + * Convert a CoordinateReferenceSystem to a identifier string like + * urn:ogc:def:crs:EPSG::4326 + * @param crs + * @return + */ + public static Optional toURN(CoordinateReferenceSystem crs) { + ArgumentChecks.ensureNonNull("crs", crs); + + String urn = null; + try { + if (Utilities.equalsIgnoreMetadata(crs, CommonCRS.WGS84.normalizedGeographic())) { + crs = CommonCRS.WGS84.normalizedGeographic(); + } + + urn = IdentifiedObjects.lookupURN(crs, Citations.EPSG); + } catch (FactoryException e) { + GeoJSONParser.LOGGER.log(Level.WARNING, "Unable to extract epsg code from given CRS "+crs, e); + } + + return Optional.ofNullable(urn); + } + + /** + * Try to extract/parse the CoordinateReferenceSystem from a GeoJSONObject. + * Use WGS_84 as fallback CRS. + * @param obj GeoJSONObject + * @return GeoJSONObject CoordinateReferenceSystem or fallback CRS (WGS84). + * @throws MalformedURLException + * @throws DataStoreException + */ + public static CoordinateReferenceSystem getCRS(GeoJSONObject obj) throws MalformedURLException, DataStoreException { + CoordinateReferenceSystem crs = null; + try { + if (obj.getCrs() != null) { + crs = obj.getCrs().getCRS(); + } + } catch (FactoryException e) { + throw new DataStoreException(e.getMessage(), e); + } + + if (crs == null) { + crs = DEFAULT_CRS; + } + return crs; + } + + /** + * Utility method Create geotk Envelope if bbox array is filled. + * @return Envelope or null. + */ + public static Envelope getEnvelope(GeoJSONObject obj, CoordinateReferenceSystem crs) { + + double[] bbox = obj.getBbox(); + if (bbox != null) { + GeneralEnvelope env = new GeneralEnvelope(crs); + int dim = bbox.length/2; + if (dim == 2) { + env.setRange(0, bbox[0], bbox[2]); + env.setRange(1, bbox[1], bbox[3]); + } else if (dim == 3) { + env.setRange(0, bbox[0], bbox[3]); + env.setRange(1, bbox[1], bbox[4]); + } + return env; + } + return null; + } + + /** + * Return file name without extension + * @param file candidate + * @return String + */ + public static String getNameWithoutExt(Path file) { + return IOUtilities.filenameWithoutExtension(file.toUri().toString()); + } + + /** + * Returns the filename extension from a {@link String}, {@link File}, {@link URL} or + * {@link URI}. If no extension is found, returns an empty string. + * + * @param path The path as a {@link String}, {@link File}, {@link URL} or {@link URI}. + * @return The filename extension in the given path, or an empty string if none. + */ + public static String extension(final Object path) { + return IOUtilities.extension(path); + } + + /** + * Write an empty FeatureCollection in a file + * @param f output file + * @throws IOException + */ + public static void writeEmptyFeatureCollection(Path f) throws IOException { + + try (OutputStream outStream = Files.newOutputStream(f, CREATE, WRITE, TRUNCATE_EXISTING); + JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) { + + //start write feature collection. + writer.writeStartObject(); + writer.writeStringField(TYPE, FEATURE_COLLECTION); + writer.writeArrayFieldStart(FEATURES); + writer.writeEndArray(); + writer.writeEndObject(); + writer.flush(); + } + } + + /** + * Useful method to help write an object into a JsonGenerator. + * This method can handle : + *
    + *
  • Arrays
  • + *
  • Collection
  • + *
  • Numbers (Double, Float, Short, BigInteger, BigDecimal, integer, Long, Byte)
  • + *
  • Boolean
  • + *
  • String
  • + *
+ * @param value + * @param writer + * @throws IOException + * @throws IllegalArgumentException + */ + public static void writeValue(Object value, JsonGenerator writer) throws IOException, IllegalArgumentException { + + if (value == null) { + writer.writeNull(); + return; + } + + Class binding = value.getClass(); + + if (binding.isArray()) { + if (byte.class.isAssignableFrom(binding.getComponentType())) { + writer.writeBinary((byte[]) value); + } else { + writer.writeStartArray(); + final int size = Array.getLength(value); + for (int i = 0; i < size; i++) { + writeValue(Array.get(value, i), writer); + } + writer.writeEndArray(); + } + + } else if (Collection.class.isAssignableFrom(binding)) { + writer.writeStartArray(); + Collection coll = (Collection) value; + for (Object obj : coll) { + writeValue(obj, writer); + } + writer.writeEndArray(); + + } else if (Double.class.isAssignableFrom(binding)) { + writer.writeNumber((Double) value); + } else if (Float.class.isAssignableFrom(binding)) { + writer.writeNumber((Float) value); + } else if (Short.class.isAssignableFrom(binding)) { + writer.writeNumber((Short) value); + } else if (Byte.class.isAssignableFrom(binding)) { + writer.writeNumber((Byte) value); + } else if (BigInteger.class.isAssignableFrom(binding)) { + writer.writeNumber((BigInteger) value); + } else if (BigDecimal.class.isAssignableFrom(binding)) { + writer.writeNumber((BigDecimal) value); + } else if (Integer.class.isAssignableFrom(binding)) { + writer.writeNumber((Integer) value); + } else if (Long.class.isAssignableFrom(binding)) { + writer.writeNumber((Long) value); + + } else if (Boolean.class.isAssignableFrom(binding)) { + writer.writeBoolean((Boolean) value); + } else if (String.class.isAssignableFrom(binding)) { + writer.writeString(String.valueOf(value)); + } else { + //fallback + writer.writeString(String.valueOf(value)); + } + } + + /** + * Compare {@link JsonLocation} equality without sourceRef test. + * @param loc1 + * @param loc2 + * @return + */ + public static boolean equals(JsonLocation loc1, JsonLocation loc2) { + if (loc1 == null) { + return (loc2 == null); + } + + return loc2 != null && (loc1.getLineNr() == loc2.getLineNr() && + loc1.getColumnNr() == loc2.getColumnNr() && + loc1.getByteOffset() == loc2.getByteOffset() && + loc1.getCharOffset() == loc2.getCharOffset()); + } + + /** + * Check wether the given data type contains an identifier property according + * to SIS convention (see {@link AttributeConvention#IDENTIFIER_PROPERTY}). + * + * @param toSearchIn The data type to scan for an identifier. + * @return True if an sis:identifier property is available. False otherwise. + */ + public static boolean hasIdentifier(final FeatureType toSearchIn) { + try { + toSearchIn.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString()); + return true; + } catch (PropertyNotFoundException ex) { + return false; + } + } + + /** + * If an sis:identifier property is available (see + * {@link AttributeConvention#IDENTIFIER_PROPERTY}), we try to acquire it + * value type (see {@link AttributeType#getValueClass() }. If we cannot + * determine the value type for this property, we simply return an empty + * optional. Note that an error is thrown if the given feature type does not + * contain any identifier property. + * + * @param toSearchIn The property to extract identifier from. + * @return The value class of found property if we can determine it (i.e: + * it's an attribute or an operation from which we can unravel an + * attribute), or an empty object if the property cannot provide value + * class. + * @throws PropertyNotFoundException If no + * {@link AttributeConvention#IDENTIFIER_PROPERTY} is present in the input. + */ + public static Optional getIdentifierType(final FeatureType toSearchIn) throws PropertyNotFoundException { + final PropertyType idProperty = toSearchIn.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString()); + return castOrUnwrap(idProperty).map(AttributeType::getValueClass); + } + + /** + * Create a converter to set values of arbitrary type into the sis:identifier + * property of a given feature type. + * Note: RFC7946 specifies that identifier must be either numeric or string. + * + * @param target The feature type which specifies the sis:identifier, and by + * extension the output value class for the converter to create. + * @return A function capable of converting arbitrary objects into required + * type for sis:identifier property. + * @throws IllegalArgumentException If the given data type provides a bad value + * class for identifier property. + */ + public static Function getIdentifierConverter(final FeatureType target) throws IllegalArgumentException { + final Class identifierType = GeoJSONUtils.getIdentifierType(target) + .orElseThrow(() -> new IllegalArgumentException("Cannot determine the value type for identifier property. Should either be a string or a number.")); + final Function converter; + if (Numbers.isFloat(identifierType)) { + converter = input -> Double.parseDouble(input.toString()); + } else if (Long.class.isAssignableFrom(identifierType)) { + converter = input -> Long.parseLong(input.toString()); + } else if (Numbers.isInteger(identifierType)) { + converter = input -> Integer.parseInt(input.toString()); + } else if (String.class.isAssignableFrom(identifierType)) { + converter = Object::toString; + } else { + throw new IllegalArgumentException("Unsupported type for identifier property. RFC 7946 asks for a string or number data."); + } + + return input -> { + if (input == null || identifierType.getClass().isAssignableFrom(input.getClass())) { + return input; + } + + return converter.apply(input); + }; + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/LiteJsonLocation.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/LiteJsonLocation.java new file mode 100644 index 0000000..3fe39f4 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/LiteJsonLocation.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson; + +import com.fasterxml.jackson.core.JsonLocation; + +import java.util.Objects; + +/** + * Lightweight pojo of {@link JsonLocation} without internal source object + * reference and offset. Because since 2.3.x+ of jackson byteOffset and + * charOffset values depend of underling source type. (InputStream to use + * byteOffset, BufferedReader to use charOffset) + * + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class LiteJsonLocation { + + private final int lineNr; + private final int columnNr; + + public LiteJsonLocation(JsonLocation location) { + this.lineNr = location.getLineNr(); + this.columnNr = location.getColumnNr(); + } + + public int getLineNr() { + return lineNr; + } + + public int getColumnNr() { + return columnNr; + } + + /** + * Check if an JsonLocation position (line and column) is before current + * LiteJsonLocation. + * + * @param o JsonLocation + * @return true if before and false if input JsonLocation is equals or after + * current LiteJsonLocation + */ + public boolean isBefore(JsonLocation o) { + if (o == null) { + return false; + } + LiteJsonLocation that = new LiteJsonLocation(o); + + return lineNr < that.lineNr || (lineNr == that.lineNr && columnNr < that.columnNr); + } + + /** + * Test equality with LiteJsonLocation and JsonLocation input objects + * + * @param o + * @return + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + // not equals if o is null or not an instance of LiteJsonLocation or JsonLocation + if (o == null + || (!LiteJsonLocation.class.isAssignableFrom(o.getClass()) + && !JsonLocation.class.isAssignableFrom(o.getClass()))) { + return false; + } + + LiteJsonLocation that; + if (JsonLocation.class.isAssignableFrom(o.getClass())) { + that = new LiteJsonLocation((JsonLocation) o); + } else { + that = (LiteJsonLocation) o; + } + + return lineNr == that.lineNr && columnNr == that.columnNr; + } + + @Override + public int hashCode() { + return Objects.hash(lineNr, columnNr); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("LiteJsonLocation{"); + sb.append("lineNr=").append(lineNr); + sb.append(", columnNr=").append(columnNr); + sb.append('}'); + return sb.toString(); + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONCRS.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONCRS.java new file mode 100644 index 0000000..63401ef --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONCRS.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson.binding; + +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import org.apache.sis.referencing.crs.AbstractCRS; +import org.apache.sis.referencing.cs.AxesConvention; +import static org.apache.sis.storage.geojson.GeoJSONConstants.*; +import org.apache.sis.internal.geojson.GeoJSONUtils; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.FactoryException; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class GeoJSONCRS implements Serializable { + + private String type; + private final Map properties = new HashMap<>(); + + public GeoJSONCRS() { + } + + public GeoJSONCRS(CoordinateReferenceSystem crs) { + type = CRS_NAME; + setCRS(crs); + } + + public GeoJSONCRS(URL url, String crsType) { + type = CRS_LINK; + if (url != null && crsType != null) { + properties.put(HREF, url.toString()); + properties.put(TYPE, crsType); + } + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getProperties() { + return properties; + } + + public CoordinateReferenceSystem getCRS() throws FactoryException, MalformedURLException { + if (type.equals(CRS_NAME)) { + String name = properties.get(NAME); + CoordinateReferenceSystem crs = org.apache.sis.referencing.CRS.forCode(name); + if (!name.startsWith("urn")) { + //legacy names, we force longitude first for those + crs = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); + } + return crs; + } else if (type.equals(CRS_LINK)) { + final String href = properties.get(HREF); + final String crsType = properties.get(TYPE); + return GeoJSONUtils.parseCRS(href, crsType); + } + return null; + } + + public void setCRS(CoordinateReferenceSystem crs) { + type = CRS_NAME; + GeoJSONUtils.toURN(crs) + .ifPresent(urn -> properties.put(NAME, urn)); + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeature.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeature.java new file mode 100644 index 0000000..a22a580 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeature.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson.binding; + +import org.apache.sis.storage.geojson.GeoJSONConstants; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class GeoJSONFeature extends GeoJSONObject { + + private GeoJSONGeometry geometry; + /** + * Identifier (id attribute) of the feature. According to RFC 7946, it is + * optional and can either be a number or a string. + */ + private Object id; + private Map properties = new HashMap<>(); + + public GeoJSONFeature() { + setType(GeoJSONConstants.FEATURE); + } + + public GeoJSONGeometry getGeometry() { + return geometry; + } + + public void setGeometry(GeoJSONGeometry geometry) { + this.geometry = geometry; + } + + public Object getId() { + return id; + } + + public void setId(Object id) { + this.id = id; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeatureCollection.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeatureCollection.java new file mode 100644 index 0000000..ba2465b --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeatureCollection.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson.binding; + +import org.apache.sis.internal.geojson.GeoJSONParser; +import org.apache.sis.internal.geojson.LiteJsonLocation; +import org.apache.sis.storage.geojson.GeoJSONConstants; +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.apache.sis.util.collection.BackingStoreException; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class GeoJSONFeatureCollection extends GeoJSONObject implements Iterator, Closeable { + + private List features = new ArrayList<>(); + + transient JsonLocation currentPos; + transient GeoJSONFeature current; + transient int currentIdx; + transient InputStream readStream; + transient JsonParser parser; + + /** + * If current GeoJSONFeatureCollection is in lazy parsing mode, sourceInput + * should be not {@code null} and used to create {@link JsonParser object} + */ + transient Path sourceInput; + transient LiteJsonLocation startPos; + transient LiteJsonLocation endPos; + transient Boolean lazyMode; + + public GeoJSONFeatureCollection(Boolean lazyMode) { + setType(GeoJSONConstants.FEATURE_COLLECTION); + this.lazyMode = lazyMode; + } + + public List getFeatures() { + return features; + } + + public void setFeatures(List features) { + this.features = features; + } + + public void setStartPosition(JsonLocation startPos) { + this.startPos = new LiteJsonLocation(startPos); + } + + public void setEndPosition(JsonLocation endPos) { + this.endPos = new LiteJsonLocation(endPos); + } + + public void setSourceInput(Path input) { + this.sourceInput = input; + } + + public Boolean isLazyMode() { + return lazyMode; + } + + @Override + public boolean hasNext() { + try { + findNext(); + return current != null; + } catch (IOException e) { + throw new BackingStoreException(e.getMessage(), e); + } + } + + @Override + public GeoJSONFeature next() { + try { + findNext(); + final GeoJSONFeature ob = current; + current = null; + if (ob == null) { + throw new BackingStoreException("No more feature."); + } + return ob; + } catch (IOException e) { + throw new BackingStoreException(e.getMessage(), e); + } + } + + /** + * Find next Feature from features list or as lazy parsing. + * + * @throws IOException + */ + private void findNext() throws IOException { + if (current != null) { + return; + } + if (lazyMode) { + if (sourceInput == null || startPos == null || endPos == null) { + return; + } + + if (parser == null) { + readStream = Files.newInputStream(sourceInput); + parser = GeoJSONParser.FACTORY.createParser(readStream); + } + + //loop to FeatureCollection start + if (currentPos == null) { + while (!startPos.equals(currentPos)) { + parser.nextToken(); + currentPos = parser.getCurrentLocation(); + } + } + + current = null; + + // set parser to feature object start + while (parser.getCurrentToken() != JsonToken.START_OBJECT && !endPos.equals(currentPos)) { + + if (parser.getCurrentToken() != JsonToken.START_OBJECT && endPos.isBefore(currentPos)) { + //cannot find collection end token and no more start object token + //break loop to avoid infinite search + break; + } + parser.nextToken(); + currentPos = parser.getCurrentLocation(); + } + + if (!endPos.equals(currentPos)) { + GeoJSONObject obj = GeoJSONParser.parseGeoJSONObject(parser); + if (obj instanceof GeoJSONFeature) { + current = (GeoJSONFeature) obj; + } + currentPos = parser.getCurrentLocation(); + } + } else { + if (currentIdx < features.size()) { + current = features.get(currentIdx++); + } else { + current = null; + } + } + } + + @Override + public void remove() { + //do nothing + } + + @Override + public void close() { + //close read stream + if (readStream != null) { + try { + readStream.close(); + } catch (IOException e) { + throw new BackingStoreException(e.getMessage(), e); + } + } + //close parser + if (parser != null && !parser.isClosed()) { + try { + parser.close(); + } catch (IOException e) { + throw new BackingStoreException(e.getMessage(), e); + } + } + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONGeometry.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONGeometry.java new file mode 100644 index 0000000..1c86ea0 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONGeometry.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson.binding; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.sis.storage.geojson.GeoJSONConstants; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.impl.CoordinateArraySequence; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class GeoJSONGeometry extends GeoJSONObject implements Serializable { + + public GeoJSONGeometry() { + } + + /** + * POINT + */ + public static class GeoJSONPoint extends GeoJSONGeometry { + + private double[] coordinates; + + public GeoJSONPoint() { + setType(GeoJSONConstants.POINT); + } + + public double[] getCoordinates() { + return coordinates; + } + + public void setCoordinates(double[] coordinates) { + this.coordinates = coordinates; + } + + } + + /** + * MULTI-POINT + */ + public static class GeoJSONMultiPoint extends GeoJSONGeometry { + + private double[][] coordinates; + + public GeoJSONMultiPoint() { + setType(GeoJSONConstants.MULTI_POINT); + } + + public double[][] getCoordinates() { + return coordinates; + } + + public void setCoordinates(double[][] coordinates) { + this.coordinates = coordinates; + } + } + + /** + * LINESTRING + */ + public static class GeoJSONLineString extends GeoJSONGeometry { + + private double[][] coordinates; + + public GeoJSONLineString() { + setType(GeoJSONConstants.LINESTRING); + } + + public double[][] getCoordinates() { + return coordinates; + } + + public void setCoordinates(double[][] coordinates) { + this.coordinates = coordinates; + } + } + + /** + * MULTI-LINESTRING + */ + public static class GeoJSONMultiLineString extends GeoJSONGeometry { + + private double[][][] coordinates; + + public GeoJSONMultiLineString() { + setType(GeoJSONConstants.MULTI_LINESTRING); + } + + public double[][][] getCoordinates() { + return coordinates; + } + + public void setCoordinates(double[][][] coordinates) { + this.coordinates = coordinates; + } + } + + /** + * POLYGON + */ + public static class GeoJSONPolygon extends GeoJSONGeometry { + + private double[][][] coordinates; + + public GeoJSONPolygon() { + setType(GeoJSONConstants.POLYGON); + } + + public double[][][] getCoordinates() { + return coordinates; + } + + public void setCoordinates(double[][][] coordinates) { + this.coordinates = coordinates; + } + } + + /** + * MULTI-POLYGON + */ + public static class GeoJSONMultiPolygon extends GeoJSONGeometry { + + private double[][][][] coordinates; + + public GeoJSONMultiPolygon() { + setType(GeoJSONConstants.MULTI_POLYGON); + } + + public double[][][][] getCoordinates() { + return coordinates; + } + + public void setCoordinates(double[][][][] coordinates) { + this.coordinates = coordinates; + } + } + + /** + * GEOMETRY-COLLECTION + */ + public static class GeoJSONGeometryCollection extends GeoJSONGeometry { + + protected List geometries = new ArrayList(); + + public GeoJSONGeometryCollection() { + setType(GeoJSONConstants.GEOMETRY_COLLECTION); + } + + public List getGeometries() { + return geometries; + } + + public void setGeometries(List geometries) { + this.geometries = geometries; + } + } + + private static final GeometryFactory GF = new GeometryFactory(); + + /** + * Convert GeoJSONGeometry into JTS Geometry with included CRS + * + * @param jsonGeometry + * @param crs + * @return JTS Geometry + */ + public static Geometry toJTS(GeoJSONGeometry jsonGeometry, CoordinateReferenceSystem crs) { + + if (jsonGeometry != null) { + if (crs == null) { + throw new IllegalArgumentException("Null Coordinate Reference System."); + } + + if (jsonGeometry instanceof GeoJSONPoint) { + return toJTS((GeoJSONPoint) jsonGeometry, crs); + } else if (jsonGeometry instanceof GeoJSONLineString) { + return toJTS((GeoJSONLineString) jsonGeometry, crs); + } else if (jsonGeometry instanceof GeoJSONPolygon) { + return toJTS((GeoJSONPolygon) jsonGeometry, crs); + } else if (jsonGeometry instanceof GeoJSONMultiPoint) { + return toJTS((GeoJSONMultiPoint) jsonGeometry, crs); + } else if (jsonGeometry instanceof GeoJSONMultiLineString) { + return toJTS((GeoJSONMultiLineString) jsonGeometry, crs); + } else if (jsonGeometry instanceof GeoJSONMultiPolygon) { + return toJTS((GeoJSONMultiPolygon) jsonGeometry, crs); + } else if (jsonGeometry instanceof GeoJSONGeometryCollection) { + return toJTS((GeoJSONGeometryCollection) jsonGeometry, crs); + } else { + throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry); + } + } + return null; + } + + private static Coordinate toCoordinate(double[] coord) { + if (coord.length == 2) { + return new Coordinate(coord[0], coord[1]); + } else if (coord.length == 3) { + return new Coordinate(coord[0], coord[1], coord[2]); + } else { + throw new IllegalArgumentException("Coordinates not valid : " + Arrays.toString(coord)); + } + } + + private static CoordinateSequence toCoordinateSequence(double[][] coords) { + + Coordinate[] coordinates = new Coordinate[coords.length]; + if (coords.length > 0) { + for (int i = 0; i < coords.length; i++) { + coordinates[i] = toCoordinate(coords[i]); + } + } + return new CoordinateArraySequence(coordinates); + } + + private static LinearRing toLinearRing(double[][] coords) { + return GF.createLinearRing(toCoordinateSequence(coords)); + } + + private static Polygon toPolygon(double[][][] coords, CoordinateReferenceSystem crs) { + + LinearRing exterior = toLinearRing(coords[0]); + LinearRing[] holes = new LinearRing[coords.length - 1]; + if (coords.length > 1) { + for (int i = 0; i < holes.length; i++) { + holes[i] = toLinearRing(coords[i + 1]); + } + } + + Polygon polygon = GF.createPolygon(exterior, holes); + polygon.setUserData(crs); + return polygon; + } + + private static Point toJTS(GeoJSONPoint jsonPoint, CoordinateReferenceSystem crs) { + double[] coord = jsonPoint.getCoordinates(); + + final Point pt = GF.createPoint(toCoordinate(coord)); + pt.setUserData(crs); + return pt; + } + + private static LineString toJTS(GeoJSONLineString jsonLS, CoordinateReferenceSystem crs) { + double[][] coord = jsonLS.getCoordinates(); + + LineString line = GF.createLineString(toCoordinateSequence(coord)); + line.setUserData(crs); + return line; + } + + private static Geometry toJTS(GeoJSONPolygon jsonPolygon, CoordinateReferenceSystem crs) { + double[][][] coord = jsonPolygon.getCoordinates(); + + if (coord.length <= 0) { + return GF.buildGeometry(Collections.EMPTY_LIST); + } + + return toPolygon(coord, crs); + } + + private static MultiPoint toJTS(GeoJSONMultiPoint jsonMP, CoordinateReferenceSystem crs) { + double[][] coords = jsonMP.getCoordinates(); + + Coordinate[] coordinates = new Coordinate[coords.length]; + if (coords.length > 0) { + for (int i = 0; i < coords.length; i++) { + coordinates[i] = toCoordinate(coords[i]); + } + } + + MultiPoint mpt = GF.createMultiPoint(GF.getCoordinateSequenceFactory().create(coordinates)); + mpt.setUserData(crs); + return mpt; + } + + private static MultiLineString toJTS(GeoJSONMultiLineString jsonMLS, CoordinateReferenceSystem crs) { + double[][][] coords = jsonMLS.getCoordinates(); + + LineString[] lines = new LineString[coords.length]; + if (coords.length > 0) { + for (int i = 0; i < coords.length; i++) { + lines[i] = GF.createLineString(toCoordinateSequence(coords[i])); + } + } + + MultiLineString mls = GF.createMultiLineString(lines); + mls.setUserData(crs); + return mls; + } + + private static MultiPolygon toJTS(GeoJSONMultiPolygon jsonMP, CoordinateReferenceSystem crs) { + double[][][][] coords = jsonMP.getCoordinates(); + + Polygon[] polygons = new Polygon[coords.length]; + if (coords.length > 0) { + for (int i = 0; i < coords.length; i++) { + polygons[i] = toPolygon(coords[i], crs); + } + } + + MultiPolygon mp = GF.createMultiPolygon(polygons); + mp.setUserData(crs); + return mp; + } + + private static GeometryCollection toJTS(GeoJSONGeometryCollection jsonGC, CoordinateReferenceSystem crs) { + if (jsonGC.getGeometries() != null) { + + int size = jsonGC.getGeometries().size(); + Geometry[] geometries = new Geometry[size]; + + for (int i = 0; i < size; i++) { + geometries[i] = toJTS(jsonGC.getGeometries().get(i), crs); + } + + GeometryCollection gc = GF.createGeometryCollection(geometries); + gc.setUserData(crs); + return gc; + } + return null; + } + + /** + * Convert JTS geometry into a GeoJSONGeometry. + * + * @param geom JTS Geometry + * @return GeoJSONGeometry + */ + public static GeoJSONGeometry toGeoJSONGeometry(Geometry geom) { + if (geom == null) { + throw new IllegalArgumentException("Null Geometry."); + } + + if (geom instanceof Point) { + return toGeoJSONGeometry((Point) geom); + } else if (geom instanceof LineString) { + return toGeoJSONGeometry((LineString) geom); + } else if (geom instanceof Polygon) { + return toGeoJSONGeometry((Polygon) geom); + } else if (geom instanceof MultiPoint) { + return toGeoJSONGeometry((MultiPoint) geom); + } else if (geom instanceof MultiLineString) { + return toGeoJSONGeometry((MultiLineString) geom); + } else if (geom instanceof MultiPolygon) { + return toGeoJSONGeometry((MultiPolygon) geom); + } else if (geom instanceof GeometryCollection) { + return toGeoJSONGeometry((GeometryCollection) geom); + } else { + throw new IllegalArgumentException("Unsupported geometry type : " + geom); + } + } + + private static double[] toArray(Coordinate coord) { + double x = coord.getOrdinate(0); + double y = coord.getOrdinate(1); + //do not use getOrdinate for Z, may raise an exception + double z = coord.getZ(); + + if (Double.isNaN(z)) { + return new double[]{x, y}; + } else { + return new double[]{x, y, z}; + } + } + + private static double[][] toArray(Coordinate[] coords) { + double[][] result = new double[coords.length][]; + + for (int i = 0; i < coords.length; i++) { + result[i] = toArray(coords[i]); + } + return result; + } + + private static double[][] toArray(CoordinateSequence coords) { + return toArray(coords.toCoordinateArray()); + } + + private static double[][][] toArray(CoordinateSequence[] coords) { + double[][][] result = new double[coords.length][][]; + + for (int i = 0; i < coords.length; i++) { + result[i] = toArray(coords[i]); + } + return result; + } + + private static double[][][][] toArray(CoordinateSequence[][] coords) { + double[][][][] result = new double[coords.length][][][]; + + for (int i = 0; i < coords.length; i++) { + result[i] = toArray(coords[i]); + } + return result; + } + + private static GeoJSONPoint toGeoJSONGeometry(Point pt) { + GeoJSONPoint jsonPt = new GeoJSONPoint(); + jsonPt.setCoordinates(toArray(pt.getCoordinate())); + return jsonPt; + } + + private static GeoJSONLineString toGeoJSONGeometry(LineString line) { + GeoJSONLineString jsonln = new GeoJSONLineString(); + jsonln.setCoordinates(toArray(line.getCoordinateSequence())); + return jsonln; + } + + private static GeoJSONPolygon toGeoJSONGeometry(Polygon polygon) { + GeoJSONPolygon jsonpoly = new GeoJSONPolygon(); + CoordinateSequence[] coords = getCoordinateSequencesFromPolygon(polygon); + jsonpoly.setCoordinates(toArray(coords)); + return jsonpoly; + } + + private static CoordinateSequence[] getCoordinateSequencesFromPolygon(Polygon polygon) { + int totalRings = polygon.getNumInteriorRing() + 1; + CoordinateSequence[] coords = new CoordinateSequence[totalRings]; + coords[0] = polygon.getExteriorRing().getCoordinateSequence(); + + if (totalRings > 1) { + for (int i = 0; i < totalRings - 1; i++) { + coords[i + 1] = polygon.getInteriorRingN(i).getCoordinateSequence(); + } + } + return coords; + } + + private static GeoJSONMultiPoint toGeoJSONGeometry(MultiPoint mpt) { + GeoJSONMultiPoint jsonMpt = new GeoJSONMultiPoint(); + jsonMpt.setCoordinates(toArray(mpt.getCoordinates())); + return jsonMpt; + } + + private static GeoJSONMultiLineString toGeoJSONGeometry(MultiLineString mln) { + GeoJSONMultiLineString jsonMln = new GeoJSONMultiLineString(); + int totalRings = mln.getNumGeometries(); + CoordinateSequence[] coords = new CoordinateSequence[totalRings]; + for (int i = 0; i < totalRings; i++) { + coords[i] = ((LineString) mln.getGeometryN(i)).getCoordinateSequence(); + } + jsonMln.setCoordinates(toArray(coords)); + return jsonMln; + } + + private static GeoJSONMultiPolygon toGeoJSONGeometry(MultiPolygon multiPolygon) { + GeoJSONMultiPolygon jsonMPoly = new GeoJSONMultiPolygon(); + int totalPoly = multiPolygon.getNumGeometries(); + + CoordinateSequence[][] coords = new CoordinateSequence[totalPoly][]; + for (int i = 0; i < totalPoly; i++) { + coords[i] = getCoordinateSequencesFromPolygon((Polygon) multiPolygon.getGeometryN(i)); + } + + jsonMPoly.setCoordinates(toArray(coords)); + return jsonMPoly; + } + + private static GeoJSONGeometryCollection toGeoJSONGeometry(GeometryCollection geometryCollection) { + GeoJSONGeometryCollection coll = new GeoJSONGeometryCollection(); + int numGeometries = geometryCollection.getNumGeometries(); + + for (int i = 0; i < numGeometries; i++) { + coll.getGeometries().add(toGeoJSONGeometry(geometryCollection.getGeometryN(i))); + } + + return coll; + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONObject.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONObject.java new file mode 100644 index 0000000..44ead79 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONObject.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.geojson.binding; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import static org.apache.sis.internal.geojson.binding.GeoJSONGeometry.*; + +import java.io.Serializable; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GeoJSONFeatureCollection.class, name = "FeatureCollection"), + @JsonSubTypes.Type(value = GeoJSONFeature.class, name = "Feature"), + @JsonSubTypes.Type(value = GeoJSONPoint.class, name = "Point"), + @JsonSubTypes.Type(value = GeoJSONLineString.class, name = "LineString"), + @JsonSubTypes.Type(value = GeoJSONPolygon.class, name = "Polygon"), + @JsonSubTypes.Type(value = GeoJSONMultiPoint.class, name = "MultiPoint"), + @JsonSubTypes.Type(value = GeoJSONMultiLineString.class, name = "MultiLineString"), + @JsonSubTypes.Type(value = GeoJSONMultiPolygon.class, name = "MultiPolygon"), + @JsonSubTypes.Type(value = GeoJSONGeometryCollection.class, name = "GeometryCollection") +}) +public class GeoJSONObject implements Serializable { + + private String type; + private double[] bbox; + private GeoJSONCRS crs; + + public GeoJSONObject() { + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public double[] getBbox() { + return bbox; + } + + public void setBbox(double[] bbox) { + this.bbox = bbox; + } + + public GeoJSONCRS getCrs() { + return crs; + } + + public void setCrs(GeoJSONCRS crs) { + this.crs = crs; + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java new file mode 100644 index 0000000..4d1c94e --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import org.apache.sis.util.iso.ResourceInternationalString; +import org.apache.sis.util.resources.IndexedResourceBundle; +import org.opengis.util.InternationalString; + + +/** + * Locale-dependent resources for words or simple sentences. + */ +public final class Bundle extends IndexedResourceBundle { + /** + * Resource keys. This class is used when compiling sources, but no dependencies to + * {@code Keys} should appear in any resulting class files. Since the Java compiler + * inlines final integer values, using long identifiers will not bloat the constant + * pools of compiled classes. + */ + public static final class Keys { + private Keys() { + } + + /** + * Number of decimals + */ + public static final short coordinate_accuracy = 1; + + /** + * Number of decimals (default 7). + */ + public static final short coordinate_accuracy_remarks = 2; + + /** + * GeoJSON data file (.json) + */ + public static final short datastoreDescription = 3; + + public static final short datastoreFolderDescription = 4; + + public static final short datastoreFolderTitle = 5; + + /** + * GeoJSON + */ + public static final short datastoreTitle = 6; + } + + /** + * Constructs a new resource bundle loading data from the given UTF file. + * + * @param filename The file or the JAR entry containing resources. + */ + public Bundle(final java.net.URL filename) { + super(filename); + } + + /** + * Returns resources in the given locale. + * + * @param locale The locale, or {@code null} for the default locale. + * @return Resources in the given locale. + * @throws MissingResourceException if resources can't be found. + */ + public static Bundle getResources(Locale locale) throws MissingResourceException { + return getBundle(Bundle.class, locale); + } + + /** + * The international string to be returned by {@link formatInternational}. + */ + private static final class International extends ResourceInternationalString { + private static final long serialVersionUID = -9199238559657784488L; + + International(final int key) { + super(Bundle.class.getName(), String.valueOf(key)); + } + + @Override + protected ResourceBundle getBundle(final Locale locale) { + return getResources(locale); + } + } + + /** + * Gets an international string for the given key. This method does not check for the key + * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown + * when a {@link InternationalString#toString} method is invoked. + * + * @param key The key for the desired string. + * @return An international string for the given key. + */ + public static InternationalString formatInternational(final short key) { + return new International(key); + } + + /** + * Gets an international string for the given key. This method does not check for the key + * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown + * when a {@link InternationalString#toString} method is invoked. + * + * {@note This method is redundant with the one expecting Object..., but is + * provided for binary compatibility with previous Geotk versions. It also avoid the + * creation of a temporary array. There is no risk of confusion since the two methods + * delegate their work to the same format method anyway.} + * + * @param key The key for the desired string. + * @param arg Values to substitute to "{0}". + * @return An international string for the given key. + * + * @todo Current implementation just invokes {@link #format}. Need to format only when + * {@code toString(Locale)} is invoked. + */ + public static InternationalString formatInternational(final short key, final Object arg) { + return new org.apache.sis.util.iso.SimpleInternationalString(format(key, arg)); + } + + /** + * Gets an international string for the given key. This method does not check for the key + * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown + * when a {@link InternationalString#toString} method is invoked. + * + * @param key The key for the desired string. + * @param args Values to substitute to "{0}", "{1}", etc. + * @return An international string for the given key. + * + * @todo Current implementation just invokes {@link #format}. Need to format only when + * {@code toString(Locale)} is invoked. + */ + public static InternationalString formatInternational(final short key, final Object... args) { + return new org.apache.sis.util.iso.SimpleInternationalString(format(key, args)); + } + + /** + * Gets a string for the given key from this resource bundle or one of its parents. + * + * @param key The key for the desired string. + * @return The string for the given key. + * @throws MissingResourceException If no object for the given key can be found. + */ + public static String format(final short key) throws MissingResourceException { + return getResources(null).getString(key); + } + + /** + * Gets a string for the given key are replace all occurrence of "{0}" + * with values of {@code arg0}. + * + * @param key The key for the desired string. + * @param arg0 Value to substitute to "{0}". + * @return The formatted string for the given key. + * @throws MissingResourceException If no object for the given key can be found. + */ + public static String format(final short key, + final Object arg0) throws MissingResourceException + { + return getResources(null).getString(key, arg0); + } + + /** + * Gets a string for the given key are replace all occurrence of "{0}", + * "{1}", with values of {@code arg0}, {@code arg1}. + * + * @param key The key for the desired string. + * @param arg0 Value to substitute to "{0}". + * @param arg1 Value to substitute to "{1}". + * @return The formatted string for the given key. + * @throws MissingResourceException If no object for the given key can be found. + */ + public static String format(final short key, + final Object arg0, + final Object arg1) throws MissingResourceException + { + return getResources(null).getString(key, arg0, arg1); + } + + /** + * Gets a string for the given key are replace all occurrence of "{0}", + * "{1}", with values of {@code arg0}, {@code arg1}, etc. + * + * @param key The key for the desired string. + * @param arg0 Value to substitute to "{0}". + * @param arg1 Value to substitute to "{1}". + * @param arg2 Value to substitute to "{2}". + * @return The formatted string for the given key. + * @throws MissingResourceException If no object for the given key can be found. + */ + public static String format(final short key, + final Object arg0, + final Object arg1, + final Object arg2) throws MissingResourceException + { + return getResources(null).getString(key, arg0, arg1, arg2); + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties new file mode 100644 index 0000000..819dbb2 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties @@ -0,0 +1,5 @@ + +datastoreTitle=GeoJSON +datastoreDescription=GeoJSON data file (.json) +coordinate_accuracy=Number of decimals +coordinate_accuracy_remarks=Number of decimals (default 7). diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties new file mode 100644 index 0000000..819dbb2 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties @@ -0,0 +1,5 @@ + +datastoreTitle=GeoJSON +datastoreDescription=GeoJSON data file (.json) +coordinate_accuracy=Number of decimals +coordinate_accuracy_remarks=Number of decimals (default 7). diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties new file mode 100644 index 0000000..9938477 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties @@ -0,0 +1,5 @@ + +datastoreTitle=GeoJSON +datastoreDescription=fichier GeoJSON de donn\u00e9es vectorielles (.json) +coordinate_accuracy=Chiffres apr\u00e8s la virgule +coordinate_accuracy_remarks=Nombre de chiffres apr\u00e8s la virgule. diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONConstants.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONConstants.java new file mode 100644 index 0000000..fbe2780 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONConstants.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import org.apache.sis.util.Static; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public final class GeoJSONConstants extends Static { + + public static final String FEATURE_COLLECTION = "FeatureCollection"; + public static final String FEATURE = "Feature"; + + public static final String POINT = "Point"; + public static final String LINESTRING = "LineString"; + public static final String POLYGON = "Polygon"; + public static final String MULTI_POINT = "MultiPoint"; + public static final String MULTI_LINESTRING = "MultiLineString"; + public static final String MULTI_POLYGON = "MultiPolygon"; + public static final String GEOMETRY_COLLECTION = "GeometryCollection"; + + public static final String CRS_NAME = "name"; + public static final String CRS_LINK = "link"; + + public static final String CRS_TYPE_PROJ4 = "proj4"; + public static final String CRS_TYPE_OGCWKT = "ogcwkt"; + public static final String CRS_TYPE_ESRIWKT = "esriwkt"; + + public static final String TYPE = "type"; + public static final String FEATURES = "features"; + public static final String GEOMETRY = "geometry"; + public static final String GEOMETRIES = "geometries"; + public static final String COORDINATES = "coordinates"; + public static final String PROPERTIES = "properties"; + public static final String CRS = "crs"; + public static final String NAME = "name"; + public static final String HREF = "href"; + public static final String BBOX = "bbox"; + public static final String ID = "id"; +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java new file mode 100644 index 0000000..8aa4c0f --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import com.fasterxml.jackson.core.JsonEncoding; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.locks.ReadWriteLock; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.util.collection.BackingStoreException; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +final class GeoJSONFileWriter extends GeoJSONReader { + + private final GeoJSONWriter writer; + + private Feature edited; + private Feature lastWritten; + private Path tmpFile; + + public GeoJSONFileWriter(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock, + final String encoding, final int doubleAccuracy) throws DataStoreException { + super(jsonFile, featureType, rwLock); + + JsonEncoding jsonEncoding = JsonEncoding.UTF8; + + try { + final String name = featureType.getName().tip().toString(); + tmpFile = jsonFile.resolveSibling(name + ".wjson"); + writer = new GeoJSONWriter(tmpFile, jsonEncoding, doubleAccuracy, false); + + //start write feature collection. + writer.writeStartFeatureCollection(crs, null); + writer.flush(); + } catch (IOException ex) { + throw new DataStoreException(ex.getMessage(), ex); + } + } + + @Override + public FeatureType getFeatureType() { + return super.getFeatureType(); + } + + @Override + public Feature next() throws BackingStoreException { + try { + write(); + edited = super.next(); + } catch (BackingStoreException ex) { + //we reach append mode + //create empty feature + edited = featureType.newInstance(); + if (hasIdentifier) { + edited.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(currentFeatureIdx++)); + } + } + return edited; + } + + public void write(Feature edited) throws BackingStoreException { + this.edited = edited; + write(); + } + + public void write() throws BackingStoreException { + if (edited == null || edited.equals(lastWritten)) { + return; + } + + lastWritten = edited; + try { + writer.writeFeature(edited); + writer.flush(); + } catch (IOException | IllegalArgumentException e) { + throw new BackingStoreException(e.getMessage(), e); + } + } + + @Override + public void remove() { + edited = null; + } + + @Override + public void close() { + try (GeoJSONWriter toClose = writer) { + toClose.writeEndFeatureCollection(); + toClose.flush(); + } catch (IOException ex) { + throw new BackingStoreException(ex); + } finally { + super.close(); + } + + //flip files + rwlock.writeLock().lock(); + try { + Files.move(tmpFile, jsonFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + throw new BackingStoreException(ex); + } finally { + rwlock.writeLock().unlock(); + } + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java new file mode 100644 index 0000000..3781740 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import java.io.IOException; +import java.io.Reader; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import org.apache.sis.internal.storage.Capability; +import org.apache.sis.internal.storage.StoreMetadata; +import org.apache.sis.internal.storage.io.IOUtilities; +import org.apache.sis.parameter.ParameterBuilder; +import org.apache.sis.storage.DataStore; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreProvider; +import static org.apache.sis.storage.DataStoreProvider.LOCATION; +import org.apache.sis.storage.FeatureSet; +import org.apache.sis.storage.ProbeResult; +import org.apache.sis.storage.StorageConnector; +import org.opengis.parameter.ParameterDescriptor; +import org.opengis.parameter.ParameterDescriptorGroup; +import org.opengis.parameter.ParameterValueGroup; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +@StoreMetadata( + formatName = GeoJSONProvider.NAME, + capabilities = {Capability.READ, Capability.WRITE, Capability.CREATE}, + fileSuffixes = {"json", "geojson", "topojson"}, + resourceTypes = {FeatureSet.class}) +public final class GeoJSONProvider extends DataStoreProvider { + + public static final String NAME = "geojson"; + + public static final String ENCODING = "UTF-8"; + + /** + * The {@value} MIME type. + */ + public static final String MIME_TYPE = "application/json"; + + private static final List EXTENSIONS = Arrays.asList("json", "geojson", "topojson"); + + public static final ParameterDescriptor PATH = new ParameterBuilder() + .addName(LOCATION) + .addName("path") + .setRequired(true) + .create(URI.class, null); + + /** + * Optional + */ + public static final ParameterDescriptor COORDINATE_ACCURACY = new ParameterBuilder() + .addName("coordinate_accuracy") + .addName(Bundle.formatInternational(Bundle.Keys.coordinate_accuracy)) + .setRemarks(Bundle.formatInternational(Bundle.Keys.coordinate_accuracy_remarks)) + .setRequired(false) + .create(Integer.class, 7); + + public static final ParameterDescriptorGroup PARAMETERS_DESCRIPTOR = + new ParameterBuilder() + .addName(NAME) + .addName(Bundle.formatInternational(Bundle.Keys.datastoreTitle)) + .setDescription(Bundle.formatInternational(Bundle.Keys.datastoreDescription)) + .createGroup(PATH, COORDINATE_ACCURACY); + + @Override + public String getShortName() { + return NAME; + } + + /** + * {@inheritDoc } + */ + @Override + public ParameterDescriptorGroup getOpenParameters() { + return PARAMETERS_DESCRIPTOR; + } + + /** + * {@inheritDoc } + */ + @Override + public GeoJSONStore open(final ParameterValueGroup params) throws DataStoreException { + return new GeoJSONStore(this, params); + } + + @Override + public ProbeResult probeContent(StorageConnector connector) throws DataStoreException { + Path p = connector.getStorageAs(Path.class); + String extension = IOUtilities.extension(p).toLowerCase(); + if (EXTENSIONS.contains(extension)) { + try { + final ByteBuffer buffer = connector.getStorageAs(ByteBuffer.class); + final Reader reader; + if (buffer != null) { + buffer.mark(); + reader = null; + } else { + // User gave us explicitely a Reader (e.g. a StringReader wrapping a String instance). + reader = connector.getStorageAs(Reader.class); + if (reader == null) { + return ProbeResult.UNSUPPORTED_STORAGE; + } + reader.mark(2048); // Should be no more than {@code StorageConnector.DEFAULT_BUFFER_SIZE / 2} + } + boolean ok = false; + if (nextAfterSpaces(buffer, reader) == '{') { + ok = true; + } + if (buffer != null) { + buffer.reset(); + } else { + reader.reset(); + } + if (ok) { + return new ProbeResult(true, MIME_TYPE, null); + } + } catch (IOException e) { + throw new DataStoreException(e); + } + } + return ProbeResult.UNSUPPORTED_STORAGE; + + } + + /** + * Returns the next character which is not a white space, or -1 if the end + * of stream is reached. Exactly one of {@code buffer} and {@code reader} + * shall be non-null. + */ + private static int nextAfterSpaces(final ByteBuffer buffer, final Reader reader) throws IOException { + if (buffer != null) { + while (buffer.hasRemaining()) { + final char c = (char) buffer.get(); + if (!Character.isWhitespace(c)) { + return c; + } + } + return -1; + } + int c; + while ((c = IOUtilities.readCodePoint(reader)) >= 0) { + if (!Character.isWhitespace(c)) { + break; + } + } + return c; + } + + @Override + public DataStore open(StorageConnector connector) throws DataStoreException { + try { + final Path path = connector.getStorageAs(Path.class); + return new GeoJSONStore(this, path, null); + } catch (IllegalArgumentException ex) { + throw new DataStoreException(ex.getMessage(), ex); + } + } + +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java new file mode 100644 index 0000000..bfb0ef5 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java @@ -0,0 +1,356 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +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.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.internal.geojson.binding.GeoJSONFeature; +import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry; +import org.apache.sis.internal.geojson.binding.GeoJSONObject; +import org.apache.sis.internal.geojson.GeoJSONParser; +import org.apache.sis.internal.geojson.GeoJSONUtils; +import org.apache.sis.util.ObjectConverter; +import org.apache.sis.util.ObjectConverters; +import org.apache.sis.util.UnconvertibleObjectException; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.logging.Logging; +import org.locationtech.jts.geom.Geometry; +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.PropertyType; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +class GeoJSONReader implements Iterator, AutoCloseable { + + private static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson"); + private final Map, ObjectConverter> convertersCache = new HashMap<>(); + + private GeoJSONObject jsonObj; + private Boolean toRead = true; + + protected final ReadWriteLock rwlock; + protected final FeatureType featureType; + protected final Path jsonFile; + protected Feature current; + protected int currentFeatureIdx; + + /** + * A flag indicating if we should read identifiers from read stream. it's + * activated if the feature type given at built contains an + * {@link AttributeConvention#IDENTIFIER_PROPERTY}. + */ + protected final boolean hasIdentifier; + final Function idConverter; + final CoordinateReferenceSystem crs; + final String geometryName; + + public GeoJSONReader(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock) { + hasIdentifier = GeoJSONUtils.hasIdentifier(featureType); + if (hasIdentifier) { + idConverter = GeoJSONUtils.getIdentifierConverter(featureType); + } else { + // It should not be used, but we don't set it to null in case someons use it by mistake. + idConverter = input -> input; + } + + final PropertyType defaultGeometry = GeoJSONUtils.getDefaultGeometry(featureType); + crs = GeoJSONUtils.getCRS(defaultGeometry); + geometryName = defaultGeometry.getName().toString(); + + this.jsonFile = jsonFile; + this.featureType = featureType; + this.rwlock = rwLock; + } + + public FeatureType getFeatureType() { + return featureType; + } + + @Override + public boolean hasNext() throws BackingStoreException { + read(); + return current != null; + } + + @Override + public Feature next() throws BackingStoreException { + read(); + final Feature ob = current; + current = null; + if (ob == null) { + throw new BackingStoreException("No more records."); + } + return ob; + } + + private void read() throws BackingStoreException { + if (current != null) return; + + //first call + if (toRead) { + rwlock.readLock().lock(); + try { + jsonObj = GeoJSONParser.parse(jsonFile, true); + } catch (IOException e) { + throw new BackingStoreException(e); + } finally { + toRead = false; + rwlock.readLock().unlock(); + } + } + + if (jsonObj instanceof GeoJSONFeatureCollection) { + final GeoJSONFeatureCollection fc = (GeoJSONFeatureCollection) jsonObj; + rwlock.readLock().lock(); + try { + if (fc.hasNext()) { + current = toFeature(fc.next()); + currentFeatureIdx++; + } + } finally { + rwlock.readLock().unlock(); + } + } else if (jsonObj instanceof GeoJSONFeature) { + current = toFeature((GeoJSONFeature) jsonObj); + jsonObj = null; + } else if (jsonObj instanceof GeoJSONGeometry) { + current = toFeature((GeoJSONGeometry) jsonObj); + jsonObj = null; + } + } + + /** + * Convert a GeoJSONFeature to geotk Feature. + * + * @param jsonFeature + * @param featureId + * @return + */ + protected Feature toFeature(GeoJSONFeature jsonFeature) throws BackingStoreException { + + //Build geometry + final Geometry geom = GeoJSONGeometry.toJTS(jsonFeature.getGeometry(), crs); + + //empty feature + final Feature feature = featureType.newInstance(); + if (hasIdentifier) { + Object id = jsonFeature.getId(); + if (id == null) { + id = currentFeatureIdx; + } + feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(id)); + } + feature.setPropertyValue(geometryName, geom); + + //recursively fill other properties + final Map 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 properties) throws BackingStoreException { + 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 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 BackingStoreException { + + 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 BackingStoreException(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 key = new AbstractMap.SimpleEntry<>(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 + * @return + */ + protected Feature toFeature(GeoJSONGeometry jsonGeometry) { + final Feature feature = featureType.newInstance(); + final Geometry geom = GeoJSONGeometry.toJTS(jsonGeometry, crs); + feature.setPropertyValue(geometryName, geom); + return feature; + } + + @Override + public void remove() { + throw new BackingStoreException("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 AutoCloseable) { + ((AutoCloseable) jsonObj).close(); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Cannot close a read resource.", e); + } + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java new file mode 100644 index 0000000..b8a5ed7 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java @@ -0,0 +1,459 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.apache.sis.feature.builder.AttributeRole; +import org.apache.sis.feature.builder.AttributeTypeBuilder; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.internal.geojson.FeatureTypeUtils; +import org.apache.sis.internal.geojson.GeoJSONParser; +import org.apache.sis.internal.geojson.GeoJSONUtils; +import org.apache.sis.internal.geojson.binding.GeoJSONFeature; +import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONLineString; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPoint; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPolygon; +import org.apache.sis.internal.geojson.binding.GeoJSONObject; +import org.apache.sis.internal.storage.ResourceOnFileSystem; +import org.apache.sis.metadata.iso.DefaultMetadata; +import org.apache.sis.parameter.Parameters; +import org.apache.sis.storage.DataStore; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreProvider; +import org.apache.sis.storage.WritableFeatureSet; +import static org.apache.sis.storage.geojson.GeoJSONProvider.*; +import org.apache.sis.util.iso.Names; +import org.apache.sis.util.logging.Logging; +import org.locationtech.jts.geom.*; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; +import org.opengis.geometry.Envelope; +import org.opengis.metadata.Metadata; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.GenericName; + +/** + * + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public final class GeoJSONStore extends DataStore implements ResourceOnFileSystem, WritableFeatureSet { + + private static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson"); + private static final String DESC_FILE_SUFFIX = "_Type.json"; + + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final DataStoreProvider provider; + private final ParameterValueGroup parameters; + + private GenericName name; + private FeatureType featureType; + private Path descFile; + private Path jsonFile; + private Integer coordAccuracy; + private boolean isLocal = true; + + public GeoJSONStore(DataStoreProvider provider, final Path path, Integer coordAccuracy) + throws DataStoreException { + this(provider, toParameter(path.toUri(), coordAccuracy)); + } + + public GeoJSONStore(DataStoreProvider provider, final URI uri, Integer coordAccuracy) + throws DataStoreException { + this(provider, toParameter(uri, coordAccuracy)); + } + + public GeoJSONStore(DataStoreProvider provider, final ParameterValueGroup params) throws DataStoreException { + super(); + this.provider = provider; + this.parameters = params; + this.coordAccuracy = (Integer) params.parameter(COORDINATE_ACCURACY.getName().toString()).getValue(); + + final URI uri = (URI) params.parameter(PATH.getName().toString()).getValue(); + + //FIXME + this.isLocal = "file".equalsIgnoreCase(uri.getScheme()); + + Path tmpFile = null; + try { + tmpFile = Paths.get(uri); + } catch (FileSystemNotFoundException ex) { + throw new DataStoreException(ex); + } + + final String fileName = tmpFile.getFileName().toString(); + if (fileName.endsWith(DESC_FILE_SUFFIX)) { + this.descFile = tmpFile; + this.jsonFile = descFile.resolveSibling(fileName.replace(DESC_FILE_SUFFIX, ".json")); + } else { + this.jsonFile = tmpFile; + //search for description json file + String typeName = GeoJSONUtils.getNameWithoutExt(jsonFile); + this.descFile = jsonFile.resolveSibling(typeName + DESC_FILE_SUFFIX); + } + } + + private static ParameterValueGroup toParameter(final URI uri, Integer coordAccuracy) { + final Parameters params = Parameters.castOrWrap(GeoJSONProvider.PARAMETERS_DESCRIPTOR.createValue()); + params.getOrCreate(GeoJSONProvider.PATH).setValue(uri); + params.getOrCreate(GeoJSONProvider.COORDINATE_ACCURACY).setValue(coordAccuracy); + return params; + } + + @Override + public Optional getOpenParameters() { + return Optional.of(parameters); + } + + @Override + public Metadata getMetadata() throws DataStoreException { + return new DefaultMetadata(); + } + + @Override + public DataStoreProvider getProvider() { + return provider; + } + + @Override + public Optional getIdentifier() throws DataStoreException { + checkTypeExist(); + return Optional.of(name); + } + + @Override + public FeatureType getType() throws DataStoreException { + checkTypeExist(); + return featureType; + } + + public boolean isWritable() throws DataStoreException { + return isLocal && Files.isWritable(descFile) && Files.isWritable(jsonFile); + } + + private void checkTypeExist() throws DataStoreException { + if (name == null || featureType == null) { + try { + // try to parse file only if exist and not empty + if (Files.exists(jsonFile) && Files.size(jsonFile) != 0) { + featureType = readType(); + name = featureType.getName(); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, e.getMessage(), e); + } + } + } + + /** + * Read FeatureType from a JSON-Schema file if exist or directly from the + * input JSON file. + * + * @return + * @throws DataStoreException + * @throws IOException + */ + private FeatureType readType() throws DataStoreException, IOException { + if (Files.exists(descFile) && Files.size(descFile) != 0) { + // build FeatureType from description JSON. + return FeatureTypeUtils.readFeatureType(descFile); + } else { + if (Files.exists(jsonFile) && Files.size(jsonFile) != 0) { + final String name = GeoJSONUtils.getNameWithoutExt(jsonFile); + + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + + // build FeatureType from the first Feature of JSON file. + final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true); + if (obj == null) { + throw new DataStoreException("Invalid GeoJSON file " + jsonFile.toString()); + } + + CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj); + + if (obj instanceof GeoJSONFeatureCollection) { + GeoJSONFeatureCollection jsonFeatureCollection = (GeoJSONFeatureCollection) obj; + if (!jsonFeatureCollection.hasNext()) { + //empty FeatureCollection error ? + throw new DataStoreException("Empty GeoJSON FeatureCollection " + jsonFile.toString()); + } else { + + // TODO should we analyse all Features from FeatureCollection to be sure + // that each Feature properties JSON object define exactly the same properties + // with the same bindings ? + GeoJSONFeature jsonFeature = jsonFeatureCollection.next(); + fillTypeFromFeature(ftb, crs, jsonFeature, false); + } + + } else if (obj instanceof GeoJSONFeature) { + GeoJSONFeature jsonFeature = (GeoJSONFeature) obj; + fillTypeFromFeature(ftb, crs, jsonFeature, true); + } else if (obj instanceof GeoJSONGeometry) { + ftb.addAttribute(String.class).setName("fid").addRole(AttributeRole.IDENTIFIER_COMPONENT); + ftb.addAttribute(findBinding((GeoJSONGeometry) obj)).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY); + } + + return ftb.build(); + } else { + throw new DataStoreException("Can't create FeatureType from empty/not found Json file " + jsonFile.getFileName().toString()); + } + } + } + + private void fillTypeFromFeature(FeatureTypeBuilder ftb, CoordinateReferenceSystem crs, + GeoJSONFeature jsonFeature, boolean analyseGeometry) { + if (analyseGeometry) { + ftb.addAttribute(findBinding(jsonFeature.getGeometry())).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY); + } else { + ftb.addAttribute(Geometry.class).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY); + } + for (Map.Entry property : jsonFeature.getProperties().entrySet()) { + final Object value = property.getValue(); + final Class binding = value != null ? value.getClass() : String.class; + final GenericName name = Names.createLocalName(null, null, property.getKey()); + final AttributeTypeBuilder atb = ftb.addAttribute(binding).setName(name); + if ("id".equals(property.getKey()) || "fid".equals(property.getKey())) { + atb.addRole(AttributeRole.IDENTIFIER_COMPONENT); + } + } + } + + private Class findBinding(GeoJSONGeometry jsonGeometry) { + + if (jsonGeometry instanceof GeoJSONPoint) { + return Point.class; + } else if (jsonGeometry instanceof GeoJSONLineString) { + return LineString.class; + } else if (jsonGeometry instanceof GeoJSONPolygon) { + return Polygon.class; + } else if (jsonGeometry instanceof GeoJSONMultiPoint) { + return MultiPoint.class; + } else if (jsonGeometry instanceof GeoJSONMultiLineString) { + return MultiLineString.class; + } else if (jsonGeometry instanceof GeoJSONMultiPolygon) { + return MultiPolygon.class; + } else if (jsonGeometry instanceof GeoJSONGeometryCollection) { + return GeometryCollection.class; + } else { + throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry); + } + + } + + private void writeType(FeatureType featureType) throws DataStoreException { + try { + final boolean jsonExist = Files.exists(jsonFile); + + if (jsonExist && Files.size(jsonFile) != 0) { + throw new DataStoreException(String.format("Non empty json file %s can't create new json file %s", + jsonFile.getFileName().toString(), featureType.getName())); + } + + if (!jsonExist) { + Files.createFile(jsonFile); + } + //create json with empty collection + GeoJSONUtils.writeEmptyFeatureCollection(jsonFile); + + //json schema file + final boolean descExist = Files.exists(descFile); + + if (descExist && Files.size(descFile) != 0) { + throw new DataStoreException(String.format("Non empty json schema file %s can't create new json schema %s", + descFile.getFileName().toString(), featureType.getName())); + } + + if (!descExist) { + Files.createFile(descFile); + } + //create json schema file + FeatureTypeUtils.writeFeatureType(featureType, descFile); + + this.featureType = featureType; + this.name = featureType.getName(); + } catch (IOException e) { + throw new DataStoreException(e.getMessage(), e); + } + } + + @Override + public Optional getEnvelope() throws DataStoreException { + Envelope envelope = null; + rwLock.readLock().lock(); + try { + final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true); + final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj); + envelope = GeoJSONUtils.getEnvelope(obj, crs); + } catch (IOException e) { + throw new DataStoreException(e.getMessage(), e); + } finally { + rwLock.readLock().unlock(); + } + + return Optional.ofNullable(envelope); + } + + /** + * {@inheritDoc } + */ + @Override + public Stream features(boolean parallel) throws DataStoreException { + final GeoJSONReader reader = new GeoJSONReader(jsonFile, featureType, rwLock); + final Stream stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(reader, Spliterator.ORDERED), false); + stream.onClose(reader::close); + return stream; + } + + @Override + public void add(Iterator features) throws DataStoreException { + try (GeoJSONFileWriter writer = getFeatureWriter()) { + //rewrite existing features + while (writer.hasNext()) { + writer.next(); + } + //new features + while (features.hasNext()) { + Feature feature = features.next(); + Feature next = writer.next(); + writer.write(feature); + } + } + } + + @Override + public boolean removeIf(Predicate filter) throws DataStoreException { + boolean modified = false; + try (GeoJSONFileWriter writer = getFeatureWriter()) { + //rewrite existing features + while (writer.hasNext()) { + Feature feature = writer.next(); + if (filter.test(feature)) { + writer.remove(); + modified = true; + } + } + } + return modified; + } + + @Override + public void replaceIf(Predicate filter, UnaryOperator updater) throws DataStoreException { + try (GeoJSONFileWriter writer = getFeatureWriter()) { + //rewrite existing features + while (writer.hasNext()) { + Feature feature = writer.next(); + if (filter.test(feature)) { + Feature changed = updater.apply(feature); + if (changed == null) { + writer.remove(); + } else { + writer.write(); + } + } + } + } + } + + private GeoJSONFileWriter getFeatureWriter() throws DataStoreException { + return new GeoJSONFileWriter(jsonFile, featureType, rwLock, + GeoJSONProvider.ENCODING, coordAccuracy); + } + + @Override + public void updateType(final FeatureType featureType) throws DataStoreException { + if (!isLocal) { + throw new DataStoreException("Cannot create FeatureType on remote GeoJSON"); + } + GenericName typeName = featureType.getName(); + if (typeName == null) { + throw new DataStoreException("Type name can not be null."); + } + if (!typeName.tip().toString().equals(GeoJSONUtils.getNameWithoutExt(jsonFile))) { + throw new DataStoreException("New type name should be equals to file name."); + } + + //delete previous files + rwLock.writeLock().lock(); + try { + Files.deleteIfExists(descFile); + Files.deleteIfExists(jsonFile); + Files.createFile(jsonFile); + } catch (IOException e) { + throw new DataStoreException("Can not delete GeoJSON schema.", e); + } finally { + rwLock.writeLock().unlock(); + } + + //create new type + rwLock.writeLock().lock(); + try { + writeType(featureType); + } finally { + rwLock.writeLock().unlock(); + } + } + + public void clearCache() { + name = null; + featureType = null; + } + + @Override + public void close() throws DataStoreException { + } + + /** + * {@inheritDoc } + */ + @Override + public Path[] getComponentFiles() throws DataStoreException { + List files = new ArrayList<>(); + if (Files.exists(jsonFile)) { + files.add(jsonFile); + } + if (Files.exists(descFile)) { + files.add(descFile); + } + return files.toArray(new Path[files.size()]); + } + +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java new file mode 100644 index 0000000..35c483c --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import com.fasterxml.jackson.core.JsonEncoding; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.feature.builder.PropertyTypeBuilder; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.FeatureNaming; +import org.apache.sis.storage.IllegalNameException; +import org.apache.sis.internal.geojson.GeoJSONUtils; +import org.apache.sis.util.collection.BackingStoreException; +import org.locationtech.jts.geom.Geometry; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; +import org.opengis.feature.Operation; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public final class GeoJSONStreamWriter implements Iterator, AutoCloseable { + + private final GeoJSONWriter writer; + private final FeatureType featureType; + + private Feature edited; + private Feature lastWritten; + private int currentFeatureIdx; + + private final boolean hasIdentifier; + + final Function idConverter; + + /** + * + * @param outputStream stream were GeoJSON will be written + * @param featureType {@link FeatureType} of features to write. + * @param doubleAccuracy number of coordinates fraction digits + * @throws DataStoreException + */ + public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final int doubleAccuracy) + throws DataStoreException { + this(outputStream, featureType, JsonEncoding.UTF8, doubleAccuracy); + } + + /** + * + * @param outputStream stream were GeoJSON will be written + * @param featureType {@link FeatureType} of features to write. + * @param encoding character encoding + * @param doubleAccuracy number of coordinates fraction digits + * @throws DataStoreException + */ + public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final JsonEncoding encoding, final int doubleAccuracy) + throws DataStoreException { + this(outputStream, featureType, encoding, doubleAccuracy, false); + } + + public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final JsonEncoding encoding, final int doubleAccuracy, boolean prettyPrint) + throws DataStoreException { + + //remove any operation attribute + List geometries = featureType.getProperties(true).stream() + .filter(Operation.class::isInstance) + .map(Operation.class::cast) + .filter(AttributeConvention::isGeometryAttribute) + .filter(GeoJSONUtils.IS_NOT_CONVENTION) + .collect(Collectors.toList()); + + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(featureType); + final Iterator it = ftb.properties().iterator(); + final FeatureNaming naming = new FeatureNaming(); + geometries.stream() + .map(Operation::getName) + .forEach(name -> { + try { + naming.add(null, name, name); + } catch (IllegalNameException e) { + //hack + } + }); + while (it.hasNext()) { + try { + naming.get(null, it.next().getName().toString()); + it.remove(); + } catch (IllegalNameException e) { + // normal behavior + } + } + + for (final Operation op : geometries) { + GeoJSONUtils.castOrUnwrap(op).ifPresent(ftb::addAttribute); + } + + this.featureType = ftb.build(); + hasIdentifier = GeoJSONUtils.hasIdentifier(featureType); + if (hasIdentifier) { + idConverter = GeoJSONUtils.getIdentifierConverter(featureType); + } else { + // It should not be used, but we don't set it to null in case someons use it by mistake. + idConverter = input -> input; + } + + try { + writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint); + //start write feature collection. + writer.writeStartFeatureCollection(GeoJSONUtils.getCRS(featureType), null); + writer.flush(); + } catch (IOException ex) { + throw new DataStoreException(ex.getMessage(), ex); + } + } + + /** + * Utility method to write a single Feature into an OutputStream + * + * @param outputStream + * @param feature to write + * @param encoding + * @param doubleAccuracy + * @param prettyPrint + */ + public static void writeSingleFeature(OutputStream outputStream, Feature feature, final JsonEncoding encoding, + final int doubleAccuracy, boolean prettyPrint) throws IOException { + + try (GeoJSONWriter writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint)) { + writer.writeSingleFeature(feature); + } + } + + /** + * Utility method to write a single Geometry into an OutputStream + * + * @param outputStream + * @param geometry to write + * @param encoding + * @param doubleAccuracy + * @param prettyPrint + */ + public static void writeSingleGeometry(OutputStream outputStream, Geometry geometry, final JsonEncoding encoding, + final int doubleAccuracy, boolean prettyPrint) throws IOException { + + try (GeoJSONWriter writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint)) { + writer.writeSingleGeometry(geometry); + } + } + + public FeatureType getFeatureType() { + return featureType; + } + + @Override + public Feature next() throws BackingStoreException { + edited = featureType.newInstance(); + if (hasIdentifier) { + edited.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(currentFeatureIdx++)); + } + return edited; + } + + @Override + public void remove() throws BackingStoreException { + throw new BackingStoreException("Not supported on reader."); + } + + public void write() throws BackingStoreException { + if (edited == null || edited.equals(lastWritten)) { + return; + } + + lastWritten = edited; + try { + writer.writeFeature(edited); + writer.flush(); + } catch (IOException | IllegalArgumentException e) { + throw new BackingStoreException(e.getMessage(), e); + } + } + + @Override + public boolean hasNext() throws BackingStoreException { + return true; + } + + @Override + public void close() { + try { + writer.writeEndFeatureCollection(); + writer.flush(); + writer.close(); + } catch (IOException ex) { + throw new BackingStoreException(ex); + } + } +} diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java new file mode 100644 index 0000000..c919d12 --- /dev/null +++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java @@ -0,0 +1,469 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.geojson; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; +import java.text.NumberFormat; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.logging.Level; +import java.util.stream.Collectors; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONLineString; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPoint; +import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPolygon; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.internal.geojson.GeoJSONParser; +import static org.apache.sis.storage.geojson.GeoJSONConstants.*; +import org.apache.sis.internal.geojson.GeoJSONUtils; +import org.apache.sis.util.Utilities; +import org.locationtech.jts.geom.Geometry; +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.PropertyNotFoundException; +import org.opengis.feature.PropertyType; +import org.opengis.geometry.Envelope; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +final class GeoJSONWriter implements Closeable, Flushable { + + private static final 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; + private boolean isSingleFeature; + private boolean isSingleGeometry; + + private final NumberFormat numberFormat; + + GeoJSONWriter(Path file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException { + this(Files.newOutputStream(file, CREATE, WRITE, TRUNCATE_EXISTING), encoding, doubleAccuracy, prettyPrint); + } + + 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); + numberFormat = NumberFormat.getInstance(Locale.US); + numberFormat.setGroupingUsed(false); + numberFormat.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 && !Utilities.equalsApproximately(crs, CommonCRS.defaultGeographic())) { + if (writeCRS(crs)) { + writeNewLine(); + } else { + throw new IOException("Cannot determine a valid URN for " + crs.getName()); + } + } + + 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); + /* As defined in GeoJSON spec, identifier is an optional attribute. For + * more details, see https://tools.ietf.org/html/rfc7946#section-3.2 + */ + try { + final Object idValue = feature.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString()); + // TODO : search for a property named id or identifier ? + if (idValue != null) { + writeAttribute(ID, idValue, true); + } + } catch (PropertyNotFoundException e) { + GeoJSONParser.LOGGER.log(Level.FINE, "Cannot write ID cause no matching property has been found.", e); + } + + //write CRS + if (single) { + final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(feature.getType()); + if (crs != null && !Utilities.equalsApproximately(crs, CommonCRS.defaultGeographic())) { + if (!writeCRS(crs)) { + throw new IOException("Cannot determine a valid URN for " + crs.getName()); + } + } + } + + //write geometry + final Optional geom = GeoJSONUtils.getDefaultGeometryValue(feature) + .filter(Geometry.class::isInstance) + .map(Geometry.class::cast); + + if (geom.isPresent()) { + writer.writeFieldName(GEOMETRY); + writeFeatureGeometry(geom.get()); + } + + //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 boolean writeCRS(CoordinateReferenceSystem crs) throws IOException { + final Optional urn = GeoJSONUtils.toURN(crs); + if (urn.isPresent()) { + writer.writeObjectFieldStart(CRS); + writer.writeStringField(TYPE, CRS_NAME); + writer.writeObjectFieldStart(PROPERTIES); + writer.writeStringField(NAME, urn.get()); + writer.writeEndObject();//close properties + writer.writeEndObject();//close crs + } + + return urn.isPresent(); + } + + /** + * 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 descriptors = type.getProperties(true).stream() + .filter(GeoJSONUtils.IS_NOT_CONVENTION) + .collect(Collectors.toList()); + 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 = GeoJSONGeometry.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 = GeoJSONGeometry.toGeoJSONGeometry(geom); + writeGeoJSONGeometry(jsonGeometry); + } + + /** + * Write a GeometryAttribute + * + * @param geom + * @throws IOException + */ + private void writeFeatureGeometry(Geometry geom) throws IOException { + writeGeoJSONGeometry(GeoJSONGeometry.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 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(numberFormat.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(); + } + } +} diff --git a/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider new file mode 100644 index 0000000..33f5d70 --- /dev/null +++ b/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider @@ -0,0 +1 @@ +org.apache.sis.storage.geojson.GeoJSONProvider diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java new file mode 100644 index 0000000..a2c2255 --- /dev/null +++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.storage.geojson; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; +import org.apache.sis.feature.FeatureComparator; +import org.apache.sis.feature.builder.AttributeRole; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.internal.geojson.FeatureTypeUtils; +import org.apache.sis.internal.geojson.GeoJSONUtils; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.test.TestCase; +import org.apache.sis.util.iso.SimpleInternationalString; +import static org.junit.Assert.*; +import org.junit.Test; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyType; +import org.opengis.util.FactoryException; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class FeatureTypeUtilsTest extends TestCase { + + public static void main(String[] args) throws Exception { + new FeatureTypeUtilsTest().writeReadFTTest(); + } + + @Test + public void writeReadFTTest() throws Exception { + + Path featureTypeFile = Files.createTempFile("complexFT", ".json"); + + FeatureType featureType = createComplexType(); + FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile); + + assertTrue(Files.size(featureTypeFile) > 0); + + FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile); + + assertNotNull(readFeatureType); + assertTrue(hasAGeometry(readFeatureType)); + assertNotNull(GeoJSONUtils.getCRS(readFeatureType)); + + equalsIgnoreConvention(featureType, readFeatureType); + } + + @Test + public void writeReadNoCRSFTTest() throws Exception { + + Path featureTypeFile = Files.createTempFile("geomFTNC", ".json"); + + FeatureType featureType = createGeometryNoCRSFeatureType(); + FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile); + + assertTrue(Files.size(featureTypeFile) > 0); + + FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile); + + assertNotNull(readFeatureType); + assertTrue(hasAGeometry(readFeatureType)); + assertNull(GeoJSONUtils.getCRS(readFeatureType)); + + equalsIgnoreConvention(featureType, readFeatureType); + } + + @Test + public void writeReadCRSFTTest() throws Exception { + + Path featureTypeFile = Files.createTempFile("geomFTC", ".json"); + + FeatureType featureType = createGeometryCRSFeatureType(); + FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile); + + assertTrue(Files.size(featureTypeFile) > 0); + + FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile); + + assertNotNull(readFeatureType); + assertTrue(hasAGeometry(readFeatureType)); + assertNotNull(GeoJSONUtils.getCRS(readFeatureType)); + + equalsIgnoreConvention(featureType, readFeatureType); + } + + public static FeatureType createComplexType() throws FactoryException { + FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + + ftb.setName("complexAtt1"); + ftb.addAttribute(Long.class).setName("longProp2"); + ftb.addAttribute(String.class).setName("stringProp2"); + final FeatureType complexAtt1 = ftb.build(); + + ftb = new FeatureTypeBuilder(); + ftb.setName("complexAtt2"); + ftb.addAttribute(Long.class).setName("longProp2"); + ftb.addAttribute(Date.class).setName("dateProp"); + final FeatureType complexAtt2 = ftb.build(); + + ftb = new FeatureTypeBuilder(); + ftb.setName("complexFT"); + ftb.addAttribute(Polygon.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + ftb.addAttribute(Long.class).setName("longProp"); + ftb.addAttribute(String.class).setName("stringProp"); + ftb.addAttribute(Integer.class).setName("integerProp"); + ftb.addAttribute(Boolean.class).setName("booleanProp"); + ftb.addAttribute(Date.class).setName("dateProp"); + + ftb.addAssociation(complexAtt1).setName("complexAtt1"); + ftb.addAssociation(complexAtt2).setName("complexAtt2").setMinimumOccurs(0).setMaximumOccurs(Integer.MAX_VALUE); + ftb.setDescription(new SimpleInternationalString("Description")); + return ftb.build(); + } + + private FeatureType createGeometryNoCRSFeatureType() { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName("FT1"); + ftb.addAttribute(Point.class).setName("geometry").addRole(AttributeRole.DEFAULT_GEOMETRY); + ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); + ftb.addAttribute(String.class).setName("type"); + + return ftb.build(); + } + + private FeatureType createGeometryCRSFeatureType() { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName("FT2"); + ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); + ftb.addAttribute(String.class).setName("type"); + + return ftb.build(); + } + + /** + * Loop on properties, returns true if there is at least one geometry property. + * + * @param type + * @return true if type has a geometry. + */ + public static boolean hasAGeometry(FeatureType type) { + for (PropertyType pt : type.getProperties(true)){ + if (AttributeConvention.isGeometryAttribute(pt)) return true; + } + return false; + } + + + /** + * Test field equality ignoring convention properties. + */ + public static void equalsIgnoreConvention(FeatureType type1, FeatureType type2) { + final FeatureComparator comparator = new FeatureComparator(type1, type2); + comparator.ignoredProperties.add(AttributeConvention.IDENTIFIER); + comparator.ignoredProperties.add("identifier"); + comparator.compare(); + } + +} diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java new file mode 100644 index 0000000..8868e52 --- /dev/null +++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java @@ -0,0 +1,344 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.storage.geojson; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Iterator; +import org.apache.sis.feature.FeatureComparator; +import org.apache.sis.feature.builder.AttributeRole; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.internal.geojson.GeoJSONParser; +import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection; +import org.apache.sis.internal.geojson.binding.GeoJSONObject; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStores; +import org.apache.sis.storage.WritableFeatureSet; +import org.apache.sis.test.TestCase; +import org.apache.sis.util.iso.Names; +import static org.junit.Assert.*; +import org.junit.Test; +import org.locationtech.jts.geom.*; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; +import org.opengis.util.GenericName; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class GeoJSONReadTest extends TestCase { + + @Test + public void readPointTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/point.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "point"), name); + + testFeatureTypes(buildGeometryFeatureType("point", Point.class), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readMultiPointTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multipoint.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "multipoint"), name); + + testFeatureTypes(buildGeometryFeatureType("multipoint", MultiPoint.class), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readLineStringTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/linestring.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "linestring"), name); + + testFeatureTypes(buildGeometryFeatureType("linestring", LineString.class), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readMultiLineStringTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multilinestring.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "multilinestring"), name); + + testFeatureTypes(buildGeometryFeatureType("multilinestring", MultiLineString.class), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readPolygonTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/polygon.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "polygon"), name); + + testFeatureTypes(buildGeometryFeatureType("polygon", Polygon.class), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readMultiPolygonTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multipolygon.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "multipolygon"), name); + + testFeatureTypes(buildGeometryFeatureType("multipolygon", MultiPolygon.class), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readGeometryCollectionTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/geometrycollection.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "geometrycollection"), name); + + testFeatureTypes(buildGeometryFeatureType("geometrycollection", GeometryCollection.class), ft); + + assertEquals(1l, store.features(false).count()); + + } + + @Test + public void readFeatureTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/feature.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "feature"), name); + + testFeatureTypes(buildSimpleFeatureType("feature"), ft); + + assertEquals(1l, store.features(false).count()); + } + + @Test + public void readFeatureCollectionTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "featurecollection"), name); + + testFeatureTypes(buildFCFeatureType("featurecollection"), ft); + + assertEquals(7l, store.features(false).count()); + } + + /** + * Test reading of Features with array as properties value + * @throws DataStoreException + */ + @Test + public void readPropertyArrayTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/f_prop_array.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + assertEquals(Names.createLocalName(null, null, "f_prop_array"), name); + + testFeatureTypes(buildPropertyArrayFeatureType("f_prop_array", Geometry.class), ft); + + assertEquals(2l, store.features(false).count()); + + Double[][] array1 = new Double[5][5]; + Double[][] array2 = new Double[5][5]; + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + array1[i][j] = (double) (i + j); + array2[i][j] = (double) (i - j); + } + } + + Iterator ite = store.features(false).iterator(); + Feature feat1 = ite.next(); + assertArrayEquals(array1, (Double[][]) feat1.getProperty("array").getValue()); + + Feature feat2 = ite.next(); + assertArrayEquals(array2, (Double[][]) feat2.getProperty("array").getValue()); + + } + + /** + * This test ensure that properties fields with null value doesn't rise NullPointerException + * @throws DataStoreException + */ + @Test + public void readNullPropsTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + + assertEquals(15l, store.features(false).count()); + } + + /** + * This test ensure integer types over Integer.MAX_VALUE are converted to Long. + * @throws DataStoreException + */ + @Test + public void readLongTest() throws DataStoreException, URISyntaxException { + URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/longValue.json"); + + WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file); + assertNotNull(store); + + FeatureType ft = store.getType(); + GenericName name = ft.getName(); + + Feature feature = store.features(false).findFirst().get(); + assertEquals(853555090789l, feature.getPropertyValue("size")); + } + + /** + * Test GeoJSONParser full and lazy reading on FeatureCollection + */ + @Test + public void parserTest() throws URISyntaxException, IOException { + URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json"); + Path fcPath = Paths.get(fcFile.toURI()); + + // test with full reading + GeoJSONObject geoJSONObject = GeoJSONParser.parse(fcPath, false); + assertTrue(geoJSONObject instanceof GeoJSONFeatureCollection); + GeoJSONFeatureCollection geojsonFC = (GeoJSONFeatureCollection) geoJSONObject; + assertFalse(geojsonFC.isLazyMode()); + assertEquals(7, geojsonFC.getFeatures().size()); + + for (int i = 0; i < 7; i++) { + assertTrue(geojsonFC.hasNext()); + assertNotNull(geojsonFC.next()); + } + assertFalse(geojsonFC.hasNext()); //end of collection + + + // test in lazy reading + geoJSONObject = GeoJSONParser.parse(fcPath, true); + assertTrue(geoJSONObject instanceof GeoJSONFeatureCollection); + geojsonFC = (GeoJSONFeatureCollection) geoJSONObject; + assertTrue(geojsonFC.isLazyMode()); + assertEquals(0, geojsonFC.getFeatures().size()); //lazy don't know number of features + + for (int i = 0; i < 7; i++) { + assertTrue(geojsonFC.hasNext()); + assertNotNull(geojsonFC.next()); + } + assertFalse(geojsonFC.hasNext()); //end of collection + + } + + private FeatureType buildPropertyArrayFeatureType(String name, Class geomClass) { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(Double[][].class).setName("array"); + ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + return ftb.build(); + } + + private FeatureType buildGeometryFeatureType(String name, Class geomClass) { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(String.class).setName("fid").addRole(AttributeRole.IDENTIFIER_COMPONENT); + ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + return ftb.build(); + } + + private FeatureType buildSimpleFeatureType(String name) { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(Polygon.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + ftb.addAttribute(String.class).setName("name"); + return ftb.build(); + } + + private FeatureType buildFCFeatureType(String name) { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(Geometry.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + ftb.addAttribute(String.class).setName("name"); + ftb.addAttribute(String.class).setName("address"); + return ftb.build(); + } + + private void testFeatureTypes(FeatureType expected, FeatureType result) { + final FeatureComparator comparator = new FeatureComparator(expected, result); + comparator.compare(); + } +} diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java new file mode 100644 index 0000000..55b0028 --- /dev/null +++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java @@ -0,0 +1,581 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.storage.geojson; + +import org.apache.sis.feature.FeatureComparator; +import com.fasterxml.jackson.core.JsonEncoding; +import java.io.*; +import java.lang.reflect.Array; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Stream; +import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.feature.builder.AttributeRole; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.WritableFeatureSet; +import org.apache.sis.storage.geojson.GeoJSONProvider; +import org.apache.sis.storage.geojson.GeoJSONStore; +import org.apache.sis.storage.geojson.GeoJSONStreamWriter; +import org.apache.sis.util.iso.SimpleInternationalString; +import org.apache.sis.test.TestCase; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.junit.Test; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.io.WKTReader; +import org.opengis.feature.AttributeType; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureAssociationRole; +import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyType; + +/** + * @author Quentin Boileau (Geomatys) + * @author Johann Sorel (Geomatys) + * @version 2.0 + * @since 2.0 + * @module + */ +public class GeoJSONWriteTest extends TestCase { + + private static final GeometryFactory GF = new GeometryFactory(); + private static final WKTReader WKT_READER = new WKTReader(); + private static final Properties PROPERTIES = new Properties(); + + @BeforeClass + public static void init() throws IOException { + PROPERTIES.load(GeoJSONWriteTest.class.getResourceAsStream("/org/apache/sis/internal/storage/geojson/geometries.properties")); + } + + @Test + public void writeSimpleFTTest() throws Exception { + + final Path file = Files.createTempFile("point", ".json"); + + final WritableFeatureSet store = new GeoJSONStore(new GeoJSONProvider(), file, 7); + assertNotNull(store); + final String typeName = file.getFileName().toString().replace(".json", ""); + + + //test creating an unvalid feature type + final FeatureType unvalidFeatureType = buildGeometryFeatureType("test", Point.class); + try { + store.updateType(unvalidFeatureType); + fail(); + } catch (DataStoreException ex) { + //normal exception + } + + + //test writing and reading a feature + final FeatureType validFeatureType = buildGeometryFeatureType(typeName, Point.class); + store.updateType(validFeatureType); + assertNotNull(store.getType()); + assertTrue(Files.exists(file)); + + final Point expectedPoint = GF.createPoint(new Coordinate(-105.01621, 39.57422)); + final Feature feature = store.getType().newInstance(); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), expectedPoint); + feature.setPropertyValue("type","simple"); + store.add(Arrays.asList(feature).iterator()); + + assertTrue(Files.exists(file)); + + + try (Stream stream = store.features(false)) { + final Iterator reader = stream.iterator(); + assertTrue(reader.hasNext()); + final Feature f = reader.next(); + assertEquals("simple", f.getPropertyValue("type")); + assertEquals(expectedPoint, f.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString())); + } + + Files.deleteIfExists(file); + } + + @Test + public void writeAbstractGeometryTest() throws Exception { + + Path file = Files.createTempFile("geoms", ".json"); + + WritableFeatureSet store = new GeoJSONStore(new GeoJSONProvider(), file, 7); + assertNotNull(store); + + String typeName = file.getFileName().toString().replace(".json", ""); + FeatureType validFeatureType = buildGeometryFeatureType(typeName, Geometry.class); + + store.updateType(validFeatureType); + assertNotNull(store.getType()); + assertTrue(Files.exists(file)); + + Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point")); + MultiPoint mpt = (MultiPoint)WKT_READER.read(PROPERTIES.getProperty("multipoint")); + LineString line = (LineString)WKT_READER.read(PROPERTIES.getProperty("linestring")); + MultiLineString mline = (MultiLineString)WKT_READER.read(PROPERTIES.getProperty("multilinestring")); + Polygon poly = (Polygon)WKT_READER.read(PROPERTIES.getProperty("polygon")); + MultiPolygon mpoly = (MultiPolygon)WKT_READER.read(PROPERTIES.getProperty("multipolygon")); + GeometryCollection coll = (GeometryCollection)WKT_READER.read(PROPERTIES.getProperty("geometrycollection")); + + Feature feature = store.getType().newInstance(); + feature.setPropertyValue("type","Point"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt); + store.add(Arrays.asList(feature).iterator()); + + feature = store.getType().newInstance(); + feature.setPropertyValue("type","MultiPoint"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mpt); + store.add(Arrays.asList(feature).iterator()); + + feature = store.getType().newInstance(); + feature.setPropertyValue("type","LineString"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), line); + store.add(Arrays.asList(feature).iterator()); + + feature = store.getType().newInstance(); + feature.setPropertyValue("type","MultiLineString"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mline); + store.add(Arrays.asList(feature).iterator()); + + feature = store.getType().newInstance(); + feature.setPropertyValue("type","Polygon"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), poly); + store.add(Arrays.asList(feature).iterator()); + + feature = store.getType().newInstance(); + feature.setPropertyValue("type","MultiPolygon"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mpoly); + store.add(Arrays.asList(feature).iterator()); + + feature = store.getType().newInstance(); + feature.setPropertyValue("type","GeometryCollection"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), coll); + store.add(Arrays.asList(feature).iterator()); + + assertTrue(Files.exists(file)); + + assertEquals(7, store.features(false).count()); + + try (Stream stream = store.features(false)) { + Iterator ite = stream.iterator(); + while (ite.hasNext()) { + Feature f = ite.next(); + //System.out.println(f); + Geometry geom = (Geometry)f.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString()); + + if (geom instanceof Point) { + assertTrue(pt.equalsExact(geom, 0.0000001)); + } else if (geom instanceof MultiPoint) { + assertTrue(mpt.equalsExact(geom, 0.0000001)); + } else if (geom instanceof LineString) { + assertTrue(line.equalsExact(geom, 0.0000001)); + } else if (geom instanceof MultiLineString) { + assertTrue(mline.equalsExact(geom, 0.0000001)); + } else if (geom instanceof Polygon) { + assertTrue(poly.equalsExact(geom, 0.0000001)); + } else if (geom instanceof MultiPolygon) { + assertTrue(mpoly.equalsExact(geom, 0.0000001)); + } else if (geom instanceof GeometryCollection) { + assertTrue(coll.equalsExact(geom, 0.0000001)); + } + } + } + + Files.deleteIfExists(file); + } + + @Test + public void writeComplexFeaturesTest() throws Exception { + Path file = Files.createTempFile("complex", ".json"); + + WritableFeatureSet store = new GeoJSONStore(new GeoJSONProvider(), file, 7); + assertNotNull(store); + + String typeName = file.getFileName().toString().replace(".json", ""); + + FeatureType complexFT = buildComplexFeatureType(typeName); + + store.updateType(complexFT); + assertNotNull(store.getType()); + assertTrue(Files.exists(file)); + + Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point")); + Feature expected = null; + Feature feature = store.getType().newInstance(); + feature.setPropertyValue("longProp",100l); + feature.setPropertyValue("stringProp","Some String"); + feature.setPropertyValue("integerProp",15); + feature.setPropertyValue("booleanProp",true); + + final FeatureType level1Type = ((FeatureAssociationRole)feature.getType().getProperty("level1")).getValueType(); + + final Feature level11 = level1Type.newInstance(); + level11.setPropertyValue("longProp2",66446l); + + final FeatureType level2Type = ((FeatureAssociationRole)level11.getType().getProperty("level2")).getValueType(); + + final Feature level211 = level2Type.newInstance(); + level211.setPropertyValue("level2prop","text"); + final Feature level212 = level2Type.newInstance(); + level212.setPropertyValue("level2prop","text2"); + final Feature level213 = level2Type.newInstance(); + level213.setPropertyValue("level2prop","text3"); + + level11.setPropertyValue("level2", Arrays.asList(level211,level212,level213)); + + + Feature level12 = level1Type.newInstance(); + level12.setPropertyValue("longProp2",4444444l); + + final Feature level221 = level2Type.newInstance(); + level221.setPropertyValue("level2prop","fish"); + final Feature level222 = level2Type.newInstance(); + level222.setPropertyValue("level2prop","cat"); + final Feature level223 = level2Type.newInstance(); + level223.setPropertyValue("level2prop","dog"); + + level12.setPropertyValue("level2", Arrays.asList(level221,level222,level223)); + + feature.setPropertyValue("level1",Arrays.asList(level11,level12)); + + feature.setPropertyValue("geometry", pt); + expected = copy(feature); + store.add(Arrays.asList(feature).iterator()); + + + assertTrue(Files.exists(file)); + + assertEquals(1, store.features(false).count()); + + try (Stream stream = store.features(false)) { + Iterator ite = stream.iterator(); + while (ite.hasNext()) { + Feature candidate = ite.next(); + FeatureComparator comparator = new FeatureComparator(expected, candidate); + comparator.ignoredProperties.add(AttributeConvention.IDENTIFIER); + comparator.compare(); + } + } + Files.deleteIfExists(file); + } + + @Test + public void writeStreamTest() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + FeatureType validFeatureType = buildGeometryFeatureType("simpleFT", Point.class); + + Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point")); + + try (GeoJSONStreamWriter fw = new GeoJSONStreamWriter(baos, validFeatureType, 4)) { + Feature feature = fw.next(); + feature.setPropertyValue("type","feat1"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt); + fw.write(); + + feature = fw.next(); + feature.setPropertyValue("type","feat2"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt); + fw.write(); + + } + + String outputJSON = baos.toString("UTF-8"); + assertNotNull(outputJSON); + assertFalse(outputJSON.isEmpty()); + + String expected = "{\n" + + "\"type\":\"FeatureCollection\"\n" + + ",\"features\":[\n" + + "{\"type\":\"Feature\",\"id\":0,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"type\":\"feat1\"}}\n" + + ",{\"type\":\"Feature\",\"id\":1,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"type\":\"feat2\"}}\n" + + "]}"; + + assertEquals(expected, outputJSON); + } + + @Test + public void writeStreamSingleFeatureTest() throws Exception { + FeatureType validFeatureType = buildGeometryFeatureType("simpleFT", Point.class); + + Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point")); + + final String outputJSON; + try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Feature feature = validFeatureType.newInstance(); + feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), 0); + feature.setPropertyValue("type","feat1"); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt); + GeoJSONStreamWriter.writeSingleFeature(baos, feature, JsonEncoding.UTF8, 4, false); + + outputJSON = baos.toString("UTF-8"); + } + + assertNotNull(outputJSON); + assertFalse(outputJSON.isEmpty()); + + String expected = "{\"type\":\"Feature\",\"id\":0," + + "\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]}," + + "\"properties\":{\"type\":\"feat1\"}}"; + assertEquals(expected, outputJSON); + } + + @Test + public void writeStreamSingleGeometryTest() throws Exception { + Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point")); + + final String outputJSON; + try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + GeoJSONStreamWriter.writeSingleGeometry(baos, pt, JsonEncoding.UTF8, 4, false); + + outputJSON = baos.toString("UTF-8"); + } + + assertNotNull(outputJSON); + assertFalse(outputJSON.isEmpty()); + + String expected = "{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]}"; + assertEquals(expected, outputJSON); + } + + @Test + public void writePropertyArrayTest() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + FeatureType validFeatureType = buildPropertyArrayFeatureType("arrayFT", Point.class); + + Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point")); + + double[][] array1 = new double[5][5]; + double[][] array2 = new double[5][5]; + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + array1[i][j] = i+j; + array2[i][j] = i-j; + } + } + + try (GeoJSONStreamWriter fw = new GeoJSONStreamWriter(baos, validFeatureType, 4)) { + Feature feature = fw.next(); + feature.setPropertyValue("array",array1); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt); + fw.write(); + + feature = fw.next(); + feature.setPropertyValue("array",array2); + feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt); + fw.write(); + + } + + String outputJSON = baos.toString("UTF-8"); + assertNotNull(outputJSON); + assertFalse(outputJSON.isEmpty()); + + String expected = "{\n" + + "\"type\":\"FeatureCollection\"\n" + + ",\"features\":[\n" + + "{\"type\":\"Feature\",\"id\":\"0\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"array\":[[0.0,1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0,5.0],[2.0,3.0,4.0,5.0,6.0],[3.0,4.0,5.0,6.0,7.0],[4.0,5.0,6.0,7.0,8.0]]}}\n" + + ",{\"type\":\"Feature\",\"id\":\"1\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"array\":[[0.0,-1.0,-2.0,-3.0,-4.0],[1.0,0.0,-1.0,-2.0,-3.0],[2.0,1.0,0.0,-1.0,-2.0],[3.0,2.0,1.0,0.0,-1.0],[4.0,3.0,2.0,1.0,0.0]]}}\n" + + "]}"; + assertEquals(expected, outputJSON); + } + + private FeatureType buildGeometryFeatureType(String name, Class geomClass) { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(Integer.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); + ftb.addAttribute(String.class).setName("type"); + ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + return ftb.build(); + } + + private FeatureType buildPropertyArrayFeatureType(String name, Class geomClass) { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); + ftb.addAttribute(double[][].class).setName("array"); + ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + return ftb.build(); + } + + /** + * Build 2 level Feature complex + */ + private FeatureType buildComplexFeatureType(String name) { + FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + + ftb.setName("level2"); + ftb.addAttribute(String.class).setName("level2prop"); + final FeatureType level2 = ftb.build(); + + ftb = new FeatureTypeBuilder(); + ftb.setName("level1"); + ftb.addAttribute(Long.class).setName("longProp2"); + ftb.addAssociation(level2).setName("level2").setMinimumOccurs(1).setMaximumOccurs(5); + final FeatureType level1 = ftb.build(); + + ftb = new FeatureTypeBuilder(); + ftb.setName(name); + ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); + ftb.addAttribute(Long.class).setName("longProp"); + ftb.addAttribute(String.class).setName("stringProp"); + ftb.addAttribute(Integer.class).setName("integerProp"); + ftb.addAttribute(Boolean.class).setName("booleanProp"); + ftb.addAssociation(level1).setName("level1").setMinimumOccurs(1).setMaximumOccurs(3); + ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY); + ftb.setDescription(new SimpleInternationalString("Description")); + return ftb.build(); + } + + /** + * Create a copy of given feature. + * This is not a deep copy, only the feature and associated feature are copied, + * values are not copied. + */ + public static Feature copy(Feature feature){ + return copy(feature, false); + } + + /** + * @param deep true for a deep copy + */ + private static Feature copy(Feature feature, boolean deep){ + final FeatureType type = feature.getType(); + + final Feature cp = type.newInstance(); + + final Collection props = type.getProperties(true); + for (PropertyType pt : props) { + if (pt instanceof AttributeType ){ + final String name = pt.getName().toString(); + final Object val = feature.getPropertyValue(name); + if(val!=null){ + cp.setPropertyValue(name, deep ? deepCopy(val) : val); + } + } else if(pt instanceof FeatureAssociationRole) { + final String name = pt.getName().toString(); + final Object val = feature.getPropertyValue(name); + if (deep) { + if(val!=null){ + cp.setPropertyValue(name, deepCopy(val)); + } + } else { + if(val instanceof Collection){ + final Collection col = (Collection) val; + final Collection cpCol = new ArrayList(col.size()); + for(Iterator ite=col.iterator();ite.hasNext();){ + cpCol.add(copy((Feature)ite.next())); + } + cp.setPropertyValue(name, cpCol); + }else if(val!=null){ + cp.setPropertyValue(name, copy((Feature)val)); + } + } + + } + } + return cp; + } + + /** + * Make a deep copy of given Feature. + * + * @param feature Feature to copy + * @return Deep copy of the feature + */ + public static Feature deepCopy(Feature feature){ + return copy(feature, true); + } + + /** + * Make a copy of given object. + * Multiplace cases are tested to make a deep copy. + * + * @param candidate + * @return copied object + */ + public static Object deepCopy(final Object candidate) { + if(candidate==null) return null; + + if(candidate instanceof String || + candidate instanceof Number || + candidate instanceof URL || + candidate instanceof URI || + candidate.getClass().isPrimitive() || + candidate instanceof Character || + candidate instanceof GridCoverage){ + //we consider those immutable + return candidate; + }else if(candidate instanceof Feature){ + return deepCopy((Feature)candidate); + }else if(candidate instanceof Geometry){ + return ((Geometry)candidate).clone(); + }else if(candidate instanceof Date){ + return ((Date)candidate).clone(); + }else if(candidate instanceof Date){ + return ((Date)candidate).clone(); + }else if(candidate instanceof Object[]){ + final Object[] array = (Object[])candidate; + final Object[] copy = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + copy[i] = deepCopy(array[i]); + } + return copy; + }else if(candidate instanceof List){ + final List list = (List)candidate; + final int size = list.size(); + final List cp = new ArrayList(size); + for(int i=0;i ite=map.entrySet().iterator(); ite.hasNext();) { + final Map.Entry entry = ite.next(); + cp.put(entry.getKey(), deepCopy(entry.getValue())); + } + return Collections.unmodifiableMap(cp); + } + + //array type + final Class clazz = candidate.getClass(); + if(clazz.isArray()){ + final Class compClazz = clazz.getComponentType(); + final int length = Array.getLength(candidate); + final Object cp = Array.newInstance(compClazz, length); + + if(compClazz.isPrimitive()){ + System.arraycopy(candidate, 0, cp, 0, length); + }else{ + for(int i=0;i