From 0a864310c94c36569889a657772d0a4cc9b100f1 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Thu, 9 Aug 2018 18:28:52 -0700 Subject: [PATCH 1/7] Unify OsmMap and GoogleMap geoshape activities into GeoShapeActivity. (Defines the MapFragment interface and moves Google-specific and OSM-specific implementations into GoogleMapFragment and OsmMapFragment.) --- collect_app/build.gradle | 6 +- collect_app/src/main/AndroidManifest.xml | 8 +- .../android/activities/GeoShapeActivity.java | 324 ++++++++++ .../activities/GeoShapeGoogleMapActivity.java | 470 -------------- .../activities/GeoShapeOsmMapActivity.java | 590 ------------------ .../android/fragments/OsmMapFragment.java | 46 -- .../android/map/GoogleMapFragment.java | 393 ++++++++++++ .../odk/collect/android/map/MapFragment.java | 137 ++++ .../org/odk/collect/android/map/MapPoint.java | 72 +++ .../collect/android/map/OsmMapFragment.java | 343 ++++++++++ .../collect/android/spatial/MapHelper.java | 6 +- .../android/widgets/GeoShapeWidget.java | 27 +- .../activities/GeoShapeActivityTest.java | 87 +++ .../GeoShapeGoogleMapActivityTest.java | 106 ---- 14 files changed, 1375 insertions(+), 1240 deletions(-) create mode 100644 collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java delete mode 100644 collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeGoogleMapActivity.java delete mode 100644 collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOsmMapActivity.java delete mode 100644 collect_app/src/main/java/org/odk/collect/android/fragments/OsmMapFragment.java create mode 100644 collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java create mode 100644 collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java create mode 100644 collect_app/src/main/java/org/odk/collect/android/map/MapPoint.java create mode 100644 collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java create mode 100644 collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java delete mode 100644 collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeGoogleMapActivityTest.java diff --git a/collect_app/build.gradle b/collect_app/build.gradle index ac8fe5ed7d6..39c4432a53f 100644 --- a/collect_app/build.gradle +++ b/collect_app/build.gradle @@ -157,7 +157,7 @@ android { returnDefaultValues = true all { // https://discuss.circleci.com/t/11207/24 - // it seems any number works, but 1024 - 2048 seem reasonable + // it seems any number works, but 1024 - 2048 seem reasonable maxHeapSize = "2048M" } } @@ -273,6 +273,10 @@ dependencies { implementation "com.jakewharton:butterknife:8.8.1" annotationProcessor "com.jakewharton:butterknife-compiler:8.8.1" + // Annotations understood by FindBugs + compileOnly 'com.google.code.findbugs:annotations:3.0.1' + compileOnly 'com.google.code.findbugs:jsr305:3.0.2' + // Used to generate documentation screenshots. androidTestImplementation "tools.fastlane:screengrab:1.1.0" diff --git a/collect_app/src/main/AndroidManifest.xml b/collect_app/src/main/AndroidManifest.xml index b60d98af389..7d994a14c5a 100644 --- a/collect_app/src/main/AndroidManifest.xml +++ b/collect_app/src/main/AndroidManifest.xml @@ -205,10 +205,7 @@ the specific language governing permissions and limitations under the License. android:name=".activities.GeoPointOsmMapActivity" android:configChanges="orientation" /> - - - \ No newline at end of file + diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java new file mode 100644 index 00000000000..9468bd94b2f --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2016 Nafundi + * + * 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 org.odk.collect.android.activities; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.support.annotation.VisibleForTesting; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.ImageButton; + +import org.odk.collect.android.R; +import org.odk.collect.android.application.Collect; +import org.odk.collect.android.map.GoogleMapFragment; +import org.odk.collect.android.map.MapFragment; +import org.odk.collect.android.map.MapPoint; +import org.odk.collect.android.map.OsmMapFragment; +import org.odk.collect.android.preferences.PreferenceKeys; +import org.odk.collect.android.spatial.MapHelper; +import org.odk.collect.android.utilities.ToastUtils; +import org.odk.collect.android.widgets.GeoShapeWidget; +import org.osmdroid.tileprovider.IRegisterReceiver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.odk.collect.android.utilities.PermissionUtils.checkIfLocationPermissionsGranted; + +/** Activity for entering or editing a polygon on a map. */ +public class GeoShapeActivity extends CollectAbstractActivity implements IRegisterReceiver { + public static final String PREF_VALUE_GOOGLE_MAPS = "google_maps"; + + private MapFragment map; + private int shapeId = -1; // will be a positive featureId once map is ready + private ImageButton gpsButton; + private ImageButton clearButton; + + private MapHelper helper; + private AlertDialog zoomDialog; + private View zoomDialogView; + private Button zoomPointButton; + private Button zoomLocationButton; + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!checkIfLocationPermissionsGranted(this)) { + finish(); + return; + } + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setTitle(getString(R.string.geoshape_title)); + setContentView(R.layout.geoshape_layout); + createMapFragment().addTo(this, R.id.map_container, this::initMap); + } + + public MapFragment createMapFragment() { + String mapSdk = getIntent().getStringExtra(PreferenceKeys.KEY_MAP_SDK); + return (mapSdk == null || mapSdk.equals(PREF_VALUE_GOOGLE_MAPS)) ? + new GoogleMapFragment() : new OsmMapFragment(); + } + + @Override protected void onStart() { + super.onStart(); + Collect.getInstance().getActivityLogger().logOnStart(this); + if (map != null) { + map.setGpsLocationEnabled(true); + } + } + + @Override protected void onStop() { + map.setGpsLocationEnabled(false); + Collect.getInstance().getActivityLogger().logOnStop(this); + super.onStop(); + } + + @Override public void onBackPressed() { + if (!map.getPointsOfShape(shapeId).isEmpty()) { + showBackDialog(); + } else { + finish(); + } + } + + @Override public void destroy() { } + + private void initMap(MapFragment newMapFragment) { + if (newMapFragment == null) { + finish(); + return; + } + + map = newMapFragment; + map.setLongPressListener(this::addVertex); + + if (map instanceof GoogleMapFragment) { + helper = new MapHelper(this, ((GoogleMapFragment) map).getGoogleMap()); + } else if (map instanceof OsmMapFragment) { + helper = new MapHelper(this, ((OsmMapFragment) map).getMapView(), this); + } + helper.setBasemap(); + + gpsButton = findViewById(R.id.gps); + gpsButton.setOnClickListener(v -> showZoomDialog()); + + clearButton = findViewById(R.id.clear); + clearButton.setOnClickListener(v -> showClearDialog()); + + ImageButton saveButton = findViewById(R.id.save); + saveButton.setOnClickListener(v -> finishWithResult()); + + ImageButton layersButton = findViewById(R.id.layers); + layersButton.setOnClickListener(v -> helper.showLayersDialog()); + + List points = new ArrayList<>(); + Intent intent = getIntent(); + if (intent != null && intent.hasExtra(GeoShapeWidget.SHAPE_LOCATION)) { + points = parsePoints(intent.getStringExtra(GeoShapeWidget.SHAPE_LOCATION)); + } + shapeId = map.addDraggableShape(points); + gpsButton.setEnabled(!points.isEmpty()); + clearButton.setEnabled(!points.isEmpty()); + + zoomDialogView = getLayoutInflater().inflate(R.layout.geo_zoom_dialog, null); + + zoomLocationButton = zoomDialogView.findViewById(R.id.zoom_location); + zoomLocationButton.setOnClickListener(v -> { + MapPoint location = map.getGpsLocation(); + if (location != null) { + map.setCenter(location); + map.setZoom(15); + } + zoomDialog.dismiss(); + }); + + zoomPointButton = zoomDialogView.findViewById(R.id.zoom_saved_location); + zoomPointButton.setOnClickListener(v -> { + map.zoomToBoundingBox(map.getPointsOfShape(shapeId), 0.8); + zoomDialog.dismiss(); + }); + + map.setGpsLocationEnabled(true); + if (!points.isEmpty()) { + map.zoomToBoundingBox(points, 0.8); + } else { + map.runOnGpsLocationReady(this::onGpsLocationReady); + } + } + + @SuppressWarnings("unused") + private void onGpsLocationReady(MapFragment map) { + gpsButton.setEnabled(true); + if (getWindow().isActive()) { + showZoomDialog(); + } + } + + private void addVertex(MapPoint point) { + map.appendPointToShape(shapeId, point); + clearButton.setEnabled(true); + } + + private void clear() { + map.clearFeatures(); + shapeId = map.addDraggableShape(new ArrayList<>()); + clearButton.setEnabled(false); + } + + private void showClearDialog() { + if (!map.getPointsOfShape(shapeId).isEmpty()) { + new AlertDialog.Builder(this) + .setMessage(R.string.geo_clear_warning) + .setPositiveButton(R.string.clear, (dialog, id) -> clear()) + .setNegativeButton(R.string.cancel, null) + .show(); + } + } + + private void showBackDialog() { + new AlertDialog.Builder(this) + .setMessage(getString(R.string.geo_exit_warning)) + .setPositiveButton(R.string.discard, (dialog, id) -> finish()) + .setNegativeButton(R.string.cancel, null) + .show(); + + } + + private void finishWithResult() { + List points = map.getPointsOfShape(shapeId); + // Allow an empty result (no points), or a polygon with at least 3 points, + // but no degenerate 1-point or 2-point polygons. + if (!points.isEmpty() && points.size() < 3) { + ToastUtils.showShortToastInMiddle(getString(R.string.polygon_validator)); + return; + } + + setResult(RESULT_OK, new Intent().putExtra( + FormEntryActivity.GEOSHAPE_RESULTS, formatPoints(points))); + finish(); + } + + /** + * Parses a form result string, as previously formatted by formatPoints, + * into a list of polygon vertices. + */ + private List parsePoints(String coords) { + List points = new ArrayList<>(); + for (String vertex : (coords == null ? "" : coords).split(";")) { + String[] words = vertex.trim().split(" "); + if (words.length >= 2) { + double lat; + double lon; + try { + lat = Double.parseDouble(words[0]); + lon = Double.parseDouble(words[1]); + } catch (NumberFormatException e) { + continue; + } + points.add(new MapPoint(lat, lon)); + } + } + // Polygons are stored with a last point that duplicates the first + // point. To prepare the polygon for display and editing, we need + // to remove this duplicate point. + int count = points.size(); + if (count > 1 && points.get(0).equals(points.get(count - 1))) { + points.remove(count - 1); + } + return points; + } + + /** + * Serializes a list of polygon vertices into a string, in the format + * appropriate for storing as the result of this form question. + */ + private String formatPoints(List points) { + String result = ""; + if (points.size() > 1) { + // Polygons are stored with a last point that duplicates the + // first point. Add this extra point if it's not already present. + if (!points.get(0).equals(points.get(points.size() - 1))) { + points.add(points.get(0)); + } + for (MapPoint point : points) { + // TODO(ping): Remove excess precision when we're ready for the output to change. + result += String.format(Locale.US, "%s %s 0.0 0.0;", + Double.toString(point.lat), Double.toString(point.lon)); + } + } + return result.trim(); + } + + private void showZoomDialog() { + if (zoomDialog == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.zoom_to_where)); + builder.setView(zoomDialogView) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + dialog.cancel(); + zoomDialog.dismiss(); + } + }); + zoomDialog = builder.create(); + } + + if (map.getGpsLocation() != null) { + zoomLocationButton.setEnabled(true); + zoomLocationButton.setBackgroundColor(Color.parseColor("#50cccccc")); + zoomLocationButton.setTextColor(themeUtils.getPrimaryTextColor()); + } else { + zoomLocationButton.setEnabled(false); + zoomLocationButton.setBackgroundColor(Color.parseColor("#50e2e2e2")); + zoomLocationButton.setTextColor(Color.parseColor("#FF979797")); + } + + if (!map.getPointsOfShape(shapeId).isEmpty()) { + zoomPointButton.setEnabled(true); + zoomPointButton.setBackgroundColor(Color.parseColor("#50cccccc")); + zoomPointButton.setTextColor(themeUtils.getPrimaryTextColor()); + } else { + zoomPointButton.setEnabled(false); + zoomPointButton.setBackgroundColor(Color.parseColor("#50e2e2e2")); + zoomPointButton.setTextColor(Color.parseColor("#FF979797")); + } + zoomDialog.show(); + } + + @VisibleForTesting public boolean isGpsButtonEnabled() { + return gpsButton != null && gpsButton.isEnabled(); + } + + @VisibleForTesting public boolean isZoomDialogShowing() { + return zoomDialog != null && zoomDialog.isShowing(); + } + + @VisibleForTesting public MapFragment getMapFragment() { + return map; + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeGoogleMapActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeGoogleMapActivity.java deleted file mode 100644 index 7f2da1a522a..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeGoogleMapActivity.java +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Copyright (C) 2016 Nafundi - * - * 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 org.odk.collect.android.activities; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Color; -import android.location.Location; -import android.os.Bundle; -import android.os.Handler; -import android.provider.Settings; -import android.view.View; -import android.view.Window; -import android.widget.Button; -import android.widget.ImageButton; - -import com.google.android.gms.location.LocationListener; -import com.google.android.gms.maps.CameraUpdate; -import com.google.android.gms.maps.CameraUpdateFactory; -import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener; -import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener; -import com.google.android.gms.maps.SupportMapFragment; -import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.LatLngBounds; -import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.MarkerOptions; -import com.google.android.gms.maps.model.Polygon; -import com.google.android.gms.maps.model.PolygonOptions; - -import org.odk.collect.android.R; -import org.odk.collect.android.application.Collect; -import org.odk.collect.android.location.client.LocationClient; -import org.odk.collect.android.location.client.LocationClients; -import org.odk.collect.android.spatial.MapHelper; -import org.odk.collect.android.utilities.ToastUtils; -import org.odk.collect.android.widgets.GeoShapeWidget; - -import java.util.ArrayList; -import java.util.Collections; - -import static org.odk.collect.android.utilities.PermissionUtils.checkIfLocationPermissionsGranted; - -/** - * Version of the GeoShapeGoogleMapActivity that uses the new Maps v2 API and Fragments to enable - * specifying a location via placing a tracker on a map. - * - * @author jonnordling@gmail.com - */ - -public class GeoShapeGoogleMapActivity extends CollectAbstractActivity implements LocationListener, - OnMarkerDragListener, OnMapLongClickListener, LocationClient.LocationClientListener { - - private LocationClient locationClient; - - private GoogleMap map; - private Location curLocation; - private LatLng curlatLng; - private PolygonOptions polygonOptions; - private Polygon polygon; - private final ArrayList markerArray = new ArrayList(); - private ImageButton gpsButton; - private ImageButton clearButton; - - private MapHelper helper; - private AlertDialog zoomDialog; - private AlertDialog errorDialog; - private View zoomDialogView; - private Button zoomPointButton; - private Button zoomLocationButton; - private boolean foundFirstLocation; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (!checkIfLocationPermissionsGranted(this)) { - finish(); - return; - } - - requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.geoshape_layout); - SupportMapFragment mapFragment = new SupportMapFragment(); - getSupportFragmentManager().beginTransaction() - .add(R.id.map_container, mapFragment).commit(); - mapFragment.getMapAsync(this::setupMap); - - gpsButton = findViewById(R.id.gps); - gpsButton.setEnabled(false); - } - - @Override - protected void onStart() { - super.onStart(); - Collect.getInstance().getActivityLogger().logOnStart(this); - - locationClient = LocationClients.clientForContext(this); - locationClient.setListener(this); - locationClient.start(); - } - - @Override - protected void onStop() { - locationClient.stop(); - - Collect.getInstance().getActivityLogger().logOnStop(this); - super.onStop(); - } - - private void setupMap(GoogleMap googleMap) { - map = googleMap; - if (map == null) { - ToastUtils.showShortToast(R.string.google_play_services_error_occured); - finish(); - return; - } - helper = new MapHelper(this, map); - map.setMyLocationEnabled(true); - map.setOnMapLongClickListener(this); - map.setOnMarkerDragListener(this); - map.getUiSettings().setMyLocationButtonEnabled(false); - map.getUiSettings().setCompassEnabled(true); - map.getUiSettings().setZoomControlsEnabled(false); - - polygonOptions = new PolygonOptions(); - polygonOptions.strokeColor(Color.RED); - polygonOptions.zIndex(1); - - gpsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // if(curLocation !=null){ - // map.animateCamera(CameraUpdateFactory.newLatLngZoom(curlatLng,16)); - // } - showZoomDialog(); - } - }); - - clearButton = findViewById(R.id.clear); - clearButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (!markerArray.isEmpty()) { - showClearDialog(); - } - } - }); - ImageButton returnButton = findViewById(R.id.save); - returnButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - returnLocation(); - } - }); - - Intent intent = getIntent(); - if (intent != null && intent.getExtras() != null) { - if (intent.hasExtra(GeoShapeWidget.SHAPE_LOCATION)) { - clearButton.setEnabled(true); - String s = intent.getStringExtra(GeoShapeWidget.SHAPE_LOCATION); - gpsButton.setEnabled(true); - overlayIntentPolygon(s); - } - } - - ImageButton layersButton = findViewById(R.id.layers); - layersButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - helper.showLayersDialog(); - } - }); - - zoomDialogView = getLayoutInflater().inflate(R.layout.geo_zoom_dialog, null); - - zoomLocationButton = zoomDialogView.findViewById(R.id.zoom_location); - zoomLocationButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (curLocation != null && curlatLng != null) { - map.animateCamera(CameraUpdateFactory.newLatLngZoom(curlatLng, 17)); - } - zoomDialog.dismiss(); - } - }); - - zoomPointButton = zoomDialogView.findViewById(R.id.zoom_saved_location); - zoomPointButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // zoomToCentroid(); - zoomToBounds(); - zoomDialog.dismiss(); - } - }); - // If there is a last know location go there - if (curLocation != null) { - curlatLng = new LatLng(curLocation.getLatitude(), curLocation.getLongitude()); - foundFirstLocation = true; - gpsButton.setEnabled(true); - showZoomDialog(); - } - - helper.setBasemap(); - } - - private void returnLocation() { - String finalReturnString = generateReturnString(); - Intent i = new Intent(); - i.putExtra( - FormEntryActivity.GEOSHAPE_RESULTS, - finalReturnString); - setResult(RESULT_OK, i); - if (markerArray.size() < 4) { - ToastUtils.showShortToastInMiddle(getString(R.string.polygon_validator)); - } else { - finish(); - } - } - - private void overlayIntentPolygon(String str) { - map.setOnMapLongClickListener(null); - clearButton.setEnabled(true); - String s = str.replace("; ", ";"); - String[] sa = s.split(";"); - for (int i = 0; i < (sa.length - 1); i++) { - String[] sp = sa[i].split(" "); - double[] gp = new double[4]; - String lat = sp[0].replace(" ", ""); - String lng = sp[1].replace(" ", ""); - gp[0] = Double.parseDouble(lat); - gp[1] = Double.parseDouble(lng); - LatLng point = new LatLng(gp[0], gp[1]); - polygonOptions.add(point); - MarkerOptions markerOptions = new MarkerOptions().position(point).draggable(true); - Marker marker = map.addMarker(markerOptions); - markerArray.add(marker); - } - polygon = map.addPolygon(polygonOptions); - update_polygon(); - - } - - private String generateReturnString() { - String tempString = ""; - //Add the first marker to the end of the array, so the first and the last are the same - if (markerArray.size() > 1) { - if (Collections.frequency(markerArray, markerArray.get(0)) < 2) { - markerArray.add(markerArray.get(0)); - } - for (int i = 0; i < markerArray.size(); i++) { - String lat = Double.toString(markerArray.get(i).getPosition().latitude); - String lng = Double.toString(markerArray.get(i).getPosition().longitude); - String alt = "0.0"; - String acu = "0.0"; - tempString = tempString + lat + " " + lng + " " + alt + " " + acu + ";"; - } - } - return tempString; - } - - @Override - public void onLocationChanged(Location location) { - // If there is a location allow for user to be able to fly there - gpsButton.setEnabled(true); - curLocation = location; - curlatLng = new LatLng(curLocation.getLatitude(), curLocation.getLongitude()); - - if (!foundFirstLocation) { - showZoomDialog(); - foundFirstLocation = true; - } - } - - private void update_polygon() { - ArrayList tempLat = new ArrayList(); - for (int i = 0; i < markerArray.size(); i++) { - LatLng latLng = markerArray.get(i).getPosition(); - tempLat.add(latLng); - } - polygon.setPoints(tempLat); - } - - @Override - public void onMapLongClick(LatLng latLng) { - MarkerOptions markerOptions = new MarkerOptions().position(latLng).draggable(true); - Marker marker = map.addMarker(markerOptions); - markerArray.add(marker); - - if (polygon == null) { - clearButton.setEnabled(true); - polygonOptions.add(latLng); - polygon = map.addPolygon(polygonOptions); - } else { - update_polygon(); - } - } - - @Override - public void onMarkerDragStart(Marker marker) { - update_polygon(); - } - - @Override - public void onMarkerDrag(Marker marker) { - update_polygon(); - } - - @Override - public void onMarkerDragEnd(Marker marker) { - update_polygon(); - } - - private void zoomToBounds() { - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - public void run() { - LatLngBounds.Builder builder = new LatLngBounds.Builder(); - for (Marker marker : markerArray) { - builder.include(marker.getPosition()); - } - LatLngBounds bounds = builder.build(); - int padding = 200; // offset from edges of the map in pixels - CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, padding); - map.animateCamera(cu); - } - }, 100); - } - - private void clearFeatures() { - map.clear(); - polygon = null; - polygonOptions = new PolygonOptions(); - polygonOptions.strokeColor(Color.RED); - polygonOptions.zIndex(1); - markerArray.clear(); - map.setOnMapLongClickListener(this); - - } - - private void showClearDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.geo_clear_warning)) - .setPositiveButton(getString(R.string.clear), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - clearFeatures(); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // User cancelled the dialog - - } - }).show(); - - } - - public void showZoomDialog() { - if (zoomDialog == null) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.zoom_to_where)); - builder.setView(zoomDialogView) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.cancel(); - zoomDialog.dismiss(); - } - }); - zoomDialog = builder.create(); - } - - if (zoomLocationButton != null) { - if (curLocation != null) { - zoomLocationButton.setEnabled(true); - zoomLocationButton.setBackgroundColor(Color.parseColor("#50cccccc")); - zoomLocationButton.setTextColor(themeUtils.getPrimaryTextColor()); - } else { - zoomLocationButton.setEnabled(false); - zoomLocationButton.setBackgroundColor(Color.parseColor("#50e2e2e2")); - zoomLocationButton.setTextColor(Color.parseColor("#FF979797")); - } - - if (!markerArray.isEmpty()) { - zoomPointButton.setEnabled(true); - zoomPointButton.setBackgroundColor(Color.parseColor("#50cccccc")); - zoomPointButton.setTextColor(themeUtils.getPrimaryTextColor()); - } else { - zoomPointButton.setEnabled(false); - zoomPointButton.setBackgroundColor(Color.parseColor("#50e2e2e2")); - zoomPointButton.setTextColor(Color.parseColor("#FF979797")); - } - } - - zoomDialog.show(); - } - - private void showGPSDisabledAlertToUser() { - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); - alertDialogBuilder.setMessage(getString(R.string.gps_enable_message)) - .setCancelable(false) - .setPositiveButton(getString(R.string.enable_gps), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - startActivityForResult( - new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0); - } - }); - alertDialogBuilder.setNegativeButton(getString(R.string.cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - errorDialog = alertDialogBuilder.create(); - errorDialog.show(); - } - - @Override - public void onClientStart() { - locationClient.requestLocationUpdates(this); - if (!locationClient.isLocationAvailable()) { - showGPSDisabledAlertToUser(); - } else { - gpsButton.setEnabled(true); - } - } - - @Override - public void onClientStartFailure() { - showGPSDisabledAlertToUser(); - } - - @Override - public void onClientStop() { - - } - - public ImageButton getGpsButton() { - return gpsButton; - } - - public AlertDialog getZoomDialog() { - return zoomDialog; - } - - public AlertDialog getErrorDialog() { - return errorDialog; - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOsmMapActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOsmMapActivity.java deleted file mode 100644 index e2daf1bbcef..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOsmMapActivity.java +++ /dev/null @@ -1,590 +0,0 @@ -/* - * Copyright (C) 2016 GeoODK - * - * 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 org.odk.collect.android.activities; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Color; -import android.graphics.Paint; -import android.location.LocationManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.support.v4.content.ContextCompat; -import android.view.View; -import android.view.Window; -import android.widget.Button; -import android.widget.ImageButton; - -import org.odk.collect.android.R; -import org.odk.collect.android.fragments.OsmMapFragment; -import org.odk.collect.android.spatial.MapHelper; -import org.odk.collect.android.utilities.ToastUtils; -import org.odk.collect.android.widgets.GeoShapeWidget; -import org.osmdroid.events.MapEventsReceiver; -import org.osmdroid.events.MapListener; -import org.osmdroid.events.ScrollEvent; -import org.osmdroid.events.ZoomEvent; -import org.osmdroid.tileprovider.IRegisterReceiver; -import org.osmdroid.util.BoundingBox; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.MapEventsOverlay; -import org.osmdroid.views.overlay.Marker; -import org.osmdroid.views.overlay.Polyline; -import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; -import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static org.odk.collect.android.utilities.PermissionUtils.checkIfLocationPermissionsGranted; - -/** - * Version of the GeoPointMapActivity that uses the new Maps v2 API and Fragments to enable - * specifying a location via placing a tracker on a map. - * - * @author jonnordling@gmail.com - */ - -public class GeoShapeOsmMapActivity extends CollectAbstractActivity implements IRegisterReceiver { - private MapView map; - private final ArrayList mapMarkers = new ArrayList(); - private Polyline polyline; - public int zoomLevel = 3; - public static final int STROKE_WIDTH = 5; - public String finalReturnString; - private MapEventsOverlay overlayEvents; - private boolean clearButtonTest; - private ImageButton clearButton; - public boolean gpsStatus = true; - private ImageButton locationButton; - public MyLocationNewOverlay myLocationOverlay; - public boolean dataLoaded; - - private MapHelper helper; - - private AlertDialog zoomDialog; - private View zoomDialogView; - - private Button zoomPointButton; - private Button zoomLocationButton; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (!checkIfLocationPermissionsGranted(this)) { - finish(); - return; - } - - requestWindowFeature(Window.FEATURE_NO_TITLE); - setTitle(getString(R.string.geoshape_title)); - setContentView(R.layout.geoshape_layout); - OsmMapFragment mapFragment = new OsmMapFragment(); - getSupportFragmentManager().beginTransaction() - .add(R.id.map_container, mapFragment).commit(); - mapFragment.getMapAsync(this::setupMap); - } - - private void setupMap(MapView map) { - this.map = map; - helper = new MapHelper(this, map, this); - map.setMultiTouchControls(true); - map.setBuiltInZoomControls(true); - map.setTilesScaledToDpi(true); - map.setMapListener(mapViewListener); - overlayPointPathListener(); - ImageButton saveButton = findViewById(R.id.save); - clearButton = findViewById(R.id.clear); - saveButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - returnLocation(); - } - }); - clearButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (!mapMarkers.isEmpty()) { - showClearDialog(); - } - } - }); - ImageButton layersButton = findViewById(R.id.layers); - layersButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - helper.showLayersDialog(); - - } - }); - locationButton = findViewById(R.id.gps); - locationButton.setEnabled(false); - locationButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View v) { - showZoomDialog(); - } - }); - - GpsMyLocationProvider imlp = new GpsMyLocationProvider(this.getBaseContext()); - imlp.setLocationUpdateMinDistance(1000); - imlp.setLocationUpdateMinTime(60000); - myLocationOverlay = new MyLocationNewOverlay(map); - - Intent intent = getIntent(); - if (intent != null && intent.getExtras() != null) { - if (intent.hasExtra(GeoShapeWidget.SHAPE_LOCATION)) { - clearButton.setEnabled(true); - dataLoaded = true; - String s = intent.getStringExtra(GeoShapeWidget.SHAPE_LOCATION); - overlayIntentPolygon(s); - //zoomToCentroid(); - locationButton.setEnabled(true); - zoomToBounds(); - } - } else { - myLocationOverlay.runOnFirstFix(centerAroundFix); - clearButton.setEnabled(false); - final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - public void run() { - GeoPoint point = new GeoPoint(34.08145, -39.85007); - map.getController().setZoom(3); - map.getController().setCenter(point); - } - }, 100); - - } - - map.invalidate(); - - zoomDialogView = getLayoutInflater().inflate(R.layout.geo_zoom_dialog, null); - - zoomLocationButton = zoomDialogView.findViewById(R.id.zoom_location); - zoomLocationButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - zoomToMyLocation(); - map.invalidate(); - zoomDialog.dismiss(); - } - }); - - zoomPointButton = zoomDialogView.findViewById(R.id.zoom_saved_location); - zoomPointButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - //zoomToCentroid(); - zoomToBounds(); - map.invalidate(); - zoomDialog.dismiss(); - } - }); - } - - @Override - protected void onResume() { - super.onResume(); - if (map != null) { - helper.setBasemap(); - } - - upMyLocationOverlayLayers(); - } - - @Override - public void onBackPressed() { - if (!mapMarkers.isEmpty()) { - showBackDialog(); - } else { - finish(); - } - } - - @Override - protected void onPause() { - disableMyLocation(); - super.onPause(); - } - - @Override - protected void onStop() { - disableMyLocation(); - super.onStop(); - } - - private void overlayIntentPolygon(String str) { - clearButton.setEnabled(true); - clearButtonTest = true; - String s = str.replace("; ", ";"); - String[] sa = s.split(";"); - for (int i = 0; i < (sa.length - 1); i++) { - String[] sp = sa[i].split(" "); - double[] gp = new double[4]; - String lat = sp[0].replace(" ", ""); - String lng = sp[1].replace(" ", ""); - gp[0] = Double.parseDouble(lat); - gp[1] = Double.parseDouble(lng); - Marker marker = new Marker(map); - marker.setPosition(new GeoPoint(gp[0], gp[1])); - marker.setDraggable(true); - marker.setIcon(ContextCompat.getDrawable(this, R.drawable.ic_place_black)); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); - marker.setOnMarkerClickListener(nullMarkerListener); - mapMarkers.add(marker); - // pathOverlay.addPoint(marker.getPosition()); - marker.setDraggable(true); - marker.setOnMarkerDragListener(dragListener); - map.getOverlays().add(marker); - } - update_polygon(); - map.getOverlays().remove(overlayEvents); - } - - private final Handler handler = new Handler(Looper.getMainLooper()); - - private final Runnable centerAroundFix = new Runnable() { - public void run() { - handler.post(new Runnable() { - public void run() { - locationButton.setEnabled(true); - showZoomDialog(); - } - }); - } - }; - - private void showGPSDisabledAlertToUser() { - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); - alertDialogBuilder.setMessage(getString(R.string.gps_enable_message)) - .setCancelable(false) - .setPositiveButton(getString(R.string.enable_gps), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - startActivityForResult( - new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0); - } - }); - alertDialogBuilder.setNegativeButton(getString(R.string.cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - AlertDialog alert = alertDialogBuilder.create(); - alert.show(); - } - - private void upMyLocationOverlayLayers() { - LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - overlayMyLocationLayers(); - } else { - showGPSDisabledAlertToUser(); - } - - } - - private void overlayMyLocationLayers() { - map.getOverlays().add(myLocationOverlay); - myLocationOverlay.setEnabled(true); - myLocationOverlay.enableMyLocation(); - } - - private void zoomToMyLocation() { - if (myLocationOverlay.getMyLocation() != null) { - map.getController().setZoom(15); - map.getController().setCenter(myLocationOverlay.getMyLocation()); - } - } - - private void disableMyLocation() { - LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - myLocationOverlay.setEnabled(false); - myLocationOverlay.disableFollowLocation(); - myLocationOverlay.disableMyLocation(); - gpsStatus = false; - } - } - - private void overlayPointPathListener() { - overlayEvents = new MapEventsOverlay(receive); - polyline = new Polyline(); - polyline.setColor(Color.RED); - Paint paint = polyline.getPaint(); - paint.setStrokeWidth(STROKE_WIDTH); - map.getOverlays().add(polyline); - map.getOverlays().add(overlayEvents); - map.invalidate(); - } - - private void clearFeatures() { - clearButtonTest = false; - mapMarkers.clear(); - polyline.setPoints(new ArrayList()); - map.getOverlays().clear(); - clearButton.setEnabled(false); - //saveButton.setEnabled(false); - overlayPointPathListener(); - overlayMyLocationLayers(); - map.invalidate(); - - } - - private void showClearDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.geo_clear_warning)) - .setPositiveButton(getString(R.string.clear), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - clearFeatures(); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - - } - }).show(); - - } - - private void showBackDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.geo_exit_warning)) - .setPositiveButton(getString(R.string.discard), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - finish(); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - - } - }).show(); - - } - - private String generateReturnString() { - String tempString = ""; - if (mapMarkers.size() > 1) { - if (Collections.frequency(mapMarkers, mapMarkers.get(0)) < 2) { - mapMarkers.add(mapMarkers.get(0)); - } - for (int i = 0; i < mapMarkers.size(); i++) { - String lat = Double.toString(mapMarkers.get(i).getPosition().getLatitude()); - String lng = Double.toString(mapMarkers.get(i).getPosition().getLongitude()); - String alt = "0.0"; - String acu = "0.0"; - tempString = tempString + lat + " " + lng + " " + alt + " " + acu + ";"; - } - } - return tempString; - } - - private void returnLocation() { - finalReturnString = generateReturnString(); - Intent i = new Intent(); - i.putExtra( - FormEntryActivity.GEOSHAPE_RESULTS, - finalReturnString); - setResult(RESULT_OK, i); - if (mapMarkers.size() < 4) { - ToastUtils.showShortToastInMiddle(getString(R.string.polygon_validator)); - } else { - finish(); - } - } - - private void update_polygon() { - List points = new ArrayList<>(); - for (int i = 0; i < mapMarkers.size(); i++) { - points.add(mapMarkers.get(i).getPosition()); - } - points.add(mapMarkers.get(0).getPosition()); - - polyline.setPoints(points); - map.invalidate(); - } - - private final MapEventsReceiver receive = new MapEventsReceiver() { - @Override - public boolean longPressHelper(GeoPoint point) { - if (!clearButtonTest) { - clearButton.setEnabled(true); - clearButtonTest = true; - } - Marker marker = new Marker(map); - marker.setPosition(point); - marker.setDraggable(true); - marker.setIcon(ContextCompat.getDrawable(GeoShapeOsmMapActivity.this, R.drawable.ic_place_black)); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); - marker.setOnMarkerClickListener(nullMarkerListener); - mapMarkers.add(marker); - marker.setDraggable(true); - marker.setOnMarkerDragListener(dragListener); - map.getOverlays().add(marker); - List points = polyline.getPoints(); - points.add(marker.getPosition()); - polyline.setPoints(points); - update_polygon(); - map.invalidate(); - return false; - } - - @Override - public boolean singleTapConfirmedHelper(GeoPoint arg0) { - return false; - } - }; - - private final MapListener mapViewListener = new MapListener() { - @Override - public boolean onZoom(ZoomEvent zoomLev) { - zoomLevel = zoomLev.getZoomLevel(); - return false; - } - - @Override - public boolean onScroll(ScrollEvent arg0) { - return false; - } - - }; - - private final Marker.OnMarkerDragListener dragListener = new Marker.OnMarkerDragListener() { - @Override - public void onMarkerDragStart(Marker marker) { - - } - - @Override - public void onMarkerDragEnd(Marker marker) { - update_polygon(); - - } - - @Override - public void onMarkerDrag(Marker marker) { - update_polygon(); - - } - }; - - private final Marker.OnMarkerClickListener nullMarkerListener = new Marker.OnMarkerClickListener() { - - @Override - public boolean onMarkerClick(Marker arg0, MapView arg1) { - return false; - } - }; - - /* - This functions should be added to the mapHelper Class - - */ - private void zoomToBounds() { - map.getController().setZoom(4); - map.invalidate(); - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - public void run() { - double minLat = Double.MAX_VALUE; - double maxLat = Double.MIN_VALUE; - double minLong = Double.MAX_VALUE; - double maxLong = Double.MIN_VALUE; - Integer size = mapMarkers.size(); - for (int i = 0; i < size; i++) { - GeoPoint tempMarker = mapMarkers.get(i).getPosition(); - if (tempMarker.getLatitude() < minLat) { - minLat = tempMarker.getLatitude(); - } - if (tempMarker.getLatitude() > maxLat) { - maxLat = tempMarker.getLatitude(); - } - if (tempMarker.getLongitude() < minLong) { - minLong = tempMarker.getLongitude(); - } - if (tempMarker.getLongitude() > maxLong) { - maxLong = tempMarker.getLongitude(); - } - } - BoundingBox boundingBox = new BoundingBox(maxLat, maxLong, minLat, minLong); - map.zoomToBoundingBox(boundingBox, false); - map.invalidate(); - } - }, 100); - map.invalidate(); - - } - - public void showZoomDialog() { - - if (zoomDialog == null) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.zoom_to_where)); - builder.setView(zoomDialogView) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.cancel(); - zoomDialog.dismiss(); - } - }); - zoomDialog = builder.create(); - } - //If feature enable zoom to button else disable - if (myLocationOverlay.getMyLocation() != null) { - zoomLocationButton.setEnabled(true); - zoomLocationButton.setBackgroundColor(Color.parseColor("#50cccccc")); - zoomLocationButton.setTextColor(themeUtils.getPrimaryTextColor()); - } else { - zoomLocationButton.setEnabled(false); - zoomLocationButton.setBackgroundColor(Color.parseColor("#50e2e2e2")); - zoomLocationButton.setTextColor(Color.parseColor("#FF979797")); - } - - if (!mapMarkers.isEmpty()) { - zoomPointButton.setEnabled(true); - zoomPointButton.setBackgroundColor(Color.parseColor("#50cccccc")); - zoomPointButton.setTextColor(themeUtils.getPrimaryTextColor()); - } else { - zoomPointButton.setEnabled(false); - zoomPointButton.setBackgroundColor(Color.parseColor("#50e2e2e2")); - zoomPointButton.setTextColor(Color.parseColor("#FF979797")); - } - zoomDialog.show(); - } - - @Override - public void destroy() { - - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/fragments/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/fragments/OsmMapFragment.java deleted file mode 100644 index dadd1c2969e..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/fragments/OsmMapFragment.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.odk.collect.android.fragments; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.odk.collect.android.R; -import org.osmdroid.views.MapView; - -import java.util.ArrayList; -import java.util.List; - -/** A Fragment that just contains a single org.osmdroid.views.MapView. */ -public class OsmMapFragment extends Fragment { - private MapView mapView; - private final List mapReadyCallbacks = new ArrayList<>(); - - /** An interface for receiving the MapView object when it is ready. */ - public interface MapReadyCallback { - void onMapReady(MapView map); - } - - /** - * Ensures that the callback will be invoked once with the MapView object, - * either now (if the MapView already exists) or later (when it is created). - */ - public void getMapAsync(MapReadyCallback callback) { - mapReadyCallbacks.add(callback); - invokeMapReadyCallbacks(); - } - - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.osm_map_layout, container, false); - mapView = view.findViewById(R.id.osm_map_view); - invokeMapReadyCallbacks(); - return view; - } - - private void invokeMapReadyCallbacks() { - while (mapView != null && !mapReadyCallbacks.isEmpty()) { - mapReadyCallbacks.remove(0).onMapReady(mapView); - } - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java new file mode 100644 index 00000000000..e909cdfd0b7 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2018 Nafundi + * + * 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 org.odk.collect.android.map; + +import android.app.AlertDialog; +import android.content.Intent; +import android.graphics.Color; +import android.location.Location; +import android.os.Handler; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.app.FragmentActivity; + +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.SupportMapFragment; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; + +import org.odk.collect.android.R; +import org.odk.collect.android.location.client.LocationClient; +import org.odk.collect.android.location.client.LocationClients; +import org.odk.collect.android.utilities.ToastUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class GoogleMapFragment extends SupportMapFragment implements + MapFragment, LocationListener, LocationClient.LocationClientListener, + GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener, + GoogleMap.OnMarkerDragListener { + protected GoogleMap map; + protected List gpsLocationReadyListeners = new ArrayList<>(); + protected MapFragment.PointListener clickListener; + protected MapFragment.PointListener longPressListener; + protected LocationClient locationClient; + protected MapPoint lastLocationFix; + protected int nextFeatureId = 1; + protected Map features = new HashMap<>(); + protected AlertDialog gpsErrorDialog; + protected boolean gpsLocationEnabled; + + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "This flag is exposed for Robolectric tests to set") + @VisibleForTesting public static boolean callReadyListenerSynchronously; + + @Override public void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener) { + activity.getSupportFragmentManager() + .beginTransaction().add(containerId, this).commitNow(); + getMapAsync((GoogleMap map) -> { + if (map == null) { + ToastUtils.showShortToast(R.string.google_play_services_error_occured); + if (listener != null) { + listener.onReady(null); + } + return; + } + this.map = map; + map.setOnMapClickListener(this); + map.setOnMapLongClickListener(this); + map.setOnMarkerDragListener(this); + map.getUiSettings().setCompassEnabled(true); + // Show the blue dot on the map, but hide the Google-provided + // "go to my location" button; we have our own button for that. + map.setMyLocationEnabled(true); + map.getUiSettings().setMyLocationButtonEnabled(false); + if (listener != null) { + listener.onReady(this); + } + }); + + // In Robolectric tests, getMapAsync() never gets around to calling its + // callback; we have to invoke the ready listener directly. + if (callReadyListenerSynchronously) { + listener.onReady(this); + } + } + + // TOOD(ping): This method is only used by MapHelper. Remove this after + // MapFragment adds support for selectable basemaps. + public GoogleMap getGoogleMap() { + return map; + } + + @Override public double getZoom() { + return map.getCameraPosition().zoom; + } + + @Override public double setZoom(double requestedZoom) { + float actualZoom = (float) clamp(requestedZoom, map.getMinZoomLevel(), map.getMaxZoomLevel()); + map.animateCamera(CameraUpdateFactory.zoomTo(actualZoom)); + return actualZoom; + } + + @Override public @NonNull MapPoint getCenter() { + LatLng target = map.getCameraPosition().target; + return new MapPoint(target.latitude, target.longitude); + } + + @Override public void setCenter(@Nullable MapPoint center) { + if (center != null) { + LatLng target = new LatLng(center.lat, center.lon); + map.animateCamera(CameraUpdateFactory.newLatLng(target)); + } + } + + @Override public void zoomToBoundingBox(Iterable points, double scaleFactor) { + if (points != null) { + int count = 0; + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + for (MapPoint point : points) { + builder.include(toLatLng(point)); + count++; + } + if (count > 0) { + final LatLngBounds bounds = expandBounds(builder.build(), 1 / scaleFactor); + new Handler().postDelayed(() -> { + map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); + }, 100); + } + } + } + + protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { + double north = bounds.northeast.latitude; + double south = bounds.southwest.latitude; + double latCenter = (north + south) / 2; + double latRadius = ((north - south) / 2) * factor; + north = Math.min(90, latCenter + latRadius); + south = Math.max(-90, latCenter - latRadius); + + double east = bounds.northeast.longitude; + double west = bounds.southwest.longitude; + while (east < west) { + east += 360; + } + double lonCenter = (east + west) / 2; + double lonRadius = Math.min(180 - 1e-6, ((east - west) / 2) * factor); + east = lonCenter + lonRadius; + west = lonCenter - lonRadius; + + return new LatLngBounds(new LatLng(south, west), new LatLng(north, east)); + } + + @Override public int addDraggableShape(Iterable points) { + int featureId = nextFeatureId++; + features.put(featureId, new DraggableShape(map, points)); + return featureId; + } + + @Override public void appendPointToShape(int featureId, @NonNull MapPoint point) { + MapFeature feature = features.get(featureId); + if (feature != null && feature instanceof DraggableShape) { + ((DraggableShape) feature).addPoint(point); + } + } + + @Override public @NonNull List getPointsOfShape(int featureId) { + MapFeature feature = features.get(featureId); + if (feature instanceof DraggableShape) { + return ((DraggableShape) feature).getPoints(); + } + return new ArrayList<>(); + } + + @Override public void removeFeature(int featureId) { + MapFeature feature = features.get(featureId); + if (feature != null) { + feature.dispose(); + } + } + + @Override public void clearFeatures() { + map.clear(); + features.clear(); + } + + @Override public void setClickListener(@Nullable PointListener listener) { + clickListener = listener; + } + + @Override public void setLongPressListener(@Nullable PointListener listener) { + longPressListener = listener; + } + + @Override public void setGpsLocationEnabled(boolean enable) { + if (enable != gpsLocationEnabled) { + gpsLocationEnabled = enable; + if (locationClient == null) { + locationClient = LocationClients.clientForContext(getActivity()); + locationClient.setListener(this); + } + if (enable) { + locationClient.start(); + } else { + locationClient.stop(); + } + } + } + + @Override public void runOnGpsLocationReady(@NonNull ReadyListener listener) { + if (lastLocationFix != null) { + listener.onReady(this); + } else { + gpsLocationReadyListeners.add(listener); + } + } + + @Override public void onLocationChanged(Location location) { + lastLocationFix = new MapPoint(location.getLatitude(), location.getLongitude()); + for (ReadyListener listener : gpsLocationReadyListeners) { + listener.onReady(this); + } + gpsLocationReadyListeners.clear(); + } + + @Override public @Nullable MapPoint getGpsLocation() { + return lastLocationFix; + } + + @Override public void onMapClick(LatLng latLng) { + if (clickListener != null) { + clickListener.onPoint(fromLatLng(latLng)); + } + } + + @Override public void onMapLongClick(LatLng latLng) { + if (longPressListener != null) { + longPressListener.onPoint(fromLatLng(latLng)); + } + } + + @Override public void onMarkerDragStart(Marker marker) { + updateFeatures(); + } + + @Override public void onMarkerDrag(Marker marker) { + updateFeatures(); + } + + @Override public void onMarkerDragEnd(Marker marker) { + updateFeatures(); + } + + @Override public void onClientStart() { + locationClient.requestLocationUpdates(this); + if (!locationClient.isLocationAvailable()) { + showGpsDisabledAlert(); + } + } + + @Override public void onClientStartFailure() { + showGpsDisabledAlert(); + } + + @Override public void onClientStop() { } + + protected void showGpsDisabledAlert() { + gpsErrorDialog = new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.gps_enable_message)) + .setCancelable(false) + .setPositiveButton(getString(R.string.enable_gps), + (dialog, id) -> startActivityForResult( + new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0)) + .setNegativeButton(getString(R.string.cancel), + (dialog, id) -> dialog.cancel()) + .create(); + gpsErrorDialog.show(); + } + + @VisibleForTesting public boolean isGpsErrorDialogShowing() { + return gpsErrorDialog != null && gpsErrorDialog.isShowing(); + } + + protected void updateFeatures() { + for (MapFeature feature : features.values()) { + feature.update(); + } + } + + protected static double clamp(double x, double min, double max) { + return x < min ? min : x > max ? max : x; + } + + protected static @NonNull MapPoint fromLatLng(@NonNull LatLng latLng) { + return new MapPoint(latLng.latitude, latLng.longitude); + } + + protected static @NonNull LatLng toLatLng(@NonNull MapPoint point) { + return new LatLng(point.lat, point.lon); + } + + /** + * A MapFeature is a physical feature on a map, such as a point, a road, + * a building, a region, etc. It is presented to the user as one editable + * object, though its appearance may be constructed from multiple overlays + * (e.g. geometric elements, handles for manipulation, etc.). + */ + interface MapFeature { + /** Updates the feature's geometry after any UI handles have moved. */ + void update(); + + /** Removes the feature from the map, leaving it no longer usable. */ + void dispose(); + } + + /** A polygon that can be manipulated by dragging markers at its vertices. */ + protected static class DraggableShape implements MapFeature { + final GoogleMap map; + final List markers = new ArrayList<>(); + Polygon polygon; + + public DraggableShape(GoogleMap map, Iterable points) { + this.map = map; + for (MapPoint point : points) { + markers.add(map.addMarker( + new MarkerOptions().position(toLatLng(point)).draggable(true))); + } + // We don't always add a Polygon, because GoogleMap.addPolygon() will + // crash with zero points; let update() decide whether to create one. + update(); + } + + public void update() { + if (markers.isEmpty()) { + if (polygon != null) { + polygon.remove(); + polygon = null; + } + return; + } + + List latLngs = new ArrayList<>(); + for (Marker marker : markers) { + latLngs.add(marker.getPosition()); + } + if (polygon == null) { + PolygonOptions polyOpts = new PolygonOptions(); + polyOpts.strokeColor(Color.RED); + polyOpts.zIndex(1); + polyOpts.addAll(latLngs); + polygon = map.addPolygon(polyOpts); + } else { + polygon.setPoints(latLngs); + } + } + + public void dispose() { + for (Marker marker : markers) { + marker.remove(); + } + markers.clear(); + polygon.remove(); + polygon = null; + } + + public List getPoints() { + List points = new ArrayList<>(); + for (Marker marker : markers) { + points.add(fromLatLng(marker.getPosition())); + } + return points; + } + + public void addPoint(MapPoint point) { + markers.add(map.addMarker( + new MarkerOptions().position(toLatLng(point)).draggable(true))); + update(); + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java new file mode 100644 index 00000000000..459c535df6c --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 Nafundi + * + * 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 org.odk.collect.android.map; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.view.View; + +import java.util.List; + +/** + * Interface for Fragments that render a map view. The plan is to have one + * implementation for each map SDK, e.g. GoogleMapFragment, OsmMapFragment, etc. + * + * This is intended to be a single map API that provides all functionality needed + * for the three geo widgets (collecting or editing a point, a trace, or a shape): + * - Basic control of the viewport (panning, zooming) + * - Displaying and getting the current GPS location + * - Requesting a callback on the first GPS location fix + * - Requesting callbacks for short clicks and long presses on the map + * - (to do) Adding editable points to the map + * - (to do) Adding editable traces (polylines) to the map + * - Adding editable shapes (closed polygons) to the map + * + * Editable points, traces, and shapes are called "map features" in this API. + * To keep the API small, features are not exposed as objects; instead, they are + * identified by integer feature IDs. To keep the API unified (instead of behaving + * like three different APIs), the map always supports all three kinds of features, + * even though the geo widgets only use one kind of feature at a time. + */ +public interface MapFragment { + /** + * Adds the map Fragment to an activity. The containerId should be the + * resource ID of a View, into which the map view will be placed. The + * listener will be invoked on the UI thread, with this MapFragment when the + * map is ready, or with null if there is a problem initializing the map. + */ + void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener); + + /** Gets the View that is displaying the map. */ + View getView(); + + /** + * Gets the current zoom level. For maps that only support zooming by + * powers of 2, the zoom level will always be an integer. + */ + double getZoom(); + + /** + * Changes the zoom level to bring it as close as possible to the given value, + * possibly with animation, and returns the actual zoom that will be reached. + */ + double setZoom(double zoom); + + /** Gets the point currently shown at the center of the map view. */ + @NonNull MapPoint getCenter(); + + /** Pans the map view to center on the given point, possibly with animation. */ + void setCenter(@Nullable MapPoint center); + + /** + * Adjusts the map's viewport and zoom level to enclose all of the given + * points. A scaleFactor of 1.0 ensures that all the points will be just + * visible in the viewport; a scaleFactor less than 1 shrinks the view + * beyond that. For example, a scaleFactor of 0.8 causes the bounding box + * to occupy at most 80% of the width and 80% of the height of the viewport, + * ensuring a margin of at least 10% on all sides. + */ + void zoomToBoundingBox(Iterable points, double scaleFactor); + + /** + * Adds a polygonal shape to the map with the given sequence of vertices. + * The polygon's vertices will have handles that can be dragged by the user. + * Returns a positive integer, the featureId for the newly added polyline. + */ + int addDraggableShape(Iterable points); + + /** Appends a vertex to the polygonal shape specified by featureId. */ + void appendPointToShape(int featureId, @NonNull MapPoint point); + + /** + * Returns the vertices of the polygonal shape specified by featureId, or an + * empty list if the featureId does not identify an existing polygonal shape. + */ + @NonNull List getPointsOfShape(int featureId); + + /** Removes a specified map feature from the map. */ + void removeFeature(int featureId); + + /** Removes all map features from the map. */ + void clearFeatures(); + + /** + * Enables/disables GPS tracking. While enabled, the GPS location is shown + * on the map, and fixes are passed to the listener (setGpsLocationListener). + * The activity is responsible for disabling GPS location if it doesn't want + * to keep getting callbacks after it has been stopped or destroyed. + */ + void setGpsLocationEnabled(boolean enabled); + + /** Gets the last GPS location fix, or null if there hasn't been one. */ + @Nullable MapPoint getGpsLocation(); + + /** + * Queues a callback to be invoked on the UI thread as soon as a GPS fix is + * available. If there already is a location fix, the callback is invoked + * immediately; otherwise, when a fix is obtained, it will be invoked once. + */ + void runOnGpsLocationReady(@NonNull ReadyListener listener); + + /** Registers a callback for a click on the map. */ + void setClickListener(@Nullable PointListener listener); + + /** Registers a callback for a long press on the map. */ + void setLongPressListener(@Nullable PointListener listener); + + interface ReadyListener { + void onReady(@Nullable MapFragment mapFragment); + } + + interface PointListener { + void onPoint(@NonNull MapPoint point); + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/map/MapPoint.java b/collect_app/src/main/java/org/odk/collect/android/map/MapPoint.java new file mode 100644 index 00000000000..fb44982eee1 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/map/MapPoint.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 Nafundi + * + * 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 org.odk.collect.android.map; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +public class MapPoint implements Parcelable { + public final double lat; + public final double lon; + + public MapPoint(double lat, double lon) { + this.lat = lat; + this.lon = lon; + } + + private MapPoint(Parcel parcel) { + this.lat = parcel.readDouble(); + this.lon = parcel.readDouble(); + } + + @Override public String toString() { + return String.format(Locale.US, "MapPoint(%+.6f, %+.6f)", lat, lon); + } + + @Override public int hashCode() { + return Double.valueOf(lat).hashCode() * 31 + Double.valueOf(lon).hashCode(); + } + + @Override public boolean equals(Object other) { + return other == this || (other instanceof MapPoint && + ((MapPoint) other).lat == this.lat && + ((MapPoint) other).lon == this.lon + ); + } + + // Implementation of the Parcelable interface. + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public MapPoint createFromParcel(Parcel parcel) { + return new MapPoint(parcel); + } + + public MapPoint[] newArray(int size) { + return new MapPoint[size]; + } + }; + + @Override public int describeContents() { + return 0; + } + + @Override public void writeToParcel(Parcel parcel, int flags) { + parcel.writeDouble(lat); + parcel.writeDouble(lon); + } + +} diff --git a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java new file mode 100644 index 00000000000..a1a3f42c52e --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2018 Nafundi + * + * 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 org.odk.collect.android.map; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Paint; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.content.ContextCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.odk.collect.android.R; +import org.osmdroid.api.IGeoPoint; +import org.osmdroid.events.MapEventsReceiver; +import org.osmdroid.util.BoundingBox; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.MapEventsOverlay; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.Polyline; +import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OsmMapFragment extends Fragment implements MapFragment, MapEventsReceiver { + protected MapView map; + protected ReadyListener readyListener; + protected MapFragment.PointListener clickListener; + protected MapFragment.PointListener longPressListener; + protected MyLocationNewOverlay myLocationOverlay; + protected int nextFeatureId = 1; + protected Map features = new HashMap<>(); + + @VisibleForTesting protected AlertDialog gpsErrorDialog; + + @Override public void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener) { + readyListener = listener; + activity.getSupportFragmentManager() + .beginTransaction().add(containerId, this).commit(); + } + + // TOOD(ping): This method is only used by MapHelper. Remove this after + // MapFragment adds support for selectable basemaps. + public MapView getMapView() { + return map; + } + + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.osm_map_layout, container, false); + map = view.findViewById(R.id.osm_map_view); + map.setMultiTouchControls(true); + map.setBuiltInZoomControls(true); + map.setTilesScaledToDpi(true); + map.getOverlays().add(new MapEventsOverlay(this)); + myLocationOverlay = new MyLocationNewOverlay(map); + if (readyListener != null) { + new Handler().postDelayed(() -> readyListener.onReady(this), 100); + } + return view; + } + + @Override public boolean singleTapConfirmedHelper(GeoPoint geoPoint) { + if (clickListener != null) { + clickListener.onPoint(fromGeoPoint(geoPoint)); + return true; + } + return false; + } + + @Override public boolean longPressHelper(GeoPoint geoPoint) { + if (longPressListener != null) { + longPressListener.onPoint(fromGeoPoint(geoPoint)); + return true; + } + return false; + } + + @Override public double getZoom() { + return map.getZoomLevel(); + } + + @Override public double setZoom(double requestedZoom) { + int actualZoom = (int) Math.round( + clamp(requestedZoom, map.getMinZoomLevel(), map.getMaxZoomLevel())); + map.getController().setZoom(actualZoom); + return actualZoom; + } + + @Override public @NonNull MapPoint getCenter() { + return fromGeoPoint(map.getMapCenter()); + } + + @Override public void setCenter(@Nullable MapPoint center) { + if (center != null) { + map.getController().setCenter(toGeoPoint(center)); + } + } + + @Override public void zoomToBoundingBox(Iterable points, double scaleFactor) { + if (points != null) { + int count = 0; + List geoPoints = new ArrayList<>(); + for (MapPoint point : points) { + geoPoints.add(toGeoPoint(point)); + count++; + } + if (count > 0) { + map.getController().setZoom(4); + map.invalidate(); + map.zoomToBoundingBox(BoundingBox.fromGeoPoints( + geoPoints).increaseByScale((float) (1 / scaleFactor)), true); + } + } + } + + @Override public int addDraggableShape(Iterable points) { + int featureId = nextFeatureId++; + features.put(featureId, new DraggableShape(map, points)); + return featureId; + } + + @Override public void appendPointToShape(int featureId, @NonNull MapPoint point) { + MapFeature feature = features.get(featureId); + if (feature != null && feature instanceof DraggableShape) { + ((DraggableShape) feature).addPoint(point); + } + } + + @Override public @NonNull List getPointsOfShape(int featureId) { + MapFeature feature = features.get(featureId); + if (feature != null && feature instanceof DraggableShape) { + return ((DraggableShape) feature).getPoints(); + } + return new ArrayList<>(); + } + + @Override public void removeFeature(int featureId) { + MapFeature feature = features.get(featureId); + if (feature != null) { + feature.dispose(); + } + } + + @Override public void clearFeatures() { + map.getOverlays().clear(); + map.getOverlays().add(new MapEventsOverlay(this)); + map.getOverlays().add(myLocationOverlay); + map.invalidate(); + features.clear(); + } + + @Override public void setClickListener(@Nullable PointListener listener) { + clickListener = listener; + } + + @Override public void setLongPressListener(@Nullable PointListener listener) { + longPressListener = listener; + } + + @Override public void runOnGpsLocationReady(@NonNull ReadyListener listener) { + myLocationOverlay.runOnFirstFix(() -> getActivity().runOnUiThread(() -> listener.onReady(this))); + } + + @Override public void setGpsLocationEnabled(boolean enabled) { + if (enabled) { + LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); + if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + map.getOverlays().add(myLocationOverlay); + myLocationOverlay.setEnabled(true); + myLocationOverlay.enableMyLocation(); + } else { + showGpsDisabledAlert(); + } + } else { + myLocationOverlay.setEnabled(false); + myLocationOverlay.disableFollowLocation(); + myLocationOverlay.disableMyLocation(); + } + } + + @Override public @Nullable MapPoint getGpsLocation() { + IGeoPoint geoPoint = myLocationOverlay.getMyLocation(); + return geoPoint == null ? null : fromGeoPoint(geoPoint); + } + + protected void showGpsDisabledAlert() { + gpsErrorDialog = new AlertDialog.Builder(getContext()) + .setMessage(getString(R.string.gps_enable_message)) + .setCancelable(false) + .setPositiveButton(getString(R.string.enable_gps), + (dialog, id) -> startActivityForResult( + new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0)) + .setNegativeButton(getString(R.string.cancel), + (dialog, id) -> dialog.cancel()) + .create(); + gpsErrorDialog.show(); + } + + @VisibleForTesting public AlertDialog getGpsErrorDialog() { + return gpsErrorDialog; + } + + protected void updateFeatures() { + for (MapFeature feature : features.values()) { + feature.update(); + } + } + + protected static double clamp(double x, double min, double max) { + return x < min ? min : x > max ? max : x; + } + + protected static @NonNull MapPoint fromGeoPoint(@NonNull IGeoPoint geoPoint) { + return new MapPoint(geoPoint.getLatitude(), geoPoint.getLongitude()); + } + + protected static @NonNull GeoPoint toGeoPoint(@NonNull MapPoint point) { + return new GeoPoint(point.lat, point.lon); + } + + /** + * A MapFeature is a physical feature on a map, such as a point, a road, + * a building, a region, etc. It is presented to the user as one editable + * object, though its appearance may be constructed from multiple overlays + * (e.g. geometric elements, handles for manipulation, etc.). + */ + interface MapFeature { + /** Updates the feature's geometry after any UI handles have moved. */ + void update(); + + /** Removes the feature from the map, leaving it no longer usable. */ + void dispose(); + } + + /** A polygon that can be manipulated by dragging markers at its vertices. */ + protected static class DraggableShape implements MapFeature, Marker.OnMarkerClickListener, Marker.OnMarkerDragListener { + final MapView map; + final List markers = new ArrayList<>(); + final Polyline polyline; + public static final int STROKE_WIDTH = 5; + + public DraggableShape(MapView map, Iterable points) { + this.map = map; + polyline = new Polyline(); + polyline.setColor(Color.RED); + Paint paint = polyline.getPaint(); + paint.setStrokeWidth(STROKE_WIDTH); + map.getOverlays().add(polyline); + for (MapPoint point : points) { + addMarker(point); + } + update(); + } + + public void update() { + List geoPoints = new ArrayList<>(); + for (Marker marker : markers) { + geoPoints.add(marker.getPosition()); + } + if (!markers.isEmpty()) { + geoPoints.add(markers.get(0).getPosition()); // close the polygon + } + polyline.setPoints(geoPoints); + map.invalidate(); + } + + public void dispose() { + for (Marker marker : markers) { + map.getOverlays().remove(marker); + } + markers.clear(); + update(); + } + + public List getPoints() { + List points = new ArrayList<>(); + for (Marker marker : markers) { + points.add(fromGeoPoint(marker.getPosition())); + } + return points; + } + + public void addPoint(MapPoint point) { + addMarker(point); + update(); + } + + protected void addMarker(MapPoint point) { + Marker marker = new Marker(map); + marker.setPosition(toGeoPoint(point)); + marker.setDraggable(true); + marker.setIcon(ContextCompat.getDrawable(map.getContext(), R.drawable.ic_place_black)); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + marker.setOnMarkerClickListener(this); + marker.setOnMarkerDragListener(this); + map.getOverlays().add(marker); + markers.add(marker); + } + + @Override public void onMarkerDragStart(Marker marker) { + } + + @Override public void onMarkerDragEnd(Marker marker) { + update(); + } + + @Override public void onMarkerDrag(Marker marker) { + update(); + } + + @Override public boolean onMarkerClick(Marker marker, MapView map) { + // Prevent the text bubble from appearing when a marker is clicked. + return false; + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/spatial/MapHelper.java b/collect_app/src/main/java/org/odk/collect/android/spatial/MapHelper.java index c1c5fe5ff48..7dce5ffa494 100644 --- a/collect_app/src/main/java/org/odk/collect/android/spatial/MapHelper.java +++ b/collect_app/src/main/java/org/odk/collect/android/spatial/MapHelper.java @@ -138,7 +138,7 @@ public void setBasemap() { break; } - } else { + } else if (osmMap != null) { //OSMMAP String basemap = getOsmBasemap(); @@ -152,11 +152,11 @@ public void setBasemap() { case OPENMAP_USGS_SAT: tileSource = tileFactory.getUsgsSat(); break; - + case OPENMAP_USGS_IMG: tileSource = tileFactory.getUsgsImg(); break; - + case OPENMAP_STAMEN_TERRAIN: tileSource = tileFactory.getStamenTerrain(); break; diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/GeoShapeWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoShapeWidget.java index b7bd245874a..518f5b07601 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/GeoShapeWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoShapeWidget.java @@ -28,8 +28,7 @@ import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.R; import org.odk.collect.android.activities.FormEntryActivity; -import org.odk.collect.android.activities.GeoShapeGoogleMapActivity; -import org.odk.collect.android.activities.GeoShapeOsmMapActivity; +import org.odk.collect.android.activities.GeoShapeActivity; import org.odk.collect.android.listeners.PermissionListener; import org.odk.collect.android.preferences.PreferenceKeys; import org.odk.collect.android.utilities.PlayServicesUtil; @@ -85,22 +84,14 @@ public GeoShapeWidget(Context context, FormEntryPrompt prompt) { } private void startGeoShapeActivity() { - Intent i; - if (mapSDK.equals(GOOGLE_MAP_KEY)) { - if (PlayServicesUtil.isGooglePlayServicesAvailable(getContext())) { - i = new Intent(getContext(), GeoShapeGoogleMapActivity.class); - } else { - PlayServicesUtil.showGooglePlayServicesAvailabilityErrorDialog(getContext()); - return; - } - } else { - i = new Intent(getContext(), GeoShapeOsmMapActivity.class); - } - String s = answerDisplay.getText().toString(); - if (s.length() != 0) { - i.putExtra(SHAPE_LOCATION, s); + if (mapSDK.equals(GOOGLE_MAP_KEY) && !PlayServicesUtil.isGooglePlayServicesAvailable(getContext())) { + PlayServicesUtil.showGooglePlayServicesAvailabilityErrorDialog(getContext()); + return; } - ((Activity) getContext()).startActivityForResult(i, RequestCodes.GEOSHAPE_CAPTURE); + Intent intent = new Intent(getContext(), GeoShapeActivity.class) + .putExtra(SHAPE_LOCATION, answerDisplay.getText().toString()) + .putExtra(PreferenceKeys.KEY_MAP_SDK, mapSDK); + ((Activity) getContext()).startActivityForResult(intent, RequestCodes.GEOSHAPE_CAPTURE); } private void updateButtonLabelsAndVisibility(boolean dataAvailable) { @@ -153,4 +144,4 @@ public void denied() { } }); } -} \ No newline at end of file +} diff --git a/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java b/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java new file mode 100644 index 00000000000..48939882bbd --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 Nafundi + * + * 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 org.odk.collect.android.location.activities; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.odk.collect.android.BuildConfig; +import org.odk.collect.android.activities.GeoShapeActivity; +import org.odk.collect.android.location.client.LocationClient; +import org.odk.collect.android.location.client.LocationClients; +import org.odk.collect.android.map.GoogleMapFragment; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.odk.collect.android.location.activities.GeoPointActivityTest.newMockLocation; + +@Config(constants = BuildConfig.class) +@RunWith(RobolectricTestRunner.class) +public class GeoShapeActivityTest extends BaseGeoActivityTest { + @Rule public MockitoRule rule = MockitoJUnit.rule(); + private ActivityController controller; + private GeoShapeActivity activity; + private GoogleMapFragment map; + @Mock LocationClient locationClient; + + @Before public void setUp() throws Exception { + super.setUp(); + LocationClients.setTestClient(locationClient); + GoogleMapFragment.callReadyListenerSynchronously = true; + controller = Robolectric.buildActivity(GeoShapeActivity.class); + activity = controller.create().start().resume().visible().get(); + map = (GoogleMapFragment) activity.getMapFragment(); + } + + @Test public void activityShouldShowZoomDialogOnFirstLocation() { + verify(locationClient).start(); + + when(locationClient.isLocationAvailable()).thenReturn(true); + map.onClientStart(); + verify(locationClient).requestLocationUpdates(map); + + assertFalse(activity.isGpsButtonEnabled()); + assertFalse(activity.isZoomDialogShowing()); + map.onLocationChanged(newMockLocation()); + assertTrue(activity.isGpsButtonEnabled()); + assertTrue(activity.isZoomDialogShowing()); + + controller.stop(); + verify(locationClient).stop(); + } + + @Test public void activityShouldShowErrorDialogOnClientError() { + assertFalse(map.isGpsErrorDialogShowing()); + map.onClientStartFailure(); + assertTrue(map.isGpsErrorDialogShowing()); + } + + @Test public void activityShouldShowErrorDialogIfLocationUnavailable() { + assertFalse(map.isGpsErrorDialogShowing()); + when(locationClient.isLocationAvailable()).thenReturn(false); + map.onClientStart(); + assertTrue(map.isGpsErrorDialogShowing()); + } +} diff --git a/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeGoogleMapActivityTest.java b/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeGoogleMapActivityTest.java deleted file mode 100644 index f1430dbe8e0..00000000000 --- a/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeGoogleMapActivityTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.odk.collect.android.location.activities; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.odk.collect.android.BuildConfig; -import org.odk.collect.android.activities.GeoShapeGoogleMapActivity; -import org.odk.collect.android.location.client.LocationClient; -import org.odk.collect.android.location.client.LocationClients; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.odk.collect.android.location.activities.GeoPointActivityTest.newMockLocation; - -@Config(constants = BuildConfig.class) -@RunWith(RobolectricTestRunner.class) -public class GeoShapeGoogleMapActivityTest extends BaseGeoActivityTest { - - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - private ActivityController activityController; - - private GeoShapeGoogleMapActivity activity; - - @Mock - LocationClient locationClient; - - /** - * Runs {@link Before} each test. - */ - @Before - public void setUp() throws Exception { - super.setUp(); - activityController = Robolectric.buildActivity(GeoShapeGoogleMapActivity.class); - activity = activityController.get(); - - LocationClients.setTestClient(locationClient); - } - - @Test - public void activityShouldShowZoomDialogOnFirstLocation() { - activityController.create(); - - activityController.start(); - verify(locationClient).start(); - - when(locationClient.isLocationAvailable()).thenReturn(true); - - assertFalse(activity.getGpsButton().isEnabled()); - activity.onClientStart(); - assertTrue(activity.getGpsButton().isEnabled()); - - verify(locationClient).requestLocationUpdates(activity); - - assertNull(activity.getZoomDialog()); - activity.onLocationChanged(newMockLocation()); - assertNotNull(activity.getZoomDialog()); - - assertTrue(activity.getZoomDialog().isShowing()); - activity.getZoomDialog().dismiss(); - - activityController.stop(); - verify(locationClient).stop(); - } - - @Test - public void activityShouldShowErrorDialogOnClientError() { - activityController.create(); - activityController.start(); - - assertNull(activity.getErrorDialog()); - - activity.onClientStartFailure(); - - assertNotNull(activity.getErrorDialog()); - assertTrue(activity.getErrorDialog().isShowing()); - } - - @Test - public void activityShouldShowErrorDialogIfLocationUnavailable() { - activityController.create(); - activityController.start(); - - when(locationClient.isLocationAvailable()).thenReturn(false); - - assertNull(activity.getErrorDialog()); - - activity.onClientStart(); - - assertNotNull(activity.getErrorDialog()); - assertTrue(activity.getErrorDialog().isShowing()); - } -} \ No newline at end of file From 0624bdcb1f166e9dacd3bcbe6c47798a6e7b56e0 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Fri, 24 Aug 2018 16:21:44 +0200 Subject: [PATCH 2/7] GeoShapeActivity: prompt on Back only if the polygon has changed. --- .../odk/collect/android/activities/GeoShapeActivity.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java index 9468bd94b2f..4caf8092a8d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java @@ -51,12 +51,12 @@ public class GeoShapeActivity extends CollectAbstractActivity implements IRegist private int shapeId = -1; // will be a positive featureId once map is ready private ImageButton gpsButton; private ImageButton clearButton; - private MapHelper helper; private AlertDialog zoomDialog; private View zoomDialogView; private Button zoomPointButton; private Button zoomLocationButton; + private String originalValue = ""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -93,7 +93,7 @@ public MapFragment createMapFragment() { } @Override public void onBackPressed() { - if (!map.getPointsOfShape(shapeId).isEmpty()) { + if (!formatPoints(map.getPointsOfShape(shapeId)).equals(originalValue)) { showBackDialog(); } else { finish(); @@ -133,7 +133,8 @@ private void initMap(MapFragment newMapFragment) { List points = new ArrayList<>(); Intent intent = getIntent(); if (intent != null && intent.hasExtra(GeoShapeWidget.SHAPE_LOCATION)) { - points = parsePoints(intent.getStringExtra(GeoShapeWidget.SHAPE_LOCATION)); + originalValue = intent.getStringExtra(GeoShapeWidget.SHAPE_LOCATION); + points = parsePoints(originalValue); } shapeId = map.addDraggableShape(points); gpsButton.setEnabled(!points.isEmpty()); From 0aea79981a452101ab640d79c0a2b9f2bea5bd7b Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Sat, 25 Aug 2018 03:43:58 +0200 Subject: [PATCH 3/7] GeoShapeActivity: Fix zoom-to-GPS-location. (Replaces setCenter() with zoomToPoint() in the MapFragment interface.) --- .../android/activities/GeoShapeActivity.java | 3 +- .../android/map/GoogleMapFragment.java | 25 ++++-------- .../odk/collect/android/map/MapFragment.java | 39 ++++++++----------- .../collect/android/map/OsmMapFragment.java | 26 +++++-------- 4 files changed, 36 insertions(+), 57 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java index 4caf8092a8d..b311b52423c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java @@ -146,8 +146,7 @@ private void initMap(MapFragment newMapFragment) { zoomLocationButton.setOnClickListener(v -> { MapPoint location = map.getGpsLocation(); if (location != null) { - map.setCenter(location); - map.setZoom(15); + map.zoomToPoint(location, 16); } zoomDialog.dismiss(); }); diff --git a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java index e909cdfd0b7..a2942d78736 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java @@ -104,26 +104,17 @@ public GoogleMap getGoogleMap() { return map; } - @Override public double getZoom() { - return map.getCameraPosition().zoom; - } - - @Override public double setZoom(double requestedZoom) { - float actualZoom = (float) clamp(requestedZoom, map.getMinZoomLevel(), map.getMaxZoomLevel()); - map.animateCamera(CameraUpdateFactory.zoomTo(actualZoom)); - return actualZoom; - } - @Override public @NonNull MapPoint getCenter() { LatLng target = map.getCameraPosition().target; return new MapPoint(target.latitude, target.longitude); } - @Override public void setCenter(@Nullable MapPoint center) { - if (center != null) { - LatLng target = new LatLng(center.lat, center.lon); - map.animateCamera(CameraUpdateFactory.newLatLng(target)); - } + @Override public double getZoom() { + return map.getCameraPosition().zoom; + } + + @Override public void zoomToPoint(@NonNull MapPoint center, double zoom) { + map.animateCamera(CameraUpdateFactory.newLatLngZoom(toLatLng(center), (float) zoom)); } @Override public void zoomToBoundingBox(Iterable points, double scaleFactor) { @@ -164,7 +155,7 @@ protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { return new LatLngBounds(new LatLng(south, west), new LatLng(north, east)); } - @Override public int addDraggableShape(Iterable points) { + @Override public int addDraggableShape(@NonNull Iterable points) { int featureId = nextFeatureId++; features.put(featureId, new DraggableShape(map, points)); return featureId; @@ -172,7 +163,7 @@ protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { @Override public void appendPointToShape(int featureId, @NonNull MapPoint point) { MapFeature feature = features.get(featureId); - if (feature != null && feature instanceof DraggableShape) { + if (feature instanceof DraggableShape) { ((DraggableShape) feature).addPoint(point); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java index 459c535df6c..285f163330a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java @@ -17,12 +17,11 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; -import android.view.View; import java.util.List; /** - * Interface for Fragments that render a map view. The plan is to have one + * Interface for a Fragment that renders a map view. The plan is to have one * implementation for each map SDK, e.g. GoogleMapFragment, OsmMapFragment, etc. * * This is intended to be a single map API that provides all functionality needed @@ -37,8 +36,8 @@ * * Editable points, traces, and shapes are called "map features" in this API. * To keep the API small, features are not exposed as objects; instead, they are - * identified by integer feature IDs. To keep the API unified (instead of behaving - * like three different APIs), the map always supports all three kinds of features, + * identified by integer feature IDs. To keep the API unified (instead of having + * three distinct modes), the map always supports all three kinds of features, * even though the geo widgets only use one kind of feature at a time. */ public interface MapFragment { @@ -50,8 +49,8 @@ public interface MapFragment { */ void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener); - /** Gets the View that is displaying the map. */ - View getView(); + /** Gets the point currently shown at the center of the map view. */ + @NonNull MapPoint getCenter(); /** * Gets the current zoom level. For maps that only support zooming by @@ -60,21 +59,15 @@ public interface MapFragment { double getZoom(); /** - * Changes the zoom level to bring it as close as possible to the given value, - * possibly with animation, and returns the actual zoom that will be reached. + * Centers the map view on the given point with a zoom level as close as + * possible to the given zoom level, possibly with animation. */ - double setZoom(double zoom); - - /** Gets the point currently shown at the center of the map view. */ - @NonNull MapPoint getCenter(); - - /** Pans the map view to center on the given point, possibly with animation. */ - void setCenter(@Nullable MapPoint center); + void zoomToPoint(@NonNull MapPoint center, double zoom); /** - * Adjusts the map's viewport and zoom level to enclose all of the given - * points. A scaleFactor of 1.0 ensures that all the points will be just - * visible in the viewport; a scaleFactor less than 1 shrinks the view + * Adjusts the map's viewport to enclose all of the given points, possibly + * with animation. A scaleFactor of 1.0 ensures that all the points will be + * just visible in the viewport; a scaleFactor less than 1 shrinks the view * beyond that. For example, a scaleFactor of 0.8 causes the bounding box * to occupy at most 80% of the width and 80% of the height of the viewport, * ensuring a margin of at least 10% on all sides. @@ -86,7 +79,7 @@ public interface MapFragment { * The polygon's vertices will have handles that can be dragged by the user. * Returns a positive integer, the featureId for the newly added polyline. */ - int addDraggableShape(Iterable points); + int addDraggableShape(@NonNull Iterable points); /** Appends a vertex to the polygonal shape specified by featureId. */ void appendPointToShape(int featureId, @NonNull MapPoint point); @@ -105,9 +98,8 @@ public interface MapFragment { /** * Enables/disables GPS tracking. While enabled, the GPS location is shown - * on the map, and fixes are passed to the listener (setGpsLocationListener). - * The activity is responsible for disabling GPS location if it doesn't want - * to keep getting callbacks after it has been stopped or destroyed. + * on the map, and the first GPS fix will trigger any pending callbacks set + * by runOnGpsLocationReady(). */ void setGpsLocationEnabled(boolean enabled); @@ -118,6 +110,9 @@ public interface MapFragment { * Queues a callback to be invoked on the UI thread as soon as a GPS fix is * available. If there already is a location fix, the callback is invoked * immediately; otherwise, when a fix is obtained, it will be invoked once. + * To begin searching for a GPS fix, call setGpsLocationEnabled(true). + * Activities that set callbacks should call setGpsLocationEnabled(false) + * in their onStop() or onDestroy() methods, to prevent invalid callbacks. */ void runOnGpsLocationReady(@NonNull ReadyListener listener); diff --git a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java index a1a3f42c52e..f3246762b2b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -57,8 +57,7 @@ public class OsmMapFragment extends Fragment implements MapFragment, MapEventsRe protected MyLocationNewOverlay myLocationOverlay; protected int nextFeatureId = 1; protected Map features = new HashMap<>(); - - @VisibleForTesting protected AlertDialog gpsErrorDialog; + protected AlertDialog gpsErrorDialog; @Override public void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener) { readyListener = listener; @@ -102,23 +101,18 @@ public MapView getMapView() { return false; } - @Override public double getZoom() { - return map.getZoomLevel(); - } - - @Override public double setZoom(double requestedZoom) { - int actualZoom = (int) Math.round( - clamp(requestedZoom, map.getMinZoomLevel(), map.getMaxZoomLevel())); - map.getController().setZoom(actualZoom); - return actualZoom; - } - @Override public @NonNull MapPoint getCenter() { return fromGeoPoint(map.getMapCenter()); } - @Override public void setCenter(@Nullable MapPoint center) { + @Override public double getZoom() { + return map.getZoomLevel(); + } + + @Override public void zoomToPoint(@NonNull MapPoint center, double zoom) { if (center != null) { + // setCenter() must be done last; setZoom() does not preserve the center. + map.getController().setZoom((int) Math.round(zoom)); map.getController().setCenter(toGeoPoint(center)); } } @@ -140,7 +134,7 @@ public MapView getMapView() { } } - @Override public int addDraggableShape(Iterable points) { + @Override public int addDraggableShape(@NonNull Iterable points) { int featureId = nextFeatureId++; features.put(featureId, new DraggableShape(map, points)); return featureId; @@ -155,7 +149,7 @@ public MapView getMapView() { @Override public @NonNull List getPointsOfShape(int featureId) { MapFeature feature = features.get(featureId); - if (feature != null && feature instanceof DraggableShape) { + if (feature instanceof DraggableShape) { return ((DraggableShape) feature).getPoints(); } return new ArrayList<>(); From 4b55e6cfb04b74d7e03295d94ad6106b4314ce03 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Tue, 4 Sep 2018 06:28:06 +0300 Subject: [PATCH 4/7] GeoShapeActivity: Fix zoom button and adjust default map view. --- .../android/activities/GeoShapeActivity.java | 14 ++++++++------ .../odk/collect/android/map/OsmMapFragment.java | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java index b311b52423c..f8d9967cba3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java @@ -49,7 +49,7 @@ public class GeoShapeActivity extends CollectAbstractActivity implements IRegist private MapFragment map; private int shapeId = -1; // will be a positive featureId once map is ready - private ImageButton gpsButton; + private ImageButton zoomButton; private ImageButton clearButton; private MapHelper helper; private AlertDialog zoomDialog; @@ -118,8 +118,8 @@ private void initMap(MapFragment newMapFragment) { } helper.setBasemap(); - gpsButton = findViewById(R.id.gps); - gpsButton.setOnClickListener(v -> showZoomDialog()); + zoomButton = findViewById(R.id.gps); + zoomButton.setOnClickListener(v -> showZoomDialog()); clearButton = findViewById(R.id.clear); clearButton.setOnClickListener(v -> showClearDialog()); @@ -137,7 +137,7 @@ private void initMap(MapFragment newMapFragment) { points = parsePoints(originalValue); } shapeId = map.addDraggableShape(points); - gpsButton.setEnabled(!points.isEmpty()); + zoomButton.setEnabled(!points.isEmpty()); clearButton.setEnabled(!points.isEmpty()); zoomDialogView = getLayoutInflater().inflate(R.layout.geo_zoom_dialog, null); @@ -161,13 +161,14 @@ private void initMap(MapFragment newMapFragment) { if (!points.isEmpty()) { map.zoomToBoundingBox(points, 0.8); } else { + map.zoomToPoint(new MapPoint(0, -30), 2); map.runOnGpsLocationReady(this::onGpsLocationReady); } } @SuppressWarnings("unused") private void onGpsLocationReady(MapFragment map) { - gpsButton.setEnabled(true); + zoomButton.setEnabled(true); if (getWindow().isActive()) { showZoomDialog(); } @@ -176,6 +177,7 @@ private void onGpsLocationReady(MapFragment map) { private void addVertex(MapPoint point) { map.appendPointToShape(shapeId, point); clearButton.setEnabled(true); + zoomButton.setEnabled(true); } private void clear() { @@ -311,7 +313,7 @@ public void onCancel(DialogInterface dialog) { } @VisibleForTesting public boolean isGpsButtonEnabled() { - return gpsButton != null && gpsButton.isEnabled(); + return zoomButton != null && zoomButton.isEnabled(); } @VisibleForTesting public boolean isZoomDialogShowing() { diff --git a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java index f3246762b2b..2b68c2a4620 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -76,6 +76,7 @@ public MapView getMapView() { map = view.findViewById(R.id.osm_map_view); map.setMultiTouchControls(true); map.setBuiltInZoomControls(true); + map.setMinZoomLevel(1); map.setTilesScaledToDpi(true); map.getOverlays().add(new MapEventsOverlay(this)); myLocationOverlay = new MyLocationNewOverlay(map); From 40d3b8fd91c77dcf8d57c7ff6118b1b05586c410 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Tue, 4 Sep 2018 23:49:20 -0400 Subject: [PATCH 5/7] GeoShapeActivity: Fix zooming to a polygon that has only one vertex. --- .../org/odk/collect/android/map/GoogleMapFragment.java | 8 ++++++-- .../java/org/odk/collect/android/map/OsmMapFragment.java | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java index a2942d78736..5a791113efa 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java @@ -121,11 +121,15 @@ public GoogleMap getGoogleMap() { if (points != null) { int count = 0; LatLngBounds.Builder builder = new LatLngBounds.Builder(); + LatLng latLng = null; for (MapPoint point : points) { - builder.include(toLatLng(point)); + latLng = toLatLng(point); + builder.include(latLng); count++; } - if (count > 0) { + if (count == 1) { + map.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 16)); + } else if (count > 1) { final LatLngBounds bounds = expandBounds(builder.build(), 1 / scaleFactor); new Handler().postDelayed(() -> { map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); diff --git a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java index 2b68c2a4620..c931fa1fffe 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -126,9 +126,10 @@ public MapView getMapView() { geoPoints.add(toGeoPoint(point)); count++; } - if (count > 0) { - map.getController().setZoom(4); - map.invalidate(); + if (count == 1) { + map.getController().setCenter(geoPoints.get(0)); + map.getController().setZoom(16); + } else if (count > 1) { map.zoomToBoundingBox(BoundingBox.fromGeoPoints( geoPoints).increaseByScale((float) (1 / scaleFactor)), true); } From 9fc8d2ed350950a286340dc9e998536b26d3e879 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Tue, 18 Sep 2018 13:00:55 -0700 Subject: [PATCH 6/7] OsmMapFragment: Add a 100-ms delay in zoomToBoundingBox(). --- .../org/odk/collect/android/map/OsmMapFragment.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java index c931fa1fffe..534cfa8df44 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -130,8 +130,16 @@ public MapView getMapView() { map.getController().setCenter(geoPoints.get(0)); map.getController().setZoom(16); } else if (count > 1) { - map.zoomToBoundingBox(BoundingBox.fromGeoPoints( - geoPoints).increaseByScale((float) (1 / scaleFactor)), true); + // TODO(ping): Find a better solution. + // zoomToBoundingBox sometimes fails to zoom correctly, either + // zooming by the correct amount but leaving the bounding box + // off-center, or centering correctly but not zooming in enough. + // Adding a 100-ms delay avoids the problem most of the time, but + // not always; it's here because the old GeoShapeOsmMapActivity + // did it, not because it's known to be the best solution. + final BoundingBox box = BoundingBox.fromGeoPoints(geoPoints) + .increaseByScale((float) (1 / scaleFactor)); + new Handler().postDelayed(() -> map.zoomToBoundingBox(box, false), 100); } } } From 6991ba6f940d76c1e9cd54cce49ea6afc85bdb15 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Wed, 5 Sep 2018 10:16:33 -0400 Subject: [PATCH 7/7] Clean up MapFragment implementations. (Makes GoogleMapFragment tolerate a null "map" field, which can occur when Google Play Services is unavailable during tests; allows each MapFragment implementation to define its own default view center and default zoom level; removes some unused methods.) --- .../android/activities/GeoShapeActivity.java | 24 ++----- .../android/map/GoogleMapFragment.java | 62 ++++++++++++++----- .../odk/collect/android/map/MapFragment.java | 12 +++- .../collect/android/map/OsmMapFragment.java | 32 +++++----- .../activities/GeoShapeActivityTest.java | 2 +- 5 files changed, 80 insertions(+), 52 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java index f8d9967cba3..3bb3083ff56 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeActivity.java @@ -15,7 +15,6 @@ package org.odk.collect.android.activities; import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; @@ -144,10 +143,7 @@ private void initMap(MapFragment newMapFragment) { zoomLocationButton = zoomDialogView.findViewById(R.id.zoom_location); zoomLocationButton.setOnClickListener(v -> { - MapPoint location = map.getGpsLocation(); - if (location != null) { - map.zoomToPoint(location, 16); - } + map.zoomToPoint(map.getGpsLocation()); zoomDialog.dismiss(); }); @@ -161,12 +157,11 @@ private void initMap(MapFragment newMapFragment) { if (!points.isEmpty()) { map.zoomToBoundingBox(points, 0.8); } else { - map.zoomToPoint(new MapPoint(0, -30), 2); map.runOnGpsLocationReady(this::onGpsLocationReady); } } - @SuppressWarnings("unused") + @SuppressWarnings("unused") // the "map" parameter is intentionally unused private void onGpsLocationReady(MapFragment map) { zoomButton.setEnabled(true); if (getWindow().isActive()) { @@ -275,17 +270,10 @@ private void showZoomDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.zoom_to_where)); builder.setView(zoomDialogView) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.cancel(); - zoomDialog.dismiss(); - } + .setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()) + .setOnCancelListener(dialog -> { + dialog.cancel(); + zoomDialog.dismiss(); }); zoomDialog = builder.create(); } diff --git a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java index 5a791113efa..317bcdcc1fa 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/GoogleMapFragment.java @@ -52,6 +52,10 @@ public class GoogleMapFragment extends SupportMapFragment implements MapFragment, LocationListener, LocationClient.LocationClientListener, GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener, GoogleMap.OnMarkerDragListener { + public static final LatLng INITIAL_CENTER = new LatLng(0, -30); + public static final float INITIAL_ZOOM = 2; + public static final float POINT_ZOOM = 16; + protected GoogleMap map; protected List gpsLocationReadyListeners = new ArrayList<>(); protected MapFragment.PointListener clickListener; @@ -63,8 +67,10 @@ public class GoogleMapFragment extends SupportMapFragment implements protected AlertDialog gpsErrorDialog; protected boolean gpsLocationEnabled; + // During Robolectric tests, Google Play Services is unavailable; sadly, the + // "map" field will be null and many operations will need to be stubbed out. @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "This flag is exposed for Robolectric tests to set") - @VisibleForTesting public static boolean callReadyListenerSynchronously; + @VisibleForTesting public static boolean testMode; @Override public void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener) { activity.getSupportFragmentManager() @@ -86,6 +92,8 @@ public class GoogleMapFragment extends SupportMapFragment implements // "go to my location" button; we have our own button for that. map.setMyLocationEnabled(true); map.getUiSettings().setMyLocationButtonEnabled(false); + map.setMinZoomPreference(1); + map.moveCamera(CameraUpdateFactory.newLatLngZoom(INITIAL_CENTER, INITIAL_ZOOM)); if (listener != null) { listener.onReady(this); } @@ -93,7 +101,7 @@ public class GoogleMapFragment extends SupportMapFragment implements // In Robolectric tests, getMapAsync() never gets around to calling its // callback; we have to invoke the ready listener directly. - if (callReadyListenerSynchronously) { + if (testMode) { listener.onReady(this); } } @@ -105,30 +113,48 @@ public GoogleMap getGoogleMap() { } @Override public @NonNull MapPoint getCenter() { + if (map == null) { // during Robolectric tests, map will be null + return fromLatLng(INITIAL_CENTER); + } LatLng target = map.getCameraPosition().target; return new MapPoint(target.latitude, target.longitude); } @Override public double getZoom() { + if (map == null) { // during Robolectric tests, map will be null + return INITIAL_ZOOM; + } return map.getCameraPosition().zoom; } - @Override public void zoomToPoint(@NonNull MapPoint center, double zoom) { - map.animateCamera(CameraUpdateFactory.newLatLngZoom(toLatLng(center), (float) zoom)); + @Override public void zoomToPoint(@Nullable MapPoint center) { + zoomToPoint(center, POINT_ZOOM); + } + + @Override public void zoomToPoint(@Nullable MapPoint center, double zoom) { + if (map == null) { // during Robolectric tests, map will be null + return; + } + if (center != null) { + map.animateCamera(CameraUpdateFactory.newLatLngZoom(toLatLng(center), (float) zoom)); + } } @Override public void zoomToBoundingBox(Iterable points, double scaleFactor) { + if (map == null) { // during Robolectric tests, map will be null + return; + } if (points != null) { int count = 0; LatLngBounds.Builder builder = new LatLngBounds.Builder(); - LatLng latLng = null; + MapPoint lastPoint = null; for (MapPoint point : points) { - latLng = toLatLng(point); - builder.include(latLng); + lastPoint = point; + builder.include(toLatLng(point)); count++; } if (count == 1) { - map.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 16)); + zoomToPoint(lastPoint); } else if (count > 1) { final LatLngBounds bounds = expandBounds(builder.build(), 1 / scaleFactor); new Handler().postDelayed(() -> { @@ -188,7 +214,9 @@ protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { } @Override public void clearFeatures() { - map.clear(); + if (map != null) { // during Robolectric tests, map will be null + map.clear(); + } features.clear(); } @@ -295,10 +323,6 @@ protected void updateFeatures() { } } - protected static double clamp(double x, double min, double max) { - return x < min ? min : x > max ? max : x; - } - protected static @NonNull MapPoint fromLatLng(@NonNull LatLng latLng) { return new MapPoint(latLng.latitude, latLng.longitude); } @@ -329,6 +353,9 @@ protected static class DraggableShape implements MapFeature { public DraggableShape(GoogleMap map, Iterable points) { this.map = map; + if (map == null) { // during Robolectric tests, map will be null + return; + } for (MapPoint point : points) { markers.add(map.addMarker( new MarkerOptions().position(toLatLng(point)).draggable(true))); @@ -367,8 +394,10 @@ public void dispose() { marker.remove(); } markers.clear(); - polygon.remove(); - polygon = null; + if (polygon != null) { + polygon.remove(); + polygon = null; + } } public List getPoints() { @@ -380,6 +409,9 @@ public List getPoints() { } public void addPoint(MapPoint point) { + if (map == null) { // during Robolectric tests, map will be null + return; + } markers.add(map.addMarker( new MarkerOptions().position(toLatLng(point)).draggable(true))); update(); diff --git a/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java index 285f163330a..29a388257e6 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/MapFragment.java @@ -58,11 +58,17 @@ public interface MapFragment { */ double getZoom(); + /** + * Centers the map view on the given point, zooming in to a close-up level + * deemed appropriate by the implementation, possibly with animation. + */ + void zoomToPoint(@Nullable MapPoint center); + /** * Centers the map view on the given point with a zoom level as close as * possible to the given zoom level, possibly with animation. */ - void zoomToPoint(@NonNull MapPoint center, double zoom); + void zoomToPoint(@Nullable MapPoint center, double zoom); /** * Adjusts the map's viewport to enclose all of the given points, possibly @@ -116,10 +122,10 @@ public interface MapFragment { */ void runOnGpsLocationReady(@NonNull ReadyListener listener); - /** Registers a callback for a click on the map. */ + /** Sets or clears the callback for a click on the map. */ void setClickListener(@Nullable PointListener listener); - /** Registers a callback for a long press on the map. */ + /** Sets or clears the callback for a long press on the map. */ void setLongPressListener(@Nullable PointListener listener); interface ReadyListener { diff --git a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java index 534cfa8df44..5ac4920706d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -50,6 +50,10 @@ import java.util.Map; public class OsmMapFragment extends Fragment implements MapFragment, MapEventsReceiver { + public static final GeoPoint INITIAL_CENTER = new GeoPoint(0.0, -30.0); + public static final int INITIAL_ZOOM = 2; + public static final int POINT_ZOOM = 16; + protected MapView map; protected ReadyListener readyListener; protected MapFragment.PointListener clickListener; @@ -71,12 +75,15 @@ public MapView getMapView() { return map; } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + @Override public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.osm_map_layout, container, false); - map = view.findViewById(R.id.osm_map_view); + map = view.findViewById(R.id.osm_map_view); map.setMultiTouchControls(true); map.setBuiltInZoomControls(true); map.setMinZoomLevel(1); + map.getController().setCenter(INITIAL_CENTER); + map.getController().setZoom(INITIAL_ZOOM); map.setTilesScaledToDpi(true); map.getOverlays().add(new MapEventsOverlay(this)); myLocationOverlay = new MyLocationNewOverlay(map); @@ -110,7 +117,11 @@ public MapView getMapView() { return map.getZoomLevel(); } - @Override public void zoomToPoint(@NonNull MapPoint center, double zoom) { + @Override public void zoomToPoint(@Nullable MapPoint center) { + zoomToPoint(center, POINT_ZOOM); + } + + @Override public void zoomToPoint(@Nullable MapPoint center, double zoom) { if (center != null) { // setCenter() must be done last; setZoom() does not preserve the center. map.getController().setZoom((int) Math.round(zoom)); @@ -122,13 +133,14 @@ public MapView getMapView() { if (points != null) { int count = 0; List geoPoints = new ArrayList<>(); + MapPoint lastPoint = null; for (MapPoint point : points) { + lastPoint = point; geoPoints.add(toGeoPoint(point)); count++; } if (count == 1) { - map.getController().setCenter(geoPoints.get(0)); - map.getController().setZoom(16); + zoomToPoint(lastPoint); } else if (count > 1) { // TODO(ping): Find a better solution. // zoomToBoundingBox sometimes fails to zoom correctly, either @@ -231,16 +243,6 @@ protected void showGpsDisabledAlert() { return gpsErrorDialog; } - protected void updateFeatures() { - for (MapFeature feature : features.values()) { - feature.update(); - } - } - - protected static double clamp(double x, double min, double max) { - return x < min ? min : x > max ? max : x; - } - protected static @NonNull MapPoint fromGeoPoint(@NonNull IGeoPoint geoPoint) { return new MapPoint(geoPoint.getLatitude(), geoPoint.getLongitude()); } diff --git a/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java b/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java index 48939882bbd..dee62aaa48d 100644 --- a/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/location/activities/GeoShapeActivityTest.java @@ -49,7 +49,7 @@ public class GeoShapeActivityTest extends BaseGeoActivityTest { @Before public void setUp() throws Exception { super.setUp(); LocationClients.setTestClient(locationClient); - GoogleMapFragment.callReadyListenerSynchronously = true; + GoogleMapFragment.testMode = true; controller = Robolectric.buildActivity(GeoShapeActivity.class); activity = controller.create().start().resume().visible().get(); map = (GoogleMapFragment) activity.getMapFragment();