Skip to content

Commit 1aa852a

Browse files
authored
Merge pull request OSGeo#11982 from dbaston/vrt-common-res
BuildVRT: Add -resolution=compatible
2 parents f8d1d28 + 591fc4b commit 1aa852a

14 files changed

+363
-29
lines changed

apps/gdalalg_raster_calc.cpp

+21-6
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,32 @@ UpdateSourceProperties(SourceProperties &out, const std::string &dsn,
183183
return std::nullopt;
184184
}
185185

186-
// Choose the finest resolution
187-
// TODO modify to use common resolution (https://github.com/OSGeo/gdal/issues/11497)
186+
// Find a common resolution
188187
if (source.nX > out.nX)
189188
{
190-
out.nX = source.nX;
191-
out.gt[1] = source.gt[1];
189+
auto dx = CPLGreatestCommonDivisor(out.gt[1], source.gt[1]);
190+
if (dx == 0)
191+
{
192+
CPLError(CE_Failure, CPLE_AppDefined,
193+
"Failed to find common resolution for inputs.");
194+
return std::nullopt;
195+
}
196+
out.nX = static_cast<int>(
197+
std::round(static_cast<double>(out.nX) * out.gt[1] / dx));
198+
out.gt[1] = dx;
192199
}
193200
if (source.nY > out.nY)
194201
{
195-
out.nY = source.nY;
196-
out.gt[5] = source.gt[5];
202+
auto dy = CPLGreatestCommonDivisor(out.gt[5], source.gt[5]);
203+
if (dy == 0)
204+
{
205+
CPLError(CE_Failure, CPLE_AppDefined,
206+
"Failed to find common resolution for inputs.");
207+
return std::nullopt;
208+
}
209+
out.nY = static_cast<int>(
210+
std::round(static_cast<double>(out.nY) * out.gt[5] / dy));
211+
out.gt[5] = dy;
197212
}
198213

199214
if (srsMismatch)

apps/gdalalg_raster_mosaic.cpp

+7-6
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ GDALRasterMosaicAlgorithm::GDALRasterMosaicAlgorithm()
5454
_("Target resolution (in destination CRS units)"),
5555
&m_resolution)
5656
.SetDefault("same")
57-
.SetMetaVar("<xres>,<yres>|same|average|highest|lowest");
57+
.SetMetaVar("<xres>,<yres>|same|average|common|highest|lowest");
5858
arg.AddValidationAction(
5959
[this, &arg]()
6060
{
6161
const std::string val = arg.Get<std::string>();
6262
if (val != "average" && val != "highest" && val != "lowest" &&
63-
val != "same")
63+
val != "same" && val != "common")
6464
{
6565
const auto aosTokens =
6666
CPLStringList(CSLTokenizeString2(val.c_str(), ",", 0));
@@ -70,10 +70,11 @@ GDALRasterMosaicAlgorithm::GDALRasterMosaicAlgorithm()
7070
CPLAtof(aosTokens[0]) <= 0 ||
7171
CPLAtof(aosTokens[1]) <= 0)
7272
{
73-
ReportError(CE_Failure, CPLE_AppDefined,
74-
"resolution: two comma separated positive "
75-
"values should be provided, or 'same', "
76-
"'average', 'highest' or 'lowest'");
73+
ReportError(
74+
CE_Failure, CPLE_AppDefined,
75+
"resolution: two comma separated positive "
76+
"values should be provided, or 'same', "
77+
"'average', 'common', 'highest' or 'lowest'");
7778
return false;
7879
}
7980
}

apps/gdalalg_raster_stack.cpp

