Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support paging api for sequences from panoramax.xyz #2774

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/assets/imagery/imagery_vespucci.geojson
Original file line number Diff line number Diff line change
@@ -132,7 +132,7 @@
"type": "tms",
"tile_type": "mvt",
"category": "internal",
"url": "https://panoramax.openstreetmap.fr/api/map/{z}/{x}/{y}.pbf"
"url": "https://panoramax.xyz/api/map/{z}/{x}/{y}.mvt"
},
"type": "Feature"
},
10 changes: 2 additions & 8 deletions src/main/assets/panoramax-style.json
Original file line number Diff line number Diff line change
@@ -62,10 +62,7 @@
"minzoom": 19,
"layout": {
"icon-image": "arrow",
"icon-rotate": {
"property": "heading",
"type": "identity"
}
"icon-rotate": ["to-number", ["get","heading"]]
},
"paint": {
"icon-color": "rgba(0, 215, 0, 1)"
@@ -83,10 +80,7 @@
"minzoom": 19,
"layout": {
"icon-image": "arrow",
"icon-rotate": {
"property": "heading",
"type": "identity"
}
"icon-rotate": ["to-number", ["get","heading"]]
},
"paint": {
"icon-color": "rgba(100, 200, 0, 1)"
Original file line number Diff line number Diff line change
@@ -51,36 +51,44 @@ 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<String> ids = getIds(root);
saveIdsAndUpdate(ids);
}
ArrayList<String> ids = new ArrayList<>();
querySequence(url, ids);
saveIdsAndUpdate(ids);
} catch (IOException ex) {
Log.e(DEBUG_TAG, "query sequence failed with " + ex.getMessage());
}
}

/**
* @param url
* @throws IOException
*/
protected void querySequence(URL url, 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;
}
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");
}
getIds(root, ids);
}
}

/**
* Add ids list to state and update map
*
@@ -92,9 +100,10 @@ public void run() {
* Get list of ids from a sequence
*
* @param root top level JsonElement
* @param ids
* @return a List of ids
* @throws IOException if the ids can't be found
*/
@NonNull
protected abstract ArrayList<String> getIds(@NonNull JsonElement root) throws IOException;
protected abstract ArrayList<String> getIds(@NonNull JsonElement root, ArrayList<String> ids) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -208,13 +208,12 @@ protected void saveIdsAndUpdate(ArrayList<String> ids) {
}

