All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.drag.DraggableSupport Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2016 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */

package gov.nasa.worldwind.drag;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.*;
import gov.nasa.worldwind.util.Logging;

import java.awt.*;

/**
 * Utility functions which support dragging operations on objects implementing the {@link Movable} or {@link Movable2}
 * interface.
 */
public class DraggableSupport
{
    /**
     * The {@link DraggableSupport#computeRelativePoint(Line, Globe, SceneController, double)} method uses a numeric
     * search method to determine the coordinates of the desired position. The numeric solver will stop at a defined
     * threshold or step limit. The default threshold is provided here. This value is utilized when using the static
     * drag functions provided by this class.
     */
    public static final double DEFAULT_CONVERGENCE_THRESHOLD = 0.1;
    /**
     * The {@link DraggableSupport#computeRelativePoint(Line, Globe, SceneController, double)} method uses a numeric
     * search method to determine the coordinates of the desired position. The numeric solver will stop at a defined
     * threshold or step limit. The default step limit is provided here. This value is utilized when using the static
     * drag functions provided by this class.
     */
    public static final int DEFAULT_STEP_LIMIT = 20;
    /**
     * Initial drag operation offset in x and y screen coordinates, between the object reference position and the screen
     * point. Used for screen size constant drag operations.
     */
    protected Vec4 initialScreenPointOffset = null;
    /**
     * Initial drag operation cartesian coordinates of the objects reference position. Used for globe size constant drag
     * operations.
     */
    protected Vec4 initialEllipsoidalReferencePoint = null;
    /**
     * Initial drag operation cartesian coordinates of the initial screen point.
     */
    protected Vec4 initialEllipsoidalScreenPoint = null;
    /**
     * This instances step limit when using the solver to determine the position of objects using the
     * {@link WorldWind#RELATIVE_TO_GROUND} altitude mode. Increasing this value will increase solver runtime and may
     * cause the event loop to hang. If the solver exceeds the number of steps specified here, it will fall back to
     * using a position calculated by intersection with the ellipsoid.
     */
    protected int stepLimit = DEFAULT_STEP_LIMIT;
    /**
     * This instances convergence threshold for the solver used to determine the position of objects using the
     * {@link WorldWind#RELATIVE_TO_GROUND} altitude mode. Decreasing this value will increase solver runtime and may
     * cause the event loop to hang. If the solver does not converge to the threshold specified here, it will fall back
     * to using a position calculated by intersection with the ellipsoid.
     */
    protected double convergenceThreshold = DEFAULT_CONVERGENCE_THRESHOLD;
    /**
     * The object that will be subject to the drag operations. This object should implement {@link Movable} or {@link
     * Movable2}.
     */
    protected final Object dragObject;
    /**
     * The altitude mode of the object to be dragged.
     */
    protected int altitudeMode;

