/* * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2012 Hannes Janetzek * * 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 . */ package org.oscim.view; import java.lang.ref.WeakReference; import org.oscim.core.BoundingBox; import org.oscim.core.GeoPoint; import org.oscim.core.MapPosition; import org.oscim.core.MercatorProjection; import org.oscim.core.Tile; import org.oscim.utils.FastMath; import org.oscim.utils.GeometryUtils.Point2D; import org.oscim.utils.GlUtils; import android.graphics.Point; import android.opengl.Matrix; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.Log; /** * A MapPosition stores the latitude and longitude coordinate of a MapView * together with its zoom level, rotation and tilt */ public class MapViewPosition { //private static final String TAG = MapViewPosition.class.getName(); public final static int MAX_ZOOMLEVEL = 17; public final static int MIN_ZOOMLEVEL = 2; private final static float MAX_ANGLE = 65; private final MapView mMapView; private double mLatitude; private double mLongitude; private byte mZoomLevel; // 1.0 - 2.0 scale per level private float mScale; // 2^mZoomLevel * mScale; private float mMapScale; private float mRotation; public float mTilt; // private static final int REF_ZOOM = 20; private double mPosX; private double mPosY; private final AnimationHandler mHandler; MapViewPosition(MapView mapView) { mMapView = mapView; mLatitude = Double.NaN; mLongitude = Double.NaN; mZoomLevel = -1; mScale = 1; mRotation = 0.0f; mTilt = 0; mMapScale = 1; mHandler = new AnimationHandler(this); } private final float[] mProjMatrix = new float[16]; private final float[] mProjMatrixI = new float[16]; private final float[] mUnprojMatrix = new float[16]; private final float[] mViewMatrix = new float[16]; private final float[] mVPMatrix = new float[16]; private final float[] mRotMatrix = new float[16]; private final float[] mTmpMatrix = new float[16]; // temporary vars: only use in synchronized functions! private final Point2D mMovePoint = new Point2D(); private final float[] mv = { 0, 0, 0, 1 }; private final float[] mu = { 0, 0, 0, 1 }; private final float[] mBBoxCoords = new float[8]; private float mHeight, mWidth; public final static float VIEW_DISTANCE = 3.0f; public final static float VIEW_NEAR = 1; public final static float VIEW_FAR = 8; // scale map plane at VIEW_DISTANCE to near plane public final static float VIEW_SCALE = (VIEW_NEAR / VIEW_DISTANCE) * 0.5f; void setViewport(int width, int height) { float s = VIEW_SCALE; float aspect = height / (float) width; Matrix.frustumM(mProjMatrix, 0, -s, s, aspect * s, -aspect * s, VIEW_NEAR, VIEW_FAR); GlUtils.setTranslation(mTmpMatrix, 0, 0, -VIEW_DISTANCE); Matrix.multiplyMM(mProjMatrix, 0, mProjMatrix, 0, mTmpMatrix, 0); Matrix.invertM(mProjMatrixI, 0, mProjMatrix, 0); mHeight = height; mWidth = width; updateMatrix(); } public synchronized boolean getMapPosition(final MapPosition mapPosition) { // if (!isValid()) // return false; if (mapPosition.lat == mLatitude && mapPosition.lon == mLongitude && mapPosition.zoomLevel == mZoomLevel && mapPosition.scale == mScale && mapPosition.angle == mRotation && mapPosition.tilt == mTilt) return false; byte z = mZoomLevel; mapPosition.lat = mLatitude; mapPosition.lon = mLongitude; mapPosition.angle = mRotation; mapPosition.tilt = mTilt; mapPosition.scale = mScale; mapPosition.zoomLevel = z; mapPosition.x = mPosX; mapPosition.y = mPosY; return true; } /** * get a copy of current matrices * * @param view ... * @param proj ... * @param vp view and projection */ public synchronized void getMatrix(float[] view, float[] proj, float[] vp) { if (view != null) System.arraycopy(mViewMatrix, 0, view, 0, 16); if (proj != null) System.arraycopy(mProjMatrix, 0, proj, 0, 16); if (vp != null) System.arraycopy(mVPMatrix, 0, vp, 0, 16); } public synchronized void getMapViewProjection(float[] box) { float t = getZ(1); float t2 = getZ(-1); unproject(1, -1, t, box, 0); // top-right unproject(-1, -1, t, box, 2); // top-left unproject(-1, 1, t2, box, 4); // bottom-left unproject(1, 1, t2, box, 6); // bottom-right } // get the z-value of the map-plane for a point on screen private float getZ(float y) { // calculate the intersection of a ray from // camera origin and the map plane // origin is moved by VIEW_DISTANCE double cx = VIEW_DISTANCE; // 'height' of the ray double ry = y * (mHeight / mWidth) * 0.5f; // tilt of the plane (center is kept on x = 0) double t = Math.toRadians(mTilt); double px = y * Math.sin(t); double py = y * Math.cos(t); double ua = 1 + (px * ry) / (py * cx); mv[0] = 0; mv[1] = (float) (ry / ua); mv[2] = (float) (cx - cx / ua); mv[3] = 1; Matrix.multiplyMV(mv, 0, mProjMatrix, 0, mv, 0); return mv[2] / mv[3]; } private void unproject(float x, float y, float z, float[] coords, int position) { mv[0] = x; mv[1] = y; mv[2] = z; mv[3] = 1; Matrix.multiplyMV(mv, 0, mUnprojMatrix, 0, mv, 0); if (mv[3] != 0) { coords[position + 0] = mv[0] / mv[3]; coords[position + 1] = mv[1] / mv[3]; } } /** @return the current center point of the MapView. */ public synchronized GeoPoint getMapCenter() { return new GeoPoint(mLatitude, mLongitude); } /** * @return a MapPosition or null, if this map position is not valid. * @see #isValid() */ public synchronized MapPosition getMapPosition() { if (!isValid()) { return null; } return new MapPosition(mLatitude, mLongitude, mZoomLevel, mScale, mRotation); } /** @return the current zoom level of the MapView. */ public synchronized byte getZoomLevel() { return mZoomLevel; } /** @return the current scale of the MapView. */ public synchronized float getScale() { return mScale; } /** * ... * * @return BoundingBox containing view */ public synchronized BoundingBox getViewBox() { float[] coords = mBBoxCoords; float t = getZ(1); float t2 = getZ(-1); unproject(1, -1, t, coords, 0); // top-right unproject(-1, -1, t, coords, 2); // top-left unproject(-1, 1, t2, coords, 4); // bottom-left unproject(1, 1, t2, coords, 6); // bottom-right byte z = mZoomLevel; double dx, dy; double minLat = 0, minLon = 0, maxLat = 0, maxLon = 0, lon, lat; for (int i = 0; i < 8; i += 2) { dx = mPosX - coords[i + 0] / mScale; dy = mPosY - coords[i + 1] / mScale; lon = MercatorProjection.pixelXToLongitude(dx, z); lat = MercatorProjection.pixelYToLatitude(dy, z); if (i == 0) { minLon = maxLon = lon; minLat = maxLat = lat; } else { if (lat > maxLat) maxLat = lat; else if (lat < minLat) minLat = lat; if (lon > maxLon) maxLon = lon; else if (lon < minLon) minLon = lon; } } BoundingBox bbox = new BoundingBox(minLat, minLon, maxLat, maxLon); Log.d(">>>", "getScreenBoundingBox " + bbox); return bbox; } /** * for x,y in screen coordinates get the point on the map in map-tile * coordinates * * @param x ... * @param y ... * @param reuse ... * @return ... */ public synchronized Point getScreenPointOnMap(float x, float y, Point reuse) { Point out = reuse == null ? new Point() : reuse; float mx = ((mWidth / 2) - x) / (mWidth / 2); float my = ((mHeight / 2) - y) / (mHeight / 2); unproject(-mx, my, getZ(-my), mu, 0); out.x = (int) (mPosX + mu[0] / mScale); out.y = (int) (mPosY + mu[1] / mScale); //Log.d(TAG, "getScreenPointOnMap " + reuse); return out; } /** * get the GeoPoint for x,y in screen coordinates * * @param x screen pixel x * @param y screen pixel y * @return the corresponding GeoPoint */ public synchronized GeoPoint fromScreenPixels(float x, float y) { float mx = ((mWidth / 2) - x) / (mWidth / 2); float my = ((mHeight / 2) - y) / (mHeight / 2); unproject(-mx, my, getZ(-my), mu, 0); double dx = mPosX + mu[0] / mScale; double dy = mPosY + mu[1] / mScale; GeoPoint p = new GeoPoint( MercatorProjection.pixelYToLatitude(dy, mZoomLevel), MercatorProjection.pixelXToLongitude(dx, mZoomLevel)); //Log.d(TAG, "fromScreenPixels " + p); return p; } /** * get the screen pixel for a GeoPoint * * @param geoPoint ... * @param reuse ... * @return ... */ public synchronized Point project(GeoPoint geoPoint, Point reuse) { Point out = reuse == null ? new Point() : reuse; double x = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mZoomLevel); double y = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mZoomLevel); mv[0] = (float) (x - mPosX) * mScale; mv[1] = (float) (y - mPosY) * mScale; mv[2] = 0; mv[3] = 1; Matrix.multiplyMV(mv, 0, mVPMatrix, 0, mv, 0); out.x = (int) (mv[0] / mv[3] * mWidth / 2); out.y = (int) (mv[1] / mv[3] * mHeight / 2); return out; } // public static Point project(float x, float y, float[] matrix, float[] tmpVec, Point reuse) { // Point out = reuse == null ? new Point() : reuse; // // tmpVec[0] = x; // tmpVec[1] = y; // tmpVec[2] = 0; // tmpVec[3] = 1; // // Matrix.multiplyMV(tmpVec, 0, matrix, 0, tmpVec, 0); // // out.x = (int) (tmpVec[0] / tmpVec[3] * mWidth / 2); // out.y = (int) (tmpVec[1] / tmpVec[3] * mHeight / 2); // // return out; // } private void updateMatrix() { // --- view matrix // 1. scale to window coordinates // 2. rotate // 3. tilt // --- projection matrix // 4. translate to VIEW_DISTANCE // 5. apply projection Matrix.setRotateM(mRotMatrix, 0, mRotation, 0, 0, 1); // tilt map float tilt = mTilt; Matrix.setRotateM(mTmpMatrix, 0, tilt, 1, 0, 0); // apply first rotation, then tilt Matrix.multiplyMM(mRotMatrix, 0, mTmpMatrix, 0, mRotMatrix, 0); // scale to window coordinates GlUtils.setScaleM(mTmpMatrix, 1 / mWidth, 1 / mWidth, 1); Matrix.multiplyMM(mViewMatrix, 0, mRotMatrix, 0, mTmpMatrix, 0); Matrix.multiplyMM(mVPMatrix, 0, mProjMatrix, 0, mViewMatrix, 0); //--- unproject matrix: // Matrix.multiplyMM(mTmpMatrix, 0, mProjMatrix, 0, mViewMatrix, 0); // Matrix.invertM(mUnprojMatrix, 0, mTmpMatrix, 0); // inverse scale GlUtils.setScaleM(mUnprojMatrix, mWidth, mWidth, 1); // inverse rotation and tilt Matrix.transposeM(mTmpMatrix, 0, mRotMatrix, 0); // (AB)^-1 = B^-1*A^-1, unapply scale, tilt and rotation Matrix.multiplyMM(mTmpMatrix, 0, mUnprojMatrix, 0, mTmpMatrix, 0); // (AB)^-1 = B^-1*A^-1, unapply projection Matrix.multiplyMM(mUnprojMatrix, 0, mTmpMatrix, 0, mProjMatrixI, 0); } /** @return true if this MapViewPosition is valid, false otherwise. */ public synchronized boolean isValid() { if (Double.isNaN(mLatitude)) { return false; } else if (mLatitude < MercatorProjection.LATITUDE_MIN) { return false; } else if (mLatitude > MercatorProjection.LATITUDE_MAX) { return false; } if (Double.isNaN(mLongitude)) { return false; } else if (mLongitude < MercatorProjection.LONGITUDE_MIN) { return false; } else if (mLongitude > MercatorProjection.LONGITUDE_MAX) { return false; } return true; } /** * Moves this MapViewPosition by the given amount of pixels. * * @param mx the amount of pixels to move the map horizontally. * @param my the amount of pixels to move the map vertically. */ public synchronized void moveMap(float mx, float my) { Point2D p = getMove(mx, my); mLatitude = MercatorProjection.pixelYToLatitude(mPosY - p.y, mZoomLevel); mLatitude = MercatorProjection.limitLatitude(mLatitude); mLongitude = MercatorProjection.pixelXToLongitude(mPosX - p.x, mZoomLevel); mLongitude = MercatorProjection.wrapLongitude(mLongitude); updatePosition(); } private Point2D getMove(float mx, float my) { double dx = mx / mScale; double dy = my / mScale; if (mMapView.mRotationEnabled || mMapView.mCompassEnabled) { double rad = Math.toRadians(mRotation); double rcos = Math.cos(rad); double rsin = Math.sin(rad); double x = dx * rcos + dy * rsin; double y = dx * -rsin + dy * rcos; dx = x; dy = y; } mMovePoint.x = dx; mMovePoint.y = dy; return mMovePoint; } /** * - * * @param scale ... * @param pivotX ... * @param pivotY ... * @return true if scale was changed */ public synchronized boolean scaleMap(float scale, float pivotX, float pivotY) { // sanitize input if (scale < 0.5) scale = 0.5f; else if (scale > 2) scale = 2; float newScale = mMapScale * scale; int z = FastMath.log2((int) newScale); if (z < MIN_ZOOMLEVEL || (z >= MAX_ZOOMLEVEL && mScale >= 8)) return false; if (z > MAX_ZOOMLEVEL) { // z17 shows everything, just increase scaling // need to fix this for ScanBox if (mScale * scale > 4) return false; mScale *= scale; mMapScale = newScale; } else { mZoomLevel = (byte) z; updatePosition(); mScale = newScale / (1 << z); mMapScale = newScale; } if (pivotX != 0 || pivotY != 0) moveMap(pivotX * (1.0f - scale), pivotY * (1.0f - scale)); return true; } /** * rotate map around pivot cx,cy * * @param angle ... * @param cx ... * @param cy ... */ public synchronized void rotateMap(float angle, float cx, float cy) { moveMap(cx, cy); mRotation += angle; updateMatrix(); } public synchronized void setRotation(float f) { mRotation = f; updateMatrix(); } public synchronized boolean tilt(float move) { float tilt = mTilt + move; if (tilt > MAX_ANGLE) tilt = MAX_ANGLE; else if (tilt < 0) tilt = 0; if (mTilt == tilt) return false; setTilt(tilt); return true; } public synchronized void setTilt(float f) { mTilt = f; updateMatrix(); } private void setMapCenter(double latitude, double longitude) { mLatitude = MercatorProjection.limitLatitude(latitude); mLongitude = MercatorProjection.limitLongitude(longitude); updatePosition(); } synchronized void setMapCenter(GeoPoint geoPoint) { setMapCenter(geoPoint.getLatitude(), geoPoint.getLongitude()); } synchronized void setMapCenter(MapPosition mapPosition) { //mZoomLevel = mMapView.limitZoomLevel(mapPosition.zoomLevel); setZoomLevelLimit(mapPosition.zoomLevel); mMapScale = 1 << mZoomLevel; setMapCenter(mapPosition.lat, mapPosition.lon); } synchronized void setZoomLevel(byte zoomLevel) { //mZoomLevel = mMapView.limitZoomLevel(zoomLevel); setZoomLevelLimit(zoomLevel); mMapScale = 1 << mZoomLevel; mScale = 1; updatePosition(); } private void setZoomLevelLimit(byte zoomLevel) { mZoomLevel = zoomLevel; if (mZoomLevel > MAX_ZOOMLEVEL) mZoomLevel = MAX_ZOOMLEVEL; else if (mZoomLevel < MIN_ZOOMLEVEL) mZoomLevel = MIN_ZOOMLEVEL; } private void updatePosition() { mPosX = MercatorProjection.longitudeToPixelX(mLongitude, mZoomLevel); mPosY = MercatorProjection.latitudeToPixelY(mLatitude, mZoomLevel); } private double mStartX; private double mStartY; private double mEndX; private double mEndY; private float mDuration = 500; public synchronized void animateTo(BoundingBox bbox) { double dx = MercatorProjection.longitudeToX(bbox.getMaxLongitude()) - MercatorProjection.longitudeToX(bbox.getMinLongitude()); double dy = MercatorProjection.latitudeToY(bbox.getMinLatitude()) - MercatorProjection.latitudeToY(bbox.getMaxLatitude()); double log4 = Math.log(4); double zx = -log4 * Math.log(dx) + (mWidth / Tile.TILE_SIZE); double zy = -log4 * Math.log(dy) + (mHeight / Tile.TILE_SIZE); double z = Math.min(zx, zy); if (z > MAX_ZOOMLEVEL) z = MAX_ZOOMLEVEL; else if (z < MIN_ZOOMLEVEL) z = MIN_ZOOMLEVEL; mZoomLevel = (byte) Math.floor(z); mScale = (float) (1 + (z - mZoomLevel)); // global scale mMapScale = (1 << mZoomLevel) * mScale; //Log.d(TAG, "zoom: " + bbox + " " + zx + " " + zy + " / " + mScale + " " + mZoomLevel); setMapCenter(bbox.getCenterPoint()); // updatePosition(); // // // reset rotation/tilt // mTilt = 0; // mRotation = 0; // updateMatrix(); // // GeoPoint geoPoint = bbox.getCenterPoint(); // mEndX = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mZoomLevel); // mEndY = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mZoomLevel); // mStartX = mPosX; // mStartY = mPosY; // // mDuration = 300; // mHandler.start((int) mDuration); } public synchronized void animateTo(GeoPoint geoPoint) { //MercatorProjection.projectPoint(geoPoint, mZoomLevel, mTmpPoint); mEndX = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mZoomLevel); mEndY = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), mZoomLevel); mStartX = mPosX; mStartY = mPosY; mDuration = 300; mHandler.start((int) mDuration); } public synchronized void animateTo(float dx, float dy, float duration) { getMove(dx, dy); mEndX = mPosX - mMovePoint.x; mEndY = mPosY - mMovePoint.y; mStartX = mPosX; mStartY = mPosY; mDuration = duration; mHandler.start((int) mDuration); } synchronized void setMapPosition(double x, double y) { mLatitude = MercatorProjection.pixelYToLatitude(y, mZoomLevel); mLatitude = MercatorProjection.limitLatitude(mLatitude); mLongitude = MercatorProjection.pixelXToLongitude(x, mZoomLevel); mLongitude = MercatorProjection.wrapLongitude(mLongitude); updatePosition(); } void onTick(long millisLeft) { double adv = millisLeft / mDuration; double mx = (mStartX + (mEndX - mStartX) * (1.0 - adv)); double my = (mStartY + (mEndY - mStartY) * (1.0 - adv)); setMapPosition(mx, my); mMapView.redrawMap(true); } void onFinish() { setMapPosition(mEndX, mEndY); mMapView.redrawMap(true); } static class AnimationHandler extends Handler { private final WeakReference mMapViewPosition; private static final int MSG = 1; long mMillisInFuture; long mInterval = 16; long mStopTimeInFuture; AnimationHandler(MapViewPosition mapAnimator) { mMapViewPosition = new WeakReference(mapAnimator); } public synchronized final void start(int millis) { mMillisInFuture = millis; MapViewPosition animator = mMapViewPosition.get(); if (animator == null) return; if (mMillisInFuture <= 0) { animator.onFinish(); return; } mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture; removeMessages(MSG); sendMessage(obtainMessage(MSG)); } public final void cancel() { removeMessages(MSG); } @Override public void handleMessage(Message msg) { MapViewPosition animator = mMapViewPosition.get(); if (animator == null) return; final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime(); if (millisLeft <= 0) { animator.onFinish(); } else if (millisLeft < mInterval) { // no tick, just delay until done sendMessageDelayed(obtainMessage(MSG), millisLeft); } else { long lastTickStart = SystemClock.elapsedRealtime(); animator.onTick(millisLeft); // take into account user's onTick taking time to // execute long delay = lastTickStart + mInterval - SystemClock.elapsedRealtime(); // special case: user's onTick took more than interval // to // complete, skip to next interval while (delay < 0) delay += mInterval; sendMessageDelayed(obtainMessage(MSG), delay); } } } }