WorldWind Java Google Earth Like Zoom
Asked Answered
V

1

9

I've created an input handler for NASA Worldwind that I'm trying to replicate Google Earth like zooming with.

I'm trying to make zoom towards the mouse cursor, instead of the center of the screen (like it does by default).

I've got it somewhat working -- except it doesn't zoom towards the lat/long under the cursor consistently, it seems to drift too far. What I want to happen is that the same lat/long is held under the cursor during the duration of the zoom. So, for instance, if you are hovering the cursor over a particular landmark (like a body of water), it will stay under the cursor as the wheel is scrolled.

The code I'm using is based heavily on this: https://forum.worldwindcentral.com/forum/world-wind-java-forums/development-help/11977-zoom-at-mouse-cursor?p=104793#post104793

Here is my Input Handler:

import java.awt.event.MouseWheelEvent;

import gov.nasa.worldwind.awt.AbstractViewInputHandler;
import gov.nasa.worldwind.awt.ViewInputAttributes;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.view.orbit.BasicOrbitView;
import gov.nasa.worldwind.view.orbit.OrbitViewInputHandler;

public class ZoomToCursorViewInputHandler extends OrbitViewInputHandler {
    protected class ZoomActionHandler extends VertTransMouseWheelActionListener {
        @Override
        public boolean inputActionPerformed(AbstractViewInputHandler inputHandler, MouseWheelEvent mouseWheelEvent,
                ViewInputAttributes.ActionAttributes viewAction) {
            double zoomInput = mouseWheelEvent.getWheelRotation();
                Position position = getView().computePositionFromScreenPoint(mousePoint.x, mousePoint.y);


            // Zoom toward the cursor if we're zooming in. Move straight out when zooming
            // out.
            if (zoomInput < 0 && position != null)
                return this.zoomToPosition(position, zoomInput, viewAction);
            else
                return super.inputActionPerformed(inputHandler, mouseWheelEvent, viewAction);
        }

        protected boolean zoomToPosition(Position position, double zoomInput,
                ViewInputAttributes.ActionAttributes viewAction) {


            double zoomChange = zoomInput * getScaleValueZoom(viewAction);

            BasicOrbitView view = (BasicOrbitView) getView();
            System.out.println("================================");

            System.out.println("Center Position: \t\t"+view.getCenterPosition());
            System.out.println("Mouse is on Position: \t\t"+position);

            Vec4 centerVector = view.getCenterPoint();
            Vec4 cursorVector = view.getGlobe().computePointFromLocation(position);
            Vec4 delta = cursorVector.subtract3(centerVector);

            delta = delta.multiply3(-zoomChange);

            centerVector = centerVector.add3(delta);
            Position newPosition = view.getGlobe().computePositionFromPoint(centerVector);

            System.out.println("New Center Position is: \t"+newPosition);

            setCenterPosition(view, uiAnimControl, newPosition, viewAction);

            onVerticalTranslate(zoomChange, viewAction);


            return true;
        }
    }

    public ZoomToCursorViewInputHandler() {
        ViewInputAttributes.ActionAttributes actionAttrs = this.getAttributes()
                .getActionMap(ViewInputAttributes.DEVICE_MOUSE_WHEEL)
                .getActionAttributes(ViewInputAttributes.VIEW_VERTICAL_TRANSLATE);
        actionAttrs.setMouseActionListener(new ZoomActionHandler());
    }
}

To enable, set this property in the worldwind.xml to point to this class:

<Property name="gov.nasa.worldwind.avkey.ViewInputHandlerClassName"
        value="gov.nasa.worldwindx.examples.ZoomToCursorViewInputHandler"/>
