DuvenhageAlgorithm.java

  1. /* Copyright 2013-2022 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="http://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.      */
  67.     public DuvenhageAlgorithm(final TileUpdater updater, final int maxCachedTiles,
  68.                               final boolean flatBody) {
  69.         this.cache = new TilesCache<>(new MinMaxTreeTileFactory(), updater, maxCachedTiles);
  70.         this.flatBody = flatBody;
  71.         this.algorithmId = flatBody ? AlgorithmId.DUVENHAGE_FLAT_BODY : AlgorithmId.DUVENHAGE;
  72.     }

  73.     /** {@inheritDoc} */
  74.     @Override
  75.     public NormalizedGeodeticPoint intersection(final ExtendedEllipsoid ellipsoid,
  76.                                                 final Vector3D position, final Vector3D los) {

  77.         DumpManager.dumpAlgorithm(this.algorithmId);

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

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

  82.         NormalizedGeodeticPoint current = null;
  83.         double hMax = tile.getMaxElevation();
  84.         while (current == null) {

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

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

  105.                 if (current == null) {
  106.                     throw new RuggedException(RuggedMessages.DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT);
  107.                 }

  108.             } else {
  109.                 current = ellipsoid.transform(entryP, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  110.             }

  111.             if (tile.getLocation(current.getLatitude(), current.getLongitude()) != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
  112.                 // the entry point is in another tile
  113.                 tile    = cache.getTile(current.getLatitude(), current.getLongitude());
  114.                 hMax    = FastMath.max(hMax, tile.getMaxElevation());
  115.                 current = null;
  116.             }

  117.         }

  118.         // loop along the path
  119.         while (true) {

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

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

  138.             if (intersection != null) {
  139.                 // we have found the intersection
  140.                 return intersection;
  141.             } else if (exit.atSide()) {
  142.                 // no intersection on this tile, we can proceed to next part of the line-of-sight

  143.                 // select next tile after current point
  144.                 final Vector3D forward = new Vector3D(1.0, ellipsoid.transform(exit.getPoint()), STEP, los);
  145.                 current = ellipsoid.transform(forward, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  146.                 tile = cache.getTile(current.getLatitude(), current.getLongitude());

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

  153.             } else {

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

  168.     /** {@inheritDoc} */
  169.     @Override
  170.     public NormalizedGeodeticPoint refineIntersection(final ExtendedEllipsoid ellipsoid,
  171.                                                       final Vector3D position, final Vector3D los,
  172.                                                       final NormalizedGeodeticPoint closeGuess) {

  173.         DumpManager.dumpAlgorithm(this.algorithmId);

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

  186.         } else {
  187.             // regular curved ellipsoid model

  188.             NormalizedGeodeticPoint currentGuess = closeGuess;

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

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

  205.                 final Vector3D                topoLOS           = ellipsoid.convertLos(normalizedProjected, los);
  206.                 final int                     iLat              = tile.getFloorLatitudeIndex(normalizedProjected.getLatitude());
  207.                 final int                     iLon              = tile.getFloorLongitudeIndex(normalizedProjected.getLongitude());
  208.                 final NormalizedGeodeticPoint foundIntersection = tile.cellIntersection(normalizedProjected, topoLOS, iLat, iLon);

  209.                 if (foundIntersection != null) {
  210.                     // nominal case, we were able to refine the intersection
  211.                     return foundIntersection;
  212.                 } else {
  213.                     // extremely rare case: we have lost the intersection

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


  232.                     // We recompute fully a new guess, starting from the point after current cell
  233.                     final GeodeticPoint currentGuessGP = intersection(ellipsoid, cellExit, los);
  234.                     currentGuess = new NormalizedGeodeticPoint(currentGuessGP.getLatitude(),
  235.                                                                currentGuessGP.getLongitude(),
  236.                                                                currentGuessGP.getAltitude(),
  237.                                                                projected.getLongitude());
  238.                 }

  239.             }

  240.             // no intersection found
  241.             return null;

  242.         } // end test on flatbody

  243.     }

  244.     /** {@inheritDoc} */
  245.     @Override
  246.     public double getElevation(final double latitude, final double longitude) {

  247.         DumpManager.dumpAlgorithm(this.algorithmId);
  248.         final Tile tile = cache.getTile(latitude, longitude);
  249.         return tile.interpolateElevation(latitude, longitude);
  250.     }

  251.     /** {@inheritDoc} */
  252.     @Override
  253.     public AlgorithmId getAlgorithmId() {
  254.         return this.algorithmId;
  255.     }

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

  276.         if (depth > 30) {
  277.             // this should never happen
  278.             throw new RuggedInternalError(null);
  279.         }

  280.         if (searchDomainSize(entryLat, entryLon, exitLat, exitLon) < 4) {
  281.             // we have narrowed the search down to a few cells
  282.             return noRecurseIntersection(ellipsoid, position, los, tile,
  283.                                          entry, entryLat, entryLon, exitLat, exitLon);
  284.         }

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

  292.         NormalizedGeodeticPoint previousGP    = entry;
  293.         int                     previousLat   = entryLat;
  294.         int                     previousLon   = entryLon;
  295.         final double            angularMargin = STEP / ellipsoid.getEquatorialRadius();

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

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

  302.                 // compute segment endpoints
  303.                 final double longitude = tile.getLongitudeAtIndex(crossingLon);
  304.                 if (longitude >= FastMath.min(entry.getLongitude(), exit.getLongitude()) - angularMargin &&
  305.                     longitude <= FastMath.max(entry.getLongitude(), exit.getLongitude()) + angularMargin) {

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

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

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

  353.                     // prepare next segment
  354.                     previousGP  = crossingGP;
  355.                     previousLat = crossingLat;
  356.                     previousLon = crossingLonAfter;

  357.                 }

  358.             }
  359.         } else {
  360.             // recurse through latitude crossings
  361.             final int[] crossings = tile.getCrossedBoundaryRows(previousLat, exitLat, level + 1);
  362.             for (final int crossingLat : crossings) {

  363.                 // compute segment endpoints
  364.                 final double latitude = tile.getLatitudeAtIndex(crossingLat);
  365.                 if (latitude >= FastMath.min(entry.getLatitude(), exit.getLatitude()) - angularMargin &&
  366.                     latitude <= FastMath.max(entry.getLatitude(), exit.getLatitude()) + angularMargin) {

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

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

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

  415.                     // prepare next segment
  416.                     previousGP  = crossingGP;
  417.                     previousLat = crossingLatAfter;
  418.                     previousLon = crossingLon;

  419.                 }

  420.             }
  421.         }

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

  437.     }

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

  457.         NormalizedGeodeticPoint intersectionGP = null;
  458.         double intersectionDot = Double.POSITIVE_INFINITY;
  459.         for (int i = FastMath.min(entryLat, exitLat); i <= FastMath.max(entryLat, exitLat); ++i) {
  460.             for (int j = FastMath.min(entryLon, exitLon); j <= FastMath.max(entryLon, exitLon); ++j) {
  461.                 final NormalizedGeodeticPoint gp = tile.cellIntersection(entry, ellipsoid.convertLos(entry, los), i, j);
  462.                 if (gp != null) {

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

  476.                         if (gpImproved != null) {
  477.                             final Vector3D point = ellipsoid.transform(gpImproved);
  478.                             final double dot = Vector3D.dotProduct(point.subtract(position), los);
  479.                             if (dot < intersectionDot) {
  480.                                 intersectionGP  = gpImproved;
  481.                                 intersectionDot = dot;
  482.                             }
  483.                         }
  484.                     }

  485.                 }
  486.             }
  487.         }

  488.         return intersectionGP;

  489.     }

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

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

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

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

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

  566.             default :
  567.                 // this should never happen
  568.                 throw new RuggedInternalError(null);
  569.         }

  570.     }

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

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

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

  616.     /** Point at tile boundary. */
  617.     private static class LimitPoint {

  618.         /** Coordinates. */
  619.         private final NormalizedGeodeticPoint point;

  620.         /** Limit status. */
  621.         private final boolean side;

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

  634.         /** Simple constructor.
  635.          * @param point coordinates
  636.          * @param side if true, the point is on a side limit, otherwise
  637.          * it is on a top/bottom limit
  638.          */
  639.         LimitPoint(final NormalizedGeodeticPoint point, final boolean side) {
  640.             this.point = point;
  641.             this.side  = side;
  642.         }

  643.         /** Get the point coordinates.
  644.          * @return point coordinates
  645.          */
  646.         public NormalizedGeodeticPoint getPoint() {
  647.             return point;
  648.         }

  649.         /** Check if point is on the side of a tile.
  650.          * @return true if the point is on a side limit, otherwise
  651.          * it is on a top/bottom limit
  652.          */
  653.         public boolean atSide() {
  654.             return side;
  655.         }

  656.     }
  657. }