+7-6
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ GDALRasterStackAlgorithm::GDALRasterStackAlgorithm()
5454
_("Target resolution (in destination CRS units)"),
5555
&m_resolution)
5656
.SetDefault("same")
57-
.SetMetaVar("<xres>,<yres>|same|average|highest|lowest");
57+
.SetMetaVar("<xres>,<yres>|same|average|common|highest|lowest");
5858
arg.AddValidationAction(
5959
[this, &arg]()
6060
{
6161
const std::string val = arg.Get<std::string>();
6262
if (val != "average" && val != "highest" && val != "lowest" &&
63-
val != "same")
63+
val != "same" && val != "common")
6464
{
6565
const auto aosTokens =
6666
CPLStringList(CSLTokenizeString2(val.c_str(), ",", 0));
@@ -70,10 +70,11 @@ GDALRasterStackAlgorithm::GDALRasterStackAlgorithm()
7070
CPLAtof(aosTokens[0]) <= 0 ||
7171
CPLAtof(aosTokens[1]) <= 0)
7272
{
73-
ReportError(CE_Failure, CPLE_AppDefined,
74-
"resolution: two comma separated positive "
75-
"values should be provided, or 'same', "
76-
"'average', 'highest' or 'lowest'");
73+
ReportError(
74+
CE_Failure, CPLE_AppDefined,
75+
"resolution: two comma-separated positive "
76+
"values should be provided, or 'same', "
77+
"'average', 'common', 'highest' or 'lowest'");
7778
return false;
7879
}
7980
}

apps/gdalbuildvrt_lib.cpp

+23-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include "commonutils.h"
3535
#include "cpl_conv.h"
3636
#include "cpl_error.h"
37+
#include "cpl_float.h"
3738
#include "cpl_progress.h"
3839
#include "cpl_string.h"
3940
#include "cpl_vsi.h"
@@ -64,7 +65,8 @@ typedef enum
6465
HIGHEST_RESOLUTION,
6566
AVERAGE_RESOLUTION,
6667
SAME_RESOLUTION,
67-
USER_RESOLUTION
68+
USER_RESOLUTION,
69+
COMMON_RESOLUTION,
6870
} ResolutionStrategy;
6971

