/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("output-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 | .AddHiddenAlias("dst-nodata") |
141 | 0 | .SetMinValueIncluded(0) |
142 | 0 | .SetMaxValueIncluded(255); |
143 | 0 | AddArg("observer-spacing", 0, _("Cell Spacing between observers"), |
144 | 0 | &m_opts.observerSpacing) |
145 | 0 | .SetDefault(m_opts.observerSpacing) |
146 | 0 | .SetMinValueIncluded(1); |
147 | |
|
148 | 0 | m_numThreadsStr = std::to_string(m_numThreads); |
149 | 0 | AddNumThreadsArg(&m_numThreads, &m_numThreadsStr); |
150 | 0 | } |
151 | | |
152 | | /************************************************************************/ |
153 | | /* GDALRasterViewshedAlgorithm::RunStep() */ |
154 | | /************************************************************************/ |
155 | | |
156 | | bool GDALRasterViewshedAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt) |
157 | 0 | { |
158 | 0 | auto pfnProgress = ctxt.m_pfnProgress; |
159 | 0 | auto pProgressData = ctxt.m_pProgressData; |
160 | 0 | auto poSrcDS = m_inputDataset[0].GetDatasetRef(); |
161 | 0 | CPLAssert(poSrcDS); |
162 | 0 | CPLAssert(!m_outputDataset.GetDatasetRef()); |
163 | | |
164 | 0 | GDALRasterBandH sdBand = nullptr; |
165 | 0 | if (auto sdDataset = m_sdFilename.GetDatasetRef()) |
166 | 0 | { |
167 | 0 | if (sdDataset->GetRasterCount() == 0) |
168 | 0 | { |
169 | 0 | ReportError( |
170 | 0 | CE_Failure, CPLE_AppDefined, |
171 | 0 | "The standard deviation dataset must have one raster band"); |
172 | 0 | return false; |
173 | 0 | } |
174 | 0 | sdBand = GDALRasterBand::FromHandle(sdDataset->GetRasterBand(1)); |
175 | 0 | } |
176 | | |
177 | 0 | if (GetArg("height")->IsExplicitlySet()) |
178 | 0 | { |
179 | 0 | if (m_observerPos.size() == 3) |
180 | 0 | { |
181 | 0 | ReportError(CE_Failure, CPLE_AppDefined, |
182 | 0 | "Height can't be specified in both 'position' and " |
183 | 0 | "'height' arguments"); |
184 | 0 | return false; |
185 | 0 | } |
186 | 0 | } |
187 | | |
188 | 0 | if (m_observerPos.size()) |
189 | 0 | { |
190 | 0 | m_opts.observer.x = m_observerPos[0]; |
191 | 0 | m_opts.observer.y = m_observerPos[1]; |
192 | 0 | if (m_observerPos.size() == 3) |
193 | 0 | m_opts.observer.z = m_observerPos[2]; |
194 | 0 | else |
195 | 0 | m_opts.observer.z = 2; |
196 | 0 | } |
197 | |
|
198 | 0 | if (!GetArg("curvature-coefficient")->IsExplicitlySet()) |
199 | 0 | { |
200 | 0 | m_opts.curveCoeff = gdal::viewshed::adjustCurveCoeff( |
201 | 0 | m_opts.curveCoeff, GDALDataset::ToHandle(poSrcDS)); |
202 | 0 | } |
203 | |
|
204 | 0 | if (m_outputMode == "normal") |
205 | 0 | m_opts.outputMode = gdal::viewshed::OutputMode::Normal; |
206 | 0 | else if (m_outputMode == "DEM") |
207 | 0 | m_opts.outputMode = gdal::viewshed::OutputMode::DEM; |
208 | 0 | else if (m_outputMode == "ground") |
209 | 0 | m_opts.outputMode = gdal::viewshed::OutputMode::Ground; |
210 | 0 | else if (m_outputMode == "cumulative") |
211 | 0 | m_opts.outputMode = gdal::viewshed::OutputMode::Cumulative; |
212 | |
|
213 | 0 | m_opts.numJobs = static_cast<uint8_t>(std::clamp(m_numThreads, 0, 255)); |
214 | |
|
215 | 0 | m_opts.outputFilename = |
216 | 0 | CPLGenerateTempFilenameSafe( |
217 | 0 | CPLGetBasenameSafe(poSrcDS->GetDescription()).c_str()) + |
218 | 0 | ".tif"; |
219 | 0 | m_opts.outputFormat = "GTiff"; |
220 | |
|
221 | 0 | if (m_opts.outputMode == gdal::viewshed::OutputMode::Cumulative) |
222 | 0 | { |
223 | 0 | static const std::vector<std::string> badArgs{ |
224 | 0 | "visible-value", "invisible-value", "max-distance", |
225 | 0 | "min-distance", "start-angle", "end-angle", |
226 | 0 | "low-pitch", "high-pitch", "position"}; |
227 | |
|
228 | 0 | for (const auto &arg : badArgs) |
229 | 0 | if (GetArg(arg)->IsExplicitlySet()) |
230 | 0 | { |
231 | 0 | std::string err = |
232 | 0 | "Option '" + arg + "' can't be used in cumulative mode."; |
233 | 0 | ReportError(CE_Failure, CPLE_AppDefined, "%s", err.c_str()); |
234 | 0 | return false; |
235 | 0 | } |
236 | | |
237 | 0 | auto poSrcDriver = poSrcDS->GetDriver(); |
238 | 0 | if (EQUAL(poSrcDS->GetDescription(), "") || !poSrcDriver || |
239 | 0 | EQUAL(poSrcDriver->GetDescription(), "MEM")) |
240 | 0 | { |
241 | 0 | ReportError( |
242 | 0 | CE_Failure, CPLE_AppDefined, |
243 | 0 | "In cumulative mode, the input dataset must be opened by name"); |
244 | 0 | return false; |
245 | 0 | } |
246 | 0 | gdal::viewshed::Cumulative oViewshed(m_opts); |
247 | 0 | const bool bSuccess = oViewshed.run( |
248 | 0 | m_inputDataset[0].GetName().c_str(), |
249 | 0 | pfnProgress ? pfnProgress : GDALDummyProgress, pProgressData); |
250 | 0 | if (bSuccess) |
251 | 0 | { |
252 | 0 | m_outputDataset.Set(std::unique_ptr<GDALDataset>( |
253 | 0 | GDALDataset::Open(m_opts.outputFilename.c_str(), |
254 | 0 | GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR, |
255 | 0 | nullptr, nullptr, nullptr))); |
256 | 0 | } |
257 | 0 | } |
258 | 0 | else |
259 | 0 | { |
260 | 0 | static const std::vector<std::string> badArgs{ |
261 | 0 | "observer-spacing", GDAL_ARG_NAME_NUM_THREADS}; |
262 | 0 | for (const auto &arg : badArgs) |
263 | 0 | if (GetArg(arg)->IsExplicitlySet()) |
264 | 0 | { |
265 | 0 | std::string err = |
266 | 0 | "Option '" + arg + "' can't be used in standard mode."; |
267 | 0 | ReportError(CE_Failure, CPLE_AppDefined, "%s", err.c_str()); |
268 | 0 | return false; |
269 | 0 | } |
270 | 0 | static const std::vector<std::string> goodArgs{"position"}; |
271 | 0 | for (const auto &arg : goodArgs) |
272 | 0 | if (!GetArg(arg)->IsExplicitlySet()) |
273 | 0 | { |
274 | 0 | std::string err = |
275 | 0 | "Option '" + arg + "' must be specified in standard mode."; |
276 | 0 | ReportError(CE_Failure, CPLE_AppDefined, "%s", err.c_str()); |
277 | 0 | return false; |
278 | 0 | } |
279 | | |
280 | 0 | gdal::viewshed::Viewshed oViewshed(m_opts); |
281 | 0 | const bool bSuccess = oViewshed.run( |
282 | 0 | GDALRasterBand::ToHandle(poSrcDS->GetRasterBand(m_band)), sdBand, |
283 | 0 | pfnProgress ? pfnProgress : GDALDummyProgress, pProgressData); |
284 | 0 | if (bSuccess) |
285 | 0 | { |
286 | 0 | m_outputDataset.Set(oViewshed.output()); |
287 | 0 | } |
288 | 0 | } |
289 | | |
290 | 0 | auto poOutDS = m_outputDataset.GetDatasetRef(); |
291 | 0 | if (poOutDS && poOutDS->GetDescription()[0]) |
292 | 0 | { |
293 | | // In file systems that allow it (all but Windows...), we want to |
294 | | // delete the temporary file as soon as soon as possible after |
295 | | // having open it, so that if someone kills the process there are |
296 | | // no temp files left over. If that unlink() doesn't succeed |
297 | | // (on Windows), then the file will eventually be deleted when |
298 | | // poTmpDS is cleaned due to MarkSuppressOnClose(). |
299 | 0 | VSIUnlink(poOutDS->GetDescription()); |
300 | 0 | poOutDS->MarkSuppressOnClose(); |
301 | 0 | } |
302 | |
|
303 | 0 | return poOutDS != nullptr; |
304 | 0 | } |
305 | | |
306 | | GDALRasterViewshedAlgorithmStandalone:: |
307 | 0 | ~GDALRasterViewshedAlgorithmStandalone() = default; |
308 | | |
309 | | //! @endcond |