/*
This file is part of OpenSatNav.
OpenSatNav is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenSatNav 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenSatNav. If not, see <http://www.gnu.org/licenses/>.
*/
// Created by plusminus on 17:45:56 - 25.09.2008
package org.andnav.osm.views;
import java.util.ArrayList;
import java.util.List;
import org.andnav.osm.util.BoundingBoxE6;
import org.andnav.osm.util.GeoPoint;
import org.andnav.osm.util.MyMath;
import org.andnav.osm.util.constants.OpenStreetMapConstants;
import org.andnav.osm.views.controller.OpenStreetMapViewController;
import org.andnav.osm.views.overlay.OpenStreetMapViewOverlay;
import org.andnav.osm.views.util.OpenStreetMapRendererInfo;
import org.andnav.osm.views.util.OpenStreetMapTileDownloader;
import org.andnav.osm.views.util.OpenStreetMapTileFilesystemProvider;
import org.andnav.osm.views.util.OpenStreetMapTileProvider;
import org.andnav.osm.views.util.Util;
import org.andnav.osm.views.util.constants.OpenStreetMapViewConstants;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Paint.Style;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.GestureDetector.OnGestureListener;
public class OpenStreetMapView extends View implements OpenStreetMapConstants, OpenStreetMapViewConstants {
// ===========================================================
// Constants
// ===========================================================
final OpenStreetMapRendererInfo DEFAULTRENDERER = OpenStreetMapRendererInfo.MAPNIK;
// ===========================================================
// Fields
// ===========================================================
protected int mLatitudeE6 = 0, mLongitudeE6 = 0;
protected int mZoomLevel = 0;
protected OpenStreetMapRendererInfo mRendererInfo;
protected final OpenStreetMapTileProvider mTileProvider;
protected final GestureDetector mGestureDetector = new GestureDetector(
new OpenStreetMapViewGestureDetectorListener());
protected final List<OpenStreetMapViewOverlay> mOverlays = new ArrayList<OpenStreetMapViewOverlay>();
protected final Paint mPaint = new Paint();
private int mTouchDownX;
private int mTouchDownY;
private int mTouchMapOffsetX;
private int mTouchMapOffsetY;
private OpenStreetMapView mMiniMap, mMaxiMap;
private OpenStreetMapViewController mController;
private int mMiniMapOverriddenVisibility = NOT_SET;
private int mMiniMapZoomDiff = NOT_SET;
// ===========================================================
// Constructors
// ===========================================================
/**
* XML Constructor (uses default Renderer)
*/
public OpenStreetMapView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mRendererInfo = DEFAULTRENDERER;
this.mTileProvider = new OpenStreetMapTileProvider(context, new SimpleInvalidationHandler());
}
/**
* Standard Constructor for {@link OpenStreetMapView}.
*
* @param context
* @param aRendererInfo
* pass a {@link OpenStreetMapRendererInfo} you like.
*/
public OpenStreetMapView(final Context context, final OpenStreetMapRendererInfo aRendererInfo) {
super(context);
this.mRendererInfo = aRendererInfo;
this.mTileProvider = new OpenStreetMapTileProvider(context, new SimpleInvalidationHandler());
}
/**
*
* @param context
* @param aRendererInfo
* pass a {@link OpenStreetMapRendererInfo} you like.
* @param osmv
* another {@link OpenStreetMapView}, to share the TileProvider
* with.<br/>
* May significantly improve the render speed, when using the
* same {@link OpenStreetMapRendererInfo}.
*/
public OpenStreetMapView(final Context context, final OpenStreetMapRendererInfo aRendererInfo,
final OpenStreetMapView aMapToShareTheTileProviderWith) {
super(context);
this.mRendererInfo = aRendererInfo;
this.mTileProvider = aMapToShareTheTileProviderWith.mTileProvider;
}
// ===========================================================
// Getter & Setter
// ===========================================================
/**
* This MapView takes control of the {@link OpenStreetMapView} passed as
* parameter.<br />
* I.e. it zoomes it to x levels less than itself and centers it the same
* coords.<br />
* Its pretty usefull when the MiniMap uses the same TileProvider.
*
* @see OpenStreetMapView.OpenStreetMapView(
* @param aOsmvMinimap
* @param aZoomDiff
* 3 is a good Value. Pass {@link OpenStreetMapViewConstants}
* .NOT_SET to disable autozooming of the minimap.
*/
public void setMiniMap(final OpenStreetMapView aOsmvMinimap, final int aZoomDiff) {
this.mMiniMapZoomDiff = aZoomDiff;
this.mMiniMap = aOsmvMinimap;
aOsmvMinimap.setMaxiMap(this);
// Synchronize the Views.
this.setMapCenter(this.mLatitudeE6, this.mLongitudeE6);
this.setZoomLevel(this.getZoomLevel());
}
public boolean hasMiniMap() {
return this.mMiniMap != null;
}
/**
* @return {@link View}.GONE or {@link View}.VISIBLE or {@link View}
* .INVISIBLE or {@link OpenStreetMapViewConstants}.NOT_SET
* */
public int getOverrideMiniMapVisiblity() {
return this.mMiniMapOverriddenVisibility;
}
/**
* Use this method if you want to make the MiniMap visible i.e.: always or
* never. Use {@link View}.GONE , {@link View}.VISIBLE, {@link View}
* .INVISIBLE. Use {@link OpenStreetMapViewConstants}.NOT_SET to reset this
* feature.
*
* @param aVisiblity
*/
public void setOverrideMiniMapVisiblity(final int aVisiblity) {
switch (aVisiblity) {
case View.GONE:
case View.VISIBLE:
case View.INVISIBLE:
if (this.mMiniMap != null)
this.mMiniMap.setVisibility(aVisiblity);
case NOT_SET:
this.setZoomLevel(this.mZoomLevel);
break;
default:
throw new IllegalArgumentException("See javadoch of this method !!!");
}
this.mMiniMapOverriddenVisibility = aVisiblity;
}
protected void setMaxiMap(final OpenStreetMapView aOsmvMaxiMap) {
this.mMaxiMap = aOsmvMaxiMap;
}
public OpenStreetMapViewController getController() {
if (this.mController != null)
return this.mController;
else
return this.mController = new OpenStreetMapViewController(this);
}
/**
* You can add/remove/reorder your Overlays using the List of
* {@link OpenStreetMapViewOverlay}. The first (index 0) Overlay gets drawn
* first, the one with the highest as the last one.
*/
public List<OpenStreetMapViewOverlay> getOverlays() {
return this.mOverlays;
}
public double getLatitudeSpan() {
return this.getDrawnBoundingBoxE6().getLongitudeSpanE6() / 1E6;
}
public int getLatitudeSpanE6() {
return this.getDrawnBoundingBoxE6().getLatitudeSpanE6();
}
public double getLongitudeSpan() {
return this.getDrawnBoundingBoxE6().getLatitudeSpanE6() / 1E6;
}
public int getLongitudeSpanE6() {
return this.getDrawnBoundingBoxE6().getLatitudeSpanE6();
}
public BoundingBoxE6 getDrawnBoundingBoxE6() {
return getBoundingBox(this.getWidth(), this.getHeight());
}
public BoundingBoxE6 getVisibleBoundingBoxE6() {
// final ViewParent parent = this.getParent();
// if(parent instanceof RotateView){
// final RotateView par = (RotateView)parent;
// return getBoundingBox(par.getMeasuredWidth(),
// par.getMeasuredHeight());
// }else{
return getBoundingBox(this.getWidth(), this.getHeight());
// }
}
private BoundingBoxE6 getBoundingBox(final int pViewWidth, final int pViewHeight) {
/*
* Get the center MapTile which is above this.mLatitudeE6 and
* this.mLongitudeE6 .
*/
final int[] centerMapTileCoords = Util.getMapTileFromCoordinates(this.mLatitudeE6, this.mLongitudeE6,
this.mZoomLevel, null);
final BoundingBoxE6 tmp = Util.getBoundingBoxFromMapTile(centerMapTileCoords, this.mZoomLevel);
final int mLatitudeSpan_2 = (int) (1.0f * tmp.getLatitudeSpanE6() * pViewHeight / this.mRendererInfo.MAPTILE_SIZEPX) / 2;
final int mLongitudeSpan_2 = (int) (1.0f * tmp.getLongitudeSpanE6() * pViewWidth / this.mRendererInfo.MAPTILE_SIZEPX) / 2;
final int north = this.mLatitudeE6 + mLatitudeSpan_2;
final int south = this.mLatitudeE6 - mLatitudeSpan_2;
final int west = this.mLongitudeE6 - mLongitudeSpan_2;
final int east = this.mLongitudeE6 + mLongitudeSpan_2;
return new BoundingBoxE6(north, east, south, west);
}
/**
* This class is only meant to be used during on call of onDraw(). Otherwise
* it may produce strange results.
*
* @return
*/
public OpenStreetMapViewProjection getProjection() {
return new OpenStreetMapViewProjection();
}
public void setMapCenter(final GeoPoint aCenter) {
this.setMapCenter(aCenter.getLatitudeE6(), aCenter.getLongitudeE6());
}
public void setMapCenter(final double aLatitude, final double aLongitude) {
this.setMapCenter((int) (aLatitude * 1E6), (int) (aLongitude * 1E6));
}
public void setMapCenter(final int aLatitudeE6, final int aLongitudeE6) {
this.setMapCenter(aLatitudeE6, aLongitudeE6, true);
}
protected void setMapCenter(final int aLatitudeE6, final int aLongitudeE6, final boolean doPassFurther) {
this.mLatitudeE6 = aLatitudeE6;
this.mLongitudeE6 = aLongitudeE6;
if (doPassFurther && this.mMiniMap != null)
this.mMiniMap.setMapCenter(aLatitudeE6, aLongitudeE6, false);
else if (this.mMaxiMap != null)
this.mMaxiMap.setMapCenter(aLatitudeE6, aLongitudeE6, false);
this.postInvalidate();
}
public void setRenderer(final OpenStreetMapRendererInfo aRenderer) {
this.mRendererInfo = aRenderer;
this.setZoomLevel(this.mZoomLevel); // Invalidates the map and zooms to
// the maximum level of the
// renderer.
}
/**
* @param aZoomLevel
* between 0 (equator) and 18/19(closest), depending on the
* Renderer chosen.
*/
public void setZoomLevel(final int aZoomLevel) {
this.mZoomLevel = Math.max(0, Math.min(this.mRendererInfo.ZOOM_MAXLEVEL, aZoomLevel));
if (this.mMiniMap != null) {
if (this.mZoomLevel < this.mMiniMapZoomDiff) {
if (this.mMiniMapOverriddenVisibility == NOT_SET)
this.mMiniMap.setVisibility(View.INVISIBLE);
} else {
if (this.mMiniMapOverriddenVisibility == NOT_SET && this.mMiniMap.getVisibility() != View.VISIBLE) {
this.mMiniMap.setVisibility(View.VISIBLE);
}
if (this.mMiniMapZoomDiff != NOT_SET)
this.mMiniMap.setZoomLevel(this.mZoomLevel - this.mMiniMapZoomDiff);
}
}
this.postInvalidate();
}
/**
* Zooms in if possible.
*/
public void zoomIn() {
//
// final String nextBelowMaptileUrlString =
// this.mRendererInfo.getTileURLString(Util
// .getMapTileFromCoordinates(this.mLatitudeE6, this.mLongitudeE6,
// this.mZoomLevel + 1,
// null), this.mZoomLevel + 1);
// this.mTileProvider.preCacheTile(nextBelowMaptileUrlString);
this.setZoomLevel(this.mZoomLevel + 1);
}
/**
* Zooms out if possible.
*/
public void zoomOut() {
this.setZoomLevel(this.mZoomLevel - 1);
}
public boolean canZoomIn() {
if (this.mZoomLevel >= this.mRendererInfo.ZOOM_MAXLEVEL)
return false;
else
return true;
}
public boolean canZoomOut() {
if (this.mZoomLevel == 0)
return false;
else
return true;
}
/**
* @return the current ZoomLevel between 0 (equator) and 18/19(closest),
* depending on the Renderer chosen.
*/
public int getZoomLevel() {
return this.mZoomLevel;
}
public GeoPoint getMapCenter() {
return new GeoPoint(this.mLatitudeE6, this.mLongitudeE6);
}
public int getMapCenterLatitudeE6() {
return this.mLatitudeE6;
}
public int getMapCenterLongitudeE6() {
return this.mLongitudeE6;
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
public void onLongPress(MotionEvent e) {
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
if (osmvo.onLongPress(e, this))
return;
}
public boolean onSingleTapUp(MotionEvent e) {
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
if (osmvo.onSingleTapUp(e, this))
return true;
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
if (osmvo.onKeyDown(keyCode, event, this))
return true;
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
if (osmvo.onKeyUp(keyCode, event, this))
return true;
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
if (osmvo.onTrackballEvent(event, this))
return true;
return super.onTrackballEvent(event);
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
if (osmvo.onTouchEvent(event, this))
return true;
this.mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.mTouchDownX = (int) event.getX();
this.mTouchDownY = (int) event.getY();
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
this.mTouchMapOffsetX = (int) event.getX() - this.mTouchDownX;
this.mTouchMapOffsetY = (int) event.getY() - this.mTouchDownY;
invalidate();
return true;
case MotionEvent.ACTION_UP:
final int viewWidth_2 = this.getWidth() / 2;
final int viewHeight_2 = this.getHeight() / 2;
final GeoPoint newCenter = this.getProjection().fromPixels(viewWidth_2, viewHeight_2);
this.mTouchMapOffsetX = 0;
this.mTouchMapOffsetY = 0;
this.setMapCenter(newCenter); // Calls invalidate
}
return super.onTouchEvent(event);
}
@Override
public void onDraw(final Canvas c) {
final long startMs = System.currentTimeMillis();
/*
* Do some calculations and drag attributes to local variables to save
* some performance.
*/
final int zoomLevel = this.mZoomLevel;
final int viewWidth = this.getWidth();
final int viewHeight = this.getHeight();
final int tileSizePx = this.mRendererInfo.MAPTILE_SIZEPX;
/*
* Get the center MapTile which is above this.mLatitudeE6 and
* this.mLongitudeE6 .
*/
final int[] centerMapTileCoords = Util.getMapTileFromCoordinates(this.mLatitudeE6, this.mLongitudeE6,
zoomLevel, null);
/*
* Calculate the Latitude/Longitude on the left-upper ScreenCoords of
* the center MapTile. So in the end we can determine which MapTiles we
* additionally need next to the centerMapTile.
*/
final Point upperLeftCornerOfCenterMapTile = getUpperLeftCornerOfCenterMapTileInScreen(centerMapTileCoords,
tileSizePx, null);
final int centerMapTileScreenLeft = upperLeftCornerOfCenterMapTile.x;
final int centerMapTileScreenTop = upperLeftCornerOfCenterMapTile.y;
final int centerMapTileScreenRight = centerMapTileScreenLeft + tileSizePx;
final int centerMapTileScreenBottom = centerMapTileScreenTop + tileSizePx;
/*
* Calculate the amount of tiles needed for each side around the center
* one.
*/
final int additionalTilesNeededToLeftOfCenter =
(int) Math.ceil((float) (viewWidth + centerMapTileScreenLeft) / tileSizePx);
final int additionalTilesNeededToRightOfCenter =
(int) Math.ceil((float) (2*viewWidth - centerMapTileScreenRight) / tileSizePx);
final int additionalTilesNeededToTopOfCenter =
(int) Math.ceil((float) (viewHeight + centerMapTileScreenTop) / tileSizePx);
final int additionalTilesNeededToBottomOfCenter =
(int) Math.ceil((float) (2*viewHeight - centerMapTileScreenBottom) / tileSizePx);
final int mapTileUpperBound = (int) Math.pow(2, zoomLevel);
final int[] mapTileCoords = new int[] { centerMapTileCoords[MAPTILE_LATITUDE_INDEX],
centerMapTileCoords[MAPTILE_LONGITUDE_INDEX] };
int numTiles = (additionalTilesNeededToTopOfCenter+additionalTilesNeededToBottomOfCenter+1)*
(additionalTilesNeededToLeftOfCenter+additionalTilesNeededToRightOfCenter+1);
// Log.d(DEBUGTAG, "Rendering y-range: -" + additionalTilesNeededToTopOfCenter+ " to " + additionalTilesNeededToBottomOfCenter
// + ", x-range: -" + additionalTilesNeededToLeftOfCenter + " to " + additionalTilesNeededToRightOfCenter
// + " : total: " + numTiles);
mTileProvider.ensureMemoryCacheSize(numTiles);
/* Draw all the MapTiles (from the upper left to the lower right). */
for (int y = -additionalTilesNeededToTopOfCenter; y <= additionalTilesNeededToBottomOfCenter; y++) {
for (int x = -additionalTilesNeededToLeftOfCenter; x <= additionalTilesNeededToRightOfCenter; x++) {
/*
* Add/substract the difference of the tile-position to the one
* of the center.
*/
final int tileLeft = this.mTouchMapOffsetX + centerMapTileScreenLeft + (x * tileSizePx);
final int tileRight = tileLeft + tileSizePx;
final int tileTop = this.mTouchMapOffsetY + centerMapTileScreenTop + (y * tileSizePx);
final int tileBottom = tileTop + tileSizePx;
if (c.quickReject(tileLeft, tileTop, tileRight, tileBottom, Canvas.EdgeType.BW))
continue;
/* Construct a URLString, which represents the MapTile. */
mapTileCoords[MAPTILE_LATITUDE_INDEX] = MyMath.mod(centerMapTileCoords[MAPTILE_LATITUDE_INDEX] + y,
mapTileUpperBound);
mapTileCoords[MAPTILE_LONGITUDE_INDEX] = MyMath.mod(centerMapTileCoords[MAPTILE_LONGITUDE_INDEX] + x,
mapTileUpperBound);
final String tileURLString = this.mRendererInfo.getTileURLString(mapTileCoords, zoomLevel);
/* Draw the MapTile 'i tileSizePx' above of the centerMapTile */
final Bitmap currentMapTile = this.mTileProvider.getMapTile(tileURLString);
c.drawBitmap(currentMapTile, tileLeft, tileTop, this.mPaint);
if (DEBUGMODE) {
c.drawLine(tileLeft, tileTop, tileRight, tileTop, this.mPaint);
c.drawLine(tileLeft, tileTop, tileLeft, tileBottom, this.mPaint);
}
}
}
/* Draw all Overlays. */
for (OpenStreetMapViewOverlay osmvo : this.mOverlays)
osmvo.onManagedDraw(c, this);
this.mPaint.setStyle(Style.STROKE);
if (this.mMaxiMap != null) // If this is a MiniMap
c.drawRect(0, 0, viewWidth - 1, viewHeight - 1, this.mPaint);
if (DEBUGMODE) {
final long endMs = System.currentTimeMillis();
Log.d(DEBUGTAG, "Rendering overall: " + (endMs - startMs) + "ms");
}
}
// ===========================================================
// Methods
// ===========================================================
/**
* @param centerMapTileCoords
* @param tileSizePx
* @param reuse
* just pass null if you do not have a Point to be 'recycled'.
*/
private Point getUpperLeftCornerOfCenterMapTileInScreen(final int[] centerMapTileCoords, final int tileSizePx,
final Point reuse) {
final Point out = (reuse != null) ? reuse : new Point();
final int viewWidth = this.getWidth();
final int viewWidth_2 = viewWidth / 2;
final int viewHeight = this.getHeight();
final int viewHeight_2 = viewHeight / 2;
/*
* Calculate the Latitude/Longitude on the left-upper ScreenCoords of
* the center MapTile. So in the end we can determine which MapTiles we
* additionally need next to the centerMapTile.
*/
final BoundingBoxE6 bb = Util.getBoundingBoxFromMapTile(centerMapTileCoords, this.mZoomLevel);
final float[] relativePositionInCenterMapTile = bb
.getRelativePositionOfGeoPointInBoundingBoxWithLinearInterpolation(this.mLatitudeE6, this.mLongitudeE6,
null);
final int centerMapTileScreenLeft = viewWidth_2
- (int) (0.5f + (relativePositionInCenterMapTile[MAPTILE_LONGITUDE_INDEX] * tileSizePx));
final int centerMapTileScreenTop = viewHeight_2
- (int) (0.5f + (relativePositionInCenterMapTile[MAPTILE_LATITUDE_INDEX] * tileSizePx));
out.set(centerMapTileScreenLeft, centerMapTileScreenTop);
return out;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
/**
* This class may return valid results until the underlying
* {@link OpenStreetMapView} gets modified in any way (i.e. new center).
*
* @author Nicolas Gramlich
*/
public class OpenStreetMapViewProjection {
final int viewWidth;
final int viewHeight;
final BoundingBoxE6 bb;
final int zoomLevel;
final int tileSizePx;
final int[] centerMapTileCoords;
final Point upperLeftCornerOfCenterMapTile;
public OpenStreetMapViewProjection() {
viewWidth = OpenStreetMapView.this.getWidth();
viewHeight = OpenStreetMapView.this.getHeight();
/*
* Do some calculations and drag attributes to local variables to
* save some performance.
*/
zoomLevel = OpenStreetMapView.this.mZoomLevel; // TODO Draw to
// attributes and so
// make it only
// 'valid' for a
// short time.
tileSizePx = OpenStreetMapView.this.mRendererInfo.MAPTILE_SIZEPX;
/*
* Get the center MapTile which is above this.mLatitudeE6 and
* this.mLongitudeE6 .
*/
centerMapTileCoords = Util.getMapTileFromCoordinates(OpenStreetMapView.this.mLatitudeE6,
OpenStreetMapView.this.mLongitudeE6, zoomLevel, null);
upperLeftCornerOfCenterMapTile = getUpperLeftCornerOfCenterMapTileInScreen(centerMapTileCoords, tileSizePx,
null);
bb = OpenStreetMapView.this.getDrawnBoundingBoxE6();
}
/**
* Converts x/y ScreenCoordinates to the underlying GeoPoint.
*
* @param x
* @param y
* @return GeoPoint under x/y.
*/
public GeoPoint fromPixels(float x, float y) {
/* Subtract the offset caused by touch. */
x -= OpenStreetMapView.this.mTouchMapOffsetX;
y -= OpenStreetMapView.this.mTouchMapOffsetY;
return bb.getGeoPointOfRelativePositionWithLinearInterpolation(x / viewWidth, y / viewHeight);
}
private static final int EQUATORCIRCUMFENCE = 40075;
public float metersToEquatorPixels(final float aMeters) {
int zoomLevelDifference = 19 - zoomLevel;
return (aMeters / EQUATORCIRCUMFENCE * OpenStreetMapView.this.mRendererInfo.MAPTILE_SIZEPX)/zoomLevelDifference;
}
/**
* Converts a GeoPoint to its ScreenCoordinates. <br/>
* <br/>
* <b>CAUTION</b> ! Conversion currently has a large error on
* <code>zoomLevels <= 7</code>.<br/>
* The Error on ZoomLevels higher than 7, the error is below
* <code>1px</code>.<br/>
* TODO: Add a linear interpolation to minimize this error.
*
* <PRE>
* Zoom Error(m) Error(px)
* 11 6m 1/12px
* 10 24m 1/6px
* 8 384m 1/2px
* 6 6144m 3px
* 4 98304m 10px
* </PRE>
*
* @param in
* the GeoPoint you want the onScreenCoordinates of.
* @param reuse
* just pass null if you do not have a Point to be
* 'recycled'.
* @return the Point containing the approximated ScreenCoordinates of
* the GeoPoint passed.
*/
public Point toPixels(final GeoPoint in, final Point reuse) {
return toPixels(in, reuse, true);
}
protected Point toPixels(final GeoPoint in, final Point reuse, final boolean doGudermann) {
final Point out = (reuse != null) ? reuse : new Point();
final int[] underGeopointTileCoords = Util.getMapTileFromCoordinates(in.getLatitudeE6(), in
.getLongitudeE6(), zoomLevel, null);
/*
* Calculate the Latitude/Longitude on the left-upper ScreenCoords
* of the MapTile.
*/
final BoundingBoxE6 bb = Util.getBoundingBoxFromMapTile(underGeopointTileCoords, zoomLevel);
final float[] relativePositionInCenterMapTile;
if (doGudermann && zoomLevel < 7)
relativePositionInCenterMapTile = bb
.getRelativePositionOfGeoPointInBoundingBoxWithExactGudermannInterpolation(in.getLatitudeE6(),
in.getLongitudeE6(), null);
else
relativePositionInCenterMapTile = bb.getRelativePositionOfGeoPointInBoundingBoxWithLinearInterpolation(
in.getLatitudeE6(), in.getLongitudeE6(), null);
final int tileDiffX = centerMapTileCoords[MAPTILE_LONGITUDE_INDEX]
- underGeopointTileCoords[MAPTILE_LONGITUDE_INDEX];
final int tileDiffY = centerMapTileCoords[MAPTILE_LATITUDE_INDEX]
- underGeopointTileCoords[MAPTILE_LATITUDE_INDEX];
final int underGeopointTileScreenLeft = upperLeftCornerOfCenterMapTile.x - (tileSizePx * tileDiffX);
final int underGeopointTileScreenTop = upperLeftCornerOfCenterMapTile.y - (tileSizePx * tileDiffY);
final int x = underGeopointTileScreenLeft
+ (int) (relativePositionInCenterMapTile[MAPTILE_LONGITUDE_INDEX] * tileSizePx);
final int y = underGeopointTileScreenTop
+ (int) (relativePositionInCenterMapTile[MAPTILE_LATITUDE_INDEX] * tileSizePx);
/* Add up the offset caused by touch. */
out.set(x + OpenStreetMapView.this.mTouchMapOffsetX, y + OpenStreetMapView.this.mTouchMapOffsetY);
return out;
}
public Path toPixels(final List<GeoPoint> in, final Path reuse) {
return toPixels(in, reuse, true);
}
protected Path toPixels(final List<GeoPoint> in, final Path reuse, final boolean doGudermann)
throws IllegalArgumentException {
if (in.size() < 2)
throw new IllegalArgumentException("List of GeoPoints needs to be at least 2.");
final Path out = (reuse != null) ? reuse : new Path();
int i = 0;
for (GeoPoint gp : in) {
i++;
final int[] underGeopointTileCoords = Util.getMapTileFromCoordinates(gp.getLatitudeE6(), gp
.getLongitudeE6(), zoomLevel, null);
/*
* Calculate the Latitude/Longitude on the left-upper
* ScreenCoords of the MapTile.
*/
final BoundingBoxE6 bb = Util.getBoundingBoxFromMapTile(underGeopointTileCoords, zoomLevel);
final float[] relativePositionInCenterMapTile;
if (doGudermann && zoomLevel < 7)
relativePositionInCenterMapTile = bb
.getRelativePositionOfGeoPointInBoundingBoxWithExactGudermannInterpolation(gp
.getLatitudeE6(), gp.getLongitudeE6(), null);
else
relativePositionInCenterMapTile = bb
.getRelativePositionOfGeoPointInBoundingBoxWithLinearInterpolation(gp.getLatitudeE6(), gp
.getLongitudeE6(), null);
final int tileDiffX = centerMapTileCoords[MAPTILE_LONGITUDE_INDEX]
- underGeopointTileCoords[MAPTILE_LONGITUDE_INDEX];
final int tileDiffY = centerMapTileCoords[MAPTILE_LATITUDE_INDEX]
- underGeopointTileCoords[MAPTILE_LATITUDE_INDEX];
final int underGeopointTileScreenLeft = upperLeftCornerOfCenterMapTile.x - (tileSizePx * tileDiffX);
final int underGeopointTileScreenTop = upperLeftCornerOfCenterMapTile.y - (tileSizePx * tileDiffY);
final int x = underGeopointTileScreenLeft
+ (int) (relativePositionInCenterMapTile[MAPTILE_LONGITUDE_INDEX] * tileSizePx);
final int y = underGeopointTileScreenTop
+ (int) (relativePositionInCenterMapTile[MAPTILE_LATITUDE_INDEX] * tileSizePx);
/* Add up the offset caused by touch. */
if (i == 0)
out
.moveTo(x + OpenStreetMapView.this.mTouchMapOffsetX, y
+ OpenStreetMapView.this.mTouchMapOffsetY);
else
out
.lineTo(x + OpenStreetMapView.this.mTouchMapOffsetX, y
+ OpenStreetMapView.this.mTouchMapOffsetY);
}
return out;
}
}
private class SimpleInvalidationHandler extends Handler {
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case OpenStreetMapTileDownloader.MAPTILEDOWNLOADER_SUCCESS_ID:
case OpenStreetMapTileFilesystemProvider.MAPTILEFSLOADER_SUCCESS_ID:
OpenStreetMapView.this.invalidate();
break;
}
}
}
private class OpenStreetMapViewGestureDetectorListener implements OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// TODO Could be used for smoothly 'scroll-out' the map on a fast
// motion.
return false;
}
@Override
public void onLongPress(MotionEvent e) {
OpenStreetMapView.this.onLongPress(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return OpenStreetMapView.this.onSingleTapUp(e);
}
}
}