package org.osmdroid.views; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IProjection; import org.osmdroid.util.BoundingBoxE6; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.util.TileSystem; import org.osmdroid.views.util.constants.MapViewConstants; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import org.osmdroid.util.BoundingBoxE6; /** * A Projection serves to translate between the coordinate system of x/y on-screen pixel coordinates * and that of latitude/longitude points on the surface of the earth. You obtain a Projection from * MapView.getProjection(). You should not hold on to this object for more than one draw, since the * projection of the map could change. <br> * <br>Uses the web mercator projection * <b>Note:</b> This class will "wrap" all pixel and lat/long values that overflow their bounds * (rather than clamping to their bounds). * * @author Marc Kurtz * @author Nicolas Gramlich * @author Manuel Stahl */ public class Projection implements IProjection, MapViewConstants { private final int mMapViewWidth; private final int mMapViewHeight; // The offsets will take us from the MapView's current coordinate system // to a 0,0 coordinate system protected final int mOffsetX; protected final int mOffsetY; protected final float mMultiTouchScale; private final Matrix mRotateAndScaleMatrix = new Matrix(); private final Matrix mUnrotateAndScaleMatrix = new Matrix(); private final float[] mRotateScalePoints = new float[2]; private final BoundingBox mBoundingBoxProjection; private final int mZoomLevelProjection; private final Rect mScreenRectProjection; private final Rect mIntrinsicScreenRectProjection; private MapView mapView; Projection(MapView mapView) { this.mapView=mapView; mZoomLevelProjection = mapView.getZoomLevel(false); mScreenRectProjection = mapView.getScreenRect(null); mIntrinsicScreenRectProjection = mapView.getIntrinsicScreenRect(null); mMapViewWidth = mapView.getWidth(); mMapViewHeight = mapView.getHeight(); mOffsetX = -mapView.getScrollX(); mOffsetY = -mapView.getScrollY(); mRotateAndScaleMatrix.set(mapView.mRotateScaleMatrix); mRotateAndScaleMatrix.invert(mUnrotateAndScaleMatrix); mMultiTouchScale = mapView.mMultiTouchScale; final IGeoPoint neGeoPoint = fromPixels(mMapViewWidth, 0, null); final IGeoPoint swGeoPoint = fromPixels(0, mMapViewHeight, null); mBoundingBoxProjection = new BoundingBox(neGeoPoint.getLatitude(), neGeoPoint.getLongitude(), swGeoPoint.getLatitude(), swGeoPoint.getLongitude()); } public int getZoomLevel() { return mZoomLevelProjection; } public BoundingBox getBoundingBox() { return mBoundingBoxProjection; } @Deprecated public BoundingBoxE6 getBoundingBoxE6() { BoundingBoxE6 x = new BoundingBoxE6(mBoundingBoxProjection.getLatNorth(), mBoundingBoxProjection.getLonEast(), mBoundingBoxProjection.getLatSouth(), mBoundingBoxProjection.getLonWest()); return x; } public Rect getScreenRect() { return mScreenRectProjection; } public Rect getIntrinsicScreenRect() { return mIntrinsicScreenRectProjection; } public float getMapOrientation() { return mapView.getMapOrientation(); } @Override public IGeoPoint fromPixels(int x, int y) { return fromPixels(x, y, null); } public IGeoPoint fromPixels(int x, int y, GeoPoint reuse) { //reverting https://github.com/osmdroid/osmdroid/issues/459 //due to relapse of https://github.com/osmdroid/osmdroid/issues/507 //reverted functionality is now on the method fromPixelsRotationSensitive return TileSystem.PixelXYToLatLong(x - mOffsetX, y - mOffsetY, mZoomLevelProjection, reuse); } public IGeoPoint fromPixelsRotationSensitive(int x, int y, GeoPoint reuse) { Point point = unrotateAndScalePoint(x, y, null); return TileSystem.PixelXYToLatLong(point.x - mOffsetX, point.y - mOffsetY, mZoomLevelProjection, reuse); } @Override public Point toPixels(final IGeoPoint in, final Point reuse) { Point out = TileSystem.LatLongToPixelXY(in.getLatitude(), in.getLongitude(), getZoomLevel(), reuse); out = toPixelsFromMercator(out.x, out.y, out); out = adjustForDateLine(out.x, out.y, out); return out; } protected Point adjustForDateLine(int x, int y, Point reuse) { final Point out = reuse != null ? reuse : new Point(); out.set(x, y); out.offset(-mMapViewWidth / 2, -mMapViewHeight / 2); // final int mapSize = TileSystem.MapSize(getZoomLevel()); final int absX = Math.abs(out.x); final int absY = Math.abs(out.y); final int yCompare = mapSize > mMapViewHeight ? mapSize : mMapViewHeight; // avoid too early vertical adjusting if (absX > Math.abs(out.x - mapSize)) { out.x -= mapSize; } if (absX > Math.abs(out.x + mapSize)) { out.x += mapSize; } if (absY > Math.abs(out.y - yCompare) && mMapViewHeight / 2 < mapSize ) { // use yCorrection instead of mapSize out.y -= mapSize; } if (absY > Math.abs(out.y + yCompare) || mMapViewHeight / 2 >= mapSize) { // use yCorrection instead of mapSize out.y += mapSize; } out.offset(mMapViewWidth / 2, mMapViewHeight / 2); return out; } /** * A wrapper for {@link #toProjectedPixels(int, int, Point)} */ public Point toProjectedPixels(final GeoPoint geoPoint, final Point reuse) { return toProjectedPixels(geoPoint.getLatitude(), geoPoint.getLongitude(), reuse); } /** * Performs only the first computationally heavy part of the projection. Call * {@link #toPixelsFromProjected(Point, Point)} to get the final position. * * @param latituteE6 * the latitute of the point * @param longitudeE6 * the longitude of the point * @param reuse * just pass null if you do not have a Point to be 'recycled'. * @return intermediate value to be stored and passed to toMapPixelsTranslated. */ public Point toProjectedPixels(final int latituteE6, final int longitudeE6, final Point reuse) { return TileSystem.LatLongToPixelXY(latituteE6 * 1E-6, longitudeE6 * 1E-6, microsoft.mappoint.TileSystem.getMaximumZoomLevel(), reuse); } /** * Performs only the first computationally heavy part of the projection. Call * {@link #toPixelsFromProjected(Point, Point)} to get the final position. * * @param latitude * the latitute of the point * @param longitude * the longitude of the point * @param reuse * just pass null if you do not have a Point to be 'recycled'. * @return intermediate value to be stored and passed to toMapPixelsTranslated. */ public Point toProjectedPixels(final double latitude, final double longitude, final Point reuse) { return TileSystem.LatLongToPixelXY(latitude, longitude, microsoft.mappoint.TileSystem.getMaximumZoomLevel(), reuse); } /** * Performs the second computationally light part of the projection. * * @param in * the Point calculated by the {@link #toProjectedPixels(int, int, Point)} * @param reuse * just pass null if you do not have a Point to be 'recycled'. * @return the Point containing the coordinates of the initial GeoPoint passed to the * {@link #toProjectedPixels(int, int, Point)}. */ public Point toPixelsFromProjected(final Point in, final Point reuse) { Point out = reuse != null ? reuse : new Point(); final int zoomDifference = microsoft.mappoint.TileSystem.getMaximumZoomLevel() - getZoomLevel(); out.set(in.x >> zoomDifference, in.y >> zoomDifference); out = toPixelsFromMercator(out.x, out.y, out); out = adjustForDateLine(out.x, out.y, out); return out; } public Point toPixelsFromMercator(int x, int y, Point reuse) { final Point out = reuse != null ? reuse : new Point(); out.set(x, y); out.offset(mOffsetX, mOffsetY); return out; } public Point toMercatorPixels(int x, int y, Point reuse) { final Point out = reuse != null ? reuse : new Point(); out.set(x, y); out.offset(-mOffsetX, -mOffsetY); return out; } @Override public float metersToEquatorPixels(final float meters) { return meters / (float) TileSystem.GroundResolution(0, mZoomLevelProjection); } /** * Converts a distance in meters to one in (horizontal) pixels at the current zoomlevel and at * the current latitude at the center of the screen. * * @param meters * the distance in meters * @return The number of pixels corresponding to the distance, if measured at the center of the * screen, at the current zoom level. The return value may only be approximate. */ public float metersToPixels(final float meters) { return meters / (float) TileSystem.GroundResolution(getBoundingBox().getCenter().getLatitude(), mZoomLevelProjection); } @Override public IGeoPoint getNorthEast() { return fromPixels(mMapViewWidth, 0, null); } @Override public IGeoPoint getSouthWest() { return fromPixels(0, mMapViewHeight, null); } /** * This will provide a Matrix that will revert the current map's scaling and rotation. This can * be useful when drawing to a fixed location on the screen. */ public Matrix getInvertedScaleRotateCanvasMatrix() { return mUnrotateAndScaleMatrix; } /** * This will revert the current map's scaling and rotation for a point. This can be useful when * drawing to a fixed location on the screen. */ public Point unrotateAndScalePoint(int x, int y, Point reuse) { if (reuse == null) reuse = new Point(); if (getMapOrientation() != 0 || mMultiTouchScale != 1.0f) { mRotateScalePoints[0] = x; mRotateScalePoints[1] = y; mUnrotateAndScaleMatrix.mapPoints(mRotateScalePoints); reuse.set((int) mRotateScalePoints[0], (int) mRotateScalePoints[1]); } else reuse.set(x, y); return reuse; } /** * This will apply the current map's scaling and rotation for a point. This can be useful when * converting MotionEvents to a screen point. */ public Point rotateAndScalePoint(int x, int y, Point reuse) { if (reuse == null) reuse = new Point(); if (getMapOrientation() != 0 || mMultiTouchScale != 1.0f) { mRotateScalePoints[0] = x; mRotateScalePoints[1] = y; mRotateAndScaleMatrix.mapPoints(mRotateScalePoints); reuse.set((int) mRotateScalePoints[0], (int) mRotateScalePoints[1]); } else reuse.set(x, y); return reuse; } /** * @since 5.6 */ public void detach(){ mapView=null; } }