DuvenhageAlgorithm.java

  1. /* Copyright 2013-2017 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.errors.OrekitException;
  22. import org.orekit.rugged.api.AlgorithmId;
  23. import org.orekit.rugged.errors.DumpManager;
  24. import org.orekit.rugged.errors.RuggedException;
  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.  */
  40. public class DuvenhageAlgorithm implements IntersectionAlgorithm {

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

  43.     /** Cache for DEM tiles. */
  44.     private final TilesCache<MinMaxTreeTile> cache;

  45.     /** Flag for flat-body hypothesis. */
  46.     private final boolean flatBody;

  47.     /** Simple constructor.
  48.      * @param updater updater used to load Digital Elevation Model tiles
  49.      * @param maxCachedTiles maximum number of tiles stored in the cache
  50.      * @param flatBody if true, the body is considered flat, i.e. lines computed
  51.      * from entry/exit points in the DEM are considered to be straight lines also
  52.      * in geodetic coordinates. The sagitta resulting from real ellipsoid curvature
  53.      * is therefore <em>not</em> corrected in this case. As this computation is not
  54.      * costly (a few percents overhead), it is highly recommended to set this parameter
  55.      * to {@code false}. This flag is mainly intended for comparison purposes with other systems.
  56.      */
  57.     public DuvenhageAlgorithm(final TileUpdater updater, final int maxCachedTiles,
  58.                               final boolean flatBody) {
  59.         this.cache = new TilesCache<MinMaxTreeTile>(new MinMaxTreeTileFactory(), updater, maxCachedTiles);
  60.         this.flatBody = flatBody;
  61.     }

  62.     /** {@inheritDoc} */
  63.     @Override
  64.     public NormalizedGeodeticPoint intersection(final ExtendedEllipsoid ellipsoid,
  65.                                                 final Vector3D position, final Vector3D los)
  66.         throws RuggedException {
  67.         try {

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

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

  71.             // compute atmosphere deviation

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

  74.             NormalizedGeodeticPoint current = null;
  75.             double hMax = tile.getMaxElevation();
  76.             while (current == null) {

  77.                 // find where line-of-sight crosses tile max altitude
  78.                 final Vector3D entryP = ellipsoid.pointAtAltitude(position, los, hMax + STEP);
  79.                 if (Vector3D.dotProduct(entryP.subtract(position), los) < 0) {
  80.                     // the entry point is behind spacecraft!
  81.                     throw new RuggedException(RuggedMessages.DEM_ENTRY_POINT_IS_BEHIND_SPACECRAFT);
  82.                 }
  83.                 current = ellipsoid.transform(entryP, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());

  84.                 if (tile.getLocation(current.getLatitude(), current.getLongitude()) != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
  85.                     // the entry point is in another tile
  86.                     tile    = cache.getTile(current.getLatitude(), current.getLongitude());
  87.                     hMax    = FastMath.max(hMax, tile.getMaxElevation());
  88.                     current = null;
  89.                 }

  90.             }

  91.             // loop along the path
  92.             while (true) {

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

  95.                 // compute intersection with Digital Elevation Model
  96.                 final int entryLat = FastMath.max(0,
  97.                                                   FastMath.min(tile.getLatitudeRows() - 1,
  98.                                                                tile.getFloorLatitudeIndex(current.getLatitude())));
  99.                 final int entryLon = FastMath.max(0,
  100.                                                   FastMath.min(tile.getLongitudeColumns() - 1,
  101.                                                                tile.getFloorLongitudeIndex(current.getLongitude())));
  102.                 final int exitLat  = FastMath.max(0,
  103.                                                   FastMath.min(tile.getLatitudeRows() - 1,
  104.                                                                tile.getFloorLatitudeIndex(exit.getPoint().getLatitude())));
  105.                 final int exitLon  = FastMath.max(0,
  106.                                                   FastMath.min(tile.getLongitudeColumns() - 1,
  107.                                                                tile.getFloorLongitudeIndex(exit.getPoint().getLongitude())));
  108.                 NormalizedGeodeticPoint intersection = recurseIntersection(0, ellipsoid, position, los, tile,
  109.                                                                            current, entryLat, entryLon,
  110.                                                                            exit.getPoint(), exitLat, exitLon);

  111.                 if (intersection != null) {
  112.                     // we have found the intersection
  113.                     return intersection;
  114.                 } else if (exit.atSide()) {
  115.                     // no intersection on this tile, we can proceed to next part of the line-of-sight

  116.                     // select next tile after current point
  117.                     final Vector3D forward = new Vector3D(1.0, ellipsoid.transform(exit.getPoint()), STEP, los);
  118.                     current = ellipsoid.transform(forward, ellipsoid.getBodyFrame(), null, tile.getMinimumLongitude());
  119.                     tile = cache.getTile(current.getLatitude(), current.getLongitude());

  120.                     if (tile.interpolateElevation(current.getLatitude(), current.getLongitude()) >= current.getAltitude()) {
  121.                         // extremely rare case! The line-of-sight traversed the Digital Elevation Model
  122.                         // during the very short forward step we used to move to next tile
  123.                         // we consider this point to be OK
  124.                         return current;
  125.                     }

  126.                 } else {

  127.                     // this should never happen
  128.                     // we should have left the loop with an intersection point
  129.                     // try a fallback non-recursive search
  130.                     intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  131.                                                          current, entryLat, entryLon,
  132.                                                          exitLat, exitLon);
  133.                     if (intersection != null) {
  134.                         return intersection;
  135.                     } else {
  136.                         throw RuggedException.createInternalError(null);
  137.                     }

  138.                 }


  139.             }


  140.         } catch (OrekitException oe) {
  141.             throw new RuggedException(oe, oe.getSpecifier(), oe.getParts());
  142.         }
  143.     }

  144.     /** {@inheritDoc} */
  145.     @Override
  146.     public NormalizedGeodeticPoint refineIntersection(final ExtendedEllipsoid ellipsoid,
  147.                                                       final Vector3D position, final Vector3D los,
  148.                                                       final NormalizedGeodeticPoint closeGuess)
  149.         throws RuggedException {
  150.         try {

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

  152.             if (flatBody) {
  153.                 // under the (bad) flat-body assumption, the reference point must remain
  154.                 // at DEM entry and exit, even if we already have a much better close guess :-(
  155.                 // this is in order to remain consistent with other systems
  156.                 final Tile tile = cache.getTile(closeGuess.getLatitude(), closeGuess.getLongitude());
  157.                 final Vector3D      exitP  = ellipsoid.pointAtAltitude(position, los, tile.getMinElevation());
  158.                 final Vector3D      entryP = ellipsoid.pointAtAltitude(position, los, tile.getMaxElevation());
  159.                 final NormalizedGeodeticPoint entry  = ellipsoid.transform(entryP, ellipsoid.getBodyFrame(), null,
  160.                                                                            tile.getMinimumLongitude());
  161.                 return tile.cellIntersection(entry, ellipsoid.convertLos(entryP, exitP),
  162.                                               tile.getFloorLatitudeIndex(closeGuess.getLatitude()),
  163.                                               tile.getFloorLongitudeIndex(closeGuess.getLongitude()));
  164.             } else {
  165.                 final Vector3D      delta     = ellipsoid.transform(closeGuess).subtract(position);
  166.                 final double        s         = Vector3D.dotProduct(delta, los) / los.getNormSq();
  167.                 final GeodeticPoint projected = ellipsoid.transform(new Vector3D(1, position, s, los),
  168.                                                                     ellipsoid.getBodyFrame(), null);
  169.                 final NormalizedGeodeticPoint normalizedProjected = new NormalizedGeodeticPoint(projected.getLatitude(),
  170.                                                                                                 projected.getLongitude(),
  171.                                                                                                 projected.getAltitude(),
  172.                                                                                                 closeGuess.getLongitude());
  173.                 final Tile          tile      = cache.getTile(normalizedProjected.getLatitude(),
  174.                                                               normalizedProjected.getLongitude());
  175.                 return tile.cellIntersection(normalizedProjected, ellipsoid.convertLos(normalizedProjected, los),
  176.                                               tile.getFloorLatitudeIndex(normalizedProjected.getLatitude()),
  177.                                               tile.getFloorLongitudeIndex(normalizedProjected.getLongitude()));
  178.             }
  179.         } catch (OrekitException oe) {
  180.             throw new RuggedException(oe, oe.getSpecifier(), oe.getParts());
  181.         }
  182.     }

  183.     /** {@inheritDoc} */
  184.     @Override
  185.     public double getElevation(final double latitude, final double longitude)
  186.         throws RuggedException {
  187.         DumpManager.dumpAlgorithm(flatBody ? AlgorithmId.DUVENHAGE_FLAT_BODY : AlgorithmId.DUVENHAGE);
  188.         final Tile tile = cache.getTile(latitude, longitude);
  189.         return tile.interpolateElevation(latitude, longitude);
  190.     }

  191.     /** Compute intersection of line with Digital Elevation Model in a sub-tile.
  192.      * @param depth recursion depth
  193.      * @param ellipsoid reference ellipsoid
  194.      * @param position pixel position in ellipsoid frame
  195.      * @param los pixel line-of-sight in ellipsoid frame
  196.      * @param tile Digital Elevation Model tile
  197.      * @param entry line-of-sight entry point in the sub-tile
  198.      * @param entryLat index to use for interpolating entry point elevation
  199.      * @param entryLon index to use for interpolating entry point elevation
  200.      * @param exit line-of-sight exit point from the sub-tile
  201.      * @param exitLat index to use for interpolating exit point elevation
  202.      * @param exitLon index to use for interpolating exit point elevation
  203.      * @return point at which the line first enters ground, or null if does not enter
  204.      * ground in the search sub-tile
  205.      * @exception RuggedException if intersection cannot be found
  206.      * @exception OrekitException if points cannot be converted to geodetic coordinates
  207.      */
  208.     private NormalizedGeodeticPoint recurseIntersection(final int depth, final ExtendedEllipsoid ellipsoid,
  209.                                                         final Vector3D position, final Vector3D los,
  210.                                                         final MinMaxTreeTile tile,
  211.                                                         final NormalizedGeodeticPoint entry, final int entryLat, final int entryLon,
  212.                                                         final NormalizedGeodeticPoint exit, final int exitLat, final int exitLon)
  213.         throws RuggedException, OrekitException {

  214.         if (depth > 30) {
  215.             // this should never happen
  216.             throw RuggedException.createInternalError(null);
  217.         }

  218.         if (searchDomainSize(entryLat, entryLon, exitLat, exitLon) < 4) {
  219.             // we have narrowed the search down to a few cells
  220.             return noRecurseIntersection(ellipsoid, position, los, tile,
  221.                                          entry, entryLat, entryLon, exitLat, exitLon);
  222.         }

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

  230.         NormalizedGeodeticPoint previousGP    = entry;
  231.         int                     previousLat   = entryLat;
  232.         int                     previousLon   = entryLon;
  233.         final double            angularMargin = STEP / ellipsoid.getEquatorialRadius();

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

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

  240.                 // compute segment endpoints
  241.                 final double longitude = tile.getLongitudeAtIndex(crossingLon);
  242.                 if (longitude >= FastMath.min(entry.getLongitude(), exit.getLongitude()) - angularMargin &&
  243.                     longitude <= FastMath.max(entry.getLongitude(), exit.getLongitude()) + angularMargin) {

  244.                     NormalizedGeodeticPoint crossingGP = null;
  245.                     if (!flatBody) {
  246.                         try {
  247.                             // full computation of crossing point
  248.                             final Vector3D crossingP = ellipsoid.pointAtLongitude(position, los, longitude);
  249.                             crossingGP = ellipsoid.transform(crossingP, ellipsoid.getBodyFrame(), null,
  250.                                                              tile.getMinimumLongitude());
  251.                         } catch  (RuggedException re) {
  252.                             // in some very rare cases of numerical noise, we miss the crossing point
  253.                             crossingGP = null;
  254.                         }
  255.                     }
  256.                     if (crossingGP == null) {
  257.                         // linear approximation of crossing point
  258.                         final double d  = exit.getLongitude() - entry.getLongitude();
  259.                         final double cN = (exit.getLongitude() - longitude) / d;
  260.                         final double cX = (longitude - entry.getLongitude()) / d;
  261.                         crossingGP = new NormalizedGeodeticPoint(cN * entry.getLatitude() + cX * exit.getLatitude(),
  262.                                                                  longitude,
  263.                                                                  cN * entry.getAltitude() + cX * exit.getAltitude(),
  264.                                                                  tile.getMinimumLongitude());
  265.                     }
  266.                     final int crossingLat =
  267.                             FastMath.max(0,
  268.                                          FastMath.min(tile.getLatitudeRows() - 1,
  269.                                                       tile.getFloorLatitudeIndex(crossingGP.getLatitude())));

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

  273.                     if (inRange(crossingLonBefore, entryLon, exitLon)) {
  274.                         // look for intersection
  275.                         final NormalizedGeodeticPoint intersection;
  276.                         if (searchDomainSize(previousLat, previousLon, crossingLat, crossingLonBefore) <
  277.                             searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  278.                             intersection = recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  279.                                                                previousGP, previousLat, previousLon,
  280.                                                                crossingGP, crossingLat, crossingLonBefore);
  281.                         } else {
  282.                             // we failed to reduce domain size, probably due to numerical problems
  283.                             intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  284.                                                                  previousGP, previousLat, previousLon,
  285.                                                                  crossingLat, crossingLonBefore);
  286.                         }
  287.                         if (intersection != null) {
  288.                             return intersection;
  289.                         }
  290.                     }

  291.                     // prepare next segment
  292.                     previousGP  = crossingGP;
  293.                     previousLat = crossingLat;
  294.                     previousLon = crossingLonAfter;

  295.                 }

  296.             }
  297.         } else {
  298.             // recurse through latitude crossings
  299.             final int[] crossings = tile.getCrossedBoundaryRows(previousLat, exitLat, level + 1);
  300.             for (final int crossingLat : crossings) {

  301.                 // compute segment endpoints
  302.                 final double latitude = tile.getLatitudeAtIndex(crossingLat);
  303.                 if (latitude >= FastMath.min(entry.getLatitude(), exit.getLatitude()) - angularMargin &&
  304.                     latitude <= FastMath.max(entry.getLatitude(), exit.getLatitude()) + angularMargin) {

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

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

  336.                     if (inRange(crossingLatBefore, entryLat, exitLat)) {
  337.                         // look for intersection
  338.                         final NormalizedGeodeticPoint intersection;
  339.                         if (searchDomainSize(previousLat, previousLon, crossingLatBefore, crossingLon) <
  340.                             searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  341.                             intersection = recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  342.                                                                previousGP, previousLat, previousLon,
  343.                                                                crossingGP, crossingLatBefore, crossingLon);
  344.                         } else {
  345.                             intersection = noRecurseIntersection(ellipsoid, position, los, tile,
  346.                                                                  previousGP, previousLat, previousLon,
  347.                                                                  crossingLatBefore, crossingLon);
  348.                         }
  349.                         if (intersection != null) {
  350.                             return intersection;
  351.                         }
  352.                     }

  353.                     // prepare next segment
  354.                     previousGP  = crossingGP;
  355.                     previousLat = crossingLatAfter;
  356.                     previousLon = crossingLon;

  357.                 }

  358.             }
  359.         }

  360.         if (inRange(previousLat, entryLat, exitLat) && inRange(previousLon, entryLon, exitLon)) {
  361.             // last part of the segment, up to exit point
  362.             if (searchDomainSize(previousLat, previousLon, exitLat, exitLon) <
  363.                 searchDomainSize(entryLat, entryLon, exitLat, exitLon)) {
  364.                 return recurseIntersection(depth + 1, ellipsoid, position, los, tile,
  365.                                            previousGP, previousLat, previousLon,
  366.                                            exit, exitLat, exitLon);
  367.             } else {
  368.                 return noRecurseIntersection(ellipsoid, position, los, tile,
  369.                                              previousGP, previousLat, previousLon,
  370.                                              exitLat, exitLon);
  371.             }
  372.         } else {
  373.             return null;
  374.         }

  375.     }

  376.     /** Compute intersection of line with Digital Elevation Model in a sub-tile, without recursion.
  377.      * @param ellipsoid reference ellipsoid
  378.      * @param position pixel position in ellipsoid frame
  379.      * @param los pixel line-of-sight in ellipsoid frame
  380.      * @param tile Digital Elevation Model tile
  381.      * @param entry line-of-sight entry point in the sub-tile
  382.      * @param entryLat index to use for interpolating entry point elevation
  383.      * @param entryLon index to use for interpolating entry point elevation
  384.      * @param exitLat index to use for interpolating exit point elevation
  385.      * @param exitLon index to use for interpolating exit point elevation
  386.      * @return point at which the line first enters ground, or null if does not enter
  387.      * ground in the search sub-tile
  388.      * @exception RuggedException if intersection cannot be found
  389.      * @exception OrekitException if points cannot be converted to geodetic coordinates
  390.      */
  391.     private NormalizedGeodeticPoint noRecurseIntersection(final ExtendedEllipsoid ellipsoid,
  392.                                                           final Vector3D position, final Vector3D los,
  393.                                                           final MinMaxTreeTile tile,
  394.                                                           final NormalizedGeodeticPoint entry,
  395.                                                           final int entryLat, final int entryLon,
  396.                                                           final int exitLat, final int exitLon)
  397.         throws OrekitException, RuggedException {

  398.         NormalizedGeodeticPoint intersectionGP = null;
  399.         double intersectionDot = Double.POSITIVE_INFINITY;
  400.         for (int i = FastMath.min(entryLat, exitLat); i <= FastMath.max(entryLat, exitLat); ++i) {
  401.             for (int j = FastMath.min(entryLon, exitLon); j <= FastMath.max(entryLon, exitLon); ++j) {
  402.                 final NormalizedGeodeticPoint gp = tile.cellIntersection(entry, ellipsoid.convertLos(entry, los), i, j);
  403.                 if (gp != null) {

  404.                     // improve the point, by projecting it back on the 3D line, fixing the small body curvature at cell level
  405.                     final Vector3D      delta     = ellipsoid.transform(gp).subtract(position);
  406.                     final double        s         = Vector3D.dotProduct(delta, los) / los.getNormSq();
  407.                     final GeodeticPoint projected = ellipsoid.transform(new Vector3D(1, position, s, los),
  408.                                                                         ellipsoid.getBodyFrame(), null);
  409.                     final NormalizedGeodeticPoint normalizedProjected = new NormalizedGeodeticPoint(projected.getLatitude(),
  410.                                                                                                     projected.getLongitude(),
  411.                                                                                                     projected.getAltitude(),
  412.                                                                                                     gp.getLongitude());
  413.                     final NormalizedGeodeticPoint gpImproved = tile.cellIntersection(normalizedProjected,
  414.                                                                                      ellipsoid.convertLos(normalizedProjected, los),
  415.                                                                                      i, j);

  416.                     if (gpImproved != null) {
  417.                         final Vector3D point = ellipsoid.transform(gpImproved);
  418.                         final double dot = Vector3D.dotProduct(point.subtract(position), los);
  419.                         if (dot < intersectionDot) {
  420.                             intersectionGP  = gpImproved;
  421.                             intersectionDot = dot;
  422.                         }
  423.                     }

  424.                 }
  425.             }
  426.         }

  427.         return intersectionGP;

  428.     }

  429.     /** Compute the size of a search domain.
  430.      * @param entryLat index to use for interpolating entry point elevation
  431.      * @param entryLon index to use for interpolating entry point elevation
  432.      * @param exitLat index to use for interpolating exit point elevation
  433.      * @param exitLon index to use for interpolating exit point elevation
  434.      * @return size of the search domain
  435.      */
  436.     private int searchDomainSize(final int entryLat, final int entryLon,
  437.                                  final int exitLat, final int exitLon) {
  438.         return (FastMath.abs(entryLat - exitLat) + 1) * (FastMath.abs(entryLon - exitLon) + 1);
  439.     }

  440.     /** Check if an index is inside a range.
  441.      * @param i index to check
  442.      * @param a first bound of the range (may be either below or above b)
  443.      * @param b second bound of the range (may be either below or above a)
  444.      * @return true if i is between a and b (inclusive)
  445.      */
  446.     private boolean inRange(final int i, final int a, final int b) {
  447.         return i >= FastMath.min(a, b) && i <= FastMath.max(a, b);
  448.     }

  449.     /** Compute a line-of-sight exit point from a tile.
  450.      * @param tile tile to consider
  451.      * @param ellipsoid reference ellipsoid
  452.      * @param position pixel position in ellipsoid frame
  453.      * @param los pixel line-of-sight in ellipsoid frame
  454.      * @return exit point
  455.      * @exception RuggedException if exit point cannot be found
  456.      * @exception OrekitException if geodetic coordinates cannot be computed
  457.      */
  458.     private LimitPoint findExit(final Tile tile, final ExtendedEllipsoid ellipsoid,
  459.                                 final Vector3D position, final Vector3D los)
  460.         throws RuggedException, OrekitException {

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

  465.         switch (tile.getLocation(exitGP.getLatitude(), exitGP.getLongitude())) {
  466.             case SOUTH_WEST :
  467.                 return new LimitPoint(ellipsoid, reference,
  468.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMinimumLatitude(),  exitP),
  469.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMinimumLongitude(), exitP),
  470.                                                     position),
  471.                                       true);
  472.             case WEST :
  473.                 return new LimitPoint(ellipsoid, reference,
  474.                                       longitudeCrossing(ellipsoid, position, los, tile.getMinimumLongitude(), exitP),
  475.                                       true);
  476.             case NORTH_WEST:
  477.                 return new LimitPoint(ellipsoid, reference,
  478.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMaximumLatitude(),  exitP),
  479.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMinimumLongitude(), exitP),
  480.                                                     position),
  481.                                       true);
  482.             case NORTH :
  483.                 return new LimitPoint(ellipsoid, reference,
  484.                                       latitudeCrossing(ellipsoid, position, los, tile.getMaximumLatitude(), exitP),
  485.                                       true);
  486.             case NORTH_EAST :
  487.                 return new LimitPoint(ellipsoid, reference,
  488.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMaximumLatitude(),  exitP),
  489.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMaximumLongitude(), exitP),
  490.                                                     position),
  491.                                       true);
  492.             case EAST :
  493.                 return new LimitPoint(ellipsoid, reference,
  494.                                       longitudeCrossing(ellipsoid, position, los, tile.getMaximumLongitude(), exitP),
  495.                                       true);
  496.             case SOUTH_EAST :
  497.                 return new LimitPoint(ellipsoid, reference,
  498.                                       selectClosest(latitudeCrossing(ellipsoid, position,  los, tile.getMinimumLatitude(),  exitP),
  499.                                                     longitudeCrossing(ellipsoid, position, los, tile.getMaximumLongitude(), exitP),
  500.                                                     position),
  501.                                       true);
  502.             case SOUTH :
  503.                 return new LimitPoint(ellipsoid, reference,
  504.                                       latitudeCrossing(ellipsoid, position, los, tile.getMinimumLatitude(), exitP),
  505.                                       true);
  506.             case HAS_INTERPOLATION_NEIGHBORS :
  507.                 return new LimitPoint(exitGP, false);

  508.             default :
  509.                 // this should never happen
  510.                 throw RuggedException.createInternalError(null);
  511.         }

  512.     }

  513.     /** Select point closest to line-of-sight start.
  514.      * @param p1 first point to consider
  515.      * @param p2 second point to consider
  516.      * @param position pixel position in ellipsoid frame
  517.      * @return either p1 or p2, depending on which is closest to position
  518.      */
  519.     private Vector3D selectClosest(final Vector3D p1, final Vector3D p2, final Vector3D position) {
  520.         return Vector3D.distance(p1, position) <= Vector3D.distance(p2, position) ? p1 : p2;
  521.     }

  522.     /** Get point at some latitude along a pixel line of sight.
  523.      * @param ellipsoid reference ellipsoid
  524.      * @param position pixel position (in body frame)
  525.      * @param los pixel line-of-sight, not necessarily normalized (in body frame)
  526.      * @param latitude latitude with respect to ellipsoid
  527.      * @param closeReference reference point used to select the closest solution
  528.      * when there are two points at the desired latitude along the line
  529.      * @return point at altitude, or closeReference if no such point can be found
  530.      */
  531.     private Vector3D latitudeCrossing(final ExtendedEllipsoid ellipsoid,
  532.                                       final Vector3D position, final Vector3D los,
  533.                                       final double latitude, final Vector3D closeReference) {
  534.         try {
  535.             return ellipsoid.pointAtLatitude(position, los, latitude, closeReference);
  536.         } catch (RuggedException re) {
  537.             return closeReference;
  538.         }
  539.     }

  540.     /** Get point at some latitude along a pixel line of sight.
  541.      * @param ellipsoid reference ellipsoid
  542.      * @param position pixel position (in body frame)
  543.      * @param los pixel line-of-sight, not necessarily normalized (in body frame)
  544.      * @param longitude longitude with respect to ellipsoid
  545.      * @param closeReference reference point used to select the closest solution
  546.      * when there are two points at the desired latitude along the line
  547.      * @return point at altitude, or closeReference if no such point can be found
  548.      */
  549.     private Vector3D longitudeCrossing(final ExtendedEllipsoid ellipsoid,
  550.                                        final Vector3D position, final Vector3D los,
  551.                                        final double longitude, final Vector3D closeReference) {
  552.         try {
  553.             return ellipsoid.pointAtLongitude(position, los, longitude);
  554.         } catch (RuggedException re) {
  555.             return closeReference;
  556.         }
  557.     }

  558.     /** Point at tile boundary. */
  559.     private static class LimitPoint {

  560.         /** Coordinates. */
  561.         private final NormalizedGeodeticPoint point;

  562.         /** Limit status. */
  563.         private final boolean side;

  564.         /** Simple constructor.
  565.          * @param ellipsoid reference ellipsoid
  566.          * @param referenceLongitude reference longitude lc such that the point longitude will
  567.          * be normalized between lc-π and lc+π
  568.          * @param cartesian Cartesian point
  569.          * @param side if true, the point is on a side limit, otherwise
  570.          * it is on a top/bottom limit
  571.          * @exception OrekitException if geodetic coordinates cannot be computed
  572.          */
  573.         LimitPoint(final ExtendedEllipsoid ellipsoid, final double referenceLongitude,
  574.                    final Vector3D cartesian, final boolean side)
  575.             throws OrekitException {
  576.             this(ellipsoid.transform(cartesian, ellipsoid.getBodyFrame(), null, referenceLongitude), side);
  577.         }

  578.         /** Simple constructor.
  579.          * @param point coordinates
  580.          * @param side if true, the point is on a side limit, otherwise
  581.          * it is on a top/bottom limit
  582.          */
  583.         LimitPoint(final NormalizedGeodeticPoint point, final boolean side) {
  584.             this.point = point;
  585.             this.side  = side;
  586.         }

  587.         /** Get the point coordinates.
  588.          * @return point coordinates
  589.          */
  590.         public NormalizedGeodeticPoint getPoint() {
  591.             return point;
  592.         }

  593.         /** Check if point is on the side of a tile.
  594.          * @return true if the point is on a side limit, otherwise
  595.          * it is on a top/bottom limit
  596.          */
  597.         public boolean atSide() {
  598.             return side;
  599.         }

  600.     }

  601. }