/* * Copyright 2013-2016 microG Project Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.maps; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; 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.ViewConfiguration; import android.view.ViewGroup; import android.widget.ZoomButtonsController; import android.widget.ZoomControls; import org.microg.annotation.OriginalApi; import org.microg.internal.R; import org.microg.osmdroid.CustomResourceProxyImpl; import org.microg.osmdroid.SafeMapTileProviderBasic; import org.microg.osmdroid.SafeNetworkAvailabilityCheck; import org.osmdroid.api.IMapView; import org.osmdroid.tileprovider.tilesource.ITileSource; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.tileprovider.util.SimpleRegisterReceiver; import java.util.List; /* * TODO: * - Possibly bad trackball support, but no way to test for me * - We don't draw the reticle, but it's for trackball only afaik. * - We can't show traffic and satellite maps the same moment... */ @OriginalApi public class MapView extends ViewGroup implements IMapView { private static final String TAG = MapView.class.getName(); private static final String KEY_CENTER_LATITUDE = MapView.class.getName() + ".centerLatitude"; private static final String KEY_CENTER_LONGITUDE = MapView.class.getName() + ".centerLongitude"; private static final String KEY_ZOOM_DISPLAYED = MapView.class.getName() + ".zoomDisplayed"; private static final String KEY_ZOOM_LEVEL = MapView.class.getName() + ".zoomLevel"; // TODO: We should read these from a setting, users might want to change it... private static final ITileSource DEFAULT = TileSourceFactory.MAPNIK; private static final ITileSource TRAFFIC = TileSourceFactory.PUBLIC_TRANSPORT; private static final ITileSource SATELLITE = TileSourceFactory.MAPQUESTAERIAL; public static Context DEFAULT_CONTEXT; private GestureDetector gestureDetector; private Handler handler = new Handler(); private MapController mapController; private ReticleDrawMode reticleDrawMode; private final WrappedMapView wrapped; private boolean zoomControlsEnabled = true; private Runnable zoomControlsHideCallback; private ZoomButtonsController zoomButtonsController; private ZoomControls zoomControls; private final OverlayList.NewOverlayAttachedListener attachedListener = new OverlayList.NewOverlayAttachedListener() { @Override public void onNewOverlayAttached(Overlay overlay) { overlay.assignedMapView = MapView.this; if (getHandler() != null) { getHandler().post(new Runnable() { @Override public void run() { invalidate(); } }); } } }; private boolean satellite = false; private boolean streetView = false; private boolean traffic = false; @OriginalApi public MapView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.mapViewStyle); } @OriginalApi public MapView(Context context, AttributeSet attrs, int defStyle) { this(context, attrs, defStyle, null); } @OriginalApi public MapView(Context context, String apiKey) { this(context, null, R.attr.mapViewStyle, apiKey); } public MapView(Context context, AttributeSet attrs, int defStyle, String apiKey) { super(context, attrs, defStyle); DEFAULT_CONTEXT = context; wrapped = new WrappedMapView(context, attrs); mapController = new MapController(wrapped.getController()); addView(wrapped); // Warn the developer that his usage of MapView will not work with Google's implementation. if (attrs != null) { try { TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MapView); if (array != null) { if (apiKey == null) { apiKey = array.getString(R.styleable.MapView_apiKey); } array.recycle(); } } catch (Exception e) { // This might fail, if we can't access the internal R or it's modified Log.w(TAG, e); } } if (apiKey == null) { Log.w(TAG, "MapViews must specify an API Key to be compatible with Google's implementation."); } if (!(context instanceof MapActivity)) { Log.w(TAG, "MapViews must only be created inside instances of MapActivity to be compatible with Google's implementation."); } // Set startup location as suggested from resources try { int[] latlonE6 = getResources().getIntArray(R.array.maps_starting_lat_lng); getController().setCenter(new GeoPoint(latlonE6[0], latlonE6[1])); getController().setZoom(getResources().getIntArray(R.array.maps_starting_zoom)[0]); } catch (Exception e) { // This might fail, if we can't access the internal R or it's modified Log.w(TAG, e); } // We detect gestures non-exclusively. osmdroid uses the same gesture detection, // but we can't depend on their implementation as it's not accessible from this context. gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { displayZoomControls(false); return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { displayZoomControls(false); return false; } @Override public void onLongPress(MotionEvent e) { MapView.this.performLongClick(); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { return MapView.this.performClick(); } }); //gestureDetector.setIsLongpressEnabled(false); } @OriginalApi public boolean canCoverCenter() { Log.w(TAG, "Incomplete implementation of canCoverCenter()"); return true; // TODO } @OriginalApi protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @OriginalApi public void displayZoomControls(boolean takeFocus) { if (zoomControlsEnabled) { if ((zoomButtonsController != null) && (!zoomButtonsController.isVisible())) { zoomButtonsController.setFocusable(takeFocus); zoomButtonsController.setVisible(true); } if (zoomControls != null) { if (zoomControls.getVisibility() == View.GONE) { zoomControls.show(); } if (takeFocus) { zoomControls.requestFocus(); } handler.removeCallbacks(zoomControlsHideCallback); handler.postDelayed(zoomControlsHideCallback, ViewConfiguration.getZoomControlsTimeout()); } } } @OriginalApi protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, new GeoPoint(0, 0), LayoutParams.CENTER); } @OriginalApi @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @OriginalApi @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } @OriginalApi @Override public MapController getController() { return mapController; } @OriginalApi @Override public int getLatitudeSpan() { return wrapped.getLatitudeSpan(); } @OriginalApi @Override public int getLongitudeSpan() { return wrapped.getLongitudeSpan(); } @OriginalApi @Override public GeoPoint getMapCenter() { return new GeoPoint(wrapped.getMapCenter()); } @OriginalApi @Override public int getMaxZoomLevel() { return wrapped.getMaxZoomLevel(); } @OriginalApi public List<Overlay> getOverlays() { return new OverlayList(wrapped.getOverlays(), attachedListener); } @OriginalApi @Override public Projection getProjection() { return new ProjectionWrapper(wrapped.getProjection()); } public IMapView getWrapped() { return wrapped; } @OriginalApi public ZoomButtonsController getZoomButtonsController() { return zoomButtonsController; } @OriginalApi @Deprecated public View getZoomControls() { if (zoomControls == null) { zoomControls = new ZoomControls(getContext()); zoomControls.setZoomSpeed(2000); zoomControls.setOnZoomInClickListener(new OnClickListener() { @Override public void onClick(View v) { wrapped.getController().zoomIn(); } }); zoomControls.setOnZoomOutClickListener(new OnClickListener() { @Override public void onClick(View v) { wrapped.getController().zoomOut(); } }); zoomControls.setVisibility(View.GONE); zoomControlsHideCallback = new Runnable() { @Override public void run() { if (!zoomControls.hasFocus()) { zoomControls.hide(); } else { handler.removeCallbacks(zoomControlsHideCallback); handler.postDelayed(zoomControlsHideCallback, ViewConfiguration.getZoomControlsTimeout()); } } }; } return zoomControls; } @OriginalApi @Override public int getZoomLevel() { return wrapped.getZoomLevel(); } @OriginalApi public boolean isSatellite() { return satellite; } @OriginalApi public void setSatellite(boolean on) { satellite = on; wrapped.updateTileSource(); } @OriginalApi public boolean isStreetView() { return streetView; } @OriginalApi public void setStreetView(boolean on) { if (on) setTraffic(false); streetView = on; } @OriginalApi public boolean isTraffic() { return traffic; } @OriginalApi public void setTraffic(boolean on) { if (on) setStreetView(false); traffic = true; } @OriginalApi @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); // Fix for bug in ZoomButtonsController if (zoomButtonsController != null) zoomButtonsController.setVisible(false); } @OriginalApi @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @OriginalApi @Override public void onFocusChanged(boolean hasFocus, int direction, Rect unused) { super.onFocusChanged(hasFocus, direction, unused); } @OriginalApi @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return super.onKeyDown(keyCode, event); } @OriginalApi @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return super.onKeyUp(keyCode, event); } @OriginalApi @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (wrapped instanceof org.osmdroid.views.MapView) { wrapped.layout(l, t, r, b); } } @OriginalApi @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @OriginalApi public void onRestoreInstanceState(Bundle state) { if (state != null) { int latE6 = state.getInt(KEY_CENTER_LATITUDE, Integer.MAX_VALUE); int lonE6 = state.getInt(KEY_CENTER_LONGITUDE, Integer.MAX_VALUE); if (latE6 != Integer.MAX_VALUE && lonE6 != Integer.MAX_VALUE) { getController().setCenter(new GeoPoint(latE6, lonE6)); } int zoom = state.getInt(KEY_ZOOM_LEVEL, Integer.MAX_VALUE); if (zoom != Integer.MAX_VALUE) { getController().setZoom(zoom); } if (state.getInt(KEY_ZOOM_DISPLAYED, 0) != 0) { displayZoomControls(false); } } } @OriginalApi public void onSaveInstanceState(Bundle state) { // We use the same way to store information in the bundle as Google does, // also this is not documented, there are a number of apps known to rely on it. state.putInt(KEY_CENTER_LATITUDE, getMapCenter().getLatitudeE6()); state.putInt(KEY_CENTER_LONGITUDE, getMapCenter().getLongitudeE6()); state.putInt(KEY_ZOOM_LEVEL, getZoomLevel()); state.putInt(KEY_ZOOM_DISPLAYED, (zoomButtonsController != null) && (zoomButtonsController.isVisible()) || ((zoomControls != null) && (zoomControls.getVisibility() == View.VISIBLE)) ? 1 : 0); } @OriginalApi @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); } @OriginalApi @Override public boolean onTouchEvent(MotionEvent event) { // We only consume the touch event if it is used by the zoom button. // The gestures detected by us are also relevant for the underlying MapView! gestureDetector.onTouchEvent(event); return (zoomButtonsController != null && zoomButtonsController.isVisible() && zoomButtonsController.onTouch(this, event)); } @OriginalApi @Override public boolean onTrackballEvent(MotionEvent event) { Log.w(TAG, "Incomplete implementation of onTrackballEvent()"); // TODO we do not support trackball well, but they're kind of deprecated anyway?! return false; } @OriginalApi @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); } @OriginalApi public void preLoad() { Log.w(TAG, "Incomplete implementation of preLoad()"); // TODO not sure what this actually does... } @OriginalApi public void setBuiltInZoomControls(boolean on) { zoomControlsEnabled = on; if (zoomButtonsController == null) { zoomButtonsController = new ZoomButtonsController(this); zoomButtonsController.setZoomSpeed(2000); zoomButtonsController.setOnZoomListener(new ZoomButtonsController.OnZoomListener() { @Override public void onVisibilityChanged(boolean visible) { if (visible) { zoomButtonsController.setZoomInEnabled(wrapped.getZoomLevel() < wrapped.getMaxZoomLevel()); zoomButtonsController.setZoomOutEnabled(wrapped.getZoomLevel() > 1); } else { zoomButtonsController.setFocusable(false); } } @Override public void onZoom(boolean zoomIn) { if (zoomIn) { wrapped.getController().zoomIn(); } else { wrapped.getController().zoomOut(); } } }); } } @OriginalApi public void setReticleDrawMode(ReticleDrawMode mode) { reticleDrawMode = mode; } @OriginalApi public static enum ReticleDrawMode { DRAW_RETICLE_OVER, DRAW_RETICLE_UNDER, DRAW_RETICLE_NEVER } @OriginalApi public static class LayoutParams extends ViewGroup.LayoutParams { @OriginalApi public static int BOTTOM = 80; @OriginalApi public static int BOTTOM_CENTER = 81; @OriginalApi public static int CENTER = 17; @OriginalApi public static int CENTER_HORIZONTAL = 1; @OriginalApi public static int CENTER_VERTICAL = 16; @OriginalApi public static int LEFT = 3; @OriginalApi public static int RIGHT = 5; @OriginalApi public static int TOP = 48; @OriginalApi public static int TOP_LEFT = 51; @OriginalApi public static int MODE_MAP = 0; @OriginalApi public static int MODE_VIEW = 1; @OriginalApi public int alignment; @OriginalApi public int mode; @OriginalApi public GeoPoint point; @OriginalApi public int x; @OriginalApi public int y; @OriginalApi public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } @OriginalApi public LayoutParams(int width, int height, GeoPoint point, int alignment) { this(width, height, point, 0, 0, alignment); } @OriginalApi public LayoutParams(int width, int height, GeoPoint point, int x, int y, int alignment) { super(width, height); this.point = point; this.x = x; this.y = y; this.alignment = alignment; mode = MODE_MAP; } @OriginalApi public LayoutParams(int width, int height, int x, int y, int alignment) { super(width, height); this.x = x; this.y = y; this.alignment = alignment; mode = MODE_VIEW; } @OriginalApi public LayoutParams(ViewGroup.LayoutParams source) { super(source); if (source instanceof LayoutParams) { alignment = ((LayoutParams) source).alignment; mode = ((LayoutParams) source).mode; point = ((LayoutParams) source).point; x = ((LayoutParams) source).x; y = ((LayoutParams) source).y; } else if (source instanceof org.osmdroid.views.MapView.LayoutParams) { alignment = ((org.osmdroid.views.MapView.LayoutParams) source).alignment; x = ((org.osmdroid.views.MapView.LayoutParams) source).offsetX; y = ((org.osmdroid.views.MapView.LayoutParams) source).offsetY; point = new GeoPoint(((org.osmdroid.views.MapView.LayoutParams) source).geoPoint); if (point != null) { mode = MODE_MAP; } else { mode = MODE_VIEW; } } else { mode = MODE_VIEW; alignment = TOP_LEFT; } } /** * Converts the specified size to a readable String. * <p/> * Is a hidden API method from {@link android.view.ViewGroup}. * * @param size the size to convert * @return a String instance representing the supplied size */ protected static String sizeToString(int size) { if (size == WRAP_CONTENT) { return "wrap-content"; } if (size == MATCH_PARENT) { return "match-parent"; } return String.valueOf(size); } @OriginalApi public String debug(String output) { // We use the same output format as the original Google implementation, although i doubt anybody parses it. return output + "MapView.LayoutParams={" + "width=" + sizeToString(this.width) + ", height=" + sizeToString(this.height) + " mode=" + this.mode + " lat=" + this.point.getLatitudeE6() + " lng=" + this.point.getLongitudeE6() + " x= " + this.x + " y= " + this.y + " alignment=" + this.alignment + "}"; } } public class WrappedMapView extends org.osmdroid.views.MapView { public WrappedMapView(Context context, AttributeSet attrs) { super(context, new CustomResourceProxyImpl(context), new SafeMapTileProviderBasic(context, new SimpleRegisterReceiver(context), new SafeNetworkAvailabilityCheck(context), TileSourceFactory.DEFAULT_TILE_SOURCE), null, attrs); setMultiTouchControls(true); updateTileSource(); } public void updateTileSource() { if (satellite) { setTileSource(SATELLITE); } else if (traffic) { setTileSource(TRAFFIC); } else { setTileSource(DEFAULT); } } @Override public boolean onTouchEvent(MotionEvent event) { return MapView.this.onTouchEvent(event) || super.onTouchEvent(event); } @Override public boolean onTrackballEvent(MotionEvent event) { return MapView.this.onTrackballEvent(event) || super.onTrackballEvent(event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return MapView.this.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return MapView.this.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); } public MapView getOriginal() { return MapView.this; } } }