Coverage Report

Created: 2026-04-01 06:20

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 (oOpts.outputMode == viewshed::OutputMode::Normal)
190
0
    {
191
0
        if (!GDALIsValueInRange<uint8_t>(dfOutOfRangeVal))
192
0
        {
193
0
            CPLError(CE_Failure, CPLE_AppDefined,
194
0
                     "dfOutOfRangeVal out of range. Must be [0, 255].");
195
0
            return nullptr;
196
0
        }
197
0
    }
198
0
    oOpts.visibleVal = dfVisibleVal;
199
0
    oOpts.invisibleVal = dfInvisibleVal;
200
0
    oOpts.outOfRangeVal = dfOutOfRangeVal;
201
202
0
    const char *pszStartAngle =
203
0
        CSLFetchNameValue(papszExtraOptions, "START_ANGLE");
204
0
    if (pszStartAngle)
205
0
        oOpts.startAngle = CPLAtof(pszStartAngle);
206
207
0
    const char *pszEndAngle = CSLFetchNameValue(papszExtraOptions, "END_ANGLE");
208
0
    if (pszEndAngle)
209
0
        oOpts.endAngle = CPLAtof(pszEndAngle);
210
211
0
    const char *pszLowPitch = CSLFetchNameValue(papszExtraOptions, "LOW_PITCH");
212
0
    if (pszLowPitch)
213
0
        oOpts.lowPitch = CPLAtof(pszLowPitch);
214
215
0
    const char *pszHighPitch =
216
0
        CSLFetchNameValue(papszExtraOptions, "HIGH_PITCH");
217
0
    if (pszHighPitch)
218
0
        oOpts.highPitch = CPLAtof(pszHighPitch);
219
220
0
    gdal::viewshed::Viewshed v(oOpts);
221
222
0
    if (!pfnProgress)
223
0
        pfnProgress = GDALDummyProgress;
224
0
    v.run(hBand, pfnProgress, pProgressArg);
225
226
0
    return GDALDataset::FromHandle(v.output().release());
