1package com.xtremelabs.robolectric.shadows;
2
3import android.content.Context;
4import android.graphics.Point;
5import android.view.MotionEvent;
6import android.widget.ZoomButtonsController;
7import com.google.android.maps.GeoPoint;
8import com.google.android.maps.MapController;
9import com.google.android.maps.MapView;
10import com.google.android.maps.Overlay;
11import com.google.android.maps.Projection;
12import com.xtremelabs.robolectric.Robolectric;
13import com.xtremelabs.robolectric.internal.Implementation;
14import com.xtremelabs.robolectric.internal.Implements;
15
16import java.util.ArrayList;
17import java.util.List;
18
19import 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)
27public 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
46    public ShadowMapView(MapView mapView) {
47        realMapView = mapView;
48        zoomButtonsController = new ZoomButtonsController(mapView);
49    }
50
51    public void __constructor__(Context context, String title) {
52        super.__constructor__(context);
53    }
54
55    public static int toE6(double d) {
56        return (int) (d * 1e6);
57    }
58
59    public static double fromE6(int i) {
60        return i / 1e6;
61    }
62
63    @Implementation
64    public void setSatellite(boolean satelliteOn) {
65        this.satelliteOn = satelliteOn;
66    }
67
68    @Implementation
69    public boolean isSatellite() {
70        return satelliteOn;
71    }
72
73    @Implementation
74    public boolean canCoverCenter() {
75        return canCoverCenter;
76    }
77
78    @Implementation
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
93    public ZoomButtonsController getZoomButtonsController() {
94        return zoomButtonsController;
95    }
96
97    @Implementation
98    public void setBuiltInZoomControls(boolean useBuiltInZoomMapControls) {
99        this.useBuiltInZoomMapControls = useBuiltInZoomMapControls;
100    }
101
102    @Implementation
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
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
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
146    public List<Overlay> getOverlays() {
147        return overlays;
148    }
149
150    @Implementation
151    public GeoPoint getMapCenter() {
152        return mapCenter;
153    }
154
155    @Implementation
156    public int getLatitudeSpan() {
157        return latitudeSpan;
158    }
159
160    @Implementation
161    public int getLongitudeSpan() {
162        return longitudeSpan;
163    }
164
165    @Implementation
166    public int getZoomLevel() {
167        return zoomLevel;
168    }
169
170    @Implementation
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
215    public void preLoad() {
216        preLoadWasCalled = true;
217    }
218
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     */
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     */
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     */
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     */
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     */
268    public void setCanCoverCenter(boolean canCoverCenter) {
269        this.canCoverCenter = canCoverCenter;
270    }
271}
272