Skip to content

Commit

Permalink
Support paging api for sequences from panoramax.xyz
Browse files Browse the repository at this point in the history
Further this fixes reading the heading attribute in the tiles if it is
encoded as a string instead of an int.
  • Loading branch information
simonpoole committed Jan 22, 2025
1 parent 26d5602 commit 339c77c
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/main/assets/imagery/imagery_vespucci.geojson
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
10 changes: 2 additions & 8 deletions src/main/assets/panoramax-style.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 */
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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());

Expand Down
2 changes: 2 additions & 0 deletions src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 339c77c

Please sign in to comment.