227
0
}
228
229
namespace gdal
230
{
231
namespace viewshed
232
{
233
234
namespace
235
{
236
237
bool getTransforms(GDALRasterBand &band, GDALGeoTransform &fwdTransform,
238
                   GDALGeoTransform &revTransform)
239
0
{
240
0
    band.GetDataset()->GetGeoTransform(fwdTransform);
241
0
    if (!fwdTransform.GetInverse(revTransform))
242
0
    {
243
0
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot invert geotransform");
244
0
        return false;
245
0
    }
246
0
    return true;
247
0
}
248
249
/// Shrink the extent of a window to just cover the slice defined by rays from
250
/// (nX, nY) and [startAngle, endAngle]
251
///
252
/// @param oOutExtent  Window to modify
253
/// @param nX  X coordinate of ray endpoint.
254
/// @param nY  Y coordinate of ray endpoint.
255
/// @param startAngle  Start angle of slice (standard mathematics notion, in radians)
256
/// @param endAngle  End angle of slice (standard mathematics notion, in radians)
257
void shrinkWindowForAngles(Window &oOutExtent, int nX, int nY,
258
                           double startAngle, double endAngle)
259
0
{
260
    /// NOTE: This probably doesn't work when the observer is outside the raster and
261
    ///   needs to be enhanced for that case.
262
263
0
    if (startAngle == endAngle)
264
0
        return;
265
266
0
    Window win = oOutExtent;
267
268
    // Set the X boundaries for the angles
269
0
    int startAngleX = hIntersect(startAngle, nX, nY, win);
270
0
    int stopAngleX = hIntersect(endAngle, nX, nY, win);
271
272
0
    int xmax = nX;
273
0
    if (!rayBetween(startAngle, endAngle, 0))
274
0
    {
275
0
        xmax = std::max(xmax, startAngleX);
276
0
        xmax = std::max(xmax, stopAngleX);
277
        // Add one to xmax since we want one past the stop. [start, stop)
278
0
        oOutExtent.xStop = std::min(oOutExtent.xStop, xmax + 1);
279
0
    }
280
281
0
    int xmin = nX;
282
0
    if (!rayBetween(startAngle, endAngle, M_PI))
283
0
    {
284
0
        xmin = std::min(xmin, startAngleX);
285
0
        xmin = std::min(xmin, stopAngleX);
286
0
        oOutExtent.xStart = std::max(oOutExtent.xStart, xmin);
287
0
    }
288
289
    // Set the Y boundaries for the angles
290
0
    int startAngleY = vIntersect(startAngle, nX, nY, win);
291
0
    int stopAngleY = vIntersect(endAngle, nX, nY, win);
292
293
0
    int ymin = nY;
294
0
    if (!rayBetween(startAngle, endAngle, M_PI / 2))
295
0
    {
296
0
        ymin = std::min(ymin, startAngleY);
297
0
        ymin = std::min(ymin, stopAngleY);
298
0
        oOutExtent.yStart = std::max(oOutExtent.yStart, ymin);
299
0
    }
300
0
    int ymax = nY;
301
0
    if (!rayBetween(startAngle, endAngle, 3 * M_PI / 2))
302
0
    {
303
0
        ymax = std::max(ymax, startAngleY);
304
0
        ymax = std::max(ymax, stopAngleY);
305
        // Add one to ymax since we want one past the stop. [start, stop)
306
0
        oOutExtent.yStop = std::min(oOutExtent.yStop, ymax + 1);
307
0
    }
308
0
}
309
310
}  // unnamed namespace
311
312
0
Viewshed::Viewshed(const Options &opts) : oOpts{opts}
313
0
{
314
0
}
315
316
0
Viewshed::~Viewshed() = default;
317
318
/// Calculate the extent of the output raster in terms of the input raster and
319
/// save the input raster extent.
320
///
321
/// @return  false on error, true otherwise
322
bool Viewshed::calcExtents(int nX, int nY, const GDALGeoTransform &invGT)
323
0
{
324
    // We start with the assumption that the output size matches the input.
325
0
    oOutExtent.xStop = GDALGetRasterBandXSize(pSrcBand);
326
0
    oOutExtent.yStop = GDALGetRasterBandYSize(pSrcBand);
327
328
0
    if (!oOutExtent.contains(nX, nY))
329
0
    {
330
0
        if (oOpts.startAngle != oOpts.endAngle)
331
0
        {
332
0
            CPLError(CE_Failure, CPLE_AppDefined,
333
0
                     "Angle masking is not supported with an out-of-raster "
334
0
                     "observer.");
335
0
            return false;
336
0
        }
337
0
        CPLError(CE_Warning, CPLE_AppDefined,
338
0
                 "NOTE: The observer location falls outside of the DEM area");
339
0
    }
340
341
0
    constexpr double EPSILON = 1e-8;
342
0
    if (oOpts.maxDistance > 0)
343
0
    {
344
        //ABELL - This assumes that the transformation is only a scaling. Should be fixed.
345
        //  Find the distance in the direction of the transformed unit vector in the X and Y
346
        //  directions and use those factors to determine the limiting values in the raster space.
347
0
        int nXStart = static_cast<int>(
348
0
            std::floor(nX - invGT[1] * oOpts.maxDistance + EPSILON));
349
0
        int nXStop = static_cast<int>(
350
0
            std::ceil(nX + invGT[1] * oOpts.maxDistance - EPSILON) + 1);
351
        //ABELL - These seem to be wrong. The transform of 1 is no transform, so not
352
        //  sure why we're adding one in the first case. Really, the transformed distance
353
        // should add EPSILON. Not sure what the change should be for a negative transform,
354
        // which is what I think is being handled with the 1/0 addition/subtraction.
355
0
        int nYStart =
356
0
            static_cast<int>(std::floor(
357
0
                nY - std::fabs(invGT[5]) * oOpts.maxDistance + EPSILON)) -
358
0
            (invGT[5] > 0 ? 1 : 0);
359
0
        int nYStop = static_cast<int>(
360
0
            std::ceil(nY + std::fabs(invGT[5]) * oOpts.maxDistance - EPSILON) +
361
0
            (invGT[5] < 0 ? 1 : 0));
362
363
        // If the limits are invalid, set the window size to zero to trigger the error below.
364
0
        if (nXStart >= oOutExtent.xStop || nXStop < 0 ||
365
0
            nYStart >= oOutExtent.yStop || nYStop < 0)
366
0
        {
367
0
            oOutExtent = Window();
368
0
        }
369
0
        else
370
0
        {
371
0
            oOutExtent.xStart = std::max(nXStart, 0);
372
0
            oOutExtent.xStop = std::min(nXStop, oOutExtent.xStop);
373
374
0
            oOutExtent.yStart = std::max(nYStart, 0);
375
0
            oOutExtent.yStop = std::min(nYStop, oOutExtent.yStop);
376
0
        }
377
0
    }
378
379
0
    if (oOutExtent.xSize() == 0 || oOutExtent.ySize() == 0)
380
0
    {
381
0
        CPLError(CE_Failure, CPLE_AppDefined,
382
0
                 "Invalid target raster size due to transform "
383
0
                 "and/or distance limitation.");
384
0
        return false;
385
0
    }
386
387
0
    shrinkWindowForAngles(oOutExtent, nX, nY, oOpts.startAngle, oOpts.endAngle);
388
389
    // normalize horizontal index to [ 0, oOutExtent.xSize() )
390
0
    oCurExtent = oOutExtent;
391
0
    oCurExtent.shiftX(-oOutExtent.xStart);
392
393
0
    return true;
394
0
}
395
396
/// Compute the viewshed of a raster band.
397
///
398
/// @param band  Pointer to the raster band to be processed.
399
/// @param pfnProgress  Pointer to the progress function. Can be null.
400
/// @param pProgressArg  Argument passed to the progress function
401
/// @return  True on success, false otherwise.
402
bool Viewshed::run(GDALRasterBandH band, GDALProgressFunc pfnProgress,
403
                   void *pProgressArg)
404
0
{
405
0
    return run(band, nullptr, pfnProgress, pProgressArg);
406
0
}
407
408
/// Compute the viewshed of a raster band with .
409
///
410
/// @param band  Pointer to the raster band to be processed.
411
/// @param sdBand  Pointer to the standard deviation (SD) raster band to be processed.
412
/// @param pfnProgress  Pointer to the progress function. Can be null.
413
/// @param pProgressArg  Argument passed to the progress function
414
/// @return  True on success, false otherwise.
415
bool Viewshed::run(GDALRasterBandH band, GDALRasterBandH sdBand,
416
                   GDALProgressFunc pfnProgress, void *pProgressArg)
417
0
{
418
0
    if (sdBand)
419
0
        pSdBand = static_cast<GDALRasterBand *>(sdBand);
420
0
    pSrcBand = static_cast<GDALRasterBand *>(band);
421
422
0
    GDALGeoTransform fwdTransform, invTransform;
423
0
    if (!getTransforms(*pSrcBand, fwdTransform, invTransform))
424
0
        return false;
425
426
    // calculate observer position
427
0
    double dfX, dfY;
428
0
    invTransform.Apply(oOpts.observer.x, oOpts.observer.y, &dfX, &dfY);
429
0
    if (!GDALIsValueInRange<int>(dfX))
430
0
    {
431
0
        CPLError(CE_Failure, CPLE_AppDefined, "Observer X value out of range");
432
0
        return false;
433
0
    }
434
0
    if (!GDALIsValueInRange<int>(dfY))
435
0
    {
436
0
        CPLError(CE_Failure, CPLE_AppDefined, "Observer Y value out of range");
437
0
        return false;
438
0
    }
439
0
    int nX = static_cast<int>(dfX);
440
0
    int nY = static_cast<int>(dfY);
441
442
0
    if (oOpts.startAngle < 0 || oOpts.startAngle >= 360)
443
0
    {
444
0
        CPLError(CE_Failure, CPLE_AppDefined,
445
0
                 "Start angle out of range. Must be [0, 360).");
446
0
        return false;
447
0
    }
448
0
    if (oOpts.endAngle < 0 || oOpts.endAngle >= 360)
449
0
    {
450
0
        CPLError(CE_Failure, CPLE_AppDefined,
451
0
                 "End angle out of range. Must be [0, 360).");
452
0
        return false;
453
0
    }
454
0
    if (oOpts.highPitch > 90)
455
0
    {
456
0
        CPLError(CE_Failure, CPLE_AppDefined,
457
0
                 "Invalid highPitch. Cannot be greater than 90.");
458
0
        return false;
459
0
    }
460
0
    if (oOpts.lowPitch < -90)
461
0
    {
462
0
        CPLError(CE_Failure, CPLE_AppDefined,
463
0
                 "Invalid lowPitch. Cannot be less than -90.");
464
0
        return false;
465
0
    }
466
0
    if (oOpts.highPitch <= oOpts.lowPitch)
467
0
    {
468
0
        CPLError(CE_Failure, CPLE_AppDefined,
469
0
                 "Invalid pitch. highPitch must be > lowPitch");
470
0
        return false;
471
0
    }
472
473
    // Normalize angle to radians and standard math arrangement.
474
0
    oOpts.startAngle = normalizeAngle(oOpts.startAngle);
475
0
    oOpts.endAngle = normalizeAngle(oOpts.endAngle);
476
477
    // Must calculate extents in order to make the output dataset.
478
0
    if (!calcExtents(nX, nY, invTransform))
479
0
        return false;
480
481
0
    poDstDS = createOutputDataset(*pSrcBand, oOpts, oOutExtent);
482
0
    if (!poDstDS)
483
0
        return false;
484
485
    // Create the progress reporter.
486
0
    Progress oProgress(pfnProgress, pProgressArg, oOutExtent.ySize());
487
488
    // Execute the viewshed algorithm.
489
0
    GDALRasterBand *pDstBand = poDstDS->GetRasterBand(1);
490
0
    if (pSdBand)
491
0
    {
492
0
        ViewshedExecutor executor(*pSrcBand, *pSdBand, *pDstBand, nX, nY,
493
0
                                  oOutExtent, oCurExtent, oOpts, oProgress,
494
0
                                  /* emitWarningIfNoData = */ true);
495
0
        executor.run();
496
0
    }
497
0
    else
498
0
    {
499
0
        ViewshedExecutor executor(*pSrcBand, *pDstBand, nX, nY, oOutExtent,
500
0
                                  oCurExtent, oOpts, oProgress,
501
0
                                  /* emitWarningIfNoData = */ true);
502
0
        executor.run();
503
0
    }
504
0
    oProgress.emit(1);
505
0
    return static_cast<bool>(poDstDS);
506
0
}
507
508
// Adjust the coefficient of curvature for non-earth SRS.
509
/// \param curveCoeff  Current curve coefficient
510
/// \param hSrcDS  Source dataset
511
/// \return  Adjusted curve coefficient.
512
double adjustCurveCoeff(double curveCoeff, GDALDatasetH hSrcDS)
513
0
{
514
0
    const OGRSpatialReference *poSRS =
515
0
        GDALDataset::FromHandle(hSrcDS)->GetSpatialRef();
516
0
    if (poSRS)
517
0
    {
518
0
        OGRErr eSRSerr;
519
0
        const double dfSemiMajor = poSRS->GetSemiMajor(&eSRSerr);
520
0
        if (eSRSerr != OGRERR_FAILURE &&
521
0
            fabs(dfSemiMajor - SRS_WGS84_SEMIMAJOR) >
522
0
                0.05 * SRS_WGS84_SEMIMAJOR)
523
0
        {
524
0
            curveCoeff = 1.0;
525
0
            CPLDebug("gdal_viewshed",
526
0
                     "Using -cc=1.0 as a non-Earth CRS has been detected");
527
0
        }
528
0
    }
529
0
    return curveCoeff;
530
0
}
531
532
#if defined(__clang__) || defined(__GNUC__)
533
#pragma GCC diagnostic ignored "-Wmissing-declarations"
534
#endif
535
536
void testShrinkWindowForAngles(Window &oOutExtent, int nX, int nY,
537
                               double startAngle, double endAngle)
538
0
{
539
0
    shrinkWindowForAngles(oOutExtent, nX, nY, startAngle, endAngle);
540
0
}
541
542
}  // namespace viewshed
543
}  // namespace gdal