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