Violone answered 6/12, 2017 at 16:53 Comment(11)
Try with delta = delta.multiply3(zoomChange - 1); instead of delta = delta.multiply3(-zoomChange);Pleopod
I tried that and it doesn't seem to work. Is that the only change to try?Violone
Try delta = delta.multiply3((zoomChange - 1)/zoomChange); Just one more thing before you try this, wanted to know what value value you get in zoomChange? Is it absolute or %? i.e. when I say zoom 150% ... then are you getting 1.5 or 150? If you are getting abs i.e. 1.5 then this new change should work. If you are getting 150 then you need to divide it by 100 to get abs value 1.5.Pleopod
So, it looks like I consistently get a value of "-0.1" for zoomChange when I zoom in. I added a println right after that line above to see the value.Violone
what value do you get when you zoom out ?Pleopod
0.1 -- looks like its 0.1 for zoom out and -0.1 for zoom in.Violone
If you are getting value of "+0.1" when you zoom out and "-0.1" when you zoom in. Then in that case try with delta = delta.multiply3(-zoomChange/(1-zoomChange));Pleopod
Let us continue this discussion in chat.Pleopod
If the vector math is delta = cursorVector - centerVector, when you compute the new delta, shouldn't finding the new centerVector be centerVector = cursorVector - delta? This will keep the cursorVector constant. If I understand it, making centerVector = centerVector + delta will shift the center towards the cursor position and either overshoot or undershoot depending on the size of the delta.Interferon
Thanks for the tips. I'm a little rusty at vector math which is making this more difficult. I agree with what you're saying -- but when I try this, it overshoots it more: Vec4 centerVector = view.getCenterPoint(); Vec4 cursorVector = view.getGlobe().computePointFromLocation(position); Vec4 delta = cursorVector.subtract3(centerVector); delta = delta.multiply3(zoomChange); centerVector = cursorVector.subtract3(delta); Position newPosition = view.getGlobe().computePositionFromPoint(centerVector);Violone
In plain english: You have a center point and a mouse point. Find the difference between the mouse point and the center point. Apply the zoom change (which is either -.1 (zoom in) or .1 (zoom out)) to that difference. Reapply the newly calculated change to the mouse point to find the new center point. Do I have something wrong here?Violone
A
4

After some thinking over this problem I believe there is no closed form analytical solution for it. You just have to take into account to many things: shape of the Earth, how the "eye" moves when you move the center. So the best trick I think you can do is to "follow" the main "zoom" animation and do small adjustments after each animation step. As animation steps are small, calculation errors should also be smaller and they should accumulate less because on next step you take into account all the previous errors. So my idea in the code is roughly following: create a FixZoomPositionAnimator class as

static class FixZoomPositionAnimator extends BasicAnimator
{
    static final String VIEW_ANIM_KEY = "FixZoomPositionAnimator";
    static final double EPS = 0.005;

    private final java.awt.Point mouseControlPoint;
    private final Position mouseGeoLocation;
    private final Vec4 mouseGeoPoint;
    private final BasicOrbitView orbitView;
    private final Animator zoomAnimator;

    private int lastDxSign = 0;
    private int lastDySign = 0;
    int stepNumber = 0;
    int stepsNoAdjustments = 0;


    FixZoomPositionAnimator(BasicOrbitView orbitView, Animator zoomAnimator, java.awt.Point mouseControlPoint, Position mouseGeoLocation)
    {
        this.orbitView = orbitView;
        this.zoomAnimator = zoomAnimator;
        this.mouseControlPoint = mouseControlPoint;
        this.mouseGeoLocation = mouseGeoLocation;
        mouseGeoPoint = orbitView.getGlobe().computePointFromLocation(mouseGeoLocation);
    }

    public Point getMouseControlPoint()
    {
        return mouseControlPoint;
    }

    public Position getMouseGeoLocation()
    {
        return mouseGeoLocation;
    }

    private static int sign(double d)
    {
        if (Math.abs(d) < EPS)
            return 0;
        else if (d > 0)
            return 1;
        else
            return -1;
    }

    double calcAccelerationK(double dSign, double lastDSign)
    {
        // as we are following zoom trying to catch up - accelerate adjustment
        // but slow down if we overshot the target last time
        if (!zoomAnimator.hasNext())
            return 1.0;
        else if (dSign != lastDSign)
            return 0.5;
        else
        {
            // reduce acceleration over time
            if (stepNumber < 10)
                return 5;
            else if (stepNumber < 20)
                return 3;
            else
                return 2;
        }
    }

    static boolean isBetween(double checkedValue, double target1, double target2)
    {
        return ((target1 < checkedValue) && (checkedValue < target2))
            || ((target1 > checkedValue) && (checkedValue > target2));
    }

