DumpReplayer.java

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

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.hipparchus.analysis.differentiation.Derivative;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.OpenIntToDoubleHashMap;
import org.hipparchus.util.Pair;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.frames.Predefined;
import org.orekit.frames.Transform;
import org.orekit.rugged.api.AlgorithmId;
import org.orekit.rugged.api.Rugged;
import org.orekit.rugged.api.RuggedBuilder;
import org.orekit.rugged.linesensor.LineDatation;
import org.orekit.rugged.linesensor.LineSensor;
import org.orekit.rugged.linesensor.SensorMeanPlaneCrossing;
import org.orekit.rugged.linesensor.SensorMeanPlaneCrossing.CrossingResult;
import org.orekit.rugged.linesensor.SensorPixel;
import org.orekit.rugged.los.TimeDependentLOS;
import org.orekit.rugged.raster.TileUpdater;
import org.orekit.rugged.raster.UpdatableTile;
import org.orekit.rugged.refraction.AtmosphericRefraction;
import org.orekit.rugged.refraction.MultiLayerModel;
import org.orekit.rugged.utils.DerivativeGenerator;
import org.orekit.rugged.utils.ExtendedEllipsoid;
import org.orekit.rugged.utils.SpacecraftToObservedBody;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.ParameterDriver;

/** Replayer for Rugged debug dumps.
 * @author Luc Maisonobe
 * @author Guylaine Prat
 * @see DumpManager
 * @see Dump
 */
public class DumpReplayer {

    /** Comment start marker. */
    private static final String COMMENT_START = "#";

    /** Keyword for latitude fields. */
    private static final String LATITUDE = "latitude";

    /** Keyword for longitude fields. */
    private static final String LONGITUDE = "longitude";

    /** Keyword for elevation fields. */
    private static final String ELEVATION = "elevation";

    /** Keyword for ellipsoid equatorial radius fields. */
    private static final String AE = "ae";

    /** Keyword for ellipsoid flattening fields. */
    private static final String F = "f";

    /** Keyword for frame fields. */
    private static final String FRAME = "frame";

    /** Keyword for date fields. */
    private static final String DATE = "date";

    /** Keyword for sensor position fields. */
    private static final String POSITION = "position";

    /** Keyword for sensor line-of-sight fields. */
    private static final String LOS = "los";

    /** Keyword for light-time correction fields. */
    private static final String LIGHT_TIME = "lightTime";

    /** Keyword for aberration of light correction fields. */
    private static final String ABERRATION = "aberration";

    /** Keyword for atmospheric refraction correction fields. */
    private static final String REFRACTION = "refraction";

    /** Keyword for min date fields. */
    private static final String MIN_DATE = "minDate";

    /** Keyword for max date fields. */
    private static final String MAX_DATE = "maxDate";

    /** Keyword for time step fields. */
    private static final String T_STEP = "tStep";

    /** Keyword for overshoot tolerance fields. */
    private static final String TOLERANCE = "tolerance";

    /** Keyword for inertial frames fields. */
    private static final String INERTIAL_FRAME = "inertialFrame";

    /** Keyword for observation transform index fields. */
    private static final String INDEX = "index";

    /** Keyword for body meta-fields. */
    private static final String BODY = "body";

    /** Keyword for rotation fields. */
    private static final String R = "r";

    /** Keyword for rotation rate fields. */
    private static final String OMEGA = "Ω";

    /** Keyword for rotation acceleration fields. */
    private static final String OMEGA_DOT = "ΩDot";

    /** Keyword for spacecraft meta-fields. */
    private static final String SPACECRAFT = "spacecraft";

    /** Keyword for position fields. */
    private static final String P = "p";

    /** Keyword for velocity fields. */
    private static final String V = "v";

    /** Keyword for acceleration fields. */
    private static final String A = "a";

    /** Keyword for minimum latitude fields. */
    private static final String LAT_MIN = "latMin";

    /** Keyword for latitude step fields. */
    private static final String LAT_STEP = "latStep";

    /** Keyword for latitude rows fields. */
    private static final String LAT_ROWS = "latRows";

    /** Keyword for minimum longitude fields. */
    private static final String LON_MIN = "lonMin";

    /** Keyword for longitude step fields. */
    private static final String LON_STEP = "lonStep";

    /** Keyword for longitude columns fields. */
    private static final String LON_COLS = "lonCols";

    /** Keyword for latitude index fields. */
    private static final String LAT_INDEX = "latIndex";

    /** Keyword for longitude index fields. */
    private static final String LON_INDEX = "lonIndex";

    /** Keyword for sensor name. */
    private static final String SENSOR_NAME = "sensorName";

    /** Keyword for min line. */
    private static final String MIN_LINE = "minLine";

    /** Keyword for max line. */
    private static final String MAX_LINE = "maxLine";

    /** Keyword for line number. */
    private static final String LINE_NUMBER = "lineNumber";

    /** Keyword for number of pixels. */
    private static final String NB_PIXELS = "nbPixels";

    /** Keyword for pixel number. */
    private static final String PIXEL_NUMBER = "pixelNumber";

    /** Keyword for max number of evaluations. */
    private static final String MAX_EVAL = "maxEval";

    /** Keyword for accuracy. */
    private static final String ACCURACY = "accuracy";

    /** Keyword for normal. */
    private static final String NORMAL = "normal";

    /** Keyword for rate. */
    private static final String RATE = "rate";

    /** Keyword for cached results. */
    private static final String CACHED_RESULTS = "cachedResults";

    /** Keyword for target. */
    private static final String TARGET = "target";

    /** Keyword for target direction. */
    private static final String TARGET_DIRECTION = "targetDirection";

    /** Keyword for null result. */
    private static final String NULL_RESULT = "NULL";

