From d2d25f0cefa998608b8693040b685cf0f73e23bd Mon Sep 17 00:00:00 2001 From: simonpoole Date: Tue, 24 Oct 2023 19:21:20 +0200 Subject: [PATCH 1/2] Allow filtering Mapillary data by date This adds functionality to set a date range for Mapillary data to be filtered on. --- build.gradle | 4 +- .../layer/mapillary/MapillaryTest.java | 50 +++++- src/main/assets/mapillary-style.json | 41 ++++- .../de/blau/android/DisambiguationMenu.java | 2 +- src/main/java/de/blau/android/Main.java | 8 +- src/main/java/de/blau/android/Map.java | 2 +- .../blau/android/dialogs/DateRangeDialog.java | 152 ++++++++++++++++++ .../java/de/blau/android/dialogs/Layers.java | 14 +- .../android/layer/DateRangeInterface.java | 11 ++ .../layer/mapillary/MapillaryLoader.java | 4 +- ...{MapOverlay.java => MapillaryOverlay.java} | 117 ++++++++++---- .../de/blau/android/prefs/Preferences.java | 4 +- src/main/res/layout/daterange.xml | 19 +++ src/main/res/values/arrays.xml | 7 + src/main/res/values/strings.xml | 3 + src/main/res/values/styles.xml | 23 +++ src/test/java/de/blau/android/MapTest.java | 2 +- 17 files changed, 413 insertions(+), 50 deletions(-) create mode 100644 src/main/java/de/blau/android/dialogs/DateRangeDialog.java create mode 100644 src/main/java/de/blau/android/layer/DateRangeInterface.java rename src/main/java/de/blau/android/layer/mapillary/{MapOverlay.java => MapillaryOverlay.java} (81%) create mode 100644 src/main/res/layout/daterange.xml create mode 100644 src/main/res/values/arrays.xml diff --git a/build.gradle b/build.gradle index 6a8d17ffb9..64e320f310 100644 --- a/build.gradle +++ b/build.gradle @@ -609,7 +609,7 @@ dependencies { implementation "androidx.appcompat:appcompat-resources:1.6.1" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.preference:preference:1.1.0" - implementation "com.google.android.material:material:1.3.0" + implementation "com.google.android.material:material:1.8.0" implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.core:core:1.7.0" implementation "androidx.exifinterface:exifinterface:1.3.6" @@ -623,7 +623,7 @@ dependencies { //non-Android google implementation 'com.google.protobuf:protobuf-java:3.12.2' - implementation "com.google.code.gson:gson:2.8.9" + implementation "com.google.code.gson:gson:2.10.1" implementation "com.google.protobuf:protobuf-java:$googleProtobufVersion" implementation 'com.google.openlocationcode:openlocationcode:1.0.4' diff --git a/src/androidTest/java/de/blau/android/layer/mapillary/MapillaryTest.java b/src/androidTest/java/de/blau/android/layer/mapillary/MapillaryTest.java index 20a231d10b..c102da7090 100644 --- a/src/androidTest/java/de/blau/android/layer/mapillary/MapillaryTest.java +++ b/src/androidTest/java/de/blau/android/layer/mapillary/MapillaryTest.java @@ -1,10 +1,13 @@ package de.blau.android.layer.mapillary; +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.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -19,18 +22,23 @@ 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; import de.blau.android.Main; import de.blau.android.Map; 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.photos.MapillaryViewerActivity; import de.blau.android.prefs.AdvancedPrefDatabase; @@ -71,9 +79,9 @@ public void setup() { assertNotNull(main); TestUtils.grantPermissons(device); - + LayerUtils.removeLayer(main, LayerType.MAPILLARY); tileServer = MockTileServer.setupTileServer(main, "mapillary.mbt", true, LayerType.MAPILLARY, TileType.MVT, - de.blau.android.layer.mapillary.MapOverlay.MAPILLARY_TILES_ID); + de.blau.android.layer.mapillary.MapillaryOverlay.MAPILLARY_TILES_ID); mockApiServer = new MockWebServerPlus(); HttpUrl mockApiBaseUrl = mockApiServer.server().url("/"); @@ -110,14 +118,15 @@ public void teardown() { } catch (IOException | NullPointerException e) { // ignore } + LayerUtils.removeLayer(main, LayerType.MAPILLARY); try (TileLayerDatabase tlDb = new TileLayerDatabase(main); SQLiteDatabase db = tlDb.getWritableDatabase()) { - TileLayerDatabase.deleteLayerWithId(db, de.blau.android.layer.mapillary.MapOverlay.MAPILLARY_TILES_ID); + TileLayerDatabase.deleteLayerWithId(db, de.blau.android.layer.mapillary.MapillaryOverlay.MAPILLARY_TILES_ID); } instrumentation.waitForIdleSync(); } /** - * Add mapillary layer + * Add mapillary layer and click on one image */ @Test public void mapillaryLayer() { @@ -126,7 +135,7 @@ public void mapillaryLayer() { imageResponse.setResponseCode(200); imageResponse.setBody("{\"thumb_2048_url\": \"" + mockImagesBaseUrl.toString() + "\",\"computed_geometry\": {\"type\": \"Point\",\"coordinates\": [" + "8.407748800863,47.412813485744]" + "},\"id\": \"178993950747668\"}"); - de.blau.android.layer.mapillary.MapOverlay layer = (MapOverlay) map.getLayer(LayerType.MAPILLARY); + de.blau.android.layer.mapillary.MapillaryOverlay layer = (MapillaryOverlay) map.getLayer(LayerType.MAPILLARY); assertNotNull(layer); layer.flushCaches(main); // forces the layer to retrieve everything @@ -183,4 +192,35 @@ public void mapillaryLayer() { } } } + + /** + * Add mapillary layer and click on one image that should be filtered away + * + * Unluckily there doesn't seem to be an easy way to drag the sliders + */ + @Test + public void mapillaryLayerFilter() { + TestUtils.unlock(device); + TestUtils.sleep(); + + UiObject2 menuButton = TestUtils.getLayerButton(device, "Mapillary", LayerDialogTest.MENU_BUTTON); + menuButton.click(); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.layer_set_date_range), true, false)); + assertTrue(TestUtils.findText(device, false, main.getString(R.string.date_range_title))); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.okay), true, false)); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.Done), true, false)); + MapillaryOverlay layer = (MapillaryOverlay) map.getLayer(LayerType.MAPILLARY); + layer.setDateRange(0L, 0L); + layer.invalidate(); + // + map.getViewBox().moveTo(map, (int) (8.407748800863 * 1E7), (int) (47.412813485744 * 1E7)); + map.invalidate(); + TestUtils.zoomToLevel(device, main, 22); + TestUtils.clickAtCoordinates(device, map, 8.407748800863, 47.412813485744, true); // nothing should happen + if (TestUtils.clickText(device, false, "OK", true)) { + TestUtils.clickAtCoordinates(device, map, 8.407748800863, 47.412813485744, true); + } + assertFalse(TestUtils.clickMenuButton(device, main.getString(R.string.share), false, true)); + } + } diff --git a/src/main/assets/mapillary-style.json b/src/main/assets/mapillary-style.json index d2d207c234..a50c04750b 100644 --- a/src/main/assets/mapillary-style.json +++ b/src/main/assets/mapillary-style.json @@ -6,6 +6,19 @@ "id": "overview", "type": "line", "source-layer": "overview", + "filter": [ + "all", + [ + ">=", + "captured_at", + 0 + ], + [ + "<=", + "captured_at", + 0 + ] + ], "maxzoom": 13, "paint": { "line-opacity": 1, @@ -33,6 +46,19 @@ "id": "sequence", "type": "line", "source-layer": "sequence", + "filter": [ + "all", + [ + ">=", + "captured_at", + 0 + ], + [ + "<=", + "captured_at", + 0 + ] + ], "minzoom": 14, "paint": { "line-opacity": 1, @@ -60,6 +86,19 @@ "id": "image", "type": "symbol", "source-layer": "image", + "filter": [ + "all", + [ + ">=", + "captured_at", + 0 + ], + [ + "<=", + "captured_at", + 0 + ] + ], "minzoom": 19, "layout": { "icon-image": "arrow", @@ -92,7 +131,7 @@ "paint": { "icon-color": "rgba(200, 100, 0, 1)" } - } + } ], "id": "mapillary" } diff --git a/src/main/java/de/blau/android/DisambiguationMenu.java b/src/main/java/de/blau/android/DisambiguationMenu.java index a478bbe47a..63ffed4085 100644 --- a/src/main/java/de/blau/android/DisambiguationMenu.java +++ b/src/main/java/de/blau/android/DisambiguationMenu.java @@ -154,7 +154,7 @@ private Type typeFromObject(@NonNull ClickedObject clicked) { return Type.GPX; } if (object instanceof de.blau.android.util.mvt.VectorTileDecoder.Feature) { - return clicked.getLayer() instanceof de.blau.android.layer.mapillary.MapOverlay ? Type.MAPILLARY : Type.MVT; + return clicked.getLayer() instanceof de.blau.android.layer.mapillary.MapillaryOverlay ? Type.MAPILLARY : Type.MVT; } if (object instanceof Photo) { return Type.IMAGE; diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java index b08a471d80..1b6fdb7c7e 100644 --- a/src/main/java/de/blau/android/Main.java +++ b/src/main/java/de/blau/android/Main.java @@ -1044,15 +1044,15 @@ private void processIntents() { } break; case ACTION_MAPILLARY_SELECT: - final de.blau.android.layer.mapillary.MapOverlay mapillaryLayer = map != null - ? (de.blau.android.layer.mapillary.MapOverlay) map.getLayer(LayerType.MAPILLARY) + 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.MapOverlay.COORDINATES_KEY); + 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.MapOverlay.SET_POSITION_KEY, 0)); + mapillaryLayer.select(intent.getIntExtra(de.blau.android.layer.mapillary.MapillaryOverlay.SET_POSITION_KEY, 0)); } break; case ACTION_MAP_UPDATE: diff --git a/src/main/java/de/blau/android/Map.java b/src/main/java/de/blau/android/Map.java index 5348cbc311..e8fa680cd7 100755 --- a/src/main/java/de/blau/android/Map.java +++ b/src/main/java/de/blau/android/Map.java @@ -271,7 +271,7 @@ public void setUpLayers(@NonNull Context ctx) { } break; case MAPILLARY: - layer = new de.blau.android.layer.mapillary.MapOverlay(this); + layer = new de.blau.android.layer.mapillary.MapillaryOverlay(this); break; case BOOKMARKS: layer = new de.blau.android.layer.bookmarks.MapOverlay(this); diff --git a/src/main/java/de/blau/android/dialogs/DateRangeDialog.java b/src/main/java/de/blau/android/dialogs/DateRangeDialog.java new file mode 100644 index 0000000000..204491ff46 --- /dev/null +++ b/src/main/java/de/blau/android/dialogs/DateRangeDialog.java @@ -0,0 +1,152 @@ +package de.blau.android.dialogs; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import com.google.android.material.slider.RangeSlider; + +import de.blau.android.App; +import de.blau.android.R; +import de.blau.android.layer.DateRangeInterface; +import de.blau.android.listener.DoNothingListener; +import de.blau.android.util.ACRAHelper; +import de.blau.android.util.ImmersiveDialogFragment; +import de.blau.android.util.ThemeUtils; +import de.blau.android.views.layers.MapTilesLayer; + +/** + * Display a dialog allowing the user to change some properties of the current background + * + */ +public class DateRangeDialog extends ImmersiveDialogFragment { + + private static final String DEBUG_TAG = DateRangeDialog.class.getSimpleName(); + + private static final String TAG = "fragment_daterange"; + + public static final String LABEL_FORMAT = "yyyy MMM dd"; + + private static final String LAYERINDEX = "layer_index"; + private static final String START_DATE = "start_date"; + private static final String END_DATE = "end_date"; + + private static final long FROM_DATE = 1377990000000L; + + private SimpleDateFormat labelDate = new SimpleDateFormat(LABEL_FORMAT); + + /** + * Display a dialog allowing the user to change some properties of the current background + * + * @param activity the calling Activity + * @param layerIndex the index of the Layer + * @param endDate + * @param startDate + */ + public static void showDialog(@NonNull FragmentActivity activity, int layerIndex, long startDate, long endDate) { + dismissDialog(activity); + try { + FragmentManager fm = activity.getSupportFragmentManager(); + DateRangeDialog backgroundPropertiesFragment = newInstance(layerIndex, startDate, endDate); + backgroundPropertiesFragment.show(fm, TAG); + } catch (IllegalStateException isex) { + Log.e(DEBUG_TAG, "showDialog", isex); + } + } + + /** + * Dismiss the dialog + * + * @param activity the calling Activity + */ + private static void dismissDialog(@NonNull FragmentActivity activity) { + de.blau.android.dialogs.Util.dismissDialog(activity, TAG); + } + + /** + * Get a new DateRange dialog instance + * + * @param layerIndex the index of the Layer + * @param startDate the start date in ms since the epoch + * @param endDate the end date in ms since the epoch + * @return a DateRange instance + */ + @NonNull + private static DateRangeDialog newInstance(int layerIndex, long startDate, long endDate) { + DateRangeDialog f = new DateRangeDialog(); + Bundle args = new Bundle(); + args.putInt(LAYERINDEX, layerIndex); + args.putLong(START_DATE, startDate); + args.putLong(END_DATE, endDate); + f.setArguments(args); + f.setShowsDialog(true); + + return f; + } + + @NonNull + @SuppressLint("InflateParams") + @Override + public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { + Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.date_range_title); + final LayoutInflater inflater = ThemeUtils.getLayoutInflater(getActivity()); + DoNothingListener doNothingListener = new DoNothingListener(); + View layout = inflater.inflate(R.layout.daterange, null); + RangeSlider slider = (RangeSlider) layout.findViewById(R.id.range_slider); + slider.setLabelFormatter((float v) -> labelDate.format(new Date(fromDays(v)))); + MapTilesLayer layer = (MapTilesLayer) App.getLogic().getMap().getLayer(getArguments().getInt(LAYERINDEX, -1)); + if (layer instanceof DateRangeInterface) { + long today = new Date().getTime(); + slider.setValues(toDays(Math.max(FROM_DATE, getArguments().getLong(START_DATE, -1L))), + toDays(Math.min(getArguments().getLong(END_DATE, today), today))); + slider.setValueTo(toDays(today)); + slider.addOnChangeListener((RangeSlider s, float arg1, boolean arg2) -> { + final List values = s.getValues(); + if (values != null && values.size() == 2) { + ((DateRangeInterface) layer).setDateRange(fromDays(values.get(0)), fromDays(values.get(1))); + } + layer.invalidate(); + }); + } else { + ACRAHelper.nocrashReport(null, "layer null or doesn't implement DateRangeInterface"); + } + builder.setView(layout); + builder.setPositiveButton(R.string.okay, doNothingListener); + + return builder.create(); + } + + /** + * Convert from days to ms + * + * @param days the number of days + * @return ms + */ + private static long fromDays(float days) { + return (long) (days * 24 * 3600000L); + } + + /** + * Convert from ms to days + * + * @param ms the number of ms + * @return the number of days + */ + private static float toDays(long ms) { + return ms / (24f * 3600000L); + } +} diff --git a/src/main/java/de/blau/android/dialogs/Layers.java b/src/main/java/de/blau/android/dialogs/Layers.java index e0eb1a0dfa..a4f9e0c6ba 100644 --- a/src/main/java/de/blau/android/dialogs/Layers.java +++ b/src/main/java/de/blau/android/dialogs/Layers.java @@ -235,7 +235,7 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { if (map.getLayer(LayerType.MAPILLARY) == null) { try (KeyDatabaseHelper keys = new KeyDatabaseHelper(activity); SQLiteDatabase db = keys.getReadableDatabase()) { - if (KeyDatabaseHelper.getKey(db, de.blau.android.layer.mapillary.MapOverlay.APIKEY_KEY, EntryType.API_KEY) != null) { + if (KeyDatabaseHelper.getKey(db, de.blau.android.layer.mapillary.MapillaryOverlay.APIKEY_KEY, EntryType.API_KEY) != null) { item = popup.getMenu().add(R.string.menu_layers_enable_mapillary_layer); item.setOnMenuItemClickListener(unused -> { de.blau.android.layer.Util.addLayer(activity, LayerType.MAPILLARY); @@ -698,7 +698,7 @@ public void onClick(View arg0) { final Map map = App.getLogic().getMap(); // maybe we should use an interface here - if (layer instanceof MapTilesLayer && !(layer instanceof de.blau.android.layer.mapillary.MapOverlay)) { + if (layer instanceof MapTilesLayer && !(layer instanceof de.blau.android.layer.mapillary.MapillaryOverlay)) { // get MRU list from layer final String[] tileServerIds = ((MapTilesLayer) layer).getMRU(); final TileLayerSource tileLayerConfiguration = ((MapTilesLayer) layer).getTileLayerConfiguration(); @@ -800,6 +800,16 @@ public void onClick(View arg0) { item.setEnabled(stylingEnabled); } + if (layer instanceof de.blau.android.layer.mapillary.MapillaryOverlay) { + MenuItem item = menu.add(R.string.layer_set_date_range); + item.setOnMenuItemClickListener(unused -> { + if (layer != null) { + ((de.blau.android.layer.mapillary.MapillaryOverlay) layer).selectDateRange(getActivity(), layer.getIndex()); + } + return true; + }); + } + if (layer instanceof de.blau.android.layer.mvt.MapOverlay) { MenuItem item = menu.add(R.string.layer_load_style); item.setOnMenuItemClickListener(unused -> { diff --git a/src/main/java/de/blau/android/layer/DateRangeInterface.java b/src/main/java/de/blau/android/layer/DateRangeInterface.java new file mode 100644 index 0000000000..603440d309 --- /dev/null +++ b/src/main/java/de/blau/android/layer/DateRangeInterface.java @@ -0,0 +1,11 @@ +package de.blau.android.layer; + +public interface DateRangeInterface { + /** + * Set a date range to display + * + * @param start the lower bound for the capture date in ms since the epoch + * @param end the upper bound for the capture date in ms since the epoch + */ + public void setDateRange(long start, long end); +} 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 ca05ee8f2e..f9511bc482 100644 --- a/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java +++ b/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java @@ -212,10 +212,10 @@ 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(MapOverlay.SET_POSITION_KEY, index); + intent.putExtra(MapillaryOverlay.SET_POSITION_KEY, index); String key = ids.get(index); if (key != null && coordinates.containsKey(key)) { - intent.putExtra(MapOverlay.COORDINATES_KEY, coordinates.get(key)); + intent.putExtra(MapillaryOverlay.COORDINATES_KEY, coordinates.get(key)); } context.startActivity(intent); } diff --git a/src/main/java/de/blau/android/layer/mapillary/MapOverlay.java b/src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java similarity index 81% rename from src/main/java/de/blau/android/layer/mapillary/MapOverlay.java rename to src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java index c404969555..0b12cacf60 100644 --- a/src/main/java/de/blau/android/layer/mapillary/MapOverlay.java +++ b/src/main/java/de/blau/android/layer/mapillary/MapillaryOverlay.java @@ -6,6 +6,7 @@ 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; @@ -31,6 +32,8 @@ import de.blau.android.R; import de.blau.android.contract.FileExtensions; import de.blau.android.contract.Urls; +import de.blau.android.dialogs.DateRangeDialog; +import de.blau.android.layer.DateRangeInterface; import de.blau.android.layer.LayerType; import de.blau.android.osm.OsmParser; import de.blau.android.osm.ViewBox; @@ -58,9 +61,9 @@ import okhttp3.Response; import okhttp3.ResponseBody; -public class MapOverlay extends de.blau.android.layer.mvt.MapOverlay { +public class MapillaryOverlay extends de.blau.android.layer.mvt.MapOverlay implements DateRangeInterface { - private static final String DEBUG_TAG = MapOverlay.class.getSimpleName(); + private static final String DEBUG_TAG = MapillaryOverlay.class.getSimpleName(); public static final String MAPILLARY_TILES_ID = "MAPILLARYV4"; public static final int MAPILLARY_DEFAULT_MIN_ZOOM = 16; @@ -70,6 +73,8 @@ public class MapOverlay extends de.blau.android.layer.mvt.MapOverlay { // mapbox gl style layer ids private static final String SELECTED_IMAGE_LAYER = "selected_image"; private static final String IMAGE_LAYER = "image"; + private static final String OVERVIEW_LAYER = "overview"; + private static final String SEQUENCE_LAYER = "sequence"; // mapillary API constants private static final String CAPTURED_AT_KEY = "captured_at"; @@ -88,16 +93,18 @@ public class MapOverlay extends de.blau.android.layer.mvt.MapOverlay { public static final String SET_POSITION_KEY = "set_position"; public static final String COORDINATES_KEY = "coordinates"; - static class Selected implements Serializable { - private static final long serialVersionUID = 2L; + static class State implements Serializable { + private static final long serialVersionUID = 4L; private String sequenceId = null; private long imageId = 0; private final java.util.Map> sequenceCache = new HashMap<>(); + private long startDate = 0L; + private long endDate = new Date().getTime(); } - private Selected selected = new Selected(); - private final SavingHelper savingHelper = new SavingHelper<>(); + private State state = new State(); + private final SavingHelper savingHelper = new SavingHelper<>(); private final String apiKey; @@ -121,7 +128,7 @@ static class Selected implements Serializable { * * @param map the Map object we are displayed on */ - public MapOverlay(@NonNull final Map map) { + public MapillaryOverlay(@NonNull final Map map) { super(map, new VectorTileRenderer(), false); this.setRendererInfo(TileLayerSource.get(map.getContext(), MAPILLARY_TILES_ID, false)); this.map = map; @@ -141,6 +148,7 @@ public MapOverlay(@NonNull final Map map) { setPrefs(map.getPrefs()); resetStyling(); + setDateRange(state.startDate, state.endDate); } @Override @@ -153,16 +161,17 @@ public void onDraw(@NonNull Canvas c, @NonNull IMapView osmv) { @Override public void onSaveState(@NonNull Context ctx) throws IOException { super.onSaveState(ctx); - savingHelper.save(ctx, FILENAME, selected, false); + savingHelper.save(ctx, FILENAME, state, false); } @Override public boolean onRestoreState(@NonNull Context ctx) { boolean result = super.onRestoreState(ctx); - if (selected == null) { - selected = savingHelper.load(ctx, FILENAME, true); - if (selected != null) { - setSelected(selected.imageId); + if (state == null) { + state = savingHelper.load(ctx, FILENAME, true); + if (state != null) { + setSelected(state.imageId); + setDateRange(state.startDate, state.endDate); } } return result; @@ -206,7 +215,7 @@ public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.Vecto String sequenceId = (String) attributes.get(SEQUENCE_ID_KEY); Long id = (Long) attributes.get(ID_KEY); if (id != null && sequenceId != null) { - ArrayList keys = selected != null ? selected.sequenceCache.get(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), "Mapillary Sequence"); @@ -219,7 +228,7 @@ public void onSelected(FragmentActivity activity, de.blau.android.util.mvt.Vecto showImages(activity, id, keys); } setSelected(f); - selected.sequenceId = sequenceId; + state.sequenceId = sequenceId; } } } @@ -314,10 +323,10 @@ public void run() { } } } - if (selected == null) { - selected = new Selected(); + if (state == null) { + state = new State(); } - selected.sequenceCache.put(sequenceId, ids); + state.sequenceCache.put(sequenceId, ids); showImages(activity, id, ids); } } @@ -332,7 +341,7 @@ public void run() { @Override public void deselectObjects() { - selected = null; + state = null; setSelected(0); dirty(); } @@ -358,10 +367,10 @@ void setSelected(long id) { } } if (selectedFilter != null && selectedFilter.size() == 3) { - if (selected == null) { - selected = new Selected(); + if (state == null) { + state = new State(); } - selected.imageId = id; + state.imageId = id; selectedFilter.set(2, new JsonPrimitive(id)); map.invalidate(); dirty(); @@ -374,8 +383,8 @@ void setSelected(long id) { * @param pos the position in the sequence */ public synchronized void select(int pos) { - if (selected != null && selected.sequenceId != null) { - List ids = selected.sequenceCache.get(selected.sequenceId); + if (state != null && state.sequenceId != null) { + List ids = state.sequenceCache.get(state.sequenceId); if (ids != null) { String idStr = ids.get(pos); if (idStr != null) { @@ -384,7 +393,7 @@ public synchronized void select(int pos) { return; } } - Log.e(DEBUG_TAG, "position " + pos + " not found in sequence " + selected.sequenceId); + Log.e(DEBUG_TAG, "position " + pos + " not found in sequence " + state.sequenceId); } } @@ -403,17 +412,57 @@ public synchronized void flushCaches(@NonNull Context ctx) { } } + @Override + public void setDateRange(long start, long end) { + Style style = ((VectorTileRenderer) tileRenderer).getStyle(); + setDateRange(style.getLayer(IMAGE_LAYER), start, end); + setDateRange(style.getLayer(SEQUENCE_LAYER), start, end); + setDateRange(style.getLayer(OVERVIEW_LAYER), start, end); + map.invalidate(); + dirty(); + } + + /** + * Set a range for the capture date + * + * This manipulates the filter in the layer + * + * @param start the lower bound for the capture date in ms since the epoch + * @param end the upper bound for the capture date in ms since the epoch + */ + private void setDateRange(Layer layer, long start, long end) { + state.startDate = start; + state.endDate = end; + JsonArray filter = layer.getFilter(); + if (filter != null && filter.size() == 3) { + setFilterValue(filter.get(1), start); + setFilterValue(filter.get(2), end); + } + } + + /** + * Set the value of a filter + * + * @param filter a JsonArray representing a filter + * @param value the value to set + */ + private void setFilterValue(JsonElement filter, long value) { + if (filter instanceof JsonArray && ((JsonArray) filter).size() == 3) { + ((JsonArray) filter).set(2, new JsonPrimitive(value)); + } + } + /** * Flush the sequence cache * * @param ctx an Android Context */ private void flushSequenceCache(@NonNull Context ctx) { - if (selected != null) { - selected.imageId = 0; - selected.sequenceCache.clear(); - selected.sequenceId = null; - savingHelper.save(ctx, FILENAME, selected, false); + if (state != null) { + state.imageId = 0; + state.sequenceCache.clear(); + state.sequenceId = null; + savingHelper.save(ctx, FILENAME, state, false); } } @@ -477,4 +526,14 @@ public LayerType getType() { public int onDrawAttribution(@NonNull Canvas c, @NonNull IMapView osmv, int offset) { return offset; } + + /** + * Show a dialog to set the displayed date range + * + * @param activity the activity we are being shown on + * @param layerIndex the index of this layer + */ + public void selectDateRange(@NonNull FragmentActivity activity, int layerIndex) { + DateRangeDialog.showDialog(activity, layerIndex, state.startDate, state.endDate); + } } \ No newline at end of file diff --git a/src/main/java/de/blau/android/prefs/Preferences.java b/src/main/java/de/blau/android/prefs/Preferences.java index 70b7b1a2ad..5228cde57a 100755 --- a/src/main/java/de/blau/android/prefs/Preferences.java +++ b/src/main/java/de/blau/android/prefs/Preferences.java @@ -169,7 +169,7 @@ public Preferences(@NonNull Context ctx) { tileCacheSize = getIntPref(R.string.config_tileCacheSize_key, 100); preferRemovableStorage = prefs.getBoolean(r.getString(R.string.config_preferRemovableStorage_key), true); - mapillaryCacheSize = getIntPref(R.string.config_mapillaryCacheSize_key, de.blau.android.layer.mapillary.MapOverlay.MAPILLARY_DEFAULT_CACHE_SIZE); + mapillaryCacheSize = getIntPref(R.string.config_mapillaryCacheSize_key, de.blau.android.layer.mapillary.MapillaryOverlay.MAPILLARY_DEFAULT_CACHE_SIZE); downloadRadius = getIntPref(R.string.config_extTriggeredDownloadRadius_key, 50); maxDownloadSpeed = getIntPref(R.string.config_maxDownloadSpeed_key, 10); @@ -225,7 +225,7 @@ public Preferences(@NonNull Context ctx) { mapillarySequencesUrlV4 = prefs.getString(r.getString(R.string.config_mapillarySequencesUrlV4_key), Urls.DEFAULT_MAPILLARY_SEQUENCES_URL_V4); mapillaryImagesUrlV4 = prefs.getString(r.getString(R.string.config_mapillaryImagesUrlV4_key), Urls.DEFAULT_MAPILLARY_IMAGES_V4); - mapillaryMinZoom = getIntPref(R.string.config_mapillary_min_zoom_key, de.blau.android.layer.mapillary.MapOverlay.MAPILLARY_DEFAULT_MIN_ZOOM); + mapillaryMinZoom = getIntPref(R.string.config_mapillary_min_zoom_key, de.blau.android.layer.mapillary.MapillaryOverlay.MAPILLARY_DEFAULT_MIN_ZOOM); showCameraAction = prefs.getBoolean(r.getString(R.string.config_showCameraAction_key), true); cameraApp = prefs.getString(r.getString(R.string.config_selectCameraApp_key), ""); diff --git a/src/main/res/layout/daterange.xml b/src/main/res/layout/daterange.xml new file mode 100644 index 0000000000..1c895faa7c --- /dev/null +++ b/src/main/res/layout/daterange.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml new file mode 100644 index 0000000000..3a9868f4cc --- /dev/null +++ b/src/main/res/values/arrays.xml @@ -0,0 +1,7 @@ + + + + 15949 + 20453 + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e188ba7035..17bdfa30af 100755 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -400,6 +400,7 @@ Change style… Load style… Reset style + Set date range… Select imagery… Test… All @@ -436,6 +437,8 @@ Meta data not loaded %1$s + + Set date range Delete photo Delete permanently diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index f15dfabc2e..3c0cec38a3 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -37,6 +37,29 @@ @color/material_red + + + + + + + + diff --git a/src/test/java/de/blau/android/MapTest.java b/src/test/java/de/blau/android/MapTest.java index 4105e02075..ed1bc87c88 100644 --- a/src/test/java/de/blau/android/MapTest.java +++ b/src/test/java/de/blau/android/MapTest.java @@ -69,7 +69,7 @@ public void imageryNamesTest() { assertNotNull(mapnik); assertEquals(mapnik.getName(), map.getImageryNames().get(0)); TileLayerSource mapillary = TileLayerSource.get(ApplicationProvider.getApplicationContext(), - de.blau.android.layer.mapillary.MapOverlay.MAPILLARY_TILES_ID, false); + de.blau.android.layer.mapillary.MapillaryOverlay.MAPILLARY_TILES_ID, false); assertNotNull(mapillary); de.blau.android.layer.Util.addLayer(ApplicationProvider.getApplicationContext(), LayerType.MAPILLARY); map.setUpLayers(ApplicationProvider.getApplicationContext()); From 5bfcdf4702cfb8ab399480868102cc1ad0bf77c3 Mon Sep 17 00:00:00 2001 From: simonpoole Date: Wed, 25 Oct 2023 11:19:24 +0200 Subject: [PATCH 2/2] Update doc --- documentation/docs/help/en/Main map display.md | 5 +++++ src/main/assets/help/en/Main map display.html | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/documentation/docs/help/en/Main map display.md b/documentation/docs/help/en/Main map display.md index c92c4912a5..db3514c6d8 100644 --- a/documentation/docs/help/en/Main map display.md +++ b/documentation/docs/help/en/Main map display.md @@ -59,6 +59,8 @@ The layer dialog supports the following actions on the layer entries: * __Change style__ Show the layer styling dialog (disabled if a style has been loaded). * __Load style__ Load a mapbox-gl style. * __Reset style__ Reset the style to the default. + * Mapillary layer (additionally to _Mapbox Vector Tile layers_): + * __Set date range ...__ Select start and end date to filter Mapillary data on. Note that this functionality depends on some specifics of the loaded Mapbox GL style. * GeoJSON layers: * __Change style__ Show the layer styling dialog. * __Info__ Display some information on the contents. @@ -91,6 +93,9 @@ The layer dialog supports the following actions on the layer entries: * __Add GeoJSON layer__ Loads a GeoJSON layer from a file in to a new GeoJSON layer. * __Add background imagery layer__ Adds a tile based imagery layer from the internal configuration, which can be from ELI or JOSM, or a custom imagery layer. * __Add overlay imagery layer__ As above but assumes that the layer is partially transparent. + * __Enable photo layer__ Enables the photo layer this will display clickable icons for photos that will start an internal or external viewer. Which photos can be displayed depends strongly on your Android version and settings [Advanced preferences](Advanced%20preferences.md). + * __Enable bookmark layer__ Enables a layer displaying saved bookmarks. + * __Enable Mapillary layer__ Enables the Mapillay layer. * __Add layer from GPX file__ Adds a layer from a GPX file on device. * __Download GPX track__ Download a GPX [track that you have previously uploaded to the OSM website](https://www.openstreetmap.org/traces/mine), and create a layer from it. Note that only GPX tracks with their starting point in the area currently displayed will be available for selection, this is a limitation of the current OSM API. * __Add custom imagery__ Adds a custom imagery configuration, this can then be used just diff --git a/src/main/assets/help/en/Main map display.html b/src/main/assets/help/en/Main map display.html index 211bfd0291..2dddde92eb 100644 --- a/src/main/assets/help/en/Main map display.html +++ b/src/main/assets/help/en/Main map display.html @@ -59,6 +59,11 @@

Layer control

  • Reset style Reset the style to the default.
  • +
  • Mapillary layer (additionally to Mapbox Vector Tile layers): +
      +
    • Set date range ... Select start and end date to filter Mapillary data on. Note that this functionality depends on some specifics of the loaded Mapbox GL style.
    • +
    +
  • GeoJSON layers:
    • Change style Show the layer styling dialog.
    • @@ -111,6 +116,9 @@

      Layer control

    • Add GeoJSON layer Loads a GeoJSON layer from a file in to a new GeoJSON layer.
    • Add background imagery layer Adds a tile based imagery layer from the internal configuration, which can be from ELI or JOSM, or a custom imagery layer.
    • Add overlay imagery layer As above but assumes that the layer is partially transparent.
    • +
    • Enable photo layer Enables the photo layer this will display clickable icons for photos that will start an internal or external viewer. Which photos can be displayed depends strongly on your Android version and settings Advanced preferences.
    • +
    • Enable bookmark layer Enables a layer displaying saved bookmarks.
    • +
    • Enable Mapillary layer Enables the Mapillay layer.
    • Add layer from GPX file Adds a layer from a GPX file on device.
    • Download GPX track Download a GPX track that you have previously uploaded to the OSM website, and create a layer from it. Note that only GPX tracks with their starting point in the area currently displayed will be available for selection, this is a limitation of the current OSM API.
    • Add custom imagery Adds a custom imagery configuration, this can then be used just as any tile based imagery source, the entries can be managed in the preferences.