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 851bb80250 feat(Shapefile): add shp file read and write support
851bb80250 is described below

commit 851bb802509eb80758a42f814487e58ad0d9c1a5
Author: jsorel <[email protected]>
AuthorDate: Tue Oct 31 11:01:53 2023 +0100

    feat(Shapefile): add shp file read and write support
---
 .../org.apache.sis.storage/main/module-info.java   |   1 +
 .../main/module-info.java                          |   1 +
 .../shapefile/shp/ShapeGeometryEncoder.java        | 668 +++++++++++++++++++++
 .../sis/storage/shapefile/shp/ShapeHeader.java     | 113 ++++
 .../sis/storage/shapefile/shp/ShapeReader.java     |  66 ++
 .../sis/storage/shapefile/shp/ShapeRecord.java     |  78 +++
 .../sis/storage/shapefile/shp/ShapeType.java       |  89 +++
 .../sis/storage/shapefile/shp/ShapeWriter.java     |  65 ++
 .../apache/sis/storage/shapefile/multipoint.cpg    |   1 +
 .../apache/sis/storage/shapefile/multipoint.dbf    | Bin 0 -> 88 bytes
 .../apache/sis/storage/shapefile/multipoint.prj    |   1 +
 .../apache/sis/storage/shapefile/multipoint.shp    | Bin 0 -> 260 bytes
 .../apache/sis/storage/shapefile/multipoint.shx    | Bin 0 -> 116 bytes
 .../org/apache/sis/storage/shapefile/point.cpg     |   1 +
 .../org/apache/sis/storage/shapefile/point.dbf     | Bin 0 -> 434 bytes
 .../org/apache/sis/storage/shapefile/point.prj     |   1 +
 .../org/apache/sis/storage/shapefile/point.shp     | Bin 0 -> 156 bytes
 .../org/apache/sis/storage/shapefile/point.shx     | Bin 0 -> 116 bytes
 .../org/apache/sis/storage/shapefile/polygon.cpg   |   1 +
 .../org/apache/sis/storage/shapefile/polygon.dbf   | Bin 0 -> 88 bytes
 .../org/apache/sis/storage/shapefile/polygon.prj   |   1 +
 .../org/apache/sis/storage/shapefile/polygon.shp   | Bin 0 -> 456 bytes
 .../org/apache/sis/storage/shapefile/polygon.shx   | Bin 0 -> 116 bytes
 .../org/apache/sis/storage/shapefile/polyline.cpg  |   1 +
 .../org/apache/sis/storage/shapefile/polyline.dbf  | Bin 0 -> 88 bytes
 .../org/apache/sis/storage/shapefile/polyline.prj  |   1 +
 .../org/apache/sis/storage/shapefile/polyline.shp  | Bin 0 -> 328 bytes
 .../org/apache/sis/storage/shapefile/polyline.shx  | Bin 0 -> 116 bytes
 .../sis/storage/shapefile/shp/ShapeIOTest.java     | 319 ++++++++++
 29 files changed, 1408 insertions(+)

