世界风Java版:类似Google Earth的缩放功能

9
我创建了一个NASA Worldwind的输入处理程序,并尝试复制Google Earth的缩放效果。我想让缩放朝着鼠标光标缩放,而不是屏幕中心(默认情况下)。我已经有了一些成果--但它没有一致地向鼠标光标下的纬度/经度缩放,似乎漂移太远。我希望在缩放过程中同样的纬度/经度在鼠标光标下保持不变。因此,例如,如果您将光标悬停在特定地标(如水域)上,则在滚动滚轮时它将保持在光标下方。我使用的代码基于这个: https://forum.worldwindcentral.com/forum/world-wind-java-forums/development-help/11977-zoom-at-mouse-cursor?p=104793#post104793 这是我的输入处理程序:
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());
    }
}

要启用,请在worldwind.xml中设置此属性,指向该类:

<Property name="gov.nasa.worldwind.avkey.ViewInputHandlerClassName"
        value="gov.nasa.worldwindx.examples.ZoomToCursorViewInputHandler"/>

尝试使用delta = delta.multiply3(zoomChange - 1); 替代delta = delta.multiply3(-zoomChange); - vsoni
我尝试过了,但好像没有起作用。这是唯一的更改吗? - mainstringargs
所以,当我放大时,zoomChange的值似乎始终为“-0.1”。我在上面那行代码后面添加了一个println来查看这个值。 - mainstringargs
1
让我们在聊天中继续这个讨论 - vsoni
1
如果向量数学为delta = cursorVector - centerVector,当计算新的delta时,应该找到新的centerVector是centerVector = cursorVector - delta吗?这将保持cursorVector不变。如果我理解正确,使centerVector = centerVector + delta会将中心移向光标位置,并根据delta的大小超调或欠调。 - Michael McKay
显示剩余6条评论
1个回答

4

经过一番思考,我认为这个问题没有闭合形式的解决方案。你必须考虑到太多的因素:地球的形状,当你移动中心时“眼睛”是如何移动的。因此,我认为你可以做的最好的技巧是“跟随”主要的“缩放”动画,并在每个动画步骤之后进行小的调整。由于动画步骤很小,计算误差也应该更小,并且它们应该累积得更少,因为在下一步中,你将考虑所有先前的错误。因此,我的代码思路大致如下:创建一个FixZoomPositionAnimator类作为

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);
    }

}

更新

FixZoomPositionAnimator已更新,考虑到在大比例尺下不能假定经纬线平行于X和Y。为了解决这个问题,在中心周围使用参考点来计算调整。这意味着如果地球仪的大小小于约20像素(2 *stepPx),或者用户倾斜地球使得纬度/经度与X/Y明显不平行,则代码将无法工作。

更新 #2

我将之前的逻辑移动到了nextOld中,并添加了nextWithTilt。新函数应该可以在世界倾斜时工作,但由于基本逻辑现在更加复杂,因此没有加速度,我认为它对于更典型的情况有点更劣。此外,nextWithTilt内部仍然有很多日志记录,因为我不确定它是否真的可以正常工作。请自行承担风险。

更新 #2 结束

然后你可以这样使用它:

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 

}

很棒的答案——这非常接近我想要的。我遇到了一些情况,我会得到一大堆异常,而且地球仪会跳到一个随机位置。有什么想法吗?这些异常的形式是: - mainstringargs
@main(字符串args),你能说一下你从哪个视角缩放并从什么眼睛点来重现它吗?我最初也遇到了类似的异常,直到我引入了分支if(Math.abs(dCX)<= 1.0)和类似的分支dCY。可能应该更加严格。您可以添加各种事物的日志记录dXdYdCXdCYmouseControlPointmouseGeoLocationorbitView.getCenterPosition,以提供有关出错原因的更多详细信息。 - SergGr
1
@main(字符串[] args),我已经更新了FixZoomPositionAnimator,如果您尝试缩放到地球边缘附近的点,则可以更好地工作。请看一下。 - SergGr
1
@main(字符串[] args),我相信我的整个FixZoomPositionAnimator都依赖于默认情况下启用smoothingzoom,因此OrbitViewMoveToZoomAnimator会使用(默认)0.9乘法器分步缩放,所以我也可以对许多步骤进行“跟踪”技巧,并最终追上它。 - SergGr
1
@main(字符串[] args),我添加了一些“nextWithTilt”方法,乍一看似乎也适用于倾斜的世界。请参见我的答案中的更新#2 - SergGr
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接