Coverage Report

Created: 2026-02-14 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/alg/viewshed/viewshed.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  Viewshed Generation
4
 * Purpose:  Core algorithm implementation for viewshed generation.
5
 * Author:   Tamas Szekeres, szekerest@gmail.com
6
 *
7
 * (c) 2024 info@hobu.co
8
 *
9
 ******************************************************************************
10
 *
11
 * SPDX-License-Identifier: MIT
12
 ****************************************************************************/
13
14
#include <algorithm>
15
#include <array>
16
17
#include "gdal_alg.h"
18
#include "gdal_priv_templates.hpp"
19
20
#include "progress.h"
21
#include "util.h"
22
#include "viewshed.h"
23
#include "viewshed_executor.h"
24
25
/************************************************************************/
26
/*                        GDALViewshedGenerate()                        */
27
/************************************************************************/
28
29
/**
30
 * Create viewshed from raster DEM.
31
 *
32
 * This algorithm will generate a viewshed raster from an input DEM raster
33
 * by using a modified algorithm of "Generating Viewsheds without Using
34
 * Sightlines" published at
35
 * https://www.asprs.org/wp-content/uploads/pers/2000journal/january/2000_jan_87-90.pdf
36
 * This approach provides a relatively fast calculation, since the output raster
37
 * is generated in a single scan. The gdal/apps/gdal_viewshed.cpp mainline can
38
 * be used as an example of how to use this function. The output raster will be
39
 * of type Byte or Float64.
40
 *
41
 * \note The algorithm as implemented currently will only output meaningful
42
 * results if the georeferencing is in a projected coordinate reference system.
43
 *
44
 * @param hBand The band to read the DEM data from. Only the part of the raster
45
 * within the specified maxdistance around the observer point is processed.
46
 *
47
 * @param pszDriverName Driver name (GTiff if set to NULL)
48
 *
49
 * @param pszTargetRasterName The name of the target raster to be generated.
50
 * Must not be NULL
51
 *
52
 * @param papszCreationOptions creation options.
53
 *
54
 * @param dfObserverX observer X value (in SRS units)
55
 *
56
 * @param dfObserverY observer Y value (in SRS units)
57
 *
58
 * @param dfObserverHeight The height of the observer above the DEM surface.
59
 *
60
 * @param dfTargetHeight The height of the target above the DEM surface.
61
 * (default 0)
62
 *
63
 * @param dfVisibleVal pixel value for visibility (default 255)
64
 *
65
 * @param dfInvisibleVal pixel value for invisibility (default 0)
66
 *
67
 * @param dfOutOfRangeVal The value to be set for the cells that fall outside of
68
 * the range specified by dfMaxDistance.
69
 *
70
 * @param dfNoDataVal The value to be set for the cells that have no data.
71
 *                    If set to a negative value, nodata is not set.
72
 *                    Note: currently, no special processing of input cells at a
73
 * nodata value is done (which may result in erroneous results).
74
 *
75
 * @param dfCurvCoeff Coefficient to consider the effect of the curvature and
76
 * refraction. The height of the DEM is corrected according to the following
77
 * formula: [Height] -= dfCurvCoeff * [Target Distance]^2 / [Earth Diameter] For
78
 * the effect of the atmospheric refraction we can use 0.85714.
79
 *
80
 * @param eMode The mode of the viewshed calculation.
81
 * Possible values GVM_Diagonal = 1, GVM_Edge = 2 (default), GVM_Max = 3,
82
 * GVM_Min = 4.
83
 *
84
 * @param dfMaxDistance maximum distance range to compute viewshed.
85
 *                      It is also used to clamp the extent of the output
86
 * raster. If set to 0, then unlimited range is assumed, that is to say the
87
 *                      computation is performed on the extent of the whole
88
 * raster.
89
 *
90
 * @param pfnProgress A GDALProgressFunc that may be used to report progress
91
 * to the user, or to interrupt the algorithm.  May be NULL if not required.
92
 *
93
 * @param pProgressArg The callback data for the pfnProgress function.
94
 *
95
 * @param heightMode Type of information contained in output raster. Possible
96
 * values GVOT_NORMAL = 1 (default), GVOT_MIN_TARGET_HEIGHT_FROM_DEM = 2,
97
 *                   GVOT_MIN_TARGET_HEIGHT_FROM_GROUND = 3
98
 *
99
 *                   GVOT_NORMAL returns a raster of type Byte containing
100
 * visible locations.
101
 *
102
 *                   GVOT_MIN_TARGET_HEIGHT_FROM_DEM and
103
 * GVOT_MIN_TARGET_HEIGHT_FROM_GROUND will return a raster of type Float64
104
 * containing the minimum target height for target to be visible from the DEM
105
 * surface or ground level respectively. Parameters dfTargetHeight, dfVisibleVal
106
 * and dfInvisibleVal will be ignored.
107
 *
108
 * @param papszExtraOptions Extra options to control the viewshed analysis.
109
 * This is a NULL-terminated list of strings in "KEY=VALUE" format, or NULL for no options.
110
 * The following keys are supported:
111
 * <ul>
112
 * <li><b>START_ANGLE</b>: Mask all cells outside of the arc ('start-angle', 'end-angle'). Clockwise degrees from north. Also used to clamp the extent of the output raster.</li>
113
 * <li><b>END_ANGLE</b>:  Mask all cells outside of the arc ('start-angle', 'end-angle'). Clockwise degrees from north. Also used to clamp the extent of the output raster.</li>
114
 * <li><b>LOW_PITCH</b>: Bound observable height to be no lower than the 'low-pitch' angle from the observer. Degrees from horizontal - positive is up. Must be less than 'high-pitch'.</li>
115
 * <li><b>HIGH_PITCH</b>: Mark all cells out-of-range where the observable height would be higher than the 'high-pitch' angle from the observer. Degrees from horizontal - positive is up. Must be greater than 'low-pitch'.</li>
116
 * </ul>
117
 * If NULL, a 360-degree viewshed is calculated.
118
 *
119
 * @return not NULL output dataset on success (to be closed with GDALClose()) or
120
 * NULL if an error occurs.
121
 *
122
 * @since GDAL 3.1
123
 */
