GroundStationTransformProvider.java
/* Copyright 2022-2026 Romain Serra
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.estimation.measurements;
import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.geometry.euclidean.threed.FieldRotation;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.orekit.bodies.BodyShape;
import org.orekit.bodies.FieldGeodeticPoint;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.data.BodiesElements;
import org.orekit.data.FundamentalNutationArguments;
import org.orekit.frames.FieldStaticTransform;
import org.orekit.frames.FieldTransform;
import org.orekit.frames.Frame;
import org.orekit.frames.StaticTransform;
import org.orekit.frames.TopocentricFrame;
import org.orekit.frames.Transform;
import org.orekit.frames.TransformProvider;
import org.orekit.models.earth.displacement.StationDisplacement;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.utils.ParameterDriver;
/** Class modeling a ground station frame transform.
* <p>
* This class considers a position offset parameter w.r.t. a base {@link TopocentricFrame
* topocentric frame}.
* </p>
* <p>
* This class also adds parameters for an additional polar motion
* and an additional prime meridian orientation. Since these parameters will
* have the same name for all ground stations, they will be managed consistently
* and allow to estimate Earth orientation precisely (this is needed for precise
* orbit determination). The polar motion and prime meridian orientation will
* be applied <em>after</em> regular Earth orientation parameters, so the value
* of the estimated parameters will be correction to EOP, they will not be the
* complete EOP values by themselves. Basically, this means that for Earth, the
* following transforms are applied in order, between inertial frame and ground
* station frame (for non-Earth based ground stations, different precession nutation
* models and associated planet orientation parameters would be applied, if available):
* </p>
* @author Romain Serra
* @since 14.0
*/
class GroundStationTransformProvider implements TransformProvider {
/**
* Target frame.
*/
private final Frame frame;
/** Provider for Earth frame whose EOP parameters can be estimated. */
private final EstimatedEarthFrameProvider estimatedEarthFrameProvider;
/** Earth frame whose EOP parameters can be estimated. */
private final Frame estimatedEarthFrame;
/** Base frame associated with the station. */
private final TopocentricFrame baseFrame;
/** Fundamental nutation arguments. */
private final FundamentalNutationArguments arguments;
/** Displacement models. */
private final StationDisplacement[] displacements;
/** Driver for position offset along the East axis. */
private final ParameterDriver eastOffsetDriver;
/** Driver for position offset along the North axis. */
private final ParameterDriver northOffsetDriver;
/** Driver for position offset along the zenith axis. */
private final ParameterDriver zenithOffsetDriver;
/**
* Constructor.
* @param frame target frame
* @param baseFrame base frame associated with the station, without *any* parametric model (no station offset,
* no polar motion, no meridian shift)
* @param eastOffsetDriver driver for position offset along the East axis
* @param northOffsetDriver driver for position offset along the North axis
* @param zenithOffsetDriver driver for position offset along the zenith axis
* @param estimatedEarthFrameProvider provider for Earth frame whose EOP parameters can be estimated
* @param fundamentalNutationArguments fundamental nutation arguments
* @param displacements ground station displacement model (tides, ocean loading, atmospheric loading, thermal
* effects...)
*/
GroundStationTransformProvider(final Frame frame, final TopocentricFrame baseFrame,
final ParameterDriver eastOffsetDriver,
final ParameterDriver northOffsetDriver,
final ParameterDriver zenithOffsetDriver,
final EstimatedEarthFrameProvider estimatedEarthFrameProvider,
final FundamentalNutationArguments fundamentalNutationArguments,
final StationDisplacement... displacements) {
this.frame = frame;
this.baseFrame = baseFrame;
this.estimatedEarthFrameProvider = estimatedEarthFrameProvider;
this.estimatedEarthFrame = new Frame(baseFrame.getParent(), estimatedEarthFrameProvider,
baseFrame.getParent() + "-estimated");
this.arguments = fundamentalNutationArguments;
this.displacements = displacements.clone();
this.eastOffsetDriver = eastOffsetDriver;
this.northOffsetDriver = northOffsetDriver;
this.zenithOffsetDriver = zenithOffsetDriver;
}
/** {@inheritDoc} */
@Override
public Transform getTransform(final AbsoluteDate date) {
// take Earth offsets into account
final Transform intermediateToBody = estimatedEarthFrameProvider.getTransform(date).getInverse();
// take station offsets into account
final BodyShape baseShape = baseFrame.getParentShape();
final Vector3D origin = getOrigin(date);
final GeodeticPoint originGP = baseShape.transform(origin, baseShape.getBodyFrame(), date);
final Transform offsetToIntermediate =
new Transform(date,
new Transform(date,
new Rotation(Vector3D.PLUS_I, Vector3D.PLUS_K,
originGP.getEast(), originGP.getZenith()),
Vector3D.ZERO),
new Transform(date, origin));
if (baseFrame.getParent() == frame) {
return new Transform(date, offsetToIntermediate, intermediateToBody);
}
// combine all transforms together
final Transform bodyToInert = baseFrame.getParent().getTransformTo(frame, date);
return new Transform(date, offsetToIntermediate, new Transform(date, intermediateToBody, bodyToInert));
}
/** {@inheritDoc} */
@Override
public StaticTransform getStaticTransform(final AbsoluteDate date) {
// take Earth offsets into account
final StaticTransform intermediateToBody = estimatedEarthFrameProvider.getStaticTransform(date).getInverse();
// take station offsets into account
final BodyShape baseShape = baseFrame.getParentShape();
final Vector3D origin = getOrigin(date);
final GeodeticPoint originGP = baseShape.transform(origin, baseShape.getBodyFrame(), date);
final StaticTransform offsetToIntermediate = StaticTransform.compose(date,
StaticTransform.of(date,
new Rotation(Vector3D.PLUS_I, Vector3D.PLUS_K,
originGP.getEast(), originGP.getZenith())),
StaticTransform.of(date, origin));
if (baseFrame.getParent() == frame) {
return StaticTransform.compose(date, offsetToIntermediate, intermediateToBody);
}
// combine all transforms together
final StaticTransform bodyToInert = baseFrame.getParent().getStaticTransformTo(frame, date);
return StaticTransform.compose(date, offsetToIntermediate, StaticTransform.compose(date, intermediateToBody, bodyToInert));
}
/**
* Retrieve station's position in body shape frame.
* @param date date
* @return origin position
*/
private Vector3D getOrigin(final AbsoluteDate date) {
final double x = eastOffsetDriver.getValue(date);
final double y = northOffsetDriver.getValue(date);
final double z = zenithOffsetDriver.getValue(date);
final Frame bodyFrame = baseFrame.getParentShape().getBodyFrame();
final StaticTransform staticTopoToBody = baseFrame.getStaticTransformTo(bodyFrame, date);
final Vector3D originBeforeDisplacement = staticTopoToBody.transformPosition(new Vector3D(x, y, z));
return originBeforeDisplacement.add(computeDisplacement(date, originBeforeDisplacement));
}
/** Get the station displacement.
* @param date current date
* @param position raw position of the station in Earth frame
* before displacement is applied
* @return station displacement
*/
private Vector3D computeDisplacement(final AbsoluteDate date, final Vector3D position) {
Vector3D displacement = Vector3D.ZERO;
if (arguments != null) {
final BodiesElements elements = arguments.evaluateAll(date);
for (final StationDisplacement sd : displacements) {
// we consider all displacements apply to the same initial position,
// i.e. they apply simultaneously, not according to some order
displacement = displacement.add(sd.displacement(elements, estimatedEarthFrame, position));
}
}
return displacement;
}
/** {@inheritDoc} */
@Override
public <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date) {
// take Earth offsets into account
final FieldTransform<T> intermediateToBody = estimatedEarthFrameProvider.getTransform(date).getInverse();
// take station offsets into account
final FieldVector3D<T> origin = getOrigin(date);
return getTransform(date, origin, intermediateToBody);
}
<T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date,
final FieldVector3D<T> origin,
final FieldTransform<T> intermediateToBody) {
final Field<T> field = date.getField();
final FieldVector3D<T> zero = FieldVector3D.getZero(field);
final FieldVector3D<T> plusI = FieldVector3D.getPlusI(field);
final FieldVector3D<T> plusK = FieldVector3D.getPlusK(field);
final FieldGeodeticPoint<T> originGP = baseFrame.getParentShape().transform(origin,
baseFrame.getParentShape().getBodyFrame(), date);
final FieldTransform<T> offsetToIntermediate =
new FieldTransform<>(date,
new FieldTransform<>(date,
new FieldRotation<>(plusI, plusK,
originGP.getEast(), originGP.getZenith()),
zero),
new FieldTransform<>(date, origin));
// combine all transforms together
if (baseFrame.getParent() == frame) {
return new FieldTransform<>(date, offsetToIntermediate, intermediateToBody);
}
final FieldTransform<T> bodyToInert = baseFrame.getParent().getTransformTo(frame, date);
return new FieldTransform<>(date, offsetToIntermediate,
new FieldTransform<>(date, intermediateToBody, bodyToInert));
}
/** {@inheritDoc} */
@Override
public <T extends CalculusFieldElement<T>> FieldStaticTransform<T> getStaticTransform(final FieldAbsoluteDate<T> date) {
// take Earth offsets into account
final FieldStaticTransform<T> intermediateToBody = estimatedEarthFrameProvider.getStaticTransform(date).getInverse();
final FieldVector3D<T> origin = new FieldVector3D<>(date.getField(), getOrigin(date.toAbsoluteDate()));
// take station offsets into account
final Field<T> field = date.getField();
final FieldVector3D<T> plusI = FieldVector3D.getPlusI(field);
final FieldVector3D<T> plusK = FieldVector3D.getPlusK(field);
final FieldGeodeticPoint<T> originGP = baseFrame.getParentShape().transform(origin,
baseFrame.getParentShape().getBodyFrame(), date);
final FieldStaticTransform<T> offsetToIntermediate = FieldStaticTransform.compose(date,
FieldStaticTransform.of(date,
new FieldRotation<>(plusI, plusK, originGP.getEast(), originGP.getZenith())),
FieldStaticTransform.of(date, origin));
// combine all transforms together
if (baseFrame.getParent() == frame) {
return FieldStaticTransform.compose(date, offsetToIntermediate, intermediateToBody);
}
final FieldStaticTransform<T> bodyToInert = baseFrame.getParent().getStaticTransformTo(frame, date);
return FieldStaticTransform.compose(date, offsetToIntermediate,
FieldStaticTransform.compose(date, intermediateToBody, bodyToInert));
}
/**
* Retrieve station's position in body shape frame.
* @param <T> field type
* @param date date
* @return origin position
*/
private <T extends CalculusFieldElement<T>> FieldVector3D<T> getOrigin(final FieldAbsoluteDate<T> date) {
final AbsoluteDate absoluteDate = date.toAbsoluteDate();
final Field<T> field = date.getField();
final T x = field.getZero().newInstance(eastOffsetDriver.getValue(absoluteDate));
final T y = field.getZero().newInstance(northOffsetDriver.getValue(absoluteDate));
final T z = field.getZero().newInstance(zenithOffsetDriver.getValue(absoluteDate));
final Frame bodyFrame = baseFrame.getParentShape().getBodyFrame();
final FieldStaticTransform<T> staticTopoToBody = baseFrame.getStaticTransformTo(bodyFrame, date);
final FieldVector3D<T> originBeforeDisplacement = staticTopoToBody.transformPosition(new FieldVector3D<>(x, y, z));
return originBeforeDisplacement.add(computeDisplacement(absoluteDate, originBeforeDisplacement.toVector3D()));
}
}