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.frames.FieldStaticTransform;
33  import org.orekit.frames.FieldTransform;
34  import org.orekit.frames.Frame;
35  import org.orekit.frames.KinematicTransform;
36  import org.orekit.frames.StaticTransform;
37  import org.orekit.frames.TopocentricFrame;
38  import org.orekit.frames.TopocentricTransformProvider;
39  import org.orekit.frames.Transform;
40  import org.orekit.models.earth.displacement.StationDisplacement;
41  import org.orekit.time.AbsoluteDate;
42  import org.orekit.time.FieldAbsoluteDate;
43  import org.orekit.time.TimeInterval;
44  import org.orekit.time.clocks.ClockModel;
45  import org.orekit.utils.AngularCoordinates;
46  import org.orekit.utils.FieldAngularCoordinates;
47  import org.orekit.utils.FieldPVCoordinates;
48  import org.orekit.utils.FieldPVCoordinatesProvider;
49  import org.orekit.utils.PVCoordinatesProvider;
50  import org.orekit.utils.drivers.ParameterDriver;
51  import org.orekit.utils.TimeStampedFieldPVCoordinates;
52  
53  /** Class modeling a ground station that can perform some measurements.
54   * <p>
55   * This class adds a position offset parameter to a base {@link TopocentricFrame
56   * topocentric frame}.
57   * </p>
58   * <p>
59   * Since 9.3, this class also adds a station clock offset parameter, which manages
60   * the value that must be subtracted from the observed measurement date to get the real
61   * physical date at which the measurement was performed (i.e. the offset is negative
62   * if the ground station clock is slow and positive if it is fast).
63   * </p>
64   * <ol>
65   *   <li>station clock offset, controlled by {@link #getClockModel()} )} ()}</li>
66   *   <li>station position offset, controlled by {@link #getEastOffsetDriver()},
67   *   {@link #getNorthOffsetDriver()} and {@link #getZenithOffsetDriver()}</li>
68   * </ol>
69   * @author Luc Maisonobe
70   * @since 8.0
71   */
72  public class GroundStation extends AbstractParticipant implements GroundObserver {
73  
74      /** Position offsets scaling factor.
75       * <p>
76       * We use a power of 2 (in fact really 1.0 here) to avoid numeric noise introduction
77       * in the multiplications/divisions sequences.
78       * </p>
79       */
80      private static final double POSITION_OFFSET_SCALE = FastMath.scalb(1.0, 0);
81  
82      /** Base frame associated with the station. */
83      private final TopocentricFrame baseFrame;
84  
85      /** Driver for position offset along the East axis. */
86      private final ParameterDriver eastOffsetDriver;
87  
88      /** Driver for position offset along the North axis. */
89      private final ParameterDriver northOffsetDriver;
90  
91      /** Driver for position offset along the zenith axis. */
92      private final ParameterDriver zenithOffsetDriver;
93  
94      /**
95       * Build a ground station ignoring {@link StationDisplacement station displacements}.
96       * <p> The initial values for the station offset model
97       * ({@link #getClockModel()}, {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
98       * {@link #getZenithOffsetDriver()}) are set to 0. This implies that as long as these values are not changed, the
99       * offset frame is the same as the {@link #getBaseFrame() base frame}. As soon as some of these models are changed,
100      * the offset frame moves away from the {@link #getBaseFrame() base frame}.
101      * </p>
102      *
103      * @param baseFrame base frame associated with the station, without *any* parametric model (no station offset)
104      * @see #GroundStation(TopocentricFrame, ClockModel)
105      * @since 13.0
106      */
107     public GroundStation(final TopocentricFrame baseFrame) {
108         this(baseFrame, createEmptyPolynomialClock(baseFrame.getName()));
109     }
110 
111      /**
112      * Simple constructor.
113      * <p>
114      * The initial values for the station offset model
115      * ({@link #getClockModel()}, {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
116      * {@link #getZenithOffsetDriver()}, {@link #getClockModel()}) are set to 0. This implies that as long as
117      * these values are not changed, the offset frame is the same as the {@link #getBaseFrame() base frame}. As soon as
118      * some of these models are changed, the offset frame moves away from the {@link #getBaseFrame() base frame}.
119      * </p>
120      *
121      * @param baseFrame     base frame associated with the station, without *any* parametric model (no station offset)
122      * @param clock         new quadratic clock model with user-supplied displacements
123      * @since 12.1
124      */
125     public GroundStation(final TopocentricFrame baseFrame, final ClockModel clock) {
126         super(baseFrame.getName(), clock);
127         this.baseFrame = baseFrame;
128 
129         this.eastOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-East",
130                                                     0.0, POSITION_OFFSET_SCALE,
131                                                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
132                                                     TimeInterval.UNLIMITED);
133 
134         this.northOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-North",
135                                                      0.0, POSITION_OFFSET_SCALE,
136                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
137                                                      TimeInterval.UNLIMITED);
138 
139         this.zenithOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-Zenith",
140                                                       0.0, POSITION_OFFSET_SCALE,
141                                                       Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
142                                                       TimeInterval.UNLIMITED);
143 
144         // Add the ground station parameters to the master list.
145         addParameterDriver(this.eastOffsetDriver);
146         addParameterDriver(this.northOffsetDriver);
147         addParameterDriver(this.zenithOffsetDriver);
148 
149     }
150 
151     @Override
152     public BodyShape getParentShape() {
153         return getBaseFrame().getParentShape();
154     }
155 
156     /** Get a driver allowing to change station position along East axis.
157      * @return driver for station position offset along East axis
158      */
159     public ParameterDriver getEastOffsetDriver() {
160         return eastOffsetDriver;
161     }
162 
163     /** Get a driver allowing to change station position along North axis.
164      * @return driver for station position offset along North axis
165      */
166     public ParameterDriver getNorthOffsetDriver() {
167         return northOffsetDriver;
168     }
169 
170     /** Get a driver allowing to change station position along Zenith axis.
171      * @return driver for station position offset along Zenith axis
172      */
173     public ParameterDriver getZenithOffsetDriver() {
174         return zenithOffsetDriver;
175     }
176 
177     /** Get the base frame associated with the station.
178      * <p>
179      * The base frame corresponds to a null position offset, null
180      * polar motion, null meridian shift
181      * </p>
182      * @return base frame associated with the station
183      */
184     public TopocentricFrame getBaseFrame() {
185         return baseFrame;
186     }
187 
188     /** Get the station displacement.
189      * @param date current date
190      * @param position raw position of the station in Earth frame
191      * before displacement is applied
192      * @return station displacement
193      * @since 9.1
194      */
195     protected Vector3D computeDisplacement(final AbsoluteDate date, final Vector3D position) {
196         return Vector3D.ZERO;
197     }
198 
199     /** Get the geodetic point at the center of the offset frame.
200      * @param date current date (may be null if displacements are ignored)
201      * @return geodetic point at the center of the offset frame
202      * @since 9.1
203      */
204     public GeodeticPoint getOffsetGeodeticPoint(final AbsoluteDate date) {
205 
206         // take station offset into account
207         final double    x          = eastOffsetDriver.getValue();
208         final double    y          = northOffsetDriver.getValue();
209         final double    z          = zenithOffsetDriver.getValue();
210         final BodyShape baseShape  = baseFrame.getParentShape();
211         final StaticTransform baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), date);
212         Vector3D        origin     = baseToBody.transformPosition(new Vector3D(x, y, z));
213 
214         if (date != null) {
215             origin = origin.add(computeDisplacement(date, origin));
216         }
217 
218         return baseShape.transform(origin, baseShape.getBodyFrame(), date);
219 
220     }
221 
222     /** Get the geodetic point at the center of the offset frame.
223      * @param <T> type of the field elements
224      * @param date current date(<em>must</em> be non-null, which is a more stringent condition
225      *      *                    than in {@link #getOffsetGeodeticPoint(AbsoluteDate)}
226      * @return geodetic point at the center of the offset frame
227      * @since 12.1
228      */
229     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getOffsetGeodeticPoint(final FieldAbsoluteDate<T> date) {
230 
231         // take station offset into account
232         final double    x          = eastOffsetDriver.getValue();
233         final double    y          = northOffsetDriver.getValue();
234         final double    z          = zenithOffsetDriver.getValue();
235         final BodyShape baseShape  = baseFrame.getParentShape();
236         final FieldStaticTransform<T> baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), date);
237         FieldVector3D<T> origin    = baseToBody.transformPosition(new Vector3D(x, y, z));
238         origin = origin.add(computeDisplacement(date.toAbsoluteDate(), origin.toVector3D()));
239 
240         return baseShape.transform(origin, baseShape.getBodyFrame(), date);
241 
242     }
243 
244     /** {@inheritDoc} */
245     @Override
246     public PVCoordinatesProvider getPVCoordinatesProvider() {
247         final GeodeticPoint offsetPoint = getOffsetGeodeticPoint(AbsoluteDate.ARBITRARY_EPOCH);
248         return new TopocentricFrame(baseFrame.getParentShape(), offsetPoint, "offset");
249     }
250 
251     /** {@inheritDoc} */
252     @Override
253     public FieldPVCoordinatesProvider<Gradient> getFieldPVCoordinatesProvider(final int freeParameters,
254                                                                               final Map<String, Integer> parameterIndices) {
255         return new FieldPVCoordinatesProvider<>() {
256             @Override
257             public TimeStampedFieldPVCoordinates<Gradient> getPVCoordinates(final FieldAbsoluteDate<Gradient> date,
258                                                                             final Frame frame) {
259                 // take station offsets into account
260                 final FieldVector3D<Gradient> origin = getOrigin(date, parameterIndices);
261 
262                 // body-fixed body-centered to target (with linear approximation for performance)
263                 final Transform bodyToInertNonField = baseFrame.getParent().getTransformTo(frame, date.toAbsoluteDate());
264                 final FieldTransform<Gradient> bodyToInert = new FieldTransform<>(date.getField(),
265                         bodyToInertNonField).shiftedBy(date.durationFrom(date.toAbsoluteDate()));
266 
267                 final TimeStampedFieldPVCoordinates<Gradient> zeroPV = new TimeStampedFieldPVCoordinates<>(date,
268                         new FieldPVCoordinates<>(origin, FieldVector3D.getZero(date.getField())));
269                 return bodyToInert.transformPVCoordinates(zeroPV);
270             }
271 
272             @Override
273             public FieldVector3D<Gradient> getPosition(final FieldAbsoluteDate<Gradient> date, final Frame frame) {
274                 // take station offsets into account
275                 final FieldVector3D<Gradient> origin = getOrigin(date, parameterIndices);
276 
277                 // body-fixed body-centered to target (with linear approximation for performance)
278                 final KinematicTransform bodyToInertNonField = baseFrame.getParent().getKinematicTransformTo(frame,
279                         date.toAbsoluteDate());
280                 final FieldStaticTransform<Gradient> bodyToInert = shiftKinematicTransform(bodyToInertNonField,
281                         date.durationFrom(date.toAbsoluteDate()));
282 
283                 // combine by hand for performance reasons
284                 return bodyToInert.getRotation().applyTo(bodyToInert.getTranslation().add(origin));
285             }
286         };
287     }
288 
289     /**
290      * Retrieve station's position in body shape frame.
291      * @param date date
292      * @param indices mapping from parameters' name to derivatives' index.
293      * @return origin position
294      */
295     protected FieldVector3D<Gradient> getOrigin(final FieldAbsoluteDate<Gradient> date,
296                                                 final Map<String, Integer> indices) {
297         // compute position in topocentric frame
298         final int freeParameters = date.getField().getZero().getFreeParameters();
299         final AbsoluteDate absoluteDate = date.toAbsoluteDate();
300         final Gradient x          = eastOffsetDriver.getValue(freeParameters, indices);
301         final Gradient                       y          = northOffsetDriver.getValue(freeParameters, indices);
302         final Gradient                       z          = zenithOffsetDriver.getValue(freeParameters, indices);
303         final FieldVector3D<Gradient> position = new FieldVector3D<>(x, y, z);
304         // approximate linearly (for performance) static transform from topocentric to body shape frame
305         final Frame bodyFrame = baseFrame.getParentShape().getBodyFrame();
306         final KinematicTransform kinematicTopoToBody = baseFrame.getKinematicTransformTo(bodyFrame, absoluteDate);
307         final FieldStaticTransform<Gradient> staticTopoToBody = shiftKinematicTransform(kinematicTopoToBody,
308                 date.durationFrom(absoluteDate));
309         // apply transform and displacement
310         final FieldVector3D<Gradient>        originBeforeDisplacement     = staticTopoToBody.transformPosition(position);
311         return originBeforeDisplacement.add(computeDisplacement(absoluteDate, originBeforeDisplacement.toVector3D()));
312     }
313 
314     /**
315      * Shift a kinematic transform by a Gradient time into a FieldStaticTransform.
316      * @param kinematicTransform kinematic transform to shift
317      * @param dt time to shift by
318      * @return Field static transform shifted by dt
319      * @since 14.0
320      */
321     protected FieldStaticTransform<Gradient> shiftKinematicTransform(final KinematicTransform kinematicTransform,
322                                                                      final Gradient dt) {
323         // shift translation
324         final Field<Gradient> field = dt.getField();
325         final AbsoluteDate date = kinematicTransform.getDate();
326         final FieldVector3D<Gradient> fieldVelocity = new FieldVector3D<>(field, kinematicTransform.getVelocity());
327         final FieldVector3D<Gradient> shiftedTranslation = fieldVelocity.scalarMultiply(dt).add(kinematicTransform.getTranslation());
328         // shift rotation
329         final FieldAngularCoordinates<Gradient> fieldAngularCoordinates = new FieldAngularCoordinates<>(field,
330                 new AngularCoordinates(kinematicTransform.getRotation(), kinematicTransform.getRotationRate()));
331         final FieldVector3D<Gradient> rotationRate = fieldAngularCoordinates.getRotationRate();
332         final Gradient rate = rotationRate.getNorm();
333         final FieldRotation<Gradient> shiftedRotation = (rate.getReal() == 0.0) ?
334                 fieldAngularCoordinates.getRotation() :
335                 new FieldRotation<>(rotationRate, rate.multiply(dt), RotationConvention.FRAME_TRANSFORM)
336                         .compose(fieldAngularCoordinates.getRotation(), RotationConvention.VECTOR_OPERATOR);
337         return FieldStaticTransform.of(new FieldAbsoluteDate<>(field, date).shiftedBy(dt), shiftedTranslation,
338                 shiftedRotation);
339     }
340 
341     /** {@inheritDoc} */
342     @Override
343     public Transform getOffsetToInertial(final Frame inertial, final AbsoluteDate date,
344                                          final boolean clockOffsetAlreadyApplied) {
345 
346         // take clock offset into account
347         final AbsoluteDate offsetCompensatedDate = clockOffsetAlreadyApplied ?
348                 date :
349                 new AbsoluteDate(date, -getOffsetValue(date));
350 
351         final TopocentricFrame topocentricFrame = (TopocentricFrame) getPVCoordinatesProvider();
352         return topocentricFrame.getTransformTo(inertial, offsetCompensatedDate);
353     }
354 
355     /** {@inheritDoc} */
356     @Override
357     public FieldTransform<Gradient> getOffsetToInertial(final Frame inertial,
358                                                         final FieldAbsoluteDate<Gradient> offsetCompensatedDate,
359                                                         final int freeParameters,
360                                                         final Map<String, Integer> indices) {
361         // take station offsets into account
362         final FieldVector3D<Gradient> origin = getOrigin(offsetCompensatedDate, indices);
363         final FieldGeodeticPoint<Gradient> originGP = baseFrame.getParentShape().transform(origin, baseFrame.getParent(),
364                 offsetCompensatedDate);
365         final FieldStaticTransform<Gradient> staticOffsetToBody = TopocentricTransformProvider.getTransform(baseFrame.getParentShape(),
366                 offsetCompensatedDate, originGP).getStaticInverse();
367         final FieldTransform<Gradient> offsetToBody = new FieldTransform<>(offsetCompensatedDate,
368                 staticOffsetToBody.getTranslation(), staticOffsetToBody.getRotation());
369 
370         // Body-fixed, body-centered frame to target one
371         final FieldTransform<Gradient> bodyToInert = baseFrame.getParent().getTransformTo(inertial, offsetCompensatedDate);
372 
373         // combine all transforms together
374         return new FieldTransform<>(offsetCompensatedDate, offsetToBody, bodyToInert);
375     }
376 
377 }