    /** Pattern for delimiting regular expressions. */
    private static final Pattern SEPARATOR = Pattern.compile("\\s+");

    /** Empty pattern. */
    private static final Pattern PATTERN = Pattern.compile(" ");

    /** Constant elevation for constant elevation algorithm. */
    private double constantElevation;

    /** Algorithm identifier. */
    private AlgorithmId algorithmId;

    /** Ellipsoid. */
    private OneAxisEllipsoid ellipsoid;

    /** Tiles list. */
    private final List<ParsedTile> tiles;

    /** Sensors list. */
    private final List<ParsedSensor> sensors;

    /** Interpolator min date. */
    private AbsoluteDate minDate;

    /** Interpolator max date. */
    private AbsoluteDate maxDate;

    /** Interpolator step. */
    private double tStep;

    /** Interpolator overshoot tolerance. */
    private double tolerance;

    /** Inertial frame. */
    private Frame inertialFrame;

    /** Transforms sample from observed body frame to inertial frame. */
    private NavigableMap<Integer, Transform> bodyToInertial;

    /** Transforms sample from spacecraft frame to inertial frame. */
    private NavigableMap<Integer, Transform> scToInertial;

    /** Flag for light time correction. */
    private boolean lightTimeCorrection;

    /** Flag for aberration of light correction. */
    private boolean aberrationOfLightCorrection;

    /** Flag for atmospheric refraction. */
    private boolean atmosphericRefraction;

    /** Dumped calls. */
    private final List<DumpedCall> calls;


    /** Simple constructor.
     */
    public DumpReplayer() {
        tiles   = new ArrayList<ParsedTile>();
        sensors = new ArrayList<ParsedSensor>();
        calls   = new ArrayList<DumpedCall>();
    }

