diff --git a/src/androidTest/java/de/blau/android/photos/PhotosTest.java b/src/androidTest/java/de/blau/android/photos/PhotosTest.java
index e77ea379ae..6c1a770998 100644
--- a/src/androidTest/java/de/blau/android/photos/PhotosTest.java
+++ b/src/androidTest/java/de/blau/android/photos/PhotosTest.java
@@ -128,12 +128,17 @@ public void teardown() {
*/
// @SdkSuppress(minSdkVersion = 26)
@Test
- public void selectDisplayDelete() {
+ public void selectDisplayInfoDelete() {
addLayerAndIndex();
TestUtils.unlock(device);
assertEquals(2, App.getPhotoIndex().count());
TestUtils.clickAtCoordinates(device, main.getMap(), 7.5886112, 47.5519448, true);
+ TestUtils.clickMenuButton(device, context.getString(R.string.menu_information), false, true);
+ assertTrue(TestUtils.findText(device, false, context.getString(R.string.image_information_title)));
+ TestUtils.sleep(20000);
+ assertTrue(TestUtils.clickText(device, false, context.getString(R.string.done),true, false));
+
TestUtils.clickMenuButton(device, context.getString(R.string.delete), false, true);
assertTrue(TestUtils.clickText(device, false, context.getString(R.string.photo_viewer_delete_button), true, false));
diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java
index d40b860822..ffdb31bcf4 100644
--- a/src/main/java/de/blau/android/Main.java
+++ b/src/main/java/de/blau/android/Main.java
@@ -1029,7 +1029,7 @@ private void processIntents() {
Uri uri = intent.getData();
if (!index.deletePhoto(this, uri)) {
String path = ContentResolverUtil.getPath(this, uri);
- if (path != null && !index.deletePhoto(this, path)) {
+ if (path == null || !index.deletePhoto(this, path)) {
Log.e(DEBUG_TAG, "deleting " + uri + " from index failed");
}
}
diff --git a/src/main/java/de/blau/android/contract/Urls.java b/src/main/java/de/blau/android/contract/Urls.java
index df0d051beb..7758a854bc 100644
--- a/src/main/java/de/blau/android/contract/Urls.java
+++ b/src/main/java/de/blau/android/contract/Urls.java
@@ -29,7 +29,7 @@ private Urls() {
public static final String DEFAULT_OAM_SERVER = "https://api.openaerialmap.org/";
// these are only configurable for testing
- public static final String DEFAULT_MAPILLARY_IMAGES_V4 = "https://graph.mapillary.com/%s?access_token=%s&fields=thumb_2048_url,computed_geometry";
+ public static final String DEFAULT_MAPILLARY_IMAGES_V4 = "https://graph.mapillary.com/%s?access_token=%s&fields=thumb_2048_url,computed_geometry,computed_compass_angle,captured_at";
public static final String DEFAULT_MAPILLARY_SEQUENCES_URL_V4 = "https://graph.mapillary.com/image_ids?sequence_id=%s&access_token=%s&fields=id";
public static final String DEFAULT_OSM_WIKI = "https://wiki.openstreetmap.org/";
diff --git a/src/main/java/de/blau/android/dialogs/ElementInfo.java b/src/main/java/de/blau/android/dialogs/ElementInfo.java
index c07a311a37..7001693863 100644
--- a/src/main/java/de/blau/android/dialogs/ElementInfo.java
+++ b/src/main/java/de/blau/android/dialogs/ElementInfo.java
@@ -25,7 +25,6 @@
import android.text.SpannedString;
import android.text.style.StyleSpan;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
@@ -60,7 +59,6 @@
import de.blau.android.util.DateFormatter;
import de.blau.android.util.InfoDialogFragment;
import de.blau.android.util.ScreenMessage;
-import de.blau.android.util.ThemeUtils;
import de.blau.android.util.Util;
import de.blau.android.validation.Validator;
@@ -264,24 +262,16 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
return builder.create();
}
- /**
- * Create the view we want to display
- *
- * @param container parent view or null
- * @return the View
- */
@Override
protected View createView(ViewGroup container) {
- FragmentActivity activity = getActivity();
- LayoutInflater themedInflater = ThemeUtils.getLayoutInflater(activity);
- ScrollView sv = (ScrollView) themedInflater.inflate(R.layout.element_info_view, container, false);
+ ScrollView sv = createEmptyView(container);
TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout);
+ TableLayout.LayoutParams tp = getTableLayoutParams();
boolean compare = ue != null;
- TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- tp.setMargins(10, 2, 10, 2);
if (element != null) {
+ FragmentActivity activity = getActivity();
boolean deleted = element.getState() == OsmElement.STATE_DELETED;
tl.setColumnStretchable(1, true);
tl.setColumnStretchable(2, true);
diff --git a/src/main/java/de/blau/android/dialogs/FeatureInfo.java b/src/main/java/de/blau/android/dialogs/FeatureInfo.java
index 3595462ff9..2d2c351aff 100644
--- a/src/main/java/de/blau/android/dialogs/FeatureInfo.java
+++ b/src/main/java/de/blau/android/dialogs/FeatureInfo.java
@@ -1,7 +1,6 @@
package de.blau.android.dialogs;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
import com.google.gson.JsonElement;
@@ -12,9 +11,7 @@
import android.app.Dialog;
import android.os.Bundle;
-import android.text.Spanned;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
@@ -167,15 +164,12 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
@Override
protected View createView(@Nullable ViewGroup container) {
- FragmentActivity activity = getActivity();
- LayoutInflater inflater = ThemeUtils.getLayoutInflater(activity);
- ScrollView sv = (ScrollView) inflater.inflate(R.layout.element_info_view, container, false);
+ ScrollView sv = createEmptyView(container);
TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout);
-
- TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- tp.setMargins(10, 2, 10, 2);
+ TableLayout.LayoutParams tp = getTableLayoutParams();
if (feature != null) {
+ FragmentActivity activity = getActivity();
tl.setColumnShrinkable(1, true);
Geometry geometry = feature.geometry();
if (geometry != null) {
@@ -196,8 +190,8 @@ protected View createView(@Nullable ViewGroup container) {
Util.sharePosition(getActivity(), new double[] { p.longitude(), p.latitude() }, null);
});
- tl.addView(TableLayoutUtils.createRowWithButton(activity, R.string.location_lon_label, prettyPrint(p.longitude()), button, tp));
- tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, prettyPrint(p.latitude()), tp));
+ tl.addView(TableLayoutUtils.createRowWithButton(activity, R.string.location_lon_label, prettyPrintCoord(p.longitude()), button, tp));
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, prettyPrintCoord(p.latitude()), tp));
}
}
tl.addView(TableLayoutUtils.divider(activity));
@@ -220,25 +214,4 @@ protected View createView(@Nullable ViewGroup container) {
}
return sv;
}
-
- /**
- * Get the string resource formated as an italic string
- *
- * @param resId String resource id
- * @return a Spanned containing the string
- */
- private Spanned toItalic(int resId) {
- return Util.fromHtml("" + getString(resId) + "");
- }
-
- /**
- * Pretty print a coordinate value
- *
- * @param coord the coordinate in WGS84
- * @return a reasonable looking string representation
- */
- @NonNull
- private static String prettyPrint(double coord) {
- return String.format(Locale.US, "%.7f", coord) + "°";
- }
}
diff --git a/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java b/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java
index 5f8fb60f82..aea5147a0e 100644
--- a/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java
+++ b/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java
@@ -9,7 +9,6 @@
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
@@ -35,7 +34,6 @@
import de.blau.android.util.GeoMath;
import de.blau.android.util.InfoDialogFragment;
import de.blau.android.util.ScreenMessage;
-import de.blau.android.util.ThemeUtils;
import de.blau.android.util.Util;
/**
@@ -251,18 +249,13 @@ public void run() {
@Override
protected View createView(@Nullable ViewGroup container) {
- LayoutInflater inflater;
- FragmentActivity activity = getActivity();
- inflater = ThemeUtils.getLayoutInflater(activity);
- ScrollView sv = (ScrollView) inflater.inflate(R.layout.element_info_view, container, false);
+ ScrollView sv = createEmptyView(container);
tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout);
-
- tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- tp.setMargins(10, 2, 10, 2);
+ tp = getTableLayoutParams();
if (location != null) {
tl.setColumnShrinkable(1, true);
- updateView(activity, tl, tp);
+ updateView(getActivity(), tl, tp);
tl.postDelayed(update, 1000);
}
return sv;
diff --git a/src/main/java/de/blau/android/dialogs/ImageInfo.java b/src/main/java/de/blau/android/dialogs/ImageInfo.java
new file mode 100644
index 0000000000..1346d4c3f5
--- /dev/null
+++ b/src/main/java/de/blau/android/dialogs/ImageInfo.java
@@ -0,0 +1,142 @@
+package de.blau.android.dialogs;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+import android.widget.TableLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AlertDialog.Builder;
+import androidx.appcompat.app.AppCompatDialog;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import de.blau.android.R;
+import de.blau.android.listener.DoNothingListener;
+import de.blau.android.photos.Photo;
+import de.blau.android.util.ContentResolverUtil;
+import de.blau.android.util.DateFormatter;
+import de.blau.android.util.InfoDialogFragment;
+
+/**
+ * Very simple dialog fragment to display some info on a GeoJSON element
+ *
+ * @author simon
+ *
+ */
+public class ImageInfo extends InfoDialogFragment {
+
+ private static final String DEBUG_TAG = ImageInfo.class.getName();
+
+ private static final String URI_KEY = "uri";
+
+ private static final String TAG = "fragment_image_info";
+
+ private Uri uri = null;
+ private SimpleDateFormat dateFormat = DateFormatter.getUtcFormat("yyyy-MM-dd HH:mm:ssZ");
+
+ /**
+ * Show an info dialog for an image
+ *
+ * @param activity the calling Activity
+ * @param uriString the uri of the image as a string
+ */
+ public static void showDialog(@NonNull FragmentActivity activity, @NonNull String uriString) {
+ dismissDialog(activity);
+ try {
+ FragmentManager fm = activity.getSupportFragmentManager();
+ ImageInfo elementInfoFragment = newInstance(uriString);
+ elementInfoFragment.show(fm, TAG);
+ } catch (IllegalStateException isex) {
+ Log.e(DEBUG_TAG, "showDialog", isex);
+ }
+ }
+
+ /**
+ * Dismiss the dialog
+ *
+ * @param activity the calling Activity
+ */
+ private static void dismissDialog(@NonNull FragmentActivity activity) {
+ de.blau.android.dialogs.Util.dismissDialog(activity, TAG);
+ }
+
+ /**
+ * Create a new instance of the FeatureInfo dialog
+ *
+ * @param feature Feature to display the info on
+ *
+ * @return an instance of ElementInfo
+ */
+ @NonNull
+ private static ImageInfo newInstance(@NonNull String uriString) {
+ ImageInfo f = new ImageInfo();
+
+ Bundle args = new Bundle();
+ args.putString(URI_KEY, uriString);
+
+ f.setArguments(args);
+ f.setShowsDialog(true);
+
+ return f;
+ }
+
+ @Override
+ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
+ String uriString = getArguments().getString(URI_KEY);
+ try {
+ uri = Uri.parse(uriString);
+ } catch (Exception e) {
+ Log.e(DEBUG_TAG, "Unable to parse uri " + uriString);
+ }
+ Builder builder = new AlertDialog.Builder(getActivity());
+ DoNothingListener doNothingListener = new DoNothingListener();
+ builder.setPositiveButton(R.string.done, doNothingListener);
+ builder.setTitle(R.string.image_information_title);
+ builder.setView(createView(null));
+ return builder.create();
+ }
+
+ @Override
+ protected View createView(@Nullable ViewGroup container) {
+ ScrollView sv = createEmptyView(container);
+ TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout);
+ TableLayout.LayoutParams tp = getTableLayoutParams();
+
+ if (uri != null) {
+ tl.setColumnShrinkable(1, true);
+ try {
+ FragmentActivity activity = getActivity();
+ Photo image = new Photo(getContext(), uri, null);
+ tl.addView(TableLayoutUtils.createRow(activity, ContentResolverUtil.getPath(getContext(), uri), null, tp));
+ long size = ContentResolverUtil.getSizeColumn(getContext(), uri);
+ if (size > -1) {
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.file_size, getString(R.string.file_size_kB, size / 1024), tp));
+ }
+ String creator = image.getCreator();
+ if (creator != null) {
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.created_by, creator, tp));
+ }
+ Long captureDate = image.getCaptureDate();
+ if (captureDate != null) {
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.capture_date, dateFormat.format(new Date(captureDate)), tp));
+ }
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lon_label, prettyPrintCoord(image.getLon() / 1E7D), tp));
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, prettyPrintCoord(image.getLat() / 1E7D), tp));
+ if (image.hasDirection()) {
+ tl.addView(TableLayoutUtils.createRow(activity, R.string.direction, String.format(Locale.US, "%3d°", image.getDirection()), tp));
+ }
+ } catch (Exception ex) {
+ Log.e(DEBUG_TAG, "Exception displaying image meta data " + ex.getMessage());
+ }
+ }
+ return sv;
+ }
+}
diff --git a/src/main/java/de/blau/android/dialogs/LayerInfo.java b/src/main/java/de/blau/android/dialogs/LayerInfo.java
index 4f7775b2d0..9d77aa3c66 100644
--- a/src/main/java/de/blau/android/dialogs/LayerInfo.java
+++ b/src/main/java/de/blau/android/dialogs/LayerInfo.java
@@ -1,13 +1,8 @@
package de.blau.android.dialogs;
import android.os.Bundle;
-import android.text.Spanned;
import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.widget.ScrollView;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.appcompat.app.AppCompatDialog;
@@ -16,8 +11,6 @@
import de.blau.android.R;
import de.blau.android.listener.DoNothingListener;
import de.blau.android.util.InfoDialogFragment;
-import de.blau.android.util.ThemeUtils;
-import de.blau.android.util.Util;
/**
* A generic dialog fragment to display some info on layers
@@ -65,27 +58,4 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
builder.setView(createView(null));
return builder.create();
}
-
- /**
- * Create the view we want to display
- *
- * Classes extending LayerInfo need to override this but call through to the super method to get the view
- *
- * @param container parent view or null
- * @return the View
- */
- protected ScrollView createEmptyView(@Nullable ViewGroup container) {
- LayoutInflater inflater = ThemeUtils.getLayoutInflater(getActivity());
- return (ScrollView) inflater.inflate(R.layout.element_info_view, container, false);
- }
-
- /**
- * Get the string resource formated as an italic string
- *
- * @param resId String resource id
- * @return a Spanned containing the string
- */
- protected Spanned toItalic(int resId) {
- return Util.fromHtml("" + getString(resId) + "");
- }
}
diff --git a/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java b/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java
index f9511bc482..b5300c9aad 100644
--- a/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java
+++ b/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java
@@ -22,21 +22,23 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.exifinterface.media.ExifInterface;
+import androidx.fragment.app.FragmentActivity;
import de.blau.android.App;
import de.blau.android.Main;
import de.blau.android.R;
import de.blau.android.contract.FileExtensions;
import de.blau.android.contract.MimeTypes;
import de.blau.android.contract.Schemes;
+import de.blau.android.dialogs.ImageInfo;
import de.blau.android.osm.OsmXml;
import de.blau.android.util.ExecutorTask;
import de.blau.android.util.FileUtil;
@@ -55,9 +57,11 @@ class MapillaryLoader extends ImageLoader {
private static final int IMAGERY_LOAD_THREADS = 3;
- private static final String COORDINATES_FIELD = "coordinates";
- private static final String COMPUTED_GEOMETRY_FIELD = "computed_geometry";
- private static final String THUMB_2048_URL_FIELD = "thumb_2048_url";
+ private static final String COORDINATES_FIELD = "coordinates";
+ private static final String COMPUTED_GEOMETRY_FIELD = "computed_geometry";
+ private static final String COMPUTED_COMPASS_ANGLE_FIELD = "computed_compass_angle";
+ private static final String CAPTURED_AT_FIELD = "captured_at";
+ private static final String THUMB_2048_URL_FIELD = "thumb_2048_url";
private static final String JPG = "." + FileExtensions.JPG;
@@ -118,22 +122,20 @@ public void load(SubsamplingScaleImageView view, String key) {
throw new IOException("Download of " + key + " failed with " + mapillaryCallResponse.code() + " " + mapillaryCallResponse.message());
}
try (ResponseBody responseBody = mapillaryCallResponse.body(); InputStream inputStream = responseBody.byteStream()) {
- if (inputStream != null) {
- JsonElement root = JsonParser.parseReader(new BufferedReader(new InputStreamReader(inputStream, Charset.forName(OsmXml.UTF_8))));
- if (root.isJsonObject() && ((JsonObject) root).has(THUMB_2048_URL_FIELD)) {
- loadImage(key, imageFile, client, ((JsonObject) root).get(COMPUTED_GEOMETRY_FIELD),
- ((JsonObject) root).get(THUMB_2048_URL_FIELD).getAsString());
- } else {
- throw new IOException("Unexpected / missing response");
- }
+ if (inputStream == null) {
+ throw new IOException("No InputStream");
}
+ JsonElement root = JsonParser.parseReader(new BufferedReader(new InputStreamReader(inputStream, Charset.forName(OsmXml.UTF_8))));
+ if (!root.isJsonObject() || !((JsonObject) root).has(THUMB_2048_URL_FIELD)) {
+ throw new IOException("Unexpected / missing response");
+ }
+ loadImage(key, imageFile, client, (JsonObject) root, ((JsonObject) root).get(THUMB_2048_URL_FIELD).getAsString());
}
+ setImage(view, imageFile);
+ pruneCache();
} catch (IOException e) {
Log.e(DEBUG_TAG, e.getMessage());
- return;
}
- setImage(view, imageFile);
- pruneCache();
});
} catch (RejectedExecutionException rjee) {
Log.e(DEBUG_TAG, "Execution rejected " + rjee.getMessage());
@@ -163,33 +165,48 @@ protected Void doInBackground(Void arg) {
* @param url image url
* @throws IOException if download or writing has issues
*/
- private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull OkHttpClient client, @Nullable JsonElement point, @NonNull String url)
+ private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull OkHttpClient client, JsonObject meta, @NonNull String url)
throws IOException {
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
- if (response.isSuccessful()) {
- try (ResponseBody responseBody = response.body(); InputStream inputStream = responseBody.byteStream()) {
- if (inputStream != null) {
- try (FileOutputStream fileOutput = new FileOutputStream(imageFile)) {
- byte[] buffer = new byte[1024];
- int bufferLength = 0;
- while ((bufferLength = inputStream.read(buffer)) > 0) {
- fileOutput.write(buffer, 0, bufferLength);
- }
- }
- if (point instanceof JsonObject && imageFile.length() > 0) {
- JsonElement coords = ((JsonObject) point).get(COORDINATES_FIELD);
- if (coords instanceof JsonArray && ((JsonArray) coords).size() == 2) {
- ExifInterface exif = new ExifInterface(imageFile);
- double lat = ((JsonArray) coords).get(1).getAsDouble();
- double lon = ((JsonArray) coords).get(0).getAsDouble();
- exif.setLatLong(lat, lon);
- exif.saveAttributes();
- coordinates.put(key, new double[] { lat, lon });
- }
- }
+ if (!response.isSuccessful()) {
+ throw new IOException("Download failed " + response.message());
+ }
+ try (ResponseBody responseBody = response.body(); InputStream inputStream = responseBody.byteStream()) {
+ if (inputStream == null) {
+ throw new IOException("Download failed no InputStream");
+ }
+ try (FileOutputStream fileOutput = new FileOutputStream(imageFile)) {
+ byte[] buffer = new byte[1024];
+ int bufferLength = 0;
+ while ((bufferLength = inputStream.read(buffer)) > 0) {
+ fileOutput.write(buffer, 0, bufferLength);
}
}
+ JsonElement point = meta.get(COMPUTED_GEOMETRY_FIELD);
+ if (!(point instanceof JsonObject) || imageFile.length() == 0) {
+ throw new IOException("No geometry for image or image empty");
+ }
+ JsonElement coords = ((JsonObject) point).get(COORDINATES_FIELD);
+ if (!(coords instanceof JsonArray) || ((JsonArray) coords).size() != 2) {
+ throw new IOException("No geometry for image");
+ }
+ ExifInterface exif = new ExifInterface(imageFile);
+ double lat = ((JsonArray) coords).get(1).getAsDouble();
+ double lon = ((JsonArray) coords).get(0).getAsDouble();
+ exif.setLatLong(lat, lon);
+ JsonElement angleElement = meta.get(COMPUTED_COMPASS_ANGLE_FIELD);
+ if (angleElement instanceof JsonPrimitive) {
+ float angle = angleElement.getAsFloat();
+ exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, Integer.toString((int) (angle * 100)) + "/100");
+ exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, ExifInterface.GPS_DIRECTION_MAGNETIC);
+ }
+ JsonElement capturedAt = meta.get(CAPTURED_AT_FIELD);
+ if (capturedAt instanceof JsonPrimitive) {
+ exif.setDateTime(capturedAt.getAsLong());
+ }
+ exif.saveAttributes();
+ coordinates.put(key, new double[] { lat, lon });
}
}
@@ -231,4 +248,16 @@ public void share(Context context, String key) {
ScreenMessage.toastTopError(context, context.getString(R.string.toast_error_accessing_photo, key));
}
}
+
+ @Override
+ public boolean supportsInfo() {
+ return true;
+ }
+
+ @Override
+ public void info(@NonNull FragmentActivity activity, @NonNull String uri) {
+ Uri f = FileProvider.getUriForFile(activity, activity.getString(R.string.content_provider), new File(cacheDir, uri + JPG));
+ ImageInfo.showDialog(activity, f.toString());
+
+ }
}
diff --git a/src/main/java/de/blau/android/photos/Photo.java b/src/main/java/de/blau/android/photos/Photo.java
index 5a53103e72..cf1857e50a 100644
--- a/src/main/java/de/blau/android/photos/Photo.java
+++ b/src/main/java/de/blau/android/photos/Photo.java
@@ -43,6 +43,9 @@ public class Photo implements BoundedObject, GeoPoint, Serializable {
private int direction = 0;
private String directionRef = null; // if null direction not present
+ private Long captureDate = null;
+ private String creator = null;
+
/**
* Construct a Photo object from an Uri
*
@@ -139,6 +142,8 @@ private Photo(@NonNull ExifInterface exif, @NonNull String ref, @Nullable String
directionRef = exif.getAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF);
Log.d(DEBUG_TAG, "dir " + dir + " direction " + direction + " ref " + directionRef);
}
+ captureDate = exif.getDateTime();
+ creator = exif.getAttribute(ExifInterface.TAG_ARTIST);
}
/**
@@ -263,6 +268,7 @@ public String getRef() {
*
* @return a name for human consumption
*/
+ @Nullable
public String getDisplayName() {
return displayName != null && !"".equals(displayName) ? displayName : Uri.parse(ref).getLastPathSegment();
}
@@ -285,6 +291,22 @@ public int getDirection() {
return direction;
}
+ /**
+ * @return the capture date
+ */
+ @Nullable
+ public Long getCaptureDate() {
+ return captureDate;
+ }
+
+ /**
+ * @return the creator of the image
+ */
+ @Nullable
+ public String getCreator() {
+ return creator;
+ }
+
/**
* For the BoundedObject interface
*/
diff --git a/src/main/java/de/blau/android/photos/PhotoViewerFragment.java b/src/main/java/de/blau/android/photos/PhotoViewerFragment.java
index edfc8ed428..2953983263 100644
--- a/src/main/java/de/blau/android/photos/PhotoViewerFragment.java
+++ b/src/main/java/de/blau/android/photos/PhotoViewerFragment.java
@@ -33,6 +33,7 @@
import de.blau.android.Map;
import de.blau.android.R;
import de.blau.android.contract.Ui;
+import de.blau.android.dialogs.ImageInfo;
import de.blau.android.listener.DoNothingListener;
import de.blau.android.util.ImageLoader;
import de.blau.android.util.ImagePagerAdapter;
@@ -61,8 +62,9 @@ public class PhotoViewerFragment extends SizedDynamicImmersiveDialogFragment imp
private static final int MENUITEM_BACK = 0;
private static final int MENUITEM_SHARE = 1;
private static final int MENUITEM_GOTO = 2;
- private static final int MENUITEM_DELETE = 3;
- private static final int MENUITEM_FORWARD = 4;
+ private static final int MENUITEM_INFO = 3;
+ private static final int MENUITEM_DELETE = 4;
+ private static final int MENUITEM_FORWARD = 5;
private List photoList = null;
@@ -185,6 +187,78 @@ public void share(Context context, String uri) {
getDialog().dismiss();
}
}
+
+ @Override
+ public boolean supportsInfo() {
+ return true;
+ }
+
+ @Override
+ public void info(@NonNull FragmentActivity activity, @NonNull String uri) {
+ ImageInfo.showDialog(activity, uri);
+ }
+
+ @Override
+ public boolean supportsDelete() {
+ return true;
+ }
+
+ @Override
+ public void delete(@NonNull Context context, @NonNull String uri) {
+ new AlertDialog.Builder(getContext()).setTitle(R.string.photo_viewer_delete_title)
+ .setPositiveButton(R.string.photo_viewer_delete_button, (dialog, which) -> {
+ if (viewPager == null) {
+ return;
+ }
+ int position = getCurrentPosition();
+ final int size = photoList.size();
+ if (position >= 0 && position < size) { // avoid crashes from bouncing
+ Uri photoUri = Uri.parse(photoList.get(position));
+ try {
+ if (getShowsDialog()) {
+ // delete from in memory and on device index
+ try (PhotoIndex index = new PhotoIndex(getContext())) {
+ index.deletePhoto(getContext(), photoUri);
+ }
+ Map map = (context instanceof Main) ? ((Main) context).getMap() : null;
+ final de.blau.android.layer.photos.MapOverlay overlay = map != null ? map.getPhotoLayer() : null;
+ if (overlay != null) {
+ // as the Photo was selected before calling this it will still have a
+ // reference in the layer
+ overlay.deselectObjects();
+ overlay.invalidate();
+ }
+ } else {
+ Intent intent = new Intent(getContext(), Main.class);
+ intent.setAction(Main.ACTION_DELETE_PHOTO);
+ intent.setData(photoUri);
+ getContext().startActivity(intent);
+ }
+ // actually delete
+ if (getContext().getContentResolver().delete(photoUri, null, null) >= 1) {
+ photoList.remove(position);
+ position = Math.min(position, size - 1); // this will set pos to -1 if
+ // empty,
+ // but we will exit in that case in any case
+ if (getShowsDialog() && photoList.isEmpty()) {
+ // in fragment mode we want to stay around
+ getDialog().dismiss();
+ } else {
+ photoPagerAdapter.notifyDataSetChanged();
+ viewPager.setCurrentItem(position);
+ if (size == 1 && itemBackward != null && itemForward != null) {
+ itemBackward.setEnabled(false);
+ itemForward.setEnabled(false);
+ }
+ }
+ }
+ } catch (java.lang.SecurityException sex) {
+ Log.e(DEBUG_TAG, "Error deleting: " + sex.getMessage() + " " + sex.getClass().getName());
+ ScreenMessage.toastTopError(getContext(), getString(R.string.toast_permission_denied, sex.getMessage()));
+ }
+ }
+ }).setNeutralButton(R.string.cancel, null).show();
+ }
};
/**
@@ -243,7 +317,11 @@ private View createView(@Nullable Bundle savedInstanceState) {
menu.add(Menu.NONE, MENUITEM_SHARE, Menu.NONE, R.string.share).setIcon(R.drawable.ic_share_white_36dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.add(Menu.NONE, MENUITEM_GOTO, Menu.NONE, R.string.photo_viewer_goto).setIcon(R.drawable.ic_map_white_36dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
- if (getString(R.string.content_provider).equals(Uri.parse(photoList.get(startPos)).getAuthority())) {
+ if (photoLoader.supportsInfo()) {
+ menu.add(Menu.NONE, MENUITEM_INFO, Menu.NONE, R.string.menu_information).setIcon(R.drawable.outline_info_white_48dp)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+ if (photoLoader.supportsDelete() && getString(R.string.content_provider).equals(Uri.parse(photoList.get(startPos)).getAuthority())) {
// we can only delete stuff that is provided by our provider, currently this is a bit of a hack
menu.add(Menu.NONE, MENUITEM_DELETE, Menu.NONE, R.string.delete).setIcon(R.drawable.ic_delete_forever_white_36dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
@@ -303,93 +381,50 @@ public boolean onMenuItemClick(MenuItem item) {
int size = photoList.size();
FragmentActivity caller = getActivity();
int pos = getCurrentPosition();
- if (photoList != null && !photoList.isEmpty() && pos < size) {
- try {
- switch (item.getItemId()) {
- case MENUITEM_BACK:
- pos = pos - 1;
- if (pos == -1) {
- pos = size - 1;
- }
- viewPager.setCurrentItem(pos);
- break;
- case MENUITEM_FORWARD:
- pos = (pos + 1) % size;
- viewPager.setCurrentItem(pos);
- break;
- case MENUITEM_GOTO:
- if (photoLoader != null) {
- photoLoader.showOnMap(caller, pos);
- }
- break;
- case MENUITEM_SHARE:
- if (photoLoader != null) {
- photoLoader.share(caller, photoList.get(pos));
- }
- break;
- case MENUITEM_DELETE:
- // TODO This is not generic and only works for the photo layer
- new AlertDialog.Builder(getContext()).setTitle(R.string.photo_viewer_delete_title)
- .setPositiveButton(R.string.photo_viewer_delete_button, (dialog, which) -> {
- if (viewPager == null) {
- return;
- }
- int position = getCurrentPosition();
- if (position >= 0 && position < photoList.size()) { // avoid crashes from bouncing
- Uri photoUri = Uri.parse(photoList.get(position));
- try {
- if (getShowsDialog()) {
- // delete from in memory and on device index
- try (PhotoIndex index = new PhotoIndex(getContext())) {
- index.deletePhoto(getContext(), photoUri);
- }
- Map map = (caller instanceof Main) ? ((Main) caller).getMap() : null;
- final de.blau.android.layer.photos.MapOverlay overlay = map != null ? map.getPhotoLayer() : null;
- if (overlay != null) {
- // as the Photo was selected before calling this it will still have a
- // reference in the layer
- overlay.deselectObjects();
- overlay.invalidate();
- }
- } else {
- Intent intent = new Intent(getContext(), Main.class);
- intent.setAction(Main.ACTION_DELETE_PHOTO);
- intent.setData(photoUri);
- getContext().startActivity(intent);
- }
- // actually delete
- if (getContext().getContentResolver().delete(photoUri, null, null) >= 1) {
- photoList.remove(position);
- position = Math.min(position, size - 1); // this will set pos to -1 if
- // empty,
- // but we will exit in that case in any case
- if (getShowsDialog() && photoList.isEmpty()) {
- // in fragment mode we want to stay around
- getDialog().dismiss();
- } else {
- photoPagerAdapter.notifyDataSetChanged();
- viewPager.setCurrentItem(position);
- if (photoList.size() == 1 && itemBackward != null && itemForward != null) {
- itemBackward.setEnabled(false);
- itemForward.setEnabled(false);
- }
- }
- }
- } catch (java.lang.SecurityException sex) {
- Log.e(DEBUG_TAG, "Error deleting: " + sex.getMessage() + " " + sex.getClass().getName());
- ScreenMessage.toastTopError(getContext(), getString(R.string.toast_permission_denied, sex.getMessage()));
- }
- }
- }).setNeutralButton(R.string.cancel, null).show();
- break;
- default:
- // do nothing
+ if (photoList == null || photoList.isEmpty() || pos >= size) {
+ return false;
+ }
+ try {
+ final boolean loaderPresent = photoLoader != null;
+ switch (item.getItemId()) {
+ case MENUITEM_BACK:
+ pos = pos - 1;
+ if (pos == -1) {
+ pos = size - 1;
+ }
+ viewPager.setCurrentItem(pos);
+ break;
+ case MENUITEM_FORWARD:
+ pos = (pos + 1) % size;
+ viewPager.setCurrentItem(pos);
+ break;
+ case MENUITEM_GOTO:
+ if (loaderPresent) {
+ photoLoader.showOnMap(caller, pos);
+ }
+ break;
+ case MENUITEM_SHARE:
+ if (loaderPresent) {
+ photoLoader.share(caller, photoList.get(pos));
+ }
+ break;
+ case MENUITEM_INFO:
+ if (loaderPresent) {
+ photoLoader.info(caller, photoList.get(pos));
+ }
+ break;
+ case MENUITEM_DELETE:
+ if (loaderPresent) {
+ photoLoader.delete(caller, photoList.get(pos));
}
- } finally {
- prepareMenu();
+ break;
+ default:
+ // do nothing
}
+ } finally {
+ prepareMenu();
}
- return false;
+ return true;
}
@Override
diff --git a/src/main/java/de/blau/android/util/ImageLoader.java b/src/main/java/de/blau/android/util/ImageLoader.java
index a8fe5f3414..c9c26c3624 100644
--- a/src/main/java/de/blau/android/util/ImageLoader.java
+++ b/src/main/java/de/blau/android/util/ImageLoader.java
@@ -8,6 +8,7 @@
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
public abstract class ImageLoader implements Serializable {
@@ -50,6 +51,44 @@ public void share(@NonNull Context context, @NonNull String uri) {
// empty
}
+ /**
+ * Indicate if we support an image information function
+ *
+ * @return true if supported
+ */
+ public boolean supportsInfo() {
+ return false;
+ }
+
+ /**
+ * Show some information on the image
+ *
+ * @param activity Android activity
+ * @param uri the Uri or other reference to the photo
+ */
+ public void info(@NonNull FragmentActivity activity, @NonNull String uri) {
+ // empty
+ }
+
+ /**
+ * Indicate if we support an image deletion function
+ *
+ * @return true if supported
+ */
+ public boolean supportsDelete() {
+ return false;
+ }
+
+ /**
+ * Delete the photo
+ *
+ * @param context Android Context
+ * @param uri the Uri or other reference to the photo
+ */
+ public void delete(@NonNull Context context, @NonNull String uri) {
+ // empty
+ }
+
/**
* Set a title to display
*
diff --git a/src/main/java/de/blau/android/util/InfoDialogFragment.java b/src/main/java/de/blau/android/util/InfoDialogFragment.java
index c122ebf637..3da25a69bb 100644
--- a/src/main/java/de/blau/android/util/InfoDialogFragment.java
+++ b/src/main/java/de/blau/android/util/InfoDialogFragment.java
@@ -1,12 +1,18 @@
package de.blau.android.util;
+import java.util.Locale;
+
import android.app.Dialog;
import android.os.Bundle;
+import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ScrollView;
+import android.widget.TableLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import de.blau.android.R;
public abstract class InfoDialogFragment extends ImmersiveDialogFragment {
@@ -34,4 +40,51 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
* @return the View
*/
protected abstract View createView(@Nullable ViewGroup container);
+
+ /**
+ * Create the view we want to display
+ *
+ * Classes extending LayerInfo need to override this but call through to the super method to get the view
+ *
+ * @param container parent view or null
+ * @return the View
+ */
+ @NonNull
+ protected ScrollView createEmptyView(@Nullable ViewGroup container) {
+ LayoutInflater inflater = ThemeUtils.getLayoutInflater(getActivity());
+ return (ScrollView) inflater.inflate(R.layout.element_info_view, container, false);
+ }
+
+ /**
+ * Setup the table layout params
+ *
+ * @return an instance of TableLayout.LayoutParams
+ */
+ @NonNull
+ protected TableLayout.LayoutParams getTableLayoutParams() {
+ TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ tp.setMargins(10, 2, 10, 2);
+ return tp;
+ }
+
+ /**
+ * Get the string resource formated as an italic string
+ *
+ * @param resId String resource id
+ * @return a Spanned containing the string
+ */
+ protected Spanned toItalic(int resId) {
+ return Util.fromHtml("" + getString(resId) + "");
+ }
+
+ /**
+ * Pretty print a coordinate value
+ *
+ * @param coord the coordinate in WGS84
+ * @return a reasonable looking string representation
+ */
+ @NonNull
+ protected static String prettyPrintCoord(double coord) {
+ return String.format(Locale.US, "%.7f°", coord);
+ }
}
diff --git a/src/main/res/drawable-xhdpi/outline_info_white_48dp.png b/src/main/res/drawable-xhdpi/outline_info_white_48dp.png
new file mode 100644
index 0000000000..7d0740a8f1
Binary files /dev/null and b/src/main/res/drawable-xhdpi/outline_info_white_48dp.png differ
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 452e4b125a..eb6f4a9833 100755
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -256,6 +256,13 @@
Track points
Way points
+
+ Image info
+ Created by
+ Capture date
+ Direction
+ Size
+ %1$d kB
Too much data loaded!
You have %1$d Nodes loaded and have exceeded the configured limit. This will lead to slowdown, and if you add more data, to potential issues saving state.