LCOV - code coverage report
Current view: top level - apps - gdalalg_raster_viewshed.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 123 135 91.1 %
Date: 2026-02-01 11:59:10 Functions: 2 2 100.0 %

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

Generated by: LCOV version 1.14