    static boolean isValid(Position position)
    {
        return isBetween(position.longitude.degrees, -180, 180)
            && isBetween(position.latitude.degrees, -90, 90);
    }

    @Override
    public void next()
    {
        // super.next();   // do not call super to avoid NullPointerException!

        nextWithTilt(); // works OK on tilted Earth
        // nextOld();   // IMHO better looking but stops working is user tilts the Earth

    }

    private void nextOld()
    {
        stepNumber++;

        Vec4 curProjection = orbitView.project(mouseGeoPoint);
        Rectangle viewport = orbitView.getViewport();

        // for Y sign is inverted
        double dX = (mouseControlPoint.x - curProjection.x);
        double dY = (mouseControlPoint.y + curProjection.y - viewport.getHeight());

        if (Math.abs(dX) > EPS || Math.abs(dY) > EPS)
        {

            double dCX = (mouseControlPoint.x - viewport.getCenterX());
            double dCY = (mouseControlPoint.y + viewport.getCenterY() - viewport.getHeight());

            final double stepPx = 10;

            // As the Earth is curved and we are not guaranteed to have a frontal view on it
            // latitude an longitude lines are not really parallel to X or Y. But we assume that
            // locally they are parallel enough both around the mousePoint and around the center.
            // So we use reference points near center to calculate how we want to move the center.
            Vec4 controlPointRight = new Vec4(viewport.getCenterX() + stepPx, viewport.getCenterY());
            Vec4 geoPointRight = orbitView.unProject(controlPointRight);
            Position positionRight = (geoPointRight != null) ? orbitView.getGlobe().computePositionFromPoint(geoPointRight) : null;
            Vec4 controlPointUp = new Vec4(viewport.getCenterX(), viewport.getCenterY() - stepPx);
            Vec4 geoPointUp = orbitView.unProject(controlPointUp);
            Position positionUp = (geoPointUp != null) ? orbitView.getGlobe().computePositionFromPoint(geoPointUp) : null;

            Position centerPosition = orbitView.getCenterPosition();

            double newCenterLongDeg;
            if (Math.abs(dCX) <= 1.0) // same X => same longitude
            {
                newCenterLongDeg = mouseGeoLocation.longitude.degrees;
            }
            else if (positionRight == null)  // if controlPointRight is outside of the globe - don't try to fix this coordinate
            {
                newCenterLongDeg = centerPosition.longitude.degrees;
            }
            else
            {
                double scaleX = -dX / stepPx;
                // apply acceleration if possible
                int dXSign = sign(dX);
                double accScaleX = scaleX * calcAccelerationK(dXSign, lastDxSign);
                lastDxSign = dXSign;
                newCenterLongDeg = centerPosition.longitude.degrees * (1 - accScaleX) + positionRight.longitude.degrees * accScaleX;
                // if we overshot - use non-accelerated mode
                if (!isBetween(newCenterLongDeg, centerPosition.longitude.degrees, mouseGeoLocation.longitude.degrees)
                    || !isBetween(newCenterLongDeg, -180, 180))
                {
                    newCenterLongDeg = centerPosition.longitude.degrees * (1 - scaleX) + positionRight.longitude.degrees * scaleX;
                }
            }

            double newCenterLatDeg;
            if (Math.abs(dCY) <= 1.0) // same Y => same latitude
            {
                newCenterLatDeg = mouseGeoLocation.latitude.degrees;
            }
            else if (positionUp == null)  // if controlPointUp is outside of the globe - don't try to fix this coordinate
            {
                newCenterLatDeg = centerPosition.latitude.degrees;
            }
            else
            {
                double scaleY = -dY / stepPx;

                // apply acceleration if possible
                int dYSign = sign(dY);
                double accScaleY = scaleY * calcAccelerationK(dYSign, lastDySign);
                lastDySign = dYSign;
                newCenterLatDeg = centerPosition.latitude.degrees * (1 - accScaleY) + positionUp.latitude.degrees * accScaleY;
                // if we overshot - use non-accelerated mode
                if (!isBetween(newCenterLatDeg, centerPosition.latitude.degrees, mouseGeoLocation.latitude.degrees)
                    || !isBetween(newCenterLatDeg, -90, 90))
                {
                    newCenterLatDeg = centerPosition.latitude.degrees * (1 - scaleY) + positionUp.latitude.degrees * scaleY;
                }
            }
            Position newCenterPosition = Position.fromDegrees(newCenterLatDeg, newCenterLongDeg);
            orbitView.setCenterPosition(newCenterPosition);
        }

        if (!zoomAnimator.hasNext())
            stop();
    }

