Coverage Report

Created: 2026-10-28 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_shift_longitude.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  gdal "raster shift-longitude" subcommand
5
 * Author:   Dan Baston
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2026, ISciences LLC
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_shift_longitude.h"
14
15
#include "cpl_conv.h"
16
#include "gdal_priv.h"
17
#include "gdal_priv_templates.hpp"
18
#include "../frmts/vrt/gdal_vrt.h"
19
#include "../frmts/vrt/vrtdataset.h"
20
21
#include <algorithm>
22
23
//! @cond Doxygen_Suppress
24
25
#ifndef _
26
0
#define _(x) (x)
27
#endif
28
29
/************************************************************************/
30
/*GDALRasterShiftLongitudeAlgorithm::GDALRasterShiftLongitudeAlgorithm()*/
31
/************************************************************************/
32
33
GDALRasterShiftLongitudeAlgorithm::GDALRasterShiftLongitudeAlgorithm(
34
    bool bStandalone)
35
0
    : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL, bStandalone)
36
0
{
37
0
    AddArg("min-x", 0, _("Sets the minimum longitude value"), &m_minX)
38
0
        .SetRequired();
39
0
    AddArg("max-x", 0, _("Sets the maximum longitude value"), &m_maxX)
40
0
        .SetRequired();
41
0
    AddNodataArg(&m_nodata, /* noneAllowed = */ true, "output-nodata");
42
0
}
43
44
/************************************************************************/
45
/*             GDALRasterShiftLongitudeAlgorithm::RunStep()             */
46
/************************************************************************/
47
48
static double NormalizeLongitude(double lon)
49
0
{
50
0
    while (lon < -180.0)
51
0
        lon += 360.0;
52
0
    while (lon > 180.0)
53
0
        lon -= 360.0;
54
0
    return lon;
55
0
}
56
57
bool GDALRasterShiftLongitudeAlgorithm::RunStep(GDALPipelineStepRunContext &)
58
0
{
59
0
    CPLAssert(!m_outputDataset.GetDatasetRef());
60
61
0
    const auto poSrcDS = m_inputDataset[0].GetDatasetRef();
62
63
0
    GDALGeoTransform srcGT;
64
0
    if (poSrcDS->GetGeoTransform(srcGT) != CE_None)
65
0
    {
66
0
        ReportError(CE_Failure, CPLE_AppDefined,
67
0
                    "Input dataset does not have a geotransform");
68
0
        return false;
69
0
    }
70
71
0
    if (!srcGT.IsAxisAligned())
72
0
    {
73
0
        ReportError(CE_Failure, CPLE_AppDefined,
74
0
                    "Input dataset geotransform cannot have a rotation term");
75
0
        return false;
76
0
    }
77
78
0
    const auto nSrcXSize = poSrcDS->GetRasterXSize();
79
80
0
    const double dfSrcMinX = srcGT.xorig;
81
0
    const double dfSrcMaxX = srcGT.xorig + srcGT.xscale * nSrcXSize;
82
83
    // Snap m_minX and m_maxX to pixel boundaries of the input raster
84
0
    {
85
0
        const double dfXOff =
86
0
            std::ceil(std::abs(dfSrcMinX - m_minX) / srcGT.xscale) *
87
0
            (m_minX < dfSrcMinX ? -1 : 1);
88
0
        if (!GDALIsValueInRange<int>(dfXOff))
89
0
        {
90
0
            CPLError(CE_Failure, CPLE_AppDefined,
91
0
                     "Failed to compute pixel offset between minimum x "
92
0
                     "coordinate of input and output.");
93
0
            return false;
94
0
        }
95
0
        const int nXOff = static_cast<int>(dfXOff);
96
0
        m_minX = dfSrcMinX + nXOff * srcGT.xscale;
97
0
    }
98
0
    const double dfDstXSize = std::ceil((m_maxX - m_minX) / srcGT.xscale);
99
0
    if (!GDALIsValueInRange<int>(dfDstXSize))
100
0
    {
101
0
        CPLError(CE_Failure, CPLE_AppDefined,
102
0
                 "Size of output raster exceeds maximum allowable size.");
103
0
        return false;
104
0
    }
105
0
    const int nDstXSize = static_cast<int>(dfDstXSize);
106
107
0
    if (nDstXSize <= 0)
108
0
    {
109
0
        ReportError(CE_Failure, CPLE_AppDefined,
110
0
                    "--max-x must be greater than --min-x");
111
0
        return false;
112
0
    }
113
114
0
    m_maxX = m_minX + nDstXSize * srcGT.xscale;
115
0
    const int nYSize = poSrcDS->GetRasterYSize();
116
117
0
    auto poDstDS = std::make_unique<VRTDataset>(nDstXSize, nYSize);
118
0
    {
119
0
        GDALGeoTransform dstGT = srcGT;
120
0
        dstGT.xorig = m_minX;
121
0
        poDstDS->SetGeoTransform(dstGT);
122
0
    }
123
0
    poDstDS->SetSpatialRef(poSrcDS->GetSpatialRef());
124
125
0
    std::vector<GDALRasterWindow> aosSrcWindows;
126
0
    for (int nDstXOff = 0; nDstXOff < nDstXSize;)
127
0
    {
128
0
        double dfDstChunkMinX = m_minX + nDstXOff * srcGT.xscale;
129
130
0
        while (dfDstChunkMinX < dfSrcMinX)
131
0
        {
132
0
            dfDstChunkMinX += 360;
133
0
        }
134
0
        while (dfDstChunkMinX >= dfSrcMaxX)
135
0
        {
136
0
            dfDstChunkMinX -= 360;
137
0
        }
138
139
0
        const double dfSrcXOff =
140
0
            std::round((dfDstChunkMinX - dfSrcMinX) / srcGT.xscale);
141
0
        if (!GDALIsValueInRange<int>(dfSrcXOff))
142
0
        {
143
0
            CPLError(CE_Failure, CPLE_AppDefined,
144
0
                     "Failed to calculate source pixels for longitude range "
145
0
                     "beginning at %g",
146
0
                     dfDstChunkMinX);
147
0
            return false;
148
0
        }
149
0
        const int nSrcXOff = static_cast<int>(dfSrcXOff);
150
151
0
        const int nColumnsWanted = nDstXSize - nDstXOff;
152
        // cppcheck-suppress duplicateExpression
153
0
        const int nColumnsAvailable = nSrcXOff < 0 ? 0
154
0
                                      : nSrcXOff > nSrcXSize
155
0
                                          ? 0
156
0
                                          : nSrcXSize - nSrcXOff;
157
158
0
        if (nColumnsAvailable == 0)
159
0
        {
160
0
            const double dfMissingRangeMinX =
161
0
                NormalizeLongitude(dfDstChunkMinX);
162
0
            const int nMissingPx =
163
0
                std::min(std::abs(nSrcXOff), nDstXSize - nDstXOff);
164
0
            double dfMissingRangeMaxX =
165
0
                NormalizeLongitude(dfDstChunkMinX + srcGT.xscale * nMissingPx);
166
0
            while (dfMissingRangeMaxX < dfMissingRangeMinX)
167
0
            {
168
0
                dfMissingRangeMaxX += 360;
169
0
            }
170
171
0
            CPLError(
172
0
                CE_Warning, CPLE_AppDefined,
173
0
                "No source data available for output longitude range %g to %g",
174
0
                dfMissingRangeMinX, dfMissingRangeMaxX);
175
0
            nDstXOff += nMissingPx;
176
177
0
            const GDALRasterWindow chunk{-1, -1, nMissingPx, nYSize};
178
0
            aosSrcWindows.push_back(chunk);
179
0
            continue;
180
0
        }
181
182
0
        const GDALRasterWindow chunk{
183
0
            nSrcXOff, 0, std::min(nColumnsWanted, nColumnsAvailable), nYSize};
184
185
0
        CPLAssert(chunk.nXSize > 0);
186
187
0
        const double dfDstChunkMaxX =
188
0
            m_minX + (nDstXOff + chunk.nXSize) * srcGT.xscale;
189
0
        const double dfSrcChunkMinX = dfSrcMinX + chunk.nXOff * srcGT.xscale;
190
0
        const double dfSrcChunkMaxX =
191
0
            dfSrcMinX + (nSrcXOff + chunk.nXSize) * srcGT.xscale;
192
193
0
        CPLDebug("ShiftLongitude", "Src %g - %g, Dst %g - %g", dfSrcChunkMinX,
194
0
                 dfSrcChunkMaxX, m_minX + nDstXOff * srcGT.xscale,
195
0
                 dfDstChunkMaxX);
196
197
0
        aosSrcWindows.push_back(chunk);
198
0
        nDstXOff += chunk.nXSize;
199
0
    }
200
201
0
    for (int iBand = 1; iBand <= poSrcDS->GetRasterCount(); ++iBand)
202
0
    {
203
0
        GDALRasterBand *poSrcBand = poSrcDS->GetRasterBand(iBand);
204
0
        poDstDS->AddBand(poSrcBand->GetRasterDataType());
205
0
        VRTSourcedRasterBand *poDstBand =
206
0
            cpl::down_cast<VRTSourcedRasterBand *>(
207
0
                poDstDS->GetRasterBand(iBand));
208
0
        poDstBand->CopyCommonInfoFrom(poSrcBand);
209
210
0
        bool bHasNoData;
211
0
        double dfSrcNoData{0};
212
213
0
        {
214
0
            int bSuccess;
215
0
            dfSrcNoData = poSrcBand->GetNoDataValue(&bSuccess);
216
0
            if (!bSuccess)
217
0
            {
218
0
                dfSrcNoData = VRT_NODATA_UNSET;
219
0
            }
220
0
        }
221
222
0
        if (!m_nodata.empty())
223
0
        {
224
0
            bool inexact = false;
225
0
            if (poDstBand->SetNoDataValueAsString(m_nodata.c_str(), &inexact) !=
226
0
                CE_None)
227
0
            {
228
0
                CPLError(CE_Failure, CPLE_AppDefined,
229
0
                         "Invalid NoData value: %s", m_nodata.c_str());
230
0
                return false;
231
0
            }
232
0
            if (inexact)
233
0
            {
234
0
                CPLError(CE_Failure, CPLE_AppDefined,
235
0
                         "Specified NoData value cannot be represented in the "
236
0
                         "output data type.");
237
0
                return false;
238
0
            }
239
0
            bHasNoData = true;
240
0
        }
241
0
        else
242
0
        {
243
0
            bHasNoData = GDALCopyNoDataValue(poDstBand, poSrcBand, nullptr);
244
0
        }
245
246
0
        int nDstXOff = 0;
247
0
        for (const auto &chunk : aosSrcWindows)
248
0
        {
249
0
            const bool bDataAvailable = chunk.nXOff >= 0;
250
251
0
            if (bDataAvailable)
252
0
            {
253
0
                CPLDebug("ShiftLongitude", "Adding source chunk %d,%d %d %d",
254
0
                         chunk.nXOff, chunk.nYOff, chunk.nXSize, chunk.nYSize);
255
256
                // Scale and offset are propagated to the output band, not handled by the ComplexSource
257
0
                constexpr double dfSourceScale = 1.0;
258
0
                constexpr double dfSourceOffset = 0.0;
259
0
                constexpr int nSrcYOff = 0;
260
0
                constexpr int nDstYOff = 0;
261
0
                if (poDstBand->AddComplexSource(
262
0
                        poSrcBand, chunk.nXOff, nSrcYOff, chunk.nXSize,
263
0
                        chunk.nYSize, nDstXOff, nDstYOff, chunk.nXSize,
264
0
                        chunk.nYSize, dfSourceOffset, dfSourceScale,
265
0
                        dfSrcNoData) != CE_None)
266
0
                {
267
0
                    return false;
268
0
                }
269
0
            }
270
0
            else if (!bHasNoData)
271
0
            {
272
0
                CPLErrorOnce(
273
0
                    CE_Warning, CPLE_AppDefined,
274
0
                    "Some regions of the output dataset have no source data "
275
0
                    "available, but a NoData value has not been defined. You "
276
0
                    "can set a value using --output-nodata");
277
0
            }
278
0
            nDstXOff += chunk.nXSize;
279
0
        }
280
0
    }
281
282
0
    m_outputDataset.Set(std::move(poDstDS));
283
284
0
    return true;
285
0
}
286
287
GDALRasterShiftLongitudeAlgorithmStandalone::
288
0
    ~GDALRasterShiftLongitudeAlgorithmStandalone() = default;
289
290
//! @endcond