From 76cd7f8a01c075f4d0a11bd7b267049c884cabe0 Mon Sep 17 00:00:00 2001 From: Ka-Ping Yee Date: Fri, 17 Aug 2018 10:29:31 -0700 Subject: [PATCH] Implement GeoShapeOsmMapActivity in terms of the MapFragment interface, and provide an OSM implementation of MapFragment. --- collect_app/src/main/AndroidManifest.xml | 3 + .../activities/GeoShapeGoogleMapActivity.java | 120 ++-- .../GeoShapeOldGoogleMapActivity.java | 2 +- .../activities/GeoShapeOldOsmMapActivity.java | 596 ++++++++++++++++++ .../activities/GeoShapeOsmMapActivity.java | 546 ++++------------ .../android/map/GoogleMapFragment.java | 44 +- .../odk/collect/android/map/MapFragment.java | 24 +- .../collect/android/map/OsmMapFragment.java | 322 ++++++++++ .../android/widgets/GeoShapeWidget.java | 17 +- 9 files changed, 1169 insertions(+), 505 deletions(-) create mode 100644 collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldOsmMapActivity.java create mode 100644 collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java diff --git a/collect_app/src/main/AndroidManifest.xml b/collect_app/src/main/AndroidManifest.xml index 73a820ddf9f..3e5677e0b14 100644 --- a/collect_app/src/main/AndroidManifest.xml +++ b/collect_app/src/main/AndroidManifest.xml @@ -191,6 +191,9 @@ the specific language governing permissions and limitations under the License. + 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 index b1140f34d39..6d7e11ab6e5 100644 --- 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 @@ -53,7 +53,6 @@ public class GeoShapeGoogleMapActivity extends CollectAbstractActivity { private View zoomDialogView; private Button zoomPointButton; private Button zoomLocationButton; - private boolean foundFirstLocation; @Override public void onCreate(Bundle savedInstanceState) { @@ -65,10 +64,11 @@ public void onCreate(Bundle savedInstanceState) { } requestWindowFeature(Window.FEATURE_NO_TITLE); + setTitle(getString(R.string.geoshape_title)); setContentView(R.layout.geoshape_layout); // TODO(ping): Remove when we're ready to use this class. - ((TextView) findViewById(R.id.top_text)).setText("new GeoShapeActivity"); + ((TextView) findViewById(R.id.top_text)).setText("new Google GeoShapeActivity"); createMapFragment().addTo(this, R.id.map_container, this::setupMap); } @@ -100,10 +100,9 @@ private void setupMap(MapFragment newMapFragment) { map = newMapFragment; map.setGpsLocationEnabled(true); - map.setGpsLocationListener(this::onLocationFix); map.setLongPressListener(this::addVertex); - helper = new MapHelper(this, newMapFragment); + helper = new MapHelper(this, map); gpsButton = findViewById(R.id.gps); gpsButton.setOnClickListener(v -> showZoomDialog()); @@ -114,6 +113,9 @@ private void setupMap(MapFragment newMapFragment) { 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)) { @@ -123,14 +125,21 @@ private void setupMap(MapFragment newMapFragment) { gpsButton.setEnabled(!points.isEmpty()); clearButton.setEnabled(!points.isEmpty()); - ImageButton layersButton = findViewById(R.id.layers); - layersButton.setOnClickListener(v -> helper.showLayersDialog()); + if (!points.isEmpty()) { + map.zoomToBoundingBox(points, 0.8); + } else { + map.runOnGpsLocationReady(this::onGpsLocationReady); + } zoomDialogView = getLayoutInflater().inflate(R.layout.geo_zoom_dialog, null); zoomLocationButton = zoomDialogView.findViewById(R.id.zoom_location); zoomLocationButton.setOnClickListener(v -> { - map.setCenter(map.getGpsLocation()); + MapPoint location = map.getGpsLocation(); + if (location != null) { + map.setCenter(location); + map.setZoom(15); + } zoomDialog.dismiss(); }); @@ -142,7 +151,7 @@ private void setupMap(MapFragment newMapFragment) { // If there is a last know location go there if (hasWindowFocus() && map.getGpsLocation() != null) { - foundFirstLocation = true; + // foundFirstLocation = true; gpsButton.setEnabled(true); showZoomDialog(); } @@ -150,6 +159,34 @@ private void setupMap(MapFragment newMapFragment) { helper.setBasemap(); } + private void onGpsLocationReady(MapFragment map) { + gpsButton.setEnabled(true); + if (hasWindowFocus()) { + 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 finishWithResult() { List points = map.getPointsOfShape(shapeId); if (points.size() < 3) { @@ -209,36 +246,6 @@ private String formatPoints(List points) { return result; } - private void onLocationFix(MapPoint point) { - gpsButton.setEnabled(true); - if (hasWindowFocus() && !foundFirstLocation) { - foundFirstLocation = true; - showZoomDialog(); - } - } - - private void addVertex(MapPoint point) { - map.appendPointToShape(shapeId, point); - clearButton.setEnabled(true); - } - - private void clear() { - map.clearFeatures(); - shapeId = map.addDraggableShape(new ArrayList<>()); - map.setLongPressListener(this::addVertex); - clearButton.setEnabled(false); - } - - private void showClearDialog() { - if (!map.getPointsOfShape(shapeId).isEmpty()) { - new AlertDialog.Builder(this) - .setMessage(getString(R.string.geo_clear_warning)) - .setPositiveButton(getString(R.string.clear), (dialog, id) -> clear()) - .setNegativeButton(R.string.cancel, null) - .show(); - } - } - public void showZoomDialog() { if (zoomDialog == null) { AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -259,28 +266,25 @@ public void onCancel(DialogInterface dialog) { zoomDialog = builder.create(); } - if (zoomLocationButton != null) { - 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")); - } + 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(); } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldGoogleMapActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldGoogleMapActivity.java index b673e6f0e4b..9cad81050ea 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldGoogleMapActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldGoogleMapActivity.java @@ -98,7 +98,7 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.geoshape_layout); // TODO(ping): Remove when we're ready to use the new class. - ((TextView) findViewById(R.id.top_text)).setText("old GeoShapeActivity"); + ((TextView) findViewById(R.id.top_text)).setText("old Google GeoShapeActivity"); SupportMapFragment mapFragment = new SupportMapFragment(); getSupportFragmentManager().beginTransaction() diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldOsmMapActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldOsmMapActivity.java new file mode 100644 index 00000000000..25bc1c7d41c --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOldOsmMapActivity.java @@ -0,0 +1,596 @@ +/* + * 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 android.widget.TextView; + +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 GeoShapeOldOsmMapActivity 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); + + // TODO(ping): Remove when we're ready to use this class. + ((TextView) findViewById(R.id.top_text)).setText("old OSM GeoShapeActivity"); + + 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(GeoShapeOldOsmMapActivity.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/activities/GeoShapeOsmMapActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/GeoShapeOsmMapActivity.java index e2daf1bbcef..3d4ba040c29 100644 --- 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 @@ -18,40 +18,25 @@ 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 android.widget.TextView; import org.odk.collect.android.R; -import org.odk.collect.android.fragments.OsmMapFragment; +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.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 java.util.Locale; import static org.odk.collect.android.utilities.PermissionUtils.checkIfLocationPermissionsGranted; @@ -63,25 +48,14 @@ */ 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 MapFragment map; + private int shapeId = -1; // will be a positive featureId once map is ready private ImageButton clearButton; - public boolean gpsStatus = true; - private ImageButton locationButton; - public MyLocationNewOverlay myLocationOverlay; - public boolean dataLoaded; + private ImageButton gpsButton; private MapHelper helper; - private AlertDialog zoomDialog; private View zoomDialogView; - private Button zoomPointButton; private Button zoomLocationButton; @@ -97,125 +71,74 @@ protected void onCreate(Bundle savedInstanceState) { 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); + + // TODO(ping): Remove when we're ready to use this class. + ((TextView) findViewById(R.id.top_text)).setText("new OSM GeoShapeActivity"); + + createMapFragment().addTo(this, R.id.map_container, 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() { + // TODO(ping): Select the appropriate MapFragment implementation (Google or OSM). + public static MapFragment createMapFragment() { + return new OsmMapFragment(); + } - @Override - public void onClick(View v) { - helper.showLayersDialog(); + private void setupMap(MapFragment newMapFragment) { + map = newMapFragment; + map.setGpsLocationEnabled(true); + map.setLongPressListener(this::addVertex); - } - }); - locationButton = findViewById(R.id.gps); - locationButton.setEnabled(false); - locationButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View v) { - showZoomDialog(); - } - }); + helper = new MapHelper(this, map); - GpsMyLocationProvider imlp = new GpsMyLocationProvider(this.getBaseContext()); - imlp.setLocationUpdateMinDistance(1000); - imlp.setLocationUpdateMinTime(60000); - myLocationOverlay = new MyLocationNewOverlay(map); + gpsButton = findViewById(R.id.gps); + gpsButton.setOnClickListener(v -> showZoomDialog()); - 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); + 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()); - map.invalidate(); + if (!points.isEmpty()) { + map.zoomToBoundingBox(points, 0.8); + } else { + map.runOnGpsLocationReady(this::onGpsLocationReady); + } 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(); + 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(new View.OnClickListener() { - - @Override - public void onClick(View v) { - //zoomToCentroid(); - zoomToBounds(); - map.invalidate(); - zoomDialog.dismiss(); - } + zoomPointButton.setOnClickListener(v -> { + map.zoomToBoundingBox(map.getPointsOfShape(shapeId), 0.8); + zoomDialog.dismiss(); }); } - @Override - protected void onResume() { - super.onResume(); - if (map != null) { - helper.setBasemap(); - } - - upMyLocationOverlayLayers(); - } - @Override public void onBackPressed() { - if (!mapMarkers.isEmpty()) { + if (!map.getPointsOfShape(shapeId).isEmpty()) { showBackDialog(); } else { finish(); @@ -223,325 +146,114 @@ public void onBackPressed() { } @Override - protected void onPause() { - disableMyLocation(); - super.onPause(); + protected void onStart() { + super.onStart(); + if (map != null) map.setGpsLocationEnabled(true); } @Override protected void onStop() { - disableMyLocation(); + map.setGpsLocationEnabled(false); 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); + private void onGpsLocationReady(MapFragment map) { + gpsButton.setEnabled(true); + if (hasWindowFocus()) { + showZoomDialog(); } - 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 addVertex(MapPoint point) { + map.appendPointToShape(shapeId, point); + clearButton.setEnabled(true); } - private void clearFeatures() { - clearButtonTest = false; - mapMarkers.clear(); - polyline.setPoints(new ArrayList()); - map.getOverlays().clear(); + private void clear() { + map.clearFeatures(); + shapeId = map.addDraggableShape(new ArrayList<>()); 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(); - + 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() { - 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(); + 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 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) { + private void finishWithResult() { + List points = map.getPointsOfShape(shapeId); + if (points.size() < 3) { 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()); + return; } - points.add(mapMarkers.get(0).getPosition()); - polyline.setPoints(points); - map.invalidate(); + setResult(RESULT_OK, new Intent().putExtra( + FormEntryActivity.GEOSHAPE_RESULTS, formatPoints(points))); + finish(); } - private final MapEventsReceiver receive = new MapEventsReceiver() { - @Override - public boolean longPressHelper(GeoPoint point) { - if (!clearButtonTest) { - clearButton.setEnabled(true); - clearButtonTest = true; + /** + * 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) continue; + double lat, lon; + try { + lat = Double.parseDouble(words[0]); + lon = Double.parseDouble(words[1]); + } catch (NumberFormatException e) { + continue; } - 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; + points.add(new MapPoint(lat, lon)); } - }; - - 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(); - + // 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; + } - @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 - + /** + * Serializes a list of polygon vertices into a string, in the format + * appropriate for storing as the result of this form question. */ - 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(); + 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)); } - }, 100); - map.invalidate(); - + for (MapPoint point : points) { + result += String.format(Locale.US, "%.6f %.6f 0.0 0.0;", point.lat, point.lon); + } + } + return result; } public void showZoomDialog() { - if (zoomDialog == null) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.zoom_to_where)); @@ -560,8 +272,8 @@ public void onCancel(DialogInterface dialog) { }); zoomDialog = builder.create(); } - //If feature enable zoom to button else disable - if (myLocationOverlay.getMyLocation() != null) { + + if (map.getGpsLocation() != null) { zoomLocationButton.setEnabled(true); zoomLocationButton.setBackgroundColor(Color.parseColor("#50cccccc")); zoomLocationButton.setTextColor(themeUtils.getPrimaryTextColor()); @@ -571,7 +283,7 @@ public void onCancel(DialogInterface dialog) { zoomLocationButton.setTextColor(Color.parseColor("#FF979797")); } - if (!mapMarkers.isEmpty()) { + if (!map.getPointsOfShape(shapeId).isEmpty()) { zoomPointButton.setEnabled(true); zoomPointButton.setBackgroundColor(Color.parseColor("#50cccccc")); zoomPointButton.setTextColor(themeUtils.getPrimaryTextColor()); 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 7a888a71dfd..44734461ec6 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 @@ -1,7 +1,6 @@ package org.odk.collect.android.map; import android.app.AlertDialog; -import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.location.Location; @@ -34,9 +33,8 @@ import java.util.Map; public class GoogleMapFragment extends SupportMapFragment implements MapFragment, GoogleMap.OnMapClickListener, GoogleMap.OnMarkerDragListener, GoogleMap.OnMapLongClickListener, LocationListener, LocationClient.LocationClientListener{ - protected Context context; protected GoogleMap map; - protected MapFragment.PointListener gpsLocationListener; + protected List gpsLocationReadyListeners = new ArrayList<>(); protected MapFragment.PointListener clickListener; protected MapFragment.PointListener longPressListener; protected LocationClient locationClient; @@ -47,7 +45,6 @@ public class GoogleMapFragment extends SupportMapFragment implements MapFragment @VisibleForTesting protected AlertDialog gpsErrorDialog; @Override public void addTo(@NonNull FragmentActivity activity, int containerId, @Nullable ReadyListener listener) { - context = activity; activity.getSupportFragmentManager() .beginTransaction().add(containerId, this).commit(); getMapAsync((GoogleMap map) -> { @@ -82,7 +79,7 @@ public GoogleMap getGoogleMap() { } @Override public double setZoom(double requestedZoom) { - float actualZoom = (float) requestedZoom; + float actualZoom = (float) clamp(requestedZoom, map.getMinZoomLevel(), map.getMaxZoomLevel()); map.animateCamera(CameraUpdateFactory.zoomTo(actualZoom)); return actualZoom; } @@ -99,7 +96,7 @@ public GoogleMap getGoogleMap() { } } - @Override public void zoomToBoundingBox(Iterable points, double paddingFactor) { + @Override public void zoomToBoundingBox(Iterable points, double scaleFactor) { if (points == null) return; LatLngBounds.Builder builder = new LatLngBounds.Builder(); @@ -109,7 +106,7 @@ public GoogleMap getGoogleMap() { count++; } if (count > 0) { - final LatLngBounds bounds = expandBounds(builder.build(), 1/paddingFactor); + final LatLngBounds bounds = expandBounds(builder.build(), 1/scaleFactor); new Handler().postDelayed(() -> { map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); }, 100); @@ -166,17 +163,17 @@ protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { features.clear(); } - @Override public void setClickListener(PointListener listener) { + @Override public void setClickListener(@Nullable PointListener listener) { clickListener = listener; } - @Override public void setLongPressListener(PointListener listener) { + @Override public void setLongPressListener(@Nullable PointListener listener) { longPressListener = listener; } @Override public void setGpsLocationEnabled(boolean enabled) { if (enabled) { - locationClient = LocationClients.clientForContext(context); + locationClient = LocationClients.clientForContext(getActivity()); locationClient.setListener(this); locationClient.start(); } else { @@ -184,18 +181,23 @@ protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { } } - @Override public void setGpsLocationListener(PointListener listener) { - gpsLocationListener = listener; + @Override public void runOnGpsLocationReady(@NonNull ReadyListener listener) { + if (lastLocationFix != null) { + listener.onReady(this); + } else { + gpsLocationReadyListeners.add(listener); + } } @Override public void onLocationChanged(Location location) { - if (gpsLocationListener != null) { - lastLocationFix = new MapPoint(location.getLatitude(), location.getLongitude()); - gpsLocationListener.onPoint(lastLocationFix); + lastLocationFix = new MapPoint(location.getLatitude(), location.getLongitude()); + for (ReadyListener listener : gpsLocationReadyListeners) { + listener.onReady(this); } + gpsLocationReadyListeners.clear(); } - @Override public MapPoint getGpsLocation() { + @Override public @Nullable MapPoint getGpsLocation() { return lastLocationFix; } @@ -237,7 +239,7 @@ protected LatLngBounds expandBounds(LatLngBounds bounds, double factor) { @Override public void onClientStop() { } protected void showGpsDisabledAlert() { - gpsErrorDialog = new AlertDialog.Builder(context) + gpsErrorDialog = new AlertDialog.Builder(getActivity()) .setMessage(getString(R.string.gps_enable_message)) .setCancelable(false) .setPositiveButton(getString(R.string.enable_gps), @@ -259,11 +261,15 @@ protected void updateFeatures() { } } - protected static MapPoint fromLatLng(LatLng latLng) { + 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 LatLng toLatLng(MapPoint point) { + protected static @NonNull LatLng toLatLng(@NonNull MapPoint point) { return new LatLng(point.lat, point.lon); } 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 3474e50ec9d..6a1e91b3a67 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 @@ -10,6 +10,22 @@ /** * 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 { /** @@ -84,8 +100,12 @@ public interface MapFragment { /** Gets the last GPS location fix, or null if there hasn't been one. */ @Nullable MapPoint getGpsLocation(); - /** Registers a callback for GPS location fixes. */ - void setGpsLocationListener(@Nullable PointListener listener); + /** + * Queues a callback to be invoked as soon as a GPS location 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); 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..ec2cfc1026d --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/map/OsmMapFragment.java @@ -0,0 +1,322 @@ +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.odk.collect.android.location.client.LocationClient; +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 List gpsLocationReadyListeners = new ArrayList<>(); + protected MapFragment.PointListener clickListener; + protected MapFragment.PointListener longPressListener; + protected MyLocationNewOverlay myLocationOverlay; + + protected LocationClient locationClient; + protected MapPoint lastLocationFix = null; + 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(); + } + + @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) return; + 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(() -> { 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()); + } + 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/widgets/GeoShapeWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/GeoShapeWidget.java index ff1680f2d67..bb6dfbe37ee 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 @@ -31,14 +31,17 @@ import org.odk.collect.android.activities.FormEntryActivity; import org.odk.collect.android.activities.GeoShapeGoogleMapActivity; import org.odk.collect.android.activities.GeoShapeOldGoogleMapActivity; +import org.odk.collect.android.activities.GeoShapeOldOsmMapActivity; import org.odk.collect.android.activities.GeoShapeOsmMapActivity; import org.odk.collect.android.listeners.PermissionListener; import org.odk.collect.android.preferences.PreferenceKeys; import org.odk.collect.android.utilities.PlayServicesUtil; import org.odk.collect.android.widgets.interfaces.BinaryWidget; -import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; -import static org.odk.collect.android.utilities.PermissionUtils.requestLocationPermissions; +import static org.odk.collect.android.utilities.ApplicationConstants + .RequestCodes; +import static org.odk.collect.android.utilities.PermissionUtils + .requestLocationPermissions; /** * GeoShapeWidget is the widget that allows the user to get Collect multiple GPS points. @@ -94,17 +97,15 @@ private void startGeoShapeActivity() { Intent i; if (mapSDK.equals(GOOGLE_MAP_KEY)) { if (PlayServicesUtil.isGooglePlayServicesAvailable(getContext())) { - if (isRingerOn()) { - i = new Intent(getContext(), GeoShapeOldGoogleMapActivity.class); - } else { - i = new Intent(getContext(), GeoShapeGoogleMapActivity.class); - } + i = new Intent(getContext(), isRingerOn() ? + GeoShapeOldGoogleMapActivity.class : GeoShapeGoogleMapActivity.class); } else { PlayServicesUtil.showGooglePlayServicesAvailabilityErrorDialog(getContext()); return; } } else { - i = new Intent(getContext(), GeoShapeOsmMapActivity.class); + i = new Intent(getContext(), isRingerOn() ? + GeoShapeOldOsmMapActivity.class : GeoShapeOsmMapActivity.class); } String s = answerDisplay.getText().toString(); if (s.length() != 0) {