    private void nextWithTilt()
    {
        stepNumber++;

        if (!zoomAnimator.hasNext() || (stepsNoAdjustments > 20))
        {
            System.out.println("Stop after " + stepNumber);
            stop();
        }

        Vec4 curProjection = orbitView.project(mouseGeoPoint);
        Rectangle viewport = orbitView.getViewport();
        System.out.println("----------------------------------");
        System.out.println("Mouse: mouseControlPoint = " + mouseControlPoint + "\t location = " + mouseGeoLocation + "\t viewSize = " + viewport);
        System.out.println("Mouse: curProjection = " + curProjection);

        double dX = (mouseControlPoint.x - curProjection.x);
        double dY = (viewport.getHeight() - mouseControlPoint.y - curProjection.y);  // Y is inverted
        Vec4 dTgt = new Vec4(dX, dY);

        // sometimes if you zoom close to the edge curProjection is calculated as somewhere
        // way beyond where it is and it leads to overflow. This is a protection against it
        if (Math.abs(dX) > viewport.width / 4 || Math.abs(dY) > viewport.height / 4)
        {
            Vec4 unproject = orbitView.unProject(new Vec4(mouseControlPoint.x, viewport.getHeight() - mouseControlPoint.y));
            System.out.println("!!!End Mouse:"
                + " dX = " + dX + "\t" + " dY = " + dY
                + "\n" + "unprojectPt = " + unproject
                + "\n" + "unprojectPos = " + orbitView.getGlobe().computePositionFromPoint(unproject)
            );

            stepsNoAdjustments += 1;
            return;
        }

        if (Math.abs(dX) <= EPS && Math.abs(dY) <= EPS)
        {
            stepsNoAdjustments += 1;
            System.out.println("Mouse: No adjustment: " + " dX = " + dX + "\t" + " dY = " + dY);
            return;
        }
        else
        {
            stepsNoAdjustments = 0;
        }

        // create reference points about 10px away from the center to the Up and to the Right
        // and then map them to screen coordinates and geo coordinates
        // Unfortunately unproject often generates points far from the Earth surface (and
        // thus with significantly less difference in lat/long)
        // So this longer but more fool-proof calculation is used
        final double stepPx = 10;
        Position centerPosition = orbitView.getCenterPosition();
        Position eyePosition = orbitView.getEyePosition();
        double pixelGeoSize = orbitView.computePixelSizeAtDistance(eyePosition.elevation - centerPosition.elevation);
        Vec4 geoCenterPoint = orbitView.getCenterPoint();
        Vec4 geoRightPoint = geoCenterPoint.add3(new Vec4(pixelGeoSize * stepPx, 0, 0));
        Vec4 geoUpPoint = geoCenterPoint.add3(new Vec4(0, pixelGeoSize * stepPx, 0));

        Position geoRightPosition = orbitView.getGlobe().computePositionFromPoint(geoRightPoint);
        Position geoUpPosition = orbitView.getGlobe().computePositionFromPoint(geoUpPoint);

        Vec4 controlCenter = orbitView.project(geoCenterPoint);
        Vec4 controlRight = orbitView.project(geoRightPoint);
        Vec4 controlUp = orbitView.project(geoUpPoint);

        Vec4 controlRightDif = controlRight.subtract3(controlCenter);
        controlRightDif = new Vec4(controlRightDif.x, controlRightDif.y); // ignore z for scale calculation
        Vec4 controlUpDif = controlUp.subtract3(controlCenter);
        controlUpDif = new Vec4(controlUpDif.x, controlUpDif.y); // ignore z for scale calculation

        double scaleRight = -dTgt.dot3(controlRightDif) / controlRightDif.getLengthSquared3();
        double scaleUp = -dTgt.dot3(controlUpDif) / controlUpDif.getLengthSquared3();

        Position posRightDif = geoRightPosition.subtract(centerPosition);
        Position posUpDif = geoUpPosition.subtract(centerPosition);

        double totalLatDifDeg = posRightDif.latitude.degrees * scaleRight + posUpDif.latitude.degrees * scaleUp;
        double totalLongDifDeg = posRightDif.longitude.degrees * scaleRight + posUpDif.longitude.degrees * scaleUp;
        Position totalDif = Position.fromDegrees(totalLatDifDeg, totalLongDifDeg);

        // don't copy elevation!
        Position newCenterPosition = Position.fromDegrees(centerPosition.latitude.degrees + totalLatDifDeg,
            centerPosition.longitude.degrees + totalLongDifDeg);

        // if we overshot - try to slow down
        if (!isValid(newCenterPosition))
        {
            newCenterPosition = Position.fromDegrees(centerPosition.latitude.degrees + totalLatDifDeg / 2,
                centerPosition.longitude.degrees + totalLongDifDeg / 2);
            if (!isValid(newCenterPosition))
            {
                System.out.println("Too much overshot: " + newCenterPosition);
                stepsNoAdjustments += 1;
                return;
            }
        }

        System.out.println("Mouse:"
            + " dX = " + dX + "\t" + " dY = " + dY

            + "\n"
            + " centerPosition = " + centerPosition

            + "\n"
            + " geoUpPoint = " + geoUpPoint + "\t " + " geoUpPosition = " + geoUpPosition
            + "\n"
            + " geoRightPoint = " + geoRightPoint + "\t " + " geoRightPosition = " + geoRightPosition

            + "\n"
            + " posRightDif = " + posRightDif
            + "\t"
            + " posUpDif = " + posUpDif
            + "\n"
            + " scaleRight = " + scaleRight + "\t" + " scaleUp = " + scaleUp);
        System.out.println("Mouse: oldCenterPosition = " + centerPosition);
        System.out.println("Mouse: newCenterPosition = " + newCenterPosition);

        orbitView.setCenterPosition(newCenterPosition);
    }

}

