/*
Copyright (C) 2001, 2006 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.awt;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.geom.*;

import javax.swing.event.*;
import java.awt.event.*;
import java.util.*;
import java.util.logging.Level;

/**
 * @author tag
 * @version $Id: AWTInputHandler.java 1820 2007-05-10 16:05:29Z dcollins $
 */
@SuppressWarnings({"UnnecessaryReturnStatement"})
public class AWTInputHandler extends AVListImpl
    implements KeyListener, MouseListener, MouseMotionListener, MouseWheelListener, InputHandler
{
    private WorldWindow worldWindow = null;
    private final EventListenerList eventListeners = new EventListenerList();
    private Position previousPickPosition = null;
    private PickedObjectList lastPickedObjects = null;
    private PickedObjectList hoverObjects;
    private boolean isHovering = false;
    private javax.swing.Timer hoverTimer = new javax.swing.Timer(600, new ActionListener()
    {
        public void actionPerformed(ActionEvent actionEvent)
        {
            if (AWTInputHandler.this.hoverMatches())
            {
                AWTInputHandler.this.isHovering = true;
                AWTInputHandler.this.callSelectListeners(new SelectEvent(AWTInputHandler.this.worldWindow,
                    SelectEvent.HOVER, null, AWTInputHandler.this.hoverObjects));
                AWTInputHandler.this.hoverTimer.stop();
            }
        }
    });
    // Current AWT mouse state.
    private java.awt.Point lastMousePoint = new java.awt.Point();
    //    private volatile int mouseClickButton;
    //    private volatile int mouseClickCount;
    //    private final int mouseClickCoalesceTime = 350;
    // Repeating AWT key timer. Used to simulate keyboard polling.
    private final Integer[] POLLED_KEYS =
        {
            KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT,
            KeyEvent.VK_UP, KeyEvent.VK_DOWN, KeyEvent.VK_PAGE_UP,
            KeyEvent.VK_PAGE_DOWN, KeyEvent.VK_ADD, KeyEvent.VK_EQUALS,
            KeyEvent.VK_SUBTRACT, KeyEvent.VK_MINUS
        };
    private KeyPollTimer keyPollTimer = new KeyPollTimer(25, Arrays.asList(POLLED_KEYS),
        new ActionListener()
        {
            public void actionPerformed(ActionEvent actionEvent)
            {
                if (actionEvent == null)
                    return;
                Object source = actionEvent.getSource();
                if (source != null && source instanceof Integer)
                    AWTInputHandler.this.keysPolled((Integer) source, actionEvent.getModifiers());
            }
        });
    // Value interpolation timer. Smooths responses to certain input events.
    private InterpolatorTimer interpolatorTimer = new InterpolatorTimer(15);
    private java.beans.PropertyChangeListener viewPropertyListener = null;
    private InterpolatorTimer.ViewProperties viewTarget = null;
    private boolean smoothViewChange = true;
    // View LatLon properties.
    private final double viewLatLonMinChangeFactor = 0.00001;
    private final double viewLatLonMaxChangeFactor = 0.2;
    private final double viewLatLonErrorThresold = 0.000000001;
    private final double viewLatLonStepCoefficient = 0.6;
    private final double viewLatLonCenterStepCoefficient = 0.1;
    // View heading and pitch (angle) properties.
    private final double viewAngleChangeFactor = 0.25;
    private final double viewAngleErrorThreshold = 0.0001;
    private final double viewAngleStepCoefficient = 0.3;
    private final double viewAngleResetStepCoefficient = 0.1;
    // View zoom properties.
    private final double viewZoomChangeFactor = 0.05;
    private final double viewZoomErrorThreshold = 0.001;
    private final double viewZoomStepCoefficient = 0.1;

    public void setEventSource(WorldWindow newWorldWindow)
    {
        if (newWorldWindow != null && !(newWorldWindow instanceof java.awt.Component))
        {
            String message = WorldWind.retrieveErrMsg("awt.AWTInputHandler.EventSourceNotAComponent");
            WorldWind.logger().log(Level.FINER, message);
            throw new IllegalArgumentException(message);
        }

        if (newWorldWindow == this.worldWindow)
            return;

        if (this.worldWindow != null)
        {
            java.awt.Component c = (java.awt.Component) this.worldWindow;
            c.removeKeyListener(this);
            c.removeMouseMotionListener(this);
            c.removeMouseListener(this);
            c.removeMouseWheelListener(this);

            if (this.keyPollTimer != null)
                c.removeKeyListener(this.keyPollTimer);
        }

        this.worldWindow = newWorldWindow;

        if (this.worldWindow == null)
            return;

        java.awt.Component c = (java.awt.Component) this.worldWindow;
        c.addKeyListener(this);
        c.addMouseMotionListener(this);
        c.addMouseListener(this);
        c.addMouseWheelListener(this);

        if (this.keyPollTimer != null)
            c.addKeyListener(this.keyPollTimer);
    }

    public WorldWindow getEventSource()
    {
        return this.worldWindow;
    }

    public void setHoverDelay(int delay)
    {
        this.hoverTimer.setDelay(delay);
    }

    public int getHoverDelay()
    {
        return this.hoverTimer.getDelay();
    }

    public void keyTyped(KeyEvent keyEvent)
    {
        if (this.worldWindow == null) // include this test to ensure any derived implementation performs it
            return;
    }

    public void keyPressed(KeyEvent keyEvent)
    {
        if (this.worldWindow == null) // include this test to ensure any derived implementation performs it
            return;
    }

    public void keyReleased(KeyEvent keyEvent)
    {
        if (this.worldWindow == null)
            return;

        if (keyEvent == null)
            return;

        View view = this.worldWindow.getView();
        if (view == null)
            return;

        int keyCode = keyEvent.getKeyCode();
        if (keyCode == KeyEvent.VK_SPACE)
        {
            this.interpolatorTimer.stop();
        }
        else if (keyCode == KeyEvent.VK_N)
        {
            InterpolatorTimer.ViewProperties newProps = new InterpolatorTimer.ViewProperties();
            newProps.heading = Angle.fromDegrees(0);
            this.setViewProperties(view, newProps, this.viewAngleResetStepCoefficient, this.viewAngleErrorThreshold,
                true);
        }
        else if (keyCode == KeyEvent.VK_R)
        {
            InterpolatorTimer.ViewProperties newProps = new InterpolatorTimer.ViewProperties();
            newProps.heading = Angle.fromDegrees(0);
            newProps.pitch = Angle.fromDegrees(0);
            this.setViewProperties(view, newProps, this.viewAngleResetStepCoefficient, this.viewAngleErrorThreshold,
                true);
        }
    }

    public void keysPolled(int keyCode, int modifiers)
    {
        if (this.worldWindow == null)
            return;

        View view = this.worldWindow.getView();
        if (view == null)
            return;

        int slowMask = (modifiers & InputEvent.ALT_DOWN_MASK);
        boolean slow = slowMask != 0x0;

        if (areModifiersExactly(modifiers, slowMask))
        {
            double sinHeading = view.getHeading().sin();
            double cosHeading = view.getHeading().cos();
            double latFactor = 0;
            double lonFactor = 0;
            if (keyCode == KeyEvent.VK_LEFT)
            {
                latFactor = sinHeading;
                lonFactor = -cosHeading;
            }
            else if (keyCode == KeyEvent.VK_RIGHT)
            {
                latFactor = -sinHeading;
                lonFactor = cosHeading;
            }
            else if (keyCode == KeyEvent.VK_UP)
            {
                latFactor = cosHeading;
                lonFactor = sinHeading;
            }
            else if (keyCode == KeyEvent.VK_DOWN)
            {
                latFactor = -cosHeading;
                lonFactor = -sinHeading;
            }
            if (latFactor != 0 || lonFactor != 0)
            {
                Globe globe = this.worldWindow.getModel().getGlobe();
                if (globe != null)
                {
                    LatLon latLonChange = this.computeViewLatLonChange(view, globe, 10 * latFactor, 10 * lonFactor,
                        slow);
                    this.setViewLatLon(view, this.computeNewViewLatLon(view, latLonChange.getLatitude(),
                        latLonChange.getLongitude()));
                    return;
                }
            }
        }

        double headingFactor = 0;
        double pitchFactor = 0;
        if (areModifiersExactly(modifiers, slowMask))
        {
            if (keyCode == KeyEvent.VK_PAGE_DOWN)
                pitchFactor = 1;
            else if (keyCode == KeyEvent.VK_PAGE_UP)
                pitchFactor = -1;
        }
        else if (areModifiersExactly(modifiers, InputEvent.SHIFT_DOWN_MASK | slowMask))
        {
            if (keyCode == KeyEvent.VK_LEFT)
                headingFactor = -1;
            else if (keyCode == KeyEvent.VK_RIGHT)
                headingFactor = 1;
            else if (keyCode == KeyEvent.VK_UP)
                pitchFactor = -1;
            else if (keyCode == KeyEvent.VK_DOWN)
                pitchFactor = 1;
        }
        if (headingFactor != 0)
        {
            this.setViewAngle(view, this.computeNewViewHeading(view,
                this.computeViewAngleChange(4 * headingFactor, slow)), null);
            return;
        }
        else if (pitchFactor != 0)
        {
            this.setViewAngle(view, null, this.computeNewViewPitch(view,
                this.computeViewAngleChange(4 * pitchFactor, slow)));
            return;
        }

        double zoomFactor = 0;
        if (areModifiersExactly(modifiers, slowMask))
        {
            if (keyCode == KeyEvent.VK_ADD ||
                keyCode == KeyEvent.VK_EQUALS)
                zoomFactor = -1;
            else if (keyCode == KeyEvent.VK_SUBTRACT ||
                keyCode == KeyEvent.VK_MINUS)
                zoomFactor = 1;
        }
        else if (areModifiersExactly(modifiers, InputEvent.CTRL_DOWN_MASK | slowMask)
            || areModifiersExactly(modifiers, InputEvent.META_DOWN_MASK | slowMask))
        {
            if (keyCode == KeyEvent.VK_UP)
                zoomFactor = -1;
            else if (keyCode == KeyEvent.VK_DOWN)
                zoomFactor = 1;
        }
        if (zoomFactor != 0)
        {
            this.setViewZoom(view, this.computeNewViewZoom(view, this.computeZoomViewChange(zoomFactor, slow)));
            return;
        }
    }

    public void mouseClicked(final MouseEvent mouseEvent)
    {
        if (this.worldWindow == null) // include this test to ensure any subsequent implementation performs it
            return;

        if (mouseEvent == null)
            return;

        View view = this.worldWindow.getView();
        if (view == null)
            return;

//        int button = mouseEvent.getButton();
//        if (this.mouseClickButton != button)
//            this.mouseClickCount = 1;
//        else
//            this.mouseClickCount = mouseEvent.getClickCount();
//        this.mouseClickButton = button;
//        if (this.mouseClickCount == 1)
//        {
//            javax.swing.Timer clickTimer = new javax.swing.Timer(this.mouseClickCoalesceTime, new ActionListener()
//            {
//                public void actionPerformed(ActionEvent evt)
//                {
//                    AWTInputHandler.this.mouseClickedCoalesced(mouseEvent, AWTInputHandler.this.mouseClickCount);
//                }
//            });
//            clickTimer.setRepeats(false);
//            clickTimer.start();
//        }

//        if (   (null != this.lastPickedObjects)
//            && (null != this.lastPickedObjects.getTopObject())
//            && this.lastPickedObjects.getTopObject().isTerrain())
//            return;

        if (this.lastPickedObjects != null && this.lastPickedObjects.size() > 0)
        {
            PickedObject top = this.lastPickedObjects.getTopObject();
            if (top != null && top.isTerrain())
            {
                InterpolatorTimer.ViewProperties newProps = new InterpolatorTimer.ViewProperties();
                Position position = top.getPosition();
                newProps.latLon = new LatLon(position.getLatitude(), position.getLongitude());
                this.setViewProperties(view, newProps, this.viewLatLonCenterStepCoefficient,
                    this.viewLatLonErrorThresold, true);
                return;
            }

            // Something is under the cursor, so it's deemed "selected".

            if (MouseEvent.BUTTON1 == mouseEvent.getButton())
            {
                if (mouseEvent.getClickCount() % 2 == 1)
                {
                    this.callSelectListeners(new SelectEvent(this.worldWindow, SelectEvent.LEFT_CLICK,
                        mouseEvent, this.lastPickedObjects));
                }
                else
                {
                    this.callSelectListeners(new SelectEvent(this.worldWindow, SelectEvent.LEFT_DOUBLE_CLICK,
                        mouseEvent, this.lastPickedObjects));
                }
            }
            else if (MouseEvent.BUTTON3 == mouseEvent.getButton())
            {
                this.callSelectListeners(new SelectEvent(this.worldWindow, SelectEvent.RIGHT_CLICK,
                    mouseEvent, this.lastPickedObjects));
            }

            view.firePropertyChange(AVKey.VIEW, null, view);
        }
    }

    public void mousePressed(MouseEvent mouseEvent)
    {
        if (this.worldWindow == null)
            return;

        this.cancelHover();
    }

    public void mouseReleased(MouseEvent mouseEvent)
    {
        if (this.worldWindow == null)
            return;

        this.doHover(true);
    }

    public void mouseEntered(MouseEvent mouseEvent)
    {
        if (this.worldWindow == null)
            return;

        this.cancelHover();
    }

    public void mouseExited(MouseEvent mouseEvent)
    {
        if (this.worldWindow == null)
            return;

        this.cancelHover();
    }

    public void mouseDragged(MouseEvent mouseEvent)
    {
        if (this.worldWindow == null)
            return;

        if (mouseEvent == null)
            return;

        View view = this.worldWindow.getView();
        if (view == null)
            return;

        if (this.worldWindow.getModel() == null)
            return;

        java.awt.Point mouseMove = new java.awt.Point(mouseEvent.getPoint().x - this.lastMousePoint.x,
            mouseEvent.getPoint().y - this.lastMousePoint.y);

        if (areModifiersExactly(mouseEvent, InputEvent.BUTTON1_DOWN_MASK))
        {
            LatLon latLonChange = null;

            Position prev = view.computePositionFromScreenPoint(this.lastMousePoint.x, this.lastMousePoint.y);
            Position cur = view.computePositionFromScreenPoint(mouseEvent.getPoint().x, mouseEvent.getPoint().y);
            if (prev != null && cur != null)
            {
                latLonChange = new LatLon(prev.getLatitude().subtract(cur.getLatitude()),
                    prev.getLongitude().subtract(cur.getLongitude()));
            }
            else
            {
                Globe globe = this.worldWindow.getModel().getGlobe();
                if (globe != null)
                {
                    double sinHeading = view.getHeading().sin();
                    double cosHeading = view.getHeading().cos();
                    double latFactor = cosHeading * mouseMove.y + sinHeading * mouseMove.x;
                    double lonFactor = sinHeading * mouseMove.y - cosHeading * mouseMove.x;
                    latLonChange = this.computeViewLatLonChange(view, globe, latFactor, lonFactor, false);
                }
            }

            if (latLonChange != null)
            {
                this.setViewLatLon(view, this.computeNewViewLatLon(view, latLonChange.getLatitude(),
                    latLonChange.getLongitude()));
            }
        }
        else if (areModifiersExactly(mouseEvent, InputEvent.BUTTON3_DOWN_MASK)
            || areModifiersExactly(mouseEvent, InputEvent.BUTTON1_DOWN_MASK | InputEvent.CTRL_DOWN_MASK))
        {
            double headingDirection = 1;
            Object source = mouseEvent.getSource();
            if (source != null && source instanceof java.awt.Component)
            {
                java.awt.Component component = (java.awt.Component) source;
                if (mouseEvent.getPoint().y < component.getHeight() / 2)
                    headingDirection = -1;
            }
            this.setViewAngle(view,
                this.computeNewViewHeading(view, this.computeViewAngleChange(headingDirection * mouseMove.x, false)),
                this.computeNewViewPitch(view, this.computeViewAngleChange(mouseMove.y, false)));
        }

        this.lastMousePoint = mouseEvent.getPoint();
    }

    public void mouseMoved(MouseEvent mouseEvent)
    {
        if (this.worldWindow == null)
            return;

        if (mouseEvent == null)
            return;

        this.lastMousePoint = mouseEvent.getPoint();

        View view = this.worldWindow.getView();
        if (view == null)
            return;

        // Forward event to mouseDragged() for OS X.
//        if (areModifiersExactly(mouseEvent, InputEvent.CTRL_DOWN_MASK))
//        {
//            this.mouseDragged(mouseEvent);
//        }

        Model model = this.worldWindow.getModel();
        if (model == null)
            return;

        Globe globe = model.getGlobe();
        if (globe == null)
            return;

        this.lastPickedObjects = this.worldWindow.pick(mouseEvent.getPoint());
        PickedObject top = null;
        if (this.lastPickedObjects != null && this.lastPickedObjects.size() > 0)
            top = this.lastPickedObjects.getTopObject();

        PickedObjectList selected = null;
        if (!(top == null || top.isTerrain())) // if not terrain
            selected = this.lastPickedObjects;

        this.callSelectListeners(new SelectEvent(this.worldWindow, SelectEvent.ROLLOVER, mouseEvent, selected));

        this.doHover(true);

        Position p = null;
        if (null != top && top.hasPosition())
            p = top.getPosition();
        else if (this.lastPickedObjects != null && this.lastPickedObjects.getTerrainObject() != null)
            p = this.lastPickedObjects.getTerrainObject().getPosition();

        this.callPositionListeners(new PositionEvent(this.worldWindow, mouseEvent, this.previousPickPosition, p));
        this.previousPickPosition = p;
    }

    public void mouseWheelMoved(MouseWheelEvent mouseWheelEvent)
    {
        if (this.worldWindow == null)
            return;

        if (mouseWheelEvent == null)
            return;

        View view = this.worldWindow.getView();
        if (view == null)
            return;

        int wheelRotation = mouseWheelEvent.getWheelRotation();
        double wheelDirection = Math.signum(wheelRotation);
        this.setViewZoom(view, this.computeNewViewZoom(view, this.computeZoomViewChange(wheelDirection, false)));
    }

    private static boolean areModifiersExactly(InputEvent inputEvent, int mask)
    {
        return areModifiersExactly(inputEvent.getModifiersEx(), mask);
    }

    private static boolean areModifiersExactly(int modifiersEx, int mask)
    {
        return modifiersEx == mask;
    }

    private boolean isPickListEmpty(PickedObjectList pickList)
    {
        return pickList == null || pickList.size() < 1;
    }

    private void doHover(boolean reset)
    {
        if (!(this.isPickListEmpty(this.hoverObjects) || this.isPickListEmpty(this.lastPickedObjects)))
        {
            PickedObject hover = this.hoverObjects.getTopObject();
            PickedObject last = this.lastPickedObjects.getTopObject();

            if (hover != null && last != null && hover.getObject().equals(last.getObject()))
            {
                return; // object picked is the hover object. don't do anything but wait for the timer to expire.
            }
        }

        this.cancelHover();

        if (!reset)
            return;

        if ((null != this.lastPickedObjects)
            && (null != this.lastPickedObjects.getTopObject())
            && this.lastPickedObjects.getTopObject().isTerrain())
            return;

        this.hoverObjects = this.lastPickedObjects;
        this.hoverTimer.restart();
    }

    private void cancelHover()
    {
        if (this.isHovering)
        {
            this.callSelectListeners(new SelectEvent(this.worldWindow, SelectEvent.HOVER, null, null));
        }

        this.isHovering = false;
        this.hoverObjects = null;
        this.hoverTimer.stop();
    }

    private boolean hoverMatches()
    {
        if (this.isPickListEmpty(this.lastPickedObjects) || this.isPickListEmpty(this.hoverObjects))
            return false;

        PickedObject lastTop = this.lastPickedObjects.getTopObject();

        if (null != lastTop && lastTop.isTerrain())
            return false;

        PickedObject newTop = this.hoverObjects.getTopObject();
        //noinspection SimplifiableIfStatement
        if (lastTop == null || newTop == null || lastTop.getObject() == null || newTop.getObject() == null)
            return false;

        return lastTop.getObject().equals(newTop.getObject());
    }

    public void addSelectListener(SelectListener listener)
    {
        this.eventListeners.add(SelectListener.class, listener);
    }

    public void removeSelectListener(SelectListener listener)
    {
        this.eventListeners.remove(SelectListener.class, listener);
    }

    public void addPositionListener(PositionListener listener)
    {
        this.eventListeners.add(PositionListener.class, listener);
    }

    public void removePositionListener(PositionListener listener)
    {
        this.eventListeners.remove(PositionListener.class, listener);
    }

    private void callSelectListeners(SelectEvent event)
    {
        for (SelectListener listener : this.eventListeners.getListeners(SelectListener.class))
        {
            listener.selected(event);
        }
    }

    private void callPositionListeners(PositionEvent event)
    {
        for (PositionListener listener : this.eventListeners.getListeners(PositionListener.class))
        {
            listener.moved(event);
        }
    }

    private void setViewProperties(final View view, InterpolatorTimer.ViewProperties newProperties,
        double stepCoefficient, double errorThreshold, boolean forceSmooth)
    {
        if (view == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.ViewIsNull");
            WorldWind.logger().log(Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (newProperties == null)
        {
            String message = WorldWind.retrieveErrMsg("awt.InterpolatorTimer.ViewPropertiesIsNull");
            WorldWind.logger().log(Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (forceSmooth || this.smoothViewChange)
        {
            if (this.viewPropertyListener == null)
            {
                this.viewPropertyListener = new java.beans.PropertyChangeListener()
                {
                    public void propertyChange(java.beans.PropertyChangeEvent evt)
                    {
                        Object newValue = evt.getNewValue();
                        if (newValue == null)
                        {
                            AWTInputHandler.this.viewTarget = null;
                        }
                        else if (newValue instanceof InterpolatorTimer.ViewProperties)
                        {
                            InterpolatorTimer.ViewProperties viewProps = (InterpolatorTimer.ViewProperties) newValue;
                            if (viewProps.latLon != null)
                                view.goToLatLon(viewProps.latLon);
                            if (viewProps.heading != null)
                                view.setHeading(viewProps.heading);
                            if (viewProps.pitch != null)
                                view.setPitch(viewProps.pitch);
                            if (viewProps.zoom != null)
                                view.setZoom(viewProps.zoom);
                            view.firePropertyChange(AVKey.VIEW, null, view);
                        }
                    }
                };
            }
            InterpolatorTimer.ViewProperties begin = new InterpolatorTimer.ViewProperties();
            if (newProperties.latLon != null)
                begin.latLon = new LatLon(view.getPosition().getLatitude(), view.getPosition().getLongitude());
            if (newProperties.heading != null)
                begin.heading = view.getHeading();
            if (newProperties.pitch != null)
                begin.pitch = view.getPitch();
            if (newProperties.zoom != null)
                begin.zoom = view.getZoom();
            this.viewTarget = newProperties;
            this.interpolatorTimer.start(stepCoefficient, errorThreshold, begin, newProperties,
                this.viewPropertyListener);
        }
        else
        {
            this.viewTarget = null;
            this.interpolatorTimer.stop();
            if (newProperties.latLon != null)
                view.goToLatLon(newProperties.latLon);
            if (newProperties.heading != null)
                view.setHeading(newProperties.heading);
            if (newProperties.pitch != null)
                view.setPitch(newProperties.pitch);
            if (newProperties.zoom != null)
                view.setZoom(newProperties.zoom);
            view.firePropertyChange(AVKey.VIEW, null, view);
        }
    }

    private void setViewLatLon(final View view, LatLon newLatLon)
    {
        if (newLatLon == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LatLonIsNull");
            WorldWind.logger().log(Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.viewTarget = new InterpolatorTimer.ViewProperties();
        this.viewTarget.latLon = newLatLon;
        this.setViewProperties(view, this.viewTarget, this.viewLatLonStepCoefficient, this.viewLatLonErrorThresold,
            false);
    }

    private LatLon computeNewViewLatLon(View view, Angle latChange, Angle lonChange)
    {
        double latDegrees;
        double lonDegrees;
        if (this.viewTarget != null && this.viewTarget.latLon != null)
        {
            latDegrees = this.viewTarget.latLon.getLatitude().getDegrees();
            lonDegrees = this.viewTarget.latLon.getLongitude().getDegrees();
        }
        else
        {
            latDegrees = view.getPosition().getLatitude().getDegrees();
            lonDegrees = view.getPosition().getLongitude().getDegrees();
        }
        latDegrees = latDegrees + latChange.getDegrees();
        lonDegrees = lonDegrees + lonChange.getDegrees();
        if (latDegrees < -90)
            latDegrees = -90;
        else if (latDegrees > 90)
            latDegrees = 90;
        if (lonDegrees < -180)
            lonDegrees = lonDegrees + 360;
        else if (lonDegrees > 180)
            lonDegrees = lonDegrees - 360;
        return LatLon.fromDegrees(latDegrees, lonDegrees);
    }

    private LatLon computeViewLatLonChange(View view, Globe globe, double latFactor, double lonFactor, boolean slow)
    {
        Point eye = view.getEyePoint();
        if (eye == null)
            return null;

        double normAlt = clamp((eye.length() / globe.getMaximumRadius()) - 1, 0, 1);
        double factor = ((1 - normAlt) * this.viewLatLonMinChangeFactor + normAlt * this.viewLatLonMaxChangeFactor)
            * (slow ? 2.5e-1 : 1);
        return LatLon.fromDegrees(latFactor * factor, lonFactor * factor);
    }

    private void setViewAngle(final View view, Angle newHeading, Angle newPitch)
    {
        if (newHeading == null && newPitch == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
            WorldWind.logger().log(Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.viewTarget = new InterpolatorTimer.ViewProperties();
        this.viewTarget.heading = newHeading;
        this.viewTarget.pitch = newPitch;
        this.setViewProperties(view, this.viewTarget, this.viewAngleStepCoefficient, this.viewAngleErrorThreshold,
            false);
    }

    private Angle computeNewViewHeading(View view, Angle change)
    {
        double degrees;
        if (this.viewTarget != null && this.viewTarget.heading != null)
            degrees = this.viewTarget.heading.getDegrees();
        else
            degrees = view.getHeading().getDegrees();
        degrees = degrees + change.getDegrees();
        if (degrees < 0)
            degrees = degrees + 360;
        else if (degrees > 360)
            degrees = degrees - 360;
        return Angle.fromDegrees(degrees);
    }

    private Angle computeViewAngleChange(double factor, boolean slow)
    {
        return Angle.fromDegrees(factor * this.viewAngleChangeFactor * (slow ? 2.5e-1 : 1));
    }

    private Angle computeNewViewPitch(View view, Angle change)
    {
        double degrees;
        if (this.viewTarget != null && this.viewTarget.pitch != null)
            degrees = this.viewTarget.pitch.getDegrees();
        else
            degrees = view.getPitch().getDegrees();
        degrees = degrees + change.getDegrees();
        Angle[] constraints = view.getPitchConstraints();
        return Angle.fromDegrees(clamp(degrees, constraints[0].getDegrees(), constraints[1].getDegrees()));
    }

    private void setViewZoom(final View view, double newZoom)
    {
        this.viewTarget = new InterpolatorTimer.ViewProperties();
        this.viewTarget.zoom = newZoom;
        this.setViewProperties(view, this.viewTarget, this.viewZoomStepCoefficient, this.viewZoomErrorThreshold,
            false);
    }

    private double computeNewViewZoom(View view, double change)
    {
        double logZoom;
        if (this.viewTarget != null && this.viewTarget.zoom != null)
            logZoom = Math.log(this.viewTarget.zoom);
        else
            logZoom = Math.log(view.getZoom());
        logZoom = logZoom + change;
        double[] constraints = view.getZoomConstraints();
        return clamp(Math.exp(logZoom), constraints[0], constraints[1]);
    }

    private double computeZoomViewChange(double factor, boolean slow)
    {
        return factor * this.viewZoomChangeFactor * (slow ? 2.5e-1 : 1);
    }

    private static double clamp(double x, double min, double max)
    {
        return x < min ? min : (x > max ? max : x);
    }
}
