Skip to content

Commit

Permalink
Merge branch 'improve_behaviour_for_large_panoramax_sequences'
Browse files Browse the repository at this point in the history
  • Loading branch information
simonpoole committed Jan 23, 2025
2 parents 504e29a + 30e4695 commit 9416e7b
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 61 deletions.
4 changes: 4 additions & 0 deletions src/main/java/de/blau/android/dialogs/Progress.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class Progress extends ImmersiveDialogFragment {
public static final int PROGRESS_LOADING_PRESET = 14;
public static final int PROGRESS_IMPORTING_FILE = 15;
public static final int PROGRESS_DOWNLOAD_TASKS = 16;
public static final int PROGRESS_DOWNLOAD_SEQUENCE = 17;

private int dialogType;
private String messageArg;
Expand Down Expand Up @@ -133,6 +134,7 @@ public static void dismissAll(@NonNull FragmentActivity activity) {
dismissDialog(activity, PROGRESS_LOADING_PRESET);
dismissDialog(activity, PROGRESS_IMPORTING_FILE);
dismissDialog(activity, PROGRESS_DOWNLOAD_TASKS);
dismissDialog(activity, PROGRESS_DOWNLOAD_SEQUENCE);
}

/**
Expand Down Expand Up @@ -176,6 +178,8 @@ private static String getTag(int dialogType) {
return "dialog_progress_importing_file";
case PROGRESS_DOWNLOAD_TASKS:
return "dialog_progress_download_tasks";
case PROGRESS_DOWNLOAD_SEQUENCE:
return "dialog_progress_download_sequence";
default:
Log.w(DEBUG_TAG, "Unknown dialog type " + dialogType);
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/de/blau/android/dialogs/ProgressDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ public static AlertDialog get(@NonNull Context ctx, int dialogType, @Nullable St
titleId = R.string.progress_title;
messageId = R.string.progress_download_tasks_message;
break;
case Progress.PROGRESS_DOWNLOAD_SEQUENCE:
titleId = R.string.progress_title;
messageId = R.string.progress_download_sequence_message;
break;
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,33 @@ protected AbstractSequenceFetcher(@NonNull FragmentActivity activity, @NonNull S
public void run() {
try {
URL url = new URL(String.format(urlTemplate, sequenceId, apiKey));
Log.d(DEBUG_TAG, "query sequence: " + url.toString());
ArrayList<String> ids = new ArrayList<>();
querySequence(url, ids);
do {
Log.d(DEBUG_TAG, "query sequence: " + url.toString());
url = querySequence(url, ids);
} while (url != null);
saveIdsAndUpdate(ids);
} catch (IOException ex) {
Log.e(DEBUG_TAG, "query sequence failed with " + ex.getMessage());
}
}

/**
* @param url
* @throws IOException
* query the api for a sequence
*
* @param url the URL
* @throws IOException if IO goes wrong
*/
protected void querySequence(URL url, ArrayList<String> ids) throws IOException {
@Nullable
protected URL querySequence(@NonNull URL url, @NonNull ArrayList<String> ids) throws IOException {
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;
Call call = client.newCall(request);
Response callResponse = call.execute();
if (!callResponse.isSuccessful()) {
return null;
}
ResponseBody responseBody = mapillaryCallResponse.body();
ResponseBody responseBody = callResponse.body();
try (InputStream inputStream = responseBody.byteStream()) {
if (inputStream == null) {
throw new IOException("null InputStream");
Expand All @@ -85,7 +90,7 @@ protected void querySequence(URL url, ArrayList<String> ids) throws IOException
if (!root.isJsonObject()) {
throw new IOException("root is not a JsonObject");
}
getIds(root, ids);
return getIds(root, ids);
}
}

Expand All @@ -104,6 +109,6 @@ protected void querySequence(URL url, ArrayList<String> ids) throws IOException
* @return a List of ids
* @throws IOException if the ids can't be found
*/
@NonNull
protected abstract ArrayList<String> getIds(@NonNull JsonElement root, ArrayList<String> ids) throws IOException;
@Nullable
protected abstract URL getIds(@NonNull JsonElement root, ArrayList<String> ids) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import com.google.gson.JsonArray;
Expand Down Expand Up @@ -38,6 +38,7 @@
import de.blau.android.resources.KeyDatabaseHelper.EntryType;
import de.blau.android.util.SavingHelper;
import de.blau.android.util.ScreenMessage;
import de.blau.android.util.collections.MRUHashMap;
import de.blau.android.util.mvt.VectorTileRenderer;
import de.blau.android.util.mvt.style.Layer;
import de.blau.android.util.mvt.style.Style;
Expand Down Expand Up @@ -71,13 +72,15 @@ public class MapillaryOverlay extends AbstractImageOverlay {
public static final String FILENAME = "mapillary" + "." + FileExtensions.RES;

static class State implements Serializable {
private static final long serialVersionUID = 4L;
private static final long serialVersionUID = 5L;

private String sequenceId = null;
private long imageId = 0;
private final java.util.Map<String, ArrayList<String>> sequenceCache = new HashMap<>();
private long startDate = 0L;
private long endDate = new Date().getTime();
private static final int RETAINED_SEQUENCES = 100;

private String sequenceId = null;
private long imageId = 0;
private final MRUHashMap<String, ArrayList<String>> sequenceCache = new MRUHashMap<>(RETAINED_SEQUENCES);
private long startDate = 0L;
private long endDate = new Date().getTime();
}

private State mapillaryState = new State();
Expand All @@ -99,7 +102,7 @@ public MapillaryOverlay(@NonNull final Map map) {
ScreenMessage.toastTopError(context, context.getString(R.string.toast_api_key_missing, APIKEY_KEY));
}
}
setDateRange(mapillaryState.startDate, mapillaryState.endDate);
setDateRange(0, new Date().getTime());
}

@Override
Expand All @@ -111,7 +114,7 @@ public void onSaveState(@NonNull Context ctx) throws IOException {
@Override
public boolean onRestoreState(@NonNull Context ctx) {
boolean result = super.onRestoreState(ctx);
if (mapillaryState == null) {
if (mapillaryState.sequenceCache.isEmpty()) {
mapillaryState = savingHelper.load(ctx, FILENAME, true);
if (mapillaryState != null) {
setSelected(mapillaryState.imageId);
Expand Down Expand Up @@ -208,7 +211,7 @@ protected void saveIdsAndUpdate(ArrayList<String> ids) {
}

@Override
protected ArrayList<String> getIds(JsonElement root, ArrayList<String> ids) throws IOException {
protected URL getIds(JsonElement root, ArrayList<String> ids) throws IOException {
JsonElement data = ((JsonObject) root).get(DATA_KEY);
if (!(data instanceof JsonArray)) {
throw new IOException("data not a JsonArray");
Expand All @@ -222,7 +225,7 @@ protected ArrayList<String> getIds(JsonElement root, ArrayList<String> ids) thro
}
}
}
return ids;
return null;
}
}

Expand Down Expand Up @@ -284,8 +287,10 @@ public void selectImage(int pos) {

@Override
public void setDateRange(long start, long end) {
mapillaryState.startDate = start;
mapillaryState.endDate = end;
if (mapillaryState != null) {
mapillaryState.startDate = start;
mapillaryState.endDate = end;
}
Style style = ((VectorTileRenderer) tileRenderer).getStyle();
setDateRange(style, IMAGE_LAYER, start, end, null);
setDateRange(style, SEQUENCE_LAYER, start, end, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@
import de.blau.android.contract.FileExtensions;
import de.blau.android.contract.Urls;
import de.blau.android.dialogs.DateRangeDialog;
import de.blau.android.dialogs.Progress;
import de.blau.android.layer.LayerType;
import de.blau.android.layer.streetlevel.AbstractImageOverlay;
import de.blau.android.layer.streetlevel.AbstractSequenceFetcher;
import de.blau.android.layer.streetlevel.ImageViewerActivity;
import de.blau.android.photos.PhotoViewerFragment;
import de.blau.android.prefs.Preferences;
import de.blau.android.util.DateFormatter;
import de.blau.android.util.ExecutorTask;
import de.blau.android.util.SavingHelper;
import de.blau.android.util.ScreenMessage;
import de.blau.android.util.collections.MRUHashMap;
import de.blau.android.util.mvt.VectorTileRenderer;
import de.blau.android.util.mvt.style.Layer;
import de.blau.android.util.mvt.style.Style;
Expand Down Expand Up @@ -70,20 +73,53 @@ public class PanoramaxOverlay extends AbstractImageOverlay {
/** this is the format used by panoramax */
private final SimpleDateFormat dateFormat = DateFormatter.getUtcFormat("yyyy-MM-dd");

private static final String API_COLLECTIONS_ITEMS = "api/collections/%s/items";
private static final String API_COLLECTIONS_ITEMS = "api/collections/%s/items?limit=1000";
private String panoramaxSequencesUrl = Urls.DEFAULT_PANORAMAX_API_URL + API_COLLECTIONS_ITEMS;

public static final String FILENAME = "panoramax" + "." + FileExtensions.RES;

private static final int MAX_IMAGE_IDS = 100; // max number of image ids left and right to send to the viewer

static class State implements Serializable {
private static final long serialVersionUID = 2L;

private String sequenceId = null;
private String imageId = null;
private final java.util.Map<String, ArrayList<String>> sequenceCache = new HashMap<>();
private final java.util.Map<String, String> urlCache = new HashMap<>();
private long startDate = 0L;
private long endDate = new Date().getTime();
private static final long serialVersionUID = 3L;

private static final int RETAINED_SEQUENCES = 50;

private String sequenceId = null;
private String imageId = null;
private final MRUHashMap<String, ArrayList<String>> sequenceCache = new MRUHashMap<>(RETAINED_SEQUENCES);
private final java.util.Map<String, String> urlCache = new HashMap<>();
private long startDate = 0L;
private long endDate = new Date().getTime();

/**
* Add a sequence to the sequence cache, if the cache capacity is exceeded, entries will be removed from the url
* cache too
*
* @param id the sequence id
* @param list the list of image ids in the sequence
*/
void cacheSequence(@NonNull String id, @NonNull ArrayList<String> list) {
synchronized (urlCache) {
final ArrayList<String> removedList = sequenceCache.put(id, list);
Log.d(DEBUG_TAG, "Sequence cache size " + sequenceCache.size());
if (removedList != null) {
new ExecutorTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void input) throws Exception {
// remove entries from URL cache
Log.d(DEBUG_TAG, "Removing " + removedList.size() + " entries from url cache");
synchronized (urlCache) {
for (String url : removedList) {
urlCache.remove(url);
}
}
return null;
}
}.execute();
}
}
}
}

private State panoramaxState = new State();
Expand All @@ -105,7 +141,7 @@ public PanoramaxOverlay(@NonNull final Map map) {
} catch (Exception ex) {
Log.e(DEBUG_TAG, "Unparsable tile url " + ex.getMessage());
}
setDateRange(panoramaxState.startDate, panoramaxState.endDate);
setDateRange(0, new Date().getTime());
}

@Override
Expand All @@ -117,7 +153,7 @@ public void onSaveState(@NonNull Context ctx) throws IOException {
@Override
public boolean onRestoreState(@NonNull Context ctx) {
boolean result = super.onRestoreState(ctx);
if (panoramaxState == null) {
if (panoramaxState.sequenceCache.isEmpty()) {
panoramaxState = savingHelper.load(ctx, FILENAME, true);
if (panoramaxState != null) {
setSelected(panoramaxState.imageId);
Expand Down Expand Up @@ -147,13 +183,23 @@ public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.Vecto
if (id != null && sequenceId != null) {
ArrayList<String> keys = panoramaxState != null ? panoramaxState.sequenceCache.get(sequenceId) : null;
if (keys == null) {
try {
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());
return;
}
new ExecutorTask<Void, Integer, Void>() {
@Override
protected void onPreExecute() {
Progress.showDialog(activity, Progress.PROGRESS_DOWNLOAD_SEQUENCE);
}

@Override
protected Void doInBackground(Void param) {
new PanoramaxSequenceFetcher(activity, panoramaxSequencesUrl, sequenceId, id).run();
return null;
}

@Override
protected void onPostExecute(Void result) {
Progress.dismissDialog(activity, Progress.PROGRESS_DOWNLOAD_SEQUENCE);
}
}.execute();
} else {
showImages(activity, id, keys);
}
Expand Down Expand Up @@ -194,13 +240,20 @@ private String getSequenceId(@NonNull java.util.Map<String, Object> attributes)
* @param id id of the image
* @param ids list of all ids in the sequence
*/
private void showImages(@NonNull FragmentActivity activity, @NonNull String id, @NonNull ArrayList<String> ids) {
private void showImages(@NonNull FragmentActivity activity, @NonNull String id, @NonNull List<String> ids) {
int pos = ids.indexOf(id);
// sequences can be very large, we show an excerpt of ~200 around the current position
ArrayList<String> tempIds = new ArrayList<>(ids.subList(Math.max(0, pos - MAX_IMAGE_IDS), Math.min(pos + MAX_IMAGE_IDS, ids.size())));
pos = tempIds.indexOf(id);
java.util.Map<String, String> tempUrlCache = new HashMap<>();
for (String tempId : tempIds) {
tempUrlCache.put(tempId, panoramaxState.urlCache.get(tempId));
}
if (pos >= 0 && cacheDir != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
PhotoViewerFragment.showDialog(activity, ids, pos, new PanoramaxLoader(cacheDir, cacheSize, ids, panoramaxState.urlCache));
PhotoViewerFragment.showDialog(activity, tempIds, pos, new PanoramaxLoader(cacheDir, cacheSize, tempIds, tempUrlCache));
} else {
ImageViewerActivity.start(activity, ids, pos, new PanoramaxLoader(cacheDir, cacheSize, ids, panoramaxState.urlCache));
ImageViewerActivity.start(activity, tempIds, pos, new PanoramaxLoader(cacheDir, cacheSize, tempIds, tempUrlCache));
}
activity.runOnUiThread(() -> map.invalidate());
return;
Expand Down Expand Up @@ -248,12 +301,12 @@ protected void saveIdsAndUpdate(ArrayList<String> ids) {
if (panoramaxState == null) {
panoramaxState = new State();
}
panoramaxState.sequenceCache.put(sequenceId, ids);
panoramaxState.cacheSequence(sequenceId, ids);
showImages(activity, id, ids);
}

@Override
protected ArrayList<String> getIds(JsonElement root, ArrayList<String> ids) throws IOException {
protected URL getIds(JsonElement root, ArrayList<String> ids) throws IOException {
JsonElement features = ((JsonObject) root).get(FEATURES_KEY);
if (!(features instanceof JsonArray)) {
throw new IOException("features not a JsonArray");
Expand All @@ -271,12 +324,11 @@ protected ArrayList<String> getIds(JsonElement root, ArrayList<String> ids) thro
if (element instanceof JsonObject && ((JsonObject) element).has(REL_KEY)
&& NEXT_VALUE.equals(((JsonObject) element).get(REL_KEY).getAsString())) {
Log.d(DEBUG_TAG, "get next page");
querySequence(new URL(((JsonObject) element).get(HREF_KEY).getAsString()), ids);
break;
return new URL(((JsonObject) element).get(HREF_KEY).getAsString());
}
}
}
return ids;
return null;
}

/**
Expand Down Expand Up @@ -371,8 +423,10 @@ public void selectImage(int pos) {

@Override
public void setDateRange(long start, long end) {
panoramaxState.startDate = start;
panoramaxState.endDate = end;
if (panoramaxState != null) {
panoramaxState.startDate = start;
panoramaxState.endDate = end;
}
Style style = ((VectorTileRenderer) tileRenderer).getStyle();
setDateRange(style, IMAGE_LAYER, start, end, timestampFormat);
setDateRange(style, SEQUENCE_LAYER, start, end, dateFormat);
Expand Down
Loading

0 comments on commit 9416e7b

Please sign in to comment.