Update

FixZoomPositionAnimator was updated to take into account the fact that one a large scale you can't assume that longitude and latitude lines go parallel to X and Y. To work this around reference points around the center are used to calculate adjustment. This means that the code will not work if the globe size is less than about 20px (2*stepPx) or if the user has tilted the Earth making latitude/longitude significantly non-parallel to X/Y.

End of Update

Update #2

I've moved previous logic to nextOld and added nextWithTilt. The new function should work even if the world is tilted but as the base logic is more complicated now, there is no acceleration anymore which IMHO makes it a bit worse for more typical cases. Also there are still a log of logging inside nextWithTilt because I'm not quite sure it really works OK. Use at your own risk.

End of Update #2

and then you may use it as

public class ZoomToCursorViewInputHandler extends OrbitViewInputHandler
{
    public ZoomToCursorViewInputHandler()
    {
        ViewInputAttributes.ActionAttributes actionAttrs = this.getAttributes()
            .getActionMap(ViewInputAttributes.DEVICE_MOUSE_WHEEL)
            .getActionAttributes(ViewInputAttributes.VIEW_VERTICAL_TRANSLATE);
        actionAttrs.setMouseActionListener(new ZoomActionHandler());
    }

    protected class ZoomActionHandler extends VertTransMouseWheelActionListener
    {
        @Override
        public boolean inputActionPerformed(AbstractViewInputHandler inputHandler, MouseWheelEvent mouseWheelEvent,
            final ViewInputAttributes.ActionAttributes viewAction)
        {
            double zoomInput = mouseWheelEvent.getWheelRotation();
            Position position = wwd.getCurrentPosition();
            Point mouseControlPoint = mouseWheelEvent.getPoint();

            // Zoom toward the cursor if we're zooming in. Move straight out when zooming
            // out.
            if (zoomInput < 0 && position != null)
            {
                boolean res = super.inputActionPerformed(inputHandler, mouseWheelEvent, viewAction);

                BasicOrbitView view = (BasicOrbitView) getView();
                OrbitViewMoveToZoomAnimator zoomAnimator = (OrbitViewMoveToZoomAnimator) uiAnimControl.get(VIEW_ANIM_ZOOM);

                // for continuous scroll preserve the original target if mouse was not moved
                FixZoomPositionAnimator old = (FixZoomPositionAnimator) uiAnimControl.get(FixZoomPositionAnimator.VIEW_ANIM_KEY);
                if (old != null && old.getMouseControlPoint().equals(mouseControlPoint))
                {
                    position = old.getMouseGeoLocation();
                }
                FixZoomPositionAnimator fixZoomPositionAnimator = new FixZoomPositionAnimator(view, zoomAnimator, mouseControlPoint, position);
                fixZoomPositionAnimator.start();
                uiAnimControl.put(FixZoomPositionAnimator.VIEW_ANIM_KEY, fixZoomPositionAnimator);
                return res;
            }
            else
            {

                uiAnimControl.remove(FixZoomPositionAnimator.VIEW_ANIM_KEY); // when zoom direction changes we don't want to make position adjustments anymore
                return super.inputActionPerformed(inputHandler, mouseWheelEvent, viewAction);
            }
        }
    }

