1 package com.xtremelabs.robolectric.shadows;
2 
3 import android.content.Context;
4 import android.graphics.Point;
5 import android.view.MotionEvent;
6 import android.widget.ZoomButtonsController;
7 import com.google.android.maps.GeoPoint;
8 import com.google.android.maps.MapController;
9 import com.google.android.maps.MapView;
10 import com.google.android.maps.Overlay;
11 import com.google.android.maps.Projection;
12 import com.xtremelabs.robolectric.Robolectric;
13 import com.xtremelabs.robolectric.internal.Implementation;
14 import com.xtremelabs.robolectric.internal.Implements;
15 
16 import java.util.ArrayList;
17 import java.util.List;
18 
19 import static com.xtremelabs.robolectric.RobolectricForMaps.shadowOf;
20 
21 /**
22  * Shadow of {@code MapView} that simulates the internal state of a {@code MapView}. Supports {@code Projection}s,
23  * {@code Overlay}s, and {@code TouchEvent}s
24  */
25 @SuppressWarnings({"UnusedDeclaration"})
26 @Implements(MapView.class)
27 public class ShadowMapView extends ShadowViewGroup {
28     private boolean satelliteOn;
29     private MapController mapController;
30     private List<Overlay> overlays = new ArrayList<Overlay>();
31     GeoPoint mapCenter = new GeoPoint(10, 10);
32     int longitudeSpan = 20;
33     int latitudeSpan = 30;
34     int zoomLevel = 1;
35     private ShadowMapController shadowMapController;
36     private ZoomButtonsController zoomButtonsController;
37     private MapView realMapView;
38     private Projection projection;
39     private boolean useBuiltInZoomMapControls;
40     private boolean mouseDownOnMe = false;
41     private Point lastTouchEventPoint;
42     private GeoPoint mouseDownCenter;
43     private boolean preLoadWasCalled;
44     private boolean canCoverCenter = true;
45 
ShadowMapView(MapView mapView)46     public ShadowMapView(MapView mapView) {
47         realMapView = mapView;
48         zoomButtonsController = new ZoomButtonsController(mapView);
49     }
50 
__constructor__(Context context, String title)51     public void __constructor__(Context context, String title) {
52         super.__constructor__(context);
53     }
54 
toE6(double d)55     public static int toE6(double d) {
56         return (int) (d * 1e6);
57     }
58 
fromE6(int i)59     public static double fromE6(int i) {
60         return i / 1e6;
61     }
62 
63     @Implementation
setSatellite(boolean satelliteOn)64     public void setSatellite(boolean satelliteOn) {
65         this.satelliteOn = satelliteOn;
66     }
67 
68     @Implementation
isSatellite()69     public boolean isSatellite() {
70         return satelliteOn;
71     }
72 
73     @Implementation
canCoverCenter()74     public boolean canCoverCenter() {
75         return canCoverCenter;
76     }
77 
78     @Implementation
getController()79     public MapController getController() {
80         if (mapController == null) {
81             try {
82                 mapController = Robolectric.newInstanceOf(MapController.class);
83                 shadowMapController = shadowOf(mapController);
84                 shadowMapController.setShadowMapView(this);
85             } catch (Exception e) {
86                 throw new RuntimeException(e);
87             }
88         }
89         return mapController;
90     }
91 
92     @Implementation
getZoomButtonsController()93     public ZoomButtonsController getZoomButtonsController() {
94         return zoomButtonsController;
95     }
96 
97     @Implementation
setBuiltInZoomControls(boolean useBuiltInZoomMapControls)98     public void setBuiltInZoomControls(boolean useBuiltInZoomMapControls) {
99         this.useBuiltInZoomMapControls = useBuiltInZoomMapControls;
100     }
101 
102     @Implementation
getProjection()103     public com.google.android.maps.Projection getProjection() {
104         if (projection == null) {
105             projection = new Projection() {
106                 @Override public Point toPixels(GeoPoint geoPoint, Point point) {
107                     if (point == null) {
108                         point = new Point();
109                     }
110 
111                     point.y = scaleDegree(geoPoint.getLatitudeE6(), bottom, top, mapCenter.getLatitudeE6(), latitudeSpan);
112                     point.x = scaleDegree(geoPoint.getLongitudeE6(), left, right, mapCenter.getLongitudeE6(), longitudeSpan);
113                     return point;
114                 }
115 
116                 @Override public GeoPoint fromPixels(int x, int y) {
117                     int lat = scalePixel(y, bottom, -realMapView.getHeight(), mapCenter.getLatitudeE6(), latitudeSpan);
118                     int lng = scalePixel(x, left, realMapView.getWidth(), mapCenter.getLongitudeE6(), longitudeSpan);
119                     return new GeoPoint(lat, lng);
120                 }
121 
122                 @Override public float metersToEquatorPixels(float v) {
123                     return 0;
124                 }
125             };
126         }
127         return projection;
128     }
129 
scalePixel(int pixel, int minPixel, int maxPixel, int centerDegree, int spanDegrees)130     private int scalePixel(int pixel, int minPixel, int maxPixel, int centerDegree, int spanDegrees) {
131         int offsetPixels = pixel - minPixel;
132         double ratio = offsetPixels / ((double) maxPixel);
133         int minDegrees = centerDegree - spanDegrees / 2;
134         return (int) (minDegrees + spanDegrees * ratio);
135     }
136 
scaleDegree(int degree, int minPixel, int maxPixel, int centerDegree, int spanDegrees)137     private int scaleDegree(int degree, int minPixel, int maxPixel, int centerDegree, int spanDegrees) {
138         int minDegree = centerDegree - spanDegrees / 2;
139         int offsetDegrees = degree - minDegree;
140         double ratio = offsetDegrees / ((double) spanDegrees);
141         int spanPixels = maxPixel - minPixel;
142         return (int) (minPixel + spanPixels * ratio);
143     }
144 
145     @Implementation
getOverlays()146     public List<Overlay> getOverlays() {
147         return overlays;
148     }
149 
150     @Implementation
getMapCenter()151     public GeoPoint getMapCenter() {
152         return mapCenter;
153     }
154 
155     @Implementation
getLatitudeSpan()156     public int getLatitudeSpan() {
157         return latitudeSpan;
158     }
159 
160     @Implementation
getLongitudeSpan()161     public int getLongitudeSpan() {
162         return longitudeSpan;
163     }
164 
165     @Implementation
getZoomLevel()166     public int getZoomLevel() {
167         return zoomLevel;
168     }
169 
170     @Implementation
dispatchTouchEvent(MotionEvent event)171     @Override public boolean dispatchTouchEvent(MotionEvent event) {
172         for (Overlay overlay : overlays) {
173             if (overlay.onTouchEvent(event, realMapView)) {
174                 return true;
175             }
176         }
177 
178         GeoPoint mouseGeoPoint = getProjection().fromPixels((int) event.getX(), (int) event.getY());
179         int diffX = 0;
180         int diffY = 0;
181         if (mouseDownOnMe) {
182             diffX = (int) event.getX() - lastTouchEventPoint.x;
183             diffY = (int) event.getY() - lastTouchEventPoint.y;
184         }
185 
186         switch (event.getAction()) {
187             case MotionEvent.ACTION_DOWN:
188                 mouseDownOnMe = true;
189                 mouseDownCenter = getMapCenter();
190                 break;
191             case MotionEvent.ACTION_MOVE:
192                 if (mouseDownOnMe) {
193                     moveByPixels(-diffX, -diffY);
194                 }
195                 break;
196             case MotionEvent.ACTION_UP:
197                 if (mouseDownOnMe) {
198                     moveByPixels(-diffX, -diffY);
199                     mouseDownOnMe = false;
200                 }
201                 break;
202 
203             case MotionEvent.ACTION_CANCEL:
204                 getController().setCenter(mouseDownCenter);
205                 mouseDownOnMe = false;
206                 break;
207         }
208 
209         lastTouchEventPoint = new Point((int) event.getX(), (int) event.getY());
210 
211         return super.dispatchTouchEvent(event);
212     }
213 
214     @Implementation
preLoad()215     public void preLoad() {
216         preLoadWasCalled = true;
217     }
218 
moveByPixels(int x, int y)219     private void moveByPixels(int x, int y) {
220         Point center = getProjection().toPixels(mapCenter, null);
221         center.offset(x, y);
222         mapCenter = getProjection().fromPixels(center.x, center.y);
223     }
224 
225     /**
226      * Non-Android accessor.
227      *
228      * @return whether to use built in zoom map controls
229      */
getUseBuiltInZoomMapControls()230     public boolean getUseBuiltInZoomMapControls() {
231         return useBuiltInZoomMapControls;
232     }
233 
234     /**
235      * Non-Android accessor.
236      *
237      * @return whether {@link #preLoad()} has been called on this {@code MapView}
238      */
preLoadWasCalled()239     public boolean preLoadWasCalled() {
240         return preLoadWasCalled;
241     }
242 
243     /**
244      * Non-Android accessor to set the latitude span (the absolute value of the difference between the Northernmost and
245      * Southernmost latitudes visible on the map) of this {@code MapView}
246      *
247      * @param latitudeSpan the new latitude span for this {@code MapView}
248      */
setLatitudeSpan(int latitudeSpan)249     public void setLatitudeSpan(int latitudeSpan) {
250         this.latitudeSpan = latitudeSpan;
251     }
252 
253     /**
254      * Non-Android accessor to set the longitude span (the absolute value of the difference between the Easternmost and
255      * Westernmost longitude visible on the map) of this {@code MapView}
256      *
257      * @param longitudeSpan the new latitude span for this {@code MapView}
258      */
setLongitudeSpan(int longitudeSpan)259     public void setLongitudeSpan(int longitudeSpan) {
260         this.longitudeSpan = longitudeSpan;
261     }
262 
263     /**
264      * Non-Android accessor that controls the value to be returned by {@link #canCoverCenter()}
265      *
266      * @param canCoverCenter the value to be returned by {@link #canCoverCenter()}
267      */
setCanCoverCenter(boolean canCoverCenter)268     public void setCanCoverCenter(boolean canCoverCenter) {
269         this.canCoverCenter = canCoverCenter;
270     }
271 }
272