This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sis.git
commit 0beabad7541b35a0e29e3a5bfb485d321262f369 Merge: 3841c7aeae 15f1d671c9 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Apr 26 18:19:41 2022 +0200 Merge branch 'geoapi-3.1' .../java/org/apache/sis/console/AboutCommand.java | 2 +- application/sis-javafx/pom.xml | 5 + .../main/java/org/apache/sis/gui/DataViewer.java | 64 +- .../main/java/org/apache/sis/gui/RecentFiles.java | 2 +- .../apache/sis/gui/coverage/BandRangeTable.java | 5 +- .../apache/sis/gui/coverage/CoverageCanvas.java | 35 +- .../apache/sis/gui/coverage/CoverageExplorer.java | 2 +- .../org/apache/sis/gui/coverage/GridViewSkin.java | 127 +++- .../org/apache/sis/gui/coverage/ImageRequest.java | 2 +- .../apache/sis/gui/coverage/IsolineRenderer.java | 4 +- .../sis/gui/coverage/StyledRenderingData.java | 2 +- .../org/apache/sis/gui/dataset/DataWindow.java | 8 +- .../org/apache/sis/gui/dataset/ExpandableList.java | 2 +- .../org/apache/sis/gui/dataset/FeatureList.java | 2 +- .../org/apache/sis/gui/dataset/ResourceTree.java | 4 +- .../org/apache/sis/gui/dataset/SelectedData.java | 11 +- .../org/apache/sis/gui/dataset/WindowManager.java | 13 +- .../java/org/apache/sis/gui/map/MapCanvas.java | 7 +- .../java/org/apache/sis/gui/map/MapCanvasAWT.java | 2 +- .../org/apache/sis/gui/map/OperationFinder.java | 2 +- .../java/org/apache/sis/gui/map/StatusBar.java | 3 +- .../apache/sis/gui/referencing/AuthorityCodes.java | 2 +- .../org/apache/sis/internal/gui/MouseDrags.java | 55 ++ .../org/apache/sis/internal/gui/RecentChoices.java | 30 +- .../org/apache/sis/internal/gui/Resources.java | 12 +- .../apache/sis/internal/gui/Resources.properties | 2 + .../sis/internal/gui/Resources_fr.properties | 2 + .../apache/sis/internal/gui/control/ColorCell.java | 2 +- .../apache/sis/internal/gui/io/FileAccessView.java | 20 +- .../org/apache/sis/internal/doclet/Rewriter.java | 2 +- .../sis/util/resources/ResourceCompilerMojo.java | 4 +- .../java/org/apache/sis/coverage/CategoryList.java | 2 +- .../org/apache/sis/coverage/SampleDimension.java | 50 +- .../main/java/org/apache/sis/coverage/ToNaN.java | 2 +- .../org/apache/sis/coverage/grid/GridCoverage.java | 4 +- .../apache/sis/coverage/grid/GridCoverage2D.java | 7 +- .../sis/coverage/grid/GridCoverageBuilder.java | 8 +- .../apache/sis/coverage/grid/GridDerivation.java | 6 +- .../org/apache/sis/coverage/grid/GridExtent.java | 41 +- .../org/apache/sis/coverage/grid/GridGeometry.java | 10 +- .../apache/sis/coverage/grid/ImageRenderer.java | 3 +- .../apache/sis/feature/CharacteristicTypeMap.java | 2 +- .../apache/sis/feature/DefaultAssociationRole.java | 4 +- .../org/apache/sis/feature/DefaultFeatureType.java | 4 +- .../java/org/apache/sis/feature/FeatureFormat.java | 2 +- .../main/java/org/apache/sis/feature/Features.java | 4 +- .../org/apache/sis/feature/NamedFeatureType.java | 2 +- .../apache/sis/feature/StringJoinOperation.java | 2 +- .../java/org/apache/sis/image/BandSelectImage.java | 10 +- .../java/org/apache/sis/image/ComputedImage.java | 4 +- .../java/org/apache/sis/image/ComputedTiles.java | 2 +- .../main/java/org/apache/sis/image/DataType.java | 55 +- .../java/org/apache/sis/image/ImageCombiner.java | 51 +- .../java/org/apache/sis/image/ImageProcessor.java | 23 +- .../java/org/apache/sis/image/PixelIterator.java | 18 +- .../java/org/apache/sis/image/RecoloredImage.java | 97 ++- .../org/apache/sis/image/StatisticsCalculator.java | 2 +- .../main/java/org/apache/sis/image/Transferer.java | 2 +- .../java/org/apache/sis/image/Visualization.java | 20 +- .../org/apache/sis/index/tree/NodeIterator.java | 4 +- .../sis/internal/coverage/CoverageCombiner.java | 307 ++++++++ .../internal/coverage/j2d/ColorModelFactory.java | 110 ++- .../sis/internal/coverage/j2d/Colorizer.java | 86 ++- .../sis/internal/coverage/j2d/ColorsForRange.java | 61 +- .../sis/internal/coverage/j2d/ImageUtilities.java | 17 +- .../sis/internal/coverage/j2d/TileOpExecutor.java | 4 +- .../apache/sis/internal/feature/GeometryType.java | 2 +- .../sis/internal/feature/GeometryWrapper.java | 2 +- .../org/apache/sis/internal/feature/Resources.java | 7 +- .../sis/internal/feature/Resources.properties | 1 + .../sis/internal/feature/Resources_fr.properties | 1 + .../internal/feature/SpatialOperationContext.java | 2 +- .../sis/internal/feature/j2d/PathBuilder.java | 2 +- .../apache/sis/internal/feature/j2d/Polyline.java | 2 +- .../internal/feature/jts/PathIteratorAdapter.java | 4 +- .../sis/internal/filter/FunctionRegister.java | 2 +- .../internal/processing/image/IsolineTracer.java | 4 +- .../sis/internal/processing/image/Isolines.java | 2 +- .../apache/sis/coverage/SampleDimensionTest.java | 4 +- .../apache/sis/coverage/grid/GridExtentTest.java | 10 + .../apache/sis/feature/FeatureOperationsTest.java | 2 +- .../feature/builder/AttributeTypeBuilderTest.java | 2 +- .../builder/CharacteristicTypeBuilderTest.java | 2 +- .../java/org/apache/sis/image/DataTypeTest.java | 25 +- .../sis/internal/coverage/j2d/ColorizerTest.java | 4 +- .../internal/coverage/j2d/ImageUtilitiesTest.java | 8 +- .../java/org/apache/sis/internal/jaxb/Context.java | 4 +- .../apache/sis/internal/jaxb/TypeRegistration.java | 2 +- .../sis/internal/jaxb/gco/GO_CharacterString.java | 2 +- .../apache/sis/internal/jaxb/gco/GO_DateTime.java | 4 +- .../internal/jaxb/gco/ObjectIdentification.html | 2 +- .../apache/sis/internal/jaxb/gco/PropertyType.java | 4 +- .../metadata/replace/ReferenceSystemMetadata.java | 2 +- .../apache/sis/internal/metadata/Identifiers.java | 20 +- .../sis/internal/metadata/MetadataUtilities.java | 6 +- .../apache/sis/internal/metadata/Resources.java | 2 +- .../apache/sis/internal/metadata/package-info.java | 2 +- .../sis/internal/metadata/sql/SQLUtilities.java | 2 +- .../sis/internal/metadata/sql/ScriptRunner.java | 2 +- .../org/apache/sis/metadata/MetadataStandard.java | 4 +- .../org/apache/sis/metadata/MetadataVisitor.java | 2 +- .../main/java/org/apache/sis/metadata/Pruner.java | 2 +- .../org/apache/sis/metadata/TreeNodeChildren.java | 2 +- .../org/apache/sis/metadata/iso/ISOMetadata.java | 2 +- .../sis/metadata/iso/citation/DefaultContact.java | 4 + .../iso/extent/DefaultGeographicBoundingBox.java | 12 +- .../org/apache/sis/metadata/sql/Dispatcher.java | 19 +- .../sis/metadata/sql/IdentifierGenerator.java | 2 +- .../apache/sis/metadata/sql/MetadataSource.java | 116 ++- .../apache/sis/metadata/sql/TableHierarchy.java | 4 + .../main/java/org/apache/sis/xml/Transformer.java | 4 +- .../org/apache/sis/xml/TransformingReader.java | 2 +- .../org/apache/sis/xml/TransformingWriter.java | 4 +- .../org/apache/sis/metadata/sql/Contents.sql | 18 +- .../sis/metadata/sql/MetadataFallbackVerifier.java | 2 +- .../java/org/apache/sis/util/iso/NamesTest.java | 2 +- .../sis/internal/map/coverage/RenderingData.java | 2 +- .../main/java/org/apache/sis/portrayal/Canvas.java | 8 +- .../MultiResolutionCoverageLoaderTest.java | 4 +- .../apache/sis/internal/gazetteer/Resources.java | 2 +- .../gazetteer/MilitaryGridReferenceSystem.java | 2 +- .../sis/geometry/AbstractDirectPosition.java | 2 +- .../org/apache/sis/geometry/AbstractEnvelope.java | 2 +- .../org/apache/sis/geometry/ArrayEnvelope.java | 2 +- .../org/apache/sis/geometry/CoordinateFormat.java | 12 +- .../org/apache/sis/geometry/DirectPosition2D.java | 2 +- .../java/org/apache/sis/geometry/Envelopes.java | 2 +- .../apache/sis/geometry/GeneralDirectPosition.java | 2 +- .../org/apache/sis/geometry/GeneralEnvelope.java | 5 +- .../referencing/CC_GeneralOperationParameter.java | 18 +- .../jaxb/referencing/CC_OperationMethod.java | 8 +- .../internal/referencing/CoordinateOperations.java | 4 +- .../sis/internal/referencing/ExtentSelector.java | 4 +- .../referencing/PositionalAccuracyConstant.java | 2 +- .../apache/sis/internal/referencing/Resources.java | 2 +- .../sis/internal/referencing/WKTKeywords.java | 2 +- .../sis/internal/referencing/j2d/AffineMatrix.java | 3 +- .../referencing/j2d/AffineTransform2D.java | 36 +- .../referencing/j2d/ImmutableAffineTransform.java | 14 +- .../referencing/j2d/ParameterizedAffine.java | 2 +- .../internal/referencing/j2d/TileOrganizer.java | 2 +- .../referencing/provider/AbstractProvider.java | 2 +- .../provider/MolodenskyInterpolation.java | 2 +- .../java/org/apache/sis/io/wkt/AbstractParser.java | 2 +- .../java/org/apache/sis/io/wkt/Convention.java | 2 +- .../main/java/org/apache/sis/io/wkt/Element.java | 6 +- .../main/java/org/apache/sis/io/wkt/Formatter.java | 12 +- .../apache/sis/io/wkt/GeodeticObjectParser.java | 24 +- .../org/apache/sis/io/wkt/MathTransformParser.java | 2 +- .../java/org/apache/sis/io/wkt/StoredTree.java | 4 +- .../java/org/apache/sis/io/wkt/WKTDictionary.java | 4 +- .../sis/parameter/DefaultParameterDescriptor.java | 2 +- .../sis/parameter/DefaultParameterValue.java | 2 +- .../java/org/apache/sis/parameter/Parameters.java | 6 +- .../sis/referencing/AbstractIdentifiedObject.java | 4 +- .../java/org/apache/sis/referencing/Builder.java | 8 +- .../sis/referencing/GeodesicsOnEllipsoid.java | 2 +- .../apache/sis/referencing/GeodeticCalculator.java | 6 +- .../sis/referencing/ImmutableIdentifier.java | 2 +- .../apache/sis/referencing/NamedIdentifier.java | 2 +- .../sis/referencing/crs/AbstractDerivedCRS.java | 15 +- .../sis/referencing/crs/DefaultDerivedCRS.java | 6 +- .../sis/referencing/crs/DefaultProjectedCRS.java | 2 +- .../org/apache/sis/referencing/cs/AbstractCS.java | 2 +- .../sis/referencing/cs/CoordinateSystems.java | 6 +- .../cs/DefaultCoordinateSystemAxis.java | 2 +- .../factory/ConcurrentAuthorityFactory.java | 2 +- .../factory/GeodeticAuthorityFactory.java | 2 +- .../referencing/factory/IdentifiedObjectSet.java | 2 +- .../operation/AbstractSingleOperation.java | 6 +- .../operation/CoordinateOperationRegistry.java | 6 +- .../referencing/operation/DefaultConversion.java | 2 +- .../operation/builder/LinearTransformBuilder.java | 4 +- .../operation/projection/AlbersEqualArea.java | 2 +- .../operation/projection/CassiniSoldner.java | 2 +- .../operation/projection/CylindricalEqualArea.java | 2 +- .../projection/LambertConicConformal.java | 2 +- .../referencing/operation/projection/Mercator.java | 4 +- .../projection/ModifiedAzimuthalEquidistant.java | 2 +- .../operation/projection/NormalizedProjection.java | 6 +- .../operation/projection/ObliqueMercator.java | 2 +- .../operation/projection/ObliqueStereographic.java | 2 +- .../operation/projection/Orthographic.java | 2 +- .../operation/projection/PolarStereographic.java | 2 +- .../operation/projection/Polyconic.java | 2 +- .../operation/projection/Sinusoidal.java | 2 +- .../operation/projection/TransverseMercator.java | 2 +- .../operation/transform/AbstractMathTransform.java | 12 +- .../transform/AbstractMathTransform2D.java | 4 +- .../operation/transform/ConcatenatedTransform.java | 4 +- .../operation/transform/ContextualParameters.java | 2 +- .../transform/CoordinateSystemTransform.java | 2 +- .../transform/InterpolatedGeocentricTransform.java | 2 +- .../operation/transform/MathTransforms.java | 2 +- .../sis/io/wkt/GeodeticObjectParserTest.java | 10 +- .../factory/ConcurrentAuthorityFactoryTest.java | 2 +- .../transform/AbstractMathTransformTest.java | 2 +- .../operation/transform/MathTransformTestCase.java | 4 +- .../sis/internal/system/DelayedRunnable.java | 2 +- .../org/apache/sis/internal/system/Fallback.java | 2 +- .../sis/internal/system/OptionalDependency.java | 2 +- .../apache/sis/internal/util/AbstractIterator.java | 2 +- .../apache/sis/internal/util/CollectionsExt.java | 2 +- .../apache/sis/internal/util/DefinitionURI.java | 2 +- .../org/apache/sis/internal/util/DoubleDouble.java | 2 +- .../sis/internal/util/ListOfUnknownSize.java | 4 +- .../org/apache/sis/internal/util/Numerics.java | 49 ++ .../main/java/org/apache/sis/io/LineAppender.java | 2 +- .../java/org/apache/sis/math/FunctionProperty.java | 2 +- .../java/org/apache/sis/math/MathFunctions.java | 2 +- .../main/java/org/apache/sis/math/Statistics.java | 68 +- .../src/main/java/org/apache/sis/math/Vector.java | 2 +- .../main/java/org/apache/sis/measure/Angle.java | 2 +- .../java/org/apache/sis/measure/AngleFormat.java | 2 +- .../sis/measure/FormattedCharacterIterator.java | 2 +- .../main/java/org/apache/sis/measure/Prefixes.java | 2 +- .../main/java/org/apache/sis/measure/Scalar.java | 17 +- .../java/org/apache/sis/measure/SystemUnit.java | 2 +- .../java/org/apache/sis/measure/UnitFormat.java | 4 +- .../java/org/apache/sis/measure/UnitRegistry.java | 14 +- .../java/org/apache/sis/measure/UnitServices.java | 1 + .../main/java/org/apache/sis/measure/Units.java | 80 +- .../java/org/apache/sis/measure/package-info.java | 1 + .../main/java/org/apache/sis/setup/OptionKey.java | 4 +- .../main/java/org/apache/sis/util/ArraysExt.java | 20 +- .../java/org/apache/sis/util/CharSequences.java | 6 +- .../src/main/java/org/apache/sis/util/Locales.java | 4 +- .../main/java/org/apache/sis/util/Utilities.java | 4 +- .../java/org/apache/sis/util/collection/Cache.java | 6 +- .../org/apache/sis/util/collection/RangeSet.java | 2 +- .../sis/util/collection/TreeTableFormat.java | 2 +- .../sis/util/collection/WeakValueHashMap.java | 2 +- .../org/apache/sis/util/logging/LoggerAdapter.java | 2 +- .../apache/sis/util/logging/PerformanceLevel.java | 4 +- .../java/org/apache/sis/util/resources/Errors.java | 17 +- .../apache/sis/util/resources/Errors.properties | 5 +- .../apache/sis/util/resources/Errors_fr.properties | 5 +- .../sis/util/resources/IndexedResourceBundle.java | 6 +- .../org/apache/sis/util/resources/Messages.java | 7 +- .../apache/sis/util/resources/Messages.properties | 1 + .../sis/util/resources/Messages_fr.properties | 1 + .../org/apache/sis/util/resources/Vocabulary.java | 2 +- .../org/apache/sis/measure/UnitNames.properties | 2 + .../org/apache/sis/measure/UnitNames_fr.properties | 3 +- .../org/apache/sis/internal/util/NumericsTest.java | 22 + .../org/apache/sis/math/DecimalFunctionsTest.java | 2 +- .../java/org/apache/sis/math/StatisticsTest.java | 18 +- .../org/apache/sis/measure/UnitFormatTest.java | 5 +- .../java/org/apache/sis/measure/UnitsTest.java | 87 ++- ide-project/NetBeans/build.xml | 7 + ide-project/NetBeans/nbproject/project.properties | 4 +- .../org/apache/sis/storage/landsat/BandGroup.java | 13 +- .../apache/sis/storage/landsat/LandsatStore.java | 3 + .../sis/storage/landsat/LandsatStoreProvider.java | 3 +- .../apache/sis/storage/landsat/MetadataReader.java | 6 +- .../storage/landsat/doc-files/MetadataMapping.html | 2 +- .../apache/sis/storage/landsat/package-info.java | 2 +- .../org/apache/sis/internal/geotiff/Resources.java | 2 +- .../sis/internal/geotiff/SchemaModifier.java | 5 +- .../org/apache/sis/storage/geotiff/CRSBuilder.java | 8 +- .../org/apache/sis/storage/geotiff/DataCube.java | 24 + .../org/apache/sis/storage/geotiff/DataSubset.java | 10 +- .../apache/sis/storage/geotiff/GeoTiffStore.java | 26 +- .../sis/storage/geotiff/GeoTiffStoreProvider.java | 11 +- .../sis/storage/geotiff/GridGeometryBuilder.java | 7 +- .../sis/storage/geotiff/ImageFileDirectory.java | 52 +- .../sis/storage/geotiff/ImageMetadataBuilder.java | 5 +- .../sis/storage/geotiff/MultiResolutionImage.java | 28 +- .../org/apache/sis/storage/geotiff/Reader.java | 4 +- .../internal/storage/inflater/CCITTRLETest.java | 2 +- .../apache/sis/storage/geotiff/GeoKeysTest.java | 4 +- .../sis/storage/geotiff/XMLMetadataTest.java | 4 +- .../java/org/apache/sis/internal/netcdf/Axis.java | 2 +- .../org/apache/sis/internal/netcdf/CRSBuilder.java | 2 +- .../org/apache/sis/internal/netcdf/DataType.java | 2 +- .../sis/internal/netcdf/DiscreteSampling.java | 4 +- .../org/apache/sis/internal/netcdf/FeatureSet.java | 4 +- .../apache/sis/internal/netcdf/NamedElement.java | 4 +- .../apache/sis/internal/netcdf/RasterResource.java | 29 +- .../org/apache/sis/internal/netcdf/Resources.java | 2 +- .../org/apache/sis/internal/netcdf/Variable.java | 2 +- .../sis/internal/netcdf/ucar/LogAdapter.java | 13 +- .../apache/sis/storage/netcdf/MetadataReader.java | 14 +- .../org/apache/sis/storage/netcdf/NetcdfStore.java | 3 + .../sis/storage/netcdf/NetcdfStoreProvider.java | 11 +- .../org/apache/sis/internal/netcdf/TestCase.java | 28 +- .../internal/netcdf/impl/ChannelDecoderTest.java | 3 +- .../sis/storage/netcdf/MetadataReaderTest.java | 2 + .../storage/netcdf/NetcdfStoreProviderTest.java | 5 +- .../sis/internal/shapefile/jdbc/AbstractJDBC.java | 2 +- storage/sis-sqlstore/pom.xml | 2 +- .../apache/sis/internal/sql/feature/Analyzer.java | 2 +- .../apache/sis/internal/sql/feature/Column.java | 73 +- .../apache/sis/internal/sql/feature/Database.java | 53 +- .../sis/internal/sql/feature/FeatureAdapter.java | 2 +- .../sis/internal/sql/feature/FeatureAnalyzer.java | 2 +- .../sis/internal/sql/feature/FeatureIterator.java | 2 +- .../sis/internal/sql/feature/InfoStatements.java | 17 +- .../apache/sis/internal/sql/feature/Relation.java | 2 +- .../apache/sis/internal/sql/feature/Resources.java | 2 +- .../sis/internal/sql/feature/SelectionClause.java | 2 +- .../org/apache/sis/internal/sql/feature/Table.java | 8 +- .../sis/internal/sql/feature/ValueGetter.java | 81 +- .../sis/internal/sql/postgis/ExtentEstimator.java | 8 +- .../sis/internal/sql/postgis/ObjectGetter.java | 76 ++ .../apache/sis/internal/sql/postgis/Postgres.java | 35 +- .../java/org/apache/sis/storage/sql/SQLStore.java | 5 +- .../apache/sis/storage/sql/SQLStoreProvider.java | 2 +- .../sis/internal/storage/AbstractGridResource.java | 578 -------------- .../sis/internal/storage/AggregatedFeatureSet.java | 10 +- .../org/apache/sis/internal/storage/CodeType.java | 2 +- .../internal/storage/ConcatenatedFeatureSet.java | 7 +- .../internal/storage/DocumentedStoreProvider.java | 45 +- .../sis/internal/storage/MemoryFeatureSet.java | 5 +- .../sis/internal/storage/MemoryGridResource.java | 5 +- .../sis/internal/storage/MetadataBuilder.java | 148 +++- .../apache/sis/internal/storage/PRJDataStore.java | 511 +++++++++++++ .../apache/sis/internal/storage/RangeArgument.java | 386 ++++++++++ .../sis/internal/storage/ResourceOnFileSystem.java | 6 +- .../org/apache/sis/internal/storage/Resources.java | 47 +- .../sis/internal/storage/Resources.properties | 9 + .../sis/internal/storage/Resources_fr.properties | 9 + .../sis/internal/storage/StoreUtilities.java | 22 +- .../sis/internal/storage/TiledGridCoverage.java | 10 +- .../sis/internal/storage/TiledGridResource.java | 9 +- .../apache/sis/internal/storage/URIDataStore.java | 161 ++-- .../internal/storage/WritableResourceSupport.java | 236 ++++++ .../sis/internal/storage/csv/FeatureIterator.java | 6 +- .../storage/csv/MovingFeatureIterator.java | 2 +- .../org/apache/sis/internal/storage/csv/Store.java | 5 +- .../sis/internal/storage/csv/StoreProvider.java | 12 +- .../sis/internal/storage/esri/AsciiGridStore.java | 570 ++++++++++++++ .../storage/esri/AsciiGridStoreProvider.java | 139 ++++ .../sis/internal/storage/esri/CharactersView.java | 239 ++++++ .../sis/internal/storage/esri/RasterStore.java | 520 +++++++++++++ .../RawRasterLayout.java} | 30 +- .../sis/internal/storage/esri/RawRasterReader.java | 264 +++++++ .../sis/internal/storage/esri/RawRasterStore.java | 544 ++++++++++++++ .../storage/esri/RawRasterStoreProvider.java | 115 +++ .../sis/internal/storage/esri/WritableStore.java | 311 ++++++++ .../sis/internal/storage/esri/package-info.java | 57 ++ .../apache/sis/internal/storage/folder/Store.java | 5 +- .../sis/internal/storage/folder/StoreProvider.java | 4 +- .../sis/internal/storage/image/FormatFilter.java | 246 ++++++ .../internal/storage/image/WarningListener.java | 70 ++ .../internal/storage/image/WorldFileResource.java | 363 +++++++++ .../sis/internal/storage/image/WorldFileStore.java | 829 +++++++++++++++++++++ .../storage/image/WorldFileStoreProvider.java | 134 ++++ .../internal/storage/image/WritableResource.java | 77 ++ .../sis/internal/storage/image/WritableStore.java | 535 +++++++++++++ .../sis/internal/storage/image/package-info.java | 50 ++ .../sis/internal/storage/io/ChannelDataInput.java | 34 +- .../sis/internal/storage/io/ChannelDataOutput.java | 4 +- .../sis/internal/storage/io/ChannelFactory.java | 126 +++- .../storage/io/ChannelImageInputStream.java | 17 +- .../storage/io/ChannelImageOutputStream.java | 2 +- .../internal/storage/io/HyperRectangleReader.java | 22 +- .../sis/internal/storage/io/IOUtilities.java | 40 +- .../org/apache/sis/internal/storage/io/Region.java | 22 +- .../internal/storage/io/RewindableLineReader.java | 2 +- .../org/apache/sis/internal/storage/wkt/Store.java | 33 +- .../sis/internal/storage/wkt/StoreFormat.java | 14 +- .../sis/internal/storage/wkt/StoreProvider.java | 22 +- .../sis/internal/storage/wkt/package-info.java | 2 +- .../sis/internal/storage/xml/AbstractProvider.java | 13 +- .../org/apache/sis/internal/storage/xml/Store.java | 3 +- .../sis/internal/storage/xml/StoreProvider.java | 4 +- .../{internal => }/storage/AbstractFeatureSet.java | 49 +- .../sis/storage/AbstractGridCoverageResource.java | 229 ++++++ .../{internal => }/storage/AbstractResource.java | 223 +++--- .../org/apache/sis/storage/CoverageSubset.java | 9 +- .../java/org/apache/sis/storage/DataOptionKey.java | 19 +- .../java/org/apache/sis/storage/DataStore.java | 10 +- .../org/apache/sis/storage/DataStoreException.java | 4 +- .../org/apache/sis/storage/DataStoreProvider.java | 10 +- .../java/org/apache/sis/storage/FeatureQuery.java | 2 +- .../java/org/apache/sis/storage/FeatureSubset.java | 5 +- ...ion.java => IncompatibleResourceException.java} | 36 +- .../sis/storage/ReadOnlyStorageException.java | 2 +- .../main/java/org/apache/sis/storage/Resource.java | 6 +- ...on.java => ResourceAlreadyExistsException.java} | 37 +- .../org/apache/sis/storage/StorageConnector.java | 187 ++++- .../sis/storage/WritableGridCoverageResource.java | 60 +- .../apache/sis/storage/event/StoreListeners.java | 217 ++++-- .../java/org/apache/sis/storage/tiling/Tile.java | 93 +++ .../org/apache/sis/storage/tiling/TileMatrix.java | 166 +++++ .../apache/sis/storage/tiling/TileMatrixSet.java | 91 +++ .../org/apache/sis/storage/tiling/TileStatus.java | 69 ++ .../apache/sis/storage/tiling/TiledResource.java | 52 ++ .../sis/storage/tiling/WritableTileMatrix.java | 61 ++ .../sis/storage/tiling/WritableTileMatrixSet.java | 84 +++ .../sis/storage/tiling/WritableTiledResource.java | 76 ++ .../apache/sis/storage/tiling/package-info.java | 75 ++ .../org.apache.sis.storage.DataStoreProvider | 5 +- .../internal/storage/MemoryGridResourceTest.java | 2 +- .../sis/internal/storage/MetadataBuilderTest.java | 4 +- ...ridResourceTest.java => RangeArgumentTest.java} | 29 +- .../internal/storage/esri/AsciiGridStoreTest.java | 131 ++++ .../internal/storage/esri/BILConsistencyTest.java | 79 ++ .../internal/storage/esri/BIPConsistencyTest.java | 79 ++ .../internal/storage/esri/BSQConsistencyTest.java | 79 ++ .../internal/storage/esri/WritableStoreTest.java | 176 +++++ .../storage/image/SelfConsistencyTest.java | 83 +++ .../internal/storage/image/WorldFileStoreTest.java | 165 ++++ .../sis/internal/storage/io/IOUtilitiesTest.java | 14 +- .../internal/storage/wkt/StoreProviderTest.java | 2 +- .../java/org/apache/sis/storage/DataStoreMock.java | 2 +- .../org/apache/sis/storage/GridResourceMock.java | 3 +- .../apache/sis/storage/StorageConnectorTest.java | 2 +- .../sis/storage/event/StoreListenersTest.java | 24 +- .../sis/test/storage/CoverageReadConsistency.java | 6 +- .../apache/sis/test/storage/SubsampledImage.java | 109 ++- .../apache/sis/test/suite/StorageTestSuite.java | 9 +- .../org/apache/sis/internal/storage/esri/BIL.hdr | 9 + .../org/apache/sis/internal/storage/esri/BIL.raw | Bin 0 -> 243 bytes .../org/apache/sis/internal/storage/esri/BIP.hdr | 14 + .../org/apache/sis/internal/storage/esri/BIP.raw | Bin 0 -> 243 bytes .../org/apache/sis/internal/storage/esri/BIP.stx | 7 + .../org/apache/sis/internal/storage/esri/BSQ.hdr | 13 + .../org/apache/sis/internal/storage/esri/BSQ.raw | Bin 0 -> 243 bytes .../org/apache/sis/internal/storage/esri/grid.asc | 34 + .../org/apache/sis/internal/storage/esri/grid.clr | 14 + .../org/apache/sis/internal/storage/esri/grid.prj | 9 + .../apache/sis/internal/storage/image/README.md | 11 + .../apache/sis/internal/storage/image/gradient.pgw | 6 + .../apache/sis/internal/storage/image/gradient.png | Bin 0 -> 176 bytes .../apache/sis/internal/storage/image/gradient.prj | 8 + .../apache/sis/internal/storage/gpx/Reader.java | 4 +- .../org/apache/sis/internal/storage/gpx/Store.java | 1 + .../sis/internal/storage/gpx/StoreProvider.java | 9 +- .../org/apache/sis/internal/storage/gpx/Types.java | 2 +- .../internal/storage/xml/stream/StaxDataStore.java | 2 +- .../storage/xml/stream/StaxStreamReader.java | 4 +- 433 files changed, 12149 insertions(+), 1992 deletions(-) diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java index 4ba06beffb,ef079ac668..4fdc3e3ff2 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java @@@ -306,10 -320,10 +306,10 @@@ public class DefaultAssociationRole ext } /** - * Potentially invoked after {@link #search(FeatureType, Collection, GenericName, List)} for searching + * Potentially invoked after {@code search(FeatureType, Collection, GenericName, List)} for searching * in associations of associations. * - * <p>Current implementation does not check that there is no duplicated names. Even if we did so, + * <p>Current implementation does not check that there are no duplicated names. Even if we did so, * a graph of feature types may have no duplicated names at this time but some duplicated names * later. We rather put a warning in {@link #DefaultAssociationRole(Map, GenericName, int, int)} * javadoc.</p> diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java index 882be37f69,bd814c2dc6..fa0484864b --- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java @@@ -678,13 -689,13 +678,13 @@@ public class DefaultFeatureType extend } /* * Ensures that all properties defined in this feature type is also defined - * in the given property, and that the former is assignable from the later. + * in the given property, and that the former is assignable from the latter. */ - for (final Map.Entry<String, PropertyType> entry : byName.entrySet()) { - final PropertyType other; + for (final Map.Entry<String, AbstractIdentifiedType> entry : byName.entrySet()) { + final AbstractIdentifiedType other; try { other = type.getProperty(entry.getKey()); - } catch (PropertyNotFoundException e) { + } catch (IllegalArgumentException e) { /* * A property in this FeatureType does not exist in the given FeatureType. * Catching exceptions is not an efficient way to perform this check, but diff --cc core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultContact.java index ea56c4e5ec,eafc326697..f36f8f3d50 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultContact.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultContact.java @@@ -262,8 -244,14 +262,12 @@@ public class DefaultContact extends ISO } } if (ignored != null) { + /* + * Log a warning for ignored property using a call to `ignored.toString()` instead of `ignored` + * because we want the property to appear as "TelephoneType[FOO]" instead of "FOO". + */ Context.warningOccured(Context.current(), DefaultContact.class, "getPhone", - Messages.class, Messages.Keys.IgnoredPropertyAssociatedTo_1, ignored.toString()); + Messages.class, Messages.Keys.IgnoredPropertyAssociatedTo_1, ignored); } } } diff --cc core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java index 3a3b1e4218,adb318d941..c2d7184cad --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java @@@ -161,13 -161,9 +161,14 @@@ final class Dispatcher implements Invoc */ Object value; try { - method = supercede(method); - if (method == null) return null; + final long nb = nullValues; value = fetchValue(source.getLookupInfo(method.getDeclaringClass()), method); + if (value == null) { + nullValues = nb; + method = supercede(method); ++ if (method == null) return null; + value = fetchValue(source.getLookupInfo(method.getDeclaringClass()), method); + } } catch (ReflectiveOperationException | SQLException | MetadataStoreException e) { throw new BackingStoreException(error(method), e); } @@@ -334,8 -330,16 +335,16 @@@ * confuses this {@code Dispatcher} class. We need the method in the base interface. */ private static Method supercede(Method method) throws NoSuchMethodException { - if (method.getDeclaringClass() == ResponsibleParty.class && "getRole".equals(method.getName())) { - method = DefaultResponsibility.class.getMethod("getRole"); + if (method.getDeclaringClass() == ResponsibleParty.class) { + if ("getRole".equals(method.getName())) { - method = Responsibility.class.getMethod("getRole"); ++ method = DefaultResponsibility.class.getMethod("getRole"); + } else { + /* + * `getIndividualName()`, `getOrganisationName()`, `getPositionName()` and + * `getContactInfo()` has no direct equivalence in `Responsibility` class. + */ + return null; + } } return method; } diff --cc core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java index 8607b5f6a3,4cf0c3509c..996f32f332 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java @@@ -844,8 -853,29 +855,29 @@@ public class MetadataSource implements public <T> T lookup(final Class<T> type, final String identifier) throws MetadataStoreException { ArgumentChecks.ensureNonNull("type", type); ArgumentChecks.ensureNonEmpty("identifier", identifier); + return type.cast(lookup(type, identifier, true)); + } + + /** + * Implementation of public {@link #lookup(Class, String)} method. + * + * <h4>Deferred database access</h4> + * This method may or may not query the database immediately, at implementation choice. + * It the database is not queried immediately, invalid identifiers may not be detected + * during this method invocation. Instead, an invalid identifier may be detected only + * when a getter method is invoked on the returned metadata object. In such case, + * an {@link org.apache.sis.util.collection.BackingStoreException} will be thrown + * at getter method invocation time. + * + * @param type the interface to implement or the {@link ControlledVocabulary} type. + * @param identifier the identifier of the record for the metadata entity to be created. + * @param verify whether to check for record existence. + * @return an implementation of the required interface, or the code list element. + * @throws MetadataStoreException if a SQL query failed or if the metadata has not been found. + */ + private Object lookup(final Class<?> type, final String identifier, boolean verify) throws MetadataStoreException { Object value; - if (ControlledVocabulary.class.isAssignableFrom(type)) { + if (CodeList.class.isAssignableFrom(type)) { value = getCodeList(type, identifier); } else { final CacheKey key = new CacheKey(type, identifier); @@@ -924,10 -973,10 +975,10 @@@ { /* * If the identifier is prefixed with a table name as in "{Organisation}identifier", - * the name between bracket is a subtype of the given 'type' argument. + * the name between bracket is a subtype of the given `type` argument. */ final Class<?> type = TableHierarchy.subType(info.getMetadataType(), toSearch.identifier); - final Class<?> returnType = method.getReturnType(); + final Class<?> returnType = Interim.getReturnType(method); final boolean wantCollection = Collection.class.isAssignableFrom(returnType); final Class<?> elementType = wantCollection ? Classes.boundOfParameterizedProperty(method) : returnType; final boolean isMetadata = standard.isMetadata(elementType); diff --cc core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java index 5807f11d94,e6be6798b2..fb6a77b17f --- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java @@@ -2032,9 -2028,9 +2032,9 @@@ class GeodeticObjectParser extends Math final Unit<?> unit = parseUnit(element); /* * A ParametricCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS of kind ParametricCRS. - * In the later case, the datum is null and we have instead DerivingConversion element from a BaseParametricCRS. + * In the latter case, the datum is null and we have instead DerivingConversion element from a BaseParametricCRS. */ - ParametricDatum datum = null; + Datum datum = null; SingleCRS baseCRS = null; Conversion fromBase = null; if (!isBaseCRS) { diff --cc storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java index 2db1bb0ce7,5cbab55a27..5d6dd435e7 --- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java +++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java @@@ -22,10 -22,12 +22,11 @@@ import java.util.EnumMap import java.util.Iterator; import java.io.IOException; import java.lang.reflect.UndeclaredThrowableException; + import org.apache.sis.storage.AbstractResource; import org.apache.sis.storage.DataStoreException; - import org.apache.sis.internal.storage.AbstractResource; import org.apache.sis.internal.netcdf.ucar.DecoderWrapper; import org.apache.sis.setup.GeometryLibrary; + import org.apache.sis.storage.event.StoreListeners; -import org.opengis.test.dataset.TestData; import ucar.nc2.dataset.NetcdfDataset; import ucar.nc2.NetcdfFile; import org.junit.AfterClass; diff --cc storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java index 2488cac3a5,d426ecd587..19c45b421f --- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java +++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java @@@ -20,10 -20,8 +20,9 @@@ import java.io.IOException import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import org.apache.sis.internal.netcdf.TestData; import org.apache.sis.internal.netcdf.Decoder; import org.apache.sis.internal.netcdf.DecoderTest; - import org.apache.sis.internal.storage.AbstractResource; import org.apache.sis.internal.storage.io.ChannelDataInput; import org.apache.sis.storage.DataStoreException; import org.apache.sis.setup.GeometryLibrary; diff --cc storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java index 2511f268d2,05b889d543..b8079a82ce --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java @@@ -190,10 -191,10 +190,10 @@@ final class Table extends AbstractFeatu * * @todo This constructor is not yet used because it is an unfinished work. * We need to invent some mechanism for using a subset of the columns. - * A starting point is {@link org.apache.sis.storage.FeatureQuery#expectedType(FeatureType)}. + * A starting point is {@link org.apache.sis.storage.FeatureQuery#expectedType(DefaultFeatureType)}. */ Table(final Table parent) { - super(parent); + super(parent.listeners); database = parent.database; query = parent.query; name = parent.name; diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java index 4c66e43eb0,f71a6dc628..20901a02fc --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java @@@ -28,9 -29,10 +29,10 @@@ import org.apache.sis.geometry.Envelope import org.apache.sis.storage.FeatureSet; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.event.StoreListeners; + import org.apache.sis.storage.AbstractFeatureSet; // Branch-dependent imports -import org.opengis.feature.FeatureType; +import org.apache.sis.feature.DefaultFeatureType; /** @@@ -141,10 -143,11 +143,11 @@@ abstract class AggregatedFeatureSet ext * @throws DataStoreException if an error occurred while reading metadata from the data stores. */ @Override - protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException { - super.createMetadata(metadata); + protected Metadata createMetadata() throws DataStoreException { + final MetadataBuilder metadata = new MetadataBuilder(); + metadata.addDefaultMetadata(this, listeners); for (final FeatureSet fs : dependencies()) { - final FeatureType type = fs.getType(); + final DefaultFeatureType type = fs.getType(); metadata.addSource(fs.getMetadata(), ScopeCode.FEATURE_TYPE, (type == null) ? null : new CharSequence[] {type.getName().toInternationalString()}); } diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java index b2b47db179,1f6a737326..b2bc097536 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java @@@ -26,6 -26,8 +26,7 @@@ import java.awt.image.SampleModel import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.RenderedImage; import java.awt.image.Raster; + import org.opengis.util.GenericName; -import org.opengis.coverage.CannotEvaluateException; import org.opengis.geometry.MismatchedDimensionException; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.grid.GridExtent; diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java index 74985689a6,200f531923..bccca7a201 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java @@@ -33,7 -34,7 +33,8 @@@ import org.apache.sis.coverage.grid.Gri import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridRoundingMode; +import org.apache.sis.coverage.CannotEvaluateException; + import org.apache.sis.storage.AbstractGridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.RasterLoadingStrategy; import org.apache.sis.storage.event.StoreListeners; diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java index 0000000000,326a0ea3ee..27b59aaace mode 000000,100644..100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java @@@ -1,0 -1,236 +1,236 @@@ + /* + * 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.internal.storage; + + import java.util.Locale; + import java.io.IOException; + import java.nio.channels.WritableByteChannel; + import java.awt.geom.AffineTransform; + import org.opengis.util.FactoryException; + import org.opengis.referencing.operation.MathTransform; + import org.opengis.referencing.operation.TransformException; + import org.apache.sis.coverage.grid.GridCoverage; + import org.apache.sis.coverage.grid.GridExtent; + import org.apache.sis.storage.GridCoverageResource; + import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.DataStoreReferencingException; + import org.apache.sis.storage.ReadOnlyStorageException; + import org.apache.sis.storage.ResourceAlreadyExistsException; + import org.apache.sis.storage.IncompatibleResourceException; + import org.apache.sis.storage.WritableGridCoverageResource; + import org.apache.sis.internal.storage.io.ChannelDataInput; + import org.apache.sis.internal.storage.io.ChannelDataOutput; + import org.apache.sis.internal.coverage.CoverageCombiner; + import org.apache.sis.referencing.operation.matrix.AffineTransforms2D; + import org.apache.sis.referencing.operation.transform.TransformSeparator; + import org.apache.sis.util.resources.Errors; + import org.apache.sis.util.ArgumentChecks; + import org.apache.sis.util.Classes; + import org.apache.sis.util.Localized; + + // Branch-dependent imports -import org.opengis.coverage.CannotEvaluateException; ++import org.apache.sis.coverage.CannotEvaluateException; + + + /** + * Helper classes for the management of {@link WritableGridCoverageResource.CommonOption}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ + public final class WritableResourceSupport implements Localized { + /** + * The resource where to write. + */ + private final GridCoverageResource resource; + + /** + * {@code true} if the {@link WritableGridCoverageResource.CommonOption.REPLACE} option has been specified. + * At most one of {@code replace} and {@link #update} can be {@code true}. + */ + private boolean replace; + + /** + * {@code true} if the {@link WritableGridCoverageResource.CommonOption.UPDATE} option has been specified. + * At most one of {@link #replace} and {@code update} can be {@code true}. + */ + private boolean update; + + /** + * Creates a new helper class for the given options. + * + * @param resource the resource where to write. + * @param options configuration of the write operation. + */ + public WritableResourceSupport(final GridCoverageResource resource, final WritableGridCoverageResource.Option[] options) { + this.resource = resource; + ArgumentChecks.ensureNonNull("options", options); + for (final WritableGridCoverageResource.Option option : options) { + replace |= WritableGridCoverageResource.CommonOption.REPLACE.equals(option); + update |= WritableGridCoverageResource.CommonOption.UPDATE .equals(option); + } + if (replace & update) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.MutuallyExclusiveOptions_2, + WritableGridCoverageResource.CommonOption.REPLACE, + WritableGridCoverageResource.CommonOption.UPDATE)); + } + } + + /** + * Returns the locale used by the resource for error messages, or {@code null} if unknown. + * + * @return the locale used by the resource for error messages, or {@code null} if unknown. + */ + @Override + public final Locale getLocale() { + return (resource instanceof Localized) ? ((Localized) resource).getLocale() : null; + } + + /** + * Returns the writable channel positioned at the beginning of the stream. + * The returned channel should <em>not</em> be closed + * because it is the same channel than the one used by {@code input}. + * Caller should invoke {@link ChannelDataOutput#flush()} after usage. + * + * @param input the input from which to get the writable channel. + * @return the writable channel. + * @throws IOException if the stream position can not be reset. + * @throws DataStoreException if the channel is read-only. + */ + public final ChannelDataOutput channel(final ChannelDataInput input) throws IOException, DataStoreException { + if (input.channel instanceof WritableByteChannel && input.rewind()) { + return new ChannelDataOutput(input.filename, (WritableByteChannel) input.channel, input.buffer); + } else { + throw new ReadOnlyStorageException(canNotWrite()); + } + } + + /** + * Returns {@code true} if the caller should add or replace the resource + * or {@code false} if it needs to update an existing resource. + * Current heuristic: + * + * <ul> + * <li>If the given channel is empty, then this method always returns {@code true}.</li> + * <li>Otherwise this method returns {@code true} if the {@code REPLACE} option was specified, + * or returns {@code false} if the {@code UPDATE} option was specified, + * or thrown a {@link ResourceAlreadyExistsException} otherwise.</li> + * </ul> + * + * @param input the channel to test for emptiness, or {@code null} if unknown. + * @return whether the caller should replace ({@code true}) or update ({@code false}) the resource. + * @throws IOException if an error occurred while checking the channel length. + * @throws ResourceAlreadyExistsException if the resource exists and the writer + * should neither updating or replacing it. + * @throws DataStoreException if another kind of error occurred with the resource. + */ + public final boolean replace(final ChannelDataInput input) throws IOException, DataStoreException { + if (update) { + return isEmpty(input); + } else if (replace || isEmpty(input)) { + return true; + } else { + Object identifier = resource.getIdentifier().orElse(null); + if (identifier == null && input != null) identifier = input.filename; + throw new ResourceAlreadyExistsException(Resources.forLocale(getLocale()) + .getString(Resources.Keys.ResourceAlreadyExists_1, identifier)); + } + } + + /** + * Returns {@code true} if the given channel is empty. + * In case of doubt, this method conservatively returns {@code false}. + * + * @param input the channel to test for emptiness, or {@code null} if unknown. + * @return {@code true} if the channel is empty, or {@code false} if not or if unknown. + */ + private static boolean isEmpty(final ChannelDataInput input) throws IOException { + return (input != null) && input.length() == 0; + } + + /** + * Reads the current coverage in the resource and updates its content with cell values from the given coverage. + * This method can be used as a simple implementation of {@link WritableGridCoverageResource.CommonOption#UPDATE}. + * This method returns the updated coverage; it is caller responsibility to write it. + * + * <p>This method can be used when updating the coverage requires to read it fully, then write if fully. + * Advanced writers should try to update only the modified parts (typically some tiles) instead.</p> + * + * @param coverage the coverage to use for updating the currently existing coverage. + * @return the updated coverage that the caller should write. + * @throws DataStoreException if an error occurred while reading or updating the coverage. + */ + public final GridCoverage update(final GridCoverage coverage) throws DataStoreException { + final GridCoverage existing = resource.read(null, null); + final CoverageCombiner combiner = new CoverageCombiner(existing, 0, 1); + try { + if (!combiner.apply(coverage)) { + throw new ReadOnlyStorageException(canNotWrite()); + } + } catch (TransformException e) { + throw new DataStoreReferencingException(canNotWrite(), e); + } + return existing; + } + + /** + * Returns the "grid to CRS" transform as a two-dimensional affine transform. + * This is a convenience method for writers that support only this kind of transform. + * + * @param extent the extent of the grid coverage to write. + * @param gridToCRS the "grid to CRS" transform of the coverage to write. + * @return the given "grid to CRS" as a two-dimensional affine transform. + * @throws DataStoreException if the affine transform can not be extracted from the given "grid to CRS" transform. + */ + public final AffineTransform getAffineTransform2D(final GridExtent extent, final MathTransform gridToCRS) + throws DataStoreException + { + final TransformSeparator s = new TransformSeparator(gridToCRS); + try { + s.addSourceDimensions(extent.getSubspaceDimensions(2)); + return AffineTransforms2D.castOrCopy(s.separate()); + } catch (FactoryException | CannotEvaluateException e) { + throw new DataStoreReferencingException(canNotWrite(), e); + } catch (IllegalArgumentException e) { + throw new IncompatibleResourceException(canNotWrite(), e); + } + } + + /** + * Returns the message for an exception saying that we can not write the resource. + * + * @return a localized "Can not write resource" message. + * @throws DataStoreException if an error occurred while preparing the error message. + */ + public final String canNotWrite() throws DataStoreException { + Object identifier = resource.getIdentifier().orElse(null); + if (identifier == null) identifier = Classes.getShortClassName(resource); + return Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotWriteResource_1, identifier); + } + + /** + * Returns the message for an exception saying that rotations are not supported. + * + * @param format name of the format that does not support rotations. + * @return a localized "rotation not supported" message. + */ + public final String rotationNotSupported(final String format) { + return Resources.forLocale(getLocale()).getString(Resources.Keys.RotationNotSupported_1, format); + } + } diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java index 0000000000,0e588da647..c4948e86a5 mode 000000,100644..100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java @@@ -1,0 -1,520 +1,520 @@@ + /* + * 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.internal.storage.esri; + + import java.util.List; + import java.util.Arrays; + import java.util.Optional; + import java.util.Hashtable; + import java.util.logging.Level; + import java.io.IOException; + import java.io.FileNotFoundException; + import java.nio.file.NoSuchFileException; + import java.nio.file.Path; + import java.awt.image.ColorModel; + import java.awt.image.DataBuffer; + import java.awt.image.SampleModel; + import java.awt.image.BufferedImage; + import java.awt.image.WritableRaster; + import org.opengis.geometry.Envelope; + import org.opengis.metadata.Metadata; + import org.opengis.metadata.maintenance.ScopeCode; + import org.opengis.referencing.operation.TransformException; + import org.apache.sis.metadata.sql.MetadataStoreException; + import org.apache.sis.coverage.SampleDimension; + import org.apache.sis.coverage.grid.GridGeometry; + import org.apache.sis.coverage.grid.GridCoverage2D; + import org.apache.sis.image.PlanarImage; + import org.apache.sis.storage.GridCoverageResource; + import org.apache.sis.storage.DataStoreProvider; + import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.DataStoreReferencingException; + import org.apache.sis.storage.StorageConnector; + import org.apache.sis.internal.storage.PRJDataStore; + import org.apache.sis.internal.storage.MetadataBuilder; + import org.apache.sis.internal.coverage.j2d.ColorModelFactory; + import org.apache.sis.internal.coverage.j2d.ImageUtilities; + import org.apache.sis.internal.storage.RangeArgument; + import org.apache.sis.internal.storage.Resources; + import org.apache.sis.internal.util.UnmodifiableArrayList; + import org.apache.sis.internal.util.Numerics; + import org.apache.sis.util.resources.Vocabulary; + import org.apache.sis.util.CharSequences; + import org.apache.sis.util.ArraysExt; + import org.apache.sis.math.Statistics; + + + /** + * Base class for the implementation of ASCII Grid or raw binary store. + * This base class manages the reading of following auxiliary files: + * + * <ul> + * <li>{@code *.stx} for statistics about bands.</li> + * <li>{@code *.clr} for the image color map.</li> + * <li>{@code *.prj} for the CRS definition.</li> + * </ul> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ + abstract class RasterStore extends PRJDataStore implements GridCoverageResource { + /** + * Band to make visible if an image contains many bands + * but a color map is defined for only one band. + */ + private static final int VISIBLE_BAND = 0; + + /** + * Keyword for the number of rows in the image. + */ + static final String NROWS = "NROWS"; + + /** + * Keyword for the number of columns in the image. + */ + static final String NCOLS = "NCOLS"; + + /** + * The filename extension of {@code "*.stx"} and {@code "*.clr"} files. + * + * @see #getComponentFiles() + */ + private static final String STX = "stx", CLR = "clr"; + + /** + * The color model, created from the {@code "*.clr"} file content when first needed. + * The color model and sample dimensions are created together because they depend on + * the same properties. + */ + private ColorModel colorModel; + + /** + * The sample dimensions, created from the {@code "*.stx"} file content when first needed. + * The sample dimensions and color model are created together because they depend on the same properties. + * This list is unmodifiable. + * + * @see #getSampleDimensions() + */ + private List<SampleDimension> sampleDimensions; + + /** + * The value to replace by NaN values, or {@link Double#NaN} if none. + */ + double nodataValue; + + /** + * The metadata object, or {@code null} if not yet created. + */ + Metadata metadata; + + /** + * Creates a new raster store from the given file, URL or stream. + * + * @param provider the factory that created this {@code DataStore} instance, or {@code null} if unspecified. + * @param connector information about the storage (file, URL, stream, <i>etc</i>). + * @throws DataStoreException if an error occurred while creating the data store for the given storage. + */ + RasterStore(final DataStoreProvider provider, final StorageConnector connector) throws DataStoreException { + super(provider, connector); + nodataValue = Double.NaN; + listeners.useWarningEventsOnly(); + } + + /** + * Returns the {@linkplain #location} as a {@code Path} component together with auxiliary files. + * + * @return the main file and auxiliary files as paths, or an empty array if unknown. + * @throws DataStoreException if the URI can not be converted to a {@link Path}. + */ + @Override + public Path[] getComponentFiles() throws DataStoreException { + return listComponentFiles(PRJ, STX, CLR); + } + + /** + * Returns the spatiotemporal extent of the raster file. + * + * @return the spatiotemporal resource extent. + * @throws DataStoreException if an error occurred while computing the envelope. + * @hidden + */ + @Override + public Optional<Envelope> getEnvelope() throws DataStoreException { + return Optional.ofNullable(getGridGeometry().getEnvelope()); + } + + /** + * Builds metadata and assigns the result to the {@link #metadata} field. + * + * @param formatName name of the raster format. + * @param formatKey key of format description in the {@code SpatialMetadata} database. + * @throws DataStoreException if an error occurred during the parsing process. + */ + final void createMetadata(final String formatName, final String formatKey) throws DataStoreException { + final GridGeometry gridGeometry = getGridGeometry(); // May cause parsing of header. + final MetadataBuilder builder = new MetadataBuilder(); + try { + builder.setPredefinedFormat(formatKey); + } catch (MetadataStoreException e) { + builder.addFormatName(formatName); + listeners.warning(e); + } - builder.addResourceScope(ScopeCode.COVERAGE, null); ++ builder.addResourceScope(ScopeCode.valueOf("COVERAGE"), null); + builder.addEncoding(encoding, MetadataBuilder.Scope.METADATA); + builder.addSpatialRepresentation(null, gridGeometry, true); + try { + builder.addExtent(gridGeometry.getEnvelope()); + } catch (TransformException e) { + throw new DataStoreReferencingException(getLocale(), formatName, getDisplayName(), null).initCause(e); + } + /* + * Do not invoke `getSampleDimensions()` because computing sample dimensions without statistics + * may cause the loading of the full image. Even if `GridCoverage.getSampleDimensions()` exists + * and could be used opportunistically, we do not use it in order to keep a deterministic behavior + * (we do not want the metadata to vary depending on the order in which methods are invoked). + */ + if (sampleDimensions != null) { + for (final SampleDimension band : sampleDimensions) { + builder.addNewBand(band); + } + } + addTitleOrIdentifier(builder); + builder.setISOStandards(false); + metadata = builder.buildAndFreeze(); + } + + /** + * Reads the {@code "*.clr"} auxiliary file. Syntax is as below, with one line per color: + * + * <pre>value red green blue</pre> + * + * The specification said that lines that do not start with a number shall be ignored as comment. + * Any characters after the fourth number shall also be ignored and can be used as comment. + * + * <h4>Limitations</h4> + * Current implementation requires the data type to be {@link DataBuffer#TYPE_BYTE} or + * {@link DataBuffer#TYPE_SHORT}. A future version could create scaled color model for + * floating point values as well. + * + * @param mapSize minimal size of index color model map. The actual size may be larger. + * @param numBands number of bands in the sample model. Only one of them will be visible. + * @return the color model, or {@code null} if the file does not contain enough entries. + * @throws NoSuchFileException if the auxiliary file has not been found (when opened from path). + * @throws FileNotFoundException if the auxiliary file has not been found (when opened from URL). + * @throws IOException if another error occurred while opening the stream. + * @throws NumberFormatException if a number can not be parsed. + */ + private ColorModel readColorMap(final int dataType, final int mapSize, final int numBands) + throws DataStoreException, IOException + { + final int maxSize; + switch (dataType) { + case DataBuffer.TYPE_BYTE: maxSize = 0xFF; break; + case DataBuffer.TYPE_USHORT: maxSize = 0xFFFF; break; + default: return null; // Limitation documented in above javadoc. + } + int count = 0; + long[] indexAndColors = ArraysExt.EMPTY_LONG; // Index in highest 32 bits, ARGB in lowest 32 bits. + for (final CharSequence line : CharSequences.splitOnEOL(readAuxiliaryFile(CLR))) { + final int end = CharSequences.skipTrailingWhitespaces(line, 0, line.length()); + final int start = CharSequences.skipLeadingWhitespaces(line, 0, end); + if (start < end && Character.isDigit(Character.codePointAt(line, start))) { + int column = 0; + long code = 0; + for (final CharSequence item : CharSequences.split(line.subSequence(start, end), ' ')) { + if (item.length() != 0) { + int value = Integer.parseInt(item.toString()); + if (column == 0) { + code = ((long) value) << Integer.SIZE; + } else { + value = Math.max(0, Math.min(255, value)); + code |= value << ((3 - column) * Byte.SIZE); + } + if (++column >= 4) break; + } + } + if (count >= indexAndColors.length) { + indexAndColors = Arrays.copyOf(indexAndColors, Math.max(count*2, 64)); + } + indexAndColors[count++] = code | 0xFF000000L; + } + } + if (count <= 1) { + return null; + } + /* + * Sort the color entries in increasing index order. Because we put the value in the highest bits, + * we can sort the `long` entries directly. If the file contains more entries than what the color + * map can contains, the last entries are discarded. + */ + Arrays.sort(indexAndColors, 0, count); + int[] ARGB = new int[Math.max(mapSize, Math.toIntExact((indexAndColors[count-1] >>> Integer.SIZE) + 1))]; + final int[] colors = new int[2]; + for (int i=1; i<count; i++) { + final int lower = (int) (indexAndColors[i-1] >>> Integer.SIZE); + final int upper = (int) (indexAndColors[i ] >>> Integer.SIZE); + if (upper >= lower) { + colors[0] = (int) indexAndColors[i-1]; + colors[1] = (int) indexAndColors[i ]; + ColorModelFactory.expand(colors, ARGB, lower, upper + 1); + } + if (upper > maxSize) { + ARGB = Arrays.copyOf(ARGB, maxSize + 1); + break; + } + } + return ColorModelFactory.createIndexColorModel(numBands, VISIBLE_BAND, ARGB, true, -1); + } + + /** + * Reads the {@code "*.stx"} auxiliary file. Syntax is as below, with one line per band. + * Value between {…} are optional and can be skipped with a # sign in place of the number. + * + * <pre>band minimum maximum {mean} {std_deviation} {linear_stretch_min} {linear_stretch_max}</pre> + * + * The specification said that lines that do not start with a number shall be ignored as comment. + * + * @todo Stretch values are not yet stored. + * + * @return statistics for each band. Some elements may be null if not specified in the file. + * @throws NoSuchFileException if the auxiliary file has not been found (when opened from path). + * @throws FileNotFoundException if the auxiliary file has not been found (when opened from URL). + * @throws IOException if another error occurred while opening the stream. + * @throws NumberFormatException if a number can not be parsed. + */ + private Statistics[] readStatistics(final String name, final SampleModel sm) + throws DataStoreException, IOException + { + final Statistics[] stats = new Statistics[sm.getNumBands()]; + for (final CharSequence line : CharSequences.splitOnEOL(readAuxiliaryFile(STX))) { + final int end = CharSequences.skipTrailingWhitespaces(line, 0, line.length()); + final int start = CharSequences.skipLeadingWhitespaces(line, 0, end); + if (start < end && Character.isDigit(Character.codePointAt(line, start))) { + int column = 0; + int band = 0; + double minimum = Double.NaN; + double maximum = Double.NaN; + double mean = Double.NaN; + double stdev = Double.NaN; + for (final CharSequence item : CharSequences.split(line.subSequence(start, end), ' ')) { + if (item.length() != 0) { + if (column == 0) { + band = Integer.parseInt(item.toString()); + } else if (item.charAt(0) != '#') { + final double value = Double.parseDouble(item.toString()); + switch (column) { + case 1: minimum = value; break; + case 2: maximum = value; break; + case 3: mean = value; break; + case 4: stdev = value; break; + } + } + column++; + } + } + if (band >= 1 && band <= stats.length) { + final int count = Math.multiplyExact(sm.getWidth(), sm.getHeight()); + stats[band - 1] = new Statistics(name, 0, count, minimum, maximum, mean, stdev, false); + } + } + } + return stats; + } + + /** + * Loads {@code "*.stx"} and {@code "*.clr"} files if present then builds {@link #sampleDimensions} and + * {@link #colorModel} from those information. If no color map is found, a grayscale color model is created. + * + * @param name name to use for the sample dimension, or {@code null} if untitled. + * @param sm the sample model to use for creating a default color model if no {@code "*.clr"} file is found. + * @param stats if the caller collected statistics by itself, those statistics for each band. Otherwise empty. + * @throws DataStoreException if an error occurred while loading an auxiliary file. + */ + final void loadBandDescriptions(String name, final SampleModel sm, Statistics... stats) throws DataStoreException { + final SampleDimension[] bands = new SampleDimension[sm.getNumBands()]; + /* + * If the "*.stx" file is found, the statistics read from that file will replace the specified one. + * Otherwise the `stats` parameter will be left unchanged. We read statistics even if a color map + * overwrite them because we need the minimum/maximum values for building the sample dimensions. + */ + try { + stats = readStatistics(name, sm); + } catch (IOException | NumberFormatException e) { + canNotReadAuxiliaryFile(STX, e); + } + /* + * Build the sample dimensions and the color model. + * Some minimum/maximum values will be used as fallback if no statistics were found. + */ + final int dataType = sm.getDataType(); + final boolean isInteger = ImageUtilities.isIntegerType(dataType); + final boolean isUnsigned = isInteger && ImageUtilities.isUnsignedType(sm); + final boolean isRGB = isInteger && (bands.length == 3 || bands.length == 4); + final SampleDimension.Builder builder = new SampleDimension.Builder(); + for (int band=0; band < bands.length; band++) { + double minimum = Double.NaN; + double maximum = Double.NaN; + if (band < stats.length) { + final Statistics s = stats[band]; + if (s != null) { // `readStatistics()` may have left some values to null. + minimum = s.minimum(); + maximum = s.maximum(); + } + } + /* + * If statistics were not specified and the sample type is integer, + * the minimum and maximum values may change for each band because + * the sample size (in bits) can vary. + */ + if (!(minimum <= maximum)) { // Use `!` for catching NaN. + minimum = 0; + maximum = 1; + if (isInteger) { + long max = Numerics.bitmask(sm.getSampleSize(band)) - 1; + if (!isUnsigned) { + max >>>= 1; + minimum = ~max; // Tild operator, not minus. + } + maximum = max; + } + } + /* + * Create the sample dimension for this band. The same "no data" value is used for all bands. + * The sample dimension is considered "converted" on the assumption that caller will replace + * all "no data" value by NaN before to return the raster to the user. + */ + if (isRGB) { + builder.setName(Vocabulary.formatInternational(RGB_BAND_NAMES[band])); + } else { + if (name != null) { + builder.setName(name); + name = null; // Use the name only for the first band. + } + builder.addQuantitative(null, minimum, maximum, null); + if (nodataValue < minimum || nodataValue > maximum) { + builder.mapQualitative(null, nodataValue, Float.NaN); + } + } + bands[band] = builder.build().forConvertedValues(!isInteger); + builder.clear(); + /* + * Create the color model using the statistics of the band that we choose to make visible, + * or using a RGB color model if the number of bands or the data type is compatible. + * The color file is optional and will be used if present. + */ + if (band == VISIBLE_BAND) { + if (isRGB) { + colorModel = ColorModelFactory.createRGB(sm); + } else { + try { + colorModel = readColorMap(dataType, (int) (maximum + 1), bands.length); + } catch (IOException | NumberFormatException e) { + canNotReadAuxiliaryFile(CLR, e); + } + if (colorModel == null) { + colorModel = ColorModelFactory.createGrayScale(dataType, bands.length, band, minimum, maximum); + } + } + } + } + sampleDimensions = UnmodifiableArrayList.wrap(bands); + } + + /** + * Sends a warning about a failure to read an optional auxiliary file. + * This is used for errors that affect only the rendering, not the georeferencing. + * + * @param suffix suffix of the auxiliary file. + * @param exception error that occurred while reading the auxiliary file. + */ + private void canNotReadAuxiliaryFile(final String suffix, final Exception exception) { + Level level = Level.WARNING; + if (exception instanceof NoSuchFileException || exception instanceof FileNotFoundException) { + level = Level.FINE; + } + listeners.warning(level, Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, suffix), exception); + } + + /** + * Default names of bands when the color model is RGB or RGBA. + */ + private static final short[] RGB_BAND_NAMES = { + Vocabulary.Keys.Red, + Vocabulary.Keys.Green, + Vocabulary.Keys.Blue, + Vocabulary.Keys.Transparency + }; + + /** + * Creates the grid coverage resulting from a {@link #read(GridGeometry, int...)} operation. + * + * @param domain the effective domain after intersection and subsampling. + * @param range indices of selected bands. + * @param data the loaded data. + * @param stats statistics to save as a property, or {@code null} if none. + * @return the grid coverage. + */ + @SuppressWarnings("UseOfObsoleteCollectionType") + final GridCoverage2D createCoverage(final GridGeometry domain, final RangeArgument range, + final WritableRaster data, final Statistics stats) + { + Hashtable<String,Object> properties = null; + if (stats != null) { + final Statistics[] as = new Statistics[range.getNumBands()]; + Arrays.fill(as, stats); + properties = new Hashtable<>(); + properties.put(PlanarImage.STATISTICS_KEY, as); + } + List<SampleDimension> bands = sampleDimensions; + ColorModel cm = colorModel; + if (!range.isIdentity()) { + bands = Arrays.asList(range.select(sampleDimensions)); + cm = range.select(cm).orElse(null); + if (cm == null) { + final SampleDimension band = bands.get(VISIBLE_BAND); + cm = ColorModelFactory.createGrayScale(data.getSampleModel(), VISIBLE_BAND, band.getSampleRange().orElse(null)); + } + } + return new GridCoverage2D(domain, bands, new BufferedImage(cm, data, false, properties)); + } + + /** + * Returns the sample dimensions computed by {@code loadBandDescriptions(…)}. + * Shall be overridden by subclasses in a synchronized method. The subclass + * must ensure that {@code loadBandDescriptions(…)} has been invoked once. + * + * @return the sample dimensions, or {@code null} if not yet computed. + */ + @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") + public List<SampleDimension> getSampleDimensions() throws DataStoreException { + return sampleDimensions; + } + + /** + * Closes this data store and releases any underlying resources. + * Shall be overridden by subclasses in a synchronized method. + * + * @throws DataStoreException if an error occurred while closing this data store. + */ + @Override + public void close() throws DataStoreException { + metadata = null; + } + } diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java index 0000000000,5e57c11003..14aca45fbb mode 000000,100644..100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java @@@ -1,0 -1,309 +1,311 @@@ + /* + * 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.internal.storage.esri; + + import java.util.Map; + import java.util.LinkedHashMap; + import java.io.IOException; + import java.awt.image.DataBuffer; + import java.awt.image.RenderedImage; + import java.awt.geom.AffineTransform; -import org.opengis.coverage.grid.SequenceType; + import org.opengis.referencing.datum.PixelInCell; + import org.opengis.referencing.operation.Matrix; + import org.opengis.referencing.operation.MathTransform; + import org.opengis.referencing.operation.TransformException; + import org.apache.sis.coverage.grid.GridCoverage; + import org.apache.sis.coverage.grid.GridGeometry; + import org.apache.sis.storage.StorageConnector; + import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.DataStoreReferencingException; + import org.apache.sis.storage.WritableGridCoverageResource; + import org.apache.sis.internal.storage.WritableResourceSupport; + import org.apache.sis.internal.storage.io.ChannelDataOutput; + import org.apache.sis.referencing.operation.matrix.Matrices; + import org.apache.sis.referencing.operation.transform.MathTransforms; + import org.apache.sis.storage.IncompatibleResourceException; + import org.apache.sis.image.PixelIterator; + import org.apache.sis.util.CharSequences; + import org.apache.sis.util.StringBuilders; + ++// Branch-dependent imports ++import org.apache.sis.image.SequenceType; ++ + + /** + * An ASCII Grid store with writing capabilities. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ + final class WritableStore extends AsciiGridStore implements WritableGridCoverageResource { + /** + * The line separator for writing the ASCII file. + */ + private final String lineSeparator; + + /** + * The output if this store is write-only, or {@code null} if this store is read/write. + * This is set to {@code null} when the store is closed. + */ + private ChannelDataOutput output; + + /** + * Creates a new ASCII Grid store from the given file, URL or stream. + * + * @param provider the factory that created this {@code DataStore} instance, or {@code null} if unspecified. + * @param connector information about the storage (URL, stream, <i>etc</i>). + * @throws DataStoreException if an error occurred while opening the stream. + */ + public WritableStore(final AsciiGridStoreProvider provider, final StorageConnector connector) throws DataStoreException { + super(provider, connector, false); + lineSeparator = System.lineSeparator(); + if (!super.canReadOrWrite(false)) { + output = connector.commit(ChannelDataOutput.class, AsciiGridStoreProvider.NAME); + } + } + + /** + * Returns whether this store can read or write. + */ + @Override + boolean canReadOrWrite(final boolean write) { + return write || super.canReadOrWrite(write); + } + + /** + * Returns an estimation of how close the "CRS to grid" transform is to integer values. + * This is used for choosing whether to map pixel centers or pixel centers. + */ + private static double distanceFromIntegers(final MathTransform gridToCRS) throws TransformException { + final Matrix m = MathTransforms.getMatrix(gridToCRS.inverse()); + if (m != null && Matrices.isAffine(m)) { + final int last = m.getNumCol() - 1; + double sum = 0; + for (int j=0; j<last; j++) { + final double e = m.getElement(j, last); + sum += Math.abs(Math.rint(e) - e); + } + return sum; + } + return Double.NaN; + } + + /** + * Gets the coefficients of the affine transform. + * + * @param header the map where to put the affine transform coefficients. + * @param gg the grid geometry from which to get the affine transform. + * @param h set of helper methods. + * @return the iteration order (e.g. from left to right, then top to bottom). + * @throws DataStoreException if the header can not be written. + */ + private static SequenceType getAffineCoefficients( + final Map<String,Object> header, final GridGeometry gg, + final WritableResourceSupport h) throws DataStoreException + { + String xll = XLLCORNER; + String yll = YLLCORNER; + MathTransform gridToCRS = gg.getGridToCRS(PixelInCell.CELL_CORNER); + try { + final MathTransform alternative = gg.getGridToCRS(PixelInCell.CELL_CENTER); + if (distanceFromIntegers(alternative) < distanceFromIntegers(gridToCRS)) { + gridToCRS = alternative; + xll = XLLCENTER; + yll = YLLCENTER; + } + } catch (TransformException e) { + throw new DataStoreReferencingException(h.canNotWrite(), e); + } + final AffineTransform at = h.getAffineTransform2D(gg.getExtent(), gridToCRS); + if (at.getShearX() != 0 || at.getShearY() != 0) { + throw new IncompatibleResourceException(h.rotationNotSupported(AsciiGridStoreProvider.NAME)); + } + double scaleX = at.getScaleX(); + double scaleY = -at.getScaleY(); + double x = at.getTranslateX(); + double y = at.getTranslateY(); + if (scaleX > 0 && scaleY > 0) { + y -= scaleY * (Integer) header.get(NROWS); + } else { + /* + * TODO: future version could support other signs, provided that + * we implement `PixelIterator` for other `SequenceType` values. + */ + throw new IncompatibleResourceException(h.canNotWrite()); + } + header.put(xll, x); + header.put(yll, y); + if (scaleX == scaleY) { + header.put(CELLSIZE, scaleX); + } else { + header.put(CELLSIZES[0], scaleX); + header.put(CELLSIZES[1], scaleY); + } + return SequenceType.LINEAR; + } + + /** + * Writes the content of the given map as the header of ASCII Grid file. + */ + private void writeHeader(final Map<String,Object> header, final ChannelDataOutput out) throws IOException { + int maxKeyLength = 0; + int maxValLength = 0; + for (final Map.Entry<String,Object> entry : header.entrySet()) { + final String text = entry.getValue().toString(); + entry.setValue(text); + maxValLength = Math.max(maxValLength, text.length()); + maxKeyLength = Math.max(maxKeyLength, entry.getKey().length()); + } + for (final Map.Entry<String,Object> entry : header.entrySet()) { + String text = entry.getKey(); + write(text, out); + write(CharSequences.spaces(maxKeyLength - text.length() + 1), out); + text = (String) entry.getValue(); + write(CharSequences.spaces(maxValLength - text.length()), out); + write(text, out); + write(lineSeparator, out); + } + } + + /** + * Writes a new coverage in the data store for this resource. If a coverage already exists for this resource, + * then it will be overwritten only if the {@code TRUNCATE} or {@code UPDATE} option is specified. + * + * @param coverage new data to write in the data store for this resource. + * @param options configuration of the write operation. + * @throws DataStoreException if an error occurred while writing data in the underlying data store. + */ + @Override + public synchronized void write(GridCoverage coverage, final Option... options) throws DataStoreException { + final WritableResourceSupport h = new WritableResourceSupport(this, options); // Does argument validation. + final int band = 0; // May become configurable in a future version. + try { + /* + * If `output` is non-null, we are in write-only mode and there is no previously existing image. + * Otherwise an image may exist and the behavior will depend on which options were supplied. + */ + if (output == null && !h.replace(input().input)) { + coverage = h.update(coverage); + } + final RenderedImage data = coverage.render(null); // Fail if not two-dimensional. + final Map<String,Object> header = new LinkedHashMap<>(); + header.put(NCOLS, data.getWidth()); + header.put(NROWS, data.getHeight()); + final SequenceType order = getAffineCoefficients(header, coverage.getGridGeometry(), h); + /* + * Open the destination channel only after the coverage has been validated by above method calls. + * After this point we should not have any validation errors. Write the nodata value even if it is + * "NaN" because the default is -9999, and we need to overwrite that default if it can not be used. + */ + final ChannelDataOutput out = (output != null) ? output : h.channel(input().input); + final Number nodataValue = setCoverage(coverage, data, band); + header.put(NODATA_VALUE, nodataValue); + writeHeader(header, out); + /* + * Writes all sample values. + */ + final float nodataAsFloat = nodataValue.floatValue(); + final double nodataAsDouble = nodataValue.doubleValue(); + final StringBuilder buffer = new StringBuilder(); + final PixelIterator it = new PixelIterator.Builder().setIteratorOrder(order).create(data); + final int dataType = it.getDataType().toDataBufferType(); + final int width = it.getDomain().width; + int remaining = width; + while (it.next()) { + switch (dataType) { + case DataBuffer.TYPE_DOUBLE: { + double value = it.getSampleDouble(band); + if (Double.isNaN(value)) { + value = nodataAsDouble; + } + buffer.append(value); + StringBuilders.trimFractionalPart(buffer); + break; + } + case DataBuffer.TYPE_FLOAT: { + float value = it.getSampleFloat(band); + if (Float.isNaN(value)) { + value = nodataAsFloat; + } + buffer.append(value); + StringBuilders.trimFractionalPart(buffer); + break; + } + default: { + buffer.append(it.getSample(band)); + break; + } + } + write(buffer, out); + buffer.setLength(0); + if (--remaining != 0) { + out.writeByte(' '); + } else { + write(lineSeparator, out); + remaining = width; + } + } + out.flush(); + writePRJ(); + /* + * If the channel is write-only (e.g. if we are writing in an `OutputStream`), + * we will not be able to write a second time. + */ + if (output != null) { + output = null; + out.channel.close(); + } + } catch (IOException e) { + closeOnError(e); + throw new DataStoreException(e); + } + } + + /** + * Writes the given text to the output. All characters must be US-ASCII (this is not verified). + */ + private static void write(final CharSequence text, final ChannelDataOutput out) throws IOException { + final int length = text.length(); + out.ensureBufferAccepts(length); + for (int i=0; i<length; i++) { + out.buffer.put((byte) text.charAt(i)); + } + } + + /** + * Closes this data store and releases any underlying resources. + * + * @throws DataStoreException if an error occurred while closing this data store. + */ + @Override + public synchronized void close() throws DataStoreException { + final ChannelDataOutput out = output; + output = null; + if (out != null) try { + out.channel.close(); + } catch (IOException e) { + throw new DataStoreException(e); + } + /* + * No need for try-with-resource because only one + * of `input` and `output` should be non-null. + */ + super.close(); + } + } diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java index 0000000000,4ff7398186..cab79cb71e mode 000000,100644..100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java @@@ -1,0 -1,829 +1,829 @@@ + /* + * 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.internal.storage.image; + + import java.util.Arrays; + import java.util.Collection; + import java.util.Map; + import java.util.HashMap; + import java.util.LinkedHashMap; + import java.util.logging.Level; + import java.io.IOException; + import java.io.EOFException; + import java.io.FileNotFoundException; + import java.io.UncheckedIOException; + import java.nio.file.Files; + import java.nio.file.Path; + import java.nio.file.NoSuchFileException; + import java.nio.file.StandardOpenOption; + import javax.imageio.ImageIO; + import javax.imageio.ImageReader; + import javax.imageio.ImageWriter; + import javax.imageio.spi.ImageReaderSpi; + import javax.imageio.stream.ImageInputStream; + import org.opengis.metadata.Metadata; + import org.opengis.metadata.maintenance.ScopeCode; + import org.opengis.referencing.datum.PixelInCell; + import org.opengis.referencing.operation.TransformException; + import org.apache.sis.coverage.grid.GridExtent; + import org.apache.sis.coverage.grid.GridGeometry; + import org.apache.sis.storage.Resource; + import org.apache.sis.storage.Aggregate; + import org.apache.sis.storage.StorageConnector; + import org.apache.sis.storage.GridCoverageResource; + import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.DataStoreClosedException; + import org.apache.sis.storage.DataStoreContentException; + import org.apache.sis.storage.DataStoreReferencingException; + import org.apache.sis.storage.ReadOnlyStorageException; + import org.apache.sis.storage.UnsupportedStorageException; + import org.apache.sis.internal.storage.Resources; + import org.apache.sis.internal.storage.PRJDataStore; + import org.apache.sis.internal.storage.io.IOUtilities; + import org.apache.sis.internal.referencing.j2d.AffineTransform2D; + import org.apache.sis.internal.storage.MetadataBuilder; + import org.apache.sis.internal.util.ListOfUnknownSize; + import org.apache.sis.metadata.sql.MetadataStoreException; + import org.apache.sis.util.collection.BackingStoreException; + import org.apache.sis.util.resources.Errors; + import org.apache.sis.util.CharSequences; + import org.apache.sis.util.ArraysExt; + import org.apache.sis.setup.OptionKey; + + + /** + * A data store which creates grid coverages from Image I/O readers using <cite>World File</cite> convention. + * Georeferencing is defined by two auxiliary files having the same name than the image file but different suffixes: + * + * <ul class="verbose"> + * <li>A text file containing the coefficients of the affine transform mapping pixel coordinates to geodesic coordinates. + * The reader expects one coefficient per line, in the same order than the order expected by the + * {@link java.awt.geom.AffineTransform#AffineTransform(double[]) AffineTransform(double[])} constructor, which is + * <var>scaleX</var>, <var>shearY</var>, <var>shearX</var>, <var>scaleY</var>, <var>translateX</var>, <var>translateY</var>. + * The reader looks for a file having the following suffixes, in preference order: + * <ol> + * <li>The first letter of the image file extension, followed by the last letter of + * the image file extension, followed by {@code 'w'}. Example: {@code "tfw"} for + * {@code "tiff"} images, and {@code "jgw"} for {@code "jpeg"} images.</li> + * <li>The extension of the image file with a {@code 'w'} appended.</li> + * <li>The {@code "wld"} extension.</li> + * </ol> + * </li> + * <li>A text file containing the <cite>Coordinate Reference System</cite> (CRS) definition + * in <cite>Well Known Text</cite> (WKT) syntax. + * The reader looks for a file having the {@code ".prj"} extension.</li> + * </ul> + * + * Every auxiliary text file are expected to be encoded in UTF-8 + * and every numbers are expected to be formatted in US locale. + * + * <h2>Type of input objects</h2> + * The {@link StorageConnector} input should be an instance of the following types: + * {@link java.nio.file.Path}, {@link java.io.File}, {@link java.net.URL} or {@link java.net.URI}. + * Other types such as {@link ImageInputStream} are also accepted but in those cases the auxiliary files can not be read. + * For any input of unknown type, this data store first checks if an {@link ImageReader} accepts the input type directly. + * If none is found, this data store tries to {@linkplain ImageIO#createImageInputStream(Object) create an input stream} + * from the input object. + * + * <p>The storage input object may also be an {@link ImageReader} instance ready for use + * (i.e. with its {@linkplain ImageReader#setInput(Object) input set} to a non-null value). + * In that case, this data store will use the given image reader as-is. + * The image reader will be {@linkplain ImageReader#dispose() disposed} + * and its input closed (if {@link AutoCloseable}) when this data store is {@linkplain #close() closed}.</p> + * + * <h2>Handling of multi-image files</h2> + * Because some image formats can store an arbitrary amount of images, + * this data store is considered as an aggregate with one resource per image. + * All image should have the same size and all resources will share the same {@link GridGeometry}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ + class WorldFileStore extends PRJDataStore implements Aggregate { + /** + * Image I/O format names (ignoring case) for which we have an entry in the {@code SpatialMetadata} database. + */ + private static final String[] KNOWN_FORMATS = { + "PNG" + }; + + /** + * Index of the main image. This is relevant only with formats capable to store an arbitrary amount of images. + * Current implementation assumes that the main image is always the first one, but it may become configurable + * in a future version if useful. + * + * @see #width + * @see #height + */ + static final int MAIN_IMAGE = 0; + + /** + * The default World File suffix when it can not be determined from {@link #location}. + * This is a GDAL convention. + */ + private static final String DEFAULT_SUFFIX = "wld"; + + /** + * The "cell center" versus "cell corner" interpretation of translation coefficients. + * The ESRI specification said that the coefficients map to pixel center. + */ + static final PixelInCell CELL_ANCHOR = PixelInCell.CELL_CENTER; + + /** + * The filename extension (may be an empty string), or {@code null} if unknown. + * It does not include the leading dot. + */ + final String suffix; + + /** + * The filename extension for the auxiliary "world file". + * For the TIFF format, this is typically {@code "tfw"}. + * This is computed as a side-effect of {@link #readWorldFile()}. + */ + private String suffixWLD; + + /** + * The image reader, set by the constructor and cleared when the store is closed. + * May also be null if the store is initially write-only, in which case a reader + * may be created the first time than an image is read. + * + * @see #reader() + */ + private ImageReader reader; + + /** + * Width and height of the main image. + * The {@link #gridGeometry} is assumed valid only for images having this size. + * + * @see #MAIN_IMAGE + * @see #gridGeometry + */ + private int width, height; + + /** + * The conversion from pixel center to CRS, or {@code null} if none or not yet computed. + * The grid extent has the size given by {@link #width} and {@link #height}. + * + * @see #crs + * @see #width + * @see #height + * @see #getGridGeometry(int) + */ + private GridGeometry gridGeometry; + + /** + * All images in this resource, created when first needed. + * Elements in this list will also be created when first needed. + * + * @see #components() + */ + private Components components; + + /** + * The metadata object, or {@code null} if not yet created. + * + * @see #getMetadata() + */ + private Metadata metadata; + + /** + * Identifiers used by a resource. Identifiers must be unique in the data store, + * so after an identifier has been used it can not be reused anymore even if the + * resource having that identifier has been removed. + * Values associated to identifiers tell whether the resource still exist. + * + * @see WorldFileResource#getIdentifier() + */ + final Map<String,Boolean> identifiers; + + /** + * Creates a new store from the given file, URL or stream. + * + * @param provider the factory that created this {@code DataStore} instance, or {@code null} if unspecified. + * @param connector information about the storage (URL, stream, <i>etc</i>). + * @param readOnly whether to fail if the channel can not be opened at least in read mode. + * @throws DataStoreException if an error occurred while opening the stream. + * @throws IOException if an error occurred while creating the image reader instance. + */ + WorldFileStore(final WorldFileStoreProvider provider, final StorageConnector connector, final boolean readOnly) + throws DataStoreException, IOException + { + super(provider, connector); + identifiers = new HashMap<>(); + listeners.useWarningEventsOnly(); + final Object storage = connector.getStorage(); + if (storage instanceof ImageReader) { + reader = (ImageReader) storage; + suffix = IOUtilities.extension(reader.getInput()); + configureReader(); + return; + } + if (storage instanceof ImageWriter) { + suffix = IOUtilities.extension(((ImageWriter) storage).getOutput()); + return; + } + suffix = IOUtilities.extension(storage); + if (!(readOnly || fileExists(connector))) { + /* + * If the store is opened in read-write mode, create the image reader only + * if the file exists and is non-empty. Otherwise we let `reader` to null + * and the caller will create an image writer instead. + */ + return; + } + /* + * Search for a reader that claim to be able to read the storage input. + * First we try readers associated to the file suffix. If no reader is + * found, we try all other readers. + */ + final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>(); + if (suffix != null) { + reader = FormatFilter.SUFFIX.createReader(suffix, connector, deferred); + } + if (reader == null) { + reader = FormatFilter.SUFFIX.createReader(null, connector, deferred); + fallback: if (reader == null) { + /* + * If no reader has been found, maybe `StorageConnector` has not been able to create + * an `ImageInputStream`. It may happen if the storage object is of unknown type. + * Check if it is the case, then try all providers that we couldn't try because of that. + */ + ImageInputStream stream = null; + for (final Map.Entry<ImageReaderSpi,Boolean> entry : deferred.entrySet()) { + if (entry.getValue()) { + if (stream == null) { + if (!readOnly) { + // ImageOutputStream is both read and write. + stream = ImageIO.createImageOutputStream(storage); + } + if (stream == null) { + stream = ImageIO.createImageInputStream(storage); + if (stream == null) break; + } + } + final ImageReaderSpi p = entry.getKey(); + if (p.canDecodeInput(stream)) { + connector.closeAllExcept(storage); + reader = p.createReaderInstance(); + reader.setInput(stream); + break fallback; + } + } + } + throw new UnsupportedStorageException(super.getLocale(), WorldFileStoreProvider.NAME, + storage, connector.getOption(OptionKey.OPEN_OPTIONS)); + } + } + configureReader(); + /* + * Do not invoke any method that may cause the image reader to start reading the stream, + * because the `WritableStore` subclass will want to save the initial stream position. + */ + } + + /** + * Sets the locale to use for warning messages, if supported. If the reader + * does not support the locale, the reader's default locale will be used. + */ + private void configureReader() { + try { + reader.setLocale(listeners.getLocale()); + } catch (IllegalArgumentException e) { + // Ignore + } + reader.addIIOReadWarningListener(new WarningListener(listeners)); + } + + /** + * Returns {@code true} if the image file exists and is non-empty. + * This is used for checking if an {@link ImageReader} should be created. + * If the file is going to be truncated, then it is considered already empty. + * + * @param connector the connector to use for opening the file. + * @return whether the image file exists and is non-empty. + */ + private boolean fileExists(final StorageConnector connector) throws DataStoreException, IOException { + if (!ArraysExt.contains(connector.getOption(OptionKey.OPEN_OPTIONS), StandardOpenOption.TRUNCATE_EXISTING)) { + for (Path path : super.getComponentFiles()) { + if (Files.isRegularFile(path) && Files.size(path) > 0) { + return true; + } + } + } + return false; + } + + /** + * Returns the preferred suffix for the auxiliary world file. For TIFF images, this is {@code "tfw"}. + * This method tries to use the same case (lower-case or upper-case) than the suffix of the main file. + */ + private String getWorldFileSuffix() { + if (suffix != null) { + final int length = suffix.length(); + if (suffix.codePointCount(0, length) >= 2) { + boolean lower = true; + for (int i = length; i > 0;) { + final int c = suffix.codePointBefore(i); + lower = Character.isLowerCase(c); if ( lower) break; + lower = !Character.isUpperCase(c); if (!lower) break; + i -= Character.charCount(c); + } + // If the case can not be determined, `lower` will default to `true`. + return new StringBuilder(3) + .appendCodePoint(suffix.codePointAt(0)) + .appendCodePoint(suffix.codePointBefore(length)) + .append(lower ? 'w' : 'W').toString(); + } + } + return DEFAULT_SUFFIX; + } + + /** + * Reads the "World file" by searching for an auxiliary file with a suffix inferred from + * the suffix of the main file. This method tries suffixes with the following conventions, + * in preference order. + * + * <ol> + * <li>First letter of main file suffix, followed by last letter, followed by {@code 'w'}.</li> + * <li>Full suffix of the main file followed by {@code 'w'}.</li> + * <li>{@value #DEFAULT_SUFFIX}.</li> + * </ol> + * + * @return the "World file" content as an affine transform, or {@code null} if none was found. + * @throws IOException if an I/O error occurred. + * @throws DataStoreException if the auxiliary file content can not be parsed. + */ + private AffineTransform2D readWorldFile() throws IOException, DataStoreException { + IOException warning = null; + final String preferred = getWorldFileSuffix(); + loop: for (int convention=0;; convention++) { + final String wld; + switch (convention) { + default: break loop; + case 0: wld = preferred; break; // First file suffix to search. + case 2: wld = DEFAULT_SUFFIX; break; // File suffix to search in last resort. + case 1: { + if (preferred.equals(DEFAULT_SUFFIX)) break loop; + wld = suffix + preferred.charAt(preferred.length() - 1); + break; + } + } + try { + return readWorldFile(wld); + } catch (NoSuchFileException | FileNotFoundException e) { + if (warning == null) { + warning = e; + } else { + warning.addSuppressed(e); + } + } + } + if (warning != null) { + listeners.warning(resources().getString(Resources.Keys.CanNotReadAuxiliaryFile_1, preferred), warning); + } + return null; + } + + /** + * Reads the "World file" by parsing an auxiliary file with the given suffix. + * + * @param wld suffix of the auxiliary file. + * @return the "World file" content as an affine transform. + * @throws IOException if an I/O error occurred. + * @throws DataStoreException if the file content can not be parsed. + */ + private AffineTransform2D readWorldFile(final String wld) throws IOException, DataStoreException { + final AuxiliaryContent content = readAuxiliaryFile(wld); + final String filename = content.getFilename(); + final CharSequence[] lines = CharSequences.splitOnEOL(readAuxiliaryFile(wld)); + final int expected = 6; // Expected number of elements. + int count = 0; // Actual number of elements. + final double[] elements = new double[expected]; + for (int i=0; i<expected; i++) { + final String line = lines[i].toString().trim(); + if (!line.isEmpty() && line.charAt(0) != '#') { + if (count >= expected) { + throw new DataStoreContentException(errors().getString(Errors.Keys.TooManyOccurrences_2, expected, "coefficient")); + } + try { + elements[count++] = Double.parseDouble(line); + } catch (NumberFormatException e) { + throw new DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2, filename, i), e); + } + } + } + if (count != expected) { + throw new EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, filename)); + } + if (filename != null) { + final int s = filename.lastIndexOf('.'); + if (s >= 0) { + suffixWLD = filename.substring(s+1); + } + } + return new AffineTransform2D(elements); + } + + /** + * Returns the localized resources for producing warnings or error messages. + */ + final Resources resources() { + return Resources.forLocale(listeners.getLocale()); + } + + /** + * Returns the localized resources for producing error messages. + */ + private Errors errors() { + return Errors.getResources(listeners.getLocale()); + } + + /** + * Returns paths to the main file together with auxiliary files. + * + * @return paths to the main file and auxiliary files, or an empty array if unknown. + * @throws DataStoreException if the URI can not be converted to a {@link Path}. + */ + @Override + public final synchronized Path[] getComponentFiles() throws DataStoreException { + if (suffixWLD == null) try { + getGridGeometry(MAIN_IMAGE); // Will compute `suffixWLD` as a side effect. + } catch (IOException e) { + throw new DataStoreException(e); + } + return listComponentFiles(suffixWLD, PRJ); // `suffixWLD` still null if file was not found. + } + + /** + * Gets the grid geometry for image at the given index. + * This method should be invoked only once per image, and the result cached. + * + * @param index index of the image for which to read the grid geometry. + * @return grid geometry of the image at the given index. + * @throws IndexOutOfBoundsException if the image index is out of bounds. + * @throws IOException if an I/O error occurred. + * @throws DataStoreException if the {@code *.prj} or {@code *.tfw} auxiliary file content can not be parsed. + */ + final GridGeometry getGridGeometry(final int index) throws IOException, DataStoreException { + assert Thread.holdsLock(this); + final ImageReader reader = reader(); + if (gridGeometry == null) { + final AffineTransform2D gridToCRS; + width = reader.getWidth (MAIN_IMAGE); + height = reader.getHeight(MAIN_IMAGE); + gridToCRS = readWorldFile(); + readPRJ(); + gridGeometry = new GridGeometry(new GridExtent(width, height), CELL_ANCHOR, gridToCRS, crs); + } + if (index != MAIN_IMAGE) { + final int w = reader.getWidth (index); + final int h = reader.getHeight(index); + if (w != width || h != height) { + // Can not use `gridToCRS` and `crs` because they may not apply. + return new GridGeometry(new GridExtent(w, h), CELL_ANCHOR, null, null); + } + } + return gridGeometry; + } + + /** + * Sets the store-wide grid geometry when a new coverage is written. The {@link WritableStore} implementation + * is responsible for making sure that the new grid geometry is compatible with preexisting grid geometry. + * + * @param index index of the image for which to set the grid geometry. + * @param gg the new grid geometry. + * @return suffix of the "world file", or {@code null} if the image can not be written. + */ + String setGridGeometry(final int index, final GridGeometry gg) throws IOException, DataStoreException { + if (index != MAIN_IMAGE) { + return null; + } + final GridExtent extent = gg.getExtent(); + final int w = Math.toIntExact(extent.getSize(WorldFileResource.X_DIMENSION)); + final int h = Math.toIntExact(extent.getSize(WorldFileResource.Y_DIMENSION)); + final String s = (suffixWLD != null) ? suffixWLD : getWorldFileSuffix(); + crs = gg.isDefined(GridGeometry.CRS) ? gg.getCoordinateReferenceSystem() : null; + gridGeometry = gg; // Set only after success of all the above. + width = w; + height = h; + suffixWLD = s; + return s; + } + + /** + * Returns information about the data store as a whole. + */ + @Override + public final synchronized Metadata getMetadata() throws DataStoreException { + if (metadata == null) try { + final MetadataBuilder builder = new MetadataBuilder(); + String format = reader().getFormatName(); + for (final String key : KNOWN_FORMATS) { + if (key.equalsIgnoreCase(format)) { + try { + builder.setPredefinedFormat(key); + format = null; + } catch (MetadataStoreException e) { + listeners.warning(Level.FINE, null, e); + } + break; + } + } + builder.addFormatName(format); // Does nothing if `format` is null. - builder.addResourceScope(ScopeCode.COVERAGE, null); ++ builder.addResourceScope(ScopeCode.valueOf("COVERAGE"), null); + builder.addSpatialRepresentation(null, getGridGeometry(MAIN_IMAGE), true); + if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) { + builder.addExtent(gridGeometry.getEnvelope()); + } + addTitleOrIdentifier(builder); + builder.setISOStandards(false); + metadata = builder.buildAndFreeze(); + } catch (IOException e) { + throw new DataStoreException(e); + } catch (TransformException e) { + throw new DataStoreReferencingException(e); + } + return metadata; + } + + /** + * Returns all images in this store. Note that fetching the size of the list is a potentially costly operation. + * + * @return list of images in this store. + */ + @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") + public final synchronized Collection<? extends GridCoverageResource> components() throws DataStoreException { + if (components == null) try { + components = new Components(reader().getNumImages(false)); + } catch (IOException e) { + throw new DataStoreException(e); + } + return components; + } + + /** + * Returns all images in this store, or {@code null} if none and {@code create} is false. + * + * @param create whether to create the component list if it was not already created. + * @param numImages number of images, or any negative value if unknown. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + final Components components(final boolean create, final int numImages) { + if (components == null && create) { + components = new Components(numImages); + } + return components; + } + + /** + * A list of images where each {@link WorldFileResource} instance is initialized when first needed. + * Fetching the list size may be a costly operation and will be done only if requested. + */ + final class Components extends ListOfUnknownSize<WorldFileResource> { + /** + * Size of this list, or any negative value if unknown. + */ + private int size; + + /** + * All elements in this list. Some array elements may be {@code null} if the image + * has never been requested. + */ + private WorldFileResource[] images; + + /** + * Creates a new list of images. + * + * @param numImages number of images, or any negative value if unknown. + */ + private Components(final int numImages) { + size = numImages; + images = new WorldFileResource[Math.max(numImages, 1)]; + } + + /** + * Returns the number of images in this list. + * This method may be costly when invoked for the first time. + */ + @Override + public int size() { + synchronized (WorldFileStore.this) { + if (size < 0) try { + size = reader().getNumImages(true); + images = ArraysExt.resize(images, size); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (DataStoreException e) { + throw new BackingStoreException(e); + } + return size; + } + } + + /** + * Returns the number of images if this information is known, or any negative value otherwise. + * This is used by {@link ListOfUnknownSize} for optimizing some operations. + */ + @Override + protected int sizeIfKnown() { + synchronized (WorldFileStore.this) { + return size; + } + } + + /** + * Returns {@code true} if an element exists at the given index. + * Current implementations is not more efficient than {@link #get(int)}. + */ + @Override + protected boolean exists(final int index) { + synchronized (WorldFileStore.this) { + if (size >= 0) { + return index >= 0 && index < size; + } + try { + return get(index) != null; + } catch (IndexOutOfBoundsException e) { + return false; + } + } + } + + /** + * Returns the image at the given index. New instances are created when first requested. + * + * @param index index of the image for which to get a resource. + * @return resource for the image identified by the given index. + * @throws IndexOutOfBoundsException if the image index is out of bounds. + */ + @Override + public WorldFileResource get(final int index) { + synchronized (WorldFileStore.this) { + WorldFileResource image = null; + if (index < images.length) { + image = images[index]; + } + if (image == null) try { + image = createImageResource(index); + if (index >= images.length) { + images = Arrays.copyOf(images, Math.max(images.length * 2, index + 1)); + } + images[index] = image; + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (DataStoreException e) { + throw new BackingStoreException(e); + } + return image; + } + } + + /** + * Invoked <em>after</em> an image has been added to the image file. + * This method adds in this list a reference to the newly added file. + * + * @param image the image to add to this list. + */ + final void added(final WorldFileResource image) { + size = image.getImageIndex(); + if (size >= images.length) { + images = Arrays.copyOf(images, size * 2); + } + images[size++] = image; + } + + /** + * Invoked <em>after</em> an image has been removed from the image file. + * This method performs no bounds check (it must be done by the caller). + * + * @param index index of the image that has been removed. + */ + final void removed(int index) throws DataStoreException { + final int last = images.length - 1; + System.arraycopy(images, index+1, images, index, last - index); + images[last] = null; + size--; + while (index < last) { + final WorldFileResource image = images[index++]; + if (image != null) image.decrementImageIndex(); + } + } + + /** + * Removes the element at the specified position in this list. + */ + @Override + public WorldFileResource remove(final int index) { + final WorldFileResource image = get(index); + try { + WorldFileStore.this.remove(image); + } catch (DataStoreException e) { + throw new UnsupportedOperationException(e); + } + return image; + } + } + + /** + * Invoked by {@link Components} when the caller want to remove a resource. + * The actual implementation is provided by {@link WritableStore}. + */ + void remove(final Resource resource) throws DataStoreException { + throw new ReadOnlyStorageException(); + } + + /** + * Creates a {@link GridCoverageResource} for the specified image. + * This method is invoked by {@link Components} when first needed + * and the result is cached by the caller. + * + * @param index index of the image for which to create a resource. + * @return resource for the image identified by the given index. + * @throws IndexOutOfBoundsException if the image index is out of bounds. + */ + WorldFileResource createImageResource(final int index) throws DataStoreException, IOException { + return new WorldFileResource(this, listeners, index, getGridGeometry(index)); + } + + /** + * Prepares an image reader compatible with the writer and sets its input. + * This method is invoked for switching from write mode to read mode. + * Its actual implementation is provided by {@link WritableResource}. + * + * @param current the current image reader, or {@code null} if none. + * @return the image reader to use, or {@code null} if none. + * @throws IOException if an error occurred while preparing the reader. + */ + ImageReader prepareReader(ImageReader current) throws IOException { + return null; + } + + /** + * Returns the reader without doing any validation. The reader may be {@code null} either + * because the store is closed or because the store is initially opened in write-only mode. + * The reader may have a {@code null} input. + */ + final ImageReader getCurrentReader() { + return reader; + } + + /** + * Returns the reader if it has not been closed. + * + * @throws DataStoreClosedException if this data store is closed. + * @throws IOException if an error occurred while preparing the reader. + */ + final ImageReader reader() throws DataStoreException, IOException { + assert Thread.holdsLock(this); + ImageReader current = reader; + if (current == null || current.getInput() == null) { + reader = current = prepareReader(current); + if (current == null) { + throw new DataStoreClosedException(getLocale(), WorldFileStoreProvider.NAME, StandardOpenOption.READ); + } + configureReader(); + } + return current; + } + + /** + * Closes this data store and releases any underlying resources. + * + * @throws DataStoreException if an error occurred while closing this data store. + */ + @Override + public synchronized void close() throws DataStoreException { + final ImageReader codec = reader; + reader = null; + metadata = null; + components = null; + gridGeometry = null; + if (codec != null) try { + final Object input = codec.getInput(); + codec.setInput(null); + codec.dispose(); + if (input instanceof AutoCloseable) { + ((AutoCloseable) input).close(); + } + } catch (Exception e) { + throw new DataStoreException(e); + } + } + } diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java index 1122b57fbe,e8c094b156..3cde789dbe --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/AbstractFeatureSet.java @@@ -19,13 -19,12 +19,12 @@@ package org.apache.sis.storage import java.util.Optional; import java.util.OptionalLong; import org.opengis.util.GenericName; - import org.apache.sis.storage.DataStore; - import org.apache.sis.storage.DataStoreException; - import org.apache.sis.storage.FeatureSet; + import org.opengis.metadata.Metadata; import org.apache.sis.storage.event.StoreListeners; + import org.apache.sis.internal.storage.MetadataBuilder; // Branch-dependent imports -import org.opengis.feature.FeatureType; +import org.apache.sis.feature.DefaultFeatureType; /** diff --cc storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MetadataBuilderTest.java index 5b4473178e,24dfdf538f..6c2c8eabbb --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MetadataBuilderTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MetadataBuilderTest.java @@@ -81,8 -83,8 +81,8 @@@ public final strictfp class MetadataBui private static void verifyCopyrightParsing(final String notice) { final MetadataBuilder builder = new MetadataBuilder(); builder.parseLegalNotice(notice); - final LegalConstraints constraints = (LegalConstraints) getSingleton(getSingleton( + final DefaultLegalConstraints constraints = (DefaultLegalConstraints) getSingleton(getSingleton( - builder.build(false).getIdentificationInfo()).getResourceConstraints()); + builder.build().getIdentificationInfo()).getResourceConstraints()); assertEquals("useConstraints", Restriction.COPYRIGHT, getSingleton(constraints.getUseConstraints())); final Citation ref = getSingleton(constraints.getReferences()); diff --cc storage/sis-storage/src/test/java/org/apache/sis/internal/storage/esri/AsciiGridStoreTest.java index 0000000000,ee5d64bd2a..6c84085b8d mode 000000,100644..100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/esri/AsciiGridStoreTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/esri/AsciiGridStoreTest.java @@@ -1,0 -1,133 +1,131 @@@ + /* + * 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.internal.storage.esri; + + import java.util.List; + import java.awt.image.Raster; + import java.awt.image.RenderedImage; + import org.opengis.metadata.Metadata; + import org.opengis.metadata.extent.GeographicBoundingBox; -import org.opengis.metadata.identification.Identification; ++import org.opengis.metadata.identification.DataIdentification; + import org.apache.sis.coverage.Category; + import org.apache.sis.coverage.grid.GridCoverage; + import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.StorageConnector; + import org.apache.sis.storage.ProbeResult; + import org.apache.sis.test.TestCase; + import org.junit.Test; + + import static org.junit.Assert.*; + import static org.apache.sis.test.TestUtilities.getSingleton; + + + /** + * Tests {@link AsciiGridStore} and {@link AsciiGridStoreProvider}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ + public final strictfp class AsciiGridStoreTest extends TestCase { + /** + * Returns a storage connector with the URL to the test data. + */ + private static StorageConnector testData() { + return new StorageConnector(AsciiGridStoreTest.class.getResource("grid.asc")); + } + + /** + * Tests {@link AsciiGridStoreProvider#probeContent(StorageConnector)} method. + * + * @throws DataStoreException if en error occurred while reading the CSV file. + */ + @Test + public void testProbeContent() throws DataStoreException { + final AsciiGridStoreProvider p = new AsciiGridStoreProvider(); + final ProbeResult r = p.probeContent(testData()); + assertTrue(r.isSupported()); + assertEquals("text/plain", r.getMimeType()); + } + + /** + * Tests the metadata of the {@code "grid.asc"} file. This test reads only the header. + * It should not test sample dimensions or pixel values, because doing so read the full + * image and is the purpose of {@link #testRead()}. + * + * @throws DataStoreException if an error occurred while reading the file. + */ + @Test + public void testMetadata() throws DataStoreException { + try (AsciiGridStore store = new AsciiGridStore(null, testData(), true)) { + assertEquals("grid", store.getIdentifier().get().toString()); + final Metadata metadata = store.getMetadata(); + /* + * Format information is hard-coded in "SpatialMetadata" database. Complete string should + * be "ESRI ArcInfo ASCII Grid format" but it depends on the presence of Derby dependency. + */ - final Identification id = getSingleton(metadata.getIdentificationInfo()); - final String format = getSingleton(id.getResourceFormats()).getFormatSpecificationCitation().getTitle().toString(); - assertTrue(format, format.contains("ASCII Grid")); ++ final DataIdentification id = (DataIdentification) getSingleton(metadata.getIdentificationInfo()); + /* + * This information should have been read from the PRJ file. + */ + assertEquals("WGS 84 / World Mercator", + getSingleton(metadata.getReferenceSystemInfo()).getName().getCode()); + final GeographicBoundingBox bbox = (GeographicBoundingBox) + getSingleton(getSingleton(id.getExtents()).getGeographicElements()); + assertEquals(-84, bbox.getSouthBoundLatitude(), 1); + assertEquals(+85, bbox.getNorthBoundLatitude(), 1); + /* + * Verify that the metadata is cached. + */ + assertSame(metadata, store.getMetadata()); + } + } + + /** + * Tests reading a few values from the {@code "grid.asc"} file. + * + * @throws DataStoreException if an error occurred while reading the file. + */ + @Test + public void testRead() throws DataStoreException { + try (AsciiGridStore store = new AsciiGridStore(null, testData(), true)) { + final List<Category> categories = getSingleton(store.getSampleDimensions()).getCategories(); + assertEquals(2, categories.size()); + assertEquals( -2, categories.get(0).getSampleRange().getMinDouble(), 1); + assertEquals( 30, categories.get(0).getSampleRange().getMaxDouble(), 1); + assertEquals(-9999, categories.get(1).forConvertedValues(false).getSampleRange().getMinDouble(), 0); + /* + * Check sample values. + */ + final GridCoverage coverage = store.read(null, null); + final RenderedImage image = coverage.render(null); + assertEquals(10, image.getWidth()); + assertEquals(20, image.getHeight()); + final Raster tile = image.getTile(0,0); + assertEquals( 1.061f, tile.getSampleFloat(0, 0, 0), 0f); + assertEquals(Float.NaN, tile.getSampleFloat(9, 0, 0), 0f); + assertEquals(Float.NaN, tile.getSampleFloat(9, 19, 0), 0f); + assertEquals( -1.075f, tile.getSampleFloat(0, 19, 0), 0f); + assertEquals( 27.039f, tile.getSampleFloat(4, 10, 0), 0f); + /* + * Verify that the coverage is cached. + */ + assertSame(coverage, store.read(null, null)); + } + } + } diff --cc storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java index 0000000000,ec967d2d67..7ff00be156 mode 000000,100644..100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java @@@ -1,0 -1,167 +1,165 @@@ + /* + * 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.internal.storage.image; + + import java.io.IOException; + import java.nio.file.Path; + import java.nio.file.Files; + import java.nio.file.DirectoryStream; + import java.nio.file.StandardOpenOption; + import org.opengis.metadata.Metadata; + import org.opengis.metadata.extent.GeographicBoundingBox; -import org.opengis.metadata.identification.Identification; ++import org.opengis.metadata.identification.DataIdentification; + import org.apache.sis.coverage.grid.GridCoverage; + import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.GridCoverageResource; + import org.apache.sis.storage.StorageConnector; + import org.apache.sis.storage.ProbeResult; + import org.apache.sis.storage.ResourceAlreadyExistsException; + import org.apache.sis.setup.OptionKey; + import org.apache.sis.test.TestCase; + import org.junit.Test; + + import static org.junit.Assert.*; + import static org.apache.sis.test.TestUtilities.getSingleton; + + + /** + * Tests {@link WorldFileStore} and {@link WorldFileStoreProvider}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ + public final strictfp class WorldFileStoreTest extends TestCase { + /** + * Returns a storage connector with the URL to the test data. + */ + private static StorageConnector testData() { + return new StorageConnector(WorldFileStoreTest.class.getResource("gradient.png")); + } + + /** + * Tests {@link WorldFileStoreProvider#probeContent(StorageConnector)} method. + * + * @throws DataStoreException if en error occurred while reading the CSV file. + */ + @Test + public void testProbeContent() throws DataStoreException { + final WorldFileStoreProvider provider = new WorldFileStoreProvider(); + final ProbeResult result = provider.probeContent(testData()); + assertTrue(result.isSupported()); + assertEquals("image/png", result.getMimeType()); + } + + /** + * Tests the metadata of the {@code "gradient.png"} file. + * + * @throws DataStoreException if an error occurred during Image I/O or data store operations. + */ + @Test + public void testMetadata() throws DataStoreException { + final WorldFileStoreProvider provider = new WorldFileStoreProvider(); + try (WorldFileStore store = provider.open(testData())) { + assertFalse(store instanceof WritableStore); + assertEquals("gradient", store.getIdentifier().get().toString()); + final Metadata metadata = store.getMetadata(); - final Identification id = getSingleton(metadata.getIdentificationInfo()); - final String format = getSingleton(id.getResourceFormats()).getFormatSpecificationCitation().getTitle().toString(); - assertTrue(format, format.contains("PNG")); ++ final DataIdentification id = (DataIdentification) getSingleton(metadata.getIdentificationInfo()); + assertEquals("WGS 84", getSingleton(metadata.getReferenceSystemInfo()).getName().getCode()); + final GeographicBoundingBox bbox = (GeographicBoundingBox) + getSingleton(getSingleton(id.getExtents()).getGeographicElements()); + assertEquals( -90, bbox.getSouthBoundLatitude(), STRICT); + assertEquals( +90, bbox.getNorthBoundLatitude(), STRICT); + assertEquals(-180, bbox.getWestBoundLongitude(), STRICT); + assertEquals(+180, bbox.getEastBoundLongitude(), STRICT); + /* + * Verify that the metadata is cached. + */ + assertSame(metadata, store.getMetadata()); + } + } + + /** + * Tests reading the coverage and writing it in a new file. + * + * @throws DataStoreException if an error occurred during Image I/O or data store operations. + * @throws IOException if an error occurred when creating, reading or deleting temporary files. + */ + @Test + public void testReadWrite() throws DataStoreException, IOException { + final Path directory = Files.createTempDirectory("SIS-"); + try { + final WorldFileStoreProvider provider = new WorldFileStoreProvider(); + try (WorldFileStore source = provider.open(testData())) { + assertFalse(source instanceof WritableStore); + final GridCoverageResource resource = getSingleton(source.components()); + assertEquals("identifier", "1", resource.getIdentifier().get().toString()); + /* + * Above `resource` is the content of "gradient.png" file. + * Write the resource in a new file using a different format. + */ + final Path targetPath = directory.resolve("copy.jpg"); + final StorageConnector connector = new StorageConnector(targetPath); + connector.setOption(OptionKey.OPEN_OPTIONS, new StandardOpenOption[] { + StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE + }); + try (WritableStore target = (WritableStore) provider.open(connector)) { + assertEquals(0, target.isMultiImages()); + final WritableResource copy = (WritableResource) target.add(resource); + assertEquals(1, target.isMultiImages()); + assertNotSame(resource, copy); + assertEquals (resource.getGridGeometry(), copy.getGridGeometry()); + assertEquals (resource.getSampleDimensions(), copy.getSampleDimensions()); + /* + * Verify that attempt to write again without `REPLACE` mode fails. + */ + final GridCoverage coverage = resource.read(null, null); + try { + copy.write(coverage); + fail("Should not have replaced existing resource."); + } catch (ResourceAlreadyExistsException e) { + final String message = e.getMessage(); + assertTrue(message, message.contains("1")); // "1" is the image identifier. + } + /* + * Try to write again in `REPLACE` mode. + */ + copy.write(coverage, WritableResource.CommonOption.REPLACE); + assertEquals(1, target.isMultiImages()); + } + /* + * Verify that the 3 files have been written. The JGW file content is verified, + * but the PRJ file content is not fully verified because it may vary. + */ + assertTrue(Files.size(targetPath) > 0); + assertTrue(Files.readAllLines(directory.resolve("copy.prj")) + .stream().anyMatch((line) -> line.contains("WGS 84"))); + assertArrayEquals(new String[] { + "2.8125", "0.0", "0.0", "-2.8125", "-178.59375", "88.59375" + }, Files.readAllLines(directory.resolve("copy.jgw")).toArray()); + } + } finally { + try (DirectoryStream<Path> entries = Files.newDirectoryStream(directory)) { + for (Path entry : entries) { + Files.delete(entry); + } + } + Files.delete(directory); + } + } + }