7072
struct DatasetProperty
@@ -1016,6 +1018,21 @@ std::string VRTBuilder::AnalyseRaster(GDALDatasetH hDS,
10161018
// ns_res is negative, the highest resolution is the max value.
10171019
ns_res = std::max(ns_res, padfGeoTransform[GEOTRSFRM_NS_RES]);
10181020
}
1021+
else if (resolutionStrategy == COMMON_RESOLUTION)
1022+
{
1023+
we_res = CPLGreatestCommonDivisor(
1024+
we_res, padfGeoTransform[GEOTRSFRM_WE_RES]);
1025+
if (we_res == 0)
1026+
{
1027+
return "Failed to get common resolution";
1028+
}
1029+
ns_res = CPLGreatestCommonDivisor(
1030+
ns_res, padfGeoTransform[GEOTRSFRM_NS_RES]);
1031+
if (ns_res == 0)
1032+
{
1033+
return "Failed to get common resolution";
1034+
}
1035+
}
10191036
else
10201037
{
10211038
we_res = std::max(we_res, padfGeoTransform[GEOTRSFRM_WE_RES]);
@@ -1942,6 +1959,8 @@ GDALDatasetH GDALBuildVRT(const char *pszDest, int nSrcCount,
19421959
eStrategy = LOWEST_RESOLUTION;
19431960
else if (EQUAL(sOptions.osResolution.c_str(), "same"))
19441961
eStrategy = SAME_RESOLUTION;
1962+
else if (EQUAL(sOptions.osResolution.c_str(), "common"))
1963+
eStrategy = COMMON_RESOLUTION;
19451964

19461965
/* If -srcnodata is specified, use it as the -vrtnodata if the latter is not
19471966
*/
@@ -2075,7 +2094,7 @@ GDALBuildVRTOptionsGetParser(GDALBuildVRTOptions *psOptions,
20752094
"the default value which is 'location'."));
20762095

20772096
argParser->add_argument("-resolution")
2078-
.metavar("user|average|highest|lowest|same")
2097+
.metavar("user|average|common|highest|lowest|same")
20792098
.action(
20802099
[psOptions](const std::string &s)
20812100
{
@@ -2084,7 +2103,8 @@ GDALBuildVRTOptionsGetParser(GDALBuildVRTOptions *psOptions,
20842103
!EQUAL(psOptions->osResolution.c_str(), "average") &&
20852104
!EQUAL(psOptions->osResolution.c_str(), "highest") &&
20862105
!EQUAL(psOptions->osResolution.c_str(), "lowest") &&
2087-
!EQUAL(psOptions->osResolution.c_str(), "same"))
2106+
!EQUAL(psOptions->osResolution.c_str(), "same") &&
2107+
!EQUAL(psOptions->osResolution.c_str(), "common"))
20882108
{
20892109
throw std::invalid_argument(
20902110
CPLSPrintf("Illegal resolution value (%s).",

autotest/cpp/test_cpl.cpp

+33
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include "cpl_compressor.h"
2323
#include "cpl_error.h"
24+
#include "cpl_float.h"
2425
#include "cpl_hash_set.h"
2526
#include "cpl_list.h"
2627
#include "cpl_mask.h"
@@ -5763,4 +5764,36 @@ TEST_F(test_cpl, VSIGlob)
57635764
VSIUnlink(osFilenameRadix.c_str());
57645765
}
57655766

5767+
TEST_F(test_cpl, CPLGreatestCommonDivisor)
5768+
{
5769+
CPLErrorStateBackuper state(CPLQuietErrorHandler);
5770+
5771+
// These tests serve to document the current behavior.
5772+
// In some cases the results are dependent on various
5773+
// hardcoded epsilons and it may be appropriate to change
5774+
// the expected results.
5775+
5776+
EXPECT_EQ(CPLGreatestCommonDivisor(3.0, 5.0), 1.0);
5777+
EXPECT_EQ(CPLGreatestCommonDivisor(3.0 / 3600, 5.0 / 3600), 1.0 / 3600);
5778+
EXPECT_EQ(CPLGreatestCommonDivisor(5.0 / 3600, 2.5 / 3600), 2.5 / 3600);
5779+
EXPECT_EQ(CPLGreatestCommonDivisor(1.0 / 10, 1.0), 1.0 / 10);
5780+
EXPECT_EQ(CPLGreatestCommonDivisor(1.0 / 17, 1.0 / 13), 1.0 / 221);
5781+
EXPECT_EQ(CPLGreatestCommonDivisor(1.0 / 17, 1.0 / 3600), 1.0 / 61200);
5782+
5783+
// GLO-90 resolutoins
5784+
EXPECT_EQ(CPLGreatestCommonDivisor(3.0 / 3600, 4.5 / 3600), 1.5 / 3600);
5785+
5786+
// WorldDEM resolutions
5787+
EXPECT_EQ(CPLGreatestCommonDivisor(0.4 / 3600, 0.6 / 3600), 0.2 / 3600);
5788+
EXPECT_EQ(CPLGreatestCommonDivisor(0.6 / 3600, 0.8 / 3600), 0.2 / 3600);
5789+
5790+
EXPECT_EQ(CPLGreatestCommonDivisor(M_PI, M_PI / 6), M_PI / 6);
5791+
EXPECT_EQ(CPLGreatestCommonDivisor(M_PI / 5, M_PI / 6),
5792+
0); // Ideally we would get M_PI / 30
5793+
5794+
EXPECT_EQ(CPLGreatestCommonDivisor(2.999999, 3.0), 0);
5795+
EXPECT_EQ(CPLGreatestCommonDivisor(2.9999999, 3.0), 0);
5796+
EXPECT_EQ(CPLGreatestCommonDivisor(2.99999999, 3.0), 2.99999999);
5797+
}
5798+
57665799
} // namespace

autotest/utilities/test_gdalalg_raster_calc.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# SPDX-License-Identifier: MIT
1212
###############################################################################
1313

14+
import functools
15+
import math
1416
import re
1517

1618
import gdaltest
@@ -281,7 +283,7 @@ def test_gdalalg_calc_different_resolutions(calc, tmp_vsimem):
281283

282284
xmax = 60
283285
ymax = 60
284-
resolutions = [10, 20, 60]
286+
resolutions = [30, 20, 60]
285287

286288
inputs = [tmp_vsimem / f"in_{i}.tif" for i in range(len(resolutions))]
287289
outfile = tmp_vsimem / "out.tif"
@@ -305,8 +307,8 @@ def test_gdalalg_calc_different_resolutions(calc, tmp_vsimem):
305307
assert calc.Run()
306308

307309
with gdal.Open(outfile) as ds:
308-
assert ds.RasterXSize == xmax / min(resolutions)
309-
assert ds.RasterYSize == ymax / min(resolutions)
310+
assert ds.RasterXSize == xmax / functools.reduce(math.gcd, resolutions)
311+
assert ds.RasterYSize == ymax / functools.reduce(math.gcd, resolutions)
310312

311313
assert np.all(ds.ReadAsArray() == sum(resolutions))
312314

autotest/utilities/test_gdalalg_raster_mosaic.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,28 @@ def test_gdalalg_raster_mosaic_resolution_lowest():
156156
assert ds.GetGeoTransform() == pytest.approx((2.0, 1.0, 0.0, 49.0, 0.0, -1.0))
157157

158158

159+
def test_gdalalg_raster_mosaic_resolution_common():
160+
161+
# resolution = 3
162+
src1_ds = gdal.GetDriverByName("MEM").Create("", 5, 5)
163+
src1_ds.SetGeoTransform([2, 3, 0, 49, 0, -3])
164+
165+
# resolution = 5
166+
src2_ds = gdal.GetDriverByName("MEM").Create("", 3, 3)
167+
src2_ds.SetGeoTransform([17, 5, 0, 49, 0, -5])
168+
169+
alg = get_mosaic_alg()
170+
alg["input"] = [src1_ds, src2_ds]
171+
alg["output"] = ""
172+
alg["resolution"] = "common"
173+
assert alg.Run()
174+
ds = alg["output"].GetDataset()
175+
176+
assert ds.RasterXSize == 30
177+
assert ds.RasterYSize == 15
178+
assert ds.GetGeoTransform() == pytest.approx((2.0, 1.0, 0.0, 49.0, 0.0, -1.0))
179+
180+
159181
def test_gdalalg_raster_mosaic_resolution_custom():
160182

161183
src1_ds = gdal.GetDriverByName("MEM").Create("", 1, 1)
@@ -215,19 +237,19 @@ def test_gdalalg_raster_mosaic_resolution_invalid():
215237
alg = get_mosaic_alg()
216238
with pytest.raises(
217239
Exception,
218-
match="resolution: two comma separated positive values should be provided, or 'same', 'average', 'highest' or 'lowest'",
240+
match="resolution: two comma separated positive values should be provided, or ",
219241
):
220242
alg["resolution"] = "invalid"
221243

222244
with pytest.raises(
223245
Exception,
224-
match="resolution: two comma separated positive values should be provided, or 'same', 'average', 'highest' or 'lowest'",
246+
match="resolution: two comma separated positive values should be provided, or ",
225247
):
226248
alg["resolution"] = "0.5"
227249

228250
with pytest.raises(
229251
Exception,
230-
match="resolution: two comma separated positive values should be provided, or 'same', 'average', 'highest' or 'lowest'",
252+
match="resolution: two comma separated positive values should be provided, or ",
231253
):
232254
alg["resolution"] = "-0.5,-0.5"
233255

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env pytest
2+
# -*- coding: utf-8 -*-
3+
###############################################################################
4+
# Project: GDAL/OGR Test Suite
5+
# Purpose: 'gdal raster stack' testing
6+
# Author: Daniel Baston
7+
#
8+
###############################################################################
9+
# Copyright (c) 2025, ISciences LLC
10+
#
11+
# SPDX-License-Identifier: MIT
12+
###############################################################################
13+
14+
import pytest
15+
16+
from osgeo import gdal
17+
18+
19+
@pytest.fixture()
20+
def stack():
21+
return gdal.GetGlobalAlgorithmRegistry()["raster"]["stack"]
22+
23+
24+
def test_gdalalg_raster_stack_resolution_common(stack):
25+
26+
# resolution = 3
27+
src1_ds = gdal.GetDriverByName("MEM").Create("", 5, 5)
28+
src1_ds.SetGeoTransform([2, 3, 0, 49, 0, -3])
29+
30+
# resolution = 5
31+
src2_ds = gdal.GetDriverByName("MEM").Create("", 3, 3)
32+
src2_ds.SetGeoTransform([2, 5, 0, 49, 0, -5])
33+
34+
stack["input"] = [src1_ds, src2_ds]
35+
stack["output"] = ""
36+
stack["resolution"] = "common"
37+
assert stack.Run()
38+
ds = stack["output"].GetDataset()
39+
40+
assert ds.RasterCount == 2
41+
assert ds.RasterXSize == 15
42+
assert ds.RasterYSize == 15
43+
assert ds.GetGeoTransform() == pytest.approx((2.0, 1.0, 0.0, 49.0, 0.0, -1.0))

autotest/utilities/test_gdalbuildvrt_lib.py

+46
Original file line numberDiff line numberDiff line change
@@ -923,3 +923,49 @@ def test_gdalbuildvrt_lib_nodata_invalid(tmp_vsimem, dtype, nodata):
923923
gdal.CE_Warning, "cannot represent the specified NoData value"
924924
):
925925
gdal.BuildVRT("", [tmp_vsimem / "in.tif"], VRTNodata=nodata)
926+
927+
928+
###############################################################################
929+
930+
931+
# Test --resolution=common
932+
# Also tested by C++ test_cpl.CPLGreatestCommonDivisor
933+
@gdaltest.enable_exceptions()
934+
@pytest.mark.parametrize(
935+
"resolutions,expected",
936+
[
937+
([5 / 3600, 3 / 3600], 1 / 3600),
938+
([5, 3], 1),
939+
([5 / 3600, 2.5 / 3600], 2.5 / 3600),
940+
([1 / 10, 1], 1 / 10),
941+
([1 / 10, 1 / 3], 1 / 30),
942+
([1 / 17, 1 / 13], 1 / 221),
943+
([1 / 17, 1 / 3600], 1 / 61200),
944+
([2.9999999, 3], "common resolution"),
945+
],
946+
)
947+
def test_gdalbuildvrt_resolution_common(tmp_vsimem, resolutions, expected):
948+
949+
inputs = []
950+
951+
width = 5
952+
height = 5
953+
954+
for i, res in enumerate(resolutions):
955+
fname = tmp_vsimem / f"in_{i}.tif"
956+
inputs.append(fname)
957+
958+
nx = round(width / res)
959+
ny = round(height / res)
960+
961+
with gdal.GetDriverByName("GTiff").Create(fname, nx, ny) as ds:
962+
ds.SetGeoTransform((0, res, 0, height, 0, -res))
963+
964+
if type(expected) is str:
965+
with pytest.raises(Exception, match=expected):
966+
gdal.BuildVRT("", inputs, resolution="common", strict=True)
967+
else:
968+
with gdal.BuildVRT("", inputs, resolution="common", strict=True) as ds:
969+
gt = ds.GetGeoTransform()
970+
assert gt[1] == expected
971+
assert -gt[5] == expected

0 commit comments

Comments
 (0)