diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java index 5026bcbe6..8f496b104 100644 --- a/src/main/java/de/blau/android/Main.java +++ b/src/main/java/de/blau/android/Main.java @@ -137,6 +137,8 @@ import de.blau.android.layer.DownloadInterface; import de.blau.android.layer.LayerType; import de.blau.android.layer.MapViewLayer; +import de.blau.android.layer.NetworkImageLoader; +import de.blau.android.layer.SelectImageInterface; import de.blau.android.layer.geojson.MapOverlay; import de.blau.android.listener.UpdateViewListener; import de.blau.android.osm.BoundingBox; @@ -244,7 +246,7 @@ public class Main extends FullScreenAppCompatActivity public static final String ACTION_EXIT = "de.blau.android.EXIT"; public static final String ACTION_UPDATE = "de.blau.android.UPDATE"; public static final String ACTION_DELETE_PHOTO = "de.blau.android.DELETE_PHOTO"; - public static final String ACTION_MAPILLARY_SELECT = "de.blau.android.ACTION_MAPILLARY_SELECT"; + public static final String ACTION_IMAGE_SELECT = "de.blau.android.ACTION_MAPILLARY_SELECT"; public static final String ACTION_MAP_UPDATE = "de.blau.android.MAP_UPDATE"; public static final String ACTION_PUSH_SELECTION = "de.blau.android.PUSH_SELECTION"; public static final String ACTION_POP_SELECTION = "de.blau.android.POP_SELECTION"; @@ -1005,16 +1007,11 @@ private void processIntents() { photoLayer.invalidate(); } break; - case ACTION_MAPILLARY_SELECT: - final de.blau.android.layer.mapillary.MapillaryOverlay mapillaryLayer = map != null - ? (de.blau.android.layer.mapillary.MapillaryOverlay) map.getLayer(LayerType.MAPILLARY) - : null; - if (mapillaryLayer != null) { - double[] coords = intent.getDoubleArrayExtra(de.blau.android.layer.mapillary.MapillaryOverlay.COORDINATES_KEY); - if (coords != null) { - map.getViewBox().moveTo(map, (int) (coords[1] * 1E7), (int) (coords[0] * 1E7)); - } - mapillaryLayer.select(intent.getIntExtra(de.blau.android.layer.mapillary.MapillaryOverlay.SET_POSITION_KEY, 0)); + case ACTION_IMAGE_SELECT: + if (map != null) { + SelectImageInterface layer = (SelectImageInterface) map + .getLayer((LayerType) intent.getSerializableExtra(NetworkImageLoader.LAYER_TYPE_KEY)); + selectImageOnLayer(intent, layer); } break; case ACTION_MAP_UPDATE: @@ -1075,6 +1072,22 @@ private void processIntents() { } } + /** + * Select an image on a layer + * + * @param intent the intent that we need to process + * @param layer the relevant layer + */ + private void selectImageOnLayer(Intent intent, final SelectImageInterface layer) { + if (layer != null) { + double[] coords = intent.getDoubleArrayExtra(NetworkImageLoader.COORDINATES_KEY); + if (coords != null) { + map.getViewBox().moveTo(map, (int) (coords[1] * 1E7), (int) (coords[0] * 1E7)); + } + layer.selectImage(intent.getIntExtra(NetworkImageLoader.SET_POSITION_KEY, 0)); + } + } + /** * If we have been started by a shortcut, process mode and other setup here */ diff --git a/src/main/java/de/blau/android/layer/NetworkImageLoader.java b/src/main/java/de/blau/android/layer/NetworkImageLoader.java new file mode 100644 index 000000000..a01bcb35d --- /dev/null +++ b/src/main/java/de/blau/android/layer/NetworkImageLoader.java @@ -0,0 +1,134 @@ +package de.blau.android.layer; + +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.davemorrissey.labs.subscaleview.ImageSource; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; +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.util.ExecutorTask; +import de.blau.android.util.FileUtil; +import de.blau.android.util.ImageLoader; +import de.blau.android.util.ScreenMessage; + +public abstract class NetworkImageLoader extends ImageLoader { + private static final long serialVersionUID = 1L; + + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, NetworkImageLoader.class.getSimpleName().length()); + protected static final String DEBUG_TAG = NetworkImageLoader.class.getSimpleName().substring(0, TAG_LEN); + + protected static final String JPG = "." + FileExtensions.JPG; + + public static final String SET_POSITION_KEY = "set_position"; + public static final String COORDINATES_KEY = "coordinates"; + public static final String LAYER_TYPE_KEY = "layer_type"; + + protected final File cacheDir; + protected final long cacheSize; + protected final String imageUrl; + protected final Map coordinates = new HashMap<>(); + protected final List ids; + + /** + * Construct a new loader + * + * @param cacheDir the cacheDir that should be used as a destination for the images + * @param cacheSize max size of the cache + * @param imageUrl base url for retrieving the image + * @param ids list of images ids + */ + protected NetworkImageLoader(@NonNull File cacheDir, long cacheSize, @NonNull String imageUrl, List ids) { + this.cacheDir = cacheDir; + this.cacheSize = cacheSize; + this.imageUrl = imageUrl; + this.ids = ids; + } + + /** + * Prune the image cache + */ + protected void pruneCache() { + new ExecutorTask() { + @Override + protected Void doInBackground(Void arg) { + FileUtil.pruneCache(cacheDir, cacheSize); + return null; + } + }.execute(); + } + + /** + * Set the image + * + * @param view the ImageView to set it in + * @param imageFile the file + */ + protected void setImage(@NonNull SubsamplingScaleImageView view, @NonNull File imageFile) { + view.post(() -> { // needs to run on the ui thread + view.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE); + view.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF); + view.setImage(ImageSource.uri(Uri.parse(Schemes.FILE + ":" + imageFile.getAbsolutePath()))); + }); + } + + @Override + public void showOnMap(Context context, int index) { + if (!App.isPropertyEditorRunning()) { + Intent intent = new Intent(context, Main.class); + intent.setAction(Main.ACTION_IMAGE_SELECT); + intent.putExtra(SET_POSITION_KEY, index); + String key = ids.get(index); + if (key != null && coordinates.containsKey(key)) { + intent.putExtra(COORDINATES_KEY, coordinates.get(key)); + } + intent.putExtra(LAYER_TYPE_KEY, getLayerType()); + context.startActivity(intent); + } + } + + /** + * Get the LayerType we are associated with + * + * @return a LayerType + */ + abstract protected LayerType getLayerType(); + + @Override + public void share(Context context, String key) { + File imageFile = new File(cacheDir, key + JPG); + if (imageFile.exists()) { + Uri f = FileProvider.getUriForFile(context, context.getString(R.string.content_provider), imageFile); + de.blau.android.layer.photos.Util.sharePhoto(context, key, f, MimeTypes.JPEG); + } else { + 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/layer/SelectImageInterface.java b/src/main/java/de/blau/android/layer/SelectImageInterface.java new file mode 100644 index 000000000..0cd54926b --- /dev/null +++ b/src/main/java/de/blau/android/layer/SelectImageInterface.java @@ -0,0 +1,10 @@ +package de.blau.android.layer; + +public interface SelectImageInterface { + /** + * Select a specific image in the selected sequence + * + * @param pos the position in the sequence + */ + public void selectImage(int pos); +} diff --git a/src/main/java/de/blau/android/layer/mapillary/AbstractSequenceFetcher.java b/src/main/java/de/blau/android/layer/mapillary/AbstractSequenceFetcher.java new file mode 100644 index 000000000..16bf5852e --- /dev/null +++ b/src/main/java/de/blau/android/layer/mapillary/AbstractSequenceFetcher.java @@ -0,0 +1,120 @@ +package de.blau.android.layer.mapillary; + +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import de.blau.android.App; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public abstract class AbstractSequenceFetcher implements Runnable { + + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, AbstractSequenceFetcher.class.getSimpleName().length()); + private static final String DEBUG_TAG = AbstractSequenceFetcher.class.getSimpleName().substring(0, TAG_LEN); + + protected final FragmentActivity activity; + final String urlTemplate; + protected final String sequenceId; + final String apiKey; + + /** + * Construct a new instance + * + * @param activity the calling Activity + * @param sequenceId the sequence id + * @param id the image id + */ + public AbstractSequenceFetcher(@NonNull FragmentActivity activity, @NonNull String urlTemplate, @NonNull String sequenceId, @Nullable String apiKey) { + this.activity = activity; + this.urlTemplate = urlTemplate; + this.sequenceId = sequenceId; + this.apiKey = apiKey; + } + + @Override + public void run() { + try { + URL url = new URL(String.format(urlTemplate, sequenceId, apiKey)); + Log.d(DEBUG_TAG, "query sequence: " + url.toString()); + Request request = new Request.Builder().url(url).build(); + OkHttpClient client = App.getHttpClient().newBuilder().connectTimeout(20000, TimeUnit.MILLISECONDS).readTimeout(20000, TimeUnit.MILLISECONDS) + .build(); + Call mapillaryCall = client.newCall(request); + Response mapillaryCallResponse = mapillaryCall.execute(); + if (!mapillaryCallResponse.isSuccessful()) { + return; + } + ResponseBody responseBody = mapillaryCallResponse.body(); + try (InputStream inputStream = responseBody.byteStream()) { + if (inputStream == null) { + throw new IOException("null InputStream"); + } + StringBuilder sb = new StringBuilder(); + int cp; + while ((cp = inputStream.read()) != -1) { + sb.append((char) cp); + } + JsonElement root = JsonParser.parseString(sb.toString()); + if (!root.isJsonObject()) { + throw new IOException("root is not a JsonObject"); + } + ArrayList ids = getIds(root); + saveIdsAndUpdate(ids); + } + } catch (IOException ex) { + Log.d(DEBUG_TAG, "query sequence failed with " + ex.getMessage()); + } + } + + /** + * @param ids + */ + abstract protected void saveIdsAndUpdate(ArrayList ids); + // { + // if (state == null) { + // state = new State(); + // } + // state.sequenceCache.put(sequenceId, ids); + // showImages(activity, id, ids); + // } + + /** + * @param root + * @return + * @throws IOException + */ + abstract protected ArrayList getIds(JsonElement root) throws IOException; + // { + // JsonElement data = ((JsonObject) root).get(DATA_KEY); + // if (!(data instanceof JsonArray)) { + // throw new IOException("data not a JsonArray"); + // } + // JsonArray idArray = data.getAsJsonArray(); + // ArrayList ids = new ArrayList<>(); + // for (JsonElement element : idArray) { + // if (element instanceof JsonObject) { + // JsonElement temp = ((JsonObject) element).get(ID_KEY); + // if (temp != null) { + // ids.add(temp.getAsString()); + // } + // } + // } + // return ids; + // } + +} 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 4f9bf1217..441478795 100644 --- a/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java +++ b/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java @@ -1,5 +1,7 @@ package de.blau.android.layer.mapillary; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -8,15 +10,12 @@ import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -25,35 +24,23 @@ import com.google.gson.JsonPrimitive; import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; -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.util.ExecutorTask; -import de.blau.android.util.FileUtil; -import de.blau.android.util.ImageLoader; -import de.blau.android.util.ScreenMessage; +import de.blau.android.layer.LayerType; +import de.blau.android.layer.NetworkImageLoader; import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -class MapillaryLoader extends ImageLoader { +class MapillaryLoader extends NetworkImageLoader { private static final long serialVersionUID = 2L; - protected static final String DEBUG_TAG = MapillaryLoader.class.getSimpleName().substring(0, Math.min(23, MapillaryLoader.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, MapillaryLoader.class.getSimpleName().length()); + protected static final String DEBUG_TAG = MapillaryLoader.class.getSimpleName().substring(0, TAG_LEN); private static final int IMAGERY_LOAD_THREADS = 3; @@ -63,14 +50,6 @@ class MapillaryLoader extends ImageLoader { 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; - - final File cacheDir; - final long cacheSize; - final String imageUrl; - private final Map coordinates = new HashMap<>(); - private final List ids; - private transient ThreadPoolExecutor mThreadPool; /** @@ -82,10 +61,7 @@ class MapillaryLoader extends ImageLoader { * @param ids list of images ids */ MapillaryLoader(@NonNull File cacheDir, long cacheSize, @NonNull String imageUrl, List ids) { - this.cacheDir = cacheDir; - this.cacheSize = cacheSize; - this.imageUrl = imageUrl; - this.ids = ids; + super(cacheDir, cacheSize, imageUrl, ids); } @SuppressLint("NewApi") // StandardCharsets is desugared for APIs < 19. @@ -143,19 +119,6 @@ public void load(SubsamplingScaleImageView view, String key) { } } - /** - * Prune the image cache - */ - private void pruneCache() { - new ExecutorTask() { - @Override - protected Void doInBackground(Void arg) { - FileUtil.pruneCache(cacheDir, cacheSize); - return null; - } - }.execute(); - } - /** * Download the image * @@ -211,54 +174,8 @@ private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull Ok } } - /** - * Set the image - * - * @param view the ImageView to set it in - * @param imageFile the file - */ - void setImage(@NonNull SubsamplingScaleImageView view, @NonNull File imageFile) { - view.post(() -> { // needs to run on the ui thread - view.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE); - view.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF); - view.setImage(ImageSource.uri(Uri.parse(Schemes.FILE + ":" + imageFile.getAbsolutePath()))); - }); - } - @Override - public void showOnMap(Context context, int index) { - if (!App.isPropertyEditorRunning()) { - Intent intent = new Intent(context, Main.class); - intent.setAction(Main.ACTION_MAPILLARY_SELECT); - intent.putExtra(MapillaryOverlay.SET_POSITION_KEY, index); - String key = ids.get(index); - if (key != null && coordinates.containsKey(key)) { - intent.putExtra(MapillaryOverlay.COORDINATES_KEY, coordinates.get(key)); - } - context.startActivity(intent); - } - } - - @Override - public void share(Context context, String key) { - File imageFile = new File(cacheDir, key + JPG); - if (imageFile.exists()) { - Uri f = FileProvider.getUriForFile(context, context.getString(R.string.content_provider), imageFile); - de.blau.android.layer.photos.Util.sharePhoto(context, key, f, MimeTypes.JPEG); - } else { - 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()); - + protected LayerType getLayerType() { + return LayerType.MAPILLARY; } } diff --git a/src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java b/src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java index fd3a9dc91..e302af66a 100644 --- a/src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java +++ b/src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java @@ -6,17 +6,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.Serializable; -import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.concurrent.TimeUnit; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import android.content.Context; @@ -29,7 +26,6 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; -import de.blau.android.App; import de.blau.android.Map; import de.blau.android.R; import de.blau.android.contract.FileExtensions; @@ -37,6 +33,7 @@ import de.blau.android.dialogs.DateRangeDialog; import de.blau.android.layer.DateRangeInterface; import de.blau.android.layer.LayerType; +import de.blau.android.layer.SelectImageInterface; import de.blau.android.osm.OsmParser; import de.blau.android.osm.ViewBox; import de.blau.android.photos.MapillaryViewerActivity; @@ -57,13 +54,8 @@ import de.blau.android.util.mvt.style.Style; import de.blau.android.util.mvt.style.Symbol; import de.blau.android.views.IMapView; -import okhttp3.Call; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -public class MapillaryOverlay extends de.blau.android.layer.mvt.MapOverlay implements DateRangeInterface { +public class MapillaryOverlay extends de.blau.android.layer.mvt.MapOverlay implements DateRangeInterface, SelectImageInterface { private static final int TAG_LEN = Math.min(LOG_TAG_LEN, MapillaryOverlay.class.getSimpleName().length()); private static final String DEBUG_TAG = MapillaryOverlay.class.getSimpleName().substring(0, TAG_LEN); @@ -92,9 +84,7 @@ public class MapillaryOverlay extends de.blau.android.layer.mvt.MapOverlay imple public static final String APIKEY_KEY = "MAPILLARY_CLIENT_TOKEN"; - public static final String FILENAME = "mapillary" + "." + FileExtensions.RES; - public static final String SET_POSITION_KEY = "set_position"; - public static final String COORDINATES_KEY = "coordinates"; + public static final String FILENAME = "mapillary" + "." + FileExtensions.RES; static class State implements Serializable { private static final long serialVersionUID = 4L; @@ -221,7 +211,8 @@ public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.Vecto ArrayList keys = state != null ? state.sequenceCache.get(sequenceId) : null; if (keys == null) { try { - Thread t = new Thread(null, new SequenceFetcher(activity, sequenceId, id), "Mapillary Sequence"); + Thread t = new Thread(null, new MapillarySequenceFetcher(activity, mapillarySequencesUrl, sequenceId, id, apiKey), + "Mapillary Sequence"); t.start(); } catch (SecurityException | IllegalThreadStateException e) { Log.e(DEBUG_TAG, "Unable to run SequenceFetcher " + e.getMessage()); @@ -274,11 +265,9 @@ private void showImages(@NonNull FragmentActivity activity, @NonNull Long id, @N * @author simon * */ - private class SequenceFetcher implements Runnable { + private class MapillarySequenceFetcher extends AbstractSequenceFetcher { - final FragmentActivity activity; - final String sequenceId; - final Long id; + final Long id; /** * Construct a new instance @@ -287,58 +276,38 @@ private class SequenceFetcher implements Runnable { * @param sequenceId the sequence id * @param id the image id */ - public SequenceFetcher(@NonNull FragmentActivity activity, @NonNull String sequenceId, @NonNull Long id) { - this.activity = activity; - this.sequenceId = sequenceId; + public MapillarySequenceFetcher(@NonNull FragmentActivity activity, @NonNull String urlTemplate, @NonNull String sequenceId, @NonNull Long id, + @NonNull String apiKey) { + super(activity, urlTemplate, sequenceId, apiKey); this.id = id; } @Override - public void run() { - try { - URL url = new URL(String.format(mapillarySequencesUrl, sequenceId, apiKey)); - Log.d(DEBUG_TAG, "query sequence: " + url.toString()); - Request request = new Request.Builder().url(url).build(); - OkHttpClient client = App.getHttpClient().newBuilder().connectTimeout(20000, TimeUnit.MILLISECONDS).readTimeout(20000, TimeUnit.MILLISECONDS) - .build(); - Call mapillaryCall = client.newCall(request); - Response mapillaryCallResponse = mapillaryCall.execute(); - if (mapillaryCallResponse.isSuccessful()) { - ResponseBody responseBody = mapillaryCallResponse.body(); - try (InputStream inputStream = responseBody.byteStream()) { - if (inputStream != null) { - StringBuilder sb = new StringBuilder(); - int cp; - while ((cp = inputStream.read()) != -1) { - sb.append((char) cp); - } - JsonElement root = JsonParser.parseString(sb.toString()); - if (root.isJsonObject()) { - JsonElement data = ((JsonObject) root).get(DATA_KEY); - if (data instanceof JsonArray) { - JsonArray idArray = data.getAsJsonArray(); - ArrayList ids = new ArrayList<>(); - for (JsonElement element : idArray) { - if (element instanceof JsonObject) { - JsonElement temp = ((JsonObject) element).get(ID_KEY); - if (temp != null) { - ids.add(temp.getAsString()); - } - } - } - if (state == null) { - state = new State(); - } - state.sequenceCache.put(sequenceId, ids); - showImages(activity, id, ids); - } - } - } + protected void saveIdsAndUpdate(ArrayList ids) { + if (state == null) { + state = new State(); + } + state.sequenceCache.put(sequenceId, ids); + showImages(activity, id, ids); + } + + @Override + protected ArrayList getIds(JsonElement root) throws IOException { + JsonElement data = ((JsonObject) root).get(DATA_KEY); + if (!(data instanceof JsonArray)) { + throw new IOException("data not a JsonArray"); + } + JsonArray idArray = data.getAsJsonArray(); + ArrayList ids = new ArrayList<>(); + for (JsonElement element : idArray) { + if (element instanceof JsonObject) { + JsonElement temp = ((JsonObject) element).get(ID_KEY); + if (temp != null) { + ids.add(temp.getAsString()); } } - } catch (IOException ex) { - Log.d(DEBUG_TAG, "query sequence failed with " + ex.getMessage()); } + return ids; } } @@ -380,23 +349,21 @@ void setSelected(long id) { } } - /** - * Select a specific image in the selected sequence - * - * @param pos the position in the sequence - */ - public synchronized void select(int pos) { - if (state != null && state.sequenceId != null) { - List ids = state.sequenceCache.get(state.sequenceId); - if (ids != null) { - String idStr = ids.get(pos); - if (idStr != null) { - long id = Long.parseLong(ids.get(pos)); - setSelected(id); - return; + @Override + public void selectImage(int pos) { + synchronized (this) { + if (state != null && state.sequenceId != null) { + List ids = state.sequenceCache.get(state.sequenceId); + if (ids != null) { + String idStr = ids.get(pos); + if (idStr != null) { + long id = Long.parseLong(ids.get(pos)); + setSelected(id); + return; + } } + Log.e(DEBUG_TAG, "position " + pos + " not found in sequence " + state.sequenceId); } - Log.e(DEBUG_TAG, "position " + pos + " not found in sequence " + state.sequenceId); } } diff --git a/src/main/java/de/blau/android/layer/panoramax/PanoramaxLoader.java b/src/main/java/de/blau/android/layer/panoramax/PanoramaxLoader.java index b4011b0ac..17d582298 100644 --- a/src/main/java/de/blau/android/layer/panoramax/PanoramaxLoader.java +++ b/src/main/java/de/blau/android/layer/panoramax/PanoramaxLoader.java @@ -1,61 +1,39 @@ package de.blau.android.layer.panoramax; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; -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.util.ExecutorTask; -import de.blau.android.util.FileUtil; -import de.blau.android.util.ImageLoader; -import de.blau.android.util.ScreenMessage; +import de.blau.android.layer.LayerType; +import de.blau.android.layer.NetworkImageLoader; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -class PanoramaxLoader extends ImageLoader { +class PanoramaxLoader extends NetworkImageLoader { private static final long serialVersionUID = 2L; - protected static final String DEBUG_TAG = PanoramaxLoader.class.getSimpleName().substring(0, Math.min(23, PanoramaxLoader.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, PanoramaxLoader.class.getSimpleName().length()); + protected static final String DEBUG_TAG = PanoramaxLoader.class.getSimpleName().substring(0, TAG_LEN); private static final int IMAGERY_LOAD_THREADS = 3; - private static final String JPG = "." + FileExtensions.JPG; - - final File cacheDir; - final long cacheSize; - final String imageUrl; - private final Map coordinates = new HashMap<>(); - private final List ids; - private transient ThreadPoolExecutor mThreadPool; /** @@ -67,10 +45,7 @@ class PanoramaxLoader extends ImageLoader { * @param ids list of images ids */ PanoramaxLoader(@NonNull File cacheDir, long cacheSize, @NonNull String imageUrl, List ids) { - this.cacheDir = cacheDir; - this.cacheSize = cacheSize; - this.imageUrl = imageUrl; - this.ids = ids; + super(cacheDir, cacheSize, imageUrl, ids); } @SuppressLint("NewApi") // StandardCharsets is desugared for APIs < 19. @@ -97,12 +72,9 @@ public void load(SubsamplingScaleImageView view, String key) { mThreadPool.execute(() -> { Log.d(DEBUG_TAG, "querying server for " + key); try { - String urlString = String.format(imageUrl, key); - URL url = new URL(urlString); - System.out.println(urlString); OkHttpClient client = App.getHttpClient().newBuilder().connectTimeout(20000, TimeUnit.MILLISECONDS) .readTimeout(20000, TimeUnit.MILLISECONDS).build(); - loadImage(key, imageFile, client, urlString); + loadImage(imageFile, client, String.format(imageUrl, key)); setImage(view, imageFile); pruneCache(); } catch (IOException e) { @@ -114,29 +86,15 @@ public void load(SubsamplingScaleImageView view, String key) { } } - /** - * Prune the image cache - */ - private void pruneCache() { - new ExecutorTask() { - @Override - protected Void doInBackground(Void arg) { - FileUtil.pruneCache(cacheDir, cacheSize); - return null; - } - }.execute(); - } - /** * Download the image * - * @param key image key * @param imageFile target file to save the image in * @param client OkHttp client * @param url image url * @throws IOException if download or writing has issues */ - private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull OkHttpClient client, @NonNull String url) throws IOException { + private void loadImage(@NonNull File imageFile, @NonNull OkHttpClient client, @NonNull String url) throws IOException { Request request = new Request.Builder().url(url).build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) { @@ -156,54 +114,9 @@ private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull Ok } } - /** - * Set the image - * - * @param view the ImageView to set it in - * @param imageFile the file - */ - void setImage(@NonNull SubsamplingScaleImageView view, @NonNull File imageFile) { - view.post(() -> { // needs to run on the ui thread - view.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE); - view.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF); - view.setImage(ImageSource.uri(Uri.parse(Schemes.FILE + ":" + imageFile.getAbsolutePath()))); - }); - } - - @Override - public void showOnMap(Context context, int index) { - if (!App.isPropertyEditorRunning()) { - Intent intent = new Intent(context, Main.class); - intent.setAction(Main.ACTION_MAPILLARY_SELECT); - intent.putExtra(PanoramaxOverlay.SET_POSITION_KEY, index); - String key = ids.get(index); - if (key != null && coordinates.containsKey(key)) { - intent.putExtra(PanoramaxOverlay.COORDINATES_KEY, coordinates.get(key)); - } - context.startActivity(intent); - } - } - @Override - public void share(Context context, String key) { - File imageFile = new File(cacheDir, key + JPG); - if (imageFile.exists()) { - Uri f = FileProvider.getUriForFile(context, context.getString(R.string.content_provider), imageFile); - de.blau.android.layer.photos.Util.sharePhoto(context, key, f, MimeTypes.JPEG); - } else { - ScreenMessage.toastTopError(context, context.getString(R.string.toast_error_accessing_photo, key)); - } + protected LayerType getLayerType() { + return LayerType.PANORAMAX; } - @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/layer/panoramax/PanoramaxOverlay.java b/src/main/java/de/blau/android/layer/panoramax/PanoramaxOverlay.java index 98c69a6f4..29b88c2c4 100644 --- a/src/main/java/de/blau/android/layer/panoramax/PanoramaxOverlay.java +++ b/src/main/java/de/blau/android/layer/panoramax/PanoramaxOverlay.java @@ -37,6 +37,9 @@ import de.blau.android.dialogs.DateRangeDialog; import de.blau.android.layer.DateRangeInterface; import de.blau.android.layer.LayerType; +import de.blau.android.layer.SelectImageInterface; +import de.blau.android.layer.mapillary.AbstractSequenceFetcher; +import de.blau.android.layer.mapillary.MapillaryOverlay; import de.blau.android.osm.OsmParser; import de.blau.android.osm.ViewBox; import de.blau.android.photos.MapillaryViewerActivity; @@ -63,7 +66,7 @@ import okhttp3.Response; import okhttp3.ResponseBody; -public class PanoramaxOverlay extends de.blau.android.layer.mvt.MapOverlay implements DateRangeInterface { +public class PanoramaxOverlay extends de.blau.android.layer.mvt.MapOverlay implements DateRangeInterface, SelectImageInterface { private static final int TAG_LEN = Math.min(LOG_TAG_LEN, PanoramaxOverlay.class.getSimpleName().length()); private static final String DEBUG_TAG = PanoramaxOverlay.class.getSimpleName().substring(0, TAG_LEN); @@ -92,9 +95,7 @@ public class PanoramaxOverlay extends de.blau.android.layer.mvt.MapOverlay imple public static final String APIKEY_KEY = "MAPILLARY_CLIENT_TOKEN"; - public static final String FILENAME = "panoramax" + "." + FileExtensions.RES; - public static final String SET_POSITION_KEY = "set_position"; - public static final String COORDINATES_KEY = "coordinates"; + public static final String FILENAME = "panoramax" + "." + FileExtensions.RES; static class State implements Serializable { private static final long serialVersionUID = 1L; @@ -109,8 +110,6 @@ static class State implements Serializable { private State state = new State(); private final SavingHelper savingHelper = new SavingHelper<>(); - private final String apiKey; - /** Map this is an overlay of. */ private Map map = null; @@ -142,12 +141,6 @@ public PanoramaxOverlay(@NonNull final Map map) { } catch (IOException e) { Log.e(DEBUG_TAG, "Unable to create cache directory " + e.getMessage()); } - try (KeyDatabaseHelper keys = new KeyDatabaseHelper(context); SQLiteDatabase db = keys.getReadableDatabase()) { - apiKey = KeyDatabaseHelper.getKey(db, APIKEY_KEY, EntryType.API_KEY); - if (apiKey == null) { - ScreenMessage.toastTopError(context, context.getString(R.string.toast_api_key_missing, APIKEY_KEY)); - } - } setPrefs(map.getPrefs()); resetStyling(); @@ -213,17 +206,15 @@ public void invalidate() { @Override public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.VectorTileDecoder.Feature f) { if (isPoint(f)) { - System.out.println("onSelected " + f.toString()); // we ignore anything except the images for now java.util.Map attributes = f.getAttributes(); String sequenceId = (String) attributes.get(SEQUENCE_ID_KEY); String id = (String) attributes.get(ID_KEY); - System.out.println(attributes.toString()); if (id != null && sequenceId != null) { ArrayList keys = state != null ? state.sequenceCache.get(sequenceId) : null; if (keys == null) { try { - Thread t = new Thread(null, new SequenceFetcher(activity, sequenceId, id), "Panoramax Sequence"); + Thread t = new Thread(null, new PanoramaxSequenceFetcher(activity, panoramaxSequencesUrl, sequenceId, id), "Panoramax Sequence"); t.start(); } catch (SecurityException | IllegalThreadStateException e) { Log.e(DEBUG_TAG, "Unable to run SequenceFetcher " + e.getMessage()); @@ -258,7 +249,7 @@ private boolean isPoint(@NonNull de.blau.android.util.mvt.VectorTileDecoder.Feat private void showImages(@NonNull FragmentActivity activity, @NonNull String id, @NonNull ArrayList ids) { int pos = ids.indexOf(id); if (pos >= 0 && cacheDir != null) { - String imagesUrl = String.format(panoramaxImagesUrl, "%s", apiKey); + String imagesUrl = String.format(panoramaxImagesUrl, "%s"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { PhotoViewerFragment.showDialog(activity, ids, pos, new PanoramaxLoader(cacheDir, cacheSize, imagesUrl, ids)); } else { @@ -271,16 +262,16 @@ private void showImages(@NonNull FragmentActivity activity, @NonNull String id, } /** - * Runnable that will fetch a sequence from the Mapillary API + * Runnable that will fetch a sequence from the Panoramax API * * @author simon * */ - private class SequenceFetcher implements Runnable { + private class PanoramaxSequenceFetcher extends AbstractSequenceFetcher { - final FragmentActivity activity; - final String sequenceId; - final String id; + private static final String FEATURES_KEY = "features"; + + private final String id; /** * Construct a new instance @@ -289,61 +280,38 @@ private class SequenceFetcher implements Runnable { * @param sequenceId the sequence id * @param id the image id */ - public SequenceFetcher(@NonNull FragmentActivity activity, @NonNull String sequenceId, @NonNull String id) { - System.out.println("SequenceFetcher " + sequenceId + " " + id); - this.activity = activity; - this.sequenceId = sequenceId; + public PanoramaxSequenceFetcher(@NonNull FragmentActivity activity, @NonNull String urlTemplate, @NonNull String sequenceId, @NonNull String id) { + super(activity,urlTemplate, sequenceId, null); this.id = id; } @Override - public void run() { - try { - System.out.println(panoramaxSequencesUrl); - URL url = new URL(String.format(panoramaxSequencesUrl, sequenceId, apiKey)); - Log.d(DEBUG_TAG, "query sequence: " + url.toString()); - Request request = new Request.Builder().url(url).build(); - OkHttpClient client = App.getHttpClient().newBuilder().connectTimeout(20000, TimeUnit.MILLISECONDS).readTimeout(20000, TimeUnit.MILLISECONDS) - .build(); - Call mapillaryCall = client.newCall(request); - Response mapillaryCallResponse = mapillaryCall.execute(); - if (mapillaryCallResponse.isSuccessful()) { - ResponseBody responseBody = mapillaryCallResponse.body(); - try (InputStream inputStream = responseBody.byteStream()) { - if (inputStream != null) { - StringBuilder sb = new StringBuilder(); - int cp; - while ((cp = inputStream.read()) != -1) { - sb.append((char) cp); - } - JsonElement root = JsonParser.parseString(sb.toString()); - if (root.isJsonObject()) { - JsonElement data = ((JsonObject) root).get("features"); - if (data instanceof JsonArray) { - JsonArray featuresArray = data.getAsJsonArray(); - ArrayList ids = new ArrayList<>(); - System.out.println("features)"); - for (JsonElement element : featuresArray) { - if (element instanceof JsonObject) { - JsonElement temp = ((JsonObject) element).get(ID_KEY); - if (temp != null) { - ids.add(temp.getAsString()); - } - } - } - if (state == null) { - state = new State(); - } - state.sequenceCache.put(sequenceId, ids); - showImages(activity, id, ids); - } - } - } + protected void saveIdsAndUpdate(ArrayList ids) { + if (state == null) { + state = new State(); + } + state.sequenceCache.put(sequenceId, ids); + showImages(activity, id, ids); + + } + + @Override + protected ArrayList getIds(JsonElement root) throws IOException { + JsonElement features = ((JsonObject) root).get(FEATURES_KEY); + if (!(features instanceof JsonArray)) { + throw new IOException("features not a JsonArray"); + } + JsonArray featuresArray = features.getAsJsonArray(); + ArrayList ids = new ArrayList<>(); + for (JsonElement element : featuresArray) { + if (element instanceof JsonObject) { + JsonElement temp = ((JsonObject) element).get(ID_KEY); + if (temp != null) { + ids.add(temp.getAsString()); } } - } catch (IOException ex) { - Log.d(DEBUG_TAG, "query sequence failed with " + ex.getMessage()); } + return ids; } } @@ -385,22 +353,20 @@ void setSelected(@Nullable String id) { } } - /** - * Select a specific image in the selected sequence - * - * @param pos the position in the sequence - */ - public synchronized void select(int pos) { - if (state != null && state.sequenceId != null) { - List ids = state.sequenceCache.get(state.sequenceId); - if (ids != null) { - String id = ids.get(pos); - if (id != null) { - setSelected(id); - return; + @Override + public void selectImage(int pos) { + synchronized (this) { + if (state != null && state.sequenceId != null) { + List ids = state.sequenceCache.get(state.sequenceId); + if (ids != null) { + String id = ids.get(pos); + if (id != null) { + setSelected(id); + return; + } } + Log.e(DEBUG_TAG, "position " + pos + " not found in sequence " + state.sequenceId); } - Log.e(DEBUG_TAG, "position " + pos + " not found in sequence " + state.sequenceId); } }