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