1   /* Copyright 2002-2020 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.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.OrekitIllegalArgumentException;
37  import org.orekit.errors.OrekitIllegalStateException;
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.AbsolutePVCoordinates;
48  import org.orekit.utils.TimeStampedAngularCoordinates;
49  import org.orekit.utils.TimeStampedPVCoordinates;
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      /** Trajectory state, when it is not an orbit. */
95      private final AbsolutePVCoordinates absPva;
96  
97      /** Attitude. */
98      private final Attitude attitude;
99  
100     /** Current mass (kg). */
101     private final double mass;
102 
103     /** Additional states. */
104     private final Map<String, double[]> additional;
105 
106     /** Build a spacecraft state from orbit only.
107      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
108      * @param orbit the orbit
109      */
110     public SpacecraftState(final Orbit orbit) {
111         this(orbit,
112              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
113              DEFAULT_MASS, null);
114     }
115 
116     /** Build a spacecraft state from orbit and attitude provider.
117      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
118      * @param orbit the orbit
119      * @param attitude attitude
120      * @exception IllegalArgumentException if orbit and attitude dates
121      * or frames are not equal
122      */
123     public SpacecraftState(final Orbit orbit, final Attitude attitude)
124         throws IllegalArgumentException {
125         this(orbit, attitude, DEFAULT_MASS, null);
126     }
127 
128     /** Create a new instance from orbit and mass.
129      * <p>Attitude law is set to an unspecified default attitude.</p>
130      * @param orbit the orbit
131      * @param mass the mass (kg)
132      */
133     public SpacecraftState(final Orbit orbit, final double mass) {
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      */
156     public SpacecraftState(final Orbit orbit, final Map<String, double[]> additional) {
157         this(orbit,
158              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
159              DEFAULT_MASS, additional);
160     }
161 
162     /** Build a spacecraft state from orbit and attitude provider.
163      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
164      * @param orbit the orbit
165      * @param attitude attitude
166      * @param additional additional states
167      * @exception IllegalArgumentException if orbit and attitude dates
168      * or frames are not equal
169      */
170     public SpacecraftState(final Orbit orbit, final Attitude attitude, final Map<String, double[]> additional)
171         throws IllegalArgumentException {
172         this(orbit, attitude, DEFAULT_MASS, additional);
173     }
174 
175     /** Create a new instance from orbit and mass.
176      * <p>Attitude law is set to an unspecified default attitude.</p>
177      * @param orbit the orbit
178      * @param mass the mass (kg)
179      * @param additional additional states
180      */
181     public SpacecraftState(final Orbit orbit, final double mass, final Map<String, double[]> additional) {
182         this(orbit,
183              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
184              mass, additional);
185     }
186 
187     /** Build a spacecraft state from orbit, attitude provider and mass.
188      * @param orbit the orbit
189      * @param attitude attitude
190      * @param mass the mass (kg)
191      * @param additional additional states (may be null if no additional states are available)
192      * @exception IllegalArgumentException if orbit and attitude dates
193      * or frames are not equal
194      */
195     public SpacecraftState(final Orbit orbit, final Attitude attitude,
196                            final double mass, final Map<String, double[]> additional)
197         throws IllegalArgumentException {
198         checkConsistency(orbit, attitude);
199         this.orbit      = orbit;
200         this.absPva     = null;
201         this.attitude   = attitude;
202         this.mass       = mass;
203         if (additional == null) {
204             this.additional = Collections.emptyMap();
205         } else {
206             this.additional = new HashMap<String, double[]>(additional.size());
207             for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
208                 this.additional.put(entry.getKey(), entry.getValue().clone());
209             }
210         }
211     }
212 
213 
214 
215     /** Build a spacecraft state from orbit only.
216      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
217      * @param absPva position-velocity-acceleration
218      */
219     public SpacecraftState(final AbsolutePVCoordinates absPva) {
220         this(absPva,
221              new LofOffset(absPva.getFrame(), LOFType.VVLH).getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
222              DEFAULT_MASS, null);
223     }
224 
225     /** Build a spacecraft state from orbit and attitude provider.
226      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
227      * @param absPva position-velocity-acceleration
228      * @param attitude attitude
229      * @exception IllegalArgumentException if orbit and attitude dates
230      * or frames are not equal
231      */
232     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude)
233         throws IllegalArgumentException {
234         this(absPva, attitude, DEFAULT_MASS, null);
235     }
236 
237     /** Create a new instance from orbit and mass.
238      * <p>Attitude law is set to an unspecified default attitude.</p>
239      * @param absPva position-velocity-acceleration
240      * @param mass the mass (kg)
241      */
242     public SpacecraftState(final AbsolutePVCoordinates absPva, final double mass) {
243         this(absPva,
244              new LofOffset(absPva.getFrame(), LOFType.VVLH).getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
245              mass, null);
246     }
247 
248     /** Build a spacecraft state from orbit, attitude provider and mass.
249      * @param absPva position-velocity-acceleration
250      * @param attitude attitude
251      * @param mass the mass (kg)
252      * @exception IllegalArgumentException if orbit and attitude dates
253      * or frames are not equal
254      */
255     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude, final double mass)
256         throws IllegalArgumentException {
257         this(absPva, attitude, mass, null);
258     }
259 
260     /** Build a spacecraft state from orbit only.
261      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
262      * @param absPva position-velocity-acceleration
263      * @param additional additional states
264      */
265     public SpacecraftState(final AbsolutePVCoordinates absPva, final Map<String, double[]> additional) {
266         this(absPva,
267              new LofOffset(absPva.getFrame(), LOFType.VVLH).getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
268              DEFAULT_MASS, additional);
269     }
270 
271     /** Build a spacecraft state from orbit and attitude provider.
272      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
273      * @param absPva position-velocity-acceleration
274      * @param attitude attitude
275      * @param additional additional states
276      * @exception IllegalArgumentException if orbit and attitude dates
277      * or frames are not equal
278      */
279     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude, final Map<String, double[]> additional)
280         throws IllegalArgumentException {
281         this(absPva, attitude, DEFAULT_MASS, additional);
282     }
283 
284     /** Create a new instance from orbit and mass.
285      * <p>Attitude law is set to an unspecified default attitude.</p>
286      * @param absPva position-velocity-acceleration
287      * @param mass the mass (kg)
288      * @param additional additional states
289      */
290     public SpacecraftState(final AbsolutePVCoordinates absPva, final double mass, final Map<String, double[]> additional) {
291         this(absPva,
292              new LofOffset(absPva.getFrame(), LOFType.VVLH).getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
293              mass, additional);
294     }
295 
296     /** Build a spacecraft state from orbit, attitude provider and mass.
297      * @param absPva position-velocity-acceleration
298      * @param attitude attitude
299      * @param mass the mass (kg)
300      * @param additional additional states (may be null if no additional states are available)
301      * @exception IllegalArgumentException if orbit and attitude dates
302      * or frames are not equal
303      */
304     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude,
305                            final double mass, final Map<String, double[]> additional)
306         throws IllegalArgumentException {
307         checkConsistency(absPva, attitude);
308         this.orbit      = null;
309         this.absPva     = absPva;
310         this.attitude   = attitude;
311         this.mass       = mass;
312         if (additional == null) {
313             this.additional = Collections.emptyMap();
314         } else {
315             this.additional = new HashMap<String, double[]>(additional.size());
316             for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
317                 this.additional.put(entry.getKey(), entry.getValue().clone());
318             }
319         }
320     }
321 
322     /** Add an additional state.
323      * <p>
324      * {@link SpacecraftState SpacecraftState} instances are immutable,
325      * so this method does <em>not</em> change the instance, but rather
326      * creates a new instance, which has the same orbit, attitude, mass
327      * and additional states as the original instance, except it also
328      * has the specified state. If the original instance already had an
329      * additional state with the same name, it will be overridden. If it
330      * did not have any additional state with that name, the new instance
331      * will have one more additional state than the original instance.
332      * </p>
333      * @param name name of the additional state
334      * @param value value of the additional state
335      * @return a new instance, with the additional state added
336      * @see #hasAdditionalState(String)
337      * @see #getAdditionalState(String)
338      * @see #getAdditionalStates()
339      */
340     public SpacecraftState addAdditionalState(final String name, final double... value) {
341         final Map<String, double[]> newMap = new HashMap<String, double[]>(additional.size() + 1);
342         newMap.putAll(additional);
343         newMap.put(name, value.clone());
344         if (absPva == null) {
345             return new SpacecraftState(orbit, attitude, mass, newMap);
346         } else {
347             return new SpacecraftState(absPva, attitude, mass, newMap);
348         }
349     }
350 
351     /** Check orbit and attitude dates are equal.
352      * @param orbit the orbit
353      * @param attitude attitude
354      * @exception IllegalArgumentException if orbit and attitude dates
355      * are not equal
356      */
357     private static void checkConsistency(final Orbit orbit, final Attitude attitude)
358         throws IllegalArgumentException {
359         if (FastMath.abs(orbit.getDate().durationFrom(attitude.getDate())) >
360             DATE_INCONSISTENCY_THRESHOLD) {
361             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
362                                                      orbit.getDate(), attitude.getDate());
363         }
364         if (orbit.getFrame() != attitude.getReferenceFrame()) {
365             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
366                                                      orbit.getFrame().getName(),
367                                                      attitude.getReferenceFrame().getName());
368         }
369     }
370 
371     /** Check if the state contains an orbit part.
372      * <p>
373      * A state contains either an {@link AbsolutePVCoordinates absolute
374      * position-velocity-acceleration} or an {@link Orbit orbit}.
375      * </p>
376      * @return true if state contains an orbit (in which case {@link #getOrbit()}
377      * will not throw an exception), or false if the state contains an
378      * absolut position-velocity-acceleration (in which case {@link #getAbsPVA()}
379      * will not throw an exception)
380      */
381     public boolean isOrbitDefined() {
382         return orbit != null;
383     }
384 
385     /** Check AbsolutePVCoordinates and attitude dates are equal.
386      * @param absPva position-velocity-acceleration
387      * @param attitude attitude
388      * @exception IllegalArgumentException if orbit and attitude dates
389      * are not equal
390      */
391     private static void checkConsistency(final AbsolutePVCoordinates absPva, final Attitude attitude)
392         throws IllegalArgumentException {
393         if (FastMath.abs(absPva.getDate().durationFrom(attitude.getDate())) >
394             DATE_INCONSISTENCY_THRESHOLD) {
395             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
396                                                      absPva.getDate(), attitude.getDate());
397         }
398         if (absPva.getFrame() != attitude.getReferenceFrame()) {
399             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
400                                                      absPva.getFrame().getName(),
401                                                      attitude.getReferenceFrame().getName());
402         }
403     }
404 
405     /** Get a time-shifted state.
406      * <p>
407      * The state can be slightly shifted to close dates. This shift is based on
408      * simple models. For orbits, the model is a Keplerian one if no derivatives
409      * are available in the orbit, or Keplerian plus quadratic effect of the
410      * non-Keplerian acceleration if derivatives are available. For attitude,
411      * a polynomial model is used. Neither mass nor additional states change.
412      * Shifting is <em>not</em> intended as a replacement for proper orbit
413      * and attitude propagation but should be sufficient for small time shifts
414      * or coarse accuracy.
415      * </p>
416      * <p>
417      * As a rough order of magnitude, the following table shows the extrapolation
418      * errors obtained between this simple shift method and an {@link
419      * org.orekit.propagation.numerical.NumericalPropagator numerical
420      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
421      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
422      * Beware that these results will be different for other orbits.
423      * </p>
424      * <table border="1">
425      * <caption>Extrapolation Error</caption>
426      * <tr style="background-color: #ccccff"><th>interpolation time (s)</th>
427      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
428      * <tr><td style="background-color: #eeeeff; padding:5px"> 60</td><td>  18</td><td> 1.1</td></tr>
429      * <tr><td style="background-color: #eeeeff; padding:5px">120</td><td>  72</td><td> 9.1</td></tr>
430      * <tr><td style="background-color: #eeeeff; padding:5px">300</td><td> 447</td><td> 140</td></tr>
431      * <tr><td style="background-color: #eeeeff; padding:5px">600</td><td>1601</td><td>1067</td></tr>
432      * <tr><td style="background-color: #eeeeff; padding:5px">900</td><td>3141</td><td>3307</td></tr>
433      * </table>
434      * @param dt time shift in seconds
435      * @return a new state, shifted with respect to the instance (which is immutable)
436      * except for the mass and additional states which stay unchanged
437      */
438     public SpacecraftState shiftedBy(final double dt) {
439         if (absPva == null) {
440             return new SpacecraftState(orbit.shiftedBy(dt), attitude.shiftedBy(dt),
441                     mass, additional);
442         } else {
443             return new SpacecraftState(absPva.shiftedBy(dt), attitude.shiftedBy(dt),
444                     mass, additional);
445         }
446     }
447 
448     /** {@inheritDoc}
449      * <p>
450      * The additional states that are interpolated are the ones already present
451      * in the instance. The sample instances must therefore have at least the same
452      * additional states has the instance. They may have more additional states,
453      * but the extra ones will be ignored.
454      * </p>
455      * <p>
456      * The instance and all the sample instances <em>must</em> be based on similar
457      * trajectory data, i.e. they must either all be based on orbits or all be based
458      * on absolute position-velocity-acceleration. Any inconsistency will trigger
459      * an {@link OrekitIllegalStateException}.
460      * </p>
461      * <p>
462      * As this implementation of interpolation is polynomial, it should be used only
463      * with small samples (about 10-20 points) in order to avoid <a
464      * href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's phenomenon</a>
465      * and numerical problems (including NaN appearing).
466      * </p>
467      * @exception OrekitIllegalStateException if some instances are not based on
468      * similar trajectory data
469      */
470     public SpacecraftState interpolate(final AbsoluteDate date,
471                                        final Stream<SpacecraftState> sample) {
472 
473         // prepare interpolators
474         final List<Orbit> orbits;
475         final List<AbsolutePVCoordinates> absPvas;
476         if (isOrbitDefined()) {
477             orbits  = new ArrayList<Orbit>();
478             absPvas = null;
479         } else {
480             orbits  = null;
481             absPvas = new ArrayList<AbsolutePVCoordinates>();
482         }
483         final List<Attitude> attitudes = new ArrayList<Attitude>();
484         final HermiteInterpolator massInterpolator = new HermiteInterpolator();
485         final Map<String, HermiteInterpolator> additionalInterpolators =
486                 new HashMap<String, HermiteInterpolator>(additional.size());
487         for (final String name : additional.keySet()) {
488             additionalInterpolators.put(name, new HermiteInterpolator());
489         }
490 
491         // extract sample data
492         sample.forEach(state -> {
493             final double deltaT = state.getDate().durationFrom(date);
494             if (isOrbitDefined()) {
495                 orbits.add(state.getOrbit());
496             } else {
497                 absPvas.add(state.getAbsPVA());
498             }
499             attitudes.add(state.getAttitude());
500             massInterpolator.addSamplePoint(deltaT,
501                                             new double[] {
502                                                 state.getMass()
503                                                 });
504             for (final Map.Entry<String, HermiteInterpolator> entry : additionalInterpolators.entrySet()) {
505                 entry.getValue().addSamplePoint(deltaT, state.getAdditionalState(entry.getKey()));
506             }
507 
508         });
509 
510         // perform interpolations
511         final Orbit interpolatedOrbit;
512         final AbsolutePVCoordinates interpolatedAbsPva;
513         if (isOrbitDefined()) {
514             interpolatedOrbit  = orbit.interpolate(date, orbits);
515             interpolatedAbsPva = null;
516         } else {
517             interpolatedOrbit  = null;
518             interpolatedAbsPva = absPva.interpolate(date, absPvas);
519         }
520         final Attitude interpolatedAttitude = attitude.interpolate(date, attitudes);
521         final double interpolatedMass       = massInterpolator.value(0)[0];
522         final Map<String, double[]> interpolatedAdditional;
523         if (additional.isEmpty()) {
524             interpolatedAdditional = null;
525         } else {
526             interpolatedAdditional = new HashMap<String, double[]>(additional.size());
527             for (final Map.Entry<String, HermiteInterpolator> entry : additionalInterpolators.entrySet()) {
528                 interpolatedAdditional.put(entry.getKey(), entry.getValue().value(0));
529             }
530         }
531 
532         // create the complete interpolated state
533         if (isOrbitDefined()) {
534             return new SpacecraftState(interpolatedOrbit, interpolatedAttitude,
535                                        interpolatedMass, interpolatedAdditional);
536         } else {
537             return new SpacecraftState(interpolatedAbsPva, interpolatedAttitude,
538                                        interpolatedMass, interpolatedAdditional);
539         }
540 
541     }
542 
543     /** Get the absolute position-velocity-acceleration.
544      * <p>
545      * A state contains either an {@link AbsolutePVCoordinates absolute
546      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
547      * one is present can be checked using {@link #isOrbitDefined()}.
548      * </p>
549      * @return absolute position-velocity-acceleration
550      * @exception OrekitIllegalStateException if position-velocity-acceleration is null,
551      * which mean the state rather contains an {@link Orbit}
552      * @see #isOrbitDefined()
553      * @see #getOrbit()
554      */
555     public AbsolutePVCoordinates getAbsPVA() throws OrekitIllegalStateException {
556         if (absPva == null) {
557             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ABSOLUTE_PVCOORDINATES);
558         }
559         return absPva;
560     }
561 
562     /** Get the current orbit.
563      * <p>
564      * A state contains either an {@link AbsolutePVCoordinates absolute
565      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
566      * one is present can be checked using {@link #isOrbitDefined()}.
567      * </p>
568      * @return the orbit
569      * @exception OrekitIllegalStateException if orbit is null,
570      * which means the state rather contains an {@link AbsolutePVCoordinates absolute
571      * position-velocity-acceleration}
572      * @see #isOrbitDefined()
573      * @see #getAbsPVA()
574      */
575     public Orbit getOrbit() throws OrekitIllegalStateException {
576         if (orbit == null) {
577             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ORBIT);
578         }
579         return orbit;
580     }
581 
582     /** Get the date.
583      * @return date
584      */
585     public AbsoluteDate getDate() {
586         return (absPva == null) ? orbit.getDate() : absPva.getDate();
587     }
588 
589     /** Get the defining frame.
590      * @return the frame in which state is defined
591      */
592     public Frame getFrame() {
593         return (absPva == null) ? orbit.getFrame() : absPva.getFrame();
594     }
595 
596     /** Check if an additional state is available.
597      * @param name name of the additional state
598      * @return true if the additional state is available
599      * @see #addAdditionalState(String, double[])
600      * @see #getAdditionalState(String)
601      * @see #getAdditionalStates()
602      */
603     public boolean hasAdditionalState(final String name) {
604         return additional.containsKey(name);
605     }
606 
607     /** Check if two instances have the same set of additional states available.
608      * <p>
609      * Only the names and dimensions of the additional states are compared,
610      * not their values.
611      * </p>
612      * @param state state to compare to instance
613      * @exception MathIllegalStateException if an additional state does not have
614      * the same dimension in both states
615      */
616     public void ensureCompatibleAdditionalStates(final SpacecraftState state)
617         throws MathIllegalStateException {
618 
619         // check instance additional states is a subset of the other one
620         for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
621             final double[] other = state.additional.get(entry.getKey());
622             if (other == null) {
623                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
624                                           entry.getKey());
625             }
626             if (other.length != entry.getValue().length) {
627                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
628                                                     other.length, entry.getValue().length);
629             }
630         }
631 
632         if (state.additional.size() > additional.size()) {
633             // the other state has more additional states
634             for (final String name : state.additional.keySet()) {
635                 if (!additional.containsKey(name)) {
636                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
637                                               name);
638                 }
639             }
640         }
641 
642     }
643 
644     /** Get an additional state.
645      * @param name name of the additional state
646      * @return value of the additional state
647           * @see #addAdditionalState(String, double[])
648      * @see #hasAdditionalState(String)
649      * @see #getAdditionalStates()
650      */
651     public double[] getAdditionalState(final String name) {
652         if (!additional.containsKey(name)) {
653             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE, name);
654         }
655         return additional.get(name).clone();
656     }
657 
658     /** Get an unmodifiable map of additional states.
659      * @return unmodifiable map of additional states
660      * @see #addAdditionalState(String, double[])
661      * @see #hasAdditionalState(String)
662      * @see #getAdditionalState(String)
663      */
664     public Map<String, double[]> getAdditionalStates() {
665         return Collections.unmodifiableMap(additional);
666     }
667 
668     /** Compute the transform from state defining frame to spacecraft frame.
669      * <p>The spacecraft frame origin is at the point defined by the orbit
670      * (or absolute position-velocity-acceleration), and its orientation is
671      * defined by the attitude.</p>
672      * @return transform from specified frame to current spacecraft frame
673      */
674     public Transform toTransform() {
675         final TimeStampedPVCoordinates pv = getPVCoordinates();
676         return new Transform(pv.getDate(),
677                              new Transform(pv.getDate(), pv.negate()),
678                              new Transform(pv.getDate(), attitude.getOrientation()));
679     }
680 
681     /** Get the central attraction coefficient.
682      * @return mu central attraction coefficient (m^3/s^2), or {code Double.NaN} if the
683      * state is contains an absolute position-velocity-acceleration rather than an orbit
684      */
685     public double getMu() {
686         return (absPva == null) ? orbit.getMu() : Double.NaN;
687     }
688 
689     /** Get the Keplerian period.
690      * <p>The Keplerian period is computed directly from semi major axis
691      * and central acceleration constant.</p>
692      * @return keplerian period in seconds, or {code Double.NaN} if the
693      * state is contains an absolute position-velocity-acceleration rather
694      * than an orbit
695      */
696     public double getKeplerianPeriod() {
697         return (absPva == null) ? orbit.getKeplerianPeriod() : Double.NaN;
698     }
699 
700     /** Get the Keplerian mean motion.
701      * <p>The Keplerian mean motion is computed directly from semi major axis
702      * and central acceleration constant.</p>
703      * @return keplerian mean motion in radians per second, or {code Double.NaN} if the
704      * state is contains an absolute position-velocity-acceleration rather
705      * than an orbit
706      */
707     public double getKeplerianMeanMotion() {
708         return (absPva == null) ? orbit.getKeplerianMeanMotion() : Double.NaN;
709     }
710 
711     /** Get the semi-major axis.
712      * @return semi-major axis (m), or {code Double.NaN} if the
713      * state is contains an absolute position-velocity-acceleration rather
714      * than an orbit
715      */
716     public double getA() {
717         return (absPva == null) ? orbit.getA() : Double.NaN;
718     }
719 
720     /** Get the first component of the eccentricity vector (as per equinoctial parameters).
721      * @return e cos(ω + Ω), first component of eccentricity vector, or {code Double.NaN} if the
722      * state is contains an absolute position-velocity-acceleration rather
723      * than an orbit
724      * @see #getE()
725      */
726     public double getEquinoctialEx() {
727         return (absPva == null) ? orbit.getEquinoctialEx() : Double.NaN;
728     }
729 
730     /** Get the second component of the eccentricity vector (as per equinoctial parameters).
731      * @return e sin(ω + Ω), second component of the eccentricity vector, or {code Double.NaN} if the
732      * state is contains an absolute position-velocity-acceleration rather
733      * than an orbit
734      * @see #getE()
735      */
736     public double getEquinoctialEy() {
737         return (absPva == null) ? orbit.getEquinoctialEy() : Double.NaN;
738     }
739 
740     /** Get the first component of the inclination vector (as per equinoctial parameters).
741      * @return tan(i/2) cos(Ω), first component of the inclination vector, or {code Double.NaN} if the
742      * state is contains an absolute position-velocity-acceleration rather
743      * than an orbit
744      * @see #getI()
745      */
746     public double getHx() {
747         return (absPva == null) ? orbit.getHx() : Double.NaN;
748     }
749 
750     /** Get the second component of the inclination vector (as per equinoctial parameters).
751      * @return tan(i/2) sin(Ω), second component of the inclination vector, or {code Double.NaN} if the
752      * state is contains an absolute position-velocity-acceleration rather
753      * than an orbit
754      * @see #getI()
755      */
756     public double getHy() {
757         return (absPva == null) ? orbit.getHy() : Double.NaN;
758     }
759 
760     /** Get the true latitude argument (as per equinoctial parameters).
761      * @return v + ω + Ω true longitude argument (rad), or {code Double.NaN} if the
762      * state is contains an absolute position-velocity-acceleration rather
763      * than an orbit
764      * @see #getLE()
765      * @see #getLM()
766      */
767     public double getLv() {
768         return (absPva == null) ? orbit.getLv() : Double.NaN;
769     }
770 
771     /** Get the eccentric latitude argument (as per equinoctial parameters).
772      * @return E + ω + Ω eccentric longitude argument (rad), or {code Double.NaN} if the
773      * state is contains an absolute position-velocity-acceleration rather
774      * than an orbit
775      * @see #getLv()
776      * @see #getLM()
777      */
778     public double getLE() {
779         return (absPva == null) ? orbit.getLE() : Double.NaN;
780     }
781 
782     /** Get the mean longitude argument (as per equinoctial parameters).
783      * @return M + ω + Ω mean latitude argument (rad), or {code Double.NaN} if the
784      * state is contains an absolute position-velocity-acceleration rather
785      * than an orbit
786      * @see #getLv()
787      * @see #getLE()
788      */
789     public double getLM() {
790         return (absPva == null) ? orbit.getLM() : Double.NaN;
791     }
792 
793     // Additional orbital elements
794 
795     /** Get the eccentricity.
796      * @return eccentricity, or {code Double.NaN} if the
797      * state is contains an absolute position-velocity-acceleration rather
798      * than an orbit
799      * @see #getEquinoctialEx()
800      * @see #getEquinoctialEy()
801      */
802     public double getE() {
803         return (absPva == null) ? orbit.getE() : Double.NaN;
804     }
805 
806     /** Get the inclination.
807      * @return inclination (rad)
808      * @see #getHx()
809      * @see #getHy()
810      */
811     public double getI() {
812         return (absPva == null) ? orbit.getI() : Double.NaN;
813     }
814 
815     /** Get the {@link TimeStampedPVCoordinates} in orbit definition frame.
816      * <p>
817      * Compute the position and velocity of the satellite. This method caches its
818      * results, and recompute them only when the method is called with a new value
819      * for mu. The result is provided as a reference to the internally cached
820      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
821      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
822      * </p>
823      * @return pvCoordinates in orbit definition frame
824      */
825     public TimeStampedPVCoordinates getPVCoordinates() {
826         return (absPva == null) ? orbit.getPVCoordinates() : absPva.getPVCoordinates();
827     }
828 
829     /** Get the {@link TimeStampedPVCoordinates} in given output frame.
830      * <p>
831      * Compute the position and velocity of the satellite. This method caches its
832      * results, and recompute them only when the method is called with a new value
833      * for mu. The result is provided as a reference to the internally cached
834      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
835      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
836      * </p>
837      * @param outputFrame frame in which coordinates should be defined
838      * @return pvCoordinates in orbit definition frame
839      */
840     public TimeStampedPVCoordinates getPVCoordinates(final Frame outputFrame) {
841         return (absPva == null) ? orbit.getPVCoordinates(outputFrame) : absPva.getPVCoordinates(outputFrame);
842     }
843 
844     /** Get the attitude.
845      * @return the attitude.
846      */
847     public Attitude getAttitude() {
848         return attitude;
849     }
850 
851     /** Gets the current mass.
852      * @return the mass (kg)
853      */
854     public double getMass() {
855         return mass;
856     }
857 
858     /** Replace the instance with a data transfer object for serialization.
859      * @return data transfer object that will be serialized
860      */
861     private Object writeReplace() {
862         return isOrbitDefined() ? new DTOO(this) : new DTOA(this);
863     }
864 
865     /** Internal class used only for serialization. */
866     private static class DTOO implements Serializable {
867 
868         /** Serializable UID. */
869         private static final long serialVersionUID = 20150916L;
870 
871         /** Orbit. */
872         private final Orbit orbit;
873 
874         /** Attitude and mass double values. */
875         private double[] d;
876 
877         /** Additional states. */
878         private final Map<String, double[]> additional;
879 
880         /** Simple constructor.
881          * @param state instance to serialize
882          */
883         private DTOO(final SpacecraftState state) {
884 
885             this.orbit      = state.orbit;
886             this.additional = state.additional.isEmpty() ? null : state.additional;
887 
888             final Rotation rotation             = state.attitude.getRotation();
889             final Vector3D spin                 = state.attitude.getSpin();
890             final Vector3D rotationAcceleration = state.attitude.getRotationAcceleration();
891             this.d = new double[] {
892                 rotation.getQ0(), rotation.getQ1(), rotation.getQ2(), rotation.getQ3(),
893                 spin.getX(), spin.getY(), spin.getZ(),
894                 rotationAcceleration.getX(), rotationAcceleration.getY(), rotationAcceleration.getZ(),
895                 state.mass
896             };
897 
898         }
899 
900         /** Replace the de-serialized data transfer object with a {@link SpacecraftState}.
901          * @return replacement {@link SpacecraftState}
902          */
903         private Object readResolve() {
904             return new SpacecraftState(orbit,
905                                        new Attitude(orbit.getFrame(),
906                                                     new TimeStampedAngularCoordinates(orbit.getDate(),
907                                                                                       new Rotation(d[0], d[1], d[2], d[3], false),
908                                                                                       new Vector3D(d[4], d[5], d[6]),
909                                                                                       new Vector3D(d[7], d[8], d[9]))),
910                                        d[10], additional);
911         }
912 
913     }
914 
915     /** Internal class used only for serialization. */
916     private static class DTOA implements Serializable {
917 
918         /** Serializable UID. */
919         private static final long serialVersionUID = 20150916L;
920 
921         /** Absolute position-velocity-acceleration. */
922         private final AbsolutePVCoordinates absPva;
923 
924         /** Attitude and mass double values. */
925         private double[] d;
926 
927         /** Additional states. */
928         private final Map<String, double[]> additional;
929 
930         /** Simple constructor.
931          * @param state instance to serialize
932          */
933         private DTOA(final SpacecraftState state) {
934 
935             this.absPva     = state.absPva;
936             this.additional = state.additional.isEmpty() ? null : state.additional;
937 
938             final Rotation rotation             = state.attitude.getRotation();
939             final Vector3D spin                 = state.attitude.getSpin();
940             final Vector3D rotationAcceleration = state.attitude.getRotationAcceleration();
941             this.d = new double[] {
942                 rotation.getQ0(), rotation.getQ1(), rotation.getQ2(), rotation.getQ3(),
943                 spin.getX(), spin.getY(), spin.getZ(),
944                 rotationAcceleration.getX(), rotationAcceleration.getY(), rotationAcceleration.getZ(),
945                 state.mass
946             };
947 
948         }
949 
950         /** Replace the deserialized data transfer object with a {@link SpacecraftState}.
951          * @return replacement {@link SpacecraftState}
952          */
953         private Object readResolve() {
954             return new SpacecraftState(absPva,
955                                        new Attitude(absPva.getFrame(),
956                                                     new TimeStampedAngularCoordinates(absPva.getDate(),
957                                                                                       new Rotation(d[0], d[1], d[2], d[3], false),
958                                                                                       new Vector3D(d[4], d[5], d[6]),
959                                                                                       new Vector3D(d[7], d[8], d[9]))),
960                                        d[10], additional);
961         }
962     }
963 
964     @Override
965     public String toString() {
966         return "SpacecraftState{" +
967                 "orbit=" + orbit +
968                 ", attitude=" + attitude +
969                 ", mass=" + mass +
970                 ", additional=" + additional +
971                 '}';
972     }
973 }