This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git

commit a833e1da47405df451049d96a020a4ca4036af79
Merge: 7f0548fd16 cccbaee7b7
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Dec 4 15:11:34 2024 +0100

    Merge branch 'geoapi-3.1'

 .../org.apache.sis.feature/main/module-info.java   |   2 +
 .../org/apache/sis/feature/FeatureOperations.java  |   7 +-
 .../sis/feature/GroupAsPolylineOperation.java      |   4 +-
 .../apache/sis/feature/StringJoinOperation.java    |  14 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |  13 +-
 .../org/apache/sis/feature/internal/Resources.java |   2 +-
 .../sis/feature/internal/Resources.properties      |   2 +-
 .../sis/filter/sqlmm/GeometryConstructor.java      |   8 +-
 .../apache/sis/geometry/wrapper/Capability.java    |  44 ++
 .../apache/sis/geometry/wrapper/Dimensions.java    | 100 ++++
 .../apache/sis/geometry/wrapper/Geometries.java    | 216 ++++---
 .../apache/sis/geometry/wrapper/GeometryType.java  | 116 +++-
 .../sis/geometry/wrapper/StandardGeometries.java   |  53 +-
 .../apache/sis/geometry/wrapper/esri/Factory.java  | 211 +++++--
 .../apache/sis/geometry/wrapper/esri/Wrapper.java  |   2 +-
 .../apache/sis/geometry/wrapper/j2d/Factory.java   | 114 ++--
 .../apache/sis/geometry/wrapper/jts/Factory.java   | 449 +++++++++------
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |  78 ++-
 .../sis/filter/BinarySpatialFilterTestCase.java    |  21 +-
 .../apache/sis/filter/sqlmm/RegistryTestCase.java  |  24 +-
 .../sis/geometry/wrapper/GeometriesTestCase.java   |  32 +-
 .../sis/geometry/wrapper/esri/FactoryTest.java     |  34 ++
 .../sis/geometry/wrapper/jts/WrapperTest.java      |  78 +++
 .../org/apache/sis/metadata/sql/privy/Syntax.java  |   2 +-
 .../main/org/apache/sis/io/wkt/doc-files/ESRI.txt  |   2 +-
 .../apache/sis/referencing/cs/AxesConvention.java  |  13 +
 .../org/apache/sis/referencing/cs/AxisFilter.java  |   2 +-
 .../sis/referencing/privy/AxisDirections.java      |  47 +-
 .../test/org/apache/sis/io/wkt/ExtraCRS.txt        |   2 +-
 .../test/org/apache/sis/io/wkt/Malformed.txt       |   2 +-
 .../apache/sis/storage/netcdf/base/FeatureSet.java |  24 +-
 .../apache/sis/storage/sql/feature/Analyzer.java   |   9 +-
 .../apache/sis/storage/sql/feature/Database.java   |   1 -
 .../sis/storage/sql/feature/FeatureAdapter.java    |  11 +-
 .../apache/sis/storage/sql/feature/PrimaryKey.java |  12 +
 .../sis/storage/sql/feature/QueryAnalyzer.java     |   6 +-
 .../apache/sis/storage/sql/feature/Relation.java   |  17 +-
 .../org/apache/sis/storage/sql/feature/Table.java  |   4 +-
 .../sis/storage/sql/feature/TableAnalyzer.java     |  10 +-
 .../main/org/apache/sis/storage/gpx/Reader.java    |   4 +-
 .../sis/storage/DataStoreContentException.java     |   2 +-
 .../sis/storage/csv/MovingFeatureBuilder.java      |   7 +-
 .../main/org/apache/sis/storage/csv/Store.java     |   3 +-
 .../apache/sis/storage/image/WorldFileStore.java   |   2 +-
 .../src/org.apache.sis.util/main/module-info.java  |  11 +
 .../apache/sis/converter/ConverterRegistry.java    |  12 +-
 .../org/apache/sis/converter/DateConverter.java    |  51 +-
 .../org/apache/sis/converter/InstantConverter.java |  83 +++
 .../org/apache/sis/converter/StringConverter.java  |  82 +++
 .../main/org/apache/sis/math/FunctionProperty.java |   4 +-
 .../main/org/apache/sis/math/Vector.java           |   2 +
 .../main/org/apache/sis/util/ArraysExt.java        |  17 +-
 .../org/apache/sis/storage/gimi/GimiProvider.java  |   2 +-
 .../main/org/apache/sis/storage/gimi/Group.java    |   3 +-
 .../org/apache/sis/storage/gimi/ResourceGrid.java  |  30 +-
 .../storage/gimi/ResourceImageUncompressed.java    |   6 +-
 .../apache/sis/storage/gimi/ResourcePyramid.java   |  12 +-
 .../apache/sis/storage/gimi/ResourceUnknown.java   |   2 +-
 .../storage/gimi/internal/MatrixGridRessource.java |  28 +-
 .../isobmff/gimi/ModelTransformationProperty.java  |   2 +-
 .../isobmff/gimi/TiledImageConfigurationBox.java   |   1 -
 .../gimi/isobmff/gimi/WellKnownText2Property.java  |   4 +-
 .../org/apache/sis/storage/gsf/StructClass.java    |  10 +
 .../org/apache/sis/storage/gsf/SwathBathyPing.java |   8 +-
 optional/build.gradle.kts                          |   2 +
 .../org.apache.sis.storage.DataStoreProvider       |   2 +-
 .../org/apache/sis/storage/gdal/ErrorHandler.java  |   8 +-
 .../apache/sis/storage/gdal/FeatureIterator.java   | 493 ++++++++++++++++
 .../org/apache/sis/storage/gdal/FeatureLayer.java  | 309 ++++++++++
 .../org/apache/sis/storage/gdal/FieldAccessor.java | 628 +++++++++++++++++++++
 .../main/org/apache/sis/storage/gdal/GDAL.java     |  31 +-
 .../org/apache/sis/storage/gdal/GDALStore.java     | 103 +++-
 .../main/org/apache/sis/storage/gdal/OGR.java      | 341 +++++++++++
 .../main/org/apache/sis/storage/gdal/Opener.java   |  38 +-
 .../org/apache/sis/storage/gdal/SpatialRef.java    | 138 ++++-
 .../org/apache/sis/storage/gdal/TiledResource.java |   8 +-
 .../org/apache/sis/storage/gdal/package-info.java  |  13 +-
 .../apache/sis/storage/panama/NativeFunctions.java |  14 +-
 .../org/apache/sis/storage/panama/Resources.java   |   5 +
 .../apache/sis/storage/panama/Resources.properties |   1 +
 .../sis/storage/panama/Resources_fr.properties     |   1 +
 .../org/apache/sis/storage/gdal/GDALStoreTest.java |   2 +-
 82 files changed, 3726 insertions(+), 587 deletions(-)