    /**
     * Provides persistence of initial values of a drag operation to increase dragging precision and provide better
     * dragging behavior.
     *
     * @param dragObject   the object to be dragged.
     * @param altitudeMode the altitude mode.
     *
     * @throws IllegalArgumentException if the object is null.
     */
    public DraggableSupport(Object dragObject, int altitudeMode)
    {
        if (dragObject == null)
        {
            String msg = Logging.getMessage("nullValue.ObjectIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        if (altitudeMode != WorldWind.ABSOLUTE && altitudeMode != WorldWind.CLAMP_TO_GROUND &&
            altitudeMode != WorldWind.RELATIVE_TO_GROUND && altitudeMode != WorldWind.CONSTANT)
        {
            String msg = Logging.getMessage("generic.InvalidAltitudeMode", altitudeMode);
            Logging.logger().warning(msg);
        }

        this.dragObject = dragObject;
        this.altitudeMode = altitudeMode;
    }

    /**
     * Converts the screen position inputs to geographic movement information. Uses the information provided by the
     * {@link DragContext} object and attempts to move the object using the {@link Movable2} or {@link Movable}
     * interface. This method maintains a constant screen offset from the cursor to the reference point of the object
     * being dragged. It is suited for objects maintaining a constant screen size presentation like,
     * {@link gov.nasa.worldwindx.examples.symbology.TacticalSymbols} or
     * {@link gov.nasa.worldwind.render.PointPlacemark}.
     *
     * @param dragContext the current {@link DragContext} for this object.
     *
     * @throws IllegalArgumentException if the {@link DragContext} is null.
     */
    public void dragScreenSizeConstant(DragContext dragContext)
    {
        if (dragContext == null)
        {
            String msg = Logging.getMessage("nullValue.DragContextIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        Position referencePosition = this.getReferencePosition();
        if (referencePosition == null)
            return;

        if (dragContext.getDragState().equals(AVKey.DRAG_BEGIN))
        {
            this.initialScreenPointOffset = this.computeScreenOffsetFromReferencePosition(
                referencePosition,
                dragContext);
        }

        if (this.initialScreenPointOffset == null)
            return;

        double referenceAltitude = referencePosition.getAltitude();

        Vec4 currentPoint = new Vec4(
            dragContext.getPoint().getX(),
            dragContext.getPoint().getY()
        );

        // Apply the screen coordinate move to the current screen point
        Vec4 moveToScreenCoordinates = currentPoint.subtract3(this.initialScreenPointOffset);

        // Project the new screen point back through the globe to find a new reference position
        Line ray = dragContext.getView().computeRayFromScreenPoint(
            moveToScreenCoordinates.getX(),
            moveToScreenCoordinates.getY()
        );
        if (ray == null)
            return;

        Vec4 moveToGlobeCoordinates = this.computeGlobeIntersection(
            ray,
            referenceAltitude,
            true,
            dragContext.getGlobe(),
            dragContext.getSceneController());
        if (moveToGlobeCoordinates == null)
            return;

        Position moveTo = dragContext.getGlobe().computePositionFromPoint(moveToGlobeCoordinates);

        this.doMove(new Position(moveTo, referenceAltitude), dragContext.getGlobe());
    }

    /**
     * Converts the screen position inputs to geographic movement information. Uses the information provided by the
     * {@link DragContext} object and attempts to move the object using the {@link Movable2} or {@link Movable}
     * interface. This method maintains a constant geographic distance between the cursor and the reference point of the
     * object being dragged. It is suited for objects which maintain a constant model space or geographic size.
     *
     * @param dragContext the current {@link DragContext} for this object.
     *
     * @throws IllegalArgumentException if the {@link DragContext} is null.
     */
    public void dragGlobeSizeConstant(DragContext dragContext)
    {
        if (dragContext == null)
        {
            String msg = Logging.getMessage("nullValue.DragContextIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        Position referencePosition = this.getReferencePosition();
        if (referencePosition == null)
            return;

        if (dragContext.getDragState().equals(AVKey.DRAG_BEGIN))
        {
            this.initialEllipsoidalReferencePoint = dragContext.getGlobe()
                .computeEllipsoidalPointFromPosition(referencePosition);
            this.initialEllipsoidalScreenPoint = this.computeEllipsoidalPointFromScreen(
                dragContext,
                dragContext.getInitialPoint(),
                referencePosition.getAltitude(),
                false);
        }

        if (this.initialEllipsoidalReferencePoint == null || this.initialEllipsoidalScreenPoint == null)
            return;

        double referenceAltitude = referencePosition.getAltitude();

        Vec4 currentScreenPoint = new Vec4(
            dragContext.getPoint().getX(),
            dragContext.getPoint().getY()
        );
        Line ray = dragContext.getView()
            .computeRayFromScreenPoint(currentScreenPoint.getX(), currentScreenPoint.getY());
        if (ray == null)
            return;

        Vec4 currentPoint = this.computeGlobeIntersection(
            ray,
            referenceAltitude,
            false,
            dragContext.getGlobe(),
            dragContext.getSceneController());
        if (currentPoint == null)
            return;

        Position currentPosition = dragContext.getGlobe().computePositionFromPoint(currentPoint);
        if (currentPosition == null)
            return;

        Vec4 currentEllipsoidalPoint = dragContext.getGlobe().computeEllipsoidalPointFromPosition(currentPosition);
        if (currentEllipsoidalPoint == null)
            return;

        Vec4 rotationAxis = this.initialEllipsoidalScreenPoint.cross3(currentEllipsoidalPoint).normalize3();
        Angle rotationAngle = this.initialEllipsoidalScreenPoint.angleBetween3(currentEllipsoidalPoint);
        Matrix rotation = Matrix.fromAxisAngle(rotationAngle, rotationAxis);

        Vec4 dragObjectReferenceMoveToEllipsoidalPoint = this.initialEllipsoidalReferencePoint.transformBy3(rotation);
        Position moveToInterim = dragContext.getGlobe()
            .computePositionFromEllipsoidalPoint(dragObjectReferenceMoveToEllipsoidalPoint);
        if (moveToInterim == null)
            return;

        Position moveTo = new Position(moveToInterim, referenceAltitude);

        this.doMove(moveTo, dragContext.getGlobe());
    }

    /**
     * Returns the step limit used by the position solver method for objects using a screen size constant drag approach
     * and a {@link WorldWind#RELATIVE_TO_GROUND} altitude mode. If the solver exceeds this value it will utilize an
     * intersection with the ellipsoid at the specified altitude.
     *
     * @return the step limit.
     */
    public int getStepLimit()
    {
        return this.stepLimit;
    }

    /**
     * Sets the step limit to use for the position solver. The position solver is only used for screen size constant
     * objects with a {@link WorldWind#RELATIVE_TO_GROUND} altitude mode. The step limit is the maximum steps the solver
     * will attempt before using an intersection with the ellipsoid at the specified altitude. Set this value in
     * coordination with the convergence threshold.
     *
     * @param stepLimit the step limit to set for the solver method.
     */
    public void setStepLimit(int stepLimit)
    {
        this.stepLimit = stepLimit;
    }

    /**
     * Returns the convergence threshold used by the solver when an a screen size constant object using a
     * {@link WorldWind#RELATIVE_TO_GROUND} altitude mode needs to determine a position. When the solver finds an
     * altitude within the convergence threshold to the desired altitude, the solver will stop and return the cartesian
     * position.
     *
     * @return the convergence threshold.
     */
    public double getConvergenceThreshold()
    {
        return this.convergenceThreshold;
    }

    /**
     * Sets the convergence threshold for the screen size constant object using a {@link WorldWind#RELATIVE_TO_GROUND}
     * altitude mode. The solver will test each iterations solution altitude with the desired altitude and if it is
     * found to be within the convergence threshold, the cartesian solution will be returned. Set this value in
     * coordination with the step limit.
     *
     * @param convergenceThreshold the convergence threshold to use for the solver.
     */
    public void setConvergenceThreshold(double convergenceThreshold)
    {
        this.convergenceThreshold = convergenceThreshold;
    }

    /**
     * Returns the current altitude mode being used by the dragging calculations.
     *
     * @return the altitude mode.
     */
    public int getAltitudeMode()
    {
        return this.altitudeMode;
    }

    /**
     * Sets the altitude mode to be used during dragging calculations.
     *
     * @param altitudeMode the altitude mode to use for dragging calculations.
     */
    public void setAltitudeMode(int altitudeMode)
    {
        if (altitudeMode != WorldWind.ABSOLUTE && altitudeMode != WorldWind.CLAMP_TO_GROUND &&
            altitudeMode != WorldWind.RELATIVE_TO_GROUND && altitudeMode != WorldWind.CONSTANT)
        {
            String msg = Logging.getMessage("generic.InvalidAltitudeMode", altitudeMode);
            Logging.logger().warning(msg);
        }
        this.altitudeMode = altitudeMode;
    }

    /**
     * Determines the cartesian coordinate of a screen point given the altitude mode.
     *
     * @param dragContext         the current {@link DragContext} of the dragging event.
     * @param screenPoint         the {@link Point} of the screen to determine the position.
     * @param altitude            the altitude in meters.
     * @param utilizeSearchMethod if the altitude mode is {@link WorldWind#RELATIVE_TO_GROUND}, this determines if the
     *                            search method will be used to determine the position, please see the {@link
     *                            DraggableSupport#computeRelativePoint(Line, Globe, SceneController, double)} for more
     *                            information.
     *
     * @return the cartesian coordinates using an ellipsoidal globe, or null if a position could not be determined.
     */
    protected Vec4 computeEllipsoidalPointFromScreen(DragContext dragContext, Point screenPoint, double altitude,
        boolean utilizeSearchMethod)
    {
        Line ray = dragContext.getView().computeRayFromScreenPoint(screenPoint.getX(), screenPoint.getY());
        Vec4 globePoint = this.computeGlobeIntersection(
            ray,
            altitude,
            utilizeSearchMethod,
            dragContext.getGlobe(),
            dragContext.getSceneController());
        if (globePoint == null)
            return null;

        Position screenPosition = dragContext.getGlobe().computePositionFromPoint(globePoint);
        if (screenPosition == null)
            return null;

        return dragContext.getGlobe().computeEllipsoidalPointFromPosition(screenPosition);
    }

    /**
     * Determines the offset in screen coordinates from the previous screen point ({@link DragContext#getInitialPoint()}
     * and the objects {@link Movable#getReferencePosition()} or {@link Movable2#getReferencePosition()} methods. If the
     * object doesn't implement either of the interfaces, or there is an error determining the offset, this function
     * will return null.
     *
     * @param dragObjectReferencePosition the {@link Movable} or {@link Movable2} reference position {@link Position}.
     * @param dragContext                 the current {@link DragContext} of this drag event.
     *
     * @return a {@link Vec4} containing the x and y offsets in screen coordinates from the reference position and the
     * previous screen point.
     */
    protected Vec4 computeScreenOffsetFromReferencePosition(Position dragObjectReferencePosition,
        DragContext dragContext)
    {
        Vec4 dragObjectPoint;

        if (dragContext.getGlobe() instanceof Globe2D)
        {
            dragObjectPoint = dragContext.getGlobe().computePointFromPosition(
                new Position(dragObjectReferencePosition, 0.0));
        }
        else
        {
            // If the altitude mode is ABSOLUTE, or not recognized as a standard WorldWind altitude mode, use the
            // ABSOLUTE method as the default
            if (this.altitudeMode == WorldWind.ABSOLUTE ||
                (this.altitudeMode != WorldWind.RELATIVE_TO_GROUND && this.altitudeMode != WorldWind.CLAMP_TO_GROUND
                    && this.altitudeMode != WorldWind.CONSTANT))
            {
                dragObjectPoint = dragContext.getGlobe().computePointFromPosition(dragObjectReferencePosition);
            }
            else // Should be any one of the remaining WorldWind altitude modes: CLAMP, RELATIVE, CONSTANT
            {
                dragObjectPoint = dragContext.getSceneController().getTerrain()
                    .getSurfacePoint(dragObjectReferencePosition);
            }
        }

        if (dragObjectPoint == null)
            return null;

        Vec4 dragObjectScreenPoint = dragContext.getView().project(dragObjectPoint);
        if (dragObjectScreenPoint == null)
            return null;

        Vec4 screenPointOffset = new Vec4(
            dragContext.getInitialPoint().getX() - dragObjectScreenPoint.getX(),
            dragContext.getInitialPoint().getY() - (
                dragContext.getView().getViewport().getHeight()
                    - dragObjectScreenPoint.getY() - 1.0)
        );

        return screenPointOffset;
    }

    /**
     * Extracts the reference {@link Position} from objects implementing {@link Movable} or {@link Movable2}. For
     * objects implementing both, it utilizes the {@link Movable2} interface method. If the object does not implement
     * the interfaces, null is returned.
     *
     * @return the reference {@link Movable#getReferencePosition()} or {@link Movable2#getReferencePosition()}, or null
     * if the object didn't implement either interface.
     */
    protected Position getReferencePosition()
    {
        if (this.dragObject instanceof Movable2)
            return ((Movable2) this.dragObject).getReferencePosition();
        else if (this.dragObject instanceof Movable)
            return ((Movable) this.dragObject).getReferencePosition();

        return null;
    }

    /**
     * Executes the {@link Movable2#moveTo(Globe, Position)} or {@link Movable#moveTo(Position)} methods with the
     * provided data. If the object implements both interfaces, the {@link Movable2} is used. If the object doesn't
     * implement either, it is ignored.
     *
     * @param movePosition the {@link Position} to provide to the {@link Movable2#moveTo} method.
     * @param globe        the globe reference, may be null if the object only implements the {@link Movable}
     *                     interface.
     */
    protected void doMove(Position movePosition, Globe globe)
    {
        if (this.dragObject instanceof Movable2)
        {
            if (globe == null)
            {
                String msg = Logging.getMessage("nullValue.GlobeIsNull");
                Logging.logger().severe(msg);
                throw new IllegalArgumentException(msg);
            }
            ((Movable2) this.dragObject).moveTo(globe, movePosition);
        }
        else if (this.dragObject instanceof Movable)
            ((Movable) this.dragObject).moveTo(movePosition);
    }

    /**
     * Computes the intersection of the provided {@link Line} with the {@link Globe} while accounting for the altitude
     * mode. If a {@link Globe2D} is specified, then the intersection is calculated using the globe objects method.
     *
     * @param ray             the {@link Line} to calculate the intersection of the {@link Globe}.
     * @param altitude        the altitude mode for the intersection calculation.
     * @param useSearchMethod if the altitude mode is {@link WorldWind#RELATIVE_TO_GROUND}, this determines if the
     *                        search method will be used to determine the position, please see the {@link
     *                        DraggableSupport#computeRelativePoint(Line, Globe, SceneController, double)} for more
     *                        information.
     * @param globe           the {@link Globe} to intersect.
     * @param sceneController if an altitude mode other than {@link WorldWind#ABSOLUTE} is specified, the {@link
     *                        SceneController} which will provide terrain information.
     *
     * @return the cartesian coordinates of the intersection based on the {@link Globe}s coordinate system or null if
     * the intersection couldn't be calculated.
     */
    protected Vec4
    computeGlobeIntersection(Line ray, double altitude, boolean useSearchMethod, Globe globe,
        SceneController sceneController)
    {
        Intersection[] intersections;

        if (globe instanceof Globe2D)
        {
            // Utilize the globe intersection method for a Globe2D as it best describes the appearance and the
            // terrain intersection method returns null when crossing the dateline on a Globe2D
            intersections = globe.intersect(ray, 0.0);
        }
        else if (this.altitudeMode == WorldWind.ABSOLUTE)
        {
            // Accounts for the object being visually placed on the surface in a Globe2D Globe
            intersections = globe.intersect(ray, altitude);
        }
        else if (this.altitudeMode == WorldWind.CLAMP_TO_GROUND || this.altitudeMode == WorldWind.CONSTANT)
        {
            intersections = sceneController.getTerrain().intersect(ray);
        }
        else if (this.altitudeMode == WorldWind.RELATIVE_TO_GROUND)
        {
            // If an object is RELATIVE_TO_GROUND but has an altitude close to 0.0, use CLAMP_TO_GROUND method
            if (altitude < 1.0)
            {
                intersections = sceneController.getTerrain().intersect(ray);
            }
            else
            {
                // When an object maintains a constant screen size independent of globe orientation or eye location,
                // the dragger attempts to determine the position by testing different points of the ray for a
                // matching altitude above elevation. The method is only used in objects maintain a constant screen
                // size as the effects are less pronounced in globe constant features.
                if (useSearchMethod)
                {
                    Vec4 intersectionPoint = this.computeRelativePoint(ray, globe, sceneController, altitude);
                    // In the event the computeRelativePoint fails with the numeric approach it falls back to a
                    // ellipsoidal intersection. Need to check if the result of that calculation was also null,
                    // indicating the screen point doesn't intersect with the globe.
                    if (intersectionPoint != null)
                        intersections = new Intersection[] {new Intersection(intersectionPoint, false)};
                    else
                        intersections = null;
                }
                else
                {
                    intersections = globe.intersect(ray, altitude);
                }
            }
        }
        else
        {
            // If the altitude mode isn't recognized, the ABSOLUTE determination method is used as a fallback/default
            intersections = globe.intersect(ray, altitude);
        }

        if ((intersections != null) && (intersections.length > 0))
            return intersections[0].getIntersectionPoint();
        else
            return null;
    }

    /**
     * Attempts to find a position with the altitude specified for objects which are {@link
     * WorldWind#RELATIVE_TO_GROUND}. Using the provided {@link Line}, conducts a bisectional search along the {@link
     * Line} for a {@link Position} which is within the {@code convergenceThreshold} of the requested {@code altitude}
     * provided. The {@code stepLimit} limits the number of bisections the function will attempt. If the search does not
     * find a position within the {@code stepLimit} or within the {@code convergenceThreshold} it will provide an
     * intersection with the ellipsoid at the provided altitude.
     *
     * @param ray             the {@link Line} from the eye point and direction in globe coordinates.
     * @param globe           the current {@link Globe}.
     * @param sceneController the current {@link SceneController}.
     * @param altitude        the target altitude.
     *
     * @return a {@link Vec4} of the point in globe coordinates.
     */
    protected Vec4 computeRelativePoint(Line ray, Globe globe, SceneController sceneController, double altitude)
    {
        // Calculate the intersection of ray with the terrain
        Intersection[] intersections = sceneController.getTerrain().intersect(ray);
        if (intersections != null)
        {
            Vec4 eye = ray.getOrigin();
            Vec4 surface = intersections[0].getIntersectionPoint();
            double maxDifference = eye.getLength3() - surface.getLength3();

            // Account for extremely zoomed out instances
            if (maxDifference > (5 * altitude))
            {
                double mixAmount = (5 * altitude) / maxDifference;
                eye = Vec4.mix3(mixAmount, surface, eye);
                // maxDifference = eye.getLength3() - surface.getLength3();
            }

            // False position approximation range reduction method. In testing, using this method decreased the average
            // convergence steps by 25%-30% but resulted in greater incidents of search failure, especially in
            // dynamic terrain change regions (mountains). Without the initial approximation, an average number of
            // solver steps was approximately 15.
            // double mixPoint = altitude / maxDifference;
            // double mixLow = Math.max(0.0, mixPoint - 0.1);
            // double mixHigh = Math.min(1.0, mixPoint + 0.1);
            double mixPoint = 0.5;
            double mixLow = 0.0;
            double mixHigh = 1.0;

            // now use a bracket method to find the best spot after 20 steps unless the error threshold is reached
            double pointAlt;
            Vec4 intersectionPoint;

            for (int i = 0; i < this.stepLimit; i++)
            {

                intersectionPoint = Vec4.mix3(mixPoint, surface, eye);
                Position pointPos = globe.computePositionFromPoint(intersectionPoint);
                pointAlt = globe.getElevation(pointPos.getLatitude(), pointPos.getLongitude());
                pointAlt = pointPos.getElevation() - pointAlt;

                if (Math.abs(pointAlt - altitude) < this.convergenceThreshold)
                {
                    return intersectionPoint;
                }

                if (altitude < pointAlt)
                    mixHigh = mixPoint;
                else
                    mixLow = mixPoint;

                mixPoint = (mixHigh + mixLow) / 2.0;
            }
        }

        intersections = globe.intersect(ray, altitude);
        if (intersections != null && (intersections.length > 0))
            return intersections[0].getIntersectionPoint();

        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy