1   /* Copyright 2002-2018 CS Systèmes d'Information
2    * Licensed to CS Systèmes d'Information (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.propagation;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.stream.Stream;
26  
27  import org.hipparchus.analysis.interpolation.HermiteInterpolator;
28  import org.hipparchus.exception.LocalizedCoreFormats;
29  import org.hipparchus.exception.MathIllegalStateException;
30  import org.hipparchus.geometry.euclidean.threed.Rotation;
31  import org.hipparchus.geometry.euclidean.threed.Vector3D;
32  import org.hipparchus.util.FastMath;
33  import org.orekit.attitudes.Attitude;
34  import org.orekit.attitudes.LofOffset;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.errors.OrekitExceptionWrapper;
37  import org.orekit.errors.OrekitIllegalArgumentException;
38  import org.orekit.errors.OrekitMessages;
39  import org.orekit.frames.Frame;
40  import org.orekit.frames.LOFType;
41  import org.orekit.frames.Transform;
42  import org.orekit.orbits.Orbit;
43  import org.orekit.time.AbsoluteDate;
44  import org.orekit.time.TimeInterpolable;
45  import org.orekit.time.TimeShiftable;
46  import org.orekit.time.TimeStamped;
47  import org.orekit.utils.TimeStampedAngularCoordinates;
48  import org.orekit.utils.TimeStampedPVCoordinates;
49  
50  
51  /** This class is the representation of a complete state holding orbit, attitude
52   * and mass information at a given date.
53   *
54   * <p>It contains an {@link Orbit orbital state} at a current
55   * {@link AbsoluteDate} both handled by an {@link Orbit}, plus the current
56   * mass and attitude. Orbit and state are guaranteed to be consistent in terms
57   * of date and reference frame. The spacecraft state may also contain additional
58   * states, which are simply named double arrays which can hold any user-defined
59   * data.
60   * </p>
61   * <p>
62   * The state can be slightly shifted to close dates. This shift is based on
63   * a simple Keplerian model for orbit, a linear extrapolation for attitude
64   * taking the spin rate into account and no mass change. It is <em>not</em>
65   * intended as a replacement for proper orbit and attitude propagation but
66   * should be sufficient for either small time shifts or coarse accuracy.
67   * </p>
68   * <p>
69   * The instance <code>SpacecraftState</code> is guaranteed to be immutable.
70   * </p>
71   * @see org.orekit.propagation.numerical.NumericalPropagator
72   * @author Fabien Maussion
73   * @author V&eacute;ronique Pommier-Maurussane
74   * @author Luc Maisonobe
75   */
76  public class SpacecraftState
77      implements TimeStamped, TimeShiftable<SpacecraftState>, TimeInterpolable<SpacecraftState>, Serializable {
78  
79      /** Serializable UID. */
80      private static final long serialVersionUID = 20130407L;
81  
82      /** Default mass. */
83      private static final double DEFAULT_MASS = 1000.0;
84  
85      /**
86       * tolerance on date comparison in {@link #checkConsistency(Orbit, Attitude)}. 100 ns
87       * corresponds to sub-mm accuracy at LEO orbital velocities.
88       */
89      private static final double DATE_INCONSISTENCY_THRESHOLD = 100e-9;
90  
91      /** Orbital state. */
92      private final Orbit orbit;
93  
94      /** Attitude. */
95      private final Attitude attitude;
96  
97      /** Current mass (kg). */
98      private final double mass;
99  
100     /** Additional states. */
101     private final Map<String, double[]> additional;
102 
103     /** Build a spacecraft state from orbit only.
104      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
105      * @param orbit the orbit
106      * @exception OrekitException if default attitude cannot be computed
107      */
108     public SpacecraftState(final Orbit orbit)
109         throws OrekitException {
110         this(orbit,
111              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
112              DEFAULT_MASS, null);
113     }
114 
115     /** Build a spacecraft state from orbit and attitude provider.
116      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
117      * @param orbit the orbit
118      * @param attitude attitude
119      * @exception IllegalArgumentException if orbit and attitude dates
120      * or frames are not equal
121      */
122     public SpacecraftState(final Orbit orbit, final Attitude attitude)
123         throws IllegalArgumentException {
124         this(orbit, attitude, DEFAULT_MASS, null);
125     }
126 
127     /** Create a new instance from orbit and mass.
128      * <p>Attitude law is set to an unspecified default attitude.</p>
129      * @param orbit the orbit
130      * @param mass the mass (kg)
131      * @exception OrekitException if default attitude cannot be computed
132      */
133     public SpacecraftState(final Orbit orbit, final double mass)
134         throws OrekitException {
135         this(orbit,
136              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
137              mass, null);
138     }
139 
140     /** Build a spacecraft state from orbit, attitude provider and mass.
141      * @param orbit the orbit
142      * @param attitude attitude
143      * @param mass the mass (kg)
144      * @exception IllegalArgumentException if orbit and attitude dates
145      * or frames are not equal
146      */
147     public SpacecraftState(final Orbit orbit, final Attitude attitude, final double mass)
148         throws IllegalArgumentException {
149         this(orbit, attitude, mass, null);
150     }
151 
152     /** Build a spacecraft state from orbit only.
153      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
154      * @param orbit the orbit
155      * @param additional additional states
156      * @exception OrekitException if default attitude cannot be computed
157      */
158     public SpacecraftState(final Orbit orbit, final Map<String, double[]> additional)
159         throws OrekitException {
160         this(orbit,
161              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
162              DEFAULT_MASS, additional);
163     }
164 
165     /** Build a spacecraft state from orbit and attitude provider.
166      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
167      * @param orbit the orbit
168      * @param attitude attitude
169      * @param additional additional states
170      * @exception IllegalArgumentException if orbit and attitude dates
171      * or frames are not equal
172      */
173     public SpacecraftState(final Orbit orbit, final Attitude attitude, final Map<String, double[]> additional)
174         throws IllegalArgumentException {
175         this(orbit, attitude, DEFAULT_MASS, additional);
176     }
177 
178     /** Create a new instance from orbit and mass.
179      * <p>Attitude law is set to an unspecified default attitude.</p>
180      * @param orbit the orbit
181      * @param mass the mass (kg)
182      * @param additional additional states
183      * @exception OrekitException if default attitude cannot be computed
184      */
185     public SpacecraftState(final Orbit orbit, final double mass, final Map<String, double[]> additional)
186         throws OrekitException {
187         this(orbit,
188              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
189              mass, additional);
190     }
191 
192     /** Build a spacecraft state from orbit, attitude provider and mass.
193      * @param orbit the orbit
194      * @param attitude attitude
195      * @param mass the mass (kg)
196      * @param additional additional states (may be null if no additional states are available)
197      * @exception IllegalArgumentException if orbit and attitude dates
198      * or frames are not equal
199      */
200     public SpacecraftState(final Orbit orbit, final Attitude attitude,
201                            final double mass, final Map<String, double[]> additional)
202         throws IllegalArgumentException {
203         checkConsistency(orbit, attitude);
204         this.orbit      = orbit;
205         this.attitude   = attitude;
206         this.mass       = mass;
207         if (additional == null) {
208             this.additional = Collections.emptyMap();
209         } else {
210             this.additional = new HashMap<String, double[]>(additional.size());
211             for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
212                 this.additional.put(entry.getKey(), entry.getValue().clone());
213             }
214         }
215     }
216 
217     /** Add an additional state.
218      * <p>
219      * {@link SpacecraftState SpacecraftState} instances are immutable,
220      * so this method does <em>not</em> change the instance, but rather
221      * creates a new instance, which has the same orbit, attitude, mass
222      * and additional states as the original instance, except it also
223      * has the specified state. If the original instance already had an
224      * additional state with the same name, it will be overridden. If it
225      * did not have any additional state with that name, the new instance
226      * will have one more additional state than the original instance.
227      * </p>
228      * @param name name of the additional state
229      * @param value value of the additional state
230      * @return a new instance, with the additional state added
231      * @see #hasAdditionalState(String)
232      * @see #getAdditionalState(String)
233      * @see #getAdditionalStates()
234      */
235     public SpacecraftState addAdditionalState(final String name, final double... value) {
236         final Map<String, double[]> newMap = new HashMap<String, double[]>(additional.size() + 1);
237         newMap.putAll(additional);
238         newMap.put(name, value.clone());
239         return new SpacecraftState(orbit, attitude, mass, newMap);
240     }
241 
242     /** Check orbit and attitude dates are equal.
243      * @param orbit the orbit
244      * @param attitude attitude
245      * @exception IllegalArgumentException if orbit and attitude dates
246      * are not equal
247      */
248     private static void checkConsistency(final Orbit orbit, final Attitude attitude)
249         throws IllegalArgumentException {
250         if (FastMath.abs(orbit.getDate().durationFrom(attitude.getDate())) >
251             DATE_INCONSISTENCY_THRESHOLD) {
252             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
253                                                      orbit.getDate(), attitude.getDate());
254         }
255         if (orbit.getFrame() != attitude.getReferenceFrame()) {
256             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
257                                                      orbit.getFrame().getName(),
258                                                      attitude.getReferenceFrame().getName());
259         }
260     }
261 
262     /** Get a time-shifted state.
263      * <p>
264      * The state can be slightly shifted to close dates. This shift is based on
265      * simple models. For orbits, the model is a Keplerian one if no derivatives
266      * are available in the orbit, or Keplerian plus quadratic effect of the
267      * non-Keplerian acceleration if derivatives are available. For attitude,
268      * a polynomial model is used. Neither mass nor additional states change.
269      * Shifting is <em>not</em> intended as a replacement for proper orbit
270      * and attitude propagation but should be sufficient for small time shifts
271      * or coarse accuracy.
272      * </p>
273      * <p>
274      * As a rough order of magnitude, the following table shows the extrapolation
275      * errors obtained between this simple shift method and an {@link
276      * org.orekit.propagation.numerical.NumericalPropagator numerical
277      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
278      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
279      * Beware that these results will be different for other orbits.
280      * </p>
281      * <table border="1" cellpadding="5">
282      * <caption>Extrapolation Error</caption>
283      * <tr bgcolor="#ccccff"><th>interpolation time (s)</th>
284      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
285      * <tr><td bgcolor="#eeeeff"> 60</td><td>  18</td><td> 1.1</td></tr>
286      * <tr><td bgcolor="#eeeeff">120</td><td>  72</td><td> 9.1</td></tr>
287      * <tr><td bgcolor="#eeeeff">300</td><td> 447</td><td> 140</td></tr>
288      * <tr><td bgcolor="#eeeeff">600</td><td>1601</td><td>1067</td></tr>
289      * <tr><td bgcolor="#eeeeff">900</td><td>3141</td><td>3307</td></tr>
290      * </table>
291      * @param dt time shift in seconds
292      * @return a new state, shifted with respect to the instance (which is immutable)
293      * except for the mass and additional states which stay unchanged
294      */
295     public SpacecraftState shiftedBy(final double dt) {
296         return new SpacecraftState(orbit.shiftedBy(dt), attitude.shiftedBy(dt),
297                                    mass, additional);
298     }
299 
300     /** {@inheritDoc}
301      * <p>
302      * The additional states that are interpolated are the ones already present
303      * in the instance. The sample instances must therefore have at least the same
304      * additional states has the instance. They may have more additional states,
305      * but the extra ones will be ignored.
306      * </p>
307      * <p>
308      * As this implementation of interpolation is polynomial, it should be used only
309      * with small samples (about 10-20 points) in order to avoid <a
310      * href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's phenomenon</a>
311      * and numerical problems (including NaN appearing).
312      * </p>
313      */
314     public SpacecraftState interpolate(final AbsoluteDate date,
315                                        final Stream<SpacecraftState> sample)
316             throws OrekitException {
317 
318         // prepare interpolators
319         final List<Orbit> orbits = new ArrayList<>();
320         final List<Attitude> attitudes = new ArrayList<>();
321         final HermiteInterpolator massInterpolator = new HermiteInterpolator();
322         final Map<String, HermiteInterpolator> additionalInterpolators =
323                 new HashMap<String, HermiteInterpolator>(additional.size());
324         for (final String name : additional.keySet()) {
325             additionalInterpolators.put(name, new HermiteInterpolator());
326         }
327 
328         // extract sample data
329         try {
330             sample.forEach(state -> {
331                 try {
332                     final double deltaT = state.getDate().durationFrom(date);
333                     orbits.add(state.getOrbit());
334                     attitudes.add(state.getAttitude());
335                     massInterpolator.addSamplePoint(deltaT,
336                                                     new double[] {
337                                                          state.getMass()
338                                                     });
339                     for (final Map.Entry<String, HermiteInterpolator> entry : additionalInterpolators.entrySet()) {
340                         entry.getValue().addSamplePoint(deltaT, state.getAdditionalState(entry.getKey()));
341                     }
342                 } catch (OrekitException oe) {
343                     throw new OrekitExceptionWrapper(oe);
344                 }
345             });
346         } catch (OrekitExceptionWrapper oew) {
347             throw oew.getException();
348         }
349 
350         // perform interpolations
351         final Orbit interpolatedOrbit       = orbit.interpolate(date, orbits);
352         final Attitude interpolatedAttitude = attitude.interpolate(date, attitudes);
353         final double interpolatedMass       = massInterpolator.value(0)[0];
354         final Map<String, double[]> interpolatedAdditional;
355         if (additional.isEmpty()) {
356             interpolatedAdditional = null;
357         } else {
358             interpolatedAdditional = new HashMap<String, double[]>(additional.size());
359             for (final Map.Entry<String, HermiteInterpolator> entry : additionalInterpolators.entrySet()) {
360                 interpolatedAdditional.put(entry.getKey(), entry.getValue().value(0));
361             }
362         }
363 
364         // create the complete interpolated state
365         return new SpacecraftState(interpolatedOrbit, interpolatedAttitude,
366                                    interpolatedMass, interpolatedAdditional);
367 
368     }
369 
370     /** Gets the current orbit.
371      * @return the orbit
372      */
373     public Orbit getOrbit() {
374         return orbit;
375     }
376 
377     /** Get the date.
378      * @return date
379      */
380     public AbsoluteDate getDate() {
381         return orbit.getDate();
382     }
383 
384     /** Get the inertial frame.
385      * @return the frame
386      */
387     public Frame getFrame() {
388         return orbit.getFrame();
389     }
390 
391     /** Check if an additional state is available.
392      * @param name name of the additional state
393      * @return true if the additional state is available
394      * @see #addAdditionalState(String, double[])
395      * @see #getAdditionalState(String)
396      * @see #getAdditionalStates()
397      */
398     public boolean hasAdditionalState(final String name) {
399         return additional.containsKey(name);
400     }
401 
402     /** Check if two instances have the same set of additional states available.
403      * <p>
404      * Only the names and dimensions of the additional states are compared,
405      * not their values.
406      * </p>
407      * @param state state to compare to instance
408      * @exception OrekitException if either instance or state supports an additional
409      * state not supported by the other one
410      * @exception MathIllegalStateException if an additional state does not have
411      * the same dimension in both states
412      */
413     public void ensureCompatibleAdditionalStates(final SpacecraftState state)
414         throws OrekitException, MathIllegalStateException {
415 
416         // check instance additional states is a subset of the other one
417         for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
418             final double[] other = state.additional.get(entry.getKey());
419             if (other == null) {
420                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
421                                           entry.getKey());
422             }
423             if (other.length != entry.getValue().length) {
424                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
425                                                     other.length, entry.getValue().length);
426             }
427         }
428 
429         if (state.additional.size() > additional.size()) {
430             // the other state has more additional states
431             for (final String name : state.additional.keySet()) {
432                 if (!additional.containsKey(name)) {
433                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
434                                               name);
435                 }
436             }
437         }
438 
439     }
440 
441     /** Get an additional state.
442      * @param name name of the additional state
443      * @return value of the additional state
444      * @exception OrekitException if no additional state with that name exists
445      * @see #addAdditionalState(String, double[])
446      * @see #hasAdditionalState(String)
447      * @see #getAdditionalStates()
448      */
449     public double[] getAdditionalState(final String name) throws OrekitException {
450         if (!additional.containsKey(name)) {
451             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE, name);
452         }
453         return additional.get(name).clone();
454     }
455 
456     /** Get an unmodifiable map of additional states.
457      * @return unmodifiable map of additional states
458      * @see #addAdditionalState(String, double[])
459      * @see #hasAdditionalState(String)
460      * @see #getAdditionalState(String)
461      */
462     public Map<String, double[]> getAdditionalStates() {
463         return Collections.unmodifiableMap(additional);
464     }
465 
466     /** Compute the transform from orbite/attitude reference frame to spacecraft frame.
467      * <p>The spacecraft frame origin is at the point defined by the orbit,
468      * and its orientation is defined by the attitude.</p>
469      * @return transform from specified frame to current spacecraft frame
470      */
471     public Transform toTransform() {
472         final AbsoluteDate date = orbit.getDate();
473         return new Transform(date,
474                              new Transform(date, orbit.getPVCoordinates().negate()),
475                              new Transform(date, attitude.getOrientation()));
476     }
477 
478     /** Get the central attraction coefficient.
479      * @return mu central attraction coefficient (m^3/s^2)
480      */
481     public double getMu() {
482         return orbit.getMu();
483     }
484 
485     /** Get the Keplerian period.
486      * <p>The Keplerian period is computed directly from semi major axis
487      * and central acceleration constant.</p>
488      * @return Keplerian period in seconds
489      */
490     public double getKeplerianPeriod() {
491         return orbit.getKeplerianPeriod();
492     }
493 
494     /** Get the Keplerian mean motion.
495      * <p>The Keplerian mean motion is computed directly from semi major axis
496      * and central acceleration constant.</p>
497      * @return Keplerian mean motion in radians per second
498      */
499     public double getKeplerianMeanMotion() {
500         return orbit.getKeplerianMeanMotion();
501     }
502 
503     /** Get the semi-major axis.
504      * @return semi-major axis (m)
505      */
506     public double getA() {
507         return orbit.getA();
508     }
509 
510     /** Get the first component of the eccentricity vector (as per equinoctial parameters).
511      * @return e cos(ω + Ω), first component of eccentricity vector
512      * @see #getE()
513      */
514     public double getEquinoctialEx() {
515         return orbit.getEquinoctialEx();
516     }
517 
518     /** Get the second component of the eccentricity vector (as per equinoctial parameters).
519      * @return e sin(ω + Ω), second component of the eccentricity vector
520      * @see #getE()
521      */
522     public double getEquinoctialEy() {
523         return orbit.getEquinoctialEy();
524     }
525 
526     /** Get the first component of the inclination vector (as per equinoctial parameters).
527      * @return tan(i/2) cos(Ω), first component of the inclination vector
528      * @see #getI()
529      */
530     public double getHx() {
531         return orbit.getHx();
532     }
533 
534     /** Get the second component of the inclination vector (as per equinoctial parameters).
535      * @return tan(i/2) sin(Ω), second component of the inclination vector
536      * @see #getI()
537      */
538     public double getHy() {
539         return orbit.getHy();
540     }
541 
542     /** Get the true latitude argument (as per equinoctial parameters).
543      * @return v + ω + Ω true latitude argument (rad)
544      * @see #getLE()
545      * @see #getLM()
546      */
547     public double getLv() {
548         return orbit.getLv();
549     }
550 
551     /** Get the eccentric latitude argument (as per equinoctial parameters).
552      * @return E + ω + Ω eccentric latitude argument (rad)
553      * @see #getLv()
554      * @see #getLM()
555      */
556     public double getLE() {
557         return orbit.getLE();
558     }
559 
560     /** Get the mean latitude argument (as per equinoctial parameters).
561      * @return M + ω + Ω mean latitude argument (rad)
562      * @see #getLv()
563      * @see #getLE()
564      */
565     public double getLM() {
566         return orbit.getLM();
567     }
568 
569     // Additional orbital elements
570 
571     /** Get the eccentricity.
572      * @return eccentricity
573      * @see #getEquinoctialEx()
574      * @see #getEquinoctialEy()
575      */
576     public double getE() {
577         return orbit.getE();
578     }
579 
580     /** Get the inclination.
581      * @return inclination (rad)
582      * @see #getHx()
583      * @see #getHy()
584      */
585     public double getI() {
586         return orbit.getI();
587     }
588 
589     /** Get the {@link TimeStampedPVCoordinates} in orbit definition frame.
590      * Compute the position and velocity of the satellite. This method caches its
591      * results, and recompute them only when the method is called with a new value
592      * for mu. The result is provided as a reference to the internally cached
593      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
594      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
595      * @return pvCoordinates in orbit definition frame
596      */
597     public TimeStampedPVCoordinates getPVCoordinates() {
598         return orbit.getPVCoordinates();
599     }
600 
601     /** Get the {@link TimeStampedPVCoordinates} in given output frame.
602      * Compute the position and velocity of the satellite. This method caches its
603      * results, and recompute them only when the method is called with a new value
604      * for mu. The result is provided as a reference to the internally cached
605      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
606      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
607      * @param outputFrame frame in which coordinates should be defined
608      * @return pvCoordinates in orbit definition frame
609      * @exception OrekitException if the transformation between frames cannot be computed
610      */
611     public TimeStampedPVCoordinates getPVCoordinates(final Frame outputFrame)
612         throws OrekitException {
613         return orbit.getPVCoordinates(outputFrame);
614     }
615 
616     /** Get the attitude.
617      * @return the attitude.
618      */
619     public Attitude getAttitude() {
620         return attitude;
621     }
622 
623     /** Gets the current mass.
624      * @return the mass (kg)
625      */
626     public double getMass() {
627         return mass;
628     }
629 
630     /** Replace the instance with a data transfer object for serialization.
631      * @return data transfer object that will be serialized
632      */
633     private Object writeReplace() {
634         return new DTO(this);
635     }
636 
637     /** Internal class used only for serialization. */
638     private static class DTO implements Serializable {
639 
640         /** Serializable UID. */
641         private static final long serialVersionUID = 20140617L;
642 
643         /** Orbit. */
644         private final Orbit orbit;
645 
646         /** Attitude and mass double values. */
647         private double[] d;
648 
649         /** Additional states. */
650         private final Map<String, double[]> additional;
651 
652         /** Simple constructor.
653          * @param state instance to serialize
654          */
655         private DTO(final SpacecraftState state) {
656 
657             this.orbit      = state.orbit;
658             this.additional = state.additional.isEmpty() ? null : state.additional;
659 
660             final Rotation rotation             = state.attitude.getRotation();
661             final Vector3D spin                 = state.attitude.getSpin();
662             final Vector3D rotationAcceleration = state.attitude.getRotationAcceleration();
663             this.d = new double[] {
664                 rotation.getQ0(), rotation.getQ1(), rotation.getQ2(), rotation.getQ3(),
665                 spin.getX(), spin.getY(), spin.getZ(),
666                 rotationAcceleration.getX(), rotationAcceleration.getY(), rotationAcceleration.getZ(),
667                 state.mass
668             };
669 
670         }
671 
672         /** Replace the deserialized data transfer object with a {@link SpacecraftState}.
673          * @return replacement {@link SpacecraftState}
674          */
675         private Object readResolve() {
676             return new SpacecraftState(orbit,
677                                        new Attitude(orbit.getFrame(),
678                                                     new TimeStampedAngularCoordinates(orbit.getDate(),
679                                                                                       new Rotation(d[0], d[1], d[2], d[3], false),
680                                                                                       new Vector3D(d[4], d[5], d[6]),
681                                                                                       new Vector3D(d[7], d[8], d[9]))),
682                                        d[10], additional);
683         }
684 
685     }
686 
687 }