@Override
protected ArrayList<String> getIds(JsonElement root) throws IOException {
protected ArrayList<String> 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");
}
JsonArray idArray = data.getAsJsonArray();
ArrayList<String> ids = new ArrayList<>();
for (JsonElement element : idArray) {
if (element instanceof JsonObject) {
JsonElement temp = ((JsonObject) element).get(ID_KEY);
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ class PanoramaxLoader extends NetworkImageLoader {
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);

final Map<String, String> urls;
private final Map<String, String> urls;

/**
* Construct a new loader
Original file line number Diff line number Diff line change
@@ -4,24 +4,29 @@

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 java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.text.SpannableString;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
@@ -35,6 +40,7 @@
import de.blau.android.prefs.Preferences;
import de.blau.android.util.DateFormatter;
import de.blau.android.util.SavingHelper;
import de.blau.android.util.ScreenMessage;
import de.blau.android.util.mvt.VectorTileRenderer;
import de.blau.android.util.mvt.style.Layer;
import de.blau.android.util.mvt.style.Style;
@@ -57,6 +63,8 @@ public class PanoramaxOverlay extends AbstractImageOverlay {
private static final String FIRST_SEQUENCE_KEY = "first_sequence";
private static final String SEQUENCES_KEY = "sequences";

private static final Pattern SEQUENCES_PATTERN = Pattern.compile("^\\[\\\"([^\\, ]+)\\\".*\\]$");

private static final String DEFAULT_PANORAMAX_STYLE_JSON = "panoramax-style.json";

/** this is the format used by panoramax */
@@ -88,6 +96,15 @@ static class State implements Serializable {
*/
public PanoramaxOverlay(@NonNull final Map map) {
super(map, PANORAMAX_TILES_ID, IMAGE_LAYER, DEFAULT_PANORAMAX_STYLE_JSON);
// hack so that we can follow the preferences
try {
Uri uri = Uri.parse(layerSource.getTileUrl());
Uri prefsUri = Uri.parse(App.getPreferences(map.getContext()).getPanoramaxApiUrl());
Log.i(DEBUG_TAG, "Change host from " + uri.getAuthority() + " to " + prefsUri.getAuthority());
uri.buildUpon().authority(prefsUri.getAuthority());
} catch (Exception ex) {
Log.e(DEBUG_TAG, "Unparsable tile url " + ex.getMessage());
}
setDateRange(panoramaxState.startDate, panoramaxState.endDate);
}

@@ -123,16 +140,10 @@ public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.Vecto
}
// we ignore anything except the images for now
java.util.Map<String, Object> attributes = f.getAttributes();
String sequenceId = (String) attributes.get(FIRST_SEQUENCE_KEY);
if (sequenceId == null) {
Object o = attributes.get(SEQUENCES_KEY);
if (o instanceof String) {
sequenceId = (String) o;
}
}
String sequenceId = getSequenceId(attributes);

String id = (String) attributes.get(ID_KEY);
Log.e(DEBUG_TAG, "trying to retrieve sequence " + sequenceId + " for " + id);
Log.d(DEBUG_TAG, "trying to retrieve sequence " + sequenceId + " for " + id);
if (id != null && sequenceId != null) {
ArrayList<String> keys = panoramaxState != null ? panoramaxState.sequenceCache.get(sequenceId) : null;
if (keys == null) {
@@ -150,7 +161,30 @@ public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.Vecto
panoramaxState.sequenceId = sequenceId;
return;
}
Log.e(DEBUG_TAG, "Sequence ID " + sequenceId + " ID " + id);
String message = activity.getString(R.string.toast_panoramax_sequence_error, sequenceId, id);
ScreenMessage.toastTopError(activity, message);
Log.e(DEBUG_TAG, message);
}

/**
* Try to determine the sequence id from the atributes
*
* @param attributes the attributes
* @return the id or null if it can't be found
*/
@Nullable
private String getSequenceId(@NonNull java.util.Map<String, Object> attributes) {
String sequenceId = (String) attributes.get(FIRST_SEQUENCE_KEY);
if (sequenceId == null) {
Object o = attributes.get(SEQUENCES_KEY);
if (o instanceof String) {
Matcher m = SEQUENCES_PATTERN.matcher((String) o);
if (m.find()) {
sequenceId = m.group(1);
}
}
}
return sequenceId;
}

/**
@@ -169,9 +203,13 @@ private void showImages(@NonNull FragmentActivity activity, @NonNull String id,
ImageViewerActivity.start(activity, ids, pos, new PanoramaxLoader(cacheDir, cacheSize, ids, panoramaxState.urlCache));
}
activity.runOnUiThread(() -> map.invalidate());
return;
} else {
Log.e(DEBUG_TAG, "image id " + id + " not found in sequence");
Log.e(DEBUG_TAG, "Image id " + id + " not found in sequence");
}
String message = activity.getString(R.string.toast_panoramax_image_not_in_sequence_error, id);
ScreenMessage.toastTopError(activity, message);
Log.e(DEBUG_TAG, message);
}

/**
@@ -182,6 +220,9 @@ private void showImages(@NonNull FragmentActivity activity, @NonNull String id,
*/
private class PanoramaxSequenceFetcher extends AbstractSequenceFetcher {

private static final String LINKS_KEY = "links";
private static final String REL_KEY = "rel";
private static final String NEXT_VALUE = "next";
private static final String FEATURES_KEY = "features";
private static final String ASSETS_KEY = "assets";
private static final String HD_KEY = "hd";
@@ -212,18 +253,29 @@ protected void saveIdsAndUpdate(ArrayList<String> ids) {
}

@Override
protected ArrayList<String> getIds(JsonElement root) throws IOException {
protected ArrayList<String> 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");
}
JsonArray featuresArray = features.getAsJsonArray();
ArrayList<String> ids = new ArrayList<>();
for (JsonElement element : featuresArray) {
if (element instanceof JsonObject) {
getIdAndUrl(ids, panoramaxState.urlCache, element);
}
}
// check for paginated response
JsonElement linksArray = ((JsonObject) root).get(LINKS_KEY);
if (linksArray instanceof JsonArray) {
for (JsonElement element : ((JsonArray) linksArray)) {
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 ids;
}

@@ -255,6 +307,7 @@ private void getIdAndUrl(@NonNull List<String> ids, @NonNull java.util.Map<Strin
JsonElement href = ((JsonObject) hd).get(HREF_KEY);
if (href == null) {
Log.e(DEBUG_TAG, "href not found in sequence from API for id " + idString);
return;
}
urls.put(idString, href.getAsString());

2 changes: 2 additions & 0 deletions src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -826,6 +826,8 @@
<string name="toast_no_roles_found">No roles found, all elements can be selected</string>
<string name="toast_no_preset_found">No matching preset found, all elements can be selected</string>
<string name="toast_undeleting_all_members">Undeleting all relation members with this element</string>
<string name="toast_panoramax_sequence_error">Unable to retrieve panoramax sequence id %1$s id %2$s</string>
<string name="toast_panoramax_image_not_in_sequence_error">Image id %1$s not found in sequence</string>
<!-- Error messages -->
<string name="error_mapsplit_missing_zoom">MapSplit sources must have min and max zoom set</string>
<string name="error_pbf_no_version">Version information missing in PBF file</string>