Line data Source code
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 0 : 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 : {
134 : 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 : {
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 : }
163 :
164 0 : switch (heightMode)
165 : {
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 : }
176 :
177 0 : if (!GDALIsValueInRange<uint8_t>(dfVisibleVal))
178 : {
179 0 : CPLError(CE_Failure, CPLE_AppDefined,
180 : "dfVisibleVal out of range. Must be [0, 255].");
181 0 : return nullptr;
182 : }
183 0 : if (!GDALIsValueInRange<uint8_t>(dfInvisibleVal))
184 : {
185 0 : CPLError(CE_Failure, CPLE_AppDefined,
186 : "dfInvisibleVal out of range. Must be [0, 255].");
187 0 : return nullptr;
188 : }
189 0 : if (oOpts.outputMode == viewshed::OutputMode::Normal)
190 : {
191 0 : if (!GDALIsValueInRange<uint8_t>(dfOutOfRangeVal))
192 : {
193 0 : CPLError(CE_Failure, CPLE_AppDefined,
194 : "dfOutOfRangeVal out of range. Must be [0, 255].");
195 0 : return nullptr;
196 : }
197 : }
198 0 : oOpts.visibleVal = dfVisibleVal;
199 0 : oOpts.invisibleVal = dfInvisibleVal;
200 0 : oOpts.outOfRangeVal = dfOutOfRangeVal;
201 :
202 : 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 : 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 : }
228 :
229 : namespace gdal
230 : {
231 : namespace viewshed
232 : {
233 :
234 : namespace
235 : {
236 :
237 49 : bool getTransforms(GDALRasterBand &band, GDALGeoTransform &fwdTransform,
238 : GDALGeoTransform &revTransform)
239 : {
240 49 : band.GetDataset()->GetGeoTransform(fwdTransform);
241 49 : if (!fwdTransform.GetInverse(revTransform))
242 : {
243 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot invert geotransform");
244 0 : return false;
245 : }
246 49 : return true;
247 : }
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 62 : void shrinkWindowForAngles(Window &oOutExtent, int nX, int nY,
258 : double startAngle, double endAngle)
259 : {
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 62 : if (startAngle == endAngle)
264 47 : return;
265 :
266 15 : Window win = oOutExtent;
267 :
268 : // Set the X boundaries for the angles
269 15 : int startAngleX = hIntersect(startAngle, nX, nY, win);
270 15 : int stopAngleX = hIntersect(endAngle, nX, nY, win);
271 :
272 15 : int xmax = nX;
273 15 : if (!rayBetween(startAngle, endAngle, 0))
274 : {
275 7 : xmax = std::max(xmax, startAngleX);
276 7 : xmax = std::max(xmax, stopAngleX);
277 : // Add one to xmax since we want one past the stop. [start, stop)
278 7 : oOutExtent.xStop = std::min(oOutExtent.xStop, xmax + 1);
279 : }
280 :
281 15 : int xmin = nX;
282 15 : if (!rayBetween(startAngle, endAngle, M_PI))
283 : {
284 8 : xmin = std::min(xmin, startAngleX);
285 8 : xmin = std::min(xmin, stopAngleX);
286 8 : oOutExtent.xStart = std::max(oOutExtent.xStart, xmin);
287 : }
288 :
289 : // Set the Y boundaries for the angles
290 15 : int startAngleY = vIntersect(startAngle, nX, nY, win);
291 15 : int stopAngleY = vIntersect(endAngle, nX, nY, win);
292 :
293 15 : int ymin = nY;
294 15 : if (!rayBetween(startAngle, endAngle, M_PI / 2))
295 : {
296 7 : ymin = std::min(ymin, startAngleY);
297 7 : ymin = std::min(ymin, stopAngleY);
298 7 : oOutExtent.yStart = std::max(oOutExtent.yStart, ymin);
299 : }
300 15 : int ymax = nY;
301 15 : if (!rayBetween(startAngle, endAngle, 3 * M_PI / 2))
302 : {
303 8 : ymax = std::max(ymax, startAngleY);
304 8 : ymax = std::max(ymax, stopAngleY);
305 : // Add one to ymax since we want one past the stop. [start, stop)
306 8 : oOutExtent.yStop = std::min(oOutExtent.yStop, ymax + 1);
307 : }
308 : }
309 :
310 : } // unnamed namespace
311 :
312 49 : Viewshed::Viewshed(const Options &opts) : oOpts{opts}
313 : {
314 49 : }
315 :
316 : 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 49 : bool Viewshed::calcExtents(int nX, int nY, const GDALGeoTransform &invGT)
323 : {
324 : // We start with the assumption that the output size matches the input.
325 49 : oOutExtent.xStop = GDALGetRasterBandXSize(pSrcBand);
326 49 : oOutExtent.yStop = GDALGetRasterBandYSize(pSrcBand);
327 :
328 49 : if (!oOutExtent.contains(nX, nY))
329 : {
330 8 : if (oOpts.startAngle != oOpts.endAngle)
331 : {
332 0 : CPLError(CE_Failure, CPLE_AppDefined,
333 : "Angle masking is not supported with an out-of-raster "
334 : "observer.");
335 0 : return false;
336 : }
337 8 : CPLError(CE_Warning, CPLE_AppDefined,
338 : "NOTE: The observer location falls outside of the DEM area");
339 : }
340 :
341 49 : constexpr double EPSILON = 1e-8;
342 49 : if (oOpts.maxDistance > 0)
343 : {
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 4 : int nXStart = static_cast<int>(
348 4 : std::floor(nX - invGT[1] * oOpts.maxDistance + EPSILON));
349 4 : int nXStop = static_cast<int>(
350 4 : 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 : int nYStart =
356 4 : static_cast<int>(std::floor(
357 4 : nY - std::fabs(invGT[5]) * oOpts.maxDistance + EPSILON)) -
358 4 : (invGT[5] > 0 ? 1 : 0);
359 4 : int nYStop = static_cast<int>(
360 8 : std::ceil(nY + std::fabs(invGT[5]) * oOpts.maxDistance - EPSILON) +
361 4 : (invGT[5] < 0 ? 1 : 0));
362 :
363 : // If the limits are invalid, set the window size to zero to trigger the error below.
364 4 : if (nXStart >= oOutExtent.xStop || nXStop < 0 ||
365 4 : nYStart >= oOutExtent.yStop || nYStop < 0)
366 : {
367 0 : oOutExtent = Window();
368 : }
369 : else
370 : {
371 4 : oOutExtent.xStart = std::max(nXStart, 0);
372 4 : oOutExtent.xStop = std::min(nXStop, oOutExtent.xStop);
373 :
374 4 : oOutExtent.yStart = std::max(nYStart, 0);
375 4 : oOutExtent.yStop = std::min(nYStop, oOutExtent.yStop);
376 : }
377 : }
378 :
379 49 : if (oOutExtent.xSize() == 0 || oOutExtent.ySize() == 0)
380 : {
381 0 : CPLError(CE_Failure, CPLE_AppDefined,
382 : "Invalid target raster size due to transform "
383 : "and/or distance limitation.");
384 0 : return false;
385 : }
386 :
387 49 : shrinkWindowForAngles(oOutExtent, nX, nY, oOpts.startAngle, oOpts.endAngle);
388 :
389 : // normalize horizontal index to [ 0, oOutExtent.xSize() )
390 49 : oCurExtent = oOutExtent;
391 49 : oCurExtent.shiftX(-oOutExtent.xStart);
392 :
393 49 : return true;
394 : }
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 27 : bool Viewshed::run(GDALRasterBandH band, GDALProgressFunc pfnProgress,
403 : void *pProgressArg)
404 : {
405 27 : return run(band, nullptr, pfnProgress, pProgressArg);
406 : }
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 49 : bool Viewshed::run(GDALRasterBandH band, GDALRasterBandH sdBand,
416 : GDALProgressFunc pfnProgress, void *pProgressArg)
417 : {
418 49 : if (sdBand)
419 9 : pSdBand = static_cast<GDALRasterBand *>(sdBand);
420 49 : pSrcBand = static_cast<GDALRasterBand *>(band);
421 :
422 49 : GDALGeoTransform fwdTransform, invTransform;
423 49 : if (!getTransforms(*pSrcBand, fwdTransform, invTransform))
424 0 : return false;
425 :
426 : // calculate observer position
427 : double dfX, dfY;
428 49 : invTransform.Apply(oOpts.observer.x, oOpts.observer.y, &dfX, &dfY);
429 49 : if (!GDALIsValueInRange<int>(dfX))
430 : {
431 0 : CPLError(CE_Failure, CPLE_AppDefined, "Observer X value out of range");
432 0 : return false;
433 : }
434 49 : if (!GDALIsValueInRange<int>(dfY))
435 : {
436 0 : CPLError(CE_Failure, CPLE_AppDefined, "Observer Y value out of range");
437 0 : return false;
438 : }
439 49 : int nX = static_cast<int>(dfX);
440 49 : int nY = static_cast<int>(dfY);
441 :
442 49 : if (oOpts.startAngle < 0 || oOpts.startAngle >= 360)
443 : {
444 0 : CPLError(CE_Failure, CPLE_AppDefined,
445 : "Start angle out of range. Must be [0, 360).");
446 0 : return false;
447 : }
448 49 : if (oOpts.endAngle < 0 || oOpts.endAngle >= 360)
449 : {
450 0 : CPLError(CE_Failure, CPLE_AppDefined,
451 : "End angle out of range. Must be [0, 360).");
452 0 : return false;
453 : }
454 49 : if (oOpts.highPitch > 90)
455 : {
456 0 : CPLError(CE_Failure, CPLE_AppDefined,
457 : "Invalid highPitch. Cannot be greater than 90.");
458 0 : return false;
459 : }
460 49 : if (oOpts.lowPitch < -90)
461 : {
462 0 : CPLError(CE_Failure, CPLE_AppDefined,
463 : "Invalid lowPitch. Cannot be less than -90.");
464 0 : return false;
465 : }
466 49 : if (oOpts.highPitch <= oOpts.lowPitch)
467 : {
468 0 : CPLError(CE_Failure, CPLE_AppDefined,
469 : "Invalid pitch. highPitch must be > lowPitch");
470 0 : return false;
471 : }
472 :
473 : // Normalize angle to radians and standard math arrangement.
474 49 : oOpts.startAngle = normalizeAngle(oOpts.startAngle);
475 49 : oOpts.endAngle = normalizeAngle(oOpts.endAngle);
476 :
477 : // Must calculate extents in order to make the output dataset.
478 49 : if (!calcExtents(nX, nY, invTransform))
479 0 : return false;
480 :
481 49 : poDstDS = createOutputDataset(*pSrcBand, oOpts, oOutExtent);
482 49 : if (!poDstDS)
483 0 : return false;
484 :
485 : // Create the progress reporter.
486 98 : Progress oProgress(pfnProgress, pProgressArg, oOutExtent.ySize());
487 :
488 : // Execute the viewshed algorithm.
489 49 : GDALRasterBand *pDstBand = poDstDS->GetRasterBand(1);
490 49 : if (pSdBand)
491 : {
492 9 : ViewshedExecutor executor(*pSrcBand, *pSdBand, *pDstBand, nX, nY,
493 9 : oOutExtent, oCurExtent, oOpts, oProgress,
494 18 : /* emitWarningIfNoData = */ true);
495 9 : executor.run();
496 : }
497 : else
498 : {
499 40 : ViewshedExecutor executor(*pSrcBand, *pDstBand, nX, nY, oOutExtent,
500 40 : oCurExtent, oOpts, oProgress,
501 80 : /* emitWarningIfNoData = */ true);
502 40 : executor.run();
503 : }
504 49 : oProgress.emit(1);
505 49 : return static_cast<bool>(poDstDS);
506 : }
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 16 : double adjustCurveCoeff(double curveCoeff, GDALDatasetH hSrcDS)
513 : {
514 : const OGRSpatialReference *poSRS =
515 16 : GDALDataset::FromHandle(hSrcDS)->GetSpatialRef();
516 16 : if (poSRS)
517 : {
518 : OGRErr eSRSerr;
519 15 : const double dfSemiMajor = poSRS->GetSemiMajor(&eSRSerr);
520 15 : if (eSRSerr != OGRERR_FAILURE &&
521 15 : fabs(dfSemiMajor - SRS_WGS84_SEMIMAJOR) >
522 : 0.05 * SRS_WGS84_SEMIMAJOR)
523 : {
524 0 : curveCoeff = 1.0;
525 0 : CPLDebug("gdal_viewshed",
526 : "Using -cc=1.0 as a non-Earth CRS has been detected");
527 : }
528 : }
529 16 : return curveCoeff;
530 : }
531 :
532 : #if defined(__clang__) || defined(__GNUC__)
533 : #pragma GCC diagnostic ignored "-Wmissing-declarations"
534 : #endif
535 :
536 13 : void testShrinkWindowForAngles(Window &oOutExtent, int nX, int nY,
537 : double startAngle, double endAngle)
538 : {
539 13 : shrinkWindowForAngles(oOutExtent, nX, nY, startAngle, endAngle);
540 13 : }
541 :
542 : } // namespace viewshed
543 : } // namespace gdal
|