diff --git a/autotest/cpp/CMakeLists.txt b/autotest/cpp/CMakeLists.txt index f767451fa6b9..bfea11bc9ec1 100644 --- a/autotest/cpp/CMakeLists.txt +++ b/autotest/cpp/CMakeLists.txt @@ -91,6 +91,7 @@ add_executable( test_osr_ct.cpp test_osr_proj4.cpp test_triangulation.cpp + test_utilities.cpp test_marching_squares_contour.cpp test_marching_squares_polygon.cpp test_marching_squares_square.cpp diff --git a/autotest/cpp/test_utilities.cpp b/autotest/cpp/test_utilities.cpp new file mode 100644 index 000000000000..52849efcb387 --- /dev/null +++ b/autotest/cpp/test_utilities.cpp @@ -0,0 +1,111 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Project: C++ Test Suite for GDAL/OGR +// Purpose: Test the C API of utilities as library functions +// Author: Even Rouault +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2023, Even Rouault +/* + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ****************************************************************************/ + +#include "gdal_unit_test.h" + +#include "cpl_error.h" +#include "cpl_string.h" +#include "gdal_priv.h" +#include "gdal_utils.h" + +#include "gtest_include.h" + +namespace +{ + +struct test_utilities : public ::testing::Test +{ +}; + +TEST_F(test_utilities, GDALFootprint) +{ + CPLErrorHandlerPusher oQuietErrors(CPLQuietErrorHandler); + // Test if (pszDest == nullptr && hDstDS == nullptr) + EXPECT_EQ(GDALFootprint(/* pszDest = */ nullptr, + /* hDstDS = */ nullptr, + /* hSrcDataset = */ nullptr, + /* psOptionsIn = */ nullptr, + /* pbUsageError = */ nullptr), + nullptr); + + // Test if (hSrcDataset == nullptr) + EXPECT_EQ(GDALFootprint(/* pszDest = */ "/vsimem/out", + /* hDstDS = */ nullptr, + /* hSrcDataset = */ nullptr, + /* psOptionsIn = */ nullptr, + /* pbUsageError = */ nullptr), + nullptr); + + // Test if (hDstDS != nullptr && psOptionsIn && psOptionsIn->bCreateOutput) + { + CPLStringList aosArgv; + aosArgv.AddString("-of"); + aosArgv.AddString("Memory"); + auto poMemDrv = GetGDALDriverManager()->GetDriverByName("Memory"); + if (poMemDrv) + { + auto psOptions = GDALFootprintOptionsNew(aosArgv.List(), nullptr); + auto poInDS = std::unique_ptr( + poMemDrv->Create("", 0, 0, 0, GDT_Unknown, nullptr)); + auto poOutDS = std::unique_ptr( + poMemDrv->Create("", 0, 0, 0, GDT_Unknown, nullptr)); + EXPECT_EQ( + GDALFootprint( + /* pszDest = */ nullptr, + /* hDstDS = */ GDALDataset::ToHandle(poOutDS.get()), + /* hSrcDataset = */ GDALDataset::ToHandle(poInDS.get()), + /* psOptionsIn = */ psOptions, + /* pbUsageError = */ nullptr), + nullptr); + GDALFootprintOptionsFree(psOptions); + } + } + + // Test if (psOptions == nullptr) + // and if (poSrcDS->GetRasterCount() == 0) + { + auto poMemDrv = GetGDALDriverManager()->GetDriverByName("Memory"); + if (poMemDrv) + { + auto poInDS = std::unique_ptr( + poMemDrv->Create("", 0, 0, 0, GDT_Unknown, nullptr)); + auto poOutDS = std::unique_ptr( + poMemDrv->Create("", 0, 0, 0, GDT_Unknown, nullptr)); + EXPECT_EQ( + GDALFootprint( + /* pszDest = */ nullptr, + /* hDstDS = */ GDALDataset::ToHandle(poOutDS.get()), + /* hSrcDataset = */ GDALDataset::ToHandle(poInDS.get()), + /* psOptionsIn = */ nullptr, + /* pbUsageError = */ nullptr), + nullptr); + } + } +} + +} // namespace diff --git a/autotest/utilities/test_gdal_footprint.py b/autotest/utilities/test_gdal_footprint.py index adb3e33caafd..75661c891056 100755 --- a/autotest/utilities/test_gdal_footprint.py +++ b/autotest/utilities/test_gdal_footprint.py @@ -58,7 +58,7 @@ def test_gdal_footprint_basic(gdal_footprint_path, tmp_path): footprint_json = str(tmp_path / "out_footprint.json") (_, err) = gdaltest.runexternal_out_and_err( - gdal_footprint_path + f" -f GeoJSON ../gcore/data/byte.tif {footprint_json}" + gdal_footprint_path + f" -q -f GeoJSON ../gcore/data/byte.tif {footprint_json}" ) assert err is None or err == "", "got error/warning" diff --git a/autotest/utilities/test_gdal_footprint_lib.py b/autotest/utilities/test_gdal_footprint_lib.py index 84b8760d6e89..de1fd3b89df0 100755 --- a/autotest/utilities/test_gdal_footprint_lib.py +++ b/autotest/utilities/test_gdal_footprint_lib.py @@ -35,7 +35,7 @@ import ogrtest import pytest -from osgeo import gdal +from osgeo import gdal, osr pytestmark = pytest.mark.require_geos @@ -398,7 +398,7 @@ def test_gdal_footprint_lib_dsco_lco(tmp_vsimem): # Test option argument handling -def test_gdaldem_footprint_dict_arguments(): +def test_gdal_footprint_footprint_dict_arguments(): opt = gdal.FootprintOptions( "__RETURN_OPTION_LIST__", @@ -433,7 +433,7 @@ def test_gdaldem_footprint_dict_arguments(): # Test footprint, RGBA and overviews -def test_gdaldem_footprint_rgba_overviews(): +def test_gdal_footprint_footprint_rgba_overviews(): src_ds = gdal.GetDriverByName("MEM").Create("", 6, 6, 4) for i in range(4): @@ -516,21 +516,209 @@ def test_gdal_footprint_lib_intersection_partial(): ############################################################################### -def test_gdal_footprint_lib_intersection_full(): +def test_gdal_footprint_layerName(): - src_ds = gdal.GetDriverByName("MEM").Create("", 3, 1, 2) + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + src_ds.GetRasterBand(1).Fill(255) + out_ds = gdal.GetDriverByName("Memory").Create("", 0, 0, 0, gdal.GDT_Unknown) + + with pytest.raises(Exception, match="Cannot find layer non_existing"): + gdal.Footprint(out_ds, src_ds, layerName="non_existing") + + out_ds.CreateLayer("a") + layer_b = out_ds.CreateLayer("b") + + gdal.Footprint(out_ds, src_ds, layerName="b") + assert layer_b.GetFeatureCount() == 1 + + +############################################################################### +def test_gdal_footprint_wrong_output_format(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + + # Non existing output driver + with pytest.raises(Exception, match="Output driver `non_existing' not recognised"): + gdal.Footprint("", src_ds, format="non_existing") + + # Raster-only output driver + with pytest.raises(Exception, match="Output driver `GTiff' not recognised"): + gdal.Footprint("", src_ds, format="GTiff") + + with pytest.raises( + Exception, match="Cannot guess driver for /vsimem/out.unknown_ext" + ): + gdal.Footprint( + "/vsimem/out.unknown_ext", + src_ds, + ) + + +############################################################################### +def test_gdal_footprint_output_layer_has_crs_but_input_not(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + out_ds = gdal.GetDriverByName("Memory").Create("", 0, 0, 0, gdal.GDT_Unknown) + srs = osr.SpatialReference() + srs.ImportFromEPSG(4326) + out_ds.CreateLayer("out_lyr", srs=srs) + + with pytest.raises( + Exception, match="Output layer has CRS, but input is not georeferenced" + ): + gdal.Footprint(out_ds, src_ds, layerName="out_lyr") + + +############################################################################### +def test_gdal_footprint_wrong_number_nodata_values(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + with pytest.raises( + Exception, + match="Number of values in -srcnodata should be 1 or the number of bands", + ): + gdal.Footprint("", src_ds, format="Memory", srcNodata=[1, 2]) + + +############################################################################### +def test_gdal_footprint_wrong_bands(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + with pytest.raises(Exception, match="Invalid band number: 2"): + gdal.Footprint("", src_ds, format="Memory", bands=[2]) + + +############################################################################### +def test_gdal_footprint_wrong_ovr_on_band_with_nodata(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 2, 2, 1) src_ds.GetRasterBand(1).SetNoDataValue(0) - src_ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, b"\xFF\xFF") - src_ds.GetRasterBand(2).SetNoDataValue(0) - src_ds.GetRasterBand(2).WriteRaster(0, 0, 2, 1, b"\xFF\xFF") + with pytest.raises( + Exception, + match="Overview index 0 invalid for this dataset. Bands of this dataset have no precomputed overviews", + ): + gdal.Footprint( + "", + src_ds, + format="Memory", + ovr=0, + ) + src_ds.BuildOverviews("NEAR", [2]) + with pytest.raises( + Exception, + match=r"Overview index 1 invalid for this dataset. Value should be in \[0,0\] range", + ): + gdal.Footprint( + "", + src_ds, + format="Memory", + ovr=1, + ) + + +############################################################################### +def test_gdal_footprint_wrong_ovr_on_band_with_alpha(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 2, 2, 2) + src_ds.GetRasterBand(2).SetColorInterpretation(gdal.GCI_AlphaBand) + with pytest.raises( + Exception, + match="Overview index 0 invalid for this dataset. Mask bands of this dataset have no precomputed overviews", + ): + gdal.Footprint( + "", + src_ds, + format="Memory", + ovr=0, + ) + src_ds.BuildOverviews("NEAR", [2]) + with pytest.raises( + Exception, + match=r"Overview index 1 invalid for this dataset. Value should be in \[0,0\] range", + ): + gdal.Footprint( + "", + src_ds, + format="Memory", + ovr=1, + ) + + +############################################################################### +# + + +def test_gdal_footprint_lib_targetCoordinateSystem_georef_error(): + + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + with pytest.raises( + Exception, + match="Georeferenced coordinates requested, but input dataset has no geotransform.", + ): + gdal.Footprint( + "", + src_ds, + format="Memory", + targetCoordinateSystem="georef", + ) + + +############################################################################### +# + + +def test_gdal_footprint_lib_minRingArea(): + + # footprint area above minRingArea + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) out_ds = gdal.Footprint( "", src_ds, format="Memory", targetCoordinateSystem="pixel", - combineBands="intersection", + minRingArea="0.5", ) - assert out_ds is not None - lyr = out_ds.GetLayer(0) - f = lyr.GetNextFeature() - ogrtest.check_feature_geometry(f, "MULTIPOLYGON (((0 0,0 1,2 1,2 0,0 0)))") + out_lyr = out_ds.GetLayer(0) + assert out_lyr.GetFeatureCount() == 1 + + # footprint area below minRingArea + src_ds = gdal.GetDriverByName("MEM").Create("", 1, 1, 1) + out_ds = gdal.Footprint( + "", + src_ds, + format="Memory", + targetCoordinateSystem="pixel", + minRingArea="1.5", + ) + out_lyr = out_ds.GetLayer(0) + assert out_lyr.GetFeatureCount() == 0 + + +############################################################################### +# + + +def test_gdal_footprint_lib_destSRS_and_targetCoordinateSystem_pixel_mutually_exclusive(): + + with pytest.raises( + Exception, match="-t_cs pixel and -t_srs are mutually exclusive" + ): + gdal.Footprint( + "", + "../gcore/data/byte.tif", + format="Memory", + dstSRS="EPSG:4267", + targetCoordinateSystem="pixel", + ) + + +############################################################################### +# + + +def test_gdal_footprint_lib_srcNodata_and_ovr_mutually_exclusive(): + + with pytest.raises(Exception, match="-srcnodata and -ovr are mutually exclusive"): + gdal.Footprint( + "", "../gcore/data/byte.tif", format="Memory", srcNodata=0, ovr=0 + )