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 94bffce331 feat(Shapefile): support updateType, add a first writing support 94bffce331 is described below commit 94bffce33101a36a15f7d5a6a3d8d618455a47a9 Author: jsorel <johann.so...@geomatys.com> AuthorDate: Wed Nov 29 17:18:45 2023 +0100 feat(Shapefile): support updateType, add a first writing support --- .../sis/storage/shapefile/ShapefileStore.java | 433 +++++++++++++++++---- .../shapefile/shp/ShapeGeometryEncoder.java | 231 +++++++++-- .../sis/storage/shapefile/shp/ShapeRecord.java | 4 + .../sis/storage/shapefile/shp/ShapeWriter.java | 14 +- .../sis/storage/shapefile/ShapefileStoreTest.java | 122 +++++- .../test/module-info.java | 3 + 6 files changed, 678 insertions(+), 129 deletions(-) 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 303b6d6f20..2d6fe5e827 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 @@ -24,9 +24,7 @@ import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; +import java.nio.file.*; import java.time.LocalDate; import java.util.AbstractMap; import java.util.ArrayList; @@ -34,12 +32,14 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map.Entry; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; @@ -49,6 +49,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; import java.util.stream.StreamSupport; + +import org.apache.sis.geometry.ImmutableEnvelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; @@ -76,6 +78,9 @@ import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.ChannelDataOutput; import org.apache.sis.io.stream.IOUtilities; +import org.apache.sis.io.wkt.Convention; +import org.apache.sis.io.wkt.WKTFormat; +import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.parameter.Parameters; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.CommonCRS; @@ -587,116 +592,205 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe } @Override - public void updateType(FeatureType newType) throws DataStoreException { - if (true) throw new UnsupportedOperationException("Not supported yet."); + public synchronized void updateType(FeatureType newType) throws DataStoreException { if (!isDefaultView()) throw new DataStoreException("Resource not writable in current filter state"); if (Files.exists(shpPath)) { throw new DataStoreException("Update type is possible only when files do not exist. It can be used to create a new shapefile but not to update one."); } - final ShapeHeader shpHeader = new ShapeHeader(); - final DBFHeader dbfHeader = new DBFHeader(); - final Charset charset = userDefinedCharSet == null ? StandardCharsets.UTF_8 : userDefinedCharSet; - CoordinateReferenceSystem crs = CommonCRS.WGS84.normalizedGeographic(); - - for (PropertyType pt : newType.getProperties(true)) { - if (pt instanceof AttributeType) { - final AttributeType at = (AttributeType) pt; - final Class valueClass = at.getValueClass(); - - Integer length = AttributeConvention.getMaximalLengthCharacteristic(newType, pt); - if (length == 0) length = 255; + lock.writeLock().lock(); + try { + final ShapeHeader shpHeader = new ShapeHeader(); + shpHeader.bbox = new ImmutableEnvelope(new GeneralEnvelope(4)); + final DBFHeader dbfHeader = new DBFHeader(); + dbfHeader.fields = new DBFField[0]; + final Charset charset = userDefinedCharSet == null ? StandardCharsets.UTF_8 : userDefinedCharSet; + CoordinateReferenceSystem crs = CommonCRS.WGS84.normalizedGeographic(); + + for (PropertyType pt : newType.getProperties(true)) { + if (pt instanceof AttributeType) { + final AttributeType at = (AttributeType) pt; + final Class valueClass = at.getValueClass(); + final String attName = at.getName().tip().toString(); + + Integer length = AttributeConvention.getMaximalLengthCharacteristic(newType, pt); + if (length == null || length == 0) length = 255; + + if (Geometry.class.isAssignableFrom(valueClass)) { + if (shpHeader.shapeType != 0) { + throw new DataStoreException("Shapefile format can only contain one geometry"); + } + if (Point.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.VALUE_POINT; + else if (MultiPoint.class.isAssignableFrom(valueClass)) + shpHeader.shapeType = ShapeType.VALUE_MULTIPOINT; + else if (LineString.class.isAssignableFrom(valueClass) || MultiLineString.class.isAssignableFrom(valueClass)) + shpHeader.shapeType = ShapeType.VALUE_POLYLINE; + else if (Polygon.class.isAssignableFrom(valueClass) || MultiPolygon.class.isAssignableFrom(valueClass)) + shpHeader.shapeType = ShapeType.VALUE_POLYGON; + else throw new DataStoreException("Unsupported geometry type " + valueClass); + + Object cdt = at.characteristics().get(AttributeConvention.CRS); + if (cdt instanceof AttributeType) { + Object defaultValue = ((AttributeType) cdt).getDefaultValue(); + if (defaultValue instanceof CoordinateReferenceSystem) { + crs = (CoordinateReferenceSystem) defaultValue; + } + } - if (Geometry.class.isAssignableFrom(valueClass)) { - if (shpHeader.shapeType != 0) { - throw new DataStoreException("Shapefile format can only contain one geometry"); - } - if (Point.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.VALUE_POINT; - else if (MultiPoint.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.VALUE_MULTIPOINT; - else if (LineString.class.isAssignableFrom(valueClass) || MultiLineString.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.VALUE_POLYLINE; - else if (Polygon.class.isAssignableFrom(valueClass) || MultiPolygon.class.isAssignableFrom(valueClass)) shpHeader.shapeType = ShapeType.VALUE_POLYGON; - else throw new DataStoreException("Unsupported geometry type " + valueClass); - - Object cdt = at.characteristics().get(AttributeConvention.CRS_CHARACTERISTIC); - if (cdt instanceof CoordinateReferenceSystem) { - crs = (CoordinateReferenceSystem) cdt; + } else if (String.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_CHAR, 0, length, 0, charset)); + } else if (Byte.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_NUMBER, 0, 4, 0, null)); + } else if (Short.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_NUMBER, 0, 6, 0, null)); + } else if (Integer.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_NUMBER, 0, 9, 0, null)); + } else if (Long.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_NUMBER, 0, 19, 0, null)); + } else if (Float.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_NUMBER, 0, 11, 8, null)); + } else if (Double.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_NUMBER, 0, 33, 30, null)); + } else if (LocalDate.class.isAssignableFrom(valueClass)) { + dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) DBFField.TYPE_DATE, 0, 20, 0, null)); + } else { + LOGGER.log(Level.WARNING, "Shapefile writing, field {0} is not supported", pt.getName()); } - - } else if (String.class.isAssignableFrom(valueClass)) { - dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(idField, (char)DBFField.TYPE_CHAR, 0, length, 0, charset)); - } else if (Byte.class.isAssignableFrom(valueClass)) { - dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 4, 0, null)); - } else if (Short.class.isAssignableFrom(valueClass)) { - dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 6, 0, null)); - } else if (Integer.class.isAssignableFrom(valueClass)) { - dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 9, 0, null)); - } else if (Long.class.isAssignableFrom(valueClass)) { - dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 19, 0, null)); - } else if (Float.class.isAssignableFrom(valueClass)) { - dbfHeader.fields = ArraysExt.append(dbfHeader.fields, new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 33, 0, null)); - } else if (Double.class.isAssignableFrom(valueClass)) { - - } else if (LocalDate.class.isAssignableFrom(valueClass)) { - } else { LOGGER.log(Level.WARNING, "Shapefile writing, field {0} is not supported", pt.getName()); } - } else { - LOGGER.log(Level.WARNING, "Shapefile writing, field {0} is not supported", pt.getName()); } - } - //write shapefile - try (ShapeWriter writer = new ShapeWriter(ShpFiles.openWriteChannel(files.shpFile))) { - writer.write(shpHeader); - } catch (IOException ex){ - throw new DataStoreException("Failed to create shapefile (shp).", ex); - } - - //write shx - try (IndexWriter writer = new IndexWriter(ShpFiles.openWriteChannel(files.shxFile))) { - writer.write(shpHeader); - } catch (IOException ex){ - throw new DataStoreException("Failed to create shapefile (shx).", ex); - } + //write shapefile + try (ShapeWriter writer = new ShapeWriter(ShpFiles.openWriteChannel(files.shpFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { + writer.write(shpHeader); + } catch (IOException ex) { + throw new DataStoreException("Failed to create shapefile (shp).", ex); + } - //write dbf - try (DBFWriter writer = new DBFWriter(ShpFiles.openWriteChannel(files.dbfFile))) { - writer.write(dbfHeader); - } catch (IOException ex){ - throw new DataStoreException("Failed to create shapefile (dbf).", ex); - } + //write shx + try (IndexWriter writer = new IndexWriter(ShpFiles.openWriteChannel(files.getShx(true), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { + writer.write(shpHeader); + } catch (IOException ex) { + throw new DataStoreException("Failed to create shapefile (shx).", ex); + } - //write cpg - try { - CpgFiles.write(charset, files.cpgFile); - } catch (IOException ex) { - throw new DataStoreException("Failed to create shapefile (cpg).", ex); - } + //write dbf + try (DBFWriter writer = new DBFWriter(ShpFiles.openWriteChannel(files.getDbf(true), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { + writer.write(dbfHeader); + } catch (IOException ex) { + throw new DataStoreException("Failed to create shapefile (dbf).", ex); + } - //write prj - //todo + //write cpg + try { + CpgFiles.write(charset, files.getCpg(true)); + } catch (IOException ex) { + throw new DataStoreException("Failed to create shapefile (cpg).", ex); + } + //write prj + try { + final WKTFormat format = new WKTFormat(Locale.ENGLISH, null); + format.setConvention(Convention.WKT1_COMMON_UNITS); + format.setNameAuthority(Citations.ESRI); + format.setIndentation(WKTFormat.SINGLE_LINE); + Files.writeString(files.getPrj(true), format.format(crs), StandardCharsets.ISO_8859_1, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + } catch (IOException ex) { + throw new DataStoreException("Failed to create shapefile (prj).", ex); + } + //update file list + files.scan(); + } finally { + lock.writeLock().unlock(); + } } @Override public void add(Iterator<? extends Feature> features) throws DataStoreException { if (!isDefaultView()) throw new DataStoreException("Resource not writable in current filter state"); - throw new UnsupportedOperationException("Not supported yet."); + if (!Files.exists(shpPath)) throw new DataStoreException("FeatureType do not exist, use updateType before modifying features."); + + final Writer writer = new Writer(charset); + try { + //write existing features + try (Stream<Feature> stream = features(false)) { + Iterator<Feature> iterator = stream.iterator(); + while (iterator.hasNext()) { + writer.write(iterator.next()); + } + } + + //write new features + while (features.hasNext()) { + writer.write(features.next()); + } + + writer.finish(true); + } catch (IOException ex) { + try { + writer.finish(false); + } catch (IOException e) { + ex.addSuppressed(e); + } + throw new DataStoreException("Writing failed", ex); + } } @Override public void removeIf(Predicate<? super Feature> filter) throws DataStoreException { if (!isDefaultView()) throw new DataStoreException("Resource not writable in current filter state"); - throw new UnsupportedOperationException("Not supported yet."); + if (!Files.exists(shpPath)) throw new DataStoreException("FeatureType do not exist, use updateType before modifying features."); + + final Writer writer = new Writer(charset); + try { + //write existing features not matching filter + try (Stream<Feature> stream = features(false)) { + Iterator<Feature> iterator = stream.filter(filter.negate()).iterator(); + while (iterator.hasNext()) { + writer.write(iterator.next()); + } + } + writer.finish(true); + } catch (IOException ex) { + try { + writer.finish(false); + } catch (IOException e) { + ex.addSuppressed(e); + } + throw new DataStoreException("Writing failed", ex); + } } @Override public void replaceIf(Predicate<? super Feature> filter, UnaryOperator<Feature> updater) throws DataStoreException { if (!isDefaultView()) throw new DataStoreException("Resource not writable in current filter state"); - throw new UnsupportedOperationException("Not supported yet."); + if (!Files.exists(shpPath)) throw new DataStoreException("FeatureType do not exist, use updateType before modifying features."); + + final Writer writer = new Writer(charset); + try { + //write existing features applying modifications + try (Stream<Feature> stream = features(false)) { + Iterator<Feature> iterator = stream.iterator(); + while (iterator.hasNext()) { + Feature feature = iterator.next(); + if (filter.test(feature)) { + feature = updater.apply(feature); + } + if (feature != null) writer.write(feature); + } + } + writer.finish(true); + } catch (IOException ex) { + try { + writer.finish(false); + } catch (IOException e) { + ex.addSuppressed(e); + } + throw new DataStoreException("Writing failed", ex); + } } @Override @@ -707,7 +801,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe final Path dbf = files.getDbf(false); final Path prj = files.getPrj(false); final Path cpg = files.getCpg(false); - if (shp != null && Files.exists(shp)) paths.add(shp); + if ( Files.exists(shp)) paths.add(shp); if (shx != null && Files.exists(shx)) paths.add(shx); if (dbf != null && Files.exists(dbf)) paths.add(dbf); if (prj != null && Files.exists(prj)) paths.add(prj); @@ -734,6 +828,14 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe final String fileName = shpFile.getFileName().toString(); baseUpper = Character.isUpperCase(fileName.codePointAt(fileName.length()-1)); this.baseName = IOUtilities.filenameWithoutExtension(fileName); + scan(); + } + + /** + * Search related files. + * Should be called after data have been modified. + */ + private void scan() { shxFile = findSibling("shx"); dbfFile = findSibling("dbf"); prjFile = findSibling("prj"); @@ -784,6 +886,45 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe return cpgFile; } + /** + * Create a set of temporary files for edition. + */ + private ShpFiles createTempFiles() throws IOException{ + final Path tmp = Files.createTempFile("tmp", ".shp"); + Files.delete(tmp); + return new ShpFiles(tmp); + } + + /** + * Delete files permanently. + */ + private void deleteFiles() throws IOException{ + Files.deleteIfExists(shpFile); + if (shxFile != null) Files.deleteIfExists(shxFile); + if (dbfFile != null) Files.deleteIfExists(dbfFile); + if (cpgFile != null) Files.deleteIfExists(cpgFile); + if (prjFile != null) Files.deleteIfExists(prjFile); + } + + /** + * Override target files by current ones. + */ + private void replace(ShpFiles toReplace) throws IOException{ + replace(shpFile, toReplace.shpFile); + replace(shxFile, toReplace.getShx(true)); + replace(dbfFile, toReplace.getDbf(true)); + replace(cpgFile, toReplace.getCpg(true)); + replace(prjFile, toReplace.getPrj(true)); + } + + private static void replace(Path current, Path toReplace) throws IOException{ + if (current == null) { + Files.deleteIfExists(toReplace); + } else { + Files.move(current, toReplace, StandardCopyOption.REPLACE_EXISTING); + } + } + private Path findSibling(String extension) { Path candidate = shpFile.getParent().resolve(baseName + "." + extension); if (java.nio.file.Files.isRegularFile(candidate)) return candidate; @@ -797,9 +938,14 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe return new ChannelDataInput(path.getFileName().toString(), channel, ByteBuffer.allocate(8192), false); } - private static ChannelDataOutput openWriteChannel(Path path) throws IOException, IllegalArgumentException, DataStoreException { - final WritableByteChannel wbc = Files.newByteChannel(path, StandardOpenOption.WRITE); - return new ChannelDataOutput(path.getFileName().toString(), wbc, ByteBuffer.allocate(8000)); + private static ChannelDataOutput openWriteChannel(Path path, OpenOption ... options) throws IOException, IllegalArgumentException, DataStoreException { + final WritableByteChannel wbc; + if (options != null && options.length > 0) { + wbc = Files.newByteChannel(path, ArraysExt.append(options, StandardOpenOption.WRITE)); + } else { + wbc = Files.newByteChannel(path, StandardOpenOption.WRITE); + } + return new ChannelDataOutput(path.getFileName().toString(), wbc, ByteBuffer.allocate(8192)); } } @@ -896,5 +1042,124 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe return env; } + private class Writer { + + private final ShpFiles tempFiles; + private final ShapeWriter shpWriter; + private final DBFWriter dbfWriter; + private final IndexWriter shxWriter; + private final ShapeHeader shpHeader; + private final DBFHeader dbfHeader; + private String defaultGeomName = null; + private int inc = 0; + + private Writer(Charset charset) throws DataStoreException{ + try { + tempFiles = files.createTempFiles(); + } catch (IOException ex) { + throw new DataStoreException("Failed to create temp files", ex); + } + + try { + //get original headers and information + try (ShapeReader reader = new ShapeReader(ShpFiles.openReadChannel(files.shpFile), null)) { + shpHeader = new ShapeHeader(reader.getHeader()); + } + try (DBFReader reader = new DBFReader(ShpFiles.openReadChannel(files.dbfFile), charset, null)) { + dbfHeader = new DBFHeader(reader.getHeader()); + } + + //unchanged files + ShpFiles.replace(files.cpgFile, tempFiles.getCpg(true)); + ShpFiles.replace(files.prjFile, tempFiles.getPrj(true)); + + //start new files + + //write shapefile + 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); + } catch (IOException ex) { + try { + tempFiles.deleteFiles(); + } catch (IOException e) { + ex.addSuppressed(e); + } + throw new DataStoreException("Failed to create temp files", ex); + } + + } + + 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) { + //search for the geometry name + for (PropertyType pt : feature.getType().getProperties(true)) { + if (pt instanceof AttributeType) { + final AttributeType at = (AttributeType) pt; + final String attName = at.getName().toString(); + if (Geometry.class.isAssignableFrom(at.getValueClass())) { + defaultGeomName = attName; + } + } + } + if (defaultGeomName == null) { + throw new IOException("Failed to find a geometry attribute in given features"); + } + } + + //write geometry + Object value = feature.getPropertyValue(defaultGeomName); + if (value instanceof Geometry) { + shpRecord.geometry = (Geometry) value; + shpRecord.recordNumber = inc; + } else { + throw new IOException("Feature geometry property is not a geometry"); + } + shpWriter.write(shpRecord); + final long recordEndPosition = shpWriter.getSteamPosition(); + + //write index + shxWriter.write(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); + } + dbfWriter.write(dbfRecord); + } + + /** + * Close file writers and replace original files if true. + */ + private void finish(boolean replaceOriginals) throws IOException { + try { + shpWriter.close(); + dbfWriter.close(); + shxWriter.close(); + tempFiles.scan(); + if (replaceOriginals) { + lock.writeLock().lock(); + try { + //swap files + tempFiles.replace(files); + } finally { + lock.writeLock().unlock(); + } + } + } finally { + tempFiles.deleteFiles(); + } + } + } + } 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 f3ab943720..c069f1bd39 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 @@ -22,6 +22,9 @@ import java.util.ArrayList; import java.util.List; 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; @@ -119,6 +122,7 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { /** * Decode geometry and store it in ShapeRecord. + * This method creates and fill the record bbox if it is null. * * @param channel to read from * @param record to read into @@ -142,6 +146,8 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { */ public abstract int getEncodedLength(Geometry geom); + public abstract GeneralEnvelope getBoundingBox(Geometry geom); + /** * Read 2D Bounding box from channel. * @@ -174,10 +180,11 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { * @param shape to read from */ protected void writeBBox2D(ChannelDataOutput channel, ShapeRecord shape) throws IOException { - channel.writeDouble(shape.bbox.getMinimum(0)); - channel.writeDouble(shape.bbox.getMinimum(1)); - channel.writeDouble(shape.bbox.getMaximum(0)); - channel.writeDouble(shape.bbox.getMaximum(1)); + final Envelope env2d = shape.geometry.getEnvelopeInternal(); + channel.writeDouble(env2d.getMinX()); + channel.writeDouble(env2d.getMinY()); + channel.writeDouble(env2d.getMaxX()); + channel.writeDouble(env2d.getMaxY()); } /** @@ -266,19 +273,28 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } //Z and M - if (nbOrdinates >= 3) writeLineOrdinates(channel, shape, lines, 2); - if (nbOrdinates == 4) writeLineOrdinates(channel, shape, lines, 3); + if (nbOrdinates >= 3) writeLineOrdinates(channel, shape, lines, 2, nbPts); + if (nbOrdinates == 4) writeLineOrdinates(channel, shape, lines, 3, nbPts); } - protected void writeLineOrdinates(ChannelDataOutput channel, ShapeRecord shape,List<LineString> lines, int ordinateIndex) throws IOException { - channel.writeDouble(shape.bbox.getMinimum(ordinateIndex)); - channel.writeDouble(shape.bbox.getMaximum(ordinateIndex)); + protected void writeLineOrdinates(ChannelDataOutput channel, ShapeRecord shape,List<LineString> lines, int ordinateIndex, int nbPts) throws IOException { + + final double[] values = new double[nbPts]; + double minK = Double.MAX_VALUE; + double maxK = -Double.MAX_VALUE; + int i = 0; for (LineString line : lines) { final CoordinateSequence cs = line.getCoordinateSequence(); for (int k = 0, kn =cs.size(); k < kn; k++) { - channel.writeDouble(cs.getOrdinate(k, ordinateIndex)); + values[i] = cs.getOrdinate(k, ordinateIndex); + minK = Double.min(minK, values[i]); + maxK = Double.max(maxK, values[i]); + i++; } } + channel.writeDouble(minK); + channel.writeDouble(maxK); + channel.writeDoubles(values); } protected List<LineString> extractRings(Geometry geom) { @@ -368,6 +384,51 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { } } + protected GeneralEnvelope getLinesBoundingBox(Geometry geom) { + final List<LineString> lines = extractRings(geom); + + int nbOrdinate = 0; + GeneralEnvelope env = null; + + for (int k = 0, kn = lines.size(); k < kn; k++) { + final LineString line = lines.get(k); + final CoordinateSequence cs = line.getCoordinateSequence(); + + if (nbOrdinate == 0) { + nbOrdinate = cs.getDimension(); + } + + for (int i = 0, n = cs.size(); i < n; i++) { + if (env == null) { + env = new GeneralEnvelope(nbOrdinate); + switch (nbOrdinate) { + case 4 : + double m = cs.getOrdinate(i, 3); + env.setRange(3, m, m); + case 3 : + double z = cs.getOrdinate(i, 2); + env.setRange(2, z, z); + case 2 : + double y = cs.getOrdinate(i, 1); + env.setRange(1, y, y); + double x = cs.getOrdinate(i, 0); + env.setRange(0, x, x); + } + } else { + switch (nbOrdinate) { + case 4 : + env.add(new GeneralDirectPosition(cs.getOrdinate(i,0), cs.getOrdinate(i,1), cs.getOrdinate(i,2), cs.getOrdinate(i,3))); break; + case 3 : + env.add(new GeneralDirectPosition(cs.getOrdinate(i,0), cs.getOrdinate(i,1), cs.getOrdinate(i,2))); break; + case 2 : + env.add(new GeneralDirectPosition(cs.getOrdinate(i,0), cs.getOrdinate(i,1))); break; + } + } + } + } + return env; + } + private static class Null extends ShapeGeometryEncoder<Geometry> { private static final Null INSTANCE = new Null(); @@ -390,6 +451,10 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { public void encode(ChannelDataOutput channel, ShapeRecord shape) throws IOException { } + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + return new GeneralEnvelope(0); + } } private static class PointXY extends ShapeGeometryEncoder<Point> { @@ -417,14 +482,22 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { public void encode(ChannelDataOutput channel, ShapeRecord shape) throws IOException { final Point pt = (Point) shape.geometry; final Coordinate coord = pt.getCoordinate(); - channel.writeDouble(coord.getX()); - channel.writeDouble(coord.getY()); + final double[] xy = new double[]{coord.getX(), coord.getY()}; + channel.writeDoubles(xy); } @Override public int getEncodedLength(Geometry geom) { return 2*8; //2 ordinates } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + final Point pt = (Point) geom; + final Coordinate coord = pt.getCoordinate(); + final double[] xy = new double[]{coord.getX(), coord.getY()}; + return new GeneralEnvelope(xy, xy); + } } private static class PointXYM extends ShapeGeometryEncoder<Point> { @@ -453,15 +526,25 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { public void encode(ChannelDataOutput channel, ShapeRecord shape) throws IOException { final Point pt = (Point) shape.geometry; final Coordinate coord = pt.getCoordinate(); - channel.writeDouble(coord.getX()); - channel.writeDouble(coord.getY()); - channel.writeDouble(coord.getM()); + final double[] xym = new double[]{coord.getX(), coord.getY(), coord.getM()}; + channel.writeDoubles(xym); + if (shape.bbox == null) { + shape.bbox = new GeneralEnvelope(xym,xym); + } } @Override public int getEncodedLength(Geometry geom) { return 3*8; //3 ordinates } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + final Point pt = (Point) geom; + final Coordinate coord = pt.getCoordinate(); + final double[] xym = new double[]{coord.getX(), coord.getY(), coord.getM()}; + return new GeneralEnvelope(xym, xym); + } } private static class PointXYZM extends ShapeGeometryEncoder<Point> { @@ -493,16 +576,22 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { public void encode(ChannelDataOutput channel, ShapeRecord shape) throws IOException { final Point pt = (Point) shape.geometry; final Coordinate coord = pt.getCoordinate(); - channel.writeDouble(coord.getX()); - channel.writeDouble(coord.getY()); - channel.writeDouble(coord.getZ()); - channel.writeDouble(coord.getM()); + final double[] xyzm = new double[]{coord.getX(), coord.getY(), coord.getZ(), coord.getM()}; + channel.writeDoubles(xyzm); } @Override public int getEncodedLength(Geometry geom) { return 4*8; //4 ordinates } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + final Point pt = (Point) geom; + final Coordinate coord = pt.getCoordinate(); + final double[] xyzm = new double[]{coord.getX(), coord.getY(), coord.getZ(), coord.getM()}; + return new GeneralEnvelope(xyzm, xyzm); + } } private static class MultiPointXY extends ShapeGeometryEncoder<MultiPoint> { @@ -529,8 +618,8 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { channel.writeInt(nbPts); for (int i = 0; i < nbPts; i++) { final Point pt = (Point) geometry.getGeometryN(i); - channel.writeDouble(pt.getX()); - channel.writeDouble(pt.getY()); + final double[] xy = new double[]{pt.getX(), pt.getY()}; + channel.writeDoubles(xy); } } @@ -540,6 +629,20 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { + 4 //nbPts + ((MultiPoint) geom).getNumGeometries() * 2 * 8; } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + final GeneralEnvelope env = new GeneralEnvelope(2); + final MultiPoint pts = (MultiPoint) geom; + for (int i = 0, n = pts.getNumGeometries(); i < n; i++) { + final Point pt = (Point)pts.getGeometryN(i); + final Coordinate coord = pt.getCoordinate(); + final double[] xy = new double[]{coord.getX(), coord.getY()}; + if (i == 0) env.setEnvelope(xy[0], xy[1], xy[0], xy[1]); + else env.add(new GeneralDirectPosition(xy)); + } + return env; + } } private static class MultiPointXYM extends ShapeGeometryEncoder<MultiPoint> { @@ -575,15 +678,21 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { channel.writeInt(nbPts); for (int i = 0; i < nbPts; i++) { final Point pt = (Point) geometry.getGeometryN(i); - channel.writeDouble(pt.getX()); - channel.writeDouble(pt.getY()); + final double[] xy = new double[]{pt.getX(), pt.getY()}; + channel.writeDoubles(xy); } - channel.writeDouble(shape.bbox.getMinimum(2)); - channel.writeDouble(shape.bbox.getMaximum(2)); + final double[] m = new double[nbPts]; + double minM = Double.MAX_VALUE; + double maxM = -Double.MAX_VALUE; for (int i = 0; i < nbPts; i++) { final Point pt = (Point) geometry.getGeometryN(i); - channel.writeDouble(pt.getCoordinate().getM()); + m[i] = pt.getCoordinate().getM(); + minM = Double.min(minM, m[i]); + maxM = Double.max(maxM, m[i]); } + channel.writeDouble(minM); + channel.writeDouble(maxM); + channel.writeDoubles(m); } @Override @@ -592,6 +701,20 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { + 4 //nbPts + ((MultiPoint) geom).getNumGeometries() * 3 * 8; } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + final GeneralEnvelope env = new GeneralEnvelope(3); + final MultiPoint pts = (MultiPoint) geom; + for (int i = 0, n = pts.getNumGeometries(); i < n; i++) { + final Point pt = (Point)pts.getGeometryN(i); + final Coordinate coord = pt.getCoordinate(); + final double[] xym = new double[]{coord.getX(), coord.getY(), coord.getM()}; + if (i == 0) env.setEnvelope(xym[0], xym[1], xym[2], xym[0], xym[1], xym[2]); + else env.add(new GeneralDirectPosition(xym)); + } + return env; + } } private static class MultiPointXYZM extends ShapeGeometryEncoder<MultiPoint> { @@ -634,18 +757,33 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { channel.writeDouble(pt.getX()); channel.writeDouble(pt.getY()); } - channel.writeDouble(shape.bbox.getMinimum(2)); - channel.writeDouble(shape.bbox.getMaximum(2)); + + final double[] z = new double[nbPts]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; for (int i = 0; i < nbPts; i++) { final Point pt = (Point) geometry.getGeometryN(i); - channel.writeDouble(pt.getCoordinate().getZ()); + z[i] = pt.getCoordinate().getZ(); + minZ = Double.min(minZ, z[i]); + maxZ = Double.max(maxZ, z[i]); } - channel.writeDouble(shape.bbox.getMinimum(3)); - channel.writeDouble(shape.bbox.getMaximum(3)); + channel.writeDouble(minZ); + channel.writeDouble(maxZ); + channel.writeDoubles(z); + + + final double[] m = new double[nbPts]; + double minM = Double.MAX_VALUE; + double maxM = -Double.MAX_VALUE; for (int i = 0; i < nbPts; i++) { final Point pt = (Point) geometry.getGeometryN(i); - channel.writeDouble(pt.getCoordinate().getM()); + m[i] = pt.getCoordinate().getM(); + minM = Double.min(minM, m[i]); + maxM = Double.max(maxM, m[i]); } + channel.writeDouble(minM); + channel.writeDouble(maxM); + channel.writeDoubles(m); } @Override @@ -654,6 +792,20 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { + 4 //nbPts + ((MultiPoint) geom).getNumGeometries() * 4 * 8; } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + final GeneralEnvelope env = new GeneralEnvelope(4); + final MultiPoint pts = (MultiPoint) geom; + for (int i = 0, n = pts.getNumGeometries(); i < n; i++) { + final Point pt = (Point)pts.getGeometryN(i); + final Coordinate coord = pt.getCoordinate(); + final double[] xyzm = new double[]{coord.getX(), coord.getY(), coord.getZ(), coord.getM()}; + if (i == 0) env.setEnvelope(xyzm[0], xyzm[1], xyzm[2], xyzm[3], xyzm[0], xyzm[1], xyzm[2], xyzm[3]); + else env.add(new GeneralDirectPosition(xyzm)); + } + return env; + } } private static class Polyline extends ShapeGeometryEncoder<MultiLineString> { @@ -689,6 +841,11 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { + nbGeom * 4 //offsets table + nbPoints * nbOrdinates * 8; //all ordinates } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + return getLinesBoundingBox(geom); + } } private static class Polygon extends ShapeGeometryEncoder<MultiPolygon> { @@ -728,6 +885,11 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { + nbGeom * 4 //offsets table + nbPoints * nbOrdinates * 8; //all ordinates } + + @Override + public GeneralEnvelope getBoundingBox(Geometry geom) { + return getLinesBoundingBox(geom); + } } private static class MultiPatch extends ShapeGeometryEncoder<MultiPolygon> { @@ -752,5 +914,10 @@ public abstract class ShapeGeometryEncoder<T extends Geometry> { public int getEncodedLength(Geometry geom) { throw new UnsupportedOperationException("Not supported yet."); } + + @Override + public GeneralEnvelope getBoundingBox(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/ShapeRecord.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java index b98ddfd0fc..77a0776864 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,11 +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; /** 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 2459802a3e..cabd09d663 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 @@ -43,6 +43,13 @@ public final class ShapeWriter implements AutoCloseable{ return header; } + /** + * @return current position in the stream + */ + public long getSteamPosition() { + return channel.getBitOffset(); + } + /** * Header will be copied and modified. * Use getHeader to obtain the new header. @@ -57,11 +64,12 @@ public final class ShapeWriter implements AutoCloseable{ public void write(ShapeRecord record) throws IOException { record.write(channel, io); + final GeneralEnvelope geomBox = io.getBoundingBox(record.geometry); if (bbox == null) { - bbox = new GeneralEnvelope(record.bbox.getDimension()); - bbox.setEnvelope(record.bbox); + bbox = new GeneralEnvelope(geomBox.getDimension()); + bbox.setEnvelope(geomBox); } else { - bbox.add(record.bbox); + bbox.add(geomBox); } } diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java index 6d0152954f..6fd3fcf34c 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java +++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java @@ -16,13 +16,20 @@ */ package org.apache.sis.storage.shapefile; +import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.util.Iterator; +import java.util.List; import java.util.stream.Stream; +import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.util.Utilities; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.geometry.GeneralEnvelope; @@ -30,6 +37,7 @@ import org.apache.sis.referencing.CommonCRS; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.FeatureQuery; import org.apache.sis.storage.FeatureSet; +import org.apache.sis.feature.internal.AttributeConvention; // Test dependencies import static org.junit.jupiter.api.Assertions.*; @@ -49,6 +57,8 @@ import org.opengis.filter.FilterFactory; */ public class ShapefileStoreTest { + private static final GeometryFactory GF = new GeometryFactory(); + @Test public void testStream() throws URISyntaxException, DataStoreException { final URL url = ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp"); @@ -171,21 +181,65 @@ public class ShapefileStoreTest { try (final ShapefileStore store = new ShapefileStore(Paths.get(url.toURI()))) { Path[] componentFiles = store.getComponentFiles(); assertEquals(5, componentFiles.length); - componentFiles[0].toString().endsWith("point.shp"); - componentFiles[1].toString().endsWith("point.shx"); - componentFiles[2].toString().endsWith("point.dbf"); - componentFiles[3].toString().endsWith("point.prj"); - componentFiles[4].toString().endsWith("point.cpg"); + assertTrue(componentFiles[0].toString().endsWith("point.shp")); + assertTrue(componentFiles[1].toString().endsWith("point.shx")); + assertTrue(componentFiles[2].toString().endsWith("point.dbf")); + assertTrue(componentFiles[3].toString().endsWith("point.prj")); + assertTrue(componentFiles[4].toString().endsWith("point.cpg")); } } /** * Test creating a new shapefile. */ - @Ignore @Test - public void testCreate() throws URISyntaxException, DataStoreException { - //todo + public void testCreate() throws URISyntaxException, DataStoreException, IOException { + final Path temp = Files.createTempFile("test", ".shp"); + Files.delete(temp); + final String name = temp.getFileName().toString().split("\\.")[0]; + try (final ShapefileStore store = new ShapefileStore(temp)) { + Path[] componentFiles = store.getComponentFiles(); + assertEquals(0, componentFiles.length); + + {//create type + final FeatureType type = createType(); + store.updateType(type); + } + + {//check files have been created + componentFiles = store.getComponentFiles(); + assertEquals(5, componentFiles.length); + assertTrue(componentFiles[0].toString().endsWith(name+".shp")); + assertTrue(componentFiles[1].toString().endsWith(name+".shx")); + assertTrue(componentFiles[2].toString().endsWith(name+".dbf")); + assertTrue( componentFiles[3].toString().endsWith(name+".prj")); + assertTrue(componentFiles[4].toString().endsWith(name+".cpg")); + } + + {// check created type + FeatureType type = store.getType(); + assertEquals(name, type.getName().toString()); + System.out.println(type.toString()); + assertEquals(9, type.getProperties(true).size()); + assertNotNull(type.getProperty("sis:identifier")); + assertNotNull(type.getProperty("sis:envelope")); + assertNotNull(type.getProperty("sis:geometry")); + final var geomProp = (AttributeType) type.getProperty("geometry"); + final var idProp = (AttributeType) type.getProperty("id"); + final var textProp = (AttributeType) type.getProperty("text"); + final var integerProp = (AttributeType) type.getProperty("integer"); + final var floatProp = (AttributeType) type.getProperty("float"); + final var dateProp = (AttributeType) type.getProperty("date"); + final AttributeType crsChar = (AttributeType) geomProp.characteristics().get(AttributeConvention.CRS); + assertTrue(Utilities.equalsIgnoreMetadata(CommonCRS.WGS84.geographic(),crsChar.getDefaultValue())); + assertEquals(Point.class, geomProp.getValueClass()); + assertEquals(Integer.class, idProp.getValueClass()); + assertEquals(String.class, textProp.getValueClass()); + assertEquals(Integer.class, integerProp.getValueClass()); + assertEquals(Double.class, floatProp.getValueClass()); + assertEquals(LocalDate.class, dateProp.getValueClass()); + } + } } /** @@ -193,8 +247,22 @@ public class ShapefileStoreTest { */ @Ignore @Test - public void testAddFeatures() throws URISyntaxException, DataStoreException { - //todo + public void testAddFeatures() throws URISyntaxException, DataStoreException, IOException { + final Path temp = Files.createTempFile("test", ".shp"); + Files.delete(temp); + try (final ShapefileStore store = new ShapefileStore(temp)) { + FeatureType type = createType(); + store.updateType(type); + type = store.getType(); + + Feature feature1 = createFeature1(type); + Feature feature2 = createFeature2(type); + store.add(List.of(feature1, feature2).iterator()); + + Object[] result = store.features(false).toArray(); + + + } } /** @@ -215,4 +283,38 @@ public class ShapefileStoreTest { //todo } + private static FeatureType createType() { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.setName("test"); + ftb.addAttribute(Integer.class).setName("id"); + ftb.addAttribute(String.class).setName("text"); + ftb.addAttribute(Integer.class).setName("integer"); + ftb.addAttribute(Float.class).setName("float"); + ftb.addAttribute(LocalDate.class).setName("date"); + ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()); + return ftb.build(); + } + + private static Feature createFeature1(FeatureType type) { + Feature feature = type.newInstance(); + feature.setPropertyValue("geometry", GF.createPoint(new Coordinate(10,20))); + feature.setPropertyValue("id", 1); + feature.setPropertyValue("text", "some text 1"); + feature.setPropertyValue("integer", 123); + feature.setPropertyValue("float", 123.456); + feature.setPropertyValue("date", LocalDate.of(2023, 5, 12)); + return feature; + } + + private static Feature createFeature2(FeatureType type) { + Feature feature = type.newInstance(); + feature.setPropertyValue("geometry", GF.createPoint(new Coordinate(30,40))); + feature.setPropertyValue("id", 2); + feature.setPropertyValue("text", "some text 2"); + feature.setPropertyValue("integer", 456); + feature.setPropertyValue("float", 456.789); + feature.setPropertyValue("date", LocalDate.of(2030, 6, 21)); + return feature; + } + } diff --git a/incubator/src/org.apache.sis.test.incubator/test/module-info.java b/incubator/src/org.apache.sis.test.incubator/test/module-info.java index 66086cb5c7..fed9415c58 100644 --- a/incubator/src/org.apache.sis.test.incubator/test/module-info.java +++ b/incubator/src/org.apache.sis.test.incubator/test/module-info.java @@ -24,4 +24,7 @@ module org.apache.sis.test.incubator { requires transitive junit; requires transitive org.junit.jupiter.api; requires transitive org.opengis.geoapi.conformance; + requires org.apache.sis.feature; + requires org.apache.sis.storage; + requires org.locationtech.jts; }