    /** Parse a dump file.
     * @param file dump file to parse
     */
    public void parse(final File file) {
        try {
            final BufferedReader reader =
                    new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            int l = 0;
            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                LineParser.parse(++l, file, line, this);
            }
            reader.close();
        } catch (IOException ioe) {
            throw new RuggedException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
        }
    }

    /** Create a Rugged instance from parsed data.
     * @return rugged instance
     */
    public Rugged createRugged() {
        try {
            final RuggedBuilder builder = new RuggedBuilder();

            if (algorithmId == null) {
                algorithmId = AlgorithmId.IGNORE_DEM_USE_ELLIPSOID;
            }
            builder.setAlgorithm(algorithmId);
            if (algorithmId == AlgorithmId.CONSTANT_ELEVATION_OVER_ELLIPSOID) {
                builder.setConstantElevation(constantElevation);
            } else if (algorithmId != AlgorithmId.IGNORE_DEM_USE_ELLIPSOID) {
                builder.setDigitalElevationModel(new TileUpdater() {

                    /** {@inheritDoc} */
                    @Override
                    public void updateTile(final double latitude, final double longitude, final UpdatableTile tile) {
                        for (final ParsedTile parsedTile : tiles) {
                            if (parsedTile.isInterpolable(latitude, longitude)) {
                                parsedTile.updateTile(tile);
                                return;
                            }
                        }
                        throw new RuggedException(RuggedMessages.NO_DEM_DATA,
                                                  FastMath.toDegrees(latitude), FastMath.toDegrees(longitude));
                    }
                }, 8);
            }

            builder.setEllipsoid(ellipsoid);

            builder.setLightTimeCorrection(lightTimeCorrection);
            builder.setAberrationOfLightCorrection(aberrationOfLightCorrection);
            if (atmosphericRefraction) { // Use the default model with the default configuration values
                final ExtendedEllipsoid extendedEllipsoid = builder.getEllipsoid();
                final AtmosphericRefraction atmosphericModel = new MultiLayerModel(extendedEllipsoid);
                // Build Rugged with atmospheric refraction model
                builder.setRefractionCorrection(atmosphericModel);
            }


            // build missing transforms by extrapolating the parsed ones
            final int n = (int) FastMath.ceil(maxDate.durationFrom(minDate) / tStep);
            final List<Transform> b2iList = new ArrayList<Transform>(n);
            final List<Transform> s2iList = new ArrayList<Transform>(n);
            for (int i = 0; i < n; ++i) {
                if (bodyToInertial.containsKey(i)) {
                    // the i-th transform was dumped
                    b2iList.add(bodyToInertial.get(i));
                    s2iList.add(scToInertial.get(i));
                } else {
                    // the i-th transformed was not dumped, we have to extrapolate it
                    final Map.Entry<Integer, Transform> lower  = bodyToInertial.lowerEntry(i);
                    final Map.Entry<Integer, Transform> higher = bodyToInertial.higherEntry(i);
                    final int closest;
                    if (lower == null) {
                        closest = higher.getKey();
                    } else if (higher == null) {
                        closest = lower.getKey();
                    } else {
                        closest = (i - lower.getKey() <= higher.getKey() - i) ? lower.getKey() : higher.getKey();
                    }
                    b2iList.add(bodyToInertial.get(closest).shiftedBy((i - closest) * tStep));
                    s2iList.add(scToInertial.get(closest).shiftedBy((i - closest) * tStep));
                }
            }

            // we use Rugged transforms reloading mechanism to ensure the spacecraft
            // to body transforms will be the same as the ones dumped
            final SpacecraftToObservedBody scToBody =
                    new SpacecraftToObservedBody(inertialFrame, ellipsoid.getBodyFrame(),
                                                 minDate, maxDate, tStep, tolerance,
                                                 b2iList, s2iList);
            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
            new ObjectOutputStream(bos).writeObject(scToBody);
            final ByteArrayInputStream  bis = new ByteArrayInputStream(bos.toByteArray());
            builder.setTrajectoryAndTimeSpan(bis);

            final List<SensorMeanPlaneCrossing> planeCrossings = new ArrayList<SensorMeanPlaneCrossing>();
            for (final ParsedSensor parsedSensor : sensors) {
                final LineSensor sensor = new LineSensor(parsedSensor.name,
                                                         parsedSensor,
                                                         parsedSensor.position,
                                                         parsedSensor);
                if (parsedSensor.meanPlane != null) {
                    planeCrossings.add(new SensorMeanPlaneCrossing(sensor, scToBody,
                                                                   parsedSensor.meanPlane.minLine,
                                                                   parsedSensor.meanPlane.maxLine,
                                                                   lightTimeCorrection, aberrationOfLightCorrection,
                                                                   parsedSensor.meanPlane.maxEval,
                                                                   parsedSensor.meanPlane.accuracy,
                                                                   parsedSensor.meanPlane.normal,
                                                                   Arrays.stream(parsedSensor.meanPlane.cachedResults)));
                }
                builder.addLineSensor(sensor);
            }

            final Rugged rugged = builder.build();

            final Method setPlaneCrossing = Rugged.class.getDeclaredMethod("setPlaneCrossing",
                                                                           SensorMeanPlaneCrossing.class);
            setPlaneCrossing.setAccessible(true);
            for (final SensorMeanPlaneCrossing planeCrossing : planeCrossings) {
                setPlaneCrossing.invoke(rugged, planeCrossing);
            }

            return rugged;

        } catch (IOException ioe) {
            throw new RuggedException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
        } catch (SecurityException e) {
            // this should never happen
            throw new RuggedInternalError(e);
        } catch (NoSuchMethodException e) {
            // this should never happen
            throw new RuggedInternalError(e);
        } catch (IllegalArgumentException e) {
            // this should never happen
            throw new RuggedInternalError(e);
        } catch (IllegalAccessException e) {
            // this should never happen
            throw new RuggedInternalError(e);
        } catch (InvocationTargetException e) {
            // this should never happen
            throw new RuggedInternalError(e);
        }
    }

    /** Get a sensor by name.
     * @param name sensor name
     * @return parsed sensor
     */
    private ParsedSensor getSensor(final String name) {
        for (final ParsedSensor sensor : sensors) {
            if (sensor.name.equals(name)) {
                return sensor;
            }
        }
        final ParsedSensor sensor = new ParsedSensor(name);
        sensors.add(sensor);
        return sensor;
    }

    /** Execute all dumped calls.
     * <p>
     * The dumped calls correspond to computation methods like direct or inverse
     * location.
     * </p>
     * @param rugged Rugged instance on which calls will be performed
     * @return results of all dumped calls
     */
    public Result[] execute(final Rugged rugged) {
        final Result[] results = new Result[calls.size()];
        for (int i = 0; i < calls.size(); ++i) {
            results[i] = new Result(calls.get(i).expected,
                                    calls.get(i).execute(rugged));
        }
        return results;
    }

    /** Container for replay results. */
    public static class Result {

        /** Expected result. */
        private final Object expected;

        /** Replayed result. */
        private final Object replayed;

        /** Simple constructor.
         * @param expected expected result
         * @param replayed replayed result
         */
        private Result(final Object expected, final Object replayed) {
            this.expected = expected;
            this.replayed = replayed;
        }

        /** Get the expected result.
         * @return expected result
         */
        public Object getExpected() {
            return expected;
        }

        /** Get the replayed result.
         * @return replayed result
         */
        public Object getReplayed() {
            return replayed;
        }

    }

    /** Line parsers. */
    private enum LineParser {

        /** Parser for algorithm dump lines. */
        ALGORITHM() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                try {
                    if (fields.length < 1) {
                        throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                    }
                    global.algorithmId = AlgorithmId.valueOf(fields[0]);
                    if (global.algorithmId == AlgorithmId.CONSTANT_ELEVATION_OVER_ELLIPSOID) {
                        if (fields.length < 3 || !fields[1].equals(ELEVATION)) {
                            throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                        }
                        global.constantElevation = Double.parseDouble(fields[2]);
                    }
                } catch (IllegalArgumentException iae) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
            }

        },

        /** Parser for ellipsoid dump lines. */
        ELLIPSOID() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 6 || !fields[0].equals(AE) || !fields[2].equals(F) || !fields[4].equals(FRAME)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final double ae   = Double.parseDouble(fields[1]);
                final double f    = Double.parseDouble(fields[3]);
                final Frame  bodyFrame;
                try {
                    bodyFrame = FramesFactory.getFrame(Predefined.valueOf(fields[5]));
                } catch (IllegalArgumentException iae) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                global.ellipsoid = new OneAxisEllipsoid(ae, f, bodyFrame);
            }

        },

        /** Parser for direct location calls dump lines. */
        DIRECT_LOCATION() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 16 ||
                        !fields[0].equals(DATE) ||
                        !fields[2].equals(POSITION) || !fields[6].equals(LOS) ||
                        !fields[10].equals(LIGHT_TIME) || !fields[12].equals(ABERRATION) ||
                        !fields[14].equals(REFRACTION)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final AbsoluteDate date = new AbsoluteDate(fields[1], TimeScalesFactory.getUTC());
                final Vector3D position = new Vector3D(Double.parseDouble(fields[3]),
                        Double.parseDouble(fields[4]),
                        Double.parseDouble(fields[5]));
                final Vector3D los      = new Vector3D(Double.parseDouble(fields[7]),
                        Double.parseDouble(fields[8]),
                        Double.parseDouble(fields[9]));
                if (global.calls.isEmpty()) {
                    global.lightTimeCorrection         = Boolean.parseBoolean(fields[11]);
                    global.aberrationOfLightCorrection = Boolean.parseBoolean(fields[13]);
                    global.atmosphericRefraction       = Boolean.parseBoolean(fields[15]);
                } else {
                    if (global.lightTimeCorrection != Boolean.parseBoolean(fields[11])) {
                        throw new RuggedException(RuggedMessages.LIGHT_TIME_CORRECTION_REDEFINED,
                                l, file.getAbsolutePath(), line);
                    }
                    if (global.aberrationOfLightCorrection != Boolean.parseBoolean(fields[13])) {
                        throw new RuggedException(RuggedMessages.ABERRATION_OF_LIGHT_CORRECTION_REDEFINED,
                                l, file.getAbsolutePath(), line);
                    }
                    if (global.atmosphericRefraction != Boolean.parseBoolean(fields[15])) {
                        throw new RuggedException(RuggedMessages.ATMOSPHERIC_REFRACTION_REDEFINED,
                                l, file.getAbsolutePath(), line);
                    }
                }
                global.calls.add(new DumpedCall() {

                    /** {@inheritDoc} */
                    @Override
                    public Object execute(final Rugged rugged) {
                        return rugged.directLocation(date, position, los);
                    }

                });
            }
        },

        /** Parser for direct location result dump lines. */
        DIRECT_LOCATION_RESULT() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length == 1) {
                    if (fields[0].equals(NULL_RESULT)) {
                        final GeodeticPoint gp = null;
                        final DumpedCall last = global.calls.get(global.calls.size() - 1);
                        last.expected = gp;
                    } else {
                        throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                    }
                } else if (fields.length < 6 || !fields[0].equals(LATITUDE) ||
                           !fields[2].equals(LONGITUDE) || !fields[4].equals(ELEVATION)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                } else {
                    final GeodeticPoint gp = new GeodeticPoint(Double.parseDouble(fields[1]),
                                                               Double.parseDouble(fields[3]),
                                                               Double.parseDouble(fields[5]));
                    final DumpedCall last = global.calls.get(global.calls.size() - 1);
                    last.expected = gp;
                }
            }

        },

        /** Parser for search span dump lines. */
        SPAN() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 10 ||
                        !fields[0].equals(MIN_DATE)  || !fields[2].equals(MAX_DATE) || !fields[4].equals(T_STEP)   ||
                        !fields[6].equals(TOLERANCE) || !fields[8].equals(INERTIAL_FRAME)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                global.minDate        = new AbsoluteDate(fields[1], TimeScalesFactory.getUTC());
                global.maxDate        = new AbsoluteDate(fields[3], TimeScalesFactory.getUTC());
                global.tStep          = Double.parseDouble(fields[5]);
                global.tolerance      = Double.parseDouble(fields[7]);
                global.bodyToInertial = new TreeMap<Integer, Transform>();
                global.scToInertial   = new TreeMap<Integer, Transform>();
                try {
                    global.inertialFrame = FramesFactory.getFrame(Predefined.valueOf(fields[9]));
                } catch (IllegalArgumentException iae) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
            }
        },

        /** Parser for observation transforms dump lines. */
        TRANSFORM() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 42 ||
                    !fields[0].equals(INDEX) ||
                    !fields[2].equals(BODY)  ||
                    !fields[3].equals(R)     || !fields[8].equals(OMEGA)    || !fields[12].equals(OMEGA_DOT) ||
                    !fields[16].equals(SPACECRAFT) ||
                    !fields[17].equals(P)    || !fields[21].equals(V)   || !fields[25].equals(A) ||
                    !fields[29].equals(R)    || !fields[34].equals(OMEGA)   || !fields[38].equals(OMEGA_DOT)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final int i   = Integer.parseInt(fields[1]);
                final AbsoluteDate date = global.minDate.shiftedBy(i * global.tStep);
                global.bodyToInertial.put(i,
                                          new Transform(date,
                                                        new Rotation(Double.parseDouble(fields[4]),
                                                                     Double.parseDouble(fields[5]),
                                                                     Double.parseDouble(fields[6]),
                                                                     Double.parseDouble(fields[7]),
                                                                     false),
                                                        new Vector3D(Double.parseDouble(fields[9]),
                                                                     Double.parseDouble(fields[10]),
                                                                     Double.parseDouble(fields[11])),
                                                        new Vector3D(Double.parseDouble(fields[13]),
                                                                     Double.parseDouble(fields[14]),
                                                                     Double.parseDouble(fields[15]))));
                global.scToInertial.put(i,
                                        new Transform(date,
                                                      new Transform(date,
                                                                    new Vector3D(Double.parseDouble(fields[18]),
                                                                                 Double.parseDouble(fields[19]),
                                                                                 Double.parseDouble(fields[20])),
                                                                    new Vector3D(Double.parseDouble(fields[22]),
                                                                                 Double.parseDouble(fields[23]),
                                                                                 Double.parseDouble(fields[24])),
                                                                    new Vector3D(Double.parseDouble(fields[26]),
                                                                                 Double.parseDouble(fields[27]),
                                                                                 Double.parseDouble(fields[28]))),
                                                      new Transform(date,
                                                                    new Rotation(Double.parseDouble(fields[30]),
                                                                                 Double.parseDouble(fields[31]),
                                                                                 Double.parseDouble(fields[32]),
                                                                                 Double.parseDouble(fields[33]),
                                                                                 false),
                                                                    new Vector3D(Double.parseDouble(fields[35]),
                                                                                 Double.parseDouble(fields[36]),
                                                                                 Double.parseDouble(fields[37])),
                                                                    new Vector3D(Double.parseDouble(fields[39]),
                                                                                 Double.parseDouble(fields[40]),
                                                                                 Double.parseDouble(fields[41])))));
            }

        },

        /** Parser for DEM tile global geometry dump lines. */
        DEM_TILE() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 13 ||
                        !fields[1].equals(LAT_MIN) || !fields[3].equals(LAT_STEP) || !fields[5].equals(LAT_ROWS) ||
                        !fields[7].equals(LON_MIN) || !fields[9].equals(LON_STEP) || !fields[11].equals(LON_COLS)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String name             = fields[0];
                final double minLatitude      = Double.parseDouble(fields[2]);
                final double latitudeStep     = Double.parseDouble(fields[4]);
                final int    latitudeRows     = Integer.parseInt(fields[6]);
                final double minLongitude     = Double.parseDouble(fields[8]);
                final double longitudeStep    = Double.parseDouble(fields[10]);
                final int    longitudeColumns = Integer.parseInt(fields[12]);
                for (final ParsedTile tile : global.tiles) {
                    if (tile.name.equals(name)) {
                        throw new RuggedException(RuggedMessages.TILE_ALREADY_DEFINED,
                                                  name, l, file.getAbsolutePath(), line);
                    }
                }
                global.tiles.add(new ParsedTile(name,
                                                minLatitude, latitudeStep, latitudeRows,
                                                minLongitude, longitudeStep, longitudeColumns));
            }

        },

        /** Parser for DEM cells dump lines. */
        DEM_CELL() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 7 ||
                    !fields[1].equals(LAT_INDEX) || !fields[3].equals(LON_INDEX) || !fields[5].equals(ELEVATION)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String name      = fields[0];
                final int    latIndex  = Integer.parseInt(fields[2]);
                final int    lonIndex  = Integer.parseInt(fields[4]);
                final double elevation = Double.parseDouble(fields[6]);
                for (final ParsedTile tile : global.tiles) {
                    if (tile.name.equals(name)) {
                        final int index = latIndex * tile.longitudeColumns + lonIndex;
                        tile.elevations.put(index, elevation);
                        return;
                    }
                }
                throw new RuggedException(RuggedMessages.UNKNOWN_TILE,
                                          name, l, file.getAbsolutePath(), line);
            }

        },

        /** Parser for inverse location calls dump lines. */
        INVERSE_LOCATION() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 18 ||
                        !fields[0].equals(SENSOR_NAME) ||
                        !fields[2].equals(LATITUDE) || !fields[4].equals(LONGITUDE) || !fields[6].equals(ELEVATION) ||
                        !fields[8].equals(MIN_LINE) || !fields[10].equals(MAX_LINE) ||
                        !fields[12].equals(LIGHT_TIME) || !fields[14].equals(ABERRATION) ||
                        !fields[16].equals(REFRACTION)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String sensorName = fields[1];
                final GeodeticPoint point = new GeodeticPoint(Double.parseDouble(fields[3]),
                                                              Double.parseDouble(fields[5]),
                                                              Double.parseDouble(fields[7]));
                final int minLine = Integer.parseInt(fields[9]);
                final int maxLine = Integer.parseInt(fields[11]);
                if (global.calls.isEmpty()) {
                    global.lightTimeCorrection         = Boolean.parseBoolean(fields[13]);
                    global.aberrationOfLightCorrection = Boolean.parseBoolean(fields[15]);
                    global.atmosphericRefraction       = Boolean.parseBoolean(fields[17]);
                } else {
                    if (global.lightTimeCorrection != Boolean.parseBoolean(fields[13])) {
                        throw new RuggedException(RuggedMessages.LIGHT_TIME_CORRECTION_REDEFINED,
                                                  l, file.getAbsolutePath(), line);
                    }
                    if (global.aberrationOfLightCorrection != Boolean.parseBoolean(fields[15])) {
                        throw new RuggedException(RuggedMessages.ABERRATION_OF_LIGHT_CORRECTION_REDEFINED,
                                                  l, file.getAbsolutePath(), line);
                    }
                    if (global.atmosphericRefraction != Boolean.parseBoolean(fields[17])) {
                        throw new RuggedException(RuggedMessages.ATMOSPHERIC_REFRACTION_REDEFINED,
                                                  l, file.getAbsolutePath(), line);
                    }
                }
                global.calls.add(new DumpedCall() {

                    /** {@inheritDoc} */
                    @Override
                    public Object execute(final Rugged rugged) {
                        return rugged.inverseLocation(sensorName, point, minLine, maxLine);
                    }

                });
            }

        },

        /** Parser for inverse location result dump lines. */
        INVERSE_LOCATION_RESULT() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length == 1) {
                    if (fields[0].equals(NULL_RESULT)) {
                        final SensorPixel sp = null;
                        final DumpedCall last = global.calls.get(global.calls.size() - 1);
                        last.expected = sp;
                    } else {
                        throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                    }
                } else if (fields.length < 4 || !fields[0].equals(LINE_NUMBER) || !fields[2].equals(PIXEL_NUMBER)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                } else {
                    final SensorPixel sp = new SensorPixel(Double.parseDouble(fields[1]),
                                                           Double.parseDouble(fields[3]));
                    final DumpedCall last = global.calls.get(global.calls.size() - 1);
                    last.expected = sp;
                }
            }

        },

        /** Parser for sensor dump lines. */
        SENSOR() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 8 || !fields[0].equals(SENSOR_NAME) ||
                    !fields[2].equals(NB_PIXELS) || !fields[4].equals(POSITION)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final ParsedSensor sensor = global.getSensor(fields[1]);
                sensor.setNbPixels(Integer.parseInt(fields[3]));
                sensor.setPosition(new Vector3D(Double.parseDouble(fields[5]),
                                                Double.parseDouble(fields[6]),
                                                Double.parseDouble(fields[7])));
            }

        },

        /** Parser for sensor mean plane dump lines. */
        SENSOR_MEAN_PLANE() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 16 || !fields[0].equals(SENSOR_NAME) ||
                        !fields[2].equals(MIN_LINE) || !fields[4].equals(MAX_LINE) ||
                        !fields[6].equals(MAX_EVAL) || !fields[8].equals(ACCURACY) ||
                        !fields[10].equals(NORMAL)  || !fields[14].equals(CACHED_RESULTS)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String   sensorName = fields[1];
                final int      minLine    = Integer.parseInt(fields[3]);
                final int      maxLine    = Integer.parseInt(fields[5]);
                final int      maxEval    = Integer.parseInt(fields[7]);
                final double   accuracy   = Double.parseDouble(fields[9]);
                final Vector3D normal     = new Vector3D(Double.parseDouble(fields[11]),
                        Double.parseDouble(fields[12]),
                        Double.parseDouble(fields[13]));
                final int      n          = Integer.parseInt(fields[15]);
                final CrossingResult[] cachedResults = new CrossingResult[n];
                int base = 16;
                for (int i = 0; i < n; ++i) {
                    if (fields.length < base + 15 || !fields[base].equals(LINE_NUMBER) ||
                            !fields[base + 2].equals(DATE) || !fields[base + 4].equals(TARGET) ||
                            !fields[base + 8].equals(TARGET_DIRECTION)) {
                        throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                    }
                    final double       ln                    = Double.parseDouble(fields[base + 1]);
                    final AbsoluteDate date                  = new AbsoluteDate(fields[base + 3], TimeScalesFactory.getUTC());
                    final Vector3D     target                = new Vector3D(Double.parseDouble(fields[base +  5]),
                            Double.parseDouble(fields[base +  6]),
                            Double.parseDouble(fields[base +  7]));
                    final Vector3D targetDirection           = new Vector3D(Double.parseDouble(fields[base +  9]),
                            Double.parseDouble(fields[base + 10]),
                            Double.parseDouble(fields[base + 11]));
                    final Vector3D targetDirectionDerivative = new Vector3D(Double.parseDouble(fields[base + 12]),
                            Double.parseDouble(fields[base + 13]),
                            Double.parseDouble(fields[base + 14]));
                    cachedResults[i] = new CrossingResult(date, ln, target, targetDirection, targetDirectionDerivative);
                    base += 15;
                }
                global.getSensor(sensorName).setMeanPlane(new ParsedMeanPlane(minLine, maxLine, maxEval, accuracy, normal, cachedResults));
            }
        },

        /** Parser for sensor LOS dump lines. */
        SENSOR_LOS() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 10 || !fields[0].equals(SENSOR_NAME) ||
                        !fields[2].equals(DATE) || !fields[4].equals(PIXEL_NUMBER) ||
                        !fields[6].equals(LOS)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String       sensorName  = fields[1];
                final AbsoluteDate date        = new AbsoluteDate(fields[3], TimeScalesFactory.getUTC());
                final int          pixelNumber = Integer.parseInt(fields[5]);
                final Vector3D     los         = new Vector3D(Double.parseDouble(fields[7]),
                        Double.parseDouble(fields[8]),
                        Double.parseDouble(fields[9]));
                global.getSensor(sensorName).setLOS(date, pixelNumber, los);
            }
        },

        /** Parser for sensor datation dump lines. */
        SENSOR_DATATION() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 6 || !fields[0].equals(SENSOR_NAME) ||
                        !fields[2].equals(LINE_NUMBER) || !fields[4].equals(DATE)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String       sensorName  = fields[1];
                final double       lineNumber  = Double.parseDouble(fields[3]);
                final AbsoluteDate date        = new AbsoluteDate(fields[5], TimeScalesFactory.getUTC());
                global.getSensor(sensorName).setDatation(lineNumber, date);
            }
        },

        /** Parser for sensor rate dump lines. */
        SENSOR_RATE() {

            /** {@inheritDoc} */
            @Override
            public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
                if (fields.length < 6 || !fields[0].equals(SENSOR_NAME) ||
                    !fields[2].equals(LINE_NUMBER) || !fields[4].equals(RATE)) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }
                final String       sensorName  = fields[1];
                final double       lineNumber  = Double.parseDouble(fields[3]);
                final double       rate  = Double.parseDouble(fields[5]);
                global.getSensor(sensorName).setRate(lineNumber, rate);

            }

        };

        /** Parse a line.
         * @param l line number
         * @param file dump file
         * @param line line to parse
         * @param global global parser to store parsed data
         */
        public static void parse(final int l, final File file, final String line, final DumpReplayer global) {

            final String trimmed = line.trim();
            if (trimmed.length() == 0 || trimmed.startsWith(COMMENT_START)) {
                return;
            }

            final int colon = line.indexOf(':');
            if (colon > 0) {
                final String parsedKey = PATTERN.matcher(line.substring(0, colon).trim()).replaceAll("_").toUpperCase();
                try {
                    final LineParser parser = LineParser.valueOf(parsedKey);
                    final String[] fields;
                    if (colon + 1 >= line.length()) {
                        fields = new String[0];
                    } else {
                        fields = SEPARATOR.split(line.substring(colon + 1).trim());
                    }
                    parser.parse(l, file, line, fields, global);
                } catch (IllegalArgumentException iae) {
                    throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
                }

            } else {
                throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
            }

        }

        /** Parse a line.
         * @param l line number
         * @param file dump file
         * @param line complete line
         * @param fields data fields from the line
         * @param global global parser to store parsed data
         */
        public abstract void parse(int l, File file, String line, String[] fields, DumpReplayer global);

    }

    /** Local class for handling already parsed tile data. */
    private static class ParsedTile {

        /** Name of the tile. */
        private final String name;

        /** Minimum latitude. */
        private final double minLatitude;

        /** Step in latitude (size of one raster element). */
        private final double latitudeStep;

        /** Number of latitude rows. */
        private int latitudeRows;

        /** Minimum longitude. */
        private final double minLongitude;

        /** Step in longitude (size of one raster element). */
        private final double longitudeStep;

        /** Number of longitude columns. */
        private int longitudeColumns;

        /** Raster elevation data. */
        private final OpenIntToDoubleHashMap elevations;

        /** Simple constructor.
         * @param name of the tile
         * @param minLatitude minimum latitude
         * @param latitudeStep step in latitude (size of one raster element)
         * @param latitudeRows number of latitude rows
         * @param minLongitude minimum longitude
         * @param longitudeStep step in longitude (size of one raster element)
         * @param longitudeColumns number of longitude columns
         */
        ParsedTile(final String name,
                   final double minLatitude, final double latitudeStep, final int latitudeRows,
                   final double minLongitude, final double longitudeStep, final int longitudeColumns) {
            this.name             = name;
            this.minLatitude      = minLatitude;
            this.latitudeStep     = latitudeStep;
            this.minLongitude     = minLongitude;
            this.longitudeStep    = longitudeStep;
            this.latitudeRows     = latitudeRows;
            this.longitudeColumns = longitudeColumns;
            this.elevations       = new OpenIntToDoubleHashMap();
        }

        /** Check if a point is in the interpolable region of the tile.
         * @param latitude point latitude
         * @param longitude point longitude
         * @return true if the point is in the interpolable region of the tile
         */
        public boolean isInterpolable(final double latitude, final double longitude) {
            final int latitudeIndex  = (int) FastMath.floor((latitude  - minLatitude)  / latitudeStep);
            final int longitudeIndex = (int) FastMath.floor((longitude - minLongitude) / longitudeStep);
            return (latitudeIndex  >= 0) && (latitudeIndex  <= latitudeRows     - 2) &&
                   (longitudeIndex >= 0) && (longitudeIndex <= longitudeColumns - 2);
        }

        /** Update the tile according to the Digital Elevation Model.
         * @param tile to update
         */
        public void updateTile(final UpdatableTile tile) {

            tile.setGeometry(minLatitude, minLongitude,
                             latitudeStep, longitudeStep,
                             latitudeRows, longitudeColumns);

            final OpenIntToDoubleHashMap.Iterator iterator = elevations.iterator();
            while (iterator.hasNext()) {
                iterator.advance();
                final int    index          = iterator.key();
                final int    latitudeIndex  = index / longitudeColumns;
                final int    longitudeIndex = index % longitudeColumns;
                final double elevation      = iterator.value();
                tile.setElevation(latitudeIndex, longitudeIndex, elevation);
            }

        }

    }

    /** Local class for handling already parsed sensor data. */
    private static class ParsedSensor implements LineDatation, TimeDependentLOS {

        /** Name of the sensor. */
        private final String name;

        /** Number of pixels. */
        private int nbPixels;

        /** Position. */
        private Vector3D position;

        /** Mean plane crossing finder. */
        private ParsedMeanPlane meanPlane;

        /** LOS map. */
        private final Map<Integer, List<Pair<AbsoluteDate, Vector3D>>> losMap;

        /** Datation. */
        private final List<Pair<Double, AbsoluteDate>> datation;

        /** Rate. */
        private final List<Pair<Double, Double>> rates;

        /** simple constructor.
         * @param name name of the sensor
         */
        ParsedSensor(final String name) {
            this.name     = name;
            this.losMap   = new HashMap<Integer, List<Pair<AbsoluteDate, Vector3D>>>();
            this.datation = new ArrayList<Pair<Double, AbsoluteDate>>();
            this.rates    = new ArrayList<Pair<Double, Double>>();
        }

        /** Set the mean place finder.
         * @param meanPlane mean plane finder
         */
        public void setMeanPlane(final ParsedMeanPlane meanPlane) {
            this.meanPlane = meanPlane;
        }

        /** Set the position.
         * @param position position
         */
        public void setPosition(final Vector3D position) {
            this.position = position;
        }

        /** Set the number of pixels.
         * @param nbPixels number of pixels
         */
        public void setNbPixels(final int nbPixels) {
            this.nbPixels = nbPixels;
        }

        /** {@inheritDoc} */
        @Override
        public int getNbPixels() {
            return nbPixels;
        }

        /** Set a los direction.
         * @param date date
         * @param pixelNumber number of the pixel
         * @param los los direction
         */
        public void setLOS(final AbsoluteDate date, final int pixelNumber, final Vector3D los) {
            List<Pair<AbsoluteDate, Vector3D>> list = losMap.get(pixelNumber);
            if (list == null) {
                list = new ArrayList<Pair<AbsoluteDate, Vector3D>>();
                losMap.put(pixelNumber, list);
            }
            // find insertion index to have LOS sorted chronologically
            int index = 0;
            while (index < list.size()) {
                if (list.get(index).getFirst().compareTo(date) > 0) {
                    break;
                }
                ++index;
            }
            list.add(index, new Pair<AbsoluteDate, Vector3D>(date, los));
        }

        /** {@inheritDoc} */
        @Override
        public Vector3D getLOS(final int index, final AbsoluteDate date) {
            final List<Pair<AbsoluteDate, Vector3D>> list = losMap.get(index);
            if (list == null) {
                throw new RuggedInternalError(null);
            }

            if (list.size() < 2) {
                return list.get(0).getSecond();
            }

            // find entries bracketing the the date
            int sup = 0;
            while (sup < list.size() - 1) {
                if (list.get(sup).getFirst().compareTo(date) >= 0) {
                    break;
                }
                ++sup;
            }
            final int inf = (sup == 0) ? sup++ : (sup - 1);

            final AbsoluteDate dInf  = list.get(inf).getFirst();
            final Vector3D     lInf  = list.get(inf).getSecond();
            final AbsoluteDate dSup  = list.get(sup).getFirst();
            final Vector3D     lSup  = list.get(sup).getSecond();
            final double       alpha = date.durationFrom(dInf) / dSup.durationFrom(dInf);
            return new Vector3D(alpha, lSup, 1 - alpha, lInf);

        }

        /** {@inheritDoc} */
        @Override
        public <T extends Derivative<T>> FieldVector3D<T> getLOSDerivatives(final int index, final AbsoluteDate date,
                                                                            final DerivativeGenerator<T> generator) {
            final Vector3D los = getLOS(index, date);
            return new FieldVector3D<>(generator.constant(los.getX()),
                                       generator.constant(los.getY()),
                                       generator.constant(los.getZ()));
        }

        /** Set a datation pair.
         * @param lineNumber line number
         * @param date date
         */
        public void setDatation(final double lineNumber, final AbsoluteDate date) {
            // find insertion index to have datations sorted chronologically
            int index = 0;
            while (index < datation.size()) {
                if (datation.get(index).getSecond().compareTo(date) > 0) {
                    break;
                }
                ++index;
            }
            datation.add(index, new Pair<Double, AbsoluteDate>(lineNumber, date));
        }

        /** {@inheritDoc} */
        @Override
        public AbsoluteDate getDate(final double lineNumber) {

            if (datation.size() < 2) {
                return datation.get(0).getSecond();
            }

            // find entries bracketing the line number
            int sup = 0;
            while (sup < datation.size() - 1) {
                if (datation.get(sup).getFirst() >= lineNumber) {
                    break;
                }
                ++sup;
            }
            final int inf = (sup == 0) ? sup++ : (sup - 1);

            final double       lInf  = datation.get(inf).getFirst();
            final AbsoluteDate dInf  = datation.get(inf).getSecond();
            final double       lSup  = datation.get(sup).getFirst();
            final AbsoluteDate dSup  = datation.get(sup).getSecond();
            final double       alpha = (lineNumber - lInf) / (lSup - lInf);
            return dInf.shiftedBy(alpha * dSup.durationFrom(dInf));

        }

        /** {@inheritDoc} */
        @Override
        public double getLine(final AbsoluteDate date) {

            if (datation.size() < 2) {
                return datation.get(0).getFirst();
            }

            // find entries bracketing the date
            int sup = 0;
            while (sup < datation.size() - 1) {
                if (datation.get(sup).getSecond().compareTo(date) >= 0) {
                    break;
                }
                ++sup;
            }
            final int inf = (sup == 0) ? sup++ : (sup - 1);

            final double       lInf  = datation.get(inf).getFirst();
            final AbsoluteDate dInf  = datation.get(inf).getSecond();
            final double       lSup  = datation.get(sup).getFirst();
            final AbsoluteDate dSup  = datation.get(sup).getSecond();
            final double       alpha = date.durationFrom(dInf) / dSup.durationFrom(dInf);
            return alpha * lSup + (1 - alpha) * lInf;

        }

        /** Set a rate.
         * @param lineNumber line number
         * @param rate lines rate
         */
        public void setRate(final double lineNumber, final double rate) {
            // find insertion index to have rates sorted by line numbers
            int index = 0;
            while (index < rates.size()) {
                if (rates.get(index).getFirst() > lineNumber) {
                    break;
                }
                ++index;
            }
            rates.add(index, new Pair<Double, Double>(lineNumber, rate));
        }

        /** {@inheritDoc} */
        @Override
        public double getRate(final double lineNumber) {

            if (rates.size() < 2) {
                return rates.get(0).getSecond();
            }

            // find entries bracketing the line number
            int sup = 0;
            while (sup < rates.size() - 1) {
                if (rates.get(sup).getFirst() >= lineNumber) {
                    break;
                }
                ++sup;
            }
            final int inf = (sup == 0) ? sup++ : (sup - 1);

            final double lInf  = rates.get(inf).getFirst();
            final double rInf  = rates.get(inf).getSecond();
            final double lSup  = rates.get(sup).getFirst();
            final double rSup  = rates.get(sup).getSecond();
            final double alpha = (lineNumber - lInf) / (lSup - lInf);
            return alpha * rSup + (1 - alpha) * rInf;

        }

        /** {@inheritDoc} */
        @Override
        public Stream<ParameterDriver> getParametersDrivers() {
            return Stream.<ParameterDriver>empty();
        }

    }

    /** Local class for handling already parsed mean plane data. */
    private static class ParsedMeanPlane {

        /** Min line. */
        private final int minLine;

        /** Max line. */
        private final int maxLine;

        /** Maximum number of evaluations. */
        private final int maxEval;

        /** Accuracy to use for finding crossing line number. */
        private final double accuracy;

        /** Mean plane normal. */
        private final Vector3D normal;

        /** Cached results. */
        private final CrossingResult[] cachedResults;

        /** simple constructor.
         * @param minLine min line
         * @param maxLine max line
         * @param maxEval maximum number of evaluations
         * @param accuracy accuracy to use for finding crossing line number
         * @param normal mean plane normal
         * @param cachedResults cached results
         */
        ParsedMeanPlane(final int minLine, final int maxLine,
                        final int maxEval, final double accuracy, final Vector3D normal,
                        final CrossingResult[] cachedResults) {
            this.minLine       = minLine;
            this.maxLine       = maxLine;
            this.maxEval       = maxEval;
            this.accuracy      = accuracy;
            this.normal        = normal;
            this.cachedResults = cachedResults.clone();
        }

    }

    /** Local interface for dumped calls. */
    private abstract static class DumpedCall {

        /** Expected result. */
        private Object expected;

        /** Execute a call.
         * @param rugged Rugged instance on which called should be performed
         * @return result of the call
         */
        public abstract Object execute(Rugged rugged);

    }

}