LCOV - code coverage report
Current view: top level - alg - los.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 132 132 100.0 %
Date: 2024-05-04 12:52:34 Functions: 13 13 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  Line of Sight
       4             :  * Purpose:  Core algorithm implementation for line of sight algorithms.
       5             :  * Author:   Ryan Friedman, ryanfriedman5410+gdal@gmail.com
       6             :  *
       7             :  ******************************************************************************
       8             :  *
       9             :  * Permission is hereby granted, free of charge, to any person obtaining a
      10             :  * copy of this software and associated documentation files (the "Software"),
      11             :  * to deal in the Software without restriction, including without limitation
      12             :  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
      13             :  * and/or sell copies of the Software, and to permit persons to whom the
      14             :  * Software is furnished to do so, subject to the following conditions:
      15             :  *
      16             :  * The above copyright notice and this permission notice shall be included
      17             :  * in all copies or substantial portions of the Software.
      18             :  *
      19             :  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
      20             :  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      21             :  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
      22             :  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      23             :  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
      24             :  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
      25             :  * DEALINGS IN THE SOFTWARE.
      26             :  ****************************************************************************/
      27             : 
      28             : #include <functional>
      29             : #include <cmath>
      30             : 
      31             : #include "cpl_port.h"
      32             : #include "gdal_alg.h"
      33             : 
      34             : // There's a plethora of bresenham implementations, all questionable production quality.
      35             : // Bresenham optimizes for integer math, which makes sense for raster datasets in 2D.
      36             : // For 3D, a 3D bresenham could be used if the altitude is also integer resolution.
      37             : // 2D:
      38             : // https://codereview.stackexchange.com/questions/77460/bresenhams-line-algorithm-optimization
      39             : // https://gist.github.com/ssavi-ict/092501c69e2ffec65e96a8865470ad2f
      40             : // https://blog.demofox.org/2015/01/17/bresenhams-drawing-algorithms/
      41             : // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
      42             : // https://www.cs.helsinki.fi/group/goa/mallinnus/lines/bresenh.html
      43             : // https://stackoverflow.com/questions/10060046/drawing-lines-with-bresenhams-line-algorithm
      44             : // http://www.edepot.com/linebresenham.html
      45             : // 3D:
      46             : // https://gist.github.com/yamamushi/5823518
      47             : 
      48             : // Run bresenham terrain checking from (x1, y1) to (x2, y2).
      49             : // The callback is run at every point along the line,
      50             : // which should return True if the point is above terrain.
      51             : // Bresenham2D will return true if all points have LOS between the start and end.
      52             : static bool
      53           8 : Bresenham2D(const int x1, const int y1, const int x2, const int y2,
      54             :             std::function<auto(const int, const int)->bool> OnBresenhamPoint)
      55             : {
      56           8 :     bool isAboveTerrain = true;
      57             :     int dx, dy;
      58             :     int incx, incy;
      59             : 
      60           8 :     if (x2 >= x1)
      61             :     {
      62           7 :         dx = x2 - x1;
      63           7 :         incx = 1;
      64             :     }
      65             :     else
      66             :     {
      67           1 :         dx = x1 - x2;
      68           1 :         incx = -1;
      69             :     }
      70             : 
      71           8 :     if (y2 >= y1)
      72             :     {
      73           6 :         dy = y2 - y1;
      74           6 :         incy = 1;
      75             :     }
      76             :     else
      77             :     {
      78           2 :         dy = y1 - y2;
      79           2 :         incy = -1;
      80             :     }
      81             : 
      82           8 :     auto x = x1;
      83           8 :     auto y = y1;
      84             :     int balance;
      85             : 
      86           8 :     if (dx >= dy)
      87             :     {
      88           7 :         dy <<= 1;
      89           7 :         balance = dy - dx;
      90           7 :         dx *= 2;
      91             : 
      92         254 :         while (x != x2 && isAboveTerrain)
      93             :         {
      94         247 :             isAboveTerrain &= OnBresenhamPoint(x, y);
      95         247 :             if (balance >= 0)
      96             :             {
      97         247 :                 y += incy;
      98         247 :                 balance -= dx;
      99             :             }
     100         247 :             balance += dy;
     101         247 :             x += incx;
     102             :         }
     103           7 :         isAboveTerrain &= OnBresenhamPoint(x, y);
     104             :     }
     105             :     else
     106             :     {
     107           1 :         dx *= 2;
     108           1 :         balance = dx - dy;
     109           1 :         dy *= 2;
     110             : 
     111           7 :         while (y != y2 && isAboveTerrain)
     112             :         {
     113           6 :             isAboveTerrain &= OnBresenhamPoint(x, y);
     114           6 :             if (balance >= 0)
     115             :             {
     116           4 :                 x += incx;
     117           4 :                 balance -= dy;
     118             :             }
     119           6 :             balance += dx;
     120           6 :             y += incy;
     121             :         }
     122           1 :         isAboveTerrain &= OnBresenhamPoint(x, y);
     123             :     }
     124           8 :     return isAboveTerrain;
     125             : }
     126             : 
     127             : // Get the elevation of a single point.
     128         637 : static bool GetElevation(const GDALRasterBandH hBand, const int x, const int y,
     129             :                          double &val)
     130             : {
     131             :     /// @todo GDALCachedPixelAccessor may give increased performance.
     132         637 :     return GDALRasterIO(hBand, GF_Read, x, y, 1, 1, &val, 1, 1, GDT_Float64, 0,
     133         637 :                         0) == CE_None;
     134             : }
     135             : 
     136             : // Check a single location is above terrain.
     137         637 : static bool IsAboveTerrain(const GDALRasterBandH hBand, const int x,
     138             :                            const int y, const double z)
     139             : {
     140             :     double terrainHeight;
     141         637 :     if (GetElevation(hBand, x, y, terrainHeight))
     142             :     {
     143         636 :         return z > terrainHeight;
     144             :     }
     145             :     else
     146             :     {
     147           1 :         return false;
     148             :     }
     149             : }
     150             : 
     151             : /************************************************************************/
     152             : /*                        GDALIsLineOfSightVisible()                    */
     153             : /************************************************************************/
     154             : 
     155             : /**
     156             :  * Check Line of Sight between two points.
     157             :  * Both input coordinates must be within the raster coordinate bounds.
     158             :  *
     159             :  * This algorithm will check line of sight using a Bresenham algorithm.
     160             :  * https://www.researchgate.net/publication/2411280_Efficient_Line-of-Sight_Algorithms_for_Real_Terrain_Data
     161             :  * Line of sight is computed in raster coordinate space, and thus may not be appropriate.
     162             :  * For example, datasets referenced against geographic coordinate at high latitudes may have issues.
     163             :  *
     164             :  * @param hBand The band to read the DEM data from. This must NOT be null.
     165             :  *
     166             :  * @param xA The X location (raster column) of the first point to check on the raster.
     167             :  *
     168             :  * @param yA The Y location (raster row) of the first point to check on the raster.
     169             :  *
     170             :  * @param zA The Z location (height) of the first point to check.
     171             :  *
     172             :  * @param xB The X location (raster column) of the second point to check on the raster.
     173             :  *
     174             :  * @param yB The Y location (raster row) of the second point to check on the raster.
     175             :  *
     176             :  * @param zB The Z location (height) of the second point to check.
     177             :  *
     178             :  * @param[out] pnxTerrainIntersection The X location where the LOS line
     179             :  *             intersects with terrain, or nullptr if it does not intersect
     180             :  *             terrain.
     181             :  *
     182             :  * @param[out] pnyTerrainIntersection The Y location where the LOS line
     183             :  *             intersects with terrain, or nullptr if it does not intersect
     184             :  *             terrain.
     185             :  *
     186             :  * @param papszOptions Options for the line of sight algorithm (currently ignored).
     187             :  *
     188             :  * @return True if the two points are within Line of Sight.
     189             :  *
     190             :  * @since GDAL 3.9
     191             :  */
     192             : 
     193          36 : bool GDALIsLineOfSightVisible(const GDALRasterBandH hBand, const int xA,
     194             :                               const int yA, const double zA, const int xB,
     195             :                               const int yB, const double zB,
     196             :                               int *pnxTerrainIntersection,
     197             :                               int *pnyTerrainIntersection,
     198             :                               CPL_UNUSED CSLConstList papszOptions)
     199             : {
     200          36 :     VALIDATE_POINTER1(hBand, "GDALIsLineOfSightVisible", false);
     201             : 
     202             :     // A lambda to set the X-Y intersection if it's not null
     203          24 :     auto SetXYIntersection = [&](const int x, const int y)
     204             :     {
     205          24 :         if (pnxTerrainIntersection != nullptr)
     206             :         {
     207           9 :             *pnxTerrainIntersection = x;
     208             :         }
     209          24 :         if (pnyTerrainIntersection != nullptr)
     210             :         {
     211           9 :             *pnyTerrainIntersection = y;
     212             :         }
     213          60 :     };
     214             : 
     215          36 :     if (pnxTerrainIntersection)
     216          12 :         *pnxTerrainIntersection = -1;
     217             : 
     218          36 :     if (pnyTerrainIntersection)
     219          12 :         *pnyTerrainIntersection = -1;
     220             : 
     221             :     // Perform a preliminary check of the start and end points.
     222          36 :     if (!IsAboveTerrain(hBand, xA, yA, zA))
     223             :     {
     224          11 :         SetXYIntersection(xA, yA);
     225          11 :         return false;
     226             :     }
     227          25 :     if (!IsAboveTerrain(hBand, xB, yB, zB))
     228             :     {
     229           5 :         SetXYIntersection(xB, yB);
     230           5 :         return false;
     231             :     }
     232             : 
     233             :     // If both X and Y are the same, no further checks are needed.
     234          20 :     if (xA == xB && yA == yB)
     235             :     {
     236           3 :         return true;
     237             :     }
     238             : 
     239             :     // Lambda for Linear interpolate like C++20 std::lerp.
     240         315 :     auto lerp = [](const double a, const double b, const double t)
     241         315 :     { return a + t * (b - a); };
     242             : 
     243             :     // Lambda for getting Z test height given y input along the LOS line.
     244             :     // Only to be used for vertical line checks.
     245          18 :     auto GetZValueFromY = [&](const int y) -> double
     246             :     {
     247             :         // A ratio of 0.0 corresponds to being at yA.
     248          18 :         const auto ratio =
     249          18 :             static_cast<double>(y - yA) / static_cast<double>(yB - yA);
     250          18 :         return lerp(zA, zB, ratio);
     251          17 :     };
     252             : 
     253             :     // Lambda for getting Z test height given x input along the LOS line.
     254             :     // Only to be used for horizontal line checks.
     255          36 :     auto GetZValueFromX = [&](const int x) -> double
     256             :     {
     257             :         // A ratio of 0.0 corresponds to being at xA.
     258          36 :         const auto ratio =
     259          36 :             static_cast<double>(x - xA) / static_cast<double>(xB - xA);
     260          36 :         return lerp(zA, zB, ratio);
     261          17 :     };
     262             : 
     263             :     // Lambda for checking path safety of a vertical line.
     264             :     // Returns true if the path has clear LOS.
     265           4 :     auto CheckVerticalLine = [&]() -> bool
     266             :     {
     267           4 :         CPLAssert(xA == xB);
     268           4 :         CPLAssert(yA != yB);
     269             : 
     270           4 :         if (yA < yB)
     271             :         {
     272          10 :             for (int y = yA; y <= yB; ++y)
     273             :             {
     274           9 :                 const auto zTest = GetZValueFromY(y);
     275           9 :                 if (!IsAboveTerrain(hBand, xA, y, zTest))
     276             :                 {
     277           1 :                     SetXYIntersection(xA, y);
     278           1 :                     return false;
     279             :                 }
     280             :             }
     281           1 :             return true;
     282             :         }
     283             :         else
     284             :         {
     285          10 :             for (int y = yA; y >= yB; --y)
     286             :             {
     287           9 :                 const auto zTest = GetZValueFromY(y);
     288           9 :                 if (!IsAboveTerrain(hBand, xA, y, zTest))
     289             :                 {
     290           1 :                     SetXYIntersection(xA, y);
     291           1 :                     return false;
     292             :                 }
     293             :             }
     294           1 :             return true;
     295             :         }
     296          17 :     };
     297             : 
     298             :     // Lambda for checking path safety of a horizontal line.
     299             :     // Returns true if the path has clear LOS.
     300           5 :     auto CheckHorizontalLine = [&]() -> bool
     301             :     {
     302           5 :         CPLAssert(yA == yB);
     303           5 :         CPLAssert(xA != xB);
     304             : 
     305           5 :         if (xA < xB)
     306             :         {
     307          21 :             for (int x = xA; x <= xB; ++x)
     308             :             {
     309          19 :                 const auto zTest = GetZValueFromX(x);
     310          19 :                 if (!IsAboveTerrain(hBand, x, yA, zTest))
     311             :                 {
     312           1 :                     SetXYIntersection(x, yA);
     313           1 :                     return false;
     314             :                 }
     315             :             }
     316           2 :             return true;
     317             :         }
     318             :         else
     319             :         {
     320          18 :             for (int x = xA; x >= xB; --x)
     321             :             {
     322          17 :                 const auto zTest = GetZValueFromX(x);
     323          17 :                 if (!IsAboveTerrain(hBand, x, yA, zTest))
     324             :                 {
     325           1 :                     SetXYIntersection(x, yA);
     326           1 :                     return false;
     327             :                 }
     328             :             }
     329           1 :             return true;
     330             :         }
     331          17 :     };
     332             : 
     333             :     // Handle special cases if it's a vertical or horizontal line (don't use bresenham).
     334          17 :     if (xA == xB)
     335             :     {
     336           4 :         return CheckVerticalLine();
     337             :     }
     338          13 :     if (yA == yB)
     339             :     {
     340           5 :         return CheckHorizontalLine();
     341             :     }
     342             : 
     343             :     // Use an interpolated Z height with 2D bresenham for the remaining cases.
     344             : 
     345             :     // Lambda for computing the square of a number
     346        1044 :     auto SQUARE = [](const double d) -> double { return d * d; };
     347             : 
     348             :     // Lambda for getting Z test height given x-y input along the bresenham line.
     349         261 :     auto GetZValueFromXY = [&](const int x, const int y) -> double
     350             :     {
     351         261 :         const auto rNum = SQUARE(static_cast<double>(x - xA)) +
     352         261 :                           SQUARE(static_cast<double>(y - yA));
     353         261 :         const auto rDenom = SQUARE(static_cast<double>(xB - xA)) +
     354         261 :                             SQUARE(static_cast<double>(yB - yA));
     355             :         /// @todo In order to reduce CPU cost and avoid a sqrt operation, consider
     356             :         /// the approach to just the ratio along x or y depending on whether
     357             :         /// the line is steep or shallow.
     358             :         /// See https://github.com/OSGeo/gdal/pull/9506#discussion_r1532459689.
     359             :         const double ratio =
     360         261 :             sqrt(static_cast<double>(rNum) / static_cast<double>(rDenom));
     361         261 :         return lerp(zA, zB, ratio);
     362           8 :     };
     363             : 
     364             :     // Lambda to get elevation at a bresenham-computed location.
     365         261 :     auto OnBresenhamPoint = [&](const int x, const int y) -> bool
     366             :     {
     367         261 :         const auto z = GetZValueFromXY(x, y);
     368         261 :         const auto isAbove = IsAboveTerrain(hBand, x, y, z);
     369         261 :         if (!isAbove)
     370             :         {
     371           4 :             SetXYIntersection(x, y);
     372             :         }
     373         261 :         return IsAboveTerrain(hBand, x, y, z);
     374           8 :     };
     375             : 
     376           8 :     return Bresenham2D(xA, yA, xB, yB, OnBresenhamPoint);
     377             : }

Generated by: LCOV version 1.14