DuvenhageAlgorithm.java

  1. /* Copyright 2013-2025 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.rugged.intersection.duvenhage;

  18. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  19. import org.hipparchus.util.FastMath;
  20. import org.orekit.bodies.GeodeticPoint;
  21. import org.orekit.rugged.api.AlgorithmId;
  22. import org.orekit.rugged.errors.DumpManager;
  23. import org.orekit.rugged.errors.RuggedException;
  24. import org.orekit.rugged.errors.RuggedInternalError;
  25. import org.orekit.rugged.errors.RuggedMessages;
  26. import org.orekit.rugged.intersection.IntersectionAlgorithm;
  27. import org.orekit.rugged.raster.Tile;
  28. import org.orekit.rugged.raster.TileUpdater;
  29. import org.orekit.rugged.raster.TilesCache;
  30. import org.orekit.rugged.utils.ExtendedEllipsoid;
  31. import org.orekit.rugged.utils.NormalizedGeodeticPoint;

  32. /** Digital Elevation Model intersection using Bernardt Duvenhage's algorithm.
  33.  * <p>
  34.  * The algorithm is described in the 2009 paper:
  35.  * <a href="https://researchspace.csir.co.za/dspace/bitstream/10204/3041/1/Duvenhage_2009.pdf">Using
  36.  * An Implicit Min/Max KD-Tree for Doing Efficient Terrain Line of Sight Calculations</a>.
  37.  * </p>
  38.  * @author Luc Maisonobe
  39.  * @author Guylaine Prat
  40.  */
  41. public class DuvenhageAlgorithm implements IntersectionAlgorithm {

  42.     /** Step size when skipping from one tile to a neighbor one, in meters. */
  43.     private static final double STEP = 0.01;

  44.     /** Maximum number of attempts to refine intersection.
  45.      * <p>
  46.      * This parameter is intended to prevent infinite loops.
  47.      * </p>
  48.      * @since 2.1 */
  49.     private static final int MAX_REFINING_ATTEMPTS = 100;

  50.     /** Cache for DEM tiles. */
  51.     private final TilesCache<MinMaxTreeTile> cache;

  52.     /** Flag for flat-body hypothesis. */
  53.     private final boolean flatBody;

  54.     /** Algorithm Id.
  55.      * @since 2.2 */
  56.     private final AlgorithmId algorithmId;

  57.     /** Simple constructor.
  58.      * @param updater updater used to load Digital Elevation Model tiles
  59.      * @param maxCachedTiles maximum number of tiles stored in the cache
  60.      * @param flatBody if true, the body is considered flat, i.e. lines computed
  61.      * from entry/exit points in the DEM are considered to be straight lines also
  62.      * in geodetic coordinates. The sagitta resulting from real ellipsoid curvature
  63.      * is therefore <em>not</em> corrected in this case. As this computation is not
  64.      * costly (a few percents overhead), it is highly recommended to set this parameter
  65.      * to {@code false}. This flag is mainly intended for comparison purposes with other systems
  66.      * @param isOverlappingTiles flag to tell if the DEM tiles are overlapping:
  67.      *                          true if overlapping; false otherwise.
  68.      */
  69.     public DuvenhageAlgorithm(final TileUpdater updater, final int maxCachedTiles,
  70.                               final boolean flatBody, final boolean isOverlappingTiles) {
  71.         this.cache = new TilesCache<MinMaxTreeTile>(new MinMaxTreeTileFactory(), updater,
  72.                                                     maxCachedTiles, isOverlappingTiles);
  73.         this.flatBody = flatBody;
  74.         this.algorithmId = flatBody ? AlgorithmId.DUVENHAGE_FLAT_BODY : AlgorithmId.DUVENHAGE;
  75.     }

  76.     /** {@inheritDoc} */
  77.     @Override
  78.     public NormalizedGeodeticPoint intersection(final ExtendedEllipsoid ellipsoid,
  79.                                                 final Vector3D position, final Vector3D los) {

  80.         DumpManager.dumpAlgorithm(this.algorithmId);

  81.         // compute intersection with ellipsoid
  82.         final NormalizedGeodeticPoint gp0 = ellipsoid.pointOnGround(position, los, 0.0);

  83.         // locate the entry tile along the line-of-sight
  84.         MinMaxTreeTile tile = cache.getTile(gp0.getLatitude(), gp0.getLongitude());

  85.         NormalizedGeodeticPoint current = null;
  86.         NormalizedGeodeticPoint positionGP = null;
  87.         double hMax = tile.getMaxElevation();
  88.         while (current == null) {

  89.             // find where line-of-sight crosses tile max altitude
  90.             final Vector3D entryP = ellipsoid.pointAtAltitude(position, los, hMax + STEP);
  91.             if (Vector3D.dotProduct(entryP.subtract(position), los) < 0) {
  92.                 // the entry point is behind spacecraft!

  93.                 // let's see if at least we are above DEM
  94.                 try {
  95.                     positionGP =
  96.                                     ellipsoid.transform(position, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  97.                     final double elevationAtPosition = tile.interpolateElevation(positionGP.getLatitude(), positionGP.getLongitude());
  98.                     if (positionGP.getAltitude() >= elevationAtPosition) {
  99.                         // we can use the current position as the entry point
  100.                         current = positionGP;
  101.                     } else {
  102.                         current = null;
  103.                     }
  104.                 } catch (RuggedException re) {
  105.                     if (re.getSpecifier() == RuggedMessages.OUT_OF_TILE_ANGLES) {
  106.                         // the entry point is in another tile, we can use the current position as the entry point;
  107.                         current = positionGP;
  108.                     }
  109.                 }

  110.                 if (current == null) {
  111.                     throw new RuggedException(RuggedMessages.DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT);
  112.                 }

  113.             } else {
  114.                 current = ellipsoid.transform(entryP, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  115.             }

  116.             if (tile.getLocation(current.getLatitude(), current.getLongitude()) != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
  117.                 // the entry point is in another tile
  118.                 tile    = cache.getTile(current.getLatitude(), current.getLongitude());
  119.                 hMax    = FastMath.max(hMax, tile.getMaxElevation());
  120.                 current = null;
  121.             }

  122.         }

  123.         // loop along the path
  124.         while (true) {

  125.             // find where line-of-sight exit tile
  126.             final LimitPoint exit = findExit(tile, ellipsoid, position, los);

  127.             // compute intersection with Digital Elevation Model
  128.             final int entryLat = FastMath.max(0,
  129.                                               FastMath.min(tile.getLatitudeRows() - 1,
  130.                                                            tile.getFloorLatitudeIndex(current.getLatitude())));
  131.             final int entryLon = FastMath.max(0,
  132.                                               FastMath.min(tile.getLongitudeColumns() - 1,
  133.                                                            tile.getFloorLongitudeIndex(current.getLongitude())));
  134.             final int exitLat  = FastMath.max(0,
  135.                                               FastMath.min(tile.getLatitudeRows() - 1,
  136.                                                            tile.getFloorLatitudeIndex(exit.getPoint().getLatitude())));
  137.             final int exitLon  = FastMath.max(0,
  138.                                               FastMath.min(tile.getLongitudeColumns() - 1,
  139.                                                            tile.getFloorLongitudeIndex(exit.getPoint().getLongitude())));
  140.             NormalizedGeodeticPoint intersection = recurseIntersection(0, ellipsoid, position, los, tile,
  141.                                                                        current, entryLat, entryLon,
  142.                                                                        exit.getPoint(), exitLat, exitLon);

  143.             if (intersection != null) {
  144.                 // we have found the intersection
  145.                 return intersection;
  146.             } else if (exit.atSide()) {
  147.                 // no intersection on this tile, we can proceed to next part of the line-of-sight

  148.                 // select next tile after current point
  149.                 final Vector3D forward = new Vector3D(1.0, ellipsoid.transform(exit.getPoint()), STEP, los);
  150.                 current = ellipsoid.transform(forward, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  151.                 tile = cache.getTile(current.getLatitude(), current.getLongitude());

  152.                 if (tile.interpolateElevation(current.getLatitude(), current.getLongitude()) >= current.getAltitude()) {
  153.                     // extremely rare case! The line-of-sight traversed the Digital Elevation Model
  154.                     // during the very short forward step we used to move to next tile
  155.                     // we consider this point to be OK
  156.                     return current;
  157.                 }

  158.             } else {

  159.                 // this should never happen
  160.                 // we should have left the loop with an intersection point
  161.                 // try a fallback non-recursive search
  162.                 intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  163.                                                      current, entryLat, entryLon,
  164.                                                      exitLat, exitLon);
  165.                 if (intersection != null) {
  166.                     return intersection;
  167.                 } else {
  168.                     throw new RuggedInternalError(null);
  169.                 }
  170.             }
  171.         }
  172.     }

  173.     /** {@inheritDoc} */
  174.     @Override
  175.     public NormalizedGeodeticPoint refineIntersection(final ExtendedEllipsoid ellipsoid,
  176.                                                       final Vector3D position, final Vector3D los,
  177.                                                       final NormalizedGeodeticPoint closeGuess) {

  178.         DumpManager.dumpAlgorithm(this.algorithmId);

  179.         if (flatBody) {
  180.             // under the (bad) flat-body assumption, the reference point must remain
  181.             // at DEM entry and exit, even if we already have a much better close guess :-(
  182.             // this is in order to remain consistent with other systems
  183.             final Tile tile = cache.getTile(closeGuess.getLatitude(), closeGuess.getLongitude());
  184.             final Vector3D      exitP  = ellipsoid.pointAtAltitude(position, los, tile.getMinElevation());
  185.             final Vector3D      entryP = ellipsoid.pointAtAltitude(position, los, tile.getMaxElevation());
  186.             final NormalizedGeodeticPoint entry  = ellipsoid.transform(entryP, ellipsoid.getBodyFrame(), null,
  187.                                                                        tile.getMinimumLongitude());
  188.             return tile.cellIntersection(entry, ellipsoid.convertLos(entryP, exitP),
  189.                                          tile.getFloorLatitudeIndex(closeGuess.getLatitude()),
  190.                                          tile.getFloorLongitudeIndex(closeGuess.getLongitude()));

  191.         } else {
  192.             // regular curved ellipsoid model

  193.             NormalizedGeodeticPoint currentGuess = closeGuess;
  194.             // normally, we should succeed at first attempt but in very rare cases
  195.             // we may loose the intersection (typically because some corrections introduced
  196.             // between the first intersection and the refining have slightly changed the
  197.             // relative geometry between Digital Elevation Model and Line Of Sight).
  198.             // In these rare cases, we have to recover a new intersection
  199.             for (int i = 0; i < MAX_REFINING_ATTEMPTS; ++i) {

  200.                 final Vector3D      delta      = ellipsoid.transform(currentGuess).subtract(position);
  201.                 final double        s          = Vector3D.dotProduct(delta, los) / los.getNormSq();
  202.                 final Vector3D      projectedP = new Vector3D(1, position, s, los);
  203.                 final GeodeticPoint projected  = ellipsoid.transform(projectedP, ellipsoid.getBodyFrame(), null);
  204.                 final NormalizedGeodeticPoint normalizedProjected =
  205.                         new NormalizedGeodeticPoint(projected.getLatitude(),
  206.                                                     projected.getLongitude(),
  207.                                                     projected.getAltitude(),
  208.                                                     currentGuess.getLongitude());
  209.                 final Tile tile = cache.getTile(normalizedProjected.getLatitude(), normalizedProjected.getLongitude());

  210.                 final Vector3D                topoLOS           = ellipsoid.convertLos(normalizedProjected, los);
  211.                 final int                     iLat              = tile.getFloorLatitudeIndex(normalizedProjected.getLatitude());
  212.                 final int                     iLon              = tile.getFloorLongitudeIndex(normalizedProjected.getLongitude());
  213.                 final NormalizedGeodeticPoint foundIntersection = tile.cellIntersection(normalizedProjected, topoLOS, iLat, iLon);

  214.                 if (foundIntersection != null) {
  215.                     // nominal case, we were able to refine the intersection
  216.                     return foundIntersection;
  217.                 } else {
  218.                     // extremely rare case: we have lost the intersection
  219.                     // find a start point for new search, leaving the current cell behind
  220.                     final double cellBoundaryLatitude  = tile.getLatitudeAtIndex(topoLOS.getY()  <= 0 ? iLat : iLat + 1);
  221.                     final double cellBoundaryLongitude = tile.getLongitudeAtIndex(topoLOS.getX() <= 0 ? iLon : iLon + 1);
  222.                     final Vector3D cellExit = new Vector3D(1, selectClosest(latitudeCrossing(ellipsoid, projectedP,  los, cellBoundaryLatitude,  projectedP),
  223.                                                                             longitudeCrossing(ellipsoid, projectedP, los, cellBoundaryLongitude, projectedP),
  224.                                                                             projectedP),
  225.                                                            STEP, los);
  226.                     final GeodeticPoint egp = ellipsoid.transform(cellExit, ellipsoid.getBodyFrame(), null);
  227.                     final NormalizedGeodeticPoint cellExitGP = new NormalizedGeodeticPoint(egp.getLatitude(),
  228.                                                                                            egp.getLongitude(),
  229.                                                                                            egp.getAltitude(),
  230.                                                                                            currentGuess.getLongitude());
  231.                     if (tile.interpolateElevation(cellExitGP.getLatitude(), cellExitGP.getLongitude()) >= cellExitGP.getAltitude()) {
  232.                         // extremely rare case! The line-of-sight traversed the Digital Elevation Model
  233.                         // during the very short forward step we used to move to next cell
  234.                         // we consider this point to be OK
  235.                         return cellExitGP;
  236.                     }

  237.                     // We recompute fully a new guess, starting from the point after current cell
  238.                     final GeodeticPoint currentGuessGP = intersection(ellipsoid, cellExit, los);
  239.                     currentGuess = new NormalizedGeodeticPoint(currentGuessGP.getLatitude(),
  240.                                                                currentGuessGP.getLongitude(),
  241.                                                                currentGuessGP.getAltitude(),
  242.                                                                projected.getLongitude());
  243.                 }

  244.             }

  245.             // no intersection found
  246.             return null;

  247.         } // end test on flatbody

  248.     }

  249.     /** {@inheritDoc} */
  250.     @Override
  251.     public double getElevation(final double latitude, final double longitude) {

  252.         DumpManager.dumpAlgorithm(this.algorithmId);
  253.         final Tile tile = cache.getTile(latitude, longitude);
  254.         return tile.interpolateElevation(latitude, longitude);
  255.     }

  256.     /** {@inheritDoc} */
  257.     @Override
  258.     public AlgorithmId getAlgorithmId() {
  259.         return this.algorithmId;
  260.     }

  261.     /** Compute intersection of line with Digital Elevation Model in a sub-tile.
  262.      * @param depth recursion depth
  263.      * @param ellipsoid reference ellipsoid
  264.      * @param position pixel position in ellipsoid frame
  265.      * @param los pixel line-of-sight in ellipsoid frame
  266.      * @param tile Digital Elevation Model tile
  267.      * @param entry line-of-sight entry point in the sub-tile
  268.      * @param entryLat index to use for interpolating entry point elevation
  269.      * @param entryLon index to use for interpolating entry point elevation
  270.      * @param exit line-of-sight exit point from the sub-tile
  271.      * @param exitLat index to use for interpolating exit point elevation
  272.      * @param exitLon index to use for interpolating exit point elevation
  273.      * @return point at which the line first enters ground, or null if does not enter
  274.      * ground in the search sub-tile
  275.      */
  276.     private NormalizedGeodeticPoint recurseIntersection(final int depth, final ExtendedEllipsoid ellipsoid,
  277.                                                         final Vector3D position, final Vector3D los,
  278.                                                         final MinMaxTreeTile tile,
  279.                                                         final NormalizedGeodeticPoint entry, final int entryLat, final int entryLon,
  280.                                                         final NormalizedGeodeticPoint exit, final int exitLat, final int exitLon) {

  281.         if (depth > 30) {
  282.             // this should never happen
  283.             throw new RuggedInternalError(null);
  284.         }

  285.         if (searchDomainSize(entryLat, entryLon, exitLat, exitLon) < 4) {
  286.             // we have narrowed the search down to a few cells
  287.             return noRecurseIntersection(ellipsoid, position, los, tile,
  288.                                          entry, entryLat, entryLon, exitLat, exitLon);
  289.         }

  290.         // find the deepest level in the min/max kd-tree at which entry and exit share a sub-tile
  291.         final int level = tile.getMergeLevel(entryLat, entryLon, exitLat, exitLon);
  292.         if (level >= 0  && exit.getAltitude() >= tile.getMaxElevation(exitLat, exitLon, level)) {
  293.             // the line-of-sight segment is fully above Digital Elevation Model
  294.             // we can safely reject it and proceed to next part of the line-of-sight
  295.             return null;
  296.         }

  297.         NormalizedGeodeticPoint previousGP    = entry;
  298.         int                     previousLat   = entryLat;
  299.         int                     previousLon   = entryLon;
  300.         final double            angularMargin = STEP / ellipsoid.getEquatorialRadius();

  301.         // introduce all intermediate points corresponding to the line-of-sight
  302.         // intersecting the boundary between level 0 sub-tiles
  303.         if (tile.isColumnMerging(level + 1)) {
  304.             // recurse through longitude crossings

  305.             final int[] crossings = tile.getCrossedBoundaryColumns(previousLon, exitLon, level + 1);
  306.             for (final int crossingLon : crossings) {

  307.                 // compute segment endpoints
  308.                 final double longitude = tile.getLongitudeAtIndex(crossingLon);
  309.                 if (longitude >= FastMath.min(entry.getLongitude(), exit.getLongitude()) - angularMargin &&
  310.                     longitude <= FastMath.max(entry.getLongitude(), exit.getLongitude()) + angularMargin) {

  311.                     NormalizedGeodeticPoint crossingGP = null;
  312.                     if (!flatBody) {
  313.                         try {
  314.                             // full computation of crossing point
  315.                             final Vector3D crossingP = ellipsoid.pointAtLongitude(position, los, longitude);
  316.                             crossingGP = ellipsoid.transform(crossingP, ellipsoid.getBodyFrame(), null,
  317.                                                              tile.getMinimumLongitude());
  318.                         } catch (RuggedException re) {
  319.                             // in some very rare cases of numerical noise, we miss the crossing point
  320.                             crossingGP = null;
  321.                         }
  322.                     }
  323.                     if (crossingGP == null) {
  324.                         // linear approximation of crossing point
  325.                         final double d  = exit.getLongitude() - entry.getLongitude();
  326.                         final double cN = (exit.getLongitude() - longitude) / d;
  327.                         final double cX = (longitude - entry.getLongitude()) / d;
  328.                         crossingGP = new NormalizedGeodeticPoint(cN * entry.getLatitude() + cX * exit.getLatitude(),
  329.                                                                  longitude,
  330.                                                                  cN * entry.getAltitude() + cX * exit.getAltitude(),
  331.                                                                  tile.getMinimumLongitude());
  332.                     }
  333.                     final int crossingLat =
  334.                             FastMath.max(0,
  335.                                          FastMath.min(tile.getLatitudeRows() - 1,
  336.                                                       tile.getFloorLatitudeIndex(crossingGP.getLatitude())));

  337.                     // adjust indices as the crossing point is by definition between the sub-tiles
  338.                     final int crossingLonBefore = crossingLon - (entryLon <= exitLon ? 1 : 0);
  339.                     final int crossingLonAfter  = crossingLon - (entryLon <= exitLon ? 0 : 1);

  340.                     if (inRange(crossingLonBefore, entryLon, exitLon)) {
  341.                         // look for intersection
  342.                         final NormalizedGeodeticPoint intersection;
  343.                         if (searchDomainSize(previousLat, previousLon, crossingLat, crossingLonBefore) <
  344.                             searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  345.                             intersection = recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  346.                                                                previousGP, previousLat, previousLon,
  347.                                                                crossingGP, crossingLat, crossingLonBefore);
  348.                         } else {
  349.                             // we failed to reduce domain size, probably due to numerical problems
  350.                             intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  351.                                                                  previousGP, previousLat, previousLon,
  352.                                                                  crossingLat, crossingLonBefore);
  353.                         }
  354.                         if (intersection != null) {
  355.                             return intersection;
  356.                         }
  357.                     }

  358.                     // prepare next segment
  359.                     previousGP  = crossingGP;
  360.                     previousLat = crossingLat;
  361.                     previousLon = crossingLonAfter;

  362.                 }

  363.             }
  364.         } else {
  365.             // recurse through latitude crossings
  366.             final int[] crossings = tile.getCrossedBoundaryRows(previousLat, exitLat, level + 1);
  367.             for (final int crossingLat : crossings) {

  368.                 // compute segment endpoints
  369.                 final double latitude = tile.getLatitudeAtIndex(crossingLat);
  370.                 if (latitude >= FastMath.min(entry.getLatitude(), exit.getLatitude()) - angularMargin &&
  371.                     latitude <= FastMath.max(entry.getLatitude(), exit.getLatitude()) + angularMargin) {

  372.                     NormalizedGeodeticPoint crossingGP = null;
  373.                     if (!flatBody) {
  374.                         // full computation of crossing point
  375.                         try {
  376.                             final Vector3D crossingP = ellipsoid.pointAtLatitude(position, los,
  377.                                                                                  tile.getLatitudeAtIndex(crossingLat),
  378.                                                                                  ellipsoid.transform(entry));
  379.                             crossingGP = ellipsoid.transform(crossingP, ellipsoid.getBodyFrame(), null,
  380.                                                              tile.getMinimumLongitude());
  381.                         } catch (RuggedException re) {
  382.                             // in some very rare cases of numerical noise, we miss the crossing point
  383.                             crossingGP = null;
  384.                         }
  385.                     }
  386.                     if (crossingGP == null) {
  387.                         // linear approximation of crossing point
  388.                         final double d  = exit.getLatitude() - entry.getLatitude();
  389.                         final double cN = (exit.getLatitude() - latitude) / d;
  390.                         final double cX = (latitude - entry.getLatitude()) / d;
  391.                         crossingGP = new NormalizedGeodeticPoint(latitude,
  392.                                                                  cN * entry.getLongitude() + cX * exit.getLongitude(),
  393.                                                                  cN * entry.getAltitude()  + cX * exit.getAltitude(),
  394.                                                                  tile.getMinimumLongitude());
  395.                     }
  396.                     final int crossingLon =
  397.                             FastMath.max(0,
  398.                                          FastMath.min(tile.getLongitudeColumns() - 1,
  399.                                                       tile.getFloorLongitudeIndex(crossingGP.getLongitude())));

  400.                     // adjust indices as the crossing point is by definition between the sub-tiles
  401.                     final int crossingLatBefore = crossingLat - (entryLat <= exitLat ? 1 : 0);
  402.                     final int crossingLatAfter  = crossingLat - (entryLat <= exitLat ? 0 : 1);

  403.                     if (inRange(crossingLatBefore, entryLat, exitLat)) {
  404.                         // look for intersection
  405.                         final NormalizedGeodeticPoint intersection;
  406.                         if (searchDomainSize(previousLat, previousLon, crossingLatBefore, crossingLon) <
  407.                             searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  408.                             intersection = recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  409.                                                                previousGP, previousLat, previousLon,
  410.                                                                crossingGP, crossingLatBefore, crossingLon);
  411.                         } else {
  412.                             intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  413.                                                                  previousGP, previousLat, previousLon,
  414.                                                                  crossingLatBefore, crossingLon);
  415.                         }
  416.                         if (intersection != null) {
  417.                             return intersection;
  418.                         }
  419.                     }

  420.                     // prepare next segment
  421.                     previousGP  = crossingGP;
  422.                     previousLat = crossingLatAfter;
  423.                     previousLon = crossingLon;

  424.                 }

  425.             }
  426.         }

  427.         if (inRange(previousLat, entryLat, exitLat) && inRange(previousLon, entryLon, exitLon)) {
  428.             // last part of the segment, up to exit point
  429.             if (searchDomainSize(previousLat, previousLon, exitLat, exitLon) <
  430.                 searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  431.                 return recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  432.                                            previousGP, previousLat, previousLon,
  433.                                            exit, exitLat, exitLon);
  434.             } else {
  435.                 return noRecurseIntersection(ellipsoid, position, los, tile,
  436.                                              previousGP, previousLat, previousLon,
  437.                                              exitLat, exitLon);
  438.             }
  439.         } else {
  440.             return null;
  441.         }

  442.     }

  443.     /** Compute intersection of line with Digital Elevation Model in a sub-tile, without recursion.
  444.      * @param ellipsoid reference ellipsoid
  445.      * @param position pixel position in ellipsoid frame
  446.      * @param los pixel line-of-sight in ellipsoid frame
  447.      * @param tile Digital Elevation Model tile
  448.      * @param entry line-of-sight entry point in the sub-tile
  449.      * @param entryLat index to use for interpolating entry point elevation
  450.      * @param entryLon index to use for interpolating entry point elevation
  451.      * @param exitLat index to use for interpolating exit point elevation
  452.      * @param exitLon index to use for interpolating exit point elevation
  453.      * @return point at which the line first enters ground, or null if does not enter
  454.      * ground in the search sub-tile
  455.      */
  456.     private NormalizedGeodeticPoint noRecurseIntersection(final ExtendedEllipsoid ellipsoid,
  457.                                                           final Vector3D position, final Vector3D los,
  458.                                                           final MinMaxTreeTile tile,
  459.                                                           final NormalizedGeodeticPoint entry,
  460.                                                           final int entryLat, final int entryLon,
  461.                                                           final int exitLat, final int exitLon) {

  462.         NormalizedGeodeticPoint intersectionGP = null;
  463.         double intersectionDot = Double.POSITIVE_INFINITY;
  464.         for (int i = FastMath.min(entryLat, exitLat); i <= FastMath.max(entryLat, exitLat); ++i) {
  465.             for (int j = FastMath.min(entryLon, exitLon); j <= FastMath.max(entryLon, exitLon); ++j) {
  466.                 final NormalizedGeodeticPoint gp = tile.cellIntersection(entry, ellipsoid.convertLos(entry, los), i, j);
  467.                 if (gp != null) {

  468.                     // improve the point, by projecting it back on the 3D line, fixing the small body curvature at cell level
  469.                     final Vector3D delta = ellipsoid.transform(gp).subtract(position);
  470.                     final double   s     = Vector3D.dotProduct(delta, los) / los.getNormSq();
  471.                     if (s > 0) {
  472.                         final GeodeticPoint projected = ellipsoid.transform(new Vector3D(1, position, s, los),
  473.                                                                             ellipsoid.getBodyFrame(), null);
  474.                         final NormalizedGeodeticPoint normalizedProjected = new NormalizedGeodeticPoint(projected.getLatitude(),
  475.                                                                                                         projected.getLongitude(),
  476.                                                                                                         projected.getAltitude(),
  477.                                                                                                         gp.getLongitude());
  478.                         final NormalizedGeodeticPoint gpImproved = tile.cellIntersection(normalizedProjected,
  479.                                                                                          ellipsoid.convertLos(normalizedProjected, los),
  480.                                                                                          i, j);

  481.                         if (gpImproved != null) {
  482.                             final Vector3D point = ellipsoid.transform(gpImproved);
  483.                             final double dot = Vector3D.dotProduct(point.subtract(position), los);
  484.                             if (dot < intersectionDot) {
  485.                                 intersectionGP  = gpImproved;
  486.                                 intersectionDot = dot;
  487.                             }
  488.                         }
  489.                     }

  490.                 }
  491.             }
  492.         }

  493.         return intersectionGP;

  494.     }

  495.     /** Compute the size of a search domain.
  496.      * @param entryLat index to use for interpolating entry point elevation
  497.      * @param entryLon index to use for interpolating entry point elevation
  498.      * @param exitLat index to use for interpolating exit point elevation
  499.      * @param exitLon index to use for interpolating exit point elevation
  500.      * @return size of the search domain
  501.      */
  502.     private int searchDomainSize(final int entryLat, final int entryLon,
  503.                                  final int exitLat, final int exitLon) {
  504.         return (FastMath.abs(entryLat - exitLat) + 1) * (FastMath.abs(entryLon - exitLon) + 1);
  505.     }

  506.     /** Check if an index is inside a range.
  507.      * @param i index to check
  508.      * @param a first bound of the range (may be either below or above b)
  509.      * @param b second bound of the range (may be either below or above a)
  510.      * @return true if i is between a and b (inclusive)
  511.      */
  512.     private boolean inRange(final int i, final int a, final int b) {
  513.         return i >= FastMath.min(a, b) && i <= FastMath.max(a, b);
  514.     }

  515.     /** Compute a line-of-sight exit point from a tile.
  516.      * @param tile tile to consider
  517.      * @param ellipsoid reference ellipsoid
  518.      * @param position pixel position in ellipsoid frame
  519.      * @param los pixel line-of-sight in ellipsoid frame
  520.      * @return exit point
  521.      */
  522.     private LimitPoint findExit(final Tile tile, final ExtendedEllipsoid ellipsoid,
  523.                                 final Vector3D position, final Vector3D los) {

  524.         // look for an exit at bottom
  525.         final double                  reference = tile.getMinimumLongitude();
  526.         final Vector3D                exitP     = ellipsoid.pointAtAltitude(position, los, tile.getMinElevation() - STEP);
  527.         final NormalizedGeodeticPoint exitGP    = ellipsoid.transform(exitP, ellipsoid.getBodyFrame(), null, reference);

  528.         switch (tile.getLocation(exitGP.getLatitude(), exitGP.getLongitude())) {
  529.             case SOUTH_WEST :
  530.                 return new LimitPoint(ellipsoid, reference,
  531.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMinimumLatitude(),  exitP),
  532.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMinimumLongitude(), exitP),
  533.                                                     position),
  534.                                       true);
  535.             case WEST :
  536.                 return new LimitPoint(ellipsoid, reference,
  537.                                       longitudeCrossing(ellipsoid, position, los, tile.getMinimumLongitude(), exitP),
  538.                                       true);
  539.             case NORTH_WEST:
  540.                 return new LimitPoint(ellipsoid, reference,
  541.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMaximumLatitude(),  exitP),
  542.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMinimumLongitude(), exitP),
  543.                                                     position),
  544.                                       true);
  545.             case NORTH :
  546.                 return new LimitPoint(ellipsoid, reference,
  547.                                       latitudeCrossing(ellipsoid, position, los, tile.getMaximumLatitude(), exitP),
  548.                                       true);
  549.             case NORTH_EAST :
  550.                 return new LimitPoint(ellipsoid, reference,
  551.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMaximumLatitude(),  exitP),
  552.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMaximumLongitude(), exitP),
  553.                                                     position),
  554.                                       true);
  555.             case EAST :
  556.                 return new LimitPoint(ellipsoid, reference,
  557.                                       longitudeCrossing(ellipsoid, position, los, tile.getMaximumLongitude(), exitP),
  558.                                       true);
  559.             case SOUTH_EAST :
  560.                 return new LimitPoint(ellipsoid, reference,
  561.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMinimumLatitude(),  exitP),
  562.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMaximumLongitude(), exitP),
  563.                                                     position),
  564.                                       true);
  565.             case SOUTH :
  566.                 return new LimitPoint(ellipsoid, reference,
  567.                                       latitudeCrossing(ellipsoid, position, los, tile.getMinimumLatitude(), exitP),
  568.                                       true);
  569.             case HAS_INTERPOLATION_NEIGHBORS :
  570.                 return new LimitPoint(exitGP, false);

  571.             default :
  572.                 // this should never happen
  573.                 throw new RuggedInternalError(null);
  574.         }

  575.     }

  576.     /** Select point closest to line-of-sight start.
  577.      * @param p1 first point to consider
  578.      * @param p2 second point to consider
  579.      * @param position pixel position in ellipsoid frame
  580.      * @return either p1 or p2, depending on which is closest to position
  581.      */
  582.     private Vector3D selectClosest(final Vector3D p1, final Vector3D p2, final Vector3D position) {
  583.         return Vector3D.distance(p1, position) <= Vector3D.distance(p2, position) ? p1 : p2;
  584.     }

  585.     /** Get point at some latitude along a pixel line of sight.
  586.      * @param ellipsoid reference ellipsoid
  587.      * @param position pixel position (in body frame)
  588.      * @param los pixel line-of-sight, not necessarily normalized (in body frame)
  589.      * @param latitude latitude with respect to ellipsoid
  590.      * @param closeReference reference point used to select the closest solution
  591.      * when there are two points at the desired latitude along the line
  592.      * @return point at latitude, or closeReference if no such point can be found
  593.      */
  594.     private Vector3D latitudeCrossing(final ExtendedEllipsoid ellipsoid,
  595.                                       final Vector3D position, final Vector3D los,
  596.                                       final double latitude, final Vector3D closeReference) {
  597.         try {
  598.             return ellipsoid.pointAtLatitude(position, los, latitude, closeReference);
  599.         } catch (RuggedException re) {
  600.             return closeReference;
  601.         }
  602.     }

  603.     /** Get point at some latitude along a pixel line of sight.
  604.      * @param ellipsoid reference ellipsoid
  605.      * @param position pixel position (in body frame)
  606.      * @param los pixel line-of-sight, not necessarily normalized (in body frame)
  607.      * @param longitude longitude with respect to ellipsoid
  608.      * @param closeReference reference point used to select the closest solution
  609.      * when there are two points at the desired longitude along the line
  610.      * @return point at longitude, or closeReference if no such point can be found
  611.      */
  612.     private Vector3D longitudeCrossing(final ExtendedEllipsoid ellipsoid,
  613.                                        final Vector3D position, final Vector3D los,
  614.                                        final double longitude, final Vector3D closeReference) {
  615.         try {
  616.             return ellipsoid.pointAtLongitude(position, los, longitude);
  617.         } catch (RuggedException re) {
  618.             return closeReference;
  619.         }
  620.     }

  621.     /** Point at tile boundary. */
  622.     private static class LimitPoint {

  623.         /** Coordinates. */
  624.         private final NormalizedGeodeticPoint point;

  625.         /** Limit status. */
  626.         private final boolean side;

  627.         /** Simple constructor.
  628.          * @param ellipsoid reference ellipsoid
  629.          * @param referenceLongitude reference longitude lc such that the point longitude will
  630.          * be normalized between lc-π and lc+π
  631.          * @param cartesian Cartesian point
  632.          * @param side if true, the point is on a side limit, otherwise
  633.          * it is on a top/bottom limit
  634.          */
  635.         LimitPoint(final ExtendedEllipsoid ellipsoid, final double referenceLongitude,
  636.                    final Vector3D cartesian, final boolean side) {
  637.             this(ellipsoid.transform(cartesian, ellipsoid.getBodyFrame(), null, referenceLongitude), side);
  638.         }

  639.         /** Simple constructor.
  640.          * @param point coordinates
  641.          * @param side if true, the point is on a side limit, otherwise
  642.          * it is on a top/bottom limit
  643.          */
  644.         LimitPoint(final NormalizedGeodeticPoint point, final boolean side) {
  645.             this.point = point;
  646.             this.side  = side;
  647.         }

  648.         /** Get the point coordinates.
  649.          * @return point coordinates
  650.          */
  651.         public NormalizedGeodeticPoint getPoint() {
  652.             return point;
  653.         }

  654.         /** Check if point is on the side of a tile.
  655.          * @return true if the point is on a side limit, otherwise
  656.          * it is on a top/bottom limit
  657.          */
  658.         public boolean atSide() {
  659.             return side;
  660.         }

  661.     }
  662. }