DuvenhageAlgorithm.java

  1. /* Copyright 2013-2019 CS Systèmes d'Information
  2.  * Licensed to CS Systèmes d'Information (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.     /** Simple constructor.
  55.      * @param updater updater used to load Digital Elevation Model tiles
  56.      * @param maxCachedTiles maximum number of tiles stored in the cache
  57.      * @param flatBody if true, the body is considered flat, i.e. lines computed
  58.      * from entry/exit points in the DEM are considered to be straight lines also
  59.      * in geodetic coordinates. The sagitta resulting from real ellipsoid curvature
  60.      * is therefore <em>not</em> corrected in this case. As this computation is not
  61.      * costly (a few percents overhead), it is highly recommended to set this parameter
  62.      * to {@code false}. This flag is mainly intended for comparison purposes with other systems.
  63.      */
  64.     public DuvenhageAlgorithm(final TileUpdater updater, final int maxCachedTiles,
  65.                               final boolean flatBody) {
  66.         this.cache = new TilesCache<MinMaxTreeTile>(new MinMaxTreeTileFactory(), updater, maxCachedTiles);
  67.         this.flatBody = flatBody;
  68.     }

  69.     /** {@inheritDoc} */
  70.     @Override
  71.     public NormalizedGeodeticPoint intersection(final ExtendedEllipsoid ellipsoid,
  72.                                                 final Vector3D position, final Vector3D los) {

  73.         DumpManager.dumpAlgorithm(flatBody ? AlgorithmId.DUVENHAGE_FLAT_BODY : AlgorithmId.DUVENHAGE);

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

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

  78.         NormalizedGeodeticPoint current = null;
  79.         double hMax = tile.getMaxElevation();
  80.         while (current == null) {

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

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

  101.                 if (current == null) {
  102.                     throw new RuggedException(RuggedMessages.DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT);
  103.                 }

  104.             } else {
  105.                 current = ellipsoid.transform(entryP, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  106.             }

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

  113.         }

  114.         // loop along the path
  115.         while (true) {

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

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

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

  139.                 // select next tile after current point
  140.                 final Vector3D forward = new Vector3D(1.0, ellipsoid.transform(exit.getPoint()), STEP, los);
  141.                 current = ellipsoid.transform(forward, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  142.                 tile = cache.getTile(current.getLatitude(), current.getLongitude());

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

  149.             } else {

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

  164.     /** {@inheritDoc} */
  165.     @Override
  166.     public NormalizedGeodeticPoint refineIntersection(final ExtendedEllipsoid ellipsoid,
  167.                                                       final Vector3D position, final Vector3D los,
  168.                                                       final NormalizedGeodeticPoint closeGuess) {

  169.         DumpManager.dumpAlgorithm(flatBody ? AlgorithmId.DUVENHAGE_FLAT_BODY : AlgorithmId.DUVENHAGE);

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

  182.         } else {
  183.             // regular curved ellipsoid model

  184.             NormalizedGeodeticPoint currentGuess = closeGuess;

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

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

  201.                 final Vector3D                topoLOS           = ellipsoid.convertLos(normalizedProjected, los);
  202.                 final int                     iLat              = tile.getFloorLatitudeIndex(normalizedProjected.getLatitude());
  203.                 final int                     iLon              = tile.getFloorLongitudeIndex(normalizedProjected.getLongitude());
  204.                 final NormalizedGeodeticPoint foundIntersection = tile.cellIntersection(normalizedProjected, topoLOS, iLat, iLon);

  205.                 if (foundIntersection != null) {
  206.                     // nominal case, we were able to refine the intersection
  207.                     return foundIntersection;
  208.                 } else {
  209.                     // extremely rare case: we have lost the intersection

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


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

  235.             }

  236.             // no intersection found
  237.             return null;

  238.         } // end test on flatbody

  239.     }

  240.     /** {@inheritDoc} */
  241.     @Override
  242.     public double getElevation(final double latitude, final double longitude) {

  243.         DumpManager.dumpAlgorithm(flatBody ? AlgorithmId.DUVENHAGE_FLAT_BODY : AlgorithmId.DUVENHAGE);
  244.         final Tile tile = cache.getTile(latitude, longitude);
  245.         return tile.interpolateElevation(latitude, longitude);
  246.     }

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

  267.         if (depth > 30) {
  268.             // this should never happen
  269.             throw new RuggedInternalError(null);
  270.         }

  271.         if (searchDomainSize(entryLat, entryLon, exitLat, exitLon) < 4) {
  272.             // we have narrowed the search down to a few cells
  273.             return noRecurseIntersection(ellipsoid, position, los, tile,
  274.                                          entry, entryLat, entryLon, exitLat, exitLon);
  275.         }

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

  283.         NormalizedGeodeticPoint previousGP    = entry;
  284.         int                     previousLat   = entryLat;
  285.         int                     previousLon   = entryLon;
  286.         final double            angularMargin = STEP / ellipsoid.getEquatorialRadius();

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

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

  293.                 // compute segment endpoints
  294.                 final double longitude = tile.getLongitudeAtIndex(crossingLon);
  295.                 if (longitude >= FastMath.min(entry.getLongitude(), exit.getLongitude()) - angularMargin &&
  296.                     longitude <= FastMath.max(entry.getLongitude(), exit.getLongitude()) + angularMargin) {

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

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

  326.                     if (inRange(crossingLonBefore, entryLon, exitLon)) {
  327.                         // look for intersection
  328.                         final NormalizedGeodeticPoint intersection;
  329.                         if (searchDomainSize(previousLat, previousLon, crossingLat, crossingLonBefore) <
  330.                             searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  331.                             intersection = recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  332.                                                                previousGP, previousLat, previousLon,
  333.                                                                crossingGP, crossingLat, crossingLonBefore);
  334.                         } else {
  335.                             // we failed to reduce domain size, probably due to numerical problems
  336.                             intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  337.                                                                  previousGP, previousLat, previousLon,
  338.                                                                  crossingLat, crossingLonBefore);
  339.                         }
  340.                         if (intersection != null) {
  341.                             return intersection;
  342.                         }
  343.                     }

  344.                     // prepare next segment
  345.                     previousGP  = crossingGP;
  346.                     previousLat = crossingLat;
  347.                     previousLon = crossingLonAfter;

  348.                 }

  349.             }
  350.         } else {
  351.             // recurse through latitude crossings
  352.             final int[] crossings = tile.getCrossedBoundaryRows(previousLat, exitLat, level + 1);
  353.             for (final int crossingLat : crossings) {

  354.                 // compute segment endpoints
  355.                 final double latitude = tile.getLatitudeAtIndex(crossingLat);
  356.                 if (latitude >= FastMath.min(entry.getLatitude(), exit.getLatitude()) - angularMargin &&
  357.                     latitude <= FastMath.max(entry.getLatitude(), exit.getLatitude()) + angularMargin) {

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

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

  389.                     if (inRange(crossingLatBefore, entryLat, exitLat)) {
  390.                         // look for intersection
  391.                         final NormalizedGeodeticPoint intersection;
  392.                         if (searchDomainSize(previousLat, previousLon, crossingLatBefore, crossingLon) <
  393.                             searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  394.                             intersection = recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  395.                                                                previousGP, previousLat, previousLon,
  396.                                                                crossingGP, crossingLatBefore, crossingLon);
  397.                         } else {
  398.                             intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  399.                                                                  previousGP, previousLat, previousLon,
  400.                                                                  crossingLatBefore, crossingLon);
  401.                         }
  402.                         if (intersection != null) {
  403.                             return intersection;
  404.                         }
  405.                     }

  406.                     // prepare next segment
  407.                     previousGP  = crossingGP;
  408.                     previousLat = crossingLatAfter;
  409.                     previousLon = crossingLon;

  410.                 }

  411.             }
  412.         }

  413.         if (inRange(previousLat, entryLat, exitLat) && inRange(previousLon, entryLon, exitLon)) {
  414.             // last part of the segment, up to exit point
  415.             if (searchDomainSize(previousLat, previousLon, exitLat, exitLon) <
  416.                 searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  417.                 return recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  418.                                            previousGP, previousLat, previousLon,
  419.                                            exit, exitLat, exitLon);
  420.             } else {
  421.                 return noRecurseIntersection(ellipsoid, position, los, tile,
  422.                                              previousGP, previousLat, previousLon,
  423.                                              exitLat, exitLon);
  424.             }
  425.         } else {
  426.             return null;
  427.         }

  428.     }

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

  448.         NormalizedGeodeticPoint intersectionGP = null;
  449.         double intersectionDot = Double.POSITIVE_INFINITY;
  450.         for (int i = FastMath.min(entryLat, exitLat); i <= FastMath.max(entryLat, exitLat); ++i) {
  451.             for (int j = FastMath.min(entryLon, exitLon); j <= FastMath.max(entryLon, exitLon); ++j) {
  452.                 final NormalizedGeodeticPoint gp = tile.cellIntersection(entry, ellipsoid.convertLos(entry, los), i, j);
  453.                 if (gp != null) {

  454.                     // improve the point, by projecting it back on the 3D line, fixing the small body curvature at cell level
  455.                     final Vector3D delta = ellipsoid.transform(gp).subtract(position);
  456.                     final double   s     = Vector3D.dotProduct(delta, los) / los.getNormSq();
  457.                     if (s > 0) {
  458.                         final GeodeticPoint projected = ellipsoid.transform(new Vector3D(1, position, s, los),
  459.                                                                             ellipsoid.getBodyFrame(), null);
  460.                         final NormalizedGeodeticPoint normalizedProjected = new NormalizedGeodeticPoint(projected.getLatitude(),
  461.                                                                                                         projected.getLongitude(),
  462.                                                                                                         projected.getAltitude(),
  463.                                                                                                         gp.getLongitude());
  464.                         final NormalizedGeodeticPoint gpImproved = tile.cellIntersection(normalizedProjected,
  465.                                                                                          ellipsoid.convertLos(normalizedProjected, los),
  466.                                                                                          i, j);

  467.                         if (gpImproved != null) {
  468.                             final Vector3D point = ellipsoid.transform(gpImproved);
  469.                             final double dot = Vector3D.dotProduct(point.subtract(position), los);
  470.                             if (dot < intersectionDot) {
  471.                                 intersectionGP  = gpImproved;
  472.                                 intersectionDot = dot;
  473.                             }
  474.                         }
  475.                     }

  476.                 }
  477.             }
  478.         }

  479.         return intersectionGP;

  480.     }

  481.     /** Compute the size of a search domain.
  482.      * @param entryLat index to use for interpolating entry point elevation
  483.      * @param entryLon index to use for interpolating entry point elevation
  484.      * @param exitLat index to use for interpolating exit point elevation
  485.      * @param exitLon index to use for interpolating exit point elevation
  486.      * @return size of the search domain
  487.      */
  488.     private int searchDomainSize(final int entryLat, final int entryLon,
  489.                                  final int exitLat, final int exitLon) {
  490.         return (FastMath.abs(entryLat - exitLat) + 1) * (FastMath.abs(entryLon - exitLon) + 1);
  491.     }

  492.     /** Check if an index is inside a range.
  493.      * @param i index to check
  494.      * @param a first bound of the range (may be either below or above b)
  495.      * @param b second bound of the range (may be either below or above a)
  496.      * @return true if i is between a and b (inclusive)
  497.      */
  498.     private boolean inRange(final int i, final int a, final int b) {
  499.         return i >= FastMath.min(a, b) && i <= FastMath.max(a, b);
  500.     }

  501.     /** Compute a line-of-sight exit point from a tile.
  502.      * @param tile tile to consider
  503.      * @param ellipsoid reference ellipsoid
  504.      * @param position pixel position in ellipsoid frame
  505.      * @param los pixel line-of-sight in ellipsoid frame
  506.      * @return exit point
  507.      */
  508.     private LimitPoint findExit(final Tile tile, final ExtendedEllipsoid ellipsoid,
  509.                                 final Vector3D position, final Vector3D los) {

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

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

  557.             default :
  558.                 // this should never happen
  559.                 throw new RuggedInternalError(null);
  560.         }

  561.     }

  562.     /** Select point closest to line-of-sight start.
  563.      * @param p1 first point to consider
  564.      * @param p2 second point to consider
  565.      * @param position pixel position in ellipsoid frame
  566.      * @return either p1 or p2, depending on which is closest to position
  567.      */
  568.     private Vector3D selectClosest(final Vector3D p1, final Vector3D p2, final Vector3D position) {
  569.         return Vector3D.distance(p1, position) <= Vector3D.distance(p2, position) ? p1 : p2;
  570.     }

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

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

  607.     /** Point at tile boundary. */
  608.     private static class LimitPoint {

  609.         /** Coordinates. */
  610.         private final NormalizedGeodeticPoint point;

  611.         /** Limit status. */
  612.         private final boolean side;

  613.         /** Simple constructor.
  614.          * @param ellipsoid reference ellipsoid
  615.          * @param referenceLongitude reference longitude lc such that the point longitude will
  616.          * be normalized between lc-π and lc+π
  617.          * @param cartesian Cartesian point
  618.          * @param side if true, the point is on a side limit, otherwise
  619.          * it is on a top/bottom limit
  620.          */
  621.         LimitPoint(final ExtendedEllipsoid ellipsoid, final double referenceLongitude,
  622.                    final Vector3D cartesian, final boolean side) {
  623.             this(ellipsoid.transform(cartesian, ellipsoid.getBodyFrame(), null, referenceLongitude), side);
  624.         }

  625.         /** Simple constructor.
  626.          * @param point coordinates
  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 NormalizedGeodeticPoint point, final boolean side) {
  631.             this.point = point;
  632.             this.side  = side;
  633.         }

  634.         /** Get the point coordinates.
  635.          * @return point coordinates
  636.          */
  637.         public NormalizedGeodeticPoint getPoint() {
  638.             return point;
  639.         }

  640.         /** Check if point is on the side of a tile.
  641.          * @return true if the point is on a side limit, otherwise
  642.          * it is on a top/bottom limit
  643.          */
  644.         public boolean atSide() {
  645.             return side;
  646.         }

  647.     }

  648. }