You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2019/11/12 16:18:21 UTC
[sis] 01/01: GeoJson : add GeoJson DataStore
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 fb85b0adda25b99acc0f58f0334425a45a240718
Author: jsorel <jo...@geomatys.com>
AuthorDate: Tue Oct 29 17:07:57 2019 +0100
GeoJson : add GeoJson DataStore
---
storage/pom.xml | 1 +
storage/{ => sis-geojson}/pom.xml | 102 +--
.../org/apache/sis/storage/geojson/Bundle.java | 218 ++++++
.../apache/sis/storage/geojson/Bundle.properties | 9 +
.../sis/storage/geojson/Bundle_en.properties | 9 +
.../sis/storage/geojson/Bundle_fr.properties | 9 +
.../sis/storage/geojson/GeoJSONFileWriter.java | 133 ++++
.../sis/storage/geojson/GeoJSONProvider.java | 181 +++++
.../apache/sis/storage/geojson/GeoJSONReader.java | 347 +++++++++
.../apache/sis/storage/geojson/GeoJSONStore.java | 452 ++++++++++++
.../sis/storage/geojson/GeoJSONStreamWriter.java | 221 ++++++
.../apache/sis/storage/geojson/GeoJSONWriter.java | 455 ++++++++++++
.../sis/storage/geojson/binding/GeoJSONCRS.java | 94 +++
.../storage/geojson/binding/GeoJSONFeature.java | 68 ++
.../geojson/binding/GeoJSONFeatureCollection.java | 189 +++++
.../storage/geojson/binding/GeoJSONGeometry.java | 177 +++++
.../sis/storage/geojson/binding/GeoJSONObject.java | 77 ++
.../storage/geojson/utils/FeatureTypeUtils.java | 596 +++++++++++++++
.../geojson/utils/GeoJSONFeatureIterator.java | 44 ++
.../sis/storage/geojson/utils/GeoJSONMembres.java | 42 ++
.../sis/storage/geojson/utils/GeoJSONParser.java | 680 +++++++++++++++++
.../sis/storage/geojson/utils/GeoJSONTypes.java | 48 ++
.../sis/storage/geojson/utils/GeoJSONUtils.java | 581 +++++++++++++++
.../sis/storage/geojson/utils/GeometryUtils.java | 353 +++++++++
.../storage/geojson/utils/LiteJsonLocation.java | 104 +++
.../org.apache.sis.storage.DataStoreProvider | 1 +
.../storage/geojson/FeatureTypeUtilsTest.java | 265 +++++++
.../internal/storage/geojson/GeoJSONReadTest.java | 351 +++++++++
.../internal/storage/geojson/GeoJSONWriteTest.java | 577 +++++++++++++++
.../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 +++++++++++++
44 files changed, 9229 insertions(+), 75 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 @@
<module>sis-sqlstore</module>
<module>sis-netcdf</module>
<module>sis-geotiff</module>
+ <module>sis-geojson</module>
<module>sis-earth-observation</module>
<module>sis-gdal</module>
</modules>
diff --git a/storage/pom.xml b/storage/sis-geojson/pom.xml
similarity index 54%
copy from storage/pom.xml
copy to storage/sis-geojson/pom.xml
index fe99bdb..3c49d4d 100644
--- a/storage/pom.xml
+++ b/storage/sis-geojson/pom.xml
@@ -27,7 +27,7 @@
<parent>
<groupId>org.apache.sis</groupId>
- <artifactId>parent</artifactId>
+ <artifactId>storage</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
@@ -35,12 +35,11 @@
<!-- ===========================================================
Module Description
=========================================================== -->
- <artifactId>storage</artifactId>
- <packaging>pom</packaging>
- <name>Apache SIS storage</name>
+ <groupId>org.apache.sis.storage</groupId>
+ <artifactId>sis-geojson</artifactId>
+ <name>Apache SIS GeoJSON storage</name>
<description>
- Group of modules for reading and writing data from/to various storages.
- Storages are typically file formats or a database schemas.
+ Geojson DataStore implementation.
</description>
@@ -72,26 +71,6 @@
</developer>
</developers>
<contributors>
- <contributor>
- <name>Thi Phuong Hao Nguyen</name>
- <email>nguyenthiphuonghao243@gmail.com</email>
- <organization>VNSC</organization>
- <organizationUrl>http://vnsc.org.vn</organizationUrl>
- <timezone>+7</timezone>
- <roles>
- <role>developer</role>
- </roles>
- </contributor>
- <contributor>
- <name>Minh Chinh Vu</name>
- <email>chinhvm.uet.1995@gmail.com</email>
- <organization>VNSC</organization>
- <organizationUrl>http://vnsc.org.vn</organizationUrl>
- <timezone>+7</timezone>
- <roles>
- <role>developer</role>
- </roles>
- </contributor>
</contributors>
@@ -100,21 +79,18 @@
=========================================================== -->
<build>
<plugins>
-
- <!-- Compile properties files into resources UTF files and
- collect JAR files in <root>/target/binaries directory. -->
<plugin>
- <groupId>org.apache.sis.core</groupId>
- <artifactId>sis-build-helper</artifactId>
- <version>${sis.plugin.version}</version>
- <executions>
- <execution>
- <goals>
- <goal>compile-resources</goal>
- <goal>collect-jars</goal>
- </goals>
- </execution>
- </executions>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>
+ org.apache.sis.storage.geojson
+ </Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
</plugin>
</plugins>
</build>
@@ -125,52 +101,28 @@
=========================================================== -->
<dependencies>
<dependency>
- <groupId>org.apache.sis.core</groupId>
- <artifactId>sis-metadata</artifactId>
+ <groupId>org.apache.sis.storage</groupId>
+ <artifactId>sis-storage</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.sis.core</groupId>
- <artifactId>sis-referencing</artifactId>
+ <artifactId>sis-cql</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.opengis</groupId>
- <artifactId>geoapi-pending</artifactId>
+ <groupId>org.locationtech.jts</groupId>
+ <artifactId>jts-core</artifactId>
</dependency>
-
- <!-- Test dependencies -->
<dependency>
- <groupId>org.opengis</groupId>
- <artifactId>geoapi-conformance</artifactId>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.10.0</version>
</dependency>
- <dependency>
- <groupId>org.apache.derby</groupId>
- <artifactId>derby</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.sis.core</groupId>
- <artifactId>sis-utility</artifactId>
- <version>${project.version}</version>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- </dependencies>
+ </dependencies>
- <!-- ===========================================================
- Sub-modules included in the build
- =========================================================== -->
- <modules>
- <module>sis-storage</module>
- <module>sis-shapefile</module>
- <module>sis-xmlstore</module>
- <module>sis-sqlstore</module>
- <module>sis-netcdf</module>
- <module>sis-geotiff</module>
- <module>sis-earth-observation</module>
- <module>sis-gdal</module>
- </modules>
+ <repositories>
+ </repositories>
</project>
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..b6074e0
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java
@@ -0,0 +1,218 @@
+/*
+ * 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.ResourceBundle;
+import java.util.MissingResourceException;
+import org.opengis.util.InternationalString;
+import org.apache.sis.util.iso.ResourceInternationalString;
+import org.apache.sis.util.resources.IndexedResourceBundle;
+
+
+/**
+ * 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;
+
+ /**
+ * multiple GeoJSON data files (.json)
+ */
+ public static final short datastoreFolderDescription = 4;
+
+ /**
+ * GeoJSON (folder)
+ */
+ 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 <code>Object...</code>, 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 <code>format</code> 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}", <i>etc</i>.
+ * @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..9b25fe2
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties
@@ -0,0 +1,9 @@
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+datastoreTitle=GeoJSON
+datastoreDescription=GeoJSON data file (.json)
+datastoreFolderTitle=GeoJSON (folder)
+datastoreFolderDescription=multiple GeoJSON data files (.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..9b25fe2
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties
@@ -0,0 +1,9 @@
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+datastoreTitle=GeoJSON
+datastoreDescription=GeoJSON data file (.json)
+datastoreFolderTitle=GeoJSON (folder)
+datastoreFolderDescription=multiple GeoJSON data files (.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..b905f93
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties
@@ -0,0 +1,9 @@
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+datastoreTitle=GeoJSON
+datastoreDescription=fichier GeoJSON de donn\u00e9es vectorielles (.json)
+datastoreFolderTitle=GeoJSON (dossier)
+datastoreFolderDescription=Multiples fichiers de donn\u00e9es GeoJSON (.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/GeoJSONFileWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java
new file mode 100644
index 0000000..c0d8e2b
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java
@@ -0,0 +1,133 @@
+/*
+ * 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.Closeable;
+import org.apache.sis.storage.DataStoreException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Iterator;
+import java.util.concurrent.locks.ReadWriteLock;
+import org.apache.sis.internal.feature.AttributeConvention;
+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
+ */
+class GeoJSONFileWriter extends GeoJSONReader {
+
+ private final GeoJSONWriter writer;
+
+ private Feature edited = null;
+ private Feature lastWritten = null;
+ 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 (final 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..ab7148a
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java
@@ -0,0 +1,181 @@
+/*
+ * 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 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<String> EXTENSIONS = Arrays.asList("json", "geojson", "topojson");
+
+ public static final ParameterDescriptor<URI> PATH = new ParameterBuilder()
+ .addName(LOCATION)
+ .addName("path")
+ .setRequired(true)
+ .create(URI.class, null);
+
+ /**
+ * Optional
+ */
+ public static final ParameterDescriptor<Integer> 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..4ab24df
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java
@@ -0,0 +1,347 @@
+/*
+ * 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.locationtech.jts.geom.Geometry;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeature;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.storage.geojson.binding.GeoJSONObject;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeometryUtils;
+import org.apache.sis.util.ObjectConverters;
+import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.ObjectConverter;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+import java.io.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.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.util.collection.BackingStoreException;
+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;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+public class GeoJSONReader implements Iterator<Feature>, AutoCloseable {
+
+ private final static Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson");
+ private final Map<Map.Entry<Class, Class>, ObjectConverter> convertersCache = new HashMap<>();
+
+ private GeoJSONObject jsonObj = null;
+ private Boolean toRead = true;
+
+ protected final ReadWriteLock rwlock;
+ protected final FeatureType featureType;
+ protected final Path jsonFile;
+ protected Feature current = null;
+ protected int currentFeatureIdx = 0;
+
+ /**
+ * 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 = GeometryUtils.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<String, Object> properties = jsonFeature.getProperties();
+ fillFeature(feature, properties);
+
+ return feature;
+ }
+
+ /**
+ * Recursively fill a ComplexAttribute with properties map
+ * @param feature
+ * @param properties
+ */
+ private void fillFeature(Feature feature, Map<String, Object> properties) throws 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<Feature> subs = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ final Feature subComplexAttribute = assoType.newInstance();
+ fillFeature(subComplexAttribute, (Map) Array.get(value, i));
+ subs.add(subComplexAttribute);
+ }
+ feature.setPropertyValue(attName,subs);
+ }
+ } else if (value instanceof Map) {
+ final Feature subComplexAttribute = assoType.newInstance();
+ fillFeature(subComplexAttribute, (Map) value);
+ feature.setPropertyValue(attName, subComplexAttribute);
+ }
+
+ } else if(type instanceof AttributeType) {
+ final Attribute<?> property = (Attribute<?>) feature.getProperty( type.getName().toString());
+ fillProperty(property, value);
+ }
+ }
+ }
+
+ /**
+ * Try to convert value as expected in PropertyType description.
+ * @param prop
+ * @param value
+ */
+ private void fillProperty(Attribute prop, Object value) throws 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<Class, Class> 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 = GeometryUtils.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..a57b41f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java
@@ -0,0 +1,452 @@
+/*
+ * 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.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.Query;
+import org.apache.sis.storage.WritableFeatureSet;
+import org.apache.sis.util.logging.Logging;
+import static org.apache.sis.storage.geojson.GeoJSONProvider.*;
+import org.apache.sis.storage.geojson.binding.*;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import org.apache.sis.storage.geojson.utils.FeatureTypeUtils;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.util.iso.Names;
+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 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<ParameterValueGroup> getOpenParameters() {
+ return Optional.of(parameters);
+ }
+
+ @Override
+ public Metadata getMetadata() throws DataStoreException {
+ return new DefaultMetadata();
+ }
+
+ @Override
+ public DataStoreProvider getProvider() {
+ return provider;
+ }
+
+ @Override
+ public Optional<GenericName> 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<String, Object> 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<? extends Geometry> 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<Envelope> 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<Feature> features(boolean parallel) throws DataStoreException {
+ final GeoJSONReader reader = new GeoJSONReader(jsonFile, featureType, rwLock);
+ final Stream<Feature> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(reader, Spliterator.ORDERED), false);
+ stream.onClose(reader::close);
+ return stream;
+ }
+
+ @Override
+ public void add(Iterator<? extends Feature> 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<? super Feature> 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<? super Feature> filter, UnaryOperator<Feature> 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<Path> 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..f43a42d
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java
@@ -0,0 +1,221 @@
+/*
+ * 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 org.locationtech.jts.geom.Geometry;
+import org.apache.sis.storage.DataStoreException;
+
+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.FeatureNaming;
+import org.apache.sis.storage.IllegalNameException;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.util.collection.BackingStoreException;
+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 class GeoJSONStreamWriter implements Iterator<Feature>, AutoCloseable {
+
+ private final GeoJSONWriter writer;
+ private final FeatureType featureType;
+
+ private Feature edited = null;
+ private Feature lastWritten = null;
+ private int currentFeatureIdx = 0;
+
+ 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<Operation> 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<PropertyTypeBuilder> 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 (final 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 (final 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..72aac30
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java
@@ -0,0 +1,455 @@
+/*
+ * 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 org.locationtech.jts.geom.Geometry;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.storage.geojson.utils.GeometryUtils;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.util.Utilities;
+import static org.apache.sis.storage.geojson.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+import static org.apache.sis.storage.geojson.binding.GeoJSONGeometry.*;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+class GeoJSONWriter implements Closeable, Flushable {
+
+ private final static String SYS_LF;
+ static {
+ String lf = null;
+ try {
+ lf = System.getProperty("line.separator");
+ } catch (Throwable t) { } // access exception?
+ SYS_LF = (lf == null) ? "\n" : lf;
+ }
+
+ private final JsonGenerator writer;
+ private final OutputStream outputStream;
+ private boolean first = true;
+ private boolean prettyPrint = true;
+
+ // state boolean to ensure that we can't call writeStartFeatureCollection
+ // if we first called writeSingleFeature
+ private boolean isFeatureCollection = false;
+ private boolean isSingleFeature = false;
+ private boolean isSingleGeometry = false;
+
+ private final NumberFormat numberFormat;
+
+ @Deprecated
+ GeoJSONWriter(File file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException {
+ this(file.toPath(), encoding, doubleAccuracy, prettyPrint);
+ }
+
+ GeoJSONWriter(Path file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException {
+ this(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<Geometry> 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<String> 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<? extends PropertyType> 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 = GeometryUtils.toGeoJSONGeometry((Geometry) geom.getValue());
+ writeGeoJSONGeometry(jsonGeometry);
+ }
+
+ /**
+ * Write a JTS Geometry
+ * @param geom
+ * @throws IOException
+ */
+ void writeSingleGeometry(Geometry geom) throws IOException {
+ assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+ "writeSingleGeometry can called only once per GeoJSONWriter.";
+ isSingleGeometry = true;
+ GeoJSONGeometry jsonGeometry = GeometryUtils.toGeoJSONGeometry(geom);
+ writeGeoJSONGeometry(jsonGeometry);
+ }
+
+ /**
+ * Write a GeometryAttribute
+ * @param geom
+ * @throws IOException
+ */
+ private void writeFeatureGeometry(Geometry geom) throws IOException {
+ writeGeoJSONGeometry(GeometryUtils.toGeoJSONGeometry(geom));
+ }
+
+ /**
+ * Write a GeoJSONGeometry
+ * @param jsonGeometry
+ * @throws IOException
+ */
+ private void writeGeoJSONGeometry(GeoJSONGeometry jsonGeometry) throws IOException {
+ writer.writeStartObject();
+ writer.writeStringField(TYPE, jsonGeometry.getType());
+
+ if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+ List<GeoJSONGeometry> geometries = ((GeoJSONGeometryCollection) jsonGeometry).getGeometries();
+ writer.writeArrayFieldStart(GEOMETRIES); // "geometries" : [
+ for (GeoJSONGeometry geometry : geometries) {
+ writeGeoJSONGeometry(geometry);
+ }
+ writer.writeEndArray(); // "]"
+ } else {
+ writer.writeArrayFieldStart(COORDINATES); // "coordinates" : [
+ if (jsonGeometry instanceof GeoJSONPoint) {
+ writeArray(((GeoJSONPoint) jsonGeometry).getCoordinates());
+ } else if (jsonGeometry instanceof GeoJSONLineString) {
+ writeArray(((GeoJSONLineString) jsonGeometry).getCoordinates());
+ } else if (jsonGeometry instanceof GeoJSONPolygon) {
+ writeArray(((GeoJSONPolygon) jsonGeometry).getCoordinates());
+ } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+ writeArray(((GeoJSONMultiPoint) jsonGeometry).getCoordinates());
+ } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+ writeArray(((GeoJSONMultiLineString) jsonGeometry).getCoordinates());
+ } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+ writeArray(((GeoJSONMultiPolygon) jsonGeometry).getCoordinates());
+ } else {
+ throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+ }
+ writer.writeEndArray(); // "]"
+ }
+
+ writer.writeEndObject();
+ }
+
+ private void writeArray(double[] coordinates) throws IOException {
+ for (double coordinate : coordinates) {
+ writer.writeNumber(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/java/org/apache/sis/storage/geojson/binding/GeoJSONCRS.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONCRS.java
new file mode 100644
index 0000000..cd118c0
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONCRS.java
@@ -0,0 +1,94 @@
+/*
+ * 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.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.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+import org.apache.sis.storage.geojson.utils.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<String, String> 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<String, String> 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/storage/geojson/binding/GeoJSONFeature.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeature.java
new file mode 100644
index 0000000..f15d589
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/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.storage.geojson.binding;
+
+import org.apache.sis.storage.geojson.utils.GeoJSONTypes;
+
+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 = null;
+ /**
+ * 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 = null;
+ private Map<String, Object> properties = new HashMap<>();
+
+ public GeoJSONFeature() {
+ setType(GeoJSONTypes.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<String, Object> getProperties() {
+ return properties;
+ }
+
+ public void setProperties(Map<String, Object> properties) {
+ this.properties = properties;
+ }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeatureCollection.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeatureCollection.java
new file mode 100644
index 0000000..a563cde
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeatureCollection.java
@@ -0,0 +1,189 @@
+/*
+ * 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.binding;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import org.apache.sis.storage.geojson.utils.*;
+
+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.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 GeoJSONFeatureIterator<GeoJSONFeature> {
+
+ private List<GeoJSONFeature> features = new ArrayList<>();
+
+ transient JsonLocation currentPos = null;
+ transient GeoJSONFeature current = null;
+ transient int currentIdx = 0;
+ 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 = null;
+ transient LiteJsonLocation startPos = null;
+ transient LiteJsonLocation endPos = null;
+ transient Boolean lazyMode;
+
+ public GeoJSONFeatureCollection(Boolean lazyMode) {
+ setType(GeoJSONTypes.FEATURE_COLLECTION);
+ this.lazyMode = lazyMode;
+ }
+
+ public List<GeoJSONFeature> getFeatures() {
+ return features;
+ }
+
+ public void setFeatures(List<GeoJSONFeature> 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/storage/geojson/binding/GeoJSONGeometry.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONGeometry.java
new file mode 100644
index 0000000..4c0a1db
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONGeometry.java
@@ -0,0 +1,177 @@
+/*
+ * 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.binding;
+
+import org.apache.sis.storage.geojson.utils.GeoJSONTypes;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @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 = null;
+
+ public GeoJSONPoint() {
+ setType(GeoJSONTypes.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 = null;
+
+ public GeoJSONMultiPoint() {
+ setType(GeoJSONTypes.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 = null;
+
+ public GeoJSONLineString() {
+ setType(GeoJSONTypes.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 = null;
+
+ public GeoJSONMultiLineString() {
+ setType(GeoJSONTypes.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 = null;
+
+ public GeoJSONPolygon() {
+ setType(GeoJSONTypes.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 = null;
+
+ public GeoJSONMultiPolygon() {
+ setType(GeoJSONTypes.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<GeoJSONGeometry> geometries = new ArrayList<GeoJSONGeometry>();
+
+ public GeoJSONGeometryCollection() {
+ setType(GeoJSONTypes.GEOMETRY_COLLECTION);
+ }
+
+ public List<GeoJSONGeometry> getGeometries() {
+ return geometries;
+ }
+
+ public void setGeometries(List<GeoJSONGeometry> geometries) {
+ this.geometries = geometries;
+ }
+ }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONObject.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONObject.java
new file mode 100644
index 0000000..5658c23
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/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.storage.geojson.binding;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import static org.apache.sis.storage.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 = null;
+ private GeoJSONCRS crs = null;
+
+ 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/utils/FeatureTypeUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/FeatureTypeUtils.java
new file mode 100644
index 0000000..8f6e50b
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/FeatureTypeUtils.java
@@ -0,0 +1,596 @@
+/*
+ * 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.utils;
+
+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 java.util.logging.Logger;
+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.apache.sis.util.logging.Logging;
+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 <a href="http://json-schema.org/">JSON-Schema</a> 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 Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson.utils");
+// private static final FilterFactory2 FF = (FilterFactory2) DefaultFactories.forBuildin(FilterFactory.class);
+
+ 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<FeatureType> 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<AttributeType<?>> 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<? extends PropertyType> descriptors = ft.getProperties(true);
+ List<String> 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<Filter> 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<String> 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<String> 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<Object, Object> 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<Object, Object> parseUserDataMap(JsonParser parser) throws IOException {
+
+ Map<Object, Object> 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<String> parseRequiredArray(JsonParser parser) throws IOException {
+ List<String> 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/storage/geojson/utils/GeoJSONFeatureIterator.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONFeatureIterator.java
new file mode 100644
index 0000000..697b0f4
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONFeatureIterator.java
@@ -0,0 +1,44 @@
+/*
+ * 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.utils;
+
+import java.io.Closeable;
+import java.util.Iterator;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeature;
+import org.apache.sis.util.collection.BackingStoreException;
+
+/**
+ * Custom FeatureIterator used for lazy parsing of GeoJSONFeature in a json file.
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+public interface GeoJSONFeatureIterator<F extends GeoJSONFeature> extends Iterator<F>, Closeable {
+
+ @Override
+ F next() throws BackingStoreException;
+
+ @Override
+ boolean hasNext() throws BackingStoreException;
+
+ @Override
+ void close() throws BackingStoreException;
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONMembres.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONMembres.java
new file mode 100644
index 0000000..9416ba6
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONMembres.java
@@ -0,0 +1,42 @@
+/*
+ * 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.utils;
+
+import org.apache.sis.util.Static;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+public final class GeoJSONMembres extends Static {
+
+ 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/utils/GeoJSONParser.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONParser.java
new file mode 100644
index 0000000..e7a678f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONParser.java
@@ -0,0 +1,680 @@
+/*
+ * 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.utils;
+
+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.storage.geojson.binding.*;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import static org.apache.sis.storage.geojson.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+
+/**
+ * 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 final static 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<String, Object> parseMap(JsonParser p) throws IOException {
+ Map<String, Object> map = new HashMap<>();
+ final JsonToken currentToken = p.getCurrentToken();
+ if (currentToken == JsonToken.VALUE_NULL) {
+ return map;
+ }
+
+ if (currentToken != JsonToken.START_OBJECT) {
+ LOGGER.log(Level.WARNING, "Expect START_OBJECT token but got "+currentToken+" for "+p.getCurrentName());
+ return map;
+ }
+
+ assert(currentToken == JsonToken.START_OBJECT);
+ while (p.nextToken() != JsonToken.END_OBJECT) {
+ String key = p.getCurrentName();
+ JsonToken next = p.nextToken();
+
+ map.put(key, getValue(next, p));
+ }
+ return map;
+ }
+
+ /**
+ * Parse a List of Objects.
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p parser jackson parser with current token on a START_ARRAY.
+ * @return List of Objects
+ * @throws IOException
+ */
+ private static List<Object> parseArray(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ List<Object> list = new ArrayList<>();
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ list.add(getValue(p.getCurrentToken(), p));
+ }
+ return list;
+ }
+
+ /**
+ * Parse a List of Objects.
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p parser jackson parser with current token on a START_ARRAY.
+ * @return array object typed after first element class
+ * @throws IOException
+ */
+ private static Object parseArray2(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ List<Object> list = new ArrayList<>();
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ list.add(getValue(p.getCurrentToken(), p));
+ }
+
+ if (list.isEmpty()) {
+ return new Object[0];
+ }
+
+ Class binding = list.get(0).getClass();
+ Object newArray = Array.newInstance(binding, list.size());
+ for (int i = 0; i < list.size(); i++) {
+ Array.set(newArray, i, list.get(i));
+ }
+
+ return newArray;
+ }
+
+ /**
+ * Convert the current token to appropriate object.
+ * Supported (String, Integer, Float, Boolean, Null, Array, Map)
+ *
+ * @param token current token
+ * @param p parser
+ * @return current token value String or Integer or Float or Boolean or null or an array or a map.
+ * @throws IOException
+ */
+ static Object getValue(JsonToken token, JsonParser p) throws IOException {
+ if (token == JsonToken.VALUE_STRING) {
+ return p.getValueAsString();
+ } else if (token == JsonToken.VALUE_NUMBER_INT) {
+ 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.utils.GeoJSONTypes}
+ * @return GeoJSONObject
+ */
+ private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type) {
+ return getOrCreateFromType(object, type, Boolean.FALSE);
+
+ }
+
+ /**
+ * Create GeoJSONObject using type.
+ * If previous object is not null, forward bbox and crs parameter to the new GeoJSONObject.
+ *
+ * @param object previous object.
+ * @param type see {@link org.apache.sis.storage.geojson.utils.GeoJSONTypes}
+ * @param lazy lazy mode flag
+ * @return GeoJSONObject
+ */
+ private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type, Boolean lazy) {
+
+ GeoJSONObject result = object;
+ if (type != null) {
+ switch (type) {
+ case FEATURE_COLLECTION:
+ if (result instanceof GeoJSONFeatureCollection) {
+ return result;
+ } else {
+ result = new GeoJSONFeatureCollection(lazy);
+ }
+ break;
+ case FEATURE:
+ if (result instanceof GeoJSONFeature) {
+ return result;
+ } else {
+ result = new GeoJSONFeature();
+ }
+ break;
+ case POINT:
+ if (result instanceof GeoJSONPoint) {
+ return result;
+ } else {
+ result = new GeoJSONPoint();
+ }
+ break;
+ case LINESTRING:
+ if (result instanceof GeoJSONLineString) {
+ return result;
+ } else {
+ result = new GeoJSONLineString();
+ }
+ break;
+ case POLYGON:
+ if (result instanceof GeoJSONPolygon) {
+ return result;
+ } else {
+ result = new GeoJSONPolygon();
+ }
+ break;
+ case MULTI_POINT:
+ if (result instanceof GeoJSONMultiPoint) {
+ return result;
+ } else {
+ result = new GeoJSONMultiPoint();
+ }
+ break;
+ case MULTI_LINESTRING:
+ if (result instanceof GeoJSONMultiLineString) {
+ return result;
+ } else {
+ result = new GeoJSONMultiLineString();
+ }
+ break;
+ case MULTI_POLYGON:
+ if (result instanceof GeoJSONMultiPolygon) {
+ return result;
+ } else {
+ result = new GeoJSONMultiPolygon();
+ }
+ break;
+ case GEOMETRY_COLLECTION:
+ if (result instanceof GeoJSONGeometryCollection) {
+ return result;
+ } else {
+ result = new GeoJSONGeometryCollection();
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown type " + type);
+ }
+
+ if (object != null) {
+ result.setBbox(object.getBbox());
+ result.setCrs(object.getCrs());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Parse the bbox array.
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p JsonParser location MUST be on a START_ARRAY token.
+ * @return an array of double with a length of 4 or 6.
+ * @throws IOException
+ */
+ private static double[] parseBBoxArray(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ double[] bbox = new double[4];
+
+ int idx = 0;
+ // messages is array, loop until token equal to "]"
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ if (idx == 4) {
+ bbox = Arrays.copyOf(bbox, 6);
+ }
+ bbox[idx++] = p.getDoubleValue();
+ }
+
+ return bbox;
+ }
+
+ /**
+ * Parse a CRS Object.
+ * JsonParser location MUST be on a START_OBJECT token.
+ * @param p JsonParser location MUST be on a START_OBJECT token.
+ * @return GeoJSONCRS
+ * @throws IOException
+ */
+ private static GeoJSONCRS parseCRS(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_OBJECT);
+ GeoJSONCRS crs = new GeoJSONCRS();
+
+ while (p.nextToken() != JsonToken.END_OBJECT) {
+ String fieldName = p.getCurrentName();
+ if (TYPE.equals(fieldName)) {
+ crs.setType(p.nextTextValue());
+ } else if (PROPERTIES.equals(fieldName)) {
+ //object
+ p.nextToken();
+ while (p.nextToken() != JsonToken.END_OBJECT) {
+ crs.getProperties().put(p.getCurrentName(), p.getValueAsString());
+ }
+ }
+ }
+
+ return crs;
+ }
+
+ /**
+ * Parse a Coordinate.
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p JsonParser location MUST be on a START_ARRAY token.
+ * @return an array of double like [X,Y,(Z)]
+ * @throws IOException
+ */
+ private static double[] parsePoint(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ double[] pt = new double[2];
+
+ int idx = 0;
+ // messages is array, loop until token equal to "]"
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ if (idx == 2) {
+ pt = Arrays.copyOf(pt, 3);
+ }
+ pt[idx++] = p.getDoubleValue();
+ }
+ return pt;
+ }
+
+ /**
+ * Parse a LineString/MultiPoint.
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p JsonParser location MUST be on a START_ARRAY token.
+ * @return an array of double like [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+ * @throws IOException
+ */
+ private static double[][] parseLineString(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ List<double[]> line = new ArrayList<>();
+
+ // messages is array, loop until token equal to "]"
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ line.add(parsePoint(p));
+ }
+ return line.toArray(new double[line.size()][]);
+ }
+
+ /**
+ * Parse a List of LineString or Polygon.
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p JsonParser location MUST be on a START_ARRAY token.
+ * @return an array of double like
+ * [
+ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], ...
+ * ]
+ * @throws IOException
+ */
+ private static double[][][] parseMultiLineString(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ List<double[][]> lines = new ArrayList<>();
+
+ // messages is array, loop until token equal to "]"
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ lines.add(parseLineString(p));
+ }
+ return lines.toArray(new double[lines.size()][][]);
+ }
+
+ /**
+ * Parse a List of Polygons (list of list of LineString).
+ * JsonParser location MUST be on a START_ARRAY token.
+ * @param p JsonParser location MUST be on a START_ARRAY token.
+ * @return an array of double like
+ * [[
+ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+ * ],[
+ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+ * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+ * ], ... ]
+ * @throws IOException
+ */
+ private static double[][][][] parseMultiPolygon(JsonParser p) throws IOException {
+ assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+ List<double[][][]> polygons = new ArrayList<>();
+
+ // messages is array, loop until token equal to "]"
+ while (p.nextToken() != JsonToken.END_ARRAY) {
+ polygons.add(parseMultiLineString(p));
+ }
+ return polygons.toArray(new double[polygons.size()][][][]);
+ }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONTypes.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONTypes.java
new file mode 100644
index 0000000..cc580d9
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONTypes.java
@@ -0,0 +1,48 @@
+/*
+ * 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.utils;
+
+import org.apache.sis.util.Static;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+public final class GeoJSONTypes 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";
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONUtils.java
new file mode 100644
index 0000000..55a41ac
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONUtils.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.storage.geojson.utils;
+
+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.storage.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.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+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<IdentifiedType> 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 <T> 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> 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<? extends PropertyType> 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<Object> 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<AttributeType<?>> 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<String> 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<String> 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 :
+ * <ul>
+ * <li>Arrays</li>
+ * <li>Collection</li>
+ * <li>Numbers (Double, Float, Short, BigInteger, BigDecimal, integer, Long, Byte)</li>
+ * <li>Boolean</li>
+ * <li>String</li>
+ * </ul>
+ * @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<Class> 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/storage/geojson/utils/GeometryUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeometryUtils.java
new file mode 100644
index 0000000..b41748f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeometryUtils.java
@@ -0,0 +1,353 @@
+/*
+ * 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.utils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.sis.storage.geojson.binding.*;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import org.apache.sis.util.Static;
+import org.locationtech.jts.geom.*;
+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 final class GeometryUtils extends Static {
+
+ 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/storage/geojson/utils/LiteJsonLocation.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/LiteJsonLocation.java
new file mode 100644
index 0000000..1cb4636
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/LiteJsonLocation.java
@@ -0,0 +1,104 @@
+/*
+ * 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.utils;
+
+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/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..4a2b1c0
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.junit.Test;
+import org.opengis.util.FactoryException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
+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.storage.geojson.utils.FeatureTypeUtils;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.test.TestCase;
+
+import static org.junit.Assert.*;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+import org.opengis.util.GenericName;
+
+/**
+ * @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 boolean equalsIgnoreConvention(FeatureType type1, FeatureType type2){
+
+ if (type1 == type2) {
+ return true;
+ }
+
+ //check base properties
+ if (!Objects.equals(type1.getName(), type2.getName()) ||
+ !Objects.equals(type1.getDefinition(), type2.getDefinition()) ||
+ !Objects.equals(type1.getDesignation(), type2.getDesignation()) ||
+ !Objects.equals(type1.getDesignation(), type2.getDesignation()) ||
+ !Objects.equals(type1.isAbstract(), type2.isAbstract())){
+ return false;
+ }
+
+ //check super types
+ final Set<? extends FeatureType> super1 = type1.getSuperTypes();
+ final Set<? extends FeatureType> super2 = type2.getSuperTypes();
+ if(super1.size() != super2.size()) return false;
+ final Iterator<? extends FeatureType> site1 = super1.iterator();
+ final Iterator<? extends FeatureType> site2 = super2.iterator();
+ while(site1.hasNext()){
+ if(!equalsIgnoreConvention(site1.next(), site2.next())) return false;
+ }
+
+ //check properties
+ final Set<GenericName> visited = new HashSet<>();
+ for (PropertyType pt1 : type1.getProperties(true)) {
+ visited.add(pt1.getName());
+ if (AttributeConvention.contains(pt1.getName())) continue;
+ try {
+ final PropertyType pt2 = type2.getProperty(pt1.getName().toString());
+ if (!equalsIgnoreConvention(pt1, pt2)) return false;
+ } catch (PropertyNotFoundException ex) {
+ return false;
+ }
+ }
+
+ for (PropertyType pt2 : type2.getProperties(true)) {
+ if (AttributeConvention.contains(pt2.getName()) || visited.contains(pt2.getName())) continue;
+ try {
+ final PropertyType pt1 = type1.getProperty(pt2.getName().toString());
+ if (!equalsIgnoreConvention(pt1, pt2)) return false;
+ } catch (PropertyNotFoundException ex) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean equalsIgnoreConvention(PropertyType pt1, PropertyType pt2){
+ if(pt1 instanceof FeatureAssociationRole){
+ if(pt2 instanceof FeatureAssociationRole){
+ final FeatureAssociationRole far1 = (FeatureAssociationRole) pt1;
+ final FeatureAssociationRole far2 = (FeatureAssociationRole) pt2;
+
+ //check base properties
+ if (!Objects.equals(far1.getName(), far2.getName()) ||
+ !Objects.equals(far1.getDefinition(), far2.getDefinition()) ||
+ !Objects.equals(far1.getDesignation(), far2.getDesignation()) ||
+ !Objects.equals(far1.getDesignation(), far2.getDesignation())){
+ return false;
+ }
+
+ if(far1.getMinimumOccurs()!=far2.getMinimumOccurs()||
+ far1.getMaximumOccurs()!=far2.getMaximumOccurs()){
+ return false;
+ }
+
+ if(!equalsIgnoreConvention(far1.getValueType(), far2.getValueType())){
+ return false;
+ }
+
+ }else{
+ return false;
+ }
+ }else if(!pt1.equals(pt2)){
+ return false;
+ }
+ return true;
+ }
+
+}
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..b9e1831
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.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.DataStores;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.geojson.GeoJSONStore;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONObject;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+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.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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<Feature> 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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");
+
+ GeoJSONStore store = (GeoJSONStore) 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(AttributeConvention.GEOMETRY_PROPERTY).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(geomClass).setName(AttributeConvention.GEOMETRY_PROPERTY).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) {
+ for(PropertyType desc : expected.getProperties(true)){
+ PropertyType td = result.getProperty(desc.getName().toString());
+ assertNotNull(td);
+ if(td instanceof AttributeType){
+ assertEquals(((AttributeType) td).getValueClass(), ((AttributeType)desc).getValueClass());
+ }
+ }
+ }
+}
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..d4894c8
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java
@@ -0,0 +1,577 @@
+/*
+ * 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 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.geojson.GeoJSONProvider;
+import org.apache.sis.storage.geojson.GeoJSONStreamWriter;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.storage.geojson.GeoJSONStore;
+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 GeoJSONStore 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<Feature> stream = store.features(false)) {
+ final Iterator<Feature> 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");
+
+ GeoJSONStore 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<Feature> stream = store.features(false)) {
+ Iterator<Feature> 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");
+
+ GeoJSONStore 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<Feature> stream = store.features(false)) {
+ Iterator<Feature> ite = stream.iterator();
+ while (ite.hasNext()) {
+ Feature candidate = ite.next();
+ assertEquals(expected, candidate);
+ }
+ }
+ 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<? extends PropertyType> 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<size;i++){
+ cp.add(deepCopy(list.get(i)));
+ }
+ return cp;
+ }else if (candidate instanceof Map) {
+ final Map map = (Map)candidate;
+ final Map cp = new HashMap(map.size());
+ for(final Iterator<Map.Entry> 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<length; i++){
+ Array.set(cp, i, deepCopy(Array.get(candidate, i)));
+ }
+ }
+ return cp;
+ }
+
+ //could not copy
+ return candidate;
+ }
+
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java
new file mode 100644
index 0000000..1043c1c
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.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.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.storage.geojson.utils.LiteJsonLocation;
+import org.apache.sis.test.TestCase;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+public class LiteJsonLocationTest extends TestCase {
+
+ @Test
+ public void testEquality () throws URISyntaxException, IOException {
+ URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+ Path fcPath = Paths.get(fcFile.toURI());
+
+ JsonLocation streamLocation = null;
+ JsonLocation readerLocation = null;
+
+ //get Location from stream
+ try (InputStream stream = Files.newInputStream(fcPath);
+ JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+ streamLocation = moveAndReturnPos(parser);
+ }
+
+ //get Location from reader
+ try (BufferedReader reader = Files.newBufferedReader(fcPath, Charset.forName("UTF-8"));
+ JsonParser parser = GeoJSONParser.FACTORY.createParser(reader)) {
+ readerLocation = moveAndReturnPos(parser);
+ }
+
+ Assert.assertFalse(streamLocation.equals(readerLocation));
+ Assert.assertFalse(GeoJSONUtils.equals(streamLocation, readerLocation));
+
+ LiteJsonLocation liteStreamLocation = new LiteJsonLocation(streamLocation);
+ LiteJsonLocation liteReaderLocation = new LiteJsonLocation(readerLocation);
+
+ Assert.assertTrue(liteStreamLocation.equals(liteReaderLocation));
+
+ Assert.assertTrue(liteStreamLocation.equals(streamLocation));
+ Assert.assertTrue(liteStreamLocation.equals(readerLocation));
+
+ Assert.assertTrue(liteReaderLocation.equals(streamLocation));
+ Assert.assertTrue(liteReaderLocation.equals(readerLocation));
+
+ }
+
+
+ @Test
+ public void testBefore () throws URISyntaxException, IOException {
+ URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+ Path fcPath = Paths.get(fcFile.toURI());
+
+ //get Location from stream
+ try (InputStream stream = Files.newInputStream(fcPath);
+ JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+ parser.nextToken();
+
+ JsonLocation currentLocation = parser.getCurrentLocation();
+ LiteJsonLocation liteJsonLocation = new LiteJsonLocation(currentLocation);
+ Assert.assertFalse(liteJsonLocation.isBefore(currentLocation));
+
+ parser.nextToken();
+ currentLocation = parser.getCurrentLocation();
+ Assert.assertTrue(liteJsonLocation.isBefore(currentLocation));
+ }
+ }
+
+
+ private JsonLocation moveAndReturnPos(JsonParser parser) throws IOException {
+ parser.nextToken();
+ parser.nextToken();
+ parser.nextToken();
+ parser.nextToken();
+ return parser.getCurrentLocation();
+ }
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java b/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java
new file mode 100644
index 0000000..175eaee
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java
@@ -0,0 +1,34 @@
+/*
+ * 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.test.suite;
+
+import org.apache.sis.test.TestSuite;
+import org.junit.runners.Suite;
+import org.junit.BeforeClass;
+
+
+/**
+ * All tests from the {@code sis-geojson} module, in rough dependency order.
+ */
+@Suite.SuiteClasses({
+ org.apache.sis.internal.storage.geojson.FeatureTypeUtilsTest.class,
+ org.apache.sis.internal.storage.geojson.GeoJSONReadTest.class,
+ org.apache.sis.internal.storage.geojson.GeoJSONWriteTest.class,
+ org.apache.sis.internal.storage.geojson.LiteJsonLocationTest.class
+})
+public final strictfp class GeoJSONTestSuite extends TestSuite {
+}
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json
new file mode 100644
index 0000000..4ca0f62
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json
@@ -0,0 +1,7 @@
+{
+ "type":"FeatureCollection"
+ ,"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}
+ ,"features":[
+ {"type":"Feature","id":"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]]}}
+ ,{"type":"Feature","id":"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]]}}
+]}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json
new file mode 100644
index 0000000..248f582
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json
@@ -0,0 +1,69 @@
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -80.72487831115721,
+ 35.26545403190955
+ ],
+ [
+ -80.72135925292969,
+ 35.26727607954368
+ ],
+ [
+ -80.71517944335938,
+ 35.26769654625573
+ ],
+ [
+ -80.7125186920166,
+ 35.27035945142482
+ ],
+ [
+ -80.70857048034668,
+ 35.268257165144064
+ ],
+ [
+ -80.70479393005371,
+ 35.268397319259996
+ ],
+ [
+ -80.70324897766113,
+ 35.26503355355979
+ ],
+ [
+ -80.71088790893555,
+ 35.2553619492954
+ ],
+ [
+ -80.71681022644043,
+ 35.2553619492954
+ ],
+ [
+ -80.7150936126709,
+ 35.26054831539319
+ ],
+ [
+ -80.71869850158691,
+ 35.26026797976481
+ ],
+ [
+ -80.72032928466797,
+ 35.26061839914875
+ ],
+ [
+ -80.72264671325684,
+ 35.26033806376283
+ ],
+ [
+ -80.72487831115721,
+ 35.26545403190955
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "name": "Plaza Road Park"
+ }
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json
new file mode 100644
index 0000000..e4d7ae4
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json
@@ -0,0 +1,158 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -80.87088507656375,
+ 35.21515162500578
+ ]
+ },
+ "properties": {
+ "name": "ABBOTT NEIGHBORHOOD PARK",
+ "address": "1300 SPRUCE ST"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -80.83775386582222,
+ 35.24980190252168
+ ]
+ },
+ "properties": {
+ "name": "DOUBLE OAKS CENTER",
+ "address": "1326 WOODWARD AV"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -80.83827000459532,
+ 35.25674709224663
+ ]
+ },
+ "properties": {
+ "name": "DOUBLE OAKS NEIGHBORHOOD PARK",
+ "address": "2605 DOUBLE OAKS RD"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -80.83697759172735,
+ 35.25751734669229
+ ]
+ },
+ "properties": {
+ "name": "DOUBLE OAKS POOL",
+ "address": "1200 NEWLAND RD"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -80.81647652154736,
+ 35.40148708491418
+ ]
+ },
+ "properties": {
+ "name": "DAVID B. WAYMER FLYING REGIONAL PARK",
+ "address": "15401 HOLBROOKS RD"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -80.83556459443902,
+ 35.39917224760999
+ ]
+ },
+ "properties": {
+ "name": "DAVID B. WAYMER COMMUNITY PARK",
+ "address": "302 HOLBROOKS RD"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -80.72487831115721,
+ 35.26545403190955
+ ],
+ [
+ -80.72135925292969,
+ 35.26727607954368
+ ],
+ [
+ -80.71517944335938,
+ 35.26769654625573
+ ],
+ [
+ -80.7125186920166,
+ 35.27035945142482
+ ],
+ [
+ -80.70857048034668,
+ 35.268257165144064
+ ],
+ [
+ -80.70479393005371,
+ 35.268397319259996
+ ],
+ [
+ -80.70324897766113,
+ 35.26503355355979
+ ],
+ [
+ -80.71088790893555,
+ 35.2553619492954
+ ],
+ [
+ -80.71681022644043,
+ 35.2553619492954
+ ],
+ [
+ -80.7150936126709,
+ 35.26054831539319
+ ],
+ [
+ -80.71869850158691,
+ 35.26026797976481
+ ],
+ [
+ -80.72032928466797,
+ 35.26061839914875
+ ],
+ [
+ -80.72264671325684,
+ 35.26033806376283
+ ],
+ [
+ -80.72487831115721,
+ 35.26545403190955
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "name": "Plaza Road Park"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties
new file mode 100644
index 0000000..f638ee6
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties
@@ -0,0 +1,8 @@
+# WKT geometries for test purpose
+point=POINT(-105.01621 39.57422)
+multipoint=MULTIPOINT((-105.01621 39.57422),(-80.6665134 35.0539943))
+linestring=LINESTRING(-101.744384765625 39.32155002466662,-101.5521240234375 39.330048552942415,-101.40380859375 39.330048552942415,-101.33239746093749 39.364032338047984,-101.041259765625 39.36827914916011,-100.975341796875 39.30454987014581,-100.9149169921875 39.24501680713314,-100.843505859375 39.16414104768742,-100.8050537109375 39.104488809440475,-100.491943359375 39.10022600175347,-100.43701171875 39.095962936305476,-100.338134765625 39.095962936305476,-100.1953125 39.0277188402116 [...]
+multilinestring=MULTILINESTRING((-105.0214433670044 39.57805759162015,-105.02150774002075 39.57780951131517,-105.02157211303711 39.57749527498758,-105.02157211303711 39.57716449836683,-105.02157211303711 39.57703218727656,-105.02152919769287 39.57678410330158),(-105.01989841461182 39.574997872470774,-105.01959800720215 39.57489863607502,-105.01906156539916 39.57478286010041),(-105.01717329025269 39.5744024519653,-105.01698017120361 39.574385912433804,-105.0166368484497 39.574385912433804 [...]
+polygon=POLYGON((-84.32281494140625 34.9895035675793,-84.29122924804688 35.21981940793435,-84.24041748046875 35.25459097465022,-84.22531127929688 35.266925688950074,-84.20745849609375 35.26580442886754,-84.19921875 35.24674063355999,-84.16213989257812 35.24113278166642,-84.12368774414062 35.24898366572645,-84.09072875976562 35.24898366572645,-84.08798217773438 35.264683153268116,-84.04266357421875 35.27701633139884,-84.03030395507812 35.291589484566124,-84.0234375 35.306160014550784,-84. [...]
+multipolygon=MULTIPOLYGON(((-84.32281494140625 34.9895035675793,-84.29122924804688 35.21981940793435,-84.24041748046875 35.25459097465022,-84.22531127929688 35.266925688950074,-84.20745849609375 35.26580442886754,-84.19921875 35.24674063355999,-84.16213989257812 35.24113278166642,-84.12368774414062 35.24898366572645,-84.09072875976562 35.24898366572645,-84.08798217773438 35.264683153268116,-84.04266357421875 35.27701633139884,-84.03030395507812 35.291589484566124,-84.0234375 35.306160014 [...]
+geometrycollection=GEOMETRYCOLLECTION(POINT(-80.66080570220947 35.04939206472683),POLYGON((-80.66458225250244 35.04496519190309,-80.66344499588013 35.04603679820616,-80.66258668899536 35.045580049697556,-80.66387414932251 35.044280059194946,-80.66458225250244 35.04496519190309)),LINESTRING(-80.66237211227417 35.05950973022538,-80.66269397735596 35.0592638296087,-80.66284418106079 35.05893010615862,-80.66308021545409 35.05833291342246,-80.66359519958496 35.057753281001425,-80.663874149322 [...]
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json
new file mode 100644
index 0000000..6287457
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json
@@ -0,0 +1,188 @@
+{
+ "type": "GeometryCollection",
+ "geometries": [
+ {
+ "type": "Point",
+ "coordinates": [
+ -80.66080570220947,
+ 35.04939206472683
+ ]
+ },
+ {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -80.66458225250244,
+ 35.04496519190309
+ ],
+ [
+ -80.66344499588013,
+ 35.04603679820616
+ ],
+ [
+ -80.66258668899536,
+ 35.045580049697556
+ ],
+ [
+ -80.66387414932251,
+ 35.044280059194946
+ ],
+ [
+ -80.66458225250244,
+ 35.04496519190309
+ ]
+ ]
+ ]
+ },
+ {
+ "type": "LineString",
+ "coordinates": [
+ [
+ -80.66237211227417,
+ 35.05950973022538
+ ],
+ [
+ -80.66269397735596,
+ 35.0592638296087
+ ],
+ [
+ -80.66284418106079,
+ 35.05893010615862
+ ],
+ [
+ -80.66308021545409,
+ 35.05833291342246
+ ],
+ [
+ -80.66359519958496,
+ 35.057753281001425
+ ],
+ [
+ -80.66387414932251,
+ 35.05740198662245
+ ],
+ [
+ -80.66441059112549,
+ 35.05703312589789
+ ],
+ [
+ -80.66486120223999,
+ 35.056787217822475
+ ],
+ [
+ -80.66541910171509,
+ 35.05650617911516
+ ],
+ [
+ -80.66563367843628,
+ 35.05631296444281
+ ],
+ [
+ -80.66601991653441,
+ 35.055891403570705
+ ],
+ [
+ -80.66619157791138,
+ 35.05545227534804
+ ],
+ [
+ -80.66619157791138,
+ 35.05517123204622
+ ],
+ [
+ -80.66625595092773,
+ 35.05489018777713
+ ],
+ [
+ -80.6662130355835,
+ 35.054222703761525
+ ],
+ [
+ -80.6662130355835,
+ 35.05392409072499
+ ],
+ [
+ -80.66595554351807,
+ 35.05290528508858
+ ],
+ [
+ -80.66569805145262,
+ 35.052044560077285
+ ],
+ [
+ -80.66550493240356,
+ 35.0514824490509
+ ],
+ [
+ -80.665762424469,
+ 35.05048117920187
+ ],
+ [
+ -80.66617012023926,
+ 35.04972582715769
+ ],
+ [
+ -80.66651344299316,
+ 35.049286665781096
+ ],
+ [
+ -80.66692113876343,
+ 35.0485313026898
+ ],
+ [
+ -80.66700696945189,
+ 35.048215102112344
+ ],
+ [
+ -80.66707134246826,
+ 35.04777593261294
+ ],
+ [
+ -80.66704988479614,
+ 35.04738946150025
+ ],
+ [
+ -80.66696405410767,
+ 35.04698542156371
+ ],
+ [
+ -80.66681385040283,
+ 35.046353007216055
+ ],
+ [
+ -80.66659927368164,
+ 35.04596652937105
+ ],
+ [
+ -80.66640615463257,
+ 35.04561518428889
+ ],
+ [
+ -80.6659984588623,
+ 35.045193568195565
+ ],
+ [
+ -80.66552639007568,
+ 35.044877354697526
+ ],
+ [
+ -80.6649899482727,
+ 35.04454357245502
+ ],
+ [
+ -80.66449642181396,
+ 35.04417465365292
+ ],
+ [
+ -80.66385269165039,
+ 35.04387600387859
+ ],
+ [
+ -80.66303730010986,
+ 35.043717894732545
+ ]
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json
new file mode 100644
index 0000000..a7a82b8
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json
@@ -0,0 +1,109 @@
+{
+ "type": "LineString",
+ "coordinates": [
+ [
+ -101.744384765625,
+ 39.32155002466662
+ ],
+ [
+ -101.5521240234375,
+ 39.330048552942415
+ ],
+ [
+ -101.40380859375,
+ 39.330048552942415
+ ],
+ [
+ -101.33239746093749,
+ 39.364032338047984
+ ],
+ [
+ -101.041259765625,
+ 39.36827914916011
+ ],
+ [
+ -100.975341796875,
+ 39.30454987014581
+ ],
+ [
+ -100.9149169921875,
+ 39.24501680713314
+ ],
+ [
+ -100.843505859375,
+ 39.16414104768742
+ ],
+ [
+ -100.8050537109375,
+ 39.104488809440475
+ ],
+ [
+ -100.491943359375,
+ 39.10022600175347
+ ],
+ [
+ -100.43701171875,
+ 39.095962936305476
+ ],
+ [
+ -100.338134765625,
+ 39.095962936305476
+ ],
+ [
+ -100.1953125,
+ 39.027718840211605
+ ],
+ [
+ -100.008544921875,
+ 39.01064750994083
+ ],
+ [
+ -99.86572265625,
+ 39.00211029922512
+ ],
+ [
+ -99.6844482421875,
+ 38.97222194853654
+ ],
+ [
+ -99.51416015625,
+ 38.929502416386605
+ ],
+ [
+ -99.38232421875,
+ 38.92095542046727
+ ],
+ [
+ -99.3218994140625,
+ 38.89530825492018
+ ],
+ [
+ -99.1131591796875,
+ 38.86965182408357
+ ],
+ [
+ -99.0802001953125,
+ 38.85682013474361
+ ],
+ [
+ -98.82202148437499,
+ 38.85682013474361
+ ],
+ [
+ -98.44848632812499,
+ 38.84826438869913
+ ],
+ [
+ -98.20678710937499,
+ 38.84826438869913
+ ],
+ [
+ -98.02001953125,
+ 38.8782049970615
+ ],
+ [
+ -97.635498046875,
+ 38.87392853923629
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json
new file mode 100644
index 0000000..40c662c
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json
@@ -0,0 +1,24 @@
+
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -105.01621,
+ 39.57422
+ ]
+ },
+ "properties": {
+ "size": 853555090789
+ }
+ }],
+ "crs": {
+ "type": "name",
+ "properties": {
+ "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+ }
+ }
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json
new file mode 100644
index 0000000..8f23c20
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json
@@ -0,0 +1,85 @@
+{
+ "type": "MultiLineString",
+ "coordinates": [
+ [
+ [
+ -105.0214433670044,
+ 39.57805759162015
+ ],
+ [
+ -105.02150774002075,
+ 39.57780951131517
+ ],
+ [
+ -105.02157211303711,
+ 39.57749527498758
+ ],
+ [
+ -105.02157211303711,
+ 39.57716449836683
+ ],
+ [
+ -105.02157211303711,
+ 39.57703218727656
+ ],
+ [
+ -105.02152919769287,
+ 39.57678410330158
+ ]
+ ],
+ [
+ [
+ -105.01989841461182,
+ 39.574997872470774
+ ],
+ [
+ -105.01959800720215,
+ 39.57489863607502
+ ],
+ [
+ -105.01906156539916,
+ 39.57478286010041
+ ]
+ ],
+ [
+ [
+ -105.01717329025269,
+ 39.5744024519653
+ ],
+ [
+ -105.01698017120361,
+ 39.574385912433804
+ ],
+ [
+ -105.0166368484497,
+ 39.574385912433804
+ ],
+ [
+ -105.01650810241699,
+ 39.5744024519653
+ ],
+ [
+ -105.0159502029419,
+ 39.574270135602866
+ ]
+ ],
+ [
+ [
+ -105.0142765045166,
+ 39.57397242286402
+ ],
+ [
+ -105.01412630081175,
+ 39.57403858136094
+ ],
+ [
+ -105.0138258934021,
+ 39.57417089816531
+ ],
+ [
+ -105.01331090927124,
+ 39.57445207053608
+ ]
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json
new file mode 100644
index 0000000..63a7143
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json
@@ -0,0 +1,13 @@
+{
+ "type": "MultiPoint",
+ "coordinates": [
+ [
+ -105.01621,
+ 39.57422
+ ],
+ [
+ -80.6665134,
+ 35.0539943
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json
new file mode 100644
index 0000000..f25f85b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json
@@ -0,0 +1,815 @@
+{
+ "type": "MultiPolygon",
+ "coordinates": [
+ [
+ [
+ [
+ -84.32281494140625,
+ 34.9895035675793
+ ],
+ [
+ -84.29122924804688,
+ 35.21981940793435
+ ],
+ [
+ -84.24041748046875,
+ 35.25459097465022
+ ],
+ [
+ -84.22531127929688,
+ 35.266925688950074
+ ],
+ [
+ -84.20745849609375,
+ 35.26580442886754
+ ],
+ [
+ -84.19921875,
+ 35.24674063355999
+ ],
+ [
+ -84.16213989257812,
+ 35.24113278166642
+ ],
+ [
+ -84.12368774414062,
+ 35.24898366572645
+ ],
+ [
+ -84.09072875976562,
+ 35.24898366572645
+ ],
+ [
+ -84.08798217773438,
+ 35.264683153268116
+ ],
+ [
+ -84.04266357421875,
+ 35.27701633139884
+ ],
+ [
+ -84.03030395507812,
+ 35.291589484566124
+ ],
+ [
+ -84.0234375,
+ 35.306160014550784
+ ],
+ [
+ -84.03305053710936,
+ 35.32745068492882
+ ],
+ [
+ -84.03579711914062,
+ 35.34313496028189
+ ],
+ [
+ -84.03579711914062,
+ 35.348735749472546
+ ],
+ [
+ -84.01657104492188,
+ 35.35545618392078
+ ],
+ [
+ -84.01107788085938,
+ 35.37337460834958
+ ],
+ [
+ -84.00970458984374,
+ 35.39128905521763
+ ],
+ [
+ -84.01931762695312,
+ 35.41479572901859
+ ],
+ [
+ -84.00283813476562,
+ 35.429344044107154
+ ],
+ [
+ -83.93692016601562,
+ 35.47409160773029
+ ],
+ [
+ -83.91220092773438,
+ 35.47632833265728
+ ],
+ [
+ -83.88885498046875,
+ 35.504282143299655
+ ],
+ [
+ -83.88473510742186,
+ 35.516578738902936
+ ],
+ [
+ -83.8751220703125,
+ 35.52104976129943
+ ],
+ [
+ -83.85314941406249,
+ 35.52104976129943
+ ],
+ [
+ -83.82843017578125,
+ 35.52104976129943
+ ],
+ [
+ -83.8092041015625,
+ 35.53446133418443
+ ],
+ [
+ -83.80233764648438,
+ 35.54116627999813
+ ],
+ [
+ -83.76800537109374,
+ 35.56239491058853
+ ],
+ [
+ -83.7432861328125,
+ 35.56239491058853
+ ],
+ [
+ -83.71994018554688,
+ 35.56239491058853
+ ],
+ [
+ -83.67050170898438,
+ 35.569097520776054
+ ],
+ [
+ -83.6334228515625,
+ 35.570214567965984
+ ],
+ [
+ -83.61007690429688,
+ 35.576916524038616
+ ],
+ [
+ -83.59634399414061,
+ 35.574682600980914
+ ],
+ [
+ -83.5894775390625,
+ 35.55904339525896
+ ],
+ [
+ -83.55239868164062,
+ 35.56574628576276
+ ],
+ [
+ -83.49746704101562,
+ 35.563512051219696
+ ],
+ [
+ -83.47000122070312,
+ 35.586968406786475
+ ],
+ [
+ -83.4466552734375,
+ 35.60818490437746
+ ],
+ [
+ -83.37936401367188,
+ 35.63609277863135
+ ],
+ [
+ -83.35739135742188,
+ 35.65618041632016
+ ],
+ [
+ -83.32305908203124,
+ 35.66622234103479
+ ],
+ [
+ -83.3148193359375,
+ 35.65394870599763
+ ],
+ [
+ -83.29971313476561,
+ 35.660643649881614
+ ],
+ [
+ -83.28598022460938,
+ 35.67180064238771
+ ],
+ [
+ -83.26126098632811,
+ 35.6907639509368
+ ],
+ [
+ -83.25714111328125,
+ 35.69968630125201
+ ],
+ [
+ -83.25576782226562,
+ 35.715298012125295
+ ],
+ [
+ -83.23516845703125,
+ 35.72310272092263
+ ],
+ [
+ -83.19808959960936,
+ 35.72756221127198
+ ],
+ [
+ -83.16238403320312,
+ 35.753199435570316
+ ],
+ [
+ -83.15826416015625,
+ 35.76322914549896
+ ],
+ [
+ -83.10333251953125,
+ 35.76991491635478
+ ],
+ [
+ -83.08685302734375,
+ 35.7843988251953
+ ],
+ [
+ -83.0511474609375,
+ 35.787740890986576
+ ],
+ [
+ -83.01681518554688,
+ 35.78328477203738
+ ],
+ [
+ -83.001708984375,
+ 35.77882840327371
+ ],
+ [
+ -82.96737670898438,
+ 35.793310688351724
+ ],
+ [
+ -82.94540405273438,
+ 35.820040281161
+ ],
+ [
+ -82.9193115234375,
+ 35.85121343450061
+ ],
+ [
+ -82.9083251953125,
+ 35.86902116501695
+ ],
+ [
+ -82.90557861328125,
+ 35.87792352995116
+ ],
+ [
+ -82.91244506835938,
+ 35.92353244718235
+ ],
+ [
+ -82.88360595703125,
+ 35.94688293218141
+ ],
+ [
+ -82.85614013671875,
+ 35.951329861522666
+ ],
+ [
+ -82.8424072265625,
+ 35.94243575255426
+ ],
+ [
+ -82.825927734375,
+ 35.92464453144099
+ ],
+ [
+ -82.80670166015625,
+ 35.927980690382704
+ ],
+ [
+ -82.80532836914062,
+ 35.94243575255426
+ ],
+ [
+ -82.77923583984375,
+ 35.97356075349624
+ ],
+ [
+ -82.78060913085938,
+ 35.99245209055831
+ ],
+ [
+ -82.76138305664062,
+ 36.00356252895066
+ ],
+ [
+ -82.69546508789062,
+ 36.04465753921525
+ ],
+ [
+ -82.64465332031249,
+ 36.060201412392914
+ ],
+ [
+ -82.61306762695312,
+ 36.060201412392914
+ ],
+ [
+ -82.60620117187499,
+ 36.033552893400376
+ ],
+ [
+ -82.60620117187499,
+ 35.991340960635405
+ ],
+ [
+ -82.60620117187499,
+ 35.97911749857497
+ ],
+ [
+ -82.5787353515625,
+ 35.96133453736691
+ ],
+ [
+ -82.5677490234375,
+ 35.951329861522666
+ ],
+ [
+ -82.53067016601562,
+ 35.97244935753683
+ ],
+ [
+ -82.46475219726562,
+ 36.006895355244666
+ ],
+ [
+ -82.41668701171875,
+ 36.070192281208456
+ ],
+ [
+ -82.37960815429686,
+ 36.10126686921446
+ ],
+ [
+ -82.35488891601562,
+ 36.117908916563685
+ ],
+ [
+ -82.34115600585936,
+ 36.113471382052175
+ ],
+ [
+ -82.29583740234375,
+ 36.13343831245866
+ ],
+ [
+ -82.26287841796874,
+ 36.13565654678543
+ ],
+ [
+ -82.23403930664062,
+ 36.13565654678543
+ ],
+ [
+ -82.2216796875,
+ 36.154509006695
+ ],
+ [
+ -82.20382690429688,
+ 36.15561783381855
+ ],
+ [
+ -82.19009399414062,
+ 36.144528857027744
+ ],
+ [
+ -82.15438842773438,
+ 36.15007354140755
+ ],
+ [
+ -82.14065551757812,
+ 36.134547437460064
+ ],
+ [
+ -82.1337890625,
+ 36.116799556445024
+ ],
+ [
+ -82.12142944335938,
+ 36.10570509327921
+ ],
+ [
+ -82.08984375,
+ 36.10792411128649
+ ],
+ [
+ -82.05276489257811,
+ 36.12678323326429
+ ],
+ [
+ -82.03628540039062,
+ 36.12900165569652
+ ],
+ [
+ -81.91268920898438,
+ 36.29409768373033
+ ],
+ [
+ -81.89071655273438,
+ 36.30959215409138
+ ],
+ [
+ -81.86325073242188,
+ 36.33504067209607
+ ],
+ [
+ -81.83029174804688,
+ 36.34499652561904
+ ],
+ [
+ -81.80145263671875,
+ 36.35605709240176
+ ],
+ [
+ -81.77947998046874,
+ 36.34610265300638
+ ],
+ [
+ -81.76162719726562,
+ 36.33835943134047
+ ],
+ [
+ -81.73690795898438,
+ 36.33835943134047
+ ],
+ [
+ -81.71905517578125,
+ 36.33835943134047
+ ],
+ [
+ -81.70669555664062,
+ 36.33504067209607
+ ],
+ [
+ -81.70669555664062,
+ 36.342784223707234
+ ],
+ [
+ -81.72317504882812,
+ 36.357163062654365
+ ],
+ [
+ -81.73278808593749,
+ 36.379279167407965
+ ],
+ [
+ -81.73690795898438,
+ 36.40028364332352
+ ],
+ [
+ -81.73690795898438,
+ 36.41354670392876
+ ],
+ [
+ -81.72454833984374,
+ 36.423492513472326
+ ],
+ [
+ -81.71768188476562,
+ 36.445589751779174
+ ],
+ [
+ -81.69845581054688,
+ 36.47541104282962
+ ],
+ [
+ -81.69845581054688,
+ 36.51073994146672
+ ],
+ [
+ -81.705322265625,
+ 36.53060536411363
+ ],
+ [
+ -81.69158935546875,
+ 36.55929085774001
+ ],
+ [
+ -81.68060302734375,
+ 36.56480607840351
+ ],
+ [
+ -81.68197631835938,
+ 36.58686302344181
+ ],
+ [
+ -81.04202270507812,
+ 36.56370306576917
+ ],
+ [
+ -80.74264526367186,
+ 36.561496993252575
+ ],
+ [
+ -79.89120483398438,
+ 36.54053616262899
+ ],
+ [
+ -78.68408203124999,
+ 36.53943280355122
+ ],
+ [
+ -77.88345336914062,
+ 36.54053616262899
+ ],
+ [
+ -76.91665649414062,
+ 36.54163950596125
+ ],
+ [
+ -76.91665649414062,
+ 36.55046568575947
+ ],
+ [
+ -76.31103515625,
+ 36.551568887374
+ ],
+ [
+ -75.79605102539062,
+ 36.54936246839778
+ ],
+ [
+ -75.6298828125,
+ 36.07574221562703
+ ],
+ [
+ -75.4925537109375,
+ 35.82226734114509
+ ],
+ [
+ -75.3936767578125,
+ 35.639441068973916
+ ],
+ [
+ -75.41015624999999,
+ 35.43829554739668
+ ],
+ [
+ -75.43212890625,
+ 35.263561862152095
+ ],
+ [
+ -75.487060546875,
+ 35.18727767598896
+ ],
+ [
+ -75.5914306640625,
+ 35.17380831799959
+ ],
+ [
+ -75.9210205078125,
+ 35.04798673426734
+ ],
+ [
+ -76.17919921875,
+ 34.867904962568744
+ ],
+ [
+ -76.41540527343749,
+ 34.62868797377061
+ ],
+ [
+ -76.4593505859375,
+ 34.57442951865274
+ ],
+ [
+ -76.53076171875,
+ 34.53371242139567
+ ],
+ [
+ -76.5911865234375,
+ 34.551811369170494
+ ],
+ [
+ -76.651611328125,
+ 34.615126683462194
+ ],
+ [
+ -76.761474609375,
+ 34.63320791137959
+ ],
+ [
+ -77.069091796875,
+ 34.59704151614417
+ ],
+ [
+ -77.376708984375,
+ 34.45674800347809
+ ],
+ [
+ -77.5909423828125,
+ 34.3207552752374
+ ],
+ [
+ -77.8326416015625,
+ 33.97980872872457
+ ],
+ [
+ -77.9150390625,
+ 33.80197351806589
+ ],
+ [
+ -77.9754638671875,
+ 33.73804486328907
+ ],
+ [
+ -78.11279296875,
+ 33.8521697014074
+ ],
+ [
+ -78.2830810546875,
+ 33.8521697014074
+ ],
+ [
+ -78.4808349609375,
+ 33.815666308702774
+ ],
+ [
+ -79.6728515625,
+ 34.8047829195724
+ ],
+ [
+ -80.782470703125,
+ 34.836349990763864
+ ],
+ [
+ -80.782470703125,
+ 34.91746688928252
+ ],
+ [
+ -80.9307861328125,
+ 35.092945313732635
+ ],
+ [
+ -81.0516357421875,
+ 35.02999636902566
+ ],
+ [
+ -81.0516357421875,
+ 35.05248370662468
+ ],
+ [
+ -81.0516357421875,
+ 35.137879119634185
+ ],
+ [
+ -82.3150634765625,
+ 35.19625600786368
+ ],
+ [
+ -82.3590087890625,
+ 35.19625600786368
+ ],
+ [
+ -82.40295410156249,
+ 35.22318504970181
+ ],
+ [
+ -82.4688720703125,
+ 35.16931803601131
+ ],
+ [
+ -82.6885986328125,
+ 35.1154153142536
+ ],
+ [
+ -82.781982421875,
+ 35.06147690849717
+ ],
+ [
+ -83.1060791015625,
+ 35.003003395276714
+ ],
+ [
+ -83.616943359375,
+ 34.99850370014629
+ ],
+ [
+ -84.05639648437499,
+ 34.985003130171066
+ ],
+ [
+ -84.22119140625,
+ 34.985003130171066
+ ],
+ [
+ -84.32281494140625,
+ 34.9895035675793
+ ]
+ ],
+ [
+ [
+ -75.69030761718749,
+ 35.74205383068037
+ ],
+ [
+ -75.5914306640625,
+ 35.74205383068037
+ ],
+ [
+ -75.5419921875,
+ 35.585851593232356
+ ],
+ [
+ -75.56396484375,
+ 35.32633026307483
+ ],
+ [
+ -75.69030761718749,
+ 35.285984736065735
+ ],
+ [
+ -75.970458984375,
+ 35.16482750605027
+ ],
+ [
+ -76.2066650390625,
+ 34.994003757575776
+ ],
+ [
+ -76.300048828125,
+ 35.02999636902566
+ ],
+ [
+ -76.409912109375,
+ 35.07946034047981
+ ],
+ [
+ -76.5252685546875,
+ 35.10642805736423
+ ],
+ [
+ -76.4208984375,
+ 35.25907654252574
+ ],
+ [
+ -76.3385009765625,
+ 35.294952147406576
+ ],
+ [
+ -76.0858154296875,
+ 35.29943548054543
+ ],
+ [
+ -75.948486328125,
+ 35.44277092585766
+ ],
+ [
+ -75.8660888671875,
+ 35.53669637839501
+ ],
+ [
+ -75.772705078125,
+ 35.567980458012094
+ ],
+ [
+ -75.706787109375,
+ 35.634976650677295
+ ],
+ [
+ -75.706787109375,
+ 35.74205383068037
+ ],
+ [
+ -75.69030761718749,
+ 35.74205383068037
+ ]
+ ]
+ ],
+ [
+ [
+ [
+ -109.0283203125,
+ 36.98500309285596
+ ],
+ [
+ -109.0283203125,
+ 40.97989806962013
+ ],
+ [
+ -102.06298828125,
+ 40.97989806962013
+ ],
+ [
+ -102.06298828125,
+ 37.00255267215955
+ ],
+ [
+ -109.0283203125,
+ 36.98500309285596
+ ]
+ ]
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json
new file mode 100644
index 0000000..9b8d23b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json
@@ -0,0 +1,7 @@
+{
+ "type": "Point",
+ "coordinates": [
+ -105.01621,
+ 39.57422
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json
new file mode 100644
index 0000000..77bc6fc
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json
@@ -0,0 +1,789 @@
+{
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -84.32281494140625,
+ 34.9895035675793
+ ],
+ [
+ -84.29122924804688,
+ 35.21981940793435
+ ],
+ [
+ -84.24041748046875,
+ 35.25459097465022
+ ],
+ [
+ -84.22531127929688,
+ 35.266925688950074
+ ],
+ [
+ -84.20745849609375,
+ 35.26580442886754
+ ],
+ [
+ -84.19921875,
+ 35.24674063355999
+ ],
+ [
+ -84.16213989257812,
+ 35.24113278166642
+ ],
+ [
+ -84.12368774414062,
+ 35.24898366572645
+ ],
+ [
+ -84.09072875976562,
+ 35.24898366572645
+ ],
+ [
+ -84.08798217773438,
+ 35.264683153268116
+ ],
+ [
+ -84.04266357421875,
+ 35.27701633139884
+ ],
+ [
+ -84.03030395507812,
+ 35.291589484566124
+ ],
+ [
+ -84.0234375,
+ 35.306160014550784
+ ],
+ [
+ -84.03305053710936,
+ 35.32745068492882
+ ],
+ [
+ -84.03579711914062,
+ 35.34313496028189
+ ],
+ [
+ -84.03579711914062,
+ 35.348735749472546
+ ],
+ [
+ -84.01657104492188,
+ 35.35545618392078
+ ],
+ [
+ -84.01107788085938,
+ 35.37337460834958
+ ],
+ [
+ -84.00970458984374,
+ 35.39128905521763
+ ],
+ [
+ -84.01931762695312,
+ 35.41479572901859
+ ],
+ [
+ -84.00283813476562,
+ 35.429344044107154
+ ],
+ [
+ -83.93692016601562,
+ 35.47409160773029
+ ],
+ [
+ -83.91220092773438,
+ 35.47632833265728
+ ],
+ [
+ -83.88885498046875,
+ 35.504282143299655
+ ],
+ [
+ -83.88473510742186,
+ 35.516578738902936
+ ],
+ [
+ -83.8751220703125,
+ 35.52104976129943
+ ],
+ [
+ -83.85314941406249,
+ 35.52104976129943
+ ],
+ [
+ -83.82843017578125,
+ 35.52104976129943
+ ],
+ [
+ -83.8092041015625,
+ 35.53446133418443
+ ],
+ [
+ -83.80233764648438,
+ 35.54116627999813
+ ],
+ [
+ -83.76800537109374,
+ 35.56239491058853
+ ],
+ [
+ -83.7432861328125,
+ 35.56239491058853
+ ],
+ [
+ -83.71994018554688,
+ 35.56239491058853
+ ],
+ [
+ -83.67050170898438,
+ 35.569097520776054
+ ],
+ [
+ -83.6334228515625,
+ 35.570214567965984
+ ],
+ [
+ -83.61007690429688,
+ 35.576916524038616
+ ],
+ [
+ -83.59634399414061,
+ 35.574682600980914
+ ],
+ [
+ -83.5894775390625,
+ 35.55904339525896
+ ],
+ [
+ -83.55239868164062,
+ 35.56574628576276
+ ],
+ [
+ -83.49746704101562,
+ 35.563512051219696
+ ],
+ [
+ -83.47000122070312,
+ 35.586968406786475
+ ],
+ [
+ -83.4466552734375,
+ 35.60818490437746
+ ],
+ [
+ -83.37936401367188,
+ 35.63609277863135
+ ],
+ [
+ -83.35739135742188,
+ 35.65618041632016
+ ],
+ [
+ -83.32305908203124,
+ 35.66622234103479
+ ],
+ [
+ -83.3148193359375,
+ 35.65394870599763
+ ],
+ [
+ -83.29971313476561,
+ 35.660643649881614
+ ],
+ [
+ -83.28598022460938,
+ 35.67180064238771
+ ],
+ [
+ -83.26126098632811,
+ 35.6907639509368
+ ],
+ [
+ -83.25714111328125,
+ 35.69968630125201
+ ],
+ [
+ -83.25576782226562,
+ 35.715298012125295
+ ],
+ [
+ -83.23516845703125,
+ 35.72310272092263
+ ],
+ [
+ -83.19808959960936,
+ 35.72756221127198
+ ],
+ [
+ -83.16238403320312,
+ 35.753199435570316
+ ],
+ [
+ -83.15826416015625,
+ 35.76322914549896
+ ],
+ [
+ -83.10333251953125,
+ 35.76991491635478
+ ],
+ [
+ -83.08685302734375,
+ 35.7843988251953
+ ],
+ [
+ -83.0511474609375,
+ 35.787740890986576
+ ],
+ [
+ -83.01681518554688,
+ 35.78328477203738
+ ],
+ [
+ -83.001708984375,
+ 35.77882840327371
+ ],
+ [
+ -82.96737670898438,
+ 35.793310688351724
+ ],
+ [
+ -82.94540405273438,
+ 35.820040281161
+ ],
+ [
+ -82.9193115234375,
+ 35.85121343450061
+ ],
+ [
+ -82.9083251953125,
+ 35.86902116501695
+ ],
+ [
+ -82.90557861328125,
+ 35.87792352995116
+ ],
+ [
+ -82.91244506835938,
+ 35.92353244718235
+ ],
+ [
+ -82.88360595703125,
+ 35.94688293218141
+ ],
+ [
+ -82.85614013671875,
+ 35.951329861522666
+ ],
+ [
+ -82.8424072265625,
+ 35.94243575255426
+ ],
+ [
+ -82.825927734375,
+ 35.92464453144099
+ ],
+ [
+ -82.80670166015625,
+ 35.927980690382704
+ ],
+ [
+ -82.80532836914062,
+ 35.94243575255426
+ ],
+ [
+ -82.77923583984375,
+ 35.97356075349624
+ ],
+ [
+ -82.78060913085938,
+ 35.99245209055831
+ ],
+ [
+ -82.76138305664062,
+ 36.00356252895066
+ ],
+ [
+ -82.69546508789062,
+ 36.04465753921525
+ ],
+ [
+ -82.64465332031249,
+ 36.060201412392914
+ ],
+ [
+ -82.61306762695312,
+ 36.060201412392914
+ ],
+ [
+ -82.60620117187499,
+ 36.033552893400376
+ ],
+ [
+ -82.60620117187499,
+ 35.991340960635405
+ ],
+ [
+ -82.60620117187499,
+ 35.97911749857497
+ ],
+ [
+ -82.5787353515625,
+ 35.96133453736691
+ ],
+ [
+ -82.5677490234375,
+ 35.951329861522666
+ ],
+ [
+ -82.53067016601562,
+ 35.97244935753683
+ ],
+ [
+ -82.46475219726562,
+ 36.006895355244666
+ ],
+ [
+ -82.41668701171875,
+ 36.070192281208456
+ ],
+ [
+ -82.37960815429686,
+ 36.10126686921446
+ ],
+ [
+ -82.35488891601562,
+ 36.117908916563685
+ ],
+ [
+ -82.34115600585936,
+ 36.113471382052175
+ ],
+ [
+ -82.29583740234375,
+ 36.13343831245866
+ ],
+ [
+ -82.26287841796874,
+ 36.13565654678543
+ ],
+ [
+ -82.23403930664062,
+ 36.13565654678543
+ ],
+ [
+ -82.2216796875,
+ 36.154509006695
+ ],
+ [
+ -82.20382690429688,
+ 36.15561783381855
+ ],
+ [
+ -82.19009399414062,
+ 36.144528857027744
+ ],
+ [
+ -82.15438842773438,
+ 36.15007354140755
+ ],
+ [
+ -82.14065551757812,
+ 36.134547437460064
+ ],
+ [
+ -82.1337890625,
+ 36.116799556445024
+ ],
+ [
+ -82.12142944335938,
+ 36.10570509327921
+ ],
+ [
+ -82.08984375,
+ 36.10792411128649
+ ],
+ [
+ -82.05276489257811,
+ 36.12678323326429
+ ],
+ [
+ -82.03628540039062,
+ 36.12900165569652
+ ],
+ [
+ -81.91268920898438,
+ 36.29409768373033
+ ],
+ [
+ -81.89071655273438,
+ 36.30959215409138
+ ],
+ [
+ -81.86325073242188,
+ 36.33504067209607
+ ],
+ [
+ -81.83029174804688,
+ 36.34499652561904
+ ],
+ [
+ -81.80145263671875,
+ 36.35605709240176
+ ],
+ [
+ -81.77947998046874,
+ 36.34610265300638
+ ],
+ [
+ -81.76162719726562,
+ 36.33835943134047
+ ],
+ [
+ -81.73690795898438,
+ 36.33835943134047
+ ],
+ [
+ -81.71905517578125,
+ 36.33835943134047
+ ],
+ [
+ -81.70669555664062,
+ 36.33504067209607
+ ],
+ [
+ -81.70669555664062,
+ 36.342784223707234
+ ],
+ [
+ -81.72317504882812,
+ 36.357163062654365
+ ],
+ [
+ -81.73278808593749,
+ 36.379279167407965
+ ],
+ [
+ -81.73690795898438,
+ 36.40028364332352
+ ],
+ [
+ -81.73690795898438,
+ 36.41354670392876
+ ],
+ [
+ -81.72454833984374,
+ 36.423492513472326
+ ],
+ [
+ -81.71768188476562,
+ 36.445589751779174
+ ],
+ [
+ -81.69845581054688,
+ 36.47541104282962
+ ],
+ [
+ -81.69845581054688,
+ 36.51073994146672
+ ],
+ [
+ -81.705322265625,
+ 36.53060536411363
+ ],
+ [
+ -81.69158935546875,
+ 36.55929085774001
+ ],
+ [
+ -81.68060302734375,
+ 36.56480607840351
+ ],
+ [
+ -81.68197631835938,
+ 36.58686302344181
+ ],
+ [
+ -81.04202270507812,
+ 36.56370306576917
+ ],
+ [
+ -80.74264526367186,
+ 36.561496993252575
+ ],
+ [
+ -79.89120483398438,
+ 36.54053616262899
+ ],
+ [
+ -78.68408203124999,
+ 36.53943280355122
+ ],
+ [
+ -77.88345336914062,
+ 36.54053616262899
+ ],
+ [
+ -76.91665649414062,
+ 36.54163950596125
+ ],
+ [
+ -76.91665649414062,
+ 36.55046568575947
+ ],
+ [
+ -76.31103515625,
+ 36.551568887374
+ ],
+ [
+ -75.79605102539062,
+ 36.54936246839778
+ ],
+ [
+ -75.6298828125,
+ 36.07574221562703
+ ],
+ [
+ -75.4925537109375,
+ 35.82226734114509
+ ],
+ [
+ -75.3936767578125,
+ 35.639441068973916
+ ],
+ [
+ -75.41015624999999,
+ 35.43829554739668
+ ],
+ [
+ -75.43212890625,
+ 35.263561862152095
+ ],
+ [
+ -75.487060546875,
+ 35.18727767598896
+ ],
+ [
+ -75.5914306640625,
+ 35.17380831799959
+ ],
+ [
+ -75.9210205078125,
+ 35.04798673426734
+ ],
+ [
+ -76.17919921875,
+ 34.867904962568744
+ ],
+ [
+ -76.41540527343749,
+ 34.62868797377061
+ ],
+ [
+ -76.4593505859375,
+ 34.57442951865274
+ ],
+ [
+ -76.53076171875,
+ 34.53371242139567
+ ],
+ [
+ -76.5911865234375,
+ 34.551811369170494
+ ],
+ [
+ -76.651611328125,
+ 34.615126683462194
+ ],
+ [
+ -76.761474609375,
+ 34.63320791137959
+ ],
+ [
+ -77.069091796875,
+ 34.59704151614417
+ ],
+ [
+ -77.376708984375,
+ 34.45674800347809
+ ],
+ [
+ -77.5909423828125,
+ 34.3207552752374
+ ],
+ [
+ -77.8326416015625,
+ 33.97980872872457
+ ],
+ [
+ -77.9150390625,
+ 33.80197351806589
+ ],
+ [
+ -77.9754638671875,
+ 33.73804486328907
+ ],
+ [
+ -78.11279296875,
+ 33.8521697014074
+ ],
+ [
+ -78.2830810546875,
+ 33.8521697014074
+ ],
+ [
+ -78.4808349609375,
+ 33.815666308702774
+ ],
+ [
+ -79.6728515625,
+ 34.8047829195724
+ ],
+ [
+ -80.782470703125,
+ 34.836349990763864
+ ],
+ [
+ -80.782470703125,
+ 34.91746688928252
+ ],
+ [
+ -80.9307861328125,
+ 35.092945313732635
+ ],
+ [
+ -81.0516357421875,
+ 35.02999636902566
+ ],
+ [
+ -81.0516357421875,
+ 35.05248370662468
+ ],
+ [
+ -81.0516357421875,
+ 35.137879119634185
+ ],
+ [
+ -82.3150634765625,
+ 35.19625600786368
+ ],
+ [
+ -82.3590087890625,
+ 35.19625600786368
+ ],
+ [
+ -82.40295410156249,
+ 35.22318504970181
+ ],
+ [
+ -82.4688720703125,
+ 35.16931803601131
+ ],
+ [
+ -82.6885986328125,
+ 35.1154153142536
+ ],
+ [
+ -82.781982421875,
+ 35.06147690849717
+ ],
+ [
+ -83.1060791015625,
+ 35.003003395276714
+ ],
+ [
+ -83.616943359375,
+ 34.99850370014629
+ ],
+ [
+ -84.05639648437499,
+ 34.985003130171066
+ ],
+ [
+ -84.22119140625,
+ 34.985003130171066
+ ],
+ [
+ -84.32281494140625,
+ 34.9895035675793
+ ]
+ ],
+ [
+ [
+ -75.69030761718749,
+ 35.74205383068037
+ ],
+ [
+ -75.5914306640625,
+ 35.74205383068037
+ ],
+ [
+ -75.5419921875,
+ 35.585851593232356
+ ],
+ [
+ -75.56396484375,
+ 35.32633026307483
+ ],
+ [
+ -75.69030761718749,
+ 35.285984736065735
+ ],
+ [
+ -75.970458984375,
+ 35.16482750605027
+ ],
+ [
+ -76.2066650390625,
+ 34.994003757575776
+ ],
+ [
+ -76.300048828125,
+ 35.02999636902566
+ ],
+ [
+ -76.409912109375,
+ 35.07946034047981
+ ],
+ [
+ -76.5252685546875,
+ 35.10642805736423
+ ],
+ [
+ -76.4208984375,
+ 35.25907654252574
+ ],
+ [
+ -76.3385009765625,
+ 35.294952147406576
+ ],
+ [
+ -76.0858154296875,
+ 35.29943548054543
+ ],
+ [
+ -75.948486328125,
+ 35.44277092585766
+ ],
+ [
+ -75.8660888671875,
+ 35.53669637839501
+ ],
+ [
+ -75.772705078125,
+ 35.567980458012094
+ ],
+ [
+ -75.706787109375,
+ 35.634976650677295
+ ],
+ [
+ -75.706787109375,
+ 35.74205383068037
+ ],
+ [
+ -75.69030761718749,
+ 35.74205383068037
+ ]
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json
new file mode 100644
index 0000000..720099b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json
@@ -0,0 +1,502 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.834892363946428,
+ 43.48256740738211
+ ],
+ [
+ 3.8171876558029396,
+ 43.47857177599835
+ ],
+ [
+ 3.6412946044113017,
+ 43.473329643558024
+ ],
+ [
+ 3.6589021586871184,
+ 43.52508450712277
+ ],
+ [
+ 3.8279642042032593,
+ 43.52668001576773
+ ],
+ [
+ 3.834892363946428,
+ 43.48256740738211
+ ]
+ ]
+ ]
+ },
+ "properties": null
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.6946333003797647,
+ 43.454643754862055
+ ],
+ [
+ 3.7292860446964538,
+ 43.40531143854098
+ ],
+ [
+ 3.636426622539704,
+ 43.43205503941921
+ ],
+ [
+ 3.6946333003797647,
+ 43.454643754862055
+ ]
+ ]
+ ]
+ },
+ "properties": null
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823533
+ ],
+ [
+ 3.6300962842853837,
+ 43.371516200704754
+ ],
+ [
+ 3.5565671428569434,
+ 43.31364096674475
+ ],
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823533
+ ],
+ [
+ 3.6300962842853837,
+ 43.371516200704754
+ ],
+ [
+ 3.5565671428569434,
+ 43.31364096674475
+ ],
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784403,
+ 43.31621752144202
+ ],
+ [
+ 3.6238508748053055,
+ 43.375682458235445
+ ],
+ [
+ 3.6300962842853837,
+ 43.37151620070485
+ ],
+ [
+ 3.556567142856943,
+ 43.31364096674483
+ ],
+ [
+ 3.5462226673784403,
+ 43.31621752144202
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.633385150301334,
+ 43.38457565109916
+ ],
+ [
+ 3.64118828280889,
+ 43.37902289968621
+ ],
+ [
+ 3.654474857633081,
+ 43.38559747028079
+ ],
+ [
+ 3.6479331503130545,
+ 43.391373120329625
+ ],
+ [
+ 3.633385150301334,
+ 43.38457565109916
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Parking visiteurs"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823523
+ ],
+ [
+ 3.6300962842853846,
+ 43.37151620070465
+ ],
+ [
+ 3.556567142856944,
+ 43.31364096674463
+ ],
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784416,
+ 43.31621752144171
+ ],
+ [
+ 3.6238508748053064,
+ 43.37568245823512
+ ],
+ [
+ 3.6300962842853846,
+ 43.371516200704534
+ ],
+ [
+ 3.5565671428569443,
+ 43.31364096674452
+ ],
+ [
+ 3.5462226673784416,
+ 43.31621752144171
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823523
+ ],
+ [
+ 3.6300962842853846,
+ 43.37151620070465
+ ],
+ [
+ 3.556567142856944,
+ 43.31364096674463
+ ],
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823523
+ ],
+ [
+ 3.6300962842853846,
+ 43.37151620070465
+ ],
+ [
+ 3.556567142856944,
+ 43.31364096674463
+ ],
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784416,
+ 43.31621752144171
+ ],
+ [
+ 3.6238508748053064,
+ 43.37568245823512
+ ],
+ [
+ 3.6300962842853846,
+ 43.371516200704534
+ ],
+ [
+ 3.5565671428569443,
+ 43.31364096674452
+ ],
+ [
+ 3.5462226673784416,
+ 43.31621752144171
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823523
+ ],
+ [
+ 3.6300962842853846,
+ 43.37151620070465
+ ],
+ [
+ 3.556567142856944,
+ 43.31364096674463
+ ],
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823533
+ ],
+ [
+ 3.6300962842853837,
+ 43.371516200704754
+ ],
+ [
+ 3.5565671428569434,
+ 43.31364096674475
+ ],
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823533
+ ],
+ [
+ 3.6300962842853837,
+ 43.371516200704754
+ ],
+ [
+ 3.5565671428569434,
+ 43.31364096674475
+ ],
+ [
+ 3.5462226673784407,
+ 43.31621752144193
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ],
+ [
+ 3.6238508748053055,
+ 43.37568245823523
+ ],
+ [
+ 3.6300962842853846,
+ 43.37151620070465
+ ],
+ [
+ 3.556567142856944,
+ 43.31364096674463
+ ],
+ [
+ 3.546222667378441,
+ 43.31621752144182
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "label": "Marathon"
+ }
+ }
+ ],
+ "crs": {
+ "type": "name",
+ "properties": {
+ "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+ }
+ }
+}