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);
+ }
+}