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