    // here goes aforementioned FixZoomPositionAnimator 

}
Accipitrine answered 16/12, 2017 at 1:39 Comment(13)
Great answer -- this is very close to what I'm looking for. I have gotten into some cases where I get a flood of exceptions and the globe jumps to a random position. Any ideas? Exceptions are of this form:Violone
gov.nasa.worldwind.WorldWindowGLAutoDrawable display SEVERE: Exception while attempting to repaint WorldWindow java.lang.IllegalArgumentException: Latitude out of range 137.00189790206832° at gov.nasa.worldwind.view.orbit.BasicOrbitView.setCenterPosition(BasicOrbitView.java:132) at gov.nasa.worldwindx.examples.ZoomToCursorViewInputHandler$FixZoomPositionAnimator.next(ZoomToCursorViewInputHandler.java:157) at gov.nasa.worldwind.animation.AnimationController.stepAnimators(AnimationController.java:78)Violone
@mainstringargs, could you say where do you zoom and from what eye point to reproduce it? I had similar exceptions originally until I introduced branch if (Math.abs(dCX) <= 1.0) and similar for dCY. Probably it should be a bit stricter. You could add logging of various things dX, dY, dCX, dCY, mouseControlPoint, mouseGeoLocation, and orbitView.getCenterPosition to provide more details on what's going wrong.Accipitrine
So my guess is that you point mouse close to the center of the window either by X or by Y and somehow geo location you originally pointed to during moves during zooming far from the center so scaleX or scaleY gets big and overflow the range of valid longitude or latitudeAccipitrine
@mainstringargs, it looks interesting. Could you come back to the chat you had with vsoni and answer some questions?Accipitrine
@mainstringargs, I've updated FixZoomPositionAnimator to work better if you try to zoom to the point near the edge of the globe. Please take a look.Accipitrine
The update looks pretty good, not seeing that exception anymore. One other question -- have you looked at this: nasaworldwind.github.io/WorldWindJava/gov/nasa/worldwind/awt/… or this nasaworldwind.github.io/WorldWindJava/gov/nasa/worldwind/awt/… ? Wondering if these could be used to control the "jitter"Violone
@mainstringargs, I believe that my whole FixZoomPositionAnimator relies on the fact that smoothing is enabled by default for zoom so OrbitViewMoveToZoomAnimator does zoom in many steps with (by default) 0.9 multiply so I can do my "following" trick over many steps as well and eventually catch up with it.Accipitrine
Found another case where exceptions can occur -- if the globe is tilted. (By holding right click and dragging the mouse). Any thoughts on that? Think its similar to the other problems previously?Violone
@mainstringargs, this is what I explicitly mentioned in my update: "This means that the code will not work if the globe size is less than about 20px (2*stepPx) or if the user has tilted the Earth making latitude/longitude significantly non-parallel to X/Y." This can be fixed but it requires less trivial math using something like Bilinear interpolation over parallelogram. I didn't bother with that. Do you really need tilt to work?Accipitrine
Ah. Got it, missed that. No problem. I realize that is more complex.Violone
@mainstringargs, I've added some nextWithTilt method that at the first glance seems to work with tilted world as well. See also my Update #2 in the answer.Accipitrine
That is awesome! Thank youViolone

© 2022 - 2024 — McMap. All rights reserved.