diff --git a/endorsed/src/org.apache.sis.storage/main/module-info.java 
b/endorsed/src/org.apache.sis.storage/main/module-info.java
index adf86a6b61..0866bdc06c 100644
--- a/endorsed/src/org.apache.sis.storage/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage/main/module-info.java
@@ -65,6 +65,7 @@ module org.apache.sis.storage {
             org.apache.sis.storage.netcdf,
             org.apache.sis.storage.geotiff,
             org.apache.sis.storage.coveragejson,        // In the "incubator" 
sub-project.
+            org.apache.sis.storage.shapefile,           // In the "incubator" 
sub-project.
             org.apache.sis.cloud.aws,
             org.apache.sis.gui;                         // In the "optional" 
sub-project.
 
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/module-info.java 
b/incubator/src/org.apache.sis.storage.shapefile/main/module-info.java
index 7a525d550b..2aad9ecdde 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/main/module-info.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/module-info.java
@@ -28,4 +28,5 @@ module org.apache.sis.storage.shapefile {
 
     exports org.apache.sis.storage.shapefile;
     exports org.apache.sis.storage.shapefile.cpg;
+    exports org.apache.sis.storage.shapefile.shp;
 }
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
new file mode 100644
index 0000000000..c9ccec984f
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
@@ -0,0 +1,668 @@
+/*
+ * 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.shp;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.io.stream.ChannelDataOutput;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
+import org.locationtech.jts.algorithm.Orientation;
+import org.locationtech.jts.algorithm.RayCrossingCounter;
+
+/**
+ * Encoders and decoders for shape types.
+ * This class should be kept separate because I might be used in ESRI 
geodatabase format.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public abstract class ShapeGeometryEncoder {
+
+    private static final GeometryFactory GF = new GeometryFactory();
+
+    protected final int shapeType;
+    protected final int dimension;
+    protected final int measures;
+    protected final int nbOrdinates;
+
+    /**
+     *
+     * @param shapeType shape type to encode
+     * @return requested encoder
+     */
+    public static ShapeGeometryEncoder getEncoder(int shapeType) {
+        switch(shapeType) {
+            //2D
+            case ShapeType.VALUE_NULL: return Null.INSTANCE;
+            case ShapeType.VALUE_POINT: return PointXY.INSTANCE;
+            case ShapeType.VALUE_POLYLINE: return Polyline.INSTANCE;
+            case ShapeType.VALUE_POLYGON: return Polygon.INSTANCE;
+            case ShapeType.VALUE_MULTIPOINT: return MultiPointXY.INSTANCE;
+            //2D+1
+            case ShapeType.VALUE_POINT_M: return PointXYM.INSTANCE;
+            case ShapeType.VALUE_POLYLINE_M: return Polyline.INSTANCE_M;
+            case ShapeType.VALUE_POLYGON_M: return Polygon.INSTANCE_M;
+            case ShapeType.VALUE_MULTIPOINT_M: return MultiPointXYM.INSTANCE;
+            //3D+1
+            case ShapeType.VALUE_POINT_ZM: return PointXYZM.INSTANCE;
+            case ShapeType.VALUE_POLYLINE_ZM: return Polyline.INSTANCE_ZM;
+            case ShapeType.VALUE_POLYGON_ZM: return Polygon.INSTANCE_ZM;
+            case ShapeType.VALUE_MULTIPOINT_ZM: return MultiPointXYZM.INSTANCE;
+            case ShapeType.VALUE_MULTIPATCH_ZM: return MultiPatch.INSTANCE;
+            default: throw new IllegalArgumentException("unknown shape type");
+        }
+    }
+
+    /**
+     * @param shapeType shape type code.
+     * @param dimension number of dimensions in processed geometries.
+     * @param measures number of measures in processed geometries.
+     */
+    protected ShapeGeometryEncoder(int shapeType, int dimension, int measures) 
{
+        this.shapeType = shapeType;
+        this.dimension = dimension;
+        this.measures = measures;
+        this.nbOrdinates = dimension + measures;
+    }
+
+    /**
+     * @return shape type code.
+     */
+    public int getShapeType() {
+        return shapeType;
+    }
+
+    /**
+     * @return number of dimensions in processed geometries.
+     */
+    public final int getDimension() {
+        return dimension;
+    }
+
+    /**
+     * @return number of measures in processed geometries.
+     */
+    public final int getMeasures() {
+        return measures;
+    }
+
+    public abstract void decode(ChannelDataInput ds, ShapeRecord record) 
throws IOException;
+
+    public abstract void encode(ChannelDataOutput ds, ShapeRecord shape) 
throws IOException;
+
+    /**
+     * Compute the encoded size of a geometry.
+     * @param geom to estimate
+     * @return geometry size in bytes once encoded.
+     */
+    public abstract int getEncodedLength(Geometry geom);
+
+    protected void readBBox2D(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+        shape.bbox = new GeneralEnvelope(getDimension());
+        shape.bbox.getLowerCorner().setOrdinate(0, ds.readDouble());
+        shape.bbox.getLowerCorner().setOrdinate(1, ds.readDouble());
+        shape.bbox.getUpperCorner().setOrdinate(0, ds.readDouble());
+        shape.bbox.getUpperCorner().setOrdinate(1, ds.readDouble());
+    }
+
+    protected void writeBBox2D(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+        ds.writeDouble(shape.bbox.getMinimum(0));
+        ds.writeDouble(shape.bbox.getMinimum(1));
+        ds.writeDouble(shape.bbox.getMaximum(0));
+        ds.writeDouble(shape.bbox.getMaximum(1));
+    }
+
+    protected LineString[] readLines(ChannelDataInput ds, ShapeRecord shape, 
boolean asRing) throws IOException {
+        readBBox2D(ds, shape);
+        final int numParts = ds.readInt();
+        final int numPoints = ds.readInt();
+        final int[] offsets = ds.readInts(numParts);
+
+        final LineString[] lines = new LineString[numParts];
+
+        //XY
+        for (int i = 0; i < numParts; i++) {
+            final int nbValues = (i == numParts - 1) ? numPoints - offsets[i] 
: offsets[i + 1] - offsets[i];
+            final double[] values;
+            if (nbOrdinates == 2) {
+                values = ds.readDoubles(nbValues * 2);
+            } else {
+                values = ds.readDoubles(nbValues * nbOrdinates);
+                for (int k = 0; k < nbValues; k++) {
+                    values[k * nbOrdinates  ] = ds.readDouble();
+                    values[k * nbOrdinates + 1] = ds.readDouble();
+                }
+            }
+            final PackedCoordinateSequence.Double pc = new 
PackedCoordinateSequence.Double(values, getDimension(), getMeasures());
+            lines[i] = asRing ? GF.createLinearRing(pc) : 
GF.createLineString(pc);
+        }
+        //Z and M
+        if (nbOrdinates >= 3)  readLineOrdinates(ds, shape, lines, 2);
+        if (nbOrdinates == 4)  readLineOrdinates(ds, shape, lines, 3);
+        return lines;
+    }
+
+    protected void readLineOrdinates(ChannelDataInput ds, ShapeRecord shape, 
LineString[] lines, int ordinateIndex) throws IOException {
+        final int nbDim = getDimension() + getMeasures();
+        shape.bbox.setRange(ordinateIndex, ds.readDouble(), ds.readDouble());
+        for (LineString line : lines) {
+            final double[] values = ((PackedCoordinateSequence.Double) 
line.getCoordinateSequence()).getRawCoordinates();
+            final int nbValues = values.length / nbDim;
+            for (int k = 0; k < nbValues; k++) {
+                values[k * nbDim + ordinateIndex] = ds.readDouble();
+            }
+        }
+    }
+
+    protected void writeLines(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+        writeBBox2D(ds, shape);
+        final List<LineString> lines = extractRings(shape.geometry);
+        final int nbLines = lines.size();
+        final int[] offsets = new int[nbLines];
+        int nbPts = 0;
+        //first loop write offsets
+        for (int i = 0; i < nbLines; i++) {
+            final LineString line = lines.get(i);
+            offsets[i] = nbPts;
+            nbPts += line.getCoordinateSequence().size();
+        }
+        ds.writeInt(nbLines);
+        ds.writeInt(nbPts);
+        ds.writeInts(offsets);
+
+        //second loop write points
+        for (int i = 0; i < nbLines; i++) {
+            final LineString line = lines.get(i);
+            final CoordinateSequence cs = line.getCoordinateSequence();
+            for (int k = 0, kn =cs.size(); k < kn; k++) {
+                ds.writeDouble(cs.getX(k));
+                ds.writeDouble(cs.getY(k));
+            }
+        }
+
+        //Z and M
+        if (nbOrdinates >= 3)  writeLineOrdinates(ds, shape, lines, 2);
+        if (nbOrdinates == 4)  writeLineOrdinates(ds, shape, lines, 3);
+    }
+
+    protected void writeLineOrdinates(ChannelDataOutput ds, ShapeRecord 
shape,List<LineString> lines, int ordinateIndex) throws IOException {
+        ds.writeDouble(shape.bbox.getMinimum(ordinateIndex));
+        ds.writeDouble(shape.bbox.getMaximum(ordinateIndex));
+        for (LineString line : lines) {
+            final CoordinateSequence cs = line.getCoordinateSequence();
+            for (int k = 0, kn =cs.size(); k < kn; k++) {
+                ds.writeDouble(cs.getOrdinate(k, ordinateIndex));
+            }
+        }
+    }
+
+    protected List<LineString> extractRings(Geometry geom) {
+        final List<LineString> lst = new ArrayList();
+        extractRings(geom, lst);
+        return lst;
+    }
+
+    private void extractRings(Geometry geom, List lst) {
+        if (geom instanceof GeometryCollection) {
+            final GeometryCollection gc = (GeometryCollection) geom;
+            for (int i = 0, n = gc.getNumGeometries(); i < n; i++) {
+                extractRings(gc.getGeometryN(i), lst);
+            }
+        } else if (geom instanceof org.locationtech.jts.geom.Polygon) {
+            final org.locationtech.jts.geom.Polygon poly = 
(org.locationtech.jts.geom.Polygon) geom;
+            lst.add(poly.getExteriorRing());
+            for (int i = 0, n = poly.getNumInteriorRing(); i < n; i++) {
+                lst.add(poly.getInteriorRingN(i));
+            }
+        } else if (geom instanceof LineString) {
+            lst.add(geom);
+        } else {
+            throw new RuntimeException("Unexpected geometry type "+geom);
+        }
+    }
+
+    protected MultiPolygon rebuild(List<LinearRing> rings) {
+
+        final int nbRing = rings.size();
+        if (nbRing == 0) {
+            return GF.createMultiPolygon();
+        } else if (rings.size() == 1) {
+            return GF.createMultiPolygon(new 
org.locationtech.jts.geom.Polygon[]{
+                    GF.createPolygon(rings.get(0))});
+        } else {
+            /*
+             * In the specification, outer rings should be in clockwise 
orientation and holes in
+             * counterclockwise.
+             */
+            final List<LinearRing> outers = new ArrayList<>();
+            final List<LinearRing> inners = new ArrayList<>();
+            for (LinearRing ls : rings) {
+                if (Orientation.isCCW(ls.getCoordinateSequence())) {
+                    inners.add(ls);
+                } else {
+                    outers.add(ls);
+                }
+            }
+            if (outers.isEmpty()) {
+                //no exterior ? bad geometry, let's consider all inner loops 
as outer
+                outers.addAll(inners);
+                inners.clear();
+            }
+
+            //build the exterior polygon
+            if (inners.isEmpty()) {
+                return 
GF.createMultiPolygon(outers.stream().map(GF::createPolygon).toArray(org.locationtech.jts.geom.Polygon[]::new));
+            }
+
+            //find which hole goes into each exterior
+            final List<org.locationtech.jts.geom.Polygon> polygones = new 
ArrayList<>(outers.size());
+            for (LinearRing out : outers) {
+                final List<LinearRing> holes = new ArrayList<>();
+                for (int i = inners.size() - 1; i >= 0; i--) {
+                    final LinearRing in = inners.get(i);
+                    final Coordinate aPt = 
in.getCoordinateSequence().getCoordinate(0);
+                    if (RayCrossingCounter.locatePointInRing(aPt, 
out.getCoordinateSequence()) != Location.EXTERIOR) {
+                        //consider the ring to be inside
+                        holes.add(inners.remove(i));
+                    }
+                }
+                polygones.add(GF.createPolygon(out, 
GeometryFactory.toLinearRingArray(holes)));
+            }
+
+            //handle unused inners rings as exteriors
+            for (LinearRing r : inners) {
+                polygones.add(GF.createPolygon(r));
+            }
+
+            return 
GF.createMultiPolygon(GeometryFactory.toPolygonArray(polygones));
+        }
+    }
+
+    private static class Null extends ShapeGeometryEncoder {
+
+        private static final Null INSTANCE = new Null();
+
+        private Null() {
+            super(ShapeType.VALUE_NULL, 2,0);
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 0;
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+        }
+
+    }
+
+    private static class PointXY extends ShapeGeometryEncoder {
+
+        private static final PointXY INSTANCE = new PointXY();
+
+        private PointXY() {
+            super(ShapeType.VALUE_POINT, 2,0);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            shape.bbox = new GeneralEnvelope(2);
+            final double x = ds.readDouble();
+            final double y = ds.readDouble();
+            shape.bbox.setRange(0, x, x);
+            shape.bbox.setRange(1, y, y);
+            shape.geometry = GF.createPoint(new CoordinateXY(x, y));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            final Point pt = (Point) shape.geometry;
+            final Coordinate coord = pt.getCoordinate();
+            ds.writeDouble(coord.getX());
+            ds.writeDouble(coord.getY());
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 2*8; //2 ordinates
+        }
+    }
+
+    private static class PointXYM extends ShapeGeometryEncoder {
+
+        private static final PointXYM INSTANCE = new PointXYM();
+        private PointXYM() {
+            super(ShapeType.VALUE_POINT_M, 2,1);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            shape.bbox = new GeneralEnvelope(3);
+            final double x = ds.readDouble();
+            final double y = ds.readDouble();
+            final double z = ds.readDouble();
+            shape.bbox.setRange(0, x, x);
+            shape.bbox.setRange(1, y, y);
+            shape.bbox.setRange(2, z, z);
+            shape.geometry = GF.createPoint(new CoordinateXYM(x, y, z));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            final Point pt = (Point) shape.geometry;
+            final Coordinate coord = pt.getCoordinate();
+            ds.writeDouble(coord.getX());
+            ds.writeDouble(coord.getY());
+            ds.writeDouble(coord.getM());
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 3*8; //3 ordinates
+        }
+    }
+
+    private static class PointXYZM extends ShapeGeometryEncoder {
+
+        private static final PointXYZM INSTANCE = new PointXYZM();
+
+        private PointXYZM() {
+            super(ShapeType.VALUE_POINT_ZM, 3,1);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            shape.bbox = new GeneralEnvelope(4);
+            final double x = ds.readDouble();
+            final double y = ds.readDouble();
+            final double z = ds.readDouble();
+            final double m = ds.readDouble();
+            shape.bbox.setRange(0, x, x);
+            shape.bbox.setRange(1, y, y);
+            shape.bbox.setRange(2, z, z);
+            shape.bbox.setRange(3, m, m);
+            shape.geometry = GF.createPoint(new CoordinateXYZM(x, y, z, m));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            final Point pt = (Point) shape.geometry;
+            final Coordinate coord = pt.getCoordinate();
+            ds.writeDouble(coord.getX());
+            ds.writeDouble(coord.getY());
+            ds.writeDouble(coord.getZ());
+            ds.writeDouble(coord.getM());
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 4*8; //4 ordinates
+        }
+    }
+
+    private static class MultiPointXY extends ShapeGeometryEncoder {
+
+        private static final MultiPointXY INSTANCE = new MultiPointXY();
+        private MultiPointXY() {
+            super(ShapeType.VALUE_MULTIPOINT, 2,0);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            readBBox2D(ds, shape);
+            int nbPt = ds.readInt();
+            final double[] coords = ds.readDoubles(nbPt * 2);
+            shape.geometry = GF.createMultiPoint(new 
PackedCoordinateSequence.Double(coords,2,0));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            writeBBox2D(ds, shape);
+            final MultiPoint geometry = (MultiPoint) shape.geometry;
+            final int nbPts = geometry.getNumGeometries();
+            ds.writeInt(nbPts);
+            for (int i = 0; i < nbPts; i++) {
+                final Point pt = (Point) geometry.getGeometryN(i);
+                ds.writeDouble(pt.getX());
+                ds.writeDouble(pt.getY());
+            }
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 4 * 8 //bbox
+                 + 4 //nbPts
+                 + ((MultiPoint) geom).getNumGeometries() * 2 * 8;
+        }
+    }
+
+    private static class MultiPointXYM extends ShapeGeometryEncoder {
+
+        private static final MultiPointXYM INSTANCE = new MultiPointXYM();
+
+        private MultiPointXYM() {
+            super(ShapeType.VALUE_MULTIPOINT_M, 2,1);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            readBBox2D(ds, shape);
+            int nbPt = ds.readInt();
+            final double[] coords = new double[nbPt * 3];
+            for (int i = 0; i < nbPt; i++) {
+                coords[i * 3    ] = ds.readDouble();
+                coords[i * 3 + 1] = ds.readDouble();
+            }
+            shape.bbox.setRange(2, ds.readDouble(), ds.readDouble());
+            for (int i = 0; i < nbPt; i++) {
+                coords[i * 3 + 2] = ds.readDouble();
+            }
+            shape.geometry = GF.createMultiPoint(new 
PackedCoordinateSequence.Double(coords, 2, 1));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            writeBBox2D(ds, shape);
+            final MultiPoint geometry = (MultiPoint) shape.geometry;
+            final int nbPts = geometry.getNumGeometries();
+            ds.writeInt(nbPts);
+            for (int i = 0; i < nbPts; i++) {
+                final Point pt = (Point) geometry.getGeometryN(i);
+                ds.writeDouble(pt.getX());
+                ds.writeDouble(pt.getY());
+            }
+            ds.writeDouble(shape.bbox.getMinimum(2));
+            ds.writeDouble(shape.bbox.getMaximum(2));
+            for (int i = 0; i < nbPts; i++) {
+                final Point pt = (Point) geometry.getGeometryN(i);
+                ds.writeDouble(pt.getCoordinate().getM());
+            }
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 6 * 8 //bbox
+                 + 4 //nbPts
+                 + ((MultiPoint) geom).getNumGeometries() * 3 * 8;
+        }
+    }
+
+    private static class MultiPointXYZM extends ShapeGeometryEncoder {
+
+        private static final MultiPointXYZM INSTANCE = new MultiPointXYZM();
+
+        private MultiPointXYZM() {
+            super(ShapeType.VALUE_MULTIPOINT_ZM, 3,1);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            readBBox2D(ds, shape);
+            int nbPt = ds.readInt();
+            final double[] coords = new double[nbPt * 4];
+            for (int i = 0; i < nbPt; i++) {
+                coords[i * 4    ] = ds.readDouble();
+                coords[i * 4 + 1] = ds.readDouble();
+            }
+            shape.bbox.setRange(2, ds.readDouble(), ds.readDouble());
+            for (int i = 0; i < nbPt; i++) {
+                coords[i * 4 + 2] = ds.readDouble();
+            }
+            shape.bbox.setRange(3, ds.readDouble(), ds.readDouble());
+            for (int i = 0; i < nbPt; i++) {
+                coords[i * 4 + 3] = ds.readDouble();
+            }
+            shape.geometry = GF.createMultiPoint(new 
PackedCoordinateSequence.Double(coords, 3, 1));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            writeBBox2D(ds, shape);
+            final MultiPoint geometry = (MultiPoint) shape.geometry;
+            final int nbPts = geometry.getNumGeometries();
+            ds.writeInt(nbPts);
+            for (int i = 0; i < nbPts; i++) {
+                final Point pt = (Point) geometry.getGeometryN(i);
+                ds.writeDouble(pt.getX());
+                ds.writeDouble(pt.getY());
+            }
+            ds.writeDouble(shape.bbox.getMinimum(2));
+            ds.writeDouble(shape.bbox.getMaximum(2));
+            for (int i = 0; i < nbPts; i++) {
+                final Point pt = (Point) geometry.getGeometryN(i);
+                ds.writeDouble(pt.getCoordinate().getZ());
+            }
+            ds.writeDouble(shape.bbox.getMinimum(3));
+            ds.writeDouble(shape.bbox.getMaximum(3));
+            for (int i = 0; i < nbPts; i++) {
+                final Point pt = (Point) geometry.getGeometryN(i);
+                ds.writeDouble(pt.getCoordinate().getM());
+            }
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            return 8 * 8 //box
+                 + 4 //nbPts
+                 + ((MultiPoint) geom).getNumGeometries() * 4 * 8;
+        }
+    }
+
+    private static class Polyline extends ShapeGeometryEncoder {
+
+        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);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            shape.geometry = GF.createMultiLineString(readLines(ds, shape, 
false));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            writeLines(ds, shape);
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            final MultiLineString ml = (MultiLineString) geom;
+            final int nbGeom = ml.getNumGeometries();
+            final int nbPoints = ml.getNumPoints();
+            return nbOrdinates * 2 * 8 //bbox
+                 + 4 * 2 //num parts and num points
+                 + nbGeom * 4 //offsets table
+                 + nbPoints * nbOrdinates * 8; //all ordinates
+        }
+    }
+
+    private static class Polygon extends ShapeGeometryEncoder {
+
+        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);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            final LineString[] rings = readLines(ds, shape, true);
+            shape.geometry = 
rebuild(Stream.of(rings).map(LinearRing.class::cast).collect(Collectors.toList()));
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            writeLines(ds, shape);
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            final MultiPolygon mp = (MultiPolygon) geom;
+            int nbGeom = mp.getNumGeometries();
+            for (int i = 0, n = nbGeom; i < n; i++) {
+                org.locationtech.jts.geom.Polygon polygon = 
(org.locationtech.jts.geom.Polygon) mp.getGeometryN(i);
+                nbGeom += polygon.getNumInteriorRing();
+            }
+            final int nbPoints = mp.getNumPoints();
+            return nbOrdinates * 2 * 8 //bbox
+                 + 4 * 2 //num parts and num points
+                 + nbGeom * 4 //offsets table
+                 + nbPoints * nbOrdinates * 8; //all ordinates
+        }
+    }
+
+    private static class MultiPatch extends ShapeGeometryEncoder {
+
+        private static final MultiPatch INSTANCE = new MultiPatch();
+
+        private MultiPatch() {
+            super(ShapeType.VALUE_MULTIPATCH_ZM, 3, 1);
+        }
+
+        @Override
+        public void decode(ChannelDataInput ds, ShapeRecord shape) throws 
IOException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        @Override
+        public void encode(ChannelDataOutput ds, ShapeRecord shape) throws 
IOException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        @Override
+        public int getEncodedLength(Geometry geom) {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+    }
+
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeHeader.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeHeader.java
new file mode 100644
index 0000000000..b0413d7150
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeHeader.java
@@ -0,0 +1,113 @@
+/*
+ * 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.shp;
+
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.io.stream.ChannelDataOutput;
+import org.opengis.geometry.Envelope;
+import java.io.IOException;
+import java.nio.ByteOrder;
+
+/**
+ * Shapefile header.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @see <a 
href="http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf";>ESRI 
Shapefile Specification</a>
+ */
+public final class ShapeHeader {
+
+    /**
+     * Constant header length.
+     */
+    public static final int HEADER_LENGTH = 100;
+    /**
+     * Shapefile header signature.
+     */
+    public static final int SIGNATURE = 9994;
+
+    /**
+     * File size.
+     */
+    public int fileLength;
+    /**
+     * Shape type.
+     */
+    public int shapeType;
+    /**
+     * Shapefile bounding box without CRS.
+     * Ordinates are in X,Y,Z,M order.
+     */
+    public Envelope bbox;
+
+    /**
+     * Read shapefile header.
+     * @param channel input channel, not null
+     * @throws IOException if an error occurred while reading.
+     */
+    public void read(final ChannelDataInput channel) throws IOException {
+        final long position = channel.getStreamPosition();
+        channel.buffer.order(ByteOrder.BIG_ENDIAN);
+        //check signature
+        if (channel.readInt() != SIGNATURE) {
+            throw new IOException("Incorrect file signature");
+        }
+        //skip unused datas
+        channel.skipBytes(5*4);
+        fileLength = channel.readInt();
+
+        channel.buffer.order(ByteOrder.LITTLE_ENDIAN);
+        final int version = channel.readInt();
+        if (version != 1000) {
+            throw new IOException("Incorrect file version, expected 1000 but 
was " + version);
+        }
+        shapeType = channel.readInt();
+        final double[] bb = channel.readDoubles(8);
+        GeneralEnvelope bbox = new GeneralEnvelope(4);
+        bbox.setRange(0, bb[0], bb[2]);
+        bbox.setRange(1, bb[1], bb[3]);
+        bbox.setRange(2, bb[4], bb[5]);
+        bbox.setRange(3, bb[6], bb[7]);
+        this.bbox = new ImmutableEnvelope(bbox);
+    }
+
+    /**
+     * Write shapefile header.
+     * @param channel output channel, not null
+     * @throws IOException if an error occurred while writing.
+     */
+    public void write(ChannelDataOutput channel) throws IOException {
+        channel.buffer.order(ByteOrder.BIG_ENDIAN);
+        channel.writeInt(SIGNATURE);
+        channel.write(new byte[5*4]);
+        channel.writeInt(fileLength);
+        channel.buffer.order(ByteOrder.LITTLE_ENDIAN);
+        channel.writeInt(1000);
+        channel.writeInt(shapeType);
+        channel.writeDouble(bbox.getMinimum(0));
+        channel.writeDouble(bbox.getMinimum(1));
+        channel.writeDouble(bbox.getMaximum(0));
+        channel.writeDouble(bbox.getMaximum(1));
+        channel.writeDouble(bbox.getMinimum(2));
+        channel.writeDouble(bbox.getMaximum(2));
+        channel.writeDouble(bbox.getMinimum(3));
+        channel.writeDouble(bbox.getMaximum(3));
+        channel.flush();
+    }
+
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeReader.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeReader.java
new file mode 100644
index 0000000000..904a02934f
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeReader.java
@@ -0,0 +1,66 @@
+/*
+ * 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.shp;
+
+import org.apache.sis.io.stream.ChannelDataInput;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Seekable shape file reader.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ShapeReader implements AutoCloseable{
+
+    private final ChannelDataInput channel;
+    private final ShapeHeader header;
+    private final ShapeGeometryEncoder geomParser;
+
+    public ShapeReader(ChannelDataInput channel) throws IOException {
+        this.channel = channel;
+        header = new ShapeHeader();
+        header.read(channel);
+        geomParser = ShapeGeometryEncoder.getEncoder(header.shapeType);
+    }
+
+    public ShapeHeader getHeader() {
+        return header;
+    }
+
+    public void moveToOffset(long position) throws IOException {
+        channel.seek(position);
+    }
+
+    public ShapeRecord next() throws IOException {
+        try {
+            final ShapeRecord record = new ShapeRecord();
+            record.read(channel);
+            record.parseGeometry(geomParser);
+            return record;
+        } 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/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
new file mode 100644
index 0000000000..8e10ceaf46
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
@@ -0,0 +1,78 @@
+/*
+ * 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.shp;
+
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.io.stream.ChannelDataOutput;
+import org.locationtech.jts.geom.Geometry;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ShapeRecord {
+
+    /**
+     * Record number.
+     */
+    public int recordNumber;
+    /**
+     * Encoded geometry.
+     */
+    public byte[] content;
+
+    public Geometry geometry;
+
+    public GeneralEnvelope bbox;
+
+    /**
+     * Read this shape record.
+     * @param channel input channel, not null
+     * @throws IOException if an error occurred while reading.
+     */
+    public void read(final ChannelDataInput channel) throws IOException {
+        channel.buffer.order(ByteOrder.BIG_ENDIAN);
+        recordNumber = channel.readInt();
+        content = channel.readBytes(channel.readInt() * 2); // x2 because size 
is in 16bit words
+    }
+
+    public void parseGeometry(ShapeGeometryEncoder io) throws IOException {
+        final ChannelDataInput di = new ChannelDataInput("", 
ByteBuffer.wrap(content));
+        di.buffer.order(ByteOrder.LITTLE_ENDIAN);
+        int shapeType = di.readInt();
+        io.decode(di,this);
+    }
+
+    /**
+     * Write this shape record.
+     * @param channel output channel to write into, not null
+     * @param io geometry encoder
+     * @throws IOException
+     */
+    public void write(ChannelDataOutput channel, ShapeGeometryEncoder io) 
throws IOException {
+        channel.buffer.order(ByteOrder.BIG_ENDIAN);
+        channel.writeInt(recordNumber);
+        channel.writeInt((io.getEncodedLength(geometry) + 4) / 2); // +4 for 
shape type and /2 because size is in 16bit words
+        channel.buffer.order(ByteOrder.LITTLE_ENDIAN);
+        channel.writeInt(io.getShapeType());
+        io.encode(channel, this);
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeType.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeType.java
new file mode 100644
index 0000000000..defcfdd89f
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeType.java
@@ -0,0 +1,89 @@
+/*
+ * 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.shp;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides a ShapefileType Enumeration.
+ *
+ * The names diverge from the specification on names.
+ * The specification says a PointZ has X/Y dimensions and 1 measure so we 
renamed it to Point_M for the sake of coherency.
+ * The same applies to PointM renamed Point_ZM since it has X/Y/Z dimensions 
and 1 measure.
+ *
+ * @author Travis L. Pinney
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see <a 
href="http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf";>ESRI 
Shapefile Specification</a>
+ */
+public enum ShapeType {
+
+    NULL (0),
+    POINT(1),
+    POLYLINE(3),
+    POLYGON(5),
+    MULTIPOINT(8),
+    POINT_M(11),
+    POLYLINE_M(13),
+    POLYGON_M(15),
+    MULTIPOINT_M(18),
+    POINT_ZM(21),
+    POLYLINE_ZM(23),
+    POLYGON_ZM(25),
+    MULTIPOINT_ZM(28),
+    MULTIPATCH_ZM(31);
+
+    public static final int VALUE_NULL = 0;
+    public static final int VALUE_POINT = 1;
+    public static final int VALUE_POLYLINE = 3;
+    public static final int VALUE_POLYGON = 5;
+    public static final int VALUE_MULTIPOINT = 8;
+    public static final int VALUE_POINT_M = 11;
+    public static final int VALUE_POLYLINE_M = 13;
+    public static final int VALUE_POLYGON_M = 15;
+    public static final int VALUE_MULTIPOINT_M = 18;
+    public static final int VALUE_POINT_ZM = 21;
+    public static final int VALUE_POLYLINE_ZM = 23;
+    public static final int VALUE_POLYGON_ZM = 25;
+    public static final int VALUE_MULTIPOINT_ZM = 28;
+    public static final int VALUE_MULTIPATCH_ZM = 31;
+
+    // used for initializing the enumeration
+    public final int value;
+
+    private ShapeType (int value ) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    private static final Map<Integer, ShapeType> lookup = new HashMap<Integer, 
ShapeType>();
+
+    static {
+        for (ShapeType ste : EnumSet.allOf(ShapeType.class)) {
+            lookup.put(ste.getValue(), ste);
+        }
+    }
+
+    public static ShapeType get(int value) {
+        return lookup.get(value);
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.java
new file mode 100644
index 0000000000..54e3953a7d
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.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.shp;
+
+import org.apache.sis.io.stream.ChannelDataOutput;
+
+import java.io.IOException;
+import java.nio.ByteOrder;
+
+/**
+ * Shape file writer.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ShapeWriter implements AutoCloseable{
+
+    private final ChannelDataOutput channel;
+
+    private ShapeGeometryEncoder io;
+
+    public ShapeWriter(ChannelDataOutput channel) throws IOException {
+        this.channel = channel;
+    }
+
+    public void write(ShapeHeader header) throws IOException {
+        header.write(channel);
+        io = ShapeGeometryEncoder.getEncoder(header.shapeType);
+    }
+
+    public void write(ShapeRecord record) throws IOException {
+        record.write(channel, io);
+    }
+
+    public void flush() throws IOException {
+        channel.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        channel.flush();
+
+        //update the file length in the header
+        final long fileLength = channel.getStreamPosition();
+        channel.seek(24);
+        channel.buffer.order(ByteOrder.BIG_ENDIAN);
+        channel.writeInt((int) fileLength);
+
+        channel.channel.close();
+    }
+
+}
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.cpg
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.cpg
new file mode 100644
index 0000000000..3ad133c048
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.cpg
@@ -0,0 +1 @@
+UTF-8
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.dbf
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.dbf
new file mode 100644
index 0000000000..5d05299bdf
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.dbf
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.prj
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.prj
new file mode 100644
index 0000000000..f45cbadf00
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.shp
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.shp
new file mode 100644
index 0000000000..90761cdf8c
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.shp
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.shx
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.shx
new file mode 100644
index 0000000000..e4c66b041c
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/multipoint.shx
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.cpg
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.cpg
new file mode 100644
index 0000000000..3ad133c048
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.cpg
@@ -0,0 +1 @@
+UTF-8
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.dbf
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.dbf
new file mode 100644
index 0000000000..67b0a0a824
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.dbf
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.prj
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.prj
new file mode 100644
index 0000000000..f45cbadf00
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.shp
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.shp
new file mode 100644
index 0000000000..c56974f48f
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.shp
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.shx
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.shx
new file mode 100644
index 0000000000..7e6257ee4d
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/point.shx
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.cpg
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.cpg
new file mode 100644
index 0000000000..3ad133c048
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.cpg
@@ -0,0 +1 @@
+UTF-8
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.dbf
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.dbf
new file mode 100644
index 0000000000..5d05299bdf
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.dbf
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.prj
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.prj
new file mode 100644
index 0000000000..f45cbadf00
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.shp
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.shp
new file mode 100644
index 0000000000..7d378ac806
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.shp
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.shx
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.shx
new file mode 100644
index 0000000000..84a1a2fc4e
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polygon.shx
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.cpg
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.cpg
new file mode 100644
index 0000000000..3ad133c048
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.cpg
@@ -0,0 +1 @@
+UTF-8
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.dbf
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.dbf
new file mode 100644
index 0000000000..5d05299bdf
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.dbf
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.prj
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.prj
new file mode 100644
index 0000000000..f45cbadf00
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
\ No newline at end of file
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.shp
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.shp
new file mode 100644
index 0000000000..0f4b56bfd0
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.shp
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.shx
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.shx
new file mode 100644
index 0000000000..170f1ca627
Binary files /dev/null and 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/polyline.shx
 differ
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/shp/ShapeIOTest.java
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/shp/ShapeIOTest.java
new file mode 100644
index 0000000000..1425e6b226
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/shp/ShapeIOTest.java
@@ -0,0 +1,319 @@
+/*
+ * 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.shp;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.storage.StorageConnector;
+
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import org.apache.sis.io.stream.ChannelDataOutput;
+import org.apache.sis.storage.DataStoreException;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.MultiLineString;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Polygon;
+
+/**
+ * @author Johann Sorel (Geomatys)
+ */
+public class ShapeIOTest {
+
+    public ShapeIOTest() {}
+
+    private ChannelDataInput openRead(String path) throws DataStoreException {
+        final URL url = ShapeIOTest.class.getResource(path);
+        final StorageConnector cnx = new StorageConnector(url);
+        final ChannelDataInput cdi = cnx.getStorageAs(ChannelDataInput.class);
+        cnx.closeAllExcept(cdi);
+        return cdi;
+    }
+
+    private ChannelDataOutput openWrite(Path path) throws DataStoreException, 
IOException {
+        if (true) {
+            //bypass a bug in StorageConnector
+            WritableByteChannel wbc = Files.newByteChannel(path, 
StandardOpenOption.WRITE);
+            return new ChannelDataOutput("", wbc, ByteBuffer.allocate(8000));
+        }
+        final StorageConnector cnx = new StorageConnector(path);
+        final ChannelDataOutput cdo = 
cnx.getStorageAs(ChannelDataOutput.class);
+        cnx.closeAllExcept(cdo);
+        return cdo;
+    }
+
+    /**
+     * Open given shape file, read it and write it to another file the compare 
them.
+     * They must be identical.
+     */
+    private void testReadAndWrite(String path) throws DataStoreException, 
IOException, URISyntaxException, URISyntaxException {
+        final ChannelDataInput cdi = openRead(path);
+
+        final Path tempFile = Files.createTempFile("tmp", ".shp");
+        final ChannelDataOutput cdo = openWrite(tempFile);
+
+        try {
+            try (ShapeReader reader = new ShapeReader(cdi);
+                 ShapeWriter writer = new ShapeWriter(cdo)) {
+
+                writer.write(reader.getHeader());
+
+                for (ShapeRecord record = reader.next(); record != null; 
record = reader.next()) {
+                    writer.write(record);
+                }
+            }
+
+            //compare files
+            final byte[] expected = 
Files.readAllBytes(Paths.get(ShapeIOTest.class.getResource(path).toURI()));
+            final byte[] result = Files.readAllBytes(tempFile);
+            assertArrayEquals(expected, result);
+
+        } finally {
+            Files.deleteIfExists(tempFile);
+        }
+    }
+
+
+    /**
+     * Test reading a point shape type.
+     */
+    @Test
+    public void testPoint() throws Exception {
+        final String path = "/org/apache/sis/storage/shapefile/point.shp";
+        final ChannelDataInput cdi = openRead(path);
+
+        try (ShapeReader reader = new ShapeReader(cdi)) {
+            final ShapeRecord record1 = reader.next();
+            assertEquals(2, record1.bbox.getDimension());
+            assertEquals(-38.5, record1.bbox.getMinimum(0), 0.1);
+            assertEquals(-38.5, record1.bbox.getMaximum(0), 0.1);
+            assertEquals(-13.0, record1.bbox.getMinimum(1), 0.1);
+            assertEquals(-13.0, record1.bbox.getMaximum(1), 0.1);
+            assertEquals(1, record1.recordNumber);
+            final Point pt1 = (Point) record1.geometry;
+            assertEquals(-38.5, pt1.getX(), 0.1);
+            assertEquals(-13.0, pt1.getY(), 0.1);
+
+            final ShapeRecord record2 = reader.next();
+            assertEquals(2, record2.bbox.getDimension());
+            assertEquals(2.1, record2.bbox.getMinimum(0), 0.1);
+            assertEquals(2.1, record2.bbox.getMaximum(0), 0.1);
+            assertEquals(42.5, record2.bbox.getMinimum(1), 0.1);
+            assertEquals(42.5, record2.bbox.getMaximum(1), 0.1);
+            assertEquals(2, record2.recordNumber);
+            final Point pt2 = (Point) record2.geometry;
+            assertEquals(2.1, pt2.getX(), 0.1);
+            assertEquals(42.5, pt2.getY(), 0.1);
+
+            //no more records
+            assertNull(reader.next());
+        }
+
+        testReadAndWrite(path);
+    }
+
+    /**
+     * Test reading a multipoint shape type.
+     */
+    @Test
+    public void testMultiPoint() throws Exception {
+        final String path = "/org/apache/sis/storage/shapefile/multipoint.shp";
+        final ChannelDataInput cdi = openRead(path);
+
+        try (ShapeReader reader = new ShapeReader(cdi)) {
+            final ShapeRecord record1 = reader.next();
+            assertEquals(2, record1.bbox.getDimension());
+            assertEquals(-38.0, record1.bbox.getMinimum(0), 0.1);
+            assertEquals(-33.5, record1.bbox.getMaximum(0), 0.1);
+            assertEquals(3.3, record1.bbox.getMinimum(1), 0.1);
+            assertEquals(6.8, record1.bbox.getMaximum(1), 0.1);
+            assertEquals(1, record1.recordNumber);
+            final MultiPoint mpt1 = (MultiPoint) record1.geometry;
+            assertEquals(2, mpt1.getNumGeometries());
+            final Point pt11 = (Point) mpt1.getGeometryN(0);
+            final Point pt12 = (Point) mpt1.getGeometryN(1);
+            assertEquals(-38.0, pt11.getX(), 0.1);
+            assertEquals(3.3, pt11.getY(), 0.1);
+            assertEquals(-33.5, pt12.getX(), 0.1);
+            assertEquals(6.8, pt12.getY(), 0.1);
+
+            final ShapeRecord record2 = reader.next();
+            assertEquals(2, record2.bbox.getDimension());
+            assertEquals(2.9, record2.bbox.getMinimum(0), 0.1);
+            assertEquals(5.1, record2.bbox.getMaximum(0), 0.1);
+            assertEquals(14.6, record2.bbox.getMinimum(1), 0.1);
+            assertEquals(16.6, record2.bbox.getMaximum(1), 0.1);
+            assertEquals(2, record2.recordNumber);
+            final MultiPoint mpt2 = (MultiPoint) record2.geometry;
+            final Point pt21 = (Point) mpt2.getGeometryN(0);
+            final Point pt22 = (Point) mpt2.getGeometryN(1);
+            assertEquals(2, mpt2.getNumGeometries());
+            assertEquals(5.1, pt21.getX(), 0.1);
+            assertEquals(14.6, pt21.getY(), 0.1);
+            assertEquals(2.9, pt22.getX(), 0.1);
+            assertEquals(16.6, pt22.getY(), 0.1);
+
+            //no more records
+            assertNull(reader.next());
+        }
+
+        testReadAndWrite(path);
+    }
+
+    /**
+     * Test reading a polyline shape type.
+     */
+    @Test
+    public void testPolyline() throws Exception {
+        final String path = "/org/apache/sis/storage/shapefile/polyline.shp";
+        final ChannelDataInput cdi = openRead(path);
+
+        try (ShapeReader reader = new ShapeReader(cdi)) {
+
+            //first record has a single 3 points line
+            final ShapeRecord record1 = reader.next();
+            assertEquals(2, record1.bbox.getDimension());
+            assertEquals(-43.0, record1.bbox.getMinimum(0), 0.1);
+            assertEquals(-38.9, record1.bbox.getMaximum(0), 0.1);
+            assertEquals(4.8, record1.bbox.getMinimum(1), 0.1);
+            assertEquals(7.7, record1.bbox.getMaximum(1), 0.1);
+            assertEquals(1, record1.recordNumber);
+            final MultiLineString ml1 = (MultiLineString) record1.geometry;
+            assertEquals(1, ml1.getNumGeometries());
+            final CoordinateSequence l1 = ((LineString) 
ml1.getGeometryN(0)).getCoordinateSequence();
+            assertEquals(3, l1.size());
+            assertEquals(-43.0, l1.getX(0), 0.1);
+            assertEquals(4.8, l1.getY(0), 0.1);
+            assertEquals(-41.9, l1.getX(1), 0.1);
+            assertEquals(7.7, l1.getY(1), 0.1);
+            assertEquals(-38.9, l1.getX(2), 0.1);
+            assertEquals(6.4, l1.getY(2), 0.1);
+
+            //second record has two 2 points lines
+            final ShapeRecord record2 = reader.next();
+            assertEquals(2, record2.bbox.getDimension());
+            assertEquals(-0.9, record2.bbox.getMinimum(0), 0.1);
+            assertEquals(5.9, record2.bbox.getMaximum(0), 0.1);
+            assertEquals(6.6, record2.bbox.getMinimum(1), 0.1);
+            assertEquals(12.1, record2.bbox.getMaximum(1), 0.1);
+            assertEquals(2, record2.recordNumber);
+            final MultiLineString ml2 = (MultiLineString) record2.geometry;
+            assertEquals(2, ml2.getNumGeometries());
+            final CoordinateSequence l21 = ((LineString) 
ml2.getGeometryN(0)).getCoordinateSequence();
+            final CoordinateSequence l22 = ((LineString) 
ml2.getGeometryN(1)).getCoordinateSequence();
+            assertEquals(2, l21.size());
+            assertEquals(2, l22.size());
+            assertEquals(-0.9, l21.getX(0), 0.1);
+            assertEquals(12.14, l21.getY(0), 0.1);
+            assertEquals(5.1, l21.getX(1), 0.1);
+            assertEquals(10.4, l21.getY(1), 0.1);
+            assertEquals(0.5, l22.getX(0), 0.1);
+            assertEquals(8.4, l22.getY(0), 0.1);
+            assertEquals(5.9, l22.getX(1), 0.1);
+            assertEquals(6.6, l22.getY(1), 0.1);
+
+            //no more records
+            assertNull(reader.next());
+        }
+
+        testReadAndWrite(path);
+    }
+
+
+    /**
+     * Test reading a polygon shape type.
+     */
+    @Test
+    public void testPolygon() throws Exception {
+        final String path = "/org/apache/sis/storage/shapefile/polygon.shp";
+        final ChannelDataInput cdi = openRead(path);
+
+        try (ShapeReader reader = new ShapeReader(cdi)) {
+            final ShapeRecord record1 = reader.next();
+            assertEquals(2, record1.bbox.getDimension());
+            assertEquals(-43.8, record1.bbox.getMinimum(0), 0.1);
+            assertEquals(-29.7, record1.bbox.getMaximum(0), 0.1);
+            assertEquals(-1.6, record1.bbox.getMinimum(1), 0.1);
+            assertEquals(14.9, record1.bbox.getMaximum(1), 0.1);
+            assertEquals(1, record1.recordNumber);
+            final MultiPolygon ml1 = (MultiPolygon) record1.geometry;
+            assertEquals(1, ml1.getNumGeometries());
+            final Polygon l1 = (Polygon) ml1.getGeometryN(0);
+            assertEquals(0, l1.getNumInteriorRing());
+            final CoordinateSequence er1 = 
l1.getExteriorRing().getCoordinateSequence();
+            assertEquals(5, er1.size());
+            assertEquals(-43.8, er1.getX(0), 0.1);
+            assertEquals(2.9, er1.getY(0), 0.1);
+            assertEquals(-42.0, er1.getX(1), 0.1);
+            assertEquals(14.9, er1.getY(1), 0.1);
+            assertEquals(-29.7, er1.getX(2), 0.1);
+            assertEquals(10.06, er1.getY(2), 0.1);
+            assertEquals(-37.9, er1.getX(3), 0.1);
+            assertEquals(-1.6, er1.getY(3), 0.1);
+
+            final ShapeRecord record2 = reader.next();
+            assertEquals(2, record2.bbox.getDimension());
+            assertEquals(-5.0, record2.bbox.getMinimum(0), 0.1);
+            assertEquals(11.6, record2.bbox.getMaximum(0), 0.1);
+            assertEquals(3.2, record2.bbox.getMinimum(1), 0.1);
+            assertEquals(18.9, record2.bbox.getMaximum(1), 0.1);
+            assertEquals(2, record2.recordNumber);
+            MultiPolygon ml2 = (MultiPolygon) record2.geometry;
+            assertEquals(1, ml2.getNumGeometries());
+            final Polygon l2 = (Polygon) ml2.getGeometryN(0);
+            assertEquals(1, l2.getNumInteriorRing());
+            CoordinateSequence out2 = 
l2.getExteriorRing().getCoordinateSequence();
+            CoordinateSequence inner2 = 
l2.getInteriorRingN(0).getCoordinateSequence();
+
+            assertEquals(5, out2.size());
+            assertEquals(-0.5, out2.getX(0), 0.1);
+            assertEquals(18.9, out2.getY(0), 0.1);
+            assertEquals(11.6, out2.getX(1), 0.1);
+            assertEquals(16.9, out2.getY(1), 0.1);
+            assertEquals(8.3, out2.getX(2), 0.1);
+            assertEquals(3.2, out2.getY(2), 0.1);
+            assertEquals(-5.0, out2.getX(3), 0.1);
+            assertEquals(6.2, out2.getY(3), 0.1);
+
+            assertEquals(5, inner2.size());
+            assertEquals(2.3, inner2.getX(0), 0.1);
+            assertEquals(14.2, inner2.getY(0), 0.1);
+            assertEquals(0.0, inner2.getX(1), 0.1);
+            assertEquals(9.1, inner2.getY(1), 0.1);
+            assertEquals(5.3, inner2.getX(2), 0.1);
+            assertEquals(7.9, inner2.getY(2), 0.1);
+            assertEquals(6.9, inner2.getX(3), 0.1);
+            assertEquals(13.1, inner2.getY(3), 0.1);
+
+            //no more records
+            assertNull(reader.next());
+        }
+
+        testReadAndWrite(path);
+    }
+}

Reply via email to