1   /* Copyright 2002-2019 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.estimation.measurements;
18  
19  import java.util.Map;
20  
21  import org.hipparchus.Field;
22  import org.hipparchus.analysis.differentiation.DSFactory;
23  import org.hipparchus.analysis.differentiation.DerivativeStructure;
24  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
25  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
26  import org.hipparchus.geometry.euclidean.threed.Rotation;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.util.FastMath;
29  import org.orekit.bodies.BodyShape;
30  import org.orekit.bodies.FieldGeodeticPoint;
31  import org.orekit.bodies.GeodeticPoint;
32  import org.orekit.data.BodiesElements;
33  import org.orekit.data.FundamentalNutationArguments;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  import org.orekit.frames.EOPHistory;
37  import org.orekit.frames.FieldTransform;
38  import org.orekit.frames.Frame;
39  import org.orekit.frames.FramesFactory;
40  import org.orekit.frames.TopocentricFrame;
41  import org.orekit.frames.Transform;
42  import org.orekit.models.earth.displacement.StationDisplacement;
43  import org.orekit.time.AbsoluteDate;
44  import org.orekit.time.FieldAbsoluteDate;
45  import org.orekit.time.TimeScalesFactory;
46  import org.orekit.time.UT1Scale;
47  import org.orekit.utils.ParameterDriver;
48  
49  /** Class modeling a ground station that can perform some measurements.
50   * <p>
51   * This class adds a position offset parameter to a base {@link TopocentricFrame
52   * topocentric frame}.
53   * </p>
54   * <p>
55   * Since 9.0, this class also adds parameters for an additional polar motion
56   * and an additional prime meridian orientation. Since these parameters will
57   * have the same name for all ground stations, they will be managed consistently
58   * and allow to estimate Earth orientation precisely (this is needed for precise
59   * orbit determination). The polar motion and prime meridian orientation will
60   * be applied <em>after</em> regular Earth orientation parameters, so the value
61   * of the estimated parameters will be correction to EOP, they will not be the
62   * complete EOP values by themselves. Basically, this means that for Earth, the
63   * following transforms are applied in order, between inertial frame and ground
64   * station frame (for non-Earth based ground stations, different precession nutation
65   * models and associated planet oritentation parameters would be applied, if available):
66   * </p>
67   * <p>
68   * Since 9.3, this class also adds a station clock offset parameter, which manages
69   * the value that must be subtracted from the observed measurement date to get the real
70   * physical date at which the measurement was performed (i.e. the offset is negative
71   * if the ground station clock is slow and positive if it is fast).
72   * </p>
73   * <ol>
74   *   <li>precession/nutation, as theoretical model plus celestial pole EOP parameters</li>
75   *   <li>body rotation, as theoretical model plus prime meridian EOP parameters</li>
76   *   <li>polar motion, which is only from EOP parameters (no theoretical models)</li>
77   *   <li>additional body rotation, controlled by {@link #getPrimeMeridianOffsetDriver()} and {@link #getPrimeMeridianDriftDriver()}</li>
78   *   <li>additional polar motion, controlled by {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()},
79   *   {@link #getPolarOffsetYDriver()} and {@link #getPolarDriftYDriver()}</li>
80   *   <li>station clock offset, controlled by {@link #getClockOffsetDriver()}</li>
81   *   <li>station position offset, controlled by {@link #getEastOffsetDriver()},
82   *   {@link #getNorthOffsetDriver()} and {@link #getZenithOffsetDriver()}</li>
83   * </ol>
84   * @author Luc Maisonobe
85   * @since 8.0
86   */
87  public class GroundStation {
88  
89      /** Suffix for ground station position and clock offset parameters names. */
90      public static final String OFFSET_SUFFIX = "-offset";
91  
92      /** Suffix for ground station intermediate frame name. */
93      public static final String INTERMEDIATE_SUFFIX = "-intermediate";
94  
95      /** Clock offset scaling factor.
96       * <p>
97       * We use a power of 2 to avoid numeric noise introduction
98       * in the multiplications/divisions sequences.
99       * </p>
100      */
101     private static final double CLOCK_OFFSET_SCALE = FastMath.scalb(1.0, -10);
102 
103     /** Position offsets scaling factor.
104      * <p>
105      * We use a power of 2 (in fact really 1.0 here) to avoid numeric noise introduction
106      * in the multiplications/divisions sequences.
107      * </p>
108      */
109     private static final double POSITION_OFFSET_SCALE = FastMath.scalb(1.0, 0);
110 
111     /** Provider for Earth frame whose EOP parameters can be estimated. */
112     private final EstimatedEarthFrameProvider estimatedEarthFrameProvider;
113 
114     /** Earth frame whose EOP parameters can be estimated. */
115     private final Frame estimatedEarthFrame;
116 
117     /** Base frame associated with the station. */
118     private final TopocentricFrame baseFrame;
119 
120     /** Fundamental nutation arguments. */
121     private final FundamentalNutationArguments arguments;
122 
123     /** Displacement models. */
124     private final StationDisplacement[] displacements;
125 
126     /** Driver for clock offset. */
127     private final ParameterDriver clockOffsetDriver;
128 
129     /** Driver for position offset along the East axis. */
130     private final ParameterDriver eastOffsetDriver;
131 
132     /** Driver for position offset along the North axis. */
133     private final ParameterDriver northOffsetDriver;
134 
135     /** Driver for position offset along the zenith axis. */
136     private final ParameterDriver zenithOffsetDriver;
137 
138     /** Build a ground station ignoring {@link StationDisplacement station displacements}.
139      * <p>
140      * The initial values for the pole and prime meridian parametric linear models
141      * ({@link #getPrimeMeridianOffsetDriver()}, {@link #getPrimeMeridianDriftDriver()},
142      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()},
143      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()}) are set to 0.
144      * The initial values for the station offset model ({@link #getClockOffsetDriver()},
145      * {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
146      * {@link #getZenithOffsetDriver()}) are set to 0.
147      * This implies that as long as these values are not changed, the offset frame is
148      * the same as the {@link #getBaseFrame() base frame}. As soon as some of these models
149      * are changed, the offset frame moves away from the {@link #getBaseFrame() base frame}.
150      * </p>
151      * @param baseFrame base frame associated with the station, without *any* parametric
152      * model (no station offset, no polar motion, no meridian shift)
153      * @see #GroundStation(TopocentricFrame, EOPHistory, StationDisplacement...)
154      */
155     public GroundStation(final TopocentricFrame baseFrame) {
156         this(baseFrame, FramesFactory.findEOP(baseFrame), new StationDisplacement[0]);
157     }
158 
159     /** Simple constructor.
160      * <p>
161      * The initial values for the pole and prime meridian parametric linear models
162      * ({@link #getPrimeMeridianOffsetDriver()}, {@link #getPrimeMeridianDriftDriver()},
163      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()},
164      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()}) are set to 0.
165      * The initial values for the station offset model ({@link #getClockOffsetDriver()},
166      * {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
167      * {@link #getZenithOffsetDriver()}, {@link #getClockOffsetDriver()}) are set to 0.
168      * This implies that as long as these values are not changed, the offset frame is
169      * the same as the {@link #getBaseFrame() base frame}. As soon as some of these models
170      * are changed, the offset frame moves away from the {@link #getBaseFrame() base frame}.
171      * </p>
172      * @param baseFrame base frame associated with the station, without *any* parametric
173      * model (no station offset, no polar motion, no meridian shift)
174      * @param eopHistory EOP history associated with Earth frames
175      * @param displacements ground station displacement model (tides, ocean loading,
176      * atmospheric loading, thermal effects...)
177      * @since 9.1
178      */
179     public GroundStation(final TopocentricFrame baseFrame, final EOPHistory eopHistory,
180                          final StationDisplacement... displacements) {
181 
182         this.baseFrame = baseFrame;
183 
184         if (eopHistory == null) {
185             throw new OrekitException(OrekitMessages.NO_EARTH_ORIENTATION_PARAMETERS);
186         }
187 
188         final UT1Scale baseUT1 = TimeScalesFactory.getUT1(eopHistory);
189         this.estimatedEarthFrameProvider = new EstimatedEarthFrameProvider(baseUT1);
190         this.estimatedEarthFrame = new Frame(baseFrame.getParent(), estimatedEarthFrameProvider,
191                                              baseFrame.getParent() + "-estimated");
192 
193         if (displacements.length == 0) {
194             arguments = null;
195         } else {
196             arguments = eopHistory.getConventions().getNutationArguments(estimatedEarthFrameProvider.getEstimatedUT1());
197         }
198 
199         this.displacements = displacements.clone();
200 
201         this.clockOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-clock",
202                                                      0.0, CLOCK_OFFSET_SCALE,
203                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
204 
205         this.eastOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-East",
206                                                     0.0, POSITION_OFFSET_SCALE,
207                                                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
208 
209         this.northOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-North",
210                                                      0.0, POSITION_OFFSET_SCALE,
211                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
212 
213         this.zenithOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-Zenith",
214                                                       0.0, POSITION_OFFSET_SCALE,
215                                                       Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
216 
217     }
218 
219     /** Get the displacement models.
220      * @return displacement models (empty if no model has been set up)
221      * @since 9.1
222      */
223     public StationDisplacement[] getDisplacements() {
224         return displacements.clone();
225     }
226 
227     /** Get a driver allowing to change station clock (which is related to measurement date).
228      * @return driver for station clock offset
229      * @since 9.3
230      */
231     public ParameterDriver getClockOffsetDriver() {
232         return clockOffsetDriver;
233     }
234 
235     /** Get a driver allowing to change station position along East axis.
236      * @return driver for station position offset along East axis
237      */
238     public ParameterDriver getEastOffsetDriver() {
239         return eastOffsetDriver;
240     }
241 
242     /** Get a driver allowing to change station position along North axis.
243      * @return driver for station position offset along North axis
244      */
245     public ParameterDriver getNorthOffsetDriver() {
246         return northOffsetDriver;
247     }
248 
249     /** Get a driver allowing to change station position along Zenith axis.
250      * @return driver for station position offset along Zenith axis
251      */
252     public ParameterDriver getZenithOffsetDriver() {
253         return zenithOffsetDriver;
254     }
255 
256     /** Get a driver allowing to add a prime meridian rotation.
257      * <p>
258      * The parameter is an angle in radians. In order to convert this
259      * value to a DUT1 in seconds, the value must be divided by
260      * {@code ave = 7.292115146706979e-5} (which is the nominal Angular Velocity
261      * of Earth from the TIRF model).
262      * </p>
263      * @return driver for prime meridian rotation
264      */
265     public ParameterDriver getPrimeMeridianOffsetDriver() {
266         return estimatedEarthFrameProvider.getPrimeMeridianOffsetDriver();
267     }
268 
269     /** Get a driver allowing to add a prime meridian rotation rate.
270      * <p>
271      * The parameter is an angle rate in radians per second. In order to convert this
272      * value to a LOD in seconds, the value must be multiplied by -86400 and divided by
273      * {@code ave = 7.292115146706979e-5} (which is the nominal Angular Velocity
274      * of Earth from the TIRF model).
275      * </p>
276      * @return driver for prime meridian rotation rate
277      */
278     public ParameterDriver getPrimeMeridianDriftDriver() {
279         return estimatedEarthFrameProvider.getPrimeMeridianDriftDriver();
280     }
281 
282     /** Get a driver allowing to add a polar offset along X.
283      * <p>
284      * The parameter is an angle in radians
285      * </p>
286      * @return driver for polar offset along X
287      */
288     public ParameterDriver getPolarOffsetXDriver() {
289         return estimatedEarthFrameProvider.getPolarOffsetXDriver();
290     }
291 
292     /** Get a driver allowing to add a polar drift along X.
293      * <p>
294      * The parameter is an angle rate in radians per second
295      * </p>
296      * @return driver for polar drift along X
297      */
298     public ParameterDriver getPolarDriftXDriver() {
299         return estimatedEarthFrameProvider.getPolarDriftXDriver();
300     }
301 
302     /** Get a driver allowing to add a polar offset along Y.
303      * <p>
304      * The parameter is an angle in radians
305      * </p>
306      * @return driver for polar offset along Y
307      */
308     public ParameterDriver getPolarOffsetYDriver() {
309         return estimatedEarthFrameProvider.getPolarOffsetYDriver();
310     }
311 
312     /** Get a driver allowing to add a polar drift along Y.
313      * <p>
314      * The parameter is an angle rate in radians per second
315      * </p>
316      * @return driver for polar drift along Y
317      */
318     public ParameterDriver getPolarDriftYDriver() {
319         return estimatedEarthFrameProvider.getPolarDriftYDriver();
320     }
321 
322     /** Get the base frame associated with the station.
323      * <p>
324      * The base frame corresponds to a null position offset, null
325      * polar motion, null meridian shift
326      * </p>
327      * @return base frame associated with the station
328      */
329     public TopocentricFrame getBaseFrame() {
330         return baseFrame;
331     }
332 
333     /** Get the estimated Earth frame, including the estimated linear models for pole and prime meridian.
334      * <p>
335      * This frame is bound to the {@link #getPrimeMeridianOffsetDriver() driver for prime meridian offset},
336      * {@link #getPrimeMeridianDriftDriver() driver prime meridian drift},
337      * {@link #getPolarOffsetXDriver() driver for polar offset along X},
338      * {@link #getPolarDriftXDriver() driver for polar drift along X},
339      * {@link #getPolarOffsetYDriver() driver for polar offset along Y},
340      * {@link #getPolarDriftYDriver() driver for polar drift along Y}, so its orientation changes when
341      * the {@link ParameterDriver#setValue(double) setValue} methods of the drivers are called.
342      * </p>
343      * @return estimated Earth frame
344      * @since 9.1
345      */
346     public Frame getEstimatedEarthFrame() {
347         return estimatedEarthFrame;
348     }
349 
350     /** Get the estimated UT1 scale, including the estimated linear models for prime meridian.
351      * <p>
352      * This time scale is bound to the {@link #getPrimeMeridianOffsetDriver() driver for prime meridian offset},
353      * and {@link #getPrimeMeridianDriftDriver() driver prime meridian drift}, so its offset from UTC changes when
354      * the {@link ParameterDriver#setValue(double) setValue} methods of the drivers are called.
355      * </p>
356      * @return estimated Earth frame
357      * @since 9.1
358      */
359     public UT1Scale getEstimatedUT1() {
360         return estimatedEarthFrameProvider.getEstimatedUT1();
361     }
362 
363     /** Get the geodetic point at the center of the offset frame.
364      * @return geodetic point at the center of the offset frame
365      * @deprecated as of 9.1, replaced by {@link #getOffsetGeodeticPoint(AbsoluteDate)}
366      */
367     @Deprecated
368     public GeodeticPoint getOffsetGeodeticPoint() {
369         return getOffsetGeodeticPoint(null);
370     }
371 
372     /** Get the station displacement.
373      * @param date current date
374      * @param position raw position of the station in Earth frame
375      * before displacement is applied
376      * @return station displacement
377      * @since 9.1
378      */
379     private Vector3D computeDisplacement(final AbsoluteDate date, final Vector3D position) {
380         Vector3D displacement = Vector3D.ZERO;
381         if (arguments != null) {
382             final BodiesElements elements = arguments.evaluateAll(date);
383             for (final StationDisplacement sd : displacements) {
384                 // we consider all displacements apply to the same initial position,
385                 // i.e. they apply simultaneously, not according to some order
386                 displacement = displacement.add(sd.displacement(elements, estimatedEarthFrame, position));
387             }
388         }
389         return displacement;
390     }
391 
392     /** Get the geodetic point at the center of the offset frame.
393      * @param date current date (may be null if displacements are ignored)
394      * @return geodetic point at the center of the offset frame
395      * @since 9.1
396      */
397     public GeodeticPoint getOffsetGeodeticPoint(final AbsoluteDate date) {
398 
399         // take station offset into account
400         final double    x          = eastOffsetDriver.getValue();
401         final double    y          = northOffsetDriver.getValue();
402         final double    z          = zenithOffsetDriver.getValue();
403         final BodyShape baseShape  = baseFrame.getParentShape();
404         final Transform baseToBody = baseFrame.getTransformTo(baseShape.getBodyFrame(), date);
405         Vector3D        origin     = baseToBody.transformPosition(new Vector3D(x, y, z));
406 
407         if (date != null) {
408             origin = origin.add(computeDisplacement(date, origin));
409         }
410 
411         return baseShape.transform(origin, baseShape.getBodyFrame(), null);
412 
413     }
414 
415     /** Get the transform between offset frame and inertial frame.
416      * <p>
417      * The offset frame takes the <em>current</em> position offset,
418      * polar motion and the meridian shift into account. The frame
419      * returned is disconnected from later changes in the parameters.
420      * When the {@link ParameterDriver parameters} managing these
421      * offsets are changed, the method must be called again to retrieve
422      * a new offset frame.
423      * </p>
424      * @param inertial inertial frame to transform to
425      * @param clockDate date of the transform as read by the ground station clock (i.e. clock offset <em>not</em> compensated)
426      * @return transform between offset frame and inertial frame, at <em>real</em> measurement
427      * date (i.e. with clock, Earth and station offsets applied)
428      */
429     public Transform getOffsetToInertial(final Frame inertial, final AbsoluteDate clockDate) {
430 
431         // take clock offset into account
432         final double offset = clockOffsetDriver.getValue();
433         final AbsoluteDatebsoluteDate offsetCompensatedDate = new AbsoluteDate(clockDate, -offset);
434 
435         // take Earth offsets into account
436         final Transform intermediateToBody = estimatedEarthFrameProvider.getTransform(offsetCompensatedDate).getInverse();
437 
438         // take station offsets into account
439         final double    x          = eastOffsetDriver.getValue();
440         final double    y          = northOffsetDriver.getValue();
441         final double    z          = zenithOffsetDriver.getValue();
442         final BodyShape baseShape  = baseFrame.getParentShape();
443         final Transform baseToBody = baseFrame.getTransformTo(baseShape.getBodyFrame(), offsetCompensatedDate);
444         Vector3D        origin     = baseToBody.transformPosition(new Vector3D(x, y, z));
445         origin = origin.add(computeDisplacement(offsetCompensatedDate, origin));
446 
447         final GeodeticPoint originGP = baseShape.transform(origin, baseShape.getBodyFrame(), offsetCompensatedDate);
448         final Transform offsetToIntermediate =
449                         new Transform(offsetCompensatedDate,
450                                       new Transform(offsetCompensatedDate,
451                                                     new Rotation(Vector3D.PLUS_I, Vector3D.PLUS_K,
452                                                                  originGP.getEast(), originGP.getZenith()),
453                                                     Vector3D.ZERO),
454                                       new Transform(offsetCompensatedDate, origin));
455 
456         // combine all transforms together
457         final Transform bodyToInert        = baseFrame.getParent().getTransformTo(inertial, offsetCompensatedDate);
458 
459         return new Transform(TransformpensatedDate, offsetToIntermediate, new Transform(offsetCompensatedDate, intermediateToBody, bodyToInert));
460 
461     }
462 
463     /** Get the transform between offset frame and inertial frame with derivatives.
464      * <p>
465      * As the East and North vectors are not well defined at pole, the derivatives
466      * of these two vectors diverge to infinity as we get closer to the pole.
467      * So this method should not be used for stations less than 0.0001 degree from
468      * either poles.
469      * </p>
470      * @param inertial inertial frame to transform to
471      * @param clockDate date of the transform as read by the ground station clock (i.e. clock offset <em>not</em> compensated)
472      * @param factory factory for the derivatives
473      * @param indices indices of the estimated parameters in derivatives computations
474      * @return transform between offset frame and inertial frame, at <em>real</em> measurement
475      * date (i.e. with clock, Earth and station offsets applied)
476      * @see #getOffsetToInertial(Frame, FieldAbsoluteDate, DSFactory, Map)
477      * @since 9.3
478      */
479     public FieldTransform<DerivativeStructure> getOffsetToInertial(final Frame inertial,
480                                                                    final AbsoluteDate clockDate,
481                                                                    final DSFactory factory,
482                                                                    final Map<String, Integer> indices) {
483         // take clock offset into account
484         final DerivativeStructure offset = clockOffsetDriver.getValue(factory, indices);
485         final FieldAbsoluteDate<DerivativeStructure> offsetCompensatedDate =
486                         new FieldAbsoluteDate<DerivativeStructure>(clockDate, offset.negate());
487 
488         return getOffsetToInertial(inertial, offsetCompensatedDate, factory, indices);
489     }
490 
491     /** Get the transform between offset frame and inertial frame with derivatives.
492      * <p>
493      * As the East and North vectors are not well defined at pole, the derivatives
494      * of these two vectors diverge to infinity as we get closer to the pole.
495      * So this method should not be used for stations less than 0.0001 degree from
496      * either poles.
497      * </p>
498      * @param inertial inertial frame to transform to
499      * @param offsetCompensatedDate date of the transform, clock offset and its derivatives already compensated
500      * @param factory factory for the derivatives
501      * @param indices indices of the estimated parameters in derivatives computations
502      * @return transform between offset frame and inertial frame, at specified date
503      * @see #getOffsetToInertial(Frame, AbsoluteDate, DSFactory, Map)
504      * @since 9.0
505      */
506     public FieldTransform<DerivativeStructure> getOffsetToInertial(final Frame inertial,
507                                                                    final FieldAbsoluteDate<DerivativeStructure> offsetCompensatedDate,
508                                                                    final DSFactory factory,
509                                                                    final Map<String, Integer> indices) {
510 
511         final Field<DerivativeStructure>         field = factory.getDerivativeField();
512         final FieldVector3D<DerivativeStructure> zero  = FieldVector3D.getZero(field);
513         final FieldVector3D<DerivativeStructure> plusI = FieldVector3D.getPlusI(field);
514         final FieldVector3D<DerivativeStructure> plusK = FieldVector3D.getPlusK(field);
515 
516         // take Earth offsets into account
517         final FieldTransform<DerivativeStructure> intermediateToBody =
518                         estimatedEarthFrameProvider.getTransform(offsetCompensatedDate, factory, indices).getInverse();
519 
520         // take station offsets into account
521         final DerivativeStructure  x          = eastOffsetDriver.getValue(factory, indices);
522         final DerivativeStructure  y          = northOffsetDriver.getValue(factory, indices);
523         final DerivativeStructure  z          = zenithOffsetDriver.getValue(factory, indices);
524         final BodyShape            baseShape  = baseFrame.getParentShape();
525         final Transform            baseToBody = baseFrame.getTransformTo(baseShape.getBodyFrame(), (AbsoluteDate) null);
526 
527         FieldVector3D<DerivativeStructure>            origin   = baseToBody.transformPosition(new FieldVector3D<>(x, y, z));
528         origin = origin.add(computeDisplacement(offsetCompensatedDate.toAbsoluteDate(), origin.toVector3D()));
529         final FieldGeodeticPoint<DerivativeStructure> originGP = baseShape.transform(origin, baseShape.getBodyFrame(), offsetCompensatedDate);
530         final FieldTransform<DerivativeStructure> offsetToIntermediate =
531                         new FieldTransform<>(offsetCompensatedDate,
532                                              new FieldTransform<>(offsetCompensatedDate,
533                                                                   new FieldRotation<>(plusI, plusK,
534                                                                                       originGP.getEast(), originGP.getZenith()),
535                                                                   zero),
536                                              new FieldTransform<>(offsetCompensatedDate, origin));
537 
538         // combine all transforms together
539         final FieldTransform<DerivativeStructure> bodyToInert        = baseFrame.getParent().getTransformTo(inertial, offsetCompensatedDate);
540 
541         return new FieldTransform<>(offsetCompensatedDate,
542                                     offsetToIntermediate,
543                                     new FieldTransform<>(offsetCompensatedDate, intermediateToBody, bodyToInert));
544 
545     }
546 
547 }