Skip to content

Commit

Permalink
Refactor to support panoramax.xyz properly, code clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
simonpoole committed Jan 21, 2025
1 parent 2da1a65 commit 7e10ca4
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 270 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package de.blau.android.layer.streetlevel.panoramax;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.BufferedReader;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -22,13 +23,11 @@
import android.app.Instrumentation.ActivityMonitor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import de.blau.android.App;
import de.blau.android.LayerUtils;
import de.blau.android.Logic;
Expand All @@ -37,33 +36,32 @@
import de.blau.android.MockTileServer;
import de.blau.android.R;
import de.blau.android.TestUtils;
import de.blau.android.dialogs.DateRangeDialog;
import de.blau.android.layer.LayerDialogTest;
import de.blau.android.layer.LayerType;
import de.blau.android.layer.streetlevel.ImageViewerActivity;
import de.blau.android.layer.streetlevel.mapillary.MapillaryOverlay;
import de.blau.android.prefs.AdvancedPrefDatabase;
import de.blau.android.prefs.Preferences;
import de.blau.android.resources.TileLayerDatabase;
import de.blau.android.resources.TileLayerSource.TileType;
import de.blau.android.util.FileUtil;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;

@RunWith(AndroidJUnit4.class)
@LargeTest
public class PanoramaxTest {

AdvancedPrefDatabase prefDB = null;
Main main = null;
UiDevice device = null;
Map map = null;
Logic logic = null;
Instrumentation instrumentation = null;
MockWebServerPlus mockApiServer = null;
MockWebServer tileServer = null;
HttpUrl mockImagesBaseUrl = null;
AdvancedPrefDatabase prefDB = null;
Main main = null;
UiDevice device = null;
Map map = null;
Logic logic = null;
Instrumentation instrumentation = null;
MockWebServerPlus mockApiServer = null;
MockWebServer tileServer = null;
HttpUrl mockApiBaseUrl = null;

@Rule
public ActivityTestRule<Main> mActivityRule = new ActivityTestRule<>(Main.class);
Expand All @@ -84,10 +82,10 @@ public void setup() {
de.blau.android.layer.streetlevel.panoramax.PanoramaxOverlay.PANORAMAX_TILES_ID);

mockApiServer = new MockWebServerPlus();
HttpUrl mockApiBaseUrl = mockApiServer.server().url("/");
mockApiBaseUrl = mockApiServer.server().url("/");
Preferences prefs = new Preferences(main);
prefs.setPanoramaxApiUrl(mockApiBaseUrl.toString());

App.getLogic().setPrefs(prefs);
map = main.getMap();
map.setPrefs(main, prefs);
Expand Down Expand Up @@ -125,7 +123,19 @@ public void teardown() {
*/
@Test
public void panoramaxLayer() {
mockApiServer.enqueue("panoramax_sequences");
MockResponse response = new MockResponse();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try (InputStream inputStream = loader.getResourceAsStream("fixtures/panoramax_sequences.json");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String sequenceJson = FileUtil.readToString(reader);
sequenceJson = sequenceJson.replaceAll("https\\://panoramax.openstreetmap.fr/", mockApiBaseUrl.toString());
Buffer buffer = new Buffer();
buffer.write(sequenceJson.getBytes());
response.setBody(buffer);
} catch (IOException e1) {
Assert.fail(e1.getMessage());
}
mockApiServer.server().enqueue(response);
de.blau.android.layer.streetlevel.panoramax.PanoramaxOverlay layer = (PanoramaxOverlay) map.getLayer(LayerType.PANORAMAX);
assertNotNull(layer);
layer.flushCaches(main); // forces the layer to retrieve everything
Expand All @@ -142,12 +152,12 @@ public void panoramaxLayer() {

ActivityMonitor monitor = instrumentation.addMonitor(ImageViewerActivity.class.getName(), null, false);
// hack around slow rendering on some emulators
map.getViewBox().moveTo(map, (int) (2.3281776 * 1E7), (int) (48.8698124 * 1E7));
map.getViewBox().moveTo(map, (int) (2.3285747 * 1E7), (int) (48.8588878 * 1E7));
map.invalidate();
TestUtils.zoomToLevel(device, main, 22);
TestUtils.clickAtCoordinates(device, map, 2.3281776, 48.8698124, true);
TestUtils.clickAtCoordinates(device, map, 2.3285747, 48.8588878, true);
if (TestUtils.clickText(device, false, "OK", true)) {
TestUtils.clickAtCoordinates(device, map, 2.3281776, 48.8698124, true);
TestUtils.clickAtCoordinates(device, map, 2.3285747, 48.8588878, true);
}
ImageViewerActivity viewer = null;
try {
Expand All @@ -166,7 +176,7 @@ public void panoramaxLayer() {
}
assertTrue(TestUtils.clickMenuButton(device, main.getString(R.string.share), false, true));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
assertTrue(TestUtils.findText(device, false, "178993950747668"));
assertTrue(TestUtils.findText(device, false, "f4fd371a-1203-4aa7-95ca-24026fa956b1"));
} else { // currently can't test this properly on Android before 10
assertTrue(TestUtils.findText(device, false, "Share with"));
}
Expand Down
Binary file modified src/androidTest/resources/panoramax.mbtiles
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void run() {
saveIdsAndUpdate(ids);
}
} catch (IOException ex) {
Log.d(DEBUG_TAG, "query sequence failed with " + ex.getMessage());
Log.e(DEBUG_TAG, "query sequence failed with " + ex.getMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
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.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 com.davemorrissey.labs.subscaleview.ImageSource;
Expand All @@ -15,8 +19,10 @@
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;
Expand Down Expand Up @@ -47,7 +53,7 @@ public abstract class NetworkImageLoader extends ImageLoader {
protected final long cacheSize;
protected final String imageUrl;
protected final Map<String, double[]> coordinates = new HashMap<>();
protected final List<String> ids;
private final List<String> ids;

private static final int IMAGERY_LOAD_THREADS = 3;
protected transient ThreadPoolExecutor mThreadPool;
Expand All @@ -67,6 +73,42 @@ protected NetworkImageLoader(@NonNull File cacheDir, long cacheSize, @NonNull St
this.ids = ids;
}

@Override
public void load(SubsamplingScaleImageView view, String key) {
File imageFile = new File(cacheDir, key + JPG);
if (imageFile.exists() && imageFile.length() > 0) {
if (!coordinates.containsKey(key)) {
try {
ExifInterface exif = new ExifInterface(imageFile);
coordinates.put(key, exif.getLatLong());
} catch (IOException e) {
Log.e(DEBUG_TAG, e.getMessage());
}
}
setImage(view, imageFile);
return;
}

initThreadPool();

// download
try {
mThreadPool.execute(getDownloader(key, view, imageFile));
} catch (RejectedExecutionException rjee) {
Log.e(DEBUG_TAG, "Execution rejected " + rjee.getMessage());
}
}

/**
* Get a runnable for the provider specific image download
*
* @param key the identifier for the image
* @param view target View
* @param imageFile target File
* @return a Runnable
*/
protected abstract Runnable getDownloader(@NonNull final String key, @NonNull final SubsamplingScaleImageView view, @NonNull final File imageFile);

/**
* Initialize the thread pool
*/
Expand All @@ -76,6 +118,26 @@ protected void initThreadPool() {
}
}

/**
* Write an InputStream to a file
*
* @param inputStream the InputStream
* @param imageFile the target File
* @throws IOException if writing goes wrond
*/
protected void writeStreamToFile(InputStream inputStream, File imageFile) throws IOException {
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);
}
}
}

/**
* Prune the image cache
*/
Expand Down Expand Up @@ -123,7 +185,7 @@ public void showOnMap(Context context, int index) {
*
* @return a LayerType
*/
abstract protected LayerType getLayerType();
protected abstract LayerType getLayerType();

@Override
public void share(Context context, String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
Expand All @@ -21,7 +19,6 @@
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

import android.annotation.SuppressLint;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.exifinterface.media.ExifInterface;
Expand Down Expand Up @@ -58,57 +55,35 @@ class MapillaryLoader extends NetworkImageLoader {
super(cacheDir, cacheSize, imageUrl, ids);
}

@SuppressLint("NewApi") // StandardCharsets is desugared for APIs < 19.
@Override
public void load(SubsamplingScaleImageView view, String key) {
File imageFile = new File(cacheDir, key + JPG);
if (imageFile.exists() && imageFile.length() > 0) {
if (!coordinates.containsKey(key)) {
try {
ExifInterface exif = new ExifInterface(imageFile);
coordinates.put(key, exif.getLatLong());
} catch (IOException e) {
Log.e(DEBUG_TAG, e.getMessage());
protected Runnable getDownloader(final String key, final SubsamplingScaleImageView view, final File imageFile) {
return () -> {
Log.d(DEBUG_TAG, "querying mapillary server for " + key);
try {
Request request = new Request.Builder().url(new URL(String.format(imageUrl, key))).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()) {
throw new IOException("Download of " + key + " failed with " + mapillaryCallResponse.code() + " " + mapillaryCallResponse.message());
}
}
setImage(view, imageFile);
return;
}
// download
initThreadPool();
try {
mThreadPool.execute(() -> {
Log.d(DEBUG_TAG, "querying server for " + key);
try {
String urlString = String.format(imageUrl, key);
URL url = new URL(urlString);
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()) {
throw new IOException("Download of " + key + " failed with " + mapillaryCallResponse.code() + " " + mapillaryCallResponse.message());
try (ResponseBody responseBody = mapillaryCallResponse.body(); InputStream inputStream = responseBody.byteStream()) {
if (inputStream == null) {
throw new IOException("No InputStream");
}
try (ResponseBody responseBody = mapillaryCallResponse.body(); InputStream inputStream = responseBody.byteStream()) {
if (inputStream == null) {
throw new IOException("No InputStream");
}
JsonElement root = JsonParser.parseReader(new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.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());
JsonElement root = JsonParser.parseReader(new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)));
if (!root.isJsonObject() || !((JsonObject) root).has(THUMB_2048_URL_FIELD)) {
throw new IOException("Unexpected / missing response");
}
setImage(view, imageFile);
pruneCache();
} catch (IOException e) {
Log.e(DEBUG_TAG, e.getMessage());
loadImage(key, imageFile, client, (JsonObject) root, ((JsonObject) root).get(THUMB_2048_URL_FIELD).getAsString());
}
});
} catch (RejectedExecutionException rjee) {
Log.e(DEBUG_TAG, "Execution rejected " + rjee.getMessage());
}
setImage(view, imageFile);
pruneCache();
} catch (IOException e) {
Log.e(DEBUG_TAG, e.getMessage());
}
};
}

/**
Expand All @@ -129,16 +104,7 @@ private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull Ok
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);
}
}
writeStreamToFile(inputStream, imageFile);
JsonElement point = meta.get(COMPUTED_GEOMETRY_FIELD);
if (!(point instanceof JsonObject) || imageFile.length() == 0) {
throw new IOException("No geometry for image or image empty");
Expand Down
Loading

0 comments on commit 7e10ca4

Please sign in to comment.