From 416dd076b1b838d24d222e0e5d288f0dd2de4a1f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 24 Jan 2024 06:02:35 +0100 Subject: [PATCH 001/132] VRT: add a NoDataFromMaskSource source The NoDataFromMaskSource is derived from the SimpleSource and shares the same properties except that it replaces the value of the source with the value of the NODATA child element when the value of the mask band of the source is less or equal to the MaskValueThreshold child element. Also add a -nodata_if_mask_less_or_equal option to gdalbuildvrt Example: ``` $ gdal_translate autotest/gcore/data/stefan_full_rgba.tif in.tif -a_srs EPSG:4326 -a_ullr -180 90 180 -90 $ gdalbuildvrt nodatafrommask.vrt in.tif -nodata_if_mask_less_or_equal 128 -vrtnodata 0 $ cat nodatafrommask.vrt GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]] -1.8000000000000000e+02, 2.2222222222222223e+00, 0.0000000000000000e+00, 9.0000000000000000e+01, 0.0000000000000000e+00, -1.2000000000000000e+00 0 Red in.tif 1 128 0 0 Green in.tif 2 128 0 0 Blue in.tif 3 128 0 ``` --- apps/gdalbuildvrt_bin.cpp | 1 + apps/gdalbuildvrt_lib.cpp | 42 ++- autotest/utilities/test_gdalbuildvrt_lib.py | 59 ++++ doc/source/drivers/raster/vrt.rst | 21 +- doc/source/programs/gdalbuildvrt.rst | 9 + frmts/vrt/data/gdalvrt.xsd | 15 + frmts/vrt/vrtdataset.h | 44 +++ frmts/vrt/vrtdriver.cpp | 1 + frmts/vrt/vrtsources.cpp | 365 ++++++++++++++++++++ swig/include/python/gdal_python.i | 5 + 10 files changed, 556 insertions(+), 6 deletions(-) diff --git a/apps/gdalbuildvrt_bin.cpp b/apps/gdalbuildvrt_bin.cpp index 84feff19088d..d2e9565de05b 100644 --- a/apps/gdalbuildvrt_bin.cpp +++ b/apps/gdalbuildvrt_bin.cpp @@ -56,6 +56,7 @@ static void Usage(bool bIsError, const char *pszErrorMsg) " [-srcnodata \"[ ]...\"] [-vrtnodata " "\"[ ]...\"\n" " [-ignore_srcmaskband]\n" + " [-nodata_if_mask_less_or_equal ]\n" " [-a_srs ]\n" " [-r " "{nearest|bilinear|cubic|cubicspline|lanczos|average|mode}]\n" diff --git a/apps/gdalbuildvrt_lib.cpp b/apps/gdalbuildvrt_lib.cpp index 93d79017ad3f..fe19524451c9 100644 --- a/apps/gdalbuildvrt_lib.cpp +++ b/apps/gdalbuildvrt_lib.cpp @@ -249,6 +249,8 @@ class VRTBuilder char *pszResampling = nullptr; char **papszOpenOptions = nullptr; bool bUseSrcMaskBand = true; + bool bNoDataFromMask = false; + double dfMaskValueThreshold = 0; /* Internal variables */ char *pszProjectionRef = nullptr; @@ -284,6 +286,7 @@ class VRTBuilder int bAllowProjectionDifference, int bAddAlpha, int bHideNoData, int nSubdataset, const char *pszSrcNoData, const char *pszVRTNoData, bool bUseSrcMaskBand, + bool bNoDataFromMask, double dfMaskValueThreshold, const char *pszOutputSRS, const char *pszResampling, const char *const *papszOpenOptionsIn); @@ -305,7 +308,8 @@ VRTBuilder::VRTBuilder( double maxYIn, int bSeparateIn, int bAllowProjectionDifferenceIn, int bAddAlphaIn, int bHideNoDataIn, int nSubdatasetIn, const char *pszSrcNoDataIn, const char *pszVRTNoDataIn, - bool bUseSrcMaskBandIn, const char *pszOutputSRSIn, + bool bUseSrcMaskBandIn, bool bNoDataFromMaskIn, + double dfMaskValueThresholdIn, const char *pszOutputSRSIn, const char *pszResamplingIn, const char *const *papszOpenOptionsIn) : bStrict(bStrictIn) { @@ -365,6 +369,8 @@ VRTBuilder::VRTBuilder( pszOutputSRS = (pszOutputSRSIn) ? CPLStrdup(pszOutputSRSIn) : nullptr; pszResampling = (pszResamplingIn) ? CPLStrdup(pszResamplingIn) : nullptr; bUseSrcMaskBand = bUseSrcMaskBandIn; + bNoDataFromMask = bNoDataFromMaskIn; + dfMaskValueThreshold = dfMaskValueThresholdIn; } /************************************************************************/ @@ -563,11 +569,14 @@ std::string VRTBuilder::AnalyseRaster(GDALDatasetH hDS, double ds_minY = ds_maxY + GDALGetRasterYSize(hDS) * padfGeoTransform[GEOTRSFRM_NS_RES]; - const int _nBands = GDALGetRasterCount(hDS); + int _nBands = GDALGetRasterCount(hDS); if (_nBands == 0) { return "Dataset has no bands"; } + if (bNoDataFromMask && + poDS->GetRasterBand(_nBands)->GetColorInterpretation() == GCI_AlphaBand) + _nBands--; GDALRasterBand *poFirstBand = poDS->GetRasterBand(1); poFirstBand->GetBlockSize(&psDatasetProperties->nBlockXSize, @@ -1298,8 +1307,20 @@ void VRTBuilder::CreateVRTNonSeparate(VRTDatasetH hVRTDS) static_cast(hVRTBand); VRTSimpleSource *poSimpleSource; - if (bAllowSrcNoData && - psDatasetProperties->abHasNoData[nSelBand - 1]) + if (bNoDataFromMask) + { + auto poNoDataFromMaskSource = new VRTNoDataFromMaskSource(); + poSimpleSource = poNoDataFromMaskSource; + poNoDataFromMaskSource->SetParameters( + (nVRTNoDataCount > 0) + ? ((j < nVRTNoDataCount) + ? padfVRTNoData[j] + : padfVRTNoData[nVRTNoDataCount - 1]) + : 0, + dfMaskValueThreshold); + } + else if (bAllowSrcNoData && + psDatasetProperties->abHasNoData[nSelBand - 1]) { auto poComplexSource = new VRTComplexSource(); poSimpleSource = poComplexSource; @@ -1747,6 +1768,8 @@ struct GDALBuildVRTOptions char *pszResampling; char **papszOpenOptions; bool bUseSrcMaskBand; + bool bNoDataFromMask; + double dfMaskValueThreshold; /*! allow or suppress progress monitor and other non-error output */ int bQuiet; @@ -1925,7 +1948,8 @@ GDALDatasetH GDALBuildVRT(const char *pszDest, int nSrcCount, psOptions->bSeparate, psOptions->bAllowProjectionDifference, psOptions->bAddAlpha, psOptions->bHideNoData, psOptions->nSubdataset, psOptions->pszSrcNoData, psOptions->pszVRTNoData, - psOptions->bUseSrcMaskBand, psOptions->pszOutputSRS, + psOptions->bUseSrcMaskBand, psOptions->bNoDataFromMask, + psOptions->dfMaskValueThreshold, psOptions->pszOutputSRS, psOptions->pszResampling, psOptions->papszOpenOptions); GDALDatasetH hDstDS = static_cast( @@ -1996,6 +2020,8 @@ GDALBuildVRTOptionsNew(char **papszArgv, psOptions->pfnProgress = GDALDummyProgress; psOptions->pProgressData = nullptr; psOptions->bUseSrcMaskBand = true; + psOptions->bNoDataFromMask = false; + psOptions->dfMaskValueThreshold = 0; psOptions->bStrict = false; /* -------------------------------------------------------------------- */ @@ -2182,6 +2208,12 @@ GDALBuildVRTOptionsNew(char **papszArgv, { psOptions->bUseSrcMaskBand = false; } + else if (EQUAL(papszArgv[iArg], "-nodata_if_mask_less_or_equal") && + iArg + 1 < argc) + { + psOptions->bNoDataFromMask = true; + psOptions->dfMaskValueThreshold = CPLAtofM(papszArgv[++iArg]); + } else if (papszArgv[iArg][0] == '-') { CPLError(CE_Failure, CPLE_NotSupported, "Unknown option name '%s'", diff --git a/autotest/utilities/test_gdalbuildvrt_lib.py b/autotest/utilities/test_gdalbuildvrt_lib.py index c49c816f67fc..def6e808b61b 100755 --- a/autotest/utilities/test_gdalbuildvrt_lib.py +++ b/autotest/utilities/test_gdalbuildvrt_lib.py @@ -748,3 +748,62 @@ def test_gdalbuildvrt_lib_stable_average(): vrt_gt = vrt_ds.GetGeoTransform() assert vrt_gt == gt + + +############################################################################### + + +def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): + + ds = gdal.GetDriverByName("MEM").Create("", 2, 1, 4) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).Fill(1) + ds.GetRasterBand(2).Fill(2) + ds.GetRasterBand(3).Fill(3) + ds.GetRasterBand(4).WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.GetRasterBand(4).SetColorInterpretation(gdal.GCI_AlphaBand) + + vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128, VRTNodata=0) + assert vrt_ds.RasterCount == 3 + assert vrt_ds.GetRasterBand(1).GetNoDataValue() == 0 + assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\x00\x01" + assert vrt_ds.GetRasterBand(2).GetNoDataValue() == 0 + assert vrt_ds.GetRasterBand(2).ReadRaster() == b"\x00\x02" + assert vrt_ds.GetRasterBand(3).GetNoDataValue() == 0 + assert vrt_ds.GetRasterBand(3).ReadRaster() == b"\x00\x03" + + assert struct.unpack( + "h" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Int16) + ) == (0, 1) + + vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128.5, VRTNodata=0) + assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\x00\x01" + + +############################################################################### + + +def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): + + src_filename = str(tmp_vsimem / "src.tif") + ds = gdal.GetDriverByName("GTiff").Create(src_filename, 2, 1, 3, gdal.GDT_UInt16) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).Fill(1) + ds.GetRasterBand(2).Fill(2) + ds.GetRasterBand(3).Fill(3) + ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) + ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.Close() + + vrt_filename = str(tmp_vsimem / "test.vrt") + gdal.BuildVRT( + vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=0 + ) + vrt_ds = gdal.Open(vrt_filename) + assert struct.unpack( + "H" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_UInt16) + ) == (0, 1) + + assert struct.unpack( + "B" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Byte) + ) == (0, 1) diff --git a/doc/source/drivers/raster/vrt.rst b/doc/source/drivers/raster/vrt.rst index 8b3333304e81..2f497e7b244b 100644 --- a/doc/source/drivers/raster/vrt.rst +++ b/doc/source/drivers/raster/vrt.rst @@ -185,7 +185,7 @@ The attributes for VRTRasterBand are: - **blockYSize** (optional, GDAL >= 3.3): block height. If not specified, defaults to the minimum of the raster height and 128. -This element may have Metadata, ColorInterp, NoDataValue, HideNoDataValue, ColorTable, GDALRasterAttributeTable, Description and MaskBand subelements as well as the various kinds of source elements such as SimpleSource, ComplexSource, AveragedSource, KernelFilteredSource and ArraySource. A raster band may have many "sources" indicating where the actual raster data should be fetched from, and how it should be mapped into the raster bands pixel space. +This element may have Metadata, ColorInterp, NoDataValue, HideNoDataValue, ColorTable, GDALRasterAttributeTable, Description and MaskBand subelements as well as the various kinds of source elements such as SimpleSource, ComplexSource, AveragedSource, NoDataFromMaskSource, KernelFilteredSource and ArraySource. A raster band may have many "sources" indicating where the actual raster data should be fetched from, and how it should be mapped into the raster bands pixel space. The allowed subelements for VRTRasterBand are : @@ -304,6 +304,8 @@ The allowed subelements for VRTRasterBand are : - **AveragedSource**: The AveragedSource is derived from the SimpleSource and shares the same properties except that it uses an averaging resampling instead of a nearest neighbour algorithm as in SimpleSource, when the size of the destination rectangle is not the same as the size of the source rectangle. Note: a more general mechanism to specify resampling algorithms can be used. See above paragraph about the 'resampling' attribute. +- **NoDataFromMaskSource**: (GDAL >= 3.9) The NoDataFromMaskSource is derived from the SimpleSource and shares the same properties except that it replaces the value of the source with the value of the NODATA child element when the value of the mask band of the source is less or equal to the MaskValueThreshold child element. + - **ComplexSource**: The ComplexSource_ is derived from the SimpleSource (so it shares the SourceFilename, SourceBand, SrcRect and DstRect elements), but it provides support to rescale and offset the range of the source values. Certain regions of the source can be masked by specifying the NODATA value, or starting with GDAL 3.3, with the true element. - **KernelFilteredSource**: The KernelFilteredSource_ is a pixel source derived from the Simple Source (so it shares the SourceFilename, SourceBand, SrcRect and DstRect elements, but it also passes the data through a simple filtering kernel specified with the Kernel element. @@ -498,6 +500,23 @@ For example, a Gaussian blur: +NoDataFromMaskSource +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.9 + +The NoDataFromMaskSource is derived from the SimpleSource and shares the same properties except that it replaces the value of the source with the value of the NODATA child element when the value of the mask band of the source is less or equal to the MaskValueThreshold child element. + +.. code-block:: xml + + + in.tif + 1 + 128 + 0 + + + ArraySource ~~~~~~~~~~~ diff --git a/doc/source/programs/gdalbuildvrt.rst b/doc/source/programs/gdalbuildvrt.rst index 253e1bcc462a..644030ba37c4 100644 --- a/doc/source/programs/gdalbuildvrt.rst +++ b/doc/source/programs/gdalbuildvrt.rst @@ -24,6 +24,7 @@ Synopsis [-addalpha] [-hidenodata] [-srcnodata "[ ]..."] [-vrtnodata "[ ]..." [-ignore_srcmaskband] + [-nodata_if_mask_less_or_equal ] [-a_srs ] [-r {nearest|bilinear|cubic|cubicspline|lanczos|average|mode}] [-oo =]... @@ -146,6 +147,14 @@ changed in later versions. not be taken into account, and in case of overlapping between sources, the last one will override previous ones in areas of overlap. +.. option:: -nodata_if_mask_less_or_equal + + .. versionadded:: 3.9 + + Insert a source, which replaces the value of the source + with the value of :option:`-vrtnodata` (or 0 if not specified) when the value + of the mask band of the source is less or equal to the threshold. + .. option:: -b Select an input to be processed. Bands are numbered from 1. diff --git a/frmts/vrt/data/gdalvrt.xsd b/frmts/vrt/data/gdalvrt.xsd index da0296e0fa7e..0f60f6ddf28d 100644 --- a/frmts/vrt/data/gdalvrt.xsd +++ b/frmts/vrt/data/gdalvrt.xsd @@ -187,6 +187,7 @@ + @@ -406,6 +407,20 @@ + + + + + + + + + + + + + + diff --git a/frmts/vrt/vrtdataset.h b/frmts/vrt/vrtdataset.h index 59dc1ecf91af..b741c0fc699e 100644 --- a/frmts/vrt/vrtdataset.h +++ b/frmts/vrt/vrtdataset.h @@ -124,6 +124,10 @@ class CPL_DLL VRTSource // do nothing /* coverity[uninit_member] */ } + inline operator GByte() const + { + return value; + } }; #ifdef __GNUC__ #pragma GCC diagnostic pop @@ -1189,6 +1193,46 @@ class VRTAveragedSource final : public VRTSimpleSource } }; +/************************************************************************/ +/* VRTNoDataFromMaskSource */ +/************************************************************************/ + +class VRTNoDataFromMaskSource final : public VRTSimpleSource +{ + CPL_DISALLOW_COPY_ASSIGN(VRTNoDataFromMaskSource) + + bool m_bNoDataSet = false; + double m_dfNoDataValue = 0; + double m_dfMaskValueThreshold = 0; + + public: + VRTNoDataFromMaskSource(); + virtual CPLErr RasterIO(GDALDataType eVRTBandDataType, int nXOff, int nYOff, + int nXSize, int nYSize, void *pData, int nBufXSize, + int nBufYSize, GDALDataType eBufType, + GSpacing nPixelSpace, GSpacing nLineSpace, + GDALRasterIOExtraArg *psExtraArgIn, + WorkingState &oWorkingState) override; + + virtual double GetMinimum(int nXSize, int nYSize, int *pbSuccess) override; + virtual double GetMaximum(int nXSize, int nYSize, int *pbSuccess) override; + virtual CPLErr GetHistogram(int nXSize, int nYSize, double dfMin, + double dfMax, int nBuckets, + GUIntBig *panHistogram, int bIncludeOutOfRange, + int bApproxOK, GDALProgressFunc pfnProgress, + void *pProgressData) override; + + void SetParameters(double dfNoDataValue, double dfMaskValueThreshold); + + virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, + std::map &) override; + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; + virtual const char *GetType() override + { + return "VRTNoDataFromMaskSource"; + } +}; + /************************************************************************/ /* VRTComplexSource */ /************************************************************************/ diff --git a/frmts/vrt/vrtdriver.cpp b/frmts/vrt/vrtdriver.cpp index 73a17a1565c1..87068b12246c 100644 --- a/frmts/vrt/vrtdriver.cpp +++ b/frmts/vrt/vrtdriver.cpp @@ -555,6 +555,7 @@ void GDALRegister_VRT() poDriver->AddSourceParser("SimpleSource", VRTParseCoreSources); poDriver->AddSourceParser("ComplexSource", VRTParseCoreSources); poDriver->AddSourceParser("AveragedSource", VRTParseCoreSources); + poDriver->AddSourceParser("NoDataFromMaskSource", VRTParseCoreSources); poDriver->AddSourceParser("KernelFilteredSource", VRTParseFilterSources); poDriver->AddSourceParser("ArraySource", VRTParseArraySource); diff --git a/frmts/vrt/vrtsources.cpp b/frmts/vrt/vrtsources.cpp index 8f899cfb2ca9..bb84cca7433b 100644 --- a/frmts/vrt/vrtsources.cpp +++ b/frmts/vrt/vrtsources.cpp @@ -2000,6 +2000,367 @@ CPLErr VRTAveragedSource::GetHistogram( return CE_Failure; } +/************************************************************************/ +/* ==================================================================== */ +/* VRTNoDataFromMaskSource */ +/* ==================================================================== */ +/************************************************************************/ + +/************************************************************************/ +/* VRTNoDataFromMaskSource() */ +/************************************************************************/ + +VRTNoDataFromMaskSource::VRTNoDataFromMaskSource() +{ +} + +/************************************************************************/ +/* XMLInit() */ +/************************************************************************/ + +CPLErr VRTNoDataFromMaskSource::XMLInit( + CPLXMLNode *psSrc, const char *pszVRTPath, + std::map &oMapSharedSources) + +{ + /* -------------------------------------------------------------------- */ + /* Do base initialization. */ + /* -------------------------------------------------------------------- */ + { + const CPLErr eErr = + VRTSimpleSource::XMLInit(psSrc, pszVRTPath, oMapSharedSources); + if (eErr != CE_None) + return eErr; + } + + if (CPLGetXMLValue(psSrc, "NODATA", nullptr) != nullptr) + { + m_bNoDataSet = true; + m_dfNoDataValue = CPLAtofM(CPLGetXMLValue(psSrc, "NODATA", "0")); + } + + m_dfMaskValueThreshold = + CPLAtofM(CPLGetXMLValue(psSrc, "MaskValueThreshold", "0")); + + return CE_None; +} + +/************************************************************************/ +/* SerializeToXML() */ +/************************************************************************/ + +CPLXMLNode *VRTNoDataFromMaskSource::SerializeToXML(const char *pszVRTPath) + +{ + CPLXMLNode *const psSrc = VRTSimpleSource::SerializeToXML(pszVRTPath); + + if (psSrc == nullptr) + return nullptr; + + CPLFree(psSrc->pszValue); + psSrc->pszValue = CPLStrdup("NoDataFromMaskSource"); + + if (m_bNoDataSet) + { + CPLSetXMLValue(psSrc, "MaskValueThreshold", + CPLSPrintf("%.18g", m_dfMaskValueThreshold)); + + GDALDataType eBandDT = GDT_Unknown; + double dfNoDataValue = m_dfNoDataValue; + const auto kMaxFloat = std::numeric_limits::max(); + if (std::fabs(std::fabs(m_dfNoDataValue) - kMaxFloat) < + 1e-10 * kMaxFloat) + { + auto l_band = GetRasterBand(); + if (l_band) + { + eBandDT = l_band->GetRasterDataType(); + if (eBandDT == GDT_Float32) + { + dfNoDataValue = + GDALAdjustNoDataCloseToFloatMax(m_dfNoDataValue); + } + } + } + CPLSetXMLValue(psSrc, "NODATA", + VRTSerializeNoData(dfNoDataValue, eBandDT, 18).c_str()); + } + + return psSrc; +} + +/************************************************************************/ +/* SetParameters() */ +/************************************************************************/ + +void VRTNoDataFromMaskSource::SetParameters(double dfNoDataValue, + double dfMaskValueThreshold) +{ + m_bNoDataSet = true; + m_dfNoDataValue = dfNoDataValue; + m_dfMaskValueThreshold = dfMaskValueThreshold; +} + +/************************************************************************/ +/* RasterIO() */ +/************************************************************************/ + +CPLErr VRTNoDataFromMaskSource::RasterIO( + GDALDataType eVRTBandDataType, int nXOff, int nYOff, int nXSize, int nYSize, + void *pData, int nBufXSize, int nBufYSize, GDALDataType eBufType, + GSpacing nPixelSpace, GSpacing nLineSpace, + GDALRasterIOExtraArg *psExtraArgIn, WorkingState &oWorkingState) + +{ + if (!m_bNoDataSet) + { + return VRTSimpleSource::RasterIO(eVRTBandDataType, nXOff, nYOff, nXSize, + nYSize, pData, nBufXSize, nBufYSize, + eBufType, nPixelSpace, nLineSpace, + psExtraArgIn, oWorkingState); + } + + GDALRasterIOExtraArg sExtraArg; + INIT_RASTERIO_EXTRA_ARG(sExtraArg); + GDALRasterIOExtraArg *psExtraArg = &sExtraArg; + + double dfXOff = nXOff; + double dfYOff = nYOff; + double dfXSize = nXSize; + double dfYSize = nYSize; + if (psExtraArgIn != nullptr && psExtraArgIn->bFloatingPointWindowValidity) + { + dfXOff = psExtraArgIn->dfXOff; + dfYOff = psExtraArgIn->dfYOff; + dfXSize = psExtraArgIn->dfXSize; + dfYSize = psExtraArgIn->dfYSize; + } + + // The window we will actually request from the source raster band. + double dfReqXOff = 0.0; + double dfReqYOff = 0.0; + double dfReqXSize = 0.0; + double dfReqYSize = 0.0; + int nReqXOff = 0; + int nReqYOff = 0; + int nReqXSize = 0; + int nReqYSize = 0; + + // The window we will actual set _within_ the pData buffer. + int nOutXOff = 0; + int nOutYOff = 0; + int nOutXSize = 0; + int nOutYSize = 0; + + bool bError = false; + if (!GetSrcDstWindow(dfXOff, dfYOff, dfXSize, dfYSize, nBufXSize, nBufYSize, + &dfReqXOff, &dfReqYOff, &dfReqXSize, &dfReqYSize, + &nReqXOff, &nReqYOff, &nReqXSize, &nReqYSize, + &nOutXOff, &nOutYOff, &nOutXSize, &nOutYSize, bError)) + { + return bError ? CE_Failure : CE_None; + } + + auto l_band = GetRasterBand(); + if (!l_band) + return CE_Failure; + + /* -------------------------------------------------------------------- */ + /* Allocate temporary buffer(s). */ + /* -------------------------------------------------------------------- */ + const auto eSrcBandDT = l_band->GetRasterDataType(); + const int nSrcBandDTSize = GDALGetDataTypeSizeBytes(eSrcBandDT); + const auto eSrcMaskBandDT = l_band->GetMaskBand()->GetRasterDataType(); + const int nSrcMaskBandDTSize = GDALGetDataTypeSizeBytes(eSrcMaskBandDT); + const bool bByteOptim = + (eSrcBandDT == GDT_Byte && eBufType == GDT_Byte && + eSrcMaskBandDT == GDT_Byte && m_dfMaskValueThreshold >= 0 && + m_dfMaskValueThreshold <= 255 && + static_cast(m_dfMaskValueThreshold) == m_dfMaskValueThreshold && + m_dfNoDataValue >= 0 && m_dfNoDataValue <= 255 && + static_cast(m_dfNoDataValue) == m_dfNoDataValue); + GByte *abyWrkBuffer; + try + { + if (bByteOptim && nOutXOff == 0 && nOutYOff == 0 && + nOutXSize == nBufXSize && nOutYSize == nBufYSize && + eSrcBandDT == eBufType && nPixelSpace == nSrcBandDTSize && + nLineSpace == nPixelSpace * nBufXSize) + { + abyWrkBuffer = static_cast(pData); + } + else + { + oWorkingState.m_abyWrkBuffer.resize(static_cast(nOutXSize) * + nOutYSize * nSrcBandDTSize); + abyWrkBuffer = &oWorkingState.m_abyWrkBuffer[0].value; + } + oWorkingState.m_abyWrkBufferMask.resize(static_cast(nOutXSize) * + nOutYSize * nSrcMaskBandDTSize); + } + catch (const std::exception &) + { + CPLError(CE_Failure, CPLE_OutOfMemory, + "Out of memory when allocating buffers"); + return CE_Failure; + } + + /* -------------------------------------------------------------------- */ + /* Load data. */ + /* -------------------------------------------------------------------- */ + if (!m_osResampling.empty()) + { + psExtraArg->eResampleAlg = GDALRasterIOGetResampleAlg(m_osResampling); + } + else if (psExtraArgIn != nullptr) + { + psExtraArg->eResampleAlg = psExtraArgIn->eResampleAlg; + } + + psExtraArg->bFloatingPointWindowValidity = TRUE; + psExtraArg->dfXOff = dfReqXOff; + psExtraArg->dfYOff = dfReqYOff; + psExtraArg->dfXSize = dfReqXSize; + psExtraArg->dfYSize = dfReqYSize; + + if (l_band->RasterIO(GF_Read, nReqXOff, nReqYOff, nReqXSize, nReqYSize, + abyWrkBuffer, nOutXSize, nOutYSize, eSrcBandDT, 0, 0, + psExtraArg) != CE_None) + { + return CE_Failure; + } + + if (l_band->GetMaskBand()->RasterIO( + GF_Read, nReqXOff, nReqYOff, nReqXSize, nReqYSize, + oWorkingState.m_abyWrkBufferMask.data(), nOutXSize, nOutYSize, + eSrcMaskBandDT, 0, 0, psExtraArg) != CE_None) + { + return CE_Failure; + } + + /* -------------------------------------------------------------------- */ + /* Do the processing. */ + /* -------------------------------------------------------------------- */ + + GByte *const pabyOut = static_cast(pData) + + nPixelSpace * nOutXOff + + static_cast(nLineSpace) * nOutYOff; + if (bByteOptim) + { + // Special case when everything fits on Byte + const GByte nMaskValueThreshold = + static_cast(m_dfMaskValueThreshold); + const GByte nNoDataValue = static_cast(m_dfNoDataValue); + size_t nSrcIdx = 0; + for (int iY = 0; iY < nOutYSize; iY++) + { + GSpacing nDstOffset = iY * nLineSpace; + for (int iX = 0; iX < nOutXSize; iX++) + { + const GByte nMaskVal = + oWorkingState.m_abyWrkBufferMask[nSrcIdx]; + if (nMaskVal <= nMaskValueThreshold) + { + pabyOut[static_cast(nDstOffset)] = nNoDataValue; + } + else + { + pabyOut[static_cast(nDstOffset)] = + abyWrkBuffer[nSrcIdx]; + } + nDstOffset += nPixelSpace; + nSrcIdx++; + } + } + } + else + { + size_t nSrcIdx = 0; + double dfMaskVal = 0; + const int nBufDTSize = GDALGetDataTypeSizeBytes(eBufType); + std::vector abyDstNoData(nBufDTSize); + GDALCopyWords(&m_dfNoDataValue, GDT_Float64, 0, abyDstNoData.data(), + eBufType, 0, 1); + for (int iY = 0; iY < nOutYSize; iY++) + { + GSpacing nDstOffset = iY * nLineSpace; + for (int iX = 0; iX < nOutXSize; iX++) + { + if (eSrcMaskBandDT == GDT_Byte) + { + dfMaskVal = oWorkingState.m_abyWrkBufferMask[nSrcIdx]; + } + else + { + GDALCopyWords(oWorkingState.m_abyWrkBufferMask.data() + + nSrcIdx * nSrcMaskBandDTSize, + eSrcMaskBandDT, 0, &dfMaskVal, GDT_Float64, 0, + 1); + } + void *const pDst = + pabyOut + static_cast(nDstOffset); + if (!(dfMaskVal > m_dfMaskValueThreshold)) + { + memcpy(pDst, abyDstNoData.data(), nBufDTSize); + } + else + { + const void *const pSrc = + abyWrkBuffer + nSrcIdx * nSrcBandDTSize; + if (eSrcBandDT == eBufType) + { + memcpy(pDst, pSrc, nBufDTSize); + } + else + { + GDALCopyWords(pSrc, eSrcBandDT, 0, pDst, eBufType, 0, + 1); + } + } + nDstOffset += nPixelSpace; + nSrcIdx++; + } + } + } + + return CE_None; +} + +/************************************************************************/ +/* GetMinimum() */ +/************************************************************************/ + +double VRTNoDataFromMaskSource::GetMinimum(int /* nXSize */, int /* nYSize */, + int *pbSuccess) +{ + *pbSuccess = FALSE; + return 0.0; +} + +/************************************************************************/ +/* GetMaximum() */ +/************************************************************************/ + +double VRTNoDataFromMaskSource::GetMaximum(int /* nXSize */, int /* nYSize */, + int *pbSuccess) +{ + *pbSuccess = FALSE; + return 0.0; +} + +/************************************************************************/ +/* GetHistogram() */ +/************************************************************************/ + +CPLErr VRTNoDataFromMaskSource::GetHistogram( + int /* nXSize */, int /* nYSize */, double /* dfMin */, double /* dfMax */, + int /* nBuckets */, GUIntBig * /* panHistogram */, + int /* bIncludeOutOfRange */, int /* bApproxOK */, + GDALProgressFunc /* pfnProgress */, void * /* pProgressData */) +{ + return CE_Failure; +} + /************************************************************************/ /* ==================================================================== */ /* VRTComplexSource */ @@ -3220,6 +3581,10 @@ VRTParseCoreSources(CPLXMLNode *psChild, const char *pszVRTPath, { poSource = new VRTComplexSource(); } + else if (EQUAL(psChild->pszValue, "NoDataFromMaskSource")) + { + poSource = new VRTNoDataFromMaskSource(); + } else { CPLError(CE_Failure, CPLE_AppDefined, diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index f50e54f50ad7..077f8fc20123 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -3665,6 +3665,7 @@ def BuildVRTOptions(options=None, srcNodata=None, VRTNodata=None, hideNodata=None, + nodataIfMaskLessOrEqual=None, strict=False, callback=None, callback_data=None): """Create a BuildVRTOptions() object that can be passed to gdal.BuildVRT() @@ -3702,6 +3703,8 @@ def BuildVRTOptions(options=None, nodata values at the VRT band level. hideNodata: whether to make the VRT band not report the NoData value. + nodataIfMaskLessOrEqual: + value of the mask band of a source below which the source band values should be replaced by VRTNodata (or 0 if not specified) strict: set to True if warnings should be failures callback: @@ -3749,6 +3752,8 @@ def BuildVRTOptions(options=None, new_options += ['-srcnodata', str(srcNodata)] if VRTNodata is not None: new_options += ['-vrtnodata', str(VRTNodata)] + if nodataIfMaskLessOrEqual is not None: + new_options += ['-nodata_if_mask_less_or_equal', str(nodataIfMaskLessOrEqual)] if hideNodata: new_options += ['-hidenodata'] if strict: From de4370ec44e18cb5a86071ea221266274643af33 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 24 Jan 2024 17:14:12 +0100 Subject: [PATCH 002/132] NoDataFromMaskSource: add RemappedValue --- autotest/utilities/test_gdalbuildvrt_lib.py | 114 ++++++++++++++++++-- doc/source/drivers/raster/vrt.rst | 4 +- frmts/vrt/data/gdalvrt.xsd | 1 + frmts/vrt/vrtdataset.h | 4 + frmts/vrt/vrtsources.cpp | 92 +++++++++++++++- 5 files changed, 205 insertions(+), 10 deletions(-) diff --git a/autotest/utilities/test_gdalbuildvrt_lib.py b/autotest/utilities/test_gdalbuildvrt_lib.py index def6e808b61b..7107dd3555c1 100755 --- a/autotest/utilities/test_gdalbuildvrt_lib.py +++ b/autotest/utilities/test_gdalbuildvrt_lib.py @@ -757,9 +757,10 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): ds = gdal.GetDriverByName("MEM").Create("", 2, 1, 4) ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) - ds.GetRasterBand(1).Fill(1) - ds.GetRasterBand(2).Fill(2) - ds.GetRasterBand(3).Fill(3) + # Test remapping of second valid pixel at 0 to 1 + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, b"\x01\x00") + ds.GetRasterBand(2).WriteRaster(0, 0, 2, 1, b"\x02\x02") + ds.GetRasterBand(3).WriteRaster(0, 0, 2, 1, b"\x03\x03") ds.GetRasterBand(4).WriteRaster(0, 0, 2, 1, b"\x00\xFF") ds.GetRasterBand(4).SetColorInterpretation(gdal.GCI_AlphaBand) @@ -779,18 +780,29 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128.5, VRTNodata=0) assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\x00\x01" + # VRTNodata=255, test remapping of 255 to 254 + ds = gdal.GetDriverByName("MEM").Create("", 2, 1, 2) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, b"\x01\xFF") + ds.GetRasterBand(2).WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.GetRasterBand(2).SetColorInterpretation(gdal.GCI_AlphaBand) + + vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128, VRTNodata=255) + assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\xFF\xFE" + ############################################################################### def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): + # UInt16, VRTNodata=0 src_filename = str(tmp_vsimem / "src.tif") ds = gdal.GetDriverByName("GTiff").Create(src_filename, 2, 1, 3, gdal.GDT_UInt16) ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) - ds.GetRasterBand(1).Fill(1) - ds.GetRasterBand(2).Fill(2) - ds.GetRasterBand(3).Fill(3) + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, struct.pack("H" * 2, 1, 0)) + ds.GetRasterBand(2).WriteRaster(0, 0, 2, 1, struct.pack("H" * 2, 2, 2)) + ds.GetRasterBand(3).WriteRaster(0, 0, 2, 1, struct.pack("H" * 2, 3, 2)) ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 2, 1, b"\x00\xFF") ds.Close() @@ -807,3 +819,93 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): assert struct.unpack( "B" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Byte) ) == (0, 1) + + # UInt16, VRTNodata=65535 + src_filename = str(tmp_vsimem / "src.tif") + ds = gdal.GetDriverByName("GTiff").Create(src_filename, 2, 1, 1, gdal.GDT_UInt16) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, struct.pack("H" * 2, 1, 65535)) + ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) + ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.Close() + + vrt_filename = str(tmp_vsimem / "test.vrt") + gdal.BuildVRT( + vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=65535 + ) + vrt_ds = gdal.Open(vrt_filename) + assert struct.unpack( + "H" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_UInt16) + ) == (65535, 65534) + + # Int16, VRTNodata=-32768 + src_filename = str(tmp_vsimem / "src.tif") + ds = gdal.GetDriverByName("GTiff").Create(src_filename, 2, 1, 1, gdal.GDT_Int16) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, struct.pack("h" * 2, 1, -32768)) + ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) + ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.Close() + + vrt_filename = str(tmp_vsimem / "test.vrt") + gdal.BuildVRT( + vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=-32768 + ) + vrt_ds = gdal.Open(vrt_filename) + assert struct.unpack( + "h" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Int16) + ) == (-32768, -32767) + + # Int16, VRTNodata=32767 + src_filename = str(tmp_vsimem / "src.tif") + ds = gdal.GetDriverByName("GTiff").Create(src_filename, 2, 1, 1, gdal.GDT_Int16) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, struct.pack("h" * 2, 1, 32767)) + ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) + ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.Close() + + vrt_filename = str(tmp_vsimem / "test.vrt") + gdal.BuildVRT( + vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=32767 + ) + vrt_ds = gdal.Open(vrt_filename) + assert struct.unpack( + "h" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Int16) + ) == (32767, 32766) + + # Float32, VRTNodata=0 + src_filename = str(tmp_vsimem / "src.tif") + ds = gdal.GetDriverByName("GTiff").Create(src_filename, 2, 1, 1, gdal.GDT_Float32) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).WriteRaster(0, 0, 2, 1, struct.pack("f" * 2, 1, 0)) + ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) + ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 2, 1, b"\x00\xFF") + ds.Close() + + vrt_filename = str(tmp_vsimem / "test.vrt") + gdal.BuildVRT( + vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=0 + ) + vrt_ds = gdal.Open(vrt_filename) + assert struct.unpack( + "f" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Float32) + ) == pytest.approx((0.0, 0.001)) + + # Float32, VRTNodata=1 + src_filename = str(tmp_vsimem / "src.tif") + ds = gdal.GetDriverByName("GTiff").Create(src_filename, 3, 1, 1, gdal.GDT_Float32) + ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) + ds.GetRasterBand(1).WriteRaster(0, 0, 3, 1, struct.pack("f" * 3, 0, 1, 2)) + ds.GetRasterBand(1).CreateMaskBand(gdal.GMF_PER_DATASET) + ds.GetRasterBand(1).GetMaskBand().WriteRaster(0, 0, 3, 1, b"\x00\xFF\xFF") + ds.Close() + + vrt_filename = str(tmp_vsimem / "test.vrt") + gdal.BuildVRT( + vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=1 + ) + vrt_ds = gdal.Open(vrt_filename) + assert struct.unpack( + "f" * 3, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Float32) + ) == pytest.approx((1.0, 1.001, 2.0)) diff --git a/doc/source/drivers/raster/vrt.rst b/doc/source/drivers/raster/vrt.rst index 2f497e7b244b..d44bf624981a 100644 --- a/doc/source/drivers/raster/vrt.rst +++ b/doc/source/drivers/raster/vrt.rst @@ -506,14 +506,16 @@ NoDataFromMaskSource .. versionadded:: 3.9 The NoDataFromMaskSource is derived from the SimpleSource and shares the same properties except that it replaces the value of the source with the value of the NODATA child element when the value of the mask band of the source is less or equal to the MaskValueThreshold child element. +An optional RemappedValue element can be set to specify the value onto which valid pixels whose value is the one of NODATA should be remapped to. When RemappedValue is not explicitly specified, for Byte bands, if NODATA=255, it is implicitly set to 254, otherwise it is set to NODATA+1. .. code-block:: xml in.tif 1 - 128 + 128 0 + 1 diff --git a/frmts/vrt/data/gdalvrt.xsd b/frmts/vrt/data/gdalvrt.xsd index 0f60f6ddf28d..cc9c13cba596 100644 --- a/frmts/vrt/data/gdalvrt.xsd +++ b/frmts/vrt/data/gdalvrt.xsd @@ -413,6 +413,7 @@ + diff --git a/frmts/vrt/vrtdataset.h b/frmts/vrt/vrtdataset.h index b741c0fc699e..1fd071d58fc5 100644 --- a/frmts/vrt/vrtdataset.h +++ b/frmts/vrt/vrtdataset.h @@ -1204,6 +1204,8 @@ class VRTNoDataFromMaskSource final : public VRTSimpleSource bool m_bNoDataSet = false; double m_dfNoDataValue = 0; double m_dfMaskValueThreshold = 0; + bool m_bHasRemappedValue = false; + double m_dfRemappedValue = 0; public: VRTNoDataFromMaskSource(); @@ -1223,6 +1225,8 @@ class VRTNoDataFromMaskSource final : public VRTSimpleSource void *pProgressData) override; void SetParameters(double dfNoDataValue, double dfMaskValueThreshold); + void SetParameters(double dfNoDataValue, double dfMaskValueThreshold, + double dfRemappedValue); virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, std::map &) override; diff --git a/frmts/vrt/vrtsources.cpp b/frmts/vrt/vrtsources.cpp index bb84cca7433b..a565d41fa7c9 100644 --- a/frmts/vrt/vrtsources.cpp +++ b/frmts/vrt/vrtsources.cpp @@ -2042,6 +2042,13 @@ CPLErr VRTNoDataFromMaskSource::XMLInit( m_dfMaskValueThreshold = CPLAtofM(CPLGetXMLValue(psSrc, "MaskValueThreshold", "0")); + if (const char *pszRemappedValue = + CPLGetXMLValue(psSrc, "RemappedValue", nullptr)) + { + m_bHasRemappedValue = true; + m_dfRemappedValue = CPLAtofM(pszRemappedValue); + } + return CE_None; } @@ -2086,6 +2093,12 @@ CPLXMLNode *VRTNoDataFromMaskSource::SerializeToXML(const char *pszVRTPath) VRTSerializeNoData(dfNoDataValue, eBandDT, 18).c_str()); } + if (m_bHasRemappedValue) + { + CPLSetXMLValue(psSrc, "RemappedValue", + CPLSPrintf("%.18g", m_dfRemappedValue)); + } + return psSrc; } @@ -2099,6 +2112,21 @@ void VRTNoDataFromMaskSource::SetParameters(double dfNoDataValue, m_bNoDataSet = true; m_dfNoDataValue = dfNoDataValue; m_dfMaskValueThreshold = dfMaskValueThreshold; + if (!m_bHasRemappedValue) + m_dfRemappedValue = m_dfNoDataValue; +} + +/************************************************************************/ +/* SetParameters() */ +/************************************************************************/ + +void VRTNoDataFromMaskSource::SetParameters(double dfNoDataValue, + double dfMaskValueThreshold, + double dfRemappedValue) +{ + SetParameters(dfNoDataValue, dfMaskValueThreshold); + m_bHasRemappedValue = true; + m_dfRemappedValue = dfRemappedValue; } /************************************************************************/ @@ -2172,13 +2200,57 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( const int nSrcBandDTSize = GDALGetDataTypeSizeBytes(eSrcBandDT); const auto eSrcMaskBandDT = l_band->GetMaskBand()->GetRasterDataType(); const int nSrcMaskBandDTSize = GDALGetDataTypeSizeBytes(eSrcMaskBandDT); + double dfRemappedValue = m_dfRemappedValue; + if (!m_bHasRemappedValue) + { + if (eSrcBandDT == GDT_Byte && + m_dfNoDataValue >= std::numeric_limits::min() && + m_dfNoDataValue <= std::numeric_limits::max() && + static_cast(m_dfNoDataValue) == m_dfNoDataValue) + { + if (m_dfNoDataValue == std::numeric_limits::max()) + dfRemappedValue = m_dfNoDataValue - 1; + else + dfRemappedValue = m_dfNoDataValue + 1; + } + else if (eSrcBandDT == GDT_UInt16 && + m_dfNoDataValue >= std::numeric_limits::min() && + m_dfNoDataValue <= std::numeric_limits::max() && + static_cast(m_dfNoDataValue) == m_dfNoDataValue) + { + if (m_dfNoDataValue == std::numeric_limits::max()) + dfRemappedValue = m_dfNoDataValue - 1; + else + dfRemappedValue = m_dfNoDataValue + 1; + } + else if (eSrcBandDT == GDT_Int16 && + m_dfNoDataValue >= std::numeric_limits::min() && + m_dfNoDataValue <= std::numeric_limits::max() && + static_cast(m_dfNoDataValue) == m_dfNoDataValue) + { + if (m_dfNoDataValue == std::numeric_limits::max()) + dfRemappedValue = m_dfNoDataValue - 1; + else + dfRemappedValue = m_dfNoDataValue + 1; + } + else + { + constexpr double EPS = 1e-3; + if (m_dfNoDataValue == 0) + dfRemappedValue = EPS; + else + dfRemappedValue = m_dfNoDataValue * (1 + EPS); + } + } const bool bByteOptim = (eSrcBandDT == GDT_Byte && eBufType == GDT_Byte && eSrcMaskBandDT == GDT_Byte && m_dfMaskValueThreshold >= 0 && m_dfMaskValueThreshold <= 255 && static_cast(m_dfMaskValueThreshold) == m_dfMaskValueThreshold && m_dfNoDataValue >= 0 && m_dfNoDataValue <= 255 && - static_cast(m_dfNoDataValue) == m_dfNoDataValue); + static_cast(m_dfNoDataValue) == m_dfNoDataValue && + dfRemappedValue >= 0 && dfRemappedValue <= 255 && + static_cast(dfRemappedValue) == dfRemappedValue); GByte *abyWrkBuffer; try { @@ -2251,6 +2323,7 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( const GByte nMaskValueThreshold = static_cast(m_dfMaskValueThreshold); const GByte nNoDataValue = static_cast(m_dfNoDataValue); + const GByte nRemappedValue = static_cast(dfRemappedValue); size_t nSrcIdx = 0; for (int iY = 0; iY < nOutYSize; iY++) { @@ -2265,8 +2338,16 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( } else { - pabyOut[static_cast(nDstOffset)] = - abyWrkBuffer[nSrcIdx]; + if (abyWrkBuffer[nSrcIdx] == nNoDataValue) + { + pabyOut[static_cast(nDstOffset)] = + nRemappedValue; + } + else + { + pabyOut[static_cast(nDstOffset)] = + abyWrkBuffer[nSrcIdx]; + } } nDstOffset += nPixelSpace; nSrcIdx++; @@ -2281,6 +2362,9 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( std::vector abyDstNoData(nBufDTSize); GDALCopyWords(&m_dfNoDataValue, GDT_Float64, 0, abyDstNoData.data(), eBufType, 0, 1); + std::vector abyRemappedValue(nBufDTSize); + GDALCopyWords(&dfRemappedValue, GDT_Float64, 0, abyRemappedValue.data(), + eBufType, 0, 1); for (int iY = 0; iY < nOutYSize; iY++) { GSpacing nDstOffset = iY * nLineSpace; @@ -2316,6 +2400,8 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( GDALCopyWords(pSrc, eSrcBandDT, 0, pDst, eBufType, 0, 1); } + if (memcmp(pDst, abyDstNoData.data(), nBufDTSize) == 0) + memcpy(pDst, abyRemappedValue.data(), nBufDTSize); } nDstOffset += nPixelSpace; nSrcIdx++; From e2eaaf924031699a1a1e9ba3f2358572858d598a Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 27 Jan 2024 00:35:59 +0100 Subject: [PATCH 003/132] NoDataFromMaskSource: rename option to -nodata_max_mask_threshold --- apps/gdalbuildvrt_bin.cpp | 2 +- apps/gdalbuildvrt_lib.cpp | 2 +- autotest/utilities/test_gdalbuildvrt_lib.py | 28 ++++++++------------- doc/source/programs/gdalbuildvrt.rst | 4 +-- swig/include/python/gdal_python.i | 8 +++--- 5 files changed, 19 insertions(+), 25 deletions(-) diff --git a/apps/gdalbuildvrt_bin.cpp b/apps/gdalbuildvrt_bin.cpp index d2e9565de05b..0437ca0f3335 100644 --- a/apps/gdalbuildvrt_bin.cpp +++ b/apps/gdalbuildvrt_bin.cpp @@ -56,7 +56,7 @@ static void Usage(bool bIsError, const char *pszErrorMsg) " [-srcnodata \"[ ]...\"] [-vrtnodata " "\"[ ]...\"\n" " [-ignore_srcmaskband]\n" - " [-nodata_if_mask_less_or_equal ]\n" + " [-nodata_max_mask_threshold ]\n" " [-a_srs ]\n" " [-r " "{nearest|bilinear|cubic|cubicspline|lanczos|average|mode}]\n" diff --git a/apps/gdalbuildvrt_lib.cpp b/apps/gdalbuildvrt_lib.cpp index fe19524451c9..45c6e68ed5c4 100644 --- a/apps/gdalbuildvrt_lib.cpp +++ b/apps/gdalbuildvrt_lib.cpp @@ -2208,7 +2208,7 @@ GDALBuildVRTOptionsNew(char **papszArgv, { psOptions->bUseSrcMaskBand = false; } - else if (EQUAL(papszArgv[iArg], "-nodata_if_mask_less_or_equal") && + else if (EQUAL(papszArgv[iArg], "-nodata_max_mask_threshold") && iArg + 1 < argc) { psOptions->bNoDataFromMask = true; diff --git a/autotest/utilities/test_gdalbuildvrt_lib.py b/autotest/utilities/test_gdalbuildvrt_lib.py index 7107dd3555c1..94f449d1e282 100755 --- a/autotest/utilities/test_gdalbuildvrt_lib.py +++ b/autotest/utilities/test_gdalbuildvrt_lib.py @@ -753,7 +753,7 @@ def test_gdalbuildvrt_lib_stable_average(): ############################################################################### -def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): +def test_gdalbuildvrt_lib_nodataMaxMaskThreshold_rgba(tmp_vsimem): ds = gdal.GetDriverByName("MEM").Create("", 2, 1, 4) ds.SetGeoTransform([0, 1, 0, 0, 0, -1]) @@ -764,7 +764,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): ds.GetRasterBand(4).WriteRaster(0, 0, 2, 1, b"\x00\xFF") ds.GetRasterBand(4).SetColorInterpretation(gdal.GCI_AlphaBand) - vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128, VRTNodata=0) + vrt_ds = gdal.BuildVRT("", [ds], nodataMaxMaskThreshold=128, VRTNodata=0) assert vrt_ds.RasterCount == 3 assert vrt_ds.GetRasterBand(1).GetNoDataValue() == 0 assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\x00\x01" @@ -777,7 +777,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): "h" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Int16) ) == (0, 1) - vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128.5, VRTNodata=0) + vrt_ds = gdal.BuildVRT("", [ds], nodataMaxMaskThreshold=128.5, VRTNodata=0) assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\x00\x01" # VRTNodata=255, test remapping of 255 to 254 @@ -787,14 +787,14 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgba(tmp_vsimem): ds.GetRasterBand(2).WriteRaster(0, 0, 2, 1, b"\x00\xFF") ds.GetRasterBand(2).SetColorInterpretation(gdal.GCI_AlphaBand) - vrt_ds = gdal.BuildVRT("", [ds], nodataIfMaskLessOrEqual=128, VRTNodata=255) + vrt_ds = gdal.BuildVRT("", [ds], nodataMaxMaskThreshold=128, VRTNodata=255) assert vrt_ds.GetRasterBand(1).ReadRaster() == b"\xFF\xFE" ############################################################################### -def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): +def test_gdalbuildvrt_lib_nodataMaxMaskThreshold_rgb_mask(tmp_vsimem): # UInt16, VRTNodata=0 src_filename = str(tmp_vsimem / "src.tif") @@ -808,9 +808,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): ds.Close() vrt_filename = str(tmp_vsimem / "test.vrt") - gdal.BuildVRT( - vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=0 - ) + gdal.BuildVRT(vrt_filename, [src_filename], nodataMaxMaskThreshold=128, VRTNodata=0) vrt_ds = gdal.Open(vrt_filename) assert struct.unpack( "H" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_UInt16) @@ -831,7 +829,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): vrt_filename = str(tmp_vsimem / "test.vrt") gdal.BuildVRT( - vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=65535 + vrt_filename, [src_filename], nodataMaxMaskThreshold=128, VRTNodata=65535 ) vrt_ds = gdal.Open(vrt_filename) assert struct.unpack( @@ -849,7 +847,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): vrt_filename = str(tmp_vsimem / "test.vrt") gdal.BuildVRT( - vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=-32768 + vrt_filename, [src_filename], nodataMaxMaskThreshold=128, VRTNodata=-32768 ) vrt_ds = gdal.Open(vrt_filename) assert struct.unpack( @@ -867,7 +865,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): vrt_filename = str(tmp_vsimem / "test.vrt") gdal.BuildVRT( - vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=32767 + vrt_filename, [src_filename], nodataMaxMaskThreshold=128, VRTNodata=32767 ) vrt_ds = gdal.Open(vrt_filename) assert struct.unpack( @@ -884,9 +882,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): ds.Close() vrt_filename = str(tmp_vsimem / "test.vrt") - gdal.BuildVRT( - vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=0 - ) + gdal.BuildVRT(vrt_filename, [src_filename], nodataMaxMaskThreshold=128, VRTNodata=0) vrt_ds = gdal.Open(vrt_filename) assert struct.unpack( "f" * 2, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Float32) @@ -902,9 +898,7 @@ def test_gdalbuildvrt_lib_nodataIfMaskLessOrEqual_rgb_mask(tmp_vsimem): ds.Close() vrt_filename = str(tmp_vsimem / "test.vrt") - gdal.BuildVRT( - vrt_filename, [src_filename], nodataIfMaskLessOrEqual=128, VRTNodata=1 - ) + gdal.BuildVRT(vrt_filename, [src_filename], nodataMaxMaskThreshold=128, VRTNodata=1) vrt_ds = gdal.Open(vrt_filename) assert struct.unpack( "f" * 3, vrt_ds.GetRasterBand(1).ReadRaster(buf_type=gdal.GDT_Float32) diff --git a/doc/source/programs/gdalbuildvrt.rst b/doc/source/programs/gdalbuildvrt.rst index 644030ba37c4..f3bf3eec0a11 100644 --- a/doc/source/programs/gdalbuildvrt.rst +++ b/doc/source/programs/gdalbuildvrt.rst @@ -24,7 +24,7 @@ Synopsis [-addalpha] [-hidenodata] [-srcnodata "[ ]..."] [-vrtnodata "[ ]..." [-ignore_srcmaskband] - [-nodata_if_mask_less_or_equal ] + [-nodata_max_mask_threshold ] [-a_srs ] [-r {nearest|bilinear|cubic|cubicspline|lanczos|average|mode}] [-oo =]... @@ -147,7 +147,7 @@ changed in later versions. not be taken into account, and in case of overlapping between sources, the last one will override previous ones in areas of overlap. -.. option:: -nodata_if_mask_less_or_equal +.. option:: -nodata_max_mask_threshold .. versionadded:: 3.9 diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index 077f8fc20123..97b83484c73e 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -3665,7 +3665,7 @@ def BuildVRTOptions(options=None, srcNodata=None, VRTNodata=None, hideNodata=None, - nodataIfMaskLessOrEqual=None, + nodataMaxMaskThreshold=None, strict=False, callback=None, callback_data=None): """Create a BuildVRTOptions() object that can be passed to gdal.BuildVRT() @@ -3703,7 +3703,7 @@ def BuildVRTOptions(options=None, nodata values at the VRT band level. hideNodata: whether to make the VRT band not report the NoData value. - nodataIfMaskLessOrEqual: + nodataMaxMaskThreshold: value of the mask band of a source below which the source band values should be replaced by VRTNodata (or 0 if not specified) strict: set to True if warnings should be failures @@ -3752,8 +3752,8 @@ def BuildVRTOptions(options=None, new_options += ['-srcnodata', str(srcNodata)] if VRTNodata is not None: new_options += ['-vrtnodata', str(VRTNodata)] - if nodataIfMaskLessOrEqual is not None: - new_options += ['-nodata_if_mask_less_or_equal', str(nodataIfMaskLessOrEqual)] + if nodataMaxMaskThreshold is not None: + new_options += ['-nodata_max_mask_threshold', str(nodataMaxMaskThreshold)] if hideNodata: new_options += ['-hidenodata'] if strict: From 5fab647d13268b6e10ac539e7dd68d90a98da634 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 31 Jan 2024 23:18:03 +0100 Subject: [PATCH 004/132] Shape: make it recognize /vsizip/foo.shp.zip directories Relates to https://github.com/conda-forge/tiledb-feedstock/issues/228 --- ogr/ogrsf_frmts/shape/ogrshapedriver.cpp | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/ogr/ogrsf_frmts/shape/ogrshapedriver.cpp b/ogr/ogrsf_frmts/shape/ogrshapedriver.cpp index d7833892fde3..69d671b29750 100644 --- a/ogr/ogrsf_frmts/shape/ogrshapedriver.cpp +++ b/ogr/ogrsf_frmts/shape/ogrshapedriver.cpp @@ -50,19 +50,28 @@ static int OGRShapeDriverIdentify(GDALOpenInfo *poOpenInfo) if (!poOpenInfo->bStatOK) return FALSE; if (poOpenInfo->bIsDirectory) - return -1; // Unsure. + { + if (STARTS_WITH(poOpenInfo->pszFilename, "/vsizip/") && + (strstr(poOpenInfo->pszFilename, ".shp.zip") || + strstr(poOpenInfo->pszFilename, ".SHP.ZIP"))) + { + return TRUE; + } + + return GDAL_IDENTIFY_UNKNOWN; // Unsure. + } if (poOpenInfo->fpL == nullptr) { return FALSE; } - CPLString osExt(CPLGetExtension(poOpenInfo->pszFilename)); - if (EQUAL(osExt, "SHP") || EQUAL(osExt, "SHX")) + const std::string osExt(CPLGetExtension(poOpenInfo->pszFilename)); + if (EQUAL(osExt.c_str(), "SHP") || EQUAL(osExt.c_str(), "SHX")) { return poOpenInfo->nHeaderBytes >= 4 && (memcmp(poOpenInfo->pabyHeader, "\x00\x00\x27\x0A", 4) == 0 || memcmp(poOpenInfo->pabyHeader, "\x00\x00\x27\x0D", 4) == 0); } - if (EQUAL(osExt, "DBF")) + if (EQUAL(osExt.c_str(), "DBF")) { if (poOpenInfo->nHeaderBytes < 32) return FALSE; @@ -82,8 +91,8 @@ static int OGRShapeDriverIdentify(GDALOpenInfo *poOpenInfo) return FALSE; return TRUE; } - if (EQUAL(osExt, "shz") || - (EQUAL(osExt, "zip") && + if (EQUAL(osExt.c_str(), "shz") || + (EQUAL(osExt.c_str(), "zip") && (CPLString(poOpenInfo->pszFilename).endsWith(".shp.zip") || CPLString(poOpenInfo->pszFilename).endsWith(".SHP.ZIP")))) { @@ -95,7 +104,7 @@ static int OGRShapeDriverIdentify(GDALOpenInfo *poOpenInfo) if (!STARTS_WITH(poOpenInfo->pszFilename, "/vsitar/") && EQUAL(CPLGetFilename(poOpenInfo->pszFilename), ".cur_input")) { - return -1; + return GDAL_IDENTIFY_UNKNOWN; } #endif return FALSE; From dc1e4d1d4c59cd8076ebf542726f2c64beaef0fc Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 31 Jan 2024 23:18:15 +0100 Subject: [PATCH 005/132] TileDB: make its identify() method more restrictive by not identifying /vsi file systems it doesn't handle Relates to https://github.com/conda-forge/tiledb-feedstock/issues/228 --- frmts/tiledb/tiledbcommon.cpp | 12 ++++++++++-- frmts/tiledb/tiledbdrivercore.cpp | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/frmts/tiledb/tiledbcommon.cpp b/frmts/tiledb/tiledbcommon.cpp index 21f51959d324..568fb09d27a7 100644 --- a/frmts/tiledb/tiledbcommon.cpp +++ b/frmts/tiledb/tiledbcommon.cpp @@ -133,9 +133,17 @@ int TileDBDataset::Identify(GDALOpenInfo *poOpenInfo) return TRUE; } + const bool bIsS3OrGS = + STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIS3/") || + STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIGS/"); + // If this is a /vsi virtual file systems, bail out, except if it is S3 or GS. + if (!bIsS3OrGS && STARTS_WITH(poOpenInfo->pszFilename, "/vsi")) + { + return false; + } + if (poOpenInfo->bIsDirectory || - ((STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIS3/") || - STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIGS/")) && + (bIsS3OrGS && !EQUAL(CPLGetExtension(poOpenInfo->pszFilename), "tif"))) { tiledb::Context ctx; diff --git a/frmts/tiledb/tiledbdrivercore.cpp b/frmts/tiledb/tiledbdrivercore.cpp index 607d624b2413..d9ae98c5da1d 100644 --- a/frmts/tiledb/tiledbdrivercore.cpp +++ b/frmts/tiledb/tiledbdrivercore.cpp @@ -51,10 +51,20 @@ static int TileDBDriverIdentifySimplified(GDALOpenInfo *poOpenInfo) return TRUE; } - if (poOpenInfo->bIsDirectory || - ((STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIS3/") || - STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIGS/")) && - !EQUAL(CPLGetExtension(poOpenInfo->pszFilename), "tif"))) + const bool bIsS3OrGS = STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIS3/") || + STARTS_WITH_CI(poOpenInfo->pszFilename, "/VSIGS/"); + // If this is a /vsi virtual file systems, bail out, except if it is S3 or GS. + if (!bIsS3OrGS && STARTS_WITH(poOpenInfo->pszFilename, "/vsi")) + { + return false; + } + + if (poOpenInfo->bIsDirectory) + { + return GDAL_IDENTIFY_UNKNOWN; + } + + if (bIsS3OrGS && !EQUAL(CPLGetExtension(poOpenInfo->pszFilename), "tif")) { return GDAL_IDENTIFY_UNKNOWN; } From e7585d91f73b916c4325e333768efd6a86cd2803 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 1 Feb 2024 13:56:07 +0100 Subject: [PATCH 006/132] CSV: parse header with line breaks (fixes #9172) --- .../ogr/data/csv/header_with_line_break.csv | 6 ++++ autotest/ogr/ogr_csv.py | 31 +++++++++++++++++++ ogr/ogrsf_frmts/csv/ogrcsvlayer.cpp | 12 ++++--- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 autotest/ogr/data/csv/header_with_line_break.csv diff --git a/autotest/ogr/data/csv/header_with_line_break.csv b/autotest/ogr/data/csv/header_with_line_break.csv new file mode 100644 index 000000000000..e56f470e181b --- /dev/null +++ b/autotest/ogr/data/csv/header_with_line_break.csv @@ -0,0 +1,6 @@ +Column one,Column two,"Column with a +line break",Column three,"Another +line break",Column four,Column five +1,2,3,4,5,6,7 +4,5,6,7,8,9,10 +7,8,9,10,11,12,13 diff --git a/autotest/ogr/ogr_csv.py b/autotest/ogr/ogr_csv.py index b8efff32ca28..9008bec734ba 100755 --- a/autotest/ogr/ogr_csv.py +++ b/autotest/ogr/ogr_csv.py @@ -2988,6 +2988,37 @@ def test_ogr_csv_getextent3d(tmp_vsimem): ############################################################################### +def test_ogr_csv_read_header_with_line_break(): + + ds = ogr.Open("data/csv/header_with_line_break.csv") + lyr = ds.GetLayer(0) + lyr_defn = lyr.GetLayerDefn() + assert [ + lyr_defn.GetFieldDefn(i).GetName() for i in range(lyr_defn.GetFieldCount()) + ] == [ + "Column one", + "Column two", + "Column with a\nline break", + "Column three", + "Another\nline break", + "Column four", + "Column five", + ] + f = lyr.GetNextFeature() + assert [f.GetField(i) for i in range(lyr_defn.GetFieldCount())] == [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + ] + + +############################################################################### + + if __name__ == "__main__": gdal.UseExceptions() if len(sys.argv) != 2: diff --git a/ogr/ogrsf_frmts/csv/ogrcsvlayer.cpp b/ogr/ogrsf_frmts/csv/ogrcsvlayer.cpp index d83f2219f318..606da4719cec 100644 --- a/ogr/ogrsf_frmts/csv/ogrcsvlayer.cpp +++ b/ogr/ogrsf_frmts/csv/ogrcsvlayer.cpp @@ -248,11 +248,15 @@ void OGRCSVLayer::BuildFeatureDefn(const char *pszNfdcGeomField, } // Tokenize without quotes to get the actual values. + VSIRewindL(fpCSV); CSLDestroy(papszTokens); - int l_nFlags = CSLT_HONOURSTRINGS; - if (!bMergeDelimiter) - l_nFlags |= CSLT_ALLOWEMPTYTOKENS; - papszTokens = CSLTokenizeString2(pszLine, szDelimiter, l_nFlags); + papszTokens = + CSVReadParseLine3L(fpCSV, m_nMaxLineSize, szDelimiter, + true, // bHonourStrings + false, // bKeepLeadingAndClosingQuotes + bMergeDelimiter, + true // bSkipBOM + ); nFieldCount = CSLCount(papszTokens); } } From 33e24daf3fa529a81061a7cfcb042e9516331cea Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 19:02:13 +0100 Subject: [PATCH 007/132] CI: add build time testing of OCI driver --- .github/workflows/ubuntu_22.04/Dockerfile.ci | 7 +++++++ .github/workflows/ubuntu_22.04/build.sh | 4 ++++ .github/workflows/ubuntu_22.04/test.sh | 3 +++ 3 files changed, 14 insertions(+) diff --git a/.github/workflows/ubuntu_22.04/Dockerfile.ci b/.github/workflows/ubuntu_22.04/Dockerfile.ci index d257da558c10..a983e3aceb49 100644 --- a/.github/workflows/ubuntu_22.04/Dockerfile.ci +++ b/.github/workflows/ubuntu_22.04/Dockerfile.ci @@ -112,6 +112,13 @@ RUN curl -L -O https://github.com/Esri/file-geodatabase-api/raw/master/FileGDB_A && rm -rf FileGDB_API-64gcc51 \ && ldconfig +# Oracle : client side (proprietary software) +RUN curl -L -O https://download.oracle.com/otn_software/linux/instantclient/199000/instantclient-basic-linux.x64-19.9.0.0.0dbru.zip \ + && curl -L -O https://download.oracle.com/otn_software/linux/instantclient/199000/instantclient-sdk-linux.x64-19.9.0.0.0dbru.zip \ + && unzip instantclient-basic-linux.x64-19.9.0.0.0dbru.zip -d /opt \ + && unzip instantclient-sdk-linux.x64-19.9.0.0.0dbru.zip -d opt \ + && apt-get install -y libaio1 + COPY requirements.txt /tmp/ RUN python3 -m pip install -U -r /tmp/requirements.txt diff --git a/.github/workflows/ubuntu_22.04/build.sh b/.github/workflows/ubuntu_22.04/build.sh index b797888baf72..79057fbf42ba 100755 --- a/.github/workflows/ubuntu_22.04/build.sh +++ b/.github/workflows/ubuntu_22.04/build.sh @@ -2,11 +2,15 @@ set -e +LD_LIBRARY_PATH="/opt/instantclient_19_9:/opt/instantclient_19_9/lib:${LD_LIBRARY_PATH}" +export LD_LIBRARY_PATH + cmake ${GDAL_SOURCE_DIR:=..} \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_FLAGS="-Werror" \ -DCMAKE_CXX_FLAGS="-Werror" \ -DUSE_CCACHE=ON \ + -DOracle_ROOT=/opt/instantclient_19_9 \ -DGDAL_USE_GEOTIFF_INTERNAL:BOOL=ON \ -DGDAL_USE_TIFF_INTERNAL:BOOL=ON diff --git a/.github/workflows/ubuntu_22.04/test.sh b/.github/workflows/ubuntu_22.04/test.sh index 5addcb8694eb..9af1dd53f7b7 100755 --- a/.github/workflows/ubuntu_22.04/test.sh +++ b/.github/workflows/ubuntu_22.04/test.sh @@ -4,6 +4,9 @@ set -e . ../scripts/setdevenv.sh +LD_LIBRARY_PATH="/opt/instantclient_19_9:/opt/instantclient_19_9/lib:${LD_LIBRARY_PATH}" +export LD_LIBRARY_PATH + export PYTEST="python3 -m pytest -vv -p no:sugar --color=no" # Run C++ tests From 0d1a434bd7e0e0bc6adba5ae1b9147938beafe58 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 20:45:53 +0100 Subject: [PATCH 008/132] gdalwarp_lib.cpp: do floating point division (CID 1534303) --- apps/gdalwarp_lib.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/gdalwarp_lib.cpp b/apps/gdalwarp_lib.cpp index 8d924e07bd8d..1e394e65c3c5 100644 --- a/apps/gdalwarp_lib.cpp +++ b/apps/gdalwarp_lib.cpp @@ -2705,8 +2705,8 @@ static GDALDatasetH GDALWarpDirect(const char *pszDest, GDALDatasetH hDstDS, // Check if transformation is inversible { - double dfX = GDALGetRasterXSize(hDstDS) / 2; - double dfY = GDALGetRasterYSize(hDstDS) / 2; + double dfX = GDALGetRasterXSize(hDstDS) / 2.0; + double dfY = GDALGetRasterYSize(hDstDS) / 2.0; double dfZ = 0; int bSuccess = false; const auto nErrorCounterBefore = CPLGetErrorCounter(); From 8cfc58cf5c87696de0af57d38bf28cf9cdf9361d Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 20:47:13 +0100 Subject: [PATCH 009/132] PG: fix memleak in error code path (master only, CID 1534302) --- ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp index a997afc2d6c3..ff93c68e772d 100644 --- a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp @@ -2491,6 +2491,7 @@ OGRLayer *OGRPGDataSource::GetLayerByName(const char *pszNameIn) if (!osSchemaName.has_value()) { CPLFree(pszNameWithoutBracket); + CPLFree(pszGeomColumnName); return nullptr; } pszSchemaName = CPLStrdup(osSchemaName->c_str()); From d9c631e9d8ed1df067dbfc989fd5f609428b3494 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 20:48:04 +0100 Subject: [PATCH 010/132] TileDB: fix performance warning (CID 1534301) --- frmts/tiledb/tiledbcommon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frmts/tiledb/tiledbcommon.cpp b/frmts/tiledb/tiledbcommon.cpp index 21f51959d324..3c80c1cfd3f1 100644 --- a/frmts/tiledb/tiledbcommon.cpp +++ b/frmts/tiledb/tiledbcommon.cpp @@ -289,7 +289,7 @@ GDALDataset *TileDBDataset::Open(GDALOpenInfo *poOpenInfo) { if (!poCandidateArray) { - poCandidateArray = poArray; + poCandidateArray = std::move(poArray); } else { From cbf03ce3b37d7cea0eeaa129658aa119432c47ed Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 20:51:37 +0100 Subject: [PATCH 011/132] exportToCF1(): make it obvious that we don't out-of-bound access param.doubles[] (CID 1534300) --- ogr/ogr_srs_cf1.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/ogr/ogr_srs_cf1.cpp b/ogr/ogr_srs_cf1.cpp index a67bef2d52e2..8add4d7a6d3b 100644 --- a/ogr/ogr_srs_cf1.cpp +++ b/ogr/ogr_srs_cf1.cpp @@ -1440,8 +1440,7 @@ OGRSpatialReference::exportToCF1(char **ppszGridMappingName, { std::string key{}; std::string valueStr{}; - size_t doubleCount = 0; - double doubles[2] = {0, 0}; + std::vector doubles{}; }; std::vector oParams; @@ -1452,16 +1451,15 @@ OGRSpatialReference::exportToCF1(char **ppszGridMappingName, Value v; v.key = key; v.valueStr = value; - oParams.push_back(v); + oParams.emplace_back(std::move(v)); }; const auto addParamDouble = [&oParams](const char *key, double value) { Value v; v.key = key; - v.doubleCount = 1; - v.doubles[0] = value; - oParams.push_back(v); + v.doubles.push_back(value); + oParams.emplace_back(std::move(v)); }; const auto addParam2Double = @@ -1469,10 +1467,9 @@ OGRSpatialReference::exportToCF1(char **ppszGridMappingName, { Value v; v.key = key; - v.doubleCount = 2; - v.doubles[0] = value1; - v.doubles[1] = value2; - oParams.push_back(v); + v.doubles.push_back(value1); + v.doubles.push_back(value2); + oParams.emplace_back(std::move(v)); }; std::string osCFProjection; @@ -1694,11 +1691,11 @@ OGRSpatialReference::exportToCF1(char **ppszGridMappingName, else { std::string osVal; - for (size_t i = 0; i < param.doubleCount; ++i) + for (const double dfVal : param.doubles) { - if (i > 0) + if (!osVal.empty()) osVal += ','; - osVal += CPLSPrintf("%.18g", param.doubles[i]); + osVal += CPLSPrintf("%.18g", dfVal); } aosKeyValues.AddNameValue(param.key.c_str(), osVal.c_str()); } From 83062d685528bf2c16ee757015b455ea5c193dcb Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 20:52:28 +0100 Subject: [PATCH 012/132] PG: fix performance warning (CID 1534299) --- ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp index ff93c68e772d..842711c08dba 100644 --- a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp @@ -2093,7 +2093,7 @@ OGRLayer *OGRPGDataSource::ICreateLayer(const char *pszLayerName, osFIDColumnNameEscaped.c_str(), pszSerialType, osFIDColumnNameEscaped.c_str()); } - osCreateTable = osCommand; + osCreateTable = std::move(osCommand); } const char *pszSI = From 4f0dbb9d06a1eceb5665a4dd954471efeac7509c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:26:27 +0100 Subject: [PATCH 013/132] HFA: make sure HFADataset::hHFA member is initialized (CID 1073932) --- frmts/hfa/hfadataset.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frmts/hfa/hfadataset.h b/frmts/hfa/hfadataset.h index 018cd18befc4..0c49a98d3660 100644 --- a/frmts/hfa/hfadataset.h +++ b/frmts/hfa/hfadataset.h @@ -53,7 +53,7 @@ class HFADataset final : public GDALPamDataset { friend class HFARasterBand; - HFAHandle hHFA; + HFAHandle hHFA = nullptr; bool bMetadataDirty = false; From ddf5de9793bb80fe4174b8d73e7a55c0bdc6fc8c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:30:58 +0100 Subject: [PATCH 014/132] Rasterlite: avoid false positive Coverity warning about nullptr deref (CID 1214415) --- frmts/rasterlite/rasterlitedataset.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/frmts/rasterlite/rasterlitedataset.cpp b/frmts/rasterlite/rasterlitedataset.cpp index 322ea55f8180..691d675a1d6b 100644 --- a/frmts/rasterlite/rasterlitedataset.cpp +++ b/frmts/rasterlite/rasterlitedataset.cpp @@ -1096,7 +1096,7 @@ GDALDataset *RasterliteDataset::Open(GDALOpenInfo *poOpenInfo) if (osTableName.empty()) { int nCountSubdataset = 0; - int nLayers = OGR_DS_GetLayerCount(hDS); + const int nLayers = OGR_DS_GetLayerCount(hDS); /* -------------------------------------------------------------------- */ /* Add raster layers as subdatasets */ @@ -1105,14 +1105,15 @@ GDALDataset *RasterliteDataset::Open(GDALOpenInfo *poOpenInfo) for (int i = 0; i < nLayers; i++) { OGRLayerH hLyr = OGR_DS_GetLayer(hDS, i); - const char *pszLayerName = OGR_L_GetName(hLyr); - if (strstr(pszLayerName, "_metadata")) + const std::string osLayerName = OGR_L_GetName(hLyr); + const auto nPosMetadata = osLayerName.find("_metadata"); + if (nPosMetadata != std::string::npos) { - char *pszShortName = CPLStrdup(pszLayerName); - *strstr(pszShortName, "_metadata") = '\0'; + const std::string osShortName = + osLayerName.substr(0, nPosMetadata); - CPLString osRasterTableName = pszShortName; - osRasterTableName += "_rasters"; + const std::string osRasterTableName = + std::string(osShortName).append("_rasters"); if (OGR_DS_GetLayerByName(hDS, osRasterTableName.c_str()) != nullptr) @@ -1120,21 +1121,19 @@ GDALDataset *RasterliteDataset::Open(GDALOpenInfo *poOpenInfo) if (poDS == nullptr) { poDS = new RasterliteDataset(); - osTableName = pszShortName; + osTableName = osShortName; } - CPLString osSubdatasetName; + std::string osSubdatasetName; if (!STARTS_WITH_CI(poOpenInfo->pszFilename, "RASTERLITE:")) osSubdatasetName += "RASTERLITE:"; osSubdatasetName += poOpenInfo->pszFilename; osSubdatasetName += ",table="; - osSubdatasetName += pszShortName; + osSubdatasetName += osShortName; poDS->AddSubDataset(osSubdatasetName.c_str()); nCountSubdataset++; } - - CPLFree(pszShortName); } } From b61b28473e070adf88c28760a0a859861b3a3bc5 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:33:37 +0100 Subject: [PATCH 015/132] LIBKML: avoid false positive Coverity warning about nullptr deref (CID 1214419) --- ogr/ogrsf_frmts/libkml/ogrlibkmldatasource.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ogr/ogrsf_frmts/libkml/ogrlibkmldatasource.cpp b/ogr/ogrsf_frmts/libkml/ogrlibkmldatasource.cpp index ba20dc1a98b0..88c124205002 100644 --- a/ogr/ogrsf_frmts/libkml/ogrlibkmldatasource.cpp +++ b/ogr/ogrsf_frmts/libkml/ogrlibkmldatasource.cpp @@ -729,7 +729,6 @@ SchemaPtr OGRLIBKMLDataSource::FindSchema(const char *pszSchemaUrl) char *pszID = nullptr; char *pszFile = nullptr; char *pszSchemaName = nullptr; - char *pszPound = nullptr; DocumentPtr poKmlDocument = nullptr; SchemaPtr poKmlSchemaResult = nullptr; @@ -746,13 +745,11 @@ SchemaPtr OGRLIBKMLDataSource::FindSchema(const char *pszSchemaUrl) m_poKmlDocKml->IsA(kmldom::Type_Document)) poKmlDocument = AsDocument(m_poKmlDocKml); } - else if ((pszPound = strchr(const_cast(pszSchemaUrl), '#')) != - nullptr) + else if (const char *pszPound = strchr(pszSchemaUrl, '#')) { pszFile = CPLStrdup(pszSchemaUrl); pszID = CPLStrdup(pszPound + 1); - pszPound = strchr(pszFile, '#'); - *pszPound = '\0'; + pszFile[pszPound - pszSchemaUrl] = '\0'; } else { From 92dc2986a8ab0495ca49fe8e27b9f27d8a93f4b9 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:36:01 +0100 Subject: [PATCH 016/132] OSM: avoid COPY_INSTEAD_OF_MOVE (CID 1525192) --- ogr/ogrsf_frmts/osm/ogrosmdatasource.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ogr/ogrsf_frmts/osm/ogrosmdatasource.cpp b/ogr/ogrsf_frmts/osm/ogrosmdatasource.cpp index d44057dc1da5..92841fc6e83b 100644 --- a/ogr/ogrsf_frmts/osm/ogrosmdatasource.cpp +++ b/ogr/ogrsf_frmts/osm/ogrosmdatasource.cpp @@ -4069,18 +4069,18 @@ bool OGROSMDataSource::TransferToDiskIfNecesserary() VSIFCloseL(m_fpNodes); m_fpNodes = nullptr; - CPLString osNewTmpDBName; - osNewTmpDBName = CPLGenerateTempFilename("osm_tmp_nodes"); + const std::string osNewTmpDBName( + CPLGenerateTempFilename("osm_tmp_nodes")); CPLDebug("OSM", "%s too big for RAM. Transferring it onto disk in %s", m_osNodesFilename.c_str(), osNewTmpDBName.c_str()); - if (CPLCopyFile(osNewTmpDBName, m_osNodesFilename) != 0) + if (CPLCopyFile(osNewTmpDBName.c_str(), m_osNodesFilename) != 0) { CPLError(CE_Failure, CPLE_AppDefined, "Cannot copy %s to %s", m_osNodesFilename.c_str(), osNewTmpDBName.c_str()); - VSIUnlink(osNewTmpDBName); + VSIUnlink(osNewTmpDBName.c_str()); m_bStopParsing = true; return false; } From 7c718b92b53a12c527c6a2aa8e8535bcee4923e2 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:37:06 +0100 Subject: [PATCH 017/132] LIBKML: avoid COPY_INSTEAD_OF_MOVE (CID 1525205) --- ogr/ogrsf_frmts/libkml/ogrlibkmllayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ogr/ogrsf_frmts/libkml/ogrlibkmllayer.cpp b/ogr/ogrsf_frmts/libkml/ogrlibkmllayer.cpp index 40211bdebf4f..d9fc28577613 100644 --- a/ogr/ogrsf_frmts/libkml/ogrlibkmllayer.cpp +++ b/ogr/ogrsf_frmts/libkml/ogrlibkmllayer.cpp @@ -254,7 +254,7 @@ OGRLIBKMLLayer::OGRLIBKMLLayer( { m_poKmlSchema = nullptr; } - kml2FeatureDef(schema, m_poOgrFeatureDefn); + kml2FeatureDef(std::move(schema), m_poOgrFeatureDefn); } } } From 71de7ebda7c6d08bf4066fb8ace1c27e5b7d3c89 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:39:44 +0100 Subject: [PATCH 018/132] PDF: avoid COPY_INSTEAD_OF_MOVE (CID 1525231) --- frmts/pdf/pdfdataset.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frmts/pdf/pdfdataset.cpp b/frmts/pdf/pdfdataset.cpp index f15ba43145d1..778f702fba4d 100644 --- a/frmts/pdf/pdfdataset.cpp +++ b/frmts/pdf/pdfdataset.cpp @@ -3903,8 +3903,8 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, if (nRecLevel == 16) return; - int nLength = poArray->GetLength(); - CPLString osCurLayer; + const int nLength = poArray->GetLength(); + std::string osCurLayer; for (int i = 0; i < nLength; i++) { GDALPDFObject *poObj = poArray->Get(i); @@ -3912,9 +3912,10 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, continue; if (i == 0 && poObj->GetType() == PDFObjectType_String) { - CPLString osName = PDFSanitizeLayerName(poObj->GetString().c_str()); + const std::string osName = + PDFSanitizeLayerName(poObj->GetString().c_str()); if (!osTopLayer.empty()) - osTopLayer = osTopLayer + "." + osName; + osTopLayer = std::string(osTopLayer).append(".").append(osName); else osTopLayer = osName; AddLayer(osTopLayer.c_str()); @@ -3923,7 +3924,7 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, else if (poObj->GetType() == PDFObjectType_Array) { ExploreLayersPdfium(poObj->GetArray(), nRecLevel + 1, osCurLayer); - osCurLayer = ""; + osCurLayer.clear(); } else if (poObj->GetType() == PDFObjectType_Dictionary) { @@ -3931,17 +3932,16 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, GDALPDFObject *poName = poDict->Get("Name"); if (poName != nullptr && poName->GetType() == PDFObjectType_String) { - CPLString osName = + const std::string osName = PDFSanitizeLayerName(poName->GetString().c_str()); // coverity[copy_paste_error] if (!osTopLayer.empty()) { - osCurLayer = osTopLayer; - osCurLayer += '.'; - osCurLayer += std::move(osName); + osCurLayer = + std::string(osTopLayer).append(".").append(osName); } else - osCurLayer = std::move(osName); + osCurLayer = osName; // CPLDebug("PDF", "Layer %s", osCurLayer.c_str()); AddLayer(osCurLayer.c_str()); From 99e34eacaac7004962a91495a89ef968ec3171ff Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:41:24 +0100 Subject: [PATCH 019/132] WMS: avoid COPY_INSTEAD_OF_MOVE (CID 1525239) --- frmts/wms/wmsmetadataset.cpp | 19 +++++++++---------- frmts/wms/wmsmetadataset.h | 3 ++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frmts/wms/wmsmetadataset.cpp b/frmts/wms/wmsmetadataset.cpp index f2dd96dba321..58f358f53871 100644 --- a/frmts/wms/wmsmetadataset.cpp +++ b/frmts/wms/wmsmetadataset.cpp @@ -231,13 +231,11 @@ char **GDALWMSMetaDataset::GetMetadata(const char *pszDomain) /* AddSubDataset() */ /************************************************************************/ -void GDALWMSMetaDataset::AddSubDataset(const char *pszLayerName, - const char *pszTitle, - CPL_UNUSED const char *pszAbstract, - const char *pszSRS, const char *pszMinX, - const char *pszMinY, const char *pszMaxX, - const char *pszMaxY, CPLString osFormat, - CPLString osTransparent) +void GDALWMSMetaDataset::AddSubDataset( + const char *pszLayerName, const char *pszTitle, + CPL_UNUSED const char *pszAbstract, const char *pszSRS, const char *pszMinX, + const char *pszMinY, const char *pszMaxX, const char *pszMaxY, + const std::string &osFormat, const std::string &osTransparent) { CPLString osSubdatasetName = "WMS:"; osSubdatasetName += osGetURL; @@ -258,10 +256,11 @@ void GDALWMSMetaDataset::AddSubDataset(const char *pszLayerName, osSubdatasetName, "BBOX", CPLSPrintf("%s,%s,%s,%s", pszMinX, pszMinY, pszMaxX, pszMaxY)); if (!osFormat.empty()) - osSubdatasetName = CPLURLAddKVP(osSubdatasetName, "FORMAT", osFormat); - if (!osTransparent.empty()) osSubdatasetName = - CPLURLAddKVP(osSubdatasetName, "TRANSPARENT", osTransparent); + CPLURLAddKVP(osSubdatasetName, "FORMAT", osFormat.c_str()); + if (!osTransparent.empty()) + osSubdatasetName = CPLURLAddKVP(osSubdatasetName, "TRANSPARENT", + osTransparent.c_str()); if (pszTitle) { diff --git a/frmts/wms/wmsmetadataset.h b/frmts/wms/wmsmetadataset.h index d82283f4d888..de306f3c9e30 100644 --- a/frmts/wms/wmsmetadataset.h +++ b/frmts/wms/wmsmetadataset.h @@ -72,7 +72,8 @@ class GDALWMSMetaDataset final : public GDALPamDataset const char *pszAbstract, const char *pszSRS, const char *pszMinX, const char *pszMinY, const char *pszMaxX, const char *pszMaxY, - CPLString osFormat, CPLString osTransparent); + const std::string &osFormat, + const std::string &osTransparent); void ExploreLayer(CPLXMLNode *psXML, const CPLString &osFormat, From 51e84863a26e53942769a93095e003091a10c5d3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:44:18 +0100 Subject: [PATCH 020/132] arrow_common: avoid COPY_INSTEAD_OF_MOVE (CID 1525324) --- ogr/ogrsf_frmts/arrow_common/ograrrowwriterlayer.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ogr/ogrsf_frmts/arrow_common/ograrrowwriterlayer.hpp b/ogr/ogrsf_frmts/arrow_common/ograrrowwriterlayer.hpp index 68b828f95a5b..72598b04e613 100644 --- a/ogr/ogrsf_frmts/arrow_common/ograrrowwriterlayer.hpp +++ b/ogr/ogrsf_frmts/arrow_common/ograrrowwriterlayer.hpp @@ -252,7 +252,8 @@ inline void OGRArrowWriterLayer::CreateSchemaCommon() break; } } - fields.emplace_back(arrow::field(poFieldDefn->GetNameRef(), dt, + fields.emplace_back(arrow::field(poFieldDefn->GetNameRef(), + std::move(dt), poFieldDefn->IsNullable())); if (poFieldDefn->GetAlternativeNameRef()[0]) bNeedGDALSchema = true; @@ -325,8 +326,9 @@ inline void OGRArrowWriterLayer::CreateSchemaCommon() break; } - std::shared_ptr field(arrow::field( - poGeomFieldDefn->GetNameRef(), dt, poGeomFieldDefn->IsNullable())); + std::shared_ptr field( + arrow::field(poGeomFieldDefn->GetNameRef(), std::move(dt), + poGeomFieldDefn->IsNullable())); if (m_bWriteFieldArrowExtensionName) { auto kvMetadata = field->metadata() @@ -338,7 +340,7 @@ inline void OGRArrowWriterLayer::CreateSchemaCommon() field = field->WithMetadata(kvMetadata); } - fields.emplace_back(field); + fields.emplace_back(std::move(field)); } m_aoEnvelopes.resize(m_poFeatureDefn->GetGeomFieldCount()); From 118655b42f679284913b0882d95827d91bc3f71f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 4 Feb 2024 21:45:40 +0100 Subject: [PATCH 021/132] HFA: avoid COPY_INSTEAD_OF_MOVE (CID 1525492) --- frmts/hfa/hfaopen.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frmts/hfa/hfaopen.cpp b/frmts/hfa/hfaopen.cpp index e9890058096c..f8ee8fecdd2c 100644 --- a/frmts/hfa/hfaopen.cpp +++ b/frmts/hfa/hfaopen.cpp @@ -3859,9 +3859,9 @@ CPLErr HFARenameReferences(HFAHandle hHFA, const char *pszNewBase, // Update the filename. if (strncmp(osFileName, pszOldBase, strlen(pszOldBase)) == 0) { - CPLString osNew = pszNewBase; - osNew += osFileName.c_str() + strlen(pszOldBase); - osFileName = osNew; + std::string osNew = pszNewBase; + osNew += (osFileName.c_str() + strlen(pszOldBase)); + osFileName = std::move(osNew); } apoNodeList[iNode]->SetStringField("dependent.string", osFileName); From ca17b0a3e26db89f31b1520eebb4d2fc067a7fae Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 5 Feb 2024 18:45:32 +0100 Subject: [PATCH 022/132] Add VSIGetDirectorySeparator() to return the directory separator for the specified path Default is forward slash. The only exception currently is the Windows file system which returns anti-slash, unless the specified path is of the form "{drive_letter}:/{rest_of_the_path}". --- port/cpl_vsi.h | 2 ++ port/cpl_vsi_virtual.h | 11 +++++++++++ port/cpl_vsil.cpp | 21 +++++++++++++++++++++ port/cpl_vsil_win32.cpp | 8 ++++++++ 4 files changed, 42 insertions(+) diff --git a/port/cpl_vsi.h b/port/cpl_vsi.h index e14b3cca37e0..cb0c293caf0c 100644 --- a/port/cpl_vsi.h +++ b/port/cpl_vsi.h @@ -380,6 +380,8 @@ char CPL_DLL **VSIReadDirRecursive(const char *pszPath); char CPL_DLL **VSIReadDirEx(const char *pszPath, int nMaxFiles); char CPL_DLL **VSISiblingFiles(const char *pszPath); +const char CPL_DLL *VSIGetDirectorySeparator(const char *pszPath); + /** Opaque type for a directory iterator */ typedef struct VSIDIR VSIDIR; diff --git a/port/cpl_vsi_virtual.h b/port/cpl_vsi_virtual.h index cb25db618305..847b270cb602 100644 --- a/port/cpl_vsi_virtual.h +++ b/port/cpl_vsi_virtual.h @@ -305,6 +305,17 @@ class CPL_DLL VSIFilesystemHandler "Duplicate() not supported on this file system"); return nullptr; } + + /** Return the directory separator. + * + * Default is forward slash. The only exception currently is the Windows + * file system which returns anti-slash, unless the specified path is of the + * form "{drive_letter}:/{rest_of_the_path}". + */ + virtual const char *GetDirectorySeparator(CPL_UNUSED const char *pszPath) + { + return "/"; + } }; #endif /* #ifndef DOXYGEN_SKIP */ diff --git a/port/cpl_vsil.cpp b/port/cpl_vsil.cpp index 1034583cdd28..0e4f533d4498 100644 --- a/port/cpl_vsil.cpp +++ b/port/cpl_vsil.cpp @@ -155,6 +155,27 @@ char **VSISiblingFiles(const char *pszFilename) return poFSHandler->SiblingFiles(pszFilename); } +/************************************************************************/ +/* VSIGetDirectorySeparator() */ +/************************************************************************/ + +/** Return the directory separator for the specified path. + * + * Default is forward slash. The only exception currently is the Windows + * file system which returns anti-slash, unless the specified path is of the + * form "{drive_letter}:/{rest_of_the_path}". + * + * @since 3.9 + */ +const char *VSIGetDirectorySeparator(const char *pszPath) +{ + if (STARTS_WITH(pszPath, "http://") || STARTS_WITH(pszPath, "https://")) + return "/"; + + VSIFilesystemHandler *poFSHandler = VSIFileManager::GetHandler(pszPath); + return poFSHandler->GetDirectorySeparator(pszPath); +} + /************************************************************************/ /* VSIReadRecursive() */ /************************************************************************/ diff --git a/port/cpl_vsil_win32.cpp b/port/cpl_vsil_win32.cpp index 634c5f36e28a..5625ae629194 100644 --- a/port/cpl_vsil_win32.cpp +++ b/port/cpl_vsil_win32.cpp @@ -83,6 +83,14 @@ class VSIWin32FilesystemHandler final : public VSIFilesystemHandler virtual bool IsLocal(const char *pszPath) override; std::string GetCanonicalFilename(const std::string &osFilename) const override; + + const char *GetDirectorySeparator(const char *pszPath) override + { + // Return forward slash for paths of the form + // "{drive_letter}:/{rest_of_the_path}", and backslash otherwise. + return (pszPath[0] && pszPath[1] == ':' && pszPath[2] == '/') ? "/" + : "\\"; + } }; /************************************************************************/ From be66914cfdb4bbaf5bc81ea5d7d041225d37c800 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 5 Feb 2024 18:47:29 +0100 Subject: [PATCH 023/132] CPLFormFilename(), CPLProjectRelativeFilename(), VSIReadDirRecursive(), NextDirEntry(): useVSIGetDirectorySeparator() For VSIReadDirRecursive() and NextDirEntry(), this will be a slight change in behavior on Windows where antislash separator will now be returned unless an absolute path of the form X:/foo is provided --- autotest/gcore/vsifile.py | 50 ++++++++++++++++++-------------- frmts/zarr/zarr_array.cpp | 8 +++-- port/cpl_path.cpp | 61 ++------------------------------------- port/cpl_vsil.cpp | 34 +++++++++------------- 4 files changed, 51 insertions(+), 102 deletions(-) diff --git a/autotest/gcore/vsifile.py b/autotest/gcore/vsifile.py index 6e1353c11eeb..0ba955c2f316 100755 --- a/autotest/gcore/vsifile.py +++ b/autotest/gcore/vsifile.py @@ -937,8 +937,9 @@ def test_vsifile_opendir(basepath): for i in range(4): entry = gdal.GetNextDirEntry(d) assert entry - if entry.name == "test": - entries_found.append(entry.name) + name = entry.name.replace("\\", "/") + if name == "test": + entries_found.append(name) assert (entry.mode & 32768) != 0 assert entry.modeKnown assert entry.size == 3 @@ -946,14 +947,14 @@ def test_vsifile_opendir(basepath): assert entry.mtime != 0 assert entry.mtimeKnown assert not entry.extra - elif entry.name == "subdir": - entries_found.append(entry.name) + elif name == "subdir": + entries_found.append(name) assert (entry.mode & 16384) != 0 - elif entry.name == "subdir/subdir2": - entries_found.append(entry.name) + elif name == "subdir/subdir2": + entries_found.append(name) assert (entry.mode & 16384) != 0 - elif entry.name == "subdir/subdir2/test2": - entries_found.append(entry.name) + elif name == "subdir/subdir2/test2": + entries_found.append(name) assert (entry.mode & 32768) != 0 else: assert False, entry.name @@ -970,19 +971,20 @@ def test_vsifile_opendir(basepath): for i in range(4): entry = gdal.GetNextDirEntry(d) assert entry - if entry.name == "test": - entries_found.append(entry.name) + name = entry.name.replace("\\", "/") + if name == "test": + entries_found.append(name) assert (entry.mode & 32768) != 0 if os.name == "posix" and basepath == "tmp/": assert entry.size == 0 - elif entry.name == "subdir": - entries_found.append(entry.name) + elif name == "subdir": + entries_found.append(name) assert (entry.mode & 16384) != 0 - elif entry.name == "subdir/subdir2": - entries_found.append(entry.name) + elif name == "subdir/subdir2": + entries_found.append(name) assert (entry.mode & 16384) != 0 - elif entry.name == "subdir/subdir2/test2": - entries_found.append(entry.name) + elif name == "subdir/subdir2/test2": + entries_found.append(name) assert (entry.mode & 32768) != 0 if os.name == "posix" and basepath == "tmp/": assert entry.size == 0 @@ -1009,7 +1011,10 @@ def test_vsifile_opendir(basepath): # Depth 1 files = set( - [l_entry.name for l_entry in gdal.listdir(basepath + "/vsifile_opendir", 1)] + [ + l_entry.name.replace("\\", "/") + for l_entry in gdal.listdir(basepath + "/vsifile_opendir", 1) + ] ) assert files == set(["test", "subdir", "subdir/subdir2"]) @@ -1030,18 +1035,19 @@ def test_vsifile_opendir(basepath): entry = gdal.GetNextDirEntry(d) assert entry.name == "subdir" entry = gdal.GetNextDirEntry(d) - assert entry.name == "subdir/subdir2" + sep = "\\" if "\\" in entry.name else "/" + assert entry.name.replace("\\", "/") == "subdir/subdir2" entry = gdal.GetNextDirEntry(d) - assert entry.name == "subdir/subdir2/test2" + assert entry.name.replace("\\", "/") == "subdir/subdir2/test2" entry = gdal.GetNextDirEntry(d) assert not entry gdal.CloseDir(d) - d = gdal.OpenDir(basepath + "/vsifile_opendir", -1, ["PREFIX=subdir/sub"]) + d = gdal.OpenDir(basepath + "/vsifile_opendir", -1, ["PREFIX=subdir" + sep + "sub"]) entry = gdal.GetNextDirEntry(d) - assert entry.name == "subdir/subdir2" + assert entry.name.replace("\\", "/") == "subdir/subdir2" entry = gdal.GetNextDirEntry(d) - assert entry.name == "subdir/subdir2/test2" + assert entry.name.replace("\\", "/") == "subdir/subdir2/test2" entry = gdal.GetNextDirEntry(d) assert not entry gdal.CloseDir(d) diff --git a/frmts/zarr/zarr_array.cpp b/frmts/zarr/zarr_array.cpp index 5a2a2e058afa..f62a43a31311 100644 --- a/frmts/zarr/zarr_array.cpp +++ b/frmts/zarr/zarr_array.cpp @@ -2210,12 +2210,16 @@ bool ZarrArray::CacheTilePresence() "present...", osDirectoryName.c_str()); uint64_t nCounter = 0; + const char chSrcFilenameDirSeparator = + VSIGetDirectorySeparator(osDirectoryName.c_str())[0]; while (const VSIDIREntry *psEntry = VSIGetNextDirEntry(psDir)) { if (!VSI_ISDIR(psEntry->nMode)) { - const CPLStringList aosTokens = - GetTileIndicesFromFilename(psEntry->pszName); + const CPLStringList aosTokens = GetTileIndicesFromFilename( + CPLString(psEntry->pszName) + .replaceAll(chSrcFilenameDirSeparator, '/') + .c_str()); if (aosTokens.size() == static_cast(m_aoDims.size())) { // Get tile indices from filename diff --git a/port/cpl_path.cpp b/port/cpl_path.cpp index 9c298967abd8..ec5efeaca505 100644 --- a/port/cpl_path.cpp +++ b/port/cpl_path.cpp @@ -53,12 +53,6 @@ constexpr int CPL_PATH_BUF_SIZE = 2048; constexpr int CPL_PATH_BUF_COUNT = 10; -#if defined(_WIN32) -constexpr char SEP_STRING[] = "\\"; -#else -constexpr char SEP_STRING[] = "/"; -#endif - static const char *CPLStaticBufferTooSmall(char *pszStaticResult) { CPLError(CE_Failure, CPLE_AppDefined, "Destination buffer too small"); @@ -486,36 +480,6 @@ const char *CPLResetExtension(const char *pszPath, const char *pszExt) return pszStaticResult; } -/************************************************************************/ -/* RequiresUnixPathSeparator() */ -/************************************************************************/ - -#if defined(_WIN32) -static bool RequiresUnixPathSeparator(const char *pszPath) -{ - return strcmp(pszPath, "/vsimem") == 0 || STARTS_WITH(pszPath, "http://") || - STARTS_WITH(pszPath, "https://") || - STARTS_WITH(pszPath, "/vsimem/") || - STARTS_WITH(pszPath, "/vsicurl/") || - STARTS_WITH(pszPath, "/vsicurl_streaming/") || - STARTS_WITH(pszPath, "/vsis3/") || - STARTS_WITH(pszPath, "/vsis3_streaming/") || - STARTS_WITH(pszPath, "/vsigs/") || - STARTS_WITH(pszPath, "/vsigs_streaming/") || - STARTS_WITH(pszPath, "/vsiaz/") || - STARTS_WITH(pszPath, "/vsiaz_streaming/") || - STARTS_WITH(pszPath, "/vsiadls/") || - STARTS_WITH(pszPath, "/vsioss/") || - STARTS_WITH(pszPath, "/vsioss_streaming/") || - STARTS_WITH(pszPath, "/vsiswift/") || - STARTS_WITH(pszPath, "/vsiswift_streaming/") || - STARTS_WITH(pszPath, "/vsihdfs/") || - STARTS_WITH(pszPath, "/vsiwebhdfs/") || - STARTS_WITH(pszPath, "/vsizip/") || - STARTS_WITH(pszPath, "/vsi7z/") || STARTS_WITH(pszPath, "/vsirar/"); -} -#endif - /************************************************************************/ /* CPLFormFilename() */ /************************************************************************/ @@ -596,22 +560,13 @@ const char *CPLFormFilename(const char *pszPath, const char *pszBasename, else { nLenPath = nLenPathOri; - pszAddedPathSep = SEP_STRING; + pszAddedPathSep = VSIGetDirectorySeparator(pszPath); } } else if (nLenPath > 0 && pszPath[nLenPath - 1] != '/' && pszPath[nLenPath - 1] != '\\') { -#if defined(_WIN32) - // FIXME? Would be better to ask the filesystems what it - // prefers as directory separator? - if (RequiresUnixPathSeparator(pszPath)) - pszAddedPathSep = "/"; - else -#endif - { - pszAddedPathSep = SEP_STRING; - } + pszAddedPathSep = VSIGetDirectorySeparator(pszPath); } if (pszExtension == nullptr) @@ -785,17 +740,7 @@ const char *CPLProjectRelativeFilename(const char *pszProjectDir, if (pszProjectDir[strlen(pszProjectDir) - 1] != '/' && pszProjectDir[strlen(pszProjectDir) - 1] != '\\') { - // FIXME: Better to ask the filesystems what it - // prefers as directory separator? - const char *pszAddedPathSep = nullptr; -#if defined(_WIN32) - if (RequiresUnixPathSeparator(pszStaticResult)) - pszAddedPathSep = "/"; - else -#endif - { - pszAddedPathSep = SEP_STRING; - } + const char *pszAddedPathSep = VSIGetDirectorySeparator(pszProjectDir); if (CPLStrlcat(pszStaticResult, pszAddedPathSep, CPL_PATH_BUF_SIZE) >= static_cast(CPL_PATH_BUF_SIZE)) return CPLStaticBufferTooSmall(pszStaticResult); diff --git a/port/cpl_vsil.cpp b/port/cpl_vsil.cpp index 0e4f533d4498..ef01860e6656 100644 --- a/port/cpl_vsil.cpp +++ b/port/cpl_vsil.cpp @@ -192,6 +192,11 @@ const char *VSIGetDirectorySeparator(const char *pszPath) * Note that no error is issued via CPLError() if the directory path is * invalid, though NULL is returned. * + * Note: since GDAL 3.9, for recursive mode, the directory separator will no + * longer be always forward slash, but will be the one returned by + * VSIGetDirectorySeparator(pszPathIn), so potentially backslash on Windows + * file systems. + * * @param pszPathIn the relative, or absolute path of a directory to read. * UTF-8 encoded. * @@ -204,11 +209,7 @@ const char *VSIGetDirectorySeparator(const char *pszPath) char **VSIReadDirRecursive(const char *pszPathIn) { -#if defined(_WIN32) - const char SEP = pszPathIn[0] == '\\' ? '\\' : '/'; -#else - constexpr char SEP = '/'; -#endif + const char SEP = VSIGetDirectorySeparator(pszPathIn)[0]; const char *const apszOptions[] = {"NAME_AND_TYPE_ONLY=YES", nullptr}; VSIDIR *psDir = VSIOpenDir(pszPathIn, -1, apszOptions); @@ -316,6 +317,11 @@ VSIDIR *VSIOpenDir(const char *pszPath, int nRecurseDepth, * The returned entry remains valid until the next call to VSINextDirEntry() * or VSICloseDir() with the same handle. * + * Note: since GDAL 3.9, for recursive mode, the directory separator will no + * longer be always forward slash, but will be the one returned by + * VSIGetDirectorySeparator(pszPathIn), so potentially backslash on Windows + * file systems. + * * @param dir Directory handled returned by VSIOpenDir(). Must not be NULL. * * @return a entry, or NULL if there is no more entry in the directory. This @@ -1396,11 +1402,7 @@ bool VSIFilesystemHandler::Sync(const char *pszSource, const char *pszTarget, GDALProgressFunc pProgressFunc, void *pProgressData, char ***ppapszOutputs) { -#if defined(_WIN32) - const char SOURCE_SEP = pszSource[0] == '\\' ? '\\' : '/'; -#else - constexpr char SOURCE_SEP = '/'; -#endif + const char SOURCE_SEP = VSIGetDirectorySeparator(pszSource)[0]; if (ppapszOutputs) { @@ -1685,11 +1687,7 @@ VSIDIR *VSIFilesystemHandler::OpenDir(const char *pszPath, int nRecurseDepth, const VSIDIREntry *VSIDIRGeneric::NextDirEntry() { -#if defined(_WIN32) - const char SEP = osRootPath[0] == '\\' ? '\\' : '/'; -#else - constexpr char SEP = '/'; -#endif + const char SEP = VSIGetDirectorySeparator(osRootPath.c_str())[0]; begin: if (VSI_ISDIR(entry.nMode) && nRecurseDepth != 0) @@ -1837,11 +1835,7 @@ int VSIFilesystemHandler::RmdirRecursive(const char *pszDirname) osDirnameWithoutEndSlash.resize(osDirnameWithoutEndSlash.size() - 1); } -#if defined(_WIN32) - const char SEP = pszDirname[0] == '\\' ? '\\' : '/'; -#else - constexpr char SEP = '/'; -#endif + const char SEP = VSIGetDirectorySeparator(pszDirname)[0]; CPLStringList aosOptions; auto poDir = From 42df990d2b874838dd05ef3339889301c3d54f95 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 5 Feb 2024 18:47:46 +0100 Subject: [PATCH 024/132] /vsis3 Sync(): use VSIGetDirectorySeparator(), and normalize target filename to the dir separator of the target filesystem. This fixes synchronization from Windows extended filenames with subdirectories to /vsis3/ to avoid antislash to be used on /vsis3 --- autotest/gcore/vsis3.py | 87 ++++++++++++++++++++++++++++++++++------- port/cpl_vsil_s3.cpp | 42 +++++++++++++------- 2 files changed, 101 insertions(+), 28 deletions(-) diff --git a/autotest/gcore/vsis3.py b/autotest/gcore/vsis3.py index 106869ab86cc..cacec03b5b45 100755 --- a/autotest/gcore/vsis3.py +++ b/autotest/gcore/vsis3.py @@ -3734,17 +3734,58 @@ def test_vsis3_sync_win32_special_filenames(aws_test_config, webserver_port, tmp prefix_path = "\\\\?\\" + tmp_path_str - f = gdal.VSIFOpenL(prefix_path + "\\testsync.txt", "wb") - assert f - gdal.VSIFCloseL(f) - - # S3 to local: S3 file is newer + # S3 to local gdal.VSICurlClearCache() handler = webserver.SequentialHandler() - handler.add( "GET", - "/out/testsync.txt", + "/bucket/", + 200, + {}, + """ + + + + false + + subdir/ + 2037-01-01T00:00:01.000Z + 0 + + + subdir/testsync.txt + 2037-01-01T00:00:01.000Z + 3 + + + """, + ) + handler.add( + "GET", + "/bucket/", + 200, + {}, + """ + + + + false + + subdir/ + 2037-01-01T00:00:01.000Z + 0 + + + subdir/testsync.txt + 2037-01-01T00:00:01.000Z + 3 + + + """, + ) + handler.add( + "GET", + "/bucket/subdir/testsync.txt", 206, { "Content-Length": "3", @@ -3755,15 +3796,35 @@ def test_vsis3_sync_win32_special_filenames(aws_test_config, webserver_port, tmp ) handler.add( "GET", - "/out/testsync.txt", + "/bucket/?delimiter=%2F&prefix=subdir%2F", + 200, + {}, + """ + + subdir/ + + false + + subdir/testsync.txt + 2037-01-01T00:00:01.000Z + 3 + + + """, + ) + handler.add( + "GET", + "/bucket/subdir/testsync.txt", 200, {"Content-Length": "3", "Last-Modified": "Mon, 01 Jan 2037 00:00:01 GMT"}, "foo", ) with webserver.install_http_handler(handler): - assert gdal.Sync("/vsis3/out/testsync.txt", prefix_path, options=options) + assert gdal.Sync("/vsis3/bucket/", prefix_path, options=options) - # Local to S3: S3 file is newer + assert gdal.VSIStatL(prefix_path + "\\subdir\\testsync.txt") is not None + + # Local to S3 gdal.VSICurlClearCache() handler = webserver.SequentialHandler() handler.add("GET", "/out/", 404) @@ -3779,14 +3840,12 @@ def test_vsis3_sync_win32_special_filenames(aws_test_config, webserver_port, tmp false - testsync.txt - 2037-01-01T00:00:01.000Z - 3 """, ) - handler.add("PUT", "/out/testsync.txt", 200) + handler.add("PUT", "/out/subdir/", 200) + handler.add("PUT", "/out/subdir/testsync.txt", 200) with webserver.install_http_handler(handler): assert gdal.Sync(prefix_path + "\\", "/vsis3/out/", options=options) diff --git a/port/cpl_vsil_s3.cpp b/port/cpl_vsil_s3.cpp index ae7381360b57..2880714b27d0 100644 --- a/port/cpl_vsil_s3.cpp +++ b/port/cpl_vsil_s3.cpp @@ -3963,7 +3963,8 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, struct ChunkToCopy { - std::string osFilename{}; + std::string osSrcFilename{}; + std::string osDstFilename{}; GIntBig nMTime = 0; std::string osETag{}; vsi_l_offset nTotalSize = 0; @@ -4078,6 +4079,14 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, std::string osTargetDir; // set in the VSI_ISDIR(sSource.st_mode) case std::string osTarget; // set in the !(VSI_ISDIR(sSource.st_mode)) case + const auto NormalizeDirSeparatorForDstFilename = + [&osSource, &osTargetDir](const std::string &s) -> std::string + { + return CPLString(s).replaceAll( + VSIGetDirectorySeparator(osSource.c_str()), + VSIGetDirectorySeparator(osTargetDir.c_str())); + }; + if (VSI_ISDIR(sSource.st_mode)) { osTargetDir = pszTarget; @@ -4109,15 +4118,16 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, const auto entry = VSIGetNextDirEntry(poTargetDir.get()); if (!entry) break; + const auto osDstName = + NormalizeDirSeparatorForDstFilename(entry->pszName); if (VSI_ISDIR(entry->nMode)) { - oSetTargetSubdirs.insert(entry->pszName); + oSetTargetSubdirs.insert(osDstName); } else { oMapExistingTargetFiles.insert( - std::pair(entry->pszName, - *entry)); + std::pair(osDstName, *entry)); } } poTargetDir.reset(); @@ -4142,11 +4152,13 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, break; if (VSI_ISDIR(entry->nMode)) { - if (oSetTargetSubdirs.find(entry->pszName) == + const auto osDstName = + NormalizeDirSeparatorForDstFilename(entry->pszName); + if (oSetTargetSubdirs.find(osDstName) == oSetTargetSubdirs.end()) { const std::string osTargetSubdir(CPLFormFilename( - osTargetDir.c_str(), entry->pszName, nullptr)); + osTargetDir.c_str(), osDstName.c_str(), nullptr)); aoSetDirsToCreate.insert(osTargetSubdir); } } @@ -4165,7 +4177,9 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, return false; } ChunkToCopy chunk; - chunk.osFilename = entry->pszName; + chunk.osSrcFilename = entry->pszName; + chunk.osDstFilename = + NormalizeDirSeparatorForDstFilename(entry->pszName); chunk.nMTime = entry->nMTime; chunk.nTotalSize = entry->nSize; chunk.osETag = @@ -4212,12 +4226,12 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, continue; const std::string osSubSource( CPLFormFilename(osSourceWithoutSlash.c_str(), - chunk.osFilename.c_str(), nullptr)); + chunk.osSrcFilename.c_str(), nullptr)); const std::string osSubTarget(CPLFormFilename( - osTargetDir.c_str(), chunk.osFilename.c_str(), nullptr)); + osTargetDir.c_str(), chunk.osDstFilename.c_str(), nullptr)); bool bSkip = false; const auto oIterExistingTarget = - oMapExistingTargetFiles.find(chunk.osFilename); + oMapExistingTargetFiles.find(chunk.osDstFilename); if (oIterExistingTarget != oMapExistingTargetFiles.end() && oIterExistingTarget->second.nSize == chunk.nTotalSize) { @@ -4334,9 +4348,9 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, CPLAssert(chunk.nStartOffset == 0); const std::string osSubSource( CPLFormFilename(osSourceWithoutSlash.c_str(), - chunk.osFilename.c_str(), nullptr)); + chunk.osSrcFilename.c_str(), nullptr)); const std::string osSubTarget(CPLFormFilename( - osTargetDir.c_str(), chunk.osFilename.c_str(), nullptr)); + osTargetDir.c_str(), chunk.osDstFilename.c_str(), nullptr)); // coverity[divide_by_zero] void *pScaledProgress = GDALCreateScaledProgress( double(nAccSize) / nTotalSize, @@ -4630,12 +4644,12 @@ bool IVSIS3LikeFSHandler::Sync(const char *pszSource, const char *pszTarget, queue->osTargetDir.empty() ? queue->osSource.c_str() : CPLFormFilename(queue->osSourceDir.c_str(), - chunk.osFilename.c_str(), nullptr)); + chunk.osSrcFilename.c_str(), nullptr)); const std::string osSubTarget( queue->osTargetDir.empty() ? queue->osTarget.c_str() : CPLFormFilename(queue->osTargetDir.c_str(), - chunk.osFilename.c_str(), nullptr)); + chunk.osDstFilename.c_str(), nullptr)); ProgressData progressData; progressData.nFileSize = chunk.nSize; From 9045af6087eb076eeca8ed33e9489ec48da31cd6 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 6 Feb 2024 22:14:21 +0100 Subject: [PATCH 025/132] Zarr: avoid potential infinite recursive calls at opening (fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=66497, master only) --- frmts/zarr/zarr.h | 41 ++++++-- frmts/zarr/zarr_sharedresource.cpp | 41 +++++++- frmts/zarr/zarr_v2_array.cpp | 151 ++++++++++------------------- frmts/zarr/zarr_v2_group.cpp | 6 +- frmts/zarr/zarr_v3_array.cpp | 54 +++-------- frmts/zarr/zarr_v3_group.cpp | 4 +- 6 files changed, 134 insertions(+), 163 deletions(-) diff --git a/frmts/zarr/zarr.h b/frmts/zarr/zarr.h index 139526fb8c69..ae0dc8a9274f 100644 --- a/frmts/zarr/zarr.h +++ b/frmts/zarr/zarr.h @@ -251,6 +251,7 @@ class ZarrSharedResource std::shared_ptr m_poPAM{}; CPLStringList m_aosOpenOptions{}; std::weak_ptr m_poWeakRootGroup{}; + std::set m_oSetArrayInLoading{}; explicit ZarrSharedResource(const std::string &osRootDirectoryName, bool bUpdatable); @@ -313,6 +314,35 @@ class ZarrSharedResource { m_poWeakRootGroup = poRootGroup; } + + bool AddArrayInLoading(const std::string &osZarrayFilename); + void RemoveArrayInLoading(const std::string &osZarrayFilename); + + struct SetFilenameAdder + { + std::shared_ptr m_poSharedResource; + const std::string m_osFilename; + const bool m_bOK; + + SetFilenameAdder( + const std::shared_ptr &poSharedResource, + const std::string &osFilename) + : m_poSharedResource(poSharedResource), m_osFilename(osFilename), + m_bOK(m_poSharedResource->AddArrayInLoading(m_osFilename)) + { + } + + ~SetFilenameAdder() + { + if (m_bOK) + m_poSharedResource->RemoveArrayInLoading(m_osFilename); + } + + bool ok() const + { + return m_bOK; + } + }; }; /************************************************************************/ @@ -518,8 +548,8 @@ class ZarrV2Group final : public ZarrGroupBase std::shared_ptr LoadArray(const std::string &osArrayName, const std::string &osZarrayFilename, const CPLJSONObject &oRoot, - bool bLoadedFromZMetadata, const CPLJSONObject &oAttributes, - std::set &oSetFilenamesInLoading) const; + bool bLoadedFromZMetadata, + const CPLJSONObject &oAttributes) const; std::shared_ptr CreateMDArray( const std::string &osName, @@ -569,10 +599,9 @@ class ZarrV3Group final : public ZarrGroupBase CreateGroup(const std::string &osName, CSLConstList papszOptions = nullptr) override; - std::shared_ptr - LoadArray(const std::string &osArrayName, - const std::string &osZarrayFilename, const CPLJSONObject &oRoot, - std::set &oSetFilenamesInLoading) const; + std::shared_ptr LoadArray(const std::string &osArrayName, + const std::string &osZarrayFilename, + const CPLJSONObject &oRoot) const; std::shared_ptr CreateMDArray( const std::string &osName, diff --git a/frmts/zarr/zarr_sharedresource.cpp b/frmts/zarr/zarr_sharedresource.cpp index cd34686da166..b0daddf2b608 100644 --- a/frmts/zarr/zarr_sharedresource.cpp +++ b/frmts/zarr/zarr_sharedresource.cpp @@ -116,9 +116,8 @@ std::shared_ptr ZarrSharedResource::OpenRootGroup() } const std::string osArrayName( CPLGetBasename(m_osRootDirectoryName.c_str())); - std::set oSetFilenamesInLoading; if (!poRG->LoadArray(osArrayName, osZarrayFilename, oRoot, false, - CPLJSONObject(), oSetFilenamesInLoading)) + CPLJSONObject())) return nullptr; return poRG; @@ -184,9 +183,7 @@ std::shared_ptr ZarrSharedResource::OpenRootGroup() const std::string osArrayName( CPLGetBasename(m_osRootDirectoryName.c_str())); poRG_V3->SetExplored(); - std::set oSetFilenamesInLoading; - if (!poRG_V3->LoadArray(osArrayName, osZarrJsonFilename, oRoot, - oSetFilenamesInLoading)) + if (!poRG_V3->LoadArray(osArrayName, osZarrJsonFilename, oRoot)) return nullptr; return poRG_V3; @@ -334,3 +331,37 @@ void ZarrSharedResource::UpdateDimensionSize( } poRG.reset(); } + +/************************************************************************/ +/* ZarrSharedResource::AddArrayInLoading() */ +/************************************************************************/ + +bool ZarrSharedResource::AddArrayInLoading(const std::string &osZarrayFilename) +{ + // Prevent too deep or recursive array loading + if (m_oSetArrayInLoading.find(osZarrayFilename) != + m_oSetArrayInLoading.end()) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Attempt at recursively loading %s", osZarrayFilename.c_str()); + return false; + } + if (m_oSetArrayInLoading.size() == 32) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Too deep call stack in LoadArray()"); + return false; + } + m_oSetArrayInLoading.insert(osZarrayFilename); + return true; +} + +/************************************************************************/ +/* ZarrSharedResource::RemoveArrayInLoading() */ +/************************************************************************/ + +void ZarrSharedResource::RemoveArrayInLoading( + const std::string &osZarrayFilename) +{ + m_oSetArrayInLoading.erase(osZarrayFilename); +} diff --git a/frmts/zarr/zarr_v2_array.cpp b/frmts/zarr/zarr_v2_array.cpp index 7617474a4b48..7ae8c89705e2 100644 --- a/frmts/zarr/zarr_v2_array.cpp +++ b/frmts/zarr/zarr_v2_array.cpp @@ -1281,45 +1281,14 @@ std::shared_ptr ZarrV2Group::LoadArray(const std::string &osArrayName, const std::string &osZarrayFilename, const CPLJSONObject &oRoot, bool bLoadedFromZMetadata, - const CPLJSONObject &oAttributesIn, - std::set &oSetFilenamesInLoading) const + const CPLJSONObject &oAttributesIn) const { - // Prevent too deep or recursive array loading - if (oSetFilenamesInLoading.find(osZarrayFilename) != - oSetFilenamesInLoading.end()) - { - CPLError(CE_Failure, CPLE_AppDefined, - "Attempt at recursively loading %s", osZarrayFilename.c_str()); - return nullptr; - } - if (oSetFilenamesInLoading.size() == 32) - { - CPLError(CE_Failure, CPLE_AppDefined, - "Too deep call stack in LoadArray()"); - return nullptr; - } - - struct SetFilenameAdder - { - std::set &m_oSetFilenames; - std::string m_osFilename; - - SetFilenameAdder(std::set &oSetFilenamesIn, - const std::string &osFilename) - : m_oSetFilenames(oSetFilenamesIn), m_osFilename(osFilename) - { - m_oSetFilenames.insert(osFilename); - } - - ~SetFilenameAdder() - { - m_oSetFilenames.erase(m_osFilename); - } - }; - - // Add osZarrayFilename to oSetFilenamesInLoading during the scope + // Add osZarrayFilename to m_poSharedResource during the scope // of this function call. - SetFilenameAdder filenameAdder(oSetFilenamesInLoading, osZarrayFilename); + ZarrSharedResource::SetFilenameAdder filenameAdder(m_poSharedResource, + osZarrayFilename); + if (!filenameAdder.ok()) + return nullptr; const auto osFormat = oRoot["zarr_format"].ToString(); if (osFormat != "2") @@ -1412,9 +1381,9 @@ ZarrV2Group::LoadArray(const std::string &osArrayName, const auto arrayDimensionsObj = oAttributes["_ARRAY_DIMENSIONS"]; const auto FindDimension = - [this, &aoDims, bLoadedFromZMetadata, &osArrayName, &oAttributes, - &oSetFilenamesInLoading](const std::string &osDimName, - std::shared_ptr &poDim, int i) + [this, &aoDims, bLoadedFromZMetadata, &osArrayName, + &oAttributes](const std::string &osDimName, + std::shared_ptr &poDim, int i) { auto oIter = m_oMapDimensions.find(osDimName); if (oIter != m_oMapDimensions.end()) @@ -1476,8 +1445,7 @@ ZarrV2Group::LoadArray(const std::string &osArrayName, if (oDoc.Load(osArrayFilenameDim)) { LoadArray(osDimName, osArrayFilenameDim, oDoc.GetRoot(), - false, CPLJSONObject(), - oSetFilenamesInLoading); + false, CPLJSONObject()); } } else @@ -1572,70 +1540,50 @@ ZarrV2Group::LoadArray(const std::string &osArrayName, if (arrayDims[i].GetType() == CPLJSONObject::Type::String) { const auto osDimFullpath = arrayDims[i].ToString(); - auto poDim = poRG->OpenDimensionFromFullname(osDimFullpath); - if (poDim == nullptr) + const std::string osArrayFullname = + (GetFullName() != "/" ? GetFullName() : std::string()) + + '/' + osArrayName; + if (aoDims.size() == 1 && + (osDimFullpath == osArrayFullname || + osDimFullpath == "/" + osArrayFullname)) { - CPLError(CE_Failure, CPLE_AppDefined, - "Cannot find NCZarr dimension %s", - osDimFullpath.c_str()); + // If this is an indexing variable, then fetch the + // dimension type and direction, and patch the dimension + std::string osType; + std::string osDirection; + ZarrArray::GetDimensionTypeDirection( + oAttributes, osType, osDirection); + + auto poDimLocal = std::make_shared( + m_poSharedResource, + std::dynamic_pointer_cast( + m_pSelf.lock()), + GetFullName(), osArrayName, osType, osDirection, + aoDims[i]->GetSize()); + aoDims[i] = poDimLocal; + + m_oMapDimensions[osArrayName] = std::move(poDimLocal); } - else if (poDim->GetSize() != aoDims[i]->GetSize()) + else if (auto poDim = + poRG->OpenDimensionFromFullname(osDimFullpath)) { - CPLError(CE_Failure, CPLE_AppDefined, - "Inconsistency in size between NCZarr " - "dimension %s and regular dimension", - osDimFullpath.c_str()); + if (poDim->GetSize() != aoDims[i]->GetSize()) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Inconsistency in size between NCZarr " + "dimension %s and regular dimension", + osDimFullpath.c_str()); + } + else + { + aoDims[i] = poDim; + } } else { - aoDims[i] = poDim; - - // If this is an indexing variable, then fetch the - // dimension type and direction, and patch the dimension - const std::string osArrayFullname = - (GetFullName() != "/" ? GetFullName() - : std::string()) + - '/' + osArrayName; - if (aoDims.size() == 1 && - osArrayFullname == poDim->GetFullName()) - { - std::string osType; - std::string osDirection; - ZarrArray::GetDimensionTypeDirection( - oAttributes, osType, osDirection); - - std::string osDimParent = osDimFullpath; - const auto nPos = osDimParent.rfind('/'); - if (nPos != std::string::npos) - { - if (nPos == 0) - osDimParent = '/'; - else - osDimParent.resize(nPos); - auto poDimParentGroup = - dynamic_cast( - poRG->OpenGroupFromFullname(osDimParent) - .get()); - if (poDimParentGroup) - { - auto poDimLocal = - std::make_shared( - m_poSharedResource, - std::dynamic_pointer_cast< - ZarrGroupBase>( - poDimParentGroup->m_pSelf - .lock()), - poDimParentGroup->GetFullName(), - poDim->GetName(), osType, - osDirection, poDim->GetSize()); - aoDims[i] = poDimLocal; - - poDimParentGroup - ->m_oMapDimensions[poDim->GetName()] = - std::move(poDimLocal); - } - } - } + CPLError(CE_Failure, CPLE_AppDefined, + "Cannot find NCZarr dimension %s", + osDimFullpath.c_str()); } } } @@ -1973,8 +1921,7 @@ ZarrV2Group::LoadArray(const std::string &osArrayName, if (oDoc.Load(osArrayFilenameDim)) { LoadArray(gridMappingName, osArrayFilenameDim, - oDoc.GetRoot(), false, CPLJSONObject(), - oSetFilenamesInLoading); + oDoc.GetRoot(), false, CPLJSONObject()); } } } diff --git a/frmts/zarr/zarr_v2_group.cpp b/frmts/zarr/zarr_v2_group.cpp index e3e8d7fe0c94..4f1d208f4c25 100644 --- a/frmts/zarr/zarr_v2_group.cpp +++ b/frmts/zarr/zarr_v2_group.cpp @@ -151,9 +151,8 @@ std::shared_ptr ZarrV2Group::OpenZarrArray(const std::string &osName, if (!oDoc.Load(osZarrayFilename)) return nullptr; const auto oRoot = oDoc.GetRoot(); - std::set oSetFilenamesInLoading; return LoadArray(osName, osZarrayFilename, oRoot, false, - CPLJSONObject(), oSetFilenamesInLoading); + CPLJSONObject()); } } @@ -332,9 +331,8 @@ void ZarrV2Group::InitFromZMetadata(const CPLJSONObject &obj) CPLFormFilename(poBelongingGroup->m_osDirectoryName.c_str(), osArrayName.c_str(), nullptr), ".zarray", nullptr); - std::set oSetFilenamesInLoading; poBelongingGroup->LoadArray(osArrayName, osZarrayFilename, oArray, true, - oAttributes, oSetFilenamesInLoading); + oAttributes); }; struct ArrayDesc diff --git a/frmts/zarr/zarr_v3_array.cpp b/frmts/zarr/zarr_v3_array.cpp index fd63f629ec27..0058a538c1d3 100644 --- a/frmts/zarr/zarr_v3_array.cpp +++ b/frmts/zarr/zarr_v3_array.cpp @@ -1071,45 +1071,14 @@ static T ParseNoDataComponent(const CPLJSONObject &oObj, bool &bOK) std::shared_ptr ZarrV3Group::LoadArray(const std::string &osArrayName, const std::string &osZarrayFilename, - const CPLJSONObject &oRoot, - std::set &oSetFilenamesInLoading) const + const CPLJSONObject &oRoot) const { - // Prevent too deep or recursive array loading - if (oSetFilenamesInLoading.find(osZarrayFilename) != - oSetFilenamesInLoading.end()) - { - CPLError(CE_Failure, CPLE_AppDefined, - "Attempt at recursively loading %s", osZarrayFilename.c_str()); - return nullptr; - } - if (oSetFilenamesInLoading.size() == 32) - { - CPLError(CE_Failure, CPLE_AppDefined, - "Too deep call stack in LoadArray()"); - return nullptr; - } - - struct SetFilenameAdder - { - std::set &m_oSetFilenames; - std::string m_osFilename; - - SetFilenameAdder(std::set &oSetFilenamesIn, - const std::string &osFilename) - : m_oSetFilenames(oSetFilenamesIn), m_osFilename(osFilename) - { - m_oSetFilenames.insert(osFilename); - } - - ~SetFilenameAdder() - { - m_oSetFilenames.erase(m_osFilename); - } - }; - - // Add osZarrayFilename to oSetFilenamesInLoading during the scope + // Add osZarrayFilename to m_poSharedResource during the scope // of this function call. - SetFilenameAdder filenameAdder(oSetFilenamesInLoading, osZarrayFilename); + ZarrSharedResource::SetFilenameAdder filenameAdder(m_poSharedResource, + osZarrayFilename); + if (!filenameAdder.ok()) + return nullptr; // Warn about unknown members (the spec suggests to error out, but let be // a bit more lenient) @@ -1252,10 +1221,9 @@ ZarrV3Group::LoadArray(const std::string &osArrayName, // Deal with dimension_names const auto dimensionNames = oRoot["dimension_names"]; - const auto FindDimension = - [this, &aoDims, &osArrayName, &oAttributes, - &oSetFilenamesInLoading](const std::string &osDimName, - std::shared_ptr &poDim, int i) + const auto FindDimension = [this, &aoDims, &osArrayName, &oAttributes]( + const std::string &osDimName, + std::shared_ptr &poDim, int i) { auto oIter = m_oMapDimensions.find(osDimName); if (oIter != m_oMapDimensions.end()) @@ -1295,8 +1263,8 @@ ZarrV3Group::LoadArray(const std::string &osArrayName, CPLJSONDocument oDoc; if (oDoc.Load(osArrayFilenameDim)) { - LoadArray(osDimName, osArrayFilenameDim, oDoc.GetRoot(), - oSetFilenamesInLoading); + LoadArray(osDimName, osArrayFilenameDim, + oDoc.GetRoot()); } } else diff --git a/frmts/zarr/zarr_v3_group.cpp b/frmts/zarr/zarr_v3_group.cpp index ae00443685f2..bfa88cf1a8ea 100644 --- a/frmts/zarr/zarr_v3_group.cpp +++ b/frmts/zarr/zarr_v3_group.cpp @@ -75,9 +75,7 @@ std::shared_ptr ZarrV3Group::OpenZarrArray(const std::string &osName, if (!oDoc.Load(osZarrayFilename)) return nullptr; const auto oRoot = oDoc.GetRoot(); - std::set oSetFilenamesInLoading; - return LoadArray(osName, osZarrayFilename, oRoot, - oSetFilenamesInLoading); + return LoadArray(osName, osZarrayFilename, oRoot); } return nullptr; From b4707f453ab353e0795ce603a9fcf72f3c6d435e Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 7 Feb 2024 21:16:07 +0100 Subject: [PATCH 026/132] jp2ecw.rst: clarify extent of Create support --- doc/source/drivers/raster/jp2ecw.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/source/drivers/raster/jp2ecw.rst b/doc/source/drivers/raster/jp2ecw.rst index 006748fa97e3..122fcba198d2 100644 --- a/doc/source/drivers/raster/jp2ecw.rst +++ b/doc/source/drivers/raster/jp2ecw.rst @@ -280,6 +280,15 @@ JP2ECW driver also arranges JP2 codestream to allow optimal access to power of two overviews. This is controlled with the creation option LEVELS." +Create support +-------------- + +While the driver advertizes the Create() capability, contrary to most other +drivers that implement it, the implementation of RasterIO() and WriteBlock() +in the JP2ECW driver does not support arbitrary random writing. +Data must be written in the dataset from top to bottom, whole line(s) at a +time. + Configuration Options --------------------- From 4f69f48220e6eab817c0d4863b7d29ea019184bc Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 8 Feb 2024 16:39:52 +0100 Subject: [PATCH 027/132] /vsicurl/: add a VSICURL_PC_URL_SIGNING path-specific option to enable Planetary Computer URL signing only on some URLs --- autotest/gcore/vsicurl.py | 49 ++++++++++++++++-------- doc/source/user/virtual_file_systems.rst | 2 +- port/cpl_vsil_curl.cpp | 13 +++++++ 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/autotest/gcore/vsicurl.py b/autotest/gcore/vsicurl.py index a750d6ab50fe..69493680ff2f 100755 --- a/autotest/gcore/vsicurl.py +++ b/autotest/gcore/vsicurl.py @@ -768,15 +768,30 @@ def test_vsicurl_planetary_computer_url_signing(server): ) with webserver.install_http_handler(handler): - with gdaltest.config_option( - "VSICURL_PC_SAS_SIGN_HREF_URL", - "http://localhost:%d/pc_sas_sign_href?href=" % server.port, - ): - statres = gdal.VSIStatL( - "/vsicurl?pc_url_signing=yes&url=http://localhost:%d/test_vsicurl_planetary_computer_url_signing.bin" - % server.port + try: + gdal.SetPathSpecificOption( + "/vsicurl/http://localhost:%d/test_vsicurl_planetary_computer_url_signing" + % server.port, + "VSICURL_PC_URL_SIGNING", + "YES", + ) + + with gdaltest.config_option( + "VSICURL_PC_SAS_SIGN_HREF_URL", + "http://localhost:%d/pc_sas_sign_href?href=" % server.port, + ): + statres = gdal.VSIStatL( + "/vsicurl/http://localhost:%d/test_vsicurl_planetary_computer_url_signing.bin" + % server.port + ) + assert statres.size == 3 + finally: + gdal.SetPathSpecificOption( + "/vsicurl/http://localhost:%d/test_vsicurl_planetary_computer_url_signing" + % server.port, + "VSICURL_PC_URL_SIGNING", + None, ) - assert statres.size == 3 # Check that signing request is done since it has expired gdal.VSICurlClearCache() @@ -788,14 +803,14 @@ def test_vsicurl_planetary_computer_url_signing(server): % server.port, 200, {}, - '{"msft:expiry":"9999-01-01T00:00:00","href":"http://localhost:%d/test_vsicurl_planetary_computer_url_signing.bin?my_token"}' + '{"msft:expiry":"9999-01-01T00:00:00","href":"http://localhost:%d/test_vsicurl_planetary_computer_url_signing.bin?my_token2"}' % server.port, ) handler.add( "HEAD", - "/test_vsicurl_planetary_computer_url_signing.bin?my_token", + "/test_vsicurl_planetary_computer_url_signing.bin?my_token2", 200, - {"Content-Length": "3"}, + {"Content-Length": "4"}, ) with webserver.install_http_handler(handler): @@ -807,7 +822,7 @@ def test_vsicurl_planetary_computer_url_signing(server): "/vsicurl?pc_url_signing=yes&url=http://localhost:%d/test_vsicurl_planetary_computer_url_signing.bin" % server.port ) - assert statres.size == 3 + assert statres.size == 4 # Check that signing request is not needed gdal.VSICurlClearCache() @@ -815,7 +830,7 @@ def test_vsicurl_planetary_computer_url_signing(server): handler = webserver.SequentialHandler() handler.add( "HEAD", - "/test_vsicurl_planetary_computer_url_signing.bin?my_token", + "/test_vsicurl_planetary_computer_url_signing.bin?my_token2", 200, {"Content-Length": "3"}, ) @@ -838,12 +853,12 @@ def test_vsicurl_planetary_computer_url_signing(server): % server.port, 200, {}, - '{"msft:expiry":"9999-01-01T00:00:00","href":"http://localhost:%d/test_vsicurl_planetary_computer_url_signing2.bin?my_token2"}' + '{"msft:expiry":"9999-01-01T00:00:00","href":"http://localhost:%d/test_vsicurl_planetary_computer_url_signing2.bin?my_token3"}' % server.port, ) handler.add( "HEAD", - "/test_vsicurl_planetary_computer_url_signing2.bin?my_token2", + "/test_vsicurl_planetary_computer_url_signing2.bin?my_token3", 200, {"Content-Length": "4"}, ) @@ -866,14 +881,14 @@ def test_vsicurl_planetary_computer_url_signing(server): handler.add( "HEAD", - "/test_vsicurl_planetary_computer_url_signing.bin?my_token", + "/test_vsicurl_planetary_computer_url_signing.bin?my_token2", 200, {"Content-Length": "3"}, ) handler.add( "HEAD", - "/test_vsicurl_planetary_computer_url_signing2.bin?my_token2", + "/test_vsicurl_planetary_computer_url_signing2.bin?my_token3", 200, {"Content-Length": "4"}, ) diff --git a/doc/source/user/virtual_file_systems.rst b/doc/source/user/virtual_file_systems.rst index 1e36919d9922..dbd62e56ea5c 100644 --- a/doc/source/user/virtual_file_systems.rst +++ b/doc/source/user/virtual_file_systems.rst @@ -390,7 +390,7 @@ Starting with GDAL 2.3, options can be passed in the filename with the following - proxy=value - proxyauth=value - proxyuserpwd=value -- pc_url_signing=yes/no: whether to use the URL signing mechanism of Microsoft Planetary Computer (https://planetarycomputer.microsoft.com/docs/concepts/sas/). (GDAL >= 3.5.2) +- pc_url_signing=yes/no: whether to use the URL signing mechanism of Microsoft Planetary Computer (https://planetarycomputer.microsoft.com/docs/concepts/sas/). (GDAL >= 3.5.2). Note that starting with GDAL 3.9, this may also be set with the path-specific option ( cf :cpp:func:`VSISetPathSpecificOption`) ``VSICURL_PC_URL_SIGNING`` set to ``YES``. - pc_collection=name: name of the collection of the dataset for Planetary Computer URL signing. Only used when pc_url_signing=yes. (GDAL >= 3.5.2) Partial downloads (requires the HTTP server to support random reading) are done with a 16 KB granularity by default. Starting with GDAL 2.3, the chunk size can be configured with the :config:`CPL_VSIL_CURL_CHUNK_SIZE` configuration option, with a value in bytes. If the driver detects sequential reading, it will progressively increase the chunk size up to 128 times :config:`CPL_VSIL_CURL_CHUNK_SIZE` (so 2 MB by default) to improve download performance. diff --git a/port/cpl_vsil_curl.cpp b/port/cpl_vsil_curl.cpp index 9063da67925e..9da0dce4b67a 100644 --- a/port/cpl_vsil_curl.cpp +++ b/port/cpl_vsil_curl.cpp @@ -310,6 +310,19 @@ static std::string VSICurlGetURLFromFilename( if (!STARTS_WITH(pszFilename, "/vsicurl/") && !STARTS_WITH(pszFilename, "/vsicurl?")) return pszFilename; + + if (pbPlanetaryComputerURLSigning) + { + // It may be more convenient sometimes to store Planetary Computer URL + // signing as a per-path specific option rather than capturing it in + // the filename with the &pc_url_signing=yes option. + if (CPLTestBool(VSIGetPathSpecificOption( + pszFilename, "VSICURL_PC_URL_SIGNING", "FALSE"))) + { + *pbPlanetaryComputerURLSigning = true; + } + } + pszFilename += strlen("/vsicurl/"); if (!STARTS_WITH(pszFilename, "http://") && !STARTS_WITH(pszFilename, "https://") && From ba0e38625ce70e31fd85fa60d4e11d4daddcc0ab Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 8 Feb 2024 19:43:23 +0100 Subject: [PATCH 028/132] Add CPLXMLNodeGetRAMUsageEstimate() --- autotest/cpp/test_cpl.cpp | 1 + port/cpl_minixml.cpp | 35 +++++++++++++++++++++++++++++++++++ port/cpl_minixml.h | 2 ++ 3 files changed, 38 insertions(+) diff --git a/autotest/cpp/test_cpl.cpp b/autotest/cpp/test_cpl.cpp index f2ab0ed538cc..5c655d4b0cb4 100644 --- a/autotest/cpp/test_cpl.cpp +++ b/autotest/cpp/test_cpl.cpp @@ -3128,6 +3128,7 @@ TEST_F(test_cpl, cpl_minixml) CPLXMLNode *psElt = CPLCreateXMLElementAndValue(psRoot, "Elt", "value"); CPLAddXMLAttributeAndValue(psElt, "attr1", "val1"); CPLAddXMLAttributeAndValue(psElt, "attr2", "val2"); + EXPECT_GE(CPLXMLNodeGetRAMUsageEstimate(psRoot), 0); char *str = CPLSerializeXMLTree(psRoot); CPLDestroyXMLNode(psRoot); ASSERT_STREQ( diff --git a/port/cpl_minixml.cpp b/port/cpl_minixml.cpp index bb623779619e..d4690a4e0fbf 100644 --- a/port/cpl_minixml.cpp +++ b/port/cpl_minixml.cpp @@ -2262,6 +2262,41 @@ void CPLCleanXMLElementName(char *pszTarget) } } +/************************************************************************/ +/* CPLXMLNodeGetRAMUsageEstimate() */ +/************************************************************************/ + +static size_t CPLXMLNodeGetRAMUsageEstimate(const CPLXMLNode *psNode, + bool bVisitSiblings) +{ + size_t nRet = sizeof(CPLXMLNode); + // malloc() aligns on 16-byte boundaries on 64 bit. + nRet += std::max(2 * sizeof(void *), strlen(psNode->pszValue) + 1); + if (bVisitSiblings) + { + for (const CPLXMLNode *psIter = psNode->psNext; psIter; + psIter = psIter->psNext) + { + nRet += CPLXMLNodeGetRAMUsageEstimate(psIter, false); + } + } + if (psNode->psChild) + { + nRet += CPLXMLNodeGetRAMUsageEstimate(psNode->psChild, true); + } + return nRet; +} + +/** Return a conservative estimate of the RAM usage of this node, its children + * and siblings. The returned values is in bytes. + * + * @since 3.9 + */ +size_t CPLXMLNodeGetRAMUsageEstimate(const CPLXMLNode *psNode) +{ + return CPLXMLNodeGetRAMUsageEstimate(psNode, true); +} + /************************************************************************/ /* CPLXMLTreeCloser::getDocumentElement() */ /************************************************************************/ diff --git a/port/cpl_minixml.h b/port/cpl_minixml.h index d0b57194a13e..d0cdcffdcd26 100644 --- a/port/cpl_minixml.h +++ b/port/cpl_minixml.h @@ -177,6 +177,8 @@ CPLXMLNode CPL_DLL *CPLParseXMLFile(const char *pszFilename); int CPL_DLL CPLSerializeXMLTreeToFile(const CPLXMLNode *psTree, const char *pszFilename); +size_t CPL_DLL CPLXMLNodeGetRAMUsageEstimate(const CPLXMLNode *psNode); + CPL_C_END #if defined(__cplusplus) && !defined(CPL_SUPRESS_CPLUSPLUS) From 9141db47fbbceb200c93e32370da3a3823aa6cfc Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 8 Feb 2024 19:43:58 +0100 Subject: [PATCH 029/132] VRT serialization: emit a warning if RAM usage of XML serialization reaches 80% of RAM (fixes #9212) --- frmts/vrt/vrtdataset.cpp | 11 +++++-- frmts/vrt/vrtdataset.h | 24 +++++++++++---- frmts/vrt/vrtderivedrasterband.cpp | 7 +++-- frmts/vrt/vrtpansharpened.cpp | 8 +++-- frmts/vrt/vrtrasterband.cpp | 9 ++++-- frmts/vrt/vrtrawrasterband.cpp | 7 +++-- frmts/vrt/vrtsourcedrasterband.cpp | 47 ++++++++++++++++++++++++------ frmts/vrt/vrtwarped.cpp | 7 +++-- 8 files changed, 92 insertions(+), 28 deletions(-) diff --git a/frmts/vrt/vrtdataset.cpp b/frmts/vrt/vrtdataset.cpp index 69e65fa68ae6..716137dd58ee 100644 --- a/frmts/vrt/vrtdataset.cpp +++ b/frmts/vrt/vrtdataset.cpp @@ -328,10 +328,14 @@ CPLXMLNode *VRTDataset::SerializeToXML(const char *pszVRTPathIn) { } CPLAssert(psLastChild); // we have at least rasterXSize + bool bHasWarnedAboutRAMUsage = false; + size_t nAccRAMUsage = 0; for (int iBand = 0; iBand < nBands; iBand++) { - CPLXMLNode *psBandTree = static_cast(papoBands[iBand]) - ->SerializeToXML(pszVRTPathIn); + CPLXMLNode *psBandTree = + static_cast(papoBands[iBand]) + ->SerializeToXML(pszVRTPathIn, bHasWarnedAboutRAMUsage, + nAccRAMUsage); if (psBandTree != nullptr) { @@ -345,7 +349,8 @@ CPLXMLNode *VRTDataset::SerializeToXML(const char *pszVRTPathIn) /* -------------------------------------------------------------------- */ if (m_poMaskBand) { - CPLXMLNode *psBandTree = m_poMaskBand->SerializeToXML(pszVRTPathIn); + CPLXMLNode *psBandTree = m_poMaskBand->SerializeToXML( + pszVRTPathIn, bHasWarnedAboutRAMUsage, nAccRAMUsage); if (psBandTree != nullptr) { diff --git a/frmts/vrt/vrtdataset.h b/frmts/vrt/vrtdataset.h index 59dc1ecf91af..cb6dfbd2ebb2 100644 --- a/frmts/vrt/vrtdataset.h +++ b/frmts/vrt/vrtdataset.h @@ -565,7 +565,9 @@ class CPL_DLL VRTRasterBand CPL_NON_FINAL : public GDALRasterBand virtual CPLErr XMLInit(CPLXMLNode *, const char *, std::map &); - virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath); + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage); CPLErr SetNoDataValue(double) override; CPLErr SetNoDataValueAsInt64(int64_t nNoData) override; @@ -705,7 +707,9 @@ class CPL_DLL VRTSourcedRasterBand CPL_NON_FINAL : public VRTRasterBand virtual CPLErr XMLInit(CPLXMLNode *, const char *, std::map &) override; - virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) override; virtual double GetMinimum(int *pbSuccess = nullptr) override; virtual double GetMaximum(int *pbSuccess = nullptr) override; @@ -804,7 +808,9 @@ class CPL_DLL VRTWarpedRasterBand final : public VRTRasterBand GDALDataType eType = GDT_Unknown); virtual ~VRTWarpedRasterBand(); - virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) override; virtual CPLErr IReadBlock(int, int, void *) override; virtual CPLErr IWriteBlock(int, int, void *) override; @@ -825,7 +831,9 @@ class VRTPansharpenedRasterBand final : public VRTRasterBand GDALDataType eDataType = GDT_Unknown); virtual ~VRTPansharpenedRasterBand(); - virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) override; virtual CPLErr IReadBlock(int, int, void *) override; @@ -906,7 +914,9 @@ class CPL_DLL VRTDerivedRasterBand CPL_NON_FINAL : public VRTSourcedRasterBand virtual CPLErr XMLInit(CPLXMLNode *, const char *, std::map &) override; - virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) override; virtual double GetMinimum(int *pbSuccess = nullptr) override; virtual double GetMaximum(int *pbSuccess = nullptr) override; @@ -947,7 +957,9 @@ class CPL_DLL VRTRawRasterBand CPL_NON_FINAL : public VRTRasterBand virtual CPLErr XMLInit(CPLXMLNode *, const char *, std::map &) override; - virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; + virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) override; virtual CPLErr IRasterIO(GDALRWFlag, int, int, int, int, void *, int, int, GDALDataType, GSpacing nPixelSpace, diff --git a/frmts/vrt/vrtderivedrasterband.cpp b/frmts/vrt/vrtderivedrasterband.cpp index e2de0b4fbca7..bfc3d3aca48a 100644 --- a/frmts/vrt/vrtderivedrasterband.cpp +++ b/frmts/vrt/vrtderivedrasterband.cpp @@ -1487,9 +1487,12 @@ CPLErr VRTDerivedRasterBand::XMLInit( /* SerializeToXML() */ /************************************************************************/ -CPLXMLNode *VRTDerivedRasterBand::SerializeToXML(const char *pszVRTPath) +CPLXMLNode *VRTDerivedRasterBand::SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) { - CPLXMLNode *psTree = VRTSourcedRasterBand::SerializeToXML(pszVRTPath); + CPLXMLNode *psTree = VRTSourcedRasterBand::SerializeToXML( + pszVRTPath, bHasWarnedAboutRAMUsage, nAccRAMUsage); /* -------------------------------------------------------------------- */ /* Set subclass. */ diff --git a/frmts/vrt/vrtpansharpened.cpp b/frmts/vrt/vrtpansharpened.cpp index 1dc2f8568ebc..cecf614d0710 100644 --- a/frmts/vrt/vrtpansharpened.cpp +++ b/frmts/vrt/vrtpansharpened.cpp @@ -1747,10 +1747,14 @@ CPLErr VRTPansharpenedRasterBand::IRasterIO( /* SerializeToXML() */ /************************************************************************/ -CPLXMLNode *VRTPansharpenedRasterBand::SerializeToXML(const char *pszVRTPathIn) +CPLXMLNode * +VRTPansharpenedRasterBand::SerializeToXML(const char *pszVRTPathIn, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) { - CPLXMLNode *psTree = VRTRasterBand::SerializeToXML(pszVRTPathIn); + CPLXMLNode *psTree = VRTRasterBand::SerializeToXML( + pszVRTPathIn, bHasWarnedAboutRAMUsage, nAccRAMUsage); /* -------------------------------------------------------------------- */ /* Set subclass. */ diff --git a/frmts/vrt/vrtrasterband.cpp b/frmts/vrt/vrtrasterband.cpp index ffdc2b75ec10..37bfd0c18312 100644 --- a/frmts/vrt/vrtrasterband.cpp +++ b/frmts/vrt/vrtrasterband.cpp @@ -646,7 +646,9 @@ CPLString VRTSerializeNoData(double dfVal, GDALDataType eDataType, /* SerializeToXML() */ /************************************************************************/ -CPLXMLNode *VRTRasterBand::SerializeToXML(const char *pszVRTPath) +CPLXMLNode *VRTRasterBand::SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) { CPLXMLNode *psTree = @@ -830,9 +832,12 @@ CPLXMLNode *VRTRasterBand::SerializeToXML(const char *pszVRTPath) /* Mask band (specific to that raster band) */ /* ==================================================================== */ + nAccRAMUsage += CPLXMLNodeGetRAMUsageEstimate(psTree); + if (m_poMaskBand != nullptr) { - CPLXMLNode *psBandTree = m_poMaskBand->SerializeToXML(pszVRTPath); + CPLXMLNode *psBandTree = m_poMaskBand->SerializeToXML( + pszVRTPath, bHasWarnedAboutRAMUsage, nAccRAMUsage); if (psBandTree != nullptr) { diff --git a/frmts/vrt/vrtrawrasterband.cpp b/frmts/vrt/vrtrawrasterband.cpp index f0ef87399d3d..0c7086effe10 100644 --- a/frmts/vrt/vrtrawrasterband.cpp +++ b/frmts/vrt/vrtrawrasterband.cpp @@ -429,7 +429,9 @@ VRTRawRasterBand::XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, /* SerializeToXML() */ /************************************************************************/ -CPLXMLNode *VRTRawRasterBand::SerializeToXML(const char *pszVRTPath) +CPLXMLNode *VRTRawRasterBand::SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) { @@ -444,7 +446,8 @@ CPLXMLNode *VRTRawRasterBand::SerializeToXML(const char *pszVRTPath) return nullptr; } - CPLXMLNode *psTree = VRTRasterBand::SerializeToXML(pszVRTPath); + CPLXMLNode *psTree = VRTRasterBand::SerializeToXML( + pszVRTPath, bHasWarnedAboutRAMUsage, nAccRAMUsage); /* -------------------------------------------------------------------- */ /* Set subclass. */ diff --git a/frmts/vrt/vrtsourcedrasterband.cpp b/frmts/vrt/vrtsourcedrasterband.cpp index b9a12c9256e3..0067f27abfac 100644 --- a/frmts/vrt/vrtsourcedrasterband.cpp +++ b/frmts/vrt/vrtsourcedrasterband.cpp @@ -1861,10 +1861,13 @@ CPLErr VRTSourcedRasterBand::XMLInit( /* SerializeToXML() */ /************************************************************************/ -CPLXMLNode *VRTSourcedRasterBand::SerializeToXML(const char *pszVRTPath) +CPLXMLNode *VRTSourcedRasterBand::SerializeToXML(const char *pszVRTPath, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) { - CPLXMLNode *psTree = VRTRasterBand::SerializeToXML(pszVRTPath); + CPLXMLNode *psTree = VRTRasterBand::SerializeToXML( + pszVRTPath, bHasWarnedAboutRAMUsage, nAccRAMUsage); CPLXMLNode *psLastChild = psTree->psChild; while (psLastChild != nullptr && psLastChild->psNext != nullptr) psLastChild = psLastChild->psNext; @@ -1872,19 +1875,45 @@ CPLXMLNode *VRTSourcedRasterBand::SerializeToXML(const char *pszVRTPath) /* -------------------------------------------------------------------- */ /* Process Sources. */ /* -------------------------------------------------------------------- */ + + GIntBig nUsableRAM = -1; + for (int iSource = 0; iSource < nSources; iSource++) { CPLXMLNode *const psXMLSrc = papoSources[iSource]->SerializeToXML(pszVRTPath); - if (psXMLSrc != nullptr) - { - if (psLastChild == nullptr) - psTree->psChild = psXMLSrc; - else - psLastChild->psNext = psXMLSrc; - psLastChild = psXMLSrc; + // Creating the CPLXMLNode tree representation of a VRT can easily + // take several times RAM usage than its string serialization, or its + // internal representation in the driver. + // We multiply the estimate by a factor of 2, experimentally found to + // be more realistic than the conservative raw estimate. + nAccRAMUsage += 2 * CPLXMLNodeGetRAMUsageEstimate(psXMLSrc); + if (!bHasWarnedAboutRAMUsage && nAccRAMUsage > 512 * 1024 * 1024) + { + if (nUsableRAM < 0) + nUsableRAM = CPLGetUsablePhysicalRAM(); + if (nUsableRAM > 0 && + nAccRAMUsage > static_cast(nUsableRAM) / 10 * 8) + { + bHasWarnedAboutRAMUsage = true; + CPLError(CE_Warning, CPLE_AppDefined, + "Serialization of this VRT file has already consumed " + "at least %.02f GB of RAM over a total of %.02f. This " + "process may abort", + double(nAccRAMUsage) / (1024 * 1024 * 1024), + double(nUsableRAM) / (1024 * 1024 * 1024)); + } } + + if (psXMLSrc == nullptr) + break; + + if (psLastChild == nullptr) + psTree->psChild = psXMLSrc; + else + psLastChild->psNext = psXMLSrc; + psLastChild = psXMLSrc; } return psTree; diff --git a/frmts/vrt/vrtwarped.cpp b/frmts/vrt/vrtwarped.cpp index cc5a93e5bd8d..84a95d63e8ac 100644 --- a/frmts/vrt/vrtwarped.cpp +++ b/frmts/vrt/vrtwarped.cpp @@ -1929,10 +1929,13 @@ CPLErr VRTWarpedRasterBand::IWriteBlock(int nBlockXOff, int nBlockYOff, /* SerializeToXML() */ /************************************************************************/ -CPLXMLNode *VRTWarpedRasterBand::SerializeToXML(const char *pszVRTPathIn) +CPLXMLNode *VRTWarpedRasterBand::SerializeToXML(const char *pszVRTPathIn, + bool &bHasWarnedAboutRAMUsage, + size_t &nAccRAMUsage) { - CPLXMLNode *const psTree = VRTRasterBand::SerializeToXML(pszVRTPathIn); + CPLXMLNode *const psTree = VRTRasterBand::SerializeToXML( + pszVRTPathIn, bHasWarnedAboutRAMUsage, nAccRAMUsage); /* -------------------------------------------------------------------- */ /* Set subclass. */ From 29316fcdd260c6a972fdeda0afa51fbf36683ef2 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 9 Feb 2024 12:31:45 +0100 Subject: [PATCH 030/132] [Lint] SQLite: modernize SRS cache --- ogr/ogrsf_frmts/sqlite/ogr_sqlite.h | 13 ++-- .../sqlite/ogrsqlitedatasource.cpp | 72 +++++++++---------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h b/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h index f933afb954e4..1180c44a241e 100644 --- a/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h +++ b/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h @@ -641,11 +641,14 @@ class OGRSQLiteDataSource final : public OGRSQLiteBaseDataSource // We maintain a list of known SRID to reduce the number of trips to // the database to get SRSes. - int m_nKnownSRID = 0; - int *m_panSRID = nullptr; - OGRSpatialReference **m_papoSRS = nullptr; - - void AddSRIDToCache(int nId, OGRSpatialReference *poSRS); + std::map> + m_oSRSCache{}; + + OGRSpatialReference *AddSRIDToCache( + int nId, + std::unique_ptr + &&poSRS); bool m_bHaveGeometryColumns = false; bool m_bIsSpatiaLiteDB = false; diff --git a/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp b/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp index 50a56f5ad9cf..65cd215ff3cb 100644 --- a/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp +++ b/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp @@ -910,13 +910,7 @@ CPLErr OGRSQLiteDataSource::Close() CPLFree(m_papoLayers); - for (int i = 0; i < m_nKnownSRID; i++) - { - if (m_papoSRS[i] != nullptr) - m_papoSRS[i]->Release(); - } - CPLFree(m_panSRID); - CPLFree(m_papoSRS); + m_oSRSCache.clear(); if (!CloseDB()) eErr = CE_Failure; @@ -4102,17 +4096,15 @@ const char *OGRSQLiteDataSource::GetSRTEXTColName() /* sure it is freshly created, or add a reference yourself if not. */ /************************************************************************/ -void OGRSQLiteDataSource::AddSRIDToCache(int nId, OGRSpatialReference *poSRS) +OGRSpatialReference *OGRSQLiteDataSource::AddSRIDToCache( + int nId, + std::unique_ptr &&poSRS) { /* -------------------------------------------------------------------- */ /* Add to the cache. */ /* -------------------------------------------------------------------- */ - m_panSRID = (int *)CPLRealloc(m_panSRID, sizeof(int) * (m_nKnownSRID + 1)); - m_papoSRS = (OGRSpatialReference **)CPLRealloc( - m_papoSRS, sizeof(void *) * (m_nKnownSRID + 1)); - m_panSRID[m_nKnownSRID] = nId; - m_papoSRS[m_nKnownSRID] = poSRS; - m_nKnownSRID++; + auto oIter = m_oSRSCache.emplace(nId, std::move(poSRS)).first; + return oIter->second.get(); } /************************************************************************/ @@ -4132,15 +4124,15 @@ int OGRSQLiteDataSource::FetchSRSId(const OGRSpatialReference *poSRS) /* -------------------------------------------------------------------- */ /* First, we look through our SRID cache, is it there? */ /* -------------------------------------------------------------------- */ - for (int i = 0; i < m_nKnownSRID; i++) + for (const auto &pair : m_oSRSCache) { - if (m_papoSRS[i] == poSRS) - return m_panSRID[i]; + if (pair.second.get() == poSRS) + return pair.first; } - for (int i = 0; i < m_nKnownSRID; i++) + for (const auto &pair : m_oSRSCache) { - if (m_papoSRS[i] != nullptr && m_papoSRS[i]->IsSame(poSRS)) - return m_panSRID[i]; + if (pair.second != nullptr && pair.second->IsSame(poSRS)) + return pair.first; } /* -------------------------------------------------------------------- */ @@ -4251,10 +4243,12 @@ int OGRSQLiteDataSource::FetchSRSId(const OGRSpatialReference *poSRS) if (nSRSId != m_nUndefinedSRID) { - auto poCachedSRS = new OGRSpatialReference(oSRS); + std::unique_ptr + poCachedSRS(new OGRSpatialReference(oSRS)); poCachedSRS->SetAxisMappingStrategy( OAMS_TRADITIONAL_GIS_ORDER); - AddSRIDToCache(nSRSId, poCachedSRS); + AddSRIDToCache(nSRSId, std::move(poCachedSRS)); } return nSRSId; @@ -4352,7 +4346,12 @@ int OGRSQLiteDataSource::FetchSRSId(const OGRSpatialReference *poSRS) sqlite3_finalize(hSelectStmt); if (nSRSId != m_nUndefinedSRID) - AddSRIDToCache(nSRSId, new OGRSpatialReference(oSRS)); + { + auto poSRSClone = std::unique_ptr( + new OGRSpatialReference(oSRS)); + AddSRIDToCache(nSRSId, std::move(poSRSClone)); + } return nSRSId; } @@ -4584,9 +4583,10 @@ int OGRSQLiteDataSource::FetchSRSId(const OGRSpatialReference *poSRS) if (nSRSId != m_nUndefinedSRID) { - auto poCachedSRS = new OGRSpatialReference(std::move(oSRS)); + std::unique_ptr + poCachedSRS(new OGRSpatialReference(std::move(oSRS))); poCachedSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); - AddSRIDToCache(nSRSId, poCachedSRS); + AddSRIDToCache(nSRSId, std::move(poCachedSRS)); } return nSRSId; @@ -4609,10 +4609,10 @@ OGRSpatialReference *OGRSQLiteDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* First, we look through our SRID cache, is it there? */ /* -------------------------------------------------------------------- */ - for (int i = 0; i < m_nKnownSRID; i++) + auto oIter = m_oSRSCache.find(nId); + if (oIter != m_oSRSCache.end()) { - if (m_panSRID[i] == nId) - return m_papoSRS[i]; + return oIter->second.get(); } /* -------------------------------------------------------------------- */ @@ -4622,7 +4622,7 @@ OGRSpatialReference *OGRSQLiteDataSource::FetchSRS(int nId) char **papszResult = nullptr; int nRowCount = 0; int nColCount = 0; - OGRSpatialReference *poSRS = nullptr; + std::unique_ptr poSRS; CPLString osCommand; osCommand.Printf("SELECT srtext FROM spatial_ref_sys WHERE srid = %d " @@ -4649,12 +4649,11 @@ OGRSpatialReference *OGRSQLiteDataSource::FetchSRS(int nId) /* Translate into a spatial reference. */ /* -------------------------------------------------------------------- */ - poSRS = new OGRSpatialReference(); + poSRS.reset(new OGRSpatialReference()); poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); if (poSRS->importFromWkt(osWKT.c_str()) != OGRERR_NONE) { - delete poSRS; - poSRS = nullptr; + poSRS.reset(); } } @@ -4706,7 +4705,7 @@ OGRSpatialReference *OGRSQLiteDataSource::FetchSRS(int nId) const char *pszWKT = (pszSRTEXTColName != nullptr) ? papszRow[3] : nullptr; - poSRS = new OGRSpatialReference(); + poSRS.reset(new OGRSpatialReference()); poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); /* Try first from EPSG code */ @@ -4729,8 +4728,7 @@ OGRSpatialReference *OGRSQLiteDataSource::FetchSRS(int nId) } else { - delete poSRS; - poSRS = nullptr; + poSRS.reset(); } sqlite3_free_table(papszResult); @@ -4756,9 +4754,7 @@ OGRSpatialReference *OGRSQLiteDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* Add to the cache. */ /* -------------------------------------------------------------------- */ - AddSRIDToCache(nId, poSRS); - - return poSRS; + return AddSRIDToCache(nId, std::move(poSRS)); } /************************************************************************/ From a9ab5fb8440b317dc6d893f1bc632ad5d19d709a Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 9 Feb 2024 12:31:59 +0100 Subject: [PATCH 031/132] [Lint] ODBC: modernize SRS cache --- ogr/ogrsf_frmts/odbc/ogr_odbc.h | 12 +++++-- ogr/ogrsf_frmts/odbc/ogrodbcdatasource.cpp | 40 +++++----------------- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/ogr/ogrsf_frmts/odbc/ogr_odbc.h b/ogr/ogrsf_frmts/odbc/ogr_odbc.h index 75d37a829c4a..3688dad950c6 100644 --- a/ogr/ogrsf_frmts/odbc/ogr_odbc.h +++ b/ogr/ogrsf_frmts/odbc/ogr_odbc.h @@ -33,6 +33,8 @@ #include "ogrsf_frmts.h" #include "cpl_odbc.h" #include "cpl_error.h" + +#include #include /************************************************************************/ @@ -190,11 +192,15 @@ class OGRODBCDataSource final : public OGRDataSource CPLODBCSession oSession; +#if 0 + // NOTE: nothing uses the SRS cache currently. Hence disabled. + // We maintain a list of known SRID to reduce the number of trips to // the database to get SRSes. - int nKnownSRID; - int *panSRID; - OGRSpatialReference **papoSRS; + std::map> + m_oSRSCache{}; +#endif // set of all lowercase table names. Note that this is only used when // opening MDB datasources, not generic ODBC ones. diff --git a/ogr/ogrsf_frmts/odbc/ogrodbcdatasource.cpp b/ogr/ogrsf_frmts/odbc/ogrodbcdatasource.cpp index 261b9b001b9f..1e56c07fd572 100644 --- a/ogr/ogrsf_frmts/odbc/ogrodbcdatasource.cpp +++ b/ogr/ogrsf_frmts/odbc/ogrodbcdatasource.cpp @@ -37,8 +37,7 @@ /************************************************************************/ OGRODBCDataSource::OGRODBCDataSource() - : papoLayers(nullptr), nLayers(0), pszName(nullptr), nKnownSRID(0), - panSRID(nullptr), papoSRS(nullptr) + : papoLayers(nullptr), nLayers(0), pszName(nullptr) { } @@ -55,14 +54,6 @@ OGRODBCDataSource::~OGRODBCDataSource() delete papoLayers[i]; CPLFree(papoLayers); - - for (int i = 0; i < nKnownSRID; i++) - { - if (papoSRS[i] != nullptr) - papoSRS[i]->Release(); - } - CPLFree(panSRID); - CPLFree(papoSRS); } /************************************************************************/ @@ -415,6 +406,9 @@ int OGRODBCDataSource::Open(GDALOpenInfo *poOpenInfo) CSLDestroy(papszTables); CSLDestroy(papszGeomCol); +#if 0 + // NOTE: nothing uses the SRS cache currently. Hence disabled. + /* -------------------------------------------------------------------- */ /* If no explicit list of tables was given, check for a list in */ /* a geometry_columns table. */ @@ -439,11 +433,6 @@ int OGRODBCDataSource::Open(GDALOpenInfo *poOpenInfo) oSRSList.GetCommand()); if (oSRSList.ExecuteSQL()) { - int nRows = 256; // A reasonable number of SRIDs to start from - panSRID = (int *)CPLMalloc(nRows * sizeof(int)); - papoSRS = (OGRSpatialReference **)CPLMalloc( - nRows * sizeof(OGRSpatialReference *)); - while (oSRSList.Fetch()) { const char *pszSRID = oSRSList.GetColData(pszSRIDCol); @@ -454,29 +443,18 @@ int OGRODBCDataSource::Open(GDALOpenInfo *poOpenInfo) if (pszSRText) { - if (nKnownSRID > nRows) - { - nRows *= 2; - panSRID = - (int *)CPLRealloc(panSRID, nRows * sizeof(int)); - papoSRS = (OGRSpatialReference **)CPLRealloc( - papoSRS, nRows * sizeof(OGRSpatialReference *)); - } - panSRID[nKnownSRID] = atoi(pszSRID); - papoSRS[nKnownSRID] = new OGRSpatialReference(); - papoSRS[nKnownSRID]->SetAxisMappingStrategy( + std::unique_ptr poSRS(new OGRSpatialReference()); + poSRS->SetAxisMappingStrategy( OAMS_TRADITIONAL_GIS_ORDER); - if (papoSRS[nKnownSRID]->importFromWkt(pszSRText) != - OGRERR_NONE) + if (poSRS->importFromWkt(pszSRText) == OGRERR_NONE ) { - delete papoSRS[nKnownSRID]; - continue; + m_oSRSCache[atoi(pszSRID)] = std::move(poSRS); } - nKnownSRID++; } } } } +#endif if (pszSRIDCol) CPLFree(pszSRIDCol); From 48acfb785d59234e3566d4ab5b10faba707e8021 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 9 Feb 2024 12:32:18 +0100 Subject: [PATCH 032/132] [Lint] MySQL: modernize SRS cache --- ogr/ogrsf_frmts/mysql/ogr_mysql.h | 10 +++--- ogr/ogrsf_frmts/mysql/ogrmysqldatasource.cpp | 37 ++++++-------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/ogr/ogrsf_frmts/mysql/ogr_mysql.h b/ogr/ogrsf_frmts/mysql/ogr_mysql.h index 6b96654bf9e7..71fd55181490 100644 --- a/ogr/ogrsf_frmts/mysql/ogr_mysql.h +++ b/ogr/ogrsf_frmts/mysql/ogr_mysql.h @@ -70,6 +70,8 @@ #include "ogrsf_frmts.h" +#include + class OGRMySQLDataSource; /************************************************************************/ @@ -259,9 +261,9 @@ class OGRMySQLDataSource final : public OGRDataSource // We maintain a list of known SRID to reduce the number of trips to // the database to get SRSes. - int nKnownSRID; - int *panSRID; - OGRSpatialReference **papoSRS; + std::map> + m_oSRSCache{}; OGRMySQLLayer *poLongResultLayer; @@ -280,7 +282,7 @@ class OGRMySQLDataSource final : public OGRDataSource int FetchSRSId(const OGRSpatialReference *poSRS); - OGRSpatialReference *FetchSRS(int nSRSId); + const OGRSpatialReference *FetchSRS(int nSRSId); OGRErr InitializeMetadataTables(); OGRErr UpdateMetadataTables(const char *pszLayerName, diff --git a/ogr/ogrsf_frmts/mysql/ogrmysqldatasource.cpp b/ogr/ogrsf_frmts/mysql/ogrmysqldatasource.cpp index fa7e1aa1cb2f..05fd14918c4f 100644 --- a/ogr/ogrsf_frmts/mysql/ogrmysqldatasource.cpp +++ b/ogr/ogrsf_frmts/mysql/ogrmysqldatasource.cpp @@ -53,8 +53,7 @@ inline void FreeResultAndNullify(MYSQL_RES *&hResult) OGRMySQLDataSource::OGRMySQLDataSource() : papoLayers(nullptr), nLayers(0), pszName(nullptr), bDSUpdate(FALSE), - hConn(nullptr), nKnownSRID(0), panSRID(nullptr), papoSRS(nullptr), - poLongResultLayer(nullptr) + hConn(nullptr), poLongResultLayer(nullptr) { } @@ -76,14 +75,6 @@ OGRMySQLDataSource::~OGRMySQLDataSource() if (hConn != nullptr) mysql_close(hConn); - - for (int i = 0; i < nKnownSRID; i++) - { - if (papoSRS[i] != nullptr) - papoSRS[i]->Release(); - } - CPLFree(panSRID); - CPLFree(papoSRS); } /************************************************************************/ @@ -557,7 +548,7 @@ OGRErr OGRMySQLDataSource::UpdateMetadataTables(const char *pszLayerName, /* OGRSpatialReference, as handles may be cached. */ /************************************************************************/ -OGRSpatialReference *OGRMySQLDataSource::FetchSRS(int nId) +const OGRSpatialReference *OGRMySQLDataSource::FetchSRS(int nId) { if (nId < 0) return nullptr; @@ -565,14 +556,12 @@ OGRSpatialReference *OGRMySQLDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* First, we look through our SRID cache, is it there? */ /* -------------------------------------------------------------------- */ - for (int i = 0; i < nKnownSRID; i++) + auto oIter = m_oSRSCache.find(nId); + if (oIter != m_oSRSCache.end()) { - if (panSRID[i] == nId) - return papoSRS[i]; + return oIter->second.get(); } - OGRSpatialReference *poSRS = nullptr; - // make sure to attempt to free any old results MYSQL_RES *hResult = mysql_store_result(GetConn()); FreeResultAndNullify(hResult); @@ -608,12 +597,12 @@ OGRSpatialReference *OGRMySQLDataSource::FetchSRS(int nId) FreeResultAndNullify(hResult); - poSRS = new OGRSpatialReference(); + std::unique_ptr poSRS( + new OGRSpatialReference()); poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); if (pszWKT == nullptr || poSRS->importFromWkt(pszWKT) != OGRERR_NONE) { - delete poSRS; - poSRS = nullptr; + poSRS.reset(); } CPLFree(pszWKT); @@ -635,14 +624,8 @@ OGRSpatialReference *OGRMySQLDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* Add to the cache. */ /* -------------------------------------------------------------------- */ - panSRID = (int *)CPLRealloc(panSRID, sizeof(int) * (nKnownSRID + 1)); - papoSRS = (OGRSpatialReference **)CPLRealloc(papoSRS, sizeof(void *) * - (nKnownSRID + 1)); - panSRID[nKnownSRID] = nId; - papoSRS[nKnownSRID] = poSRS; - nKnownSRID++; - - return poSRS; + oIter = m_oSRSCache.emplace(nId, std::move(poSRS)).first; + return oIter->second.get(); } /************************************************************************/ From b0fe4c072fc4494adaa2f6ea5c3df3a8fc9fc094 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 9 Feb 2024 12:32:46 +0100 Subject: [PATCH 033/132] [Lint] MSSQLSpatial: modernize SRS cache --- .../mssqlspatial/ogr_mssqlspatial.h | 13 ++- .../ogrmssqlspatialdatasource.cpp | 89 +++++++------------ 2 files changed, 40 insertions(+), 62 deletions(-) diff --git a/ogr/ogrsf_frmts/mssqlspatial/ogr_mssqlspatial.h b/ogr/ogrsf_frmts/mssqlspatial/ogr_mssqlspatial.h index 19fb24c82ce4..85d290be1b99 100644 --- a/ogr/ogrsf_frmts/mssqlspatial/ogr_mssqlspatial.h +++ b/ogr/ogrsf_frmts/mssqlspatial/ogr_mssqlspatial.h @@ -41,6 +41,8 @@ #include "include_msodbcsql.h" #endif +#include + class OGRMSSQLSpatialDataSource; /* layer status */ @@ -598,9 +600,9 @@ class OGRMSSQLSpatialDataSource final : public OGRDataSource // We maintain a list of known SRID to reduce the number of trips to // the database to get SRSes. - int nKnownSRID; - int *panSRID; - OGRSpatialReference **papoSRS; + std::map> + m_oSRSCache{}; OGRMSSQLSpatialTableLayer *poLayerInCopyMode; @@ -666,7 +668,10 @@ class OGRMSSQLSpatialDataSource final : public OGRDataSource static char *LaunderName(const char *pszSrcName); OGRErr InitializeMetadataTables(); - void AddSRIDToCache(int nId, OGRSpatialReference *poSRS); + OGRSpatialReference *AddSRIDToCache( + int nId, + std::unique_ptr + &&poSRS); OGRSpatialReference *FetchSRS(int nId); int FetchSRSId(const OGRSpatialReference *poSRS); diff --git a/ogr/ogrsf_frmts/mssqlspatial/ogrmssqlspatialdatasource.cpp b/ogr/ogrsf_frmts/mssqlspatial/ogrmssqlspatialdatasource.cpp index e09df6fc3f32..c06cb7b2cf67 100644 --- a/ogr/ogrsf_frmts/mssqlspatial/ogrmssqlspatialdatasource.cpp +++ b/ogr/ogrsf_frmts/mssqlspatial/ogrmssqlspatialdatasource.cpp @@ -40,10 +40,6 @@ OGRMSSQLSpatialDataSource::OGRMSSQLSpatialDataSource() : bDSUpdate(false) papoLayers = nullptr; nLayers = 0; - nKnownSRID = 0; - panSRID = nullptr; - papoSRS = nullptr; - poLayerInCopyMode = nullptr; nGeometryFormat = MSSQLGEOMETRY_NATIVE; @@ -90,13 +86,6 @@ OGRMSSQLSpatialDataSource::~OGRMSSQLSpatialDataSource() CPLFree(pszName); CPLFree(pszCatalog); - for (int i = 0; i < nKnownSRID; i++) - { - if (papoSRS[i] != nullptr) - papoSRS[i]->Release(); - } - CPLFree(panSRID); - CPLFree(papoSRS); CPLFree(pszConnection); } /************************************************************************/ @@ -1445,18 +1434,14 @@ OGRSpatialReference *OGRMSSQLSpatialDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* First, we look through our SRID cache, is it there? */ /* -------------------------------------------------------------------- */ - int i; - - for (i = 0; i < nKnownSRID; i++) + auto oIter = m_oSRSCache.find(nId); + if (oIter != m_oSRSCache.end()) { - if (panSRID[i] == nId) - return papoSRS[i]; + return oIter->second.get(); } EndCopy(); - OGRSpatialReference *poSRS = nullptr; - /* -------------------------------------------------------------------- */ /* Try looking up in spatial_ref_sys table */ /* -------------------------------------------------------------------- */ @@ -1470,15 +1455,12 @@ OGRSpatialReference *OGRMSSQLSpatialDataSource::FetchSRS(int nId) { if (oStmt.GetColData(0)) { - poSRS = new OGRSpatialReference(); + auto poSRS = std::unique_ptr( + new OGRSpatialReference()); poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); const char *pszWKT = oStmt.GetColData(0); - if (poSRS->importFromWkt(pszWKT) != OGRERR_NONE) - { - delete poSRS; - poSRS = nullptr; - } - else + if (poSRS->importFromWkt(pszWKT) == OGRERR_NONE) { const char *pszAuthorityName = poSRS->GetAuthorityName(nullptr); @@ -1491,6 +1473,8 @@ OGRSpatialReference *OGRMSSQLSpatialDataSource::FetchSRS(int nId) poSRS->Clear(); poSRS->importFromEPSG(nCode); } + + return AddSRIDToCache(nId, std::move(poSRS)); } } } @@ -1499,26 +1483,16 @@ OGRSpatialReference *OGRMSSQLSpatialDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* Try looking up the EPSG list */ /* -------------------------------------------------------------------- */ - if (!poSRS) + auto poSRS = + std::unique_ptr( + new OGRSpatialReference()); + poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); + if (poSRS->importFromEPSG(nId) == OGRERR_NONE) { - poSRS = new OGRSpatialReference(); - poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); - if (poSRS->importFromEPSG(nId) != OGRERR_NONE) - { - delete poSRS; - poSRS = nullptr; - } + return AddSRIDToCache(nId, std::move(poSRS)); } - /* -------------------------------------------------------------------- */ - /* Add to the cache. */ - /* -------------------------------------------------------------------- */ - if (poSRS) - { - AddSRIDToCache(nId, poSRS); - } - - return poSRS; + return nullptr; } /************************************************************************/ @@ -1528,18 +1502,15 @@ OGRSpatialReference *OGRMSSQLSpatialDataSource::FetchSRS(int nId) /* sure it is freshly created, or add a reference yourself if not. */ /************************************************************************/ -void OGRMSSQLSpatialDataSource::AddSRIDToCache(int nId, - OGRSpatialReference *poSRS) +OGRSpatialReference *OGRMSSQLSpatialDataSource::AddSRIDToCache( + int nId, + std::unique_ptr &&poSRS) { /* -------------------------------------------------------------------- */ /* Add to the cache. */ /* -------------------------------------------------------------------- */ - panSRID = (int *)CPLRealloc(panSRID, sizeof(int) * (nKnownSRID + 1)); - papoSRS = (OGRSpatialReference **)CPLRealloc(papoSRS, sizeof(void *) * - (nKnownSRID + 1)); - panSRID[nKnownSRID] = nId; - papoSRS[nKnownSRID] = poSRS; - nKnownSRID++; + auto oIter = m_oSRSCache.emplace(nId, std::move(poSRS)).first; + return oIter->second.get(); } /************************************************************************/ @@ -1561,15 +1532,15 @@ int OGRMSSQLSpatialDataSource::FetchSRSId(const OGRSpatialReference *poSRS) /* -------------------------------------------------------------------- */ /* First, we look through our SRID cache, is it there? */ /* -------------------------------------------------------------------- */ - for (int i = 0; i < nKnownSRID; i++) + for (const auto &pair : m_oSRSCache) { - if (papoSRS[i] == poSRS) - return panSRID[i]; + if (pair.second.get() == poSRS) + return pair.first; } - for (int i = 0; i < nKnownSRID; i++) + for (const auto &pair : m_oSRSCache) { - if (papoSRS[i] != nullptr && papoSRS[i]->IsSame(poSRS)) - return panSRID[i]; + if (pair.second != nullptr && pair.second->IsSame(poSRS)) + return pair.first; } OGRSpatialReference oSRS(*poSRS); @@ -1622,9 +1593,11 @@ int OGRMSSQLSpatialDataSource::FetchSRSId(const OGRSpatialReference *poSRS) nSRSId = atoi(oStmt.GetColData(0)); if (nSRSId != 0) { - auto poCachedSRS = new OGRSpatialReference(oSRS); + std::unique_ptr + poCachedSRS(new OGRSpatialReference(oSRS)); poCachedSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); - AddSRIDToCache(nSRSId, poCachedSRS); + AddSRIDToCache(nSRSId, std::move(poCachedSRS)); } return nSRSId; } From c5b3ceaf82b26e4279c4046b9c721e053ad6ed3a Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 9 Feb 2024 12:33:08 +0100 Subject: [PATCH 034/132] [Lint] OCI: modernize SRS cache --- ogr/ogrsf_frmts/oci/ogr_oci.h | 8 +++-- ogr/ogrsf_frmts/oci/ogrocidatasource.cpp | 39 ++++++------------------ 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/ogr/ogrsf_frmts/oci/ogr_oci.h b/ogr/ogrsf_frmts/oci/ogr_oci.h index 9375aa47d977..329c9cef0c68 100644 --- a/ogr/ogrsf_frmts/oci/ogr_oci.h +++ b/ogr/ogrsf_frmts/oci/ogr_oci.h @@ -34,6 +34,8 @@ #include "oci.h" #include "cpl_error.h" +#include + /* -------------------------------------------------------------------- */ /* Low level Oracle spatial declarations. */ /* -------------------------------------------------------------------- */ @@ -564,9 +566,9 @@ class OGROCIDataSource final : public OGRDataSource // We maintain a list of known SRID to reduce the number of trips to // the database to get SRSes. - int nKnownSRID; - int *panSRID; - OGRSpatialReference **papoSRS; + std::map> + m_oSRSCache{}; public: OGROCIDataSource(); diff --git a/ogr/ogrsf_frmts/oci/ogrocidatasource.cpp b/ogr/ogrsf_frmts/oci/ogrocidatasource.cpp index 3d2a45e49ed2..4b45d965bb92 100644 --- a/ogr/ogrsf_frmts/oci/ogrocidatasource.cpp +++ b/ogr/ogrsf_frmts/oci/ogrocidatasource.cpp @@ -59,9 +59,6 @@ OGROCIDataSource::OGROCIDataSource() bDSUpdate = FALSE; bNoLogging = FALSE; poSession = nullptr; - papoSRS = nullptr; - panSRID = nullptr; - nKnownSRID = 0; } /************************************************************************/ @@ -81,13 +78,6 @@ OGROCIDataSource::~OGROCIDataSource() CPLFree(papoLayers); - for (i = 0; i < nKnownSRID; i++) - { - papoSRS[i]->Release(); - } - CPLFree(papoSRS); - CPLFree(panSRID); - if (poSession != nullptr) delete poSession; } @@ -801,12 +791,10 @@ OGRSpatialReference *OGROCIDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* First, we look through our SRID cache, is it there? */ /* -------------------------------------------------------------------- */ - int i; - - for (i = 0; i < nKnownSRID; i++) + auto oIter = m_oSRSCache.find(nId); + if (oIter != m_oSRSCache.end()) { - if (panSRID[i] == nId) - return papoSRS[i]; + return oIter->second.get(); } /* -------------------------------------------------------------------- */ @@ -830,11 +818,11 @@ OGRSpatialReference *OGROCIDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* Turn into a spatial reference. */ /* -------------------------------------------------------------------- */ - OGRSpatialReference *poSRS = new OGRSpatialReference(); + std::unique_ptr poSRS( + new OGRSpatialReference()); poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); if (poSRS->importFromWkt(papszResult[0]) != OGRERR_NONE) { - delete poSRS; return nullptr; } @@ -853,10 +841,10 @@ OGRSpatialReference *OGROCIDataSource::FetchSRS(int nId) const char *const apszOptions[] = { "IGNORE_DATA_AXIS_TO_SRS_AXIS_MAPPING=YES", nullptr}; if (oSRS_EPSG.importFromEPSG(nId) == OGRERR_NONE && - oSRS_EPSG.IsSame(poSRS, apszOptions)) + oSRS_EPSG.IsSame(poSRS.get(), apszOptions)) { *poSRS = oSRS_EPSG; - return poSRS; + return poSRS.release(); } } @@ -865,7 +853,7 @@ OGRSpatialReference *OGROCIDataSource::FetchSRS(int nId) /* authority. */ /* -------------------------------------------------------------------- */ int bGotEPSGMapping = FALSE; - for (i = 0; anEPSGOracleMapping[i] != 0; i += 2) + for (int i = 0; anEPSGOracleMapping[i] != 0; i += 2) { if (anEPSGOracleMapping[i] == nId) { @@ -890,15 +878,8 @@ OGRSpatialReference *OGROCIDataSource::FetchSRS(int nId) /* -------------------------------------------------------------------- */ /* Add to the cache. */ /* -------------------------------------------------------------------- */ - panSRID = (int *)CPLRealloc(panSRID, sizeof(int) * (nKnownSRID + 1)); - papoSRS = (OGRSpatialReference **)CPLRealloc(papoSRS, sizeof(void *) * - (nKnownSRID + 1)); - panSRID[nKnownSRID] = nId; - papoSRS[nKnownSRID] = poSRS; - - nKnownSRID++; - - return poSRS; + oIter = m_oSRSCache.emplace(nId, std::move(poSRS)).first; + return oIter->second.get(); } /************************************************************************/ From 23edd39ad2eb3b3bd2dcdb37fdea99efda374e3f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 9 Feb 2024 15:53:36 +0100 Subject: [PATCH 035/132] GTiff: friendlier error message when attempting to create JXL compressed file with unsupported type/bits_per_sample --- frmts/gtiff/gtiffdataset_write.cpp | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/frmts/gtiff/gtiffdataset_write.cpp b/frmts/gtiff/gtiffdataset_write.cpp index 60021b170eca..c05bb12213fb 100644 --- a/frmts/gtiff/gtiffdataset_write.cpp +++ b/frmts/gtiff/gtiffdataset_write.cpp @@ -5066,6 +5066,44 @@ TIFF *GTiffDataset::CreateLL(const char *pszFilename, int nXSize, int nYSize, } } +#ifdef HAVE_JXL + if (l_nCompression == COMPRESSION_JXL) + { + // Reflects tif_jxl's GetJXLDataType() + if (eType != GDT_Byte && eType != GDT_UInt16 && eType != GDT_Float32) + { + ReportError(pszFilename, CE_Failure, CPLE_NotSupported, + "Data type %s not supported for JXL compression. Only " + "Byte, UInt16, Float32 are supported", + GDALGetDataTypeName(eType)); + return nullptr; + } + const struct + { + GDALDataType eDT; + int nBitsPerSample; + } asSupportedDTBitsPerSample[] = { + {GDT_Byte, 8}, + {GDT_UInt16, 16}, + {GDT_Float32, 32}, + }; + for (const auto &sSupportedDTBitsPerSample : asSupportedDTBitsPerSample) + { + if (eType == sSupportedDTBitsPerSample.eDT && + l_nBitsPerSample != sSupportedDTBitsPerSample.nBitsPerSample) + { + ReportError( + pszFilename, CE_Failure, CPLE_NotSupported, + "Bits per sample=%d not supported for JXL compression. " + "Only %d is supported for %s data type.", + l_nBitsPerSample, sSupportedDTBitsPerSample.nBitsPerSample, + GDALGetDataTypeName(eType)); + return nullptr; + } + } + } +#endif + int nPredictor = PREDICTOR_NONE; pszValue = CSLFetchNameValue(papszParamList, "PREDICTOR"); if (pszValue != nullptr) From 84e7beee3fc271b5a0cd8a1a9927347be7128c1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:32:35 +0000 Subject: [PATCH 036/132] build(deps): bump actions/upload-artifact from 4.3.0 to 4.3.1 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.0 to 4.3.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/26f96dfa697d77e81fd5907df203aa23a56210a8...5d5d22a31266ced268874388b861e4b58bb5c2f3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/cifuzz.yml | 2 +- .github/workflows/conda.yml | 2 +- .github/workflows/doc_build.yml | 6 +++--- .github/workflows/linux_build.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 436e145f36f6..c6c73bb69187 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -29,7 +29,7 @@ jobs: fuzz-seconds: 600 dry-run: false - name: Upload Crash - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: failure() && steps.build.outcome == 'success' with: name: artifacts diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index e0d044655ccf..8bf07876478e 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -72,7 +72,7 @@ jobs: source ../ci/travis/conda/compile.sh working-directory: ./gdal-feedstock - - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: ${{ matrix.platform }}-conda-package path: ./gdal-feedstock/packages/ diff --git a/.github/workflows/doc_build.yml b/.github/workflows/doc_build.yml index 05dbf587d7d7..0b93863643c9 100644 --- a/.github/workflows/doc_build.yml +++ b/.github/workflows/doc_build.yml @@ -81,15 +81,15 @@ jobs: # run: | # make spelling # working-directory: ./doc - - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: PDF path: doc/build/latex/gdal.pdf - - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: HTML path: doc/build/html/* - #- uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + #- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 # with: # name: Misspelled # path: doc/build/spelling/output.txt diff --git a/.github/workflows/linux_build.yml b/.github/workflows/linux_build.yml index 22d6dbbcf307..7f855b968121 100644 --- a/.github/workflows/linux_build.yml +++ b/.github/workflows/linux_build.yml @@ -322,14 +322,14 @@ jobs: docker push ${CONTAINER_NAME_FULL} - name: Upload coverage artifacts - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ matrix.id == 'coverage' }} with: name: coverage_index.html path: build-${{ matrix.id }}/coverage_html/index.html - name: Upload coverage artifacts - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ matrix.id == 'coverage' }} with: name: HTML diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5c8033037594..8ecc21491910 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -63,7 +63,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: SARIF file path: results.sarif From 13e861c540fbf9e6c3b60e59273470595638c8f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:32:38 +0000 Subject: [PATCH 037/132] build(deps): bump pre-commit/action from 3.0.0 to 3.0.1 Bumps [pre-commit/action](https://github.com/pre-commit/action) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/pre-commit/action/releases) - [Commits](https://github.com/pre-commit/action/compare/646c83fcd040023954eafda54b4db0192ce70507...2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd) --- updated-dependencies: - dependency-name: pre-commit/action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/code_checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml index 91dddf6bd13b..c0a53bca8c6b 100644 --- a/.github/workflows/code_checks.yml +++ b/.github/workflows/code_checks.yml @@ -83,7 +83,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 - - uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 # v3.0.0 + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 doxygen: runs-on: ubuntu-latest From edcf8622291ffd3945235dc857d7e26e2fee388f Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Sun, 11 Feb 2024 23:11:15 -0500 Subject: [PATCH 038/132] Doc: Add Python API docs for gdal.Dataset --- doc/source/api/python/osgeo.gdal.rst | 1 + gcore/gdaldataset.cpp | 2 +- swig/include/python/docs/gdal_dataset_docs.i | 712 ++++++++++++++++++- swig/include/python/gdal_python.i | 308 +++++++- 4 files changed, 997 insertions(+), 26 deletions(-) diff --git a/doc/source/api/python/osgeo.gdal.rst b/doc/source/api/python/osgeo.gdal.rst index f344667e2470..a3ac74b7b754 100644 --- a/doc/source/api/python/osgeo.gdal.rst +++ b/doc/source/api/python/osgeo.gdal.rst @@ -5,3 +5,4 @@ osgeo.gdal module :members: :undoc-members: :show-inheritance: + :exclude-members: thisown diff --git a/gcore/gdaldataset.cpp b/gcore/gdaldataset.cpp index 63e7d44d46ec..38503ec8d697 100644 --- a/gcore/gdaldataset.cpp +++ b/gcore/gdaldataset.cpp @@ -1094,7 +1094,7 @@ int CPL_STDCALL GDALGetRasterCount(GDALDatasetH hDS) * When a projection definition is not available an empty (but not NULL) * string is returned. * - * \note Startig with GDAL 3.0, this is a compatibility layer around + * \note Starting with GDAL 3.0, this is a compatibility layer around * GetSpatialRef() * * @return a pointer to an internal projection reference string. It should diff --git a/swig/include/python/docs/gdal_dataset_docs.i b/swig/include/python/docs/gdal_dataset_docs.i index 5849b542f626..8ff2ec83b11c 100644 --- a/swig/include/python/docs/gdal_dataset_docs.i +++ b/swig/include/python/docs/gdal_dataset_docs.i @@ -1,5 +1,5 @@ %feature("docstring") GDALDatasetShadow " -Python proxy of a raster :cpp:class:`GDALDataset`. +Python proxy of a :cpp:class:`GDALDataset`. Since GDAL 3.8, a Dataset can be used as a context manager. When exiting the context, the Dataset will be closed and @@ -8,14 +8,718 @@ data will be written to disk. %extend GDALDatasetShadow { +%feature("docstring") AbortSQL " + +Abort any SQL statement running in the data store. + +Not implemented by all drivers. See :cpp:func:`GDALDataset::AbortSQL`. + +Returns +------- +:py:const:`ogr.OGRERR_NONE` on success or :py:const:`ogr.OGRERR_UNSUPPORTED_OPERATION` if AbortSQL is not supported for this dataset. +"; + + +%feature("docstring") AddBand " + +Adds a band to a :py:class:`Dataset`. + +Not supported by all drivers. + +Parameters +----------- +datatype: int + the data type of the pixels in the new band +options: dict/list + an optional dict or list of format-specific ``NAME=VALUE`` option strings. + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +Examples +-------- +>>> ds=gdal.GetDriverByName('MEM').Create('', 10, 10) +>>> ds.RasterCount +1 +>>> ds.AddBand(gdal.GDT_Float32) +0 +>>> ds.RasterCount +2 +"; + +%feature("docstring") AddFieldDomain " + +Add a :py:class:`ogr.FieldDomain` to the dataset. + +Only a few drivers support this operation. See :cpp:func:`GDALDataset::AddFieldDomain`. + +Parameters +---------- +fieldDomain : ogr.FieldDomain + The field domain to add + +Returns +-------- +bool: + ``True`` if the field domain was added, ``False`` in case of error. + + +"; + +%feature("docstring") AddRelationship " + +Add a :py:class:`Relationship` to the dataset. + +See :cpp:func:`GDALDataset::AddRelationship`. + +Parameters +---------- +relationship : Relationship + The relationship to add + +Returns +------- +bool: + ``True`` if the field domain was added, ``False`` in case of error. + +"; + +%feature("docstring") AdviseRead " + +Advise driver of upcoming read requests. + +See :cpp:func:`GDALDataset::AdviseRead`. + +"; + +%feature("docstring") BuildOverviews " + +Build raster overview(s) for all bands. + +See :cpp:func:`GDALDataset::BuildOverviews` + +Parameters +---------- +resampling : str, optional + The resampling method to use. See :cpp:func:`GDALDataset::BuildOveriews`. +overviewlist : list + A list of overview levels (decimation factors) to build, or an + empty list to clear existing overviews. +callback : function, optional + A progress callback function +callback_data: optional + Optional data to be passed to callback function +options : dict/list, optional + A dict or list of key=value options + +Returns +------- +:py:const:`CE_Failure` if an error occurs, otherwise :py:const:`CE_None`. + +Examples +-------- +>>> import numpy as np +>>> ds = gdal.GetDriverByName('GTiff').Create('test.tif', 12, 12) +>>> ds.GetRasterBand(1).WriteArray(np.arange(12*12).reshape((12, 12))) +0 +>>> ds.BuildOverviews('AVERAGE', [2, 4]) +0 +>>> ds.GetRasterBand(1).GetOverviewCount() +2 +>>> ds.BuildOverviews(overviewlist=[]) +0 +>>> ds.GetRasterBand(1).GetOverviewCount() +0 +"; + +%feature("docstring") ClearStatistics " + +Clear statistics + +See :cpp:func:`GDALDatset::ClearStatistics`. + +"; + %feature("docstring") Close " Closes opened dataset and releases allocated resources. This method can be used to force the dataset to close when one more references to the dataset are still -reachable. If Close is never called, the dataset will +reachable. If :py:meth:`Close` is never called, the dataset will be closed automatically during garbage collection. -" -} +In most cases, it is preferable to open or create a dataset +using a context manager instead of calling :py:meth:`Close` +directly. + +"; + +%feature("docstring") CommitTransaction " +Commits a transaction, for `Datasets` that support transactions. + +See :cpp:func:`GDALDataset::CommitTransaction`. +"; + +%feature("docstring") CopyLayer " + +Duplicate an existing :py:class:`ogr.Layer`. + +See :cpp:func:`GDALDAtaset::CopyLayer`. + +Parameters +---------- +src_layer : ogr.Layer + source layer +new_name : str + name of the layer to create +options : dict/list + a dict or list of name=value driver-specific creation options + +Returns +------- +ogr.Layer, or ``None`` if an error occurs +"; + +%feature("docstring") CreateLayer " + +Create a new layer in a vector Dataset. + +Parameters +---------- +name : string + the name for the new layer. This should ideally not + match any existing layer on the datasource. +srs : osr.SpatialReference, default=None + the coordinate system to use for the new layer, or ``None`` if + no coordinate system is available. +geom_type : int, default = :py:const:`ogr.wkbUnknown` + geometry type for the layer. Use :py:const:`ogr.wkbUnknown` if there + are no constraints on the types geometry to be written. +options : dict/list, optional + Driver-specific dict or list of name=value options + +Returns +------- +ogr.Layer or ``None`` on failure. + + +Examples +-------- +>>> ds = gdal.GetDriverByName('GPKG').Create('test.gpkg', 0, 0) +>>> ds.GetLayerCount() +0 +>>> lyr = ds.CreateLayer('poly', geom_type=ogr.wkbPolygon) +>>> ds.GetLayerCount() +1 + +"; + +%feature("docstring") CreateMaskBand " + +Adds a mask band to the dataset. + +See :cpp:func:`GDALDataset::CreateMaskBand`. + +Parameters +---------- +flags : int + +Returns +------- +int + :py:const:`CE_Failure` if an error occurs, otherwise :py:const:`CE_None`. + +"; + +%feature("docstring") DeleteFieldDomain " + +Removes a field domain from the Dataset. + +Parameters +---------- +name : str + Name of the field domain to delete + +Returns +------- +bool + ``True`` if the field domain was removed, otherwise ``False``. + +"; + +%feature("docstring") DeleteRelationship " + +Removes a relationship from the Dataset. + +Parameters +---------- +name : str + Name of the relationship to remove. + +Returns +------- +bool + ``True`` if the relationship was removed, otherwise ``False``. + + +"; + +%feature("docstring") FlushCache " + +Flush all write-cached data to disk. + +See :cpp:func:`GDALDataset::FlushCache`. + +Returns +------- +int + `gdal.CE_None` in case of success +"; + + +%feature("docstring") GetDriver " + +Fetch the driver used to open or create this :py:class:`Dataset`. + +"; + +%feature("docstring") GetFieldDomain " + +Get a field domain from its name. + +Parameters +---------- +name: str + The name of the field domain + +Returns +------- +ogr.FieldDomain, or ``None`` if it is not found. +"; + +%feature("docstring") GetFieldDomainNames " + +Get a list of the names of all field domains stored in the dataset. + +Parameters +---------- +options: dict/list, optional + Driver-specific options determining how attributes should + be retrieved. + +Returns +------- +list, or ``None`` if no field domains are stored in the dataset. +"; + +%feature("docstring") GetFileList " + +Returns a list of files believed to be part of this dataset. +See :cpp:func:`GDALGetFileList`. + +"; + +%feature("docstring") GetGCPCount " + +Get number of GCPs. See :cpp:func:`GDALGetGCPCount`. + +Returns +-------- +int + +"; + +%feature("docstring") GetGCPProjection " + +Return a WKT representation of the GCP spatial reference. + +Returns +-------- +string + +"; + +%feature("docstring") GetGCPSpatialRef " + +Get output spatial reference system for GCPs. + +See :cpp:func:`GDALGetGCPSpatialRef` + +"; + +%feature("docstring") GetGCPs " + +Get the GCPs. See :cpp:func:`GDALGetGCPs`. + +Returns +-------- +tuple + a tuple of :py:class:`GCP` objects. + +"; + +%feature("docstring") GetLayerByIndex " + +Fetch a layer by index. + +Parameters +---------- +index : int + A layer number between 0 and ``GetLayerCount() - 1`` + +Returns +------- +ogr.Layer + +"; + + +%feature("docstring") GetLayerByNAme " + +Fetch a layer by name. + +Parameters +---------- +layer_name : str + +Returns +------- +ogr.Layer + +"; + +%feature("docstring") GetLayerCount " +Get the number of layers in this dataset. + +Returns +------- +int + +"; + + +%feature("docstring") GetNextFeature " + +Fetch the next available feature from this dataset. + +This method is intended for the few drivers where +:py:meth:`OGRLayer.GetNextFeature` is not efficient, but in general +:py:meth:`OGRLayer.GetNextFeature` is a more natural API. + +See :cpp:func:`GDALDataset::GetNextFeature`. + +Returns +------- +ogr.Feature + +"; + +%feature("docstring") GetProjection " + +Return a WKT representation of the dataset spatial reference. +Equivalent to :py:meth:`GetProjectionRef`. + +Returns +------- +str + +"; + +%feature("docstring") GetProjectionRef " + +Return a WKT representation of the dataset spatial reference. + +Returns +------- +str + +"; + +%feature("docstring") GetGeoTransform " + +Fetch the affine transformation coefficients. + +See :cpp:func:`GDALGetGeoTransform`. + +Parameters +----------- +can_return_null : bool, default=False + if ``True``, return ``None`` instead of the default transformation + if the transformation for this :py:class:`Dataset` has not been defined. + +Returns +------- +tuple: + a 6-member tuple representing the transformation coefficients + + +"; + +%feature("docstring") GetRasterBand " + +Fetch a :py:class:`Band` band from a :py:class:`Dataset`. See :cpp:func:`GDALGetRasterBand`. + +Parameters +----------- +nBand : int + the index of the band to fetch, from 1 to :py:attr:`RasterCount` + +Returns +-------- +Band: + the :py:class:`Band`, or ``None`` on error. + +"; + +%feature("docstring") GetRelationship " + +Get a relationship from its name. + +Returns +------- +Relationship, or ``None`` if not found. +"; + +%feature("docstring") GetRelationshipNames " + +Get a list of the names of all relationships stored in the dataset. + +Parameters +---------- +options : dict/list, optional + driver-specific options determining how the relationships shoudl be retrieved + +"; + +%feature("docstring") GetRootGroup " + +Return the root :py:class:`Group` of this dataset. +Only value for multidimensional datasets. + +Returns +------- +Group + +"; + +%feature("docstring") GetSpatialRef " + +Fetch the spatial reference for this dataset. + +Returns +-------- +osr.SpatialReference + +"; + +%feature("docstring") GetStyleTable " + +Returns dataset style table. + +Returns +------- +ogr.StyleTable + +"; + +%feature("docstring") IsLayerPrivate " + +Parameters +---------- +index : int + Index o layer to check + +Returns +------- +bool + ``True`` if the layer is a private or system table, ``False`` otherwise + + +"; + +%feature("docstring") RasterCount " + +The number of bands in this dataset. + +"; + +%feature("docstring") RasterXSize " + +Raster width in pixels. See :cpp:func:`GDALGetRasterXSize`. + +"; + +%feature("docstring") RasterYSize " + +Raster height in pixels. See :cpp:func:`GDALGetRasterYSize`. + +"; + +%feature("docstring") ResetReading " + +Reset feature reading to start on the first feature. + +This affects :py:meth:`GetNextFeature`. + +Depending on drivers, this may also have the side effect of calling +:py:meth:`OGRLayer.ResetReading` on the layers of this dataset. + +"; + +%feature("docstring") RollbackTransaction " + +Roll back a Dataset to its state before the start of the current transaction. + +For datasets that support transactions. + +Returns +------- +int + If no transaction is active, or the rollback fails, will return + :py:const:`OGRERR_FAILURE`. Datasources which do not support transactions will + always return :py:const:`OGRERR_UNSUPPORTED_OPERATION`. + +"; + +%feature("docstring") SetGCPs " +"; + +%feature("docstring") SetGeoTransform " + +Set the affine transformation coefficients. + +See :py:meth:`GetGeoTransform` for details on the meaning of the coefficients. + +Parameters +---------- +argin : tuple + +Returns +------- +:py:const:`CE_Failure` if an error occurs, otherwise :py:const:`CE_None`. + +"; + +%feature("docstring") SetProjection " + +Set the spatial reference system for this dataset. + +See :cpp:func:`GDALDataset::SetProjection`. + +Parameters +---------- +prj: + The projection string in OGC WKT or PROJ.4 format + +Returns +------- +:py:const:`CE_Failure` if an error occurs, otherwise :py:const:`CE_None`. + +"; + +%feature("docstring") SetSpatialRef " + +Set the spatial reference system for this dataset. + +Parameters +---------- +srs : SpatialReference + +Returns +------- +:py:const:`CE_Failure` if an error occurs, otherwise :py:const:`CE_None`. + +"; + +%feature("docstring") SetStyleTable " + +Set dataset style table + +Parameters +---------- +table : ogr.StyleTable +"; + +%feature("docstring") StartTransaction " + +Creates a transaction. See :cpp:func:`GDALDataset::StartTransaction`. + +Returns +------- +int + If starting the transaction fails, will return + :py:const:`ogr.OGRERR_FAILURE`. Datasources which do not support transactions will + always return :py:const:`OGRERR_UNSUPPORTED_OPERATION`. + +"; + +%feature("docstring") TestCapability " + +Test if a capability is available. + +Parameters +---------- +cap : str + Name of the capability (e.g., :py:const:`ogr.ODsCTransactions`) + +Returns +------- +bool + ``True`` if the capability is available, ``False`` if invalid or unavailable + +Examples +-------- +>>> ds = gdal.GetDriverByName('ESRI Shapefile').Create('test.shp', 0, 0) +>>> ds.TestCapability(ogr.ODsCTransactions) +False +>>> ds.TestCapability(ogr.ODsCMeasuredGeometries) +True +>>> ds.TestCapability(gdal.GDsCAddRelationship) +False + +"; + +%feature("docstring") UpdateFieldDomain " + +Update an existing field domain by replacing its definition. + +The existing field domain with matching name will be replaced. + +Requires the :py:const:`ogr.ODsCUpdateFieldDomain` datasset capability. + +Parameters +---------- +fieldDomain : ogr.FieldDomain + Updated field domain. + +Returns +------- +bool + ``True`` in case of success + +"; + +%feature("docstring") UpdateRelationship " + +Update an existing relationship by replacing its definition. + +The existing relationship with matching name will be replaced. + +Requires the :py:const:`gdal.GDsCUpdateFieldDomain` dataset capability. + +Parameters +---------- +relationship : Relationship + Updated relationship + +Returns +------- +bool + ``True`` in case of success + +"; + +} diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index 756b9e760427..3000a5a658a9 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -523,6 +523,10 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse buf_string, buf_xsize=None, buf_ysize=None, buf_type=None, buf_pixel_space=None, buf_line_space=None ): + """ + Write the contents of a buffer to a dataset. + + """ if buf_xsize is None: buf_xsize = xsize @@ -549,8 +553,72 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse resample_alg=gdalconst.GRIORA_NearestNeighbour, callback=None, callback_data=None): - """ Reading a chunk of a GDAL band into a numpy array. The optional (buf_xsize,buf_ysize,buf_type) - parameters should generally not be specified if buf_obj is specified. The array is returned""" + """ + Read a window of this raster band into a NumPy array. + + Parameters + ---------- + xoff : int, default=0 + The pixel offset to left side of the region of the band to + be read. This would be zero to start from the left side. + yoff : int, default=0 + The line offset to top side of the region of the band to + be read. This would be zero to start from the top side. + win_xsize : int, optional + The number of pixels to read in the x direction. By default, + equal to the number of columns in the raster. + win_ysize : int, optional + The number of rows to read in the y direction. By default, + equal to the number of bands in the raster. + buf_xsize : int, optional + The number of columns in the returned array. If not equal + to ``win_xsize``, the returned values will be determined + by ``resample_alg``. + buf_ysize : int, optional + The number of rows in the returned array. If not equal + to ``win_ysize``, the returned values will be determined + by ``resample_alg``. + buf_type : int, optional + The data type of the returned array + buf_obj : np.ndarray, optional + Optional buffer into which values will be read. If ``buf_obj`` + is specified, then ``buf_xsize``/``buf_ysize``/``buf_type`` + should generally not be specified. + resample_alg : int, default = :py:const:`gdal.GRIORA_NearestNeighbour`. + Specifies the resampling algorithm to use when the size of + the read window and the buffer are not equal. + callback : function, optional + A progress callback function + callback_data: optional + Optional data to be passed to callback function + + Returns + ------- + np.ndarray + + Examples + -------- + >>> import numpy as np + >>> ds = gdal.GetDriverByName("GTiff").Create("test.tif", 4, 4) + >>> ds.WriteArray(np.arange(16).reshape(4, 4)) + 0 + >>> band = ds.GetRasterBand(1) + >>> band.ReadAsArray() + array([[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]], dtype=uint8) + >>> band.ReadAsArray(xoff=2, yoff=2, win_xsize=2, win_ysize=2) + array([[10, 11], + [14, 15]], dtype=uint8) + >>> band.ReadAsArray(buf_xsize=2, buf_ysize=2, buf_type=gdal.GDT_Float64, resample_alg=gdal.GRIORA_Average) + array([[ 3., 5.], + [11., 13.]]) + >>> buf = np.zeros((2,2)) + >>> band.ReadAsArray(buf_obj=buf) + array([[ 5., 7.], + [13., 15.]]) + """ from osgeo import gdal_array @@ -845,8 +913,92 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, callback_data=None, interleave='band', band_list=None): - """ Reading a chunk of a GDAL band into a numpy array. The optional (buf_xsize,buf_ysize,buf_type) - parameters should generally not be specified if buf_obj is specified. The array is returned""" + """ + Read a window from raster bands into a NumPy array. + + Parameters + ---------- + xoff : int, default=0 + The pixel offset to left side of the region of the band to + be read. This would be zero to start from the left side. + yoff : int, default=0 + The line offset to top side of the region of the band to + be read. This would be zero to start from the top side. + xsize : int, optional + The number of pixels to read in the x direction. By default, + equal to the number of columns in the raster. + ysize : int, optional + The number of rows to read in the y direction. By default, + equal to the number of bands in the raster. + buf_xsize : int, optional + The number of columns in the returned array. If not equal + to ``win_xsize``, the returned values will be determined + by ``resample_alg``. + buf_ysize : int, optional + The number of rows in the returned array. If not equal + to ``win_ysize``, the returned values will be determined + by ``resample_alg``. + buf_type : int, optional + The data type of the returned array + buf_obj : np.ndarray, optional + Optional buffer into which values will be read. If ``buf_obj`` + is specified, then ``buf_xsize``/``buf_ysize``/``buf_type`` + should generally not be specified. + resample_alg : int, default = :py:const:`gdal.GRIORA_NearestNeighbour`. + Specifies the resampling algorithm to use when the size of + the read window and the buffer are not equal. + callback : function, optional + A progress callback function + callback_data: optional + Optional data to be passed to callback function + band_list : list, optional + Indexes of bands from which data should be read. By default, + data will be read from all bands. + + Returns + ------- + np.ndarray + + Examples + -------- + >>> ds = gdal.GetDriverByName("GTiff").Create("test.tif", 4, 4, bands=2) + >>> ds.WriteArray(np.arange(32).reshape(2, 4, 4)) + 0 + >>> ds.ReadAsArray() + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]], + [[16, 17, 18, 19], + [20, 21, 22, 23], + [24, 25, 26, 27], + [28, 29, 30, 31]]], dtype=uint8) + >>> ds.ReadAsArray(xoff=2, yoff=2, xsize=2, ysize=2) + array([[[10, 11], + [14, 15]], + [[26, 27], + [30, 31]]], dtype=uint8) + >>> ds.ReadAsArray(buf_xsize=2, buf_ysize=2, buf_type=gdal.GDT_Float64, resample_alg=gdal.GRIORA_Average) + array([[[ 3., 5.], + [11., 13.]], + [[19., 21.], + [27., 29.]]]) + >>> buf = np.zeros((2,2,2)) + >>> ds.ReadAsArray(buf_obj=buf) + array([[[ 5., 7.], + [13., 15.]], + [[21., 23.], + [29., 31.]]]) + >>> ds.ReadAsArray(band_list=[2,1]) + array([[[16, 17, 18, 19], + [20, 21, 22, 23], + [24, 25, 26, 27], + [28, 29, 30, 31]], + [[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]]], dtype=uint8) + """ from osgeo import gdal_array return gdal_array.DatasetReadAsArray(self, xoff, yoff, xsize, ysize, buf_obj, @@ -863,6 +1015,73 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, resample_alg=gdalconst.GRIORA_NearestNeighbour, callback=None, callback_data=None): + """ + Write the contents of a NumPy array to a Dataset. + + Parameters + ---------- + array : np.ndarray + Two- or three-dimensional array containing values to write + xoff : int, default=0 + The pixel offset to left side of the region of the band to + be written. This would be zero to start from the left side. + yoff : int, default=0 + The line offset to top side of the region of the band to + be written. This would be zero to start from the top side. + band_list : list, optional + Indexes of bands to which data should be written. By default, + it is assumed that the Dataset contains the same number of + bands as levels in ``array``. + interleave : str, default="band" + Interleaving, "band" or "pixel". For band-interleaved writing, + ``array`` should have shape ``(nband, ny, nx)``. For pixel- + interleaved-writing, ``array`` should have shape + ``(ny, nx, nbands)``. + resample_alg : int, default = :py:const:`gdal.GRIORA_NearestNeighbour` + callback : function, optional + A progress callback function + callback_data: optional + Optional data to be passed to callback function + + Returns + ------- + int: + Error code, or ``gdal.CE_None`` if no error occurred. + + Examples + -------- + + >>> import numpy as np + >>> + >>> nx = 4 + >>> ny = 3 + >>> nbands = 2 + >>> with gdal.GetDriverByName("GTiff").Create("band3_px.tif", nx, ny, bands=nbands) as ds: + ... data = np.arange(nx*ny*nbands).reshape(ny,nx,nbands) + ... ds.WriteArray(data, interleave="pixel") + ... ds.ReadAsArray() + ... + 0 + array([[[ 0, 2, 4, 6], + [ 8, 10, 12, 14], + [16, 18, 20, 22]], + [[ 1, 3, 5, 7], + [ 9, 11, 13, 15], + [17, 19, 21, 23]]], dtype=uint8) + >>> with gdal.GetDriverByName("GTiff").Create("band3_band.tif", nx, ny, bands=nbands) as ds: + ... data = np.arange(nx*ny*nbands).reshape(nbands, ny, nx) + ... ds.WriteArray(data, interleave="band") + ... ds.ReadAsArray() + ... + 0 + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]]], dtype=uint8) + """ + from osgeo import gdal_array return gdal_array.DatasetWriteArray(self, array, xoff, yoff, @@ -993,6 +1212,15 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, return gdal_array.VirtualMemGetArray( virtualmem ) def GetSubDatasets(self): + """ + Return a list of Subdatasets. + + + Returns + ------- + list + + """ sd_list = [] sd = self.GetMetadata('SUBDATASETS') @@ -1034,7 +1262,18 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, return _gdal.Dataset_BeginAsyncReader(self, xoff, yoff, xsize, ysize, buf_obj, buf_xsize, buf_ysize, buf_type, band_list, 0, 0, 0, options) def GetLayer(self, iLayer=0): - """Return the layer given an index or a name""" + """ + Get the indicated layer from the Dataset + + Parameters + ---------- + value : int/str + Name or 0-based index of the layer to delete. + + Returns + ------- + ogr.Layer, or ``None`` on error + """ _WarnIfUserHasNotSpecifiedIfUsingOgrExceptions() @@ -1046,7 +1285,21 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, raise TypeError("Input %s is not of String or Int type" % type(iLayer)) def DeleteLayer(self, value): - """Deletes the layer given an index or layer name""" + """ + Delete the indicated layer from the Dataset. + + Parameters + ---------- + value : int/str + Name or 0-based index of the layer to delete. + + Returns + ------- + int + :py:const:`ogr.OGRERR_NONE` on success or + :py:const:`ogr.OGRERR_UNSUPPORTED_OPERATION` if DeleteLayer is not supported + for this dataset. + """ if isinstance(value, str): for i in range(self.GetLayerCount()): name = self.GetLayer(i).GetName() @@ -1059,6 +1312,19 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, raise TypeError("Input %s is not of String or Int type" % type(value)) def SetGCPs(self, gcps, wkt_or_spatial_ref): + """ + Assign GCPs. + + See :cpp:func:`GDALSetGCPs`. + + Parameters + ---------- + gcps : list + a list of :py:class:`GCP` objects + wkt_or_spatial_ref : str/osr.SpatialReference + spatial reference of the GCPs + """ + if isinstance(wkt_or_spatial_ref, str): return self._SetGCPs(gcps, wkt_or_spatial_ref) else: @@ -1108,10 +1374,10 @@ def ExecuteSQL(self, statement, spatialFilter=None, dialect="", keep_ref_on_ds=F - None (or an exception if exceptions are enabled) for statements that are in error - or None for statements that have no results set, - - or a ogr.Layer handle representing a results set from the query. + - or a :py:class:`ogr.Layer` handle representing a results set from the query. - Note that this ogr.Layer is in addition to the layers in the data store - and must be released with ReleaseResultSet() before the data source is closed + Note that this :py:class:`ogr.Layer` is in addition to the layers in the data store + and must be released with :py:meth:`ReleaseResultSet` before the data source is closed (destroyed). Starting with GDAL 3.7, this method can also be used as a context manager, @@ -1175,14 +1441,14 @@ def ExecuteSQL(self, statement, spatialFilter=None, dialect="", keep_ref_on_ds=F def ReleaseResultSet(self, sql_lyr): """ReleaseResultSet(self, sql_lyr: ogr.Layer) - Release ogr.Layer returned by ExecuteSQL() (when not called as an execution manager) + Release :py:class:`ogr.Layer` returned by :py:meth:`ExecuteSQL` (when not called as a context manager) The sql_lyr object is invalidated after this call. Parameters ---------- sql_lyr: - ogr.Layer got with ExecuteSQL() + :py:class:`ogr.Layer` got with :py:meth:`ExecuteSQL` """ if sql_lyr and not hasattr(sql_lyr, "_to_release"): @@ -4178,9 +4444,9 @@ def config_options(options, thread_local=True): ---------- options: dict Dictionary of configuration options passed as key, value - thread_local: bool + thread_local: bool, default=True Whether the configuration options should be only set on the current - thread. The default is True. + thread. Returns ------- @@ -4189,8 +4455,8 @@ def config_options(options, thread_local=True): Example ------- - with gdal.config_options({"GDAL_NUM_THREADS": "ALL_CPUS"}): - gdal.Warp("out.tif", "in.tif", dstSRS="EPSG:4326") + >>> with gdal.config_options({"GDAL_NUM_THREADS": "ALL_CPUS"}): + ... gdal.Warp("out.tif", "in.tif", dstSRS="EPSG:4326") """ get_config_option = GetThreadLocalConfigOption if thread_local else GetGlobalConfigOption set_config_option = SetThreadLocalConfigOption if thread_local else SetConfigOption @@ -4215,9 +4481,9 @@ def config_option(key, value, thread_local=True): Name of the configuration option value: str Value of the configuration option - thread_local: bool + thread_local: bool, default=True Whether the configuration option should be only set on the current - thread. The default is True. + thread. Returns ------- @@ -4226,8 +4492,8 @@ def config_option(key, value, thread_local=True): Example ------- - with gdal.config_option("GDAL_NUM_THREADS", "ALL_CPUS"): - gdal.Warp("out.tif", "in.tif", dstSRS="EPSG:4326") + >>> with gdal.config_option("GDAL_NUM_THREADS", "ALL_CPUS"): + ... gdal.Warp("out.tif", "in.tif", dstSRS="EPSG:4326") """ return config_options({key: value}, thread_local=thread_local) @@ -4243,8 +4509,8 @@ def quiet_errors(): Example ------- - with gdal.ExceptionMgr(useExceptions=False), gdal.quiet_errors(): - gdal.Error(gdal.CE_Failure, gdal.CPLE_AppDefined, "you will never see me") + >>> with gdal.ExceptionMgr(useExceptions=False), gdal.quiet_errors(): + ... gdal.Error(gdal.CE_Failure, gdal.CPLE_AppDefined, "you will never see me") """ PushErrorHandler("CPLQuietErrorHandler") try: From bdbf772c539c94ae6a51638ab8c3061fe455ba8e Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Mon, 12 Feb 2024 21:46:55 -0500 Subject: [PATCH 039/132] Doc: Add Python API docs for gdal.Band --- gcore/gdalrasterband.cpp | 4 +- gcore/overview.cpp | 4 +- swig/include/python/docs/gdal_band_docs.i | 706 ++++++++++++++++++++++ swig/include/python/gdal_python.i | 114 +++- swig/python/CMakeLists.txt | 1 + 5 files changed, 822 insertions(+), 7 deletions(-) create mode 100644 swig/include/python/docs/gdal_band_docs.i diff --git a/gcore/gdalrasterband.cpp b/gcore/gdalrasterband.cpp index 67bc08496dd0..685b24c5659b 100644 --- a/gcore/gdalrasterband.cpp +++ b/gcore/gdalrasterband.cpp @@ -796,7 +796,7 @@ CPLErr CPL_STDCALL GDALWriteBlock(GDALRasterBandH hBand, int nXOff, int nYOff, * block and so forth. * * @param nYBlockOff the vertical block offset, with zero indicating - * the left most block, 1 the next block and so forth. + * the top most block, 1 the next block and so forth. * * @param pnXValid pointer to an integer in which the number of valid pixels in * the x direction will be stored @@ -804,7 +804,7 @@ CPLErr CPL_STDCALL GDALWriteBlock(GDALRasterBandH hBand, int nXOff, int nYOff, * @param pnYValid pointer to an integer in which the number of valid pixels in * the y direction will be stored * - * @return CE_None if the input parameter are valid, CE_Failure otherwise + * @return CE_None if the input parameters are valid, CE_Failure otherwise * * @since GDAL 2.2 */ diff --git a/gcore/overview.cpp b/gcore/overview.cpp index 29f23f1f14d3..6eb7dd57b11a 100644 --- a/gcore/overview.cpp +++ b/gcore/overview.cpp @@ -5636,7 +5636,9 @@ CPLErr GDALRegenerateOverviewsMultiBand( /** Undocumented * @param hSrcBand undocumented. - * @param nSampleStep undocumented. + * @param nSampleStep Step between scanlines used to compute statistics. + * When nSampleStep is equal to 1, all scanlines will + * be processed. * @param pdfMean undocumented. * @param pdfStdDev undocumented. * @param pfnProgress undocumented. diff --git a/swig/include/python/docs/gdal_band_docs.i b/swig/include/python/docs/gdal_band_docs.i new file mode 100644 index 000000000000..6685bff03e3e --- /dev/null +++ b/swig/include/python/docs/gdal_band_docs.i @@ -0,0 +1,706 @@ +%feature("docstring") GDALRasterBandShadow " + +Python proxy of a :cpp:class:`GDALRasterBand`. + +"; + +%extend GDALRasterBandShadow { + +%feature("docstring") Checksum " + +Computes a checksum from a region of a RasterBand. +See :cpp:func:`GDALChecksumImage`. + +Parameters +---------- +xoff : int, default=0 + The pixel offset to left side of the region of the band to + be read. This would be zero to start from the left side. +yoff : int, default=0 + The line offset to top side of the region of the band to + be read. This would be zero to start from the top side. +xsize : int, optional + The number of pixels to read in the x direction. By default, + equal to the number of columns in the raster. +ysize : int, optional + The number of rows to read in the y direction. By default, + equal to the number of bands in the raster. + +Returns +------- +int + checksum value, or -1 in case of error + +"; + +%feature("docstring") ComputeBandStats " + +Computes the mean and standard deviation of values in this Band. +See :cpp:func:`GDALComputeBandStats`. + +Parameters +---------- +samplestep : int, default=1 + Step between scanlines used to compute statistics. + +Returns +------- +tuple + tuple of length 2 with value of mean and standard deviation + +See Also +-------- +:py:meth:`ComputeRasterMinMax` +:py:meth:`ComputeStatistics` +:py:meth:`GetMaximum` +:py:meth:`GetMinimum` +:py:meth:`GetStatistics` +:py:meth:`SetStatistics` +"; + +%feature("docstring") CreateMaskBand " + +Add a mask band to the current band. +See :cpp:func:`GDALRasterBand::CreateMaskBand`. + +Parameters +---------- +nFlags : int + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +"; + +%feature("docstring") DeleteNoDataValue " + +Remove the nodata value for this band. + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +"; + +%feature("docstring") Fill " + +Fill this band with a constant value. +See :cpp:func:`GDALRasterBand::Fill`. + +Parameters +---------- +real_fill : float + real component of the fill value +imag_fill : float, default = 0.0 + imaginary component of the fill value + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +"; + +%feature("docstring") FlushCache " + +Flush raster data cache. +See :cpp:func:`GDALRasterBand::FlushCache`. +"; + +%feature("docstring") GetActualBlockSize " + +Fetch the actual block size for a given block offset. +See :cpp:func:`GDALRasterBand::GetActualBlockSize`. + +Parameters +---------- +nXBlockOff : int + the horizontal block offset for which to calculate the + number of valid pixels, with zero indicating the left most block, 1 the next + block and so forth. +nYBlockOff : int + the vertical block offset, with zero indicating + the top most block, 1 the next block and so forth. + +Returns +------- +tuple + tuple with the x and y dimensions of the block +"; + +%feature("docstring") GetBand " + +Return the index of this band. +See :cpp:func:`GDALRasterBand::GetBand`. + +Returns +------- +int + the (1-based) index of this band +"; + +%feature("docstring") GetBlockSize " + +Fetch the natural block size of this band. +See :cpp:func:`GDALRasterBand::GetBlockSize`. + +Returns +------- +list + list with the x and y dimensions of a block +"; + +%feature("docstring") GetCategoryNames " + +Fetch the list of category names for this raster. +See :cpp:func:`GDALRasterBand::GetCategoryNames`. + +Returns +------- +list + A list of category names, or ``None`` +"; + +%feature("docstring") GetColorInterpretation " + +Get the :cpp:enum:`GDALColorInterp` value for this band. +See :cpp:func:`GDALRasterBand::GetColorInterpretation`. + +Returns +------- +int +"; + +%feature("docstring") GetColorTable " + +Get the color table associated with this band. +See :cpp:func:`GDALRasterBand::GetColorTable`. + +Returns +------- +ColorTable or ``None`` +"; + +%feature("docstring") GetDataCoverageStatus " + +Determine whether a sub-window of the Band contains only data, only empty blocks, or a mix of both. +See :cpp:func:`GDALRasterBand::GetDataCoverageStatus`. + +Parameters +---------- +nXOff : int +nYOff : int +nXSize : int +nYSize : int +nMaskFlagStop : int, default=0 + +Returns +------- +list + First value represents a bitwise-or value of the following constants + - :py:const:`gdalconst.GDAL_DATA_COVERAGE_STATUS_DATA` + - :py:const:`gdalconst.GDAL_DATA_COVERAGE_STATUS_EMPTY` + - :py:const:`gdalconst.GDAL_DATA_COVERAGE_STATUS_UNIMPLEMENTED` + Second value represents the approximate percentage in [0, 100] of pixels in the window that have valid values + +Examples +-------- +>>> import numpy as np +>>> # Create a raster with four blocks +>>> ds = gdal.GetDriverByName('GTiff').Create('test.tif', 64, 64, options = {'SPARSE_OK':True, 'TILED':True, 'BLOCKXSIZE':32, 'BLOCKYSIZE':32}) +>>> band = ds.GetRasterBand(1) +>>> # Write some data to upper-left block +>>> band.WriteArray(np.array([[1, 2], [3, 4]])) +0 +>>> # Check status of upper-left block +>>> flags, pct = band.GetDataCoverageStatus(0, 0, 32, 32) +>>> flags == gdal.GDAL_DATA_COVERAGE_STATUS_DATA +True +>>> pct +100.0 +>>> # Check status of upper-right block +>>> flags, pct = band.GetDataCoverageStatus(32, 0, 32, 32) +>>> flags == gdal.GDAL_DATA_COVERAGE_STATUS_EMPTY +True +>>> pct +0.0 +>>> # Check status of window touching all four blocks +>>> flags, pct = band.GetDataCoverageStatus(16, 16, 32, 32) +>>> flags == gdal.GDAL_DATA_COVERAGE_STATUS_DATA | gdal.GDAL_DATA_COVERAGE_STATUS_EMPTY +True +>>> pct +25.0 +"; + +%feature("docstring") GetDataset " + +Fetch the :py:class:`Dataset` associated with this Band. +See :cpp:func:`GDALRasterBand::GetDataset`. +"; + +%feature("docstring") GetDefaultHistogram " + +Fetch the default histogram for this band. +See :cpp:func:`GDALRasterBand::GetDefaultHistogram`. + +Returns +------- +list + List with the following four elements: + - lower bound of histogram + - upper bound of histogram + - number of buckets in histogram + - tuple with counts for each bucket +"; + +%feature("docstring") GetHistogram " + +Compute raster histogram. +See :cpp:func:`GDALRasterBand::GetHistogram`. + +Parameters +---------- +min : float, default=-0.05 + the lower bound of the histogram +max : float, default=255.5 + the upper bound of the histogram +buckets : int, default=256 + the number of buckets int he histogram +include_out_of_range : bool, default=False + if ``True``, add out-of-range values into the first and last buckets +approx_ok : bool, default=True + if ``True``, compute an approximate histogram by using subsampling or overviews +callback : function, optional + A progress callback function +callback_data: optional + Optional data to be passed to callback function + +Returns +------- +list + list with length equal to ``buckets``. If ``approx_ok`` is ``False``, each + the value of each list item will equal the number of pixels in that bucket. + +Examples +-------- +>>> import numpy as np +>>> ds = gdal.GetDriverByName('MEM').Create('', 10, 10, eType=gdal.GDT_Float32) +>>> ds.WriteArray(np.random.normal(size=100).reshape(10, 10)) +0 +>>> ds.GetRasterBand(1).GetHistogram(min=-3.5, max=3.5, buckets=13, approx_ok=False) +[0, 0, 3, 9, 13, 12, 25, 22, 9, 6, 0, 1, 0] # random +"; + +%feature("docstring") GetMaskBand " + +Return the mask band associated with this band. +See :cpp:func:`GDALRasterBand::GetMaskBand`. + +Returns +------- +Band + +"; + +%feature("docstring") GetMaskFlags " + +Return the status flags of the mask band. +See :cpp:func:`GDALRasterBand::GetMaskFlags`. + +Returns +------- +int + +Examples +-------- +>>> import numpy as np +>>> ds = gdal.GetDriverByName('MEM').Create('', 10, 10) +>>> band = ds.GetRasterBand(1) +>>> band.GetMaskFlags() == gdal.GMF_ALL_VALID +True +>>> band.SetNoDataValue(22) +0 +>>> band.WriteArray(np.array([[22]])) +0 +>>> band.GetMaskBand().ReadAsArray(win_xsize=2,win_ysize=2) +array([[ 0, 255], + [255, 255]], dtype=uint8) +>>> band.GetMaskFlags() == gdal.GMF_NODATA +True + +"; + +%feature("docstring") GetMaximum " + +Fetch a previously stored maximum value for this band. +See :cpp:func:`GDALRasterBand::GetMaximum`. + +Returns +------- +float + The stored maximum value, or ``None`` if no value + has been stored. + +"; + +%feature("docstring") GetMinimum " + +Fetch a previously stored maximum value for this band. +See :cpp:func:`GDALRasterBand::GetMinimum`. + +Returns +------- +float + The stored minimum value, or ``None`` if no value + has been stored. + +"; + +%feature("docstring") GetNoDataValueAsInt64 " + +Fetch the nodata value for this band. +See :cpp:func:`GDALRasterBand::GetNoDataValueAsInt64`. + +Returns +------- +int + The nodata value, or ``None`` if it has not been set or + the data type of this band is not :py:const:`gdal.GDT_Int64`. + +"; + +%feature("docstring") GetNoDataValueAsUInt64 " + +Fetch the nodata value for this band. +See :cpp:func:`GDALRasterBand::GetNoDataValueAsUInt64`. + +Returns +------- +int + The nodata value, or ``None`` if it has not been set or + the data type of this band is not :py:const:`gdal.GDT_UInt64`. + +"; + +%feature("docstring") GetOffset " + +Fetch the raster value offset. +See :cpp:func:`GDALRasterBand::GetOffset`. + +Returns +------- +double + The offset value, or ``0.0``. + +"; + +%feature("docstring") GetOverview " + +Fetch a raster overview. +See :cpp:func:`GDALRasterBand::GetOverview`. + +Parameters +---------- +i : int + Overview index between 0 and ``GetOverviewCount() - 1``. + +Returns +------- +Band + +"; + +%feature("docstring") GetOverviewCount " + +Return the number of overview layers available. +See :cpp:func:`GDALRasterBand::GetOverviewCount`. + +Returns +------- +int + +"; + +%feature("docstring") GetRasterCategoryNames " + +Fetch the list of category names for this band. +See :cpp:func:`GDALRasterBand::GetCategoryNames`. + +Returns +------- +list + The list of names, or ``None`` if no names exist. + +"; + +%feature("docstring") GetRasterColorInterpretation " + +Return the color interpretation code for this band. +See :cpp:func:`GDALRasterBand::GetColorInterpretation`. + +Returns +------- +int + The color interpretation code (default :py:const:`gdal.GCI_Undefined`) + +"; + +%feature("docstring") GetRasterColorTable " + +Fetch the color table associated with this band. +See :cpp:func:`GDALRasterBand::GetColorTable`. + +Returns +------- +ColorTable + The :py:class:`ColorTable`, or ``None`` if it has not been defined. +"; + +%feature("docstring") GetScale " + +Fetch the band scale value. +See :cpp:func:`GDALRasterBand::GetScale`. + +Returns +------- +double + The scale value, or ``1.0``. +"; + +%feature("docstring") GetStatistics " + +Return the minimum, maximum, mean, and standard deviation of all pixel values +in this band. +See :cpp:func:`GDALRasterBand::GetStatistics` + +Parameters +---------- +approx_ok : bool + If ``True``, allow overviews or a subset of image tiles to be used in + computing the statistics. +force : bool + If ``False``, only return a result if it can be obtained without scanning + the image, i.e. from pre-existing metadata. + +Returns +------- +list + a list with the min, max, mean, and standard deviation of values + in the Band. + +See Also +-------- +:py:meth:`ComputeBandStats` +:py:meth:`ComputeRasterMinMax` +:py:meth:`GetMaximum` +:py:meth:`GetMinimum` +:py:meth:`GetStatistics` +"; + +%feature("docstring") GetUnitType " + +Return a name for the units of this raster's values. +See :cpp:func:`GDALRasterBand::GetUnitType`. + +Returns +------- +str + +Examples +-------- +>>> ds = gdal.GetDriverByName('MEM').Create('', 10, 10) +>>> ds.GetRasterBand(1).SetUnitType('ft') +0 +>>> ds.GetRasterBand(1).GetUnitType() +'ft' +"; + +%feature("docstring") HasArbitraryOverviews " + +Check for arbitrary overviews. +See :cpp:func:`GDALRasterBand::HasArbitraryOverviews`. + +Returns +------- +bool +"; + +%feature("docstring") IsMaskBand " + +Returns whether the band is a mask band. +See :cpp:func:`GDALRasterBand::IsMaskBand`. + +Returns +------- +bool +"; + +%feature("docstring") SetCategoryNames " + +Set the category names for this band. +See :cpp:func:`GDALRasterBand::SetCategoryNames`. + +Parameters +---------- +papszCategoryNames : list + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. +"; + +%feature("docstring") SetColorInterpretation " + +Set color interpretation of the band +See :cpp:func:`GDALRasterBand::SetColorInterpretation`. + +Parameters +---------- +val : int + A color interpretation code such as :py:const:`gdal.GCI_RedBand` + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. +"; + +%feature("docstring") SetColorTable " + +Set the raster color table. +See :cpp:func:`GDALRasterBand::SetColorTable`. + +Parameters +---------- +arg : ColorTable + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. +"; + +%feature("docstring") SetDefaultHistogram " + +Set default histogram. +See :cpp:func:`GDALRasterBand::SetDefaultHistogram`. + +Parameters +---------- +min : float + minimum value +max : float + maximum value +buckets_in : list + list of pixel counts for each bucket + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +See Also +-------- +:py:meth:`SetHistogram` +"; + +%feature("docstring") SetOffset " + +Set scaling offset. +See :cpp:func:`GDALRasterBand::SetOffset`. + +Parameters +---------- +val : float + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +See Also +-------- +:py:meth:`SetScale` +"; + + +%feature("docstring") SetRasterColorTable " +Deprecated. Alternate name for :py:meth:`SetColorTable`. +"; + +%feature("docstring") SetRasterColorInterpretation " +Deprecated. Alternate name for :py:meth:`SetColorInterpretation`. +"; + +%feature("docstring") SetRasterCategoryNames " +Deprecated. Alternate name for :py:meth:`SetCategoryNames`. +"; + +%feature("docstring") SetScale " +Set scaling ratio. +See :cpp:func:`GDALRasterBand::SetScale`. + +Parameters +---------- +val : float + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + +See Also +-------- +:py:meth:`SetOffset` +"; + +%feature("docstring") SetStatistics " + +Set statistics on band. +See :cpp:func:`GDALRasterBand::SetStatistics`. + +Parameters +---------- +min : float +max : float +mean : float +stdev : float + +Returns +------- +int: + :py:const:`CE_None` on apparent success or :py:const:`CE_Failure` on + failure. This method cannot detect whether metadata will be properly saved and + so may return :py:const:`gdal.`CE_None` even if the statistics will never be + saved. + +See Also +-------- +:py:meth:`ComputeBandStats` +:py:meth:`ComputeRasterMinMax` +:py:meth:`ComputeStatistics` +:py:meth:`GetMaximum` +:py:meth:`GetMinimum` +:py:meth:`GetStatistics` +"; + +%feature("docstring") SetUnitType " + +Set unit type. +See :cpp:func:`GDALRasterBand::SetUnitType`. + +Parameters +---------- +val : str + +Returns +------- +int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. +"; + +} diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index 3000a5a658a9..f987a1842f11 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -7,6 +7,7 @@ %feature("autodoc"); +%include "gdal_band_docs.i" %include "gdal_dataset_docs.i" %init %{ @@ -633,6 +634,30 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse resample_alg=gdalconst.GRIORA_NearestNeighbour, callback=None, callback_data=None): + """ + Write the contents of a NumPy array to a Band. + + Parameters + ---------- + array : np.ndarray + Two-dimensional array containing values to write + xoff : int, default=0 + The pixel offset to left side of the region of the band to + be written. This would be zero to start from the left side. + yoff : int, default=0 + The line offset to top side of the region of the band to + be written. This would be zero to start from the top side. + resample_alg : int, default = :py:const:`gdal.GRIORA_NearestNeighbour` + callback : function, optional + A progress callback function + callback_data: optional + Optional data to be passed to callback function + + Returns + ------- + int: + Error code, or ``gdal.CE_None`` if no error occurred. + """ from osgeo import gdal_array return gdal_array.BandWriteArray(self, array, xoff, yoff, @@ -717,7 +742,36 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse %feature("shadow") ComputeStatistics %{ def ComputeStatistics(self, *args, **kwargs) -> "CPLErr": - """ComputeStatistics(Band self, bool approx_ok, callback=None, callback_data=None) -> CPLErr""" + """ComputeStatistics(Band self, bool approx_ok, callback=None, callback_data=None) -> CPLErr + + Compute image statistics. + See :cpp:func:`GDALRasterBand::ComputeStatistics`. + + Parameters + ---------- + approx_ok : bool + If ``True``, compute statistics based on overviews or a + subset of tiles. + callback : function, optional + A progress callback function + callback_data: optional + Optional data to be passed to callback function + + Returns + ------- + list + a list with the min, max, mean, and standard deviation of values + in the Band. + + See Also + -------- + :py:meth:`ComputeBandStats` + :py:meth:`ComputeRasterMinMax` + :py:meth:`GetMaximum` + :py:meth:`GetMinimum` + :py:meth:`GetStatistics` + :py:meth:`SetStatistics` + """ if len(args) == 1: kwargs["approx_ok"] = args[0] @@ -737,7 +791,17 @@ def ComputeStatistics(self, *args, **kwargs) -> "CPLErr": %feature("shadow") GetNoDataValue %{ def GetNoDataValue(self): - """GetNoDataValue(Band self) -> value """ + """GetNoDataValue(Band self) -> value + + Fetch the nodata value for this band. + Unlike :cpp:func:`GDALRasterBand::GetNoDataValue`, this + method handles 64-bit integer data types. + + Returns + ------- + float/int + The nodata value, or ``None`` if it has not been set. + """ if self.DataType == gdalconst.GDT_Int64: return _gdal.Band_GetNoDataValueAsInt64(self) @@ -751,7 +815,23 @@ def GetNoDataValue(self): %feature("shadow") SetNoDataValue %{ def SetNoDataValue(self, value) -> "CPLErr": - """SetNoDataValue(Band self, value) -> CPLErr""" + """SetNoDataValue(Band self, value) -> CPLErr + + Set the nodata value for this band. + Unlike :cpp:func:`GDALRasterBand::SetNoDataValue`, this + method handles 64-bit integer types. + + Parameters + ---------- + value : float/int + The nodata value to set + + Returns + ------- + int: + :py:const:`CE_None` on success or :py:const:`CE_Failure` on failure. + + """ if self.DataType == gdalconst.GDT_Int64: return _gdal.Band_SetNoDataValueAsInt64(self, value) @@ -764,7 +844,33 @@ def SetNoDataValue(self, value) -> "CPLErr": %feature("shadow") ComputeRasterMinMax %{ def ComputeRasterMinMax(self, *args, **kwargs): - """ComputeRasterMinMax(Band self, bool approx_ok=False, bool can_return_none=False) -> (min, max) or None""" + """ComputeRasterMinMax(Band self, bool approx_ok=False, bool can_return_none=False) -> (min, max) or None + + Computes the minimum and maximum values for this Band. + See :cpp:func:`GDALComputeRasterMinMax`. + + Parameters + ---------- + approx_ok : bool, default=False + If ``False``, read all pixels in the band. If ``True``, check + :py:meth:`GetMinimum`/:py:meth:`GetMaximum` or read a subsample. + can_return_none : bool, default=False + If ``True``, return ``None`` on error. Otherwise, return a tuple + with NaN values. + + Returns + ------- + tuple + + See Also + -------- + :py:meth:`ComputeBandStats` + :py:meth:`ComputeStatistics` + :py:meth:`GetMaximum` + :py:meth:`GetMinimum` + :py:meth:`GetStatistics` + :py:meth:`SetStatistics` + """ if len(args) == 1: kwargs["approx_ok"] = args[0] diff --git a/swig/python/CMakeLists.txt b/swig/python/CMakeLists.txt index 88adc00807f9..8db7378db034 100644 --- a/swig/python/CMakeLists.txt +++ b/swig/python/CMakeLists.txt @@ -31,6 +31,7 @@ set(GDAL_PYTHON_CSOURCES ${PROJECT_SOURCE_DIR}/swig/include/python/python_exceptions.i ${PROJECT_SOURCE_DIR}/swig/include/python/python_strings.i ${PROJECT_SOURCE_DIR}/swig/include/python/typemaps_python.i + ${PROJECT_SOURCE_DIR}/swig/include/python/docs/gdal_band_docs.i ${PROJECT_SOURCE_DIR}/swig/include/python/docs/gdal_dataset_docs.i ${PROJECT_SOURCE_DIR}/swig/include/python/docs/ogr_datasource_docs.i ${PROJECT_SOURCE_DIR}/swig/include/python/docs/ogr_driver_docs.i From a2a1799d6c1ac573987d468a544fe9cecfbbda31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Fri, 16 Feb 2024 15:05:57 +0800 Subject: [PATCH 040/132] Update gdaltransform.rst to use correct result --- doc/source/programs/gdaltransform.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/programs/gdaltransform.rst b/doc/source/programs/gdaltransform.rst index 6411c2d6ea43..faeedf3544c6 100644 --- a/doc/source/programs/gdaltransform.rst +++ b/doc/source/programs/gdaltransform.rst @@ -154,7 +154,7 @@ Produces the following output in meters in the "Belge 1972 / Belgian Lambert :: - 244510.77404604 166154.532871342 -1046.79270555763 + 244296.724777415 165937.350217148 0 Image RPC Example +++++++++++++++++ From b63f9ad1881853f000b054c7dd787090da1fb9dc Mon Sep 17 00:00:00 2001 From: Lucian Plesea Date: Sat, 17 Feb 2024 13:09:53 -0800 Subject: [PATCH 041/132] Use ZSTD streaming API for compression (#9230) It is considerably faster and slightly better compression for normal (2-9) ZSTD levels, due to use of different parameters. --- frmts/mrf/mrf_band.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frmts/mrf/mrf_band.cpp b/frmts/mrf/mrf_band.cpp index 97783fa3fb6a..d3eade863d2f 100644 --- a/frmts/mrf/mrf_band.cpp +++ b/frmts/mrf/mrf_band.cpp @@ -379,10 +379,20 @@ static void *ZstdCompBlock(buf_mgr &src, size_t extrasize, int c_level, dst = dbuff.data(); } - size_t val = - ZSTD_compressCCtx(cctx, dst, size, src.buffer, src.size, c_level); + // Use the streaming interface, it's faster and better + // See discussion at https://github.com/facebook/zstd/issues/3729 + ZSTD_outBuffer output = {dst, size, 0}; + ZSTD_inBuffer input = {src.buffer, src.size, 0}; + // Set level + ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, c_level); + // First, pass a continue flag, otherwise it will compress in one go + size_t val = ZSTD_compressStream2(cctx, &output, &input, ZSTD_e_continue); + // If it worked, pass the end flag to flush the buffer + if (val == 0) + val = ZSTD_compressStream2(cctx, &output, &input, ZSTD_e_end); if (ZSTD_isError(val)) return nullptr; + val = output.pos; // If we didn't need the buffer, packed data is already in the user buffer if (dbuff.empty()) From 2a51e2f865a4179d146fb96ca9bf33260c5b01c3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 18 Feb 2024 12:26:48 +0100 Subject: [PATCH 042/132] gdalwarp: cutline zero-width sliver enhancement: avoid producing invalid polygons --- apps/gdalwarp_lib.cpp | 48 +++++++++++-- autotest/utilities/test_gdalwarp_lib.py | 89 +++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/apps/gdalwarp_lib.cpp b/apps/gdalwarp_lib.cpp index 8d924e07bd8d..ce728e1bd580 100644 --- a/apps/gdalwarp_lib.cpp +++ b/apps/gdalwarp_lib.cpp @@ -4818,16 +4818,56 @@ static void RemoveZeroWidthSlivers(OGRGeometry *poGeom) const OGRwkbGeometryType eType = wkbFlatten(poGeom->getGeometryType()); if (eType == wkbMultiPolygon) { - for (auto poSubGeom : *(poGeom->toMultiPolygon())) + auto poMP = poGeom->toMultiPolygon(); + int nNumGeometries = poMP->getNumGeometries(); + for (int i = 0; i < nNumGeometries; /* incremented in loop */) { - RemoveZeroWidthSlivers(poSubGeom); + auto poPoly = poMP->getGeometryRef(i); + RemoveZeroWidthSlivers(poPoly); + if (poPoly->IsEmpty()) + { + CPLDebug("WARP", + "RemoveZeroWidthSlivers: removing empty polygon"); + poMP->removeGeometry(i, /* bDelete = */ true); + --nNumGeometries; + } + else + { + ++i; + } } } else if (eType == wkbPolygon) { - for (auto poSubGeom : *(poGeom->toPolygon())) + auto poPoly = poGeom->toPolygon(); + if (auto poExteriorRing = poPoly->getExteriorRing()) { - RemoveZeroWidthSlivers(poSubGeom); + RemoveZeroWidthSlivers(poExteriorRing); + if (poExteriorRing->getNumPoints() < 4) + { + poPoly->empty(); + return; + } + } + int nNumInteriorRings = poPoly->getNumInteriorRings(); + for (int i = 0; i < nNumInteriorRings; /* incremented in loop */) + { + auto poRing = poPoly->getInteriorRing(i); + RemoveZeroWidthSlivers(poRing); + if (poRing->getNumPoints() < 4) + { + CPLDebug( + "WARP", + "RemoveZeroWidthSlivers: removing empty interior ring"); + constexpr int OFFSET_EXTERIOR_RING = 1; + poPoly->removeRing(i + OFFSET_EXTERIOR_RING, + /* bDelete = */ true); + --nNumInteriorRings; + } + else + { + ++i; + } } } else if (eType == wkbLineString) diff --git a/autotest/utilities/test_gdalwarp_lib.py b/autotest/utilities/test_gdalwarp_lib.py index 07ccacfc5c09..0585b04cd271 100755 --- a/autotest/utilities/test_gdalwarp_lib.py +++ b/autotest/utilities/test_gdalwarp_lib.py @@ -31,6 +31,7 @@ ############################################################################### import collections +import json import shutil import struct @@ -3004,6 +3005,94 @@ def test_gdalwarp_lib_cutline_zero_width_sliver(tmp_vsimem): assert ds is not None +############################################################################### +# Test cutline with zero-width sliver + + +def test_gdalwarp_lib_cutline_zero_width_sliver_remove_empty_polygon(tmp_vsimem): + + geojson = { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [-101.43346, 36.91886], + [-101.43337, 36.91864], + [-101.43332, 36.91865], + [-101.43342, 36.91888], + [-101.43346, 36.91886], + ] + ], + # The below polygon has a zero-width sliver + [ + [ + [-101.4311, 36.91909], + [-101.43106, 36.91913], + [-101.43111, 36.91908], + [-101.4311, 36.91909], + ] + ], + ], + } + gdal.FileFromMemBuffer(tmp_vsimem / "cutline.geojson", json.dumps(geojson)) + src_ds = gdal.GetDriverByName("MEM").Create("", 100, 100) + srs = osr.SpatialReference() + srs.ImportFromEPSG(4326) + src_ds.SetSpatialRef(srs) + src_ds.SetGeoTransform([-101.5, 0.1, 0, 37, 0, -0.1]) + ds = gdal.Warp( + "", src_ds, format="MEM", cutlineDSName=tmp_vsimem / "cutline.geojson" + ) + assert ds is not None + + +############################################################################### +# Test cutline with zero-width sliver + + +def test_gdalwarp_lib_cutline_zero_width_sliver_remove_empty_inner_ring(tmp_vsimem): + + geojson = { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [-101.5, 37], + [-101.4, 37], + [-101.4, 36.9], + [-101.5, 36.9], + [-101.5, 37], + ], + # The below ring has a zero-width sliver + [ + [-101.4311, 36.91909], + [-101.43106, 36.91913], + [-101.43111, 36.91908], + [-101.4311, 36.91909], + ], + # This one is OK + [ + [-101.49, 36.95], + [-101.48, 36.95], + [-101.48, 36.94], + [-101.49, 36.94], + [-101.49, 36.95], + ], + ] + ], + } + gdal.FileFromMemBuffer(tmp_vsimem / "cutline.geojson", json.dumps(geojson)) + src_ds = gdal.GetDriverByName("MEM").Create("", 100, 100) + srs = osr.SpatialReference() + srs.ImportFromEPSG(4326) + src_ds.SetSpatialRef(srs) + src_ds.SetGeoTransform([-101.6, 0.1, 0, 37.1, 0, -0.1]) + ds = gdal.Warp( + "", src_ds, format="MEM", cutlineDSName=tmp_vsimem / "cutline.geojson" + ) + assert ds is not None + + ############################################################################### # Test support for propagating coordinate epoch From 221241e20a64963843d89c400decdc72a8078333 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 18 Feb 2024 16:26:09 +0100 Subject: [PATCH 043/132] Doc: advertize v3.8.4 --- doc/source/about_no_title.rst | 4 ++-- doc/source/download.rst | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/doc/source/about_no_title.rst b/doc/source/about_no_title.rst index 4b9d5f68523e..4c6634af523f 100644 --- a/doc/source/about_no_title.rst +++ b/doc/source/about_no_title.rst @@ -1,13 +1,13 @@ .. include:: ./substitutions.rst -GDAL is a translator library for raster and vector geospatial data formats that is released under an MIT style Open Source :ref:`license` by the `Open Source Geospatial Foundation`_. As a library, it presents a single raster abstract data model and single vector abstract data model to the calling application for all supported formats. It also comes with a variety of useful command line utilities for data translation and processing. The `NEWS`_ page describes the January 2024 GDAL/OGR 3.8.3 release. +GDAL is a translator library for raster and vector geospatial data formats that is released under an MIT style Open Source :ref:`license` by the `Open Source Geospatial Foundation`_. As a library, it presents a single raster abstract data model and single vector abstract data model to the calling application for all supported formats. It also comes with a variety of useful command line utilities for data translation and processing. The `NEWS`_ page describes the February 2024 GDAL/OGR 3.8.4 release. .. image:: ../images/OSGeo_project.png :alt: OSGeo project :target: `Open Source Geospatial Foundation`_ .. _`Open Source Geospatial Foundation`: http://www.osgeo.org/ -.. _`NEWS`: https://github.com/OSGeo/gdal/blob/v3.8.3/NEWS.md +.. _`NEWS`: https://github.com/OSGeo/gdal/blob/v3.8.4/NEWS.md See :ref:`software_using_gdal` diff --git a/doc/source/download.rst b/doc/source/download.rst index 686f2e0f72e8..8583de31abc6 100644 --- a/doc/source/download.rst +++ b/doc/source/download.rst @@ -13,15 +13,21 @@ Download Current Release ------------------------------------------------------------------------------ +* **2024-02-18** `gdal-3.8.4.tar.gz`_ `3.8.4 Release Notes`_ (`3.8.4 md5`_) + +.. _`3.8.4 Release Notes`: https://github.com/OSGeo/gdal/blob/v3.8.4/NEWS.md +.. _`gdal-3.8.4.tar.gz`: https://github.com/OSGeo/gdal/releases/download/v3.8.4/gdal-3.8.4.tar.gz +.. _`3.8.4 md5`: https://github.com/OSGeo/gdal/releases/download/v3.8.4/gdal-3.8.4.tar.gz.md5 + +Past Releases +------------------------------------------------------------------------------ + * **2024-01-08** `gdal-3.8.3.tar.gz`_ `3.8.3 Release Notes`_ (`3.8.3 md5`_) .. _`3.8.3 Release Notes`: https://github.com/OSGeo/gdal/blob/v3.8.3/NEWS.md .. _`gdal-3.8.3.tar.gz`: https://github.com/OSGeo/gdal/releases/download/v3.8.3/gdal-3.8.3.tar.gz .. _`3.8.3 md5`: https://github.com/OSGeo/gdal/releases/download/v3.8.3/gdal-3.8.3.tar.gz.md5 -Past Releases ------------------------------------------------------------------------------- - * **2023-12-20** `gdal-3.8.2.tar.gz`_ `3.8.2 Release Notes`_ (`3.8.2 md5`_) .. _`3.8.2 Release Notes`: https://github.com/OSGeo/gdal/blob/v3.8.2/NEWS.md From 8aff060e2406d97d19b87d6cd96b422a21a50600 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 18 Feb 2024 18:51:14 +0100 Subject: [PATCH 044/132] Doc: CPLGetLastErrorMsg(): fix to reflect implementation --- port/cpl_error.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/port/cpl_error.cpp b/port/cpl_error.cpp index 20536f0cfd6a..a08d55f2a181 100644 --- a/port/cpl_error.cpp +++ b/port/cpl_error.cpp @@ -879,8 +879,8 @@ CPLErr CPL_STDCALL CPLGetLastErrorType() * been cleared by CPLErrorReset(). The returned pointer is to an internal * string that should not be altered or freed. * - * @return the last error message, or NULL if there is no posted error - * message. + * @return the last error message, or an empty string ("") if there is no + * posted error message. */ const char *CPL_STDCALL CPLGetLastErrorMsg() From 32f73d7bad039d1d10d9bc6db77c52a429ad6472 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 18 Feb 2024 19:03:14 +0100 Subject: [PATCH 045/132] delete_untagged_containers.yml: only run on OSGeo/GDAL repository --- .github/workflows/delete_untagged_containers.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/delete_untagged_containers.yml b/.github/workflows/delete_untagged_containers.yml index b6321b784857..4510b0c21ca7 100644 --- a/.github/workflows/delete_untagged_containers.yml +++ b/.github/workflows/delete_untagged_containers.yml @@ -14,6 +14,7 @@ jobs: delete-untagged-containers: name: Delete all containers from gdal-deps without tags runs-on: ubuntu-latest + if: github.repository == 'OSGeo/gdal' steps: - name: Delete all containers from gdal-deps without tags uses: Chizkiyahu/delete-untagged-ghcr-action@bbbab219998078a91c9b283dac9389b825894603 # v3.2.0 From 877fc71bfad3a44b0ddfbff79044f7ee2ea319d9 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 18 Feb 2024 19:26:19 +0100 Subject: [PATCH 046/132] /vsicurl/: Read(): emit error message when receiving HTTP 416 Range Not Satisfiable error --- port/cpl_vsil_curl.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/port/cpl_vsil_curl.cpp b/port/cpl_vsil_curl.cpp index 9063da67925e..3bc0ba7fcfdd 100644 --- a/port/cpl_vsil_curl.cpp +++ b/port/cpl_vsil_curl.cpp @@ -1974,6 +1974,22 @@ std::string VSICurlHandle::DownloadRegion(const vsi_l_offset startOffset, CPLError(CE_Failure, CPLE_AppDefined, "%d: %s", static_cast(response_code), szCurlErrBuf); } + else if (response_code == 416) /* Range Not Satisfiable */ + { + if (sWriteFuncData.pBuffer) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "%d: Range downloading not supported by this server: %s", + static_cast(response_code), sWriteFuncData.pBuffer); + } + else + { + CPLError(CE_Failure, CPLE_AppDefined, + "%d: Range downloading not supported by this server", + static_cast(response_code)); + } + } if (!oFileProp.bHasComputedFileSize && startOffset == 0) { oFileProp.bHasComputedFileSize = true; From 2917a5bc3c3f7d3911dcaa3efd56b2ebad89ce37 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 18 Feb 2024 19:42:11 +0100 Subject: [PATCH 047/132] JP2OpenJPEG: CreateCopy(): limit number of resolutions taking into account minimum block width/height (fixes #9236) --- autotest/gdrivers/jp2openjpeg.py | 14 ++++++++++++++ frmts/opjlike/jp2opjlikedataset.cpp | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/autotest/gdrivers/jp2openjpeg.py b/autotest/gdrivers/jp2openjpeg.py index e38952725803..8ecddf3bd252 100755 --- a/autotest/gdrivers/jp2openjpeg.py +++ b/autotest/gdrivers/jp2openjpeg.py @@ -3850,3 +3850,17 @@ def test_jp2openjpeg_vrt_protocol(): webserver.server_stop(webserver_process, webserver_port) gdal.VSICurlClearCache() + + +############################################################################### +# Test fix for https://github.com/OSGeo/gdal/issues/9236 + + +def test_jp2openjpeg_limit_resolution_count_from_image_size(tmp_vsimem): + + filename = str(tmp_vsimem / "out.jp2") + assert gdal.Translate(filename, "data/byte.tif", width=1024, height=7) + + # Check number of resolutions + ret = gdal.GetJPEG2000StructureAsString(filename, ["ALL=YES"]) + assert '2' in ret diff --git a/frmts/opjlike/jp2opjlikedataset.cpp b/frmts/opjlike/jp2opjlikedataset.cpp index b857d78cd44f..f044b0644f9e 100644 --- a/frmts/opjlike/jp2opjlikedataset.cpp +++ b/frmts/opjlike/jp2opjlikedataset.cpp @@ -2177,9 +2177,11 @@ GDALDataset *JP2OPJLikeDataset::CreateCopy( } const int nMaxTileDim = std::max(nBlockXSize, nBlockYSize); + const int nMinTileDim = std::min(nBlockXSize, nBlockYSize); int nNumResolutions = 1; /* Pickup a reasonable value compatible with PROFILE_1 requirements */ - while ((nMaxTileDim >> (nNumResolutions - 1)) > 128) + while ((nMaxTileDim >> (nNumResolutions - 1)) > 128 && + (nMinTileDim >> nNumResolutions) > 0) nNumResolutions++; int nMinProfile1Resolutions = nNumResolutions; const char *pszResolutions = @@ -2188,6 +2190,7 @@ GDALDataset *JP2OPJLikeDataset::CreateCopy( { nNumResolutions = atoi(pszResolutions); if (nNumResolutions <= 0 || nNumResolutions >= 32 || + (nMinTileDim >> nNumResolutions) == 0 || (nMaxTileDim >> nNumResolutions) == 0) { CPLError(CE_Warning, CPLE_NotSupported, From 8c4a89156c0d8617148a9f43a84cd25a12fe9d5b Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 00:13:01 +0100 Subject: [PATCH 048/132] PDF: update to support (and require) PDFium/6309 --- .github/workflows/cmake_builds.yml | 10 +++--- .github/workflows/ubuntu_20.04/Dockerfile.ci | 6 ++-- doc/source/drivers/raster/pdf.rst | 8 ++++- docker/ubuntu-full/Dockerfile | 6 ++-- frmts/pdf/pdfdataset.cpp | 34 +++++++++----------- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/.github/workflows/cmake_builds.yml b/.github/workflows/cmake_builds.yml index c2fd53ccad34..95f2e619023b 100644 --- a/.github/workflows/cmake_builds.yml +++ b/.github/workflows/cmake_builds.yml @@ -141,12 +141,12 @@ jobs: - name: Install pdfium run: | - wget -q https://github.com/rouault/pdfium_build_gdal_3_8/releases/download/pdfium_5952_v1/install-ubuntu2004-rev5952.tar.gz \ - && tar -xzf install-ubuntu2004-rev5952.tar.gz \ + wget -q https://github.com/rouault/pdfium_build_gdal_3_9/releases/download/pdfium_6309_v1/install-ubuntu2004-rev6309.tar.gz \ + && tar -xzf install-ubuntu2004-rev6309.tar.gz \ && sudo chown -R root:root install \ && sudo mv install/lib/* /usr/lib/ \ && sudo mv install/include/* /usr/include/ \ - && sudo rm -rf install-ubuntu2004-rev5952.tar.gz install \ + && sudo rm -rf install-ubuntu2004-rev6309.tar.gz install \ && sudo apt-get update -y \ && sudo apt-get install -y --fix-missing --no-install-recommends liblcms2-dev - name: Configure ccache @@ -433,8 +433,8 @@ jobs: - name: Install pdfium shell: bash -l {0} run: | - curl -LOs https://github.com/rouault/pdfium_build_gdal_3_8/releases/download/pdfium_5952_v1/install-win10-vs2019-x64-rev5952.zip - unzip install-win10-vs2019-x64-rev5952.zip + curl -LOs https://github.com/rouault/pdfium_build_gdal_3_9/releases/download/pdfium_6309_v1/install-win10-vs2019-x64-rev6309.zip + unzip install-win10-vs2019-x64-rev6309.zip mv install install-pdfium - name: Remove conflicting libraries diff --git a/.github/workflows/ubuntu_20.04/Dockerfile.ci b/.github/workflows/ubuntu_20.04/Dockerfile.ci index 54798996984c..58b223bcb2b3 100644 --- a/.github/workflows/ubuntu_20.04/Dockerfile.ci +++ b/.github/workflows/ubuntu_20.04/Dockerfile.ci @@ -182,12 +182,12 @@ RUN mkdir geos \ && rm -rf geos # Install pdfium -RUN wget -q https://github.com/rouault/pdfium_build_gdal_3_8/releases/download/pdfium_5952_v1/install-ubuntu2004-rev5952.tar.gz \ - && tar -xzf install-ubuntu2004-rev5952.tar.gz \ +RUN wget -q https://github.com/rouault/pdfium_build_gdal_3_9/releases/download/pdfium_6309_v1/install-ubuntu2004-rev6309.tar.gz \ + && tar -xzf install-ubuntu2004-rev6309.tar.gz \ && chown -R root:root install \ && mv install/lib/* /usr/lib/ \ && mv install/include/* /usr/include/ \ - && rm -rf install-ubuntu2004-rev5952.tar.gz install + && rm -rf install-ubuntu2004-rev6309.tar.gz install # HANA: client side # Install hdbsql tool diff --git a/doc/source/drivers/raster/pdf.rst b/doc/source/drivers/raster/pdf.rst index 3825c56e3fc2..bdc949b6550b 100644 --- a/doc/source/drivers/raster/pdf.rst +++ b/doc/source/drivers/raster/pdf.rst @@ -721,9 +721,15 @@ Only GDAL builds against static builds of PDFium have been tested. Building PDFium can be challenging, and particular builds must be used to work properly with GDAL. -With GDAL >= 3.8 +With GDAL >= 3.9 ++++++++++++++++ +The scripts in the ``__ +repository must be used to build a patched version of PDFium. + +With GDAL = 3.8 ++++++++++++++++ + The scripts in the ``__ repository must be used to build a patched version of PDFium. diff --git a/docker/ubuntu-full/Dockerfile b/docker/ubuntu-full/Dockerfile index ee94d2ad288f..c85744b94af0 100644 --- a/docker/ubuntu-full/Dockerfile +++ b/docker/ubuntu-full/Dockerfile @@ -197,12 +197,12 @@ RUN . /buildscripts/bh-set-envvars.sh \ ARG WITH_PDFIUM=yes RUN if echo "$WITH_PDFIUM" | grep -Eiq "^(y(es)?|1|true)$" ; then ( \ - wget -q https://github.com/rouault/pdfium_build_gdal_3_8/releases/download/pdfium_5952_v1/install-ubuntu2004-rev5952.tar.gz \ - && tar -xzf install-ubuntu2004-rev5952.tar.gz \ + wget -q https://github.com/rouault/pdfium_build_gdal_3_9/releases/download/pdfium_6309_v1/install-ubuntu2004-rev6309.tar.gz \ + && tar -xzf install-ubuntu2004-rev6309.tar.gz \ && chown -R root:root install \ && mv install/lib/* /usr/lib/ \ && mv install/include/* /usr/include/ \ - && rm -rf install-ubuntu2004-rev5952.tar.gz install \ + && rm -rf install-ubuntu2004-rev6309.tar.gz install \ && apt-get update -y \ && apt-get install -y --fix-missing --no-install-recommends liblcms2-dev${APT_ARCH_SUFFIX} \ && rm -rf /var/lib/apt/lists/* \ diff --git a/frmts/pdf/pdfdataset.cpp b/frmts/pdf/pdfdataset.cpp index ce51fa4ce611..74173f50058b 100644 --- a/frmts/pdf/pdfdataset.cpp +++ b/frmts/pdf/pdfdataset.cpp @@ -1566,18 +1566,17 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface return m_poParent->GetBackDrop(); } - virtual bool SetDIBits(const RetainPtr &pBitmap, - uint32_t color, const FX_RECT &src_rect, - int dest_left, int dest_top, + virtual bool SetDIBits(RetainPtr bitmap, uint32_t color, + const FX_RECT &src_rect, int dest_left, int dest_top, BlendMode blend_type) override { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->SetDIBits(pBitmap, color, src_rect, dest_left, + return m_poParent->SetDIBits(bitmap, color, src_rect, dest_left, dest_top, blend_type); } - virtual bool StretchDIBits(const RetainPtr &pBitmap, + virtual bool StretchDIBits(RetainPtr bitmap, uint32_t color, int dest_left, int dest_top, int dest_width, int dest_height, const FX_RECT *pClipRect, @@ -1586,22 +1585,21 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->StretchDIBits(pBitmap, color, dest_left, dest_top, + return m_poParent->StretchDIBits(bitmap, color, dest_left, dest_top, dest_width, dest_height, pClipRect, options, blend_type); } - virtual bool StartDIBits(const RetainPtr &pBitmap, - int bitmap_alpha, uint32_t color, - const CFX_Matrix &matrix, + virtual bool StartDIBits(RetainPtr bitmap, float alpha, + uint32_t color, const CFX_Matrix &matrix, const FXDIB_ResampleOptions &options, std::unique_ptr *handle, BlendMode blend_type) override { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->StartDIBits(pBitmap, bitmap_alpha, color, matrix, - options, handle, blend_type); + return m_poParent->StartDIBits(bitmap, alpha, color, matrix, options, + handle, blend_type); } virtual bool ContinueDIBits(CFX_ImageRenderer *handle, @@ -1655,21 +1653,21 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface return m_poParent->MultiplyAlpha(alpha); } - bool MultiplyAlpha(const RetainPtr &mask) override + bool MultiplyAlphaMask(RetainPtr mask) override { - return m_poParent->MultiplyAlpha(mask); + return m_poParent->MultiplyAlphaMask(mask); } #if defined(_SKIA_SUPPORT_) - virtual bool SetBitsWithMask(const RetainPtr &pBitmap, - const RetainPtr &pMask, int left, - int top, int bitmap_alpha, + virtual bool SetBitsWithMask(RetainPtr bitmap, + RetainPtr mask, int left, + int top, float alpha, BlendMode blend_type) override { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->SetBitsWithMask(pBitmap, pMask, left, top, - bitmap_alpha, blend_type); + return m_poParent->SetBitsWithMask(bitmap, mask, left, top, alpha, + blend_type); } virtual void SetGroupKnockout(bool group_knockout) override { From 2b225a7fd76458bfef00f1a452f2e73f71125845 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 02:40:24 +0000 Subject: [PATCH 049/132] build(deps): bump github/codeql-action from 3.24.0 to 3.24.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.0 to 3.24.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e8893c57a1f3a2b659b6b55564fdfdbbd2982911...379614612a29c9e28f31f39a59013eb8012a51f0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index af089a34730a..6d2ab2f72afe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0 + uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -148,6 +148,6 @@ jobs: key: ${{ steps.restore-cache.outputs.cache-primary-key }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0 + uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8ecc21491910..4156ae671d79 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0 + uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: sarif_file: results.sarif From 577e6ae558e48cb93bbb46db4e4a62b9476e0e07 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 10:47:05 +0100 Subject: [PATCH 050/132] docker/README.md: adverize 3.8.4 images [ci skip] --- docker/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/README.md b/docker/README.md index 1d8362c4eaea..0e22b44ab15c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -98,11 +98,11 @@ If you are getting a ``: arena 0 background thread creation failed (1) # Images of releases -Tagged images of recent past releases are available. The last ones (at time of writing) are for GDAL 3.8.3 and PROJ 9.3.1, for linux/amd64 and linux/arm64: -* ghcr.io/osgeo/gdal:alpine-small-3.8.3 -* ghcr.io/osgeo/gdal:alpine-normal-3.8.3 -* ghcr.io/osgeo/gdal:ubuntu-small-3.8.3 -* ghcr.io/osgeo/gdal:ubuntu-full-3.8.3 +Tagged images of recent past releases are available. The last ones (at time of writing) are for GDAL 3.8.4 and PROJ 9.3.1, for linux/amd64 and linux/arm64: +* ghcr.io/osgeo/gdal:alpine-small-3.8.4 +* ghcr.io/osgeo/gdal:alpine-normal-3.8.4 +* ghcr.io/osgeo/gdal:ubuntu-small-3.8.4 +* ghcr.io/osgeo/gdal:ubuntu-full-3.8.4 ## Multi-arch Images From 92e8705c1602f005f340c4effcd74ebaea5576d3 Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Mon, 19 Feb 2024 09:25:33 -0500 Subject: [PATCH 051/132] Python: Update ReadAsArray, WriteArray docstrings --- swig/include/python/gdal_python.i | 47 +++++++++++++++++++------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index f987a1842f11..6e5a3559b017 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -559,16 +559,16 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse Parameters ---------- - xoff : int, default=0 + xoff : float, default=0 The pixel offset to left side of the region of the band to be read. This would be zero to start from the left side. - yoff : int, default=0 + yoff : float, default=0 The line offset to top side of the region of the band to be read. This would be zero to start from the top side. - win_xsize : int, optional + win_xsize : float, optional The number of pixels to read in the x direction. By default, equal to the number of columns in the raster. - win_ysize : int, optional + win_ysize : float, optional The number of rows to read in the y direction. By default, equal to the number of bands in the raster. buf_xsize : int, optional @@ -600,27 +600,36 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse Examples -------- >>> import numpy as np - >>> ds = gdal.GetDriverByName("GTiff").Create("test.tif", 4, 4) + >>> ds = gdal.GetDriverByName("GTiff").Create("test.tif", 4, 4, eType=gdal.GDT_Float32) >>> ds.WriteArray(np.arange(16).reshape(4, 4)) 0 >>> band = ds.GetRasterBand(1) + >>> # Reading an entire band >>> band.ReadAsArray() - array([[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11], - [12, 13, 14, 15]], dtype=uint8) + array([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]], dtype=float32) + >>> # Reading a window of a band >>> band.ReadAsArray(xoff=2, yoff=2, win_xsize=2, win_ysize=2) - array([[10, 11], - [14, 15]], dtype=uint8) + array([[10., 11.], + [14., 15.]], dtype=float32) + >>> # Reading a band into a new buffer at higher resolution + >>> band.ReadAsArray(xoff=0.5, yoff=0.5, win_xsize=2.5, win_ysize=2.5, buf_xsize=5, buf_ysize=5) + array([[ 0., 1., 1., 2., 2.], + [ 4., 5., 5., 6., 6.], + [ 4., 5., 5., 6., 6.], + [ 8., 9., 9., 10., 10.], + [ 8., 9., 9., 10., 10.]], dtype=float32) + >>> # Reading a band into an existing buffer at lower resolution >>> band.ReadAsArray(buf_xsize=2, buf_ysize=2, buf_type=gdal.GDT_Float64, resample_alg=gdal.GRIORA_Average) - array([[ 3., 5.], - [11., 13.]]) + array([[ 2.5, 4.5], + [10.5, 12.5]]) >>> buf = np.zeros((2,2)) >>> band.ReadAsArray(buf_obj=buf) array([[ 5., 7.], [13., 15.]]) """ - from osgeo import gdal_array return gdal_array.BandReadAsArray(self, xoff, yoff, @@ -648,6 +657,7 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_path, GByte **out, vsi_l_offse The line offset to top side of the region of the band to be written. This would be zero to start from the top side. resample_alg : int, default = :py:const:`gdal.GRIORA_NearestNeighbour` + Resampling algorithm. Placeholder argument, not currently supported. callback : function, optional A progress callback function callback_data: optional @@ -1024,16 +1034,16 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, Parameters ---------- - xoff : int, default=0 + xoff : float, default=0 The pixel offset to left side of the region of the band to be read. This would be zero to start from the left side. - yoff : int, default=0 + yoff : float, default=0 The line offset to top side of the region of the band to be read. This would be zero to start from the top side. - xsize : int, optional + xsize : float, optional The number of pixels to read in the x direction. By default, equal to the number of columns in the raster. - ysize : int, optional + ysize : float, optional The number of rows to read in the y direction. By default, equal to the number of bands in the raster. buf_xsize : int, optional @@ -1144,6 +1154,7 @@ CPLErr ReadRaster1( double xoff, double yoff, double xsize, double ysize, interleaved-writing, ``array`` should have shape ``(ny, nx, nbands)``. resample_alg : int, default = :py:const:`gdal.GRIORA_NearestNeighbour` + Resampling algorithm. Placeholder argument, not currently supported. callback : function, optional A progress callback function callback_data: optional From d0231dbf232d2febd7da0ee73488dca7e728590d Mon Sep 17 00:00:00 2001 From: Dan Baston Date: Mon, 19 Feb 2024 09:26:46 -0500 Subject: [PATCH 052/132] Update swig/include/python/docs/gdal_dataset_docs.i Co-authored-by: Even Rouault --- swig/include/python/docs/gdal_dataset_docs.i | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swig/include/python/docs/gdal_dataset_docs.i b/swig/include/python/docs/gdal_dataset_docs.i index 8ff2ec83b11c..0a4ec8c07d8f 100644 --- a/swig/include/python/docs/gdal_dataset_docs.i +++ b/swig/include/python/docs/gdal_dataset_docs.i @@ -672,7 +672,7 @@ bool Examples -------- ->>> ds = gdal.GetDriverByName('ESRI Shapefile').Create('test.shp', 0, 0) +>>> ds = gdal.GetDriverByName('ESRI Shapefile').Create('test.shp', 0, 0, 0, gdal.GDT_Unknown) >>> ds.TestCapability(ogr.ODsCTransactions) False >>> ds.TestCapability(ogr.ODsCMeasuredGeometries) From fcfea6e9a5836d8bb459c717f9bae7094bbf1a47 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 19:43:57 +0100 Subject: [PATCH 053/132] PG: remove support for PostgreSQL < 9 and PostGIS < 2 (fixes #8937) --- autotest/ogr/ogr_pg.py | 82 ++--------- doc/source/drivers/vector/pg.rst | 8 +- ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp | 193 ++----------------------- ogr/ogrsf_frmts/pg/ogrpglayer.cpp | 72 ++++----- ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp | 80 ++-------- 5 files changed, 66 insertions(+), 369 deletions(-) diff --git a/autotest/ogr/ogr_pg.py b/autotest/ogr/ogr_pg.py index 42e2a9c3106b..40c7d5c8a3dc 100755 --- a/autotest/ogr/ogr_pg.py +++ b/autotest/ogr/ogr_pg.py @@ -307,12 +307,6 @@ def pg_quote_with_E(pg_autotest_ds): return True -@pytest.fixture(scope="module") -def pg_retrieve_fid(pg_version): - - return pg_version >= (8, 2) - - ############################################################################### # Create table from data/poly.shp @@ -2023,7 +2017,7 @@ def test_ogr_pg_39(pg_ds): @only_with_postgis -def test_ogr_pg_39_bis(pg_ds, pg_has_postgis, pg_postgis_version): +def test_ogr_pg_39_bis(pg_ds, pg_has_postgis): schema = current_schema(pg_ds) @@ -2038,10 +2032,6 @@ def test_ogr_pg_39_bis(pg_ds, pg_has_postgis, pg_postgis_version): # Create a view pg_ds.ExecuteSQL("CREATE VIEW testview AS SELECT * FROM inherited") - if pg_postgis_version[0] < 2: - pg_ds.ExecuteSQL( - f"INSERT INTO geometry_columns VALUES ( '', '{schema}', 'testview', 'wkb_geometry', 2, -1, 'POINT') " - ) pg_ds.ExecuteSQL( "INSERT INTO inherited (col1, wkb_geometry) VALUES ( 'a', GeomFromEWKT('POINT (0 1)') )" ) @@ -2069,10 +2059,7 @@ def test_ogr_pg_39_bis(pg_ds, pg_has_postgis, pg_postgis_version): ), ("bad geometry %s" % feat.GetGeometryRef().ExportToWkt()) # Test another geometry column - if pg_postgis_version[0] < 2: - pg_ds.ExecuteSQL( - f"INSERT INTO geometry_columns VALUES ( '', '{schema}', 'testview', 'point25D', 3, -1, 'POINT') " - ) + pg_ds.ExecuteSQL( "UPDATE inherited SET \"point25D\" = GeomFromEWKT('POINT (0 1 2)') " ) @@ -2796,12 +2783,9 @@ def test_ogr_pg_53_bis(tmp_path, pg_ds): @only_with_postgis -def test_ogr_pg_54(pg_ds, pg_postgis_version): +def test_ogr_pg_54(pg_ds): - if pg_postgis_version[0] >= 2: - sql_lyr = pg_ds.ExecuteSQL("SELECT ST_AsEWKB(GeomFromEWKT('POINT (0 1 2)'))") - else: - sql_lyr = pg_ds.ExecuteSQL("SELECT AsEWKB(GeomFromEWKT('POINT (0 1 2)'))") + sql_lyr = pg_ds.ExecuteSQL("SELECT ST_AsEWKB(GeomFromEWKT('POINT (0 1 2)'))") feat = sql_lyr.GetNextFeature() pg_ds.ReleaseResultSet(sql_lyr) @@ -3538,7 +3522,7 @@ def test_ogr_pg_70bis(pg_ds, pg_postgis_schema): @only_with_postgis -def test_ogr_pg_71(pg_ds, pg_postgis_version): +def test_ogr_pg_71(pg_ds): curve_lyr = pg_ds.CreateLayer("test_curve") curve_lyr2 = pg_ds.CreateLayer( @@ -3574,25 +3558,7 @@ def test_ogr_pg_71(pg_ds, pg_postgis_version): "GEOMETRYCOLLECTION (CIRCULARSTRING (0 1,2 3,4 5),COMPOUNDCURVE ((0 1,2 3,4 5)),CURVEPOLYGON ((0 0,0 1,1 1,1 0,0 0)),MULTICURVE ((0 0,1 1)),MULTISURFACE (((0 0,0 10,10 10,10 0,0 0))))", ]: - # would cause PostGIS 1.X to crash - if pg_postgis_version[0] < 2 and wkt == "CURVEPOLYGON EMPTY": - continue - # Parsing error of WKT by PostGIS 1.X - if ( - pg_postgis_version[0] < 2 - and "MULTICURVE" in wkt - and "CIRCULARSTRING" in wkt - ): - continue - postgis_in_wkt = wkt - while True: - z_pos = postgis_in_wkt.find("Z ") - # PostGIS 1.X doesn't like Z in WKT - if pg_postgis_version[0] < 2 and z_pos >= 0: - postgis_in_wkt = postgis_in_wkt[0:z_pos] + postgis_in_wkt[z_pos + 2 :] - else: - break # Test parsing PostGIS WKB lyr = pg_ds.ExecuteSQL("SELECT ST_GeomFromText('%s')" % postgis_in_wkt) @@ -3604,18 +3570,11 @@ def test_ogr_pg_71(pg_ds, pg_postgis_version): pg_ds.ReleaseResultSet(lyr) expected_wkt = wkt - if pg_postgis_version[0] < 2 and "EMPTY" in wkt: - expected_wkt = "GEOMETRYCOLLECTION EMPTY" assert out_wkt == expected_wkt # Test parsing PostGIS WKT - if pg_postgis_version[0] >= 2: - fct = "ST_AsText" - else: - fct = "AsEWKT" - lyr = pg_ds.ExecuteSQL( - "SELECT %s(ST_GeomFromText('%s'))" % (fct, postgis_in_wkt) + "SELECT ST_AsText(ST_GeomFromText('%s'))" % (postgis_in_wkt) ) f = lyr.GetNextFeature() g = f.GetGeometryRef() @@ -3625,8 +3584,6 @@ def test_ogr_pg_71(pg_ds, pg_postgis_version): pg_ds.ReleaseResultSet(lyr) expected_wkt = wkt - if pg_postgis_version[0] < 2 and "EMPTY" in wkt: - expected_wkt = "GEOMETRYCOLLECTION EMPTY" assert out_wkt == expected_wkt g = ogr.CreateGeometryFromWkt(wkt) @@ -3643,13 +3600,9 @@ def test_ogr_pg_71(pg_ds, pg_postgis_version): assert ret == 0, wkt fid = f.GetFID() - # AsEWKT() in PostGIS 1.X does not like CIRCULARSTRING EMPTY - if pg_postgis_version[0] < 2 and "CIRCULARSTRING" in wkt and "EMPTY" in wkt: - continue - lyr = pg_ds.ExecuteSQL( - "SELECT %s(wkb_geometry) FROM %s WHERE ogc_fid = %d" - % (fct, active_lyr.GetName(), fid) + "SELECT ST_AsText(wkb_geometry) FROM %s WHERE ogc_fid = %d" + % (active_lyr.GetName(), fid) ) f = lyr.GetNextFeature() g = f.GetGeometryRef() @@ -4165,14 +4118,14 @@ def ogr_pg_76_get_transaction_state(ds): ) -def test_ogr_pg_76(pg_ds, pg_postgis_version, use_postgis): +def test_ogr_pg_76(pg_ds, use_postgis): assert pg_ds.TestCapability(ogr.ODsCTransactions) == 1 level = int(pg_ds.GetMetadataItem("nSoftTransactionLevel", "_DEBUG_")) assert level == 0 - if use_postgis and pg_postgis_version[0] >= 2: + if use_postgis: pg_ds.StartTransaction() lyr = pg_ds.CreateLayer("will_not_be_created", options=["OVERWRITE=YES"]) lyr.CreateField(ogr.FieldDefn("foo", ogr.OFTString)) @@ -4505,10 +4458,7 @@ def test_ogr_pg_77(pg_ds, tmp_path): @only_with_postgis -def test_ogr_pg_78(pg_ds, pg_postgis_version): - - if pg_postgis_version[0] < 2: - pytest.skip("Requires PostGIS >= 2.0") +def test_ogr_pg_78(pg_ds): pg_ds.ExecuteSQL("CREATE TABLE ogr_pg_78 (ID INTEGER PRIMARY KEY)") pg_ds.ExecuteSQL("ALTER TABLE ogr_pg_78 ADD COLUMN my_geom GEOMETRY") @@ -4802,7 +4752,7 @@ def ogr_pg_83_ids(param): ], ids=ogr_pg_83_ids, ) -def test_ogr_pg_83(pg_ds, pg_postgis_version, geom_type, options, wkt, expected_wkt): +def test_ogr_pg_83(pg_ds, geom_type, options, wkt, expected_wkt): lyr = pg_ds.CreateLayer("ogr_pg_83", geom_type=geom_type, options=options) f = ogr.Feature(lyr.GetLayerDefn()) @@ -4821,12 +4771,8 @@ def test_ogr_pg_83(pg_ds, pg_postgis_version, geom_type, options, wkt, expected_ if "GEOM_TYPE=geography" in options: return - # Cannot do AddGeometryColumn( 'GEOMETRYM', 3 ) with PostGIS 2, and doesn't accept inserting a XYM geometry - if ( - pg_postgis_version[0] >= 2 - and geom_type == ogr.wkbUnknown - and options == ["DIM=XYM"] - ): + # Cannot do AddGeometryColumn( 'GEOMETRYM', 3 ) with PostGIS >= 2, and doesn't accept inserting a XYM geometry + if geom_type == ogr.wkbUnknown and options == ["DIM=XYM"]: return lyr = pg_ds.CreateLayer( diff --git a/doc/source/drivers/vector/pg.rst b/doc/source/drivers/vector/pg.rst index 055958a22572..490e1caa9708 100644 --- a/doc/source/drivers/vector/pg.rst +++ b/doc/source/drivers/vector/pg.rst @@ -19,6 +19,8 @@ instead use the :ref:`PostgreSQL SQL Dump driver `. You can find additional information on the driver in the :ref:`Advanced OGR PostgreSQL driver Information ` page. +Starting with GDAL 3.9, only PostgreSQL >= 9 and PostGIS >= 2 are supported. + Driver capabilities ------------------- @@ -81,7 +83,7 @@ and named views will be treated as layers. The driver also supports the `geography `__ -column type introduced in PostGIS 1.5. +column type. The driver also supports reading and writing the following non-linear geometry types :CIRCULARSTRING, COMPOUNDCURVE, @@ -228,7 +230,7 @@ Layer Creation Options :choices: geometry, geography, BYTEA, OID The GEOM_TYPE layer creation option can be set to one - of "geometry", "geography" (PostGIS >= 1.5), "BYTEA" or "OID" to + of "geometry", "geography", "BYTEA" or "OID" to force the type of geometry used for a table. For a PostGIS database, "geometry" is the default value. PostGIS "geography" assumes a geographic SRS (before PostGIS 2.2, it was even required to be EPSG:4326), but the @@ -266,7 +268,7 @@ Layer Creation Options :choices: 2, 3, XYM, XYZM Control the dimension of the layer. Important - to set to 2 for 2D layers with PostGIS 1.0+ as it has constraints on + to set to 2 for 2D layers as it has constraints on the geometry dimension during loading. - .. lco:: GEOMETRY_NAME diff --git a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp index f656815623b4..c05c83321f01 100644 --- a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp @@ -260,16 +260,6 @@ static void OGRPGTableEntryAddGeomColumn( psTableEntry->nGeomColumnCount++; } -static void -OGRPGTableEntryAddGeomColumn(PGTableEntry *psTableEntry, - const PGGeomColumnDesc *psGeomColumnDesc) -{ - OGRPGTableEntryAddGeomColumn( - psTableEntry, psGeomColumnDesc->pszName, psGeomColumnDesc->pszGeomType, - psGeomColumnDesc->GeometryTypeFlags, psGeomColumnDesc->nSRID, - psGeomColumnDesc->ePostgisType, psGeomColumnDesc->bNullable); -} - static void OGRPGFreeTableEntry(void *_psTableEntry) { PGTableEntry *psTableEntry = static_cast(_psTableEntry); @@ -781,17 +771,6 @@ int OGRPGDataSource::Open(const char *pszNewName, int bUpdate, int bTestOpen, isdigit(static_cast(pszSpace[1]))) { OGRPGDecodeVersionString(&sPostgreSQLVersion, pszSpace + 1); -#if defined(BINARY_CURSOR_ENABLED) - if (sPostgreSQLVersion.nMajor == 7 && sPostgreSQLVersion.nMinor < 4) - { - /* We don't support BINARY CURSOR for PostgreSQL < 7.4. */ - /* The binary protocol for arrays seems to be different from - * later versions */ - CPLDebug("PG", "BINARY cursor will finally NOT be used because " - "version < 7.4"); - bUseBinaryCursor = FALSE; - } -#endif } } OGRPGClearResult(hResult); @@ -870,19 +849,6 @@ int OGRPGDataSource::Open(const char *pszNewName, int bUpdate, int bTestOpen, } #endif -#ifdef notdef - /* This would be the quickest fix... instead, ogrpglayer has been updated to - * support */ - /* bytea hex format */ - if (sPostgreSQLVersion.nMajor >= 9) - { - // Starting with PostgreSQL 9.0, the default output format for values - // of type bytea is hex, whereas we traditionally expect escape. - hResult = OGRPG_PQexec(hPGConn, "SET bytea_output TO escape"); - OGRPGClearResult(hResult); - } -#endif - /* -------------------------------------------------------------------- */ /* Test to see if this database instance has support for the */ /* PostGIS Geometry type. If so, disable sequential scanning */ @@ -1260,7 +1226,7 @@ void OGRPGDataSource::LoadTables() if (bHavePostGIS && !bListAllTables) { osCommand.Printf( - "SELECT c.relname, n.nspname, c.relkind, g.f_geometry_column, " + "SELECT c.relname, n.nspname, g.f_geometry_column, " "g.type, g.coord_dimension, g.srid, %d, a.attnotnull, " "d.description, c.oid as oid, a.attnum as attnum " "FROM pg_class c " @@ -1278,7 +1244,7 @@ void OGRPGDataSource::LoadTables() if (bHaveGeography) osCommand += CPLString().Printf( - "UNION SELECT c.relname, n.nspname, c.relkind, " + "UNION SELECT c.relname, n.nspname, " "g.f_geography_column, " "g.type, g.coord_dimension, g.srid, %d, a.attnotnull, " "d.description, c.oid as oid, a.attnum as attnum " @@ -1298,7 +1264,7 @@ void OGRPGDataSource::LoadTables() osCommand += " ORDER BY oid, attnum"; } else - osCommand.Printf("SELECT c.relname, n.nspname, c.relkind FROM " + osCommand.Printf("SELECT c.relname, n.nspname FROM " "pg_class c, pg_namespace n " "WHERE (c.relkind in (%s) AND c.relname !~ '^pg_' " "AND c.relnamespace=n.oid)", @@ -1324,7 +1290,6 @@ void OGRPGDataSource::LoadTables() { const char *pszTable = PQgetvalue(hResult, iRecord, 0); const char *pszSchemaName = PQgetvalue(hResult, iRecord, 1); - const char *pszRelkind = PQgetvalue(hResult, iRecord, 2); const char *pszGeomColumnName = nullptr; const char *pszGeomType = nullptr; const char *pszDescription = nullptr; @@ -1335,23 +1300,15 @@ void OGRPGDataSource::LoadTables() PostgisType ePostgisType = GEOM_TYPE_UNKNOWN; if (bHavePostGIS && !bListAllTables) { - pszGeomColumnName = PQgetvalue(hResult, iRecord, 3); - pszGeomType = PQgetvalue(hResult, iRecord, 4); + pszGeomColumnName = PQgetvalue(hResult, iRecord, 2); + pszGeomType = PQgetvalue(hResult, iRecord, 3); bHasM = pszGeomType[strlen(pszGeomType) - 1] == 'M'; - nGeomCoordDimension = atoi(PQgetvalue(hResult, iRecord, 5)); - nSRID = atoi(PQgetvalue(hResult, iRecord, 6)); + nGeomCoordDimension = atoi(PQgetvalue(hResult, iRecord, 4)); + nSRID = atoi(PQgetvalue(hResult, iRecord, 5)); ePostgisType = static_cast( - atoi(PQgetvalue(hResult, iRecord, 7))); - bNullable = EQUAL(PQgetvalue(hResult, iRecord, 8), "f"); - pszDescription = PQgetvalue(hResult, iRecord, 9); - - /* We cannot reliably find geometry columns of a view that is */ - /* based on a table that inherits from another one, wit that */ - /* method, so give up, and let - * OGRPGTableLayer::ReadTableDefinition() */ - /* do the job */ - if (pszRelkind[0] == 'v' && sPostGISVersion.nMajor < 2) - pszGeomColumnName = nullptr; + atoi(PQgetvalue(hResult, iRecord, 6))); + bNullable = EQUAL(PQgetvalue(hResult, iRecord, 7), "f"); + pszDescription = PQgetvalue(hResult, iRecord, 8); } if (EQUAL(pszTable, "spatial_ref_sys") || @@ -1407,123 +1364,6 @@ void OGRPGDataSource::LoadTables() /* -------------------------------------------------------------------- */ OGRPGClearResult(hResult); - - /* With PostGIS 2.0, we don't need to query base tables of inherited */ - /* tables */ - if (bHavePostGIS && !bListAllTables && sPostGISVersion.nMajor < 2) - { - /* ------------------------------------------------------------------ - */ - /* Fetch inherited tables */ - /* ------------------------------------------------------------------ - */ - hResult = OGRPG_PQexec( - hPGConn, - "SELECT c1.relname AS derived, c2.relname AS parent, n.nspname " - "FROM pg_class c1, pg_class c2, pg_namespace n, pg_inherits i " - "WHERE i.inhparent = c2.oid AND i.inhrelid = c1.oid AND " - "c1.relnamespace=n.oid " - "AND c1.relkind in ('r', 'v') AND c1.relnamespace=n.oid AND " - "c2.relkind in ('r','v') " - "AND c2.relname !~ '^pg_' AND c2.relnamespace=n.oid"); - - if (!hResult || PQresultStatus(hResult) != PGRES_TUPLES_OK) - { - OGRPGClearResult(hResult); - - CPLError(CE_Failure, CPLE_AppDefined, "%s", - PQerrorMessage(hPGConn)); - goto end; - } - - /* ------------------------------------------------------------------ - */ - /* Parse the returned table list */ - /* ------------------------------------------------------------------ - */ - bool bHasDoneSomething = false; - do - { - /* Iterate over the tuples while we have managed to resolved at - * least one */ - /* table to its table parent with a geometry */ - /* For example if we have C inherits B and B inherits A, where A - * is a base table with a geometry */ - /* The first pass will add B to the set of tables */ - /* The second pass will add C to the set of tables */ - - bHasDoneSomething = false; - - for (int iRecord = 0; iRecord < PQntuples(hResult); iRecord++) - { - const char *pszTable = PQgetvalue(hResult, iRecord, 0); - const char *pszParentTable = - PQgetvalue(hResult, iRecord, 1); - const char *pszSchemaName = PQgetvalue(hResult, iRecord, 2); - - PGTableEntry *psEntry = OGRPGFindTableEntry( - hSetTables, pszTable, pszSchemaName); - /* We must be careful that a derived table can have its own - * geometry column(s) */ - /* and some inherited from another table */ - if (psEntry == nullptr || !psEntry->bDerivedInfoAdded) - { - PGTableEntry *psParentEntry = OGRPGFindTableEntry( - hSetTables, pszParentTable, pszSchemaName); - if (psParentEntry != nullptr) - { - /* The parent table of this table is already in the - * set, so we */ - /* can now add the table in the set if it was not in - * it already */ - - bHasDoneSomething = true; - - if (psEntry == nullptr) - psEntry = - OGRPGAddTableEntry(hSetTables, pszTable, - pszSchemaName, nullptr); - - for (int iGeomColumn = 0; - iGeomColumn < psParentEntry->nGeomColumnCount; - iGeomColumn++) - { - papsTables = - static_cast(CPLRealloc( - papsTables, sizeof(PGTableEntry *) * - (nTableCount + 1))); - papsTables[nTableCount] = - static_cast( - CPLCalloc(1, sizeof(PGTableEntry))); - papsTables[nTableCount]->pszTableName = - CPLStrdup(pszTable); - papsTables[nTableCount]->pszSchemaName = - CPLStrdup(pszSchemaName); - OGRPGTableEntryAddGeomColumn( - papsTables[nTableCount], - &psParentEntry - ->pasGeomColumns[iGeomColumn]); - nTableCount++; - - OGRPGTableEntryAddGeomColumn( - psEntry, - &psParentEntry - ->pasGeomColumns[iGeomColumn]); - } - - psEntry->bDerivedInfoAdded = true; - } - } - } - } while (bHasDoneSomething); - - /* -------------------------------------------------------------------- - */ - /* Cleanup */ - /* -------------------------------------------------------------------- - */ - OGRPGClearResult(hResult); - } } /* -------------------------------------------------------------------- */ @@ -1666,19 +1506,6 @@ OGRErr OGRPGDataSource::DeleteLayer(int iLayer) SoftStartTransaction(); - if (bHavePostGIS && sPostGISVersion.nMajor < 2) - { - // This is unnecessary if the layer is not a geometry table, - // or an inherited geometry table but it should not hurt. - osCommand.Printf( - "DELETE FROM geometry_columns WHERE f_table_name='%s' and " - "f_table_schema='%s'", - osTableName.c_str(), osSchemaName.c_str()); - - PGresult *hResult = OGRPG_PQexec(hPGConn, osCommand.c_str()); - OGRPGClearResult(hResult); - } - osCommand.Printf("DROP TABLE %s.%s CASCADE", OGRPGEscapeColumnName(osSchemaName).c_str(), OGRPGEscapeColumnName(osTableName).c_str()); diff --git a/ogr/ogrsf_frmts/pg/ogrpglayer.cpp b/ogr/ogrsf_frmts/pg/ogrpglayer.cpp index bdbc936a5fd9..611925129d8c 100644 --- a/ogr/ogrsf_frmts/pg/ogrpglayer.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpglayer.cpp @@ -649,17 +649,14 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, STARTS_WITH(pszVal, "\\x00") || STARTS_WITH(pszVal, "\\x01"))) { - poGeom = BYTEAToGeometry( - pszVal, (poDS->sPostGISVersion.nMajor < 2)); + poGeom = BYTEAToGeometry(pszVal, false); } else { const GByte *pabyVal = reinterpret_cast(pszVal); OGRGeometryFactory::createFromWkb( - pabyVal, nullptr, &poGeom, nLength, - (poDS->sPostGISVersion.nMajor < 2) ? wkbVariantPostGIS1 - : wkbVariantOldOgc); + pabyVal, nullptr, &poGeom, nLength, wkbVariantOldOgc); } if (poGeom != nullptr) @@ -686,8 +683,7 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, // Potentially dangerous to modify the result of PQgetvalue... nLength = CPLBase64DecodeInPlace(const_cast(pabyData)); OGRGeometry *poGeom = OGRGeometryFromEWKB( - const_cast(pabyData), nLength, nullptr, - poDS->sPostGISVersion.nMajor < 2); + const_cast(pabyData), nLength, nullptr, false); if (poGeom != nullptr) { @@ -721,15 +717,13 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, { GByte *pabyEWKB = BYTEAToGByteArray(pabyData, &nLength); poGeom = - OGRGeometryFromEWKB(pabyEWKB, nLength, nullptr, - poDS->sPostGISVersion.nMajor < 2); + OGRGeometryFromEWKB(pabyEWKB, nLength, nullptr, false); CPLFree(pabyEWKB); } else if (nLength >= 2 && (STARTS_WITH_CI(pabyData, "00") || STARTS_WITH_CI(pabyData, "01"))) { - poGeom = OGRGeometryFromHexEWKB( - pabyData, nullptr, poDS->sPostGISVersion.nMajor < 2); + poGeom = OGRGeometryFromHexEWKB(pabyData, nullptr, false); } else { @@ -738,7 +732,7 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, poGeom = OGRGeometryFromEWKB( const_cast( reinterpret_cast(pabyData)), - nLength, nullptr, poDS->sPostGISVersion.nMajor < 2); + nLength, nullptr, false); } if (poGeom != nullptr) @@ -773,8 +767,7 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, if (STARTS_WITH_CI(pszPostSRID, "00") || STARTS_WITH_CI(pszPostSRID, "01")) { - poGeometry = OGRGeometryFromHexEWKB( - pszWKT, nullptr, poDS->sPostGISVersion.nMajor < 2); + poGeometry = OGRGeometryFromHexEWKB(pszWKT, nullptr, false); } else OGRGeometryFactory::createFromWkt(pszPostSRID, nullptr, @@ -814,14 +807,12 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, const GByte *pabyData = reinterpret_cast(pszData); poGeometry = - OGRGeometryFromEWKB(pabyData, nLength, NULL, - poDS->sPostGISVersion.nMajor < 2); + OGRGeometryFromEWKB(pabyData, nLength, NULL, false); } if (poGeometry == nullptr) #endif { - poGeometry = BYTEAToGeometry( - pszData, (poDS->sPostGISVersion.nMajor < 2)); + poGeometry = BYTEAToGeometry(pszData, false); } } @@ -1813,9 +1804,7 @@ OGRGeometry *OGRPGLayer::OIDToGeometry(Oid oid) OGRGeometry *poGeometry = nullptr; OGRGeometryFactory::createFromWkb(pabyWKB, nullptr, &poGeometry, nBytes, - poDS->sPostGISVersion.nMajor < 2 - ? wkbVariantPostGIS1 - : wkbVariantOldOgc); + wkbVariantOldOgc); CPLFree(pabyWKB); @@ -1840,10 +1829,8 @@ Oid OGRPGLayer::GeometryToOID(OGRGeometry *poGeometry) GByte *pabyWKB = static_cast(VSI_MALLOC_VERBOSE(nWkbSize)); if (pabyWKB == nullptr) return 0; - if (poGeometry->exportToWkb(wkbNDR, pabyWKB, - (poDS->sPostGISVersion.nMajor < 2) - ? wkbVariantPostGIS1 - : wkbVariantOldOgc) != OGRERR_NONE) + if (poGeometry->exportToWkb(wkbNDR, pabyWKB, wkbVariantOldOgc) != + OGRERR_NONE) return 0; Oid oid = lo_creat(hPGConn, INV_READ | INV_WRITE); @@ -1937,14 +1924,11 @@ OGRErr OGRPGLayer::GetExtent(int iGeomField, OGREnvelope *psExtent, int bForce) OGRPGGeomFieldDefn *poGeomFieldDefn = poFeatureDefn->GetGeomFieldDefn(iGeomField); - const char *pszExtentFct = - poDS->sPostGISVersion.nMajor >= 2 ? "ST_Extent" : "Extent"; - if (TestCapability(OLCFastGetExtent)) { /* Do not take the spatial filter into account */ osCommand.Printf( - "SELECT %s(%s) FROM %s AS ogrpgextent", pszExtentFct, + "SELECT ST_Extent(%s) FROM %s AS ogrpgextent", OGRPGEscapeColumnName(poGeomFieldDefn->GetNameRef()).c_str(), GetFromClauseForGetExtent().c_str()); } @@ -1953,8 +1937,8 @@ OGRErr OGRPGLayer::GetExtent(int iGeomField, OGREnvelope *psExtent, int bForce) /* Probably not very efficient, but more efficient than client-side * implementation */ osCommand.Printf( - "SELECT %s(ST_GeomFromWKB(ST_AsBinary(%s))) FROM %s AS ogrpgextent", - pszExtentFct, + "SELECT ST_Extent(ST_GeomFromWKB(ST_AsBinary(%s))) FROM %s AS " + "ogrpgextent", OGRPGEscapeColumnName(poGeomFieldDefn->GetNameRef()).c_str(), GetFromClauseForGetExtent().c_str()); } @@ -2001,14 +1985,11 @@ OGRErr OGRPGLayer::GetExtent3D(int iGeomField, OGREnvelope3D *psExtent3D, OGRPGGeomFieldDefn *poGeomFieldDefn = poFeatureDefn->GetGeomFieldDefn(iGeomField); - const char *pszExtentFct = - poDS->sPostGISVersion.nMajor >= 2 ? "ST_3DExtent" : "ST_Extent3D"; - if (TestCapability(OLCFastGetExtent3D)) { /* Do not take the spatial filter into account */ osCommand.Printf( - "SELECT %s(%s) FROM %s AS ogrpgextent", pszExtentFct, + "SELECT ST_Extent(%s) FROM %s AS ogrpgextent", OGRPGEscapeColumnName(poGeomFieldDefn->GetNameRef()).c_str(), GetFromClauseForGetExtent().c_str()); } @@ -2017,8 +1998,8 @@ OGRErr OGRPGLayer::GetExtent3D(int iGeomField, OGREnvelope3D *psExtent3D, /* Probably not very efficient, but more efficient than client-side * implementation */ osCommand.Printf( - "SELECT %s(ST_GeomFromWKB(ST_AsBinary(%s))) FROM %s AS ogrpgextent", - pszExtentFct, + "SELECT ST_Extent(ST_GeomFromWKB(ST_AsBinary(%s))) FROM %s AS " + "ogrpgextent", OGRPGEscapeColumnName(poGeomFieldDefn->GetNameRef()).c_str(), GetFromClauseForGetExtent().c_str()); } @@ -2072,14 +2053,14 @@ OGRErr OGRPGLayer::RunGetExtentRequest(OGREnvelope &sExtent, strncpy(szVals, ptr, ptrEndParenthesis - ptr); szVals[ptrEndParenthesis - ptr] = '\0'; - char **papszTokens = CSLTokenizeString2(szVals, " ,", CSLT_HONOURSTRINGS); - int nTokenCnt = poDS->sPostGISVersion.nMajor >= 1 ? 4 : 6; + const CPLStringList aosTokens( + CSLTokenizeString2(szVals, " ,", CSLT_HONOURSTRINGS)); + constexpr int nTokenCnt = 4; - if (CSLCount(papszTokens) != nTokenCnt) + if (aosTokens.size() != nTokenCnt) { CPLError(CE_Failure, CPLE_IllegalArg, "Bad extent representation: '%s'", pszBox); - CSLDestroy(papszTokens); OGRPGClearResult(hResult); return OGRERR_FAILURE; @@ -2091,12 +2072,11 @@ OGRErr OGRPGLayer::RunGetExtentRequest(OGREnvelope &sExtent, // => X2 index calculated as nTokenCnt/2 // Y2 index calculated as nTokenCnt/2+1 - sExtent.MinX = CPLAtof(papszTokens[0]); - sExtent.MinY = CPLAtof(papszTokens[1]); - sExtent.MaxX = CPLAtof(papszTokens[nTokenCnt / 2]); - sExtent.MaxY = CPLAtof(papszTokens[nTokenCnt / 2 + 1]); + sExtent.MinX = CPLAtof(aosTokens[0]); + sExtent.MinY = CPLAtof(aosTokens[1]); + sExtent.MaxX = CPLAtof(aosTokens[nTokenCnt / 2]); + sExtent.MaxY = CPLAtof(aosTokens[nTokenCnt / 2 + 1]); - CSLDestroy(papszTokens); OGRPGClearResult(hResult); return OGRERR_NONE; diff --git a/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp b/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp index 1795238d8245..b3a4688746c9 100644 --- a/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp @@ -670,36 +670,18 @@ int OGRPGTableLayer::ReadTableDefinition() /* Identify the integer primary key. */ /* -------------------------------------------------------------------- */ - const char *pszTypnameEqualsAnyClause = - poDS->sPostgreSQLVersion.nMajor == 7 && - poDS->sPostgreSQLVersion.nMinor <= 3 - ? "ANY(SELECT '{int2, int4, int8, serial, bigserial}')" - : "ANY(ARRAY['int2','int4','int8','serial','bigserial'])"; - - const char *pszAttnumEqualAnyIndkey = - poDS->sPostgreSQLVersion.nMajor > 8 || - (poDS->sPostgreSQLVersion.nMajor == 8 && - poDS->sPostgreSQLVersion.nMinor >= 2) - ? "a.attnum = ANY(i.indkey)" - : "(i.indkey[0]=a.attnum OR i.indkey[1]=a.attnum OR " - "i.indkey[2]=a.attnum " - "OR i.indkey[3]=a.attnum OR i.indkey[4]=a.attnum OR " - "i.indkey[5]=a.attnum " - "OR i.indkey[6]=a.attnum OR i.indkey[7]=a.attnum OR " - "i.indkey[8]=a.attnum " - "OR i.indkey[9]=a.attnum)"; - - /* See #1889 for why we don't use 'AND a.attnum = ANY(i.indkey)' */ osCommand.Printf( - "SELECT a.attname, a.attnum, t.typname, t.typname = %s AS isfid " + "SELECT a.attname, a.attnum, t.typname, " + "t.typname = ANY(ARRAY['int2','int4','int8','serial','bigserial']) AS " + "isfid " "FROM pg_attribute a " "JOIN pg_type t ON t.oid = a.atttypid " "JOIN pg_index i ON i.indrelid = a.attrelid " "WHERE a.attnum > 0 AND a.attrelid = %u " "AND i.indisprimary = 't' " "AND t.typname !~ '^geom' " - "AND %s ORDER BY a.attnum", - pszTypnameEqualsAnyClause, nTableOID, pszAttnumEqualAnyIndkey); + "AND a.attnum = ANY(i.indkey) ORDER BY a.attnum", + nTableOID); PGresult *hResult = OGRPG_PQexec(hPGConn, osCommand.c_str()); @@ -1124,9 +1106,8 @@ void OGRPGTableLayer::BuildWhere() CPLsnprintf(szBox3D_2, sizeof(szBox3D_2), "%.18g %.18g", sEnvelope.MaxX, sEnvelope.MaxY); osWHERE.Printf( - "WHERE %s && %s('BOX3D(%s, %s)'::box3d,%d) ", + "WHERE %s && ST_SetSRID('BOX3D(%s, %s)'::box3d,%d) ", OGRPGEscapeColumnName(poGeomFieldDefn->GetNameRef()).c_str(), - (poDS->sPostGISVersion.nMajor >= 2) ? "ST_SetSRID" : "SetSRID", szBox3D_1, szBox3D_2, poGeomFieldDefn->nSRSId); } @@ -1275,32 +1256,17 @@ CPLString OGRPGTableLayer::BuildFields() } else if (CPLTestBool(CPLGetConfigOption("PG_USE_BASE64", "NO"))) { - if (poDS->sPostGISVersion.nMajor >= 2) - osFieldList += "encode(ST_AsEWKB("; - else - osFieldList += "encode(AsEWKB("; + osFieldList += "encode(ST_AsEWKB("; osFieldList += osEscapedGeom; osFieldList += "), 'base64') AS "; osFieldList += OGRPGEscapeColumnName( CPLSPrintf("EWKBBase64_%s", poGeomFieldDefn->GetNameRef())); } - else if (poDS->sPostGISVersion.nMajor > 1 || - (poDS->sPostGISVersion.nMajor == 1 && - poDS->sPostGISVersion.nMinor >= 1)) - /* perhaps works also for older version, but I didn't check - */ + else if (poDS->sPostGISVersion.nMajor > 0) { /* This will return EWKB in an hex encoded form */ osFieldList += osEscapedGeom; } - else if (poDS->sPostGISVersion.nMajor >= 1) - { - osFieldList += "AsEWKT("; - osFieldList += osEscapedGeom; - osFieldList += ") AS "; - osFieldList += OGRPGEscapeColumnName( - CPLSPrintf("AsEWKT_%s", poGeomFieldDefn->GetNameRef())); - } else { osFieldList += "AsText("; @@ -2123,14 +2089,10 @@ OGRErr OGRPGTableLayer::CreateFeatureViaInsert(OGRFeature *poFeature) osCommand.Printf("INSERT INTO %s DEFAULT VALUES", pszSqlTableName); int bReturnRequested = FALSE; - /* RETURNING is only available since Postgres 8.2 */ /* We only get the FID, but we also could add the unset fields to get */ /* the default values */ if (bRetrieveFID && pszFIDColumn != nullptr && - poFeature->GetFID() == OGRNullFID && - (poDS->sPostgreSQLVersion.nMajor >= 9 || - (poDS->sPostgreSQLVersion.nMajor == 8 && - poDS->sPostgreSQLVersion.nMinor >= 2))) + poFeature->GetFID() == OGRNullFID) { bReturnRequested = TRUE; osCommand += " RETURNING "; @@ -3457,13 +3419,8 @@ void OGRPGTableLayer::ResolveSRID(const OGRPGGeomFieldDefn *poGFldDefn) if (nSRSId <= 0 && poGFldDefn->ePostgisType == GEOM_TYPE_GEOMETRY && poDS->sPostGISVersion.nMajor >= 0) { - const char *psGetSRIDFct = - poDS->sPostGISVersion.nMajor >= 2 ? "ST_SRID" : "getsrid"; - CPLString osGetSRID; - osGetSRID += "SELECT "; - osGetSRID += psGetSRIDFct; - osGetSRID += "("; + osGetSRID += "SELECT ST_SRID("; osGetSRID += OGRPGEscapeColumnName(poGFldDefn->GetNameRef()); osGetSRID += ") FROM "; osGetSRID += pszSqlTableName; @@ -3865,7 +3822,7 @@ OGRErr OGRPGTableLayer::RunDeferredCreationIfNecessary() { OGRPGGeomFieldDefn *poGeomField = poFeatureDefn->GetGeomFieldDefn(i); - if (poDS->sPostGISVersion.nMajor >= 2 || + if (poDS->sPostGISVersion.nMajor > 0 || poGeomField->ePostgisType == GEOM_TYPE_GEOGRAPHY) { const char *pszGeometryType = @@ -3919,21 +3876,6 @@ OGRErr OGRPGTableLayer::RunDeferredCreationIfNecessary() } m_aosDeferredCommentOnColumns.clear(); - // For PostGIS 1.X, use AddGeometryColumn() to create geometry columns - if (poDS->sPostGISVersion.nMajor < 2) - { - for (int i = 0; i < poFeatureDefn->GetGeomFieldCount(); i++) - { - OGRPGGeomFieldDefn *poGeomField = - poFeatureDefn->GetGeomFieldDefn(i); - if (poGeomField->ePostgisType == GEOM_TYPE_GEOMETRY && - RunAddGeometryColumn(poGeomField) != OGRERR_NONE) - { - return OGRERR_FAILURE; - } - } - } - if (bCreateSpatialIndexFlag) { for (int i = 0; i < poFeatureDefn->GetGeomFieldCount(); i++) From 3b8d4a4e6af9e96a1a2d3a4443ecd32a55e92aac Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 21:40:07 +0100 Subject: [PATCH 054/132] CPLCreateOrAcquireMutexEx(): fix TSAN/valgrind --tool=helgrind warning about lock-order inversion (fixes #1108) --- port/cpl_multiproc.cpp | 52 ++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/port/cpl_multiproc.cpp b/port/cpl_multiproc.cpp index 2658ac1aa5de..65384486c29b 100644 --- a/port/cpl_multiproc.cpp +++ b/port/cpl_multiproc.cpp @@ -358,16 +358,16 @@ int CPLCreateOrAcquireMutexEx(CPLMutex **phMutex, double dfWaitInSeconds, /************************************************************************/ #ifdef MUTEX_NONE -static int CPLCreateOrAcquireMutexInternal(CPLLock **phLock, - double dfWaitInSeconds, - CPLLockType eType) +static bool CPLCreateOrAcquireMutexInternal(CPLLock **phLock, + double dfWaitInSeconds, + CPLLockType eType) { return false; } #else -static int CPLCreateOrAcquireMutexInternal(CPLLock **phLock, - double dfWaitInSeconds, - CPLLockType eType) +static bool CPLCreateOrAcquireMutexInternal(CPLLock **phLock, + double dfWaitInSeconds, + CPLLockType eType) { bool bSuccess = false; @@ -1462,35 +1462,31 @@ int CPLCreateOrAcquireMutexEx(CPLMutex **phMutex, double dfWaitInSeconds, int nOptions) { - bool bSuccess = false; - pthread_mutex_lock(&global_mutex); if (*phMutex == nullptr) { *phMutex = CPLCreateMutexInternal(true, nOptions); - bSuccess = *phMutex != nullptr; + const bool bSuccess = *phMutex != nullptr; pthread_mutex_unlock(&global_mutex); + if (!bSuccess) + return false; } else { pthread_mutex_unlock(&global_mutex); - - bSuccess = CPL_TO_BOOL(CPLAcquireMutex(*phMutex, dfWaitInSeconds)); } - return bSuccess; + return CPL_TO_BOOL(CPLAcquireMutex(*phMutex, dfWaitInSeconds)); } /************************************************************************/ /* CPLCreateOrAcquireMutexInternal() */ /************************************************************************/ -static int CPLCreateOrAcquireMutexInternal(CPLLock **phLock, - double dfWaitInSeconds, - CPLLockType eType) +static bool CPLCreateOrAcquireMutexInternal(CPLLock **phLock, + double dfWaitInSeconds, + CPLLockType eType) { - bool bSuccess = false; - pthread_mutex_lock(&global_mutex); if (*phLock == nullptr) { @@ -1507,18 +1503,17 @@ static int CPLCreateOrAcquireMutexInternal(CPLLock **phLock, *phLock = nullptr; } } - bSuccess = *phLock != nullptr; + const bool bSuccess = *phLock != nullptr; pthread_mutex_unlock(&global_mutex); + if (!bSuccess) + return false; } else { pthread_mutex_unlock(&global_mutex); - - bSuccess = - CPL_TO_BOOL(CPLAcquireMutex((*phLock)->u.hMutex, dfWaitInSeconds)); } - return bSuccess; + return CPL_TO_BOOL(CPLAcquireMutex((*phLock)->u.hMutex, dfWaitInSeconds)); } /************************************************************************/ @@ -1624,20 +1619,23 @@ static CPLMutex *CPLCreateMutexInternal(bool bAlreadyInGlobalLock, int nOptions) psItem->nOptions = nOptions; CPLInitMutex(psItem); - // Mutexes are implicitly acquired when created. - CPLAcquireMutex(reinterpret_cast(psItem), 0.0); - return reinterpret_cast(psItem); } CPLMutex *CPLCreateMutex() { - return CPLCreateMutexInternal(false, CPL_MUTEX_RECURSIVE); + CPLMutex *mutex = CPLCreateMutexInternal(false, CPL_MUTEX_RECURSIVE); + if (mutex) + CPLAcquireMutex(mutex, 0); + return mutex; } CPLMutex *CPLCreateMutexEx(int nOptions) { - return CPLCreateMutexInternal(false, nOptions); + CPLMutex *mutex = CPLCreateMutexInternal(false, nOptions); + if (mutex) + CPLAcquireMutex(mutex, 0); + return mutex; } /************************************************************************/ From a1ba66ffbb3d23a9c7a8f416a774fad6c38755ff Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 22:13:16 +0100 Subject: [PATCH 055/132] GDALGetCacheMax64(): fix TSAN complaint (fixes #1837) --- gcore/gdalrasterblock.cpp | 116 +++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/gcore/gdalrasterblock.cpp b/gcore/gdalrasterblock.cpp index 48c19b0aec5a..93eea22399ee 100644 --- a/gcore/gdalrasterblock.cpp +++ b/gcore/gdalrasterblock.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include "cpl_atomic_ops.h" #include "cpl_conv.h" @@ -43,7 +44,6 @@ #include "cpl_string.h" #include "cpl_vsi.h" -static bool bCacheMaxInitialized = false; // Will later be overridden by the default 5% if GDAL_CACHEMAX not defined. static GIntBig nCacheMax = 40 * 1024 * 1024; static GIntBig nCacheUsed = 0; @@ -153,10 +153,8 @@ void CPL_STDCALL GDALSetCacheMax64(GIntBig nNewSizeInBytes) } #endif - { - INITIALIZE_LOCK; - } - bCacheMaxInitialized = true; + // To force one-time initialization of nCacheMax if not already done + GDALGetCacheMax64(); nCacheMax = nNewSizeInBytes; /* -------------------------------------------------------------------- */ @@ -237,73 +235,77 @@ int CPL_STDCALL GDALGetCacheMax() GIntBig CPL_STDCALL GDALGetCacheMax64() { - if (!bCacheMaxInitialized) - { + static std::once_flag flagSetupGDALGetCacheMax64; + std::call_once( + flagSetupGDALGetCacheMax64, + []() { - INITIALIZE_LOCK; - } - bSleepsForBockCacheDebug = - CPLTestBool(CPLGetConfigOption("GDAL_DEBUG_BLOCK_CACHE", "NO")); + { + INITIALIZE_LOCK; + } + bSleepsForBockCacheDebug = + CPLTestBool(CPLGetConfigOption("GDAL_DEBUG_BLOCK_CACHE", "NO")); - const char *pszCacheMax = CPLGetConfigOption("GDAL_CACHEMAX", "5%"); + const char *pszCacheMax = CPLGetConfigOption("GDAL_CACHEMAX", "5%"); - GIntBig nNewCacheMax; - if (strchr(pszCacheMax, '%') != nullptr) - { - GIntBig nUsablePhysicalRAM = CPLGetUsablePhysicalRAM(); - if (nUsablePhysicalRAM > 0) + GIntBig nNewCacheMax; + if (strchr(pszCacheMax, '%') != nullptr) { - // For some reason, coverity pretends that this will overflow. - // "Multiply operation overflows on operands - // static_cast( nUsablePhysicalRAM ) and - // CPLAtof(pszCacheMax). Example values for operands: CPLAtof( - // pszCacheMax ) = 2251799813685248, - // static_cast(nUsablePhysicalRAM) = - // -9223372036854775808." coverity[overflow,tainted_data] - double dfCacheMax = static_cast(nUsablePhysicalRAM) * - CPLAtof(pszCacheMax) / 100.0; - if (dfCacheMax >= 0 && dfCacheMax < 1e15) - nNewCacheMax = static_cast(dfCacheMax); + GIntBig nUsablePhysicalRAM = CPLGetUsablePhysicalRAM(); + if (nUsablePhysicalRAM > 0) + { + // For some reason, coverity pretends that this will overflow. + // "Multiply operation overflows on operands + // static_cast( nUsablePhysicalRAM ) and + // CPLAtof(pszCacheMax). Example values for operands: CPLAtof( + // pszCacheMax ) = 2251799813685248, + // static_cast(nUsablePhysicalRAM) = + // -9223372036854775808." coverity[overflow,tainted_data] + double dfCacheMax = + static_cast(nUsablePhysicalRAM) * + CPLAtof(pszCacheMax) / 100.0; + if (dfCacheMax >= 0 && dfCacheMax < 1e15) + nNewCacheMax = static_cast(dfCacheMax); + else + nNewCacheMax = nCacheMax; + } else + { + CPLDebug("GDAL", "Cannot determine usable physical RAM."); nNewCacheMax = nCacheMax; + } } else { - CPLDebug("GDAL", "Cannot determine usable physical RAM."); - nNewCacheMax = nCacheMax; - } - } - else - { - nNewCacheMax = CPLAtoGIntBig(pszCacheMax); - if (nNewCacheMax < 100000) - { - if (nNewCacheMax < 0) + nNewCacheMax = CPLAtoGIntBig(pszCacheMax); + if (nNewCacheMax < 100000) { - CPLError(CE_Failure, CPLE_NotSupported, - "Invalid value for GDAL_CACHEMAX. " - "Using default value."); - GIntBig nUsablePhysicalRAM = CPLGetUsablePhysicalRAM(); - if (nUsablePhysicalRAM) - nNewCacheMax = nUsablePhysicalRAM / 20; + if (nNewCacheMax < 0) + { + CPLError(CE_Failure, CPLE_NotSupported, + "Invalid value for GDAL_CACHEMAX. " + "Using default value."); + GIntBig nUsablePhysicalRAM = CPLGetUsablePhysicalRAM(); + if (nUsablePhysicalRAM) + nNewCacheMax = nUsablePhysicalRAM / 20; + else + { + CPLDebug("GDAL", + "Cannot determine usable physical RAM."); + nNewCacheMax = nCacheMax; + } + } else { - CPLDebug("GDAL", - "Cannot determine usable physical RAM."); - nNewCacheMax = nCacheMax; + nNewCacheMax *= 1024 * 1024; } } - else - { - nNewCacheMax *= 1024 * 1024; - } } - } - nCacheMax = nNewCacheMax; - CPLDebug("GDAL", "GDAL_CACHEMAX = " CPL_FRMT_GIB " MB", - nCacheMax / (1024 * 1024)); - bCacheMaxInitialized = true; - } + nCacheMax = nNewCacheMax; + CPLDebug("GDAL", "GDAL_CACHEMAX = " CPL_FRMT_GIB " MB", + nCacheMax / (1024 * 1024)); + }); + // coverity[overflow_sink] return nCacheMax; } From 1510ffc5362fcf0f0bdd6a093ea4843f4766d22c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 23:30:02 +0100 Subject: [PATCH 056/132] gdaltindex: add -lco (fixes #3623) --- apps/gdaltindex_bin.cpp | 2 +- apps/gdaltindex_lib.cpp | 12 +++++++++--- autotest/utilities/test_gdaltindex_lib.py | 2 ++ doc/source/programs/gdaltindex.rst | 8 +++++++- swig/include/python/gdal_python.i | 12 ++++++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/gdaltindex_bin.cpp b/apps/gdaltindex_bin.cpp index dd1f56814fb3..25a5929bf5d2 100644 --- a/apps/gdaltindex_bin.cpp +++ b/apps/gdaltindex_bin.cpp @@ -51,7 +51,7 @@ static void Usage(bool bIsError, const char *pszErrorMsg) " [-skip_different_projection] [-t_srs ]\n" " [-src_srs_name field_name] [-src_srs_format " "{AUTO|WKT|EPSG|PROJ}]\n" - " [-lyr_name ]\n" + " [-lyr_name ] [-lco =]...\n" " [-gti_filename ]\n" " [-tr ] [-te " "]\n" diff --git a/apps/gdaltindex_lib.cpp b/apps/gdaltindex_lib.cpp index 59a6bbe124eb..79770cd2030b 100644 --- a/apps/gdaltindex_lib.cpp +++ b/apps/gdaltindex_lib.cpp @@ -75,6 +75,7 @@ struct GDALTileIndexOptions std::string osFormat{}; std::string osIndexLayerName{}; std::string osLocationField = "location"; + CPLStringList aosLCO{}; std::string osTargetSRS{}; bool bWriteAbsolutePath = false; bool bSkipDifferentProjection = false; @@ -504,9 +505,9 @@ GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount, oSRS = *poSrcSRS; } - poLayer = poTileIndexDS->CreateLayer(osLayerName.c_str(), - oSRS.IsEmpty() ? nullptr : &oSRS, - wkbPolygon, nullptr); + poLayer = poTileIndexDS->CreateLayer( + osLayerName.c_str(), oSRS.IsEmpty() ? nullptr : &oSRS, wkbPolygon, + psOptions->aosLCO.List()); if (!poLayer) return nullptr; @@ -1193,6 +1194,11 @@ GDALTileIndexOptionsNew(char **papszArgv, CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1); psOptions->osLocationField = papszArgv[++iArg]; } + else if (strcmp(papszArgv[iArg], "-lco") == 0) + { + CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1); + psOptions->aosLCO.AddString(papszArgv[++iArg]); + } else if (strcmp(papszArgv[iArg], "-t_srs") == 0) { CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1); diff --git a/autotest/utilities/test_gdaltindex_lib.py b/autotest/utilities/test_gdaltindex_lib.py index 5edd1027b92f..1ca76128ea5f 100644 --- a/autotest/utilities/test_gdaltindex_lib.py +++ b/autotest/utilities/test_gdaltindex_lib.py @@ -286,10 +286,12 @@ def test_gdaltindex_lib_gti_non_xml(tmp_path, four_tiles): colorInterpretation="gray", mask=True, metadataOptions={"foo": "bar"}, + layerCreationOptions=["FID=my_fid"], ) ds = ogr.Open(index_filename) lyr = ds.GetLayer(0) + assert lyr.GetFIDColumn() == "my_fid" assert lyr.GetMetadataItem("RESX") == "60" assert lyr.GetMetadataItem("RESY") == "60" assert lyr.GetMetadataItem("MINX") == "0" diff --git a/doc/source/programs/gdaltindex.rst b/doc/source/programs/gdaltindex.rst index f54cf6d28e73..15cd905f4188 100644 --- a/doc/source/programs/gdaltindex.rst +++ b/doc/source/programs/gdaltindex.rst @@ -21,7 +21,7 @@ Synopsis [-f ] [-tileindex ] [-write_absolute_path] [-skip_different_projection] [-t_srs ] [-src_srs_name ] [-src_srs_format {AUTO|WKT|EPSG|PROJ}] - [-lyr_name ] + [-lyr_name ] [-lco =]... [-gti_filename ] [-tr ] [-te ] [-ot ] [-bandcount ] [-nodata [,...]] @@ -130,6 +130,12 @@ tileindex, or as input for the :ref:`GTI ` driver. Layer name to create/append to in the output tile index file. +.. option:: -lco = + + .. versionadded:: 3.9 + + Layer creation option (format specific) + .. option:: The name of the output file to create/append to. The default dataset will diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index dd7cc86729f5..55c39a66711a 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -4202,6 +4202,7 @@ def TileIndexOptions(options=None, maxPixelSize=None, format=None, layerName=None, + layerCreationOptions=None, locationFieldName="location", outputSRS=None, writeAbsolutePath=None, @@ -4236,6 +4237,8 @@ def TileIndexOptions(options=None, output format ("ESRI Shapefile", "GPKG", etc...) layerName: output layer name + layerCreationOptions: + list or dict of layer creation options locationFieldName: Specifies the name of the field in the resulting vector dataset where the path of the input dataset will be stored. The default field name is "location". Can be set to None to disable creation of such field. outputSRS: @@ -4297,6 +4300,15 @@ def TileIndexOptions(options=None, new_options += ['-f', format] if layerName is not None: new_options += ['-lyr_name', layerName] + + if layerCreationOptions is not None: + if isinstance(layerCreationOptions, dict): + for k, v in layerCreationOptions.items(): + new_options += ['-lco', f'{k}={v}'] + else: + for opt in layerCreationOptions: + new_options += ['-lco', opt] + if locationFieldName is not None: new_options += ['-tileindex', locationFieldName] if outputSRS is not None: From 174a448e879d879c4844e41bb4c44d271b2f70c1 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 23:17:54 +0100 Subject: [PATCH 057/132] Win32 Stat(VSI_STAT_EXISTS_FLAG): use GetFileAttributesW() for faster performance (fixes #3139) --- port/cpl_vsil_win32.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/port/cpl_vsil_win32.cpp b/port/cpl_vsil_win32.cpp index 5625ae629194..12dd4e015b1a 100644 --- a/port/cpl_vsil_win32.cpp +++ b/port/cpl_vsil_win32.cpp @@ -838,14 +838,23 @@ int VSIWin32FilesystemHandler::Stat(const char *pszFilename, VSIStatBufL *pStatBuf, int nFlags) { - (void)nFlags; - #if defined(_MSC_VER) || __MSVCRT_VERSION__ >= 0x0601 if (CPLTestBool(CPLGetConfigOption("GDAL_FILENAME_IS_UTF8", "YES"))) { wchar_t *pwszFilename = CPLRecodeToWChar(pszFilename, CPL_ENC_UTF8, CPL_ENC_UCS2); + if (nFlags == VSI_STAT_EXISTS_FLAG) + { + memset(pStatBuf, 0, sizeof(VSIStatBufL)); + const int nResult = + (GetFileAttributesW(pwszFilename) == INVALID_FILE_ATTRIBUTES) + ? -1 + : 0; + CPLFree(pwszFilename); + return nResult; + } + int nResult = _wstat64(pwszFilename, pStatBuf); // If _wstat64() fails and the original name is not an extended one, @@ -887,6 +896,7 @@ int VSIWin32FilesystemHandler::Stat(const char *pszFilename, else #endif { + (void)nFlags; return (VSI_STAT64(pszFilename, pStatBuf)); } } From 2d1c9888ee92ec1de330e2f23c2aa0848b1c9657 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 10:11:57 +0100 Subject: [PATCH 058/132] Python bindings: gdal.Translate()/gdal.Warp()/etc.: make sure not to modify provided options[] array (fixes #9259) --- swig/include/python/gdal_python.i | 42 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index dd7cc86729f5..f38de0f718e4 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -2011,7 +2011,8 @@ def InfoOptions(options=None, format='text', deserialize=True, if '-json' in new_options: format = 'json' else: - new_options = options + import copy + new_options = copy.copy(options) if format == 'json': new_options += ['-json'] elif format != "text": @@ -2130,7 +2131,8 @@ def VectorInfoOptions(options=None, if '-json' in new_options: format = 'json' else: - new_options = options + import copy + new_options = copy.copy(options) if format == 'json': new_options += ['-json'] elif format != "text": @@ -2207,7 +2209,8 @@ def MultiDimInfoOptions(options=None, detailed=False, array=None, arrayoptions=N if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if detailed: new_options += ['-detailed'] if array: @@ -2367,7 +2370,8 @@ def TranslateOptions(options=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if outputType != gdalconst.GDT_Unknown: @@ -2633,7 +2637,8 @@ def WarpOptions(options=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if srcBands: for b in srcBands: new_options += ['-srcband', str(b)] @@ -2979,7 +2984,8 @@ def VectorTranslateOptions(options=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-f', format] if srcSRS is not None: @@ -3248,7 +3254,8 @@ def DEMProcessingOptions(options=None, colorFilename=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if creationOptions is not None: @@ -3380,7 +3387,8 @@ def NearblackOptions(options=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if creationOptions is not None: @@ -3525,7 +3533,8 @@ def GridOptions(options=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if outputType != gdalconst.GDT_Unknown: @@ -3692,7 +3701,8 @@ def RasterizeOptions(options=None, format=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if outputType != gdalconst.GDT_Unknown: @@ -3881,7 +3891,8 @@ def FootprintOptions(options=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if bands is not None: @@ -4106,7 +4117,8 @@ def BuildVRTOptions(options=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if resolution is not None: new_options += ['-resolution', str(resolution)] if outputBounds is not None: @@ -4278,7 +4290,8 @@ def TileIndexOptions(options=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if overwrite: new_options += ['-overwrite'] if recursive: @@ -4434,7 +4447,8 @@ def MultiDimTranslateOptions(options=None, format=None, creationOptions=None, if isinstance(options, str): new_options = ParseCommandLine(options) else: - new_options = options + import copy + new_options = copy.copy(options) if format is not None: new_options += ['-of', format] if creationOptions is not None: From d3970bb7d321443fdbd1e98b2293158aa725d2fd Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Mon, 19 Feb 2024 14:32:47 -0500 Subject: [PATCH 059/132] VRTDerivedRasterBand: Support Int8, (U)Int64 with Python pixel functions --- autotest/gdrivers/vrtderived.py | 45 ++++++++++++++++++++++++++++++ frmts/vrt/vrtderivedrasterband.cpp | 12 +++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/autotest/gdrivers/vrtderived.py b/autotest/gdrivers/vrtderived.py index e02b4c459f37..b5fd42965881 100755 --- a/autotest/gdrivers/vrtderived.py +++ b/autotest/gdrivers/vrtderived.py @@ -1023,6 +1023,51 @@ def identity(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, raster_ysize _validate(xml) +############################################################################### + + +@pytest.mark.parametrize("dtype", range(1, gdal.GDT_TypeCount)) +def test_vrt_derived_dtype(tmp_vsimem, dtype): + pytest.importorskip("numpy") + + input_fname = tmp_vsimem / "input.tif" + + nx = 1 + ny = 1 + + with gdal.GetDriverByName("GTiff").Create( + input_fname, nx, ny, 1, eType=gdal.GDT_Int8 + ) as input_ds: + input_ds.GetRasterBand(1).Fill(1) + gt = input_ds.GetGeoTransform() + + vrt_xml = f""" + + {', '.join([str(x) for x in gt])} + + Python + identity + + + + {input_fname} + 1 + + + + """ + + with gdal.config_option("GDAL_VRT_ENABLE_PYTHON", "YES"): + with gdal.Open(vrt_xml) as vrt_ds: + arr = vrt_ds.ReadAsArray() + if dtype not in {gdal.GDT_CInt16, gdal.GDT_CInt32}: + assert arr[0, 0] == 1 + assert vrt_ds.GetRasterBand(1).DataType == dtype + + ############################################################################### # Cleanup. diff --git a/frmts/vrt/vrtderivedrasterband.cpp b/frmts/vrt/vrtderivedrasterband.cpp index bfc3d3aca48a..b69360ef58ad 100644 --- a/frmts/vrt/vrtderivedrasterband.cpp +++ b/frmts/vrt/vrtderivedrasterband.cpp @@ -88,6 +88,9 @@ static PyObject *GDALCreateNumpyArray(PyObject *pCreateArray, void *pBuffer, case GDT_Byte: pszDataType = "uint8"; break; + case GDT_Int8: + pszDataType = "int8"; + break; case GDT_UInt16: pszDataType = "uint16"; break; @@ -100,6 +103,12 @@ static PyObject *GDALCreateNumpyArray(PyObject *pCreateArray, void *pBuffer, case GDT_Int32: pszDataType = "int32"; break; + case GDT_Int64: + pszDataType = "int64"; + break; + case GDT_UInt64: + pszDataType = "uint64"; + break; case GDT_Float32: pszDataType = "float32"; break; @@ -116,7 +125,8 @@ static PyObject *GDALCreateNumpyArray(PyObject *pCreateArray, void *pBuffer, case GDT_CFloat64: pszDataType = "complex128"; break; - default: + case GDT_Unknown: + case GDT_TypeCount: CPLAssert(FALSE); break; } From d498a060503820be11a539419460bb0666253698 Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Tue, 20 Feb 2024 11:14:06 -0500 Subject: [PATCH 060/132] Doc: Add instructions for Python API documentation --- doc/source/development/dev_documentation.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/source/development/dev_documentation.rst b/doc/source/development/dev_documentation.rst index 6946670ea065..e23d8965d7bf 100644 --- a/doc/source/development/dev_documentation.rst +++ b/doc/source/development/dev_documentation.rst @@ -30,6 +30,26 @@ Once installed, running ``sphinx-autobuild -b html source build`` from the ``doc and serve it on a local web server at ``http://127.0.0.1:8000``. The pages served will be automatically refreshed as changes are made to underlying ``rst`` documentation files. +Python API documentation +------------------------ + +Sphinx uses the `autodoc `_ extension +to generate documentation for the Python API from Python function docstrings. +To be correctly parsed by ``autodoc``, docstrings should follow the `numpydoc Style guide `_. +Docstrings may be found in two locations. If the function was defined in Python +(i.e., using a ``%pythoncode`` SWIG directive), then the docstring must be +placed within the function definition. If the function is defined in C++ only, +then the docstring should be placed in a separate file +containing only docstrings (located in :source_file:`swig/include/python/docs`). +Sphinx loads the Python bindings when generating documentation, so for it to see any changes +the following steps must be completed: + +- rebuild the Python bindings from the build directory (``cmake --build . --target python_binding``) +- make the updated Python bindings visible to Python, either by installing them, or by running ``scripts/setdevenv.sh`` + from the build directory +- update the timestamp of the ``rst`` files associated with the page where the documentation appears (e.g., ``touch doc/source/api/python/osgeo.ogr.rst``) + + .. _rst_style: Sphinx RST Style guide From c25c497756267db8380510d9defdbf9b85028cf6 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 17:05:12 +0100 Subject: [PATCH 061/132] PG: further cleanups for PostGIS < 2 --- ogr/ogrsf_frmts/pg/ogr_pg.h | 7 ++- ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp | 85 -------------------------- ogr/ogrsf_frmts/pg/ogrpglayer.cpp | 9 ++- ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp | 14 +---- 4 files changed, 13 insertions(+), 102 deletions(-) diff --git a/ogr/ogrsf_frmts/pg/ogr_pg.h b/ogr/ogrsf_frmts/pg/ogr_pg.h index ba22baef4980..557b1d84f231 100644 --- a/ogr/ogrsf_frmts/pg/ogr_pg.h +++ b/ogr/ogrsf_frmts/pg/ogr_pg.h @@ -196,7 +196,7 @@ class OGRPGLayer CPL_NON_FINAL : public OGRLayer static char *GeometryToBYTEA(const OGRGeometry *, int nPostGISMajor, int nPostGISMinor); static GByte *BYTEAToGByteArray(const char *pszBytea, int *pnLength); - static OGRGeometry *BYTEAToGeometry(const char *, int bIsPostGIS1); + static OGRGeometry *BYTEAToGeometry(const char *); Oid GeometryToOID(OGRGeometry *); OGRGeometry *OIDToGeometry(Oid); @@ -638,6 +638,11 @@ class OGRPGDataSource final : public OGRDataSource bool m_bHasGeometryColumns = false; bool m_bHasSpatialRefSys = false; + bool HavePostGIS() const + { + return bHavePostGIS; + } + int GetUndefinedSRID() const { return nUndefinedSRID; diff --git a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp index c05c83321f01..8b6a8db7e528 100644 --- a/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpgdatasource.cpp @@ -902,21 +902,6 @@ int OGRPGDataSource::Open(const char *pszNewName, int bUpdate, int bTestOpen, OGRPGDecodeVersionString(&sPostGISVersion, pszVer); } OGRPGClearResult(hResult); - - if (sPostGISVersion.nMajor == 0 && sPostGISVersion.nMinor < 8) - { - // Turning off sequential scans for PostGIS < 0.8 - hResult = OGRPG_PQexec(hPGConn, "SET ENABLE_SEQSCAN = OFF"); - - CPLDebug("PG", "SET ENABLE_SEQSCAN=OFF"); - } - else - { - // PostGIS >=0.8 is correctly integrated with query planner, - // thus PostgreSQL will use indexes whenever appropriate. - hResult = OGRPG_PQexec(hPGConn, "SET ENABLE_SEQSCAN = ON"); - } - OGRPGClearResult(hResult); } m_bHasGeometryColumns = @@ -1941,33 +1926,6 @@ OGRLayer *OGRPGDataSource::ICreateLayer(const char *pszLayerName, bCreateSpatialIndex = false; } - CPLString osEscapedTableNameSingleQuote = - OGRPGEscapeString(hPGConn, pszTableName); - const char *pszEscapedTableNameSingleQuote = - osEscapedTableNameSingleQuote.c_str(); - CPLString osEscapedSchemaNameSingleQuote = - OGRPGEscapeString(hPGConn, pszSchemaName); - const char *pszEscapedSchemaNameSingleQuote = - osEscapedSchemaNameSingleQuote.c_str(); - - if (eType != wkbNone && bHavePostGIS && sPostGISVersion.nMajor <= 1) - { - /* Sometimes there is an old cruft entry in the geometry_columns - * table if things were not properly cleaned up before. We make - * an effort to clean out such cruft. - * Note: PostGIS 2.0 defines geometry_columns as a view (no clean up is - * needed) - */ - CPLString osCommand; - osCommand.Printf("DELETE FROM geometry_columns WHERE f_table_name = %s " - "AND f_table_schema = %s", - pszEscapedTableNameSingleQuote, - pszEscapedSchemaNameSingleQuote); - - PGresult *hResult = OGRPG_PQexec(hPGConn, osCommand.c_str()); - OGRPGClearResult(hResult); - } - if (!bDeferredCreation) { SoftStartTransaction(); @@ -1991,49 +1949,6 @@ OGRLayer *OGRPGDataSource::ICreateLayer(const char *pszLayerName, OGRPGClearResult(hResult); - /* -------------------------------------------------------------------- - */ - /* Eventually we should be adding this table to a table of */ - /* "geometric layers", capturing the WKT projection, and */ - /* perhaps some other housekeeping. */ - /* -------------------------------------------------------------------- - */ - if (eType != wkbNone && bHavePostGIS && - !EQUAL(pszGeomType, "geography") && sPostGISVersion.nMajor <= 1) - { - int dim = 2; - if (GeometryTypeFlags & OGRGeometry::OGR_G_3D) - dim++; - if (GeometryTypeFlags & OGRGeometry::OGR_G_MEASURED) - dim++; - osCommand.Printf("SELECT AddGeometryColumn(%s,%s,%s,%d,'%s',%d)", - pszEscapedSchemaNameSingleQuote, - pszEscapedTableNameSingleQuote, - OGRPGEscapeString(hPGConn, pszGFldName).c_str(), - nSRSId, pszGeometryType, dim); - - hResult = OGRPG_PQexec(hPGConn, osCommand.c_str()); - - if (!hResult || PQresultStatus(hResult) != PGRES_TUPLES_OK) - { - CPLError(CE_Failure, CPLE_AppDefined, - "AddGeometryColumn failed for layer %s, layer " - "creation has failed.", - pszLayerName); - - CPLFree(pszTableName); - CPLFree(pszSchemaName); - - OGRPGClearResult(hResult); - - SoftRollbackTransaction(); - - return nullptr; - } - - OGRPGClearResult(hResult); - } - if (eType != wkbNone && bHavePostGIS && bCreateSpatialIndex) { /* -------------------------------------------------------------------- diff --git a/ogr/ogrsf_frmts/pg/ogrpglayer.cpp b/ogr/ogrsf_frmts/pg/ogrpglayer.cpp index 611925129d8c..53bfaac881c9 100644 --- a/ogr/ogrsf_frmts/pg/ogrpglayer.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpglayer.cpp @@ -649,7 +649,7 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, STARTS_WITH(pszVal, "\\x00") || STARTS_WITH(pszVal, "\\x01"))) { - poGeom = BYTEAToGeometry(pszVal, false); + poGeom = BYTEAToGeometry(pszVal); } else { @@ -812,7 +812,7 @@ OGRFeature *OGRPGLayer::RecordToFeature(PGresult *hResult, if (poGeometry == nullptr) #endif { - poGeometry = BYTEAToGeometry(pszData, false); + poGeometry = BYTEAToGeometry(pszData); } } @@ -1723,7 +1723,7 @@ GByte *OGRPGLayer::BYTEAToGByteArray(const char *pszBytea, int *pnLength) /* BYTEAToGeometry() */ /************************************************************************/ -OGRGeometry *OGRPGLayer::BYTEAToGeometry(const char *pszBytea, int bIsPostGIS1) +OGRGeometry *OGRPGLayer::BYTEAToGeometry(const char *pszBytea) { if (pszBytea == nullptr) @@ -1734,8 +1734,7 @@ OGRGeometry *OGRPGLayer::BYTEAToGeometry(const char *pszBytea, int bIsPostGIS1) OGRGeometry *poGeometry = nullptr; OGRGeometryFactory::createFromWkb(pabyWKB, nullptr, &poGeometry, nLen, - (bIsPostGIS1) ? wkbVariantPostGIS1 - : wkbVariantOldOgc); + wkbVariantOldOgc); CPLFree(pabyWKB); return poGeometry; diff --git a/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp b/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp index b3a4688746c9..925c01ddeda8 100644 --- a/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp +++ b/ogr/ogrsf_frmts/pg/ogrpgtablelayer.cpp @@ -1250,7 +1250,7 @@ CPLString OGRPGTableLayer::BuildFields() if (poGeomFieldDefn->ePostgisType == GEOM_TYPE_GEOMETRY) { - if (poDS->sPostGISVersion.nMajor < 0 || poDS->bUseBinaryCursor) + if (!poDS->HavePostGIS() || poDS->bUseBinaryCursor) { osFieldList += osEscapedGeom; } @@ -1262,18 +1262,10 @@ CPLString OGRPGTableLayer::BuildFields() osFieldList += OGRPGEscapeColumnName( CPLSPrintf("EWKBBase64_%s", poGeomFieldDefn->GetNameRef())); } - else if (poDS->sPostGISVersion.nMajor > 0) - { - /* This will return EWKB in an hex encoded form */ - osFieldList += osEscapedGeom; - } else { - osFieldList += "AsText("; + /* This will return EWKB in an hex encoded form */ osFieldList += osEscapedGeom; - osFieldList += ") AS "; - osFieldList += OGRPGEscapeColumnName( - CPLSPrintf("AsText_%s", poGeomFieldDefn->GetNameRef())); } } else if (poGeomFieldDefn->ePostgisType == GEOM_TYPE_GEOGRAPHY) @@ -3822,7 +3814,7 @@ OGRErr OGRPGTableLayer::RunDeferredCreationIfNecessary() { OGRPGGeomFieldDefn *poGeomField = poFeatureDefn->GetGeomFieldDefn(i); - if (poDS->sPostGISVersion.nMajor > 0 || + if (poDS->HavePostGIS() || poGeomField->ePostgisType == GEOM_TYPE_GEOGRAPHY) { const char *pszGeometryType = From 0c14536bb4b01f3fb57e2d5ee1039a4dd145d6ca Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 19:23:38 +0100 Subject: [PATCH 062/132] GDALWMSDataset::Initialize(): avoid potential integer overflow (fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=66619 and https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=66774) --- frmts/wms/gdalwmsdataset.cpp | 44 ++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/frmts/wms/gdalwmsdataset.cpp b/frmts/wms/gdalwmsdataset.cpp index f2667eb4b794..057efedec3a8 100644 --- a/frmts/wms/gdalwmsdataset.cpp +++ b/frmts/wms/gdalwmsdataset.cpp @@ -471,6 +471,13 @@ CPLErr GDALWMSDataset::Initialize(CPLXMLNode *config, char **l_papszOpenOptions) } m_data_window.m_tlevel = atoi(tlevel); + // Limit to 30 to avoid 1 << m_tlevel overflow + if (m_data_window.m_tlevel < 0 || m_data_window.m_tlevel > 30) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid value for TileLevel"); + return CE_Failure; + } if (ret == CE_None) { @@ -483,10 +490,43 @@ CPLErr GDALWMSDataset::Initialize(CPLXMLNode *config, char **l_papszOpenOptions) (str_tile_count_x[0] != '\0') && (str_tile_count_y[0] != '\0')) { - int tile_count_x = atoi(str_tile_count_x); - int tile_count_y = atoi(str_tile_count_y); + const int tile_count_x = atoi(str_tile_count_x); + if (tile_count_x <= 0) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid value for TileCountX"); + return CE_Failure; + } + if (tile_count_x > INT_MAX / m_block_size_x || + tile_count_x * m_block_size_x > + INT_MAX / (1 << m_data_window.m_tlevel)) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Integer overflow in tile_count_x * " + "m_block_size_x * (1 << " + "m_data_window.m_tlevel)"); + return CE_Failure; + } m_data_window.m_sx = tile_count_x * m_block_size_x * (1 << m_data_window.m_tlevel); + + const int tile_count_y = atoi(str_tile_count_y); + if (tile_count_y <= 0) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid value for TileCountY"); + return CE_Failure; + } + if (tile_count_y > INT_MAX / m_block_size_y || + tile_count_y * m_block_size_y > + INT_MAX / (1 << m_data_window.m_tlevel)) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Integer overflow in tile_count_y * " + "m_block_size_y * (1 << " + "m_data_window.m_tlevel)"); + return CE_Failure; + } m_data_window.m_sy = tile_count_y * m_block_size_y * (1 << m_data_window.m_tlevel); } From 7967a18aa6d83f19020259829fd30d87b9eaf5cd Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 19:27:22 +0100 Subject: [PATCH 063/132] GDALDeserializeGCPListFromXML(): fix memleak in error code path (master only, fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=66620) --- gcore/gdal_misc.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/gcore/gdal_misc.cpp b/gcore/gdal_misc.cpp index bb9bb7a7cc50..63a23e5edc20 100644 --- a/gcore/gdal_misc.cpp +++ b/gcore/gdal_misc.cpp @@ -4224,14 +4224,15 @@ void GDALDeserializeGCPListFromXML(CPLXMLNode *psGCPList, return true; }; + bool bOK = true; if (!ParseDoubleValue("Pixel", psGCP->dfGCPPixel)) - continue; + bOK = false; if (!ParseDoubleValue("Line", psGCP->dfGCPLine)) - continue; + bOK = false; if (!ParseDoubleValue("X", psGCP->dfGCPX)) - continue; + bOK = false; if (!ParseDoubleValue("Y", psGCP->dfGCPY)) - continue; + bOK = false; const char *pszZ = CPLGetXMLValue(psXMLGCP, "Z", nullptr); if (pszZ == nullptr) { @@ -4245,10 +4246,17 @@ void GDALDeserializeGCPListFromXML(CPLXMLNode *psGCPList, { CPLError(CE_Failure, CPLE_AppDefined, "GCP#Z=%s is an invalid value", pszZ); - continue; + bOK = false; } - (*pnGCPCount)++; + if (!bOK) + { + GDALDeinitGCPs(1, psGCP); + } + else + { + (*pnGCPCount)++; + } } } From 47c9b5906942d82698e82be71992e39d3638790d Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 19:38:33 +0100 Subject: [PATCH 064/132] VRTComplexSource::RasterIOProcessNoData(): avoid potential assertion on 32-bit builds on huge mem allocations (fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=66644) --- frmts/vrt/vrtsources.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frmts/vrt/vrtsources.cpp b/frmts/vrt/vrtsources.cpp index a565d41fa7c9..722d9e36a66c 100644 --- a/frmts/vrt/vrtsources.cpp +++ b/frmts/vrt/vrtsources.cpp @@ -3010,7 +3010,9 @@ CPLErr VRTComplexSource::RasterIOProcessNoData( // Cannot overflow since pData should at least have that number of // elements const size_t nPixelCount = static_cast(nOutXSize) * nOutYSize; - if (nPixelCount > std::numeric_limits::max() / sizeof(SourceDT)) + if (nPixelCount > + static_cast(std::numeric_limits::max()) / + sizeof(SourceDT)) { CPLError(CE_Failure, CPLE_OutOfMemory, "Too large temporary buffer"); @@ -3226,8 +3228,9 @@ CPLErr VRTComplexSource::RasterIOInternal( { // Cannot overflow since pData should at least have that number of // elements - if (nPixelCount > std::numeric_limits::max() / - static_cast(nWordSize)) + if (nPixelCount > + static_cast(std::numeric_limits::max()) / + static_cast(nWordSize)) { CPLError(CE_Failure, CPLE_OutOfMemory, "Too large temporary buffer"); From bbf6dfe375e64252f8cb60d239b81ae5e4524327 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Mon, 12 Feb 2024 19:50:53 -0800 Subject: [PATCH 065/132] This commit updates OGC map tiles api to support additional image formats beyond PNG/JPG as well as enables content negotiation. In our case, the motivating factor is we have map tiles that are stored as GeoTiffs which we would like to share via the OGC Tile API. Tiffs are one of the six predefined encodings allowed in an implementation - see https://docs.ogc.org/is/20-057/20-057.html#toc69. As part of these changes, collections which do not specify a image type are now supported via content negoation (see https://docs.ogc.org/is/20-057/20-057.html#toc28). For example, previously this command used to fail and now succeeds. gdalinfo "OGCAPI:https://test.cubewerx.com/cubewerx/cubeserv/demo/ogcapi/Daraa/collections/AgricultureSrf?f=json" The reason for failure is the link with a "rel" value of "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map" did not specify a type field. And thus the old code did not recognize it as JPEG or PNG and ignored it. Finally, this commit removes the hard-coded value of nbands (3 for JPEG and 4 for PNG). Instead, it tries to download a tile and read the actual number of bands. It tries to read tile 0,0,0. Perhaps that is too brittle? But the hard-coded band numbers weren't really correct, at least for JPEG, and definitely not for TIFF. --- .../request_collections_blueMarble.http_data | 195 ++++++++ ...WorldMercatorWGS84Quad_0_0_0.jpg.http_data | Bin 0 -> 56946 bytes ...WorldMercatorWGS84Quad_0_0_0.png.http_data | Bin 0 -> 81810 bytes ...WorldMercatorWGS84Quad_0_0_0.tif.http_data | Bin 0 -> 93781 bytes ...es_WorldMercatorWGS84Quad_f_json.http_data | 138 ++++++ ...ions_blueMarble_map_tiles_f_json.http_data | 433 ++++++++++++++++++ autotest/gdrivers/ogcapi.py | 39 ++ doc/source/drivers/raster/ogcapi.rst | 16 +- frmts/ogcapi/gdalogcapidataset.cpp | 255 ++++++++--- 9 files changed, 1002 insertions(+), 74 deletions(-) create mode 100644 autotest/gdrivers/data/ogcapi/request_collections_blueMarble.http_data create mode 100644 autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.jpg.http_data create mode 100644 autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.png.http_data create mode 100644 autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.tif.http_data create mode 100644 autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_f_json.http_data create mode 100644 autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_f_json.http_data diff --git a/autotest/gdrivers/data/ogcapi/request_collections_blueMarble.http_data b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble.http_data new file mode 100644 index 000000000000..000d0eb8d63d --- /dev/null +++ b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble.http_data @@ -0,0 +1,195 @@ +HTTP/1.1 200 OK +Date: Wed, 14 Feb 2024 05:33:24 GMT +Server: Apache/2.4.52 (Ubuntu) +Expires: Tue, 11 Feb 2025 05:49:25 GMT +Access-Control-Allow-Origin: * +Vary: Accept,Accept-Encoding,Prefer +Content-Length: 7564 +Access-Control-Allow-Headers: x-requested-with, Content-Type, origin, authorization, acc$ +Age: 171838 +Keep-Alive: timeout=5, max=100 +Connection: Keep-Alive +Content-Type: application/json + +{ + "links" : [ + { + "rel" : "self", + "type" : "application/json", + "title" : "Information about the Blue Marble Next Generation (2004) data (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble?f=json" + }, + { + "rel" : "alternate", + "type" : "text/plain", + "title" : "Information about the Blue Marble Next Generation (2004) data (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble?f=econ" + }, + { + "rel" : "alternate", + "type" : "text/mapml", + "title" : "Information about the Blue Marble Next Generation (2004) data (as MapML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble?f=mapml" + }, + { + "rel" : "alternate", + "type" : "text/html", + "title" : "Information about the Blue Marble Next Generation (2004) data (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/dggs-list", + "title" : "Discrete Global Grid Systems for Blue Marble Next Generation (2004)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/dggs" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/schema", + "type" : "application/json", + "title" : "Schema (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/schema?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/schema", + "type" : "text/plain", + "title" : "Schema (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/schema?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/schema", + "type" : "text/html", + "title" : "Schema (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/schema?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type" : "application/json", + "title" : "Queryables (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/queryables?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type" : "text/plain", + "title" : "Queryables (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/queryables?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type" : "text/html", + "title" : "Queryables (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/queryables?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/coverage", + "type" : "image/png", + "title" : "Blue Marble Next Generation (2004) (as PNG; Note: requesting large extent may result in generalized data)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/coverage?f=png" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/coverage", + "type" : "image/tiff; application=geotiff", + "title" : "Blue Marble Next Generation (2004) (as GeoTIFF; Note: requesting large extent may result in generalized data)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/coverage?f=tif" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/coverage-domainset", + "type" : "application/json", + "title" : "Blue Marble Next Generation (2004) (domain set of the coverage for this collection)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/coverage/domainset?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/coverage-rangetype", + "type" : "application/json", + "title" : "Blue Marble Next Generation (2004) (range type of the coverage for this collection)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/coverage/rangetype?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/map", + "type" : "image/png", + "title" : "Default map (as PNG)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map.png" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/map", + "type" : "image/jpeg", + "title" : "Default map (as JPG)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map.jpg" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/map", + "type" : "image/tif", + "title" : "Default map (as GeoTIFF)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map.tif" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets available for this dataset (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets available for this dataset (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets available for this dataset (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles?f=html" + }, + { + "rel" : "styles", + "type" : "text/html", + "title" : "Styles for Blue Marble Next Generation (2004) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles?f=html" + }, + { + "rel" : "styles", + "type" : "application/json", + "title" : "Styles for Blue Marble Next Generation (2004) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles?f=json" + }, + { + "rel" : "styles", + "type" : "text/plain", + "title" : "Styles for Blue Marble Next Generation (2004) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles?f=econ" + } + ], + "title" : "Blue Marble Next Generation (2004)", + "extent" : { + "spatial" : { + "bbox" : [ [ -180, -90, 180, 90 ] ], + "grid" : [ + { + "cellsCount" : 131072, + "resolution" : 0.0027465820312 + }, + { + "cellsCount" : 65536, + "resolution" : 0.0027465820312 + } + ] + }, + "temporal" : { + "interval" : [ [ "2004-01", "2004-12" ] ], + "grid" : { + "cellsCount" : 12, + "resolution" : "P1M" + } + } + }, + "crs" : [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "http://www.opengis.net/def/crs/EPSG/0/4326", + "http://www.opengis.net/def/crs/EPSG/0/3857", + "http://www.opengis.net/def/crs/EPSG/0/3395" + ], + "storageCrs" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "id" : "blueMarble", + "dataType" : "map", + "attribution" : "NASA Earth Observatory", + "minScaleDenominator" : 1091957.5469310893677, + "minCellSize" : 0.0027465820312 +} diff --git a/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.jpg.http_data b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.jpg.http_data new file mode 100644 index 0000000000000000000000000000000000000000..0760475e8302865005af2d4ef2bcb43645022b9f GIT binary patch literal 56946 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%0AiQQJEuEgxnDCI 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≫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

)Oj{w%uTUKZlvs9R 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�`P!0L7D$#;lMY;5H2dvC^2leBK{AeGGE^yD*b`abmb z^|EeL3*(!{ar-UTv8{D0Rh1R^bBmZedw&i&=n!m9FAhmU(36a;X#hm}dxLzs^a~E2 zH48ycgK1&vDq1RqAlk^w$tMC^V2ZbTX5M%Q9@*qJo0iAj@pV|>#j&OG)oR83=C(?n*? z+K(MQ16YEL?DSEX<6`zf`+$~hxsE05I#9)3zZ;BcsKwMwjA#@@5nH~E{*E2gl;z-e zh+s;8XZ*l-8bAEa2Jj68OUOPd*%3q?0SPOe25i}WZcbM=_bf?Z2xyrUXPtTkbz`RT z(hIL=jmWSNb;m#dx4(gim#6Q!3q+BtuQ-c_(M`Pgr#}%(nDp)Vnll!h$@4G&g=3C9 zmP6-E$K`b45GBS@c+Bst3uS|Y70vx7|!K5ztsZBar&8+ZKS0g{>lhQRq3pP$_TOp|-=dkoW*Sihl} zW9Q7`n7MO#@6(lpLLtVEo57^UMjEO|^8N?!^2Qr4Fcb(geUIrZedk5`hC*C>{f+$Q z*OyaSRY!G+m)~7~1urlBGqTOi&fb2;)Hm?yk|ju@#LVe4X{f6womQ!+s;9KLfEBCP zQBhWk!)BwZyqMATHLUz{HM92Jhf6NG20^fso8u#u&hXi?Wps3O(lok}9qsM(4+V(D zBa{~8v-|FQaQ`2krglUfo4#JdP$)rhZXOPsg2OHo3MU8*_0ZqlK`Itx-kd|IsUC@G zib!I%^<7b6K9`+$26dGMjIJAr$CHcG?m)6RDarSvsv2w7uEW%Ia&vNMZEYpj=OdGh zBC2tUaui4dh*=HFE?GIIo()aY8PLh5a3u*7lYy-}>FMc3#KP%v{WUE3f%vBl0LTi0 zEECHh`{l4iFb&Lf222oS1v8c8nj3Co+P?dNVKV>Z6WC+Q7%*f+>tDwk{|FH`D_49* zuCtp>Tegu&#W>~oBdMvXrX=6P;*Z|t{>L9<(JRlB@AZ;M#i=eUrMkMD;lXb7aEN$3 zN;;KhcsR_KEt@&~(8K8I>BVE0$z&1?2ZDU@#k=e?a~>|c#F90e$o1HeC6O&(Y^C$- zW<;lnEys&!b0W$L79ff;f~kQhgREfeN|g;q5=0YeL_uW1G4mK%nn!pzh-pH;&rWg4 z2oSSXg#TMGC7pj-S}`jhKIi;j^ZbK1AXqwJWep!Qn<5mI>`5a-ZkbF=djLEzacl|g ztsA)F@~1!&dH8`pf@$*7^Dpq(>wo0-TkfQ3Y$LH~gk@hfGrFmXXP$YS!{^T9!}s2$ zwzi&eV;i~j!e29eN)wS-f+OaiMJy7fy>p2Dr`1qdl23nJX6w3DOrNnowN(`~R9CQc z#g`m^^f4sj3D$4eKxIWagF`{u+B%ppb_}^*4~pHzlnG<$9|%%iHIh_DW#`U5)~{d3 z(MKG?QS*)@om3e;Y7~J`2uZdPiAS={K_!Fs?X4u@F+$-1rcM|`&8V?iqfe^j<(1Oj zww2n70!CDfB&F(v5=OQmC=#XRm6R115e^Jcl;`8aFE&$NT)_6OZc-VQX}e7#7#JoH zPU80{Oq(*E+=60C3iC*7I{Eqe7?wg`B!fG*I!nk(>@dKNnc+d<;4XYGWRH+e&!`EyZBt5ed!gN zn#QnU-5RQD#?sc>f%p`vM%C1?Zqp`0u@tjr?tx)sYv;?$%h=x4 z!gLLYCx-3ExeLL>opolZCW_ELsJQGR>%4J1+-67d+9 zoOLi2V`j5{>H8di;q}beZ8DimifgalnH+e~9K3EPzx?$@Oc_6p{btQ2 zFUQS?pM1t}D8$TZ(^>V^YSc`UwJR4h`=Dbft*9dw4Kn|zdAz*nBM#VaPmpZf_vGtL zX((lE;~3U|wVFGwKbHsp{2>FuD1*ZZrZiTf>n4JwGI4wpflw5uL&0H_`DA4?M;&?~ zh&He^5Jf~$03q8U%1Gtwo*|3hvnmTsdZStg7Y~0B5HJgbg(^Qn_q3Q;MLqp_v-0U%_iTu2L(y1hhtT26YBX7L$Jf;;v z5JfCS|92n3j|$O}|E2LCvU?82RnWatr`qR5)^@F_C}uM?vX~743=2$)c*5eVm4m$f z);2)Ql81;{!Vn8YkxxEc!lHLxhk@380YyV2g!$&zI& zIAZ<<__OfW#<53!yXIS3*78H}?>&HBFeQ~;v!SyO7&0P14%2N~yD|-; z!1=$roE)!@^3pt3uUU>}*r=(k<*buW=H_)M zKQD)jYLH2%u`G*%{2Z#rHX)?abab>MOA>tpLlorsY3bNWVU7n`v7xFer6t8wlohk| z%XKJ<%*xeYarnXeu)ev41;-ytGL`_v3lfOmQQi07gej_w8zm4+YB)>nY}sl-Ji*i% ze)_h@=@|sF$qv8-SGE~QUam-4eh!5~Q1q2>^;9;gsnatu(mvY)Er;y2{ ziNq4P+%A@{+Q^j`9>>}9k7UN&GqF3Iq!Lj^){UlR+crkkRAI9_2?m2)d-F})b@MG* zUOA;Px~_t@_6|zR%1I_uWK@kDznAuo9W47|Ip?2#GS9#IKF1$*2$^&mpU+EL)k&q* zU4v8v9CjPYR2p5^7#avLqN*IzFp(667yt4mhaYkPYC6Hge|(V%co1FP14N@??!D_8CQO>a(Z?;|fc^JlaA1&@_8z*o zxA4dxZsVdWZ^r4gGaQZ)8rV*MZx6#slhjvBY3U2%sx9Nf3$7%s8jNkIp{=8nKR~{Bh=GEJf399x*g0vd~ZZN|8)Jo z!@u1C1iOrp8vd6ly&n^K6>$PR0{(o3w-$ZQ#!Z_D^tSW-GY@mgmABya=ds)5NnC#U zMKq3|%%r9!CQX=-l|~vGiFh2B+l?p)go18Pn(+46@*yL%H;V+sUYD zDvAmzFRx_nhGq`hZ#v18%Bb2Z28Pp=N zG1M5AD$*X7!AQ|GDhH7?x6WOPN=cz1NR)4a8fo!x^Rc;G%1hdgd)pmn!)9_%;nlkUc)Jq4OeyvyG#p2%xw1^1fE;?8e<#B zu==aj>@#B@fT?RS@hbAG*lLI(5Y9larv9ryLvhP=G$@DoqX`& z2kbd>A3k64IeX8TNq#{dx|*i6tc*{Wt>W{Qs~B5d!bO)~g`j66DY zX>0?-p&%_S8yHzp#1~(#L^pI+u5HHcl!(UT*c6EgMFxdsB@BncWK@lvJ>A%CHgf!a zx_f)5tSDx5{Rl)6%F4=^KCOxRx{;iG%8_(z@1!WtN6$czB`a2Drz|00O5Z=e>qmuP zWn**iu>>RHJX~@*dvdk zs34zoMy1p*VhJ*xJNs!GH-Y)b9m#Pg9FNE4$8L8Zh$8)i0aVSvummPeoWSl=rXmO; zHbthoqL_3>LlUwRoXbj!SiNpDufOvViY##5h0o)4Dj+TygXL zfQ8p3^T{V4B3cHkRxV*j-vG7cMO=5)`Gkl27z%``ACb?>HD9sx^JVn(^>N6&IUIf1 z0X+TGA6c<-716$CrXPC@C!Kma4_tdOmtAo!^`jdpbUBdy|9U5N(LIe?eHvqvdgJOJ}!4fnO%^%0b{~-|ZB3cf5yHiY=F_DF@zsamwbE&VZXT!#?x#YqV zxbB9l*t%gA7hP~IMfv$86LGXmnmnHyr^7)yonqOFH4Fv9cs(BMHaqz_E)F{I046ln z@cSE1qrJ0>58q#eVOsPJ3{X^9LScRZ!@&Vciwkf$Z3u$EpI_R5LsHRna5)tkYHJt@ zhH2i~LT-+qj`lXbSh0dz?!2GY);2y~yn>;@Acr4$662f3a?@=$a{SRp@SDpn%QEf% zVS46&49h~4d|-gML!&e7qW!BBlFvzNPYkEs!C)wfBUh$30~jV_>%C~QMoLtXoDN(* z54}4F=?oWw zaGbGCW06Dwo1&0RM)7$ZXeOv?R_;Az;#l^bxhKJ)VLWa(mZ6bKB#>l* z%8}TWA0AeI6H-``fvIFe`EOrsWpvP_Z$wA`i?HyzOkR2Ak0c_4oPFBqgnBk};N0U` zzit_!aDZ(sYuM7-MlzA&`g7(Y%Mv!b1DD6oK6_20epD@e{ewgz5gNxd;dj}1^yXIx zh7-i&DULqwWNda1GpA1{8jteo8;d9^$mf~oUnHF|>Fw|5xfh@3_IsbCzPgkz)@-1C zdk1C3`PA1`QBhXLhUTqwcJ*@W4ObG61^CnRFHl)tNIW{g(&eA?_S=i8udn5Uci$pA z;E`-f^(TTT?urLRg|+Potf6e952HVfZVAK{jbvQM?Y5Cfs*IiJ!RPca!sjC%*J$4! z!Iu`XyBy?J`q{R+4@0x)+|i9;W;p-S)7g96C@PEcNT!o)>}aKSLXq+Pso5R@A zwb&FTJ1R>MD9q1C)v`JaS&}Hr_ux=OTuwVVdAXRTK_D2yE(@fR37pQXT2PTiR(`df z&K>RKdL({%&RLWe7LZP*DJk`H;xY4BxoRyX3=$t6WOGXgdrlq21J|5E|3HAp?z@de zue`|Vr<_gCmNndU+qJy+`U`xz_#J#XPO57faQcfm`OI@U?cysbDlFi#bB^P&+pnO| zCu4KEXd2r{G#=xidDHp$-9-dK!;GjLL3eLAHc{rJ6As|dPd`UNZa#nc^K+Qr)5k&3 zL?o9FUvbTMUh`cL3<*)o9!QcPxo-IW%YRx3f}LdAK$PP=^V}Oa3TpVntvB(^pB^U? zN#gSrbI2h_U}!397e7rhG0bkWPGJ3-<@kI)6hSAU2^8k}k!1-(P2+GnNGB7VF#lX0 zeCSrDO`S$nWd)xt{e*^5qxj_W6?i=!wluF}<(Eqcgknq_--Od{XKQOKUbjp^K>@0& z;&rd^hYCvBXL}!gA>lYkh3qk4!_Td!|CPu=l;ZV_y3O9 zo_h!_q2kHSW4GyhW3$=FWK=Fcc`gSXb24_D9goM2WtuphPB1mn84X1eaX1{F zQfV#g1=th}!@x966j{o)aN9L_RF*~J@dT1=Ls4wm`HKSheI5i09EwQKP?VaQ8eC2r zjic-NZ0TwaJ$OF~i%QT8i&M`275N1v=&>Q5yYpQ7h7x?SdT=;uz@g6!_RuLEs zbLm~Lp$ICn;$U0*4vKP}q>>2=N~`d@Z7lzM2~R$7BfB4X0F*#$zY_Tc1-PAVdIy6H z^=!kDQ%*sSmw~|{GN~xl)irEs@1}M{5#eZ(1NPsSp+JQ5esLTJ9(*9DpLP))J9cp1 z1?TeUgHI6+4RZMvSAb*#3$QHS|KLNqy0-JsgE#W^S39%vXiZ0y{@OPDzYWt$gJRFt zc$m9%|DpkcNbMvSBb_#O45vv)O)9Hh#1bZ^D5AwxboD>4fB*m>07*naRLMloKss)b zS7^hJ3!zYA=cXW`NSr;VRuN8X*i8vRbRsJvXerzdJC2+Jc6PNhws9&KUVbBmg~d$U ztAYD&e}X3-x{0S2eo874!{aY3!7vOIStb&V5)1?=EG)w7^C5~NiXvm^27&;p zVX<-J2Bu7!NJiDcvQSkOWmk74o1EQVWt)Pb>ulPzm73ZrL_x;ovg2_(F)e}az9B@x zV&6TR_|=Isu(?XH&^Y0Od$21aiImC12ZQPMm|LXLI_ocmh$E@pu$& zdF)jJ!x65$;5gQIM0xSqKQaH5)3PJbwSUe8l#R%vyC?Xg%!T~vj-vk%qTnSQbK-GX z_1=7kG7Rpy;t28ziV5_0F{ZAP=kNR_ z<(1Xkdj4TV6B%}!J_|=mAROo?nh5d9lJ~jy!6&)(cfVs`Fo33}dFQpKQ4|Ne zjUUAxyG^B`VGNr$Z(+xdcIM4Hmg9~&otIwv6S}H$@h>mr+;dLl@+&XT%9Bk441)zH z&Z4@a7^lO=QHRba+dM=EMa~-KPXtkLAnHO^8(?O=fbo0eve$kE$S#R(-ASt43I&xO z1ndk4QVjPe$@TguDi}pLkfN&Ihbd|drJ}fX0dJnbxRJ$ZmP|a7A~Y0YATWrPiPO1r zm_$;iueAev#?DD+UxJ#BGymwlm_2nSk>29Vsctpb~+PM9`Kk@iWZ*cYPkFnsG*<5?sark{6M9afl@4bm2DOlgZ z8+CUN{wT%|KVuI7vIl8SVU(OAKi-5+Wknvj9w${3=JM&1*C{Bdqsbe@0-D?z-bX z5an#VFYQYAiAq+~VVFGo=f^ntj0?b&Kr+dur!+u7H2!C9+|C-10 z3m@Zme|U*IuRf96AA283R!|ffo6SZnp2T63NhMRXwlq`UI3B0nMm!c{^XAP|low;O zIq~{)Y1z7&bVg-FWd#{kLzWe4MrH@H#$z$oecjAJ_eKsq@-#xx1RuQn4hPNNo4233 zo=h^q>@)7*#XHYr+`cEWddb_Am*z9LvkiwNkTFcoyY@*;1RO3Get#~SZf2ExR(Ag2 z=FQDKdF>@sjH&0WD{sZ^@?cpOXCAa4F0YdTXNW02j2o~ErVu-Sz zTkp7@E3UfgU+MsUZ2W9J0Lb!Yre-p^X>Z(KHy($J57xHR+tx~ZSCFs2Uct&ilaae0 zO=V>P+nbBe zIe`<7p3B33xQR(K_GR&kO}zQ`EA;dS!PL0@`rq)$JAdJ-tIlR^+eW5Noym+nc1P1q zdV9N>FlHQMO8j6`!Gis#kIUY@p@E*At#|TC=j{3&vc1NwY)6)#(D>mSa!>$GVA$p- zlu2S{Gy?Gi+qMp)s1p6@QevGF3ywd6-S#?;+WZcJJ?-=kM2V*~g2@yGeg__hjiHcA zetrc@-+GN4myDpwSTcOEtskGm&Xorrz?z){G!G9Fa2OQjlNT=fLrl5&>5dOLLu^^B~o$LsNd79_tY56NX`;(vyhCBP0?rM8%@C+)qKo$-YM)&z)y1pg1=NJr?5Z zGcH1u{Q#5~=V$v<{sRQZ5AQSaL-Dir008W+5Rxpg@3;{pqY>QB9HO>j{5~g&Y@@$> zBUk_KY2J9~a#X=0mP~QjvAyyu z=mM;28KAJ(#?TO8s=V^@LM}M>6bcJ`Jp24xC=NGXx1DF7dmaRXSD$*6l%*1hr3u+Y zat#wt#!hQO&F0`uAuF-91V8{mM0B_?4FfUr@B0V-H({D_kPPa}Jy=eQ5qbG+?CRsn z3$Nsu6He#CllCKMq|u`WE1QQI*Wg3bG)zIp=Tb23A~_z3BM-lbd+xjiHIt#FsDR=^ z2VFY@Z0d@EX)%3lDV613Ufa~p{v#{s35AG9O~%)cpf5IzXqr^z74Z7!o6tN0wlNL7 zd(GYWbMkQ76>K&KWfdd%;LWGeGBH*yeV-BKe%3Et$#qYC&Y$nOfT2JTk3*rTVHSrR zF`xT>dkQCA`VeEsj-h`rgv;%qzqf~;9ov~aWipWo6o^VU(C?Xb?n^HhMtO{YdYS%67=LS4yOZA$>r#CZ^g279{=shq}4PR z-1;&%{o*hb7i0v5iM!8Y-$Ul}{DXI}y>&esS_c?iTZG|v($_k`rtRJ2xV*IYXU)(w zQhfH=d+fXaf!`|oOrnVVceFS^YYzZGwuxARK{%$;8Bm#j=|dFRLj3Z=tEjK8qpqZg zQT5dv_sbjDbGLCEaPZL>2FyC{*XXJWW|CMULn@=Ak)a^hhG0nO1{iLga!(E;NBCK_ zwV%=DJ|6hPQ(&rW-MWpfE!$YNW&_1;8=Kd!=Fr*GIAiX1qcM=baVw!(iSew##^sGOmU%;oIE#4DM4>C%BIdR z7zU!?=Zj4}{Nsp=+3?$pMER&#IN5uzB5bqK1KDw^LnF z%fe?LV4$yufv#RG$%!w&3SG@$$R7L!m4t%5EPD6?p1b{Qj=%VB&bwtHLj%2}NaM~c zKoUg0Uim(fY-7KpPh-QD0j|E`Cf2p|ao+iN5Q!xiU6xO4Z}hJVW9iuJPByMv4S;Ee zvQq8;5`Oj`fZG@0>|b0$@4ELH2!;_vn6~#E%ygW+4nBdmp1qZmFS>)$vI?&I<>B1_ z+^0PC;EnWb`;r|!L2`5QP}KzCxJgOA2T2e~=_V2;gP}NEf`g1NDB$(ayO_~fKtpLR zS3UGPU#<9r=O4d~gldwEs|>kPm6KN!y%;ufZZ(Vt8PDp24iUWvzckE^g>2JI8 z{*)*kk)p)wW+tCWqjD9Ew1VSOAd z8~7ZYGP{-?TY3p=X(~%{XzS@iw7aP+^zgwK9f;(9@4ZZJRJi+*MO=T$0=!Oeqe0WFgz7K@ThrD@)_16fum%5xG+ z=sfiNQVyEYz(-%Sv3l($9AcW^k@cLnU@u%bmGpLQ#_jPj zamEpR@$rl7Xy49VPb}i0n}5Nf3ohsF7w_PNv##eak6(k|Ux-}>e@-5vB#?;*F)W3F z-aZ_Pjh%x-G>vPdx2KnIEJZSvVcfKN*z9hWy!tfpj7~6^WLakybH>;4-r7z?(fjRZ z^ZILlVmK6G*1j`|MuLo+G#yd+>+?T0e)b!H#~0=g_dZH27Uhrw=V2H+w_I^D7hHWm zd4(kihDIn7tF9 ziDEXMw|=_R-v19OtFj%8o5~7sc%5u)+sT;vauh*eMRO-3i~TJB>aS(rrk+BSotTD3 zd4tAhZ>^!eZVHHY5M^fVW+R=Hs4XpHZRutg8lNL-ndLBoNRM6BI#bpg_UbBqRV@IKzGFw`jNvBN` zX@gKSP7;ypZ+n@EjkVPR2gChcTyW-zy!FWnh697>y1~m2-^4*joj}rbG0?UG zr`OAlu5S8=LR1v_NUH))S*5>!fV>i86m5o~mDKBwTnCC`EX6?ofyx54E8XLR%Xe`ZPW!t|tISY%iH8RZ|8kUqn zG=5lLjsFRL_8UM^X^g|?E}*@29d%v-)k<^iJ&VZ6&Ev(V@8srpA7a&?xA5n?PGkJO zXR~zGdaClAY+d^i;Ybp{D?5NAmQ)#zsubrrvhAHL1DC^xA{Y!r(&X7BOhF))iQ$qJ z;*x}*2}A=aa*AziYaPbn5Lvx_5KILe3aHjDGVkYY`~W58DaKY8lkap>TIA&Q&svyV zSBNP|yz$WvMEUP4$xI^y2#Asm)6m#`Z!dO43Y~7=SUkjjdwFQyIKZ^=Rft*=Lw1tU zHQxVnCk-RK$Tk7l6u9h~`za|c)*gl<5S+X^iL&ObfPc@1ZEq!;Y?Q-fWFCF;^xS3^97#enbQ7 zIPa3XnJ{S&G{eN}w(;nl7t=qqgNC~C#A8F$joE`xe;Z}hjdX0=LNa6F^LXeV3Zf+< z^bQR(X3SXh#4!1JUc!+Wq7k96pad!^RJ&svC!F zQ&`m+B(I=|Rjb#KNoCkws^h}b=MhQ>lvg!y?wMzD^SyVFNkp&-8Il_82u2CVGRT5R zI4NUTCMg|KT7q~w&2Th>s)!`TG(Mk)($UqZ8I81_BCm1;sf>Z!ZzC-kpvu|O*{Ff2 zJO57@KR_y}6N;u7B7tVWakCq6xEz3tD0{E~Q~T>|gFW_gBJ4_Miju;bT& zC!W@*ac1zl6vj1FP*Y@(NgLGEk7dJ#^*nmhX*_)OIr!}kX6&&SexHM~vMMssUhFoR zp`FbXm*g;ZToYOKGf!(g~ZbuKn;Ux7<#fV-V%O&w)XZQCSKfomyoQ!Gw4JYtl z#?MX){Ev_wNm5Cj_01a@HF5-vBZ~Owji=bKaxtPhjGhkh$cxK({n>jcs-D8=F=Ke; z*~gezS4eMf1kr9o5p7@z#M3I8nkJG|aXDoaQ6QwM6nUITip93BVFKYe2`x#aFGDJ6 z5)B1O=ooU1{&%!qim+@jpuknMRD=CzRlJC|GupEv*3xOeiVv z^3;kh#@E~E?@l2qe|0&#J7S2^-@ivel9_h+C^mi(LJ(!@niOn`jh0O@)-^}iIjoZ7 z7uaK;!oN5-F1t*i ze+PrXHtHJ3W9exu%Ro)Vu-g?hT|*cB^mO&(DI86{k|MvN9zCsNcjxiZtIuF%VwhS6 zr`t_Yc?}sggO&(VTwaVRIf(^_=<1Kq(iLQVOCNRhB^XJKjLYKLCC#WwjZNEnS+lj5 zJ)27D>5a2G84k7+3}wFex*roi`wbu!w~=2UP*R=4F~?rUh(Z@yGKQK_ z(bF2gf9wP9z2<0=wrawW2$_t|oP+1GW9t|Aa$KmIKz}GsZjKuxt)bXuWV=Z$njx;5 zq*M*3TOp}y3G$~cXrid6A_&s($+SG{_6A-2U9wGBX@Qv}CdImxKVbz*~ zXs4_~q<<(zB$8tKxDgnNodmFM^$>$YySmW+?Q0}kR?(~_x%iw$b`JFsi^}x01i!t; zU#z2;4(jWC?Cef~DKfUP0C%#BzJUO_1%=E%=N3YtAj_6~h?E)R>+M}EeQzNjy!sfs z&pM7}Z$HEBkG+GEJDOJ)zQi}xXx`l?Gs>1B8cm}{Vsv*0kn{`#!!aCog+y8-7LT*! z)7N-w;gei=@fE15ireMo-GzVPlTVl8bvr37EkV@{3^j?(;YQUZWZ6#pkc+a?5`zzmGE%8(vqW%D5YfenB0OvQn!>(rIz zkXz{H`L{Om#s{BMyGIk1PCF0Z_c&jF_$Q)?1c6MNlx|S$@gvG2c{yGNdwbAQ2E(Zg z*FUg`WHQZ^iQ`D8lf3uFa}I+ej<0TXfkn40&Se8e3AVfN&bIN(wQBqQgj*W+JIh{jKyOS3lzl0~A`+|$k zJ|17$SdKsHFouWwc=wfO$t|j+Fb8zYBstuV-KO9%Z45^TaTgV$C~l_BK7sbHKS6PL z=}2I6V_em`nFMm(Cv@Q9Ew2u0E~j4dUR(Qs)drpv|SuYQ?L zYw8xr7V~E}(%LsfAZ5|jVPNVp5G~3pY*ZB%fS@DGc9yR0L==9ygXU)+0(^sLNCanu zL|kJ^V=cM)C3u|@AFSI($@sn5y7V|!aN{B{NBvcL4vPh~rsgyx)Dut0TD9_KuG-2_IZia*Ax1`wrRhUMK>C-Ej zF|m-cTss#Wu{*hAoL_!)#Gf7*!NM~07QTl5N^{2r9nGFD-uQy6-r^fc~HX17aAA9c^CFynEcmD2u zW96!@?ym0X?&+R9m>B>BF^D7xf3^nvtx_qvzH*b*=-6iL@?v+umy2@t!J&b`%-SIDF*x8@YLIg1jv; zO_M?)OUVY)$|9wpGBHDWWE);%gFim<3=D>62T1Fc8NeHEhEM*)&!O5)L>!SX7CAUp z<=Dj~CTlf1oei{7{LFv%?>Tq$Tg)$a8L3U-d0kTNkh3Id6f!nmWo~wkFWvhHrJTv{ zedd299;{I)2n`z1g9;xlX;&LuKbPkHtF>TY&Hh8dmcgv zj&BaomXD0VHVrnqK4Fydf!(t_czy-KVHfCRIm7DOfbs1XSx2#O!AE%@DG_+X>nh|+ zYXH7pyrm3)5W@@f{&!8$Z#Ow`;2>$g&)l{V_SN#tOinPFwK#Zi4%5wIry=bq!V3bN ze1={(X0y}9bR-=wpyFm3#1Y*f#I;QfV^}jVQpmE<>C#$>hw8^~@yV}Lmk(V(P1Ufl zb1o&z;@J}y=>{p+O_y;*N~Tmm#|hbjgDp}-dxap<{KJpj%&pgt^NSz9l|TIJr=ANI zsyO6}|KacQvp;f}_ujsp&wc&$ zlH1Bz4u9yAY@9vL|M^E>WVDhc3{qxhYTWzS37mq#AWG2~BswLD4FVN$c+VJqDn}aS(flt&6A%)eA05I{|3cH{Q2gNsK!oQHW2ZKN*RZY(9~vUc;|r`KK!m-*!cpPY!=T8 zv0V$(u?a&D)6|TO7wNTo>>V$V&1mlZtxxjx-~2c#87d4_DpgkN1186(`Rm{M`#kyi zf5TUP`;+|XzyCM~ZnydRAO0fu{_an5_dOq`-EPwGLNJ% z-zFP|tTx-E7%cc6*{nltqRa5WgSM zY;_2u7&}$mvu6Sl3CQ7AKnuX2vG~SG!(ENHln3y9N^Xy5&-(0|b{WrSxc~SfN6sx_ z#vxPH5s-q($qJ5R(CPV%6dj}iwoA-Rr~m*U07*naRBLm*-DP*dVY3s^Z2K&)c&x3) ztgH>VxYFT=-@T7|&!<@z!}9{);{419_hG7(-TQYjU2%vKftiMkmK-vrGN&&#sn^@s zjzz)HXqC|N4PxoAzS<_#4(0Jl;-HT#jqr<~{4oE`Km0felG?U0w2=6IjG1+4HaC!# z&0UA~@q_>M@9@!k-pTnVpJvBc4jBd%^OBK*g)%Z|m0}nscf9K^+RX+(|Jz^X$KJJ{ zFZ|JOu(Z^mHaUtGCf$Kg6vbGs!(b5MXrE)J7BDjz4({E}Q%8?-Vr`YYf5nkw3#`>wx#gBydHkWTGU#;)IxW^W8WggUFa7BsQD0u>^vVMHi5vK} zfBWm)`-MNJ*^PPl_)*%em|hY?QPFP&wAOqENr;m-7%vuCxzHq02|35+sq-sbY&7U} zLY}PqB%8r7exd}@8cLsSrA6f}YXF`;vBb~)?L++TXP==^wD9AIeh_lg?kW1-P~ZQ| zYKM)shvk^WVM-Vz-1qP@gSujABca=i82A!tln{o4zH|=aQLoOfu?ow}{kL4;Po+LW zTKvYR?&M1k9pg`a?eDPB@tGVgQ*s2gy*H8#)=5kQWfj@0DOla?;*3obq>_@Y z=ro$l6cg5cgQbg0ICg8Wf8KVwJGA z-eYQV6m2_{vIcXzc9XRX2EI?by@8c2@}8S-V5&Mw!GuP0fHE})ch8c_i1X_$o_+jD z*4i;2`k_zKXm3y`8VIMt+S)RU9mRTmgJb6(=GTAiH<{mPQz^OB`vYc1%cQZUz3!87 zG8D2l)nb-L%O?s`R$5*5?5NS~dF-315riq0Em%Gi53>MhKp+g8+H8Q=-8%QwTh;&w z>G1QPxSPi2GWS2#<74mI$3m-46eg7Nd5R+ij-OxR@WCk*25ZeO4?o^UnmL5D5z<0P z_p*vUVOVITp|7Y-IIM0A(0AO5SXzIXPqMZ6P65f^e0z~U|Mj2X=&6fLW(2uXi9l%# z9ny^*3Z}*`Rp{26g5T>ik|~VvXP^HX)x1lw zRHokOkk2~!g8`Q1Lfj*haWJI8AQnV%#J_oPnJ@muq`=KXiSlZ*4K^aIJaA2~yQ zEKiw`(9BZE=lIDV|1kgepL~U{J#d0MZra0v8*bsBeCb=937^a|H$l!-DQzLm! zUaT|lBQ`r8Nvb$;(jQ8|X#fLhWl^!uz`OeDui^p1cw-8I-m(S&;EVU2WO22LP6Hl4 zvBcE2BJJLQ@qC8u+b8+JJ@038qt3lg9p~`&X`VX2fezo(XvJZgP10B#ZqnCo^)NUf`m zSXw4ZJItR;k(Pt`%m0k^-~L}^*8@V{^|>#=YRP0A75dRPomNmzx2u5h@ylrh`F%5$}=a= za?9LytW>e=d2Gf#%2tNC$r+BcI!F~G;|S?w=?w-vbM6@q?VRG<=NGYZ7I8m8T8i3q zmTnSLvMr3VLpuss+w8E>@-Q8PAH8iCt%2ggVx3T@#72q~u$r-W^mFIPmt*>!5+ori zPLbBuj3r%@2k@PAj?yMVY@G-ufRHxdezwIAy|2dUi!qyPJ&c@UwI49yx=c(~=?@~F zSzM<75_>NdCukKuXL*Ith)emAWcfkldVtzck;@k`O^Fm5%dwc6o*-2z_doC`cied!QJk`K&m7^}S)5Xt&;He8 zyyLdRByq^S-*}8uCoi&R_bfVz>3K2JGh?I!A2J1$QUrmA>aMeU`vkR#B9A=1$~T`_ z;O<*yaC2Eoc^4yUv)Jr2UM*rcc}!E{xHdoa(f9HX-ZO{l)e-Ipqxl|BpXt45tU_k^ z_VIJP|DAhCBA?zMMx`3Ukd^*`K^Wj-vbfs7u_b%QM>xG!XQoo(p|ckm$-8vgJ}aAD zVp4Wgi#S<}Fh~%F#1z9rN89~`e8$Fg45VeSeR71X<*?jtu+V6Okc3eLlLao^-vF&S zw0ng0x*AG*8(TNo_C0+vDZZ;swAJc|KmYaNK3$pxrbi3(eIFbXTbN``$?VR{1t|bj z9H8O=ga?<#RvGg!Zyra*eTvf_@2*`kc+ae$;x|{v?^^hq|Lb3I;>>wQs^gg6GJCcc z8FM;}7h-J7L=JqSwFXIhjb^inQyf7Hflfkf%jCl!`2oD<24y!UYOc~H&m;Fg#k&q~ zN16^{zr|S2=7wEkgza_WexFRnqU(iV*+^-SgaZt(OWrVecD}=o*=hdbGd~T$+|J$9 zDkYX%1I*GkTK$wXO2~=;r4o{$iz9vfdJ`2XYGV~{y6N5g&F}oo6~`;nGXM6^9^&MM zHP)9KoZncd8AMEFGl)Q8n-;!~S?zaut;zrN@1c*(2Y5}*5-z7 zGZ=b_5sNpK2+jmT%S+h-)7q3sP=ZF`i!8zGvva>mrlK(m zDPhkg7}%&)4`JAZU7M-hS=v#Pa?!{4-u%LitMS>R-{ez&>wVP9F84ljk$p4U&`CrZ zrx;o@vU3mB*(rqMU}f|4+HIO0pL`)hA!CsSJq!YbfNZIR<2n=z8LU!)?(#(*e&RIU zZjY&oL$;JB4MLo35x?KU$`>&V6CDQV>JI!A#%kl7J~L0FA9CL#=lJ~JJizFPB&1RqOAY#Wvfvj@yGClguOOH~NhX`YEmtrc2VuE5`3iAsnIsGWgHp~Q zPEu4Fp>j2p%#pQ0V^AEO#IPO4$I5J*9A~mxB5NCDZ5t5>^fs2TtJ}~?06HZ#a)e%s zyldbOI=GHQtJUPbN6!EzkFECk@GUds%QbR%STdnd%%A`j`b6y(D1+SGLHa!pFIN27 zmruOxxOFt(;>9k9=F0R@M5$&Z=g=DrIJI7<7eu()z;rF1cw_^qikGrLF2ZoxT&T!=H3Iji4Hdo*SAFQzFor;5Z6}bKG5^#_8Zo&{hK|>#=S7&6aj;#&M4_`ev|A#Z zGZ}O}`hJR``y4-Yjz%}6+x0;T9NXrf{r$rT(?vHrM25kMXO5u|2%VtX4K~+WBt{Nn zbPj9V6iFD;>D2kLPyOE2hK)3b9dzgGJq&45a!h=sn68eHa~ujK7c*z@_50gbY`BqT zk@eF)@4x;KXWLC?OJ$slL8?;@Rj0URcAAYfZ}{M_%c-Nod*u1|$@3YR_OAH>03>S z5JRIO3=6M$$?e+$!5)?N*u5iIDKzEq*zYO!$P0zdkX ze~bU;b5HW;UpacUAwP$ZF7;N6-LoU8L~&?#8-AQJJzk|3cKPQ0-pjh?dPI}b@_*{9#~8Jlx)wL!ZbAzeX!B%)Z=vnnMe1jO1e&1tiG>a((hYwBi z&8Nk5{Fj>tJC&N>WPaZ$UfB)~k^xTU6y`tlY*a&&go>@-STlm3%KmN)I z7Ur)Wqtc$o+WLT}8a<-0&#r12l`33Qv)S$Q=-E|NMo}J-bT;B)g#QYYAK(KwO>%mE z>w5v;7Y5*k2KMIa7Hw#lhEzfkm-xlPFsx^PJV!HG2lA4$a=HG zv*%V=Yjj9-%EgsUnyoJ9*BVqD3$Gs`ZHF)s7(!4fjkDS6a_Zs+S<5EWlJ)gK{)Z+=#eCdGBP)!G2Q-O1X?}NR&>{Lh#_J)7-LS zhB(y(gAm)$G#gDa`2qt!W};f6SjdsJG|lxEOMZg14BGW3&zx^k%vqE&7D^lB@>zlq z{@|~lVi-hwu?+U7fBqIe{U0BD@c^#@6?gdX2X~USEFL?*z~WgSDPCS^V`RpoxtP23 z{4R`X-m`z4ZyoXY;EiS0LZ9_+kGyFyF;?QCZ?#`mAK|_4tMK)&t^y&b5^s>i-}lV- zr3dgrgyFKes4?vUag;LXzVvD_ad5!El>DRr>JaUGgXXfhB%awt3m>gwv{JlLqK+?Q zb-m8*ckHFT*(IB`@ZuCxn&h$$DJk`qPq*8r-WqW1>@p+8JWHEBOeq;eDa-W^BUy(q zi3#JB2aX-X5+=JQxAEA~3!Iy;v)=SLF~5m72w7_d>>Md#=dvuX)aeI`;|uF}UPu^3 zC?uO+Kt5|@V$zEf?z;UQ)YsNIa(3gnfSl4j{@{Q9aXxh0G+%h&0^fLIsoXM0Wt+UW76{@60O-kF-9lwaZO3lb*OiI8eNY#R&;`pqU$i4 z&k)55(=kbPO2IW5cmtY)gxwP^%WE-p8Dpg}87E7@wYYBgPGk~L8672~Vua&h#}QH( z$WoD?`WJtBwP9Wr75BN}juN$ehQ*Zzr%zvdicZCSrf-rgt&hRV1_%kRX3uRI&OGcx z1k$|nc@Wi5V@2|E1{ zFOHFxBn%VAN*R1VCD1X6N~!w;x_(GE3UFPA&3>Pp<1kq%u-t63(ryuXDX~siZucnW z^Q?4RRNV|~y*^95ESoQu_5b-Xm0rg^({_y8pHTxB{IugUw}) zP!2+77@1BeXcI3;(I_&x43$il2{%v4b!m73$C_(Iy#(747^Z>aY6krX9VKWTqgBKk zd6 zjY`d{PU`RQ{Ku(SB8(S1`)_XnE}`N8XPxS!4`nT1~2s zOFQ*242$NPk5g2bjsvk^ER$iS+s80q{X&e?uPnXwY7nkRDG!C3MLug17zVxdE`>sg zb7xYd|J|GO9kYXGPX^)x<+a5ZhrZH`})g_;@VKL z_o@_kCJu(I9dl4|AEcr*D=8IS7M?XhajC^uffx$SsyISgHY#Z|xi^F9N*13E0fn@N z!A+GUpe>}e^`_d}9>67DcrwoOsZJRvose}hxLH9OD7wB!Lxq&g3@e>3LYTxcs5IfO z{X1FMXcGkqfgVbZrwJ6s4EmkmjV~EZY=VJJchjK0B4{tcpzY9J*NEiul_@o*`Sy`> zc)gI?_6%W?VA}?p9gmqxnR?$tNlhVRuvG6;tL7MlG13skNkTtPC>Kq7UORkQ_+AW{lxqwF z!%gFou#W^%AV#a9Hsa1(G^V4_5>}4~NUMl2zB?8nC0eChqEPIGylnukf(A^dhn3`r zjgW2-5u}P(3A}zl&yNrYqy<6)LNHz`veIf}n8WC^jv-Yk(hyiVNt`uNE&W`Z^;Tlg z>7b(op)BmYPkS|B&`mHc!61o=MS|;^gpnpi;RP{DE4tl~AWCs^l8sKl%tVP;ChW>r zx%1#I{*Nzxi}%gWvE3D9&6HkbaN@!e!qxo8KYI4L)$-M%Rr0C?HME4V=TNC7Oz+9D zu&kJywwXO}2P@~7K?smycw>ylOzskxTA*Y?e#EA|ntM^Tp-MnnTW|8i+XmpO9Dc7$ z(2tpz%hPWANFgvBgE&kPhQQ96s5C|S0xcDBpjhkmupCJm3rx#KX$4A>M2d>*@?*Ep z@Wiu?m-RZf0^9dYf$+$bOblC)hA~=8w5J%v2|CuKiNdfX(hwN7AZJ(vX+qr_FzV)z zwn=@njxq(skvua~Gc*%LF9~^ibB%`{2rgSKUoEd@@&gDVQSku33z?dxRxqJD%2F~Q zxYz+3x|zSjG+7^nFFE-$S4gT0)%S`w>WkG)&P73|pcCg){|1NVEng zV<6CAOGr`>2HH~W%sCtrlC(ZRr_rmvXkR7vC`e8(v#(>7jTEodN z1a8(GN^%f1iTN0h7LqiaIuWFm} zO1aW~wl)2dH;AXtq%SLM@}Zq3dLS_kjfIU$AWbyd6zrNP4Fy6BNK=hMA)VnWsAY_u zk)*N0umqNrU`z4^i|Y6+Up&3WyRWmDc_-|@y@*P-j(CKA-p_7hfMG~tKjpR?wz0C_ z1D#+>3@}7c+M_y(Bo( zf8MKJipozGC{A^#?TAo`kBU8}Z+P)NpwidByY&0o001xB9hS>wX}1O_6mgW`I3|W+ zkVJ~5Zh)IH$mSf1WrtkW9JlX;%I|o$2S=?6T+m= zJMVJ1=bjRGzb}U}-&}7%Jnv_}p;1y1#ujTmpBwj0;<_$lV>yykku6%d84JTSu@PkQ zd4we?Ym*Dj7Wdq}iyLMtjAaTO+%`ci6l{bc&pvi}d-YaD1D&ozx$H8MNg2H{f~W&~ z=1^UO{bLhku_;d4-1BQX&q@)N8OW-&eimoP%4Wy7{ zU6(LU7_^650K=3xu1ljAaMS)NRyTa4qX`BP121O#_y`|3FiSD-kOq=iD<(@7s<_;8 zW0@lZ1JQZ&Cy9_5I`uxc?-}Rh`Bid-42`x2NKDHh7$lgcAc;~m0+~-Rvj(kpNMfhd zN=0TWldQK|h{1qoqc&$QuF$wfAr%#eXsxeW#`}6903B##F2t7+H8sLUn1Kxs(ujqn z4vh^#drh->AqK6HR-VX65ov+j?y*>&|86c`7&hL<(DnD47VYI8IErjl&{-M~bz{Of zp**dbzRn~!9@AaFJU-&Br3i6t*Y8?*+W@=_Kl1+FC}q%zJTfK8pc7%47TCeC)?a86 ztq=wXm4Y%b3=Ieh`3(JjM0+qm3Plo0?2JtsYIaUkSPuKFwFlHYJ@UC6duo$RlpR{_ zm^&vd?mV=OM@}`~bZIGpOJ-+Eq3W>N7?80gsZ>mia{Yr>9V?NmUq zY$7Ej1lW$CXxW@^w>kd!ivv+Fg)mIC5YK5Zycr0?z;D}l4V!Lllw`R9k;hO!UV>1N zs^|7jTKVWCMF^M0Gu@X=OWyLSkFxssMYQ(unAvPtBodGK%^ivE|l6P&wI3eRW1fF7|kV7cR{;_ewG~{@DjqA$O zoZMXH(TCrBPe9ooH{4dBzBa&d4dO@>m@&DGg)2-3BEr-L%F?KwWW;gFj2Kj90Rxk6 zugjU$7Iz)mO*2Zl_siGb=%eEP3*6{850%DD?+Hl~jh(TW9;@QmCXX~N{Kw`Q?x+)_ zRYYlxyo%vcghK4xj(qPph`K31e-m-?%n_A0>GF)j(llSDTO&-`%tX`6LnnPaan&OAF_BX%wRN8V;LOHs1HqLc9<4aeA3P zlHeL-i%=8m*vbSDvbBd_w(XCcDr%+t@a(}qDXdP5HvzFadGem&|9-@gOZgwhS=HZr zW;K2PA`;BSq&W*-)2459pFAvi`M3FyAtl{yflkH}TGn|$jq~Hz*x7}fnhQn-0)58y z`9q(m8^;x;GKD4u9HP_uY?E7Tj3V+9J|S#en-#pD)hOYPfar+uWKVasY%b5&Ko4D5 zr%C+X8Tl~(2Be(5@g{iT`+*1N*JXJ1x_Nrc;#Scr?5bV1XxQe&m=j&Xc5ymP9J6&u z0t2ZHnkNmr^e0mzGvu%g&!_;LG$!{v+-9OpN%f1{34o9EBp!lGf6#l)VNJj=Z~VXf zN>B`D3SlL;RBTMS^^qKIxvmL?EDjsZ>5d+))#@ry8eT(0HJ)L9GMS1^{d-o^Int20 zY*gTqxtjrX>&*3&$-o^T3@Gui!$VAu zzm_~I?kSVT$EM;w@_V>up)&+V9pGp5R$nQ-3eanwDyXr{Xwb6r{!8%G3!l)7?8*G)m;(3_eqP z`||yZ(n;f~%e62mC=6Dpr=&%aHClmFpO$PJf^1J$sOm22?`6p2Rb7I9h$q14UB4$O zUZs#{Lo1-eP9tK3lB1I(EM}i1SX?5^^^9I~q9@!8fxdmMcs__Sdd!r)nyf#uJBRLu zp>93&*Qxq-pDs5`a{J#7{7cT zgo(vSXt~}g23r*!w|J*48)odQC0AU$Jv|Z(A(s({xUB0XbI<#6ZTR0wZx)ggT<)h8 z#su4+83eTUuel0CznS4_kTQ6Kl)6^?x*#pt_{;s6P0e`6JrUId)g|H?lal7WiC|3i z8*yK${e*&{l7|CX)DCa=L1bR`SG-?7mLwXQnSS@D-xYe788pac0lg^}ZJ? z@&mFo&ioB~voE=9isPL+vvn-4OSkAuieIE7r8tucdxN97^K7DWs$D!25;QmrjyPy@ zdxYIGt{D5|fMETez{5TR@d~{udc#k67+o+axkit}N)*M{^t{?V{iohssV{|Tj5paWQvU8h2bXB_jY z<>DeTvz z2{9QpSv`sDOQ^?jkvp~k5@JKAf8d&M6@jBf@K|IA@#cbh#iZ%sK;JY<#A>5WqzJv1h;cj>`u%Elg^1%ipO|1K7PgReVwQ->(yV>I>^XWc_%C4!n-qo?19ixqc9S4O>P*?n2eb zKdHm1EYm-XIrRAbbzqk9y4pN1hq6i0F*|!83GExdae3z9ct?){ z!p%S6OM)l`WHky>7ZS^SR@7DsGyLyzx&HLRaceYHOhhFW^S0ZDAwuAEyMIhC0X5nL z=Zykse$K%a;u{z;3jwhp3C60c5;fziTNr%%C!Nj%yOHY#bF8Jc=}Fi zc%$#gIy}VOe^YenOMc5&o91_-anaUk_JCwZR>T(Sxa;N`m|+hbRiI`f`1*u>Csu@0 z@VTo7&r?b3Dl|PeM}h#Z3bZlZ;ApxQT1;Y^liNZUP~PSt2o>z~*=Yah%tD!|!Ob!t zp>q=8=N0c}Vv=*#M!e!0soa54`RDHwZKd?j)^%7IaeM)#^WX1KMQ<%>tnzOtHZ6`$ zu0Pa|>~08hf}ST2G>=u$Pdaz*>^h_ZnAKbeWX1aX-ZNAhKP9m%ES}v{Mmy62BKyA~ zANa&c%1Xn3yI*c>8(d&hXIeV;$^YFb3S_=65xBqHUBGi+l&Qkkl{54v4VN$K8}IeL z7WufClaY0^hT_`qQle-$_!G7FN2@7@tzM|V2~Xz-sMEqTzh}dW&-GO&{ct|nK+%DU z`x~mxY%R!zG|uaSu!>bZ6)@<@Wl~J!EnI?~jh>xo+(ORAt$TQzb7|}J7Fo9Vnf#Xz zYle7YwSGu!-QPQ*`nJrHc2g2qC2%!2?W5>pI55!p1Ig>0_#pEwZC;<+j+jPHz9N}c4#&T4 z!bYisfNDQnaa4*AU%5)9;+r(RR1b(Qk9Vy%Ak@5N?2lvw$$gghqasumKdKQxCW_;B zr-$CFcgnr|p?}H(iI(kDCkPgjF*0{(n4q%RlGF*y>vA;64k4#xg3jk$hO9?h zeqPSTdI=-G7#lnb#JYKucs4D5{-jtNt?uKS_j0j#Ve%|8RU*cwrGr)861_ex@a zsrsHj0t!y7=&XGy!34ZSd4wa$U;Mmg7A|;KdDAFk#5uO01>&Tn<9K2m)ZIzXyKg2_ zJ~l-3hZYfRL`rUn$3uE_UN8EbN!Ak3!6%=UB+8b;4Qpe-^FM^CMOf5m};7t-zvH6gv=Ry zK7j?6W7HS?<(ZYX>7Ck6F1IDn4?!kx%`mBT;i42xqS@0K*QfLrm~}kTq^Ss$EB=|T zEDDAwwXP9|zIlN8_(Rv*vg9kqeB>uZ)K~7C}mp|GggiHOk!56 zocon1TK$$o)acZbr7mQsA`1W$BsuZr6Vv!C%(rAtifoa8f zcD};Bi*4?aLZhdc5}Gx;VTkL4m+G0P?HCy!>+?8nW3QDGn=VGoi3HnPLBwpaCZuQ5 zv;uao88~(%h}r$~itr#55ZU+o3O_57NtLjfx$)44S)u;BM5S+`LU(cx4r`=afyEAI z7+FDK%Kv1ViU7z$&a8sTtmb@R|{9)`xn0~=Wy zDgkfwYqFIxOXIIrWk&RBFFkFeXVM=aWyM482DsP2y@RNZChsel99{GX)1QD*-ztgG zroXqaq)SwP(HkRT6wz|UiqvOI^5^zO*saJDpk6HIvNq(c@ottbwk8He)pCi3et_7e zr5Psoe4y$AW%=?3ybx+@JuhFOsI9KYf`mVs1IT3(>l!bGAe3fhp8sfyIZO!7zTvQX z^-G*VQmu^%@QZ2)Vs&K=Z3(5zVzyX|+!*SrfQE$ZbnjE^Z9 zz#QR{TqzI?Pi99?A^eDAsNn(fTpDtynkP5bs0*`^p~V7^RERq}$C zDwgcw#@~>*DadzauH|5>N~@6Kf$E>1ufwHjn9zRDDzx-(S+-<2Sy}are=6KnJ8%Ci zZ{D-*&dMn=7TE7iPy}z$^DnH!@x34~Z!N~ucDi?EJ{uhF|O4wC7Kcno69OhS0sNvm~JnS(S zFYXscv091I(!$rLafiwn?eldd8l2bxo}s{RVL2m2zx52T!ANDv&r+vLMSx(`;enwO z(hdFrx{KdQjFuf{ZZam!po{I0!qB>bXrU~7x_ZV~?PeRaYWF`t7tq-6Bp=EsR#e4- zhSxi*zmOv419#zPOB#|$?cBI!TqbsZkO~g8gEBfJj^o;aA8BM9)(a~AKs^fFp4`$b zb9bKhY(4vCVJCurcLS3W-}mLmw*uq7PfrRi}IfPX#wMVO*fHYp{3Ny;||xue#>jVS|$;gvt$ zAjf0&!|&d|0KKxRe~!&Lz#KF4ki6ookl!xJ8@>gB&FFiauD7v=cZTe}dC^<_nbDG%B;uv zHj<%?%j%_GD%!UDV(0vjcl*Of_%;V;)Axbl{cN6}e1iWpFRx@nsrc`L!H%g*uhco3 z_`vm1vcZiK{?W!Xuid84A1>;Yrrztni!ydId}JdUG7-Mmdow$4Oq%V_`hHIqn{6L# ze)v{M>BRznOE?uw&}<4@)Ch@PqLV`-qm7Hj1i7aS@XGC6FPBj!fXU+g2)~VaP}bJ} zHgWuO1r-n^6$)2SkkM(qoNC*m>Om0@ZcE#b{yM*g!6fB(%Et z(&Z5A#u%OeINC0Dqpxv#c1wX%Jd1^w53vyO0A^R$&y(5i;eSM3d%NzS~PFAV)Y zgB<22TElLISNf(l$tGcKbxOFIZL`r7xTiKM3sV~C4vyE=b9LAKJ!E|>!%M@W;qkvN zc2SH|Tu~_PjDSKHg=m-@<{m>XCtx5Zhx9lUCCq0VWOqtD@o+%F)p|o`=u?GFtZspM zGDZ5I_LoHGwM8oOw^L}AExtL(ZqtNh0+!DLb&%4VpOncyQN>+mausnT#wU06A*$J_SFYMO!!}Bt!OUIq4N6A{mRoOyC0fwWS*z@=5sV z2}i$+kw!K3unlXJ@cnK(Y}t;xUOV^jChGGBO24?&GFZJ`*$Qc|uPct4HW0o9#5Zyd zL{s35({F1fE5O_I{l|9pW7ZAd3ln7(s~tI6KWK!LQcJclU+5i2(1#+7kl~;ItPvmPob}5_vo3C7)2?!#O1#_^B(9mK?>J$bC`c?VH zp)LEj3&}xUZ!7cd=vG~38`*~ zfwaZAKgKzszm-jdy`oT1c-{h#AS>dv{UDOdz3?V+qk@x!; zUsUkzJVahpxm_;VJ5-{1jAjUAj@Ry;1^JR_gROj5p`Hjm2MTraiO%q=6ZnKJo;*p{&V}5cl_)*8u}-CKb1g zjC@hiUusVjDDcKx1f&!oY$Aym=zKZwZxRHZPPYn?Z@CSp zG3+Y1O4!Pz{bdowBcSeA+Ua3-0Z(SOW^{+( zK$zKdJSgN*<1j3q>88CAy)K8)sHpi&g5KN1(;X2LD2xbLQVd55s#BeN{d{YvB=pR1 zOB{TBCf442-B>|zgK!{J?R88#9)$#3rxzT3xO23rx{eQ(>KUJn!@sHnVP?ZXSC;ZD`pQ59YJ zd?J&+K|l7d2OW#1BWqeVxqx69WEYMhC4?gP1Kkl7@dNVQs;RO2mA8)Kh=U00h zV*lhs`f(D9w;~%O%u}(U4xh~ibi6+D0g%{eT)9yq3QdD-}eW<#gr0DmkhEY8oh0Eu%o<(dkg5#H{|h#ah<=hd-aH>%L< zPh6r>-r}~wn0uuAf@?qQWJvp7c7EM9gY!@3k-re0kQ0Q)!2W^)9Iv)8hHIyK@Blh> zB-Y15&d1Q3QQvIovUfMLA#^J|gLoV&rZtTy9Y>IxT~3f38s7`}Dz_dyageuLofw1k zh~!OkWgd!Lhz|KW@|Q=H(P8L(I>{5c#?CP_Z5r25#>0@e(gS1~%I`w80p{K@Q^G zy|4A76xjx{KH(r<0w{^{p^AZ7_qx6rx%tDLJ6aRYavLjsMJ zKsYBJ*ZLtCt-@^50V`%0F%b7(@z6`0d`gW&w|KxU7mq3))t zi0CqmH9n{4C+9TtLlJS4-EoW+84iLo)}>M4vDMS`ds?j*j^|7`I36D-pX{R3Lqx|U z+ji95FW`59Sw7&YzjNDD`E@oZhycn6;F+GrI0ku)*}R=6P_UKr@g)u9RRu~cNw8?k zqo~*&pD4@!_fpr=VEBzD45xdqZ4Q37@Vi8LKc7t3eZ>uDWT!<5-nMS*KvEob~B?vkPuyt_WX<`_1~T&ik@N z-o}z{6dFFf;$0;)0fVBY;bxBKF%L&79{Gj>qd`6oh>t9BH>YSg%rHZj1c%59s4L;O^SSIN;#D5YE%Td#BY@9D6VI!*x}CZQSfec z(V9$rD@%$ap@8(-PfQ7_YzA4R7lbT%s1t1?+A`{ZTr4$B@`C!4X*a_8w~TAMvgKq*x7UvyK(uf5n#SX&5L|=MBb9nV=8Ta3B=Nv$t+MCe~+c9?o zlUiO+TGEhI+)PUm4joi!O^2N-!nvHd|e7`NMM=(*u zNS{v}M<7FffHJ#UAs^?@D7LH^BmOO?L>rt^F@+`!1H8KYwwqs)mT*~Odg#^$3*SkA zA<};XNiwD0IDU`WDUi>ELe6z2Mk7hcD&V*A#{we2<9U}TNDnf^V=~ILu}o`+2qnt! zn>QDhCB&G&mFG|*nZeK^!NjCu!@9Wew5aYHvF6AyOFtWy)Cc9jgFMjzPYrCiaZiYT9_ zaFM07k&XpmGV#!+ggP+G(a_4P*Kbh>3Jg~vg}^Zs7zwNs`o@T%#I&gr$}lI}Co+$z55L z`O#!ccy^*3&tYhKhGZC52h~B!3Me}3Q7mF93L=?vyZUallh^f6yoT4iEf*eH$5olt zx&6rBiFxT6Vg`c~N+^@xZ%B)2wlNEeKSx*}%->TA2qH%%8`te1d1epjOC;l?+rdY8 zmLGchhrlq1*+s$nR$K7nYoExvDj2E<(u(ai0;|Z?#dWpQ!zBd}swvY$%P&DflPD~C z8OtZ10{1{7!Lypjj%0h{{}Bvqmrj?llfD~i$Mp^pN;s#NHF7f#hm3~ay|0G9F62FD zM(rz7*K*<%mOiuIc8x*0etxL8z0)VXDClBLN7y5}j1IjS(xN`4x^$|gLS zAQFJdMJ=cMsH6{HoG`K~pPd$Xh8MB}g%IM5{*YXN_`(M#j|m^>^(g)lU|J3pA{u>t zHY*S+c0Ls`sv9a=%3O@QOCRq=<8$nt9xP3xs*V|@8V6IJZcpo3yg&K>T!2JJN1GLx z*wWozzl+1iD?dJ$XEqt-0wZ|aQs{g{pz=M?u2-GrCX@PY6czQV00vSb)EY)-x9wvivfsDrL)jNpfm- zniCw=Ws1Fn!_Pxe6vL!X^k4!xCds);sB6dL8FLW3^c!s%TsAXO$A@|TansMIO~yB4 z5=TmgDf?8CiuAgZ?UH+r|6Fp?LIZ(L+)+UDa?0( zlg&>SqVhyhKK-_+I9zQ?XW-LZBRVY^5IMih9Bn(4?43>%hVtsPnCVRq91R=OhJGU0 zCWAzpsES;~A{)hyPYZ|NS5ck{<52g(#6V<`t4oLtzuhAXJv5DlP#jLNl<7+|jygm! zqm%!$Wr2*sL)_02x)G_m(MP;zp?q!o&RJp?O_2~nu-)8n!1-FQup`%*N5c~uwRH_; z3440=agFHw1qWdM6`cQCmw(GFUtdUCgaP2W4^`(Ip4LPjKATl|Zgiu>*g-a9GZMNc zdqa2eLh?RVC8OU$I6qTNBv%-8=LxYY6A{WI4r8A%6(dtG>-D0T+GtMRLfb+pQE0>{ zkjk>p*$%&hBDkmr+?7hqC!m-JQIYl{cvDlp-<7}LB}3bOoWk2Jhl_fE5{LP9_0>LX zI>kJZt7AJl!s~O{ri^z5oVc${>Ew+j77rSpcqmgr7!LwiR9+1w3A*GO)X6_@zOI&3 zTx6Sm3zl^W%Isz<7qx5KcYI@#PxnlyGVA^;}MBYGpWV(+K^x32O}y_ z6$9?gL|&wW%MRL3B6={G@rb5ulf!^O8_aO_(0#P>Iw0# z-Cu^{@ujZJ_27d37*oiFG9TsGQO8}s$~1bB&~2L2E6{+m2;7+ps3)P1!NE48MQ5{^ zWtOy1O{I@@p3T4uBP};=cUAQ#qlv(C*SJ8F?$o+=N$h5m zr5uf9g|lEmmvd|c$~eY^ag-u1n#4<>#HD7+`e<^{lQy{$TFI|pM8l|kK6!}46D=W= z)+<5*6uHXkXyS)Uk_3IiM}nQuAF{(2NAgsJUlxzSR>h!Q@)%JUC^bl;zb3U~UaHGw zWtdeosXJWRx!-y{dP%woU{x?V4eiBM@k3NCXMFAOdgFi)ImGjqusoY+;a8DRk<{B-p6r9<2!wKcu=S7O%J42zOOF6XM`Xosbv_k4v}K##AR9O^^`** zUGS+v^zoO-icDVr3OFxjx>Lt;VXI6m7hE-5__N>peSvdr;e%{K+)^$0YDp3-MLg3g z+|IL2Y~QKrPj2r-Q~m@NcND$ZhC?~L70)G9O`gg6PF|4eKtPHHV3g@5{X%{+zKCYV ztVWvl2Oel09;}P&@(_DycMtqA2HntAB3InK;EApE^(7razrNxtjkrANV9l+nx>p=z z?_F)OI21Zv@M2?u+cM0BV(s0mnryZceR;6-E;rH%oX+{h9AO-1q%Ts+b-GbH6Wup# zvmWa7bm9nSxMn69lmWJmTMdPuJuL%IH(xp}l5N@%7}|6{)p2+DKT5RPLW?~8vuf#X zmco^K9q%w28MI*CdtB(6a2H_%t)YCCHZ{ry(tEwKyUa*BPCs;2<98Z!f8S=J-bxPRb%p)oVWE*F% zdi_w!as9_YEY8**(M^WtID^eOfO7ZlLz2qB@n|=7!J^NJ09tui_igb5K(WB0uqIzz zdMFzj77XzWf{!7aJcq4|V$F&q$(Tb_l%SJJTGu&W|IQX?IsKIN%^4B{RtW1x8ISsvZ5zbc0Gmx+{=Q98B{drW97( zocCwF0`yrvPZniP5s^8kz6?n{TXrm5oUcv6%uiUDW+gSZe-BRF0bDQD@8BT?07+C5Gt88|(%pM00SaX>INUsMC zGB4hLTU|MTKobc=+C!7i_$9YtoQW{yn434%7-emHNy6T-3YOGx!lu24b$zk))nJvL z1EF%_=W4AP@k7GHcJs=_?sqkJ6|LfqP^SCQ8!|C}*D*^ANlpbj)`G8IbYA!N#W9K`totx#H3#gvDWsNU=qTZ z$i;<9*D0qSDRz)lJX+XDrfQrRKwK=D4z4b+Z9Gc09sS~9bH<>lM)9ZGdpFAyXmQzc z_ljRu$N?=qN$s;<wgl>Gam#svg81vr2`kEq&4IC<=^5P*ui z^ALVNe(QcC2+H?*Jt~4osoUeF)78!?8A^W|+hNL~y=3L8q2*=VqSc+fHden0zWRUr zW@ZQU>)QMCS$C!Mr6QBEZc5bX^sQtXj7PqD zrEw(@hwP`b-=&<*)^^p1!tx{<6yIglG+|OVu@OwCzx~uF_fI634QWE46x3?8INxRX zO^yD2P3Uh8QLoMfB}CVemsCLNX5@p&oY5Hqh}32Oe#!KN=!9B$G)^U5QA_x0>|M;yEg55*>?A9@wAmtLz>6%C1OwK&&z6olMmym93z1zr)n<9uAb1mz3)-9!4|yz9bpr#Hatw?({( zn_F5%1ua4tH0nm-!2J<_lokhn0lI>M0;G=z*(B`})P;FJNsuc+)@s^x6INEfM-EYf zgim5Tpn_h!f+pe|{aMuIP)L$(rUo*gL1)&Ok%n&^z^73hho^A@;IF4q10+iAja5@e z&s;9naa8{T9TJ#sQ##2tv;#CG6{+t!UA#aU)oK`;!d__?#Y{Vts`1t$9p34w7?#PU z+ty1QTewH-zy8dmzZtxS z@v;s9u1pIWIdRf4MHLkY5+oN1c`FIIUXVaYTIxlSm)~M>Y7?xCF?ZJxQJ6|kloze=qnn5i)WrW1qBw6cdy$E z){>-S!!C-smBTL6R|mouwkk?jV?mFO+mDsxa1_y5pYMBFzO(}|_VN!Gr<`^J$X{g~ zf4K_a%QYm|-ZE#kGjD)%Z&ULH(DRQfKxf0yQiWv+BRUta>FU;I)gxFonSK$QPxyLP zXGE>vWkVPXDp6n-nZ`O!Gu0N5ZQG*X*{bjnt?ASVT^%Nvx%QE2+=5+NTg9mqQa)+% zfB`$VW$9FNZnka2Ho1%TJaR3YyxWq2nmerTH_;t|H)x&rMF-Sf3>pM+nAm1q@-ZAj z*oB&U3Kafp3%YSQFcBB1$?7Hv2$ZT{X=XUe)q95)d@GHwM$eewLI$;!R=ECD-F`zK zk&umH;Szu1IT{RgjZ82Z(CAcPc#pxj7+BW(MHlRRdH+}SAGM7>l zu-RdQXOZhr#^~c~;0No4?=O=ZTJJ$pQKu6m6T<7EhMw4+kEjJe$NN*!J7wU#$j8?+ z&pb1kBo1DYx!cyA;1&+u3S4;x9v&WTZSCFp5(#~u!;H;Y0yKNpeYSN0j%@H1-g$im`%k_18cV1>_U!&<;D}Q<@Ff3Sf6ilFgD{cgm$QRT6rbh}j_D=*P9@7!s z(HpZ7V%G(Nk7g@9aPCwirP$!P`{>Kc1jh9!ZZ^z2VXKkqlV97@KC|kh| znVhz|yf|e)IejdEOVamu2f{WzH|W&l7mnSn+q!=cjIrO`+?;m3uT9U)G(oBkUGWXE z(a2$!(i3xr#Oiz#S@|Fr@=b?eW_da!mJ=Z=xn=D(OkW=V$jWC+!L0XWCx}eW>W)<9 zV1M^JYyMDduypw?d%yNV}af^;hhH*tcrO>*J2?m^W! zKUg)y$x=&xH~ezTD=_^)asnZrV6psmX!QH+(Bmkv{7@TG`E|g*?MI=4QJqHkS;9fK zVVx7Y{%FLdkbn75VQTU}Xx{ds+YFWK@J*Pk9oW{{$?OU{y$DQjOVTAtPQWN|s@hsS zciQ{1ruj}=E5Ik;9xZS;jt4r!cb(pwS+U)`WK8UeP!2M0Rqa~RtQK_2HyU!Y?%=;k zKQ<%2`UKxHAX-4md%;+P$>b@7NUB$;(r>pyky!r`o2kk_t%Pw*L5k*GyjZn3Y_z*P z`}i|j)%yh2yb7J86?ge$|9mQ0TH9ah2#N*w?T&OmB<;)$?xP!e+|<}~YyiOYlh?X! zezsLbGnIHvBqn7t*CO&uY+#b=_FX55opd;sl7q zJq&QS?%0s6CQOKgOsJ*wrY*dK$T9TNOlRJw8r*XTVA6saYFP$AXGRm3!4UEUy`0So z)pJf67NMBK8T+5>N(Fh(8;3M$ z(8q>S#Q#|LuVH8h3eh*~*db?_Zqn%Sq1(OLnMAww(gHJxB?-N&@MN%pyqpFTx^TH| zet`U=jDO(D%K^J!bKA1pQq1d*@LmR|ZVAm|#CO7yvu3+o?Zq`KL+D*BVsF$`sJpIfI;y(2A!?q>&l+fD1Pz*#VLQ}UuI zeq+Rv%ca39Gs0c?RROtq;MduUS9~YmgFL==;e9=$?t(CqBxP1VeL~1WJ!YOAMq`t& zv0_t2I)q`S<@FtM(^q)Ekpx9&1>Pw^@Q$4n{i$r{#R-CFfv+Nc2?FPjXdMC1$`CU0 zaxaue(Dzr$x(<>kDY9F3g6 zmVQ?PkeC4;^2^%#XP|M^jl2VH%kSq$ZrHjya#}Q4%+#sObpQ>W0DM#yNRa#7oek)E zLS{rU$63TV#%`0E9-5WJr2bPK;}TXIDuU;cBim{8n2ygUH-VbCqC`vit;D{V!_e+_ z^~#$P%{g5SXOQC3f)DKWCO=V6>Ie}rHi^lmo6*$4-xPR!k>_@p*Vr|NbR{yM%$5e( zCb^bo_}2;7OQdDxGD!wSu_I!onak(#9pcF!4%4EaZAQD$QjxMi~EN*Ol?x+!gf|7DErw;Upe5uRyG)OTjxixbeUKo znyUId_2$%5{9`9${pLY?ZYE}kyr$S&6 ze_c;9jt$N_1I!&A6F~tE!+Gx)7}uv(B2ipQV^4RJu}0Qqnl)-QE84ocgM>o`UfUCQ zAR`R6*`Xya3r|l^!Qiixt!-@(SxO_9U0C=fmWpSRrT4$P0e>4bx*M^p8$%OifP8E? zx1&Njc0g=b2u4GIg?N8N77Vww=9f}gUX ziOnXZDV)Wz=|||5Y`9HpU3!_mz}?BF*iEr9BjFJ`L?0lX_;U|+s?SUAPkzWBM~K=5 z^cEgTF<8pyFKM5Er62un3ql~EbMooO3RRJ~TuhUNLL>dfLYw?cEVA zR+qz-Ir5|o+k5bu^8!)7!-M;%dp$&fH#ZLsgdwiASYh-SVo<`#Bz?B9wT*o_?@HQv ziDH=xIM<>KcX~T@e|^dFG!*aK0WT`BR)9_xjvw{!f1lKo$P@VM_m*T5-)%y!T$xPPew zS_9QnK~hXUnN$UoO6uv5%$oZ7;{-xl>T|-=U5^HT=q|6KwP{Vh!b!zXlg~vd90m$TxI*2GK zd>g2i%2f5)?`kotoJoyOXG~Y2WzD=yz`XK1iBXvwdbrkb0N34|rgj|9Iwvt@(> z!`a*i!a>aH`n2+;WRLCUfJ=G~ucbsGx1$XXd~5KNvZzQ6&vA>F-HYecX53D_#Qws% zzC)-(^DWV(#go;HzUOwA&^nHyU#|!4$a31l7aN3jYwvOOzFjmCmLZ2?I$M-?(7JgQ zj>2jSu!za;^+`~^nv%7fW3OwImtau@;7RZwXi$U8dL-g6#`r!2^Y7wv`MdE`{$%>r zV$OzMx*L&5u(nNi*x`;+U;(uFHkxtugu4Vm(~2EWuM_c%U|Nkj1i4hq$UUMLHBVj) zP9ZNot06N>C@D2I0p7W}w|nrh{pSJO8U3!=1YY4J-?D+}O&AS}z~=7mX^001(cpl4 zp}%-54c5Qh5~*bp*+fKCax2Ys5FY8bthpf6FWf|ybj<(#ridC%Lbnb`6#1jAs6>nV z8Nx+UX-ZaXU5+cVn*APFDyym(VnZD}P}w~B+qr(Sp^f!G;e|ns;&P)&)V7%oG|P*W zL#Z7(Yd&5lp};`a?8cra1A$uPTg|!{BaL49ObP)xj8~lf#CQBAgj5wpU%MdI*zr=; z+wyWVscy_q!w|1tm|Z693nbe%{?Ts;XBU*9K-U=Nl^V9owq|I;q;WPo%v5oJ8jvjwS=AFs$ zRWHmPff5f};rr+G-6A)>99cOrTm-bGP@0L$8>pWc*ErFOgH?)WxhgBQS7Z^rtGlC$ znn-&^42Dyy9h{>S3%YI(v1Eu0!5?RpSRzjbN&49S)9A&ZldJ2jAR)TUq{S1Azjwdw z?d=1-riROvhvCVHCneTn2+o=F{{o3E=`v{V zvKrTyucjbd?)u*6T$iGLhzuNie&z?RyRaDbs37gx-Rq1RLnfz&w8~?ES7A{RIVVaT zH{@gal5|s-g$@nPhVKX$5mfB&9Z}?)sw}P{TCo<#`L<13zsQ+=^<+ldT+Gn!d_M)( zDHBlk7+c0es)Cb2oP{JrIXt-%nyOi>h<8D8QCw+lyNS3#l6qX(Lk~>M_{BjCUpsc) zqz@K2$tIB4Pf~(@ojy@%!$(rVIX9TYJk8MNr0wra6WLeggp@0fL@1Q<{O`u!p#>R# zgod*tm<{BFN|!hGkB%=U_HY8FG2snclD`@|{*)3z#~jQ;*b`Gz%PeB9=qH|wrT^Fg zs2nD^NV=RdQl!(0fO8_s-K}Tipn<*v3w%YXkAi*0^TJ9t%i{m;vc>fVn-K@SaKyC> zA$}l{)hB=q^DTcm^Oz#{Y$-hq_%DX^U;FW*KCr>-RYP}5-?KjdXQPWJ*@B(7;{62P zq8_qG_9`g5stYRzhXgVK&v>`@>SLSdp3e~PiUJ9u3k+4EA)0S#Y3TszyV~lWvuw=@ zu}qyjZft3ET^;ig82qN#&9TT!{OPA9nbE$jDsmu%D^kJ@?On!yDft<%b(lNb$-Z@Z z=X=_B@wxD36AWE3kghC&1hMj|kYN4`%}?U$lT9tv+VS*=PHo}exwFZPTdTI@8FZJD zP74NPK5TadxWa4mnJE z8R%lgYwu9M?~--`-#INRYP^YyFJU=BxfJo^s1icxYp3Wx#;{y&i~l9LVJzBRB0NYv zsJ+$0=|Y4|&LZA=sH!Y7ejjl|bt!-F6*_;Skc7fy+s(B&bvZ8+SQN>q#7jJVD{8k^ z3*6>n;b?TfP7?HC2FNeN!Sv;MJj zdKddo$BkXI0k0PWS~k1(8Q~!$0Z_;77N_Of3-JA`oSB&kvHUYK@c!NLL;4OPuAAJz z%DFRN^78V`a)TgnmslNS(S7GjW`;JFL~a!r7-(<&ou=W3e~%$Kc(ZH)$+-4a>sAO& z;`vP&sN2kBW?~Wov6>-9+go6eeqt@D!lC`oAeoZ6g?)e}fV410{2TLlk6{wqEsoAt ztS0{Rl4SMFCqo@4PmIi8rS#V`d}ln6n1=Y=b2-kp?TOe@SSKeZ%eLI5^i&g{n(cqu zWX;Jj9J*QHg))rBSDF4V89e6043+9%IVV-JBU477I112Z1IsAQY1ATo<4&1x+N!(K zi(@`c`7F2=hV+w+Fi?b2kQQcaOo8%_F%10B~cUW3+# zFbb&3Ze>=06O@X)?`KeDlvc`@2^|ksIjZ(t@3?%Lq9h2R5I|ux^*G{z)r4KMHCnMG z$#SwhryfNV#sCtjN{7fpx-g+EulfmEMi=8=iCV43%E}6FdevUO?`Qsym%ifF{KZ`( zj&2tG;wzeC+o;%emSyATD}+F7ZEd;gq5tHYU(37y`rP~3&&KNgdgZoma*^-*;r3awXx&DCnUb?=EL>4ZmpD}KTHUth(^sveeh9T2uRY5 zcC&$Xj-t?LT@Xbv&iQ%mf%54{AQ^w34}HF#TKn%XNfMt%2+0_7_y-l2jh$AGFB02UC8GQ<2-oxvz2vjXKb^*9FXcu`P4jkv6&2=5TAE&CV5 zgO#m{-3|r=wr$(S;nfi@e^!gn9!Y4{Lf-qCJ~v-2#^XgPMZe#t*=&yONF+((lCnYM zp@XX$EZ1DVL}Hp`#xHU)6R&glM0aeEl>=B=Ss5>t<-Myn&NNNYTGMW~F~%?)4jBvv zG#U+}$hYA-=gtuAsvE0NQQ#m5#y?dgt=caFfBP@SpWkRS$g*twd@3hY6y6J0dO|gb3vCFLd@wSLQt(2I9e{Ig z>@;h&+PLf`rKB*n6x19W>+5XWwvD{dgrT1s|ImA`273VEl4SZSHgoJ@MXI4d|vJMWxuMu)mTRy254i*vYcAIMv@leCDW@m;XKz> zb^th{DD?*9p)Iw|OWo!0BuQdvN6ORyJ5!7Qp8K@RVB$*~FG}bzD z3$u-oG&i&xAzS?cVGt07A@w+-I~Wnj%8B?KU#Uv3@rDNwN8y+)NE}GN8V}$fn)9Vh z>l{-01y1uq69$Sxdv*{7lDx2hMM*~xgye;#9tT9B!Z{cW{kV_gh}<}w@eO7I&(Ojs zgmS!8GbGa_L2F$$zHL&k*I8Lv;p(fe2E3N`${Qm7{3B~)rcgcOFJ`Ng*t>TxgTVkPC3ACgV?nAqXGOt<5bWN) zn=lNCBHw3U4dUuEtKU_+5XbTOy;W2W!?GM!8h6#br?nn;`Xxz%wO-t-#ImXsX^f%Y z?~fe`z_MNWf+Y{ST2{;oD-c(T^0#VoIig){=Y<76iIh*?fUbu z&f)l&Iv%quBRO(WKq!SOCzx-nOUP$XSGavd6^$+ubkmb#2*f(PbATmBRK!gOL_*BD z81*H3>tL)yk!R47#!^WEv|WeJa+%bkDsv3#5=B9?19;7(((r-AEx@L#fxEzhSj=(0Ks+ZLtI;e`y8n= zt>U~s5(yb&AYycba;sXYYhUX(YB0!pATijdA(ut9ba~htc;v4h{~` zbsawVyCi~Rq|8Bw9f0?-2ZDbPf?8+!-wewc-txvM%M!=O$H}W~8yK*OXxFb_hf*pv z`Z(t>olXG&H*VZO7o*urUP3+kw{G3Sa=8S61pAiDCA{~Et>@3=C@%!13LNDaQ3yc< z;9xL-);bYlUDu`h{=vaP;uRK)1)8S8pZl8EV!2$REK6irhQq@{6a^V%UDs)CtaX4V zqo*sPhjlJ_S&rxoYn7y^!6|`r4)gguI*rXu>ieFI*N_VBXtshB0ju>IB0AUky2s9B zg5BL+xInKn#sDFp%ya0xgiso$C&O#o7VGsI=l9Q0ULEh-{{B9UC3NAfp|Tu+#d3u_ z&ma)Lt5gtT)0mQF86X5sW=qPaeZY7)K$UAiN@RJC#j-(fES&eqGZIoM3G}^zR+{GH z#q}}9z-%Eo#I+;HPY6IoNADaFb)7K)1WxBGWLl!m6_f~Au3Jp!D~tvuObl*SnPawU ziFk89me$xX8TFE(IUjov0*xZiG7aac|31rdh(IICAO!+8_c!ATN+6Pt?=hmJ+Q<8J zIB#&g2Cf{FKubx9yY@y1fyiSSrxQsjQX15C9r7&0WHP~Oxx}@j6^sjb@^8NxWsze& zJ3`k{Q068z4h-A-Fw3%JKO9gcfE01G))Uj0(8Me{ynygM$NPS%$|Rdko|87+u$a8+p>a^EhK~G#Wu`oi?;m z3WLD_Sw;#%Mg-0|tk>%li5cSamli^#_wwFjGMQksa|T*#{J`IM6^@UOF_}#8!b;Pj zj}bj^ls`S4CdYy3ECFKpIsM79EIAAYT-MqoP-4VYmLFP~5=kOH|+=tbA4G3b57t2*jN7g-c*o}vSloklFNm&4zwdb6RYaJV~bR_G7kEp>x zDl$3=Ab>_&>x>+OvDAn|#3W)wrh0#-H5@2q(253UB*z&%`aY&qASbSrO1W*XW(i6IFs28vDJ7bo<_7oxc;`@R2^Tf^G9n4?P4J%31%ih)J(Qxz ztFsIq6fK2RFs6r%hlXTIlaWeE)e$Apc7&jO0M;#uwum?c>N10M4rhi1#(BUf4;W+t zC(9PbT2eX!kV2+(avhyM@0H`@V?2281w4FVgeoVJ^X)(L+j!%fz8;rvt#NR0kQ@(3 zdGCFS{NDS7+*6QZgWyzv$v1v5k;V={DTTegy+pokdr@Nyx~{{ybLTJ^3@{iB?sy%5 z^jQ}#UQEpTVzEHmwy3Hq87sr;-QC^vdPW4C&$F=uNSm9Demow-TALz&p6AJtv0{pU z-?lCKW{E3Tp2pjL`t!K=-h1Jk!(By-y3FrDAI4D6I{zO-RlY`EPv=-rL**$cHfw2} zPuJe}G$4b&mvdC^37$$>?8OCuhweL%t~bbw93c=JtQ9q^oJ^-^ng%<=5_y(E2x@zl zlByi5Zqrga91b9rg3fc4@q1KdflLvk7D~pAytRoOMero(kqUX1(L6mtj5Zse5CUh$ zr0;Mp*1SW2R0>(ahgApwlt=UWc~7}>2mw+@=O8!xtMB`$fTS6H##lIK6AB}iJV0r> zHYv$DEL#JXDi@i8&ZzH!?$zeC0TNYN0tm^F1p=hf^f`gnfcBb)vxXdrvoL*+GFOBc zti!OPPE3-B3m8X+*cpdX3+#_FG{$3)1-Kwk6r}ISG8Op@7uBZ$Qs-DMm)P6egE1sy z@461xZ%^^c(GgxaUgLrNBB6GM{T#43fB;C&ApM?=c-xS-^KAeLwhBSenxQKjRF-8h z#^BbiTgb8uv)K%N-=_{deqUxF`N0HQp656{J;l+{5u9^q+ZI9yEEWrNT^AcM=4k`7 z@oXh%-$Z3j*ifE9jvuUQq834naLoPfUGeDZOkuQc5`QV626Q1P{^-B7?Oa-jm=)X_fW}qX`*t zlM;7+X4g}*Rv*b-fEaytN=ga@M>(}pBnqkvjSxJXAw7uc4FUib1Su&2Nyvco37d&X z&e5Tr5E4?UGyqge2^mwPTxTfD0@gVgW8l4`6fO#aLU4d1zTkM}AX1=^&^m*FLzYp1 zY}*b>N;Dm%IGI)$)+HvBNlMX~W4rId8C)K{2|xaWe+{2}-XbdpcOY`Q#?Ag=6qRLE zQS#nX8IU(F8$z+lULXP}x z+a}eQwKjF^?dEp7j5Do~OlGEEWkN@S?FQ za5kI4TH18H3E7w#ZEPF7_oT_^(%rTLna}5Oj=)AVq_xIuHp6H%N=^kp%nwCc*Xs*9 zGWvy?$LlYwuY_FIzNU0-&?`Bm$(8C^NcGt96GwC*x|n4tqNjv~7pp7}QmXrjKcdAT-f?g|=;Q zZg+&kd5`^Zj++NZIJ+}OkZ}UB^a$RghPd>ZE z%g&N+Aq1-6ad2nS8`vONM&yzWSSghvJ#St8OH==&ahPfa|d(I^pmbzNh*Tq29Gmh1csof*pWwKHj# z=Xu)ntlZn*-%p!>4V{~n`o2#@=IQAvX0sWVoyQk%&J%aRpT~>G-n40&6p~QDvDs@x`oImu<^5GmbE85raRB0J_6)W)=h)D zDk(kiM5b9wv-rHHlq03JVl%3Xb8Uf5Fw;`vkDj>!9~JYbjfdtcfunZiL0!OlYK|HX zhnOuJ1OPfKAf-aCMdTNvfXG>#nGDcdk6cDxtsLRXVS`*NRGCE6Ian94TCLMQxpMV7 znx@0m>$mW0pSXs$>+ref0$!NW91DAV1y!*LVgM3aZre7YD*xB7f2tFw*vy4c{JkL`FgTHH*W$D_Kq+yprQpcbW3NDIJ0WItkAIXGs{VY6j3NUAk+6y zT49r95gHJJm;@6^V`~huxasLRv=_9_;5=7mVnCC${>-?0A1F$HnRDK$$*3F@hNXg% z3V=YKljuu;m7o#kbX8ReqHjlSc5v(UIt>+JgJuUuxjKE{$#|4>1|e={ z{vE%swHu9pp63Z!@VW)>BEf_x>>)NCK$Mwo9NXnDei0WhT)?$!*ODHhZCkwl4c~;` zOH@UMtQx_H64S#Q7>~yUf#bbVN+p#gBLGGN>;xzWCI-BTsRwU<6H@~rsYGe41u5nn z5RGnF7r4C)xO8@e0E(g^gvb|=k*#}!aa|(Q8^sW#CcNcROG zQjyXS%AwhT@<37NNVP{v6e%606j-lT$g-Sx4$ya?)e1>zr_~1KQb*@TCaxrD144X$ z+qTeIh5!L$P15sQ+YyuREy_HHjFk*21Z*G4Xio==><}0| z1Aw%a6hdG)9HOr4WXNaEoJk&2Yn}3_ z5H=^TY)4gogb-4%0zc0MBK&$oRTAo(r$kt4oeYLi&^7rGR!uCG$qeh(;AFlgu0nbe zC)pMQ)ngL*k+yB|-1X-Z!pWm9g87TOu2IL?g>BoSEDAJj2c^b2z{0 zb|ga)aDnFOX(iD#EzXT3X3GYBPsV4R$86Q%`I81^k)=Gj?F{;8L|xmW>uA275CU1A zQ`yeC*n|@+ir9!%cuzP-Jc1Bp{EWU3HoS}rq}gZvLhue0u?r|g%02E^Afhr$5gbCo zc{;aG2$|L(1W@Hj6Dg;Zj3dIxk>;Ap#UTKb!62r5Ip)g--czf!^MHef1Td~O_QoZt zKDETE?J=lJ3~QR~oWY_AIYzpL)ERavGGrfsBG-8O>T@_=bhvS}!0R6x;h7_Y4}bXI zr$Ea4knbO_6WcQ*eE<;i`8;^hb6D`#1ja(upK{-PZH*-`ZoDsKNy zYn{YRJe+j3TBWq)=P&qxnqBf?Qi zVKJX$G9Cev`qPEj^a)sNpmhdg4T_xDZf6aNzO0XJ#tyv=bYNxQL&zM1Qsd^Vg)EWXNZ!njLQBB7 z*pvm(KvE7MaqVdiK|pXGy(1N%51?bj1pPd3deoW1WJLSB?QQyeV+@2;5C|C7C6=v4 zrhwi8KtP@;#HJ)FX3BMizMUpjKQ zf4D|blo(Vbz`J@X@X|d(Rc+rvgbgC+0LBl#Ubb_|3;g9UfsKfvF&h>~H8xD86jrMh zq@;;DOaPW;nH&U1>cL<@ghjluJZWb#nUHta)NXrlaFE!3z7|&aap{Z=m>q(NB%Jr8 zOkA(m39_>XWCzCA!>HxtEG$$_vjtyZhF@pv(L?SM_xx9(1|6abcy zBj6k14$ZRL0zIQa=hl1kK+23FW}dI?R|Lp%N|10G#Cs9c;=aB(AkZ&n!jIB z6u5k2jw*5t+iR4JJdR?g$Uufo$A9<8iUKYK_z1B>0P@%fQ^zXJB(_;gAyAbiR_m4y z=&)32(2Ba?%Dg~bC_Hz#K&=9*oJhaAEU;RyV5|d)cr>griKJo_#}1y?GtcuBxr6|? zGtq;103`(^R~P{5?FZHU%UmaEvG)N8lyY%-xyW-UP1PFf=ruGrgzTcsGRRoU+o>~1 zO^rp3@o)jC3OXf9YlW(y_Uq-krP3iJ%ACrOnG%>Tdk6uXu6tBDRXUnJplN!TzJm`E ztER#A<24RvYuuc!aaJvH{``5Yj$eRTpW-!lXBgBK5FAct)N=pWr5c~R(GgOPa&xxb z?ZCw7emls^5HZ`7t~5`5+d8y9b2{+SJ6yAg-;Nk#$uVeHi>lL@M-O0r>w*zB<` zT(|)5eNqha=Ww*mbE>;{U3W)602=~t9A?n@QN|pcX9p}63oI53lx2Abg5dAMklYxP zkb_d{jz#1J4k09GUiruacAEhBGJEh`jiV@Ec8&(B_I*#$s-?lHi^YN}9(|AF=^Q}7 zcr>DXIi@q-QIC4pb!j4y_oO&%IwC|Zmkqtc?b8OUb%z^=GgwFS1eWUt7k4Y{jB1SQ z3cYbSn$A%a)cRVN)bPdY$7{>?F@z1`WQ1jHoJSz2g_jDvjR6(xg!E z0cg4oO-~I%%f_L~WE28P%+_>%;k2=kS&nrmkZXbc$pBeiV1HcUXlXGVjqy8|=eTh9 zOR#@7PWu%SKlzjI#@_A@zVL-F;2;0w2k^;{{v7_!H|`{Y@5>)Th!o7WgO@lxJq^70 z0PcuTQf^2iU{n9+2TOi=qp>sKi(tjw{~ugtN5M6H&Q(L8(ium8Yi)}D*4kv4d7h`u zvREvVf%3*-56@1B4U|8h5lz>1sjR3qiDs&b`lbOS@{hFx{F*!`9RcUmyk?9R0A`2p zBuxXTwv8CY0D&yi&`RNx*E&3Yfi~{Pe*cg0wXb?9q$Dzj`4Mg)s_Qzn`9A&h(|F*4 z2h!(#`F-Mh$WD~6X}w-!JRV~{pJOl_QZ63Tux;dLEp_)vsnA=8x-8&afDZw|6K6u? zl7aQWe7VGhv%66pNcVg)8ldehyfYZob!^Nb&SAM+C8Cv`IXl_yw9`2Uo#pV}MZ`e! z6==GShx33B8)WC5gN%Ev?|WoYzzK@##yJRVsuSFh#gz~s08&DWP5XHOuv|B&N;;3w zMqVS&v$*aay(NypIO;oqkU*emL1jVQiV^`H0=)&!jB;E(T;tq0!;QJcxjJA}6Q?zw z&*4Re-Eob!H_%F9*%*xLEWvuE6#nkFzZqxFUBu4U{SdzAO)rHGecJaNsQBk?v}&!T zW}t0j=ST7oV$<2i&tn7L-``I}Q=aE{$bT7CD5cU(%X9_`$`^mxmT*o?$wj;?F*EcXD9PIY_0G2iKZr2<-0e!djb< z5*cBioCRMKZHK<7CC57A5VjpNmwdTqy}bv6AYB5dl89UdJJdYO;H)K5 zQ&76{ho8F)B}vH_EKNF!MLP%pA$SBIpoD;ulx75w3bD5|zKmqO4ug`W_hwqb1_7aH zhM#dn7Uqi1Cd^dqlnfSwGQ+4Y;hjNM&~ak`)hVnC7#0FeXCi-4!FiAA%FsTQWS<8o zGqg5fTxU30nG_6v=@)+d(pNn+P1}LJcf_!!%&$RY>H-V zFjZBho64xfdm6UFRSjU{n7GUGuDk9^PMOI~4hGDTSi9j__0O=Qg?q`1* z-~D&q1_Om$_!N=3BC=dAA@dTyn2g=h2gPQhJAVP1oZc&yQ`ko@JwH5+2naK3w1GhRbtgSv~7c`$kBHlplIZn z7F6AujK^3s7CVC+ow4}v?_I`szWFuq&f=-(4)J_5#5cX1PQCt~x4r^D^=qHPdw=62 z`1Zf@7QpxEeufZ|eewO3+mN4cK1Va&d~7s))A`T#W&y|h0ENR+N-}fnM^Jg*gky4^4%X9>K7Aq@NRVAV{gpkq( zt!d~g8$Z7%zZa(;tcq-!CQV!7d&mw#3KF|`@3CGtaMs|~36XncQNUV*vwJ&02#^Rg z*itJ90W4Q*G;N3FigXUcL4~%X!Jc`hF&faR#${1JX$_j|M}G?5Qy>D5=IMDnbL|#Z zO$(hVz<+Q=!|YNqg5j2ZR9X+Eaj}yt&L31RQ$PBhNE<(<1<}O`7)lcsPJ_9^P&= z6>Zbt(vF7kq%9a!HKwZ`@BwYtpsp%Zbrq2c(A$8MMTdE7(Fcvz0+Z1Y<2pxG(0LaN zn_;$GLyCaTSkzUCZ+qh_ad>ou%P*Yb75AUVfBf_<0Kkv@%m=YJ_&mxY$G5%pFTsaR zK;Cxlz$S33C!ToXN!|>+0T^0uN8{}m@q;5XzfNnNHsy8{=G+-Tg3CEcnZAUsF|8DVN`t5O1drP17VO4Ixl$Ar6N7 zTm@i9$mAsF&VAowx-$5KKl&10^VkCjA>iQPHg4X$iHjF6BFi$gEs=r4x{&*w)?5`~=f&5dHNo7fs^CMwYODR4oiuLFN*k#tibRBNBP}Uqgv=h}(GboV+?uZOk_&riyB?kOFvg)Ohyc#=JQ8(uPXL-W zr8G%mGff0yrZr}ZC91MOu66A4Qy31Y1j-$WAyEAvAVN^-43qH)lVJrL2a8BaY&-|F zZKIHq7=CZ*V3HsNv=9(dz|l-_+WYZCLLg^H@98kC>0Cx*EcnDE@2QGFsfEOu$pFCz zG=>CItKOkdB#s(a8o3sz3x!*A2ivW%TDMSA;N0#Iof8l$L!p4)7(Dm<4O~2b9_ROl zc>n)+25FG{Nw=nqu-9d_ZJ?-d(Y@<&HUc>ek4@v2w7D#2iew^dc8MjbX}Ml@{vC(|jUpb7ySg%AR_Z{Nnw z&Q4kjV@N2po&U3w;)v?Khs%aIzf&a`&+0;k?|kUUcs#~p)!^xCH}Q&x?oEydVMCNM z0%_Y8H+pwo?t5_z8C$qzq^FTMLQ zeD*ougP&U9tL`cA-ESV_Js-M--SHTI@CW}BuX_B|@Q$ucN(mAC9jUkwTLVG1Ap%Dg zhR*yP-jGTuJo}}~DdKUdj{h7VAE(@JJRV~>971c2)oPV;kJHmr07wkC)Nw5(@ zz=aDJ-~u2ciecHa)>MvU!}Q*#(a}3QJ6J9jxPJ2}YSA~%Lc`%O8D3e^rgF~Vu?OzK zd~I-h)}|&Ic0>*lnTXDzr z>t9vW*~w0a0|r2duC8nJ#$dTxV>le(csfIoY3z<@MBMarMsiyL933Ac%cuo>5bs}I zk)f73p~O5>SV!R#0x2b(qd}UN?%IX-0pno}9Ysnf^A()4XnKP);~KMNi@K&gXZjwQ zQYi(X`w=6ecTwd>-^T_Li#baZlf5UW=zYL)MdP_-bT|di16Zw4*A+q_2d6VaH^v&2 zIgL8AmbwOuOrh;9N-bc6j52D4wG~)222Iyyk8?xb=O%)8I#c{g2_C!^c1NVVo>2%CbU{X&lbh$$7J# z3L)^nONL&2y!d$W@u%h?x4i*FgYDcr&o_fLQoaY^;fEhi`87Z}CYRi@j1JZukH`4L zCw>oI*Wv2bs|hB4{_~$t5rjXFi8+SJ^Z7h_eS+EBWk?nRZ9~_yXU`_c&nZKS%#m

hjKtfPkXjN6j+M7PD)9LgCdpkpX|GVCc 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 literal 0 HcmV?d00001 diff --git a/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.tif.http_data b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_0_0_0.tif.http_data new file mode 100644 index 0000000000000000000000000000000000000000..70944fa0c06f93f4f083cad529752b7d35570d65 GIT binary patch literal 93781 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 literal 0 HcmV?d00001 diff --git a/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_f_json.http_data b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_f_json.http_data new file mode 100644 index 000000000000..d044f8585f0a --- /dev/null +++ b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_WorldMercatorWGS84Quad_f_json.http_data @@ -0,0 +1,138 @@ +HTTP/1.1 200 OK +Date: Wed, 14 Feb 2024 05:33:25 GMT +Server: Apache/2.4.52 (Ubuntu) +Expires: Thu, 13 Feb 2025 04:01:39 GMT +Access-Control-Allow-Origin: * +Vary: Accept,Accept-Encoding,Prefer +Content-Length: 5775 +Access-Control-Allow-Headers: x-requested-with, Content-Type, origin, authorization, acc$ +Age: 5505 +Keep-Alive: timeout=5, max=100 +Connection: Keep-Alive +Content-Type: application/json + +{ + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/WorldMercatorWGS84Quad", + "crs" : "http://www.opengis.net/def/crs/EPSG/0/3395", + "dataType" : "map", + "tileMatrixSetLimits" : [ + { "tileMatrix" : "0", "minTileRow" : 0, "maxTileRow" : 0, "minTileCol" : 0, "maxTileCol" : 0 }, + { "tileMatrix" : "1", "minTileRow" : 0, "maxTileRow" : 1, "minTileCol" : 0, "maxTileCol" : 1 }, + { "tileMatrix" : "2", "minTileRow" : 0, "maxTileRow" : 3, "minTileCol" : 0, "maxTileCol" : 3 }, + { "tileMatrix" : "3", "minTileRow" : 0, "maxTileRow" : 7, "minTileCol" : 0, "maxTileCol" : 7 }, + { "tileMatrix" : "4", "minTileRow" : 0, "maxTileRow" : 15, "minTileCol" : 0, "maxTileCol" : 15 }, + { "tileMatrix" : "5", "minTileRow" : 0, "maxTileRow" : 31, "minTileCol" : 0, "maxTileCol" : 31 }, + { "tileMatrix" : "6", "minTileRow" : 0, "maxTileRow" : 63, "minTileCol" : 0, "maxTileCol" : 63 }, + { "tileMatrix" : "7", "minTileRow" : 0, "maxTileRow" : 127, "minTileCol" : 0, "maxTileCol" : 127 }, + { "tileMatrix" : "8", "minTileRow" : 0, "maxTileRow" : 255, "minTileCol" : 0, "maxTileCol" : 255 }, + { "tileMatrix" : "9", "minTileRow" : 0, "maxTileRow" : 511, "minTileCol" : 0, "maxTileCol" : 511 } + ], + "links" : [ + { + "rel" : "self", + "type" : "application/json", + "title" : "The JSON representation of the WorldMercatorWGS84Quad map tileset for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?f=json" + }, + { + "rel" : "alternate", + "type" : "text/plain", + "title" : "The ECON representation of the WorldMercatorWGS84Quad map tileset for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?f=econ" + }, + { + "rel" : "alternate", + "type" : "text/html", + "title" : "The HTML representation of the WorldMercatorWGS84Quad map tileset for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?=html" + }, + { + "rel" : "alternate", + "type" : "application/json+tile", + "title" : "The TileJSON representation of the WorldMercatorWGS84Quad map tileset for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?f=tilejson" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "WorldMercatorWGS84QuadTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/WorldMercatorWGS84Quad" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/geodata", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble" + }, + { + "rel" : "item", + "type" : "application/vnd.gnosis-map-tile", + "title" : "WorldMercatorWGS84Quad map tiles for blueMarble (as GNOSIS Map Tiles)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad/{tileMatrix}/{tileRow}/{tileCol}.gmt", + "templated" : true + }, + { + "rel" : "item", + "type" : "image/png", + "title" : "WorldMercatorWGS84Quad map tiles for blueMarble (as PNG)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad/{tileMatrix}/{tileRow}/{tileCol}.png", + "templated" : true + }, + { + "rel" : "item", + "type" : "image/jpeg", + "title" : "WorldMercatorWGS84Quad map tiles for blueMarble (as JPG)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad/{tileMatrix}/{tileRow}/{tileCol}.jpg", + "templated" : true + }, + { + "rel" : "item", + "type" : "image/tiff; application=geotiff", + "title" : "WorldMercatorWGS84Quad map tiles for blueMarble (as GeoTIFF)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad/{tileMatrix}/{tileRow}/{tileCol}.tif", + "templated" : true + } + ], + "layers" : [ + { + "id" : "blueMarble", + "dataType" : "map", + "minScaleDenominator" : 1091957.5469310893677, + "minCellSize" : 305.748113140705, + "maxTileMatrix" : "9", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/geodata", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble" + } + ], + "propertiesSchema" : { + "$schema" : "https://json-schema.org/draft/2020-12/schema", + "type" : "object", + "properties" : { "a" : { + "title" : "Alpha channel", + "type" : "integer", + "x-ogc-property-seq" : 4 + }, "b" : { + "title" : "Blue channel", + "type" : "integer", + "x-ogc-property-seq" : 3 + }, "g" : { + "title" : "Green channel", + "type" : "integer", + "x-ogc-property-seq" : 2 + }, "r" : { + "title" : "Red channel", + "type" : "integer", + "x-ogc-property-seq" : 1 + } } + } + } + ], + "centerPoint" : { + "coordinates" : [ 0, 0 ], + "tileMatrix" : "4", + "scaleDenominator" : 34942641.501794859767, + "cellSize" : 9783.9396205025605, + "crs" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + } +} diff --git a/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_f_json.http_data b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_f_json.http_data new file mode 100644 index 000000000000..1694f8e8e181 --- /dev/null +++ b/autotest/gdrivers/data/ogcapi/request_collections_blueMarble_map_tiles_f_json.http_data @@ -0,0 +1,433 @@ +HTTP/1.1 200 OK +Date: Wed, 14 Feb 2024 05:33:25 GMT +Server: Apache/2.4.52 (Ubuntu) +Expires: Thu, 13 Feb 2025 04:01:39 GMT +Access-Control-Allow-Origin: * +Vary: Accept,Accept-Encoding,Prefer +Content-Length: 20038 +Access-Control-Allow-Headers: x-requested-with, Content-Type, origin, authorization, acc$ +Age: 5505 +Keep-Alive: timeout=5, max=100 +Connection: Keep-Alive +Content-Type: application/json + +{ + "links" : [ + { + "rel" : "self", + "type" : "application/json", + "title" : "The JSON representation of the available map tilesets for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/?f=json" + }, + { + "rel" : "alternate", + "type" : "text/plain", + "title" : "The ECON representation of the available map tilesets for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/?f=econ" + }, + { + "rel" : "alternate", + "type" : "text/html", + "title" : "The HTML representation of the available map tilesets for blueMarble", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets for blueMarble (default style) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/default/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets for blueMarble (default style) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/default/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets for blueMarble (default style) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/default/map/tiles?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets for blueMarble (evi style) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/evi/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets for blueMarble (evi style) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/evi/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets for blueMarble (evi style) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/evi/map/tiles?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets for blueMarble (nir style) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/nir/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets for blueMarble (nir style) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/nir/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets for blueMarble (nir style) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/nir/map/tiles?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets for blueMarble (scl style) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/scl/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets for blueMarble (scl style) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/scl/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets for blueMarble (scl style) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/scl/map/tiles?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets for blueMarble (ndvi style) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/ndvi/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets for blueMarble (ndvi style) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/ndvi/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets for blueMarble (ndvi style) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/ndvi/map/tiles?f=html" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "application/json", + "title" : "Map tilesets for blueMarble (evi2 style) (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/evi2/map/tiles?f=json" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/plain", + "title" : "Map tilesets for blueMarble (evi2 style) (as ECON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/evi2/map/tiles?f=econ" + }, + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", + "type" : "text/html", + "title" : "Map tilesets for blueMarble (evi2 style) (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/styles/evi2/map/tiles?f=html" + } + ], + "tilesets" : [ + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/CDB1GlobalGrid", + "crs" : "http://www.opengis.net/def/crs/EPSG/0/4326", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "CDB1GlobalGridTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/CDB1GlobalGrid" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "CDB1GlobalGrid map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/CDB1GlobalGrid?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "CDB1GlobalGrid map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/CDB1GlobalGrid?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "CDB1GlobalGrid map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/CDB1GlobalGrid?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/GlobalCRS84Pixel", + "crs" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "GlobalCRS84PixelTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/GlobalCRS84Pixel" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "GlobalCRS84Pixel map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GlobalCRS84Pixel?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "GlobalCRS84Pixel map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GlobalCRS84Pixel?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "GlobalCRS84Pixel map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GlobalCRS84Pixel?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/GlobalCRS84Scale", + "crs" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "GlobalCRS84ScaleTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/GlobalCRS84Scale" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "GlobalCRS84Scale map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GlobalCRS84Scale?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "GlobalCRS84Scale map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GlobalCRS84Scale?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "GlobalCRS84Scale map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GlobalCRS84Scale?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/GNOSISGlobalGrid", + "crs" : "http://www.opengis.net/def/crs/EPSG/0/4326", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "GNOSISGlobalGridTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/GNOSISGlobalGrid" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "GNOSISGlobalGrid map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GNOSISGlobalGrid?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "GNOSISGlobalGrid map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GNOSISGlobalGrid?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "GNOSISGlobalGrid map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GNOSISGlobalGrid?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/GoogleCRS84Quad", + "crs" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "GoogleCRS84QuadTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/GoogleCRS84Quad" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "GoogleCRS84Quad map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GoogleCRS84Quad?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "GoogleCRS84Quad map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GoogleCRS84Quad?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "GoogleCRS84Quad map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/GoogleCRS84Quad?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/ISEA9R", + "crs" : "http://www.opengis.net/def/crs/OGC/0/153456", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "ISEA9RTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/ISEA9R" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "ISEA9R map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/ISEA9R?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "ISEA9R map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/ISEA9R?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "ISEA9R map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/ISEA9R?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad", + "crs" : "http://www.opengis.net/def/crs/EPSG/0/3857", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "WebMercatorQuadTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/WebMercatorQuad" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "WebMercatorQuad map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WebMercatorQuad?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "WebMercatorQuad map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WebMercatorQuad?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "WebMercatorQuad map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WebMercatorQuad?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/WorldCRS84Quad", + "crs" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "WorldCRS84QuadTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/WorldCRS84Quad" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "WorldCRS84Quad map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldCRS84Quad?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "WorldCRS84Quad map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldCRS84Quad?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "WorldCRS84Quad map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldCRS84Quad?f=tilejson" + } + ] + }, + { + "title" : "Blue Marble Next Generation (2004)", + "tileMatrixSetURI" : "http://www.opengis.net/def/tilematrixset/OGC/1.0/WorldMercatorWGS84Quad", + "crs" : "http://www.opengis.net/def/crs/EPSG/0/3395", + "dataType" : "map", + "links" : [ + { + "rel" : "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type" : "application/json", + "title" : "WorldMercatorWGS84QuadTileMatrixSet definition (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/tileMatrixSets/WorldMercatorWGS84Quad" + }, + { + "rel" : "self", + "type" : "application/json", + "title" : "WorldMercatorWGS84Quad map tileset for blueMarble (as JSON)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?f=json" + }, + { + "rel" : "self", + "type" : "text/html", + "title" : "WorldMercatorWGS84Quad map tileset for blueMarble (as HTML)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?f=html" + }, + { + "rel" : "self", + "type" : "application/json+tilejson", + "title" : "WorldMercatorWGS84Quad map tileset for blueMarble (in TileJSON format)", + "href" : "http://127.0.0.1:8080/fakeogcapi/collections/blueMarble/map/tiles/WorldMercatorWGS84Quad?f=tilejson" + } + ] + } + ] +} diff --git a/autotest/gdrivers/ogcapi.py b/autotest/gdrivers/ogcapi.py index d078ca0bb81d..f12a239ec7ed 100644 --- a/autotest/gdrivers/ogcapi.py +++ b/autotest/gdrivers/ogcapi.py @@ -438,3 +438,42 @@ def test_ogc_api_raster_tiles(): assert ds.GetRasterBand(1).ReadBlock( ds.RasterXSize // 2 // 256, ds.RasterYSize // 2 // 256 ) + + +@pytest.mark.parametrize( + "image_format,raster_count,statistics", + ( + ("AUTO", 4, [0.0, 255.0, 83.8084411621094, 30.532715248645687]), + ("PNG", 4, [0.0, 255.0, 83.8084411621094, 30.532715248645687]), + ("PNG_PREFERRED", 4, [0.0, 255.0, 83.8084411621094, 30.532715248645687]), + ("JPEG", 3, [0.0, 255.0, 83.83631896972656, 30.486497283147653]), + ("JPEG_PREFERRED", 3, [0.0, 255.0, 83.83631896972656, 30.486497283147653]), + ("GEOTIFF", 4, [0.0, 255.0, 83.8084411621094, 30.532715248645687]), + ), +) +@pytest.mark.require_driver("WMS") +def test_ogc_api_raster_tiles_format(image_format, raster_count, statistics): + + ds = gdal.OpenEx( + f"OGCAPI:http://127.0.0.1:{gdaltest.webserver_port}/fakeogcapi/collections/blueMarble", + gdal.OF_RASTER, + open_options=[ + "API=TILES", + "CACHE=NO", + "TILEMATRIXSET=WorldMercatorWGS84Quad", + f"IMAGE_FORMAT={image_format}", + ], + ) + + assert ds is not None + + assert ds.RasterCount == raster_count + assert ds.RasterXSize == 131072 + assert ds.GetRasterBand(1).GetOverviewCount() == 9 + + # For some reason these tests fail on Github on Ubuntu 20.04 gcc and Ubuntu 20.04 coverage, but + # pass on all other builds. + # assert ds.RasterYSize == 1586181 + # assert ds.GetRasterBand(1).GetStatistics(True, True) == statistics + + del ds diff --git a/doc/source/drivers/raster/ogcapi.rst b/doc/source/drivers/raster/ogcapi.rst index 09d84ae05136..04204af49f7e 100644 --- a/doc/source/drivers/raster/ogcapi.rst +++ b/doc/source/drivers/raster/ogcapi.rst @@ -81,16 +81,18 @@ The following open options are available: GeoJSON items otherwise. - .. oo:: IMAGE_FORMAT - :choices: AUTO, PNG, PNG_PREFERRED, JPEG, JPEG_PREFERRED + :choices: AUTO, PNG, PNG_PREFERRED, JPEG, JPEG_PREFERRED, GEOTIFF :default: AUTO Which format to use for pixel acquisition, for tiles or map API. - Defaults to AUTO, which means - that PNG will be used if available, and fallback to JPEG otherwise. - If specifying PNG or JPEG, they must be available, otherwise the driver will - return an error. If specifying the one of the PNG_PREFERRED or JPEG_PREFERRED - value, the specified format will be used if available, and the driver will - fallback to the other format otherwise. + AUTO - This is the default and specifies that PNG images will be checked first, + then JPEG and then any additional formats the server supports. + PNG_PREFERRED - Same as AUTO + JPEG_PREFERRED - Similar to AUTO, but the order is JPEG, PNG and then any additional + formats the server supports + JPEG - Use only JPEG images. If none are available then the driver will return an error + PNG - Use only PNG images. If none are available then the driver will return an error + GEOTIFF - Use only GEOTIFF images. If none are available then the driver will return an error - .. oo:: VECTOR_FORMAT :choices: AUTO, GEOJSON, GEOJSON_PREFERRED, MVT, MVT_PREFERRED diff --git a/frmts/ogcapi/gdalogcapidataset.cpp b/frmts/ogcapi/gdalogcapidataset.cpp index 60434f426ec0..0d24d1f9acf2 100644 --- a/frmts/ogcapi/gdalogcapidataset.cpp +++ b/frmts/ogcapi/gdalogcapidataset.cpp @@ -76,6 +76,7 @@ class OGCAPIDataset final : public GDALDataset double m_adfGeoTransform[6]; OGRSpatialReference m_oSRS{}; + CPLString m_osTileData{}; // Classic OGC API features /items access std::unique_ptr m_poOAPIFDS{}; @@ -92,6 +93,8 @@ class OGCAPIDataset final : public GDALDataset CPLString BuildURL(const std::string &href) const; void SetRootURLFromURL(const std::string &osURL); + int FigureBands(const std::string &osContentType, + const CPLString &osImageURL); bool InitFromFile(GDALOpenInfo *poOpenInfo); bool InitFromURL(GDALOpenInfo *poOpenInfo); @@ -110,6 +113,12 @@ class OGCAPIDataset final : public GDALDataset ", " MEDIA_TYPE_JSON, CPLStringList *paosHeaders = nullptr); + std::unique_ptr + OpenTile(const CPLString &osURLPattern, int nMatrix, int nColumn, int nRow, + bool &bEmptyContent, unsigned int nOpenTileFlags = 0, + const CPLString &osPrefix = {}, + const char *const *papszOpenOptions = nullptr); + bool InitWithMapAPI(GDALOpenInfo *poOpenInfo, const CPLJSONObject &oCollection, double dfXMin, double dfYMin, double dfXMax, double dfYMax); @@ -235,7 +244,6 @@ class OGCAPITiledLayer final false; // prevent recursion in EstablishFields() OGCAPITiledLayerFeatureDefn *m_poFeatureDefn = nullptr; OGREnvelope m_sEnvelope{}; - CPLString m_osTileData{}; std::unique_ptr m_poUnderlyingDS{}; OGRLayer *m_poUnderlyingLayer = nullptr; int m_nCurY = 0; @@ -587,6 +595,53 @@ bool OGCAPIDataset::DownloadJSon(const CPLString &osURL, CPLJSONDocument &oDoc, return oDoc.LoadMemory(osResult); } +/************************************************************************/ +/* OpenTile() */ +/************************************************************************/ + +std::unique_ptr +OGCAPIDataset::OpenTile(const CPLString &osURLPattern, int nMatrix, int nColumn, + int nRow, bool &bEmptyContent, + unsigned int nOpenTileFlags, const CPLString &osPrefix, + const char *const *papszOpenTileOptions) +{ + CPLString osURL(osURLPattern); + osURL.replaceAll("{tileMatrix}", CPLSPrintf("%d", nMatrix)); + osURL.replaceAll("{tileCol}", CPLSPrintf("%d", nColumn)); + osURL.replaceAll("{tileRow}", CPLSPrintf("%d", nRow)); + + CPLString osContentType; + if (!this->Download(osURL, nullptr, nullptr, m_osTileData, osContentType, + true, nullptr)) + { + return nullptr; + } + + bEmptyContent = m_osTileData.empty(); + if (bEmptyContent) + return nullptr; + + CPLString osTempFile; + osTempFile.Printf("/vsimem/ogcapi/%p", this); + VSIFCloseL(VSIFileFromMemBuffer(osTempFile.c_str(), + reinterpret_cast(&m_osTileData[0]), + m_osTileData.size(), false)); + + GDALDataset *result = nullptr; + + if (osPrefix.empty()) + result = GDALDataset::Open(osTempFile.c_str(), nOpenTileFlags, nullptr, + papszOpenTileOptions); + else + result = + GDALDataset::Open((osPrefix + ":" + osTempFile).c_str(), + nOpenTileFlags, nullptr, papszOpenTileOptions); + + VSIUnlink(osTempFile); + + return std::unique_ptr(result); +} + /************************************************************************/ /* Identify() */ /************************************************************************/ @@ -628,6 +683,37 @@ void OGCAPIDataset::SetRootURLFromURL(const std::string &osURL) m_osRootURL.assign(pszStr, pszPtr - pszStr); } +/************************************************************************/ +/* FigureBands() */ +/************************************************************************/ + +int OGCAPIDataset::FigureBands(const std::string &osContentType, + const CPLString &osImageURL) +{ + int result = 0; + + if (osContentType == "image/png") + { + result = 4; + } + else if (osContentType == "image/jpeg") + { + result = 3; + } + else + { + // Since we don't know the format download a tile and find out + bool bEmptyContent = false; + std::unique_ptr dataset = + OpenTile(osImageURL, 0, 0, 0, bEmptyContent, GDAL_OF_RASTER); + + // Return the bands from the image, if we didn't get an image then assume 3. + result = dataset ? (int)dataset->GetBands().size() : 3; + } + + return result; +} + /************************************************************************/ /* InitFromFile() */ /************************************************************************/ @@ -1004,21 +1090,63 @@ bool OGCAPIDataset::InitFromURL(GDALOpenInfo *poOpenInfo) /* SelectImageURL() */ /************************************************************************/ -static const CPLString SelectImageURL(const char *const *papszOptionOptions, - const CPLString &osPNG_URL, - const CPLString &osJPEG_URL) +static const std::pair +SelectImageURL(const char *const *papszOptionOptions, + std::map &oMapItemUrls) { - const char *pszFormat = + // Map IMAGE_FORMATS to their content types. Would be nice if this was + // globally defined someplace + const std::map> + oFormatContentTypeMap = { + {"AUTO", + {"image/png", "image/jpeg", "image/tiff; application=geotiff"}}, + {"PNG_PREFERRED", + {"image/png", "image/jpeg", "image/tiff; application=geotiff"}}, + {"JPEG_PREFERRED", + {"image/jpeg", "image/png", "image/tiff; application=geotiff"}}, + {"PNG", {"image/png"}}, + {"JPEG", {"image/jpeg"}}, + {"GEOTIFF", {"image/tiff; application=geotiff"}}}; + + // Get the IMAGE_FORMAT + const std::string osFormat = CSLFetchNameValueDef(papszOptionOptions, "IMAGE_FORMAT", "AUTO"); - if (EQUAL(pszFormat, "AUTO") || EQUAL(pszFormat, "PNG_PREFERRED")) - return !osPNG_URL.empty() ? osPNG_URL : osJPEG_URL; - else if (EQUAL(pszFormat, "PNG")) - return osPNG_URL; - else if (EQUAL(pszFormat, "JPEG")) - return osJPEG_URL; - else if (EQUAL(pszFormat, "JPEG_PREFERRED")) - return !osJPEG_URL.empty() ? osJPEG_URL : osPNG_URL; - return CPLString(); + + // Get a list of content types we will search for in priority order based on IMAGE_FORMAT + auto iterFormat = oFormatContentTypeMap.find(osFormat); + if (iterFormat == oFormatContentTypeMap.end()) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Unknown IMAGE_FORMAT specified: %s", osFormat.c_str()); + return std::pair(); + } + std::vector oContentTypes = iterFormat->second; + + // For "special" IMAGE_FORMATS we will also accept additional content types + // specified by the server. Note that this will likely result in having + // some content types duplicated in the vector but that is fine. + if (osFormat == "AUTO" || osFormat == "PNG_PREFERRED" || + osFormat == "JPEG_PREFERRED") + { + std::transform(oMapItemUrls.begin(), oMapItemUrls.end(), + std::back_inserter(oContentTypes), + [](const auto &pair) { return pair.first; }); + } + + // Loop over each content type - return the first one we find + for (auto &oContentType : oContentTypes) + { + auto iterContentType = oMapItemUrls.find(oContentType); + if (iterContentType != oMapItemUrls.end()) + { + return *iterContentType; + } + } + + CPLError(CE_Failure, CPLE_AppDefined, + "Server does not support specified IMAGE_FORMAT: %s", + osFormat.c_str()); + return std::pair(); } /************************************************************************/ @@ -1052,35 +1180,40 @@ bool OGCAPIDataset::InitWithMapAPI(GDALOpenInfo *poOpenInfo, double dfYMin, double dfXMax, double dfYMax) { auto oLinks = oRoot["links"].ToArray(); - CPLString osPNG_URL; - CPLString osJPEG_URL; + + // Key - mime type, Value url + std::map oMapItemUrls; for (const auto &oLink : oLinks) { if (oLink["rel"].ToString() == "http://www.opengis.net/def/rel/ogc/1.0/map" && - oLink["type"].ToString() == "image/png") + oLink["type"].IsValid()) { - osPNG_URL = BuildURL(oLink["href"].ToString()); + oMapItemUrls[oLink["type"].ToString()] = + BuildURL(oLink["href"].ToString()); } - else if (oLink["rel"].ToString() == - "http://www.opengis.net/def/rel/ogc/1.0/map" && - oLink["type"].ToString() == "image/jpeg") + else { - osJPEG_URL = BuildURL(oLink["href"].ToString()); + // For lack of additional information assume we are getting some bytes + oMapItemUrls["application/octet-stream"] = + BuildURL(oLink["href"].ToString()); } } - CPLString osImageURL = - SelectImageURL(poOpenInfo->papszOpenOptions, osPNG_URL, osJPEG_URL); + const std::pair oContentUrlPair = + SelectImageURL(poOpenInfo->papszOpenOptions, oMapItemUrls); + const std::string osContentType = oContentUrlPair.first; + const CPLString osImageURL = oContentUrlPair.second; + if (osImageURL.empty()) { CPLError(CE_Failure, CPLE_AppDefined, - "Cannot find link to PNG or JPEG images"); + "Cannot find link to tileset items"); return false; } - const int l_nBands = ((osImageURL == osPNG_URL) ? 4 : 3); + int l_nBands = FigureBands(osContentType, osImageURL); int nOverviewCount = 0; int nLargestDim = std::max(nRasterXSize, nRasterYSize); while (nLargestDim > 256) @@ -1631,12 +1764,14 @@ bool OGCAPIDataset::InitWithTilesAPI(GDALOpenInfo *poOpenInfo, CPLError(CE_Failure, CPLE_AppDefined, "Missing links for tileset"); return false; } - CPLString osPNG_URL; - CPLString osJPEG_URL; + + // Key - mime type, Value url + std::map oMapItemUrls; CPLString osMVT_URL; CPLString osGEOJSON_URL; CPLString osTilingSchemeURL; bool bTilingSchemeURLJson = false; + for (const auto &oLink : oLinks) { const auto osRel = oLink.GetString("rel"); @@ -1657,13 +1792,15 @@ bool OGCAPIDataset::InitWithTilesAPI(GDALOpenInfo *poOpenInfo, } else if (bIsMap) { - if (osRel == "item" && osType == "image/png") + if (osRel == "item" && !osType.empty()) { - osPNG_URL = BuildURL(oLink["href"].ToString()); + oMapItemUrls[osType] = BuildURL(oLink["href"].ToString()); } - else if (osRel == "item" && osType == "image/jpeg") + else if (osRel == "item") { - osJPEG_URL = BuildURL(oLink["href"].ToString()); + // For lack of additional information assume we are getting some bytes + oMapItemUrls["application/octet-stream"] = + BuildURL(oLink["href"].ToString()); } } else @@ -1719,8 +1856,11 @@ bool OGCAPIDataset::InitWithTilesAPI(GDALOpenInfo *poOpenInfo, } } - const CPLString osRasterURL = - SelectImageURL(poOpenInfo->papszOpenOptions, osPNG_URL, osJPEG_URL); + const std::pair oContentUrlPair = + SelectImageURL(poOpenInfo->papszOpenOptions, oMapItemUrls); + const std::string osContentType = oContentUrlPair.first; + const CPLString osRasterURL = oContentUrlPair.second; + const CPLString osVectorURL = SelectVectorFormatURL( poOpenInfo->papszOpenOptions, osMVT_URL, osGEOJSON_URL); if (osRasterURL.empty() && osVectorURL.empty()) @@ -1877,7 +2017,8 @@ bool OGCAPIDataset::InitWithTilesAPI(GDALOpenInfo *poOpenInfo, CPLGetConfigOption("GDAL_WMS_MAX_CONNECTIONS", "5"))); const char *pszTileMatrix = CSLFetchNameValue(poOpenInfo->papszOpenOptions, "TILEMATRIX"); - const int l_nBands = ((osRasterURL == osPNG_URL) ? 4 : 3); + + int l_nBands = FigureBands(osContentType, osRasterURL); for (const auto &tileMatrix : tms->tileMatrixList()) { @@ -2331,37 +2472,17 @@ void OGCAPITiledLayer::ResetReading() GDALDataset *OGCAPITiledLayer::OpenTile(int nX, int nY, bool &bEmptyContent) { - bEmptyContent = false; - CPLString osURL(m_osTileURL); - int nCoalesce = GetCoalesceFactorForRow(nY); if (nCoalesce <= 0) return nullptr; nX = (nX / nCoalesce) * nCoalesce; - osURL.replaceAll("{tileCol}", CPLSPrintf("%d", nX)); - osURL.replaceAll("{tileRow}", CPLSPrintf("%d", nY)); - - CPLString osContentType; - if (!m_poDS->Download(osURL, nullptr, nullptr, m_osTileData, osContentType, - true, nullptr)) - { - return nullptr; - } - bEmptyContent = m_osTileData.empty(); - if (bEmptyContent) - return nullptr; - - CPLString osTempFile; - osTempFile.Printf("/vsimem/ogcapi/%p", this); - VSIFCloseL(VSIFileFromMemBuffer(osTempFile.c_str(), - reinterpret_cast(&m_osTileData[0]), - m_osTileData.size(), false)); + const char *const *papszOpenOptions = nullptr; + CPLString poPrefix; + CPLStringList aosOpenOptions; - GDALDataset *poTileDS; if (m_bIsMVT) { - CPLStringList aosOpenOptions; const double dfOriX = m_bInvertAxis ? m_oTileMatrix.mTopLeftY : m_oTileMatrix.mTopLeftX; const double dfOriY = @@ -2382,16 +2503,16 @@ GDALDataset *OGCAPITiledLayer::OpenTile(int nX, int nY, bool &bEmptyContent) "@GEOREF_TILEDIMY", CPLSPrintf("%.18g", m_oTileMatrix.mResY * m_oTileMatrix.mTileWidth)); - poTileDS = - GDALDataset::Open(("MVT:" + osTempFile).c_str(), GDAL_OF_VECTOR, - nullptr, aosOpenOptions.List()); - } - else - { - poTileDS = GDALDataset::Open(osTempFile.c_str(), GDAL_OF_VECTOR); + + papszOpenOptions = aosOpenOptions.List(); + poPrefix = "MVT"; } - VSIUnlink(osTempFile); - return poTileDS; + + std::unique_ptr dataset = m_poDS->OpenTile( + m_osTileURL, stoi(m_oTileMatrix.mId), nX, nY, bEmptyContent, + GDAL_OF_VECTOR, poPrefix, papszOpenOptions); + + return dataset.release(); } /************************************************************************/ From 9ad3d29d410c7c5aa45ebcfeb0f49d758271f2c0 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 21:36:19 +0100 Subject: [PATCH 066/132] CPLVerifyConfiguration(): make it rely only on static_assert() --- port/cpl_conv.cpp | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/port/cpl_conv.cpp b/port/cpl_conv.cpp index 99f17ed953c2..3491cd6386a5 100644 --- a/port/cpl_conv.cpp +++ b/port/cpl_conv.cpp @@ -83,6 +83,10 @@ #endif #include +#if __cplusplus >= 202002L +#include // For std::endian +#endif + #include "cpl_config.h" #include "cpl_multiproc.h" #include "cpl_string.h" @@ -186,8 +190,6 @@ void *CPLMalloc(size_t nSize) if (nSize == 0) return nullptr; - CPLVerifyConfiguration(); - if ((nSize >> (8 * sizeof(nSize) - 1)) != 0) { // coverity[dead_error_begin] @@ -1564,33 +1566,36 @@ int CPLPrintTime(char *pszBuffer, int nMaxLen, const char *pszFormat, void CPLVerifyConfiguration() { - static bool verified = false; - if (verified) - { - return; - } - verified = true; - /* -------------------------------------------------------------------- */ /* Verify data types. */ /* -------------------------------------------------------------------- */ - CPL_STATIC_ASSERT(sizeof(GInt32) == 4); - CPL_STATIC_ASSERT(sizeof(GInt16) == 2); - CPL_STATIC_ASSERT(sizeof(GByte) == 1); + static_assert(sizeof(short) == 2); // We unfortunately rely on this + static_assert(sizeof(int) == 4); // We unfortunately rely on this + static_assert(sizeof(float) == 4); // We unfortunately rely on this + static_assert(sizeof(double) == 8); // We unfortunately rely on this + static_assert(sizeof(GInt64) == 8); + static_assert(sizeof(GInt32) == 4); + static_assert(sizeof(GInt16) == 2); + static_assert(sizeof(GByte) == 1); /* -------------------------------------------------------------------- */ /* Verify byte order */ /* -------------------------------------------------------------------- */ - GInt32 nTest = 1; - #ifdef CPL_LSB - if (reinterpret_cast(&nTest)[0] != 1) +#if __cplusplus >= 202002L + static_assert(std::endian::native == std::endian::little); +#elif defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) + static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); #endif -#ifdef CPL_MSB - if (reinterpret_cast(&nTest)[3] != 1) +#elif defined(CPL_MSB) +#if __cplusplus >= 202002L + static_assert(std::endian::native == std::endian::big); +#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) + static_assert(__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__); +#endif +#else +#error "CPL_LSB or CPL_MSB must be defined" #endif - CPLError(CE_Fatal, CPLE_AppDefined, - "CPLVerifyConfiguration(): byte order set wrong."); } #ifdef DEBUG_CONFIG_OPTIONS From 0cf5e7d2f1bc8ac2b444d9c0b384ee18fedba176 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 20 Feb 2024 12:31:50 +1000 Subject: [PATCH 067/132] [gpkg] Ensure that mapping tables are inserted into gpkg_contents Fixes auto-created relationship mapping tables cannot be read from gpkg --- autotest/ogr/ogr_gpkg.py | 10 +++++++ doc/source/drivers/vector/gpkg.rst | 6 ++++ .../gpkg/ogrgeopackagedatasource.cpp | 29 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/autotest/ogr/ogr_gpkg.py b/autotest/ogr/ogr_gpkg.py index 6d73b0106f0c..f0e3ae75487b 100755 --- a/autotest/ogr/ogr_gpkg.py +++ b/autotest/ogr/ogr_gpkg.py @@ -7260,6 +7260,16 @@ def get_query_row_count(query): == 1 ) + # validate mapping table was created + assert get_query_row_count("SELECT * FROM 'origin_table_dest_table'") == 0 + # validate mapping table is present in gpkg_contents + assert ( + get_query_row_count( + "SELECT * FROM gpkg_contents WHERE table_name='origin_table_dest_table' AND data_type='attributes'" + ) + == 1 + ) + lyr = ds.CreateLayer("origin_table2", geom_type=ogr.wkbNone) fld_defn = ogr.FieldDefn("o_pkey", ogr.OFTInteger) assert lyr.CreateField(fld_defn) == ogr.OGRERR_NONE diff --git a/doc/source/drivers/vector/gpkg.rst b/doc/source/drivers/vector/gpkg.rst index 8f9425c041cb..c8882d952941 100644 --- a/doc/source/drivers/vector/gpkg.rst +++ b/doc/source/drivers/vector/gpkg.rst @@ -177,6 +177,12 @@ only be updated to change their base or related table fields, or the relationshi table type. It is not permissible to change the base or related table itself, or the mapping table details. If this is desired then a new relationship should be created instead. +Note that when a many-to-many relationship is created in a GeoPackage, GDAL will always +insert the mapping table into the gpkg_contents table. Formally this is not required +by the Related Tables Extension (instead, the table should only be listed in gpkgext_relations), +however failing to list the mapping table in gpkg_contents prevents it from being usable +in some other applications (e.g. ESRI software). + Dataset open options -------------------- diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp index 0763596bc6be..30271c1c3457 100644 --- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp +++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp @@ -9986,6 +9986,35 @@ bool GDALGeoPackageDataset::AddRelationship( return false; } + /* + * Strictly speaking we should NOT be inserting the mapping table into gpkg_contents. + * The related tables extension explicitly states that the mapping table should only be + * in the gpkgext_relations table and not in gpkg_contents. (See also discussion at + * https://github.com/opengeospatial/geopackage/issues/679). + * + * However, if we don't insert the mapping table into gpkg_contents then it is no longer + * visible to some clients (eg ESRI software only allows opening tables that are present + * in gpkg_contents). So we'll do this anyway, for maximum compatiblity and flexibility. + * + * More related discussion is at https://github.com/OSGeo/gdal/pull/9258 + */ + pszSQL = sqlite3_mprintf( + "INSERT INTO gpkg_contents " + "(table_name,data_type,identifier,description,last_change,srs_id) " + "VALUES " + "('%q','attributes','%q','Mapping table for relationship between " + "%q and %q',%s,0)", + osMappingTableName.c_str(), /*table_name*/ + osMappingTableName.c_str(), /*identifier*/ + osLeftTableName.c_str(), /*description left table name*/ + osRightTableName.c_str(), /*description right table name*/ + GDALGeoPackageDataset::GetCurrentDateEscapedSQL().c_str()); + + // Note -- we explicitly ignore failures here, because hey, we aren't really + // supposed to be adding this table to gpkg_contents anyway! + (void)SQLCommand(hDB, pszSQL); + sqlite3_free(pszSQL); + pszSQL = sqlite3_mprintf( "CREATE INDEX \"idx_%w_base_id\" ON \"%w\" (base_id);", osMappingTableName.c_str(), osMappingTableName.c_str()); From f9eb6f97b59f1f35808755e93ff7a0c1f2dc9817 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 21 Feb 2024 09:32:48 +1000 Subject: [PATCH 068/132] [openfilegdb] Correctly use "features" as related table type Previously we reported the table type as "feature", which did not match the accepted values reported by GDAL_DMD_RELATIONSHIP_RELATED_TABLE_TYPES for the drivers (and differed from the "features" string used by other drivers, eg. GPKG). This is purely a GDAL implementation detail, it does not affect how the relationships are actually recorded in the GDB (as we only check for the table type of "media" to apply special handling) --- autotest/ogr/ogr_fgdb.py | 12 ++++++------ autotest/ogr/ogr_openfilegdb.py | 12 ++++++------ autotest/ogr/ogr_openfilegdb_write.py | 4 ++-- autotest/ogr/ogr_pgeo.py | 10 +++++----- autotest/utilities/test_ogrinfo_lib.py | 4 ++-- ogr/ogrsf_frmts/openfilegdb/filegdb_relationship.h | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/autotest/ogr/ogr_fgdb.py b/autotest/ogr/ogr_fgdb.py index da1a1ea8dd3e..f0c483500dcd 100755 --- a/autotest/ogr/ogr_fgdb.py +++ b/autotest/ogr/ogr_fgdb.py @@ -2875,7 +2875,7 @@ def test_ogr_filegdb_read_relationships(openfilegdb_drv, fgdb_drv): assert rel.GetRightMappingTableFields() is None assert rel.GetForwardPathLabel() == "my forward path label" assert rel.GetBackwardPathLabel() == "my backward path label" - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("simple_one_to_many") assert rel is not None @@ -2887,7 +2887,7 @@ def test_ogr_filegdb_read_relationships(openfilegdb_drv, fgdb_drv): assert rel.GetType() == gdal.GRT_ASSOCIATION assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["parent_pk"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("simple_many_to_many") assert rel is not None @@ -2901,7 +2901,7 @@ def test_ogr_filegdb_read_relationships(openfilegdb_drv, fgdb_drv): assert rel.GetLeftMappingTableFields() == ["origin_foreign_key"] assert rel.GetRightTableFields() == ["parent_pk"] assert rel.GetRightMappingTableFields() == ["destination_foreign_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_one_to_one") assert rel is not None @@ -2913,7 +2913,7 @@ def test_ogr_filegdb_read_relationships(openfilegdb_drv, fgdb_drv): assert rel.GetType() == gdal.GRT_COMPOSITE assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["parent_pk"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_one_to_many") assert rel is not None @@ -2925,7 +2925,7 @@ def test_ogr_filegdb_read_relationships(openfilegdb_drv, fgdb_drv): assert rel.GetType() == gdal.GRT_COMPOSITE assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["parent_pk"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_many_to_many") assert rel is not None @@ -2939,7 +2939,7 @@ def test_ogr_filegdb_read_relationships(openfilegdb_drv, fgdb_drv): assert rel.GetLeftMappingTableFields() == ["origin_foreign_key"] assert rel.GetRightTableFields() == ["parent_pk"] assert rel.GetRightMappingTableFields() == ["dest_foreign_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("points__ATTACHREL") assert rel is not None diff --git a/autotest/ogr/ogr_openfilegdb.py b/autotest/ogr/ogr_openfilegdb.py index 901e10ae3990..c9c2ad519b6c 100755 --- a/autotest/ogr/ogr_openfilegdb.py +++ b/autotest/ogr/ogr_openfilegdb.py @@ -2386,7 +2386,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetRightMappingTableFields() is None assert rel.GetForwardPathLabel() == "my forward path label" assert rel.GetBackwardPathLabel() == "my backward path label" - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("simple_one_to_many") assert rel is not None @@ -2398,7 +2398,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetType() == gdal.GRT_ASSOCIATION assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["parent_pk"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("simple_many_to_many") assert rel is not None @@ -2412,7 +2412,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetLeftMappingTableFields() == ["origin_foreign_key"] assert rel.GetRightTableFields() == ["parent_pk"] assert rel.GetRightMappingTableFields() == ["destination_foreign_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_one_to_one") assert rel is not None @@ -2424,7 +2424,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetType() == gdal.GRT_COMPOSITE assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["parent_pk"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_one_to_many") assert rel is not None @@ -2436,7 +2436,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetType() == gdal.GRT_COMPOSITE assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["parent_pk"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_many_to_many") assert rel is not None @@ -2450,7 +2450,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetLeftMappingTableFields() == ["origin_foreign_key"] assert rel.GetRightTableFields() == ["parent_pk"] assert rel.GetRightMappingTableFields() == ["dest_foreign_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("points__ATTACHREL") assert rel is not None diff --git a/autotest/ogr/ogr_openfilegdb_write.py b/autotest/ogr/ogr_openfilegdb_write.py index 7e105bfe8823..690a127879d4 100755 --- a/autotest/ogr/ogr_openfilegdb_write.py +++ b/autotest/ogr/ogr_openfilegdb_write.py @@ -2833,7 +2833,7 @@ def test_ogr_openfilegdb_write_relationships(): assert retrieved_rel.GetRightTableFields() == ["dest_pkey"] assert retrieved_rel.GetForwardPathLabel() == "fwd label" assert retrieved_rel.GetBackwardPathLabel() == "backward label" - assert retrieved_rel.GetRelatedTableType() == "feature" + assert retrieved_rel.GetRelatedTableType() == "features" items_lyr = ds.GetLayerByName("GDB_Items") f = items_lyr.GetFeature(8) @@ -3182,7 +3182,7 @@ def test_ogr_openfilegdb_write_relationships(): assert retrieved_rel.GetRightTableFields() == ["dest_pkey"] assert retrieved_rel.GetForwardPathLabel() == "my new fwd label" assert retrieved_rel.GetBackwardPathLabel() == "my new backward label" - assert retrieved_rel.GetRelatedTableType() == "feature" + assert retrieved_rel.GetRelatedTableType() == "features" # change relationship tables lyr = ds.CreateLayer("new_origin_table", geom_type=ogr.wkbNone) diff --git a/autotest/ogr/ogr_pgeo.py b/autotest/ogr/ogr_pgeo.py index e9e147445335..66f4a3f39aed 100755 --- a/autotest/ogr/ogr_pgeo.py +++ b/autotest/ogr/ogr_pgeo.py @@ -910,7 +910,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetRightMappingTableFields() is None assert rel.GetForwardPathLabel() == "forward label" assert rel.GetBackwardPathLabel() == "backward label" - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("simple_one_to_many") assert rel is not None @@ -922,7 +922,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetType() == gdal.GRT_ASSOCIATION assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["primary_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("simple_many_to_many") assert rel is not None @@ -936,7 +936,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetLeftMappingTableFields() == ["rel_pk"] assert rel.GetRightTableFields() == ["primary_key"] assert rel.GetRightMappingTableFields() == ["rel_primary_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_one_to_one") assert rel is not None @@ -948,7 +948,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetType() == gdal.GRT_COMPOSITE assert rel.GetLeftTableFields() == ["pk"] assert rel.GetRightTableFields() == ["primary_key"] - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("composite_one_to_many") assert rel is not None @@ -962,7 +962,7 @@ def test_ogr_openfilegdb_read_relationships(): assert rel.GetLeftMappingTableFields() == None assert rel.GetRightTableFields() == ["primary_key"] assert rel.GetRightMappingTableFields() == None - assert rel.GetRelatedTableType() == "feature" + assert rel.GetRelatedTableType() == "features" rel = ds.GetRelationship("points__ATTACHREL") assert rel is not None diff --git a/autotest/utilities/test_ogrinfo_lib.py b/autotest/utilities/test_ogrinfo_lib.py index 1fdc10130dce..0cdcbc214fe3 100755 --- a/autotest/utilities/test_ogrinfo_lib.py +++ b/autotest/utilities/test_ogrinfo_lib.py @@ -637,7 +637,7 @@ def test_ogrinfo_lib_relationships(): ret = gdal.VectorInfo(ds) expected = """Relationship: composite_many_to_many Type: Composite - Related table type: feature + Related table type: features Cardinality: ManyToMany Left table name: table6 Right table name: table7 @@ -667,7 +667,7 @@ def test_ogrinfo_lib_json_relationships(): # print(ret["relationships"]["composite_many_to_many"]) assert ret["relationships"]["composite_many_to_many"] == { "type": "Composite", - "related_table_type": "feature", + "related_table_type": "features", "cardinality": "ManyToMany", "left_table_name": "table6", "right_table_name": "table7", diff --git a/ogr/ogrsf_frmts/openfilegdb/filegdb_relationship.h b/ogr/ogrsf_frmts/openfilegdb/filegdb_relationship.h index ca6f1205958e..9381458e71c4 100644 --- a/ogr/ogrsf_frmts/openfilegdb/filegdb_relationship.h +++ b/ogr/ogrsf_frmts/openfilegdb/filegdb_relationship.h @@ -242,7 +242,7 @@ ParseXMLRelationshipDef(const std::string &domainDef) } else { - poRelationship->SetRelatedTableType("feature"); + poRelationship->SetRelatedTableType("features"); } return poRelationship; } From 16f4077199a5229876958e4e1e34a98069858410 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 21 Feb 2024 13:50:16 +0100 Subject: [PATCH 069/132] ogr/ogrsf_frmts/shape/COPYING: remove that LGPL license file The presence of that file causes some confusion. This is for shapelib, and shapelib is licensed as SPDX-License-Identifier: MIT OR LGPL-2.0-or-later We opt for the MIT option for the shapelib copy in GDAL --- ogr/ogrsf_frmts/shape/COPYING | 483 ---------------------------------- 1 file changed, 483 deletions(-) delete mode 100644 ogr/ogrsf_frmts/shape/COPYING diff --git a/ogr/ogrsf_frmts/shape/COPYING b/ogr/ogrsf_frmts/shape/COPYING deleted file mode 100644 index 0b643ac83c8b..000000000000 --- a/ogr/ogrsf_frmts/shape/COPYING +++ /dev/null @@ -1,483 +0,0 @@ - - GNU LIBRARY GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1991 Free Software Foundation, Inc. - 675 Mass Ave, Cambridge, MA 02139, USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the library GPL. It is - numbered 2 because it goes with version 2 of the ordinary GPL.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Library General Public License, applies to some -specially designated Free Software Foundation software, and to any -other libraries whose authors decide to use it. You can use it for -your libraries, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if -you distribute copies of the library, or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link a program with the library, you must provide -complete object files to the recipients so that they can relink them -with the library, after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - Our method of protecting your rights has two steps: (1) copyright -the library, and (2) offer you this license which gives you legal -permission to copy, distribute and/or modify the library. - - Also, for each distributor's protection, we want to make certain -that everyone understands that there is no warranty for this free -library. If the library is modified by someone else and passed on, we -want its recipients to know that what they have is not the original -version, so that any problems introduced by others will not reflect on -the original authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that companies distributing free -software will individually obtain patent licenses, thus in effect -transforming the program into proprietary software. To prevent this, -we have made it clear that any patent must be licensed for everyone's -free use or not licensed at all. - - Most GNU software, including some libraries, is covered by the ordinary -GNU General Public License, which was designed for utility programs. This -license, the GNU Library General Public License, applies to certain -designated libraries. This license is quite different from the ordinary -one; be sure to read it in full, and don't assume that anything in it is -the same as in the ordinary license. - - The reason we have a separate public license for some libraries is that -they blur the distinction we usually make between modifying or adding to a -program and simply using it. Linking a program with a library, without -changing the library, is in some sense simply using the library, and is -analogous to running a utility program or application program. However, in -a textual and legal sense, the linked executable is a combined work, a -derivative of the original library, and the ordinary General Public License -treats it as such. - - Because of this blurred distinction, using the ordinary General -Public License for libraries did not effectively promote software -sharing, because most developers did not use the libraries. We -concluded that weaker conditions might promote sharing better. - - However, unrestricted linking of non-free programs would deprive the -users of those programs of all benefit from the free status of the -libraries themselves. This Library General Public License is intended to -permit developers of non-free programs to use free libraries, while -preserving your freedom as a user of such programs to change the free -libraries that are incorporated in them. (We have not seen how to achieve -this as regards changes in header files, but we have achieved it as regards -changes in the actual functions of the Library.) The hope is that this -will lead to faster development of free libraries. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, while the latter only -works together with the library. - - Note that it is possible for a library to be covered by the ordinary -General Public License rather than by this special one. - - GNU LIBRARY GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library which -contains a notice placed by the copyright holder or other authorized -party saying it may be distributed under the terms of this Library -General Public License (also called "this License"). Each licensee is -addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also compile or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - c) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - d) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the source code distributed need not include anything that is normally -distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Library General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - Appendix: How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! - From 6cb3b1024c2e588ef30fbb3b761c1c9e7cf4dac4 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 21 Feb 2024 15:29:05 +0100 Subject: [PATCH 070/132] CI: try to fix issue with Windows Conda builds Working: - https://github.com/OSGeo/gdal/actions/runs/7987290493/job/21809463985?pr=9270 uses: image windows-2022 20240211.1.0, cmake 3.27.9, MSVC 19.37.32826.1 KO: - https://github.com/OSGeo/gdal/actions/runs/7987537106/job/21814263291?pr=9271 uses: image windows-2022 20240218.2.0, cmake 3.28.3, MSVC 19.38.33135.0 --- .github/workflows/cmake_builds.yml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cmake_builds.yml b/.github/workflows/cmake_builds.yml index 95f2e619023b..f910078d5bf0 100644 --- a/.github/workflows/cmake_builds.yml +++ b/.github/workflows/cmake_builds.yml @@ -417,8 +417,6 @@ jobs: use-mamba: true auto-update-conda: true use-only-tar-bz2: false - - run: | - cmake --version - name: Install dependency shell: bash -l {0} run: | @@ -429,7 +427,11 @@ jobs: cfitsio freexl geotiff libjpeg-turbo libpq libspatialite libwebp-base pcre pcre2 postgresql \ sqlite tiledb zstd cryptopp cgal doxygen librttopo libkml openssl xz \ openjdk ant qhull armadillo blas blas-devel libblas libcblas liblapack liblapacke blosc libarchive \ - arrow-cpp pyarrow libaec + arrow-cpp pyarrow libaec cmake + - name: Check CMake version + shell: bash -l {0} + run: | + cmake --version - name: Install pdfium shell: bash -l {0} run: | @@ -452,7 +454,7 @@ jobs: # Build PDF driver as plugin due to the PDFium build including libopenjp2 symbols which would conflict with external libopenjp2 run: | mkdir -p $GITHUB_WORKSPACE/build - cmake -G "${generator}" -Werror=dev "-DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/install-gdal" "-DUSE_CCACHE=ON" "-DCMAKE_PREFIX_PATH=${CONDA_PREFIX}" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DGDAL_ENABLE_PLUGINS:BOOL=ON -DGDAL_ENABLE_PLUGINS_NO_DEPS:BOOL=ON -DGDAL_USE_PUBLICDECOMPWT:BOOL=ON -DPUBLICDECOMPWT_URL=https://github.com/rouault/PublicDecompWT -DBUILD_JAVA_BINDINGS=OFF -DBUILD_CSHARP_BINDINGS=ON -DGDAL_USE_MYSQL:BOOL=OFF -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DWERROR_DEV_FLAG="-Werror=dev" -DCMAKE_BUILD_TYPE=Release -DPDFIUM_ROOT=$GITHUB_WORKSPACE/install-pdfium -DGDAL_ENABLE_DRIVER_PDF_PLUGIN:BOOL=ON -DCMAKE_UNITY_BUILD=ON + cmake -G "${generator}" -Werror=dev "-DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/install-gdal" "-DUSE_CCACHE=ON" "-DCMAKE_PREFIX_PATH=${CONDA}/envs/gdalenv" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DGDAL_ENABLE_PLUGINS:BOOL=ON -DGDAL_ENABLE_PLUGINS_NO_DEPS:BOOL=ON -DGDAL_USE_PUBLICDECOMPWT:BOOL=ON -DPUBLICDECOMPWT_URL=https://github.com/rouault/PublicDecompWT -DBUILD_JAVA_BINDINGS=OFF -DBUILD_CSHARP_BINDINGS=ON -DGDAL_USE_MYSQL:BOOL=OFF -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DWERROR_DEV_FLAG="-Werror=dev" -DCMAKE_BUILD_TYPE=Release -DPDFIUM_ROOT=$GITHUB_WORKSPACE/install-pdfium -DGDAL_ENABLE_DRIVER_PDF_PLUGIN:BOOL=ON -DCMAKE_UNITY_BUILD=ON - name: Build shell: bash -l {0} run: cmake --build $GITHUB_WORKSPACE/build --config Release -j 2 @@ -512,17 +514,19 @@ jobs: use-mamba: true auto-update-conda: true use-only-tar-bz2: false - - run: | - cmake --version - name: Install dependency shell: bash -l {0} run: | - conda install --yes --quiet proj pytest pytest-env pytest-benchmark filelock lxml + conda install --yes --quiet proj pytest pytest-env pytest-benchmark filelock lxml cmake + - name: Check CMake version + shell: bash -l {0} + run: | + cmake --version - name: Configure shell: bash -l {0} run: | mkdir -p $GITHUB_WORKSPACE/build - cmake -A ${architecture} -G "${generator}" -Werror=dev "-DCMAKE_CXX_COMPILER_LAUNCHER=clcache" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DGDAL_USE_EXTERNAL_LIBS:BOOL=OFF -DWERROR_DEV_FLAG="-Werror=dev" + cmake -A ${architecture} -G "${generator}" "-DCMAKE_PREFIX_PATH=${CONDA}/envs/gdalenv" -Werror=dev "-DCMAKE_CXX_COMPILER_LAUNCHER=clcache" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DGDAL_USE_EXTERNAL_LIBS:BOOL=OFF -DWERROR_DEV_FLAG="-Werror=dev" - name: Build shell: bash -l {0} run: cmake --build $GITHUB_WORKSPACE/build --config RelWithDebInfo -j 2 @@ -530,12 +534,12 @@ jobs: shell: bash -l {0} run: | rm -f build/CMakeCache.txt - cmake -A ${architecture} -G "${generator}" -Werror=dev "-DCMAKE_CXX_COMPILER_LAUNCHER=clcache" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DGDAL_USE_EXTERNAL_LIBS:BOOL=OFF -DGDAL_USE_PNG_INTERNAL=OFF -DGDAL_USE_JPEG_INTERNAL=OFF -DGDAL_USE_JPEG12_INTERNAL=OFF -DGDAL_USE_GIF_INTERNAL=OFF -DGDAL_USE_LERC_INTERNAL=OFF -DGDAL_USE_LERCV1_INTERNAL=OFF -DGDAL_USE_QHULL_INTERNAL=OFF -DGDAL_USE_OPENCAD_INTERNAL=OFF -DWERROR_DEV_FLAG="-Werror=dev" + cmake -A ${architecture} -G "${generator}" "-DCMAKE_PREFIX_PATH=${CONDA}/envs/gdalenv" -Werror=dev "-DCMAKE_CXX_COMPILER_LAUNCHER=clcache" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DGDAL_USE_EXTERNAL_LIBS:BOOL=OFF -DGDAL_USE_PNG_INTERNAL=OFF -DGDAL_USE_JPEG_INTERNAL=OFF -DGDAL_USE_JPEG12_INTERNAL=OFF -DGDAL_USE_GIF_INTERNAL=OFF -DGDAL_USE_LERC_INTERNAL=OFF -DGDAL_USE_LERCV1_INTERNAL=OFF -DGDAL_USE_QHULL_INTERNAL=OFF -DGDAL_USE_OPENCAD_INTERNAL=OFF -DWERROR_DEV_FLAG="-Werror=dev" - name: Configure with even less dependencies, and disabling all optional drivers shell: bash -l {0} run: | rm -f build/CMakeCache.txt - cmake -A ${architecture} -G "${generator}" -Werror=dev "-DCMAKE_CXX_COMPILER_LAUNCHER=clcache" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DGDAL_USE_EXTERNAL_LIBS:BOOL=OFF -DGDAL_USE_PNG_INTERNAL=OFF -DGDAL_USE_JPEG_INTERNAL=OFF -DGDAL_USE_JPEG12_INTERNAL=OFF -DGDAL_USE_GIF_INTERNAL=OFF -DGDAL_USE_LERC_INTERNAL=OFF -DGDAL_USE_LERCV1_INTERNAL=OFF -DGDAL_USE_QHULL_INTERNAL=OFF -DGDAL_USE_OPENCAD_INTERNAL=OFF -DGDAL_BUILD_OPTIONAL_DRIVERS=OFF -DOGR_BUILD_OPTIONAL_DRIVERS=OFF -DWERROR_DEV_FLAG="-Werror=dev" + cmake -A ${architecture} -G "${generator}" "-DCMAKE_PREFIX_PATH=${CONDA}/envs/gdalenv" -Werror=dev "-DCMAKE_CXX_COMPILER_LAUNCHER=clcache" -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build" -DCMAKE_C_FLAGS=" /WX" -DCMAKE_CXX_FLAGS=" /WX" -DGDAL_USE_EXTERNAL_LIBS:BOOL=OFF -DGDAL_USE_PNG_INTERNAL=OFF -DGDAL_USE_JPEG_INTERNAL=OFF -DGDAL_USE_JPEG12_INTERNAL=OFF -DGDAL_USE_GIF_INTERNAL=OFF -DGDAL_USE_LERC_INTERNAL=OFF -DGDAL_USE_LERCV1_INTERNAL=OFF -DGDAL_USE_QHULL_INTERNAL=OFF -DGDAL_USE_OPENCAD_INTERNAL=OFF -DGDAL_BUILD_OPTIONAL_DRIVERS=OFF -DOGR_BUILD_OPTIONAL_DRIVERS=OFF -DWERROR_DEV_FLAG="-Werror=dev" - name: Build shell: bash -l {0} run: cmake --build $GITHUB_WORKSPACE/build --config RelWithDebInfo -j 2 From 993c1ed2208d9470a8be3705fdf251a2d2d5ed4d Mon Sep 17 00:00:00 2001 From: Dave Martinez Date: Thu, 22 Feb 2024 13:23:09 +1100 Subject: [PATCH 071/132] Dockerfile Ubuntu - Remove apache arrow deb file --- docker/ubuntu-full/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/ubuntu-full/Dockerfile b/docker/ubuntu-full/Dockerfile index c85744b94af0..21ce77bc742f 100644 --- a/docker/ubuntu-full/Dockerfile +++ b/docker/ubuntu-full/Dockerfile @@ -243,7 +243,8 @@ RUN . /buildscripts/bh-set-envvars.sh \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -V libparquet-dev${APT_ARCH_SUFFIX}=${ARROW_VERSION} \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -V libarrow-acero-dev${APT_ARCH_SUFFIX}=${ARROW_VERSION} \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -V libarrow-dataset-dev${APT_ARCH_SUFFIX}=${ARROW_VERSION} \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && rm apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb RUN apt-get update -y \ && apt-get install -y --fix-missing --no-install-recommends rsync ccache \ From c5c1e9ef89e3590788400a7497178356002aecd2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 22 Feb 2024 12:57:46 +1000 Subject: [PATCH 072/132] [gpkg] Ensure that tables present in gpkgext_relations can be read Tables which are listed in gpkgext_relations but NOT gpkg_contents should be readable as valid layers. The GeoPackage related tables extension explicitly states that tables in gpkgext_relations are not required to be listed in gpkg_contents. --- autotest/ogr/ogr_gpkg.py | 22 +++++++++++++++++++ .../gpkg/ogrgeopackagedatasource.cpp | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/autotest/ogr/ogr_gpkg.py b/autotest/ogr/ogr_gpkg.py index f0e3ae75487b..936b91c0577f 100755 --- a/autotest/ogr/ogr_gpkg.py +++ b/autotest/ogr/ogr_gpkg.py @@ -7008,6 +7008,13 @@ def test_ogr_gpkg_relations(tmp_vsimem, tmp_path): assert rel.GetRightMappingTableFields() == ["related_id"] assert rel.GetRelatedTableType() == "attributes" + # ensure that the mapping table, which is present in gpkgext_relations but + # NOT gpkg_contents can be opened as a layer + lyr = ds.GetLayer("my_mapping_table") + assert lyr is not None + assert lyr.GetLayerDefn().GetFieldDefn(0).GetName() == "base_id" + assert lyr.GetLayerDefn().GetFieldDefn(1).GetName() == "related_id" + lyr = ds.GetLayer("a") lyr.Rename("a_renamed") lyr.AlterFieldDefn( @@ -7269,6 +7276,21 @@ def get_query_row_count(query): ) == 1 ) + # force delete from gpkg_contents, and then ensure that we CAN successfully + # load layers which are present ONLY in gpkgext_relations but NOT + # gpkg_contents (i.e. datasources which follow the Related Tables specification + # exactly) + ds.ExecuteSQL( + "DELETE FROM gpkg_contents WHERE table_name='origin_table_dest_table'" + ) + + ds = gdal.OpenEx(filename, gdal.OF_VECTOR | gdal.OF_UPDATE) + # ensure that the mapping table, which is present in gpkgext_relations but + # NOT gpkg_contents can be opened as a layer + lyr = ds.GetLayer("origin_table_dest_table") + assert lyr is not None + assert lyr.GetLayerDefn().GetFieldDefn(0).GetName() == "base_id" + assert lyr.GetLayerDefn().GetFieldDefn(1).GetName() == "related_id" lyr = ds.CreateLayer("origin_table2", geom_type=ogr.wkbNone) fld_defn = ogr.FieldDefn("o_pkey", ogr.OFTInteger) diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp index 30271c1c3457..80eb0d7ac7a3 100644 --- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp +++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp @@ -1567,6 +1567,7 @@ int GDALGeoPackageDataset::Open(GDALOpenInfo *poOpenInfo, CheckUnknownExtensions(); int bRet = FALSE; + bool bHasGPKGExtRelations = false; if (poOpenInfo->nOpenFlags & GDAL_OF_VECTOR) { m_bHasGPKGGeometryColumns = @@ -1575,6 +1576,7 @@ int GDALGeoPackageDataset::Open(GDALOpenInfo *poOpenInfo, "name = 'gpkg_geometry_columns' AND " "type IN ('table', 'view')", nullptr) == 1; + bHasGPKGExtRelations = HasGpkgextRelationsTable(); } if (m_bHasGPKGGeometryColumns) { @@ -1614,6 +1616,18 @@ int GDALGeoPackageDataset::Open(GDALOpenInfo *poOpenInfo, bHasASpatialOrAttributes = (oResultTable && oResultTable->RowCount() == 1); } + if (bHasGPKGExtRelations) + { + osSQL += "UNION ALL " + "SELECT mapping_table_name, mapping_table_name, 0 as " + "is_spatial, NULL, NULL, 0, 0, 0 AS " + "xmin, 0 AS ymin, 0 AS xmax, 0 AS ymax, 0 AS " + "is_in_gpkg_contents, 'table' AS object_type " + "FROM gpkgext_relations WHERE " + "lower(mapping_table_name) NOT IN (SELECT " + "lower(table_name) FROM " + "gpkg_contents)"; + } if (EQUAL(pszListAllTables, "YES") || (!bHasASpatialOrAttributes && EQUAL(pszListAllTables, "AUTO"))) { @@ -1632,6 +1646,12 @@ int GDALGeoPackageDataset::Open(GDALOpenInfo *poOpenInfo, "'st_geometry_columns', 'geometry_columns') " "AND lower(name) NOT IN (SELECT lower(table_name) FROM " "gpkg_contents)"; + if (bHasGPKGExtRelations) + { + osSQL += " AND lower(name) NOT IN (SELECT " + "lower(mapping_table_name) FROM " + "gpkgext_relations)"; + } } const int nTableLimit = GetOGRTableLimit(); if (nTableLimit > 0) From 410b295b40f0e0c2ec18e11e847164d9d338cae1 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 22 Feb 2024 12:47:33 +0100 Subject: [PATCH 073/132] gdal2tiles.py: fix exception when -v flag is used and overview tiles are generated (3.7.0 regression) (fixes #9272) --- autotest/pyscripts/gdal2tiles/test_logger.py | 72 +++++++++++++++++++ autotest/pyscripts/gdal2tiles/test_vsimem.py | 4 +- .../gdal-utils/osgeo_utils/gdal2tiles.py | 5 +- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 autotest/pyscripts/gdal2tiles/test_logger.py diff --git a/autotest/pyscripts/gdal2tiles/test_logger.py b/autotest/pyscripts/gdal2tiles/test_logger.py new file mode 100644 index 000000000000..de9cd087b1bd --- /dev/null +++ b/autotest/pyscripts/gdal2tiles/test_logger.py @@ -0,0 +1,72 @@ +#!/usr/bin/env pytest +# -*- coding: utf-8 -*- +############################################################################### +# $Id$ +# +# Project: GDAL/OGR Test Suite +# Purpose: gdal2tiles.py testing +# Author: Even Rouault +# +############################################################################### +# Copyright (c) 2024, 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. +############################################################################### + +import pytest + +from osgeo import gdal +from osgeo_utils import gdal2tiles + + +def test_gdal2tiles_logger(): + + if gdal.GetDriverByName("PNG") is None: + pytest.skip("PNG driver is missing") + + gdal2tiles.main( + argv=[ + "gdal2tiles", + "--verbose", + "-z", + "13-14", + "../../gcore/data/byte.tif", + "/vsimem/gdal2tiles", + ] + ) + + assert set(gdal.ReadDirRecursive("/vsimem/gdal2tiles")) == set( + [ + "13/", + "13/1418/", + "13/1418/4916.png", + "13/1419/", + "13/1419/4916.png", + "14/", + "14/2837/", + "14/2837/9833.png", + "14/2838/", + "14/2838/9833.png", + "googlemaps.html", + "leaflet.html", + "openlayers.html", + "tilemapresource.xml", + ] + ) + gdal.RmdirRecursive("/vsimem/gdal2tiles") diff --git a/autotest/pyscripts/gdal2tiles/test_vsimem.py b/autotest/pyscripts/gdal2tiles/test_vsimem.py index c6c61ce946d3..340e0238f7c4 100644 --- a/autotest/pyscripts/gdal2tiles/test_vsimem.py +++ b/autotest/pyscripts/gdal2tiles/test_vsimem.py @@ -40,7 +40,9 @@ def test_gdal2tiles_vsimem(): if gdal.GetDriverByName("PNG") is None: pytest.skip("PNG driver is missing") - gdal2tiles.main(argv=["-q", "../../gcore/data/byte.tif", "/vsimem/gdal2tiles"]) + gdal2tiles.main( + argv=["gdal2tiles", "-q", "../../gcore/data/byte.tif", "/vsimem/gdal2tiles"] + ) assert set(gdal.ReadDirRecursive("/vsimem/gdal2tiles")) == set( [ diff --git a/swig/python/gdal-utils/osgeo_utils/gdal2tiles.py b/swig/python/gdal-utils/osgeo_utils/gdal2tiles.py index 07a4c556a012..4f9198d0e654 100644 --- a/swig/python/gdal-utils/osgeo_utils/gdal2tiles.py +++ b/swig/python/gdal-utils/osgeo_utils/gdal2tiles.py @@ -1574,7 +1574,10 @@ def create_overview_tile( gdal.Unlink(aux_xml) if options.verbose: - logger.debug(f"\tbuild from zoom {base_tz}, tiles: %s" % ",".join(base_tiles)) + logger.debug( + f"\tbuild from zoom {base_tz}, tiles: %s" + % ",".join(["(%d, %d)" % (t[0], t[1]) for t in base_tiles]) + ) # Create a KML file for this tile. if tile_job_info.kml: From a84650057e093a0511847283cc5757afa5242e66 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 22 Feb 2024 17:11:43 +0100 Subject: [PATCH 074/132] Doc: sponsors: add Satelligence as a supporter level sponsor --- doc/source/sponsors/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/sponsors/index.rst b/doc/source/sponsors/index.rst index bd17ad8a0f44..059f558eea7d 100644 --- a/doc/source/sponsors/index.rst +++ b/doc/source/sponsors/index.rst @@ -131,6 +131,10 @@ the health of the project: `Regrid `__ + .. container:: horizontal-logo + + `Satelligence `__ + .. container:: horizontal-logo `Space Intelligence `__ From 502f1a37a98e81a7acfa2f43bfcf99043f362158 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 22 Feb 2024 15:14:41 +1000 Subject: [PATCH 075/132] [gpkg] Also read relationships defined using foreign key constraints When reading relationships, always read relationships defined using foreign key constraints regardless of whether or not the related tables extension is in use. The related table extension only permits definition of many-to-many relationships, so there's a strong case for supporting one-to-many relationships defined outside of this extension. In fact it's what's recommended upstream: https://github.com/opengeospatial/geopackage/issues/678#issuecomment-1954402113 --- autotest/ogr/ogr_gpkg.py | 27 ++++++++++ doc/source/drivers/vector/gpkg.rst | 5 +- ogr/ogrsf_frmts/gpkg/ogr_geopackage.h | 2 +- .../gpkg/ogrgeopackagedatasource.cpp | 18 +++++-- ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h | 5 +- .../sqlite/ogrsqlitedatasource.cpp | 51 +++++++++++++++---- 6 files changed, 89 insertions(+), 19 deletions(-) diff --git a/autotest/ogr/ogr_gpkg.py b/autotest/ogr/ogr_gpkg.py index 936b91c0577f..9f49f594b8fa 100755 --- a/autotest/ogr/ogr_gpkg.py +++ b/autotest/ogr/ogr_gpkg.py @@ -7093,6 +7093,33 @@ def test_ogr_gpkg_relations(tmp_vsimem, tmp_path): assert rel.GetRightMappingTableFields() == ["related_id"] assert rel.GetRelatedTableType() == "features" + # a one-to-many relationship defined using foreign key constraints + ds = gdal.OpenEx(filename, gdal.OF_VECTOR | gdal.OF_UPDATE) + ds.ExecuteSQL( + "CREATE TABLE test_relation_a(artistid INTEGER PRIMARY KEY, artistname TEXT)" + ) + ds.ExecuteSQL( + "CREATE TABLE test_relation_b(trackid INTEGER, trackname TEXT, trackartist INTEGER, FOREIGN KEY(trackartist) REFERENCES test_relation_a(artistid))" + ) + ds = None + + ds = gdal.OpenEx(filename, gdal.OF_VECTOR | gdal.OF_UPDATE) + assert ds.GetRelationshipNames() == [ + "custom_type", + "test_relation_a_test_relation_b", + ] + assert ds.GetRelationship("custom_type") is not None + rel = ds.GetRelationship("test_relation_a_test_relation_b") + assert rel is not None + assert rel.GetName() == "test_relation_a_test_relation_b" + assert rel.GetLeftTableName() == "test_relation_a" + assert rel.GetRightTableName() == "test_relation_b" + assert rel.GetCardinality() == gdal.GRC_ONE_TO_MANY + assert rel.GetType() == gdal.GRT_ASSOCIATION + assert rel.GetLeftTableFields() == ["artistid"] + assert rel.GetRightTableFields() == ["trackartist"] + assert rel.GetRelatedTableType() == "features" + ds = None diff --git a/doc/source/drivers/vector/gpkg.rst b/doc/source/drivers/vector/gpkg.rst index c8882d952941..798192145868 100644 --- a/doc/source/drivers/vector/gpkg.rst +++ b/doc/source/drivers/vector/gpkg.rst @@ -168,9 +168,8 @@ Relationships .. versionadded:: 3.6 -Relationship retrieval is supported, respecting the OGC GeoPackage Related Tables Extension. -If the Related Tables Extension is not in use then relationships will be reported for tables -which utilize FOREIGN KEY constraints. +Many-to-many relationship retrieval is supported, respecting the OGC GeoPackage Related Tables Extension. +One-to-many relationships will also be reported for tables which utilize FOREIGN KEY constraints. Relationship creation, deletion and updating is supported since GDAL 3.7. Relationships can only be updated to change their base or related table fields, or the relationship related diff --git a/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h b/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h index 2456e58a7fd3..cf6a7ca9f410 100644 --- a/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h +++ b/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h @@ -280,7 +280,7 @@ class GDALGeoPackageDataset final : public OGRSQLiteBaseDataSource, void FixupWrongRTreeTrigger(); void FixupWrongMedataReferenceColumnNameUpdate(); void ClearCachedRelationships(); - void LoadRelationships() const; + void LoadRelationships() const override; void LoadRelationshipsUsingRelatedTablesExtension() const; static std::string GenerateNameForRelationship(const char *pszBaseTableName, diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp index 80eb0d7ac7a3..d370f76c05d8 100644 --- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp +++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp @@ -2038,14 +2038,24 @@ void GDALGeoPackageDataset::ClearCachedRelationships() void GDALGeoPackageDataset::LoadRelationships() const { + m_osMapRelationships.clear(); + + std::vector oExcludedTables; if (HasGpkgextRelationsTable()) { LoadRelationshipsUsingRelatedTablesExtension(); + + for (const auto &oRelationship : m_osMapRelationships) + { + oExcludedTables.emplace_back( + oRelationship.second->GetMappingTableName()); + } } - else - { - LoadRelationshipsFromForeignKeys(); - } + + // Also load relationships defined using foreign keys (i.e. one-to-many + // relationships). Here we must exclude any relationships defined from the + // related tables extension, we don't want them included twice. + LoadRelationshipsFromForeignKeys(oExcludedTables); m_bHasPopulatedRelationships = true; } diff --git a/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h b/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h index 0be1906e8ccf..650196827f7c 100644 --- a/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h +++ b/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h @@ -216,7 +216,10 @@ class OGRSQLiteBaseDataSource CPL_NON_FINAL : public GDALPamDataset OGRErr PragmaCheck(const char *pszPragma, const char *pszExpected, int nRowsExpected); - void LoadRelationshipsFromForeignKeys() const; + virtual void LoadRelationships() const; + + void LoadRelationshipsFromForeignKeys( + const std::vector &excludedTables) const; bool IsSpatialiteLoaded(); static int MakeSpatialiteVersionNumber(int x, int y, int z) diff --git a/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp b/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp index 65cd215ff3cb..3aa14d3ee8bd 100644 --- a/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp +++ b/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp @@ -284,7 +284,9 @@ std::vector OGRSQLiteDataSource::GetRelationshipNames( { if (!m_bHasPopulatedRelationships) - LoadRelationshipsFromForeignKeys(); + { + LoadRelationships(); + } std::vector oasNames; oasNames.reserve(m_osMapRelationships.size()); @@ -305,7 +307,9 @@ OGRSQLiteDataSource::GetRelationship(const std::string &name) const { if (!m_bHasPopulatedRelationships) - LoadRelationshipsFromForeignKeys(); + { + LoadRelationships(); + } auto it = m_osMapRelationships.find(name); if (it == m_osMapRelationships.end()) @@ -704,18 +708,28 @@ OGRErr OGRSQLiteBaseDataSource::PragmaCheck(const char *pszPragma, } /************************************************************************/ -/* LoadRelationshipsFromForeignKeys() */ +/* LoadRelationships() */ /************************************************************************/ -void OGRSQLiteBaseDataSource::LoadRelationshipsFromForeignKeys() const +void OGRSQLiteBaseDataSource::LoadRelationships() const { m_osMapRelationships.clear(); + LoadRelationshipsFromForeignKeys({}); + m_bHasPopulatedRelationships = true; +} + +/************************************************************************/ +/* LoadRelationshipsFromForeignKeys() */ +/************************************************************************/ + +void OGRSQLiteBaseDataSource::LoadRelationshipsFromForeignKeys( + const std::vector &excludedTables) const +{ if (hDB) { - auto oResult = SQLQuery( - hDB, + std::string osSQL = "SELECT m.name, p.id, p.seq, p.\"table\" AS base_table_name, " "p.\"from\", p.\"to\", " "p.on_delete FROM sqlite_master m " @@ -728,8 +742,27 @@ void OGRSQLiteBaseDataSource::LoadRelationshipsFromForeignKeys() const // Same with Spatialite system tables "AND base_table_name NOT IN ('geometry_columns', " "'spatial_ref_sys', 'views_geometry_columns', " - "'virts_geometry_columns') " - "ORDER BY m.name"); + "'virts_geometry_columns') "; + if (!excludedTables.empty()) + { + std::string oExcludedTablesList; + for (const auto &osExcludedTable : excludedTables) + { + oExcludedTablesList += !oExcludedTablesList.empty() ? "," : ""; + char *pszEscapedName = + sqlite3_mprintf("'%q'", osExcludedTable.c_str()); + oExcludedTablesList += pszEscapedName; + sqlite3_free(pszEscapedName); + } + + osSQL += "AND base_table_name NOT IN (" + oExcludedTablesList + + ")" + " AND m.name NOT IN (" + + oExcludedTablesList + ") "; + } + osSQL += "ORDER BY m.name"; + + auto oResult = SQLQuery(hDB, osSQL.c_str()); if (!oResult) { @@ -806,8 +839,6 @@ void OGRSQLiteBaseDataSource::LoadRelationshipsFromForeignKeys() const std::move(poRelationship); } } - - m_bHasPopulatedRelationships = true; } } From 9d207db65e480a4f0f0ec2b336fc9e8bb4f7531f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 23 Feb 2024 09:26:19 +1000 Subject: [PATCH 076/132] Avoid some duplicate code by moving identical methods to base class --- ogr/ogrsf_frmts/gpkg/ogr_geopackage.h | 6 -- .../gpkg/ogrgeopackagedatasource.cpp | 39 --------- ogr/ogrsf_frmts/sqlite/ogr_sqlite.h | 6 -- ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h | 5 +- .../sqlite/ogrsqlitedatasource.cpp | 86 +++++++++---------- 5 files changed, 47 insertions(+), 95 deletions(-) diff --git a/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h b/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h index cf6a7ca9f410..d93290259a84 100644 --- a/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h +++ b/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h @@ -345,12 +345,6 @@ class GDALGeoPackageDataset final : public OGRSQLiteBaseDataSource, bool AddFieldDomain(std::unique_ptr &&domain, std::string &failureReason) override; - std::vector - GetRelationshipNames(CSLConstList papszOptions = nullptr) const override; - - const GDALRelationship * - GetRelationship(const std::string &name) const override; - bool AddRelationship(std::unique_ptr &&relationship, std::string &failureReason) override; diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp index d370f76c05d8..484ea32f100f 100644 --- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp +++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp @@ -9823,45 +9823,6 @@ bool GDALGeoPackageDataset::AddFieldDomain( return true; } -/************************************************************************/ -/* GetRelationshipNames() */ -/************************************************************************/ - -std::vector GDALGeoPackageDataset::GetRelationshipNames( - CPL_UNUSED CSLConstList papszOptions) const - -{ - if (!m_bHasPopulatedRelationships) - LoadRelationships(); - - std::vector oasNames; - oasNames.reserve(m_osMapRelationships.size()); - for (auto it = m_osMapRelationships.begin(); - it != m_osMapRelationships.end(); ++it) - { - oasNames.emplace_back(it->first); - } - return oasNames; -} - -/************************************************************************/ -/* GetRelationship() */ -/************************************************************************/ - -const GDALRelationship * -GDALGeoPackageDataset::GetRelationship(const std::string &name) const - -{ - if (!m_bHasPopulatedRelationships) - LoadRelationships(); - - auto it = m_osMapRelationships.find(name); - if (it == m_osMapRelationships.end()) - return nullptr; - - return it->second.get(); -} - /************************************************************************/ /* AddRelationship() */ /************************************************************************/ diff --git a/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h b/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h index 1180c44a241e..da32aa6a63f8 100644 --- a/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h +++ b/ogr/ogrsf_frmts/sqlite/ogr_sqlite.h @@ -792,12 +792,6 @@ class OGRSQLiteDataSource final : public OGRSQLiteBaseDataSource return m_bHaveGeometryColumns; } - std::vector - GetRelationshipNames(CSLConstList papszOptions = nullptr) const override; - - const GDALRelationship * - GetRelationship(const std::string &name) const override; - bool AddRelationship(std::unique_ptr &&relationship, std::string &failureReason) override; bool ValidateRelationship(const GDALRelationship *poRelationship, diff --git a/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h b/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h index 650196827f7c..8031fd796fbd 100644 --- a/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h +++ b/ogr/ogrsf_frmts/sqlite/ogrsqlitebase.h @@ -217,9 +217,12 @@ class OGRSQLiteBaseDataSource CPL_NON_FINAL : public GDALPamDataset int nRowsExpected); virtual void LoadRelationships() const; - void LoadRelationshipsFromForeignKeys( const std::vector &excludedTables) const; + std::vector + GetRelationshipNames(CSLConstList papszOptions = nullptr) const override; + const GDALRelationship * + GetRelationship(const std::string &name) const override; bool IsSpatialiteLoaded(); static int MakeSpatialiteVersionNumber(int x, int y, int z) diff --git a/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp b/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp index 3aa14d3ee8bd..bddea3f46272 100644 --- a/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp +++ b/ogr/ogrsf_frmts/sqlite/ogrsqlitedatasource.cpp @@ -275,49 +275,6 @@ int OGRSQLiteBaseDataSource::GetSpatialiteVersionNumber() return v; } -/************************************************************************/ -/* GetRelationshipNames() */ -/************************************************************************/ - -std::vector OGRSQLiteDataSource::GetRelationshipNames( - CPL_UNUSED CSLConstList papszOptions) const - -{ - if (!m_bHasPopulatedRelationships) - { - LoadRelationships(); - } - - std::vector oasNames; - oasNames.reserve(m_osMapRelationships.size()); - for (auto it = m_osMapRelationships.begin(); - it != m_osMapRelationships.end(); ++it) - { - oasNames.emplace_back(it->first); - } - return oasNames; -} - -/************************************************************************/ -/* GetRelationship() */ -/************************************************************************/ - -const GDALRelationship * -OGRSQLiteDataSource::GetRelationship(const std::string &name) const - -{ - if (!m_bHasPopulatedRelationships) - { - LoadRelationships(); - } - - auto it = m_osMapRelationships.find(name); - if (it == m_osMapRelationships.end()) - return nullptr; - - return it->second.get(); -} - /************************************************************************/ /* AddRelationship() */ /************************************************************************/ @@ -842,6 +799,49 @@ void OGRSQLiteBaseDataSource::LoadRelationshipsFromForeignKeys( } } +/************************************************************************/ +/* GetRelationshipNames() */ +/************************************************************************/ + +std::vector OGRSQLiteBaseDataSource::GetRelationshipNames( + CPL_UNUSED CSLConstList papszOptions) const + +{ + if (!m_bHasPopulatedRelationships) + { + LoadRelationships(); + } + + std::vector oasNames; + oasNames.reserve(m_osMapRelationships.size()); + for (auto it = m_osMapRelationships.begin(); + it != m_osMapRelationships.end(); ++it) + { + oasNames.emplace_back(it->first); + } + return oasNames; +} + +/************************************************************************/ +/* GetRelationship() */ +/************************************************************************/ + +const GDALRelationship * +OGRSQLiteBaseDataSource::GetRelationship(const std::string &name) const + +{ + if (!m_bHasPopulatedRelationships) + { + LoadRelationships(); + } + + auto it = m_osMapRelationships.find(name); + if (it == m_osMapRelationships.end()) + return nullptr; + + return it->second.get(); +} + /***********************************************************************/ /* prepareSql() */ /***********************************************************************/ From fecb13af88ffda32ef2ba65a91d902e77801407f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Fri, 23 Feb 2024 18:33:07 +0100 Subject: [PATCH 077/132] PMTiles: fix 'Non increasing tile_id' error when opening some files (fixes #9288) --- .../ogr/data/pmtiles/subset7_truncated.pmtiles | Bin 0 -> 30000 bytes autotest/ogr/ogr_pmtiles.py | 12 ++++++++++++ ogr/ogrsf_frmts/pmtiles/ogr_pmtiles.h | 4 +++- .../pmtiles/ogrpmtilestileiterator.cpp | 5 +++-- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 autotest/ogr/data/pmtiles/subset7_truncated.pmtiles diff --git a/autotest/ogr/data/pmtiles/subset7_truncated.pmtiles b/autotest/ogr/data/pmtiles/subset7_truncated.pmtiles new file mode 100644 index 0000000000000000000000000000000000000000..44057c2036e3226f0a999d2ad749f888c756ede4 GIT binary patch literal 30000 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::max(); + uint64_t m_nLastTileId = INVALID_LAST_TILE_ID; // Computed values from zoom leven and min/max x/y uint64_t m_nMinTileId = std::numeric_limits::max(); diff --git a/ogr/ogrsf_frmts/pmtiles/ogrpmtilestileiterator.cpp b/ogr/ogrsf_frmts/pmtiles/ogrpmtilestileiterator.cpp index 674f685e6f62..25e40f4dedce 100644 --- a/ogr/ogrsf_frmts/pmtiles/ogrpmtilestileiterator.cpp +++ b/ogr/ogrsf_frmts/pmtiles/ogrpmtilestileiterator.cpp @@ -250,7 +250,7 @@ pmtiles::entry_zxy OGRPMTilesTileIterator::GetNextTile(uint32_t *pnRunLength) static_cast(m_nZoomLevel), m_nCurX, m_nCurY); m_nMaxTileId = m_nMinTileId; - m_nLastTileId = 0; + m_nLastTileId = INVALID_LAST_TILE_ID; while (m_aoStack.size() > 1) m_aoStack.pop(); const int nMinEntryIdx = find_tile_idx_lesser_or_equal( @@ -323,7 +323,8 @@ pmtiles::entry_zxy OGRPMTilesTileIterator::GetNextTile(uint32_t *pnRunLength) break; } - if (sContext.sEntries[0].tile_id <= m_nLastTileId) + if (m_nLastTileId != INVALID_LAST_TILE_ID && + sContext.sEntries[0].tile_id <= m_nLastTileId) { CPLError(CE_Failure, CPLE_AppDefined, "Non increasing tile_id"); From 54166ef9a6d9a4d2a2c8364b8a2300a95ff9f228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sat, 24 Feb 2024 14:38:15 +0800 Subject: [PATCH 078/132] Update gdal_grid.rst to discuss creating multiband files For #9233. --- doc/source/programs/gdal_grid.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/programs/gdal_grid.rst b/doc/source/programs/gdal_grid.rst index 91ac2c90683d..e49325009087 100644 --- a/doc/source/programs/gdal_grid.rst +++ b/doc/source/programs/gdal_grid.rst @@ -461,6 +461,10 @@ following way: The :ref:`vector.csv` description page contains details on CSV format supported by GDAL/OGR. +Creating multiband files: not directly possible with gdal_grid. +One might use gdal_grid multiple times to create one band per file, +and then use gdalbuildvrt -separate + gdal_translate to combine the one-band-files into a single one. + C API ----- From eb13b168fec8e7e742d555921674a5a1e5a1af81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sat, 24 Feb 2024 15:07:54 +0800 Subject: [PATCH 079/132] Update index.rst noting offline individual HTML pages .ZIP Closes #9228. --- doc/source/index.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 1f37d01ff378..bd363060717b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -4,7 +4,10 @@ GDAL .. include:: ./about_no_title.rst -This documentation is also available as a `PDF file `_. +This documentation is also available as a `PDF file `_, +and `a .ZIP of individual HTML pages `_ for offline browsing. + + .. toctree:: :maxdepth: 2 From 93d8f80351d7aea5d61834999dceae31d1cfb27d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sat, 24 Feb 2024 17:42:24 +0800 Subject: [PATCH 080/132] Update gdal2xyz.rst man gdal_translate says -b Select an input band band for output. Bands are numbered from 1. Multiple -b switches may be used to select a set of input bands to write to the output file, or to reorder bands... Thus I remove the statement about what the other program doesn't have. --- doc/source/programs/gdal2xyz.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/programs/gdal2xyz.rst b/doc/source/programs/gdal2xyz.rst index a3dc4738e959..b0cab43e3df5 100644 --- a/doc/source/programs/gdal2xyz.rst +++ b/doc/source/programs/gdal2xyz.rst @@ -28,8 +28,7 @@ Description ----------- The :program:`gdal2xyz` utility can be used to translate a raster file into xyz format. -`gdal2xyz` can be used as an alternative to `gdal_translate of=xyz`, but supporting other options, -for example: +`gdal2xyz` can be used as an alternative to `gdal_translate of=xyz`. Features include: * Select more then one band * Skip or replace nodata value From 016b6b4097d0620c9669f1f12c28c0c32dd5ff6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sat, 24 Feb 2024 18:13:43 +0800 Subject: [PATCH 081/132] Update gdal2xyz.rst removing bogus [< >] 1. You'll get an error if you don't give dst_dataset. 2. If you think about it, `[< >]` is a contradiction too. --- doc/source/programs/gdal2xyz.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/programs/gdal2xyz.rst b/doc/source/programs/gdal2xyz.rst index a3dc4738e959..1d45dd5f4261 100644 --- a/doc/source/programs/gdal2xyz.rst +++ b/doc/source/programs/gdal2xyz.rst @@ -22,7 +22,7 @@ Synopsis [-skipnodata] [-csv] [-srcnodata ] [-dstnodata ] - [] + Description ----------- From 5eab9bffe056d674a14de4d2a9fea84c9fd4d041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sat, 24 Feb 2024 19:39:22 +0800 Subject: [PATCH 082/132] Update gdaltransform.rst to add missing .. code-block::s (#9299) Currently each code block gets rendered with weird colors going on and off. --- doc/source/programs/gdaltransform.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/source/programs/gdaltransform.rst b/doc/source/programs/gdaltransform.rst index faeedf3544c6..59499810141c 100644 --- a/doc/source/programs/gdaltransform.rst +++ b/doc/source/programs/gdaltransform.rst @@ -144,7 +144,7 @@ Reprojection Example Simple reprojection from one projected coordinate system to another: -:: +.. code-block:: bash gdaltransform -s_srs EPSG:28992 -t_srs EPSG:31370 177502 311865 @@ -152,7 +152,7 @@ Simple reprojection from one projected coordinate system to another: Produces the following output in meters in the "Belge 1972 / Belgian Lambert 72" projection: -:: +.. code-block:: bash 244296.724777415 165937.350217148 0 @@ -165,14 +165,14 @@ used, the transformation is from output georeferenced (WGS84) coordinates back to image coordinates. -:: +.. code-block:: bash gdaltransform -i -rpc 06OCT20025052-P2AS-005553965230_01_P001.TIF 125.67206 39.85307 50 Produces this output measured in pixels and lines on the image: -:: +.. code-block:: bash 3499.49282422381 2910.83892848414 50 @@ -182,7 +182,7 @@ X,Y,Z,time transform 15-term time-dependent Helmert coordinate transformation from ITRF2000 to ITRF93 for a coordinate at epoch 2000.0 -:: +.. code-block:: bash gdaltransform -ct "+proj=pipeline +step +proj=unitconvert +xy_in=deg \ +xy_out=rad +step +proj=cart +step +proj=helmert +convention=position_vector \ @@ -194,6 +194,6 @@ for a coordinate at epoch 2000.0 Produces this output measured in longitude degrees, latitude degrees and ellipsoid height in metre: -:: +.. code-block:: bash 2.0000005420366 49.0000003766711 -0.0222802283242345 From d06d552b4b613edcad54fd283470690da26ad611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sun, 25 Feb 2024 17:05:23 +0800 Subject: [PATCH 083/132] Update index.rst to say the ZIP includes the PDF https://github.com/OSGeo/gdal/pull/9294#issuecomment-1962284238 --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index bd363060717b..31f6f8829ba7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -5,7 +5,7 @@ GDAL .. include:: ./about_no_title.rst This documentation is also available as a `PDF file `_, -and `a .ZIP of individual HTML pages `_ for offline browsing. +and `a .ZIP of individual HTML pages `_ for offline browsing. (The .ZIP also includes that .PDF.) From 2726b07ef0ec61a34fde7d380d244f783117ca4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sun, 25 Feb 2024 17:59:40 +0800 Subject: [PATCH 084/132] Update gdaltransform.rst about srcfile (#9292) --- doc/source/programs/gdaltransform.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/programs/gdaltransform.rst b/doc/source/programs/gdaltransform.rst index 59499810141c..c2b0eab829d0 100644 --- a/doc/source/programs/gdaltransform.rst +++ b/doc/source/programs/gdaltransform.rst @@ -116,9 +116,11 @@ projection,including GCP-based transformations. .. option:: - File with source projection definition or GCP's. If - not given, source projection is read from the command-line :option:`-s_srs` - or :option:`-gcp` parameters + File with source projection definition or GCPs. If + not given, source projection/GCPs are read from the command-line :option:`-s_srs` + or :option:`-gcp` parameters. + + Note that only the SRS and/or GCPs of this input file is taken into account, and not its pixel content. .. option:: From ef28df5aa1f461ed3a81ffc3f939082116f871fc Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 11:04:01 +0100 Subject: [PATCH 085/132] Update gdaltransform.rst --- doc/source/programs/gdaltransform.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/programs/gdaltransform.rst b/doc/source/programs/gdaltransform.rst index c2b0eab829d0..1b8886df12cc 100644 --- a/doc/source/programs/gdaltransform.rst +++ b/doc/source/programs/gdaltransform.rst @@ -116,7 +116,7 @@ projection,including GCP-based transformations. .. option:: - File with source projection definition or GCPs. If + Raster dataset with source projection definition or GCPs. If not given, source projection/GCPs are read from the command-line :option:`-s_srs` or :option:`-gcp` parameters. @@ -124,7 +124,7 @@ projection,including GCP-based transformations. .. option:: - File with destination projection definition. + Raster dataset with destination projection definition. Coordinates are read as pairs, triples (for 3D,) or (since GDAL 3.0.0,) quadruplets (for X,Y,Z,time) of numbers per line from standard From 5aa9940be495578e76754f38a9c4041839768b8b Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 11:09:09 +0100 Subject: [PATCH 086/132] codeql.yml: add 'sudo apt-get update' --- .github/workflows/codeql.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6d2ab2f72afe..02e5bd9635d5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -61,6 +61,7 @@ jobs: - name: Install dependencies run: | + sudo apt-get update sudo apt-get install -y ccache cmake g++ swig python3-numpy libproj-dev libqhull-dev sudo apt-get install -y \ libblosc-dev \ From f96206900143c4854af5924dd6beb25062e9cd8c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 24 Feb 2024 19:21:46 +0100 Subject: [PATCH 087/132] ODS: declare OLCStringsAsUTF8 on newly created layers --- autotest/ogr/ogr_ods.py | 3 +++ ogr/ogrsf_frmts/ods/ogrodsdatasource.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/autotest/ogr/ogr_ods.py b/autotest/ogr/ogr_ods.py index 6977cd3fa980..16fccb20fbd3 100755 --- a/autotest/ogr/ogr_ods.py +++ b/autotest/ogr/ogr_ods.py @@ -59,6 +59,8 @@ def ogr_ods_check(ds): assert lyr.TestCapability("foo") == 0 + assert lyr.TestCapability(ogr.OLCStringsAsUTF8) == 1 + lyr = ds.GetLayer(6) assert lyr.GetName() == "Feuille7", "bad layer name" @@ -363,6 +365,7 @@ def test_ogr_ods_8(): drv = ogr.GetDriverByName("ODS") ds = drv.CreateDataSource("/vsimem/ogr_ods_8.ods") lyr = ds.CreateLayer("foo") + assert lyr.TestCapability(ogr.OLCStringsAsUTF8) == 1 lyr.CreateField(ogr.FieldDefn("Field1", ogr.OFTInteger64)) f = ogr.Feature(lyr.GetLayerDefn()) f.SetField(0, 1) diff --git a/ogr/ogrsf_frmts/ods/ogrodsdatasource.cpp b/ogr/ogrsf_frmts/ods/ogrodsdatasource.cpp index e74cdb191eaa..62a4ff213bd3 100644 --- a/ogr/ogrsf_frmts/ods/ogrodsdatasource.cpp +++ b/ogr/ogrsf_frmts/ods/ogrodsdatasource.cpp @@ -72,6 +72,7 @@ OGRODSLayer::OGRODSLayer(OGRODSDataSource *poDSIn, const char *pszName, bUpdated(CPL_TO_BOOL(bUpdatedIn)), bHasHeaderLine(false), m_poAttrQueryODS(nullptr) { + SetAdvertizeUTF8(true); } /************************************************************************/ @@ -846,7 +847,6 @@ void OGRODSDataSource::endElementTable( reinterpret_cast(poCurLayer) ->SetUpdatable(bUpdatable); - reinterpret_cast(poCurLayer)->SetAdvertizeUTF8(true); reinterpret_cast(poCurLayer)->SetUpdated(false); } From 442318b4135d1f878f7563f0157651ae29c9c4f3 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sat, 24 Feb 2024 19:21:54 +0100 Subject: [PATCH 088/132] XLSX: declare OLCStringsAsUTF8 on newly created layers --- autotest/ogr/ogr_xlsx.py | 3 +++ ogr/ogrsf_frmts/xlsx/ogrxlsxdatasource.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/autotest/ogr/ogr_xlsx.py b/autotest/ogr/ogr_xlsx.py index 39971b128d96..164bdd835d29 100755 --- a/autotest/ogr/ogr_xlsx.py +++ b/autotest/ogr/ogr_xlsx.py @@ -59,6 +59,8 @@ def ogr_xlsx_check(ds): assert lyr.TestCapability("foo") == 0 + assert lyr.TestCapability(ogr.OLCStringsAsUTF8) == 1 + lyr = ds.GetLayer(6) assert lyr.GetName() == "Feuille7", "bad layer name" @@ -277,6 +279,7 @@ def test_ogr_xlsx_8(): ds = ogr.GetDriverByName("XLSX").CreateDataSource("/vsimem/ogr_xlsx_8.xlsx") lyr = ds.CreateLayer("foo") + assert lyr.TestCapability(ogr.OLCStringsAsUTF8) == 1 for i in range(30): lyr.CreateField(ogr.FieldDefn("Field%d" % (i + 1))) f = ogr.Feature(lyr.GetLayerDefn()) diff --git a/ogr/ogrsf_frmts/xlsx/ogrxlsxdatasource.cpp b/ogr/ogrsf_frmts/xlsx/ogrxlsxdatasource.cpp index c485582f1a2f..9f730184bf1f 100644 --- a/ogr/ogrsf_frmts/xlsx/ogrxlsxdatasource.cpp +++ b/ogr/ogrsf_frmts/xlsx/ogrxlsxdatasource.cpp @@ -52,6 +52,7 @@ OGRXLSXLayer::OGRXLSXLayer(OGRXLSXDataSource *poDSIn, const char *pszFilename, poDS(poDSIn), osFilename(pszFilename), bUpdated(CPL_TO_BOOL(bUpdatedIn)), bHasHeaderLine(false) { + SetAdvertizeUTF8(true); } /************************************************************************/ @@ -822,7 +823,6 @@ void OGRXLSXDataSource::endElementTable(CPL_UNUSED const char *pszNameIn) if (poCurLayer) { ((OGRMemLayer *)poCurLayer)->SetUpdatable(CPL_TO_BOOL(bUpdatable)); - ((OGRMemLayer *)poCurLayer)->SetAdvertizeUTF8(true); ((OGRXLSXLayer *)poCurLayer)->SetUpdated(false); } From b9438ae68d6e2ee6bc9b495d7427ca800e439b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sun, 25 Feb 2024 18:58:47 +0800 Subject: [PATCH 089/132] Update software_using_gdal.rst restoring alphabetical order --- doc/source/software_using_gdal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/software_using_gdal.rst b/doc/source/software_using_gdal.rst index 7a2150ae827f..f25df05bdb7a 100644 --- a/doc/source/software_using_gdal.rst +++ b/doc/source/software_using_gdal.rst @@ -72,6 +72,7 @@ Proprietary license / Other - `3D DEM Viewer `_ from MS MacroSystem. - `Cadcorp SIS: `_ A Windows GIS with a GDAL and OGR plugins. +- `Carmenta Engine `_ (previously known as SpatialAce): A GIS Rapid Application Development environment - `CARTO `_ A cloud mapping platform to analyze and visualize geospatial data. - `Cartographica `_ Macintosh GIS package. - `CatchmentSIM `_ A Windows terrain analysis model for hydrologic applications. @@ -113,7 +114,6 @@ Proprietary license / Other - `SkylineGlobe `_ The Skyline suite of interactive applications allows you to build, view, query and analyze customized, virtual 3D landscapes. - `SpacEyes3D `_ 3D visualization software for cartographic data. - `Spatial Manager `_ A product suite designed designed to manage spatial data in a simple, fast and inexpensive way. Uses GDAL to import/export data. -- `Carmenta Engine `_ (previously known as SpatialAce): A GIS Rapid Application Development environment - `TacitView `_ An imagery visualization and exploitation package for military intelligence. - `TatukGIS `_ Desktop GIS mapping and data editing application. - `Team Awareness Kit `_ Suite of georeferenced imagery and situational awareness tools developed for military planning and execution, now available for civilian use. From 42cce530b92186170b3e2d53b8e28d5b50ad54b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sun, 25 Feb 2024 19:11:47 +0800 Subject: [PATCH 090/132] Update gdal2xyz.rst clarifying centers vs. corners --- doc/source/programs/gdal2xyz.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/programs/gdal2xyz.rst b/doc/source/programs/gdal2xyz.rst index 2258703977e7..027990c56c5d 100644 --- a/doc/source/programs/gdal2xyz.rst +++ b/doc/source/programs/gdal2xyz.rst @@ -100,5 +100,7 @@ Examples gdal2xyz -b 1 -b 2 -dstnodata 0 input.tif output.txt -To create a text file in `xyz` format from the input file `input.tif`, including the first and second bands, -while replacing the dataset nodata values with zeros. +To create a text file in `xyz` format from the input file `input.tif`. +The first columns, x and y, are the coordinates of the centers of each cell. +The remaining colums represent the first and second bands. +We also replace the dataset nodata values with zeros. From 3b3fdce2994844dee139e4d865c506ca93b4450e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sun, 25 Feb 2024 19:18:17 +0800 Subject: [PATCH 091/132] Update xyz.rst to warn only three columns are used Avoid users generating `Warning 6: XYZ driver only uses the first band of the dataset.` --- doc/source/drivers/raster/xyz.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/drivers/raster/xyz.rst b/doc/source/drivers/raster/xyz.rst index 0cb9c409b0a9..66c0d7d4c45d 100644 --- a/doc/source/drivers/raster/xyz.rst +++ b/doc/source/drivers/raster/xyz.rst @@ -14,7 +14,8 @@ the documentation of the :ref:`gdal_grid` utility). Those datasets are ASCII files with (at least) 3 columns, each line containing the X and Y coordinates of the center of the cell and the -value of the cell. +value of the cell. (Note the XYZ driver only uses the first band of +the dataset. I.e., columns beyond the third are ignored.) The spacing between each cell must be constant. From e1d7cc402b59cd8e7a809813c9a6282ed571705c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Sun, 25 Feb 2024 19:41:59 +0800 Subject: [PATCH 092/132] gdallocationinfo.rst: mention that pixel coordinates should be integer (#9307) --- doc/source/programs/gdallocationinfo.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/programs/gdallocationinfo.rst b/doc/source/programs/gdallocationinfo.rst index 8814cced6f75..92ede096f9d8 100644 --- a/doc/source/programs/gdallocationinfo.rst +++ b/doc/source/programs/gdallocationinfo.rst @@ -102,7 +102,7 @@ pixel. Currently it reports: The pixel selected is requested by x/y coordinate on the command line, or read from stdin. More than one coordinate pair can be supplied when reading -coordinates from stdin. By default pixel/line coordinates are expected. +coordinates from stdin. By default integer pixel/line coordinates are expected. However with use of the :option:`-geoloc`, :option:`-wgs84`, or :option:`-l_srs` switches it is possible to specify the location in other coordinate systems. From 84262f8050f2d48346bf1bf726ab2f9d185e9429 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 12:54:31 +0100 Subject: [PATCH 093/132] sql_sqlite_dialect.rst: avoid use of non-ASCII characters (fixes #9308) --- doc/source/user/sql_sqlite_dialect.rst | 63 ++++++++------------------ 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/doc/source/user/sql_sqlite_dialect.rst b/doc/source/user/sql_sqlite_dialect.rst index c482622667bb..f1c6d8fd8259 100644 --- a/doc/source/user/sql_sqlite_dialect.rst +++ b/doc/source/user/sql_sqlite_dialect.rst @@ -343,11 +343,11 @@ returns: :: OGRFeature(SELECT):0 - POINT (2.342878767069653 48.85661793020374) + POINT (2.34287687375113 48.856622357411) .. code-block:: shell - ogrinfo cities.csv -dialect sqlite -sql "SELECT *, ogr_geocode(city, 'country') AS country, ST_Centroid(ogr_geocode(city)) FROM cities" + ogrinfo cities.csv -dialect sqlite -sql "SELECT *, ogr_geocode(city, 'country_code') AS country_code, ST_Centroid(ogr_geocode(city)) FROM cities" returns: @@ -357,57 +357,30 @@ returns: :: OGRFeature(SELECT):0 - id (Real) = 1 - city (String) = Paris - country (String) = France métropolitaine - POINT (2.342878767069653 48.85661793020374) + city (String) = Paris + country_code (String) = fr + POINT (2.34287687375113 48.856622357411) OGRFeature(SELECT):1 - id (Real) = 2 - city (String) = London - country (String) = United Kingdom - POINT (-0.109369427546499 51.500506667319407) + city (String) = London + country_code (String) = gb + POINT (-0.109415723431508 51.5004964757441) OGRFeature(SELECT):2 - id (Real) = 3 - city (String) = Rennes - country (String) = France métropolitaine - POINT (-1.68185153381778 48.111663929761093) + city (String) = Rennes + country_code (String) = fr + POINT (-1.68185479486048 48.1116771631195) OGRFeature(SELECT):3 - id (Real) = 4 - city (String) = Strasbourg - country (String) = France métropolitaine - POINT (7.767762859150757 48.571233274141846) + city (String) = New York + country_code (String) = us + POINT (-73.9388908443975 40.6632061220125) OGRFeature(SELECT):4 - id (Real) = 5 - city (String) = New York - country (String) = United States of America - POINT (-73.938140243499049 40.663799577449979) - - OGRFeature(SELECT):5 - id (Real) = 6 - city (String) = Berlin - country (String) = Deutschland - POINT (13.402306623451983 52.501470321410636) - - OGRFeature(SELECT):6 - id (Real) = 7 - city (String) = Beijing - POINT (116.391195 39.9064702) - - OGRFeature(SELECT):7 - id (Real) = 8 - city (String) = Brasilia - country (String) = Brasil - POINT (-52.830435216371839 -10.828214867369699) - - OGRFeature(SELECT):8 - id (Real) = 9 - city (String) = Moscow - country (String) = Российская Федерация - POINT (37.367988106866868 55.556208255649558) + city (String) = Beijing + country_code (String) = cn + POINT (116.3912972 39.9057136) + .. highlight:: sql From 0343bc221b91fc327692aee846ffb73f81756c00 Mon Sep 17 00:00:00 2001 From: Jukka Rahkonen Date: Sun, 25 Feb 2024 21:23:39 +0200 Subject: [PATCH 094/132] gml.rst: fix the link to the .gfs XML schema (#9309) --- doc/source/drivers/vector/gml.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/drivers/vector/gml.rst b/doc/source/drivers/vector/gml.rst index 4e8080845b1d..89eb17d83eb2 100644 --- a/doc/source/drivers/vector/gml.rst +++ b/doc/source/drivers/vector/gml.rst @@ -791,7 +791,7 @@ Syntax of .gfs files -------------------- A XML Schema for .gfs files can be found at -https://raw.githubusercontent.com/OSGeo/gdal/master/data/gfs.xsd . +https://raw.githubusercontent.com/OSGeo/gdal/master/ogr/ogrsf_frmts/gml/data/gfs.xsd . Let's consider the following test.gml file : From fe693c6d13ccfd3c33420961891645b274095825 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 20:28:12 +0100 Subject: [PATCH 095/132] CI: add support for OSX arm64 --- .github/workflows/conda.yml | 4 ++-- ci/travis/conda/compile.sh | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index 8bf07876478e..ddcfa46891e9 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: true matrix: - platform: ['ubuntu-latest','windows-latest','macos-latest'] + platform: ['ubuntu-latest','windows-latest','macos-latest','macos-14'] env: GHA_CI_PLATFORM: ${{ matrix.platform }} @@ -47,7 +47,7 @@ jobs: path: ~/conda_pkgs_dir key: ${{ runner.os }}-${{ steps.get-date.outputs.today }}-conda-${{ env.CACHE_NUMBER }} - - uses: conda-incubator/setup-miniconda@11b562958363ec5770fef326fe8ef0366f8cbf8a # v3.0.1 + - uses: conda-incubator/setup-miniconda@392cf345b1784333caa1a1185081c71e6ffd61bc # v3.0.2 with: #miniforge-variant: Mambaforge miniforge-version: latest diff --git a/ci/travis/conda/compile.sh b/ci/travis/conda/compile.sh index c200601633d0..dcb73e80a665 100755 --- a/ci/travis/conda/compile.sh +++ b/ci/travis/conda/compile.sh @@ -5,18 +5,24 @@ mkdir -p packages CONDA_PLAT="" if grep -q "windows" <<< "$GHA_CI_PLATFORM"; then CONDA_PLAT="win" + ARCH="64" fi if grep -q "ubuntu" <<< "$GHA_CI_PLATFORM"; then CONDA_PLAT="linux" + ARCH="64" fi -if grep -q "macos" <<< "$GHA_CI_PLATFORM"; then +if grep -q "macos-14" <<< "$GHA_CI_PLATFORM"; then CONDA_PLAT="osx" + ARCH="arm64" +elif grep -q "macos" <<< "$GHA_CI_PLATFORM"; then + CONDA_PLAT="osx" + ARCH="64" fi -conda build recipe --clobber-file recipe/recipe_clobber.yaml --output-folder packages -m ".ci_support/${CONDA_PLAT}_64_.yaml" -conda create -y -n test -c ./packages/${CONDA_PLAT}-64 python libgdal gdal +conda build recipe --clobber-file recipe/recipe_clobber.yaml --output-folder packages -m ".ci_support/${CONDA_PLAT}_${ARCH}_.yaml" +conda create -y -n test -c ./packages/${CONDA_PLAT}-${ARCH} python libgdal gdal conda deactivate conda activate test From d828c6bcec0de842fb4e463a531e209e44c4cfed Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 20:53:38 +0100 Subject: [PATCH 096/132] OGCAPI: fix Coverity Scan performance warning (CID 1534765) --- frmts/ogcapi/gdalogcapidataset.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frmts/ogcapi/gdalogcapidataset.cpp b/frmts/ogcapi/gdalogcapidataset.cpp index 0d24d1f9acf2..46d91ca463a5 100644 --- a/frmts/ogcapi/gdalogcapidataset.cpp +++ b/frmts/ogcapi/gdalogcapidataset.cpp @@ -1090,9 +1090,9 @@ bool OGCAPIDataset::InitFromURL(GDALOpenInfo *poOpenInfo) /* SelectImageURL() */ /************************************************************************/ -static const std::pair +static const std::pair SelectImageURL(const char *const *papszOptionOptions, - std::map &oMapItemUrls) + std::map &oMapItemUrls) { // Map IMAGE_FORMATS to their content types. Would be nice if this was // globally defined someplace @@ -1130,7 +1130,8 @@ SelectImageURL(const char *const *papszOptionOptions, { std::transform(oMapItemUrls.begin(), oMapItemUrls.end(), std::back_inserter(oContentTypes), - [](const auto &pair) { return pair.first; }); + [](const auto &pair) -> const std::string & + { return pair.first; }); } // Loop over each content type - return the first one we find @@ -1182,7 +1183,7 @@ bool OGCAPIDataset::InitWithMapAPI(GDALOpenInfo *poOpenInfo, auto oLinks = oRoot["links"].ToArray(); // Key - mime type, Value url - std::map oMapItemUrls; + std::map oMapItemUrls; for (const auto &oLink : oLinks) { @@ -1201,10 +1202,10 @@ bool OGCAPIDataset::InitWithMapAPI(GDALOpenInfo *poOpenInfo, } } - const std::pair oContentUrlPair = + const std::pair oContentUrlPair = SelectImageURL(poOpenInfo->papszOpenOptions, oMapItemUrls); const std::string osContentType = oContentUrlPair.first; - const CPLString osImageURL = oContentUrlPair.second; + const std::string osImageURL = oContentUrlPair.second; if (osImageURL.empty()) { @@ -1231,7 +1232,7 @@ bool OGCAPIDataset::InitWithMapAPI(GDALOpenInfo *poOpenInfo, CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "MAX_CONNECTIONS", CPLGetConfigOption("GDAL_MAX_CONNECTIONS", "5"))); CPLString osWMS_XML; - char *pszEscapedURL = CPLEscapeString(osImageURL, -1, CPLES_XML); + char *pszEscapedURL = CPLEscapeString(osImageURL.c_str(), -1, CPLES_XML); osWMS_XML.Printf("" " " " %s" @@ -1766,7 +1767,7 @@ bool OGCAPIDataset::InitWithTilesAPI(GDALOpenInfo *poOpenInfo, } // Key - mime type, Value url - std::map oMapItemUrls; + std::map oMapItemUrls; CPLString osMVT_URL; CPLString osGEOJSON_URL; CPLString osTilingSchemeURL; @@ -1856,10 +1857,10 @@ bool OGCAPIDataset::InitWithTilesAPI(GDALOpenInfo *poOpenInfo, } } - const std::pair oContentUrlPair = + const std::pair oContentUrlPair = SelectImageURL(poOpenInfo->papszOpenOptions, oMapItemUrls); const std::string osContentType = oContentUrlPair.first; - const CPLString osRasterURL = oContentUrlPair.second; + const std::string osRasterURL = oContentUrlPair.second; const CPLString osVectorURL = SelectVectorFormatURL( poOpenInfo->papszOpenOptions, osMVT_URL, osGEOJSON_URL); From 99ff19e56a13d9a4fc06d6391e066a76a18414db Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 20:54:29 +0100 Subject: [PATCH 097/132] PDF: fix Coverity Scan performance warning (CID 1534770, 1534767, 1534766, 1534763) --- frmts/pdf/pdfdataset.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frmts/pdf/pdfdataset.cpp b/frmts/pdf/pdfdataset.cpp index 74173f50058b..04da2242b58a 100644 --- a/frmts/pdf/pdfdataset.cpp +++ b/frmts/pdf/pdfdataset.cpp @@ -1572,8 +1572,8 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->SetDIBits(bitmap, color, src_rect, dest_left, - dest_top, blend_type); + return m_poParent->SetDIBits(std::move(bitmap), color, src_rect, + dest_left, dest_top, blend_type); } virtual bool StretchDIBits(RetainPtr bitmap, @@ -1585,9 +1585,9 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->StretchDIBits(bitmap, color, dest_left, dest_top, - dest_width, dest_height, pClipRect, - options, blend_type); + return m_poParent->StretchDIBits(std::move(bitmap), color, dest_left, + dest_top, dest_width, dest_height, + pClipRect, options, blend_type); } virtual bool StartDIBits(RetainPtr bitmap, float alpha, @@ -1598,8 +1598,8 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface { if (!bEnableBitmap && !bTemporaryEnableVectorForTextStroking) return true; - return m_poParent->StartDIBits(bitmap, alpha, color, matrix, options, - handle, blend_type); + return m_poParent->StartDIBits(std::move(bitmap), alpha, color, matrix, + options, handle, blend_type); } virtual bool ContinueDIBits(CFX_ImageRenderer *handle, @@ -1655,7 +1655,7 @@ class GDALPDFiumRenderDeviceDriver : public RenderDeviceDriverIface bool MultiplyAlphaMask(RetainPtr mask) override { - return m_poParent->MultiplyAlphaMask(mask); + return m_poParent->MultiplyAlphaMask(std::move(mask)); } #if defined(_SKIA_SUPPORT_) From f1c5937e1c346ec73b1e87fa463759d1a221a33a Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 20:54:42 +0100 Subject: [PATCH 098/132] ZARR: fix Coverity Scan performance warning (CID 1534762) --- frmts/zarr/zarr_v2_array.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frmts/zarr/zarr_v2_array.cpp b/frmts/zarr/zarr_v2_array.cpp index 7ae8c89705e2..ada168366aaa 100644 --- a/frmts/zarr/zarr_v2_array.cpp +++ b/frmts/zarr/zarr_v2_array.cpp @@ -1576,7 +1576,7 @@ ZarrV2Group::LoadArray(const std::string &osArrayName, } else { - aoDims[i] = poDim; + aoDims[i] = std::move(poDim); } } else From 5c1585c3a774518643ec1b35e0c8e4ff16a6706d Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 20:55:42 +0100 Subject: [PATCH 099/132] VRT: fix Coverity Scan warnings about singleton vs array access (CID 1534769, 1534764) --- frmts/vrt/vrtsources.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frmts/vrt/vrtsources.cpp b/frmts/vrt/vrtsources.cpp index 722d9e36a66c..d608e08d3510 100644 --- a/frmts/vrt/vrtsources.cpp +++ b/frmts/vrt/vrtsources.cpp @@ -2251,7 +2251,7 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( static_cast(m_dfNoDataValue) == m_dfNoDataValue && dfRemappedValue >= 0 && dfRemappedValue <= 255 && static_cast(dfRemappedValue) == dfRemappedValue); - GByte *abyWrkBuffer; + GByte *pabyWrkBuffer; try { if (bByteOptim && nOutXOff == 0 && nOutYOff == 0 && @@ -2259,13 +2259,14 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( eSrcBandDT == eBufType && nPixelSpace == nSrcBandDTSize && nLineSpace == nPixelSpace * nBufXSize) { - abyWrkBuffer = static_cast(pData); + pabyWrkBuffer = static_cast(pData); } else { oWorkingState.m_abyWrkBuffer.resize(static_cast(nOutXSize) * nOutYSize * nSrcBandDTSize); - abyWrkBuffer = &oWorkingState.m_abyWrkBuffer[0].value; + pabyWrkBuffer = + reinterpret_cast(oWorkingState.m_abyWrkBuffer.data()); } oWorkingState.m_abyWrkBufferMask.resize(static_cast(nOutXSize) * nOutYSize * nSrcMaskBandDTSize); @@ -2296,7 +2297,7 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( psExtraArg->dfYSize = dfReqYSize; if (l_band->RasterIO(GF_Read, nReqXOff, nReqYOff, nReqXSize, nReqYSize, - abyWrkBuffer, nOutXSize, nOutYSize, eSrcBandDT, 0, 0, + pabyWrkBuffer, nOutXSize, nOutYSize, eSrcBandDT, 0, 0, psExtraArg) != CE_None) { return CE_Failure; @@ -2338,7 +2339,7 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( } else { - if (abyWrkBuffer[nSrcIdx] == nNoDataValue) + if (pabyWrkBuffer[nSrcIdx] == nNoDataValue) { pabyOut[static_cast(nDstOffset)] = nRemappedValue; @@ -2346,7 +2347,7 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( else { pabyOut[static_cast(nDstOffset)] = - abyWrkBuffer[nSrcIdx]; + pabyWrkBuffer[nSrcIdx]; } } nDstOffset += nPixelSpace; @@ -2390,9 +2391,10 @@ CPLErr VRTNoDataFromMaskSource::RasterIO( else { const void *const pSrc = - abyWrkBuffer + nSrcIdx * nSrcBandDTSize; + pabyWrkBuffer + nSrcIdx * nSrcBandDTSize; if (eSrcBandDT == eBufType) { + // coverity[overrun-buffer-arg] memcpy(pDst, pSrc, nBufDTSize); } else From a1d9d797cf0b5505eb92f4dfe02ebc3153318c80 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 25 Feb 2024 20:56:05 +0100 Subject: [PATCH 100/132] VRT: fix Coverity Scan warning about nullptr deref (CID 1534768) --- frmts/vrt/vrtsourcedrasterband.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frmts/vrt/vrtsourcedrasterband.cpp b/frmts/vrt/vrtsourcedrasterband.cpp index 0067f27abfac..0e755053b0a4 100644 --- a/frmts/vrt/vrtsourcedrasterband.cpp +++ b/frmts/vrt/vrtsourcedrasterband.cpp @@ -1883,6 +1883,9 @@ CPLXMLNode *VRTSourcedRasterBand::SerializeToXML(const char *pszVRTPath, CPLXMLNode *const psXMLSrc = papoSources[iSource]->SerializeToXML(pszVRTPath); + if (psXMLSrc == nullptr) + break; + // Creating the CPLXMLNode tree representation of a VRT can easily // take several times RAM usage than its string serialization, or its // internal representation in the driver. @@ -1906,9 +1909,6 @@ CPLXMLNode *VRTSourcedRasterBand::SerializeToXML(const char *pszVRTPath, } } - if (psXMLSrc == nullptr) - break; - if (psLastChild == nullptr) psTree->psChild = psXMLSrc; else From c228e2a1f0e10f90b8c4e20ce85de5c4c50bf10f Mon Sep 17 00:00:00 2001 From: Kai Pastor Date: Sun, 25 Feb 2024 22:22:01 +0100 Subject: [PATCH 101/132] Disable my_test_sqlite3_ext in static builds --- ogr/ogrsf_frmts/sqlite/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ogr/ogrsf_frmts/sqlite/CMakeLists.txt b/ogr/ogrsf_frmts/sqlite/CMakeLists.txt index d296fe5e15fb..14f9f6af311e 100644 --- a/ogr/ogrsf_frmts/sqlite/CMakeLists.txt +++ b/ogr/ogrsf_frmts/sqlite/CMakeLists.txt @@ -95,8 +95,7 @@ if (GDAL_USE_SPATIALITE) endif () # Test sqlite3 extension -find_path(SQLITE3EXT_INCLUDE_DIR NAMES sqlite3ext.h) -if (SQLITE3EXT_INCLUDE_DIR) +if (HAVE_SQLITE3EXT_H AND BUILD_SHARED_LIBS) add_library(my_test_sqlite3_ext MODULE my_test_sqlite3_ext.c) gdal_standard_includes(my_test_sqlite3_ext) get_target_property(PLUGIN_OUTPUT_DIR ${GDAL_LIB_TARGET_NAME} PLUGIN_OUTPUT_DIR) From f304ca54938651f0efb01af90b67b3217f96cc59 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 26 Feb 2024 02:47:30 +0100 Subject: [PATCH 102/132] Make sure our vendored flatbuffers copy has a unique namespace Cf https://lists.osgeo.org/pipermail/gdal-dev/2024-February/058544.html --- ogr/ogrsf_frmts/flatgeobuf/CMakeLists.txt | 2 ++ ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufdataset.cpp | 6 ++++++ scripts/cppcheck.sh | 1 + 3 files changed, 9 insertions(+) diff --git a/ogr/ogrsf_frmts/flatgeobuf/CMakeLists.txt b/ogr/ogrsf_frmts/flatgeobuf/CMakeLists.txt index ec4ec7a2d7f9..7fbbf2050227 100644 --- a/ogr/ogrsf_frmts/flatgeobuf/CMakeLists.txt +++ b/ogr/ogrsf_frmts/flatgeobuf/CMakeLists.txt @@ -11,3 +11,5 @@ add_gdal_driver( gdal_standard_includes(ogr_FlatGeobuf) target_include_directories(ogr_FlatGeobuf PRIVATE $ $) +# Quick and dirty way of modifying the default flatbuffers namespace to gdal_flatbuffers +target_compile_definitions(ogr_FlatGeobuf PRIVATE -Dflatbuffers=gdal_flatbuffers) diff --git a/ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufdataset.cpp b/ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufdataset.cpp index 39261755d63f..75a9afa40e2d 100644 --- a/ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufdataset.cpp +++ b/ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufdataset.cpp @@ -32,6 +32,12 @@ #include "header_generated.h" +// For users not using CMake... +#ifndef flatbuffers +#error \ + "Make sure to build with -Dflatbuffers=gdal_flatbuffers (for example) to avoid potential conflict of flatbuffers" +#endif + using namespace flatbuffers; using namespace FlatGeobuf; diff --git a/scripts/cppcheck.sh b/scripts/cppcheck.sh index 7f66b755f7e5..7927eb6b70f8 100755 --- a/scripts/cppcheck.sh +++ b/scripts/cppcheck.sh @@ -98,6 +98,7 @@ for dirname in alg port gcore ogr frmts gnm apps fuzzers; do -D__x86_64__ \ -DFLT_EVAL_METHOD \ -DKDU_HAS_ROI_RECT \ + -Dflatbuffers=gdal_flatbuffers \ --include="${CPL_CONFIG_H}" \ --include=port/cpl_port.h \ -I "${CPL_CONFIG_H_DIR}" \ From 0c50bbc187bcbd4eb2c45e6b7c17c224a8337567 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 02:11:33 +0000 Subject: [PATCH 103/132] build(deps): bump conda-incubator/setup-miniconda from 3.0.1 to 3.0.2 Bumps [conda-incubator/setup-miniconda](https://github.com/conda-incubator/setup-miniconda) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/conda-incubator/setup-miniconda/releases) - [Changelog](https://github.com/conda-incubator/setup-miniconda/blob/main/CHANGELOG.md) - [Commits](https://github.com/conda-incubator/setup-miniconda/compare/v3.0.1...392cf345b1784333caa1a1185081c71e6ffd61bc) --- updated-dependencies: - dependency-name: conda-incubator/setup-miniconda dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/cmake_builds.yml | 6 +++--- .github/workflows/macos.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake_builds.yml b/.github/workflows/cmake_builds.yml index f910078d5bf0..8111b4b4f575 100644 --- a/.github/workflows/cmake_builds.yml +++ b/.github/workflows/cmake_builds.yml @@ -409,7 +409,7 @@ jobs: shell: pwsh run: | echo "JAVA_HOME=$env:JAVA_HOME_11_X64" >> %GITHUB_ENV% - - uses: conda-incubator/setup-miniconda@11b562958363ec5770fef326fe8ef0366f8cbf8a # v3.0.1 + - uses: conda-incubator/setup-miniconda@392cf345b1784333caa1a1185081c71e6ffd61bc # v3.0.2 with: activate-environment: gdalenv miniforge-variant: Mambaforge @@ -506,7 +506,7 @@ jobs: git config --global core.autocrlf false - name: Checkout GDAL uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: conda-incubator/setup-miniconda@11b562958363ec5770fef326fe8ef0366f8cbf8a # v3.0.1 + - uses: conda-incubator/setup-miniconda@392cf345b1784333caa1a1185081c71e6ffd61bc # v3.0.2 with: activate-environment: gdalenv miniforge-variant: Mambaforge @@ -653,7 +653,7 @@ jobs: git config --global core.autocrlf false - name: Checkout GDAL uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: conda-incubator/setup-miniconda@11b562958363ec5770fef326fe8ef0366f8cbf8a # v3.0.1 + - uses: conda-incubator/setup-miniconda@392cf345b1784333caa1a1185081c71e6ffd61bc # v3.0.2 with: activate-environment: gdalenv python-version: 3.9 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 1dc6e6d7e747..28e3d5e5bf23 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: conda-incubator/setup-miniconda@11b562958363ec5770fef326fe8ef0366f8cbf8a # v3.0.1 + - uses: conda-incubator/setup-miniconda@392cf345b1784333caa1a1185081c71e6ffd61bc # v3.0.2 with: channels: conda-forge auto-update-conda: true From 46b78a5c151737edb0e81d341d4b5b1b6433a432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 02:11:39 +0000 Subject: [PATCH 104/132] build(deps): bump github/codeql-action from 3.24.3 to 3.24.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.3 to 3.24.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/379614612a29c9e28f31f39a59013eb8012a51f0...47b3d888fe66b639e431abf22ebca059152f1eea) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 02e5bd9635d5..daa0604025ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -149,6 +149,6 @@ jobs: key: ${{ steps.restore-cache.outputs.cache-primary-key }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4156ae671d79..71e2979ebde0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 + uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 with: sarif_file: results.sarif From b308b64c6a9fa4c14c17b76584902178107e59fa Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 18:24:26 +0100 Subject: [PATCH 105/132] OGRSpatialReference::exportToXML(): error out on unsupported projection method (refs #9223) --- autotest/osr/osr_xml.py | 56 +++++++++++++++++++++++++++++++++++++++++ ogr/ogr_srs_xml.cpp | 7 +++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/autotest/osr/osr_xml.py b/autotest/osr/osr_xml.py index f804aec67ded..41ef4d1377bf 100755 --- a/autotest/osr/osr_xml.py +++ b/autotest/osr/osr_xml.py @@ -31,6 +31,7 @@ import re import gdaltest +import pytest from osgeo import osr @@ -200,3 +201,58 @@ def test_osr_xml_2(): expected = re.sub(r' gml:id="[^"]*"', "", expected, 0) assert got == expected + + +############################################################################### +# Test the osr.SpatialReference.ExportToXML() function. +# + + +def test_osr_xml_export_failure(): + + srs = osr.SpatialReference() + srs.ImportFromWkt( + """PROJCRS["Africa_Albers_Equal_Area_Conic", + BASEGEOGCRS["WGS 84", + DATUM["World Geodetic System 1984", + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + ID["EPSG",4326]], + CONVERSION["Albers Equal Area", + METHOD["Albers Equal Area", + ID["EPSG",9822]], + PARAMETER["Latitude of false origin",0, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8821]], + PARAMETER["Longitude of false origin",25, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8822]], + PARAMETER["Latitude of 1st standard parallel",20, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8823]], + PARAMETER["Latitude of 2nd standard parallel",-23, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8824]], + PARAMETER["Easting at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8826]], + PARAMETER["Northing at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8827]]], + CS[Cartesian,2], + AXIS["easting",east, + ORDER[1], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]], + AXIS["northing",north, + ORDER[2], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]]]""" + ) + + with pytest.raises( + Exception, match="Unhandled projection method Albers_Conic_Equal_Area" + ): + srs.ExportToXML() diff --git a/ogr/ogr_srs_xml.cpp b/ogr/ogr_srs_xml.cpp index d2732338c11d..aecc723d8e24 100644 --- a/ogr/ogr_srs_xml.cpp +++ b/ogr/ogr_srs_xml.cpp @@ -633,8 +633,10 @@ static CPLXMLNode *exportProjCSToXML(const OGRSpatialReference *poSRS) } else { - CPLError(CE_Warning, CPLE_NotSupported, + CPLError(CE_Failure, CPLE_NotSupported, "Unhandled projection method %s", pszProjection); + CPLDestroyXMLNode(psCRS_XML); + return nullptr; } /* -------------------------------------------------------------------- */ @@ -693,6 +695,9 @@ OGRErr OGRSpatialReference::exportToXML(char **ppszRawXML, else return OGRERR_UNSUPPORTED_SRS; + if (!psXMLTree) + return OGRERR_FAILURE; + *ppszRawXML = CPLSerializeXMLTree(psXMLTree); CPLDestroyXMLNode(psXMLTree); From 4e2f3a84820bed6eefd60acb217047de84c46120 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 18:37:50 +0100 Subject: [PATCH 106/132] Add a GDALJP2Metadata::IsSRSCompatible() method (refs #9223) --- gcore/gdaljp2metadata.cpp | 68 +++++++++++++++++++++++---------------- gcore/gdaljp2metadata.h | 2 ++ 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/gcore/gdaljp2metadata.cpp b/gcore/gdaljp2metadata.cpp index a92e4dcc94a9..e15e8aee6599 100644 --- a/gcore/gdaljp2metadata.cpp +++ b/gcore/gdaljp2metadata.cpp @@ -1252,6 +1252,32 @@ GDALJP2Box *GDALJP2Metadata::CreateJP2GeoTIFF() return poBox; } +/************************************************************************/ +/* IsSRSCompatible() */ +/************************************************************************/ + +/* Returns true if the SRS can be references through a EPSG code, or encoded + * as a GML SRS + */ +bool GDALJP2Metadata::IsSRSCompatible(const OGRSpatialReference *poSRS) +{ + const char *pszAuthName = poSRS->GetAuthorityName(nullptr); + const char *pszAuthCode = poSRS->GetAuthorityCode(nullptr); + + if (pszAuthName && pszAuthCode && EQUAL(pszAuthName, "epsg")) + { + if (atoi(pszAuthCode)) + return true; + } + + CPLErrorHandlerPusher oErrorHandler(CPLQuietErrorHandler); + CPLErrorStateBackuper oErrorStateBackuper; + char *pszGMLDef = nullptr; + const bool bRet = (poSRS->exportToXML(&pszGMLDef, nullptr) == OGRERR_NONE); + CPLFree(pszGMLDef); + return bRet; +} + /************************************************************************/ /* GetGMLJP2GeoreferencingInfo() */ /************************************************************************/ @@ -1269,43 +1295,28 @@ void GDALJP2Metadata::GetGMLJP2GeoreferencingInfo( bNeedAxisFlip = false; OGRSpatialReference oSRS(m_oSRS); - if (oSRS.IsProjected()) - { - const char *pszAuthName = oSRS.GetAuthorityName("PROJCS"); + const char *pszAuthName = oSRS.GetAuthorityName(nullptr); + const char *pszAuthCode = oSRS.GetAuthorityCode(nullptr); - if (pszAuthName != nullptr && EQUAL(pszAuthName, "epsg")) - { - nEPSGCode = atoi(oSRS.GetAuthorityCode("PROJCS")); - } - } - else if (oSRS.IsGeographic()) + if (pszAuthName && pszAuthCode && EQUAL(pszAuthName, "epsg")) { - const char *pszAuthName = oSRS.GetAuthorityName("GEOGCS"); - - if (pszAuthName != nullptr && EQUAL(pszAuthName, "epsg")) - { - nEPSGCode = atoi(oSRS.GetAuthorityCode("GEOGCS")); - } + nEPSGCode = atoi(pszAuthCode); } - // Save error state as importFromEPSGA() will call CPLReset() - CPLErrorNum errNo = CPLGetLastErrorNo(); - CPLErr eErr = CPLGetLastErrorType(); - CPLString osLastErrorMsg = CPLGetLastErrorMsg(); - - // Determine if we need to flip axis. Reimport from EPSG and make - // sure not to strip axis definitions to determine the axis order. - if (nEPSGCode != 0 && oSRS.importFromEPSGA(nEPSGCode) == OGRERR_NONE) { - if (oSRS.EPSGTreatsAsLatLong() || oSRS.EPSGTreatsAsNorthingEasting()) + CPLErrorStateBackuper oErrorStateBackuper; + // Determine if we need to flip axis. Reimport from EPSG and make + // sure not to strip axis definitions to determine the axis order. + if (nEPSGCode != 0 && oSRS.importFromEPSG(nEPSGCode) == OGRERR_NONE) { - bNeedAxisFlip = true; + if (oSRS.EPSGTreatsAsLatLong() || + oSRS.EPSGTreatsAsNorthingEasting()) + { + bNeedAxisFlip = true; + } } } - // Restore error state - CPLErrorSetState(eErr, errNo, osLastErrorMsg); - /* -------------------------------------------------------------------- */ /* Prepare coverage origin and offset vectors. Take axis */ /* order into account if needed. */ @@ -1369,6 +1380,7 @@ void GDALJP2Metadata::GetGMLJP2GeoreferencingInfo( { char *pszGMLDef = nullptr; + CPLErrorStateBackuper oErrorStateBackuper; if (oSRS.exportToXML(&pszGMLDef, nullptr) == OGRERR_NONE) { char *pszWKT = nullptr; diff --git a/gcore/gdaljp2metadata.h b/gcore/gdaljp2metadata.h index fdc3044f5aff..b3646a7d0fed 100644 --- a/gcore/gdaljp2metadata.h +++ b/gcore/gdaljp2metadata.h @@ -235,6 +235,8 @@ class CPL_DLL GDALJP2Metadata static GDALJP2Box *CreateIPRBox(GDALDataset *poSrcDS); static int IsUUID_MSI(const GByte *abyUUID); static int IsUUID_XMP(const GByte *abyUUID); + + static bool IsSRSCompatible(const OGRSpatialReference *poSRS); }; CPLXMLNode *GDALGetJPEG2000Structure(const char *pszFilename, VSILFILE *fp, From 61d7268ce2a6306b8aad72b77913101dc26100cc Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 18:38:17 +0100 Subject: [PATCH 107/132] JP2OpenJPEG: do not export GMLJP2 if the SRS isn't compatible (fixes #9223) --- autotest/gdrivers/jp2openjpeg.py | 59 +++++++++++++++++++++++++++++ frmts/opjlike/jp2opjlikedataset.cpp | 16 ++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/autotest/gdrivers/jp2openjpeg.py b/autotest/gdrivers/jp2openjpeg.py index 8ecddf3bd252..feaddd440910 100755 --- a/autotest/gdrivers/jp2openjpeg.py +++ b/autotest/gdrivers/jp2openjpeg.py @@ -3864,3 +3864,62 @@ def test_jp2openjpeg_limit_resolution_count_from_image_size(tmp_vsimem): # Check number of resolutions ret = gdal.GetJPEG2000StructureAsString(filename, ["ALL=YES"]) assert '2' in ret + + +############################################################################### +# Test unsupported XML SRS + + +def test_jp2openjpeg_unsupported_srs_for_gmljp2(tmp_vsimem): + + filename = str(tmp_vsimem / "out.jp2") + # There is no EPSG code and Albers Equal Area is not supported by OGRSpatialReference::exportToXML() + wkt = """PROJCRS["Africa_Albers_Equal_Area_Conic", + BASEGEOGCRS["WGS 84", + DATUM["World Geodetic System 1984", + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + ID["EPSG",4326]], + CONVERSION["Albers Equal Area", + METHOD["Albers Equal Area", + ID["EPSG",9822]], + PARAMETER["Latitude of false origin",0, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8821]], + PARAMETER["Longitude of false origin",25, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8822]], + PARAMETER["Latitude of 1st standard parallel",20, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8823]], + PARAMETER["Latitude of 2nd standard parallel",-23, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8824]], + PARAMETER["Easting at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8826]], + PARAMETER["Northing at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8827]]], + CS[Cartesian,2], + AXIS["easting",east, + ORDER[1], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]], + AXIS["northing",north, + ORDER[2], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]]]""" + gdal.ErrorReset() + assert gdal.Translate( + filename, "data/byte.tif", outputSRS=wkt, format="JP2OpenJPEG" + ) + assert gdal.GetLastErrorMsg() == "" + ds = gdal.Open(filename) + ref_srs = osr.SpatialReference() + ref_srs.ImportFromWkt(wkt) + assert ds.GetSpatialRef().IsSame(ref_srs) + # Check that we do *not* have a GMLJP2 box + assert "xml:gml.root-instance" not in ds.GetMetadataDomainList() diff --git a/frmts/opjlike/jp2opjlikedataset.cpp b/frmts/opjlike/jp2opjlikedataset.cpp index f044b0644f9e..b803c2846397 100644 --- a/frmts/opjlike/jp2opjlikedataset.cpp +++ b/frmts/opjlike/jp2opjlikedataset.cpp @@ -2461,7 +2461,7 @@ GDALDataset *JP2OPJLikeDataset::CreateCopy( else { const OGRSpatialReference *poSRS = poSrcDS->GetSpatialRef(); - if (poSRS != nullptr) + if (poSRS) { bGeoreferencingCompatOfGeoJP2 = TRUE; oJP2MD.SetSpatialRef(poSRS); @@ -2471,10 +2471,18 @@ GDALDataset *JP2OPJLikeDataset::CreateCopy( { bGeoreferencingCompatOfGeoJP2 = TRUE; oJP2MD.SetGeoTransform(adfGeoTransform); + if (poSRS && !poSRS->IsEmpty()) + { + bGeoreferencingCompatOfGMLJP2 = + GDALJP2Metadata::IsSRSCompatible(poSRS); + if (!bGeoreferencingCompatOfGMLJP2) + { + CPLDebug( + CODEC::debugId(), + "Cannot write GMLJP2 box due to unsupported SRS"); + } + } } - bGeoreferencingCompatOfGMLJP2 = - poSRS != nullptr && !poSRS->IsEmpty() && - poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None; } if (poSrcDS->GetMetadata("RPC") != nullptr) { From 60538c25baf8590cd99b1ea6549c6b8721fb378c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 18:38:30 +0100 Subject: [PATCH 108/132] JP2LURA: do not export GMLJP2 if the SRS isn't compatible (fixes #9223) --- frmts/jp2lura/jp2luradataset.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/frmts/jp2lura/jp2luradataset.cpp b/frmts/jp2lura/jp2luradataset.cpp index c3382e3811d7..6af9b66dcf7a 100644 --- a/frmts/jp2lura/jp2luradataset.cpp +++ b/frmts/jp2lura/jp2luradataset.cpp @@ -713,10 +713,19 @@ GDALDataset *JP2LuraDataset::CreateCopy(const char *pszFilename, { bGeoreferencingCompatOfGeoJP2 = true; oJP2MD.SetGeoTransform(adfGeoTransform); + + if (poSRS && !poSRS->IsEmpty()) + { + bGeoreferencingCompatOfGMLJP2 = + GDALJP2Metadata::IsSRSCompatible(poSRS); + if (!bGeoreferencingCompatOfGMLJP2) + { + CPLDebug( + "JP2LURA", + "Cannot write GMLJP2 box due to unsupported SRS"); + } + } } - bGeoreferencingCompatOfGMLJP2 = - poSRS != nullptr && !poSRS->IsEmpty() && - poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None; } if (poSrcDS->GetMetadata("RPC") != nullptr) { From ee7e0bb80220f866b48e28f04a6217c4d24c9fbd Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 18:38:43 +0100 Subject: [PATCH 109/132] JP2KAK: do not export GMLJP2 if the SRS isn't compatible (fixes #9223) --- autotest/gdrivers/jp2kak.py | 59 +++++++++++++++++++++++++++++++++- frmts/jp2kak/jp2kakdataset.cpp | 46 +++++++++++++++++++------- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/autotest/gdrivers/jp2kak.py b/autotest/gdrivers/jp2kak.py index 209032e4e7c6..c8f14d7aa6eb 100755 --- a/autotest/gdrivers/jp2kak.py +++ b/autotest/gdrivers/jp2kak.py @@ -33,7 +33,7 @@ import gdaltest import pytest -from osgeo import gdal +from osgeo import gdal, osr pytestmark = pytest.mark.require_driver("JP2KAK") @@ -953,3 +953,60 @@ def find_xml_node(ar, element_name, only_attributes=False): if found is not None: return found return None + + +############################################################################### +# Test unsupported XML SRS + + +def test_jp2kak_unsupported_srs_for_gmljp2(tmp_vsimem): + + filename = str(tmp_vsimem / "out.jp2") + # There is no EPSG code and Albers Equal Area is not supported by OGRSpatialReference::exportToXML() + wkt = """PROJCRS["Africa_Albers_Equal_Area_Conic", + BASEGEOGCRS["WGS 84", + DATUM["World Geodetic System 1984", + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + ID["EPSG",4326]], + CONVERSION["Albers Equal Area", + METHOD["Albers Equal Area", + ID["EPSG",9822]], + PARAMETER["Latitude of false origin",0, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8821]], + PARAMETER["Longitude of false origin",25, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8822]], + PARAMETER["Latitude of 1st standard parallel",20, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8823]], + PARAMETER["Latitude of 2nd standard parallel",-23, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8824]], + PARAMETER["Easting at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8826]], + PARAMETER["Northing at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8827]]], + CS[Cartesian,2], + AXIS["easting",east, + ORDER[1], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]], + AXIS["northing",north, + ORDER[2], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]]]""" + gdal.ErrorReset() + assert gdal.Translate(filename, "data/byte.tif", outputSRS=wkt, format="JP2KAK") + assert gdal.GetLastErrorMsg() == "" + ds = gdal.Open(filename) + ref_srs = osr.SpatialReference() + ref_srs.ImportFromWkt(wkt) + assert ds.GetSpatialRef().IsSame(ref_srs) + # Check that we do *not* have a GMLJP2 box + assert "xml:gml.root-instance" not in ds.GetMetadataDomainList() diff --git a/frmts/jp2kak/jp2kakdataset.cpp b/frmts/jp2kak/jp2kakdataset.cpp index 287fea1931b1..4738b11c69d5 100644 --- a/frmts/jp2kak/jp2kakdataset.cpp +++ b/frmts/jp2kak/jp2kakdataset.cpp @@ -2701,7 +2701,7 @@ static GDALDataset *JP2KAKCreateCopy(const char *pszFilename, { const char *pszGMLJP2V2Def = CSLFetchNameValue(papszOptions, "GMLJP2V2_DEF"); - GDALJP2Box *poBox; + GDALJP2Box *poBox = nullptr; if (pszGMLJP2V2Def != nullptr) { poBox = oJP2MD.CreateGMLJP2V2(nXSize, nYSize, pszGMLJP2V2Def, @@ -2709,18 +2709,42 @@ static GDALDataset *JP2KAKCreateCopy(const char *pszFilename, } else { - poBox = oJP2MD.CreateGMLJP2(nXSize, nYSize); + const OGRSpatialReference *poSRS = + poSrcDS->GetGCPCount() > 0 ? poSrcDS->GetGCPSpatialRef() + : poSrcDS->GetSpatialRef(); + if (!poSRS || poSRS->IsEmpty() || + GDALJP2Metadata::IsSRSCompatible(poSRS)) + { + poBox = oJP2MD.CreateGMLJP2(nXSize, nYSize); + } + else if (CSLFetchNameValue(papszOptions, "GMLJP2")) + { + CPLError(CE_Warning, CPLE_AppDefined, + "GMLJP2 box was explicitly required but cannot be " + "written due " + "to lack of georeferencing and/or unsupported " + "georeferencing " + "for GMLJP2"); + } + else + { + CPLDebug("JP2KAK", + "Cannot write GMLJP2 box due to unsupported SRS"); + } } - try + if (poBox) { - JP2KAKWriteBox(&family, poBox); - } - catch (...) - { - CPLDebug("JP2KAK", "JP2KAKWriteBox) - caught exception."); - oCodeStream.destroy(); - delete poBox; - return nullptr; + try + { + JP2KAKWriteBox(&family, poBox); + } + catch (...) + { + CPLDebug("JP2KAK", "JP2KAKWriteBox) - caught exception."); + oCodeStream.destroy(); + delete poBox; + return nullptr; + } } } if (CPLFetchBool(papszOptions, "GeoJP2", true)) From 38c9e51709d4af176f26df264809ca3b33b81882 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 19:02:25 +0100 Subject: [PATCH 110/132] GPKG: CreateFeature(): allow creating a feature with FID=0 (refs #9225) --- autotest/ogr/ogr_gpkg.py | 24 +++++++++++++++++++ .../gpkg/ogrgeopackagetablelayer.cpp | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/autotest/ogr/ogr_gpkg.py b/autotest/ogr/ogr_gpkg.py index 9f49f594b8fa..6d58c77795dd 100755 --- a/autotest/ogr/ogr_gpkg.py +++ b/autotest/ogr/ogr_gpkg.py @@ -2295,6 +2295,30 @@ def test_ogr_gpkg_22(tmp_vsimem): assert f.GetFID() == 1234567890123 +############################################################################### +# Test creating a feature with FID 0 + + +def test_ogr_gpkg_create_with_fid_0(tmp_vsimem): + + fname = tmp_vsimem / "test_ogr_gpkg_create_with_fid_0.gpkg" + + ds = gdaltest.gpkg_dr.CreateDataSource(fname) + lyr = ds.CreateLayer("test") + + feat = ogr.Feature(lyr.GetLayerDefn()) + feat.SetFID(0) + lyr.CreateFeature(feat) + feat = None + + ds = None + + ds = ogr.Open(fname) + lyr = ds.GetLayerByName("test") + f = lyr.GetNextFeature() + assert f.GetFID() == 0 + + ############################################################################### # Test not nullable fields diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagetablelayer.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagetablelayer.cpp index c26f19be2df1..9813599d8ca3 100644 --- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagetablelayer.cpp +++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagetablelayer.cpp @@ -2433,7 +2433,7 @@ OGRErr OGRGeoPackageTableLayer::CreateOrUpsertFeature(OGRFeature *poFeature, #if SQLITE_VERSION_NUMBER >= 3035000L sqlite3_column_int64(m_poInsertStatement, 0) #else - 0 + OGRNullFID #endif : sqlite3_last_insert_rowid(m_poDS->GetDB()); @@ -2446,7 +2446,7 @@ OGRErr OGRGeoPackageTableLayer::CreateOrUpsertFeature(OGRFeature *poFeature, m_poInsertStatement = nullptr; } - if (nFID != 0) + if (nFID != OGRNullFID) { poFeature->SetFID(nFID); if (m_iFIDAsRegularColumnIndex >= 0) From 1e4cb8d9374cd2494ff09f633caf46bd099b423f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 19:03:03 +0100 Subject: [PATCH 111/132] ogr2ogr: force -preserve_fid when doing GPX to GPKG (fixes #9225) --- apps/ogr2ogr_lib.cpp | 15 +++++++++++++++ autotest/ogr/ogr_gpx.py | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/apps/ogr2ogr_lib.cpp b/apps/ogr2ogr_lib.cpp index 4a74a870a731..a3c404c53133 100644 --- a/apps/ogr2ogr_lib.cpp +++ b/apps/ogr2ogr_lib.cpp @@ -4324,6 +4324,21 @@ SetupTargetLayer::Setup(OGRLayer *poSrcLayer, const char *pszNewLayerName, bPreserveFID = false; } } + // Detect scenario of converting from GPX to a format like GPKG + // Cf https://github.com/OSGeo/gdal/issues/9225 + else if (!bPreserveFID && !m_bUnsetFid && !bAppend && + m_poSrcDS->GetDriver() && + EQUAL(m_poSrcDS->GetDriver()->GetDescription(), "GPX") && + pszDestCreationOptions && + (strstr(pszDestCreationOptions, "='FID'") != nullptr || + strstr(pszDestCreationOptions, "=\"FID\"") != nullptr) && + CSLFetchNameValue(m_papszLCO, "FID") == nullptr) + { + CPLDebug("GDALVectorTranslate", + "Forcing -preserve_fid because source is GPX and layers " + "have FID cross references"); + bPreserveFID = true; + } // Detect scenario of converting GML2 with fid attribute to GPKG else if (EQUAL(m_poDstDS->GetDriver()->GetDescription(), "GPKG") && CSLFetchNameValue(m_papszLCO, "FID") == nullptr) diff --git a/autotest/ogr/ogr_gpx.py b/autotest/ogr/ogr_gpx.py index f9bff84c1dc7..3af9c0a10a8c 100755 --- a/autotest/ogr/ogr_gpx.py +++ b/autotest/ogr/ogr_gpx.py @@ -572,3 +572,22 @@ def test_ogr_gpx_N_MAX_LINKS(): lyr = ds.GetLayerByName("track_points") f = lyr.GetNextFeature() assert f["link3_href"] is None + + +############################################################################### +# Test preservation of FID when converting to GPKG +# (https://github.com/OSGeo/gdal/issues/9225) + + +@pytest.mark.require_driver("GPKG") +def test_ogr_gpx_convert_to_gpkg(tmp_vsimem): + + outfilename = str(tmp_vsimem / "out.gpkg") + gdal.VectorTranslate(outfilename, "data/gpx/test.gpx") + + ds = ogr.Open(outfilename) + lyr = ds.GetLayer("tracks") + f = lyr.GetNextFeature() + assert f.GetFID() == 0 + f = lyr.GetNextFeature() + assert f.GetFID() == 1 From db4544420fdd7f2504bde88a1f68846e8ba870ed Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 22:41:17 +0100 Subject: [PATCH 112/132] VRTDerivedRasterBand: avoid potential initialization order fiasco (fixes #2884) --- frmts/vrt/vrtdataset.h | 2 +- frmts/vrt/vrtderivedrasterband.cpp | 35 ++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/frmts/vrt/vrtdataset.h b/frmts/vrt/vrtdataset.h index 439f3341f0db..aa6e352b296e 100644 --- a/frmts/vrt/vrtdataset.h +++ b/frmts/vrt/vrtdataset.h @@ -909,7 +909,7 @@ class CPL_DLL VRTDerivedRasterBand CPL_NON_FINAL : public VRTSourcedRasterBand GDALDerivedPixelFuncWithArgs pfnPixelFunc, const char *pszMetadata); - static std::pair * + static const std::pair * GetPixelFunction(const char *pszFuncNameIn); void SetPixelFunctionName(const char *pszFuncNameIn); diff --git a/frmts/vrt/vrtderivedrasterband.cpp b/frmts/vrt/vrtderivedrasterband.cpp index b69360ef58ad..f264826c4d6a 100644 --- a/frmts/vrt/vrtderivedrasterband.cpp +++ b/frmts/vrt/vrtderivedrasterband.cpp @@ -50,10 +50,6 @@ using namespace GDALPy; #define GDAL_VRT_ENABLE_PYTHON_DEFAULT "TRUSTED_MODULES" #endif -static std::map> - osMapPixelFunction; - /* Flags for getting buffers */ #define PyBUF_WRITABLE 0x0001 #define PyBUF_FORMAT 0x0004 @@ -231,6 +227,20 @@ void VRTDerivedRasterBand::Cleanup() { } +/************************************************************************/ +/* GetGlobalMapPixelFunction() */ +/************************************************************************/ + +static std::map> & +GetGlobalMapPixelFunction() +{ + static std::map> + gosMapPixelFunction; + return gosMapPixelFunction; +} + /************************************************************************/ /* AddPixelFunction() */ /************************************************************************/ @@ -261,7 +271,7 @@ CPLErr CPL_STDCALL GDALAddDerivedBandPixelFunc( return CE_None; } - osMapPixelFunction[pszName] = { + GetGlobalMapPixelFunction()[pszName] = { [pfnNewFunction](void **papoSources, int nSources, void *pData, int nBufXSize, int nBufYSize, GDALDataType eSrcType, GDALDataType eBufType, int nPixelSpace, int nLineSpace, @@ -299,13 +309,13 @@ CPLErr CPL_STDCALL GDALAddDerivedBandPixelFuncWithArgs( const char *pszName, GDALDerivedPixelFuncWithArgs pfnNewFunction, const char *pszMetadata) { - if (pszName == nullptr || pszName[0] == '\0' || pfnNewFunction == nullptr) + if (!pszName || pszName[0] == '\0' || !pfnNewFunction) { return CE_None; } - osMapPixelFunction[pszName] = {pfnNewFunction, - pszMetadata != nullptr ? pszMetadata : ""}; + GetGlobalMapPixelFunction()[pszName] = {pfnNewFunction, + pszMetadata ? pszMetadata : ""}; return CE_None; } @@ -353,7 +363,7 @@ CPLErr VRTDerivedRasterBand::AddPixelFunction( * @return A derived band pixel function, or NULL if none have been * registered for pszFuncName. */ -std::pair * +const std::pair * VRTDerivedRasterBand::GetPixelFunction(const char *pszFuncNameIn) { if (pszFuncNameIn == nullptr || pszFuncNameIn[0] == '\0') @@ -361,9 +371,10 @@ VRTDerivedRasterBand::GetPixelFunction(const char *pszFuncNameIn) return nullptr; } - auto oIter = osMapPixelFunction.find(pszFuncNameIn); + const auto &oMapPixelFunction = GetGlobalMapPixelFunction(); + const auto oIter = oMapPixelFunction.find(pszFuncNameIn); - if (oIter == osMapPixelFunction.end()) + if (oIter == oMapPixelFunction.end()) return nullptr; return &(oIter->second); @@ -945,7 +956,7 @@ CPLErr VRTDerivedRasterBand::IRasterIO( } /* ---- Get pixel function for band ---- */ - std::pair *poPixelFunc = nullptr; + const std::pair *poPixelFunc = nullptr; std::vector> oAdditionalArgs; if (EQUAL(m_poPrivate->m_osLanguage, "C")) From f220fc9b146e1e1705d6dc93cefd258ed5004143 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 15:30:37 +0100 Subject: [PATCH 113/132] GDALDeserializeGCPListFromXML(): make it take a const CPLXMLNode* --- gcore/gdal_misc.cpp | 6 +++--- gcore/gdal_priv.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gcore/gdal_misc.cpp b/gcore/gdal_misc.cpp index 63a23e5edc20..b219a9437154 100644 --- a/gcore/gdal_misc.cpp +++ b/gcore/gdal_misc.cpp @@ -4125,7 +4125,7 @@ void GDALSerializeGCPListToXML(CPLXMLNode *psParentNode, GDAL_GCP *pasGCPList, /* GDALDeserializeGCPListFromXML() */ /************************************************************************/ -void GDALDeserializeGCPListFromXML(CPLXMLNode *psGCPList, +void GDALDeserializeGCPListFromXML(const CPLXMLNode *psGCPList, GDAL_GCP **ppasGCPList, int *pnGCPCount, OGRSpatialReference **ppoGCP_SRS) { @@ -4168,7 +4168,7 @@ void GDALDeserializeGCPListFromXML(CPLXMLNode *psGCPList, // Count GCPs. int nGCPMax = 0; - for (CPLXMLNode *psXMLGCP = psGCPList->psChild; psXMLGCP != nullptr; + for (const CPLXMLNode *psXMLGCP = psGCPList->psChild; psXMLGCP != nullptr; psXMLGCP = psXMLGCP->psNext) { @@ -4185,7 +4185,7 @@ void GDALDeserializeGCPListFromXML(CPLXMLNode *psGCPList, if (nGCPMax == 0) return; - for (CPLXMLNode *psXMLGCP = psGCPList->psChild; + for (const CPLXMLNode *psXMLGCP = psGCPList->psChild; *ppasGCPList != nullptr && psXMLGCP != nullptr; psXMLGCP = psXMLGCP->psNext) { diff --git a/gcore/gdal_priv.h b/gcore/gdal_priv.h index 60f0be90536b..0368de11c838 100644 --- a/gcore/gdal_priv.h +++ b/gcore/gdal_priv.h @@ -4056,7 +4056,7 @@ double GDALAdjustNoDataCloseToFloatMax(double dfVal); void GDALSerializeGCPListToXML(CPLXMLNode *psParentNode, GDAL_GCP *pasGCPList, int nGCPCount, const OGRSpatialReference *poGCP_SRS); -void GDALDeserializeGCPListFromXML(CPLXMLNode *psGCPList, +void GDALDeserializeGCPListFromXML(const CPLXMLNode *psGCPList, GDAL_GCP **ppasGCPList, int *pnGCPCount, OGRSpatialReference **ppoGCP_SRS); From 187541adc48ccc6d592331591399d5d690bf0514 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 15:30:53 +0100 Subject: [PATCH 114/132] VRT: make XMLInit() method take a const CPLXMLNode* --- frmts/vrt/vrtdataset.cpp | 28 +++++++++------------ frmts/vrt/vrtdataset.h | 40 +++++++++++++++--------------- frmts/vrt/vrtderivedrasterband.cpp | 7 +++--- frmts/vrt/vrtdriver.cpp | 2 +- frmts/vrt/vrtfilters.cpp | 4 +-- frmts/vrt/vrtmultidim.cpp | 6 ++--- frmts/vrt/vrtpansharpened.cpp | 15 +++++------ frmts/vrt/vrtrasterband.cpp | 19 +++++++------- frmts/vrt/vrtrawrasterband.cpp | 2 +- frmts/vrt/vrtsourcedrasterband.cpp | 4 +-- frmts/vrt/vrtsources.cpp | 30 ++++++++++++---------- frmts/vrt/vrtwarped.cpp | 12 ++++++--- 12 files changed, 87 insertions(+), 82 deletions(-) diff --git a/frmts/vrt/vrtdataset.cpp b/frmts/vrt/vrtdataset.cpp index 716137dd58ee..5e99d7437ed6 100644 --- a/frmts/vrt/vrtdataset.cpp +++ b/frmts/vrt/vrtdataset.cpp @@ -434,7 +434,7 @@ VRTRasterBand *VRTDataset::InitBand(const char *pszSubclass, int nBand, /* XMLInit() */ /************************************************************************/ -CPLErr VRTDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) +CPLErr VRTDataset::XMLInit(const CPLXMLNode *psTree, const char *pszVRTPathIn) { if (pszVRTPathIn != nullptr) @@ -443,7 +443,7 @@ CPLErr VRTDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) /* -------------------------------------------------------------------- */ /* Check for an SRS node. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psSRSNode = CPLGetXMLNode(psTree, "SRS"); + const CPLXMLNode *psSRSNode = CPLGetXMLNode(psTree, "SRS"); if (psSRSNode) { if (m_poSRS) @@ -480,11 +480,12 @@ CPLErr VRTDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) /* -------------------------------------------------------------------- */ /* Check for a GeoTransform node. */ /* -------------------------------------------------------------------- */ - if (strlen(CPLGetXMLValue(psTree, "GeoTransform", "")) > 0) + const char *pszGT = CPLGetXMLValue(psTree, "GeoTransform", ""); + if (strlen(pszGT) > 0) { - const char *pszGT = CPLGetXMLValue(psTree, "GeoTransform", ""); - char **papszTokens = CSLTokenizeStringComplex(pszGT, ",", FALSE, FALSE); - if (CSLCount(papszTokens) != 6) + const CPLStringList aosTokens( + CSLTokenizeStringComplex(pszGT, ",", FALSE, FALSE)); + if (aosTokens.size() != 6) { CPLError(CE_Warning, CPLE_AppDefined, "GeoTransform node does not have expected six values."); @@ -492,19 +493,15 @@ CPLErr VRTDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) else { for (int iTA = 0; iTA < 6; iTA++) - m_adfGeoTransform[iTA] = CPLAtof(papszTokens[iTA]); + m_adfGeoTransform[iTA] = CPLAtof(aosTokens[iTA]); m_bGeoTransformSet = TRUE; } - - CSLDestroy(papszTokens); } /* -------------------------------------------------------------------- */ /* Check for GCPs. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psGCPList = CPLGetXMLNode(psTree, "GCPList"); - - if (psGCPList != nullptr) + if (const CPLXMLNode *psGCPList = CPLGetXMLNode(psTree, "GCPList")) { GDALDeserializeGCPListFromXML(psGCPList, &m_pasGCPList, &m_nGCPCount, &m_poGCP_SRS); @@ -520,9 +517,9 @@ CPLErr VRTDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) /* -------------------------------------------------------------------- */ /* Parse dataset mask band first */ - CPLXMLNode *psMaskBandNode = CPLGetXMLNode(psTree, "MaskBand"); + const CPLXMLNode *psMaskBandNode = CPLGetXMLNode(psTree, "MaskBand"); - CPLXMLNode *psChild = nullptr; + const CPLXMLNode *psChild = nullptr; if (psMaskBandNode) psChild = psMaskBandNode->psChild; else @@ -581,8 +578,7 @@ CPLErr VRTDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) } } - CPLXMLNode *psGroup = CPLGetXMLNode(psTree, "Group"); - if (psGroup) + if (const CPLXMLNode *psGroup = CPLGetXMLNode(psTree, "Group")) { const char *pszName = CPLGetXMLValue(psGroup, "name", nullptr); if (pszName == nullptr || !EQUAL(pszName, "/")) diff --git a/frmts/vrt/vrtdataset.h b/frmts/vrt/vrtdataset.h index 439f3341f0db..3319366bf5eb 100644 --- a/frmts/vrt/vrtdataset.h +++ b/frmts/vrt/vrtdataset.h @@ -154,7 +154,7 @@ class CPL_DLL VRTSource int bApproxOK, GDALProgressFunc pfnProgress, void *pProgressData) = 0; - virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *psTree, const char *, std::map &) = 0; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) = 0; @@ -172,17 +172,17 @@ class CPL_DLL VRTSource }; typedef VRTSource *(*VRTSourceParser)( - CPLXMLNode *, const char *, + const CPLXMLNode *, const char *, std::map &oMapSharedSources); VRTSource * -VRTParseCoreSources(CPLXMLNode *psTree, const char *, +VRTParseCoreSources(const CPLXMLNode *psTree, const char *, std::map &oMapSharedSources); VRTSource * -VRTParseFilterSources(CPLXMLNode *psTree, const char *, +VRTParseFilterSources(const CPLXMLNode *psTree, const char *, std::map &oMapSharedSources); VRTSource * -VRTParseArraySource(CPLXMLNode *psTree, const char *, +VRTParseArraySource(const CPLXMLNode *psTree, const char *, std::map &oMapSharedSources); /************************************************************************/ @@ -335,7 +335,7 @@ class CPL_DLL VRTDataset CPL_NON_FINAL : public GDALDataset char **papszOptions) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath); - virtual CPLErr XMLInit(CPLXMLNode *, const char *); + virtual CPLErr XMLInit(const CPLXMLNode *, const char *); virtual CPLErr IBuildOverviews(const char *, int, const int *, int, const int *, GDALProgressFunc, void *, @@ -419,7 +419,7 @@ class CPL_DLL VRTWarpedDataset final : public VRTDataset const char *pszDomain = "") override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; - virtual CPLErr XMLInit(CPLXMLNode *, const char *) override; + virtual CPLErr XMLInit(const CPLXMLNode *, const char *) override; virtual CPLErr AddBand(GDALDataType eType, char **papszOptions = nullptr) override; @@ -481,10 +481,10 @@ class VRTPansharpenedDataset final : public VRTDataset virtual CPLErr FlushCache(bool bAtClosing) override; - virtual CPLErr XMLInit(CPLXMLNode *, const char *) override; + virtual CPLErr XMLInit(const CPLXMLNode *, const char *) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; - CPLErr XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, + CPLErr XMLInit(const CPLXMLNode *psTree, const char *pszVRTPath, GDALRasterBandH hPanchroBandIn, int nInputSpectralBandsIn, GDALRasterBandH *pahInputSpectralBandsIn); @@ -567,7 +567,7 @@ class CPL_DLL VRTRasterBand CPL_NON_FINAL : public GDALRasterBand VRTRasterBand(); virtual ~VRTRasterBand(); - virtual CPLErr XMLInit(CPLXMLNode *, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *, const char *, std::map &); virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, bool &bHasWarnedAboutRAMUsage, @@ -709,7 +709,7 @@ class CPL_DLL VRTSourcedRasterBand CPL_NON_FINAL : public VRTRasterBand virtual CPLErr SetMetadataItem(const char *pszName, const char *pszValue, const char *pszDomain = "") override; - virtual CPLErr XMLInit(CPLXMLNode *, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, bool &bHasWarnedAboutRAMUsage, @@ -916,7 +916,7 @@ class CPL_DLL VRTDerivedRasterBand CPL_NON_FINAL : public VRTSourcedRasterBand void SetSourceTransferType(GDALDataType eDataType); void SetPixelFunctionLanguage(const char *pszLanguage); - virtual CPLErr XMLInit(CPLXMLNode *, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, bool &bHasWarnedAboutRAMUsage, @@ -959,7 +959,7 @@ class CPL_DLL VRTRawRasterBand CPL_NON_FINAL : public VRTRasterBand GDALDataType eType = GDT_Unknown); virtual ~VRTRawRasterBand(); - virtual CPLErr XMLInit(CPLXMLNode *, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath, bool &bHasWarnedAboutRAMUsage, @@ -1010,7 +1010,7 @@ class VRTDriver final : public GDALDriver const char *pszDomain = "") override; VRTSource * - ParseSource(CPLXMLNode *psSrc, const char *pszVRTPath, + ParseSource(const CPLXMLNode *psSrc, const char *pszVRTPath, std::map &oMapSharedSources); void AddSourceParser(const char *pszElementName, VRTSourceParser pfnParser); }; @@ -1091,7 +1091,7 @@ class CPL_DLL VRTSimpleSource CPL_NON_FINAL : public VRTSource double dfYDstRatio); virtual ~VRTSimpleSource(); - virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *psTree, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; @@ -1240,7 +1240,7 @@ class VRTNoDataFromMaskSource final : public VRTSimpleSource void SetParameters(double dfNoDataValue, double dfMaskValueThreshold, double dfRemappedValue); - virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *psTree, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; virtual const char *GetType() override @@ -1332,7 +1332,7 @@ class CPL_DLL VRTComplexSource CPL_NON_FINAL : public VRTSimpleSource void *pProgressData) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; - virtual CPLErr XMLInit(CPLXMLNode *, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *, const char *, std::map &) override; virtual const char *GetType() override { @@ -1415,7 +1415,7 @@ class VRTKernelFilteredSource CPL_NON_FINAL : public VRTFilteredSource VRTKernelFilteredSource(); virtual ~VRTKernelFilteredSource(); - virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *psTree, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; @@ -1438,7 +1438,7 @@ class VRTAverageFilteredSource final : public VRTKernelFilteredSource explicit VRTAverageFilteredSource(int nKernelSize); virtual ~VRTAverageFilteredSource(); - virtual CPLErr XMLInit(CPLXMLNode *psTree, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *psTree, const char *, std::map &) override; virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; }; @@ -1454,7 +1454,7 @@ class VRTFuncSource final : public VRTSource VRTFuncSource(); virtual ~VRTFuncSource(); - virtual CPLErr XMLInit(CPLXMLNode *, const char *, + virtual CPLErr XMLInit(const CPLXMLNode *, const char *, std::map &) override { return CE_Failure; diff --git a/frmts/vrt/vrtderivedrasterband.cpp b/frmts/vrt/vrtderivedrasterband.cpp index b69360ef58ad..7bf09532618b 100644 --- a/frmts/vrt/vrtderivedrasterband.cpp +++ b/frmts/vrt/vrtderivedrasterband.cpp @@ -1406,7 +1406,7 @@ int VRTDerivedRasterBand::IGetDataCoverageStatus( /************************************************************************/ CPLErr VRTDerivedRasterBand::XMLInit( - CPLXMLNode *psTree, const char *pszVRTPath, + const CPLXMLNode *psTree, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -1457,10 +1457,11 @@ CPLErr VRTDerivedRasterBand::XMLInit( return CE_Failure; } - CPLXMLNode *psArgs = CPLGetXMLNode(psTree, "PixelFunctionArguments"); + const CPLXMLNode *const psArgs = + CPLGetXMLNode(psTree, "PixelFunctionArguments"); if (psArgs != nullptr) { - for (CPLXMLNode *psIter = psArgs->psChild; psIter != nullptr; + for (const CPLXMLNode *psIter = psArgs->psChild; psIter; psIter = psIter->psNext) { if (psIter->eType == CXT_Attribute) diff --git a/frmts/vrt/vrtdriver.cpp b/frmts/vrt/vrtdriver.cpp index 87068b12246c..2883944b4e13 100644 --- a/frmts/vrt/vrtdriver.cpp +++ b/frmts/vrt/vrtdriver.cpp @@ -136,7 +136,7 @@ void VRTDriver::AddSourceParser(const char *pszElementName, /************************************************************************/ VRTSource * -VRTDriver::ParseSource(CPLXMLNode *psSrc, const char *pszVRTPath, +VRTDriver::ParseSource(const CPLXMLNode *psSrc, const char *pszVRTPath, std::map &oMapSharedSources) { diff --git a/frmts/vrt/vrtfilters.cpp b/frmts/vrt/vrtfilters.cpp index dfd41b7a1137..3350afeff354 100644 --- a/frmts/vrt/vrtfilters.cpp +++ b/frmts/vrt/vrtfilters.cpp @@ -592,7 +592,7 @@ CPLErr VRTKernelFilteredSource::FilterData(int nXSize, int nYSize, /************************************************************************/ CPLErr VRTKernelFilteredSource::XMLInit( - CPLXMLNode *psTree, const char *pszVRTPath, + const CPLXMLNode *psTree, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -705,7 +705,7 @@ CPLXMLNode *VRTKernelFilteredSource::SerializeToXML(const char *pszVRTPath) /************************************************************************/ VRTSource * -VRTParseFilterSources(CPLXMLNode *psChild, const char *pszVRTPath, +VRTParseFilterSources(const CPLXMLNode *psChild, const char *pszVRTPath, std::map &oMapSharedSources) { diff --git a/frmts/vrt/vrtmultidim.cpp b/frmts/vrt/vrtmultidim.cpp index d4305915a126..d4706b081659 100644 --- a/frmts/vrt/vrtmultidim.cpp +++ b/frmts/vrt/vrtmultidim.cpp @@ -2627,7 +2627,7 @@ class VRTArraySource : public VRTSource } CPLErr - XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, + XMLInit(const CPLXMLNode *psTree, const char *pszVRTPath, std::map &oMapSharedSources) override; CPLXMLNode *SerializeToXML(const char *pszVRTPath) override; }; @@ -2706,7 +2706,7 @@ ParseSingleSourceArray(const CPLXMLNode *psSingleSourceArray, /************************************************************************/ CPLErr VRTArraySource::XMLInit( - CPLXMLNode *psTree, const char *pszVRTPath, + const CPLXMLNode *psTree, const char *pszVRTPath, std::map & /*oMapSharedSources*/) { const auto poArray = ParseArray(psTree, pszVRTPath, "ArraySource"); @@ -2978,7 +2978,7 @@ static std::shared_ptr ParseArray(const CPLXMLNode *psTree, /************************************************************************/ VRTSource * -VRTParseArraySource(CPLXMLNode *psChild, const char *pszVRTPath, +VRTParseArraySource(const CPLXMLNode *psChild, const char *pszVRTPath, std::map &oMapSharedSources) { VRTSource *poSource = nullptr; diff --git a/frmts/vrt/vrtpansharpened.cpp b/frmts/vrt/vrtpansharpened.cpp index cecf614d0710..e055848eb2fd 100644 --- a/frmts/vrt/vrtpansharpened.cpp +++ b/frmts/vrt/vrtpansharpened.cpp @@ -271,14 +271,14 @@ char **VRTPansharpenedDataset::GetFileList() /* XMLInit() */ /************************************************************************/ -CPLErr VRTPansharpenedDataset::XMLInit(CPLXMLNode *psTree, +CPLErr VRTPansharpenedDataset::XMLInit(const CPLXMLNode *psTree, const char *pszVRTPathIn) { return XMLInit(psTree, pszVRTPathIn, nullptr, 0, nullptr); } -CPLErr VRTPansharpenedDataset::XMLInit(CPLXMLNode *psTree, +CPLErr VRTPansharpenedDataset::XMLInit(const CPLXMLNode *psTree, const char *pszVRTPathIn, GDALRasterBandH hPanchroBandIn, int nInputSpectralBandsIn, @@ -299,7 +299,7 @@ CPLErr VRTPansharpenedDataset::XMLInit(CPLXMLNode *psTree, /* Parse PansharpeningOptions */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psOptions = CPLGetXMLNode(psTree, "PansharpeningOptions"); + const CPLXMLNode *psOptions = CPLGetXMLNode(psTree, "PansharpeningOptions"); if (psOptions == nullptr) { CPLError(CE_Failure, CPLE_AppDefined, "Missing PansharpeningOptions"); @@ -325,7 +325,8 @@ CPLErr VRTPansharpenedDataset::XMLInit(CPLXMLNode *psTree, if (hPanchroBandIn == nullptr) { - CPLXMLNode *psPanchroBand = CPLGetXMLNode(psOptions, "PanchroBand"); + const CPLXMLNode *psPanchroBand = + CPLGetXMLNode(psOptions, "PanchroBand"); if (psPanchroBand == nullptr) { CPLError(CE_Failure, CPLE_AppDefined, "PanchroBand missing"); @@ -433,8 +434,8 @@ CPLErr VRTPansharpenedDataset::XMLInit(CPLXMLNode *psTree, } std::vector adfWeights; - CPLXMLNode *psAlgOptions = CPLGetXMLNode(psOptions, "AlgorithmOptions"); - if (psAlgOptions != nullptr) + if (const CPLXMLNode *psAlgOptions = + CPLGetXMLNode(psOptions, "AlgorithmOptions")) { const char *pszWeights = CPLGetXMLValue(psAlgOptions, "Weights", nullptr); @@ -499,7 +500,7 @@ CPLErr VRTPansharpenedDataset::XMLInit(CPLXMLNode *psTree, /* First pass on spectral datasets to check their georeferencing. */ /* -------------------------------------------------------------------- */ int iSpectralBand = 0; - for (CPLXMLNode *psIter = psOptions->psChild; psIter; + for (const CPLXMLNode *psIter = psOptions->psChild; psIter; psIter = psIter->psNext) { GDALDataset *poDataset; diff --git a/frmts/vrt/vrtrasterband.cpp b/frmts/vrt/vrtrasterband.cpp index 37bfd0c18312..c4f3df26d2bb 100644 --- a/frmts/vrt/vrtrasterband.cpp +++ b/frmts/vrt/vrtrasterband.cpp @@ -343,7 +343,7 @@ VRTParseColorTable(const CPLXMLNode *psColorTable) /************************************************************************/ CPLErr -VRTRasterBand::XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, +VRTRasterBand::XMLInit(const CPLXMLNode *psTree, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -481,20 +481,18 @@ VRTRasterBand::XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, /* -------------------------------------------------------------------- */ /* Histograms */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psHist = CPLGetXMLNode(psTree, "Histograms"); + const CPLXMLNode *psHist = CPLGetXMLNode(psTree, "Histograms"); if (psHist != nullptr) { - CPLXMLNode *psNext = psHist->psNext; - psHist->psNext = nullptr; - - m_psSavedHistograms = CPLCloneXMLTree(psHist); - psHist->psNext = psNext; + CPLXMLNode sHistTemp = *psHist; + sHistTemp.psNext = nullptr; + m_psSavedHistograms = CPLCloneXMLTree(&sHistTemp); } /* ==================================================================== */ /* Overviews */ /* ==================================================================== */ - CPLXMLNode *psNode = psTree->psChild; + const CPLXMLNode *psNode = psTree->psChild; for (; psNode != nullptr; psNode = psNode->psNext) { @@ -507,7 +505,8 @@ VRTRasterBand::XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, /* Prepare filename. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psFileNameNode = CPLGetXMLNode(psNode, "SourceFilename"); + const CPLXMLNode *psFileNameNode = + CPLGetXMLNode(psNode, "SourceFilename"); const char *pszFilename = psFileNameNode ? CPLGetXMLValue(psFileNameNode, nullptr, nullptr) : nullptr; @@ -557,7 +556,7 @@ VRTRasterBand::XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, /* ==================================================================== */ /* Mask band (specific to that raster band) */ /* ==================================================================== */ - CPLXMLNode *psMaskBandNode = CPLGetXMLNode(psTree, "MaskBand"); + const CPLXMLNode *psMaskBandNode = CPLGetXMLNode(psTree, "MaskBand"); if (psMaskBandNode) psNode = psMaskBandNode->psChild; else diff --git a/frmts/vrt/vrtrawrasterband.cpp b/frmts/vrt/vrtrawrasterband.cpp index 0c7086effe10..e5c25753a518 100644 --- a/frmts/vrt/vrtrawrasterband.cpp +++ b/frmts/vrt/vrtrawrasterband.cpp @@ -344,7 +344,7 @@ CPLVirtualMem *VRTRawRasterBand::GetVirtualMemAuto(GDALRWFlag eRWFlag, /************************************************************************/ CPLErr -VRTRawRasterBand::XMLInit(CPLXMLNode *psTree, const char *pszVRTPath, +VRTRawRasterBand::XMLInit(const CPLXMLNode *psTree, const char *pszVRTPath, std::map &oMapSharedSources) { diff --git a/frmts/vrt/vrtsourcedrasterband.cpp b/frmts/vrt/vrtsourcedrasterband.cpp index 0067f27abfac..b790c1a7e129 100644 --- a/frmts/vrt/vrtsourcedrasterband.cpp +++ b/frmts/vrt/vrtsourcedrasterband.cpp @@ -1813,7 +1813,7 @@ CPLErr CPL_STDCALL VRTAddSource(VRTSourcedRasterBandH hVRTBand, /************************************************************************/ CPLErr VRTSourcedRasterBand::XMLInit( - CPLXMLNode *psTree, const char *pszVRTPath, + const CPLXMLNode *psTree, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -1830,7 +1830,7 @@ CPLErr VRTSourcedRasterBand::XMLInit( VRTDriver *const poDriver = static_cast(GDALGetDriverByName("VRT")); - for (CPLXMLNode *psChild = psTree->psChild; + for (const CPLXMLNode *psChild = psTree->psChild; psChild != nullptr && poDriver != nullptr; psChild = psChild->psNext) { if (psChild->eType != CXT_Element) diff --git a/frmts/vrt/vrtsources.cpp b/frmts/vrt/vrtsources.cpp index 722d9e36a66c..f3a090bed7a9 100644 --- a/frmts/vrt/vrtsources.cpp +++ b/frmts/vrt/vrtsources.cpp @@ -512,7 +512,7 @@ CPLXMLNode *VRTSimpleSource::SerializeToXML(const char *pszVRTPath) /************************************************************************/ CPLErr -VRTSimpleSource::XMLInit(CPLXMLNode *psSrc, const char *pszVRTPath, +VRTSimpleSource::XMLInit(const CPLXMLNode *psSrc, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -523,7 +523,8 @@ VRTSimpleSource::XMLInit(CPLXMLNode *psSrc, const char *pszVRTPath, /* -------------------------------------------------------------------- */ /* Prepare filename. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psSourceFileNameNode = CPLGetXMLNode(psSrc, "SourceFilename"); + const CPLXMLNode *psSourceFileNameNode = + CPLGetXMLNode(psSrc, "SourceFilename"); const char *pszFilename = psSourceFileNameNode ? CPLGetXMLValue(psSourceFileNameNode, nullptr, "") : ""; @@ -2019,7 +2020,7 @@ VRTNoDataFromMaskSource::VRTNoDataFromMaskSource() /************************************************************************/ CPLErr VRTNoDataFromMaskSource::XMLInit( - CPLXMLNode *psSrc, const char *pszVRTPath, + const CPLXMLNode *psSrc, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -2033,10 +2034,10 @@ CPLErr VRTNoDataFromMaskSource::XMLInit( return eErr; } - if (CPLGetXMLValue(psSrc, "NODATA", nullptr) != nullptr) + if (const char *pszNODATA = CPLGetXMLValue(psSrc, "NODATA", nullptr)) { m_bNoDataSet = true; - m_dfNoDataValue = CPLAtofM(CPLGetXMLValue(psSrc, "NODATA", "0")); + m_dfNoDataValue = CPLAtofM(pszNODATA); } m_dfMaskValueThreshold = @@ -2628,7 +2629,7 @@ CPLXMLNode *VRTComplexSource::SerializeToXML(const char *pszVRTPath) /************************************************************************/ CPLErr -VRTComplexSource::XMLInit(CPLXMLNode *psSrc, const char *pszVRTPath, +VRTComplexSource::XMLInit(const CPLXMLNode *psSrc, const char *pszVRTPath, std::map &oMapSharedSources) { @@ -2645,12 +2646,15 @@ VRTComplexSource::XMLInit(CPLXMLNode *psSrc, const char *pszVRTPath, /* -------------------------------------------------------------------- */ /* Complex parameters. */ /* -------------------------------------------------------------------- */ - if (CPLGetXMLValue(psSrc, "ScaleOffset", nullptr) != nullptr || - CPLGetXMLValue(psSrc, "ScaleRatio", nullptr) != nullptr) + const char *pszScaleOffset = CPLGetXMLValue(psSrc, "ScaleOffset", nullptr); + const char *pszScaleRatio = CPLGetXMLValue(psSrc, "ScaleRatio", nullptr); + if (pszScaleOffset || pszScaleRatio) { m_nProcessingFlags |= PROCESSING_FLAG_SCALING_LINEAR; - m_dfScaleOff = CPLAtof(CPLGetXMLValue(psSrc, "ScaleOffset", "0")); - m_dfScaleRatio = CPLAtof(CPLGetXMLValue(psSrc, "ScaleRatio", "1")); + if (pszScaleOffset) + m_dfScaleOff = CPLAtof(pszScaleOffset); + if (pszScaleRatio) + m_dfScaleRatio = CPLAtof(pszScaleRatio); } else if (CPLGetXMLValue(psSrc, "Exponent", nullptr) != nullptr && CPLGetXMLValue(psSrc, "DstMin", nullptr) != nullptr && @@ -2672,10 +2676,10 @@ VRTComplexSource::XMLInit(CPLXMLNode *psSrc, const char *pszVRTPath, m_dfDstMax = CPLAtof(CPLGetXMLValue(psSrc, "DstMax", "0.0")); } - if (CPLGetXMLValue(psSrc, "NODATA", nullptr) != nullptr) + if (const char *pszNODATA = CPLGetXMLValue(psSrc, "NODATA", nullptr)) { m_nProcessingFlags |= PROCESSING_FLAG_NODATA; - m_osNoDataValueOri = CPLGetXMLValue(psSrc, "NODATA", "0"); + m_osNoDataValueOri = pszNODATA; m_dfNoDataValue = CPLAtofM(m_osNoDataValueOri.c_str()); } @@ -3649,7 +3653,7 @@ CPLErr VRTFuncSource::GetHistogram( /************************************************************************/ VRTSource * -VRTParseCoreSources(CPLXMLNode *psChild, const char *pszVRTPath, +VRTParseCoreSources(const CPLXMLNode *psChild, const char *pszVRTPath, std::map &oMapSharedSources) { diff --git a/frmts/vrt/vrtwarped.cpp b/frmts/vrt/vrtwarped.cpp index 84a95d63e8ac..642241b7392b 100644 --- a/frmts/vrt/vrtwarped.cpp +++ b/frmts/vrt/vrtwarped.cpp @@ -1283,7 +1283,8 @@ CPLErr CPL_STDCALL GDALInitializeWarpedVRT(GDALDatasetH hDS, /* XMLInit() */ /************************************************************************/ -CPLErr VRTWarpedDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) +CPLErr VRTWarpedDataset::XMLInit(const CPLXMLNode *psTree, + const char *pszVRTPathIn) { @@ -1330,7 +1331,8 @@ CPLErr VRTWarpedDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) /* -------------------------------------------------------------------- */ /* Find the GDALWarpOptions XML tree. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *const psOptionsTree = CPLGetXMLNode(psTree, "GDALWarpOptions"); + const CPLXMLNode *const psOptionsTree = + CPLGetXMLNode(psTree, "GDALWarpOptions"); if (psOptionsTree == nullptr) { CPLError(CE_Failure, CPLE_AppDefined, @@ -1355,14 +1357,16 @@ CPLErr VRTWarpedDataset::XMLInit(CPLXMLNode *psTree, const char *pszVRTPathIn) else pszAbsolutePath = CPLStrdup(pszRelativePath); - CPLSetXMLValue(psOptionsTree, "SourceDataset", pszAbsolutePath); + CPLXMLNode *psOptionsTreeCloned = CPLCloneXMLTree(psOptionsTree); + CPLSetXMLValue(psOptionsTreeCloned, "SourceDataset", pszAbsolutePath); CPLFree(pszAbsolutePath); /* -------------------------------------------------------------------- */ /* And instantiate the warp options, and corresponding warp */ /* operation. */ /* -------------------------------------------------------------------- */ - GDALWarpOptions *psWO = GDALDeserializeWarpOptions(psOptionsTree); + GDALWarpOptions *psWO = GDALDeserializeWarpOptions(psOptionsTreeCloned); + CPLDestroyXMLNode(psOptionsTreeCloned); if (psWO == nullptr) return CE_Failure; From a0c20b00861856d8b4e368b7eae35da73545a897 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 22 Feb 2024 19:42:44 +0100 Subject: [PATCH 115/132] PDF: deal with the situation where a multipage PDF has layers with same name but specific to some page(s) --- ...ayer_with_same_name_on_different_pages.pdf | Bin 0 -> 100215 bytes autotest/gdrivers/pdf.py | 42 +++ frmts/pdf/gdal_pdf.h | 33 ++- frmts/pdf/pdfdataset.cpp | 261 +++++++++++++++--- 4 files changed, 293 insertions(+), 43 deletions(-) create mode 100644 autotest/gdrivers/data/pdf/layer_with_same_name_on_different_pages.pdf diff --git a/autotest/gdrivers/data/pdf/layer_with_same_name_on_different_pages.pdf b/autotest/gdrivers/data/pdf/layer_with_same_name_on_different_pages.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5c942a9c5c3138bc90669cbd6c759ab1430b4ad1 GIT binary patch literal 100215 zcmZ77WmFwawL4Q!fLTqJlvxd6V+68v0f^h%*aIzX0U)5I3A30YDYKG2(ALO?SyWb?)Xd1n3BatZ z@;`yS!~YG0gaCG?|IW?%-$j@u04|m$05xebWW@h=a{agS|5fly+1LtT0%BHoHU|A$ zlGHzIG5@Few?bBCF)@2LQXQ6m2J*0Sk+QOIa_KQk1MQt1NO}Lssr^&5lf5(01mHx< z^Y6Vn2_ASWT+9jpJ9Cf)DfhpfQkFI#0Fabf>Yt4QBmgG%rT`%!ClC-| zWQ&aGmKofsSIx-_%yKsnc@OorHOx8WPV?JAN|6qTih~bqOO5EwosJcZkF*kPvcbK* zG#8BMU0*m&I7r^`_?8hmN*Q?FN`8at`+m#(c@^_{wPoP@I3@gXW#Id^^PjB2`^~Vy z=gH;g)0n}DF!kq7Nsg~*F>?_^tcn^=&uw zearW?gv7UHNdD74@j<)5avl8X@xEseola%>%VXoy>)qqy?v3KQy8H7Q{_~yWBPK`R z_i5|%>GS>Q^Fdyz+V}O+2XS`m?K;P2^qD&dS3TfP^YhB}Q{4J#=~G+W_vP_ZN%@oe z)9f>``}8?R^ZtV|uju|1;9lC@EqY>ou&RFo)_$qxwCXf&?>Yqgo<@BJeti1Q6nB0c z-ur$EdrbOzSoY;~=UkQKJhy8&WmWq=cl&;v2!A|@GoI5|f={cphc)%R^=ynLZp#aK zLq|@^OBOq$VjE+`3&x(x>aC-wAFF49qvX~}fCitD(ngm~D`JE4CS2Wasn%JnM(@U% zBLEQJpkNY_*EdP%_tE#Ix3U7u!-_PgF>CePrF7dQocaURfAZzt2O3(==>{j-tR7U1 z_L0xK22PBEFu=Jo`I+9-^{NKxl;@;d+zhW1h#P9_m7n}nb&Hx~>tOkNYnSsO>Z9x% zGIL>-frb#0)Zy8i*`E*4)td_E9-HZ}j^MJn@)1Q(^~1IYI^br+nD+XJ^8)E(BbTRd zleWz@tkDliFtDpyMfta`ToVhh1!JNo*)Sl=`j$Jch>1?k0 zkyw&{mCjzRTdh?)c2Hr}LUn9uK{|qW_MS@PhWO`3cx~EAmgj3aZIa+vWAaC1j(hmQ zVdHR2`5kJfKA-25a2;{`Ny@##85Tcqm%sH+v>HQ2f_32>rRUf zu3pf!u}xI8_8r@U0Pm5rZCiG^tFEX4c=Zx#YpT?iT`;QQfzNu%K+|edAu7k+UYqEo zycpvMq*rienQo_l@qWtc75%=fk*wr|qOVy(5xq9Yos~ox+U#WUYQlZrQEhETbQUb4 zogpf@cz~r~N7J7YR)| zbU%Bt;=3^#CH2WZYP_pq@^3s@?XKrH)?y`qD)vPVKX$VTNobqwIHV%BEZ@9!-LN=L zIj$diD}|TVKtK#%y3e?ur^eHiUkcG}+!$jvHlr+U;$86Mp9q`-HY*7}5oa?5TQA&a zmS%0lWiokO6oc8au2X_+B<-|)jNhg) z+0+J63rofP4)>mzAfd2>wwB|2GxUvTqf>46>ert~9fjX=&BM1G=Z43LtI=&YbrPI^ zwBmGIcWvu!kz8xe$ui7I3wo+EcdF~rd&VtMF(J2UN29=1gu)F8mVE?Lr#~O7fYT!D$4GC1Wjhkh3?UOfU)d8yj<~ zSCflU`<%~ClxO{vThto~TjkUXLt`CZ`pNsoHvqt4cBGCY1ca-7k^H$@47AXH?5i(cLW;_hEaJ2Ch}RDB@lFC?`8VVg8kuj=l%1|hg=wm|11z(cw}xLOLnmX_Q`TsLd?__5u{+KVq)MOL%{`q$p0*-?>$7 zN&%rirk~1B^@CcLh^BcLQi7^3fI{%!`LI(Dr3GdFEP8%qW(>Ptm_@la(7)&~IvLG7 zuufjl*5(z=N<}5!n)70ybzD%Zw?Xyg5I7+D+lh(=GV_i)( zC~-$=rtqU25txb{T^7EjOvJ)bZhl}eV5lkF_%*e?+c@Q+VIs(QU9CIiWYome`g?sl zIp;{O&uZp2_%7{)&nExK>OhTSg);C);kH`}Yb)h)#vO>7Zo^B<67 zi_+dhi{Uf%I<=7%=c-Z_z6I+IP*&zO7R3ZqQ7HbDE=!pAjB*OiWiC~k89bk5&w13Y zw@TbpN-w|nidoY68)$(oJuCV6`qw|U#=54uYS^7Ap4fa4^nP1WZ|p2X!W#4IR6hnJ z-7j|7D!)sSD&2%dT{OZL`od~|{Tr0#+??9wYs!{M8KuUhhf@%Auv}C7SHHMPxCzgr zy(Wy?JLH>xQEBCGcb>}JJ(uvP*JAmn!8e{?{c9(-$435U;U=BmK8oyDRv$mE}i0-CYoGRuXO;OC*-LG1+s#BSudLyOxBZ2#8$nU}#c$$|9LX8J0Fs+2*okLlwqdv09z(Qppi4`Yp`oCj{^ z45$ii`ktoyw8T~a`>SmDn2&88VUeJ=nzkKH2WjInfPB-G%iHEAtTT3Zk+EB0oZ@Lp zNzghVoughe^mvx|ae|YLu0Q6|B7)3+@VNY_l6%^6^?=Y6Z$t?5>vHMbdc;ud`lJPU zZcnEufJ7^jG^N)kjjyVK**f7YmXIUR-M!y@a3aakL8G#J%~W?d>90?JBG>I-(9$i4 zAwySO);au?dZM!=Ij=Q~48hP;@9fVT%@hMN^%~!!;5}MNlw$Hh5M|o3fjj?N9sYGk zwPLa&x@lTs%jTe|RzQY1Wiu#ic%8W_qnXjTl{Z}?5R`GgWmlR@|3~ka4JMnq{-r+g zWX?s2R0R7$nr(fviNJ_PM)Ew;rHe8-)xyDq7CJ|kWF$7b^ZIueJse;?R5gVYOjgvY zj7f$FQTzFg&SJE{1~v~tktdnp@49%F&G>vBsDhIwV`80SmmkXHU;3}X4})z*p>n;$ z!4>>n%>FS_&9}iz^m^a;f9`l?5`jFERO?F?qh`jBO%Y}*4lZWmf8mcQ0TkM&pLR_z zaQLuk91a*88Vz^JwZBa+#LpeOru>@!^go-YT4Y2m?uaFT-is_qKD5cmyppYLbDkI1 zG)e$mQZbgTNW?cMgQ+RR!g6+Au-yOdMRwf{*pl9}sR0qGp4B;5SYvw26VJ zWJcP<#tr7Jxh%SU9NYf&$jbW4-{%dcNc-HSuoY11Bj2ql<*SLjhjJhHRyM*BgL)D~ zDzSry+@F;JqeKzucHozcG#TJ3xR3%j;y9t8SJ^xTqtMKL&%h3iMuz14?E+s7Ci1qN zsg5pfAjTqJAV3B!FqiRqD6PN!y zp*xW?q}N}v*pqn|`rY}{PI-mUSXom@WlgBgXUl2tO8JaX%eckrm2PLLI#M+lp2>+N z_=T*?+}9huYAye`{mXrI^xo&1@BJR?=h~JRPU4iSiV*oeFrfxm)VjGF>*9F*LFwFq zCL&94jVv&{PWr1GY|bfKH9keGVQ{t#gT> zoNQF~G{bI#kzev|kUFR__qyJ2dqkf+7Sp~IIoULY!+oV|Hpf0i09?nA7<)WWe$aG} zsyA76oW$a>Lf9){iH3)+N1!rnj0Mbw zVfJIU@_f*?%}-UBFA?7U5zshe&>Lt-4lAvMIU9 zW6}y=n$@qgVLKICkBlT9oC%FajQO7X(fonRFu^^{FqQ|W*Ej9i@3K{(<9J*Mr&q#) zBFI14dNN{tJeSgy#$cB6JzJ;$uT-qx#(q^st$hQ`Ic1?yoeM)g0!;kOJ~dd9{FtnG zI(w2Bq*dq1(=Pb?`HUZOJA|6Qo&vA#G>gfT=10KCP{g zCbzRt1>dgEr@|jl&T$>mkh_j@VCjM5T29U1TLvnm*J|rPO7-Wza-MA}vq8dA{t7puA|Gzi7(j7}zdx(h<{v z>2SC?s(3~z?_kMBRTjyp_yCyRMZeDwT_DYXBs zdrC~GkbwjvbE`zWdn(H($NXK0<~6qGz-4Sk@yw=KvfBuE7uTEZu^L8GBSTKC}y9u zhv>YwG)(=*{Ul~F93&aE_<3#Cu_8)hEJMi3s0t`v;^Kp4qzvHzT?B(#UTGFP?Bj0h$DvYY~c~X;`+Krtv}HR zN0c4?jlwS8LpuyVOEt)yg?-TD7ZtuhzskP0`E2?hd$BHxkuD@8S>47wGNCwQz6!fw z$zO(x>^=}LpSkPWcnLbUsLYSNC2U6Sv1{4uID;fet7n@!j@(oBYQpY0ZUlcd zd$6#|p0!px%RWe}m`1Ji-5K4QO5`T;i9W5rxQlQ6_<`yQARL-Lz;HE*$GkuP1D)X9 zI-tFZdfi;rvm!V|sU(t8bkJgk6B<Y2Bmgs*7&~YMZNv$BT4!wNb`HIu&zU{{Am{y@+&tOxsX21V;_{7@`pB( z-Z%fK-~lZo-l)ZPP_*B{gp+|6c~5%#sgT9$>GfyTyUp1`f@c5k)xM2#SZy>}NG{5_ z`$cw2>Vx~F*U&gLffuAjNAnj-0BDSthq-wy?KjC6)av(00KwUt$`{wch%q?9=fr?d zJQvfZR8&5-)4mc`8PkS(YQ67@ac|HF)f`GMW6|&HE+xt7LU?&+7B2+eWm&D{(tSTI z=d3yJP70R@sJ%$^>E*zGtT93fU00H=x_3i5Mor|;9Ozd>(u!#gjuLfy755v*et&84 zO|4(0A#?cWv0i2q_7Z0}Q6?yL5u8OWAM**Xr_qTB_iq|FzC5Q|?!T9PiL(TsUsq8y{<_0PFt;o`vT~MFN_Xn|TGG0o&~dQi zz0A!-d7_^2_$u3wCW@3B?G-S25Vm88N?`qv@%XpsxM zOkd8X@Ugb!;><{(<4O*7d(N9h6&g#j4{c%a_CshP8u(yQv?><~odQq&oX} zZ}6qBkX!?|6JwOZV<`GE{6y5aVZ%*?T)nwzU?+nCi9ug7X)FB`fz)k%;t&7%A0+1= zqHoI-`Q&rQg}>o>BUyLhS47YI(|Mr@Z&7(41Fr+9G|I2VXWvmWu~PQ?*6&akSV5l< zriJ6VF$B93GJ2<3$k+K&!rxmW6jpv#*RK+PVFVIjtkPzCAAIwjDfhzFdvNjjQ~Q%L zZ2vejEDlTjJC#rD#2g{KI6SfcrmZXVyQfKS0L`-3^yttb(XIAgQhgMZHE4^@V<-VBdlz1|j@9+;r*iA01SL-oeV44j*epUuK<8k`wSi1s%n0^# z;_RP;2WTX-ZA2Q! zgQ6Ah@URK{`j{#Ot)CFmC-u}DF0&l7G;aCe>@PWyl+$MU^%@xJi<$$8uY94W3hhLg zERLNy^0cqvk-5v6nI)UNxRpjYRAObON`R2eXLel0eMnjuM&`xRa=(-=0pCTt6wkLS z>#kmiL#~)u5}TmFEZuo0qS{6_T)#)q3z^u+{xH=7x=yazOSs_g6B#nHVeTd3Dx6NP zz7Tuk7;x@Nq50sQpW@bX>Oqb6uE}5+fxdivdnqr7|?{LskCCUARxLijUS#^W0=I} z6X|GDdOm+1mg+#jR=M_A_bj&N|ABs!{!4M0nr4&Qe>=}^Apk~)m$iNH%wo%Ra1kwx z+Iw*w?!a*UTf3dUG~wI!)mHlSud@=}$~Lf!{*;p|E%)5zw9s$gGJ6I2_AkG{7p}Z@ z-HPTq$n4&~6EV*`UkKBg^4wi2Rf>Mfq@nkx!Y3f>;XBi#&1xMr@t&rR#8`LUH_5U| zy)?C;nGe$$PLA1(wpBTs(ctGy3=#ZcU+gruq%MhYU65<%MI&`SETu1sk%%3Rl4W>y zB%-rl6=~~6=J60erGEso0I^wCFDe6^f+ zp)8@O0nwqHr2YV?VihLHE_9qTCKzt-bBJQfs*uBk<9v zFn6wb;YkIzjcR5Xu7>}vaGa;Q-r5IZ_eeQ5&g8@V5_k!E*&?0tgVL>*OIncas3pLX z_jjW{GXN`6qr6+e_{`3%VS(oX6~(xR1KS-1Y}pcUX7_O~+m^KE$3YMF{(to-L*C-c z(5E(-1AkGT9EvP>GyHVfm>s0dBd6GYi7hSkf<#W{<9&evLmP41{8@jK`L3sSE_v6G zjSQK)NB4E2RxK?Dx*KB`jZD@&Yq zi|7<5vOi=;MBdg1SN$(WITtBn2p7lXjjZI1?|`|dop!RQ$mF;)!UK%pLb-;Q>sGv6 zF9K8dr(po46=g^W;`h6PJe^*V!sF5`5%$aYOfr`7`mIRhfN!di-b7T0@BL5YQj$eVO(LT>YSZeQPzo`MPg{fgt1B5+bcg z`MqBlw$JRZ<+vmyNYid5co}kTisPo>*8VL+ED09jwX)XU85-_Va;VFTiT-(AFv?!? z1$WX|@q{h}ETuhM_meho|Z@><6NT#Y73xRLRO)@N@*QRjVJ9R)42GESJ5B)%n! zaJztzeNmSVPVo51+b^7Fw@}!z@EVf@bydN~-D==uObuUl5RUNa zFf5t8X%JLTx(gz_BQ0CndkQ~XWWjfcBxOV)gER$6TAJT+x><&01LM3H2vle2u_8ed zbLc#ld{UK+(AR25J3@ry*M39H6J61xTO&U-Nf3|`|3o0SAqln!TqK~8MK=AxqF~9% zlGfepz-q+kiA?*z=bkGe;+iuHNI5QPDB-Uc5edn4V&&PJE^--&DeP9yIBDaC|MPWh z9w7YFqe6|Jycbof(O?*C%3G%xOY+jsrkunyM$$GqO{-yN{)|;%rl4$2JTe4NbVn>b za(h|60>Pyx55mB;jKtigA!NTkhe#d$&2ztmqzU;{)|(ozRVl5sY31*JRjsO{+shmF+< zA>UYHTGffAEbxBotQe;77Y2vT=rD5872Op5G4OoSCEzRbz?6XU`J+j#FkLDiW5u|i zx+ihZo;E6LeY;E2^{PZ_z51Qow%;zf;Wwe=h~rnw1HVIAw^(4fV;IUD*O^zd-ibun z?G9XKg+gS6Kv(oRsD}k3zydlJ6?nrMqXF5Ik6S>)>L84RYg}pg$YQ2!S{En44CB-b zJhn49YGEmG1V9kl@b5*YEGJRIxCPkyZtSU3^dV%iC5ul*uUHGik2W|?N+ugxBTSPn z2;Bd;ZZ~e}tJub~5>kbuivbWoL%}ZPQyFk~qIci&6*(Q5djYxQjqgD%?!(mWwR!Z7 za}#-JZOnJ@c>!hz^&Vf_6&m4FV}2@s`0-`Z#YmK=4atpfRhY%T!+osVQ@zYn9h8>; zRmV&Y&1O^<)F<}r6S$9cKjpohE;uYLnP7K$@ceU*$@Yja`U>&p9f07AO6Su@DlD3u zgEqMZolmkor)&te&s*8*Ysq2V+7chgK|4_XatA;1atU>NYVd>2ThsBbWQ$hCRsql#weOW^`w^1(4TAau+v818=^1kFP4Mj*^7}hu;S0gleKX7XCHlrz zL3g+GZ{G;IPh$TMKaEf9l@CG04+OSP!JZGu=uf|mPvYZG73?b&iK`#LE0x?UZ{B|K z>wfX-_Dh02?=^|bH6Ksq&Szr2XTiI7&tr@yZ)H)Blql9kV#bvYno_0kc*8KsONnpq z*ccx@K7-I2us?r^J?=TCL=_h{rHOA~E801MZB)$ac=9)4_nl(f$^QA+J63DQT5VSF z^V?xk@_d!J67n$oc-6c_%#7bnjV-RA)Hb@CDG-mpNvB-{%LgZ;vPtx~xNaj>3aS>Z zw;rI@XJj9AwNX&o@~Sohb^1{Bg~dhmBvx~I|H5-nj176y_~Oh9`EY^-Xg6R%VOPU@ zazIJLB3C84uuHQ1qE!MLeDPOu8-h)C3JAv?ADt1rrneWhf5MeSHF=46spSUw)lq9! z(Cm2T3aLE{8{$1yHpVg~QWW)9+3P`(Y7;lG@>|EY%3XdGu_7^$(wDR(dxvnkOE8vc z!4g&la!|;~PhN5INj4bEQ%*HRaa*WY>M4O<@}7adl~BiGz)C#0p5$i=Y6)n%$3$Ls zm3d^jMn0+zB_ht`Y)lB&y%s|#%stOYD7np3{h(jD9|HLH`t1+>3!$8C>gv8GEHy&l zXW8zUAFzGHTN`0lBCsfKz~^3cz4mM9-`KKql~UTN=$6KBkbL;73LC1HScIkD(A~&Y zDE7ik`z@e!D9NrB~Ztj_^f)k_=ZNv!PYq9E~h@^ zx>_ST+nEm{@G}PhM^h0y4KpZ@j(N? z%G;pdDMDUDe_M^JE%u#`M6}Lr@1MY#4zV+^qRVr=##i#4>K#H6m-*Rw)oLCtBs1nc zdumvb=q>P7+%29Mk3%Rjll})DM%(Tk20dh@t8kvrH}m%+%WRqQ{8b0I>*JTzp%y|# z^OD;Lx|DL;c2KMX#Kl#e@RAFq3C|ImK~J9Md|aeyCTUkE0q5BgQLe9U;4Z|vP-fhD z!ik#Np%Te}A{EbNlS{I^PBThi&UctyzUUhKw7i8zbXv9JHC?=mAURwkJf)^3M5_{D z%6c}bh_|>}$mDW^*C5#j&=vOsZni8{_<{GUnop3@%ycGl57Z@^s{Zdy8wjJW zYR|RxVO4xH3j=_rGP6Ht@M0FxNJGZ5E_|b~Y!y;>Qzq3NGKRSe&0=O1s<|QGq*Ls( zi}a+Dc`td4waIUAvl?PM^I0ZPt@&2R7#T0)3R07VlWd`D%=>>-{MkvKlHqPw#8Rxl zwFpNE>I(Q+=UU9gwus~LT%)k!uAQa5QM6UrCx>6oHp59hZLg%14%Tzv*}TYc8my>$ z$7Wj1=sGYjUlp2M$`}z{8|o#+5YEp-3a`d6u{1uh!(TDH^~O6G`inMZhXhT`RKpgl zl}=wZ@MWgVs6{oo9O3Ff8Z;2VpjjJov{fMeclp3PH;4B63%8*k=HMQM3&gBDb4*!q zUuy?=2z2H=-C9bp0f)v5>@8@I!X*oqN~UXT6-≤x{7?#>I(EeKsTr8@|JH4(f-+ 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 &oOCGRef, + int iPageOfInterest, int nPageCount); #endif #if defined(HAVE_POPPLER) - void ExploreLayersPoppler(GDALPDFArray *poArray, CPLString osTopLayer, + void ExploreLayersPoppler(GDALPDFArray *poArray, int iPageOfInterest, + int nPageCount, CPLString osTopLayer, int nRecLevel, int &nVisited, bool &bStop); - void FindLayersPoppler(); + void FindLayersPoppler(int iPageOfInterest); void TurnLayersOnOffPoppler(); std::vector> m_oLayerOCGListPoppler{}; #endif #ifdef HAVE_PDFIUM - void ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, + void ExploreLayersPdfium(GDALPDFArray *poArray, int iPageOfInterest, + int nPageCount, int nRecLevel, CPLString osTopLayer = ""); - void FindLayersPdfium(); + void FindLayersPdfium(int iPageOfInterest); void PDFiumRenderPageBitmap(FPDF_BITMAP bitmap, FPDF_PAGE page, int start_x, int start_y, int size_x, int size_y, const char *pszRenderingOptions); @@ -306,7 +314,18 @@ class PDFDataset final : public GDALPamDataset m_oMapOCGNumGenToVisibilityStatePdfium{}; #endif - CPLStringList m_osLayerList{}; + // Map OCGs identified by their (number, generation) to the list of pages + // where they are referenced from. + std::map, std::vector> m_oMapOCGNumGenToPages{}; + + struct LayerStruct + { + std::string osName{}; + int nInsertIdx = 0; + int iPage = 0; + }; + std::vector m_oLayerNameSet{}; + CPLStringList m_aosLayerNames{}; struct LayerWithRef { @@ -326,6 +345,8 @@ class PDFDataset final : public GDALPamDataset const char *pszLayerName); void FindLayersGeneric(GDALPDFDictionary *poPageDict); + void MapOCGsToPages(); + bool m_bUseOCG = false; static const char *GetOption(char **papszOpenOptions, diff --git a/frmts/pdf/pdfdataset.cpp b/frmts/pdf/pdfdataset.cpp index 74173f50058b..f8eecb93fddc 100644 --- a/frmts/pdf/pdfdataset.cpp +++ b/frmts/pdf/pdfdataset.cpp @@ -3559,31 +3559,106 @@ void PDFDataset::ParseInfo(GDALPDFObject *poInfoObj) #if defined(HAVE_POPPLER) || defined(HAVE_PDFIUM) /************************************************************************/ -/* AddLayer() */ +/* AddLayer() */ /************************************************************************/ -void PDFDataset::AddLayer(const char *pszLayerName) +void PDFDataset::AddLayer(const std::string &osName, int iPage) { - int nNewIndex = m_osLayerList.size() /*/ 2*/; + LayerStruct layerStruct; + layerStruct.osName = osName; + layerStruct.nInsertIdx = static_cast(m_oLayerNameSet.size()); + layerStruct.iPage = iPage; + m_oLayerNameSet.emplace_back(std::move(layerStruct)); +} + +/************************************************************************/ +/* CreateLayerList() */ +/************************************************************************/ + +void PDFDataset::CreateLayerList() +{ + // Sort layers by prioritizing page number and then insertion index + std::sort(m_oLayerNameSet.begin(), m_oLayerNameSet.end(), + [](const LayerStruct &a, const LayerStruct &b) + { + if (a.iPage < b.iPage) + return true; + if (a.iPage > b.iPage) + return false; + return a.nInsertIdx < b.nInsertIdx; + }); - if (nNewIndex == 100) + if (m_oLayerNameSet.size() >= 100) + { + for (const auto &oLayerStruct : m_oLayerNameSet) + { + m_aosLayerNames.AddNameValue( + CPLSPrintf("LAYER_%03d_NAME", m_aosLayerNames.size()), + oLayerStruct.osName.c_str()); + } + } + else { - CPLStringList osNewLayerList; - for (int i = 0; i < 100; i++) + for (const auto &oLayerStruct : m_oLayerNameSet) { - osNewLayerList.AddNameValue(CPLSPrintf("LAYER_%03d_NAME", i), - m_osLayerList[/*2 * */ i] + - strlen("LAYER_00_NAME=")); + m_aosLayerNames.AddNameValue( + CPLSPrintf("LAYER_%02d_NAME", m_aosLayerNames.size()), + oLayerStruct.osName.c_str()); } - m_osLayerList = std::move(osNewLayerList); } +} - char szFormatName[64]; - snprintf(szFormatName, sizeof(szFormatName), "LAYER_%%0%dd_NAME", - nNewIndex >= 100 ? 3 : 2); +/************************************************************************/ +/* BuildPostfixedLayerNameAndAddLayer() */ +/************************************************************************/ - m_osLayerList.AddNameValue(CPLSPrintf(szFormatName, nNewIndex), - pszLayerName); +/** Append a suffix with the page number(s) to the provided layer name, if + * it makes sense (that is if it is a multiple page PDF and we haven't selected + * a specific name). And also call AddLayer() on it if successful. + * If may return an empty string if the layer isn't used by the page of interest + */ +std::string PDFDataset::BuildPostfixedLayerNameAndAddLayer( + const std::string &osName, const std::pair &oOCGRef, + int iPageOfInterest, int nPageCount) +{ + std::string osPostfixedName = osName; + int iLayerPage = 0; + if (nPageCount > 1 && !m_oMapOCGNumGenToPages.empty()) + { + const auto oIterToPages = m_oMapOCGNumGenToPages.find(oOCGRef); + if (oIterToPages != m_oMapOCGNumGenToPages.end()) + { + const auto &anPages = oIterToPages->second; + if (iPageOfInterest > 0) + { + if (std::find(anPages.begin(), anPages.end(), + iPageOfInterest) == anPages.end()) + { + return std::string(); + } + } + else if (anPages.size() == 1) + { + iLayerPage = anPages.front(); + osPostfixedName += CPLSPrintf(" (page %d)", anPages.front()); + } + else + { + osPostfixedName += " (pages "; + for (size_t j = 0; j < anPages.size(); ++j) + { + if (j > 0) + osPostfixedName += ", "; + osPostfixedName += CPLSPrintf("%d", anPages[j]); + } + osPostfixedName += ')'; + } + } + } + + AddLayer(osPostfixedName, iLayerPage); + + return osPostfixedName; } #endif // defined(HAVE_POPPLER) || defined(HAVE_PDFIUM) @@ -3595,6 +3670,7 @@ void PDFDataset::AddLayer(const char *pszLayerName) /************************************************************************/ void PDFDataset::ExploreLayersPoppler(GDALPDFArray *poArray, + int iPageOfInterest, int nPageCount, CPLString osTopLayer, int nRecLevel, int &nVisited, bool &bStop) { @@ -3628,13 +3704,13 @@ void PDFDataset::ExploreLayersPoppler(GDALPDFArray *poArray, } else osTopLayer = std::move(osName); - AddLayer(osTopLayer.c_str()); + AddLayer(osTopLayer, 0); m_oLayerOCGListPoppler.push_back(std::pair(osTopLayer, nullptr)); } else if (poObj->GetType() == PDFObjectType_Array) { - ExploreLayersPoppler(poObj->GetArray(), osCurLayer, nRecLevel + 1, - nVisited, bStop); + ExploreLayersPoppler(poObj->GetArray(), iPageOfInterest, nPageCount, + osCurLayer, nRecLevel + 1, nVisited, bStop); if (bStop) return; osCurLayer = ""; @@ -3665,10 +3741,17 @@ void PDFDataset::ExploreLayersPoppler(GDALPDFArray *poArray, OptionalContentGroup *ocg = optContentConfig->findOcgByRef(r); if (ocg) { - AddLayer(osCurLayer.c_str()); + const auto oRefPair = std::pair(poObj->GetRefNum().toInt(), + poObj->GetRefGen()); + const std::string osPostfixedName = + BuildPostfixedLayerNameAndAddLayer( + osCurLayer, oRefPair, iPageOfInterest, nPageCount); + if (osPostfixedName.empty()) + continue; + m_oLayerOCGListPoppler.push_back( - std::make_pair(osCurLayer, ocg)); - m_aoLayerWithRef.emplace_back(osCurLayer.c_str(), + std::make_pair(osPostfixedName, ocg)); + m_aoLayerWithRef.emplace_back(osPostfixedName.c_str(), poObj->GetRefNum(), r.gen); } } @@ -3680,8 +3763,13 @@ void PDFDataset::ExploreLayersPoppler(GDALPDFArray *poArray, /* FindLayersPoppler() */ /************************************************************************/ -void PDFDataset::FindLayersPoppler() +void PDFDataset::FindLayersPoppler(int iPageOfInterest) { + int nPageCount = 0; + const auto poPages = GetPagesKids(); + if (poPages) + nPageCount = poPages->GetLength(); + OCGs *optContentConfig = m_poDocPoppler->getOptContentConfig(); if (optContentConfig == nullptr || !optContentConfig->isOk()) return; @@ -3692,7 +3780,8 @@ void PDFDataset::FindLayersPoppler() GDALPDFArray *poArray = GDALPDFCreateArray(array); int nVisited = 0; bool bStop = false; - ExploreLayersPoppler(poArray, CPLString(), 0, nVisited, bStop); + ExploreLayersPoppler(poArray, iPageOfInterest, nPageCount, CPLString(), + 0, nVisited, bStop); delete poArray; } else @@ -3704,14 +3793,15 @@ void PDFDataset::FindLayersPoppler() { const char *pszLayerName = (const char *)ocg->getName()->c_str(); - AddLayer(pszLayerName); + AddLayer(pszLayerName, 0); m_oLayerOCGListPoppler.push_back( std::make_pair(CPLString(pszLayerName), ocg)); } } } - m_oMDMD_PDF.SetMetadata(m_osLayerList.List(), "LAYERS"); + CreateLayerList(); + m_oMDMD_PDF.SetMetadata(m_aosLayerNames.List(), "LAYERS"); } /************************************************************************/ @@ -3895,7 +3985,8 @@ void PDFDataset::TurnLayersOnOffPoppler() /* ExploreLayersPdfium() */ /************************************************************************/ -void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, +void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int iPageOfInterest, + int nPageCount, int nRecLevel, CPLString osTopLayer) { if (nRecLevel == 16) @@ -3916,12 +4007,13 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, osTopLayer = std::string(osTopLayer).append(".").append(osName); else osTopLayer = osName; - AddLayer(osTopLayer.c_str()); + AddLayer(osTopLayer, 0); m_oMapLayerNameToOCGNumGenPdfium[osTopLayer] = std::pair(-1, -1); } else if (poObj->GetType() == PDFObjectType_Array) { - ExploreLayersPdfium(poObj->GetArray(), nRecLevel + 1, osCurLayer); + ExploreLayersPdfium(poObj->GetArray(), iPageOfInterest, nPageCount, + nRecLevel + 1, osCurLayer); osCurLayer.clear(); } else if (poObj->GetType() == PDFObjectType_Dictionary) @@ -3942,11 +4034,17 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, osCurLayer = osName; // CPLDebug("PDF", "Layer %s", osCurLayer.c_str()); - AddLayer(osCurLayer.c_str()); - m_aoLayerWithRef.emplace_back(osCurLayer, poObj->GetRefNum(), - poObj->GetRefGen()); - m_oMapLayerNameToOCGNumGenPdfium[osCurLayer] = + const auto oRefPair = std::pair(poObj->GetRefNum().toInt(), poObj->GetRefGen()); + const std::string osPostfixedName = + BuildPostfixedLayerNameAndAddLayer( + osCurLayer, oRefPair, iPageOfInterest, nPageCount); + if (osPostfixedName.empty()) + continue; + + m_aoLayerWithRef.emplace_back( + osPostfixedName, poObj->GetRefNum(), poObj->GetRefGen()); + m_oMapLayerNameToOCGNumGenPdfium[osPostfixedName] = oRefPair; } } } @@ -3956,8 +4054,13 @@ void PDFDataset::ExploreLayersPdfium(GDALPDFArray *poArray, int nRecLevel, /* FindLayersPdfium() */ /************************************************************************/ -void PDFDataset::FindLayersPdfium() +void PDFDataset::FindLayersPdfium(int iPageOfInterest) { + int nPageCount = 0; + const auto poPages = GetPagesKids(); + if (poPages) + nPageCount = poPages->GetLength(); + GDALPDFObject *poCatalog = GetCatalog(); if (poCatalog == nullptr || poCatalog->GetType() != PDFObjectType_Dictionary) @@ -3965,7 +4068,8 @@ void PDFDataset::FindLayersPdfium() GDALPDFObject *poOrder = poCatalog->LookupObject("OCProperties.D.Order"); if (poOrder != nullptr && poOrder->GetType() == PDFObjectType_Array) { - ExploreLayersPdfium(poOrder->GetArray(), 0); + ExploreLayersPdfium(poOrder->GetArray(), iPageOfInterest, nPageCount, + 0); } #if 0 else @@ -3987,7 +4091,8 @@ void PDFDataset::FindLayersPdfium() } #endif - m_oMDMD_PDF.SetMetadata(m_osLayerList.List(), "LAYERS"); + CreateLayerList(); + m_oMDMD_PDF.SetMetadata(m_aosLayerNames.List(), "LAYERS"); } /************************************************************************/ @@ -4179,6 +4284,84 @@ PDFDataset::VisibilityState PDFDataset::GetVisibilityStateForOGCPdfium(int nNum, #endif /* HAVE_PDFIUM */ +/************************************************************************/ +/* GetPagesKids() */ +/************************************************************************/ + +GDALPDFArray *PDFDataset::GetPagesKids() +{ + const auto poCatalog = GetCatalog(); + if (!poCatalog || poCatalog->GetType() != PDFObjectType_Dictionary) + { + return nullptr; + } + const auto poKids = poCatalog->LookupObject("Pages.Kids"); + if (!poKids || poKids->GetType() != PDFObjectType_Array) + { + return nullptr; + } + return poKids->GetArray(); +} + +/************************************************************************/ +/* MapOCGsToPages() */ +/************************************************************************/ + +void PDFDataset::MapOCGsToPages() +{ + const auto poKidsArray = GetPagesKids(); + if (!poKidsArray) + { + return; + } + const int nKidsArrayLenght = poKidsArray->GetLength(); + for (int iPage = 0; iPage < nKidsArrayLenght; ++iPage) + { + const auto poPage = poKidsArray->Get(iPage); + if (poPage && poPage->GetType() == PDFObjectType_Dictionary) + { + const auto poXObject = poPage->LookupObject("Resources.XObject"); + if (poXObject && poXObject->GetType() == PDFObjectType_Dictionary) + { + for (const auto &oNameObjectPair : + poXObject->GetDictionary()->GetValues()) + { + const auto poProperties = + oNameObjectPair.second->LookupObject( + "Resources.Properties"); + if (poProperties && + poProperties->GetType() == PDFObjectType_Dictionary) + { + const auto &oMap = + poProperties->GetDictionary()->GetValues(); + for (const auto &[osKey, poObj] : oMap) + { + if (poObj->GetRefNum().toBool() && + poObj->GetType() == PDFObjectType_Dictionary) + { + GDALPDFObject *poType = + poObj->GetDictionary()->Get("Type"); + GDALPDFObject *poName = + poObj->GetDictionary()->Get("Name"); + if (poType && + poType->GetType() == PDFObjectType_Name && + poType->GetName() == "OCG" && poName && + poName->GetType() == PDFObjectType_String) + { + m_oMapOCGNumGenToPages + [std::pair(poObj->GetRefNum().toInt(), + poObj->GetRefGen())] + .push_back(iPage + 1); + } + } + } + } + } + } + } + } +} + /************************************************************************/ /* FindLayerOCG() */ /************************************************************************/ @@ -5266,6 +5449,8 @@ PDFDataset *PDFDataset::Open(GDALOpenInfo *poOpenInfo) CPLFree(pszNeatLineWkt); } + poDS->MapOCGsToPages(); + #ifdef HAVE_POPPLER if (bUseLib.test(PDFLIB_POPPLER)) { @@ -5297,7 +5482,8 @@ PDFDataset *PDFDataset::Open(GDALOpenInfo *poOpenInfo) } /* Find layers */ - poDS->FindLayersPoppler(); + poDS->FindLayersPoppler( + (bOpenSubdataset || bOpenSubdatasetImage) ? iPage : 0); /* Turn user specified layers on or off */ poDS->TurnLayersOnOffPoppler(); @@ -5369,7 +5555,8 @@ PDFDataset *PDFDataset::Open(GDALOpenInfo *poOpenInfo) delete poRoot; /* Find layers */ - poDS->FindLayersPdfium(); + poDS->FindLayersPdfium((bOpenSubdataset || bOpenSubdatasetImage) ? iPage + : 0); /* Turn user specified layers on or off */ poDS->TurnLayersOnOffPdfium(); From e2743637a1856ef5397dfb6c9cf767ce74f33a33 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Tue, 20 Feb 2024 15:58:50 +0100 Subject: [PATCH 116/132] GDALPamDataset/GDALPamRasterBand: make XMLInit() accept a const CPLXMLNode* argument --- gcore/gdal_pam.h | 4 ++-- gcore/gdalpamdataset.cpp | 48 ++++++++++++++++--------------------- gcore/gdalpamrasterband.cpp | 43 +++++++++++++-------------------- 3 files changed, 40 insertions(+), 55 deletions(-) diff --git a/gcore/gdal_pam.h b/gcore/gdal_pam.h index b40ae21826a7..73ae1e25a939 100644 --- a/gcore/gdal_pam.h +++ b/gcore/gdal_pam.h @@ -129,7 +129,7 @@ class CPL_DLL GDALPamDataset : public GDALDataset GDALDatasetPamInfo *psPam = nullptr; virtual CPLXMLNode *SerializeToXML(const char *); - virtual CPLErr XMLInit(CPLXMLNode *, const char *); + virtual CPLErr XMLInit(const CPLXMLNode *, const char *); virtual CPLErr TryLoadXML(char **papszSiblingFiles = nullptr); virtual CPLErr TrySaveXML(); @@ -278,7 +278,7 @@ class CPL_DLL GDALPamRasterBand : public GDALRasterBand protected: //! @cond Doxygen_Suppress virtual CPLXMLNode *SerializeToXML(const char *pszVRTPath); - virtual CPLErr XMLInit(CPLXMLNode *, const char *); + virtual CPLErr XMLInit(const CPLXMLNode *, const char *); void PamInitialize(); void PamClear(); diff --git a/gcore/gdalpamdataset.cpp b/gcore/gdalpamdataset.cpp index e672bef224fc..1f0c326762e1 100644 --- a/gcore/gdalpamdataset.cpp +++ b/gcore/gdalpamdataset.cpp @@ -420,14 +420,13 @@ void GDALPamDataset::PamClear() /* XMLInit() */ /************************************************************************/ -CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) +CPLErr GDALPamDataset::XMLInit(const CPLXMLNode *psTree, const char *pszUnused) { /* -------------------------------------------------------------------- */ /* Check for an SRS node. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psSRSNode = CPLGetXMLNode(psTree, "SRS"); - if (psSRSNode) + if (const CPLXMLNode *psSRSNode = CPLGetXMLNode(psTree, "SRS")) { if (psPam->poSRS) psPam->poSRS->Release(); @@ -463,12 +462,12 @@ CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) /* -------------------------------------------------------------------- */ /* Check for a GeoTransform node. */ /* -------------------------------------------------------------------- */ - if (strlen(CPLGetXMLValue(psTree, "GeoTransform", "")) > 0) + const char *pszGT = CPLGetXMLValue(psTree, "GeoTransform", ""); + if (strlen(pszGT) > 0) { - const char *pszGT = CPLGetXMLValue(psTree, "GeoTransform", ""); - - char **papszTokens = CSLTokenizeStringComplex(pszGT, ",", FALSE, FALSE); - if (CSLCount(papszTokens) != 6) + const CPLStringList aosTokens( + CSLTokenizeStringComplex(pszGT, ",", FALSE, FALSE)); + if (aosTokens.size() != 6) { CPLError(CE_Warning, CPLE_AppDefined, "GeoTransform node does not have expected six values."); @@ -476,19 +475,15 @@ CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) else { for (int iTA = 0; iTA < 6; iTA++) - psPam->adfGeoTransform[iTA] = CPLAtof(papszTokens[iTA]); + psPam->adfGeoTransform[iTA] = CPLAtof(aosTokens[iTA]); psPam->bHaveGeoTransform = TRUE; } - - CSLDestroy(papszTokens); } /* -------------------------------------------------------------------- */ /* Check for GCPs. */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psGCPList = CPLGetXMLNode(psTree, "GCPList"); - - if (psGCPList != nullptr) + if (const CPLXMLNode *psGCPList = CPLGetXMLNode(psTree, "GCPList")) { if (psPam->poGCP_SRS) psPam->poGCP_SRS->Release(); @@ -527,8 +522,9 @@ CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) // over the root PAMDataset SRS node. // ArcGIS 9.3: GeodataXform as a root element - CPLXMLNode *psGeodataXform = CPLGetXMLNode(psTree, "=GeodataXform"); - CPLXMLNode *psValueAsXML = nullptr; + const CPLXMLNode *psGeodataXform = + CPLGetXMLNode(psTree, "=GeodataXform"); + CPLXMLTreeCloser oTreeValueAsXML(nullptr); if (psGeodataXform != nullptr) { char *apszMD[2]; @@ -543,10 +539,10 @@ CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) char **papszXML = oMDMD.GetMetadata("xml:ESRI"); if (CSLCount(papszXML) == 1) { - psValueAsXML = CPLParseXMLString(papszXML[0]); - if (psValueAsXML) + oTreeValueAsXML.reset(CPLParseXMLString(papszXML[0])); + if (oTreeValueAsXML) psGeodataXform = - CPLGetXMLNode(psValueAsXML, "=GeodataXform"); + CPLGetXMLNode(oTreeValueAsXML.get(), "=GeodataXform"); } } @@ -686,14 +682,12 @@ CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) } } } - if (psValueAsXML) - CPLDestroyXMLNode(psValueAsXML); } /* -------------------------------------------------------------------- */ /* Process bands. */ /* -------------------------------------------------------------------- */ - for (CPLXMLNode *psBandTree = psTree->psChild; psBandTree != nullptr; + for (const CPLXMLNode *psBandTree = psTree->psChild; psBandTree; psBandTree = psBandTree->psNext) { if (psBandTree->eType != CXT_Element || @@ -719,16 +713,16 @@ CPLErr GDALPamDataset::XMLInit(CPLXMLNode *psTree, const char *pszUnused) /* -------------------------------------------------------------------- */ /* Preserve Array information. */ /* -------------------------------------------------------------------- */ - for (CPLXMLNode *psIter = psTree->psChild; psIter; psIter = psIter->psNext) + for (const CPLXMLNode *psIter = psTree->psChild; psIter; + psIter = psIter->psNext) { if (psIter->eType == CXT_Element && strcmp(psIter->pszValue, "Array") == 0) { - CPLXMLNode *psNextBackup = psIter->psNext; - psIter->psNext = nullptr; + CPLXMLNode sArrayTmp = *psIter; + sArrayTmp.psNext = nullptr; psPam->m_apoOtherNodes.emplace_back( - CPLXMLTreeCloser(CPLCloneXMLTree(psIter))); - psIter->psNext = psNextBackup; + CPLXMLTreeCloser(CPLCloneXMLTree(&sArrayTmp))); } } diff --git a/gcore/gdalpamrasterband.cpp b/gcore/gdalpamrasterband.cpp index 880452399310..a75f1cc3df70 100644 --- a/gcore/gdalpamrasterband.cpp +++ b/gcore/gdalpamrasterband.cpp @@ -372,7 +372,7 @@ void GDALPamRasterBand::PamClear() /* XMLInit() */ /************************************************************************/ -CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, +CPLErr GDALPamRasterBand::XMLInit(const CPLXMLNode *psTree, const char * /* pszUnused */) { PamInitialize(); @@ -387,8 +387,8 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, /* -------------------------------------------------------------------- */ GDALMajorObject::SetDescription(CPLGetXMLValue(psTree, "Description", "")); - const char *pszNoDataValue = CPLGetXMLValue(psTree, "NoDataValue", nullptr); - if (pszNoDataValue != nullptr) + if (const char *pszNoDataValue = + CPLGetXMLValue(psTree, "NoDataValue", nullptr)) { const char *pszLEHex = CPLGetXMLValue(psTree, "NoDataValue.le_hex_equiv", nullptr); @@ -436,12 +436,10 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, GDALPamRasterBand::SetScale(pszScale ? CPLAtof(pszScale) : 1.0); } - const char *pszUnitType = CPLGetXMLValue(psTree, "UnitType", nullptr); - if (pszUnitType) + if (const char *pszUnitType = CPLGetXMLValue(psTree, "UnitType", nullptr)) GDALPamRasterBand::SetUnitType(pszUnitType); - const char *pszInterp = CPLGetXMLValue(psTree, "ColorInterp", nullptr); - if (pszInterp) + if (const char *pszInterp = CPLGetXMLValue(psTree, "ColorInterp", nullptr)) { GDALPamRasterBand::SetColorInterpretation( GDALGetColorInterpretationByName(pszInterp)); @@ -450,12 +448,11 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, /* -------------------------------------------------------------------- */ /* Category names. */ /* -------------------------------------------------------------------- */ - const auto psCategoryNames = CPLGetXMLNode(psTree, "CategoryNames"); - if (psCategoryNames) + if (const auto psCategoryNames = CPLGetXMLNode(psTree, "CategoryNames")) { CPLStringList oCategoryNames; - for (CPLXMLNode *psEntry = psCategoryNames->psChild; psEntry != nullptr; + for (const CPLXMLNode *psEntry = psCategoryNames->psChild; psEntry; psEntry = psEntry->psNext) { /* Don't skip tag with empty content */ @@ -475,13 +472,12 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, /* -------------------------------------------------------------------- */ /* Collect a color table. */ /* -------------------------------------------------------------------- */ - const auto psColorTable = CPLGetXMLNode(psTree, "ColorTable"); - if (psColorTable) + if (const auto psColorTable = CPLGetXMLNode(psTree, "ColorTable")) { GDALColorTable oTable; int iEntry = 0; - for (CPLXMLNode *psEntry = psColorTable->psChild; psEntry != nullptr; + for (const CPLXMLNode *psEntry = psColorTable->psChild; psEntry; psEntry = psEntry->psNext) { if (!(psEntry->eType == CXT_Element && @@ -505,8 +501,7 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, /* -------------------------------------------------------------------- */ /* Do we have a complete set of stats? */ /* -------------------------------------------------------------------- */ - const char *pszMinimum = CPLGetXMLValue(psTree, "Minimum", nullptr); - if (pszMinimum) + if (const char *pszMinimum = CPLGetXMLValue(psTree, "Minimum", nullptr)) { const char *pszMaximum = CPLGetXMLValue(psTree, "Maximum", nullptr); if (pszMaximum) @@ -517,8 +512,7 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, } } - const char *pszMean = CPLGetXMLValue(psTree, "Mean", nullptr); - if (pszMean) + if (const char *pszMean = CPLGetXMLValue(psTree, "Mean", nullptr)) { const char *pszStandardDeviation = CPLGetXMLValue(psTree, "StandardDeviation", nullptr); @@ -533,26 +527,23 @@ CPLErr GDALPamRasterBand::XMLInit(CPLXMLNode *psTree, /* -------------------------------------------------------------------- */ /* Histograms */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psHist = CPLGetXMLNode(psTree, "Histograms"); - if (psHist != nullptr) + if (const CPLXMLNode *psHist = CPLGetXMLNode(psTree, "Histograms")) { - CPLXMLNode *psNext = psHist->psNext; - psHist->psNext = nullptr; - + CPLXMLNode sHistTemp = *psHist; + sHistTemp.psNext = nullptr; if (psPam->psSavedHistograms != nullptr) { CPLDestroyXMLNode(psPam->psSavedHistograms); psPam->psSavedHistograms = nullptr; } - psPam->psSavedHistograms = CPLCloneXMLTree(psHist); - psHist->psNext = psNext; + psPam->psSavedHistograms = CPLCloneXMLTree(&sHistTemp); } /* -------------------------------------------------------------------- */ /* Raster Attribute Table */ /* -------------------------------------------------------------------- */ - CPLXMLNode *psRAT = CPLGetXMLNode(psTree, "GDALRasterAttributeTable"); - if (psRAT != nullptr) + if (const CPLXMLNode *psRAT = + CPLGetXMLNode(psTree, "GDALRasterAttributeTable")) { delete psPam->poDefaultRAT; auto poNewRAT = new GDALDefaultRasterAttributeTable(); From c3847cd317b97d3a1e18fcb5f7f29878889a4e16 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 26 Feb 2024 12:56:33 +0100 Subject: [PATCH 117/132] gdalinfo: do not call GDALGetFileList() if -nofl is specified (#9243) --- apps/gdalinfo_lib.cpp | 50 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/apps/gdalinfo_lib.cpp b/apps/gdalinfo_lib.cpp index d72cbbe036ab..577fc617b962 100644 --- a/apps/gdalinfo_lib.cpp +++ b/apps/gdalinfo_lib.cpp @@ -290,30 +290,31 @@ char *GDALInfo(GDALDatasetH hDataset, const GDALInfoOptions *psOptions) GDALGetDriverShortName(hDriver), GDALGetDriverLongName(hDriver)); } - // The list of files of a raster FileGDB is not super useful and potentially - // super long, so omit it, unless the -json mode is enabled - char **papszFileList = - (!bJson && EQUAL(GDALGetDriverShortName(hDriver), "OpenFileGDB")) - ? nullptr - : GDALGetFileList(hDataset); - - if (papszFileList == nullptr || *papszFileList == nullptr) + if (psOptions->bShowFileList) { - if (bJson) + // The list of files of a raster FileGDB is not super useful and potentially + // super long, so omit it, unless the -json mode is enabled + char **papszFileList = + (!bJson && EQUAL(GDALGetDriverShortName(hDriver), "OpenFileGDB")) + ? nullptr + : GDALGetFileList(hDataset); + + if (!papszFileList || *papszFileList == nullptr) { - json_object *poFiles = json_object_new_array(); - json_object_object_add(poJsonObject, "files", poFiles); + if (bJson) + { + json_object *poFiles = json_object_new_array(); + json_object_object_add(poJsonObject, "files", poFiles); + } + else + { + Concat(osStr, psOptions->bStdoutOutput, + "Files: none associated\n"); + } } else { - Concat(osStr, psOptions->bStdoutOutput, "Files: none associated\n"); - } - } - else - { - if (bJson) - { - if (psOptions->bShowFileList) + if (bJson) { json_object *poFiles = json_object_new_array(); @@ -327,20 +328,17 @@ char *GDALInfo(GDALDatasetH hDataset, const GDALInfoOptions *psOptions) json_object_object_add(poJsonObject, "files", poFiles); } - } - else - { - Concat(osStr, psOptions->bStdoutOutput, "Files: %s\n", - papszFileList[0]); - if (psOptions->bShowFileList) + else { + Concat(osStr, psOptions->bStdoutOutput, "Files: %s\n", + papszFileList[0]); for (int i = 1; papszFileList[i] != nullptr; i++) Concat(osStr, psOptions->bStdoutOutput, " %s\n", papszFileList[i]); } } + CSLDestroy(papszFileList); } - CSLDestroy(papszFileList); if (bJson) { From 29b71011a0a32ed9072221cbf110afba5a840f55 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 19 Feb 2024 18:38:56 +0100 Subject: [PATCH 118/132] JP2ECW: do not export GMLJP2 if the SRS isn't compatible (fixes #9223) --- autotest/gdrivers/ecw.py | 60 +++++++++++++++++++++++++++++++++++++ frmts/ecw/ecwcreatecopy.cpp | 25 +++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/autotest/gdrivers/ecw.py b/autotest/gdrivers/ecw.py index 388e31663c75..008057c1c11e 100755 --- a/autotest/gdrivers/ecw.py +++ b/autotest/gdrivers/ecw.py @@ -2210,6 +2210,66 @@ def test_ecw_read_uint32_jpeg2000(): ) +############################################################################### +# Test unsupported XML SRS + + +def test_jp2ecw_unsupported_srs_for_gmljp2(tmp_vsimem): + + if gdaltest.jp2ecw_drv is None or not has_write_support(): + pytest.skip("ECW write support not available") + + filename = str(tmp_vsimem / "out.jp2") + # There is no EPSG code and Albers Equal Area is not supported by OGRSpatialReference::exportToXML() + wkt = """PROJCRS["Africa_Albers_Equal_Area_Conic", + BASEGEOGCRS["WGS 84", + DATUM["World Geodetic System 1984", + ELLIPSOID["WGS 84",6378137,298.257223563, + LENGTHUNIT["metre",1]]], + PRIMEM["Greenwich",0, + ANGLEUNIT["degree",0.0174532925199433]], + ID["EPSG",4326]], + CONVERSION["Albers Equal Area", + METHOD["Albers Equal Area", + ID["EPSG",9822]], + PARAMETER["Latitude of false origin",0, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8821]], + PARAMETER["Longitude of false origin",25, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8822]], + PARAMETER["Latitude of 1st standard parallel",20, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8823]], + PARAMETER["Latitude of 2nd standard parallel",-23, + ANGLEUNIT["degree",0.0174532925199433], + ID["EPSG",8824]], + PARAMETER["Easting at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8826]], + PARAMETER["Northing at false origin",0, + LENGTHUNIT["metre",1], + ID["EPSG",8827]]], + CS[Cartesian,2], + AXIS["easting",east, + ORDER[1], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]], + AXIS["northing",north, + ORDER[2], + LENGTHUNIT["metre",1, + ID["EPSG",9001]]]]""" + gdal.ErrorReset() + assert gdal.Translate(filename, "data/byte.tif", outputSRS=wkt, format="JP2ECW") + assert gdal.GetLastErrorMsg() == "" + ds = gdal.Open(filename) + ref_srs = osr.SpatialReference() + ref_srs.ImportFromWkt(wkt) + assert ds.GetSpatialRef().IsSame(ref_srs) + # Check that we do *not* have a GMLJP2 box + assert "xml:gml.root-instance" not in ds.GetMetadataDomainList() + + ############################################################################### diff --git a/frmts/ecw/ecwcreatecopy.cpp b/frmts/ecw/ecwcreatecopy.cpp index ed0ac5b0f266..cb9b8738b066 100644 --- a/frmts/ecw/ecwcreatecopy.cpp +++ b/frmts/ecw/ecwcreatecopy.cpp @@ -1032,10 +1032,33 @@ CPLErr GDALECWCompressor::Initialize( const char *pszGMLJP2V2Def = CSLFetchNameValue(papszOptions, "GMLJP2V2_DEF"); if (pszGMLJP2V2Def != nullptr) + { WriteJP2Box(oJP2MD.CreateGMLJP2V2(nXSize, nYSize, pszGMLJP2V2Def, poSrcDS)); + } else - WriteJP2Box(oJP2MD.CreateGMLJP2(nXSize, nYSize)); + { + if (!poSRS || poSRS->IsEmpty() || + GDALJP2Metadata::IsSRSCompatible(poSRS)) + { + WriteJP2Box(oJP2MD.CreateGMLJP2(nXSize, nYSize)); + } + else if (CSLFetchNameValue(papszOptions, "GMLJP2")) + { + CPLError(CE_Warning, CPLE_AppDefined, + "GMLJP2 box was explicitly required but " + "cannot be written due " + "to lack of georeferencing and/or unsupported " + "georeferencing " + "for GMLJP2"); + } + else + { + CPLDebug( + "JP2ECW", + "Cannot write GMLJP2 box due to unsupported SRS"); + } + } } if (CPLFetchBool(papszOptions, "GeoJP2", true)) WriteJP2Box(oJP2MD.CreateJP2GeoTIFF()); From 2a06a472130a84907311136dd92adbb829f2144e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Tue, 27 Feb 2024 13:38:07 +0800 Subject: [PATCH 119/132] Update gdal_translate.rst to fold long line For https://github.com/OSGeo/gdal/issues/9330 --- doc/source/programs/gdal_translate.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/programs/gdal_translate.rst b/doc/source/programs/gdal_translate.rst index e55d4e43f681..8c13b82d5e56 100644 --- a/doc/source/programs/gdal_translate.rst +++ b/doc/source/programs/gdal_translate.rst @@ -443,7 +443,8 @@ To create a JPEG-compressed TIFF with internal mask from a RGBA dataset :: - gdal_translate rgba.tif withmask.tif -b 1 -b 2 -b 3 -mask 4 -co COMPRESS=JPEG -co PHOTOMETRIC=YCBCR --config GDAL_TIFF_INTERNAL_MASK YES + gdal_translate rgba.tif withmask.tif -b 1 -b 2 -b 3 -mask 4 -co COMPRESS=JPEG \ + -co PHOTOMETRIC=YCBCR --config GDAL_TIFF_INTERNAL_MASK YES To create a RGBA dataset from a RGB dataset with a mask From 1040d94ead4762ffd22c92c59ebd4cd3b6d9794c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Tue, 27 Feb 2024 13:42:50 +0800 Subject: [PATCH 120/132] Update gdaltransform.rst backward bracket --- doc/source/programs/gdaltransform.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/programs/gdaltransform.rst b/doc/source/programs/gdaltransform.rst index 1b8886df12cc..38242760900a 100644 --- a/doc/source/programs/gdaltransform.rst +++ b/doc/source/programs/gdaltransform.rst @@ -6,7 +6,7 @@ gdaltransform .. only:: html - Transforms coordinates. + Transforms coordinates .. Index:: gdaltransform @@ -16,7 +16,7 @@ Synopsis .. code-block:: gdaltransform [--help] [--help-general] - [-i] [-s_srs ] [-t_srs ] [-to >NAME>=]... + [-i] [-s_srs ] [-t_srs ] [-to =]... [-s_coord_epoch ] [-t_coord_epoch ] [-ct ] [-order ] [-tps] [-rpc] [-geoloc] [-gcp [elevation]]... [-output_xy] From d47fa66302a64b4a938791a9e819be99e32db231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Tue, 27 Feb 2024 14:13:13 +0800 Subject: [PATCH 121/132] Update gdal_grid.rst to warn of errors due to not setting type --- doc/source/programs/gdal_grid.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/programs/gdal_grid.rst b/doc/source/programs/gdal_grid.rst index e49325009087..9e72295f7b8a 100644 --- a/doc/source/programs/gdal_grid.rst +++ b/doc/source/programs/gdal_grid.rst @@ -48,6 +48,9 @@ computer. .. include:: options/ot.rst +If not set then a default type is used, which might not be supported +by the relevant driver, causing a error. + .. include:: options/of.rst .. option:: -txe From 3671b633de32db8dd1f0231f56c29197b3b3c38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 19:32:20 +0800 Subject: [PATCH 122/132] Update gdal_grid.rst adding crucial final step --- doc/source/programs/gdal_grid.rst | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/doc/source/programs/gdal_grid.rst b/doc/source/programs/gdal_grid.rst index 9e72295f7b8a..ee250bc50749 100644 --- a/doc/source/programs/gdal_grid.rst +++ b/doc/source/programs/gdal_grid.rst @@ -447,15 +447,27 @@ content: -This description specifies so called 2.5D geometry with three coordinates X, Y -and Z. Z value will be used for interpolation. Now you can use *dem.vrt* -with all OGR programs (start with :ref:`ogrinfo` to test that everything works -fine). The datasource will contain a single layer called *"dem"* filled -with point features constructed from values in CSV file. Using this technique -you can handle CSV files with more than three columns, switch columns, etc. - -If your CSV file does not contain column headers then it can be handled in the -following way: +This description specifies so called 2.5D geometry with three coordinates +X, Y and Z. The Z value will be used for interpolation. Now you can +use *dem.vrt* with all OGR programs (start with :ref:`ogrinfo` to test that +everything works fine). The datasource will contain a single layer called +*"dem"* filled with point features constructed from values in the CSV file. +Using this technique you can handle CSV files with more than three +columns, switch columns, etc. OK, now the final step: + +.. code-block:: + + gdal_grid dem.vrt demv.tif + +Or, if we do not wish to use a VRT file: + +.. code-block:: + + gdal_grid -l dem -oo X_POSSIBLE_NAMES=Easting \ + -oo Y_POSSIBLE_NAMES=Northing -zfield Elevation dem.csv dem.tif + +If your CSV file does not contain column headers then it can be handled +in the VRT file in the following way: .. code-block:: xml From 83b6f246da373d8d01dd2c4a343f7d43317dd8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 19:39:45 +0800 Subject: [PATCH 123/132] Update gdallocationinfo.rst grammar --- doc/source/programs/gdallocationinfo.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/programs/gdallocationinfo.rst b/doc/source/programs/gdallocationinfo.rst index 92ede096f9d8..5f8d9df82970 100644 --- a/doc/source/programs/gdallocationinfo.rst +++ b/doc/source/programs/gdallocationinfo.rst @@ -94,9 +94,9 @@ pixel. Currently it reports: - The location of the pixel in pixel/line space. - The result of a LocationInfo metadata query against the datasource. - This is implement for VRT files which will report the + This is implemented for VRT files which will report the file(s) used to satisfy requests for that pixel, and by the - :ref:`raster.mbtiles` driver + :ref:`raster.mbtiles` driver. - The raster pixel value of that pixel for all or a subset of the bands. - The unscaled pixel value if a Scale and/or Offset apply to the band. From e07c25b1d437830ecbadaba3e239d62702b575fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 19:50:56 +0800 Subject: [PATCH 124/132] Update vrt.rst to elucidate the two types of .vrt files! --- doc/source/drivers/raster/vrt.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/source/drivers/raster/vrt.rst b/doc/source/drivers/raster/vrt.rst index d44bf624981a..559d721bef0e 100644 --- a/doc/source/drivers/raster/vrt.rst +++ b/doc/source/drivers/raster/vrt.rst @@ -17,6 +17,11 @@ potentially applied as well as various kinds of metadata altered or added. VRT descriptions of datasets can be saved in an XML format normally given the extension .vrt. +Note .vrt files starting with + +- open with :ref:`ogrinfo`, etc. +- open with :ref:`gdalinfo`, etc. + The VRT format can also describe :ref:`gdal_vrttut_warped` and :ref:`gdal_vrttut_pansharpen` @@ -747,13 +752,13 @@ Except if (from top priority to lesser priority) : ------------------------------- So far we have described how to derive new virtual datasets from existing -files supports by GDAL. However, it is also common to need to utilize +files supported by GDAL. However, it is also common to need to utilize raw binary raster files for which the regular layout of the data is known but for which no format specific driver exists. This can be accomplished by writing a .vrt file describing the raw file. For example, the following .vrt describes a raw raster file containing -floating point complex pixels in a file called l2p3hhsso.img. The +floating point complex pixels in a file called *l2p3hhsso.img*. The image data starts from the first byte (ImageOffset=0). The byte offset between pixels is 8 (PixelOffset=8), the size of a CFloat32. The byte offset from the start of one line to the start of the next is 9376 bytes From e7f61208561e57ff66b09be0612378f9c5528620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 20:41:20 +0800 Subject: [PATCH 125/132] Update vrt.rst to make it super clear there are two .vrts I'll put this warning in several places! --- doc/source/drivers/raster/vrt.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/drivers/raster/vrt.rst b/doc/source/drivers/raster/vrt.rst index d44bf624981a..651b2e22a557 100644 --- a/doc/source/drivers/raster/vrt.rst +++ b/doc/source/drivers/raster/vrt.rst @@ -84,6 +84,10 @@ The following creations options are supported: A `XML schema of the GDAL VRT format `_ is available. +Note, .vrt files starting with +- open with ogrinfo, etc. +- open with gdalinfo, etc. + Virtual files stored on disk are kept in an XML format with the following elements. From 67990aaff42cf02abf7461d3f1bff3d5a6c0e5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 20:46:35 +0800 Subject: [PATCH 126/132] Update vrt.rst to ensure users are blindingly clear... ... about the two types of .vrt files. --- doc/source/drivers/vector/vrt.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/drivers/vector/vrt.rst b/doc/source/drivers/vector/vrt.rst index 93a2555d69ff..b1de74ac1050 100644 --- a/doc/source/drivers/vector/vrt.rst +++ b/doc/source/drivers/vector/vrt.rst @@ -544,3 +544,7 @@ Other Notes that is possible. For instance if the source is an RDBMS. You can turn off that feature by setting the *useSpatialSubquery* attribute of the GeometryField element to FALSE. +- .vrt files starting with + - open with ogrinfo, etc. + - open with gdalinfo, etc. + From 9cd90c4123316bb816de64499bd793d7ba4075b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 20:52:12 +0800 Subject: [PATCH 127/132] Update ogrlineref.rst to add my guess as to intended meaning Better double check my wording, as I have never actually used the program! --- doc/source/programs/ogrlineref.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/programs/ogrlineref.rst b/doc/source/programs/ogrlineref.rst index 744fd4219bf0..b3969b1d1875 100644 --- a/doc/source/programs/ogrlineref.rst +++ b/doc/source/programs/ogrlineref.rst @@ -6,7 +6,7 @@ ogrlineref .. only:: html - Create linear reference and provide some calculations using it. + Create linear reference and provide some calculations using it .. Index:: ogrlineref @@ -42,8 +42,8 @@ The :program:`ogrlineref` program can be used to: - return the portion of the path according to the "linear referenced" begin and end distances -The :program:`ogrlineref` creates a linear reference - a file containing -a segments of special length (e.g. 1 km in reference units) and gets coordinates, +The :program:`ogrlineref` utility creates a linear reference - a file containing +segments of a certain length (e.g. 1 km in reference units.) The user can get coordinates, linear referenced distances or sublines (subpaths) from this file. The utility does not require the ``M`` or ``Z`` components in the geometry. The results can be stored in any OGR supported format. From 23180b5bc82daf835cd7ccdffb3e67801332ef49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 20:56:14 +0800 Subject: [PATCH 128/132] Update gdal_grid.rst to fix long lines Else they go right off many people's screens! --- doc/source/programs/gdal_grid.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/source/programs/gdal_grid.rst b/doc/source/programs/gdal_grid.rst index 9e72295f7b8a..85d2d1a1cd40 100644 --- a/doc/source/programs/gdal_grid.rst +++ b/doc/source/programs/gdal_grid.rst @@ -482,7 +482,8 @@ Values to interpolate will be read from Z value of geometry record. :: - gdal_grid -a invdist:power=2.0:smoothing=1.0 -txe 85000 89000 -tye 894000 890000 -outsize 400 400 -of GTiff -ot Float64 -l dem dem.vrt dem.tiff + gdal_grid -a invdist:power=2.0:smoothing=1.0 -txe 85000 89000 -tye 894000 890000 \ + -outsize 400 400 -of GTiff -ot Float64 -l dem dem.vrt dem.tiff The next command does the same thing as the previous one, but reads values to interpolate from the attribute field specified with **-zfield** option @@ -492,5 +493,7 @@ The :config:`GDAL_NUM_THREADS` is also set to parallelize the computation. :: - gdal_grid -zfield "Elevation" -a invdist:power=2.0:smoothing=1.0 -txe 85000 89000 -tye 894000 890000 -outsize 400 400 -of GTiff -ot Float64 -l dem dem.vrt dem.tiff --config GDAL_NUM_THREADS ALL_CPUS + gdal_grid -zfield "Elevation" -a invdist:power=2.0:smoothing=1.0 -txe 85000 89000 \ + -tye 894000 890000 -outsize 400 400 -of GTiff -ot Float64 -l dem dem.vrt \ + dem.tiff --config GDAL_NUM_THREADS ALL_CPUS From bc1f69f3f596b8349b9c7b261b17445ce33b1dd2 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 28 Feb 2024 13:57:31 +0100 Subject: [PATCH 129/132] vrt.rst: fix link to XML schema (fixes #9342) --- doc/source/drivers/raster/vrt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/drivers/raster/vrt.rst b/doc/source/drivers/raster/vrt.rst index d44bf624981a..cdacbf57d000 100644 --- a/doc/source/drivers/raster/vrt.rst +++ b/doc/source/drivers/raster/vrt.rst @@ -81,7 +81,7 @@ The following creations options are supported: .vrt Format ----------- -A `XML schema of the GDAL VRT format `_ +A `XML schema of the GDAL VRT format `_ is available. Virtual files stored on disk are kept in an XML format with the following From 2615e94f172724f212141fd7fee067bba7dea138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 21:07:45 +0800 Subject: [PATCH 130/132] Update gdalwarp.rst: no more crazy colors or wrapping --- doc/source/programs/gdalwarp.rst | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/doc/source/programs/gdalwarp.rst b/doc/source/programs/gdalwarp.rst index 463113ede342..1da8b4f5c52a 100644 --- a/doc/source/programs/gdalwarp.rst +++ b/doc/source/programs/gdalwarp.rst @@ -71,7 +71,7 @@ with control information. Blue, Green, Red, NearInfraRed in an output dataset with bands ordered as Red, Green, Blue. - :: + .. code-block:: bash gdalwarp in_bgrn.tif out_rgb.tif -b 3 -b 2 -b 1 -overwrite @@ -84,7 +84,7 @@ with control information. is only useful when updating an existing dataset, e.g to warp one band at at time. - :: + .. code-block:: bash gdal_create -if in_red.tif -bands 3 out_rgb.tif gdalwarp in_red.tif out_rgb.tif -srcband 1 -dstband 1 @@ -579,13 +579,13 @@ less than 100% then you know things are IO bound. Otherwise they are CPU bound. The ``--debug`` option may also provide useful information. For instance, after running the following: -.. code-block:: +.. code-block:: bash gdalwarp --debug on abc.tif def.tif a message like the following will be output: -:: +.. code-block:: GDAL: 224 block reads on 32 block band 1 of utm.tif @@ -594,7 +594,7 @@ that 224 block reads were done, implying that lots of data was having to be re-read, presumably because of a limited IO cache. You will also see messages like: -:: +.. code-block:: GDAL: GDALWarpKernel()::GWKNearestNoMasksByte() Src=0,0,512x512 Dst=0,0,512x512 @@ -648,7 +648,7 @@ Examples - Basic transformation: -:: +.. code-block:: bash gdalwarp -t_srs EPSG:4326 input.tif output.tif @@ -657,7 +657,7 @@ Examples control points mapping the corners to lat/long could be warped to a UTM projection with a command like this: -:: +.. code-block:: bash gdalwarp -t_srs '+proj=utm +zone=11 +datum=WGS84' -overwrite raw_spot.tif utm11.tif @@ -667,19 +667,21 @@ Examples .. versionadded:: 2.2 -:: +.. code-block:: bash - gdalwarp -overwrite HDF4_SDS:ASTER_L1B:"pg-PR1B0000-2002031402_100_001":2 pg-PR1B0000-2002031402_100_001_2.tif + gdalwarp -overwrite HDF4_SDS:ASTER_L1B:"pg-PR1B0000-2002031402_100_001":2 \ + pg-PR1B0000-2002031402_100_001_2.tif - To apply a cutline on a un-georeferenced image and clip from pixel (220,60) to pixel (1160,690): -:: +.. code-block:: bash - gdalwarp -overwrite -to SRC_METHOD=NO_GEOTRANSFORM -to DST_METHOD=NO_GEOTRANSFORM -te 220 60 1160 690 -cutline cutline.csv in.png out.tif + gdalwarp -overwrite -to SRC_METHOD=NO_GEOTRANSFORM -to DST_METHOD=NO_GEOTRANSFORM \ + -te 220 60 1160 690 -cutline cutline.csv in.png out.tif where cutline.csv content is like: -:: +.. code-block:: id,WKT 1,"POLYGON((....))" @@ -688,7 +690,7 @@ where cutline.csv content is like: .. versionadded:: 2.2 -:: +.. code-block:: bash gdalwarp -overwrite in_dem.tif out_dem.tif -s_srs EPSG:4326+5773 -t_srs EPSG:4979 From ee7ab8b64586d75ce7f2bf2e821b9a8850a364a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 21:35:19 +0800 Subject: [PATCH 131/132] Update usgsdem.rst grammar --- doc/source/drivers/raster/usgsdem.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/drivers/raster/usgsdem.rst b/doc/source/drivers/raster/usgsdem.rst index 0559038d7e60..7157b2598588 100644 --- a/doc/source/drivers/raster/usgsdem.rst +++ b/doc/source/drivers/raster/usgsdem.rst @@ -10,7 +10,7 @@ USGSDEM -- USGS ASCII DEM (and CDED) GDAL includes support for reading USGS ASCII DEM files. This is the traditional format used by USGS before being replaced by SDTS, and is -the format used for CDED DEM data products from the Canada. Most popular +the format used for CDED DEM data products from Canada. Most popular variations on USGS DEM files should be supported, including correct recognition of coordinate system, and georeferenced positioning. From f9d8e8288f895292264ff4155b87e798f0dc3985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A9=8D=E4=B8=B9=E5=B0=BC=20Dan=20Jacobson?= Date: Wed, 28 Feb 2024 23:50:02 +0800 Subject: [PATCH 132/132] Update gdal_rasterize.rst grammar and colors (#9349) --- doc/source/programs/gdal_rasterize.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/programs/gdal_rasterize.rst b/doc/source/programs/gdal_rasterize.rst index 71060b79ba11..32121529f2b1 100644 --- a/doc/source/programs/gdal_rasterize.rst +++ b/doc/source/programs/gdal_rasterize.rst @@ -6,7 +6,7 @@ gdal_rasterize .. only:: html - Burns vector geometries into a raster. + Burns vector geometries into a raster .. Index:: gdal_rasterize @@ -202,7 +202,7 @@ raster data is only supported since GDAL 2.1.0. The GDAL supported output file. Must support update mode access. This file will be created (or overwritten if it already exists). -The program create a new target raster image when any of the :option:`-of`, +The program creates a new target raster image when any of the :option:`-of`, :option:`-a_nodata`, :option:`-init`, :option:`-a_srs`, :option:`-co`, :option:`-te`, :option:`-tr`, :option:`-tap`, :option:`-ts`, or :option:`-ot` options are used. The resolution or size must be specified using the :option:`-tr` or :option:`-ts` option for all new @@ -216,13 +216,13 @@ This utility is also callable from C with :cpp:func:`GDALRasterize`. .. versionadded:: 2.1 -Example -------- +Examples +-------- The following would burn all polygons from mask.shp into the RGB TIFF file work.tif with the color red (RGB = 255,0,0). -:: +.. code-block:: gdal_rasterize -b 1 -b 2 -b 3 -burn 255 -burn 0 -burn 0 -l mask mask.shp work.tif @@ -230,7 +230,7 @@ file work.tif with the color red (RGB = 255,0,0). The following would burn all "class A" buildings into the output elevation file, pulling the top elevation from the ROOF_H attribute. -:: +.. code-block:: gdal_rasterize -a ROOF_H -where "class='A'" -l footprints footprints.shp city_dem.tif @@ -238,6 +238,6 @@ The following would burn all polygons from footprint.shp into a new 1000x1000 rgb TIFF as the color red. Note that :option:`-b` is not used; the order of the :option:`-burn` options determines the bands of the output raster. -:: +.. code-block:: gdal_rasterize -burn 255 -burn 0 -burn 0 -ot Byte -ts 1000 1000 -l footprints footprints.shp mask.tif