Coverage Report

Created: 2026-04-01 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_viewshed.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  gdal "raster viewshed" subcommand
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_viewshed.h"
14
15
#include "cpl_conv.h"
16
#include "cpl_vsi_virtual.h"
17
18
#include "commonutils.h"
19
#include "gdal_priv.h"
20
21
#include "viewshed/cumulative.h"
22
#include "viewshed/viewshed.h"
23
24
#include <algorithm>
25
26
//! @cond Doxygen_Suppress
27
28
#ifndef _
29
0
#define _(x) (x)
30
#endif
31
32
/************************************************************************/
33
/*      GDALRasterViewshedAlgorithm::GDALRasterViewshedAlgorithm()      */
34
/************************************************************************/
35
36
GDALRasterViewshedAlgorithm::GDALRasterViewshedAlgorithm(bool standaloneStep)
37
0
    : GDALRasterPipelineNonNativelyStreamingAlgorithm(NAME, DESCRIPTION,
38
0
                                                      HELP_URL, standaloneStep)
39
0
{
40
0
    AddArg("position", 'p', _("Observer position"), &m_observerPos)
41
0
        .AddAlias("pos")
42
0
        .SetMetaVar("<X,Y> or <X,Y,H>")
43
0
        .SetMinCount(2)
44
0
        .SetMaxCount(3)
45
0
        .SetRepeatedArgAllowed(false);
46
0
    AddArg("height", 'z', _("Observer height"), &m_opts.observer.z);
47
48
0
    auto &sdFilenameArg =
49
0
        AddArg("sd-filename", 0, _("Filename of standard-deviation raster"),
50
0
               &m_sdFilename, GDAL_OF_RASTER);
51
0
    SetAutoCompleteFunctionForFilename(sdFilenameArg, GDAL_OF_RASTER);
52
53
0
    AddArg("target-height", 0,
54
0
           _("Height of the target above the DEM surface in the height unit of "
55
0
             "the DEM."),
56
0
           &m_opts.targetHeight)
57
0
        .SetDefault(m_opts.targetHeight);
58
0
    AddArg("mode", 0, _("Sets what information the output contains."),
59
0
           &m_outputMode)
60
0
        .SetChoices("normal", "DEM", "ground", "cumulative")
61
0
        .SetDefault(m_outputMode);
62
63
0
    AddArg("max-distance", 0,
64
0
           _("Maximum distance from observer to compute visibility. It is also "
65
0
             "used to clamp the extent of the output raster."),
66
0
           &m_opts.maxDistance)
67
0
        .SetMinValueIncluded(0);
68
0
    AddArg("min-distance", 0,
69
0
           _("Mask all cells less than this distance from the observer. Must "
70
0
             "be less "
71
0
             "than 'max-distance'."),
72
0
           &m_opts.minDistance)
73
0
        .SetMinValueIncluded(0);
74
75
0
    AddArg("start-angle", 0,
76
0
           _("Mask all cells outside of the arc ('start-angle', 'end-angle'). "
77
0
             "Clockwise degrees "
78
0
             "from north. Also used to clamp the extent of the output raster."),
79
0
           &m_opts.startAngle)
80
0
        .SetMinValueIncluded(0)
81
0
        .SetMaxValueExcluded(360);
82
0
    AddArg("end-angle", 0,
83
0
           _("Mask all cells outside of the arc ('start-angle', 'end-angle'). "
84
0
             "Clockwise degrees "
85
0
             "from north. Also used to clamp the extent of the output raster."),
86
0
           &m_opts.endAngle)
87
0
        .SetMinValueIncluded(0)
88
0
        .SetMaxValueExcluded(360);
89
90
0
    AddArg("high-pitch", 0,
91
0
           _("Mark all cells out-of-range where the observable height would be "
92
0
             "higher than the "
93
0
             "'high-pitch' angle from the observer. Degrees from horizontal - "
94
0
             "positive is up. "
95
0
             "Must be greater than 'low-pitch'."),
96
0
           &m_opts.highPitch)
97
0
        .SetMaxValueIncluded(90)
98
0
        .SetMinValueExcluded(-90);
99
0
    AddArg("low-pitch", 0,
100
0
           _("Bound observable height to be no lower than the 'low-pitch' "
101
0
             "angle from the observer. "
102
0
             "Degrees from horizontal - positive is up. Must be less than "
103
0
             "'high-pitch'."),
104
0
           &m_opts.lowPitch)
105
0
        .SetMaxValueExcluded(90)
106
0
        .SetMinValueIncluded(-90);
107
108
0
    AddArg("curvature-coefficient", 0,
109
0
           _("Coefficient to consider the effect of the curvature and "
110
0
             "refraction."),
111
0
           &m_opts.curveCoeff)
112
0
        .SetMinValueIncluded(0);
113
114
0
    AddBandArg(&m_band).SetDefault(m_band);
115
0
    AddArg("visible-value", 0, _("Pixel value to set for visible areas"),
116
0
           &m_opts.visibleVal)
117
0
        .SetDefault(m_opts.visibleVal)
118
0
        .SetMinValueIncluded(0)
119
0
        .SetMaxValueIncluded(255);
120
0
    AddArg("invisible-value", 0, _("Pixel value to set for invisible areas"),
121
0
           &m_opts.invisibleVal)
122
0
        .SetDefault(m_opts.invisibleVal)
123
0
        .SetMinValueIncluded(0)
124
0
        .SetMaxValueIncluded(255);
125
0
    AddArg("maybe-visible-value", 0,
126
0
           _("Pixel value to set for potentially visible areas"),
127
0
           &m_opts.maybeVisibleVal)
128
0
        .SetDefault(m_opts.maybeVisibleVal)
129
0
        .SetMinValueIncluded(0)
130
0
        .SetMaxValueIncluded(255);
131
0
    AddArg("out-of-range-value", 0,
132
0
           _("Pixel value to set for the cells that fall outside of the range "
133
0
             "specified by the observer location and the maximum distance"),
134
0
           &m_opts.outOfRangeVal)
135
0
        .SetDefault(m_opts.outOfRangeVal);
136
0
    AddArg("dst-nodata", 0,
137
0
           _("The value to be set for the cells in the output raster that have "
138
0
             "no data."),
139
0
           &m_opts.nodataVal)
140
0
        .SetMinValueIncluded(0)
141
0
        .SetMaxValueIncluded(255);
142
0
    AddArg("observer-spacing", 0, _("Cell Spacing between observers"),
143
0
           &m_opts.observerSpacing)
144
0
        .SetDefault(m_opts.observerSpacing)
145
0
        .SetMinValueIncluded(1);
146
147
0
    m_numThreadsStr = std::to_string(m_numThreads);
148
0
    AddNumThreadsArg(&m_numThreads, &m_numThreadsStr);
149
0
}
150
151
/************************************************************************/
152
/*                GDALRasterViewshedAlgorithm::RunStep()                */
153
/************************************************************************/
154
155
bool GDALRasterViewshedAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt)
156
0
{
157
0
    auto pfnProgress = ctxt.m_pfnProgress;
158
0
    auto pProgressData = ctxt.m_pProgressData;
159
0
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
160
0
    CPLAssert(poSrcDS);
161
0
    CPLAssert(!m_outputDataset.GetDatasetRef());
162
163
0
    GDALRasterBandH sdBand = nullptr;
164
0
    if (auto sdDataset = m_sdFilename.GetDatasetRef())
165
0
    {
166
0
        if (sdDataset->GetRasterCount() == 0)
167
0
        {
168
0
            ReportError(
169
0
                CE_Failure, CPLE_AppDefined,
170
0
                "The standard deviation dataset must have one raster band");
171
0
            return false;
172
0
        }
173
0
        sdBand = GDALRasterBand::FromHandle(sdDataset->GetRasterBand(1));
174
0
    }
175
176
0
    if (GetArg("height")->IsExplicitlySet())
177
0
    {
178
0
        if (m_observerPos.size() == 3)
179
0
        {
180
0
            ReportError(CE_Failure, CPLE_AppDefined,
181
0
                        "Height can't be specified in both 'position' and "
182
0
                        "'height' arguments");
183
0
            return false;
184
0
        }
185
0
    }
186
187
0
    if (m_observerPos.size())
188
0
    {
189
0
        m_opts.observer.x = m_observerPos[0];
190
0
        m_opts.observer.y = m_observerPos[1];
191
0
        if (m_observerPos.size() == 3)
192
0
            m_opts.observer.z = m_observerPos[2];
193
0
        else
194
0
            m_opts.observer.z = 2;
195
0
    }
196
197
0
    if (!GetArg("curvature-coefficient")->IsExplicitlySet())
198
0
    {
199
0
        m_opts.curveCoeff = gdal::viewshed::adjustCurveCoeff(
200
0
            m_opts.curveCoeff, GDALDataset::ToHandle(poSrcDS));
201
0
    }
202
203
0
    if (m_outputMode == "normal")
204
0
        m_opts.outputMode = gdal::viewshed::OutputMode::Normal;
205
0
    else if (m_outputMode == "DEM")
206
0
        m_opts.outputMode = gdal::viewshed::OutputMode::DEM;
207
0
    else if (m_outputMode == "ground")
208
0
        m_opts.outputMode = gdal::viewshed::OutputMode::Ground;
209
0
    else if (m_outputMode == "cumulative")
210
0
        m_opts.outputMode = gdal::viewshed::OutputMode::Cumulative;
211
212
0
    m_opts.numJobs = static_cast<uint8_t>(std::clamp(m_numThreads, 0, 255));
213
214
0
    m_opts.outputFilename =
215
0
        CPLGenerateTempFilenameSafe(
216
0
            CPLGetBasenameSafe(poSrcDS->GetDescription()).c_str()) +
217
0
        ".tif";
218
0
    m_opts.outputFormat = "GTiff";
219
220
0
    if (m_opts.outputMode == gdal::viewshed::OutputMode::Cumulative)
221
0
    {
222
0
        static const std::vector<std::string> badArgs{
223
0
            "visible-value", "invisible-value", "max-distance",
224
0
            "min-distance",  "start-angle",     "end-angle",
225
0
            "low-pitch",     "high-pitch",      "position"};
226
227
0
        for (const auto &arg : badArgs)
228
0
            if (GetArg(arg)->IsExplicitlySet())
229
0
            {
230
0
                std::string err =
231
0
                    "Option '" + arg + "' can't be used in cumulative mode.";
232
0
                ReportError(CE_Failure, CPLE_AppDefined, "%s", err.c_str());
233
0
                return false;
234
0
            }
235
236
0
        auto poSrcDriver = poSrcDS->GetDriver();
237
0
        if (EQUAL(poSrcDS->GetDescription(), "") || !poSrcDriver ||
238
0
            EQUAL(poSrcDriver->GetDescription(), "MEM"))
239
0
        {
240
0
            ReportError(
241
0
                CE_Failure, CPLE_AppDefined,
242
0
                "In cumulative mode, the input dataset must be opened by name");
243
0
            return false;
244
0
        }
245
0
        gdal::viewshed::Cumulative oViewshed(m_opts);
246
0
        const bool bSuccess = oViewshed.run(
247
0
            m_inputDataset[0].GetName().c_str(),
248
0
            pfnProgress ? pfnProgress : GDALDummyProgress, pProgressData);
249
0
        if (bSuccess)
250
0
        {
251
0
            m_outputDataset.Set(std::unique_ptr<GDALDataset>(
252
0
                GDALDataset::Open(m_opts.outputFilename.c_str(),
253
0
                                  GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR,
254
0
                                  nullptr, nullptr, nullptr)));
255
0
        }
256
0
    }
257
0
    else
258
0
    {
259
0
        static const std::vector<std::string> badArgs{
260
0
            "observer-spacing", GDAL_ARG_NAME_NUM_THREADS};
261
0
        for (const auto &arg : badArgs)
262
0
            if (GetArg(arg)->IsExplicitlySet())
263
0
            {
264
0
                std::string err =
265
0
                    "Option '" + arg + "' can't be used in standard mode.";
266
0
                ReportError(CE_Failure, CPLE_AppDefined, "%s", err.c_str());
267
0
                return false;
268
0
            }
269
0
        static const std::vector<std::string> goodArgs{"position"};
270
0
        for (const auto &arg : goodArgs)
271
0
            if (!GetArg(arg)->IsExplicitlySet())
272
0
            {
273
0
                std::string err =
274
0
                    "Option '" + arg + "' must be specified in standard mode.";
275
0
                ReportError(CE_Failure, CPLE_AppDefined, "%s", err.c_str());
276
0
                return false;
277
0
            }
278
279
0
        gdal::viewshed::Viewshed oViewshed(m_opts);
280
0
        const bool bSuccess = oViewshed.run(
281
0
            GDALRasterBand::ToHandle(poSrcDS->GetRasterBand(m_band)), sdBand,
282
0
            pfnProgress ? pfnProgress : GDALDummyProgress, pProgressData);
283
0
        if (bSuccess)
284
0
        {
285
0
            m_outputDataset.Set(oViewshed.output());
286
0
        }
287
0
    }
288
289
0
    auto poOutDS = m_outputDataset.GetDatasetRef();
290
0
    if (poOutDS && poOutDS->GetDescription()[0])
291
0
    {
292
        // In file systems that allow it (all but Windows...), we want to
293
        // delete the temporary file as soon as soon as possible after
294
        // having open it, so that if someone kills the process there are
295
        // no temp files left over. If that unlink() doesn't succeed
296
        // (on Windows), then the file will eventually be deleted when
297
        // poTmpDS is cleaned due to MarkSuppressOnClose().
298
0
        VSIUnlink(poOutDS->GetDescription());
299
0
        poOutDS->MarkSuppressOnClose();
300
0
    }
301
302
0
    return poOutDS != nullptr;
303
0
}
304
305
GDALRasterViewshedAlgorithmStandalone::
306
0
    ~GDALRasterViewshedAlgorithmStandalone() = default;
307
308
//! @endcond