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