diff --git a/.github/workflows/doc_checks.yml b/.github/workflows/doc_checks.yml index 745a4835d88d..899c7c85ef96 100644 --- a/.github/workflows/doc_checks.yml +++ b/.github/workflows/doc_checks.yml @@ -49,7 +49,8 @@ jobs: -DBUILD_TESTING=ON \ -DDOXYGEN_FAIL_ON_WARNINGS=ON \ -DGDAL_BUILD_OPTIONAL_DRIVERS=OFF \ - -DOGR_BUILD_OPTIONAL_DRIVERS=OFF + -DOGR_BUILD_OPTIONAL_DRIVERS=OFF \ + -DGDAL_ENABLE_DRIVER_GTI=ON cmake --build . -j$(nproc) - name: Print versions diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index dce01c92f201..4dd1dc384ec1 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -24,6 +24,7 @@ add_library( gdalalg_raster_edit.cpp gdalalg_raster_contour.cpp gdalalg_raster_hillshade.cpp + gdalalg_raster_index.cpp gdalalg_raster_mosaic.cpp gdalalg_raster_pipeline.cpp gdalalg_raster_overview_add.cpp @@ -64,6 +65,7 @@ add_library( gdalalg_vector_grid_invdistnn.cpp gdalalg_vector_grid_linear.cpp gdalalg_vector_grid_nearest.cpp + gdalalg_vector_output_abstract.cpp gdalalg_vector_reproject.cpp gdalalg_vector_select.cpp gdalalg_vector_sql.cpp diff --git a/apps/gdal_utils.h b/apps/gdal_utils.h index e862cf6a8b55..187e2535f24d 100644 --- a/apps/gdal_utils.h +++ b/apps/gdal_utils.h @@ -334,6 +334,10 @@ GDALTileIndexOptions CPL_DLL * GDALTileIndexOptionsNew(char **papszArgv, GDALTileIndexOptionsForBinary *psOptionsForBinary); +void CPL_DLL GDALTileIndexOptionsSetProgress(GDALTileIndexOptions *psOptions, + GDALProgressFunc pfnProgress, + void *pProgressData); + void CPL_DLL GDALTileIndexOptionsFree(GDALTileIndexOptions *psOptions); GDALDatasetH CPL_DLL GDALTileIndex(const char *pszDest, int nSrcCount, diff --git a/apps/gdal_utils_priv.h b/apps/gdal_utils_priv.h index c0a9a65205b1..92a0453db51b 100644 --- a/apps/gdal_utils_priv.h +++ b/apps/gdal_utils_priv.h @@ -255,6 +255,13 @@ std::string CPL_DLL GDALRasterizeAppGetParserUsage(); std::string CPL_DLL GDALDEMAppGetParserUsage(const std::string &osProcessingMode); +GDALDatasetH GDALTileIndexInternal(const char *pszDest, + GDALDatasetH hTileIndexDS, OGRLayerH hLayer, + int nSrcCount, + const char *const *papszSrcDSNames, + const GDALTileIndexOptions *psOptionsIn, + int *pbUsageError); + #endif /* #ifndef DOXYGEN_SKIP */ #endif /* GDAL_UTILS_PRIV_H_INCLUDED */ diff --git a/apps/gdalalg_raster.cpp b/apps/gdalalg_raster.cpp index cf87f09bfae6..8743b3e95a5d 100644 --- a/apps/gdalalg_raster.cpp +++ b/apps/gdalalg_raster.cpp @@ -22,6 +22,7 @@ #include "gdalalg_raster_edit.h" #include "gdalalg_raster_contour.h" #include "gdalalg_raster_hillshade.h" +#include "gdalalg_raster_index.h" #include "gdalalg_raster_mosaic.h" #include "gdalalg_raster_overview.h" #include "gdalalg_raster_pipeline.h" @@ -58,6 +59,7 @@ class GDALRasterAlgorithm final : public GDALAlgorithm RegisterSubAlgorithm(); RegisterSubAlgorithm(); RegisterSubAlgorithm(); + RegisterSubAlgorithm(); RegisterSubAlgorithm(); RegisterSubAlgorithm(); RegisterSubAlgorithm(); diff --git a/apps/gdalalg_raster_index.cpp b/apps/gdalalg_raster_index.cpp new file mode 100644 index 000000000000..1f9631a0f8d9 --- /dev/null +++ b/apps/gdalalg_raster_index.cpp @@ -0,0 +1,222 @@ +/****************************************************************************** + * + * Project: GDAL + * Purpose: gdal "raster index" subcommand + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2025, Even Rouault + * + * SPDX-License-Identifier: MIT + ****************************************************************************/ + +#include "gdalalg_raster_index.h" + +#include "cpl_conv.h" +#include "gdal_priv.h" +#include "gdal_utils_priv.h" +#include "ogrsf_frmts.h" + +//! @cond Doxygen_Suppress + +#ifndef _ +#define _(x) (x) +#endif + +/************************************************************************/ +/* GDALRasterIndexAlgorithm::GDALRasterIndexAlgorithm() */ +/************************************************************************/ + +GDALRasterIndexAlgorithm::GDALRasterIndexAlgorithm() + : GDALVectorOutputAbstractAlgorithm(NAME, DESCRIPTION, HELP_URL) +{ + AddProgressArg(); + AddInputDatasetArg(&m_inputDatasets, GDAL_OF_RASTER) + .SetAutoOpenDataset(false); + GDALVectorOutputAbstractAlgorithm::AddAllOutputArgs(); + + AddCommonOptions(); + + AddArg("source-crs-field-name", 0, + _("Name of the field to store the CRS of each dataset"), + &m_sourceCrsName) + .SetMinCharCount(1); + AddArg("source-crs-format", 0, + _("Format in which the CRS of each dataset must be written"), + &m_sourceCrsFormat) + .SetMinCharCount(1) + .SetDefault(m_sourceCrsFormat) + .SetChoices("auto", "WKT", "EPSG", "PROJ"); +} + +/************************************************************************/ +/* GDALRasterIndexAlgorithm::GDALRasterIndexAlgorithm() */ +/************************************************************************/ + +GDALRasterIndexAlgorithm::GDALRasterIndexAlgorithm( + const std::string &name, const std::string &description, + const std::string &helpURL) + : GDALVectorOutputAbstractAlgorithm(name, description, helpURL) +{ +} + +/************************************************************************/ +/* GDALRasterIndexAlgorithm::AddCommonOptions() */ +/************************************************************************/ + +void GDALRasterIndexAlgorithm::AddCommonOptions() +{ + AddArg("recursive", 0, + _("Whether input directories should be explored recursively."), + &m_recursive); + AddArg("filename-filter", 0, + _("Pattern that the filenames in input directories should follow " + "('*' and '?' wildcard)"), + &m_filenameFilter); + AddArg("min-pixel-size", 0, + _("Minimum pixel size in term of geospatial extent per pixel " + "(resolution) that a raster should have to be selected."), + &m_minPixelSize) + .SetMinValueExcluded(0); + AddArg("max-pixel-size", 0, + _("Maximum pixel size in term of geospatial extent per pixel " + "(resolution) that a raster should have to be selected."), + &m_maxPixelSize) + .SetMinValueExcluded(0); + AddArg("location-name", 0, _("Name of the field with the raster path"), + &m_locationName) + .SetDefault(m_locationName) + .SetMinCharCount(1); + AddArg("absolute-path", 0, + _("Whether the path to the input datasets should be stored as an " + "absolute path"), + &m_writeAbsolutePaths); + AddArg("dst-crs", 0, _("Destination CRS"), &m_crs) + .SetIsCRSArg() + .AddHiddenAlias("t_srs"); + + { + auto &arg = + AddArg("metadata", 0, _("Add dataset metadata item"), &m_metadata) + .SetMetaVar("="); + arg.AddValidationAction([this, &arg]() + { return ValidateKeyValue(arg); }); + arg.AddHiddenAlias("mo"); + } +} + +/************************************************************************/ +/* GDALRasterIndexAlgorithm::RunImpl() */ +/************************************************************************/ + +bool GDALRasterIndexAlgorithm::RunImpl(GDALProgressFunc pfnProgress, + void *pProgressData) +{ + CPLStringList aosSources; + for (auto &srcDS : m_inputDatasets) + { + if (srcDS.GetDatasetRef()) + { + ReportError( + CE_Failure, CPLE_IllegalArg, + "Input datasets must be provided by name, not as object"); + return false; + } + aosSources.push_back(srcDS.GetName()); + } + + auto setupRet = SetupOutputDataset(); + if (!setupRet.outDS) + return false; + + if (!SetDefaultOutputLayerNameIfNeeded(setupRet.outDS)) + return false; + + CPLStringList aosOptions; + if (m_recursive) + { + aosOptions.push_back("-recursive"); + } + for (const std::string &s : m_filenameFilter) + { + aosOptions.push_back("-filename_filter"); + aosOptions.push_back(s); + } + if (m_minPixelSize > 0) + { + aosOptions.push_back("-min_pixel_size"); + aosOptions.push_back(CPLSPrintf("%.17g", m_minPixelSize)); + } + if (m_maxPixelSize > 0) + { + aosOptions.push_back("-max_pixel_size"); + aosOptions.push_back(CPLSPrintf("%.17g", m_maxPixelSize)); + } + + if (!m_outputLayerName.empty()) + { + aosOptions.push_back("-lyr_name"); + aosOptions.push_back(m_outputLayerName); + } + + aosOptions.push_back("-tileindex"); + aosOptions.push_back(m_locationName); + + if (m_writeAbsolutePaths) + { + aosOptions.push_back("-write_absolute_path"); + } + if (m_crs.empty()) + { + if (m_sourceCrsName.empty()) + aosOptions.push_back("-skip_different_projection"); + } + else + { + aosOptions.push_back("-t_srs"); + aosOptions.push_back(m_crs); + } + if (!m_sourceCrsName.empty()) + { + aosOptions.push_back("-src_srs_name"); + aosOptions.push_back(m_sourceCrsName); + + aosOptions.push_back("-src_srs_format"); + aosOptions.push_back(CPLString(m_sourceCrsFormat).toupper()); + } + + for (const std::string &s : m_metadata) + { + aosOptions.push_back("-mo"); + aosOptions.push_back(s); + } + + if (!AddExtraOptions(aosOptions)) + return false; + + std::unique_ptr + options(GDALTileIndexOptionsNew(aosOptions.List(), nullptr), + GDALTileIndexOptionsFree); + + if (options) + { + GDALTileIndexOptionsSetProgress(options.get(), pfnProgress, + pProgressData); + } + + const bool ret = + options && GDALTileIndexInternal(m_outputDataset.GetName().c_str(), + GDALDataset::ToHandle(setupRet.outDS), + OGRLayer::ToHandle(setupRet.layer), + aosSources.size(), aosSources.List(), + options.get(), nullptr) != nullptr; + + if (ret && setupRet.newDS) + { + m_outputDataset.Set(std::move(setupRet.newDS)); + } + + return ret; +} + +//! @endcond diff --git a/apps/gdalalg_raster_index.h b/apps/gdalalg_raster_index.h new file mode 100644 index 000000000000..e3051532403a --- /dev/null +++ b/apps/gdalalg_raster_index.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * + * Project: GDAL + * Purpose: gdal "raster index" subcommand + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2025, Even Rouault + * + * SPDX-License-Identifier: MIT + ****************************************************************************/ + +#ifndef GDALALG_RASTER_INDEX_INCLUDED +#define GDALALG_RASTER_INDEX_INCLUDED + +#include "gdalalg_vector_output_abstract.h" + +//! @cond Doxygen_Suppress + +/************************************************************************/ +/* GDALRasterIndexAlgorithm */ +/************************************************************************/ + +class CPL_DLL GDALRasterIndexAlgorithm /* non final */ + : public GDALVectorOutputAbstractAlgorithm +{ + public: + static constexpr const char *NAME = "index"; + static constexpr const char *DESCRIPTION = + "Create a vector index of raster datasets."; + static constexpr const char *HELP_URL = "/programs/gdal_raster_index.html"; + + GDALRasterIndexAlgorithm(); + + GDALRasterIndexAlgorithm(const std::string &name, + const std::string &description, + const std::string &helpURL); + + protected: + void AddCommonOptions(); + + // Virtual method that may be overridden by derived classes to add options + // to GDALTileIndex() + virtual bool AddExtraOptions([[maybe_unused]] CPLStringList &aosOptions) + { + return true; + } + + std::vector m_inputDatasets{}; + + private: + bool RunImpl(GDALProgressFunc pfnProgress, void *pProgressData) override; + + bool m_recursive = false; + std::vector m_filenameFilter{}; + double m_minPixelSize = 0; + double m_maxPixelSize = 0; + std::string m_locationName = "location"; + bool m_writeAbsolutePaths = false; + std::string m_crs{}; + std::string m_sourceCrsName{}; + std::string m_sourceCrsFormat = "auto"; + std::vector m_metadata{}; +}; + +//! @endcond + +#endif diff --git a/apps/gdalalg_vector_output_abstract.cpp b/apps/gdalalg_vector_output_abstract.cpp new file mode 100644 index 000000000000..58a493f170a9 --- /dev/null +++ b/apps/gdalalg_vector_output_abstract.cpp @@ -0,0 +1,221 @@ +/****************************************************************************** + * + * Project: GDAL + * Purpose: Class to abstract outputting to a vector layer + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2025, Even Rouault + * + * SPDX-License-Identifier: MIT + ****************************************************************************/ + +#include "gdalalg_vector_output_abstract.h" + +#include "cpl_vsi.h" +#include "ogrsf_frmts.h" + +//! @cond Doxygen_Suppress + +#ifndef _ +#define _(x) (x) +#endif + +/************************************************************************/ +/* GDALVectorOutputAbstractAlgorithm::AddAllOutputArgs() */ +/************************************************************************/ + +void GDALVectorOutputAbstractAlgorithm::AddAllOutputArgs() +{ + AddOutputFormatArg(&m_outputFormat) + .AddMetadataItem(GAAMDI_REQUIRED_CAPABILITIES, + {GDAL_DCAP_VECTOR, GDAL_DCAP_CREATE}); + AddOutputDatasetArg(&m_outputDataset, GDAL_OF_VECTOR); + m_outputDataset.SetInputFlags(GADV_NAME | GADV_OBJECT); + AddCreationOptionsArg(&m_creationOptions); + AddLayerCreationOptionsArg(&m_layerCreationOptions); + AddOverwriteArg(&m_overwrite).SetMutualExclusionGroup("overwrite-update"); + AddUpdateArg(&m_update).SetMutualExclusionGroup("overwrite-update"); + AddArg("overwrite-layer", 0, + _("Whether overwriting existing layer is allowed"), + &m_overwriteLayer) + .SetDefault(false) + .AddValidationAction( + [this] + { + GetArg(GDAL_ARG_NAME_UPDATE)->Set(true); + return true; + }); + AddArg("append", 0, _("Whether appending to existing layer is allowed"), + &m_appendLayer) + .SetDefault(false) + .AddValidationAction( + [this] + { + GetArg(GDAL_ARG_NAME_UPDATE)->Set(true); + return true; + }); + { + auto &arg = AddLayerNameArg(&m_outputLayerName) + .AddAlias("nln") + .SetMinCharCount(0); + if (!m_outputLayerName.empty()) + arg.SetDefault(m_outputLayerName); + } +} + +/************************************************************************/ +/* GDALVectorOutputAbstractAlgorithm::SetupOutputDataset() */ +/************************************************************************/ + +GDALVectorOutputAbstractAlgorithm::SetupOutputDatasetRet +GDALVectorOutputAbstractAlgorithm::SetupOutputDataset() +{ + SetupOutputDatasetRet ret; + + VSIStatBufL sStat; + if (!m_update && !m_outputDataset.GetName().empty() && + (VSIStatL(m_outputDataset.GetName().c_str(), &sStat) == 0 || + std::unique_ptr( + GDALDataset::Open(m_outputDataset.GetName().c_str())))) + { + if (!m_overwrite) + { + ReportError(CE_Failure, CPLE_AppDefined, + "File '%s' already exists. Specify the --overwrite " + "option to overwrite it, or --update to update it.", + m_outputDataset.GetName().c_str()); + return ret; + } + else + { + VSIUnlink(m_outputDataset.GetName().c_str()); + } + } + + GDALDataset *poDstDS = m_outputDataset.GetDatasetRef(); + std::unique_ptr poRetDS; + if (!poDstDS) + { + if (m_outputFormat.empty()) + { + const auto aosFormats = + CPLStringList(GDALGetOutputDriversForDatasetName( + m_outputDataset.GetName().c_str(), GDAL_OF_VECTOR, + /* bSingleMatch = */ true, + /* bWarn = */ true)); + if (aosFormats.size() != 1) + { + ReportError(CE_Failure, CPLE_AppDefined, + "Cannot guess driver for %s", + m_outputDataset.GetName().c_str()); + return ret; + } + m_outputFormat = aosFormats[0]; + } + + auto poDriver = + GetGDALDriverManager()->GetDriverByName(m_outputFormat.c_str()); + if (!poDriver) + { + // shouldn't happen given checks done in GDALAlgorithm + ReportError(CE_Failure, CPLE_AppDefined, "Cannot find driver %s", + m_outputFormat.c_str()); + return ret; + } + + poRetDS.reset(poDriver->Create( + m_outputDataset.GetName().c_str(), 0, 0, 0, GDT_Unknown, + CPLStringList(m_creationOptions).List())); + if (!poRetDS) + return ret; + + poDstDS = poRetDS.get(); + } + + auto poDstDriver = poDstDS->GetDriver(); + if (poDstDriver && EQUAL(poDstDriver->GetDescription(), "ESRI Shapefile") && + EQUAL(CPLGetExtensionSafe(poDstDS->GetDescription()).c_str(), "shp") && + poDstDS->GetLayerCount() <= 1) + { + m_outputLayerName = CPLGetBasenameSafe(poDstDS->GetDescription()); + } + + auto poDstLayer = m_outputLayerName.empty() + ? nullptr + : poDstDS->GetLayerByName(m_outputLayerName.c_str()); + if (poDstLayer) + { + if (m_overwriteLayer) + { + int iLayer = -1; + const int nLayerCount = poDstDS->GetLayerCount(); + for (iLayer = 0; iLayer < nLayerCount; iLayer++) + { + if (poDstDS->GetLayer(iLayer) == poDstLayer) + break; + } + + if (iLayer < nLayerCount) + { + if (poDstDS->DeleteLayer(iLayer) != OGRERR_NONE) + { + ReportError(CE_Failure, CPLE_AppDefined, + "Cannot delete layer '%s'", + m_outputLayerName.c_str()); + return ret; + } + } + poDstLayer = nullptr; + } + else if (!m_appendLayer) + { + ReportError(CE_Failure, CPLE_AppDefined, + "Layer '%s' already exists. Specify the " + "--overwrite-layer option to overwrite it, or --append " + "to append it.", + m_outputLayerName.c_str()); + return ret; + } + } + else if (m_appendLayer || m_overwriteLayer) + { + ReportError(CE_Failure, CPLE_AppDefined, "Cannot find layer '%s'", + m_outputLayerName.c_str()); + return ret; + } + + ret.newDS = std::move(poRetDS); + ret.outDS = poDstDS; + ret.layer = poDstLayer; + return ret; +} + +/************************************************************************/ +/* GDALVectorOutputAbstractAlgorithm::SetDefaultOutputLayerNameIfNeeded */ +/************************************************************************/ + +bool GDALVectorOutputAbstractAlgorithm::SetDefaultOutputLayerNameIfNeeded( + GDALDataset *poOutDS) +{ + if (m_outputLayerName.empty()) + { + VSIStatBufL sStat; + auto poDriver = poOutDS->GetDriver(); + if (VSIStatL(m_outputDataset.GetName().c_str(), &sStat) == 0 || + (poDriver && EQUAL(poDriver->GetDescription(), "ESRI Shapefile"))) + { + m_outputLayerName = + CPLGetBasenameSafe(m_outputDataset.GetName().c_str()); + } + } + if (m_outputLayerName.empty()) + { + ReportError(CE_Failure, CPLE_AppDefined, + "Argument 'layer' must be specified"); + return false; + } + return true; +} + +//! @endcond diff --git a/apps/gdalalg_vector_output_abstract.h b/apps/gdalalg_vector_output_abstract.h new file mode 100644 index 000000000000..134bccc13a04 --- /dev/null +++ b/apps/gdalalg_vector_output_abstract.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * + * Project: GDAL + * Purpose: Class to abstract outputting to a vector layer + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2025, Even Rouault + * + * SPDX-License-Identifier: MIT + ****************************************************************************/ + +#ifndef GDALALG_VECTOR_OUTPUT_ABSTRACT_INCLUDED +#define GDALALG_VECTOR_OUTPUT_ABSTRACT_INCLUDED + +#include "gdalalgorithm.h" +#include "ogrsf_frmts.h" + +//! @cond Doxygen_Suppress + +/************************************************************************/ +/* GDALVectorOutputAbstractAlgorithm */ +/************************************************************************/ + +class CPL_DLL + GDALVectorOutputAbstractAlgorithm /* non-final*/ : public GDALAlgorithm +{ + protected: + GDALVectorOutputAbstractAlgorithm(const std::string &name, + const std::string &description, + const std::string &helpURL) + : GDALAlgorithm(name, description, helpURL) + { + } + + void AddAllOutputArgs(); + + struct SetupOutputDatasetRet + { + std::unique_ptr newDS{}; + GDALDataset *outDS = + nullptr; // either newDS.get() or m_outputDataset.GetDatasetRef() + OGRLayer *layer = nullptr; + + SetupOutputDatasetRet() = default; + SetupOutputDatasetRet(SetupOutputDatasetRet &&) = default; + SetupOutputDatasetRet &operator=(SetupOutputDatasetRet &&) = default; + + CPL_DISALLOW_COPY_ASSIGN(SetupOutputDatasetRet) + }; + + SetupOutputDatasetRet SetupOutputDataset(); + bool SetDefaultOutputLayerNameIfNeeded(GDALDataset *poOutDS); + + std::string m_outputFormat{}; + GDALArgDatasetValue m_outputDataset{}; + std::vector m_creationOptions{}; + std::vector m_layerCreationOptions{}; + std::string m_outputLayerName{}; + bool m_overwrite = false; + bool m_update = false; + bool m_overwriteLayer = false; + bool m_appendLayer = false; +}; + +//! @endcond + +#endif diff --git a/apps/gdaltindex_bin.cpp b/apps/gdaltindex_bin.cpp index 9c57b20d9640..82373a78506f 100644 --- a/apps/gdaltindex_bin.cpp +++ b/apps/gdaltindex_bin.cpp @@ -58,6 +58,12 @@ MAIN_START(argc, argv) Usage(); } + if (!(sOptionsForBinary.bQuiet)) + { + GDALTileIndexOptionsSetProgress(psOptions.get(), GDALTermProgress, + nullptr); + } + int bUsageError = FALSE; GDALDatasetH hOutDS = GDALTileIndex( sOptionsForBinary.osDest.c_str(), sOptionsForBinary.aosSrcFiles.size(), diff --git a/apps/gdaltindex_lib.cpp b/apps/gdaltindex_lib.cpp index f07a4d17b52f..b650ef969b4f 100644 --- a/apps/gdaltindex_lib.cpp +++ b/apps/gdaltindex_lib.cpp @@ -84,6 +84,8 @@ struct GDALTileIndexOptions double dfMaxPixelSize = std::numeric_limits::quiet_NaN(); std::vector aoFetchMD{}; std::set oSetFilenameFilters{}; + GDALProgressFunc pfnProgress = nullptr; + void *pProgressData = nullptr; }; /************************************************************************/ @@ -508,6 +510,17 @@ GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount, const char *const *papszSrcDSNames, const GDALTileIndexOptions *psOptionsIn, int *pbUsageError) +{ + return GDALTileIndexInternal(pszDest, nullptr, nullptr, nSrcCount, + papszSrcDSNames, psOptionsIn, pbUsageError); +} + +GDALDatasetH GDALTileIndexInternal(const char *pszDest, + GDALDatasetH hTileIndexDS, OGRLayerH hLayer, + int nSrcCount, + const char *const *papszSrcDSNames, + const GDALTileIndexOptions *psOptionsIn, + int *pbUsageError) { if (nSrcCount == 0) { @@ -546,95 +559,113 @@ GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount, /* Open or create the target datasource */ /* -------------------------------------------------------------------- */ - if (psOptions->bOverwrite) - { - CPLPushErrorHandler(CPLQuietErrorHandler); - auto hDriver = GDALIdentifyDriver(pszDest, nullptr); - if (hDriver) - GDALDeleteDataset(hDriver, pszDest); - else - VSIUnlink(pszDest); - CPLPopErrorHandler(); - } - - auto poTileIndexDS = std::unique_ptr(GDALDataset::Open( - pszDest, GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr, nullptr, nullptr)); - OGRLayer *poLayer = nullptr; - std::string osFormat; - int nMaxFieldSize = 254; + std::unique_ptr poTileIndexDSUnique; + GDALDataset *poTileIndexDS = GDALDataset::FromHandle(hTileIndexDS); + OGRLayer *poLayer = OGRLayer::FromHandle(hLayer); bool bExistingLayer = false; + std::string osFormat; - if (poTileIndexDS != nullptr) + if (!hTileIndexDS) { - auto poDriver = poTileIndexDS->GetDriver(); - if (poDriver) - osFormat = poDriver->GetDescription(); - - if (poTileIndexDS->GetLayerCount() == 1) + if (psOptions->bOverwrite) { - poLayer = poTileIndexDS->GetLayer(0); - } - else - { - if (psOptions->osIndexLayerName.empty()) - { - CPLError(CE_Failure, CPLE_AppDefined, - "-lyr_name must be specified."); - if (pbUsageError) - *pbUsageError = true; - return nullptr; - } CPLPushErrorHandler(CPLQuietErrorHandler); - poLayer = poTileIndexDS->GetLayerByName( - psOptions->osIndexLayerName.c_str()); + auto hDriver = GDALIdentifyDriver(pszDest, nullptr); + if (hDriver) + GDALDeleteDataset(hDriver, pszDest); + else + VSIUnlink(pszDest); CPLPopErrorHandler(); } - } - else - { - if (psOptions->osFormat.empty()) + + poTileIndexDSUnique.reset( + GDALDataset::Open(pszDest, GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr, + nullptr, nullptr)); + + if (poTileIndexDSUnique != nullptr) { - const auto aoDrivers = GetOutputDriversFor(pszDest, GDAL_OF_VECTOR); - if (aoDrivers.empty()) + auto poDriver = poTileIndexDSUnique->GetDriver(); + if (poDriver) + osFormat = poDriver->GetDescription(); + + if (poTileIndexDSUnique->GetLayerCount() == 1) { - CPLError(CE_Failure, CPLE_AppDefined, - "Cannot guess driver for %s", pszDest); - return nullptr; + poLayer = poTileIndexDSUnique->GetLayer(0); } else { - if (aoDrivers.size() > 1) + if (psOptions->osIndexLayerName.empty()) { - CPLError(CE_Warning, CPLE_AppDefined, - "Several drivers matching %s extension. Using %s", - CPLGetExtensionSafe(pszDest).c_str(), - aoDrivers[0].c_str()); + CPLError(CE_Failure, CPLE_AppDefined, + "Multiple layers detected: -lyr_name must be " + "specified."); + if (pbUsageError) + *pbUsageError = true; + return nullptr; } - osFormat = aoDrivers[0]; + CPLPushErrorHandler(CPLQuietErrorHandler); + poLayer = poTileIndexDSUnique->GetLayerByName( + psOptions->osIndexLayerName.c_str()); + CPLPopErrorHandler(); } } else { - osFormat = psOptions->osFormat; - } - if (!EQUAL(osFormat.c_str(), "ESRI Shapefile")) - nMaxFieldSize = 0; + if (psOptions->osFormat.empty()) + { + const auto aoDrivers = + GetOutputDriversFor(pszDest, GDAL_OF_VECTOR); + if (aoDrivers.empty()) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Cannot guess driver for %s", pszDest); + return nullptr; + } + else + { + if (aoDrivers.size() > 1) + { + CPLError( + CE_Warning, CPLE_AppDefined, + "Several drivers matching %s extension. Using %s", + CPLGetExtensionSafe(pszDest).c_str(), + aoDrivers[0].c_str()); + } + osFormat = aoDrivers[0]; + } + } + else + { + osFormat = psOptions->osFormat; + } - auto poDriver = - GetGDALDriverManager()->GetDriverByName(osFormat.c_str()); - if (poDriver == nullptr) - { - CPLError(CE_Warning, CPLE_AppDefined, "%s driver not available.", - osFormat.c_str()); - return nullptr; + auto poDriver = + GetGDALDriverManager()->GetDriverByName(osFormat.c_str()); + if (poDriver == nullptr) + { + CPLError(CE_Warning, CPLE_AppDefined, + "%s driver not available.", osFormat.c_str()); + return nullptr; + } + + poTileIndexDSUnique.reset( + poDriver->Create(pszDest, 0, 0, 0, GDT_Unknown, nullptr)); + if (!poTileIndexDSUnique) + return nullptr; } - poTileIndexDS.reset( - poDriver->Create(pszDest, 0, 0, 0, GDT_Unknown, nullptr)); - if (!poTileIndexDS) - return nullptr; + poTileIndexDS = poTileIndexDSUnique.get(); + } + + if (osFormat.empty()) + { + if (auto poOutDrv = poTileIndexDS->GetDriver()) + osFormat = poOutDrv->GetDescription(); } + const int nMaxFieldSize = + EQUAL(osFormat.c_str(), "ESRI Shapefile") ? 254 : 0; + if (poLayer) { bExistingLayer = true; @@ -986,6 +1017,8 @@ GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount, /* -------------------------------------------------------------------- */ /* loop over GDAL files, processing. */ /* -------------------------------------------------------------------- */ + int iCur = 0; + int nTotal = nSrcCount + 1; while (true) { const std::string osSrcFilename = oGDALTileIndexTileIterator.next(); @@ -1152,7 +1185,7 @@ GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount, const double dfMaxY = std::max(std::max(adfY[0], adfY[1]), std::max(adfY[2], adfY[3])); const double dfRes = - (dfMaxX - dfMinX) * (dfMaxY - dfMinY) / nXSize / nYSize; + sqrt((dfMaxX - dfMinX) * (dfMaxY - dfMinY) / nXSize / nYSize); if (!std::isnan(psOptions->dfMinPixelSize) && dfRes < psOptions->dfMinPixelSize) { @@ -1284,9 +1317,24 @@ GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount, "Failed to create feature in tile index."); return nullptr; } + + ++iCur; + if (psOptions->pfnProgress && + !psOptions->pfnProgress(static_cast(iCur) / nTotal, "", + psOptions->pProgressData)) + { + return nullptr; + } + if (iCur >= nSrcCount) + ++nTotal; } + if (psOptions->pfnProgress) + psOptions->pfnProgress(1.0, "", psOptions->pProgressData); - return GDALDataset::ToHandle(poTileIndexDS.release()); + if (poTileIndexDSUnique) + return GDALDataset::ToHandle(poTileIndexDSUnique.release()); + else + return GDALDataset::ToHandle(poTileIndexDS); } /************************************************************************/ @@ -1470,4 +1518,26 @@ void GDALTileIndexOptionsFree(GDALTileIndexOptions *psOptions) delete psOptions; } +/************************************************************************/ +/* GDALTileIndexOptionsSetProgress() */ +/************************************************************************/ + +/** + * Set a progress function. + * + * @param psOptions the options struct for GDALTileIndex(). + * @param pfnProgress the progress callback. + * @param pProgressData the user data for the progress callback. + * + * @since GDAL 3.11 + */ + +void GDALTileIndexOptionsSetProgress(GDALTileIndexOptions *psOptions, + GDALProgressFunc pfnProgress, + void *pProgressData) +{ + psOptions->pfnProgress = pfnProgress; + psOptions->pProgressData = pProgressData; +} + #undef CHECK_HAS_ENOUGH_ADDITIONAL_ARGS diff --git a/autotest/cpp/test_gdal_algorithm.cpp b/autotest/cpp/test_gdal_algorithm.cpp index 2cb9d16c14db..83bb6f72e648 100644 --- a/autotest/cpp/test_gdal_algorithm.cpp +++ b/autotest/cpp/test_gdal_algorithm.cpp @@ -1068,6 +1068,58 @@ TEST_F(test_gdal_algorithm, double_max_val_excluded) } } +TEST_F(test_gdal_algorithm, string_min_char_count) +{ + class MyAlgorithm : public MyAlgorithmWithDummyRun + { + public: + std::string m_val{}; + + MyAlgorithm() + { + AddArg("val", 0, "", &m_val).SetMinCharCount(2); + } + }; + + { + MyAlgorithm alg; + EXPECT_TRUE(alg.ParseCommandLineArguments({"--val=ab"})); + EXPECT_STREQ(alg.m_val.c_str(), "ab"); + } + + { + MyAlgorithm alg; + CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler); + EXPECT_FALSE(alg.ParseCommandLineArguments({"--val=a"})); + } +} + +TEST_F(test_gdal_algorithm, string_vector_min_char_count) +{ + class MyAlgorithm : public MyAlgorithmWithDummyRun + { + public: + std::vector m_val{}; + + MyAlgorithm() + { + AddArg("val", 0, "", &m_val).SetMinCharCount(2); + } + }; + + { + MyAlgorithm alg; + EXPECT_TRUE(alg.ParseCommandLineArguments({"--val=ab"})); + EXPECT_STREQ(alg.m_val[0].c_str(), "ab"); + } + + { + MyAlgorithm alg; + CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler); + EXPECT_FALSE(alg.ParseCommandLineArguments({"--val=a"})); + } +} + TEST_F(test_gdal_algorithm, SetDisplayInJSONUsage) { class MyAlgorithm : public MyAlgorithmWithDummyRun diff --git a/autotest/utilities/test_gdalalg_driver_gti_create.py b/autotest/utilities/test_gdalalg_driver_gti_create.py new file mode 100755 index 000000000000..c11eca1f81f7 --- /dev/null +++ b/autotest/utilities/test_gdalalg_driver_gti_create.py @@ -0,0 +1,140 @@ +#!/usr/bin/env pytest +# -*- coding: utf-8 -*- +############################################################################### +# Project: GDAL/OGR Test Suite +# Purpose: 'gdal driver gti create' testing +# Author: Even Rouault +# +############################################################################### +# Copyright (c) 2025, Even Rouault +# +# SPDX-License-Identifier: MIT +############################################################################### + +import pytest + +from osgeo import gdal, ogr + +pytestmark = pytest.mark.require_driver("GTI") + + +def get_alg(): + return gdal.GetGlobalAlgorithmRegistry()["driver"]["gti"]["create"] + + +def test_gdalalg_driver_gti_create_xml_filename(tmp_vsimem): + + xml_filename = tmp_vsimem / "out.xml" + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "my_layer" + alg["xml-filename"] = xml_filename + alg["fetch-metadata"] = "AREA_OR_POINT,area_or_point,String" + assert alg.Run() + + ds = alg["output"].GetDataset() + lyr = ds.GetLayerByName("my_layer") + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetLayerDefn().GetFieldCount() == 2 + assert lyr.GetLayerDefn().GetFieldDefn(0).GetName() == "location" + assert lyr.GetLayerDefn().GetFieldDefn(0).GetType() == ogr.OFTString + assert lyr.GetLayerDefn().GetFieldDefn(1).GetName() == "area_or_point" + assert lyr.GetLayerDefn().GetFieldDefn(1).GetType() == ogr.OFTString + f = lyr.GetNextFeature() + assert f["location"] == "../gcore/data/byte.tif" + assert f["area_or_point"] == "Area" + assert ( + f.GetGeometryRef().ExportToWkt() + == "POLYGON ((440720 3751320,441920 3751320,441920 3750120,440720 3750120,440720 3751320))" + ) + assert lyr.GetMetadata_Dict() == {} + + with gdal.VSIFile(xml_filename, "rb") as f: + assert ( + f.read() + == b"\n \n my_layer\n location\n\n" + ) + + +def test_gdalalg_driver_gti_create(): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "my_layer" + alg["resolution"] = [10, 11] + alg["datatype"] = "UInt16" + alg["bbox"] = [1, 2, 3, 4] + alg["band-count"] = 2 + alg["nodata"] = [5, 6] + alg["color-interpretation"] = ["red", "green"] + alg["mask"] = True + assert alg.Run() + + ds = alg["output"].GetDataset() + lyr = ds.GetLayerByName("my_layer") + assert lyr.GetMetadata_Dict() == { + "BAND_COUNT": "2", + "COLOR_INTERPRETATION": "red,green", + "DATA_TYPE": "UInt16", + "LOCATION_FIELD": "location", + "MASK_BAND": "YES", + "MAXX": "3", + "MAXY": "4", + "MINX": "1", + "MINY": "2", + "NODATA": "5,6", + "RESX": "10", + "RESY": "11", + } + + +def test_gdalalg_driver_gti_create_wrong_nodata(): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "my_layer" + alg["band-count"] = 3 + alg["nodata"] = [5, 6] + with pytest.raises( + Exception, match="2 nodata values whereas one or 3 were expected" + ): + alg.Run() + + +def test_gdalalg_driver_gti_create_wrong_color_interpretation(): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "my_layer" + alg["band-count"] = 3 + alg["color-interpretation"] = ["red", "green"] + with pytest.raises( + Exception, match="2 color interpretations whereas one or 3 were expected" + ): + alg.Run() + + +def test_gdalalg_driver_gti_create_wrong_fetch_metadata(): + + alg = get_alg() + with pytest.raises( + Exception, + match="'foo' is not of the form ,,", + ): + alg["fetch-metadata"] = "foo" + + alg = get_alg() + with pytest.raises( + Exception, + match="'foo,bar,baz' has an invalid field type 'baz'. It should be one of 'String', 'Integer', 'Integer64', 'Real', 'Date', 'DateTime'", + ): + alg["fetch-metadata"] = "foo,bar,baz" diff --git a/autotest/utilities/test_gdalalg_raster_index.py b/autotest/utilities/test_gdalalg_raster_index.py new file mode 100755 index 000000000000..0658432b5276 --- /dev/null +++ b/autotest/utilities/test_gdalalg_raster_index.py @@ -0,0 +1,231 @@ +#!/usr/bin/env pytest +# -*- coding: utf-8 -*- +############################################################################### +# Project: GDAL/OGR Test Suite +# Purpose: 'gdal raster index' testing +# Author: Even Rouault +# +############################################################################### +# Copyright (c) 2025, Even Rouault +# +# SPDX-License-Identifier: MIT +############################################################################### + +import os + +import pytest + +from osgeo import gdal, ogr + + +def get_alg(): + return gdal.GetGlobalAlgorithmRegistry()["raster"]["index"] + + +def test_gdalalg_raster_index_layer_must_be_specified(): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + with pytest.raises(Exception, match="Argument 'layer' must be specified"): + alg.Run() + + +def test_gdalalg_raster_index(): + + last_pct = [0] + + def my_progress(pct, msg, user_data): + last_pct[0] = pct + return True + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "my_layer" + assert alg.Run(my_progress) + assert last_pct[0] == 1.0 + ds = alg["output"].GetDataset() + lyr = ds.GetLayerByName("my_layer") + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetLayerDefn().GetFieldCount() == 1 + assert lyr.GetLayerDefn().GetFieldDefn(0).GetName() == "location" + assert lyr.GetLayerDefn().GetFieldDefn(0).GetType() == ogr.OFTString + f = lyr.GetNextFeature() + assert f["location"] == "../gcore/data/byte.tif" + assert ( + f.GetGeometryRef().ExportToWkt() + == "POLYGON ((440720 3751320,441920 3751320,441920 3750120,440720 3750120,440720 3751320))" + ) + + +def test_gdalalg_raster_index_source_by_ref(): + + alg = get_alg() + alg["input"] = gdal.GetDriverByName("MEM").Create("", 1, 1) + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "my_layer" + with pytest.raises( + Exception, match="Input datasets must be provided by name, not as object" + ): + alg.Run() + + +def test_gdalalg_raster_index_overwrite(tmp_vsimem): + + out_filename = tmp_vsimem / "out.shp" + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = out_filename + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetFeatureCount() == 1 + assert alg.Finalize() + ds.Close() + + alg = get_alg() + alg["input"] = "../gcore/data/uint16.tif" + alg["output"] = out_filename + with pytest.raises( + Exception, + match="already exists. Specify the --overwrite option to overwrite it, or --update to update it.", + ): + alg.Run() + + alg = get_alg() + alg["input"] = "../gcore/data/uint16.tif" + alg["output"] = out_filename + alg["update"] = True + with pytest.raises( + Exception, + match="Layer 'out' already exists. Specify the --overwrite-layer option to overwrite it, or --append to append it.", + ): + alg.Run() + + alg = get_alg() + alg["input"] = "../gcore/data/uint16.tif" + alg["output"] = out_filename + alg["append"] = True + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetFeatureCount() == 2 + assert alg.Finalize() + ds.Close() + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = out_filename + alg["overwrite-layer"] = True + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetFeatureCount() == 1 + assert alg.Finalize() + ds.Close() + + alg = get_alg() + alg["input"] = "../gcore/data/uint16.tif" + alg["output"] = out_filename + alg["append"] = True + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetFeatureCount() == 2 + assert alg.Finalize() + ds.Close() + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = out_filename + alg["overwrite"] = True + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "26711" + assert lyr.GetFeatureCount() == 1 + assert alg.Finalize() + ds.Close() + + +def test_gdalalg_raster_index_recursive_filter_absolute_path_location_name(): + + alg = get_alg() + alg["input"] = "../gcore/data" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "out" + alg["recursive"] = True + alg["filename-filter"] = "byt?.tif" + alg["absolute-path"] = True + alg["location-name"] = "path" + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetLayerDefn().GetFieldCount() == 1 + assert lyr.GetLayerDefn().GetFieldDefn(0).GetName() == "path" + assert lyr.GetLayerDefn().GetFieldDefn(0).GetType() == ogr.OFTString + f = lyr.GetNextFeature() + assert "byte.tif" in f["path"] + assert not f["path"].startswith("../gcore") + assert os.path.exists(f["path"]) + + +def test_gdalalg_raster_index_metadata(): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "out" + alg["metadata"] = {"foo": "bar"} + alg["filename-filter"] = "byte.tif" + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetMetadataItem("foo") == "bar" + + +@pytest.mark.parametrize("min_pixel_size,expected_count", [(61, 0), (59, 1)]) +def test_gdalalg_raster_index_min_pixel_size(min_pixel_size, expected_count): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "out" + alg["min-pixel-size"] = min_pixel_size + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetFeatureCount() == expected_count + + +def test_gdalalg_raster_index_crs(): + + alg = get_alg() + alg["input"] = "../gcore/data/byte.tif" + alg["output"] = "" + alg["output-format"] = "MEM" + alg["layer"] = "out" + alg["dst-crs"] = "EPSG:4267" + alg["source-crs-field-name"] = "source_crs" + assert alg.Run() + ds = alg["output"].GetDataset() + lyr = ds.GetLayer(0) + assert lyr.GetSpatialRef().GetAuthorityCode(None) == "4267" + f = lyr.GetNextFeature() + assert f["source_crs"] == "EPSG:26711" + assert ( + f.GetGeometryRef().ExportToWkt() + == "POLYGON ((-117.641168620797 33.9023526904272,-117.628190189534 33.9024195619211,-117.628110837847 33.8915970129623,-117.641087629972 33.8915301685907,-117.641168620797 33.9023526904272))" + ) diff --git a/autotest/utilities/test_gdaltindex_lib.py b/autotest/utilities/test_gdaltindex_lib.py index c7ddd3d8bd25..208262795525 100644 --- a/autotest/utilities/test_gdaltindex_lib.py +++ b/autotest/utilities/test_gdaltindex_lib.py @@ -428,7 +428,7 @@ def test_gdaltindex_lib_fetch_md(tmp_path, four_tiles): f = lyr.GetNextFeature() assert f["foo_field"] == "bar" assert f["dt"] == "2023/12/20 16:10:00" - assert f["pixel_size"] == pytest.approx(0.01) + assert f["pixel_size"] == pytest.approx(0.1) del ds gdal.TileIndex( diff --git a/doc/rtd/pre_build.sh b/doc/rtd/pre_build.sh index 35e397b1b706..a958a530596d 100755 --- a/doc/rtd/pre_build.sh +++ b/doc/rtd/pre_build.sh @@ -13,6 +13,7 @@ cmake \ -DGDAL_PYTHON_INSTALL_PREFIX=${PREFIX} \ -DGDAL_BUILD_OPTIONAL_DRIVERS=OFF \ -DOGR_BUILD_OPTIONAL_DRIVERS=OFF \ + -DGDAL_ENABLE_DRIVER_GTI=ON \ -DBUILD_APPS=ON \ -DBUILD_PYTHON_BINDINGS=ON \ -DBUILD_JAVA_BINDINGS=ON \ diff --git a/doc/source/conf.py b/doc/source/conf.py index 5b867e952283..0446f16482f3 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -301,6 +301,13 @@ def check_python_bindings(): [author_evenr], 1, ), + ( + "programs/gdal_raster_index", + "gdal-raster-index", + "Create a vector index of raster datasets", + [author_evenr], + 1, + ), ( "programs/gdal_raster_mosaic", "gdal-raster-mosaic", diff --git a/doc/source/drivers/raster/gti.rst b/doc/source/drivers/raster/gti.rst index 411ad6140f10..7da1ce36cfb7 100644 --- a/doc/source/drivers/raster/gti.rst +++ b/doc/source/drivers/raster/gti.rst @@ -384,7 +384,8 @@ their syntax and semantics. How to build a GTI compatible index ? ---------------------------------------- -The :ref:`gdaltindex` program may be used to generate both a vector tile index, +The :ref:`gdaltindex` program, or starting with GDA 3.11, +:ref:`gdal_driver_gti_create_subcommand`, may be used to generate both a vector tile index, and optionally a wrapping .gti XML file. A GTI compatible index may also be created by any programmatic means, provided diff --git a/doc/source/programs/gdal_driver_gti_create.rst b/doc/source/programs/gdal_driver_gti_create.rst new file mode 100644 index 000000000000..24bcb47fedf9 --- /dev/null +++ b/doc/source/programs/gdal_driver_gti_create.rst @@ -0,0 +1,245 @@ +.. _gdal_driver_gti_create_subcommand: + +================================================================================ +"gdal driver gti create" sub-command +================================================================================ + +.. versionadded:: 3.11 + +.. only:: html + + Create an index of raster datasets compatible of the GDAL Tile Index (GTI) driver. + +.. Index:: gdal driver gti create + +Synopsis +-------- + +.. program-output:: gdal driver gti create --help-doc + +Description +----------- + +:program:`gdal driver gti create` creates a vector dataset with a record for each +input raster file, an attribute containing the filename, and a polygon geometry +outlining the raster, to be used as input for the :ref:`GTI ` driver. + +This is an extension of :ref:`gdal_raster_index_subcommand`. + +There are two possibilities: + +- either use directly a vector tile index generated by :program:`gdal driver gti create` as the input + of the GTI driver + +- or generate a small XML .gti wrapper file, for easier use with non-file-based + formats such as databases, or for vector formats that do not support setting + layer metadata items. + +Formats that support layer metadata are for example GeoPackage (``--of GPKG``), +FlatGeoBuf (``--of FlatGeoBuf``) or PostGIS (``--of PG``) + +Setting :option:`--resolution` and :option:`--ot` is recommended to avoid the GTI +driver to have to deduce them by opening the first tile in the index. If the tiles +have nodata or mask band, :option:`--nodata` and :option:`--mask` should also +be set. + +In a GTI context, the extent of all tiles referenced in the tile index must +be expressed in a single CRS. Consequently, if input tiles may have different +CRS, :option:`--dst-crs` must be specified. + + +The following options are available: + +Standard options +++++++++++++++++ + + +.. include:: gdal_options/of_vector.rst + +.. include:: gdal_options/co.rst + +.. include:: options/lco.rst + +.. include:: gdal_options/overwrite.rst + +.. option:: --update + + Whether the output dataset must be opened in update mode. Implies that + it already exists. + +.. option:: --overwrite-layer + + Whether overwriting the existing output vector layer is allowed. + +.. option:: --append + + Whether appending features to the existing output vector layer is allowed + +.. option:: -l, --nln, --layer + + Provides a name for the output vector layer. + +.. option:: --recursive + + Whether input directories should be explored recursively. + +.. option:: --filename-filter + + Pattern that the filenames contained in input directories should follow. + '*' is a wildcard character that matches any number of any characters + including none. '?' is a wildcard character that matches a single character. + Comparisons are done in a case insensitive way. + Several filters may be specified. + + For example: ``--filename-filter "*.tif,*.tiff"`` + +.. option:: --min-pixel-size + + Minimum pixel size in term of geospatial extent per pixel (resolution) that + a raster should have to be selected. The pixel size + is evaluated after reprojection of its extent to the target CRS defined + by :option:`--dst-crs`. + +.. option:: --max-pixel-size + + Maximum pixel size in term of geospatial extent per pixel (resolution) that + a raster should have to be selected. The pixel size + is evaluated after reprojection of its extent to the target CRS defined + by :option:`--dst-crs`. + +.. option:: -location-name + + The output field name to hold the file path/location to the indexed + rasters. The default field name is ``location``. + +.. option:: --absolute-path + + The absolute path to the raster files is stored in the index file. + By default the raster filenames will be put in the file exactly as they + are specified on the command line. + +.. option:: --dst-crs + + Geometries of input files will be transformed to the desired target + coordinate reference system. + Default creates simple rectangular polygons in the same coordinate reference + system as the input rasters. + +.. option:: --metadata = + + Write an arbitrary layer metadata item, for formats that support layer + metadata. + This option may be repeated. + + +.. option:: --xml-filename + + Filename of the XML Virtual Tile Index file to generate, that can be used + as an input for the GDAL GTI / Virtual Raster Tile Index driver. + This can be useful when writing the tile index in a vector format that + does not support writing layer metadata items. + +.. option:: --resolution + + Target resolution in CRS unit per pixel. + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``RESX`` and ``RESY`` layer metadata items for formats that + support layer metadata. + +.. option:: --bbox ,,, + + Target extent in CRS unit. + + 'x' is longitude values for geographic CRS and easting for projected CRS. + 'y' is latitude values for geographic CRS and northing for projected CRS. + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``MINX``, ``MINY``, ``MAXX`` and ``MAXY`` layer metadata + items for formats that support layer metadata. + +.. option:: --ot, --datatype, --output-data-type + + Data type of the tiles of the tile index: ``Byte``, ``Int8``, ``UInt16``, + ``Int16``, ``UInt32``, ``Int32``, ``UInt64``, ``Int64``, ``Float32``, ``Float64``, ``CInt16``, + ``CInt32``, ``CFloat32`` or ``CFloat64`` + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``DATA_TYPE`` layer metadata item for formats that + support layer metadata. + +.. option:: --band-count + + Number of bands of the tiles of the tile index. + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``BAND_COUNT`` layer metadata item for formats that + support layer metadata. + + A mix of tiles with N and N+1 bands is allowed, provided that the color + interpretation of the (N+1)th band is alpha. The N+1 value must be written + as the band count in that situation. + + If :option:`--nodata` or :option:`--color-interpretation` are specified and have multiple + values, the band count is also inferred from that number. + +.. option:: --nodata [,...] + + Nodata value of the tiles of the tile index. + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``NODATA`` layer metadata item for formats that + support layer metadata. + +.. option:: --color-interpretation [,...] + + Color interpretation of of the tiles of the tile index: + ``red``, ``green``, ``blue``, ``alpha``, ``gray``, ``undefined``. + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``COLOR_INTERPRETATION`` layer metadata item for formats that + support layer metadata. + +.. option:: --mask + + Whether tiles in the tile index have a mask band. + + Written in the XML Virtual Tile Index if :option:`--xml-filename` + is specified, or as ``MASK_BAND`` layer metadata item for formats that + support layer metadata. + +.. option:: --fetched-metadata ,, + + Fetch a metadata item from the raster tile and write it as a field in the + tile index. + + should be the name of the raster metadata item. + ``{PIXEL_SIZE}`` may be used as a special name to indicate the pixel size. + + should be the name of the field to create in the tile index. + + should be the name of the type to create. + One of ``String``, ``Integer``, ``Integer64``, ``Real``, ``Date``, ``DateTime`` + + This option may be repeated. + + For example: ``--fetched-metadata TIFFTAG_DATETIME,creation_date,DateTime`` + +Advanced options +++++++++++++++++ + +.. include:: gdal_options/oo.rst + +.. include:: gdal_options/if.rst + +Examples +-------- + +.. example:: + + Make a tile index from GeoTIFF files, with metadata suitable + for use by the GDAL GTI / Virtual Raster Tile Index driver. + + .. code-block:: bash + + gdal driver gti create --ot Byte --resolution=60,60 --band-count=3 --color-interpretation=Red,Green,Blue *.tif tile_index.gti.gpkg diff --git a/doc/source/programs/gdal_options/srs_def_gdal_raster_reproject.rst b/doc/source/programs/gdal_options/srs_def_gdal_raster_reproject.rst new file mode 100644 index 000000000000..4c7197e6fea0 --- /dev/null +++ b/doc/source/programs/gdal_options/srs_def_gdal_raster_reproject.rst @@ -0,0 +1,8 @@ +The coordinate reference systems that can be passed are anything supported by the +OGRSpatialReference.SetFromUserInput() call, which includes EPSG Projected, +Geographic or Compound CRS (i.e. EPSG:4296), a well known text (WKT) CRS definition, +PROJ.4 declarations, or the name of a .prj file containing a WKT CRS definition. + +If the SRS has an explicit vertical datum that points to a PROJ.4 geoidgrids, +and the input dataset is a single band dataset, a vertical correction will be +applied to the values of the dataset. diff --git a/doc/source/programs/gdal_raster.rst b/doc/source/programs/gdal_raster.rst index ac4a1d6f4bbe..f955267aecdc 100644 --- a/doc/source/programs/gdal_raster.rst +++ b/doc/source/programs/gdal_raster.rst @@ -27,6 +27,7 @@ Available sub-commands - :ref:`gdal_raster_color_map_subcommand` - :ref:`gdal_raster_convert_subcommand` - :ref:`gdal_raster_hillshade_subcommand` +- :ref:`gdal_raster_index_subcommand` - :ref:`gdal_raster_mosaic_subcommand` - :ref:`gdal_raster_overview_subcommand` - :ref:`gdal_raster_pipeline_subcommand` diff --git a/doc/source/programs/gdal_raster_index.rst b/doc/source/programs/gdal_raster_index.rst new file mode 100644 index 000000000000..dfa5eae38d00 --- /dev/null +++ b/doc/source/programs/gdal_raster_index.rst @@ -0,0 +1,154 @@ +.. _gdal_raster_index_subcommand: + +================================================================================ +"gdal raster index" sub-command +================================================================================ + +.. versionadded:: 3.11 + +.. only:: html + + Create a vector index of raster datasets. + +.. Index:: gdal raster index + +Synopsis +-------- + +.. program-output:: gdal raster index --help-doc + +Description +----------- + +:program:`gdal raster index` creates a vector dataset with a record for each +input raster file, an attribute containing the filename, and a polygon geometry +outlining the raster. +This output is suitable for use with `MapServer `__ as a +raster tileindex + +See :ref:`gdal_driver_gti_create_subcommand` for an extension of this command +that create files to be used as input for the :ref:`GTI ` driver. + +The following options are available: + +Standard options +++++++++++++++++ + + +.. include:: gdal_options/of_vector.rst + +.. include:: gdal_options/co.rst + +.. include:: options/lco.rst + +.. include:: gdal_options/overwrite.rst + +.. option:: --update + + Whether the output dataset must be opened in update mode. Implies that + it already exists. + +.. option:: --overwrite-layer + + Whether overwriting the existing output vector layer is allowed. + +.. option:: --append + + Whether appending features to the existing output vector layer is allowed. + +.. option:: -l, --nln, --layer + + Provides a name for the output vector layer. + +.. option:: --recursive + + Whether input directories should be explored recursively. + +.. option:: --filename-filter + + Pattern that the filenames contained in input directories should follow. + '*' is a wildcard character that matches any number of any characters + including none. '?' is a wildcard character that matches a single character. + Comparisons are done in a case insensitive way. + Several filters may be specified. + + For example: ``--filename-filter "*.tif,*.tiff"`` + +.. option:: --min-pixel-size + + Minimum pixel size in term of geospatial extent per pixel (resolution) that + a raster should have to be selected. The pixel size + is evaluated after reprojection of its extent to the target CRS defined + by :option:`--dst-crs`. + +.. option:: --max-pixel-size + + Maximum pixel size in term of geospatial extent per pixel (resolution) that + a raster should have to be selected. The pixel size + is evaluated after reprojection of its extent to the target CRS defined + by :option:`--dst-crs`. + +.. option:: -location-name + + The output field name to hold the file path/location to the indexed + rasters. The default field name is ``location``. + +.. option:: --absolute-path + + The absolute path to the raster files is stored in the index file. + By default the raster filenames will be put in the file exactly as they + are specified on the command line. + +.. option:: --dst-crs + + Geometries of input files will be transformed to the desired target + coordinate reference system. + Default creates simple rectangular polygons in the same coordinate reference + system as the input rasters. + +.. option:: --source-crs-field-name + + The name of the field to store the CRS of each tile. This field name can be + used as the value of the TILESRS keyword in MapServer + +.. option:: --source-crs-format auto|WKT|EPSG|PROJ + + The format in which the CRS of each tile must be written. Types can be + ``auto``, ``WKT``, ``EPSG``, ``PROJ``. + This option should be used together with :option:`--source-crs-field-name`. + +.. option:: --metadata = + + Write an arbitrary layer metadata item, for formats that support layer + metadata. + This option may be repeated. + +Advanced options +++++++++++++++++ + +.. include:: gdal_options/oo.rst + +.. include:: gdal_options/if.rst + +Examples +-------- + +.. example:: + + Produce a GeoPackage with a record for every + image that the utility found in the ``doq`` folder. Each record holds + information that points to the location of the image and also a bounding rectangle + shape showing the bounds of the image: + + .. code-block:: bash + + gdal raster index doq/*.tif doq_index.gpkg + +.. example:: + + The :option:`--dst-crs` option can also be used to transform all input raster + geometries into the same output projection: + + .. code-block:: bash + + gdal raster index --dst-crs EPSG:4326 --source-crs-field-name=src_srs *.tif tile_index_mixed_crs.gpkg diff --git a/doc/source/programs/gdal_vector_convert.rst b/doc/source/programs/gdal_vector_convert.rst index 5894b2518a00..88b015a776af 100644 --- a/doc/source/programs/gdal_vector_convert.rst +++ b/doc/source/programs/gdal_vector_convert.rst @@ -32,6 +32,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. option:: --update diff --git a/doc/source/programs/gdal_vector_filter.rst b/doc/source/programs/gdal_vector_filter.rst index 8e25719445e4..1e7b2daf832c 100644 --- a/doc/source/programs/gdal_vector_filter.rst +++ b/doc/source/programs/gdal_vector_filter.rst @@ -33,6 +33,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_buffer.rst b/doc/source/programs/gdal_vector_geom_buffer.rst index e77886de59c1..38f5fcb5422c 100644 --- a/doc/source/programs/gdal_vector_geom_buffer.rst +++ b/doc/source/programs/gdal_vector_geom_buffer.rst @@ -43,6 +43,10 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_explode_collections.rst b/doc/source/programs/gdal_vector_geom_explode_collections.rst index 67b74a53958a..5c41ef439588 100644 --- a/doc/source/programs/gdal_vector_geom_explode_collections.rst +++ b/doc/source/programs/gdal_vector_geom_explode_collections.rst @@ -38,6 +38,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_make_valid.rst b/doc/source/programs/gdal_vector_geom_make_valid.rst index c26b3af2b4a6..8a70d09964ed 100644 --- a/doc/source/programs/gdal_vector_geom_make_valid.rst +++ b/doc/source/programs/gdal_vector_geom_make_valid.rst @@ -38,6 +38,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_segmentize.rst b/doc/source/programs/gdal_vector_geom_segmentize.rst index fab0110dab5e..68035235c4bd 100644 --- a/doc/source/programs/gdal_vector_geom_segmentize.rst +++ b/doc/source/programs/gdal_vector_geom_segmentize.rst @@ -35,6 +35,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_set_type.rst b/doc/source/programs/gdal_vector_geom_set_type.rst index 146edc5995bd..caeac48ec5a7 100644 --- a/doc/source/programs/gdal_vector_geom_set_type.rst +++ b/doc/source/programs/gdal_vector_geom_set_type.rst @@ -37,6 +37,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_simplify.rst b/doc/source/programs/gdal_vector_geom_simplify.rst index 0b2c7342a292..d73eafc4122e 100644 --- a/doc/source/programs/gdal_vector_geom_simplify.rst +++ b/doc/source/programs/gdal_vector_geom_simplify.rst @@ -43,6 +43,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_geom_swap_xy.rst b/doc/source/programs/gdal_vector_geom_swap_xy.rst index ff937d479fe8..e86680b6f0bf 100644 --- a/doc/source/programs/gdal_vector_geom_swap_xy.rst +++ b/doc/source/programs/gdal_vector_geom_swap_xy.rst @@ -32,6 +32,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_reproject.rst b/doc/source/programs/gdal_vector_reproject.rst index 06b04576e387..7fa041ec1ec7 100644 --- a/doc/source/programs/gdal_vector_reproject.rst +++ b/doc/source/programs/gdal_vector_reproject.rst @@ -30,6 +30,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst @@ -63,13 +65,13 @@ Standard options Set source spatial reference. If not specified the SRS found in the input dataset will be used. - .. include:: options/srs_def_gdalwarp.rst + .. include:: gdal_options/srs_def_gdal_raster_reproject.rst .. option:: -d, --dst-crs Set destination spatial reference. - .. include:: options/srs_def_gdalwarp.rst + .. include:: gdal_options/srs_def_gdal_raster_reproject.rst .. GDALG output (on-the-fly / streamed dataset) .. -------------------------------------------- diff --git a/doc/source/programs/gdal_vector_select.rst b/doc/source/programs/gdal_vector_select.rst index b4c2cf9d5262..11de1e9f9e23 100644 --- a/doc/source/programs/gdal_vector_select.rst +++ b/doc/source/programs/gdal_vector_select.rst @@ -31,6 +31,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. include:: gdal_options/active_layer.rst diff --git a/doc/source/programs/gdal_vector_sql.rst b/doc/source/programs/gdal_vector_sql.rst index 88396a1087a6..7c937bb0179e 100644 --- a/doc/source/programs/gdal_vector_sql.rst +++ b/doc/source/programs/gdal_vector_sql.rst @@ -30,6 +30,8 @@ Standard options .. include:: gdal_options/co_vector.rst +.. include:: options/lco.rst + .. include:: gdal_options/overwrite.rst .. option:: --sql |@ diff --git a/doc/source/programs/index.rst b/doc/source/programs/index.rst index 84501d540a05..def92f90bb5e 100644 --- a/doc/source/programs/index.rst +++ b/doc/source/programs/index.rst @@ -31,6 +31,7 @@ single :program:`gdal` program that accepts commands and subcommands. gdal gdal_info gdal_convert + gdal_driver_gti_create gdal_mdim gdal_mdim_info gdal_mdim_convert @@ -45,6 +46,7 @@ single :program:`gdal` program that accepts commands and subcommands. gdal_raster_convert gdal_raster_edit gdal_raster_hillshade + gdal_raster_index gdal_raster_mosaic gdal_raster_overview gdal_raster_overview_add @@ -93,6 +95,7 @@ single :program:`gdal` program that accepts commands and subcommands. - :ref:`gdal_program`: Main "gdal" entry point - :ref:`gdal_info_command`: Get information on a dataset - :ref:`gdal_convert_command`: Convert a dataset + - :ref:`gdal_driver_gti_create_subcommand`: Create an index of raster datasets compatible of the GDAL Tile Index (GTI) driver - :ref:`gdal_mdim_command`: Entry point for multidimensional commands - :ref:`gdal_mdim_info_subcommand`: Get information on a multidimensional dataset - :ref:`gdal_mdim_convert_subcommand`: Convert a multidimensional dataset @@ -107,6 +110,7 @@ single :program:`gdal` program that accepts commands and subcommands. - :ref:`gdal_raster_contour_subcommand`: Builds vector contour lines from a raster elevation model - :ref:`gdal_raster_edit_subcommand`: Edit in place a raster dataset - :ref:`gdal_raster_hillshade_subcommand`: Generate a shaded relief map + - :ref:`gdal_raster_index_subcommand`: Create a vector index of raster datasets - :ref:`gdal_raster_mosaic_subcommand`: Build a mosaic, either virtual (VRT) or materialized. - :ref:`gdal_raster_overview_subcommand`: Manage overviews of a raster dataset - :ref:`gdal_raster_overview_add_subcommand`: Add overviews to a raster dataset diff --git a/doc/source/programs/ogr2ogr.rst b/doc/source/programs/ogr2ogr.rst index dc0917c4914a..88265d41be93 100644 --- a/doc/source/programs/ogr2ogr.rst +++ b/doc/source/programs/ogr2ogr.rst @@ -144,9 +144,7 @@ output coordinate system or even reprojecting the features during translation. Dataset creation option (format specific) -.. option:: -lco = - - Layer creation option (format specific) +.. include:: options/lco.rst .. option:: -nln diff --git a/doc/source/programs/options/lco.rst b/doc/source/programs/options/lco.rst new file mode 100644 index 000000000000..39c7d919ca92 --- /dev/null +++ b/doc/source/programs/options/lco.rst @@ -0,0 +1,4 @@ + +.. option:: -lco = + + Layer creation option (format specific) diff --git a/frmts/gti/gdaltileindexdataset.cpp b/frmts/gti/gdaltileindexdataset.cpp index 288b86337438..bb252e608345 100644 --- a/frmts/gti/gdaltileindexdataset.cpp +++ b/frmts/gti/gdaltileindexdataset.cpp @@ -37,6 +37,8 @@ #include "gdal_thread_pool.h" #include "gdal_utils.h" +#include "gdalalg_raster_index.h" + #ifdef USE_NEON_OPTIMIZATIONS #define USE_SSE2_OPTIM #define USE_SSE41_OPTIM @@ -51,6 +53,10 @@ #endif #endif +#ifndef _ +#define _(x) (x) +#endif + // Semantincs of indices of a GeoTransform (double[6]) matrix constexpr int GT_TOPLEFT_X = 0; constexpr int GT_WE_RES = 1; @@ -4816,6 +4822,236 @@ void GDALTileIndexDataset::RasterIOJob::Func(void *pData) ++(*psJob->pnCompletedJobs); } +/************************************************************************/ +/* GDALGTICreateAlgorithm */ +/************************************************************************/ + +class GDALGTICreateAlgorithm final : public GDALRasterIndexAlgorithm +{ + public: + static constexpr const char *NAME = "create"; + static constexpr const char *DESCRIPTION = + "Create an index of raster datasets compatible of the GDAL Tile Index " + "(GTI) driver."; + static constexpr const char *HELP_URL = + "/programs/gdal_driver_gti_create.html"; + + GDALGTICreateAlgorithm(); + + protected: + bool AddExtraOptions(CPLStringList &aosOptions) override; + + private: + std::string m_xmlFilename{}; + std::vector m_resolution{}; + std::vector m_bbox{}; + std::string m_dataType{}; + int m_bandCount = 0; + std::vector m_nodata{}; + std::vector m_colorInterpretation{}; + bool m_mask = false; + std::vector m_fetchedMetadata{}; +}; + +/************************************************************************/ +/* GDALGTICreateAlgorithm::GDALGTICreateAlgorithm() */ +/************************************************************************/ + +GDALGTICreateAlgorithm::GDALGTICreateAlgorithm() + : GDALRasterIndexAlgorithm(NAME, DESCRIPTION, HELP_URL) +{ + AddProgressArg(); + AddInputDatasetArg(&m_inputDatasets, GDAL_OF_RASTER) + .SetAutoOpenDataset(false); + GDALVectorOutputAbstractAlgorithm::AddAllOutputArgs(); + + AddCommonOptions(); + + AddArg("xml-filename", 0, + _("Filename of the XML Virtual Tile Index file to generate, that " + "can be used as an input for the GDAL GTI / Virtual Raster Tile " + "Index driver"), + &m_xmlFilename) + .SetMinCharCount(1); + + AddArg("resolution", 0, + _("Resolution (in destination CRS units) of the virtual mosaic"), + &m_resolution) + .SetMinCount(2) + .SetMaxCount(2) + .SetMinValueExcluded(0) + .SetRepeatedArgAllowed(false) + .SetDisplayHintAboutRepetition(false) + .SetMetaVar(","); + + AddBBOXArg( + &m_bbox, + _("Bounding box (in destination CRS units) of the virtual mosaic")); + AddOutputDataTypeArg(&m_dataType, _("Datatype of the virtual mosaic")); + AddArg("band-count", 0, _("Number of bands of the virtual mosaic"), + &m_bandCount) + .SetMinValueIncluded(1); + AddArg("nodata", 0, _("Nodata value(s) of the bands of the virtual mosaic"), + &m_nodata); + AddArg("color-interpretation", 0, + _("Color interpretation(s) of the bands of the virtual mosaic"), + &m_colorInterpretation) + .SetChoices("red", "green", "blue", "alpha", "gray", "undefined"); + AddArg("mask", 0, _("Defines that the virtual mosaic has a mask band"), + &m_mask); + AddArg("fetch-metadata", 0, + _("Fetch a metadata item from source rasters and write it as a " + "field in the index."), + &m_fetchedMetadata) + .SetMetaVar(",,") + .SetPackedValuesAllowed(false) + .AddValidationAction( + [this]() + { + for (const std::string &s : m_fetchedMetadata) + { + const CPLStringList aosTokens( + CSLTokenizeString2(s.c_str(), ",", 0)); + if (aosTokens.size() != 3) + { + ReportError( + CE_Failure, CPLE_IllegalArg, + "'%s' is not of the form " + ",,", + s.c_str()); + return false; + } + bool ok = false; + for (const char *type : {"String", "Integer", "Integer64", + "Real", "Date", "DateTime"}) + { + if (EQUAL(aosTokens[2], type)) + ok = true; + } + if (!ok) + { + ReportError(CE_Failure, CPLE_IllegalArg, + "'%s' has an invalid field type '%s'. It " + "should be one of 'String', 'Integer', " + "'Integer64', 'Real', 'Date', 'DateTime'.", + s.c_str(), aosTokens[2]); + return false; + } + } + return true; + }); +} + +/************************************************************************/ +/* GDALGTICreateAlgorithm::AddExtraOptions() */ +/************************************************************************/ + +bool GDALGTICreateAlgorithm::AddExtraOptions(CPLStringList &aosOptions) +{ + if (!m_xmlFilename.empty()) + { + aosOptions.push_back("-gti_filename"); + aosOptions.push_back(m_xmlFilename); + } + if (!m_resolution.empty()) + { + aosOptions.push_back("-tr"); + aosOptions.push_back(CPLSPrintf("%.17g", m_resolution[0])); + aosOptions.push_back(CPLSPrintf("%.17g", m_resolution[1])); + } + if (!m_bbox.empty()) + { + aosOptions.push_back("-te"); + aosOptions.push_back(CPLSPrintf("%.17g", m_bbox[0])); + aosOptions.push_back(CPLSPrintf("%.17g", m_bbox[1])); + aosOptions.push_back(CPLSPrintf("%.17g", m_bbox[2])); + aosOptions.push_back(CPLSPrintf("%.17g", m_bbox[3])); + } + if (!m_dataType.empty()) + { + aosOptions.push_back("-ot"); + aosOptions.push_back(m_dataType); + } + if (m_bandCount > 0) + { + aosOptions.push_back("-bandcount"); + aosOptions.push_back(CPLSPrintf("%d", m_bandCount)); + + if (!m_nodata.empty() && m_nodata.size() != 1 && + static_cast(m_nodata.size()) != m_bandCount) + { + ReportError(CE_Failure, CPLE_IllegalArg, + "%d nodata values whereas one or %d were expected", + static_cast(m_nodata.size()), m_bandCount); + return false; + } + + if (!m_colorInterpretation.empty() && + m_colorInterpretation.size() != 1 && + static_cast(m_colorInterpretation.size()) != m_bandCount) + { + ReportError( + CE_Failure, CPLE_IllegalArg, + "%d color interpretations whereas one or %d were expected", + static_cast(m_colorInterpretation.size()), m_bandCount); + return false; + } + } + if (!m_nodata.empty()) + { + std::string val; + for (double v : m_nodata) + { + if (!val.empty()) + val += ','; + val += CPLSPrintf("%.17g", v); + } + aosOptions.push_back("-nodata"); + aosOptions.push_back(val); + } + if (!m_colorInterpretation.empty()) + { + std::string val; + for (const std::string &s : m_colorInterpretation) + { + if (!val.empty()) + val += ','; + val += s; + } + aosOptions.push_back("-colorinterp"); + aosOptions.push_back(val); + } + if (m_mask) + aosOptions.push_back("-mask"); + for (const std::string &s : m_fetchedMetadata) + { + aosOptions.push_back("-fetch_md"); + const CPLStringList aosTokens(CSLTokenizeString2(s.c_str(), ",", 0)); + for (const char *token : aosTokens) + { + aosOptions.push_back(token); + } + } + return true; +} + +/************************************************************************/ +/* GDALTileIndexInstantiateAlgorithm() */ +/************************************************************************/ + +static GDALAlgorithm * +GDALTileIndexInstantiateAlgorithm(const std::vector &aosPath) +{ + if (aosPath.size() == 1 && aosPath[0] == "create") + { + return std::make_unique().release(); + } + else + { + return nullptr; + } +} + /************************************************************************/ /* GDALRegister_GTI() */ /************************************************************************/ @@ -4859,6 +5095,9 @@ void GDALRegister_GTI() "default='ALL_CPUS'/>" ""); + poDriver->DeclareAlgorithm({"create"}); + poDriver->pfnInstantiateAlgorithm = GDALTileIndexInstantiateAlgorithm; + #ifdef BUILT_AS_PLUGIN // Used by gdaladdo and test_gdaladdo.py poDriver->SetMetadataItem("IS_PLUGIN", "YES"); diff --git a/gcore/gdalalgorithm.cpp b/gcore/gdalalgorithm.cpp index 9eb93f1fbe17..477cc6b0bcff 100644 --- a/gcore/gdalalgorithm.cpp +++ b/gcore/gdalalgorithm.cpp @@ -705,7 +705,43 @@ bool GDALAlgorithmArg::RunValidationActions() } } - if (GetType() == GAAT_INTEGER) + if (GetType() == GAAT_STRING) + { + const int nMinCharCount = GetMinCharCount(); + if (nMinCharCount > 0) + { + const auto &val = Get(); + if (val.size() < static_cast(nMinCharCount)) + { + CPLError( + CE_Failure, CPLE_IllegalArg, + "Value of argument '%s' is '%s', but should have at least " + "%d character(s)", + GetName().c_str(), val.c_str(), nMinCharCount); + ret = false; + } + } + } + else if (GetType() == GAAT_STRING_LIST) + { + const int nMinCharCount = GetMinCharCount(); + if (nMinCharCount > 0) + { + for (const auto &val : Get>()) + { + if (val.size() < static_cast(nMinCharCount)) + { + CPLError( + CE_Failure, CPLE_IllegalArg, + "Value of argument '%s' is '%s', but should have at " + "least %d character(s)", + GetName().c_str(), val.c_str(), nMinCharCount); + ret = false; + } + } + } + } + else if (GetType() == GAAT_INTEGER) { ret = ValidateIntRange(Get()) && ret; } diff --git a/gcore/gdalalgorithm.h b/gcore/gdalalgorithm.h index 3b11c766b839..c2ddaa1c7e64 100644 --- a/gcore/gdalalgorithm.h +++ b/gcore/gdalalgorithm.h @@ -730,6 +730,15 @@ class CPL_DLL GDALAlgorithmArgDecl final return *this; } + /** Sets the minimum number of characters (for arguments of type + * GAAT_STRING and GAAT_STRING_LIST) + */ + GDALAlgorithmArgDecl &SetMinCharCount(int count) + { + m_minCharCount = count; + return *this; + } + //! @cond Doxygen_Suppress GDALAlgorithmArgDecl &SetHiddenChoices() { @@ -926,6 +935,14 @@ class CPL_DLL GDALAlgorithmArgDecl final return {m_maxVal, m_maxValIsIncluded}; } + /** Return the minimum number of characters (for arguments of type + * GAAT_STRING and GAAT_STRING_LIST) + */ + inline int GetMinCharCount() const + { + return m_minCharCount; + } + /** Return whether the argument is required. Defaults to false. */ inline bool IsRequired() const @@ -1137,6 +1154,7 @@ class CPL_DLL GDALAlgorithmArgDecl final double m_maxVal = std::numeric_limits::quiet_NaN(); bool m_minValIsIncluded = false; bool m_maxValIsIncluded = false; + int m_minCharCount = 0; }; /************************************************************************/ @@ -1311,6 +1329,12 @@ class CPL_DLL GDALAlgorithmArg /* non-final */ return m_decl.GetMaxValue(); } + /** Alias for GDALAlgorithmArgDecl::GetMinCharCount() */ + inline int GetMinCharCount() const + { + return m_decl.GetMinCharCount(); + } + /** Return whether the argument value has been explicitly set with Set() */ inline bool IsExplicitlySet() const { @@ -1767,6 +1791,13 @@ class CPL_DLL GDALInConstructionAlgorithmArg final : public GDALAlgorithmArg return *this; } + /** Alias for GDALAlgorithmArgDecl::SetMinCharCount() */ + GDALInConstructionAlgorithmArg &SetMinCharCount(int count) + { + m_decl.SetMinCharCount(count); + return *this; + } + /** Alias for GDALAlgorithmArgDecl::SetHidden() */ GDALInConstructionAlgorithmArg &SetHidden() { diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index 51fb5e211cd8..321efebbd0fa 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -5592,10 +5592,14 @@ class VSIFile(BytesIO): if type == GAAT_REAL_LIST: return self.SetAsDoubleList(value) if type == GAAT_DATASET_LIST: - if isinstance(value[0], str) or isinstance(value[0], os.PathLike): + if isinstance(value, list) and (isinstance(value[0], str) or isinstance(value[0], os.PathLike)): return self.SetDatasetNames([str(v) for v in value]) - elif isinstance(value[0], Dataset): + elif isinstance(value, list) and isinstance(value[0], Dataset): return self.SetDatasets(value) + elif isinstance(value, str) or isinstance(value, os.PathLike): + return self.SetDatasetNames([str(value)]) + elif isinstance(value, Dataset): + return self.SetDatasets([value]) else: raise "Unexpected value type %s for an argument of type DatasetList" % str(type(value)) raise Exception("Unhandled algorithm argument data type")