SmoothFieldOfView.java

/* Copyright 2002-2024 CS GROUP
 * 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.geometry.fov;

import java.util.ArrayList;
import java.util.List;

import org.hipparchus.geometry.euclidean.threed.Line;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathUtils;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.frames.Frame;
import org.orekit.frames.Transform;
import org.orekit.propagation.events.VisibilityTrigger;

/** Class representing a spacecraft sensor Field Of View with shape defined by a smooth single loop.
 * @author Luc Maisonobe
 * @since 10.1
 */
public abstract class SmoothFieldOfView extends AbstractFieldOfView {

    /** Direction of the FOV center. */
    private final Vector3D center;

    /** X axis defining FoV boundary. */
    private final Vector3D xAxis;

    /** Y axis defining FoV boundary. */
    private final Vector3D yAxis;

    /** Z axis defining FoV boundary. */
    private final Vector3D zAxis;

    /** Build a new instance.
     * @param center direction of the FOV center (Z<sub>smooth</sub>), in spacecraft frame
     * @param primaryMeridian vector defining the (+X<sub>smooth</sub>, Z<sub>smooth</sub>)
     * half-plane (it is allowed to have {@code primaryMeridian} not orthogonal to
     * {@code center} as orthogonality will be fixed internally)
     * @param margin angular margin to apply to the zone (if positive,
     * the Field Of View will consider points slightly outside of the
     * zone are still visible)
     */
    protected SmoothFieldOfView(final Vector3D center, final Vector3D primaryMeridian,
                                final double margin) {

        super(margin);

        this.center = center;
        this.zAxis  = center.normalize();
        this.yAxis  = Vector3D.crossProduct(center, primaryMeridian).normalize();
        this.xAxis  = Vector3D.crossProduct(yAxis, center).normalize();

    }

    /** Get the direction of the FOV center, in spacecraft frame.
     * @return direction of the FOV center, in spacecraft frame
     */
    public Vector3D getCenter() {
        return center;
    }

    /** Get the X axis defining FoV boundary.
     * @return X axis defining FoV boundary, in spacecraft frame
     */
    public Vector3D getX() {
        return xAxis;
    }

    /** Get the Y axis defining FoV boundary.
     * @return Y axis defining FoV boundary, in spacecraft frame
     */
    public Vector3D getY() {
        return yAxis;
    }

    /** Get the Z axis defining FoV boundary.
     * @return Z axis defining FoV boundary, in spacecraft frame
     */
    public Vector3D getZ() {
        return zAxis;
    }


    /** {@inheritDoc} */
    @Override
    public List<List<GeodeticPoint>> getFootprint(final Transform fovToBody,
                                                  final OneAxisEllipsoid body,
                                                  final double angularStep) {

        final Frame     bodyFrame = body.getBodyFrame();
        final Vector3D  position  = fovToBody.transformPosition(Vector3D.ZERO);
        final double    r         = position.getNorm();
        if (body.isInside(position)) {
            throw new OrekitException(OrekitMessages.POINT_INSIDE_ELLIPSOID);
        }

        // prepare loop around FoV
        boolean                         intersectionsFound = false;
        final int                       nbPoints           = (int) FastMath.ceil(MathUtils.TWO_PI / angularStep);
        final List<GeodeticPoint>       loop               = new ArrayList<>(nbPoints);

        // loop in inverse trigonometric order, so footprint is in trigonometric order
        final double step = MathUtils.TWO_PI / nbPoints;
        for (int i = 0; i < nbPoints; ++i) {
            final Vector3D direction   = directionAt(-i * step);
            final Vector3D awaySC      = new Vector3D(r, direction);
            final Vector3D awayBody    = fovToBody.transformPosition(awaySC);
            final Line     lineOfSight = new Line(position, awayBody, 1.0e-3);
            GeodeticPoint  gp          = body.getIntersectionPoint(lineOfSight, position, bodyFrame, null);
            if (gp != null &&
                Vector3D.dotProduct(awayBody.subtract(position), body.transform(gp).subtract(position)) < 0) {
                // the intersection is in fact on the half-line pointing
                // towards the back side, it is a spurious intersection
                gp = null;
            }

            if (gp != null) {
                // the line of sight does intersect the body
                intersectionsFound = true;
            } else {
                // the line of sight does not intersect body
                // we use a point on the limb
                gp = body.transform(body.pointOnLimb(position, awayBody), bodyFrame, null);
            }

            // add the point
            loop.add(gp);

        }

        final List<List<GeodeticPoint>> footprint = new ArrayList<>();
        if (intersectionsFound) {
            // at least some of the points did intersect the body, there is a footprint
            footprint.add(loop);
        } else {
            // the Field Of View loop does not cross the body
            // either the body is outside of Field Of View, or it is fully contained
            // we check the center
            final Vector3D bodyCenter = fovToBody.toStaticTransform().getInverse().transformPosition(Vector3D.ZERO);
            if (offsetFromBoundary(bodyCenter, 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV) < 0.0) {
                // the body is fully contained in the Field Of View
                // the previous loop did compute the full limb as the footprint
                footprint.add(loop);
            }
        }

        return footprint;

    }

    /** Get boundary direction at angle.
     * @param angle phase angle of the boundary direction
     * @return boundary direction at phase angle in spacecraft frame
     */
    protected abstract Vector3D directionAt(double angle);

}