diff --cc endorsed/src/org.apache.sis.feature/main/module-info.java
index 2dfebdce96,3a65be6345..d6907a8a0b
--- a/endorsed/src/org.apache.sis.feature/main/module-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/module-info.java
@@@ -58,6 -54,8 +58,7 @@@ module org.apache.sis.feature 
              org.apache.sis.storage.netcdf,
              org.apache.sis.storage.shapefile,       // In the "incubator" 
sub-project.
              org.apache.sis.portrayal,
 -            org.apache.sis.portrayal.map,           // In the "incubator" 
sub-project.
+             org.apache.sis.storage.gdal,            // In the "optional" 
sub-project.
              org.apache.sis.gui;                     // In the "optional" 
sub-project.
  
      exports org.apache.sis.geometry.wrapper to
@@@ -65,7 -63,10 +66,8 @@@
              org.apache.sis.storage.xml,
              org.apache.sis.storage.sql,
              org.apache.sis.storage.netcdf,
+             org.apache.sis.storage.gdal,            // In the "optional" 
sub-project.
 -            org.apache.sis.storage.shapefile,       // In the "incubator" 
sub-project.
 -            org.apache.sis.portrayal.map,           // In the "incubator" 
sub-project.
 -            org.apache.sis.cql;                     // In the "incubator" 
sub-project.
 +            org.apache.sis.storage.shapefile;       // In the "incubator" 
sub-project.
  
      exports org.apache.sis.geometry.wrapper.j2d to
              org.apache.sis.gui;                     // In the "optional" 
sub-project.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index ef10b003fa,4ab41f8df7..2dc4406318
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@@ -210,9 -205,8 +208,8 @@@ public final class FeatureOperations ex
       *
       * @see <a href="https://en.wikipedia.org/wiki/Compound_key";>Compound key 
on Wikipedia</a>
       */
 -    public static Operation compound(final Map<String,?> identification, 
final String delimiter,
 -            final String prefix, final String suffix, final PropertyType... 
singleAttributes)
 +    public static AbstractOperation compound(final Map<String,?> 
identification, final String delimiter,
 +            final String prefix, final String suffix, final 
AbstractIdentifiedType... singleAttributes)
-             throws UnconvertibleObjectException
      {
          ArgumentChecks.ensureNonEmpty("delimiter", delimiter);
          if (delimiter.indexOf(StringJoinOperation.ESCAPE) >= 0) {
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
index 77d239cb1f,b702dc10e9..0fce5a316d
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
@@@ -163,12 -176,14 +163,14 @@@ final class StringJoinOperation extend
       * It is caller's responsibility to ensure that {@code delimiter} and 
{@code singleAttributes} are not null.
       * This private constructor does not verify that condition on the 
assumption that the public API did.
       *
+      * @throws UnconvertibleObjectException if at least one attributes is not 
convertible from a string.
+      * @throws IllegalArgumentException if the operation failed for another 
reason.
+      *
 -     * @see FeatureOperations#compound(Map, String, String, String, 
PropertyType...)
 +     * @see FeatureOperations#compound(Map, String, String, String, 
AbstractIdentifiedType...)
       */
      @SuppressWarnings({"rawtypes", "unchecked"})                              
          // Generic array creation.
      StringJoinOperation(final Map<String,?> identification, final String 
delimiter,
 -            final String prefix, final String suffix, final PropertyType[] 
singleAttributes)
 +            final String prefix, final String suffix, final 