124
GDALDatasetH GDALViewshedGenerate(
125
    GDALRasterBandH hBand, const char *pszDriverName,
126
    const char *pszTargetRasterName, CSLConstList papszCreationOptions,
127
    double dfObserverX, double dfObserverY, double dfObserverHeight,
128
    double dfTargetHeight, double dfVisibleVal, double dfInvisibleVal,
129
    double dfOutOfRangeVal, double dfNoDataVal, double dfCurvCoeff,
130
    GDALViewshedMode eMode, double dfMaxDistance, GDALProgressFunc pfnProgress,
131
    void *pProgressArg, GDALViewshedOutputType heightMode,
132
    [[maybe_unused]] CSLConstList papszExtraOptions)
133
0
{
134
0
    using namespace gdal;
135
136
0
    viewshed::Options oOpts;
137
0
    oOpts.outputFormat = pszDriverName;
138
0
    oOpts.outputFilename = pszTargetRasterName;
139
0
    oOpts.creationOpts = papszCreationOptions;
140
0
    oOpts.observer.x = dfObserverX;
141
0
    oOpts.observer.y = dfObserverY;
142
0
    oOpts.observer.z = dfObserverHeight;
143
0
    oOpts.targetHeight = dfTargetHeight;
144
0
    oOpts.curveCoeff = dfCurvCoeff;
145
0
    oOpts.maxDistance = dfMaxDistance;
146
0
    oOpts.nodataVal = dfNoDataVal;
147
148
0
    switch (eMode)
149
0
    {
150
0
        case GVM_Edge:
151
0
            oOpts.cellMode = viewshed::CellMode::Edge;
152
0
            break;
153
0
        case GVM_Diagonal:
154
0
            oOpts.cellMode = viewshed::CellMode::Diagonal;
155
0
            break;
156
0
        case GVM_Min:
157
0
            oOpts.cellMode = viewshed::CellMode::Min;
158
0
            break;
159
0
        case GVM_Max:
160
0
            oOpts.cellMode = viewshed::CellMode::Max;
161
0
            break;
162
0
    }
163
164
0
    switch (heightMode)
165
0
    {
166
0
        case GVOT_MIN_TARGET_HEIGHT_FROM_DEM:
167
0
            oOpts.outputMode = viewshed::OutputMode::DEM;
168
0
            break;
169
0
        case GVOT_MIN_TARGET_HEIGHT_FROM_GROUND:
170
0
            oOpts.outputMode = viewshed::OutputMode::Ground;
171
0
            break;
172
0
        case GVOT_NORMAL:
173
0
            oOpts.outputMode = viewshed::OutputMode::Normal;
174
0
            break;
175
0
    }
176
177
0
    if (!GDALIsValueInRange<uint8_t>(dfVisibleVal))
178
0
    {
179
0
        CPLError(CE_Failure, CPLE_AppDefined,
180
0
                 "dfVisibleVal out of range. Must be [0, 255].");
181
0
        return nullptr;
182
0
    }
183
0
    if (!GDALIsValueInRange<uint8_t>(dfInvisibleVal))
184
0
    {
185
0
        CPLError(CE_Failure, CPLE_AppDefined,
186
0
                 "dfInvisibleVal out of range. Must be [0, 255].");
187
0
        return nullptr;
188
0
    }
189
0
    if (!GDALIsValueInRange<uint8_t>(dfOutOfRangeVal))
190
0
    {
191
0
        CPLError(CE_Failure, CPLE_AppDefined,
192
0
                 "dfOutOfRangeVal out of range. Must be [0, 255].");
193
0
        return nullptr;
194
0
    }
195
0
    oOpts.visibleVal = dfVisibleVal;
196
0
    oOpts.invisibleVal = dfInvisibleVal;
197
0
    oOpts.outOfRangeVal = dfOutOfRangeVal;
198
199
0
    const char *pszStartAngle =
200
0
        CSLFetchNameValue(papszExtraOptions, "START_ANGLE");
201
0
    if (pszStartAngle)
202
0
        oOpts.startAngle = CPLAtof(pszStartAngle);
203
204
0
    const char *pszEndAngle = CSLFetchNameValue(papszExtraOptions, "END_ANGLE");
205
0
    if (pszEndAngle)
206
0
        oOpts.endAngle = CPLAtof(pszEndAngle);
207
208
0
    const char *pszLowPitch = CSLFetchNameValue(papszExtraOptions, "LOW_PITCH");
209
0
    if (pszLowPitch)
210
0
        oOpts.lowPitch = CPLAtof(pszLowPitch);
211
212
0
    const char *pszHighPitch =
213
0
        CSLFetchNameValue(papszExtraOptions, "HIGH_PITCH");
214
0
    if (pszHighPitch)
215
0
        oOpts.highPitch = CPLAtof(pszHighPitch);
216
217
0
    gdal::viewshed::Viewshed v(oOpts);
218
219
0
    if (!pfnProgress)
220
0
        pfnProgress = GDALDummyProgress;
221
0
    v.run(hBand, pfnProgress, pProgressArg);
222
223
0
    return GDALDataset::FromHandle(v.output().release());
224
0
}
225
226
namespace gdal
227
{
228
namespace viewshed
229
{
230
231
namespace
232
{
233
234
bool getTransforms(GDALRasterBand &band, GDALGeoTransform &fwdTransform,
235
                   GDALGeoTransform &revTransform)
236
0
{
237
0
    band.GetDataset()->GetGeoTransform(fwdTransform);
238
0
    if (!fwdTransform.GetInverse(revTransform))
239
0
    {
240
0
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot invert geotransform");
241
0
        return false;
242
0
    }
243
0
    return true;
244
0
}
245
246
/// Shrink the extent of a window to just cover the slice defined by rays from
247
/// (nX, nY) and [startAngle, endAngle]
248
///
249
/// @param oOutExtent  Window to modify
250
/// @param nX  X coordinate of ray endpoint.
251
/// @param nY  Y coordinate of ray endpoint.
252
/// @param startAngle  Start angle of slice (standard mathematics notion, in radians)
253
/// @param endAngle  End angle of slice (standard mathematics notion, in radians)
254
void shrinkWindowForAngles(Window &oOutExtent, int nX, int nY,
255
                           double startAngle, double endAngle)
256
0
{
257
    /// NOTE: This probably doesn't work when the observer is outside the raster and
258
    ///   needs to be enhanced for that case.
259
260
0
    if (startAngle == endAngle)
261
0
        return;
262
263
0
    Window win = oOutExtent;
264
265
    // Set the X boundaries for the angles
266
0
    int startAngleX = hIntersect(startAngle, nX, nY, win);
267
0
    int stopAngleX = hIntersect(endAngle, nX, nY, win);
268
269
0
    int xmax = nX;
270
0
    if (!rayBetween(startAngle, endAngle, 0))
271
0
    {
272
0
        xmax = std::max(xmax, startAngleX);
273
0
        xmax = std::max(xmax, stopAngleX);
274
        // Add one to xmax since we want one past the stop. [start, stop)
275
0
        oOutExtent.xStop = std::min(oOutExtent.xStop, xmax + 1);
276
0
    }
277
278
0
    int xmin = nX;
279
0
    if (!rayBetween(startAngle, endAngle, M_PI))
280
0
    {
281
0
        xmin = std::min(xmin, startAngleX);
282
0
        xmin = std::min(xmin, stopAngleX);
283
0
        oOutExtent.xStart = std::max(oOutExtent.xStart, xmin);
284
0
    }
285
286
    // Set the Y boundaries for the angles
287
0
    int startAngleY = vIntersect(startAngle, nX, nY, win);
288
0
    int stopAngleY = vIntersect(endAngle, nX, nY, win);
289
290
0
    int ymin = nY;
291
0
    if (!rayBetween(startAngle, endAngle, M_PI / 2))
292
0
    {
293
0
        ymin = std::min(ymin, startAngleY);
294
0
        ymin = std::min(ymin, stopAngleY);
295
0
        oOutExtent.yStart = std::max(oOutExtent.yStart, ymin);
296
0
    }
297
0
    int ymax = nY;
298
0
    if (!rayBetween(startAngle, endAngle, 3 * M_PI / 2))
299
0
    {
300
0
        ymax = std::max(ymax, startAngleY);
301
0
        ymax = std::max(ymax, stopAngleY);
302
        // Add one to ymax since we want one past the stop. [start, stop)
303
0
        oOutExtent.yStop = std::min(oOutExtent.yStop, ymax + 1);
304
0
    }
305
0
}
306
307
}  // unnamed namespace
308
309
0
Viewshed::Viewshed(const Options &opts) : oOpts{opts}
310
0
{
311
0
}
312
313
0
Viewshed::~Viewshed() = default;
314
315
/// Calculate the extent of the output raster in terms of the input raster and
316
/// save the input raster extent.
317
///
318
/// @return  false on error, true otherwise
319
bool Viewshed::calcExtents(int nX, int nY, const GDALGeoTransform &invGT)
320
0
{
321
    // We start with the assumption that the output size matches the input.
322
0
    oOutExtent.xStop = GDALGetRasterBandXSize(pSrcBand);
323
0
    oOutExtent.yStop = GDALGetRasterBandYSize(pSrcBand);
324
325
0
    if (!oOutExtent.contains(nX, nY))
326
0
    {
327
0
        if (oOpts.startAngle != oOpts.endAngle)
328
0
        {
329
0
            CPLError(CE_Failure, CPLE_AppDefined,
330
0
                     "Angle masking is not supported with an out-of-raster "
331
0
                     "observer.");
332
0
            return false;
333
0
        }
334
0
        CPLError(CE_Warning, CPLE_AppDefined,
335
0
                 "NOTE: The observer location falls outside of the DEM area");
336
0
    }
337
338
0
    constexpr double EPSILON = 1e-8;
339
0
    if (oOpts.maxDistance > 0)
340
0
    {
341
        //ABELL - This assumes that the transformation is only a scaling. Should be fixed.
342
        //  Find the distance in the direction of the transformed unit vector in the X and Y
343
        //  directions and use those factors to determine the limiting values in the raster space.
344
0
        int nXStart = static_cast<int>(
345
0
            std::floor(nX - invGT[1] * oOpts.maxDistance + EPSILON));
346
0
        int nXStop = static_cast<int>(
347
0
            std::ceil(nX + invGT[1] * oOpts.maxDistance - EPSILON) + 1);
348
        //ABELL - These seem to be wrong. The transform of 1 is no transform, so not
349
        //  sure why we're adding one in the first case. Really, the transformed distance
350
        // should add EPSILON. Not sure what the change should be for a negative transform,
351
        // which is what I think is being handled with the 1/0 addition/subtraction.
352
0
        int nYStart =
353
0
            static_cast<int>(std::floor(
354
0
                nY - std::fabs(invGT[5]) * oOpts.maxDistance + EPSILON)) -
355
0
            (invGT[5] > 0 ? 1 : 0);
356
0
        int nYStop = static_cast<int>(
357
0
            std::ceil(nY + std::fabs(invGT[5]) * oOpts.maxDistance - EPSILON) +
358
0
            (invGT[5] < 0 ? 1 : 0));
359
360
        // If the limits are invalid, set the window size to zero to trigger the error below.
361
0
        if (nXStart >= oOutExtent.xStop || nXStop < 0 ||
362
0
            nYStart >= oOutExtent.yStop || nYStop < 0)
363
0
        {
364
0
            oOutExtent = Window();
365
0
        }
366
0
        else
367
0
        {
368
0
            oOutExtent.xStart = std::max(nXStart, 0);
369
0
            oOutExtent.xStop = std::min(nXStop, oOutExtent.xStop);
370
371
0
            oOutExtent.yStart = std::max(nYStart, 0);
372
0
            oOutExtent.yStop = std::min(nYStop, oOutExtent.yStop);
373
0
        }
374
0
    }
375
376
0
    if (oOutExtent.xSize() == 0 || oOutExtent.ySize() == 0)
377
0
    {
378
0
        CPLError(CE_Failure, CPLE_AppDefined,
379
0
                 "Invalid target raster size due to transform "
380
0
                 "and/or distance limitation.");
381
0
        return false;
382
0
    }
383
384
0
    shrinkWindowForAngles(oOutExtent, nX, nY, oOpts.startAngle, oOpts.endAngle);
385
386
    // normalize horizontal index to [ 0, oOutExtent.xSize() )
387
0
    oCurExtent = oOutExtent;
388
0
    oCurExtent.shiftX(-oOutExtent.xStart);
389
390
0
    return true;
391
0
}
392
393
/// Compute the viewshed of a raster band.
394
///
395
/// @param band  Pointer to the raster band to be processed.
396
/// @param pfnProgress  Pointer to the progress function. Can be null.
397
/// @param pProgressArg  Argument passed to the progress function
398
/// @return  True on success, false otherwise.
399
bool Viewshed::run(GDALRasterBandH band, GDALProgressFunc pfnProgress,
400
                   void *pProgressArg)
401
0
{
402
0
    return run(band, nullptr, pfnProgress, pProgressArg);
403
0
}
404
405
/// Compute the viewshed of a raster band with .
406
///
407
/// @param band  Pointer to the raster band to be processed.
408
/// @param sdBand  Pointer to the standard deviation (SD) raster band to be processed.
409
/// @param pfnProgress  Pointer to the progress function. Can be null.
410
/// @param pProgressArg  Argument passed to the progress function
411
/// @return  True on success, false otherwise.
412
bool Viewshed::run(GDALRasterBandH band, GDALRasterBandH sdBand,
413
                   GDALProgressFunc pfnProgress, void *pProgressArg)
414
0
{
415
0
    if (sdBand)
416
0
        pSdBand = static_cast<GDALRasterBand *>(sdBand);
417
0
    pSrcBand = static_cast<GDALRasterBand *>(band);
418
419
0
    GDALGeoTransform fwdTransform, invTransform;
420
0
    if (!getTransforms(*pSrcBand, fwdTransform, invTransform))
421
0
        return false;
422
423
    // calculate observer position
424
0
    double dfX, dfY;
425
0
    invTransform.Apply(oOpts.observer.x, oOpts.observer.y, &dfX, &dfY);
426
0
    if (!GDALIsValueInRange<int>(dfX))
427
0
    {
428
0
        CPLError(CE_Failure, CPLE_AppDefined, "Observer X value out of range");
429
0
        return false;
430
0
    }
431
0
    if (!GDALIsValueInRange<int>(dfY))
432
0
    {
433
0
        CPLError(CE_Failure, CPLE_AppDefined, "Observer Y value out of range");
434
0
        return false;
435
0
    }
436
0
    int nX = static_cast<int>(dfX);
437
0
    int nY = static_cast<int>(dfY);
438
439
0
    if (oOpts.startAngle < 0 || oOpts.startAngle >= 360)
440
0
    {
441
0
        CPLError(CE_Failure, CPLE_AppDefined,
442
0
                 "Start angle out of range. Must be [0, 360).");
443
0
        return false;
444
0
    }
445
0
    if (oOpts.endAngle < 0 || oOpts.endAngle >= 360)
446
0
    {
447
0
        CPLError(CE_Failure, CPLE_AppDefined,
448
0
                 "End angle out of range. Must be [0, 360).");
449
0
        return false;
450
0
    }
451
0
    if (oOpts.highPitch > 90)
452
0
    {
453
0
        CPLError(CE_Failure, CPLE_AppDefined,
454
0
                 "Invalid highPitch. Cannot be greater than 90.");
455
0
        return false;
456
0
    }
457
0
    if (oOpts.lowPitch < -90)
458
0
    {
459
0
        CPLError(CE_Failure, CPLE_AppDefined,
460
0
                 "Invalid lowPitch. Cannot be less than -90.");
461
0
        return false;
462
0
    }
463
0
    if (oOpts.highPitch <= oOpts.lowPitch)
464
0
    {
465
0
        CPLError(CE_Failure, CPLE_AppDefined,
466
0
                 "Invalid pitch. highPitch must be > lowPitch");
467
0
        return false;
468
0
    }
469
470
    // Normalize angle to radians and standard math arrangement.
471
0
    oOpts.startAngle = normalizeAngle(oOpts.startAngle);
472
0
    oOpts.endAngle = normalizeAngle(oOpts.endAngle);
473
474
    // Must calculate extents in order to make the output dataset.
475
0
    if (!calcExtents(nX, nY, invTransform))
476
0
        return false;
477
478
0
    poDstDS = createOutputDataset(*pSrcBand, oOpts, oOutExtent);
479
0
    if (!poDstDS)
480
0
        return false;
481
482
    // Create the progress reporter.
483
0
    Progress oProgress(pfnProgress, pProgressArg, oOutExtent.ySize());
484
485
    // Execute the viewshed algorithm.
486
0
    GDALRasterBand *pDstBand = poDstDS->GetRasterBand(1);
487
0
    if (pSdBand)
488
0
    {
489
0
        ViewshedExecutor executor(*pSrcBand, *pSdBand, *pDstBand, nX, nY,
490
0
                                  oOutExtent, oCurExtent, oOpts, oProgress,
491
0
                                  /* emitWarningIfNoData = */ true);
492
0
        executor.run();
493
0
    }
494
0
    else
495
0
    {
496
0
        ViewshedExecutor executor(*pSrcBand, *pDstBand, nX, nY, oOutExtent,
497
0
                                  oCurExtent, oOpts, oProgress,
498
0
                                  /* emitWarningIfNoData = */ true);
499
0
        executor.run();
500
0
    }
501
0
    oProgress.emit(1);
502
0
    return static_cast<bool>(poDstDS);
503
0
}
504
505
// Adjust the coefficient of curvature for non-earth SRS.
506
/// \param curveCoeff  Current curve coefficient
507
/// \param hSrcDS  Source dataset
508
/// \return  Adjusted curve coefficient.
509
double adjustCurveCoeff(double curveCoeff, GDALDatasetH hSrcDS)
510
0
{
511
0
    const OGRSpatialReference *poSRS =
512
0
        GDALDataset::FromHandle(hSrcDS)->GetSpatialRef();
513
0
    if (poSRS)
514
0
    {
515
0
        OGRErr eSRSerr;
516
0
        const double dfSemiMajor = poSRS->GetSemiMajor(&eSRSerr);
517
0
        if (eSRSerr != OGRERR_FAILURE &&
518
0
            fabs(dfSemiMajor - SRS_WGS84_SEMIMAJOR) >
519
0
                0.05 * SRS_WGS84_SEMIMAJOR)
520
0
        {
521
0
            curveCoeff = 1.0;
522
0
            CPLDebug("gdal_viewshed",
523
0
                     "Using -cc=1.0 as a non-Earth CRS has been detected");
524
0
        }
525
0
    }
526
0
    return curveCoeff;
527
0
}
528
529
#if defined(__clang__) || defined(__GNUC__)
530
#pragma GCC diagnostic ignored "-Wmissing-declarations"
531
#endif
532
533
void testShrinkWindowForAngles(Window &oOutExtent, int nX, int nY,
534
                               double startAngle, double endAngle)
535
0
{
536
0
    shrinkWindowForAngles(oOutExtent, nX, nY, startAngle, endAngle);
537
0
}
538
539
}  // namespace viewshed
540
}  // namespace gdal