opensciencemap / vtm

a vector-tile map library written in java - running on android, desktop and within the browser
GNU Lesser General Public License v3.0
238 stars 176 forks source link

Feature: callback / listener for 'confirmed' map position change #37

Open stleusc opened 10 years ago

stleusc commented 10 years ago

I want to be able to register a listener to determine that the map position has changed. I already can do that using bind(...), but it fires while panning the map around. What do I need to know is a 'confirmed' change. What do I mean with that?

So far I solved it by tweaking MapAnitmator.java to be able to register a listener and combining this with the bind(...)

Ideal would be one callback for any confirmed map position change and being able to register for pan, rotate, tilt separately. In case of pan/rotate event both should be fired.

/*
 * Copyright 2013 Hannes Janetzek
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.map;

import java.util.Vector;

import org.oscim.core.BoundingBox;
import org.oscim.core.GeoPoint;
import org.oscim.core.MapPosition;
import org.oscim.core.MercatorProjection;
import org.oscim.core.Point;
import org.oscim.core.Tile;
import org.oscim.renderer.MapRenderer;
import org.oscim.utils.FastMath;

// TODO: rewrite

public class MapAnimator {

    public interface MapAnimatorListener {
        void animationStarted(int state);
        void animationEnded();
    }

    Vector<MapAnimatorListener> mListeners = new Vector<MapAnimator.MapAnimatorListener>();

    public void addMapAnimatorListener(MapAnimatorListener listener) {
        mListeners.add(listener);
    }

    public void removeMapAnimatorListener(MapAnimatorListener listener) {
        mListeners.remove(listener);
    }

    private void fireAnimationStarted() {
        for (MapAnimatorListener listener : mListeners)
            listener.animationStarted(mState);
    }

    private void fireAnimationEnded() {
        for (MapAnimatorListener listener : mListeners)
            listener.animationEnded();
    }

    //static final Logger log = LoggerFactory.getLogger(MapAnimator.class);

    public MapAnimator(Map map, Viewport viewport) {
        mViewport = viewport;
        mMap = map;
    }

    private final int ANIM_NONE = 0;
    private final int ANIM_MOVE = 1 << 0;
    private final int ANIM_SCALE = 1 << 1;
    private final int ANIM_FLING = 1 << 2;
    private final int ANIM_BBOX = 1 << 3;

    private final Map mMap;
    private final Viewport mViewport;

    private final MapPosition mPos = new MapPosition();
    private final MapPosition mStartPos = new MapPosition();
    private final MapPosition mDeltaPos = new MapPosition();

    private final Point mScroll = new Point();
    private final Point mPivot = new Point();
    private final Point mVelocity = new Point();

    private double mScaleBy;

    private float mDuration = 500;
    private long mAnimEnd = -1;

    private int mState = ANIM_NONE;

    public synchronized void animateTo(BoundingBox bbox) {
        // TODO for large distatance first scale out, then in

        // calculate the maximum scale at which the bbox is completely visible
        double dx = Math.abs(MercatorProjection.longitudeToX(bbox.getMaxLongitude())
                - MercatorProjection.longitudeToX(bbox.getMinLongitude()));

        double dy = Math.abs(MercatorProjection.latitudeToY(bbox.getMinLatitude())
                - MercatorProjection.latitudeToY(bbox.getMaxLatitude()));

        double zx = mMap.getWidth() / (dx * Tile.SIZE);
        double zy = mMap.getHeight() / (dy * Tile.SIZE);
        double newScale = Math.min(zx, zy);

        animateTo(500, bbox.getCenterPoint(), newScale, false);

        mState = ANIM_MOVE | ANIM_SCALE | ANIM_BBOX;
    }

    public synchronized void animateTo(long duration, GeoPoint geoPoint, double scale,
            boolean relative) {

        mViewport.getMapPosition(mPos);

        if (relative) {
            if (mAnimEnd > 0 && (mState & ANIM_SCALE) != 0)
                scale = mDeltaPos.scale * scale;
            else
                scale = mPos.scale * scale;
        }

        scale = FastMath.clamp(scale, Viewport.MIN_SCALE, Viewport.MAX_SCALE);
        mDeltaPos.scale = scale;

        mScaleBy = scale - mPos.scale;

        mStartPos.scale = mPos.scale;
        mStartPos.angle = mPos.angle;

        mStartPos.x = mPos.x;
        mStartPos.y = mPos.y;

        mDeltaPos.x = MercatorProjection.longitudeToX(geoPoint.getLongitude());
        mDeltaPos.y = MercatorProjection.latitudeToY(geoPoint.getLatitude());

        mDeltaPos.x -= mStartPos.x;
        mDeltaPos.y -= mStartPos.y;

        mState = ANIM_MOVE | ANIM_SCALE;

        animStart(duration);
    }

    public synchronized void animateZoom(long duration, double scale, float pivotX, float pivotY) {

        mViewport.getMapPosition(mPos);

        if (mAnimEnd > 0 && (mState & ANIM_SCALE) != 0)
            scale = mDeltaPos.scale * scale;
        else
            scale = mPos.scale * scale;

        scale = FastMath.clamp(scale, Viewport.MIN_SCALE, Viewport.MAX_SCALE);
        mDeltaPos.scale = scale;

        mScaleBy = scale - mPos.scale;

        mStartPos.scale = mPos.scale;
        mStartPos.angle = mPos.angle;

        mPivot.x = pivotX;
        mPivot.y = pivotY;

        mState = ANIM_SCALE;

        animStart(duration);
    }

    public synchronized void animateTo(GeoPoint geoPoint) {
        animateTo(300, geoPoint, 1, true);
    }

    public synchronized void animateFling(int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {

        if (velocityX * velocityX + velocityY * velocityY < 2048)
            return;

        mViewport.getMapPosition(mPos);

        mScroll.x = 0;
        mScroll.y = 0;

        float duration = 500;

        // pi times thumb..
        float flingFactor = (duration / 2500);
        mVelocity.x = velocityX * flingFactor;
        mVelocity.y = velocityY * flingFactor;
        FastMath.clamp(mVelocity.x, minX, maxX);
        FastMath.clamp(mVelocity.y, minY, maxY);

        mState = ANIM_FLING;

        animStart(duration);
    }

    private void animStart(float duration) {
        fireAnimationStarted();
        mDuration = duration;
        mAnimEnd = System.currentTimeMillis() + (long) duration;
        mMap.render();
    }

    private void animCancel() {
        mState = ANIM_NONE;
        mPivot.x = 0;
        mPivot.y = 0;
        fireAnimationEnded();
    }

    /**
     * called by MapRenderer at begin of each frame.
     */
    public synchronized void updateAnimation() {
        if (mState == ANIM_NONE)
            return;

        long millisLeft = mAnimEnd - MapRenderer.frametime;

        boolean changed = false;

        synchronized (mViewport) {

            // cancel animation when position was changed since last
            // update, i.e. when it was modified outside the animator.
            if (mViewport.getMapPosition(mPos)) {
                animCancel();
                return;
            }

            if (millisLeft <= 0) {
                // set final position
                if ((mState & ANIM_MOVE) != 0)
                    mViewport.moveTo(mStartPos.x + mDeltaPos.x,
                                     mStartPos.y + mDeltaPos.y);

                if ((mState & ANIM_SCALE) != 0) {
                    if (mScaleBy > 0)
                        doScale(mStartPos.scale + (mScaleBy - 1));
                    else
                        doScale(mStartPos.scale + mScaleBy);
                }

                mMap.updateMap(true);
                animCancel();
                return;
            }

            float adv = FastMath.clamp(1.0f - millisLeft / mDuration, 0, 1);

            if ((mState & ANIM_SCALE) != 0) {
                if (mScaleBy > 0)
                    doScale(mStartPos.scale + (mScaleBy * (Math.pow(2, adv) - 1)));
                else
                    doScale(mStartPos.scale + (mScaleBy * adv));

                changed = true;
            }

            if ((mState & ANIM_MOVE) != 0) {
                mViewport.moveTo(mStartPos.x + mDeltaPos.x * adv,
                                 mStartPos.y + mDeltaPos.y * adv);
                changed = true;
            }

            if ((mState & ANIM_BBOX) != 0) {
                if (mPos.angle > 180)
                    mPos.angle -= 360;
                mViewport.setRotation(mPos.angle * (1 - adv));
                mViewport.setTilt(mPos.tilt * (1 - adv));
            }

            if ((mState & ANIM_FLING) != 0) {
                adv = (float) Math.sqrt(adv);
                double dx = mVelocity.x * adv;
                double dy = mVelocity.y * adv;
                if ((dx - mScroll.x) != 0 || (dy - mScroll.y) != 0) {

                    mViewport.moveMap((float) (dx - mScroll.x),
                                      (float) (dy - mScroll.y));
                    mScroll.x = dx;
                    mScroll.y = dy;

                    changed = true;
                }
            }
            // remember current map position
            mViewport.getMapPosition(mPos);

        }

        // continue animation
        if (changed) {
            // render and inform layers that position has changed
            mMap.updateMap(true);
        } else {
            // just render next frame
            mMap.render();
        }

    }

    private void doScale(double newScale) {
        mViewport.scaleMap((float) (newScale / mPos.scale),
                           (float) mPivot.x, (float) mPivot.y);
    }

    public int getRunningAnimation() {
        return mState;
    }

    public boolean isAnimating() {
        return mState == ANIM_NONE;
    }

    public synchronized void cancel() {
        mState = ANIM_NONE;
        fireAnimationEnded();
    }
}
hjanetzek commented 10 years ago

This addition sounds reasonable to me. We are currently discussing how the general event mechanism should be done though. Falko is working on it. I'll get back to it on Monday.

In animCancel it should be

mMap.post(new Runnable{public void run(){ fireAnimationEnded();}} 

to execute it on the main-thread since updateAnimation is called from gl-thread

stleusc commented 10 years ago

What would be perfect is to be able to get:

either as 6 different listeners or as 2 listeners which option to specify type of event (pan, rotate, tilt)

stleusc commented 10 years ago

Any update for this topic?

hjanetzek commented 10 years ago

no, not yet. the decision on how event-dispatcher should be done is still pending

FalkoSchmid commented 10 years ago

we are actually working on it, however will take some more days.

stleusc commented 10 years ago

Any update/progress here? Happy to test if there is anything.

stleusc commented 10 years ago

You don't happen to have an update here, do you?

stleusc commented 10 years ago

@FalkoSchmid any update on this topic?