Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define the MapFragment interface; factor its implementation out of both GeoShape activities. #2465

Merged
6 changes: 5 additions & 1 deletion collect_app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down Expand Up @@ -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"

Expand Down
8 changes: 2 additions & 6 deletions collect_app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,7 @@ the specific language governing permissions and limitations under the License.
android:name=".activities.GeoPointOsmMapActivity"
android:configChanges="orientation" />
<activity
android:name=".activities.GeoShapeOsmMapActivity"
android:configChanges="orientation" />
<activity
android:name=".activities.GeoShapeGoogleMapActivity"
android:name=".activities.GeoShapeActivity"
android:configChanges="orientation" />
<activity
android:name=".activities.GeoTraceOsmMapActivity"
Expand Down Expand Up @@ -310,5 +307,4 @@ the specific language governing permissions and limitations under the License.
</intent-filter>
</receiver>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
/*
* 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.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 zoomButton;
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);

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 (!formatPoints(map.getPointsOfShape(shapeId)).equals(originalValue)) {
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();

zoomButton = findViewById(R.id.gps);
zoomButton.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<MapPoint> points = new ArrayList<>();
Intent intent = getIntent();
if (intent != null && intent.hasExtra(GeoShapeWidget.SHAPE_LOCATION)) {
originalValue = intent.getStringExtra(GeoShapeWidget.SHAPE_LOCATION);
points = parsePoints(originalValue);
}
shapeId = map.addDraggableShape(points);
zoomButton.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 -> {
map.zoomToPoint(map.getGpsLocation());
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") // the "map" parameter is intentionally unused
private void onGpsLocationReady(MapFragment map) {
zoomButton.setEnabled(true);
if (getWindow().isActive()) {
showZoomDialog();
}
}

private void addVertex(MapPoint point) {
map.appendPointToShape(shapeId, point);
clearButton.setEnabled(true);
zoomButton.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<MapPoint> 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<MapPoint> parsePoints(String coords) {
List<MapPoint> 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<MapPoint> 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, (dialog, id) -> dialog.cancel())
.setOnCancelListener(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 zoomButton != null && zoomButton.isEnabled();
}

@VisibleForTesting public boolean isZoomDialogShowing() {
return zoomDialog != null && zoomDialog.isShowing();
}

@VisibleForTesting public MapFragment getMapFragment() {
return map;
}
}
Loading