package org.robolectric.shadows.maps;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ZoomButtonsController;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowViewGroup;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.List;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.shadows.maps.Shadows.shadowOf;
import static org.robolectric.util.ReflectionHelpers.ClassParameter;
@Implements(MapView.class)
public class ShadowMapView extends ShadowViewGroup {
@SuppressWarnings("UnusedDeclaration") @RealObject
private MapView realMapView;
private boolean satelliteOn;
private MapController mapController;
private List<Overlay> overlays = new ArrayList<>();
GeoPoint mapCenter = new GeoPoint(10, 10);
int longitudeSpan = 20;
int latitudeSpan = 30;
int zoomLevel = 1;
private ZoomButtonsController zoomButtonsController;
private Projection projection;
private boolean useBuiltInZoomMapControls;
private boolean mouseDownOnMe = false;
private Point lastTouchEventPoint;
private GeoPoint mouseDownCenter;
private boolean preLoadWasCalled;
private boolean canCoverCenter = true;
public void __constructor__(Context context, AttributeSet attributeSet) {
setContextOnRealView(context);
this.attributeSet = attributeSet;
zoomButtonsController = new ZoomButtonsController(realMapView);
invokeConstructor(View.class, realView,
ClassParameter.from(Context.class, context),
ClassParameter.from(AttributeSet.class, attributeSet),
ClassParameter.from(int.class, 0));
invokeConstructor(ViewGroup.class, realView,
ClassParameter.from(Context.class, context),
ClassParameter.from(AttributeSet.class, attributeSet),
ClassParameter.from(int.class, 0));
}
@Override public void __constructor__(Context context, AttributeSet attributeSet, int defStyle) {
setContextOnRealView(context);
this.attributeSet = attributeSet;
zoomButtonsController = new ZoomButtonsController(realMapView);
invokeConstructor(View.class, realView,
ClassParameter.from(Context.class, context),
ClassParameter.from(AttributeSet.class, attributeSet),
ClassParameter.from(int.class, defStyle));
invokeConstructor(ViewGroup.class, realView,
ClassParameter.from(Context.class, context),
ClassParameter.from(AttributeSet.class, attributeSet),
ClassParameter.from(int.class, defStyle));
super.__constructor__(context, attributeSet, defStyle);
}
public static int toE6(double d) {
return (int) (d * 0x1e6);
}
public static double fromE6(int i) {
return i / 0x1e6;
}
@Implementation // todo 2.0-cleanup
public boolean isOpaque() {
return true;
}
@Implementation // todo 2.0-cleanup
public void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) {
}
@Implementation // todo 2.0-cleanup
public boolean onTouchEvent(MotionEvent event) {
return directlyOn(realView, View.class).onTouchEvent(event);
}
@Implementation
public void setSatellite(boolean satelliteOn) {
this.satelliteOn = satelliteOn;
}
@Implementation
public boolean isSatellite() {
return satelliteOn;
}
@Implementation
public boolean canCoverCenter() {
return canCoverCenter;
}
@Implementation
public MapController getController() {
if (mapController == null) {
try {
mapController = Shadow.newInstanceOf(MapController.class);
ShadowMapController shadowMapController = shadowOf(mapController);
shadowMapController.setShadowMapView(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return mapController;
}
@Implementation
public ZoomButtonsController getZoomButtonsController() {
return zoomButtonsController;
}
@Implementation
public void setBuiltInZoomControls(boolean useBuiltInZoomMapControls) {
this.useBuiltInZoomMapControls = useBuiltInZoomMapControls;
}
@Implementation
public com.google.android.maps.Projection getProjection() {
if (projection == null) {
projection = new Projection() {
@Override public Point toPixels(GeoPoint geoPoint, Point point) {
if (point == null) {
point = new Point();
}
point.y = scaleDegree(geoPoint.getLatitudeE6(), realView.getBottom(), realView.getTop(), mapCenter.getLatitudeE6(), latitudeSpan);
point.x = scaleDegree(geoPoint.getLongitudeE6(), realView.getLeft(), realView.getRight(), mapCenter.getLongitudeE6(), longitudeSpan);
return point;
}
@Override public GeoPoint fromPixels(int x, int y) {
int lat = scalePixel(y, realView.getBottom(), -realMapView.getHeight(), mapCenter.getLatitudeE6(), latitudeSpan);
int lng = scalePixel(x, realView.getLeft(), realMapView.getWidth(), mapCenter.getLongitudeE6(), longitudeSpan);
return new GeoPoint(lat, lng);
}
@Override public float metersToEquatorPixels(float v) {
return 0;
}
};
}
return projection;
}
private int scalePixel(int pixel, int minPixel, int maxPixel, int centerDegree, int spanDegrees) {
int offsetPixels = pixel - minPixel;
double ratio = offsetPixels / ((double) maxPixel);
int minDegrees = centerDegree - spanDegrees / 2;
return (int) (minDegrees + spanDegrees * ratio);
}
private int scaleDegree(int degree, int minPixel, int maxPixel, int centerDegree, int spanDegrees) {
int minDegree = centerDegree - spanDegrees / 2;
int offsetDegrees = degree - minDegree;
double ratio = offsetDegrees / ((double) spanDegrees);
int spanPixels = maxPixel - minPixel;
return (int) (minPixel + spanPixels * ratio);
}
@Implementation
public List<Overlay> getOverlays() {
return overlays;
}
@Implementation
public GeoPoint getMapCenter() {
return mapCenter;
}
@Implementation
public int getLatitudeSpan() {
return latitudeSpan;
}
@Implementation
public int getLongitudeSpan() {
return longitudeSpan;
}
@Implementation
public int getZoomLevel() {
return zoomLevel;
}
@Implementation
public boolean dispatchTouchEvent(MotionEvent event) {
for (Overlay overlay : overlays) {
if (overlay.onTouchEvent(event, realMapView)) {
return true;
}
}
GeoPoint mouseGeoPoint = getProjection().fromPixels((int) event.getX(), (int) event.getY());
int diffX = 0;
int diffY = 0;
if (mouseDownOnMe) {
diffX = (int) event.getX() - lastTouchEventPoint.x;
diffY = (int) event.getY() - lastTouchEventPoint.y;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mouseDownOnMe = true;
mouseDownCenter = getMapCenter();
break;
case MotionEvent.ACTION_MOVE:
if (mouseDownOnMe) {
moveByPixels(-diffX, -diffY);
}
break;
case MotionEvent.ACTION_UP:
if (mouseDownOnMe) {
moveByPixels(-diffX, -diffY);
mouseDownOnMe = false;
}
break;
case MotionEvent.ACTION_CANCEL:
getController().setCenter(mouseDownCenter);
mouseDownOnMe = false;
break;
}
lastTouchEventPoint = new Point((int) event.getX(), (int) event.getY());
return realView.dispatchTouchEvent(event);
}
@Implementation
public void preLoad() {
preLoadWasCalled = true;
}
@Implementation
public void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
private void moveByPixels(int x, int y) {
Point center = getProjection().toPixels(mapCenter, null);
center.offset(x, y);
mapCenter = getProjection().fromPixels(center.x, center.y);
}
/**
* @return whether to use built in zoom map controls
*/
public boolean getUseBuiltInZoomMapControls() {
return useBuiltInZoomMapControls;
}
/**
* @return whether {@link #preLoad()} has been called on this {@code MapView}
*/
public boolean preLoadWasCalled() {
return preLoadWasCalled;
}
/**
* Sets the latitude span (the absolute value of the difference between the Northernmost and
* Southernmost latitudes visible on the map) of this {@code MapView}
*
* @param latitudeSpan the new latitude span for this {@code MapView}
*/
public void setLatitudeSpan(int latitudeSpan) {
this.latitudeSpan = latitudeSpan;
}
/**
* Sets the longitude span (the absolute value of the difference between the Easternmost and
* Westernmost longitude visible on the map) of this {@code MapView}
*
* @param longitudeSpan the new latitude span for this {@code MapView}
*/
public void setLongitudeSpan(int longitudeSpan) {
this.longitudeSpan = longitudeSpan;
}
/**
* Controls the value to be returned by {@link #canCoverCenter()}
*
* @param canCoverCenter the value to be returned by {@link #canCoverCenter()}
*/
public void setCanCoverCenter(boolean canCoverCenter) {
this.canCoverCenter = canCoverCenter;
}
private void setContextOnRealView(Context context) {
ReflectionHelpers.setField(realView, "mContext", context);
}
}