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

jsorel pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 924462e3c6 feat(Shapefile): add shapefile store
924462e3c6 is described below

commit 924462e3c6f2f8beb8fec8f6e15689856214d08b
Author: jsorel <[email protected]>
AuthorDate: Fri Nov 3 10:16:38 2023 +0100

    feat(Shapefile): add shapefile store
---
 .../org.apache.sis.feature/main/module-info.java   |   1 +
 .../src/org.apache.sis.util/main/module-info.java  |   1 +
 .../sis/storage/shapefile/ShapefileProvider.java   |  83 +++++
 .../sis/storage/shapefile/ShapefileStore.java      | 382 +++++++++++++++++++++
 .../shapefile/shp/ShapeGeometryEncoder.java        |  53 +--
 .../sis/storage/shapefile/shx/IndexReader.java     |  65 ++++
 .../sis/storage/shapefile/ShapefileStoreTest.java  |  92 +++++
 7 files changed, 655 insertions(+), 22 deletions(-)

diff --git a/endorsed/src/org.apache.sis.feature/main/module-info.java 
b/endorsed/src/org.apache.sis.feature/main/module-info.java
index 33a7fd41d2..b9f78b899c 100644
--- a/endorsed/src/org.apache.sis.feature/main/module-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/module-info.java
@@ -52,6 +52,7 @@ module org.apache.sis.feature {
             org.apache.sis.storage.xml,
             org.apache.sis.storage.netcdf,
             org.apache.sis.portrayal,
+            org.apache.sis.storage.shapefile,        // In the "incubator" 
sub-project.
             org.apache.sis.gui;                     // In the "optional" 
sub-project.
 
     exports org.apache.sis.geometry.wrapper to
diff --git a/endorsed/src/org.apache.sis.util/main/module-info.java 
b/endorsed/src/org.apache.sis.util/main/module-info.java
index dd5262f706..ed334ac345 100644
--- a/endorsed/src/org.apache.sis.util/main/module-info.java
+++ b/endorsed/src/org.apache.sis.util/main/module-info.java
@@ -129,6 +129,7 @@ module org.apache.sis.util {
             org.apache.sis.storage.earthobservation,
             org.apache.sis.cql,                         // In the "incubator" 
sub-project.
             org.apache.sis.portrayal,
+            org.apache.sis.storage.shapefile,
             org.apache.sis.cloud.aws,
             org.apache.sis.console,
             org.apache.sis.gui,                         // In the "optional" 
sub-project.
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java
new file mode 100644
index 0000000000..3d371337a3
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java
@@ -0,0 +1,83 @@
+/*
+ * 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.shapefile;
+
+import java.net.URI;
+import java.nio.file.Path;
+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.ProbeResult;
+import org.apache.sis.storage.StorageConnector;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+
+/**
+ * Shapefile format datastore provider.
+ * 
+ * @author Johann Sorel (Geomatys)
+ * @see <a 
href="http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf";>ESRI 
Shapefile Specification</a>
+ */
+public final class ShapefileProvider extends DataStoreProvider {
+
+    public static final String NAME = "Shapefile";
+    
+    public static final String MIME_TYPE = "application/x-shapefile";
+    
+    /**
+     * URI to the shp file.
+     */
+    public static final ParameterDescriptor<URI> PATH = new ParameterBuilder()
+            .addName(LOCATION)
+            .setRequired(true)
+            .create(URI.class, null);
+    
+    public static final ParameterDescriptorGroup PARAMETERS_DESCRIPTOR =
+            new 
ParameterBuilder().addName(NAME).addName("ShapefileParameters").createGroup(
+                PATH);
+    
+    public ShapefileProvider() {        
+    }
+    
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    @Override
+    public ParameterDescriptorGroup getOpenParameters() {
+        return PARAMETERS_DESCRIPTOR;
+    }
+
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws 
DataStoreException {
+        final Path path = connector.getStorageAs(Path.class);
+        if (path != null && 
path.getFileName().toString().toLowerCase().endsWith(".shp")) {
+            return new ProbeResult(true, MIME_TYPE, null);
+        }
+        return ProbeResult.UNSUPPORTED_STORAGE;
+    }
+
+    @Override
+    public DataStore open(StorageConnector connector) throws 
DataStoreException {
+        final Path path = connector.getStorageAs(Path.class);
+        return new ShapefileStore(path);
+    }
+    
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
new file mode 100644
index 0000000000..03aa111d42
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
@@ -0,0 +1,382 @@
+/*
+ * 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.shapefile;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+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.io.stream.ChannelDataInput;
+import org.apache.sis.io.stream.ChannelDataOutput;
+import org.apache.sis.io.stream.IOUtilities;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.AbstractFeatureSet;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.Query;
+import org.apache.sis.storage.UnsupportedQueryException;
+import org.apache.sis.storage.WritableFeatureSet;
+import org.apache.sis.storage.shapefile.cpg.CpgFiles;
+import org.apache.sis.storage.shapefile.dbf.DBFField;
+import org.apache.sis.storage.shapefile.dbf.DBFHeader;
+import org.apache.sis.storage.shapefile.dbf.DBFReader;
+import org.apache.sis.storage.shapefile.dbf.DBFRecord;
+import org.apache.sis.storage.shapefile.shp.ShapeGeometryEncoder;
+import org.apache.sis.storage.shapefile.shp.ShapeHeader;
+import org.apache.sis.storage.shapefile.shp.ShapeReader;
+import org.apache.sis.storage.shapefile.shp.ShapeRecord;
+import org.apache.sis.util.collection.BackingStoreException;
+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.FactoryException;
+import org.opengis.util.GenericName;
+
+/**
+ * Shapefile datastore.
+ * 
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ShapefileStore extends DataStore implements FeatureSet {
+
+    private static final String GEOMETRY_NAME = "geometry";
+
+    private final Path shpPath;
+    private final ShpFiles files;
+    /**
+     * Internal class to inherit AbstractFeatureSet.
+     */
+    private final AsFeatureSet featureSetView = new AsFeatureSet();    
+    private FeatureType type;
+    private Charset charset;
+    
+    /**
+     * Lock to control read and write operations.
+     */
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+    public ShapefileStore(Path path) {
+        this.shpPath = path;
+        this.files = new ShpFiles(shpPath);
+    }
+
+    @Override
+    public Optional<ParameterValueGroup> getOpenParameters() {
+        final Parameters parameters = 
Parameters.castOrWrap(ShapefileProvider.PARAMETERS_DESCRIPTOR.createValue());
+        
parameters.parameter(ShapefileProvider.LOCATION).setValue(shpPath.toUri());
+        return Optional.of(parameters);
+    }
+
+    @Override
+    public void close() throws DataStoreException {
+    }
+
+
+    /*
+    Redirect FeatureSet interface to View
+    */
+    @Override
+    public Optional<GenericName> getIdentifier() throws DataStoreException {
+        return featureSetView.getIdentifier();
+    }
+
+    @Override
+    public Metadata getMetadata() throws DataStoreException {
+        return featureSetView.getMetadata();
+    }
+
+    @Override
+    public FeatureType getType() throws DataStoreException {
+        return featureSetView.getType();
+    }
+
+    @Override
+    public FeatureSet subset(Query query) throws UnsupportedQueryException, 
DataStoreException {
+        return featureSetView.subset(query);
+    }
+
+    @Override
+    public Stream<Feature> features(boolean parallel) throws 
DataStoreException {
+        return featureSetView.features(parallel);
+    }
+
+    @Override
+    public Optional<Envelope> getEnvelope() throws DataStoreException {
+        return featureSetView.getEnvelope();
+    }
+
+    private class AsFeatureSet extends AbstractFeatureSet implements 
WritableFeatureSet {
+
+        private AsFeatureSet() {
+            super(null);
+        }
+
+        @Override
+        public synchronized FeatureType getType() throws DataStoreException {
+            if (type == null) {
+                if (!Files.isRegularFile(shpPath)) {
+                    throw new DataStoreException("Shape files do not exist. 
Update FeatureType first to initialize this empty datastore");
+                }
+
+                final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+                ftb.setName(files.baseName);
+
+                //read shp header to obtain geometry type
+                final Class geometryClass;
+                try (final ShapeReader reader = new 
ShapeReader(ShpFiles.openReadChannel(shpPath))) {
+                    final ShapeHeader header = reader.getHeader();
+                    geometryClass = 
ShapeGeometryEncoder.getEncoder(header.shapeType).getValueClass();
+                } catch (IOException ex) {
+                    throw new DataStoreException("Failed to parse shape file 
header.", ex);
+                }
+
+                //read prj file for projection
+                final Path prjFile = files.getPrj(false);
+                final CoordinateReferenceSystem crs;
+                if (prjFile != null) {
+                    try {
+                        crs = CRS.fromWKT(Files.readString(prjFile, 
StandardCharsets.UTF_8));
+                    } catch (IOException | FactoryException ex) {
+                        throw new DataStoreException("Failed to parse prj 
file.", ex);
+                    }
+                } else {
+                    //shapefile often do not have a .prj, mostly those are in 
CRS:84.
+                    //we do not raise an error otherwise we would not be able 
to read a lot of data.
+                    crs = CommonCRS.WGS84.normalizedGeographic();
+                }
+
+                
ftb.addAttribute(geometryClass).setName(GEOMETRY_NAME).setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+
+                //read cpg for dbf file charset
+                final Path cpgFile = files.getCpg(false);
+                if (cpgFile != null) {
+                    try (final SeekableByteChannel channel = 
Files.newByteChannel(cpgFile, StandardOpenOption.READ)) {
+                        charset = CpgFiles.read(channel);
+                    } catch (IOException ex) {
+                        throw new DataStoreException("Failed to parse cpg 
file.", ex);
+                    }
+                } else {
+                    charset = StandardCharsets.UTF_8;
+                }
+
+                //read dbf for attributes
+                final Path dbfFile = files.getDbf(false);
+                if (dbfFile != null) {
+                    try (DBFReader reader = new 
DBFReader(ShpFiles.openReadChannel(dbfFile), charset)) {
+                        final DBFHeader header = reader.getHeader();
+                        boolean hasId = false;
+                        for (DBFField field : header.fields) {
+                            final AttributeTypeBuilder atb = 
ftb.addAttribute(field.getEncoder().getValueClass()).setName(field.fieldName);
+                            //no official but 'id' field is common
+                            if (!hasId && 
"id".equalsIgnoreCase(field.fieldName) || 
"identifier".equalsIgnoreCase(field.fieldName)) {
+                                
atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+                                hasId = true;
+                            }
+                        }
+                    } catch (IOException ex) {
+                        throw new DataStoreException("Failed to parse dbf file 
header.", ex);
+                    }
+                } else {
+                    throw new DataStoreException("DBF file is missing.");
+                }
+
+                type = ftb.build();
+            }
+            return type;
+        }
+
+        @Override
+        public Stream<Feature> features(boolean parallel) throws 
DataStoreException {
+            final FeatureType type = getType();
+            final ShapeReader shpreader;
+            final DBFReader dbfreader;
+            try {
+                shpreader = new 
ShapeReader(ShpFiles.openReadChannel(files.shpFile));
+                dbfreader = new 
DBFReader(ShpFiles.openReadChannel(files.getDbf(false)), charset);
+            } catch (IOException ex) {
+                throw new DataStoreException("Faild to open shp and dbf 
files.", ex);
+            }
+            final DBFHeader header = dbfreader.getHeader();
+
+            final Spliterator spliterator = new 
Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED) {
+                @Override
+                public boolean tryAdvance(Consumer action) {
+                    try {
+                        final ShapeRecord shpRecord = shpreader.next();
+                        if (shpRecord == null) return false;
+                        final DBFRecord dbfRecord = dbfreader.next();
+                        final Feature next = type.newInstance();
+                        next.setPropertyValue(GEOMETRY_NAME, 
shpRecord.geometry);
+                        for (int i = 0; i < header.fields.length; i++) {
+                            next.setPropertyValue(header.fields[i].fieldName, 
dbfRecord.fields[i]);
+                        }
+                        action.accept(next);
+                        return true;
+                    } catch (IOException ex) {
+                        throw new BackingStoreException(ex.getMessage(), ex);
+                    }
+                }
+            };
+            final Stream<Feature> stream = StreamSupport.stream(spliterator, 
false);
+            return stream.onClose(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        shpreader.close();
+                        dbfreader.close();
+                    } catch (IOException ex) {
+                        throw new BackingStoreException(ex.getMessage(), ex);
+                    }
+                }
+            });
+
+        }
+
+        @Override
+        public void updateType(FeatureType newType) throws DataStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        @Override
+        public void add(Iterator<? extends Feature> features) throws 
DataStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        @Override
+        public void removeIf(Predicate<? super Feature> filter) throws 
DataStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        @Override
+        public void replaceIf(Predicate<? super Feature> filter, 
UnaryOperator<Feature> updater) throws DataStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+    }
+
+    /**
+     * Manipulate the different shape files.
+     */
+    private static class ShpFiles {
+
+        private final String baseName;
+        private final boolean baseUpper;
+        private final Path shpFile;
+        private Path shxFile;
+        private Path dbfFile;
+        private Path prjFile;
+        private Path cpgFile;
+
+        public ShpFiles(Path shpFile) {
+            this.shpFile = shpFile;
+            final String fileName = shpFile.getFileName().toString();
+            baseUpper = 
Character.isUpperCase(fileName.codePointAt(fileName.length()-1));
+            this.baseName = IOUtilities.filenameWithoutExtension(fileName);
+            shxFile = findSibling("shx");
+            dbfFile = findSibling("dbf");
+            prjFile = findSibling("prj");
+            cpgFile = findSibling("cpg");
+        }
+
+        /**
+         * @param create true to create the path even if file do not exist.
+         * @return file if it exist or create is true, null otherwise
+         */
+        public Path getShx(boolean create) {
+            if (create && shxFile == null) {
+                return shpFile.getParent().resolve(baseName + "." + (baseUpper 
? "SHX" : "shx"));
+            }
+            return shxFile;
+        }
+
+        /**
+         * @param create true to create the path even if file do not exist.
+         * @return file if it exist or create is true, null otherwise
+         */
+        public Path getDbf(boolean create) {
+            if (create && dbfFile == null) {
+                return shpFile.getParent().resolve(baseName + "." + (baseUpper 
? "DBF" : "dbf"));
+            }
+            return dbfFile;
+        }
+
+        /**
+         * @param create true to create the path even if file do not exist.
+         * @return file if it exist or create is true, null otherwise
+         */
+        public Path getPrj(boolean create) {
+            if (create && prjFile == null) {
+                return shpFile.getParent().resolve(baseName + "." + (baseUpper 
? "PRJ" : "prj"));
+            }
+            return prjFile;
+        }
+
+        /**
+         * @param create true to create the path even if file do not exist.
+         * @return file if it exist or create is true, null otherwise
+         */
+        public Path getCpg(boolean create) {
+            if (create && cpgFile == null) {
+                return shpFile.getParent().resolve(baseName + "." + (baseUpper 
? "CPG" : "cpg"));
+            }
+            return cpgFile;
+        }
+
+        private Path findSibling(String extension) {
+            Path candidate = shpFile.getParent().resolve(baseName + "." + 
extension);
+            if (java.nio.file.Files.isRegularFile(candidate)) return candidate;
+            candidate = shpFile.getParent().resolve(baseName + "." + 
extension.toUpperCase());
+            if (java.nio.file.Files.isRegularFile(candidate)) return candidate;
+            return null;
+        }
+
+        private static ChannelDataInput openReadChannel(Path path) throws 
IOException {
+            final SeekableByteChannel channel = Files.newByteChannel(path, 
StandardOpenOption.READ);
+            return new ChannelDataInput(path.getFileName().toString(), 
channel, ByteBuffer.allocate(8192), false);
+        }
+
+        private static ChannelDataOutput openWriteChannel(Path path) throws 
IOException, IllegalArgumentException, DataStoreException {
+            final WritableByteChannel wbc = Files.newByteChannel(path, 
StandardOpenOption.WRITE);
+            return new ChannelDataOutput(path.getFileName().toString(), wbc, 
ByteBuffer.allocate(8000));
+        }
+    }
+
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
index c9ccec984f..853bd16391 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
@@ -35,11 +35,12 @@ import org.locationtech.jts.algorithm.RayCrossingCounter;
  *
  * @author Johann Sorel (Geomatys)
  */
-public abstract class ShapeGeometryEncoder {
+public abstract class ShapeGeometryEncoder<T extends Geometry> {
 
     private static final GeometryFactory GF = new GeometryFactory();
 
     protected final int shapeType;
+    protected final Class<T> geometryClass;
     protected final int dimension;
     protected final int measures;
     protected final int nbOrdinates;
@@ -77,8 +78,9 @@ public abstract class ShapeGeometryEncoder {
      * @param dimension number of dimensions in processed geometries.
      * @param measures number of measures in processed geometries.
      */
-    protected ShapeGeometryEncoder(int shapeType, int dimension, int measures) 
{
+    protected ShapeGeometryEncoder(int shapeType, Class<T> geometryClass, int 
dimension, int measures) {
         this.shapeType = shapeType;
+        this.geometryClass = geometryClass;
         this.dimension = dimension;
         this.measures = measures;
         this.nbOrdinates = dimension + measures;
@@ -90,6 +92,13 @@ public abstract class ShapeGeometryEncoder {
     public int getShapeType() {
         return shapeType;
     }
+    
+    /**
+     * @return geometry class handled by this encoder
+     */
+    public Class<T> getValueClass() {
+        return geometryClass;
+    }
 
     /**
      * @return number of dimensions in processed geometries.
@@ -297,12 +306,12 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class Null extends ShapeGeometryEncoder {
+    private static class Null extends ShapeGeometryEncoder<Geometry> {
 
         private static final Null INSTANCE = new Null();
 
         private Null() {
-            super(ShapeType.VALUE_NULL, 2,0);
+            super(ShapeType.VALUE_NULL, Geometry.class, 2, 0);
         }
 
         @Override
@@ -320,12 +329,12 @@ public abstract class ShapeGeometryEncoder {
 
     }
 
-    private static class PointXY extends ShapeGeometryEncoder {
+    private static class PointXY extends ShapeGeometryEncoder<Point> {
 
         private static final PointXY INSTANCE = new PointXY();
 
         private PointXY() {
-            super(ShapeType.VALUE_POINT, 2,0);
+            super(ShapeType.VALUE_POINT, Point.class, 2,0);
         }
 
         @Override
@@ -352,11 +361,11 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class PointXYM extends ShapeGeometryEncoder {
+    private static class PointXYM extends ShapeGeometryEncoder<Point> {
 
         private static final PointXYM INSTANCE = new PointXYM();
         private PointXYM() {
-            super(ShapeType.VALUE_POINT_M, 2,1);
+            super(ShapeType.VALUE_POINT_M, Point.class, 2, 1);
         }
 
         @Override
@@ -386,12 +395,12 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class PointXYZM extends ShapeGeometryEncoder {
+    private static class PointXYZM extends ShapeGeometryEncoder<Point> {
 
         private static final PointXYZM INSTANCE = new PointXYZM();
 
         private PointXYZM() {
-            super(ShapeType.VALUE_POINT_ZM, 3,1);
+            super(ShapeType.VALUE_POINT_ZM, Point.class, 3, 1);
         }
 
         @Override
@@ -424,11 +433,11 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class MultiPointXY extends ShapeGeometryEncoder {
+    private static class MultiPointXY extends ShapeGeometryEncoder<MultiPoint> 
{
 
         private static final MultiPointXY INSTANCE = new MultiPointXY();
         private MultiPointXY() {
-            super(ShapeType.VALUE_MULTIPOINT, 2,0);
+            super(ShapeType.VALUE_MULTIPOINT, MultiPoint.class, 2, 0);
         }
 
         @Override
@@ -460,12 +469,12 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class MultiPointXYM extends ShapeGeometryEncoder {
+    private static class MultiPointXYM extends 
ShapeGeometryEncoder<MultiPoint> {
 
         private static final MultiPointXYM INSTANCE = new MultiPointXYM();
 
         private MultiPointXYM() {
-            super(ShapeType.VALUE_MULTIPOINT_M, 2,1);
+            super(ShapeType.VALUE_MULTIPOINT_M, MultiPoint.class, 2, 1);
         }
 
         @Override
@@ -511,12 +520,12 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class MultiPointXYZM extends ShapeGeometryEncoder {
+    private static class MultiPointXYZM extends 
ShapeGeometryEncoder<MultiPoint> {
 
         private static final MultiPointXYZM INSTANCE = new MultiPointXYZM();
 
         private MultiPointXYZM() {
-            super(ShapeType.VALUE_MULTIPOINT_ZM, 3,1);
+            super(ShapeType.VALUE_MULTIPOINT_ZM, MultiPoint.class, 3, 1);
         }
 
         @Override
@@ -572,14 +581,14 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class Polyline extends ShapeGeometryEncoder {
+    private static class Polyline extends 
ShapeGeometryEncoder<MultiLineString> {
 
         private static final Polyline INSTANCE = new 
Polyline(ShapeType.VALUE_POLYLINE, 2, 0);
         private static final Polyline INSTANCE_M = new 
Polyline(ShapeType.VALUE_POLYLINE_M, 3, 0);
         private static final Polyline INSTANCE_ZM = new 
Polyline(ShapeType.VALUE_POLYLINE_ZM, 3, 1);
 
         private Polyline(int shapeType, int dimension, int measures) {
-            super(shapeType, dimension, measures);
+            super(shapeType, MultiLineString.class, dimension, measures);
         }
 
         @Override
@@ -604,14 +613,14 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class Polygon extends ShapeGeometryEncoder {
+    private static class Polygon extends ShapeGeometryEncoder<MultiPolygon> {
 
         private static final Polygon INSTANCE = new 
Polygon(ShapeType.VALUE_POLYGON, 2, 0);
         private static final Polygon INSTANCE_M = new 
Polygon(ShapeType.VALUE_POLYGON_M, 3, 0);
         private static final Polygon INSTANCE_ZM = new 
Polygon(ShapeType.VALUE_POLYGON_ZM, 3, 1);
 
         private Polygon(int shapeType, int dimension, int measures) {
-            super(shapeType, dimension, measures);
+            super(shapeType, MultiPolygon.class, dimension, measures);
         }
 
         @Override
@@ -641,12 +650,12 @@ public abstract class ShapeGeometryEncoder {
         }
     }
 
-    private static class MultiPatch extends ShapeGeometryEncoder {
+    private static class MultiPatch extends ShapeGeometryEncoder<MultiPolygon> 
{
 
         private static final MultiPatch INSTANCE = new MultiPatch();
 
         private MultiPatch() {
-            super(ShapeType.VALUE_MULTIPATCH_ZM, 3, 1);
+            super(ShapeType.VALUE_MULTIPATCH_ZM, MultiPolygon.class, 3, 1);
         }
 
         @Override
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java
new file mode 100644
index 0000000000..2e02455a92
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java
@@ -0,0 +1,65 @@
+/*
+ * 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.shapefile.shx;
+
+import org.apache.sis.storage.shapefile.shp.*;
+import org.apache.sis.io.stream.ChannelDataInput;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Seekable shx index file reader.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class IndexReader implements AutoCloseable{
+
+    private final ChannelDataInput channel;
+    private final ShapeHeader header;
+
+    public IndexReader(ChannelDataInput channel) throws IOException {
+        this.channel = channel;
+        header = new ShapeHeader();
+        header.read(channel);
+    }
+
+    public ShapeHeader getHeader() {
+        return header;
+    }
+
+    public void moveToOffset(long position) throws IOException {
+        channel.seek(position);
+    }
+
+    /**
+     * @return offset and length of the record in the shp file
+     */
+    public int[] next() throws IOException {
+        try {
+            return channel.readInts(2);
+        } catch (EOFException ex) {
+            //no more records
+            return null;
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        channel.channel.close();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
new file mode 100644
index 0000000000..d8d1503736
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.shapefile;
+
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.util.Iterator;
+import java.util.stream.Stream;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.shapefile.shp.ShapeIOTest;
+import org.junit.Test;
+import org.locationtech.jts.geom.Point;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ShapefileStoreTest {
+
+    @Test
+    public void testStream() throws URISyntaxException, DataStoreException {
+        final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
+        final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()));
+
+        //check feature type
+        final FeatureType type = store.getType();
+        assertEquals("point", type.getName().toString());
+        assertEquals(9, type.getProperties(true).size());
+        assertNotNull(type.getProperty("sis:identifier"));
+        assertNotNull(type.getProperty("sis:envelope"));
+        assertNotNull(type.getProperty("sis:geometry"));
+        final AttributeType geomProp = (AttributeType) 
type.getProperty("geometry");
+        final AttributeType idProp = (AttributeType) type.getProperty("id");
+        final AttributeType textProp = (AttributeType) 
type.getProperty("text");
+        final AttributeType integerProp = (AttributeType) 
type.getProperty("integer");
+        final AttributeType floatProp = (AttributeType) 
type.getProperty("float");
+        final AttributeType dateProp = (AttributeType) 
type.getProperty("date");
+        assertEquals(Point.class, geomProp.getValueClass());
+        assertEquals(Long.class, idProp.getValueClass());
+        assertEquals(String.class, textProp.getValueClass());
+        assertEquals(Long.class, integerProp.getValueClass());
+        assertEquals(Double.class, floatProp.getValueClass());
+        assertEquals(LocalDate.class, dateProp.getValueClass());
+
+        try (Stream<Feature> stream = store.features(false)) {
+            Iterator<Feature> iterator = stream.iterator();
+            assertTrue(iterator.hasNext());
+            Feature feature1 = iterator.next();
+            assertEquals(1L, feature1.getPropertyValue("id"));
+            assertEquals("text1", feature1.getPropertyValue("text"));
+            assertEquals(10L, feature1.getPropertyValue("integer"));
+            assertEquals(20.0, feature1.getPropertyValue("float"));
+            assertEquals(LocalDate.of(2023, 10, 27), 
feature1.getPropertyValue("date"));
+            Point pt1 = (Point) feature1.getPropertyValue("geometry");
+
+            assertTrue(iterator.hasNext());
+            Feature feature2 = iterator.next();
+            assertEquals(2L, feature2.getPropertyValue("id"));
+            assertEquals("text2", feature2.getPropertyValue("text"));
+            assertEquals(40L, feature2.getPropertyValue("integer"));
+            assertEquals(60.0, feature2.getPropertyValue("float"));
+            assertEquals(LocalDate.of(2023, 10, 28), 
feature2.getPropertyValue("date"));
+            Point pt2 = (Point) feature2.getPropertyValue("geometry");
+
+            
+            assertFalse(iterator.hasNext());
+        }
+
+    }
+
+}


Reply via email to