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 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