AbstractIdentifiedType[] singleAttributes)
-             throws UnconvertibleObjectException
      {
          super(identification);
          attributeNames = new String[singleAttributes.length];
@@@ -221,8 -236,14 +223,14 @@@
               * We need only their names and how to convert from String to 
their values.
               */
              attributeNames[i] = name.toString();
-             ObjectConverter<? super String, ?> converter = 
ObjectConverters.find(
-                     String.class, ((DefaultAttributeType<?>) 
propertyType).getValueClass());
 -            final Class<?> valueClass = ((AttributeType<?>) 
propertyType).getValueClass();
++            final Class<?> valueClass = ((DefaultAttributeType<?>) 
propertyType).getValueClass();
+             ObjectConverter<? super String, ?> converter;
+             try {
+                 converter = ObjectConverters.find(String.class, valueClass);
+             } catch (UnconvertibleObjectException e) {
+                 throw new 
UnconvertibleObjectException(Resources.forProperties(identification)
+                         .getString(Resources.Keys.IllegalPropertyType_2, 
name, valueClass), e);
+             }
              if (isAssociation) {
                  converter = new ForFeature(converter);
              }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index 75fd60b37e,ba638a793e..b44ee0c7d6
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@@ -854,14 -829,14 +849,14 @@@ public class FeatureTypeBuilder extend
       * @see #properties()
       * @see #getProperty(String)
       */
 -    public PropertyTypeBuilder addProperty(final PropertyType template) {
 +    public PropertyTypeBuilder addProperty(final AbstractIdentifiedType 
template) {
          ensureNonNull("template", template);
 -        if (template instanceof AttributeType<?>) {
 -            return addAttribute((AttributeType<?>) template);
 -        } else if (template instanceof FeatureAssociationRole) {
 -            return addAssociation((FeatureAssociationRole) template);
 +        if (template instanceof DefaultAttributeType<?>) {
 +            return addAttribute((DefaultAttributeType<?>) template);
 +        } else if (template instanceof DefaultAssociationRole) {
 +            return addAssociation((DefaultAssociationRole) template);
          } else {
-             final PropertyTypeBuilder property = new OperationWrapper(this, 
template);
+             final var property = new OperationWrapper(this, template);
              properties.add(property);
              clearCache();
              return property;
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryConstructor.java
index 0528807e7e,299fe553f8..8792bcc895
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryConstructor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryConstructor.java
@@@ -120,11 -121,11 +120,11 @@@ class GeometryConstructor<R,G> extends 
              } else {
                  result = 
library.createFromComponents(operation.getGeometryType().get(), value);
              }
-             final Object   geometry = library.getGeometry(result);
+             final Object   geomImpl = library.getGeometry(result);
              final Class<?> expected = operation.getReturnType(library);
-             if (!expected.isInstance(geometry)) {
+             if (!expected.isInstance(geomImpl)) {
 -                throw new InvalidFilterValueException(Errors.format(
 +                throw new IllegalArgumentException(Errors.format(
-                         Errors.Keys.IllegalArgumentClass_3, "geom", expected, 
Classes.getClass(geometry)));
+                         Errors.Keys.IllegalArgumentClass_3, "geom", expected, 
Classes.getClass(geomImpl)));
              }
              if (srid != null) {
                  final CoordinateReferenceSystem crs = getTargetCRS(input);
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
index d935240840,facdd123be..fae0cd3aee
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
@@@ -329,9 -330,11 +330,11 @@@ public abstract class Geometries<G> imp
          final Object geometry;
          final int n = point.getDimension();
          switch (n) {
-             case 2: geometry = createPoint(point.getOrdinate(0), 
point.getOrdinate(1)); break;
-             case 3: geometry = createPoint(point.getOrdinate(0), 
point.getOrdinate(1), point.getOrdinate(2)); break;
-             default: throw new 
MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_3, 
"point", (n <= 2) ? 2 : 3, n));
 -            case BIDIMENSIONAL:  geometry = 
createPoint(point.getCoordinate(0), point.getCoordinate(1)); break;
 -            case TRIDIMENSIONAL: geometry = 
createPoint(point.getCoordinate(0), point.getCoordinate(1), 
point.getCoordinate(2)); break;
++            case BIDIMENSIONAL:  geometry = createPoint(point.getOrdinate(0), 
point.getOrdinate(1)); break;
++            case TRIDIMENSIONAL: geometry = createPoint(point.getOrdinate(0), 
point.getOrdinate(1), point.getOrdinate(2)); break;
+             default: throw new MismatchedDimensionException(
+                     Errors.format(Errors.Keys.MismatchedDimension_3, "point",
+                     (n <= BIDIMENSIONAL) ? BIDIMENSIONAL : TRIDIMENSIONAL, 
n));
          }
          final GeometryWrapper wrapper = castOrWrap(geometry);
          if (point.getCoordinateReferenceSystem() != null) {
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index f6a92ba94d,c8734bea9d..7964c1e535
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
@@@ -61,10 -61,11 +61,11 @@@ import org.apache.sis.util.Debug
  import org.apache.sis.util.collection.BackingStoreException;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.filter.sqlmm.SQLMM;
+ import static org.apache.sis.geometry.wrapper.GeometryType.POINT;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.SpatialOperatorName;
 -import org.opengis.filter.DistanceOperatorName;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
 +import org.apache.sis.pending.geoapi.filter.DistanceOperatorName;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/BinarySpatialFilterTestCase.java
index 8cf9ae7c93,38fbe42ea0..51cb616902
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/BinarySpatialFilterTestCase.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/BinarySpatialFilterTestCase.java
@@@ -86,21 -88,20 +87,21 @@@ public abstract class BinarySpatialFilt
      /**
       * Creates the polygon identified by the given enumeration value.
       */
 -    private Literal<Feature,G> literal(final Polygon p) {
 +    private Literal<AbstractFeature,G> literal(final Polygon p) {
-         final byte[] coordinates;
+         final double[] coordinates;
          boolean polygon = true;
          switch (p) {
-             case RIGHT:      coordinates = new byte[] {5,  5,    5, 10,   10, 
10,   10, 5,   5,  5}; break;
-             case DISTANCE_1: coordinates = new byte[] {5, +1,   10, +1,   10, 
 4,   5,  4,   5, +1}; break;
-             case DISTANCE_3: coordinates = new byte[] {5, -1,   10, -1,   10, 
 2,   5,  2,   5, -1}; break;
-             case INTERSECT:  coordinates = new byte[] {7,  3,    9,  3,    9, 
 6,   7,  6,   7,  3}; break;
-             case CONTAINS:   coordinates = new byte[] {1,  1,   11,  1,   11, 
20,   1, 20,   1,  1}; break;
-             case CROSSES:    coordinates = new byte[] {4,  6,    7,  8,   12, 
 9};  polygon = false; break;
-             case TOUCHES:    coordinates = new byte[] {4,  2,    7,  5,    9, 
 3};  polygon = false; break;
+             case RIGHT:      coordinates = new double[] {5,  5,    5, 10,   
10, 10,   10, 5,   5,  5}; break;
+             case DISTANCE_1: coordinates = new double[] {5, +1,   10, +1,   
10,  4,   5,  4,   5, +1}; break;
+             case DISTANCE_3: coordinates = new double[] {5, -1,   10, -1,   
10,  2,   5,  2,   5, -1}; break;
+             case INTERSECT:  coordinates = new double[] {7,  3,    9,  3,    
9,  6,   7,  6,   7,  3}; break;
+             case CONTAINS:   coordinates = new double[] {1,  1,   11,  1,   
11, 20,   1, 20,   1,  1}; break;
+             case CROSSES:    coordinates = new double[] {4,  6,    7,  8,   
12,  9};  polygon = false; break;
+             case TOUCHES:    coordinates = new double[] {4,  2,    7,  5,    
9,  3};  polygon = false; break;
              default: throw new AssertionError(p);
          }
 -        return factory.literal(library.createPolyline(polygon, false, 
Dimensions.XY, DoubleBuffer.wrap(coordinates)));
 +        return (Literal<AbstractFeature,G>)
-                 factory.literal(library.createPolyline(polygon, 2, 
Vector.create(coordinates, false)));
++                factory.literal(library.createPolyline(polygon, false, 
Dimensions.XY, DoubleBuffer.wrap(coordinates)));
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index 07abaeee41,7bb776d5cc..e2157aaafb
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@@ -227,9 -228,7 +227,7 @@@ final class Table extends AbstractFeatu
                       * have been set to association names. If 
`ClassCastException` occurs here, it is a bug
                       * in our object constructions.
                       */
-                     final DefaultAssociationRole association =
-                             (DefaultAssociationRole) 
featureType.getProperty(relation.propertyName);
- 
 -                    final var association = (FeatureAssociationRole) 
featureType.getProperty(relation.propertyName);
++                    final var association = (DefaultAssociationRole) 
featureType.getProperty(relation.propertyName);
                      final Table table = 
tables.get(association.getValueType().getName());
                      if (table == null) {
                          throw new 
InternalDataStoreException(association.toString());
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/MovingFeatureBuilder.java
index 94b8d61d4b,fd3f4fadf7..ec137d47af
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/MovingFeatureBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/MovingFeatureBuilder.java
@@@ -193,10 -194,10 +194,10 @@@ final class MovingFeatureBuilder extend
       *                          source method name and logger name, then 
forward to a {@code WarningListener}.
       */
      public final <G> void storeGeometry(final String featureName, final int 
index, final int dimension,
 -            final Geometries<G> factory, final Attribute<G> dest, final 
Consumer<LogRecord> warningListener)
 +            final Geometries<G> factory, final AbstractAttribute<G> dest, 
final Consumer<LogRecord> warningListener)
      {
          int n = count[index];
-         final Vector[] vectors = new Vector[n];
+         final var vectors = new Vector[n];
          for (Period p = properties[index]; p != null; p = p.previous) {
              vectors[--n] = Vector.create(p.value, false);
          }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
index 17a3e7e588,9f9668999a..2022049dcd
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
@@@ -553,8 -556,8 +554,8 @@@ final class Store extends URIDataStore 
                              if (dissociate) {
                                  type = double[].class;
                              } else {
-                                 type = geometries.polylineClass;
+                                 type = 
geometries.getGeometryClass(GeometryType.LINESTRING);
 -                                characteristics = new AttributeType[] 
{MovingFeatureBuilder.TIME_AS_INSTANTS};
 +                                characteristics = new DefaultAttributeType[] 
{MovingFeatureBuilder.TIME_AS_INSTANTS};
                              }
                              minOccurrence = 1;
                              maxOccurrence = 1;
diff --cc 
optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureIterator.java
index 0000000000,dba0932520..feb2ef7082
mode 000000,100644..100644
--- 
a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureIterator.java
+++ 
b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureIterator.java
@@@ -1,0 -1,493 +1,493 @@@
+ /*
+  * 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.gdal;
+ 
+ import java.util.Set;
+ import java.util.HashSet;
+ import java.util.Spliterator;
+ import java.util.function.Consumer;
+ import java.util.logging.Level;
+ import java.util.logging.LogRecord;
+ import java.nio.ByteOrder;
+ import java.nio.DoubleBuffer;
+ import java.time.LocalDate;
+ import java.time.LocalTime;
+ import java.time.ZoneOffset;
+ import java.lang.foreign.Arena;
+ import java.lang.foreign.MemorySegment;
+ import java.lang.foreign.ValueLayout;
+ import java.lang.reflect.Array;
+ import org.apache.sis.util.privy.Numerics;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.storage.DataStoreException;
+ import org.apache.sis.storage.ConcurrentReadException;
+ import org.apache.sis.geometry.wrapper.Capability;
+ import org.apache.sis.geometry.wrapper.Dimensions;
+ import org.apache.sis.geometry.wrapper.Geometries;
+ import org.apache.sis.geometry.wrapper.GeometryType;
+ import org.apache.sis.storage.panama.Resources;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
++// Specific to the main branch:
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Iterator over the features returned by <abbr>OGR</abbr>.
+  * The current implementation supports only sequential accesses.
+  * All usages of this iterator must be done in the same thread.
+  *
+  * @author Johann Sorel (Geomatys)
+  * @author Martin Desruisseaux (Geomatys)
+  */
 -final class FeatureIterator implements Spliterator<Feature>, Runnable {
++final class FeatureIterator implements Spliterator<AbstractFeature>, Runnable 
{
+     /**
+      * {@code OGRwkbGeometryType} enumeration value specific to 
<abbr>GDAL</abbr>.
+      */
+     private static final int LINEAR_RING = 101;
+ 
+     /**
+      * The source of features. This object provides the feature type,
+      * the fields to read, the synchronization lock and the native functions.
+      */
+     private final FeatureLayer layer;
+ 
+     /**
+      * The number of elements, or -1 if too expansive to compute.
+      */
+     private final long count;
+ 
+     /**
+      * The arena used for allocating the buffers in this class, or {@code 
null} if none (because not needed).
+      */
+     private Arena arena;
+ 
+     /**
+      * A buffer where some <abbr>GDAL</abbr> functions will write their 
outputs, or {@code null} if not needed.
+      */
+     private MemorySegment buffer;
+ 
+     /**
+      * Buffers for extracting the date components of a field, or {@code null} 
if no date is expected.
+      * There is no {@code year} component because {@link #buffer} can be used 
directly for the years.
+      *
+      * <h4>Mapping to geometry coordinates</h4>
+      * The same buffer and the same slices are opportunistically used for 
geometry coordinates as below.
+      * This mapping relies on the fact that {@link Double#BYTES} is twice the 
value of {@link Integer#BYTES}.
+      * Therefore, we start with the buffer, skip one slice, take the next 
slice, skip one slice, <i>etc.</i>
+      *
+      * <ol>
+      *   <li><var>x</var>: {@link #buffer}</li>
+      *   <li><var>y</var>: {@link #day}</li>
+      *   <li><var>z</var>: {@link #minute}</li>
+      *   <li><var>m</var>: {@link #timezone}</li>
+      * </ol>
+      *
+      * @see FieldAccessor#NUM_DATE_COMPONENTS
+      */
+    private MemorySegment month, day, hour, minute, second, timezone;
+ 
+     /**
+      * Unsupported types encountered during the iteration.
+      * Used for avoiding to emit the same warning more than once.
+      */
+     private final Set<String> unsupportedTypes;
+ 
+     /**
+      * Whether the iterator has been initialized. After initialization, no 
other iteration
+      * can be executed on the same {@link FeatureLayer} until {@link #run()} 
is executed.
+      */
+     private boolean initialized;
+ 
+     /**
+      * Creates a new iterator for the given layer.
+      *
+      * @param  layer  the source of features.
+      * @throws Throwable if an error occurred while estimating the number of 
features.
+      */
+     FeatureIterator(final FeatureLayer layer) throws DataStoreException {
+         this.layer = layer;
+         final OGR ogr = layer.OGR();
+         try {
+             count = (long) ogr.getFeatureCount.invokeExact(layer.handle, 0);
+         } catch (Throwable e) {
+             throw GDAL.propagate(e);
+         }
+         unsupportedTypes = new HashSet<>();
+     }
+ 
+     /**
+      * Allocates the buffer if needed (if at least one field is a date/time, 
an array or a geometry).
+      * Tries to allocate only the required amount of memory. In the case of 
geometries, that amount is a guess.
+      * This method shall be invoked after construction. It is not part of the 
constructor for allowing the call
+      * to {@link #run()} if an exception it thrown.
+      *
+      * <p>This method must be invoked in a block synchronized on {@code 
layer.store}.</p>
+      *
+      * @throws ConcurrentReadException if another iteration is already in 
progress.
+      * @throws Throwable if an error occurred while reseting the stream.
+      */
+     private void initialize(final OGR ogr) throws Throwable {
+         assert Thread.holdsLock(layer.store);
+         if (layer.iterationInProgress != null) {
+             throw new 
ConcurrentReadException(Resources.forLocale(layer.store.getLocale())
+                     
.getString(Resources.Keys.UnsupportedConcurrentIteration));
+         }
+         initialized = true;     // Set now for making sure that this method 
is not executed twice.
+         int bufferSize = 0;
+         for (FieldAccessor<?> field : layer.fields) {
+             bufferSize = Math.max(bufferSize, field.bufferSize());
+         }
+         if (bufferSize >= Integer.BYTES) {
+             arena  = layer.store.changeArena(arena, true);
+             buffer = arena.allocate(bufferSize);
+             sliceBuffers();
+         }
+         layer.iterationInProgress = this;
+         ogr.resetReading.invoke(layer.handle);          // Must be done after 
`getFeatureCount`.
+     }
+ 
+     /**
+      * Creates the memory segments for the coordinates and date components.
+      * This method needs to be invoked every times that {@link #buffer} 
changed.
+      */
+     private void sliceBuffers() {
+         if (buffer.byteSize() > Integer.BYTES) {
+             for (int i=1; ; i++) {
+                 final MemorySegment s = buffer.asSlice(i * Integer.BYTES, 1);
+                 switch (i) {
+                     default: throw new AssertionError(i);
+                     case 1:  month    = s; break;
+                     case 2:  day      = s; break;   // Also used for Y 
coordinate values.
+                     case 3:  hour     = s; break;
+                     case 4:  minute   = s; break;   // Also used for Z 
coordinate values.
+                     case 5:  second   = s; break;
+                     case 6:  timezone = s; return;  // Also used for M 
coordinate values.
+                 }
+             }
+         }
+     }
+ 
+     /**
+      * Do not split the iterator, because this implementation requires 
sequential accesses.
+      */
+     @Override
 -    public Spliterator<Feature> trySplit() {
++    public Spliterator<AbstractFeature> trySplit() {
+         return null;
+     }
+ 
+     /**
+      * Returns the number of features if known, or {@link Long#MAX_VALUE} 
otherwise.
+      */
+     @Override
+     public long estimateSize() {
+         return (count >= 0) ? count : Long.MAX_VALUE;
+     }
+ 
+     /**
+      * Returns the number of features if known, or {@code -1} otherwise.
+      */
+     @Override
+     public long getExactSizeIfKnown() {
+         return count;
+     }
+ 
+     /**
+      * Specifies that this iterator never returns null elements.
+      * May also specify that the number of features is known.
+      */
+     @Override
+     public int characteristics() {
+         return (count >= 0) ? SIZED | NONNULL : NONNULL;
+     }
+ 
+     /**
+      * Fetches the next feature instance.
+      *
+      * @param  action  the action to perform with the next feature.
+      * @return whether a feature instance has been found.
+      */
+     @Override
 -    public boolean tryAdvance(final Consumer<? super Feature> action) {
++    public boolean tryAdvance(final Consumer<? super AbstractFeature> action) 
{
+         try {
+             synchronized (layer.store) {
+                 final OGR ogr = layer.OGR();
+                 if (!initialized) {
+                     initialize(ogr);
+                 }
+                 return advance(ogr, action);
+             }
+         } catch (Throwable e) {
+             throw GDAL.propagate(e);
+         }
+     }
+ 
+     /**
+      * Executes the given action on all remaining feature instances.
+      *
+      * @param action the action to perform on all remaining features.
+      */
+     @Override
+     @SuppressWarnings("empty-statement")
 -    public void forEachRemaining(final Consumer<? super Feature> action) {
++    public void forEachRemaining(final Consumer<? super AbstractFeature> 
action) {
+         try {
+             synchronized (layer.store) {
+                 final OGR ogr = layer.OGR();
+                 if (!initialized) {
+                     initialize(ogr);
+                 }
+                 while (advance(ogr, action));
+             }
+         } catch (Throwable e) {
+             throw GDAL.propagate(e);
+         }
+     }
+ 
+     /**
+      * Executes the given action on the next feature instance.
+      *
+      * @param  ogr     pointers to native functions.
+      * @param  action  the action to perform with the next feature.
+      * @return whether a feature instance has been found.
+      * @throws Throwable if a call to a native method failed.
+      */
 -    private boolean advance(final OGR ogr, final Consumer<? super Feature> 
action) throws Throwable {
++    private boolean advance(final OGR ogr, final Consumer<? super 
AbstractFeature> action) throws Throwable {
+         final MemorySegment handle = (MemorySegment) 
ogr.getNextFeature.invokeExact(layer.handle);
+         if (GDAL.isNull(handle)) {
+             return false;
+         }
 -        final Feature feature;
++        final AbstractFeature feature;
+         try {
+             feature = layer.type.newInstance();
+             for (FieldAccessor<?> field : layer.fields) {
+                 feature.setPropertyValue(field.name, field.getValue(this, 
ogr, handle));
+             }
+         } finally {
+             ogr.destroyFeature.invokeExact(handle);
+         }
+         action.accept(feature);
+         return true;
+     }
+ 
+     /**
+      * Returns a buffer where some <abbr>GDAL</abbr> functions will write 
their outputs.
+      * This method shall be invoked only from instances where {@link 
FieldAccessor#bufferSize()}
+      * returned a non-zero value.
+      */
+     final MemorySegment buffer() {
+         return buffer;
+     }
+ 
+     /**
+      * Returns whether the field in the given feature has a date.
+      * If true, the date components are stored in the temporal buffers of 
this layer.
+      * This method must be invoked in a block synchronized on {@link 
FeatureLayer#store}.
+      *
+      * @param  ogr      pointers to native functions.
+      * @param  feature  pointer to the {@code OGRFeatureH} instance.
+      * @return whether the field has date components in the given feature 
instance.
+      * @throws Throwable if an error occurred during the native method call.
+      */
+     final boolean hasDate(final OGR ogr, final MemorySegment feature, final 
int index) throws Throwable {
+         return ((int) ogr.getFieldAsDateTime.invokeExact(feature, index,
+                 buffer, month, day, hour, minute, second, timezone)) != 0;
+     }
+ 
+     /**
+      * Creates a local date from the current field values.
+      * The {@link #hasDate(OGR, MemorySegment, int)} method must have been 
invoked before this method.
+      *
+      * @return the local date of the field checked by the last call to {@link 
#hasDate(OGR, MemorySegment, int)}. Never null.
+      * @throws NullPointerException if the temporal buffers are null (no 
dates where expected for the feature type).
+      */
+     final LocalDate date() {
+         return LocalDate.of(buffer.get(ValueLayout.JAVA_INT, 0),
+                             month .get(ValueLayout.JAVA_INT, 0),
+                             day   .get(ValueLayout.JAVA_INT, 0));
+     }
+ 
+     /**
+      * Creates a local time from the current field values.
+      * The {@link #hasDate(OGR, MemorySegment, int)} method must have been 
invoked before this method.
+      *
+      * @return the local time of the field checked by the last call to {@link 
#hasDate(OGR, MemorySegment, int)}. Never null.
+      * @throws NullPointerException if the temporal buffers are null (no 
dates where expected for the feature type).
+      */
+     final LocalTime time() {
+         int sec;
+         float withMillis =  second.get(ValueLayout.JAVA_FLOAT, 0);
+         return LocalTime.of(hour  .get(ValueLayout.JAVA_INT, 0),
+                             minute.get(ValueLayout.JAVA_INT, 0),
+                             sec = (int) withMillis,
+                             Math.round((withMillis - sec) * 
Constants.NANOS_PER_SECOND));
+     }
+ 
+     /**
+      * Returns the timezone of current field values, or {@code null} if local 
or unknown.
+      * The {@link #hasDate(OGR, MemorySegment, int)} method must have been 
invoked before this method.
+      *
+      * @return the timezone of the field checked by the last call to {@link 
#hasDate(OGR, MemorySegment, int)}, or null if local.
+      * @throws NullPointerException if the temporal buffers are null (no 
dates where expected for the feature type).
+      */
+     final ZoneOffset timezone() {
+         return timezone.get(ValueLayout.JAVA_INT, 0) == 100 ? ZoneOffset.UTC 
: null;
+     }
+ 
+     /**
+      * Creates the Java geometry object from the given {@code OGRGeometryH}.
+      *
+      * @param  ogr   pointers to native functions.
+      * @param  geom  pointer to the {@code OGRGeometryH} instance.
+      * @return the Java geometry object, or {@code null} if none or not 
supported.
+      * @throws ArrayStoreException if the geometry is some kind of collection 
but contains a child of unexpected type.
+      * @throws Throwable if an error occurred during a native method call or 
Java geometry construction.
+      */
+     final Object geometry(final OGR ogr, final MemorySegment geom) throws 
Throwable {
+         assert Thread.holdsLock(layer.store);
+         final Geometries<?> library = layer.library;
+         final int code = (int) ogr.getGeometryType.invokeExact(geom);
+         final var type = (code == LINEAR_RING) ? GeometryType.LINESTRING : 
GeometryType.forBinaryType(code);
+         final boolean isPolygon = (type == GeometryType.POLYGON);
+         if (isPolygon || type.isCollection) {
+             /*
+              * If the geometry is some kind of collection (with polygon 
considered as a collection of linear rings),
+              * invoke this method recursively for all child components. Note 
that `LinearRing` is not a standard type,
+              * so we replace it by the `LineString` parent type.
+              */
+             final GeometryType componentType;
+             if (isPolygon) {
+                 componentType = GeometryType.LINESTRING;
+             } else {
+                 componentType = type.component();
+                 if (componentType == null) {
+                     unsupportedType(type.name);
+                     return null;
+                 }
+             }
+             final Class<?> componentClass = 
library.getGeometryClass(componentType);
+             final int n = (int) ogr.getGeometryCount.invokeExact(geom);
+             if (n <= 0) {
+                 return null;
+             }
+             final Object[] children = (Object[]) 
Array.newInstance(componentClass, n);
+             for (int i=0; i<n; i++) {
+                 var child = (MemorySegment) 
ogr.getGeometryRef.invokeExact(geom, i);
+                 children[i] = geometry(ogr, child);         // May throw 
ArrayStoreException.
+             }
+             return library.getGeometry(library.createFromComponents(type, 
children));
+         }
+         /*
+          * Case of Point and LineString/LinearRing. The buffer capacity must 
be verified.
+          * If the capacity is not sufficient, create a new buffer with the 
lifetime of this method call only.
+          * We do not use the iterator lifetime for avoiding memory leaks, 
because buffers may be created often
+          * if the geometry sizes are increasing in each iteration (buffers 
are released only in `Arena.close()`).
+          */
+         int pointCount = (int) ogr.getPointCount.invokeExact(geom);
+         if (pointCount <= 0) {      // `OGR_G_GetPointCount` returns 0 if not 
Point or LineString/LinearRing.
+             return null;
+         }
+         final boolean hasZ    = GeometryType.hasZ(code) && 
library.supports(Capability.Z_COORDINATE);
+         final boolean hasM    = GeometryType.hasM(code) && 
library.supports(Capability.M_COORDINATE);
+         final var  dimensions = Dimensions.forZorM(hasZ, hasM);
+         final int  stride     = dimensions.count * Double.BYTES;
+         final long capacity   = Math.multiplyFull(pointCount, stride);
+         if (buffer.byteSize() < capacity) {
+             arena  = layer.store.changeArena(arena, true);
+             buffer = arena.allocate(capacity);
+         }
+         /*
+          * (cx, cy, cz, cm) are pointers to the same buffer with an offset of 
`sizeof(double)` between each.
+          * We opportunistically reuse the slices already prepared for dates, 
by choosing those having the right offset.
+          */
+         final MemorySegment cx = buffer;
+         final MemorySegment cy = day;         // See field javadoc.
+         final MemorySegment cz = hasZ ? minute : MemorySegment.NULL;
+         final MemorySegment cm = hasM ? (hasZ ? timezone : minute) : 
MemorySegment.NULL;
+         pointCount = (int) ogr.getPoints.invokeExact(geom, cx, stride, cy, 
stride, cz, stride, cm, stride);
+         assert Math.multiplyFull(pointCount, stride) <= cx.byteSize() : 
pointCount;
+         if (pointCount <= 0) {
+             return null;
+         }
+         if (pointCount == 1) {
+             double x = cx.get(ValueLayout.JAVA_DOUBLE, 0);
+             double y = cy.get(ValueLayout.JAVA_DOUBLE, 0);
+             if (hasZ) {
+                 double z = cz.get(ValueLayout.JAVA_DOUBLE, 0);
+                 return library.createPoint(x, y, z);
+             } else {
+                 return library.createPoint(x, y);
+             }
+         }
+         /*
+          * Case of string line having more than 1 point. The `DoubleBuffer` 
wraps directly the memory segment.
+          * The caller method shall copy the coordinate values, because the 
buffer will not be valid anymore
+          * shortly after this methd call.
+          */
+         @SuppressWarnings("restricted")
+         final MemorySegment coords = 
cx.reinterpret(Math.multiplyFull(pointCount, stride));
+         final DoubleBuffer coordinates = 
coords.asByteBuffer().order(ByteOrder.nativeOrder()).asDoubleBuffer();
+         DoubleBuffer closingPoint = null;
+         if (code == LINEAR_RING && pointCount >= 3) {
+             /*
+              * Polylines are automatically recognized as linear rings if the 
last point is equal to
+              * the first point. If this is not the case while a linear ring 
was expected, copy that
+              * point in a separated vector. The two vectors will be 
concatenated by `createPolyline`.
+              */
+             final int last = pointCount - dimensions.count;
+             final double[] point = new double[dimensions.count];
+             for (int i=0; i<point.length; i++) {
+                 point[i] = coordinates.get(i);
+                 if (closingPoint == null && 
!Numerics.equalsIgnoreZeroSign(point[i], coordinates.get(last + i))) {
+                     closingPoint = DoubleBuffer.wrap(point);    // Okay even 
if we didn't finished to fill the array.
+                 }
+             }
+         }
+         switch (type) {
+             case POINT:      return library.createPoint(false, dimensions, 
coordinates);
+             case MULTIPOINT: return library.createMultiPoint(false, 
dimensions, coordinates);
+             default:         return library.createPolyline(false, false, 
dimensions, coordinates, closingPoint);
+         }
+     }
+ 
+     /**
+      * Logs a warning saying that the given type is unsupported.
+      * Each type is warned only once per iteration.
+      */
+     private void unsupportedType(final String type) {
+         if (unsupportedTypes.add(type)) {
+             layer.store.warning(FeatureLayer.class, "features",
+                     new LogRecord(Level.WARNING, 
layer.errors().getString(Errors.Keys.UnsupportedType_1, type)));
+         }
+     }
+ 
+     /**
+      * Releases the native memory allocated for this iterator and marks the 
iteration as completed.
+      * Call to this method should be registered with {@link 
java.util.stream.Stream#onClose(Runnable)}.
+      *
+      * <h4>Design note</h4>
+      * We could register this method for invocation by the garbage collector 
when the users forgot to invoke
+      * {@code Stream.close()}. But we don't in order to throw {@code 
ConcurrentReadException} systematically,
+      * for letting users know that they have a bug.
+      */
+     @Override
+     public void run() {
+         synchronized (layer.store) {
+             layer.iterationInProgress = null;
+             arena = layer.store.changeArena(arena, false);
+         }
+     }
+ }
diff --cc 
optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java
index 0000000000,c0c1b4b5c7..8b256c5307
mode 000000,100644..100644
--- 
a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java
+++ 
b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java
@@@ -1,0 -1,307 +1,309 @@@
+ /*
+  * 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.gdal;
+ 
+ import java.util.Locale;
+ import java.util.Optional;
+ import java.util.stream.Stream;
+ import java.util.stream.StreamSupport;
+ import java.lang.foreign.Arena;
+ import java.lang.foreign.AddressLayout;
+ import java.lang.foreign.MemorySegment;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
+ import org.opengis.geometry.Envelope;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ 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.feature.privy.AttributeConvention;
+ import org.apache.sis.geometry.GeneralEnvelope;
+ import org.apache.sis.geometry.wrapper.Geometries;
+ import org.apache.sis.geometry.wrapper.GeometryType;
+ import org.apache.sis.referencing.IdentifiedObjects;
+ import org.apache.sis.storage.AbstractFeatureSet;
+ import org.apache.sis.storage.DataStoreException;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.privy.Strings;
+ 
++// Specific to the main branch:
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.DefaultFeatureType;
++
+ 
+ /**
+  * Information about a feature set in <abbr>OGR</abbr>.
+  * This is a wrapper around {@code OGRLayerH} in the C/C++ <abbr>API</abbr>.
+  *
+  * @author Johann Sorel (Geomatys)
+  * @author Martin Desruisseaux (Geomatys)
+  */
+ final class FeatureLayer extends AbstractFeatureSet {
+     /**
+      * The data store which owns this feature layer.
+      * All operations must be synchronized on this store, because 
<abbr>GDAL</abbr> is not thread-safe.
+      */
+     final GDALStore store;
+ 
+     /**
+      * Pointer to the <abbr>OGR</abbr> object in native memory.
+      * This is a {@code OGRLayerH} in the C/C++ <abbr>API</abbr>.
+      */
+     final MemorySegment handle;
+ 
+     /**
+      * Description (list of fields) of all feature instances in this layer.
+      */
 -    final FeatureType type;
++    final DefaultFeatureType type;
+ 
+     /**
+      * Description of the fields to read with GDAL API. Each {@code 
FieldAccessor} contains the property name
+      * and the code to execute for fetching the field value from an {@code 
OGRFeatureH} with <abbr>OGR</abbr>,
+      */
+     final FieldAccessor<?>[] fields;
+ 
+     /**
+      * The coordinate reference system of the default geometry field (the 
first one), or {@code null} if unknown.
+      * Note that each geometry field can also have its own CRS, not 
necessarily equals to this one.
+      */
+     private final CoordinateReferenceSystem defaultCRS;
+ 
+     /**
+      * The library to use for building geometry objects.
+      */
+     final Geometries<?> library;
+ 
+     /**
+      * Non-null if an iteration is in progress. Each {@code FeatureLayer} can 
have only one iteration in progress
+      * at a given time, because <abbr>GDAL</abbr> {@code OGRLayerH} C/C++ 
<abbr>API</abbr> provides only one cursor.
+      */
+     FeatureIterator iterationInProgress;
+ 
+     /**
+      * Wraps a <abbr>GDAL</abbr> {@code OGRLayerH} and builds the feature 
type.
+      *
+      * @param  store   the data store which owns this feature layer.
+      * @param  gdal    set of native methods to use.
+      * @param  handle  the {@code OGRLayerH} to wrap.
+      * @throws Throwable if an error occurred during a call to a native 
method or in Java code.
+      */
+     private FeatureLayer(final GDALStore store, final GDAL gdal, final OGR 
ogr, final MemorySegment handle) throws Throwable {
+         super(store);
+         this.store           = store;
+         this.handle          = handle;
+         this.library         = Geometries.factory(store.library);
+         final var builder    = new FeatureTypeBuilder();
+         final var definition = (MemorySegment) 
ogr.getLayerDefinition.invokeExact(handle);
+         final int fieldCount = (int) 
ogr.getFieldCount.invokeExact(definition);
+         final int geomCount  = (int) 
ogr.getGeomFieldCount.invokeExact(definition);
+         /*
+          * Fetch the non-geometry fields.
+          */
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         final var fields = new 
FieldAccessor<?>[FieldAccessor.NUM_ADDITIONAL_FIELDS + fieldCount + geomCount];
+         int fieldIndex = 0;
+         fields[fieldIndex++] = addAttribute(builder, 
FieldAccessor.Identifier.INSTANCE);
+         for (int i=0; i<fieldCount; i++) {
+             fields[fieldIndex++] = addAttribute(builder, 
FieldAccessor.create(ogr, definition, i));
+         }
+         /*
+          * Fetch the geometry fields with the first one taken as the default 
geometry.
+          * Each geometry field can have its own coordinate reference system 
(optional).
+          * The declared geometry type may be conservatively generalized to 
`GEOMETRY`
+          * if the information provided by the driver is not reliable.
+          */
+         final String  driverName = store.getDriverName(gdal);
+         final boolean generalize = (driverName != null) && 
driverName.toLowerCase(Locale.US).contains("shapefile");
+ 
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         CoordinateReferenceSystem defaultCRS = null;
+ 
+         boolean isFirstGeometry = true;
+         for (int i=0; i<geomCount; i++) {
+             final var geomField = (MemorySegment) 
ogr.getGeomFieldDefinition.invokeExact(definition, i);
+             if (GDAL.isNull(geomField)) continue;       // Paranoiac check, 
but should never happen.
+             boolean setAsDefault = isFirstGeometry;
+             final int geomType  = (int) 
ogr.getGeomFieldType.invokeExact(geomField);
+             String name = GDAL.toString((MemorySegment) 
ogr.getGeomFieldName.invokeExact(geomField));
+             if (name == null || name.isBlank()) {
+                 name = setAsDefault ? AttributeConvention.GEOMETRY : 
"geometry" + i;
+                 setAsDefault = false;       // Because already has the 
default name.
+             }
+             /*
+              * OGR Hack: ShapeFile geometry type is not correctly detected 
because the OGR driver does
+              * not distinguish betwen polygon and multi-polygon. The same 
issue applies to line strings.
+              * However, points are okay (ShapeFile has distinct Point and 
MultiPoint types).
+              * https://code.djangoproject.com/ticket/7218
+              */
+             GeometryType gt = GeometryType.forBinaryType(geomType);
+             if (generalize && (gt == GeometryType.POLYGON || gt == 
GeometryType.LINESTRING)) {
+                 gt = GeometryType.GEOMETRY;
+             }
+             Class<?> geomClass = library.getGeometryClass(gt);
+             /*
+              * Add the geometry attribute together with the corresponding 
`FieldAccessor` for fetching geometries.
+              * Each field may have its own CRS, but the first one will be 
taken as the CRS for default geometries.
+              */
+             final CoordinateReferenceSystem crs = SpatialRef.parseCRS(store, 
gdal,
+                     (MemorySegment) 
ogr.getGeomFieldSpatialRef.invokeExact(geomField));
+ 
+             final AttributeTypeBuilder<?> attribute = 
builder.addAttribute(geomClass).setName(name).setCRS(crs);
+             if (setAsDefault) {     // First geometry as default.
+                 attribute.addRole(AttributeRole.DEFAULT_GEOMETRY);
+             }
+             fields[fieldIndex++] = new FieldAccessor.Geometry<>(name, i, 
geomClass, crs);
+             if (isFirstGeometry) {
+                 isFirstGeometry = false;
+                 defaultCRS = crs;           // May still be null.
+             }
+         }
+         this.type       = builder.setName(GDAL.toString((MemorySegment) 
ogr.getLayerName.invokeExact(handle))).build();
+         this.fields     = ArraysExt.resize(fields, fieldIndex);
+         this.defaultCRS = defaultCRS;
+     }
+ 
+     /**
+      * Adds a non-geometry attribute to a feature type. This is a helper 
method for the constructor.
+      * The returned {@link FieldAccessor} instance encapsulates the code for 
fetching the attribute value.
+      */
+     private static FieldAccessor<?> addAttribute(final FeatureTypeBuilder 
builder, final FieldAccessor<?> field) {
+         final AttributeTypeBuilder<?> attribute;
+         Class<?> ft = field.getElementClass();
+         if (ft != null) {
+             attribute = 
builder.addAttribute(ft).setMaximumOccurs(Integer.MAX_VALUE);
+         } else {
+             attribute = builder.addAttribute(field.getJavaClass());
+         }
+         attribute.setName(field.name);
+         return field;
+     }
+ 
+     /**
+      * Returns all feature layers found in the data store.
+      *
+      * @param  parent  wrapper for the {@code GDALDatasetH} of 
<abbr>GDAL</abbr> C/C++ <abbr>API</abbr>.
+      * @param  gdal    set of <abbr>GDAL</abbr> native functions.
+      * @return all layers found, or an empty array if none.
+      * @throws DataStoreException if an error occurred.
+      */
+     static FeatureLayer[] listLayers(final GDALStore parent, final GDAL gdal) 
throws DataStoreException {
+         final MemorySegment dataset = parent.handle();
+         try {
+             final int n = (int) gdal.getLayerCount.invokeExact(dataset);
+             final var layers = new FeatureLayer[n];
+             if (n != 0) {
+                 final OGR ogr = gdal.ogr();     // Initialize only if we have 
at least one layer.
+                 for (int i=0; i<n; i++) {
+                     final var layer = (MemorySegment) 
ogr.getLayer.invokeExact(dataset, i);
+                     layers[i] = new FeatureLayer(parent, gdal, ogr, layer);
+                 }
+             }
+             return layers;
+         } catch (Throwable e) {
+             throw GDAL.propagate(e);
+         }
+     }
+ 
+     /**
+      * Returns the feature type of this feature set.
+      */
+     @Override
 -    public final FeatureType getType() {
++    public final DefaultFeatureType getType() {
+         return type;
+     }
+ 
+     /**
+      * Returns the set of <abbr>OGR</abbr> native functions.
+      *
+      * @return the set of native functions.
+      * @throws DataStoreException if the native library is not available.
+      */
+     final OGR OGR() throws DataStoreException {
+         return store.getProvider().GDAL().ogr();
+     }
+ 
+     /**
+      * Returns the layer bounding box if its computation is not too expansive.
+      *
+      * @return the layer bounding box, or empty if none or too expensive to 
compute.
+      * @throws DataStoreException if GDAL raised an error.
+      */
+     @Override
+     public Optional<Envelope> getEnvelope() throws DataStoreException {
+         final OGR ogr = OGR();
+         try (Arena arena = Arena.ofConfined()) {
+             final MemorySegment extent = arena.allocate(Double.BYTES * 4);    
  // minX, maxX, minY, maxY.
+             int error;
+             synchronized (store) {
+                 try {
+                     error = (int) ogr.getLayerExtent.invokeExact(handle, 
extent, 0);
+                 } catch (Throwable e) {
+                     throw GDAL.propagate(e);
+                 }
+             }
+             if (error != 0) {
+                 // GDAL documentation said that `OGRERR_FAILURE` can simply 
means that the extent is not known.
+                 return Optional.empty();
+             }
+             final GeneralEnvelope env;
+             if (defaultCRS != null) {
+                 env = new GeneralEnvelope(defaultCRS);
+             } else {
+                 env = new GeneralEnvelope(SpatialRef.BIDIMENSIONAL);
+             }
+             final var t = AddressLayout.JAVA_DOUBLE;
+             env.setRange(0, extent.getAtIndex(t, 0), extent.getAtIndex(t, 1));
+             env.setRange(1, extent.getAtIndex(t, 2), extent.getAtIndex(t, 3));
+             return Optional.of(env);
+         } finally {
+             ErrorHandler.throwOnFailure(store, "getEnvelope");
+         }
+     }
+ 
+     /**
+      * Returns a stream of feature instances to be read from this layer.
+      * Only one stream can be executed at a time.
+      *
+      * @param  parallel  ignored since <abbr>GDAL</abbr> is not thread-safe.
+      */
+     @Override
 -    public Stream<Feature> features(boolean parallel) throws 
DataStoreException {
++    public Stream<AbstractFeature> features(boolean parallel) throws 
DataStoreException {
+         try {
+             synchronized (store) {
+                 // Ignore the `parallel` argument, as it is not supported by 
the iterator.
+                 final var it = new FeatureIterator(this);
+                 return StreamSupport.stream(it, false).onClose(it);
+             }
+         } finally {
+             ErrorHandler.throwOnFailure(store, "features");
+         }
+     }
+ 
+     /**
+      * Returns the locale-dependent resources for error messages.
+      */
+     final Errors errors() {
+         return Errors.forLocale(store.getLocale());
+     }
+ 
+     /**
+      * Returns a string representation of this layer for debugging purposes.
+      */
+     @Override
+     public String toString() {
+         return Strings.toString(getClass(), null, type.getName(), "crs", 
IdentifiedObjects.getDisplayName(defaultCRS));
+     }
+ }

Reply via email to