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