1   /* Copyright 2013-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.rugged.errors;
18  
19  import java.io.BufferedReader;
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.ObjectOutputStream;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.file.Files;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.NavigableMap;
36  import java.util.TreeMap;
37  import java.util.regex.Pattern;
38  import java.util.stream.Stream;
39  
40  import org.hipparchus.analysis.differentiation.Derivative;
41  import org.hipparchus.exception.LocalizedCoreFormats;
42  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
43  import org.hipparchus.geometry.euclidean.threed.Rotation;
44  import org.hipparchus.geometry.euclidean.threed.Vector3D;
45  import org.hipparchus.util.FastMath;
46  import org.hipparchus.util.OpenIntToDoubleHashMap;
47  import org.hipparchus.util.Pair;
48  import org.orekit.bodies.GeodeticPoint;
49  import org.orekit.bodies.OneAxisEllipsoid;
50  import org.orekit.frames.Frame;
51  import org.orekit.frames.FramesFactory;
52  import org.orekit.frames.Predefined;
53  import org.orekit.frames.Transform;
54  import org.orekit.rugged.api.AlgorithmId;
55  import org.orekit.rugged.api.Rugged;
56  import org.orekit.rugged.api.RuggedBuilder;
57  import org.orekit.rugged.linesensor.LineDatation;
58  import org.orekit.rugged.linesensor.LineSensor;
59  import org.orekit.rugged.linesensor.SensorMeanPlaneCrossing;
60  import org.orekit.rugged.linesensor.SensorMeanPlaneCrossing.CrossingResult;
61  import org.orekit.rugged.linesensor.SensorPixel;
62  import org.orekit.rugged.los.TimeDependentLOS;
63  import org.orekit.rugged.raster.TileUpdater;
64  import org.orekit.rugged.raster.UpdatableTile;
65  import org.orekit.rugged.refraction.AtmosphericRefraction;
66  import org.orekit.rugged.refraction.MultiLayerModel;
67  import org.orekit.rugged.utils.DerivativeGenerator;
68  import org.orekit.rugged.utils.ExtendedEllipsoid;
69  import org.orekit.rugged.utils.SpacecraftToObservedBody;
70  import org.orekit.time.AbsoluteDate;
71  import org.orekit.time.TimeScalesFactory;
72  import org.orekit.utils.ParameterDriver;
73  
74  /** Replayer for Rugged debug dumps.
75   * @author Luc Maisonobe
76   * @author Guylaine Prat
77   * @see DumpManager
78   * @see Dump
79   */
80  public class DumpReplayer {
81  
82      /** Comment start marker. */
83      private static final String COMMENT_START = "#";
84  
85      /** Keyword for latitude fields. */
86      private static final String LATITUDE = "latitude";
87  
88      /** Keyword for longitude fields. */
89      private static final String LONGITUDE = "longitude";
90  
91      /** Keyword for elevation fields. */
92      private static final String ELEVATION = "elevation";
93  
94      /** Keyword for ellipsoid equatorial radius fields. */
95      private static final String AE = "ae";
96  
97      /** Keyword for ellipsoid flattening fields. */
98      private static final String F = "f";
99  
100     /** Keyword for frame fields. */
101     private static final String FRAME = "frame";
102 
103     /** Keyword for date fields. */
104     private static final String DATE = "date";
105 
106     /** Keyword for sensor position fields. */
107     private static final String POSITION = "position";
108 
109     /** Keyword for sensor line-of-sight fields. */
110     private static final String LOS = "los";
111 
112     /** Keyword for light-time correction fields. */
113     private static final String LIGHT_TIME = "lightTime";
114 
115     /** Keyword for aberration of light correction fields. */
116     private static final String ABERRATION = "aberration";
117 
118     /** Keyword for atmospheric refraction correction fields. */
119     private static final String REFRACTION = "refraction";
120 
121     /** Keyword for min date fields. */
122     private static final String MIN_DATE = "minDate";
123 
124     /** Keyword for max date fields. */
125     private static final String MAX_DATE = "maxDate";
126 
127     /** Keyword for time step fields. */
128     private static final String T_STEP = "tStep";
129 
130     /** Keyword for overshoot tolerance fields. */
131     private static final String TOLERANCE = "tolerance";
132 
133     /** Keyword for inertial frames fields. */
134     private static final String INERTIAL_FRAME = "inertialFrame";
135 
136     /** Keyword for observation transform index fields. */
137     private static final String INDEX = "index";
138 
139     /** Keyword for body meta-fields. */
140     private static final String BODY = "body";
141 
142     /** Keyword for rotation fields. */
143     private static final String R = "r";
144 
145     /** Keyword for rotation rate fields. */
146     private static final String OMEGA = "Ω";
147 
148     /** Keyword for rotation acceleration fields. */
149     private static final String OMEGA_DOT = "ΩDot";
150 
151     /** Keyword for spacecraft meta-fields. */
152     private static final String SPACECRAFT = "spacecraft";
153 
154     /** Keyword for position fields. */
155     private static final String P = "p";
156 
157     /** Keyword for velocity fields. */
158     private static final String V = "v";
159 
160     /** Keyword for acceleration fields. */
161     private static final String A = "a";
162 
163     /** Keyword for minimum latitude fields. */
164     private static final String LAT_MIN = "latMin";
165 
166     /** Keyword for latitude step fields. */
167     private static final String LAT_STEP = "latStep";
168 
169     /** Keyword for latitude rows fields. */
170     private static final String LAT_ROWS = "latRows";
171 
172     /** Keyword for minimum longitude fields. */
173     private static final String LON_MIN = "lonMin";
174 
175     /** Keyword for longitude step fields. */
176     private static final String LON_STEP = "lonStep";
177 
178     /** Keyword for longitude columns fields. */
179     private static final String LON_COLS = "lonCols";
180 
181     /** Keyword for latitude index fields. */
182     private static final String LAT_INDEX = "latIndex";
183 
184     /** Keyword for longitude index fields. */
185     private static final String LON_INDEX = "lonIndex";
186 
187     /** Keyword for sensor name. */
188     private static final String SENSOR_NAME = "sensorName";
189 
190     /** Keyword for min line. */
191     private static final String MIN_LINE = "minLine";
192 
193     /** Keyword for max line. */
194     private static final String MAX_LINE = "maxLine";
195 
196     /** Keyword for line number. */
197     private static final String LINE_NUMBER = "lineNumber";
198 
199     /** Keyword for number of pixels. */
200     private static final String NB_PIXELS = "nbPixels";
201 
202     /** Keyword for pixel number. */
203     private static final String PIXEL_NUMBER = "pixelNumber";
204 
205     /** Keyword for max number of evaluations. */
206     private static final String MAX_EVAL = "maxEval";
207 
208     /** Keyword for accuracy. */
209     private static final String ACCURACY = "accuracy";
210 
211     /** Keyword for normal. */
212     private static final String NORMAL = "normal";
213 
214     /** Keyword for rate. */
215     private static final String RATE = "rate";
216 
217     /** Keyword for cached results. */
218     private static final String CACHED_RESULTS = "cachedResults";
219 
220     /** Keyword for target. */
221     private static final String TARGET = "target";
222 
223     /** Keyword for target direction. */
224     private static final String TARGET_DIRECTION = "targetDirection";
225 
226     /** Keyword for null result. */
227     private static final String NULL_RESULT = "NULL";
228 
229     /** Pattern for delimiting regular expressions. */
230     private static final Pattern SEPARATOR = Pattern.compile("\\s+");
231 
232     /** Empty pattern. */
233     private static final Pattern PATTERN = Pattern.compile(" ");
234 
235     /** Constant elevation for constant elevation algorithm. */
236     private double constantElevation;
237 
238     /** Algorithm identifier. */
239     private AlgorithmId algorithmId;
240 
241     /** Ellipsoid. */
242     private OneAxisEllipsoid ellipsoid;
243 
244     /** Tiles list. */
245     private final List<ParsedTile> tiles;
246 
247     /** Sensors list. */
248     private final List<ParsedSensor> sensors;
249 
250     /** Interpolator min date. */
251     private AbsoluteDate minDate;
252 
253     /** Interpolator max date. */
254     private AbsoluteDate maxDate;
255 
256     /** Interpolator step. */
257     private double tStep;
258 
259     /** Interpolator overshoot tolerance. */
260     private double tolerance;
261 
262     /** Inertial frame. */
263     private Frame inertialFrame;
264 
265     /** Transforms sample from observed body frame to inertial frame. */
266     private NavigableMap<Integer, Transform> bodyToInertial;
267 
268     /** Transforms sample from spacecraft frame to inertial frame. */
269     private NavigableMap<Integer, Transform> scToInertial;
270 
271     /** Flag for light time correction. */
272     private boolean lightTimeCorrection;
273 
274     /** Flag for aberration of light correction. */
275     private boolean aberrationOfLightCorrection;
276 
277     /** Flag for atmospheric refraction. */
278     private boolean atmosphericRefraction;
279 
280     /** Dumped calls. */
281     private final List<DumpedCall> calls;
282 
283 
284     /** Simple constructor.
285      */
286     public DumpReplayer() {
287         tiles   = new ArrayList<>();
288         sensors = new ArrayList<>();
289         calls   = new ArrayList<>();
290     }
291 
292     /** Parse a dump file.
293      * @param file dump file to parse
294      */
295     public void parse(final File file) {
296         try (BufferedReader reader =
297                      new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()),
298                              StandardCharsets.UTF_8))) {
299 
300             int l = 0;
301             for (String line = reader.readLine(); line != null; line = reader.readLine()) {
302                 LineParser.parse(++l, file, line, this);
303             }
304         } catch (IOException ioe) {
305             throw new RuggedException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
306         }
307     }
308 
309     /** Create a Rugged instance from parsed data.
310      * @return rugged instance
311      */
312     public Rugged createRugged() {
313         try {
314             final RuggedBuilder builder = new RuggedBuilder();
315 
316             if (algorithmId == null) {
317                 algorithmId = AlgorithmId.IGNORE_DEM_USE_ELLIPSOID;
318             }
319             builder.setAlgorithm(algorithmId);
320             if (algorithmId == AlgorithmId.CONSTANT_ELEVATION_OVER_ELLIPSOID) {
321                 builder.setConstantElevation(constantElevation);
322             } else if (algorithmId != AlgorithmId.IGNORE_DEM_USE_ELLIPSOID) {
323                 // In the case of user used a non overlapping DEM: no need here to take it into account
324                 // as Rugged during the run created if necessary zipper tiles.
325                 // At this stage, the read DEM in the dump behave like an overlapping DEM.
326                 builder.setDigitalElevationModel(new TileUpdater() {
327 
328                     /** {@inheritDoc} */
329                     @Override
330                     public void updateTile(final double latitude, final double longitude, final UpdatableTile tile) {
331                         for (final ParsedTile parsedTile : tiles) {
332                             if (parsedTile.isInterpolable(latitude, longitude)) {
333                                 parsedTile.updateTile(tile);
334                                 return;
335                             }
336                         }
337                         throw new RuggedException(RuggedMessages.NO_DEM_DATA,
338                                                   FastMath.toDegrees(latitude), FastMath.toDegrees(longitude));
339                     }
340                 }, 8);
341             }
342 
343             builder.setEllipsoid(ellipsoid);
344 
345             builder.setLightTimeCorrection(lightTimeCorrection);
346             builder.setAberrationOfLightCorrection(aberrationOfLightCorrection);
347             if (atmosphericRefraction) { // Use the default model with the default configuration values
348                 final ExtendedEllipsoid extendedEllipsoid = builder.getEllipsoid();
349                 final AtmosphericRefraction atmosphericModel = new MultiLayerModel(extendedEllipsoid);
350                 // Build Rugged with atmospheric refraction model
351                 builder.setRefractionCorrection(atmosphericModel);
352             }
353 
354 
355             // build missing transforms by extrapolating the parsed ones
356             final int n = (int) FastMath.ceil(maxDate.durationFrom(minDate) / tStep);
357             final List<Transform> b2iList = new ArrayList<>(n);
358             final List<Transform> s2iList = new ArrayList<>(n);
359             for (int i = 0; i < n; ++i) {
360                 if (bodyToInertial.containsKey(i)) {
361                     // the i-th transform was dumped
362                     b2iList.add(bodyToInertial.get(i));
363                     s2iList.add(scToInertial.get(i));
364                 } else {
365                     // the i-th transformed was not dumped, we have to extrapolate it
366                     final Map.Entry<Integer, Transform> lower  = bodyToInertial.lowerEntry(i);
367                     final Map.Entry<Integer, Transform> higher = bodyToInertial.higherEntry(i);
368                     final int closest;
369                     if (lower == null) {
370                         closest = higher.getKey();
371                     } else if (higher == null) {
372                         closest = lower.getKey();
373                     } else {
374                         closest = (i - lower.getKey() <= higher.getKey() - i) ? lower.getKey() : higher.getKey();
375                     }
376                     b2iList.add(bodyToInertial.get(closest).shiftedBy((i - closest) * tStep));
377                     s2iList.add(scToInertial.get(closest).shiftedBy((i - closest) * tStep));
378                 }
379             }
380 
381             // we use Rugged transforms reloading mechanism to ensure the spacecraft
382             // to body transforms will be the same as the ones dumped
383             final SpacecraftToObservedBody scToBody =
384                     new SpacecraftToObservedBody(inertialFrame, ellipsoid.getBodyFrame(),
385                                                  minDate, maxDate, tStep, tolerance,
386                                                  b2iList, s2iList);
387             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
388             new ObjectOutputStream(bos).writeObject(scToBody);
389             final ByteArrayInputStream  bis = new ByteArrayInputStream(bos.toByteArray());
390             builder.setTrajectoryAndTimeSpan(bis);
391 
392             final List<SensorMeanPlaneCrossing> planeCrossings = new ArrayList<>();
393             for (final ParsedSensor parsedSensor : sensors) {
394                 final LineSensor sensor = new LineSensor(parsedSensor.name,
395                                                          parsedSensor,
396                                                          parsedSensor.position,
397                                                          parsedSensor);
398                 if (parsedSensor.meanPlane != null) {
399                     planeCrossings.add(new SensorMeanPlaneCrossing(sensor, scToBody,
400                                                                    parsedSensor.meanPlane.minLine,
401                                                                    parsedSensor.meanPlane.maxLine,
402                                                                    lightTimeCorrection, aberrationOfLightCorrection,
403                                                                    parsedSensor.meanPlane.maxEval,
404                                                                    parsedSensor.meanPlane.accuracy,
405                                                                    parsedSensor.meanPlane.normal,
406                                                                    Arrays.stream(parsedSensor.meanPlane.cachedResults)));
407                 }
408                 builder.addLineSensor(sensor);
409             }
410 
411             final Rugged rugged = builder.build();
412 
413             final Method setPlaneCrossing = Rugged.class.getDeclaredMethod("setPlaneCrossing",
414                                                                            SensorMeanPlaneCrossing.class);
415             setPlaneCrossing.setAccessible(true);
416             for (final SensorMeanPlaneCrossing planeCrossing : planeCrossings) {
417                 setPlaneCrossing.invoke(rugged, planeCrossing);
418             }
419 
420             return rugged;
421 
422         } catch (IOException | NoSuchMethodException  | IllegalAccessException  | InvocationTargetException e) {
423             // this should never happen
424             throw new RuggedInternalError(e);
425         }
426     }
427 
428     /** Get a sensor by name.
429      * @param name sensor name
430      * @return parsed sensor
431      */
432     private ParsedSensor getSensor(final String name) {
433         for (final ParsedSensor sensor : sensors) {
434             if (sensor.name.equals(name)) {
435                 return sensor;
436             }
437         }
438         final ParsedSensor sensor = new ParsedSensor(name);
439         sensors.add(sensor);
440         return sensor;
441     }
442 
443     /** Execute all dumped calls.
444      * <p>
445      * The dumped calls correspond to computation methods like direct or inverse
446      * location.
447      * </p>
448      * @param rugged Rugged instance on which calls will be performed
449      * @return results of all dumped calls
450      */
451     public Result[] execute(final Rugged rugged) {
452         final Result[] results = new Result[calls.size()];
453         for (int i = 0; i < calls.size(); ++i) {
454             results[i] = new Result(calls.get(i).expected,
455                                     calls.get(i).execute(rugged));
456         }
457         return results;
458     }
459 
460     /** Container for replay results. */
461     public static class Result {
462 
463         /** Expected result. */
464         private final Object expected;
465 
466         /** Replayed result. */
467         private final Object replayed;
468 
469         /** Simple constructor.
470          * @param expected expected result
471          * @param replayed replayed result
472          */
473         private Result(final Object expected, final Object replayed) {
474             this.expected = expected;
475             this.replayed = replayed;
476         }
477 
478         /** Get the expected result.
479          * @return expected result
480          */
481         public Object getExpected() {
482             return expected;
483         }
484 
485         /** Get the replayed result.
486          * @return replayed result
487          */
488         public Object getReplayed() {
489             return replayed;
490         }
491 
492     }
493 
494     /** Line parsers. */
495     private enum LineParser {
496 
497         /** Parser for algorithm dump lines. */
498         ALGORITHM() {
499 
500             /** {@inheritDoc} */
501             @Override
502             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
503                 try {
504                     if (fields.length < 1) {
505                         throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
506                     }
507                     global.algorithmId = AlgorithmId.valueOf(fields[0]);
508                     if (global.algorithmId == AlgorithmId.CONSTANT_ELEVATION_OVER_ELLIPSOID) {
509                         if (fields.length < 3 || !fields[1].equals(ELEVATION)) {
510                             throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
511                         }
512                         global.constantElevation = Double.parseDouble(fields[2]);
513                     }
514                 } catch (IllegalArgumentException iae) {
515                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
516                 }
517             }
518 
519         },
520 
521         /** Parser for ellipsoid dump lines. */
522         ELLIPSOID() {
523 
524             /** {@inheritDoc} */
525             @Override
526             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
527                 if (fields.length < 6 || !fields[0].equals(AE) || !fields[2].equals(F) || !fields[4].equals(FRAME)) {
528                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
529                 }
530                 final double ae   = Double.parseDouble(fields[1]);
531                 final double f    = Double.parseDouble(fields[3]);
532                 final Frame  bodyFrame;
533                 try {
534                     bodyFrame = FramesFactory.getFrame(Predefined.valueOf(fields[5]));
535                 } catch (IllegalArgumentException iae) {
536                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
537                 }
538                 global.ellipsoid = new OneAxisEllipsoid(ae, f, bodyFrame);
539             }
540 
541         },
542 
543         /** Parser for direct location calls dump lines. */
544         DIRECT_LOCATION() {
545 
546             /** {@inheritDoc} */
547             @Override
548             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
549                 if (fields.length < 16 ||
550                         !fields[0].equals(DATE) ||
551                         !fields[2].equals(POSITION) || !fields[6].equals(LOS) ||
552                         !fields[10].equals(LIGHT_TIME) || !fields[12].equals(ABERRATION) ||
553                         !fields[14].equals(REFRACTION)) {
554                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
555                 }
556                 final AbsoluteDate date = new AbsoluteDate(fields[1], TimeScalesFactory.getUTC());
557                 final Vector3D position = new Vector3D(Double.parseDouble(fields[3]),
558                         Double.parseDouble(fields[4]),
559                         Double.parseDouble(fields[5]));
560                 final Vector3D los      = new Vector3D(Double.parseDouble(fields[7]),
561                         Double.parseDouble(fields[8]),
562                         Double.parseDouble(fields[9]));
563                 if (global.calls.isEmpty()) {
564                     global.lightTimeCorrection         = Boolean.parseBoolean(fields[11]);
565                     global.aberrationOfLightCorrection = Boolean.parseBoolean(fields[13]);
566                     global.atmosphericRefraction       = Boolean.parseBoolean(fields[15]);
567                 } else {
568                     if (global.lightTimeCorrection != Boolean.parseBoolean(fields[11])) {
569                         throw new RuggedException(RuggedMessages.LIGHT_TIME_CORRECTION_REDEFINED,
570                                 l, file.getAbsolutePath(), line);
571                     }
572                     if (global.aberrationOfLightCorrection != Boolean.parseBoolean(fields[13])) {
573                         throw new RuggedException(RuggedMessages.ABERRATION_OF_LIGHT_CORRECTION_REDEFINED,
574                                 l, file.getAbsolutePath(), line);
575                     }
576                     if (global.atmosphericRefraction != Boolean.parseBoolean(fields[15])) {
577                         throw new RuggedException(RuggedMessages.ATMOSPHERIC_REFRACTION_REDEFINED,
578                                 l, file.getAbsolutePath(), line);
579                     }
580                 }
581                 global.calls.add(new DumpedCall() {
582 
583                     /** {@inheritDoc} */
584                     @Override
585                     public Object execute(final Rugged rugged) {
586                         return rugged.directLocation(date, position, los);
587                     }
588 
589                 });
590             }
591         },
592 
593         /** Parser for direct location result dump lines. */
594         DIRECT_LOCATION_RESULT() {
595 
596             /** {@inheritDoc} */
597             @Override
598             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
599                 if (fields.length == 1) {
600                     if (fields[0].equals(NULL_RESULT)) {
601                         final GeodeticPoint gp = null;
602                         final DumpedCall last = global.calls.get(global.calls.size() - 1);
603                         last.expected = gp;
604                     } else {
605                         throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
606                     }
607                 } else if (fields.length < 6 || !fields[0].equals(LATITUDE) ||
608                            !fields[2].equals(LONGITUDE) || !fields[4].equals(ELEVATION)) {
609                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
610                 } else {
611                     final GeodeticPoint gp = new GeodeticPoint(Double.parseDouble(fields[1]),
612                                                                Double.parseDouble(fields[3]),
613                                                                Double.parseDouble(fields[5]));
614                     final DumpedCall last = global.calls.get(global.calls.size() - 1);
615                     last.expected = gp;
616                 }
617             }
618 
619         },
620 
621         /** Parser for search span dump lines. */
622         SPAN() {
623 
624             /** {@inheritDoc} */
625             @Override
626             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
627                 if (fields.length < 10 ||
628                         !fields[0].equals(MIN_DATE)  || !fields[2].equals(MAX_DATE) || !fields[4].equals(T_STEP)   ||
629                         !fields[6].equals(TOLERANCE) || !fields[8].equals(INERTIAL_FRAME)) {
630                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
631                 }
632                 global.minDate        = new AbsoluteDate(fields[1], TimeScalesFactory.getUTC());
633                 global.maxDate        = new AbsoluteDate(fields[3], TimeScalesFactory.getUTC());
634                 global.tStep          = Double.parseDouble(fields[5]);
635                 global.tolerance      = Double.parseDouble(fields[7]);
636                 global.bodyToInertial = new TreeMap<>();
637                 global.scToInertial   = new TreeMap<>();
638                 try {
639                     global.inertialFrame = FramesFactory.getFrame(Predefined.valueOf(fields[9]));
640                 } catch (IllegalArgumentException iae) {
641                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
642                 }
643             }
644         },
645 
646         /** Parser for observation transforms dump lines. */
647         TRANSFORM() {
648 
649             /** {@inheritDoc} */
650             @Override
651             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
652                 if (fields.length < 42 ||
653                     !fields[0].equals(INDEX) ||
654                     !fields[2].equals(BODY)  ||
655                     !fields[3].equals(R)     || !fields[8].equals(OMEGA)    || !fields[12].equals(OMEGA_DOT) ||
656                     !fields[16].equals(SPACECRAFT) ||
657                     !fields[17].equals(P)    || !fields[21].equals(V)   || !fields[25].equals(A) ||
658                     !fields[29].equals(R)    || !fields[34].equals(OMEGA)   || !fields[38].equals(OMEGA_DOT)) {
659                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
660                 }
661                 final int i   = Integer.parseInt(fields[1]);
662                 final AbsoluteDate date = global.minDate.shiftedBy(i * global.tStep);
663                 global.bodyToInertial.put(i,
664                                           new Transform(date,
665                                                         new Rotation(Double.parseDouble(fields[4]),
666                                                                      Double.parseDouble(fields[5]),
667                                                                      Double.parseDouble(fields[6]),
668                                                                      Double.parseDouble(fields[7]),
669                                                                      false),
670                                                         new Vector3D(Double.parseDouble(fields[9]),
671                                                                      Double.parseDouble(fields[10]),
672                                                                      Double.parseDouble(fields[11])),
673                                                         new Vector3D(Double.parseDouble(fields[13]),
674                                                                      Double.parseDouble(fields[14]),
675                                                                      Double.parseDouble(fields[15]))));
676                 global.scToInertial.put(i,
677                                         new Transform(date,
678                                                       new Transform(date,
679                                                                     new Vector3D(Double.parseDouble(fields[18]),
680                                                                                  Double.parseDouble(fields[19]),
681                                                                                  Double.parseDouble(fields[20])),
682                                                                     new Vector3D(Double.parseDouble(fields[22]),
683                                                                                  Double.parseDouble(fields[23]),
684                                                                                  Double.parseDouble(fields[24])),
685                                                                     new Vector3D(Double.parseDouble(fields[26]),
686                                                                                  Double.parseDouble(fields[27]),
687                                                                                  Double.parseDouble(fields[28]))),
688                                                       new Transform(date,
689                                                                     new Rotation(Double.parseDouble(fields[30]),
690                                                                                  Double.parseDouble(fields[31]),
691                                                                                  Double.parseDouble(fields[32]),
692                                                                                  Double.parseDouble(fields[33]),
693                                                                                  false),
694                                                                     new Vector3D(Double.parseDouble(fields[35]),
695                                                                                  Double.parseDouble(fields[36]),
696                                                                                  Double.parseDouble(fields[37])),
697                                                                     new Vector3D(Double.parseDouble(fields[39]),
698                                                                                  Double.parseDouble(fields[40]),
699                                                                                  Double.parseDouble(fields[41])))));
700             }
701 
702         },
703 
704         /** Parser for DEM tile global geometry dump lines. */
705         DEM_TILE() {
706 
707             /** {@inheritDoc} */
708             @Override
709             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
710                 if (fields.length < 13 ||
711                         !fields[1].equals(LAT_MIN) || !fields[3].equals(LAT_STEP) || !fields[5].equals(LAT_ROWS) ||
712                         !fields[7].equals(LON_MIN) || !fields[9].equals(LON_STEP) || !fields[11].equals(LON_COLS)) {
713                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
714                 }
715                 final String name             = fields[0];
716                 final double minLatitude      = Double.parseDouble(fields[2]);
717                 final double latitudeStep     = Double.parseDouble(fields[4]);
718                 final int    latitudeRows     = Integer.parseInt(fields[6]);
719                 final double minLongitude     = Double.parseDouble(fields[8]);
720                 final double longitudeStep    = Double.parseDouble(fields[10]);
721                 final int    longitudeColumns = Integer.parseInt(fields[12]);
722                 for (final ParsedTile tile : global.tiles) {
723                     if (tile.name.equals(name)) {
724                         throw new RuggedException(RuggedMessages.TILE_ALREADY_DEFINED,
725                                                   name, l, file.getAbsolutePath(), line);
726                     }
727                 }
728                 global.tiles.add(new ParsedTile(name,
729                                                 minLatitude, latitudeStep, latitudeRows,
730                                                 minLongitude, longitudeStep, longitudeColumns));
731             }
732 
733         },
734 
735         /** Parser for DEM cells dump lines. */
736         DEM_CELL() {
737 
738             /** {@inheritDoc} */
739             @Override
740             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
741                 if (fields.length < 7 ||
742                     !fields[1].equals(LAT_INDEX) || !fields[3].equals(LON_INDEX) || !fields[5].equals(ELEVATION)) {
743                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
744                 }
745                 final String name      = fields[0];
746                 final int    latIndex  = Integer.parseInt(fields[2]);
747                 final int    lonIndex  = Integer.parseInt(fields[4]);
748                 final double elevation = Double.parseDouble(fields[6]);
749                 for (final ParsedTile tile : global.tiles) {
750                     if (tile.name.equals(name)) {
751                         final int index = latIndex * tile.longitudeColumns + lonIndex;
752                         tile.elevations.put(index, elevation);
753                         return;
754                     }
755                 }
756                 throw new RuggedException(RuggedMessages.UNKNOWN_TILE,
757                                           name, l, file.getAbsolutePath(), line);
758             }
759 
760         },
761 
762         /** Parser for inverse location calls dump lines. */
763         INVERSE_LOCATION() {
764 
765             /** {@inheritDoc} */
766             @Override
767             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
768                 if (fields.length < 18 ||
769                         !fields[0].equals(SENSOR_NAME) ||
770                         !fields[2].equals(LATITUDE) || !fields[4].equals(LONGITUDE) || !fields[6].equals(ELEVATION) ||
771                         !fields[8].equals(MIN_LINE) || !fields[10].equals(MAX_LINE) ||
772                         !fields[12].equals(LIGHT_TIME) || !fields[14].equals(ABERRATION) ||
773                         !fields[16].equals(REFRACTION)) {
774                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
775                 }
776                 final String sensorName = fields[1];
777                 final GeodeticPoint point = new GeodeticPoint(Double.parseDouble(fields[3]),
778                                                               Double.parseDouble(fields[5]),
779                                                               Double.parseDouble(fields[7]));
780                 final int minLine = Integer.parseInt(fields[9]);
781                 final int maxLine = Integer.parseInt(fields[11]);
782                 if (global.calls.isEmpty()) {
783                     global.lightTimeCorrection         = Boolean.parseBoolean(fields[13]);
784                     global.aberrationOfLightCorrection = Boolean.parseBoolean(fields[15]);
785                     global.atmosphericRefraction       = Boolean.parseBoolean(fields[17]);
786                 } else {
787                     if (global.lightTimeCorrection != Boolean.parseBoolean(fields[13])) {
788                         throw new RuggedException(RuggedMessages.LIGHT_TIME_CORRECTION_REDEFINED,
789                                                   l, file.getAbsolutePath(), line);
790                     }
791                     if (global.aberrationOfLightCorrection != Boolean.parseBoolean(fields[15])) {
792                         throw new RuggedException(RuggedMessages.ABERRATION_OF_LIGHT_CORRECTION_REDEFINED,
793                                                   l, file.getAbsolutePath(), line);
794                     }
795                     if (global.atmosphericRefraction != Boolean.parseBoolean(fields[17])) {
796                         throw new RuggedException(RuggedMessages.ATMOSPHERIC_REFRACTION_REDEFINED,
797                                                   l, file.getAbsolutePath(), line);
798                     }
799                 }
800                 global.calls.add(new DumpedCall() {
801 
802                     /** {@inheritDoc} */
803                     @Override
804                     public Object execute(final Rugged rugged) {
805                         return rugged.inverseLocation(sensorName, point, minLine, maxLine);
806                     }
807 
808                 });
809             }
810 
811         },
812 
813         /** Parser for inverse location result dump lines. */
814         INVERSE_LOCATION_RESULT() {
815 
816             /** {@inheritDoc} */
817             @Override
818             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
819                 if (fields.length == 1) {
820                     if (fields[0].equals(NULL_RESULT)) {
821                         final SensorPixel sp = null;
822                         final DumpedCall last = global.calls.get(global.calls.size() - 1);
823                         last.expected = sp;
824                     } else {
825                         throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
826                     }
827                 } else if (fields.length < 4 || !fields[0].equals(LINE_NUMBER) || !fields[2].equals(PIXEL_NUMBER)) {
828                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
829                 } else {
830                     final SensorPixel sp = new SensorPixel(Double.parseDouble(fields[1]),
831                                                            Double.parseDouble(fields[3]));
832                     final DumpedCall last = global.calls.get(global.calls.size() - 1);
833                     last.expected = sp;
834                 }
835             }
836 
837         },
838 
839         /** Parser for sensor dump lines. */
840         SENSOR() {
841 
842             /** {@inheritDoc} */
843             @Override
844             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
845                 if (fields.length < 8 || !fields[0].equals(SENSOR_NAME) ||
846                     !fields[2].equals(NB_PIXELS) || !fields[4].equals(POSITION)) {
847                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
848                 }
849                 final ParsedSensor sensor = global.getSensor(fields[1]);
850                 sensor.setNbPixels(Integer.parseInt(fields[3]));
851                 sensor.setPosition(new Vector3D(Double.parseDouble(fields[5]),
852                                                 Double.parseDouble(fields[6]),
853                                                 Double.parseDouble(fields[7])));
854             }
855 
856         },
857 
858         /** Parser for sensor mean plane dump lines. */
859         SENSOR_MEAN_PLANE() {
860 
861             /** {@inheritDoc} */
862             @Override
863             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
864                 if (fields.length < 16 || !fields[0].equals(SENSOR_NAME) ||
865                         !fields[2].equals(MIN_LINE) || !fields[4].equals(MAX_LINE) ||
866                         !fields[6].equals(MAX_EVAL) || !fields[8].equals(ACCURACY) ||
867                         !fields[10].equals(NORMAL)  || !fields[14].equals(CACHED_RESULTS)) {
868                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
869                 }
870                 final String   sensorName = fields[1];
871                 final int      minLine    = Integer.parseInt(fields[3]);
872                 final int      maxLine    = Integer.parseInt(fields[5]);
873                 final int      maxEval    = Integer.parseInt(fields[7]);
874                 final double   accuracy   = Double.parseDouble(fields[9]);
875                 final Vector3D normal     = new Vector3D(Double.parseDouble(fields[11]),
876                         Double.parseDouble(fields[12]),
877                         Double.parseDouble(fields[13]));
878                 final int      n          = Integer.parseInt(fields[15]);
879                 final CrossingResult[] cachedResults = new CrossingResult[n];
880                 int base = 16;
881                 for (int i = 0; i < n; ++i) {
882                     if (fields.length < base + 15 || !fields[base].equals(LINE_NUMBER) ||
883                             !fields[base + 2].equals(DATE) || !fields[base + 4].equals(TARGET) ||
884                             !fields[base + 8].equals(TARGET_DIRECTION)) {
885                         throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
886                     }
887                     final double       ln                    = Double.parseDouble(fields[base + 1]);
888                     final AbsoluteDate date                  = new AbsoluteDate(fields[base + 3], TimeScalesFactory.getUTC());
889                     final Vector3D     target                = new Vector3D(Double.parseDouble(fields[base +  5]),
890                             Double.parseDouble(fields[base +  6]),
891                             Double.parseDouble(fields[base +  7]));
892                     final Vector3D targetDirection           = new Vector3D(Double.parseDouble(fields[base +  9]),
893                             Double.parseDouble(fields[base + 10]),
894                             Double.parseDouble(fields[base + 11]));
895                     final Vector3D targetDirectionDerivative = new Vector3D(Double.parseDouble(fields[base + 12]),
896                             Double.parseDouble(fields[base + 13]),
897                             Double.parseDouble(fields[base + 14]));
898                     cachedResults[i] = new CrossingResult(date, ln, target, targetDirection, targetDirectionDerivative);
899                     base += 15;
900                 }
901                 global.getSensor(sensorName).setMeanPlane(new ParsedMeanPlane(minLine, maxLine, maxEval, accuracy, normal, cachedResults));
902             }
903         },
904 
905         /** Parser for sensor LOS dump lines. */
906         SENSOR_LOS() {
907 
908             /** {@inheritDoc} */
909             @Override
910             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
911                 if (fields.length < 10 || !fields[0].equals(SENSOR_NAME) ||
912                         !fields[2].equals(DATE) || !fields[4].equals(PIXEL_NUMBER) ||
913                         !fields[6].equals(LOS)) {
914                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
915                 }
916                 final String       sensorName  = fields[1];
917                 final AbsoluteDate date        = new AbsoluteDate(fields[3], TimeScalesFactory.getUTC());
918                 final int          pixelNumber = Integer.parseInt(fields[5]);
919                 final Vector3D     los         = new Vector3D(Double.parseDouble(fields[7]),
920                         Double.parseDouble(fields[8]),
921                         Double.parseDouble(fields[9]));
922                 global.getSensor(sensorName).setLOS(date, pixelNumber, los);
923             }
924         },
925 
926         /** Parser for sensor datation dump lines. */
927         SENSOR_DATATION() {
928 
929             /** {@inheritDoc} */
930             @Override
931             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
932                 if (fields.length < 6 || !fields[0].equals(SENSOR_NAME) ||
933                         !fields[2].equals(LINE_NUMBER) || !fields[4].equals(DATE)) {
934                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
935                 }
936                 final String       sensorName  = fields[1];
937                 final double       lineNumber  = Double.parseDouble(fields[3]);
938                 final AbsoluteDate date        = new AbsoluteDate(fields[5], TimeScalesFactory.getUTC());
939                 global.getSensor(sensorName).setDatation(lineNumber, date);
940             }
941         },
942 
943         /** Parser for sensor rate dump lines. */
944         SENSOR_RATE() {
945 
946             /** {@inheritDoc} */
947             @Override
948             public void parse(final int l, final File file, final String line, final String[] fields, final DumpReplayer global) {
949                 if (fields.length < 6 || !fields[0].equals(SENSOR_NAME) ||
950                     !fields[2].equals(LINE_NUMBER) || !fields[4].equals(RATE)) {
951                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
952                 }
953                 final String       sensorName  = fields[1];
954                 final double       lineNumber  = Double.parseDouble(fields[3]);
955                 final double       rate  = Double.parseDouble(fields[5]);
956                 global.getSensor(sensorName).setRate(lineNumber, rate);
957 
958             }
959 
960         };
961 
962         /** Parse a line.
963          * @param l line number
964          * @param file dump file
965          * @param line line to parse
966          * @param global global parser to store parsed data
967          */
968         public static void parse(final int l, final File file, final String line, final DumpReplayer global) {
969 
970             final String trimmed = line.trim();
971             if (trimmed.isEmpty() || trimmed.startsWith(COMMENT_START)) {
972                 return;
973             }
974 
975             final int colon = line.indexOf(':');
976             if (colon > 0) {
977                 final String parsedKey = PATTERN.matcher(line.substring(0, colon).trim()).replaceAll("_").toUpperCase();
978                 try {
979                     final LineParser parser = LineParser.valueOf(parsedKey);
980                     final String[] fields;
981                     if (colon + 1 >= line.length()) {
982                         fields = new String[0];
983                     } else {
984                         fields = SEPARATOR.split(line.substring(colon + 1).trim());
985                     }
986                     parser.parse(l, file, line, fields, global);
987                 } catch (IllegalArgumentException iae) {
988                     throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
989                 }
990 
991             } else {
992                 throw new RuggedException(RuggedMessages.CANNOT_PARSE_LINE, l, file, line);
993             }
994 
995         }
996 
997         /** Parse a line.
998          * @param l line number
999          * @param file dump file
1000          * @param line complete line
1001          * @param fields data fields from the line
1002          * @param global global parser to store parsed data
1003          */
1004         public abstract void parse(int l, File file, String line, String[] fields, DumpReplayer global);
1005 
1006     }
1007 
1008     /** Local class for handling already parsed tile data. */
1009     private static class ParsedTile {
1010 
1011         /** Name of the tile. */
1012         private final String name;
1013 
1014         /** Minimum latitude. */
1015         private final double minLatitude;
1016 
1017         /** Step in latitude (size of one raster element). */
1018         private final double latitudeStep;
1019 
1020         /** Number of latitude rows. */
1021         private final int latitudeRows;
1022 
1023         /** Minimum longitude. */
1024         private final double minLongitude;
1025 
1026         /** Step in longitude (size of one raster element). */
1027         private final double longitudeStep;
1028 
1029         /** Number of longitude columns. */
1030         private final int longitudeColumns;
1031 
1032         /** Raster elevation data. */
1033         private final OpenIntToDoubleHashMap elevations;
1034 
1035         /** Simple constructor.
1036          * @param name of the tile
1037          * @param minLatitude minimum latitude
1038          * @param latitudeStep step in latitude (size of one raster element)
1039          * @param latitudeRows number of latitude rows
1040          * @param minLongitude minimum longitude
1041          * @param longitudeStep step in longitude (size of one raster element)
1042          * @param longitudeColumns number of longitude columns
1043          */
1044         ParsedTile(final String name,
1045                    final double minLatitude, final double latitudeStep, final int latitudeRows,
1046                    final double minLongitude, final double longitudeStep, final int longitudeColumns) {
1047             this.name             = name;
1048             this.minLatitude      = minLatitude;
1049             this.latitudeStep     = latitudeStep;
1050             this.minLongitude     = minLongitude;
1051             this.longitudeStep    = longitudeStep;
1052             this.latitudeRows     = latitudeRows;
1053             this.longitudeColumns = longitudeColumns;
1054             this.elevations       = new OpenIntToDoubleHashMap();
1055         }
1056 
1057         /** Check if a point is in the interpolable region of the tile.
1058          * @param latitude point latitude
1059          * @param longitude point longitude
1060          * @return true if the point is in the interpolable region of the tile
1061          */
1062         public boolean isInterpolable(final double latitude, final double longitude) {
1063             final int latitudeIndex  = (int) FastMath.floor((latitude  - minLatitude)  / latitudeStep);
1064             final int longitudeIndex = (int) FastMath.floor((longitude - minLongitude) / longitudeStep);
1065             return latitudeIndex  >= 0 && latitudeIndex  <= latitudeRows     - 2 &&
1066                    longitudeIndex >= 0 && longitudeIndex <= longitudeColumns - 2;
1067         }
1068 
1069         /** Update the tile according to the Digital Elevation Model.
1070          * @param tile to update
1071          */
1072         public void updateTile(final UpdatableTile tile) {
1073 
1074             tile.setGeometry(minLatitude, minLongitude,
1075                              latitudeStep, longitudeStep,
1076                              latitudeRows, longitudeColumns);
1077 
1078             final OpenIntToDoubleHashMap.Iterator iterator = elevations.iterator();
1079             while (iterator.hasNext()) {
1080                 iterator.advance();
1081                 final int    index          = iterator.key();
1082                 final int    latitudeIndex  = index / longitudeColumns;
1083                 final int    longitudeIndex = index % longitudeColumns;
1084                 final double elevation      = iterator.value();
1085                 tile.setElevation(latitudeIndex, longitudeIndex, elevation);
1086             }
1087 
1088         }
1089 
1090     }
1091 
1092     /** Local class for handling already parsed sensor data. */
1093     private static class ParsedSensor implements LineDatation, TimeDependentLOS {
1094 
1095         /** Name of the sensor. */
1096         private final String name;
1097 
1098         /** Number of pixels. */
1099         private int nbPixels;
1100 
1101         /** Position. */
1102         private Vector3D position;
1103 
1104         /** Mean plane crossing finder. */
1105         private ParsedMeanPlane meanPlane;
1106 
1107         /** LOS map. */
1108         private final Map<Integer, List<Pair<AbsoluteDate, Vector3D>>> losMap;
1109 
1110         /** Datation. */
1111         private final List<Pair<Double, AbsoluteDate>> datation;
1112 
1113         /** Rate. */
1114         private final List<Pair<Double, Double>> rates;
1115 
1116         /** simple constructor.
1117          * @param name name of the sensor
1118          */
1119         ParsedSensor(final String name) {
1120             this.name     = name;
1121             this.losMap   = new HashMap<>();
1122             this.datation = new ArrayList<>();
1123             this.rates    = new ArrayList<>();
1124         }
1125 
1126         /** Set the mean place finder.
1127          * @param meanPlane mean plane finder
1128          */
1129         public void setMeanPlane(final ParsedMeanPlane meanPlane) {
1130             this.meanPlane = meanPlane;
1131         }
1132 
1133         /** Set the position.
1134          * @param position position
1135          */
1136         public void setPosition(final Vector3D position) {
1137             this.position = position;
1138         }
1139 
1140         /** Set the number of pixels.
1141          * @param nbPixels number of pixels
1142          */
1143         public void setNbPixels(final int nbPixels) {
1144             this.nbPixels = nbPixels;
1145         }
1146 
1147         /** {@inheritDoc} */
1148         @Override
1149         public int getNbPixels() {
1150             return nbPixels;
1151         }
1152 
1153         /** Set a los direction.
1154          * @param date date
1155          * @param pixelNumber number of the pixel
1156          * @param los los direction
1157          */
1158         public void setLOS(final AbsoluteDate date, final int pixelNumber, final Vector3D los) {
1159             List<Pair<AbsoluteDate, Vector3D>> list = losMap.get(pixelNumber);
1160             if (list == null) {
1161                 list = new ArrayList<>();
1162                 losMap.put(pixelNumber, list);
1163             }
1164             // find insertion index to have LOS sorted chronologically
1165             int index = 0;
1166             while (index < list.size()) {
1167                 if (list.get(index).getFirst().compareTo(date) > 0) {
1168                     break;
1169                 }
1170                 ++index;
1171             }
1172             list.add(index, new Pair<>(date, los));
1173         }
1174 
1175         /** {@inheritDoc} */
1176         @Override
1177         public Vector3D getLOS(final int index, final AbsoluteDate date) {
1178             final List<Pair<AbsoluteDate, Vector3D>> list = losMap.get(index);
1179             if (list == null) {
1180                 throw new RuggedInternalError(null);
1181             }
1182 
1183             if (list.size() < 2) {
1184                 return list.get(0).getSecond();
1185             }
1186 
1187             // find entries bracketing the the date
1188             int sup = 0;
1189             while (sup < list.size() - 1) {
1190                 if (list.get(sup).getFirst().compareTo(date) >= 0) {
1191                     break;
1192                 }
1193                 ++sup;
1194             }
1195             final int inf = (sup == 0) ? sup++ : (sup - 1);
1196 
1197             final AbsoluteDate dInf  = list.get(inf).getFirst();
1198             final Vector3D     lInf  = list.get(inf).getSecond();
1199             final AbsoluteDate dSup  = list.get(sup).getFirst();
1200             final Vector3D     lSup  = list.get(sup).getSecond();
1201             final double       alpha = date.durationFrom(dInf) / dSup.durationFrom(dInf);
1202             return new Vector3D(alpha, lSup, 1 - alpha, lInf);
1203 
1204         }
1205 
1206         /** {@inheritDoc} */
1207         @Override
1208         public <T extends Derivative<T>> FieldVector3D<T> getLOSDerivatives(final int index, final AbsoluteDate date,
1209                                                                             final DerivativeGenerator<T> generator) {
1210             final Vector3D los = getLOS(index, date);
1211             return new FieldVector3D<>(generator.constant(los.getX()),
1212                                        generator.constant(los.getY()),
1213                                        generator.constant(los.getZ()));
1214         }
1215 
1216         /** Set a datation pair.
1217          * @param lineNumber line number
1218          * @param date date
1219          */
1220         public void setDatation(final double lineNumber, final AbsoluteDate date) {
1221             // find insertion index to have datations sorted chronologically
1222             int index = 0;
1223             while (index < datation.size()) {
1224                 if (datation.get(index).getSecond().compareTo(date) > 0) {
1225                     break;
1226                 }
1227                 ++index;
1228             }
1229             datation.add(index, new Pair<>(lineNumber, date));
1230         }
1231 
1232         /** {@inheritDoc} */
1233         @Override
1234         public AbsoluteDate getDate(final double lineNumber) {
1235 
1236             if (datation.size() < 2) {
1237                 return datation.get(0).getSecond();
1238             }
1239 
1240             // find entries bracketing the line number
1241             int sup = 0;
1242             while (sup < datation.size() - 1) {
1243                 if (datation.get(sup).getFirst() >= lineNumber) {
1244                     break;
1245                 }
1246                 ++sup;
1247             }
1248             final int inf = (sup == 0) ? sup++ : (sup - 1);
1249 
1250             final double       lInf  = datation.get(inf).getFirst();
1251             final AbsoluteDate dInf  = datation.get(inf).getSecond();
1252             final double       lSup  = datation.get(sup).getFirst();
1253             final AbsoluteDate dSup  = datation.get(sup).getSecond();
1254             final double       alpha = (lineNumber - lInf) / (lSup - lInf);
1255             return dInf.shiftedBy(alpha * dSup.durationFrom(dInf));
1256 
1257         }
1258 
1259         /** {@inheritDoc} */
1260         @Override
1261         public double getLine(final AbsoluteDate date) {
1262 
1263             if (datation.size() < 2) {
1264                 return datation.get(0).getFirst();
1265             }
1266 
1267             // find entries bracketing the date
1268             int sup = 0;
1269             while (sup < datation.size() - 1) {
1270                 if (datation.get(sup).getSecond().compareTo(date) >= 0) {
1271                     break;
1272                 }
1273                 ++sup;
1274             }
1275             final int inf = (sup == 0) ? sup++ : (sup - 1);
1276 
1277             final double       lInf  = datation.get(inf).getFirst();
1278             final AbsoluteDate dInf  = datation.get(inf).getSecond();
1279             final double       lSup  = datation.get(sup).getFirst();
1280             final AbsoluteDate dSup  = datation.get(sup).getSecond();
1281             final double       alpha = date.durationFrom(dInf) / dSup.durationFrom(dInf);
1282             return alpha * lSup + (1 - alpha) * lInf;
1283 
1284         }
1285 
1286         /** Set a rate.
1287          * @param lineNumber line number
1288          * @param rate lines rate
1289          */
1290         public void setRate(final double lineNumber, final double rate) {
1291             // find insertion index to have rates sorted by line numbers
1292             int index = 0;
1293             while (index < rates.size()) {
1294                 if (rates.get(index).getFirst() > lineNumber) {
1295                     break;
1296                 }
1297                 ++index;
1298             }
1299             rates.add(index, new Pair<>(lineNumber, rate));
1300         }
1301 
1302         /** {@inheritDoc} */
1303         @Override
1304         public double getRate(final double lineNumber) {
1305 
1306             if (rates.size() < 2) {
1307                 return rates.get(0).getSecond();
1308             }
1309 
1310             // find entries bracketing the line number
1311             int sup = 0;
1312             while (sup < rates.size() - 1) {
1313                 if (rates.get(sup).getFirst() >= lineNumber) {
1314                     break;
1315                 }
1316                 ++sup;
1317             }
1318             final int inf = (sup == 0) ? sup++ : (sup - 1);
1319 
1320             final double lInf  = rates.get(inf).getFirst();
1321             final double rInf  = rates.get(inf).getSecond();
1322             final double lSup  = rates.get(sup).getFirst();
1323             final double rSup  = rates.get(sup).getSecond();
1324             final double alpha = (lineNumber - lInf) / (lSup - lInf);
1325             return alpha * rSup + (1 - alpha) * rInf;
1326 
1327         }
1328 
1329         /** {@inheritDoc} */
1330         @Override
1331         public Stream<ParameterDriver> getParametersDrivers() {
1332             return Stream.<ParameterDriver>empty();
1333         }
1334 
1335     }
1336 
1337     /** Local class for handling already parsed mean plane data. */
1338     private static class ParsedMeanPlane {
1339 
1340         /** Min line. */
1341         private final int minLine;
1342 
1343         /** Max line. */
1344         private final int maxLine;
1345 
1346         /** Maximum number of evaluations. */
1347         private final int maxEval;
1348 
1349         /** Accuracy to use for finding crossing line number. */
1350         private final double accuracy;
1351 
1352         /** Mean plane normal. */
1353         private final Vector3D normal;
1354 
1355         /** Cached results. */
1356         private final CrossingResult[] cachedResults;
1357 
1358         /** simple constructor.
1359          * @param minLine min line
1360          * @param maxLine max line
1361          * @param maxEval maximum number of evaluations
1362          * @param accuracy accuracy to use for finding crossing line number
1363          * @param normal mean plane normal
1364          * @param cachedResults cached results
1365          */
1366         ParsedMeanPlane(final int minLine, final int maxLine,
1367                         final int maxEval, final double accuracy, final Vector3D normal,
1368                         final CrossingResult[] cachedResults) {
1369             this.minLine       = minLine;
1370             this.maxLine       = maxLine;
1371             this.maxEval       = maxEval;
1372             this.accuracy      = accuracy;
1373             this.normal        = normal;
1374             this.cachedResults = cachedResults.clone();
1375         }
1376 
1377     }
1378 
1379     /** Local interface for dumped calls. */
1380     private abstract static class DumpedCall {
1381 
1382         /** Expected result. */
1383         private Object expected;
1384 
1385         /** Execute a call.
1386          * @param rugged Rugged instance on which called should be performed
1387          * @return result of the call
1388          */
1389         public abstract Object execute(Rugged rugged);
1390 
1391     }
1392 
1393 }