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 a798c10c97 feat(Shapefile): simplify raw API, reduce scope of all unnecessary attributes, full javadoc a798c10c97 is described below commit a798c10c972e96fbca6ad417c76979a28e2bf919 Author: jsorel <johann.so...@geomatys.com> AuthorDate: Wed Dec 6 18:12:56 2023 +0100 feat(Shapefile): simplify raw API, reduce scope of all unnecessary attributes, full javadoc SIS-188 : DBFField properties have been reduced SIS-188 : classes removed SIS-189 : exception removed --- .../main/module-info.java | 3 +- .../sis/storage/shapefile/ShapefileProvider.java | 24 ++++ .../sis/storage/shapefile/ShapefileStore.java | 100 ++++++++++---- .../{dbf/DBFRecord.java => cpg/package-info.java} | 17 +-- .../apache/sis/storage/shapefile/dbf/DBFField.java | 89 ++++++++++-- .../sis/storage/shapefile/dbf/DBFHeader.java | 63 +++++++-- .../sis/storage/shapefile/dbf/DBFReader.java | 48 +++++-- .../sis/storage/shapefile/dbf/DBFWriter.java | 39 ++++-- .../dbf/{DBFRecord.java => package-info.java} | 28 ++-- .../apache/sis/storage/shapefile/package-info.java | 20 ++- .../shapefile/shp/ShapeGeometryEncoder.java | 151 ++++++++++++++++----- .../sis/storage/shapefile/shp/ShapeHeader.java | 15 +- .../sis/storage/shapefile/shp/ShapeReader.java | 29 ++++ .../sis/storage/shapefile/shp/ShapeRecord.java | 55 ++++---- .../sis/storage/shapefile/shp/ShapeType.java | 83 +++++++---- .../sis/storage/shapefile/shp/ShapeWriter.java | 48 +++++-- .../storage/shapefile/{ => shp}/package-info.java | 13 +- .../sis/storage/shapefile/shx/IndexReader.java | 27 +++- .../sis/storage/shapefile/shx/IndexWriter.java | 37 +++-- .../storage/shapefile/{ => shx}/package-info.java | 9 +- .../org/apache/sis/storage/shapefile/Snippets.java | 106 +++++++++++++++ .../sis/storage/shapefile/dbf/DBFIOTest.java | 46 +++---- .../apache/sis/storage/shapefile/dbf/Snippets.java | 95 +++++++++++++ .../sis/storage/shapefile/shp/ShapeIOTest.java | 4 +- .../apache/sis/storage/shapefile/shp/Snippets.java | 81 +++++++++++ 25 files changed, 979 insertions(+), 251 deletions(-) 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 e1b6d97f85..9bb7b321f2 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 @@ -26,8 +26,9 @@ 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; exports org.apache.sis.storage.shapefile.dbf; + exports org.apache.sis.storage.shapefile.shp; + exports org.apache.sis.storage.shapefile.shx; provides org.apache.sis.storage.DataStoreProvider with org.apache.sis.storage.shapefile.ShapefileProvider; diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java index 4a77845225..ecb18705b6 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileProvider.java @@ -36,8 +36,14 @@ import org.apache.sis.storage.StorageConnector; */ public final class ShapefileProvider extends DataStoreProvider { + /** + * Format name. + */ public static final String NAME = "esri shapefile"; + /** + * Format mime type. + */ public static final String MIME_TYPE = "application/x-shapefile"; /** @@ -48,23 +54,38 @@ public final class ShapefileProvider extends DataStoreProvider { .setRequired(true) .create(URI.class, null); + /** + * Shapefile store creation parameters. + */ public static final ParameterDescriptorGroup PARAMETERS_DESCRIPTOR = new ParameterBuilder().addName(NAME).addName("EsriShapefileParameters").createGroup( PATH); + /** + * Default constructor. + */ public ShapefileProvider() { } + /** + * {@inheritDoc } + */ @Override public String getShortName() { return NAME; } + /** + * {@inheritDoc } + */ @Override public ParameterDescriptorGroup getOpenParameters() { return PARAMETERS_DESCRIPTOR; } + /** + * {@inheritDoc } + */ @Override public ProbeResult probeContent(StorageConnector connector) throws DataStoreException { final Path path = connector.getStorageAs(Path.class); @@ -74,6 +95,9 @@ public final class ShapefileProvider extends DataStoreProvider { return ProbeResult.UNSUPPORTED_STORAGE; } + /** + * {@inheritDoc } + */ @Override public DataStore open(StorageConnector connector) throws DataStoreException { final Path path = connector.getStorageAs(Path.class); diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java index 6c755a466e..9a77f574bc 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java @@ -99,7 +99,6 @@ import org.apache.sis.storage.shapefile.cpg.CpgFiles; import org.apache.sis.storage.shapefile.dbf.DBFField; import org.apache.sis.storage.shapefile.dbf.DBFHeader; import org.apache.sis.storage.shapefile.dbf.DBFReader; -import org.apache.sis.storage.shapefile.dbf.DBFRecord; import org.apache.sis.storage.shapefile.dbf.DBFWriter; import org.apache.sis.storage.shapefile.shp.ShapeGeometryEncoder; import org.apache.sis.storage.shapefile.shp.ShapeHeader; @@ -149,18 +148,33 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe */ private final ReadWriteLock lock = new ReentrantReadWriteLock(); + /** + * Construct store from given path. + * + * @param path path to .shp file + */ public ShapefileStore(Path path) { this.shpPath = path; this.userDefinedCharSet = null; this.files = new ShpFiles(shpPath); } + /** + * Construct store from given connector. + * + * @param cnx not null + * @throws IllegalArgumentException if connector could not provide a valid Path instance + * @throws DataStoreException if connector could not provide a valid Path instance + */ public ShapefileStore(StorageConnector cnx) throws IllegalArgumentException, DataStoreException { this.shpPath = cnx.getStorageAs(Path.class); this.userDefinedCharSet = cnx.getOption(OptionKey.ENCODING); this.files = new ShpFiles(shpPath); } + /** + * {@inheritDoc } + */ @Override public Optional<ParameterValueGroup> getOpenParameters() { final Parameters parameters = Parameters.castOrWrap(ShapefileProvider.PARAMETERS_DESCRIPTOR.createValue()); @@ -168,63 +182,96 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe return Optional.of(parameters); } + /** + * {@inheritDoc } + */ @Override public void close() throws DataStoreException { } - /* - Redirect FeatureSet interface to View - */ + /** + * {@inheritDoc } + */ @Override public Optional<GenericName> getIdentifier() throws DataStoreException { return featureSetView.getIdentifier(); } + /** + * {@inheritDoc } + */ @Override public Metadata getMetadata() throws DataStoreException { return featureSetView.getMetadata(); } + /** + * {@inheritDoc } + */ @Override public FeatureType getType() throws DataStoreException { return featureSetView.getType(); } + /** + * {@inheritDoc } + */ @Override public FeatureSet subset(Query query) throws UnsupportedQueryException, DataStoreException { return featureSetView.subset(query); } + /** + * {@inheritDoc } + */ @Override public Stream<Feature> features(boolean parallel) throws DataStoreException { return featureSetView.features(parallel); } + /** + * {@inheritDoc } + */ @Override public Optional<Envelope> getEnvelope() throws DataStoreException { return featureSetView.getEnvelope(); } + /** + * {@inheritDoc } + */ @Override public void updateType(FeatureType featureType) throws DataStoreException { featureSetView.updateType(featureType); } + /** + * {@inheritDoc } + */ @Override public void add(Iterator<? extends Feature> iterator) throws DataStoreException { featureSetView.add(iterator); } + /** + * {@inheritDoc } + */ @Override public void removeIf(Predicate<? super Feature> predicate) throws DataStoreException { featureSetView.removeIf(predicate); } + /** + * {@inheritDoc } + */ @Override public void replaceIf(Predicate<? super Feature> predicate, UnaryOperator<Feature> unaryOperator) throws DataStoreException { featureSetView.replaceIf(predicate, unaryOperator); } + /** + * {@inheritDoc } + */ @Override public Path[] getComponentFiles() throws DataStoreException { return featureSetView.getComponentFiles(); @@ -416,11 +463,11 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe //move dbf to record offset, some shp record might have been skipped because of filter long offset = (long)header.headerSize + ((long)(shpRecord.recordNumber-1)) * ((long)header.recordSize); dbfreader.moveToOffset(offset); - final DBFRecord dbfRecord = dbfreader.next(); + final Object[] dbfRecord = dbfreader.next(); final Feature next = type.newInstance(); next.setPropertyValue(GEOMETRY_NAME, shpRecord.geometry); for (int i = 0; i < dbfPropertiesIndex.length; i++) { - next.setPropertyValue(header.fields[dbfPropertiesIndex[i]].fieldName, dbfRecord.fields[i]); + next.setPropertyValue(header.fields[dbfPropertiesIndex[i]].fieldName, dbfRecord[i]); } action.accept(next); return true; @@ -453,11 +500,11 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe @Override public boolean tryAdvance(Consumer action) { try { - final DBFRecord dbfRecord = dbfreader.next(); + final Object[] dbfRecord = dbfreader.next(); if (dbfRecord == null) return false; final Feature next = type.newInstance(); for (int i = 0; i < dbfPropertiesIndex.length; i++) { - next.setPropertyValue(header.fields[dbfPropertiesIndex[i]].fieldName, dbfRecord.fields[i]); + next.setPropertyValue(header.fields[dbfPropertiesIndex[i]].fieldName, dbfRecord[i]); } action.accept(next); return true; @@ -603,6 +650,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe final ShapeHeader shpHeader = new ShapeHeader(); shpHeader.bbox = new ImmutableEnvelope(new GeneralEnvelope(4)); final DBFHeader dbfHeader = new DBFHeader(); + dbfHeader.lastUpdate = LocalDate.now(); dbfHeader.fields = new DBFField[0]; final Charset charset = userDefinedCharSet == null ? StandardCharsets.UTF_8 : userDefinedCharSet; CoordinateReferenceSystem crs = CommonCRS.WGS84.normalizedGeographic(); @@ -617,16 +665,16 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe if (length == null || length == 0) length = 255; if (Geometry.class.isAssignableFrom(valueClass)) { - if (shpHeader.shapeType != 0) { + if (shpHeader.shapeType != null) { throw new DataStoreException("Shapefile format can only contain one geometry"); } - if (Point.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.VALUE_POINT; + if (Point.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.POINT; else if (MultiPoint.class.isAssignableFrom(valueClass)) - shpHeader.shapeType = ShapeType.VALUE_MULTIPOINT; + shpHeader.shapeType = ShapeType.MULTIPOINT; else if (LineString.class.isAssignableFrom(valueClass) || MultiLineString.class.isAssignableFrom(valueClass)) - shpHeader.shapeType = ShapeType.VALUE_POLYLINE; + shpHeader.shapeType = ShapeType.POLYLINE; else if (Polygon.class.isAssignableFrom(valueClass) || MultiPolygon.class.isAssignableFrom(valueClass)) - shpHeader.shapeType = ShapeType.VALUE_POLYGON; + shpHeader.shapeType = ShapeType.POLYGON; else throw new DataStoreException("Unsupported geometry type " + valueClass); Object cdt = at.characteristics().get(AttributeConvention.CRS); @@ -663,21 +711,21 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe //write shapefile try (ShapeWriter writer = new ShapeWriter(ShpFiles.openWriteChannel(files.shpFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { - writer.write(shpHeader); + writer.writeHeader(shpHeader); } catch (IOException ex) { throw new DataStoreException("Failed to create shapefile (shp).", ex); } //write shx try (IndexWriter writer = new IndexWriter(ShpFiles.openWriteChannel(files.getShx(true), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { - writer.write(shpHeader); + writer.writeHeader(shpHeader); } catch (IOException ex) { throw new DataStoreException("Failed to create shapefile (shx).", ex); } //write dbf try (DBFWriter writer = new DBFWriter(ShpFiles.openWriteChannel(files.getDbf(true), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { - writer.write(dbfHeader); + writer.writeHeader(dbfHeader); } catch (IOException ex) { throw new DataStoreException("Failed to create shapefile (dbf).", ex); } @@ -1078,9 +1126,9 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe shpWriter = new ShapeWriter(ShpFiles.openWriteChannel(tempFiles.shpFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)); dbfWriter = new DBFWriter(ShpFiles.openWriteChannel(tempFiles.getDbf(true), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)); shxWriter = new IndexWriter(ShpFiles.openWriteChannel(tempFiles.getShx(true), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)); - shpWriter.write(shpHeader); - shxWriter.write(shpHeader); - dbfWriter.write(dbfHeader); + shpWriter.writeHeader(shpHeader); + shxWriter.writeHeader(shpHeader); + dbfWriter.writeHeader(dbfHeader); } catch (IOException ex) { try { tempFiles.deleteFiles(); @@ -1095,7 +1143,6 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe private void write(Feature feature) throws IOException { inc++; //number starts at 1 final ShapeRecord shpRecord = new ShapeRecord(); - final DBFRecord dbfRecord = new DBFRecord(); final long recordStartPosition = shpWriter.getSteamPosition(); if (defaultGeomName == null) { @@ -1122,18 +1169,18 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe } else { throw new IOException("Feature geometry property is not a geometry"); } - shpWriter.write(shpRecord); + shpWriter.writeRecord(shpRecord); final long recordEndPosition = shpWriter.getSteamPosition(); //write index - shxWriter.write(Math.toIntExact(recordStartPosition), Math.toIntExact(recordEndPosition - recordStartPosition)); + shxWriter.writeRecord(Math.toIntExact(recordStartPosition), Math.toIntExact(recordEndPosition - recordStartPosition)); //copy dbf fields - dbfRecord.fields = new Object[dbfHeader.fields.length]; - for (int i = 0; i < dbfRecord.fields.length; i++) { - dbfRecord.fields[i] = feature.getPropertyValue(dbfHeader.fields[i].fieldName); + Object[] fields = new Object[dbfHeader.fields.length]; + for (int i = 0; i < fields.length; i++) { + fields[i] = feature.getPropertyValue(dbfHeader.fields[i].fieldName); } - dbfWriter.write(dbfRecord); + dbfWriter.writeRecord(fields); } /** @@ -1160,5 +1207,4 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe } } - } diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFRecord.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/cpg/package-info.java similarity index 73% copy from incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFRecord.java copy to incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/cpg/package-info.java index 1f33f949d7..93d9357898 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFRecord.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/cpg/package-info.java @@ -14,21 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.sis.storage.shapefile.dbf; - /** - * A DBF record is an array of field values. - * - * @author Johann Sorel (Geomatys) + * Shapefile CPG files utilities. */ -public final class DBFRecord { - - public static final DBFRecord DELETED = new DBFRecord(); - - public Object[] fields; - - public DBFRecord() { - } - -} +package org.apache.sis.storage.shapefile.cpg; diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java index 564bd7ba9d..341b4d4a86 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java @@ -27,6 +27,8 @@ import org.apache.sis.io.stream.ChannelDataOutput; /** + * Immutable DBase III field description. + * This class holds the different field attributes with value read and write capabilities. * * @author Johann Sorel (Geomatys) */ @@ -35,60 +37,83 @@ public final class DBFField { /** * Binary, String : b or B */ - public static final int TYPE_BINARY = 'b'; + public static final char TYPE_BINARY = 'b'; /** * Characters : c or C */ - public static final int TYPE_CHAR = 'c'; + public static final char TYPE_CHAR = 'c'; /** * Date : d or D */ - public static final int TYPE_DATE = 'd'; + public static final char TYPE_DATE = 'd'; /** * Numeric : n or N */ - public static final int TYPE_NUMBER = 'n'; + public static final char TYPE_NUMBER = 'n'; /** * Logical : l or L */ - public static final int TYPE_LOGIC = 'l'; + public static final char TYPE_LOGIC = 'l'; /** * Memo, String : m or M */ - public static final int TYPE_MEMO = 'm'; + public static final char TYPE_MEMO = 'm'; /** * TimeStamp : 8 bytes, two longs, first for date, second for time. * The date is the number of days since 01/01/4713 BC. * Time is hours * 3600000L + minutes * 60000L + Seconds * 1000L */ - public static final int TYPE_TIMESTAMP = '@'; + public static final char TYPE_TIMESTAMP = '@'; /** * Long : i or I on 4 bytes, first bit is the sign, 0 = negative */ - public static final int TYPE_LONG = 'i'; + public static final char TYPE_LONG = 'i'; /** * Autoincrement : same as Long */ - public static final int TYPE_INC = '+'; + public static final char TYPE_INC = '+'; /** * Floats : f or F */ - public static final int TYPE_FLOAT = 'f'; + public static final char TYPE_FLOAT = 'f'; /** * Double : o or O, real double on 8bytes, not string encoded */ - public static final int TYPE_DOUBLE = 'o'; + public static final char TYPE_DOUBLE = 'o'; /** * OLE : g or G */ - public static final int TYPE_OLE = 'g'; + public static final char TYPE_OLE = 'g'; + /** + * Field name. + */ public final String fieldName; + /** + * Field type. + */ public final char fieldType; + /** + * Reserved, found with different names in different spec. + * Unused in current implementation. + */ public final int fieldAddress; + /** + * Field length in binary. + */ public final int fieldLength; + /** + * Field decimal count in binary. + */ public final int fieldDecimals; - public final Charset charset; + /** + * Used to decode strings. + * Can be null. + */ + private final Charset charset; + /** + * Java value type matching field type. + */ public final Class valueClass; private final ReadMethod reader; @@ -96,6 +121,16 @@ public final class DBFField { //used by decimal format only; private NumberFormat format; + /** + * Field constructor. + * + * @param fieldName field name + * @param fieldType field data type + * @param fieldAddress unused for now, has a meaning in some specifications but not all + * @param fieldLength total field length in bytes + * @param fieldDecimals number of decimals for floating points + * @param charset String base field encoding + */ public DBFField(String fieldName, char fieldType, int fieldAddress, int fieldLength, int fieldDecimals, Charset charset) { this.fieldName = fieldName; this.fieldType = fieldType; @@ -131,6 +166,14 @@ public final class DBFField { } } + /** + * Read field description. + * + * @param channel to read from + * @param charset field text encoding + * @return dbf field description + * @throws IOException if an error occured while parsing field + */ public static DBFField read(ChannelDataInput channel, Charset charset) throws IOException { byte[] n = channel.readBytes(11); int nameSize = 0; @@ -144,6 +187,12 @@ public final class DBFField { return new DBFField(fieldName, fieldType, fieldAddress, fieldLength, fieldDecimals, charset); } + /** + * Write field description. + * + * @param channel to write into + * @throws IOException if an error occured while writing field + */ public void write(ChannelDataOutput channel) throws IOException { byte[] bytes = fieldName.getBytes(StandardCharsets.US_ASCII); if (bytes.length > 11) throw new IOException("Field name length must not be longer then 11 characters."); @@ -156,10 +205,24 @@ public final class DBFField { channel.repeat(14, (byte) 0); } + /** + * Read a value of this field type. + * + * @param channel to read from + * @return decoded field value + * @throws IOException if an error occured while parsing field value + */ public Object readValue(ChannelDataInput channel) throws IOException { return reader.readValue(channel); } + /** + * Write a value of this field type. + * + * @param channel to write into + * @param value field value + * @throws IOException if an error occured while writing field value + */ public void writeValue(ChannelDataOutput channel, Object value) throws IOException { writer.writeValue(channel, value); } diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java index dde089f6d6..8c0bca0aa0 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java @@ -19,11 +19,13 @@ package org.apache.sis.storage.shapefile.dbf; import java.io.IOException; import java.nio.ByteOrder; import java.nio.charset.Charset; +import java.time.LocalDate; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.ChannelDataOutput; /** + * Mutable DBase III file header. * * @author Johann Sorel (Geomatys) */ @@ -32,21 +34,40 @@ public final class DBFHeader { private static final int FIELD_SIZE = 32; private static final int FIELD_DESCRIPTOR_TERMINATOR = 0x0D; - public int year; - public int month; - public int day; + /** + * Date of last update. + */ + public LocalDate lastUpdate; + /** + * Number of records in the file. + */ public int nbRecord; + /** + * Total header size. + */ public int headerSize; + /** + * Size of a single record. + */ public int recordSize; + /** + * Array of field descriptors. + */ public DBFField[] fields; + /** + * Default constructor. + */ public DBFHeader() { } + /** + * Duplicate constructor. + * + * @param toCopy to copy from + */ public DBFHeader(DBFHeader toCopy) { - this.year = toCopy.year; - this.month = toCopy.month; - this.day = toCopy.day; + this.lastUpdate = toCopy.lastUpdate; this.nbRecord = toCopy.nbRecord; this.headerSize = toCopy.headerSize; this.recordSize = toCopy.recordSize; @@ -65,14 +86,22 @@ public final class DBFHeader { } } + /** + * Read header. + * + * @param channel to read from + * @param charset field text encoding + * @throws IOException if an error occured while parsing header + */ public void read(ChannelDataInput channel, Charset charset) throws IOException { channel.buffer.order(ByteOrder.LITTLE_ENDIAN); if (channel.readByte()!= 0x03) { throw new IOException("Unvalid database III magic"); } - year = channel.readUnsignedByte(); - month = channel.readUnsignedByte(); - day = channel.readUnsignedByte(); + int year = channel.readUnsignedByte(); + int month = channel.readUnsignedByte(); + int day = channel.readUnsignedByte(); + lastUpdate = LocalDate.of(year+1900, month, day); nbRecord = channel.readInt(); headerSize = channel.readUnsignedShort(); recordSize = channel.readUnsignedShort(); @@ -87,12 +116,18 @@ public final class DBFHeader { } } + /** + * Write header. + * + * @param channel to write into + * @throws IOException if an error occured while writing header + */ public void write(ChannelDataOutput channel) throws IOException { channel.buffer.order(ByteOrder.LITTLE_ENDIAN); channel.writeByte(0x03); - channel.writeByte(year); - channel.writeByte(month); - channel.writeByte(day); + channel.writeByte(lastUpdate.getYear()-1900); + channel.writeByte(lastUpdate.getMonthValue()); + channel.writeByte(lastUpdate.getDayOfMonth()); channel.writeInt(nbRecord); channel.writeShort(headerSize); channel.writeShort(recordSize); @@ -106,9 +141,7 @@ public final class DBFHeader { @Override public String toString() { final StringBuilder sb = new StringBuilder("DBFHeader{"); - sb.append("year=").append(year); - sb.append(",month=").append(month); - sb.append(",day=").append(day); + sb.append("lastUpdate=").append(lastUpdate); sb.append(",nbRecord=").append(nbRecord); sb.append(",headerSize=").append(headerSize); sb.append(",recordSize=").append(recordSize); diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java index 848743b5ac..40e7c97f0e 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java @@ -22,12 +22,17 @@ import org.apache.sis.io.stream.ChannelDataInput; /** - * Seekable dbf file reader. + * Seekable DBase file reader. * * @author Johann Sorel (Geomatys) */ public final class DBFReader implements AutoCloseable { + /** + * Unique instance used to mark a record which has been deleted. + */ + public static final Object[] DELETED_RECORD = new Object[0]; + static final int TAG_PRESENT = 0x20; static final int TAG_DELETED = 0x2a; static final int TAG_EOF = 0x1A; @@ -38,9 +43,12 @@ public final class DBFReader implements AutoCloseable { private int nbRead = 0; /** + * Constructor. + * * @param channel to read from * @param charset text encoding * @param fieldsToRead fields index in the header to decode, other fields will be skipped. must be in increment order. + * @throws IOException if a decoding error occurs on the header */ public DBFReader(ChannelDataInput channel, Charset charset, int[] fieldsToRead) throws IOException { this.channel = channel; @@ -49,24 +57,36 @@ public final class DBFReader implements AutoCloseable { this.fieldsToRead = fieldsToRead; } + /** + * Get decoded header. + * + * @return Dbase file header + */ public DBFHeader getHeader() { return header; } + /** + * Move channel to given position. + * + * @param position new position + * @throws IOException if the stream cannot be moved to the given position. + */ public void moveToOffset(long position) throws IOException { channel.seek(position); } /** + * Get next record. * - * @return record or DBFRecord.DELETED if this record has been deleted. + * @return record or DBFReader.DELETED_RECORD if this record has been deleted. * @throws IOException if a decoding error occurs */ - public DBFRecord next() throws IOException { + public Object[] next() throws IOException { if (nbRead >= header.nbRecord) { //reached records end //we do not trust the EOF if we already have the expected count - //some writes do not have it + //some incorrect files do not have it return null; } nbRead++; @@ -74,27 +94,26 @@ public final class DBFReader implements AutoCloseable { final int marker = channel.readUnsignedByte(); if (marker == TAG_DELETED) { channel.seek(channel.getStreamPosition() + header.recordSize); - return DBFRecord.DELETED; + return DELETED_RECORD; } else if (marker == TAG_EOF) { return null; } else if (marker != TAG_PRESENT) { throw new IOException("Unexpected record marker " + marker); } - final DBFRecord record = new DBFRecord(); - record.fields = new Object[header.fields.length]; + Object[] record; if (fieldsToRead == null) { //read all fields - record.fields = new Object[header.fields.length]; + record = new Object[header.fields.length]; for (int i = 0; i < header.fields.length; i++) { - record.fields[i] = header.fields[i].readValue(channel); + record[i] = header.fields[i].readValue(channel); } } else { //read only selected fields - record.fields = new Object[fieldsToRead.length]; + record = new Object[fieldsToRead.length]; for (int i = 0,k = 0; i < header.fields.length; i++) { if (k < fieldsToRead.length && fieldsToRead[k] == i) { - record.fields[k++] = header.fields[i].readValue(channel); + record[k++] = header.fields[i].readValue(channel); } else { //skip this field channel.seek(channel.getStreamPosition() + header.fields[i].fieldLength); @@ -105,8 +124,11 @@ public final class DBFReader implements AutoCloseable { return record; } - - + /** + * Release reader resources. + * + * @throws IOException If an I/O error occurs + */ @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/dbf/DBFWriter.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java index ec8acf06b3..ea7acbd53e 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java @@ -32,33 +32,54 @@ public final class DBFWriter implements AutoCloseable{ private int writtenNbRecord = 0 ; private DBFHeader header; + /** + * Constructor. + * + * @param channel to write into + */ public DBFWriter(ChannelDataOutput channel) { this.channel = channel; } - public void write(DBFHeader header) throws IOException { + /** + * Write DBase header. + * + * This should be the first call on the writer. + * Header number of records will be updated in the close method. + * + * @param header to write + * @throws IOException If an I/O error occurs + */ + public void writeHeader(DBFHeader header) throws IOException { this.header = new DBFHeader(header); this.header.updateSizes(); //recompute sizes this.header.nbRecord = 0; //force to zero, will be replaced when closing writer. this.header.write(channel); } - public void write(DBFRecord record) throws IOException { + /** + * Write a DBase record. + * + * @param fieldValues record fields to write + * @throws IOException If an I/O error occurs + */ + public void writeRecord(Object ... fieldValues) throws IOException { channel.writeByte(DBFReader.TAG_PRESENT); for (int i = 0; i < header.fields.length; i++) { - header.fields[i].writeValue(channel, record.fields[i]); + header.fields[i].writeValue(channel, fieldValues[i]); } writtenNbRecord++; } - public void flush() throws IOException { - channel.writeByte(DBFReader.TAG_EOF); - channel.flush(); - } - + /** + * Write end of file tag, update written number of record and release resources. + * + * @throws IOException If an I/O error occurs + */ @Override public void close() throws IOException { - flush(); + channel.writeByte(DBFReader.TAG_EOF); + channel.flush(); //update the nbRecord in the header channel.seek(4); diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFRecord.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/package-info.java similarity index 56% rename from incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFRecord.java rename to incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/package-info.java index 1f33f949d7..3565b802da 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFRecord.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/package-info.java @@ -14,21 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.sis.storage.shapefile.dbf; - /** - * A DBF record is an array of field values. + * DBase III format reader and writer. + * <p> + * This package is used and made for ESRI shapefile format which at it's creation time + * was related to DBase III. + * + * <h2>Reading example</h2> + *{@snippet class="org.apache.sis.storage.shapefile.dbf.Snippets" region="read"} * - * @author Johann Sorel (Geomatys) + * <h2>Writing example</h2> + *{@snippet class="org.apache.sis.storage.shapefile.dbf.Snippets" region="write"} + * + * @see <a href="https://en.wikipedia.org/wiki/.dbf">Format from Wikipedia</a> + * @see <a href="http://www.dbase.com/KnowledgeBase/int/db7_file_fmt.htm">Format from dbase.com</a> + * @see <a href="https://wiki.dbfmanager.com/dbf-structure">Format from wiki.dbfmanager.com</a> */ -public final class DBFRecord { - - public static final DBFRecord DELETED = new DBFRecord(); - - public Object[] fields; - - public DBFRecord() { - } - -} +package org.apache.sis.storage.shapefile.dbf; diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java index 174e38140d..e2ac355205 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java @@ -16,11 +16,25 @@ */ /** - * Shapefile. + * Shapefile format DataStore implementation. * - * <div class="warning">This is an experimental package, - * not yet target for any Apache SIS release at this time.</div> + * <h2>Reading example</h2> + *{@snippet class="org.apache.sis.storage.shapefile.Snippets" region="read"} + * + * <h2>Writing example</h2> + *{@snippet class="org.apache.sis.storage.shapefile.Snippets" region="write"} + * + * For raw access to DBF and SHP, use the related packages : + * <ul> + * <li>{@link org.apache.sis.storage.shapefile.shp}</li> + * <li>{@link org.apache.sis.storage.shapefile.shx}</li> + * <li>{@link org.apache.sis.storage.shapefile.dbf}</li> + * <li>{@link org.apache.sis.storage.shapefile.cpg}</li> + * </ul> + * The shapefile datastore layer is very thin and the only performance overheap + * is the mapping from DBFRecord/ShpRecord to Feature. * * @author Johann Sorel (Geomatys) + * @see <a href="http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf">ESRI Shapefile Specification</a> */ package org.apache.sis.storage.shapefile; diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java index c069f1bd39..98aef27229 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java @@ -24,12 +24,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.sis.geometry.GeneralDirectPosition; -import org.apache.sis.util.ArraysExt; import org.locationtech.jts.geom.*; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.algorithm.RayCrossingCounter; -import org.apache.sis.geometry.Envelope2D; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.ChannelDataOutput; @@ -40,51 +38,71 @@ import org.apache.sis.io.stream.ChannelDataOutput; * This class should be kept separate because I might be used in ESRI geodatabase format. * * @author Johann Sorel (Geomatys) + * @param <T> encoder geometry type */ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final GeometryFactory GF = new GeometryFactory(); - protected final int shapeType; + /** + * Encoder shape type. + */ + protected final ShapeType shapeType; + /** + * Encoder java value class. + */ protected final Class<T> geometryClass; + /** + * Number of dimension in the geometry. + */ protected final int dimension; + /** + * Number of measures in the geometry. + */ protected final int measures; + /** + * Sum of dimension and measures. + */ protected final int nbOrdinates; /** + * Get encoder for given shape type. * * @param shapeType shape type to encode * @return requested encoder */ - public static ShapeGeometryEncoder getEncoder(int shapeType) { + public static ShapeGeometryEncoder getEncoder(ShapeType 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; + case NULL: return Null.INSTANCE; + case POINT: return PointXY.INSTANCE; + case POLYLINE: return Polyline.INSTANCE; + case POLYGON: return Polygon.INSTANCE; + case 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; + case POINT_M: return PointXYM.INSTANCE; + case POLYLINE_M: return Polyline.INSTANCE_M; + case POLYGON_M: return Polygon.INSTANCE_M; + case 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; + case POINT_ZM: return PointXYZM.INSTANCE; + case POLYLINE_ZM: return Polyline.INSTANCE_ZM; + case POLYGON_ZM: return Polygon.INSTANCE_ZM; + case MULTIPOINT_ZM: return MultiPointXYZM.INSTANCE; + case MULTIPATCH_ZM: return MultiPatch.INSTANCE; default: throw new IllegalArgumentException("unknown shape type"); } } /** + * Constructor. + * * @param shapeType shape type code. + * @param geometryClass java geometry class * @param dimension number of dimensions in processed geometries. * @param measures number of measures in processed geometries. */ - protected ShapeGeometryEncoder(int shapeType, Class<T> geometryClass, int dimension, int measures) { + protected ShapeGeometryEncoder(ShapeType shapeType, Class<T> geometryClass, int dimension, int measures) { this.shapeType = shapeType; this.geometryClass = geometryClass; this.dimension = dimension; @@ -93,13 +111,17 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } /** - * @return shape type code. + * Get shape type. + * + * @return shape type. */ - public int getShapeType() { + public ShapeType getShapeType() { return shapeType; } /** + * Get java geometry value class. + * * @return geometry class handled by this encoder */ public Class<T> getValueClass() { @@ -107,6 +129,8 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } /** + * Get number of dimensions in processed geometries. + * * @return number of dimensions in processed geometries. */ public final int getDimension() { @@ -114,6 +138,8 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } /** + * Get number of measures in processed geometries. + * * @return number of measures in processed geometries. */ public final int getMeasures() { @@ -128,6 +154,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { * @param record to read into * @param filter optional filter envelope to stop geometry decoding as soon as possible * @return true if geometry pass the filter + * @throws IOException If an I/O error occurs */ public abstract boolean decode(ChannelDataInput channel, ShapeRecord record, Rectangle2D.Double filter) throws IOException; @@ -136,6 +163,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { * * @param channel to write into * @param shape geometry to encode + * @throws IOException If an I/O error occurs */ public abstract void encode(ChannelDataOutput channel, ShapeRecord shape) throws IOException; @@ -146,6 +174,12 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { */ public abstract int getEncodedLength(Geometry geom); + /** + * Calculate geometry bounding box. + * + * @param geom to compute + * @return geometry bounding box + */ public abstract GeneralEnvelope getBoundingBox(Geometry geom); /** @@ -155,6 +189,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { * @param shape to write into * @param filter optional filter envelope to stop geometry decoding as soon as possible * @return true if filter match or is null + * @throws IOException If an I/O error occurs */ protected boolean readBBox2D(ChannelDataInput channel, ShapeRecord shape, Rectangle2D.Double filter) throws IOException { final double minX = channel.readDouble(); @@ -178,6 +213,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { * * @param channel to write into * @param shape to read from + * @throws IOException If an I/O error occurs */ protected void writeBBox2D(ChannelDataOutput channel, ShapeRecord shape) throws IOException { final Envelope env2d = shape.geometry.getEnvelopeInternal(); @@ -188,11 +224,14 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } /** + * Read encoded lines. + * * @param channel to read from * @param shape to write into * @param filter optional filter envelope to stop geometry decoding as soon as possible * @param asRing true to produce LinearRing instead of LineString * @return null if filter do no match + * @throws IOException If an I/O error occurs */ protected LineString[] readLines(ChannelDataInput channel, ShapeRecord shape, Rectangle2D.Double filter, boolean asRing) throws IOException { if (!readBBox2D(channel, shape, filter)) return null; @@ -234,6 +273,15 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { return lines; } + /** + * Read lines ordinates. + * + * @param channel to read from + * @param shape to update + * @param lines to update + * @param ordinateIndex ordinate index to read + * @throws IOException If an I/O error occurs + */ protected void readLineOrdinates(ChannelDataInput channel, ShapeRecord shape, LineString[] lines, int ordinateIndex) throws IOException { final int nbDim = getDimension() + getMeasures(); shape.bbox.setRange(ordinateIndex, channel.readDouble(), channel.readDouble()); @@ -246,6 +294,13 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } } + /** + * Write given lines. + * + * @param channel to write into + * @param shape to write + * @throws IOException if an I/O exception occurs + */ protected void writeLines(ChannelDataOutput channel, ShapeRecord shape) throws IOException { writeBBox2D(channel, shape); final List<LineString> lines = extractRings(shape.geometry); @@ -277,6 +332,16 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { if (nbOrdinates == 4) writeLineOrdinates(channel, shape, lines, 3, nbPts); } + /** + * Write given lines ordinates. + * + * @param channel to write into + * @param shape to write + * @param lines to write + * @param ordinateIndex line coordinate ordinate to write + * @param nbPts number of points + * @throws IOException if an I/O exception occurs + */ protected void writeLineOrdinates(ChannelDataOutput channel, ShapeRecord shape,List<LineString> lines, int ordinateIndex, int nbPts) throws IOException { final double[] values = new double[nbPts]; @@ -297,6 +362,12 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { channel.writeDoubles(values); } + /** + * Extract all linear elements of given geometry. + * + * @param geom to extract lines from. + * @return list of lines + */ protected List<LineString> extractRings(Geometry geom) { final List<LineString> lst = new ArrayList(); extractRings(geom, lst); @@ -384,6 +455,12 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } } + /** + * Compute geometry bounding box. + * + * @param geom to compute + * @return geometry bounding box + */ protected GeneralEnvelope getLinesBoundingBox(Geometry geom) { final List<LineString> lines = extractRings(geom); @@ -434,7 +511,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final Null INSTANCE = new Null(); private Null() { - super(ShapeType.VALUE_NULL, Geometry.class, 2, 0); + super(ShapeType.NULL, Geometry.class, 2, 0); } @Override @@ -462,7 +539,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final PointXY INSTANCE = new PointXY(); private PointXY() { - super(ShapeType.VALUE_POINT, Point.class, 2,0); + super(ShapeType.POINT, Point.class, 2,0); } @Override @@ -504,7 +581,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final PointXYM INSTANCE = new PointXYM(); private PointXYM() { - super(ShapeType.VALUE_POINT_M, Point.class, 2, 1); + super(ShapeType.POINT_M, Point.class, 2, 1); } @Override @@ -552,7 +629,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final PointXYZM INSTANCE = new PointXYZM(); private PointXYZM() { - super(ShapeType.VALUE_POINT_ZM, Point.class, 3, 1); + super(ShapeType.POINT_ZM, Point.class, 3, 1); } @Override @@ -598,7 +675,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final MultiPointXY INSTANCE = new MultiPointXY(); private MultiPointXY() { - super(ShapeType.VALUE_MULTIPOINT, MultiPoint.class, 2, 0); + super(ShapeType.MULTIPOINT, MultiPoint.class, 2, 0); } @Override @@ -650,7 +727,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final MultiPointXYM INSTANCE = new MultiPointXYM(); private MultiPointXYM() { - super(ShapeType.VALUE_MULTIPOINT_M, MultiPoint.class, 2, 1); + super(ShapeType.MULTIPOINT_M, MultiPoint.class, 2, 1); } @Override @@ -722,7 +799,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final MultiPointXYZM INSTANCE = new MultiPointXYZM(); private MultiPointXYZM() { - super(ShapeType.VALUE_MULTIPOINT_ZM, MultiPoint.class, 3, 1); + super(ShapeType.MULTIPOINT_ZM, MultiPoint.class, 3, 1); } @Override @@ -810,11 +887,11 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static class Polyline extends ShapeGeometryEncoder<MultiLineString> { - private static final Polyline INSTANCE = new Polyline(ShapeType.VALUE_POLYLINE, 2, 0); - private static final Polyline INSTANCE_M = new Polyline(ShapeType.VALUE_POLYLINE_M, 3, 0); - private static final Polyline INSTANCE_ZM = new Polyline(ShapeType.VALUE_POLYLINE_ZM, 3, 1); + private static final Polyline INSTANCE = new Polyline(ShapeType.POLYLINE, 2, 0); + private static final Polyline INSTANCE_M = new Polyline(ShapeType.POLYLINE_M, 3, 0); + private static final Polyline INSTANCE_ZM = new Polyline(ShapeType.POLYLINE_ZM, 3, 1); - private Polyline(int shapeType, int dimension, int measures) { + private Polyline(ShapeType shapeType, int dimension, int measures) { super(shapeType, MultiLineString.class, dimension, measures); } @@ -850,11 +927,11 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static class Polygon extends ShapeGeometryEncoder<MultiPolygon> { - private static final Polygon INSTANCE = new Polygon(ShapeType.VALUE_POLYGON, 2, 0); - private static final Polygon INSTANCE_M = new Polygon(ShapeType.VALUE_POLYGON_M, 3, 0); - private static final Polygon INSTANCE_ZM = new Polygon(ShapeType.VALUE_POLYGON_ZM, 3, 1); + private static final Polygon INSTANCE = new Polygon(ShapeType.POLYGON, 2, 0); + private static final Polygon INSTANCE_M = new Polygon(ShapeType.POLYGON_M, 3, 0); + private static final Polygon INSTANCE_ZM = new Polygon(ShapeType.POLYGON_ZM, 3, 1); - private Polygon(int shapeType, int dimension, int measures) { + private Polygon(ShapeType shapeType, int dimension, int measures) { super(shapeType, MultiPolygon.class, dimension, measures); } @@ -897,7 +974,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { private static final MultiPatch INSTANCE = new MultiPatch(); private MultiPatch() { - super(ShapeType.VALUE_MULTIPATCH_ZM, MultiPolygon.class, 3, 1); + super(ShapeType.MULTIPATCH_ZM, MultiPolygon.class, 3, 1); } @Override 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 index dd89bdd7c8..1230638311 100644 --- 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 @@ -48,17 +48,24 @@ public final class ShapeHeader { /** * Shape type. */ - public int shapeType; + public ShapeType shapeType; /** * Shapefile bounding box without CRS. * Ordinates are in X,Y,Z,M order. */ public ImmutableEnvelope bbox; + /** + * Default constructor + */ public ShapeHeader() { - } + /** + * Copy constructor. + * + * @param toCopy header to copy + */ public ShapeHeader(ShapeHeader toCopy) { this.fileLength = toCopy.fileLength; this.shapeType = toCopy.shapeType; @@ -85,7 +92,7 @@ public final class ShapeHeader { if (version != 1000) { throw new IOException("Incorrect file version, expected 1000 but was " + version); } - shapeType = channel.readInt(); + shapeType = ShapeType.get(channel.readInt()); final double[] bb = channel.readDoubles(8); GeneralEnvelope bbox = new GeneralEnvelope(4); bbox.setRange(0, bb[0], bb[2]); @@ -107,7 +114,7 @@ public final class ShapeHeader { channel.writeInt(fileLength/2); channel.buffer.order(ByteOrder.LITTLE_ENDIAN); channel.writeInt(1000); - channel.writeInt(shapeType); + channel.writeInt(shapeType.getCode()); channel.writeDouble(bbox.getMinimum(0)); channel.writeDouble(bbox.getMinimum(1)); channel.writeDouble(bbox.getMaximum(0)); 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 index b1e097dfad..2564bd4e1f 100644 --- 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 @@ -36,6 +36,13 @@ public final class ShapeReader implements AutoCloseable{ private final ShapeGeometryEncoder geomParser; private final Rectangle2D.Double filter; + /** + * Construct reader from given channel and an optional rectangle filter. + * + * @param channel to read from + * @param filter optional filtering rectangle + * @throws IOException if a decoding error occurs + */ public ShapeReader(ChannelDataInput channel, Rectangle2D.Double filter) throws IOException { Objects.nonNull(channel); this.channel = channel; @@ -45,14 +52,31 @@ public final class ShapeReader implements AutoCloseable{ geomParser = ShapeGeometryEncoder.getEncoder(header.shapeType); } + /** + * Get header. + * + * @return shapefile header + */ public ShapeHeader getHeader() { return header; } + /** + * Move channel to given position. + * + * @param position new position + * @throws IOException if the stream cannot be moved to the given position. + */ public void moveToOffset(long position) throws IOException { channel.seek(position); } + /** + * Get next record. + * + * @return record or null if there is no more record + * @throws IOException if a decoding error occurs + */ public ShapeRecord next() throws IOException { final ShapeRecord record = new ShapeRecord(); try { @@ -72,6 +96,11 @@ public final class ShapeReader implements AutoCloseable{ } } + /** + * Release reader resources. + * + * @throws IOException If an I/O error occurs + */ @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 index 77a0776864..f288e045f4 100644 --- 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 @@ -19,18 +19,15 @@ package org.apache.sis.storage.shapefile.shp; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.nio.ByteOrder; - -import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.apache.sis.geometry.GeneralEnvelope; -import org.apache.sis.geometry.Envelope2D; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.ChannelDataOutput; -import org.locationtech.jts.geom.MultiPoint; -import org.locationtech.jts.geom.Point; - /** + * A record in a shape file. + * Contains a unique record number and it's associated geometry. + * * @author Johann Sorel (Geomatys) */ public final class ShapeRecord { @@ -40,19 +37,36 @@ public final class ShapeRecord { */ public int recordNumber; /** - * Encoded geometry. + * Record geometry */ - public byte[] content; - public Geometry geometry; - + /** + * Geometry bounding box + */ public GeneralEnvelope bbox; + /** + * Default constructor + */ + public ShapeRecord() { + } + + /** + * Constructor with initialization. + * + * @param recordNumber initial record number + * @param geometry initial geometry + */ + public ShapeRecord(int recordNumber, Geometry geometry) { + this.recordNumber = recordNumber; + this.geometry = geometry; + } + /** * Read this shape record. * * @param channel input channel, not null - * @param io geometry decoder, if null gemetry content will be stored in content array, otherwise geometry will be parsed + * @param io geometry decoder * @param filter optional filter envelope to stop geometry decoding as soon as possible * @return true if geometry pass the filter or if there is no filter * @throws IOException if an error occurred while reading. @@ -66,31 +80,26 @@ public final class ShapeRecord { final long position = channel.getStreamPosition(); channel.buffer.order(ByteOrder.LITTLE_ENDIAN); final int shapeType = channel.readInt(); - if (io == null) { - content = channel.readBytes(byteSize); - return true; - } else { - final boolean match = io.decode(channel,this, filter); - if (!match) { - //move to record end - channel.seek(position + byteSize); - } - return match; + final boolean match = io.decode(channel,this, filter); + if (!match) { + //move to record end + channel.seek(position + byteSize); } + return match; } /** * Write this shape record. * @param channel output channel to write into, not null * @param io geometry encoder - * @throws IOException + * @throws IOException if an error occurred while writing. */ 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()); + channel.writeInt(io.getShapeType().getCode()); 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 index 08b0d452b7..2eb9a32b56 100644 --- 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 @@ -35,56 +35,93 @@ import java.util.Map; */ public enum ShapeType { + /** + * Null geometry type + */ NULL (0), + /** + * Point geometry type + */ POINT(1), + /** + * Polyline geometry type + */ POLYLINE(3), + /** + * Polygon geometry type + */ POLYGON(5), + /** + * MultiPoint geometry type + */ MULTIPOINT(8), + /** + * Point with M geometry type + */ POINT_M(11), + /** + * Polyline with M geometry type + */ POLYLINE_M(13), + /** + * Polygon with M geometry type + */ POLYGON_M(15), + /** + * MultiPoint with M geometry type + */ MULTIPOINT_M(18), + /** + * Point with Z and M geometry type + */ POINT_ZM(21), + /** + * Polyline with Z and M geometry type + */ POLYLINE_ZM(23), + /** + * Polygon with Z and M geometry type + */ POLYGON_ZM(25), + /** + * MultiPoint with Z and M geometry type + */ MULTIPOINT_ZM(28), + /** + * MultiPatch with Z and M geometry type + */ 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 final int code; private ShapeType (int value ) { - this.value = value; + this.code = value; } - public int getValue() { - return value; + /** + * Get geometry type code. + * + * @return geometry type code + */ + public int getCode() { + return code; } private static final Map<Integer, ShapeType> lookup = new HashMap<Integer, ShapeType>(); static { for (ShapeType ste : EnumSet.allOf(ShapeType.class)) { - lookup.put(ste.getValue(), ste); + lookup.put(ste.getCode(), ste); } } - public static ShapeType get(int value) { - return lookup.get(value); + /** + * Get geometry type for given code. + * + * @param code geometry code + * @return ShapeType for given code + */ + public static ShapeType get(int code) { + return lookup.get(code); } } 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 index cabd09d663..ccce608141 100644 --- 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 @@ -20,6 +20,7 @@ import java.io.IOException; import org.apache.sis.io.stream.ChannelDataOutput; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.ImmutableEnvelope; +import org.locationtech.jts.geom.Geometry; /** @@ -35,15 +36,26 @@ public final class ShapeWriter implements AutoCloseable{ private GeneralEnvelope bbox; private ShapeGeometryEncoder io; - public ShapeWriter(ChannelDataOutput channel) throws IOException { + /** + * Construct writer above given channel. + * + * @param channel to write into + */ + public ShapeWriter(ChannelDataOutput channel) { this.channel = channel; } + /** + * Get header, not null after writeHeader has been call. + * @return shapefile header + */ public ShapeHeader getHeader() { return header; } /** + * Get current position in the stream. + * * @return current position in the stream */ public long getSteamPosition() { @@ -53,8 +65,10 @@ public final class ShapeWriter implements AutoCloseable{ /** * Header will be copied and modified. * Use getHeader to obtain the new header. + * @param header to write, not null + * @throws IOException If an I/O error occurs */ - public void write(ShapeHeader header) throws IOException { + public void writeHeader(ShapeHeader header) throws IOException { this.header = new ShapeHeader(header); this.header.fileLength = 0; this.header.bbox = new ImmutableEnvelope(new GeneralEnvelope(4)); @@ -62,7 +76,24 @@ public final class ShapeWriter implements AutoCloseable{ io = ShapeGeometryEncoder.getEncoder(header.shapeType); } - public void write(ShapeRecord record) throws IOException { + /** + * Write a new record. + * + * @param recordNumber record number + * @param geometry record geometry + * @throws IOException If an I/O error occurs + */ + public void writeRecord(int recordNumber, Geometry geometry) throws IOException { + writeRecord(new ShapeRecord(recordNumber, geometry)); + } + + /** + * Write a new record. + * + * @param record new record + * @throws IOException If an I/O error occurs + */ + public void writeRecord(ShapeRecord record) throws IOException { record.write(channel, io); final GeneralEnvelope geomBox = io.getBoundingBox(record.geometry); if (bbox == null) { @@ -73,13 +104,14 @@ public final class ShapeWriter implements AutoCloseable{ } } - public void flush() throws IOException { - channel.flush(); - } - + /** + * Release writer resources. + * + * @throws IOException If an I/O error occurs + */ @Override public void close() throws IOException { - flush(); + channel.flush(); //update header and rewrite it //update the file length diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/package-info.java similarity index 66% copy from incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java copy to incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/package-info.java index 174e38140d..e5c4125d38 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/package-info.java @@ -16,11 +16,14 @@ */ /** - * Shapefile. + * Shapefile format reader and writer. * - * <div class="warning">This is an experimental package, - * not yet target for any Apache SIS release at this time.</div> + * <h2>Reading example</h2> + *{@snippet class="org.apache.sis.storage.shapefile.shp.Snippets" region="read"} * - * @author Johann Sorel (Geomatys) + * <h2>Writing example</h2> + *{@snippet class="org.apache.sis.storage.shapefile.shp.Snippets" region="write"} + * + * @see <a href="http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf">ESRI Shapefile Specification</a> */ -package org.apache.sis.storage.shapefile; +package org.apache.sis.storage.shapefile.shp; diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java index ad0d49d525..c0cdcd40c8 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexReader.java @@ -32,22 +32,42 @@ public final class IndexReader implements AutoCloseable{ private final ChannelDataInput channel; private final ShapeHeader header; + /** + * Constructor. + * + * @param channel to read from + * @throws IOException if a decoding error occurs on the header + */ public IndexReader(ChannelDataInput channel) throws IOException { this.channel = channel; header = new ShapeHeader(); header.read(channel); } + /** + * Get shapefile header. + * + * @return shapefile header + */ public ShapeHeader getHeader() { return header; } + /** + * Move channel to given position. + * + * @param position new position + * @throws IOException if the stream cannot be moved to the given position. + */ public void moveToOffset(long position) throws IOException { channel.seek(position); } /** - * @return offset and length of the record in the shp file + * Get next record. + * + * @return next offset and length of the record in the shp file, null if no more records + * @throws IOException if a decoding error occurs */ public int[] next() throws IOException { try { @@ -58,6 +78,11 @@ public final class IndexReader implements AutoCloseable{ } } + /** + * Release reader resources. + * + * @throws IOException If an I/O error occurs + */ @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/shx/IndexWriter.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexWriter.java index b8666de7d0..2a242521aa 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexWriter.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/IndexWriter.java @@ -32,35 +32,56 @@ public final class IndexWriter implements AutoCloseable{ private ShapeHeader header; - public IndexWriter(ChannelDataOutput channel) throws IOException { + /** + * Constructor. + * + * @param channel to write into + */ + public IndexWriter(ChannelDataOutput channel) { this.channel = channel; } + /** + * Get shapefile header, null before the call to writeHeader. + * + * @return shapefile header + */ public ShapeHeader getHeader() { return header; } /** * Header will be copied and modified. * Use getHeader to obtain the new header. + * + * @param header to write + * @throws IOException If an I/O error occurs */ - public void write(ShapeHeader header) throws IOException { + public void writeHeader(ShapeHeader header) throws IOException { this.header = new ShapeHeader(header); this.header.fileLength = 0; header.write(channel); } - public void write(int offset, int length) throws IOException { + /** + * Write a new record. + * + * @param offset record offset + * @param length record length + * @throws IOException If an I/O error occurs + */ + public void writeRecord(int offset, int length) throws IOException { channel.writeInt(offset); channel.writeInt(length); } - public void flush() throws IOException { - channel.flush(); - } - + /** + * Release writer resources. + * + * @throws IOException If an I/O error occurs + */ @Override public void close() throws IOException { - flush(); + channel.flush(); //update header and rewrite it //update the file length diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/package-info.java similarity index 78% copy from incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java copy to incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/package-info.java index 174e38140d..e8a06a540e 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/package-info.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shx/package-info.java @@ -16,11 +16,6 @@ */ /** - * Shapefile. - * - * <div class="warning">This is an experimental package, - * not yet target for any Apache SIS release at this time.</div> - * - * @author Johann Sorel (Geomatys) + * Shapefile Shx index reader and writer. */ -package org.apache.sis.storage.shapefile; +package org.apache.sis.storage.shapefile.shx; diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/Snippets.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/Snippets.java new file mode 100644 index 0000000000..99f9057133 --- /dev/null +++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/Snippets.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.shapefile; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Stream; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.filter.DefaultFilterFactory; +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.FeatureQuery; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; +import org.opengis.filter.BinarySpatialOperator; +import org.opengis.filter.FilterFactory; + +/** + * + * @author Johann Sorel (Geomatys) + */ +final class Snippets { + + public void read() throws IllegalArgumentException, DataStoreException, IOException{ + // @start region="read" + //open datastore + try (ShapefileStore store = new ShapefileStore(Paths.get("/path/to/file.shp"))) { + + //print feature type + System.out.println(store.getType()); + + //print all features + try (Stream<Feature> features = store.features(false)) { + features.forEach(System.out::println); + } + + //print only features in envelope and only selected attributes + GeneralEnvelope bbox = new GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic()); + bbox.setRange(0, -10, 30); + bbox.setRange(1, 45, 55); + FilterFactory<Feature, Object, Object> ff = DefaultFilterFactory.forFeatures(); + BinarySpatialOperator<Feature> bboxFilter = ff.bbox(ff.property("geometry"), bbox); + + FeatureQuery query = new FeatureQuery(); + query.setProjection("att1", "att4", "att5"); + query.setSelection(bboxFilter); + + //print selected features + try (Stream<Feature> features = store.subset(query).features(false)) { + features.forEach(System.out::println); + } + + } + // @end + + } + + public void write() throws IllegalArgumentException, DataStoreException, IOException{ + // @start region="write" + //open a channel + try (ShapefileStore store = new ShapefileStore(Paths.get("/path/to/file.shp"))) { + + //define the feature type + FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName("test"); + ftb.addAttribute(Integer.class).setName("id"); + ftb.addAttribute(String.class).setName("text"); + ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()); + FeatureType type = ftb.build(); + + store.updateType(type); + type = store.getType(); + + //create features + GeometryFactory gf = new GeometryFactory(); + Feature feature = type.newInstance(); + feature.setPropertyValue("geometry", gf.createPoint(new Coordinate(10,20))); + feature.setPropertyValue("id", 1); + feature.setPropertyValue("text", "some text 1"); + + //add feature in the store + store.add(List.of(feature).iterator()); + } + // @end + } + +} diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java index e94274e828..bf86fae187 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java +++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java @@ -66,9 +66,9 @@ public class DBFIOTest { try (DBFReader reader = new DBFReader(cdi, StandardCharsets.UTF_8, null)) { final DBFHeader header = reader.getHeader(); - assertEquals(123, header.year); - assertEquals(10, header.month); - assertEquals(27, header.day); + assertEquals(123, header.lastUpdate.getYear()-1900); + assertEquals(10, header.lastUpdate.getMonthValue()); + assertEquals(27, header.lastUpdate.getDayOfMonth()); assertEquals(2, header.nbRecord); assertEquals(193, header.headerSize); assertEquals(120, header.recordSize); @@ -100,18 +100,18 @@ public class DBFIOTest { assertEquals(0, header.fields[4].fieldDecimals); - final DBFRecord record1 = reader.next(); - assertEquals(1L, record1.fields[0]); - assertEquals("text1", record1.fields[1]); - assertEquals(10L, record1.fields[2]); - assertEquals(20.0, record1.fields[3]); - assertEquals(LocalDate.of(2023, 10, 27), record1.fields[4]); + final Object[] record1 = reader.next(); + assertEquals(1L, record1[0]); + assertEquals("text1", record1[1]); + assertEquals(10L, record1[2]); + assertEquals(20.0, record1[3]); + assertEquals(LocalDate.of(2023, 10, 27), record1[4]); - final DBFRecord record2 = reader.next(); - assertEquals(2L, record2.fields[0]); - assertEquals(40L, record2.fields[2]); - assertEquals(60.0, record2.fields[3]); - assertEquals(LocalDate.of(2023, 10, 28), record2.fields[4]); + final Object[] record2 = reader.next(); + assertEquals(2L, record2[0]); + assertEquals(40L, record2[2]); + assertEquals(60.0, record2[3]); + assertEquals(LocalDate.of(2023, 10, 28), record2[4]); //no more records assertNull(reader.next()); @@ -138,10 +138,10 @@ public class DBFIOTest { try (DBFReader reader = new DBFReader(cdi, StandardCharsets.US_ASCII, null); DBFWriter writer = new DBFWriter(cdo)) { - writer.write(reader.getHeader()); + writer.writeHeader(reader.getHeader()); - for (DBFRecord record = reader.next(); record != null; record = reader.next()) { - writer.write(record); + for (Object[] record = reader.next(); record != null; record = reader.next()) { + writer.writeRecord(record); } } @@ -166,13 +166,13 @@ public class DBFIOTest { try (DBFReader reader = new DBFReader(cdi, StandardCharsets.UTF_8, new int[]{1,3})) { final DBFHeader header = reader.getHeader(); - final DBFRecord record1 = reader.next(); - assertEquals("text1", record1.fields[0]); - assertEquals(20.0, record1.fields[1]); + final Object[] record1 = reader.next(); + assertEquals("text1", record1[0]); + assertEquals(20.0, record1[1]); - final DBFRecord record2 = reader.next(); - assertEquals("text2", record2.fields[0]); - assertEquals(60.0, record2.fields[1]); + final Object[] record2 = reader.next(); + assertEquals("text2", record2[0]); + assertEquals(60.0, record2[1]); //no more records assertNull(reader.next()); diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/Snippets.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/Snippets.java new file mode 100644 index 0000000000..0813919903 --- /dev/null +++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/Snippets.java @@ -0,0 +1,95 @@ +/* + * 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.dbf; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.OpenOption; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDate; +import org.apache.sis.io.stream.ChannelDataInput; +import org.apache.sis.io.stream.ChannelDataOutput; +import org.apache.sis.setup.OptionKey; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.StorageConnector; + +/** + * + * @author Johann Sorel (Geomatys) + */ +final class Snippets { + + public void read() throws IllegalArgumentException, DataStoreException, IOException{ + // @start region="read" + //open a channel + StorageConnector cnx = new StorageConnector(Paths.get("/path/to/file.dbf")); + ChannelDataInput channel = cnx.getStorageAs(ChannelDataInput.class); + try (DBFReader reader = new DBFReader(channel, StandardCharsets.UTF_8, null)) { + + //print the DBase fields + DBFHeader header = reader.getHeader(); + for (DBFField field : header.fields) { + System.out.println(field); + } + + //iterate over records + for (Object[] record = reader.next(); record != null; record = reader.next()){ + + if (record == DBFReader.DELETED_RECORD) { + //a deleted record, those should be ignored + continue; + } + + //print record values + for (int i = 0; i < header.fields.length; i++) { + System.out.println(header.fields[i].fieldName + " : " + record[i]); + } + } + } + // @end + } + + public void write() throws IllegalArgumentException, DataStoreException, IOException{ + // @start region="write" + //open a channel + StorageConnector cnx = new StorageConnector(Paths.get("/path/to/file.dbf")); + cnx.setOption(OptionKey.OPEN_OPTIONS, new OpenOption[]{StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING}); + ChannelDataOutput channel = cnx.getStorageAs(ChannelDataOutput.class); + + //define the header + Charset charset = StandardCharsets.UTF_8; + DBFHeader header = new DBFHeader(); + header.lastUpdate = LocalDate.now(); + header.fields = new DBFField[] { + new DBFField("id", DBFField.TYPE_NUMBER, 0, 8, 0, charset), + new DBFField("desc", DBFField.TYPE_CHAR, 0, 255, 0, charset), + new DBFField("value", DBFField.TYPE_NUMBER, 0, 11, 6, charset) + }; + + //write records + try (DBFWriter writer = new DBFWriter(channel)) { + writer.writeHeader(header); + writer.writeRecord(1, "A short description", 3.14); + writer.writeRecord(2, "Another short description", 123.456); + // ... more records + } + // @end + } + +} 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 index cb36bd7458..0cd236e313 100644 --- 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 @@ -79,10 +79,10 @@ public class ShapeIOTest { try (ShapeReader reader = new ShapeReader(cdi, null); ShapeWriter writer = new ShapeWriter(cdo)) { - writer.write(reader.getHeader()); + writer.writeHeader(reader.getHeader()); for (ShapeRecord record = reader.next(); record != null; record = reader.next()) { - writer.write(record); + writer.writeRecord(record); } } diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/shp/Snippets.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/shp/Snippets.java new file mode 100644 index 0000000000..9ba5ee2e12 --- /dev/null +++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/shp/Snippets.java @@ -0,0 +1,81 @@ +/* + * 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.nio.file.OpenOption; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import org.apache.sis.io.stream.ChannelDataInput; +import org.apache.sis.io.stream.ChannelDataOutput; +import org.apache.sis.setup.OptionKey; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.StorageConnector; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; + +/** + * + * @author Johann Sorel (Geomatys) + */ +final class Snippets { + + public void read() throws IllegalArgumentException, DataStoreException, IOException{ + // @start region="read" + //open a channel + StorageConnector cnx = new StorageConnector(Paths.get("/path/to/file.shp")); + ChannelDataInput channel = cnx.getStorageAs(ChannelDataInput.class); + try (ShapeReader reader = new ShapeReader(channel, null)) { + + //print the DBase fields + ShapeHeader header = reader.getHeader(); + System.out.println(header.shapeType); + + //iterate over records + for (ShapeRecord record = reader.next(); record != null; record = reader.next()){ + System.out.println(record.recordNumber); + System.out.println(record.bbox); + System.out.println(record.geometry.toText()); + } + } + // @end + } + + public void write() throws IllegalArgumentException, DataStoreException, IOException{ + // @start region="write" + //open a channel + StorageConnector cnx = new StorageConnector(Paths.get("/path/to/file.shp")); + cnx.setOption(OptionKey.OPEN_OPTIONS, new OpenOption[]{StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING}); + ChannelDataOutput channel = cnx.getStorageAs(ChannelDataOutput.class); + + //define the header + ShapeHeader header = new ShapeHeader(); + header.shapeType = ShapeType.POINT; + + //write records + GeometryFactory gf = new GeometryFactory(); + try (ShapeWriter writer = new ShapeWriter(channel)) { + writer.writeHeader(header); + int recordNumber = 1; + writer.writeRecord(recordNumber++, gf.createPoint(new Coordinate(10,20))); + writer.writeRecord(recordNumber++, gf.createPoint(new Coordinate(-7, 45))); + // ... more records + } + // @end + } + +}