mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Nearby: Fix race condition and lag when loading pin details, faster overlay management (#6047)
* temporary fixes part one * temporary fixes part two * temporary fixes part three * temporary fixes part four * temporary fixes part five * reformatting * remove code no longer in use * Migrate NearbyParentFragmentPresenter to Kotlin * Partially replace temporary experimental fixes * Replace temporary experimental fixes part 2 * Replace temporary experimental fixes part 3 * Replace temporary fixes completely * Fix caching and loading places in Nearby list * Add place bookmarking logic, Remove all old code * Nearby Presenter: Close channel properly * Nearby pins now load starting from the center Fixes #6049 * Add comments and javadoc for Nearby Presenter * Fix warnings, Fix formatting, Add javadoc * Pass unit tests
This commit is contained in:
parent
70b4f78a5d
commit
c891c2b0df
6 changed files with 676 additions and 679 deletions
|
|
@ -20,8 +20,8 @@ import timber.log.Timber;
|
||||||
|
|
||||||
public class NearbyController extends MapController {
|
public class NearbyController extends MapController {
|
||||||
|
|
||||||
private static final int MAX_RESULTS = 1000;
|
|
||||||
private final NearbyPlaces nearbyPlaces;
|
private final NearbyPlaces nearbyPlaces;
|
||||||
|
public static final int MAX_RESULTS = 1000;
|
||||||
public static double currentLocationSearchRadius = 10.0; //in kilometers
|
public static double currentLocationSearchRadius = 10.0; //in kilometers
|
||||||
public static LatLng currentLocation; // Users latest fetched location
|
public static LatLng currentLocation; // Users latest fetched location
|
||||||
public static LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
public static LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ package fr.free.nrw.commons.nearby.contract;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
||||||
import fr.free.nrw.commons.nearby.Label;
|
import fr.free.nrw.commons.nearby.Label;
|
||||||
|
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
import fr.free.nrw.commons.nearby.Place;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -68,7 +70,7 @@ public interface NearbyParentFragmentContract {
|
||||||
|
|
||||||
Context getContext();
|
Context getContext();
|
||||||
|
|
||||||
void updateMapMarkers(List<BaseMarker> BaseMarkers);
|
void replaceMarkerOverlays(List<MarkerPlaceGroup> markerPlaceGroups);
|
||||||
|
|
||||||
void filterOutAllMarkers();
|
void filterOutAllMarkers();
|
||||||
|
|
||||||
|
|
@ -127,5 +129,7 @@ public interface NearbyParentFragmentContract {
|
||||||
void setCheckboxUnknown();
|
void setCheckboxUnknown();
|
||||||
|
|
||||||
void setAdvancedQuery(String query);
|
void setAdvancedQuery(String query);
|
||||||
|
|
||||||
|
void toggleBookmarkedStatus(Place place);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import android.graphics.drawable.Drawable;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
@ -56,6 +54,7 @@ import androidx.appcompat.app.AlertDialog.Builder;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.lifecycle.LifecycleOwnerKt;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
@ -110,16 +109,12 @@ import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -155,6 +150,29 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
FragmentNearbyParentBinding binding;
|
FragmentNearbyParentBinding binding;
|
||||||
|
|
||||||
|
public final MapEventsOverlay mapEventsOverlay = new MapEventsOverlay(new MapEventsReceiver() {
|
||||||
|
@Override
|
||||||
|
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
||||||
|
if (clickedMarker != null) {
|
||||||
|
clickedMarker.closeInfoWindow();
|
||||||
|
} else {
|
||||||
|
Timber.e("CLICKED MARKER IS NULL");
|
||||||
|
}
|
||||||
|
if (isListBottomSheetExpanded()) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
hideBottomSheet();
|
||||||
|
} else if (isDetailsBottomSheetVisible()) {
|
||||||
|
hideBottomDetailsSheet();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean longPressHelper(GeoPoint p) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
LocationServiceManager locationManager;
|
LocationServiceManager locationManager;
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -217,7 +235,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
private Runnable searchRunnable;
|
private Runnable searchRunnable;
|
||||||
private static final long SCROLL_DELAY = 800; // Delay for debounce of onscroll, in milliseconds.
|
private static final long SCROLL_DELAY = 800; // Delay for debounce of onscroll, in milliseconds.
|
||||||
|
|
||||||
private List<Place> updatedPlacesList;
|
|
||||||
private LatLng updatedLatLng;
|
private LatLng updatedLatLng;
|
||||||
private boolean searchable;
|
private boolean searchable;
|
||||||
|
|
||||||
|
|
@ -308,10 +325,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
* WLM URL
|
* WLM URL
|
||||||
*/
|
*/
|
||||||
public static final String WLM_URL = "https://commons.wikimedia.org/wiki/Commons:Mobile_app/Contributing_to_WLM_using_the_app";
|
public static final String WLM_URL = "https://commons.wikimedia.org/wiki/Commons:Mobile_app/Contributing_to_WLM_using_the_app";
|
||||||
/**
|
|
||||||
* Saves response of list of places for the first time
|
|
||||||
*/
|
|
||||||
private List<Place> places = new ArrayList<>();
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static NearbyParentFragment newInstance() {
|
public static NearbyParentFragment newInstance() {
|
||||||
|
|
@ -327,7 +340,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
view = binding.getRoot();
|
view = binding.getRoot();
|
||||||
|
|
||||||
initNetworkBroadCastReceiver();
|
initNetworkBroadCastReceiver();
|
||||||
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao);
|
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController);
|
||||||
progressDialog = new ProgressDialog(getActivity());
|
progressDialog = new ProgressDialog(getActivity());
|
||||||
progressDialog.setCancelable(false);
|
progressDialog.setCancelable(false);
|
||||||
progressDialog.setMessage("Saving in progress...");
|
progressDialog.setMessage("Saving in progress...");
|
||||||
|
|
@ -452,28 +465,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding.map.getOverlays().add(scaleBarOverlay);
|
binding.map.getOverlays().add(scaleBarOverlay);
|
||||||
binding.map.getZoomController().setVisibility(Visibility.NEVER);
|
binding.map.getZoomController().setVisibility(Visibility.NEVER);
|
||||||
binding.map.getController().setZoom(ZOOM_LEVEL);
|
binding.map.getController().setZoom(ZOOM_LEVEL);
|
||||||
binding.map.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
binding.map.getOverlays().add(mapEventsOverlay);
|
||||||
@Override
|
|
||||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
|
||||||
if (clickedMarker != null) {
|
|
||||||
clickedMarker.closeInfoWindow();
|
|
||||||
} else {
|
|
||||||
Timber.e("CLICKED MARKER IS NULL");
|
|
||||||
}
|
|
||||||
if (isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
hideBottomSheet();
|
|
||||||
} else if (isDetailsBottomSheetVisible()) {
|
|
||||||
hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean longPressHelper(GeoPoint p) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
binding.map.addMapListener(new MapListener() {
|
binding.map.addMapListener(new MapListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -605,8 +597,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
},
|
},
|
||||||
(place, isBookmarked) -> {
|
(place, isBookmarked) -> {
|
||||||
updateMarker(isBookmarked, place, null);
|
presenter.toggleBookmarkedStatus(place);
|
||||||
binding.map.invalidate();
|
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
},
|
},
|
||||||
commonPlaceClickActions,
|
commonPlaceClickActions,
|
||||||
|
|
@ -670,19 +661,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
registerNetworkReceiver();
|
registerNetworkReceiver();
|
||||||
if (isResumed() && ((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) {
|
if (isResumed() && ((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) {
|
||||||
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
||||||
if (lastFocusLocation == null && lastKnownLocation == null) {
|
locationPermissionGranted();
|
||||||
locationPermissionGranted();
|
|
||||||
} else{
|
|
||||||
if (updatedPlacesList != null) {
|
|
||||||
if (!updatedPlacesList.isEmpty()) {
|
|
||||||
loadPlacesDataAsync(updatedPlacesList, updatedLatLng);
|
|
||||||
} else {
|
|
||||||
updateMapMarkers(updatedPlacesList, getLastMapFocus(), false);
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
locationPermissionGranted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
startMapWithoutPermission();
|
startMapWithoutPermission();
|
||||||
}
|
}
|
||||||
|
|
@ -973,7 +952,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateListFragment(final List<Place> placeList) {
|
public void updateListFragment(final List<Place> placeList) {
|
||||||
places = placeList;
|
adapter.clear();
|
||||||
adapter.setItems(placeList);
|
adapter.setItems(placeList);
|
||||||
binding.bottomSheetNearby.noResultsMessage.setVisibility(
|
binding.bottomSheetNearby.noResultsMessage.setVisibility(
|
||||||
placeList.isEmpty() ? View.VISIBLE : View.GONE);
|
placeList.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
|
|
@ -1367,13 +1346,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
? getTextBetweenParentheses(
|
? getTextBetweenParentheses(
|
||||||
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
|
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
|
||||||
marker.showInfoWindow();
|
marker.showInfoWindow();
|
||||||
for (int i = 0; i < updatedPlacesList.size(); i++) {
|
presenter.handlePinClicked(updatedPlace);
|
||||||
Place pl = updatedPlacesList.get(i);
|
savePlaceToDatabase(place);
|
||||||
if (pl.location == updatedPlace.location) {
|
|
||||||
updatedPlacesList.set(i, updatedPlace);
|
|
||||||
savePlaceToDatabase(place);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Drawable icon = ContextCompat.getDrawable(getContext(),
|
Drawable icon = ContextCompat.getDrawable(getContext(),
|
||||||
getIconFor(updatedPlace, isBookMarked));
|
getIconFor(updatedPlace, isBookMarked));
|
||||||
marker.setIcon(icon);
|
marker.setIcon(icon);
|
||||||
|
|
@ -1412,12 +1386,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
setProgressBarVisibility(false);
|
setProgressBarVisibility(false);
|
||||||
presenter.lockUnlockNearby(false);
|
presenter.lockUnlockNearby(false);
|
||||||
} else {
|
} else {
|
||||||
updateMapMarkers(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng,
|
updateMapMarkers(nearbyPlacesInfo.placeList, searchLatLng, true);
|
||||||
true);
|
|
||||||
lastFocusLocation = searchLatLng;
|
lastFocusLocation = searchLatLng;
|
||||||
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
||||||
searchLatLng.getLongitude());
|
searchLatLng.getLongitude());
|
||||||
loadPlacesDataAsync(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> {
|
throwable -> {
|
||||||
|
|
@ -1457,12 +1429,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
|
|
||||||
// curLatLng is used to calculate distance from the current location to the place
|
// curLatLng is used to calculate distance from the current location to the place
|
||||||
// and distance is later on populated to the place
|
// and distance is later on populated to the place
|
||||||
updateMapMarkers(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng,
|
updateMapMarkers(nearbyPlacesInfo.placeList, searchLatLng, false);
|
||||||
false);
|
|
||||||
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
|
||||||
searchLatLng.getLongitude());
|
searchLatLng.getLongitude());
|
||||||
stopQuery();
|
stopQuery();
|
||||||
loadPlacesDataAsync(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> {
|
throwable -> {
|
||||||
|
|
@ -1475,167 +1445,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadPlacesDataAsync(List<Place> placeList, LatLng curLatLng) {
|
public void savePlaceToDatabase(Place place) {
|
||||||
List<Place> places = new ArrayList<>(placeList);
|
|
||||||
|
|
||||||
// Instead of loading all pins in a single SPARQL query, we query in batches.
|
|
||||||
// This variable controls the number of pins queried per batch.
|
|
||||||
int batchSize = 3;
|
|
||||||
|
|
||||||
updatedLatLng = curLatLng;
|
|
||||||
updatedPlacesList = new ArrayList<>(placeList);
|
|
||||||
|
|
||||||
// Sorts the places by distance to ensure the nearest pins are ready for the user as soon
|
|
||||||
// as possible.
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.N) {
|
|
||||||
Collections.sort(places,
|
|
||||||
Comparator.comparingDouble(place -> place.getDistanceInDouble(getMapFocus())));
|
|
||||||
}
|
|
||||||
stopQuery = false;
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlacesList, curLatLng, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a list of places in batches sequentially. This method handles the asynchronous
|
|
||||||
* processing of places, updating the map markers and updates the list of updated places accordingly.
|
|
||||||
*
|
|
||||||
* @param places The list of Place objects to be processed.
|
|
||||||
* @param batchSize The size of each batch to be processed.
|
|
||||||
* @param updatedPlaceList The list of Place objects to be updated.
|
|
||||||
* @param curLatLng The current location of the user.
|
|
||||||
* @param startIndex The starting index for the current batch.
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private void processBatchesSequentially(List<Place> places, int batchSize,
|
|
||||||
List<Place> updatedPlaceList, LatLng curLatLng, int startIndex) {
|
|
||||||
if (startIndex >= places.size() || stopQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int endIndex = Math.min(startIndex + batchSize, places.size());
|
|
||||||
List<Place> batch = places.subList(startIndex, endIndex);
|
|
||||||
for (int i = 0; i < batch.size(); i++) {
|
|
||||||
if (i == batch.size() - 1 && batch.get(i).name != "") {
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlaceList, curLatLng,
|
|
||||||
endIndex + batchSize);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (batch.get(i).name == "") {
|
|
||||||
if (i == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlaceList, curLatLng,
|
|
||||||
endIndex + i);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Disposable disposable = processBatch(batch, updatedPlaceList)
|
|
||||||
.subscribe(p -> {
|
|
||||||
if (stopQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!p.isEmpty() && p != updatedPlaceList) {
|
|
||||||
synchronized (updatedPlaceList) {
|
|
||||||
updatedPlaceList.clear();
|
|
||||||
updatedPlaceList.addAll((Collection<? extends Place>) p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateMapMarkers(new ArrayList<>(updatedPlaceList), curLatLng, false);
|
|
||||||
processBatchesSequentially(places, batchSize, updatedPlaceList, curLatLng, endIndex);
|
|
||||||
}, throwable -> {
|
|
||||||
Timber.e(throwable);
|
|
||||||
showErrorMessage(getString(R.string.error_fetching_nearby_places) + throwable.getLocalizedMessage());
|
|
||||||
setFilterState();
|
|
||||||
});
|
|
||||||
|
|
||||||
compositeDisposable.add(disposable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a batch of places, updating the provided place list with fetched or updated data.
|
|
||||||
* This method handles the asynchronous fetching and updating of places from the repository.
|
|
||||||
*
|
|
||||||
* @param batch The batch of Place objects to be processed.
|
|
||||||
* @param placeList The list of Place objects to be updated.
|
|
||||||
* @return An Observable emitting the updated list of Place objects.
|
|
||||||
*/
|
|
||||||
private Observable<List<?>> processBatch(List<Place> batch, List<Place> placeList) {
|
|
||||||
List<Place> toBeProcessed = new ArrayList<>();
|
|
||||||
|
|
||||||
List<Observable<Place>> placeObservables = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Place place : batch) {
|
|
||||||
Observable<Place> placeObservable = Observable
|
|
||||||
.fromCallable(() -> {
|
|
||||||
Place fetchedPlace = placesRepository.fetchPlace(place.entityID);
|
|
||||||
return fetchedPlace != null ? fetchedPlace : place;
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext(placeData -> {
|
|
||||||
if (placeData.equals(place)) {
|
|
||||||
toBeProcessed.add(place);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < placeList.size(); i++) {
|
|
||||||
Place pl = placeList.get(i);
|
|
||||||
if (pl.location.equals(place.location)) {
|
|
||||||
placeList.set(i, placeData);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
placeObservables.add(placeObservable);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Observable.zip(placeObservables, objects -> toBeProcessed)
|
|
||||||
.flatMap(processedList -> {
|
|
||||||
if (processedList.isEmpty()) {
|
|
||||||
return Observable.just(placeList);
|
|
||||||
}
|
|
||||||
return Observable.fromCallable(() -> nearbyController.getPlaces(processedList))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.map(places -> {
|
|
||||||
if (stopQuery) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
if (places == null || places.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
} else {
|
|
||||||
List<Place> updatedPlaceList = new ArrayList<>(placeList);
|
|
||||||
for (Place place : places) {
|
|
||||||
for (Place foundPlace : placeList) {
|
|
||||||
if (place.siteLinks.getWikidataLink()
|
|
||||||
.equals(foundPlace.siteLinks.getWikidataLink())) {
|
|
||||||
place.location = foundPlace.location;
|
|
||||||
place.distance = foundPlace.distance;
|
|
||||||
place.setMonument(foundPlace.isMonument());
|
|
||||||
int index = updatedPlaceList.indexOf(foundPlace);
|
|
||||||
if (index != -1) {
|
|
||||||
updatedPlaceList.set(index, place);
|
|
||||||
savePlaceToDatabase(place);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updatedPlaceList;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onErrorReturn(throwable -> {
|
|
||||||
Timber.e(throwable);
|
|
||||||
showErrorMessage(getString(R.string.error_fetching_nearby_places) + " "
|
|
||||||
+ throwable.getLocalizedMessage());
|
|
||||||
setFilterState();
|
|
||||||
return Collections.emptyList();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void savePlaceToDatabase(Place place) {
|
|
||||||
compositeDisposable.add(placesRepository
|
compositeDisposable.add(placesRepository
|
||||||
.save(place)
|
.save(place)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
|
@ -1661,8 +1471,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
*/
|
*/
|
||||||
private void updateMapMarkers(final List<Place> nearbyPlaces, final LatLng curLatLng,
|
private void updateMapMarkers(final List<Place> nearbyPlaces, final LatLng curLatLng,
|
||||||
final boolean shouldUpdateSelectedMarker) {
|
final boolean shouldUpdateSelectedMarker) {
|
||||||
presenter.updateMapMarkers(nearbyPlaces, curLatLng, shouldUpdateSelectedMarker);
|
presenter.updateMapMarkers(nearbyPlaces, curLatLng,
|
||||||
setFilterState();
|
LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1899,13 +1709,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateMapMarkers(final List<BaseMarker> BaseMarkers) {
|
|
||||||
if (binding.map != null) {
|
|
||||||
presenter.updateMapMarkersToController(BaseMarkers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filterOutAllMarkers() {
|
public void filterOutAllMarkers() {
|
||||||
clearAllMarkers();
|
clearAllMarkers();
|
||||||
|
|
@ -1925,8 +1728,11 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
final boolean displayExists = false;
|
final boolean displayExists = false;
|
||||||
final boolean displayNeedsPhoto= false;
|
final boolean displayNeedsPhoto= false;
|
||||||
final boolean displayWlm = false;
|
final boolean displayWlm = false;
|
||||||
// Remove the previous markers before updating them
|
if (selectedLabels == null || selectedLabels.size() == 0) {
|
||||||
clearAllMarkers();
|
replaceMarkerOverlays(NearbyController.markerLabelList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ArrayList<MarkerPlaceGroup> placeGroupsToShow = new ArrayList<>();
|
||||||
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
|
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
|
||||||
final Place place = markerPlaceGroup.getPlace();
|
final Place place = markerPlaceGroup.getPlace();
|
||||||
// When label filter is engaged
|
// When label filter is engaged
|
||||||
|
|
@ -1967,19 +1773,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdateMarker) {
|
if (shouldUpdateMarker) {
|
||||||
updateMarker(markerPlaceGroup.getIsBookmarked(), place,
|
placeGroupsToShow.add(
|
||||||
NearbyController.currentLocation);
|
new MarkerPlaceGroup(markerPlaceGroup.getIsBookmarked(), place)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedLabels == null || selectedLabels.size() == 0) {
|
replaceMarkerOverlays(placeGroupsToShow);
|
||||||
ArrayList<BaseMarker> markerArrayList = new ArrayList<>();
|
|
||||||
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
|
|
||||||
BaseMarker nearbyBaseMarker = new BaseMarker();
|
|
||||||
nearbyBaseMarker.setPlace(markerPlaceGroup.getPlace());
|
|
||||||
markerArrayList.add(nearbyBaseMarker);
|
|
||||||
}
|
|
||||||
addMarkersToMap(markerArrayList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1987,18 +1786,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
return binding.map == null ? null : getMapFocus();
|
return binding.map == null ? null : getMapFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets marker icon according to marker status. Sets title and distance.
|
|
||||||
*
|
|
||||||
* @param isBookmarked true if place is bookmarked
|
|
||||||
* @param place
|
|
||||||
* @param currentLatLng current location
|
|
||||||
*/
|
|
||||||
public void updateMarker(final boolean isBookmarked, final Place place,
|
|
||||||
@Nullable final LatLng currentLatLng) {
|
|
||||||
addMarkerToMap(place, isBookmarked);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlights nearest place when user clicks on home nearby banner
|
* Highlights nearest place when user clicks on home nearby banner
|
||||||
*
|
*
|
||||||
|
|
@ -2052,13 +1839,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Marker convertToMarker(Place place, Boolean isBookMarked) {
|
||||||
* Adds a marker representing a place to the map with optional bookmark icon.
|
|
||||||
*
|
|
||||||
* @param place The Place object containing information about the location.
|
|
||||||
* @param isBookMarked A Boolean flag indicating whether the place is bookmarked or not.
|
|
||||||
*/
|
|
||||||
private void addMarkerToMap(Place place, Boolean isBookMarked) {
|
|
||||||
Drawable icon = ContextCompat.getDrawable(getContext(), getIconFor(place, isBookMarked));
|
Drawable icon = ContextCompat.getDrawable(getContext(), getIconFor(place, isBookMarked));
|
||||||
GeoPoint point = new GeoPoint(place.location.getLatitude(), place.location.getLongitude());
|
GeoPoint point = new GeoPoint(place.location.getLatitude(), place.location.getLongitude());
|
||||||
Marker marker = new Marker(binding.map);
|
Marker marker = new Marker(binding.map);
|
||||||
|
|
@ -2094,22 +1875,27 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
binding.map.getOverlays().add(marker);
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds multiple markers representing places to the map and handles item gestures.
|
* Adds multiple markers representing places to the map and handles item gestures.
|
||||||
*
|
*
|
||||||
* @param nearbyBaseMarkers The list of Place objects containing information about the
|
* @param markerPlaceGroups The list of marker place groups containing the places and
|
||||||
* locations.
|
* their bookmarked status
|
||||||
*/
|
*/
|
||||||
private void addMarkersToMap(List<BaseMarker> nearbyBaseMarkers) {
|
@Override
|
||||||
|
public void replaceMarkerOverlays(final List<MarkerPlaceGroup> markerPlaceGroups) {
|
||||||
for(int i = 0; i< nearbyBaseMarkers.size(); i++){
|
ArrayList<Marker> newMarkers = new ArrayList<>(markerPlaceGroups.size());
|
||||||
addMarkerToMap(nearbyBaseMarkers.get(i).getPlace(), false);
|
for (MarkerPlaceGroup markerPlaceGroup : markerPlaceGroups) {
|
||||||
|
newMarkers.add(
|
||||||
|
convertToMarker(markerPlaceGroup.getPlace(), markerPlaceGroup.getIsBookmarked()));
|
||||||
}
|
}
|
||||||
|
clearAllMarkers();
|
||||||
|
binding.map.getOverlays().addAll(newMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
|
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
|
||||||
* string.
|
* string.
|
||||||
|
|
@ -2400,7 +2186,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
binding.map.invalidate();
|
binding.map.invalidate();
|
||||||
GeoPoint geoPoint = mapCenter;
|
GeoPoint geoPoint = mapCenter;
|
||||||
if (geoPoint != null) {
|
if (geoPoint != null) {
|
||||||
List<Overlay> overlays = binding.map.getOverlays();
|
|
||||||
ScaleDiskOverlay diskOverlay =
|
ScaleDiskOverlay diskOverlay =
|
||||||
new ScaleDiskOverlay(this.getContext(),
|
new ScaleDiskOverlay(this.getContext(),
|
||||||
geoPoint, 2000, UnitOfMeasure.foot);
|
geoPoint, 2000, UnitOfMeasure.foot);
|
||||||
|
|
@ -2434,28 +2219,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
scaleBarOverlay.setBackgroundPaint(barPaint);
|
scaleBarOverlay.setBackgroundPaint(barPaint);
|
||||||
scaleBarOverlay.enableScaleBar();
|
scaleBarOverlay.enableScaleBar();
|
||||||
binding.map.getOverlays().add(scaleBarOverlay);
|
binding.map.getOverlays().add(scaleBarOverlay);
|
||||||
binding.map.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
|
binding.map.getOverlays().add(mapEventsOverlay);
|
||||||
@Override
|
|
||||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
|
||||||
if (clickedMarker != null) {
|
|
||||||
clickedMarker.closeInfoWindow();
|
|
||||||
} else {
|
|
||||||
Timber.e("CLICKED MARKER IS NULL");
|
|
||||||
}
|
|
||||||
if (isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
hideBottomSheet();
|
|
||||||
} else if (isDetailsBottomSheetVisible()) {
|
|
||||||
hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean longPressHelper(GeoPoint p) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
binding.map.setMultiTouchControls(true);
|
binding.map.setMultiTouchControls(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2510,21 +2274,14 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
@Override
|
@Override
|
||||||
public void onBottomSheetItemClick(@Nullable View view, int position) {
|
public void onBottomSheetItemClick(@Nullable View view, int position) {
|
||||||
BottomSheetItem item = dataList.get(position);
|
BottomSheetItem item = dataList.get(position);
|
||||||
boolean isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
|
|
||||||
switch (item.getImageResourceId()) {
|
switch (item.getImageResourceId()) {
|
||||||
case R.drawable.ic_round_star_border_24px:
|
case R.drawable.ic_round_star_border_24px:
|
||||||
bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
|
presenter.toggleBookmarkedStatus(selectedPlace);
|
||||||
updateBookmarkButtonImage(selectedPlace);
|
updateBookmarkButtonImage(selectedPlace);
|
||||||
isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
|
|
||||||
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
|
|
||||||
binding.map.invalidate();
|
|
||||||
break;
|
break;
|
||||||
case R.drawable.ic_round_star_filled_24px:
|
case R.drawable.ic_round_star_filled_24px:
|
||||||
bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
|
presenter.toggleBookmarkedStatus(selectedPlace);
|
||||||
updateBookmarkButtonImage(selectedPlace);
|
updateBookmarkButtonImage(selectedPlace);
|
||||||
isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
|
|
||||||
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
|
|
||||||
binding.map.invalidate();
|
|
||||||
break;
|
break;
|
||||||
case R.drawable.ic_directions_black_24dp:
|
case R.drawable.ic_directions_black_24dp:
|
||||||
Utils.handleGeoCoordinates(this.getContext(), selectedPlace.getLocation());
|
Utils.handleGeoCoordinates(this.getContext(), selectedPlace.getLocation());
|
||||||
|
|
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
package fr.free.nrw.commons.nearby.presenter;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.CUSTOM_QUERY;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
|
|
||||||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
|
|
||||||
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.CHECKED;
|
|
||||||
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNCHECKED;
|
|
||||||
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN;
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
|
||||||
|
|
||||||
import android.location.Location;
|
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
|
||||||
import fr.free.nrw.commons.location.LocationUpdateListener;
|
|
||||||
import fr.free.nrw.commons.nearby.CheckBoxTriStates;
|
|
||||||
import fr.free.nrw.commons.nearby.Label;
|
|
||||||
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
|
||||||
import fr.free.nrw.commons.nearby.NearbyController;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
|
||||||
import fr.free.nrw.commons.utils.LocationUtils;
|
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.List;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class NearbyParentFragmentPresenter
|
|
||||||
implements NearbyParentFragmentContract.UserActions,
|
|
||||||
WikidataEditListener.WikidataP18EditListener,
|
|
||||||
LocationUpdateListener {
|
|
||||||
|
|
||||||
private boolean isNearbyLocked;
|
|
||||||
private LatLng currentLatLng;
|
|
||||||
|
|
||||||
private boolean placesLoadedOnce;
|
|
||||||
|
|
||||||
BookmarkLocationsDao bookmarkLocationDao;
|
|
||||||
|
|
||||||
private @Nullable String customQuery;
|
|
||||||
|
|
||||||
private static final NearbyParentFragmentContract.View DUMMY = (NearbyParentFragmentContract.View) Proxy.newProxyInstance(
|
|
||||||
NearbyParentFragmentContract.View.class.getClassLoader(),
|
|
||||||
new Class[]{NearbyParentFragmentContract.View.class}, (proxy, method, args) -> {
|
|
||||||
if (method.getName().equals("onMyEvent")) {
|
|
||||||
return null;
|
|
||||||
} else if (String.class == method.getReturnType()) {
|
|
||||||
return "";
|
|
||||||
} else if (Integer.class == method.getReturnType()) {
|
|
||||||
return Integer.valueOf(0);
|
|
||||||
} else if (int.class == method.getReturnType()) {
|
|
||||||
return 0;
|
|
||||||
} else if (Boolean.class == method.getReturnType()) {
|
|
||||||
return Boolean.FALSE;
|
|
||||||
} else if (boolean.class == method.getReturnType()) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
private NearbyParentFragmentContract.View nearbyParentFragmentView = DUMMY;
|
|
||||||
|
|
||||||
|
|
||||||
public NearbyParentFragmentPresenter(BookmarkLocationsDao bookmarkLocationDao) {
|
|
||||||
this.bookmarkLocationDao = bookmarkLocationDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void attachView(NearbyParentFragmentContract.View view) {
|
|
||||||
this.nearbyParentFragmentView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void detachView() {
|
|
||||||
this.nearbyParentFragmentView = DUMMY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeNearbyPreferences(JsonKvStore applicationKvStore) {
|
|
||||||
Timber.d("Remove place objects");
|
|
||||||
applicationKvStore.remove(PLACE_OBJECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initializeMapOperations() {
|
|
||||||
lockUnlockNearby(false);
|
|
||||||
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);
|
|
||||||
nearbyParentFragmentView.setCheckBoxAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets click listeners of FABs, and 2 bottom sheets
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setActionListeners(JsonKvStore applicationKvStore) {
|
|
||||||
nearbyParentFragmentView.setFABPlusAction(v -> {
|
|
||||||
if (applicationKvStore.getBoolean("login_skipped", false)) {
|
|
||||||
// prompt the user to login
|
|
||||||
nearbyParentFragmentView.displayLoginSkippedWarning();
|
|
||||||
} else {
|
|
||||||
nearbyParentFragmentView.animateFABs();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nearbyParentFragmentView.setFABRecenterAction(v -> {
|
|
||||||
nearbyParentFragmentView.recenterMap(currentLatLng);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean backButtonClicked() {
|
|
||||||
if (nearbyParentFragmentView.isAdvancedQueryFragmentVisible()) {
|
|
||||||
nearbyParentFragmentView.showHideAdvancedQueryFragment(false);
|
|
||||||
return true;
|
|
||||||
} else if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
nearbyParentFragmentView.listOptionMenuItemClicked();
|
|
||||||
return true;
|
|
||||||
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
|
||||||
nearbyParentFragmentView.setBottomSheetDetailsSmaller();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markerUnselected() {
|
|
||||||
nearbyParentFragmentView.hideBottomSheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nearby updates takes time, since they are network operations. During update time, we don't
|
|
||||||
* want to get any other calls from user. So locking nearby.
|
|
||||||
*
|
|
||||||
* @param isNearbyLocked true means lock, false means unlock
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void lockUnlockNearby(boolean isNearbyLocked) {
|
|
||||||
this.isNearbyLocked = isNearbyLocked;
|
|
||||||
if (isNearbyLocked) {
|
|
||||||
nearbyParentFragmentView.disableFABRecenter();
|
|
||||||
} else {
|
|
||||||
nearbyParentFragmentView.enableFABRecenter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method should be the single point to update Map and List. Triggered by location changes
|
|
||||||
*
|
|
||||||
* @param locationChangeType defines if location changed significantly or slightly
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void updateMapAndList(LocationChangeType locationChangeType) {
|
|
||||||
Timber.d("Presenter updates map and list");
|
|
||||||
if (isNearbyLocked) {
|
|
||||||
Timber.d("Nearby is locked, so updateMapAndList returns");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nearbyParentFragmentView.isNetworkConnectionEstablished()) {
|
|
||||||
Timber.d("Network connection is not established");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LatLng lastLocation = nearbyParentFragmentView.getLastMapFocus();
|
|
||||||
if (nearbyParentFragmentView.getMapCenter() != null) {
|
|
||||||
currentLatLng = nearbyParentFragmentView.getMapCenter();
|
|
||||||
} else {
|
|
||||||
currentLatLng = lastLocation;
|
|
||||||
}
|
|
||||||
if (currentLatLng == null) {
|
|
||||||
Timber.d("Skipping update of nearby places as location is unavailable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Significant changed - Markers and current location will be updated together
|
|
||||||
* Slightly changed - Only current position marker will be updated
|
|
||||||
*/
|
|
||||||
if (locationChangeType.equals(CUSTOM_QUERY)) {
|
|
||||||
Timber.d("ADVANCED_QUERY_SEARCH");
|
|
||||||
lockUnlockNearby(true);
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(true);
|
|
||||||
LatLng updatedLocationByUser = LocationUtils.deriveUpdatedLocationFromSearchQuery(
|
|
||||||
customQuery);
|
|
||||||
if (updatedLocationByUser == null) {
|
|
||||||
updatedLocationByUser = lastLocation;
|
|
||||||
}
|
|
||||||
nearbyParentFragmentView.populatePlaces(updatedLocationByUser, customQuery);
|
|
||||||
} else if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|
|
||||||
|| locationChangeType.equals(MAP_UPDATED)) {
|
|
||||||
lockUnlockNearby(true);
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(true);
|
|
||||||
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapCenter());
|
|
||||||
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
|
|
||||||
Timber.d("SEARCH_CUSTOM_AREA");
|
|
||||||
lockUnlockNearby(true);
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(true);
|
|
||||||
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapFocus());
|
|
||||||
} else { // Means location changed slightly, ie user is walking or driving.
|
|
||||||
Timber.d("Means location changed slightly");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates places for custom location, should be used for finding nearby places around a
|
|
||||||
* location where you are not at.
|
|
||||||
*
|
|
||||||
* @param nearbyPlaces This variable has placeToCenter list information and distances.
|
|
||||||
*/
|
|
||||||
public void updateMapMarkers(List<Place> nearbyPlaces, LatLng currentLatLng,
|
|
||||||
boolean shouldTrackPosition) {
|
|
||||||
if (null != nearbyParentFragmentView) {
|
|
||||||
nearbyParentFragmentView.clearAllMarkers();
|
|
||||||
List<BaseMarker> baseMarkers = NearbyController
|
|
||||||
.loadAttractionsFromLocationToBaseMarkerOptions(currentLatLng,
|
|
||||||
// Curlatlang will be used to calculate distances
|
|
||||||
nearbyPlaces);
|
|
||||||
nearbyParentFragmentView.updateMapMarkers(baseMarkers);
|
|
||||||
lockUnlockNearby(false); // So that new location updates wont come
|
|
||||||
nearbyParentFragmentView.setProgressBarVisibility(false);
|
|
||||||
nearbyParentFragmentView.updateListFragment(nearbyPlaces);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some centering task may need to wait for map to be ready, if they are requested before map is
|
|
||||||
* ready. So we will remember it when the map is ready
|
|
||||||
*/
|
|
||||||
private void handleCenteringTaskIfAny() {
|
|
||||||
if (!placesLoadedOnce) {
|
|
||||||
placesLoadedOnce = true;
|
|
||||||
nearbyParentFragmentView.centerMapToPlace(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWikidataEditSuccessful() {
|
|
||||||
updateMapAndList(MAP_UPDATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedSignificantly(LatLng latLng) {
|
|
||||||
Timber.d("Location significantly changed");
|
|
||||||
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedSlightly(LatLng latLng) {
|
|
||||||
Timber.d("Location significantly changed");
|
|
||||||
updateMapAndList(LOCATION_SLIGHTLY_CHANGED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationChangedMedium(LatLng latLng) {
|
|
||||||
Timber.d("Location changed medium");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void filterByMarkerType(List<Label> selectedLabels, int state,
|
|
||||||
boolean filterForPlaceState, boolean filterForAllNoneType) {
|
|
||||||
if (filterForAllNoneType) {// Means we will set labels based on states
|
|
||||||
switch (state) {
|
|
||||||
case UNKNOWN:
|
|
||||||
// Do nothing
|
|
||||||
break;
|
|
||||||
case UNCHECKED:
|
|
||||||
//TODO
|
|
||||||
nearbyParentFragmentView.filterOutAllMarkers();
|
|
||||||
nearbyParentFragmentView.setRecyclerViewAdapterItemsGreyedOut();
|
|
||||||
break;
|
|
||||||
case CHECKED:
|
|
||||||
// Despite showing all labels NearbyFilterState still should be applied
|
|
||||||
nearbyParentFragmentView.filterMarkersByLabels(selectedLabels,
|
|
||||||
filterForPlaceState, false);
|
|
||||||
nearbyParentFragmentView.setRecyclerViewAdapterAllSelected();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nearbyParentFragmentView.filterMarkersByLabels(selectedLabels,
|
|
||||||
filterForPlaceState, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@MainThread
|
|
||||||
public void updateMapMarkersToController(List<BaseMarker> baseMarkers) {
|
|
||||||
NearbyController.markerLabelList.clear();
|
|
||||||
for (int i = 0; i < baseMarkers.size(); i++) {
|
|
||||||
BaseMarker nearbyBaseMarker = baseMarkers.get(i);
|
|
||||||
NearbyController.markerLabelList.add(
|
|
||||||
new MarkerPlaceGroup(
|
|
||||||
bookmarkLocationDao.findBookmarkLocation(nearbyBaseMarker.getPlace()),
|
|
||||||
nearbyBaseMarker.getPlace()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCheckboxUnknown() {
|
|
||||||
nearbyParentFragmentView.setCheckBoxState(CheckBoxTriStates.UNKNOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setAdvancedQuery(String query) {
|
|
||||||
this.customQuery = query;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void searchViewGainedFocus() {
|
|
||||||
if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
|
||||||
// Back should first hide the bottom sheet if it is expanded
|
|
||||||
nearbyParentFragmentView.hideBottomSheet();
|
|
||||||
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
|
||||||
nearbyParentFragmentView.hideBottomDetailsSheet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates a search for places within the area. Depending on whether the search
|
|
||||||
* is close to the current location, the map and list are updated
|
|
||||||
* accordingly.
|
|
||||||
*/
|
|
||||||
public void searchInTheArea(){
|
|
||||||
if (searchCloseToCurrentLocation()) {
|
|
||||||
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);
|
|
||||||
} else {
|
|
||||||
updateMapAndList(SEARCH_CUSTOM_AREA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if search this area button is used around our current location, so that we can
|
|
||||||
* continue following our current location again
|
|
||||||
*
|
|
||||||
* @return Returns true if search this area button is used around our current location
|
|
||||||
*/
|
|
||||||
public boolean searchCloseToCurrentLocation() {
|
|
||||||
if (null == nearbyParentFragmentView.getLastMapFocus()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
//TODO
|
|
||||||
Location mylocation = new Location("");
|
|
||||||
Location dest_location = new Location("");
|
|
||||||
dest_location.setLatitude(nearbyParentFragmentView.getMapFocus().getLatitude());
|
|
||||||
dest_location.setLongitude(nearbyParentFragmentView.getMapFocus().getLongitude());
|
|
||||||
mylocation.setLatitude(nearbyParentFragmentView.getLastMapFocus().getLatitude());
|
|
||||||
mylocation.setLongitude(nearbyParentFragmentView.getLastMapFocus().getLongitude());
|
|
||||||
Float distance = mylocation.distanceTo(dest_location);
|
|
||||||
|
|
||||||
if (distance > 2000.0 * 3 / 4) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onMapReady() {
|
|
||||||
if (null != nearbyParentFragmentView) {
|
|
||||||
initializeMapOperations();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,597 @@
|
||||||
|
package fr.free.nrw.commons.nearby.presenter
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import fr.free.nrw.commons.BaseMarker
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
|
||||||
|
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||||
|
import fr.free.nrw.commons.nearby.CheckBoxTriStates
|
||||||
|
import fr.free.nrw.commons.nearby.Label
|
||||||
|
import fr.free.nrw.commons.nearby.MarkerPlaceGroup
|
||||||
|
import fr.free.nrw.commons.nearby.NearbyController
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.nearby.PlacesRepository
|
||||||
|
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract
|
||||||
|
import fr.free.nrw.commons.utils.LocationUtils
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataEditListener.WikidataP18EditListener
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
class NearbyParentFragmentPresenter
|
||||||
|
(
|
||||||
|
val bookmarkLocationDao: BookmarkLocationsDao,
|
||||||
|
val placesRepository: PlacesRepository,
|
||||||
|
val nearbyController: NearbyController
|
||||||
|
) :
|
||||||
|
NearbyParentFragmentContract.UserActions,
|
||||||
|
WikidataP18EditListener, LocationUpdateListener {
|
||||||
|
|
||||||
|
private var isNearbyLocked = false
|
||||||
|
private var currentLatLng: LatLng? = null
|
||||||
|
|
||||||
|
private var placesLoadedOnce = false
|
||||||
|
|
||||||
|
private var customQuery: String? = null
|
||||||
|
|
||||||
|
private var nearbyParentFragmentView: NearbyParentFragmentContract.View = DUMMY
|
||||||
|
|
||||||
|
private val clickedPlaces = CopyOnWriteArrayList<Place>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used to tell the asynchronous place detail loading job that a pin was clicked
|
||||||
|
* so as to prevent it from turning grey on the next pin detail update
|
||||||
|
*
|
||||||
|
* @param place the place whose details have already been loaded because clicked pin
|
||||||
|
*/
|
||||||
|
fun handlePinClicked(place: Place) {
|
||||||
|
clickedPlaces.add(place)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the currently running job for async loading of pin details, cancelled when new pins are come
|
||||||
|
private var loadPlacesDataAyncJob: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - **batchSize**: number of places to fetch details of in a single request
|
||||||
|
* - **connnectionCount**: number of parallel requests
|
||||||
|
*/
|
||||||
|
private object LoadPlacesAsyncOptions {
|
||||||
|
const val BATCH_SIZE = 3
|
||||||
|
const val CONNECTION_COUNT = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private var schedulePlacesUpdateJob: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - **skippedCount**: stores the number of updates skipped
|
||||||
|
* - **skipLimit**: maximum number of consecutive updates that can be skipped
|
||||||
|
* - **skipDelayMs**: The delay (in milliseconds) to wait for a new update.
|
||||||
|
*
|
||||||
|
* @see schedulePlacesUpdate
|
||||||
|
*/
|
||||||
|
private object SchedulePlacesUpdateOptions {
|
||||||
|
var skippedCount = 0
|
||||||
|
const val SKIP_LIMIT = 3
|
||||||
|
const val SKIP_DELAY_MS = 500L
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to tell the asynchronous place detail loading job that the places' bookmarked status
|
||||||
|
// changed so as to prevent inconsistencies
|
||||||
|
private var bookmarkChangedPlaces = CopyOnWriteArrayList<Place>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a UI update for the provided list of `MarkerPlaceGroup` objects. Since, the update
|
||||||
|
* is performed on the main thread, it waits for a `SchedulePlacesUpdateOptions.skipDelayMs`
|
||||||
|
* to see if a new update comes, and if one does, it discards the scheduled UI update.
|
||||||
|
*
|
||||||
|
* @param markerPlaceGroups The new list of `MarkerPlaceGroup` objects. If the list is empty, no
|
||||||
|
* update will be performed.
|
||||||
|
*
|
||||||
|
* @see SchedulePlacesUpdateOptions
|
||||||
|
*/
|
||||||
|
private suspend fun schedulePlacesUpdate(markerPlaceGroups: List<MarkerPlaceGroup>) =
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (markerPlaceGroups.isEmpty()) return@withContext
|
||||||
|
schedulePlacesUpdateJob?.cancel()
|
||||||
|
schedulePlacesUpdateJob = launch {
|
||||||
|
if (SchedulePlacesUpdateOptions.skippedCount++
|
||||||
|
< SchedulePlacesUpdateOptions.SKIP_LIMIT
|
||||||
|
) {
|
||||||
|
delay(SchedulePlacesUpdateOptions.SKIP_DELAY_MS)
|
||||||
|
}
|
||||||
|
SchedulePlacesUpdateOptions.skippedCount = 0
|
||||||
|
updatePlaceGroupsToControllerAndRender(markerPlaceGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the user action of toggling the bookmarked status of a given place. Updates the
|
||||||
|
* bookmark status in the database, updates the UI to reflect the new state.
|
||||||
|
*
|
||||||
|
* @param place The place whose bookmarked status is to be toggled. If the place is `null`,
|
||||||
|
* the operation is skipped.
|
||||||
|
*/
|
||||||
|
override fun toggleBookmarkedStatus(place: Place?) {
|
||||||
|
if (place == null) return
|
||||||
|
val nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place)
|
||||||
|
bookmarkChangedPlaces.add(place)
|
||||||
|
val placeIndex =
|
||||||
|
NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location }
|
||||||
|
NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup(
|
||||||
|
nowBookmarked,
|
||||||
|
NearbyController.markerLabelList[placeIndex].place
|
||||||
|
)
|
||||||
|
nearbyParentFragmentView.setFilterState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachView(view: NearbyParentFragmentContract.View) {
|
||||||
|
this.nearbyParentFragmentView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun detachView() {
|
||||||
|
this.nearbyParentFragmentView = DUMMY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeNearbyPreferences(applicationKvStore: JsonKvStore) {
|
||||||
|
Timber.d("Remove place objects")
|
||||||
|
applicationKvStore.remove(PLACE_OBJECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeMapOperations() {
|
||||||
|
lockUnlockNearby(false)
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|
||||||
|
nearbyParentFragmentView.setCheckBoxAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets click listeners of FABs, and 2 bottom sheets
|
||||||
|
*/
|
||||||
|
override fun setActionListeners(applicationKvStore: JsonKvStore?) {
|
||||||
|
nearbyParentFragmentView.setFABPlusAction(View.OnClickListener { v: View? ->
|
||||||
|
if (applicationKvStore != null && applicationKvStore.getBoolean(
|
||||||
|
"login_skipped", false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// prompt the user to login
|
||||||
|
nearbyParentFragmentView.displayLoginSkippedWarning()
|
||||||
|
} else {
|
||||||
|
nearbyParentFragmentView.animateFABs()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nearbyParentFragmentView.setFABRecenterAction(View.OnClickListener { v: View? ->
|
||||||
|
nearbyParentFragmentView.recenterMap(currentLatLng)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun backButtonClicked(): Boolean {
|
||||||
|
if (nearbyParentFragmentView.isAdvancedQueryFragmentVisible()) {
|
||||||
|
nearbyParentFragmentView.showHideAdvancedQueryFragment(false)
|
||||||
|
return true
|
||||||
|
} else if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
nearbyParentFragmentView.listOptionMenuItemClicked()
|
||||||
|
return true
|
||||||
|
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
||||||
|
nearbyParentFragmentView.setBottomSheetDetailsSmaller()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markerUnselected() {
|
||||||
|
nearbyParentFragmentView.hideBottomSheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nearby updates takes time, since they are network operations. During update time, we don't
|
||||||
|
* want to get any other calls from user. So locking nearby.
|
||||||
|
*
|
||||||
|
* @param isNearbyLocked true means lock, false means unlock
|
||||||
|
*/
|
||||||
|
override fun lockUnlockNearby(isNearbyLocked: Boolean) {
|
||||||
|
this.isNearbyLocked = isNearbyLocked
|
||||||
|
if (isNearbyLocked) {
|
||||||
|
nearbyParentFragmentView.disableFABRecenter()
|
||||||
|
} else {
|
||||||
|
nearbyParentFragmentView.enableFABRecenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should be the single point to update Map and List. Triggered by location changes
|
||||||
|
*
|
||||||
|
* @param locationChangeType defines if location changed significantly or slightly
|
||||||
|
*/
|
||||||
|
override fun updateMapAndList(locationChangeType: LocationChangeType?) {
|
||||||
|
Timber.d("Presenter updates map and list")
|
||||||
|
if (isNearbyLocked) {
|
||||||
|
Timber.d("Nearby is locked, so updateMapAndList returns")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearbyParentFragmentView.isNetworkConnectionEstablished()) {
|
||||||
|
Timber.d("Network connection is not established")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastLocation = nearbyParentFragmentView.getLastMapFocus()
|
||||||
|
currentLatLng = if (nearbyParentFragmentView.getMapCenter() != null) {
|
||||||
|
nearbyParentFragmentView.getMapCenter()
|
||||||
|
} else {
|
||||||
|
lastLocation
|
||||||
|
}
|
||||||
|
if (currentLatLng == null) {
|
||||||
|
Timber.d("Skipping update of nearby places as location is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Significant changed - Markers and current location will be updated together
|
||||||
|
* Slightly changed - Only current position marker will be updated
|
||||||
|
*/
|
||||||
|
if (locationChangeType == LocationChangeType.CUSTOM_QUERY) {
|
||||||
|
Timber.d("ADVANCED_QUERY_SEARCH")
|
||||||
|
lockUnlockNearby(true)
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(true)
|
||||||
|
var updatedLocationByUser = LocationUtils.deriveUpdatedLocationFromSearchQuery(
|
||||||
|
customQuery!!
|
||||||
|
)
|
||||||
|
if (updatedLocationByUser == null) {
|
||||||
|
updatedLocationByUser = lastLocation
|
||||||
|
}
|
||||||
|
nearbyParentFragmentView.populatePlaces(updatedLocationByUser, customQuery)
|
||||||
|
} else if (locationChangeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
|
||||||
|
|| locationChangeType == LocationChangeType.MAP_UPDATED
|
||||||
|
) {
|
||||||
|
lockUnlockNearby(true)
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(true)
|
||||||
|
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapCenter())
|
||||||
|
} else if (locationChangeType == LocationChangeType.SEARCH_CUSTOM_AREA) {
|
||||||
|
Timber.d("SEARCH_CUSTOM_AREA")
|
||||||
|
lockUnlockNearby(true)
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(true)
|
||||||
|
nearbyParentFragmentView.populatePlaces(nearbyParentFragmentView.getMapFocus())
|
||||||
|
} else { // Means location changed slightly, ie user is walking or driving.
|
||||||
|
Timber.d("Means location changed slightly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update places on the map, and asynchronously load their details from cache and Wikidata query
|
||||||
|
*
|
||||||
|
* @param nearbyPlaces This variable has the list of placecs
|
||||||
|
* @param scope the lifecycle scope of `nearbyParentFragment`'s `viewLifecycleOwner`
|
||||||
|
*/
|
||||||
|
fun updateMapMarkers(
|
||||||
|
nearbyPlaces: List<Place>?, currentLatLng: LatLng,
|
||||||
|
scope: LifecycleCoroutineScope?
|
||||||
|
) {
|
||||||
|
val nearbyPlaceGroups = nearbyPlaces?.sortedBy { it.getDistanceInDouble(currentLatLng) }
|
||||||
|
?.take(NearbyController.MAX_RESULTS)
|
||||||
|
?.map {
|
||||||
|
// currently only the place's location is known but bookmarks are stored by name
|
||||||
|
MarkerPlaceGroup(
|
||||||
|
false,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: return
|
||||||
|
|
||||||
|
loadPlacesDataAyncJob?.cancel()
|
||||||
|
lockUnlockNearby(false) // So that new location updates wont come
|
||||||
|
nearbyParentFragmentView.setProgressBarVisibility(false)
|
||||||
|
|
||||||
|
updatePlaceGroupsToControllerAndRender(nearbyPlaceGroups)
|
||||||
|
|
||||||
|
loadPlacesDataAyncJob = scope?.launch(Dispatchers.IO) {
|
||||||
|
// clear past clicks and bookmarkChanged queues
|
||||||
|
clickedPlaces.clear()
|
||||||
|
bookmarkChangedPlaces.clear()
|
||||||
|
var clickedPlacesIndex = 0
|
||||||
|
var bookmarkChangedPlacesIndex = 0
|
||||||
|
|
||||||
|
val updatedGroups = nearbyPlaceGroups.toMutableList()
|
||||||
|
// first load cached places:
|
||||||
|
val indicesToUpdate = mutableListOf<Int>()
|
||||||
|
for (i in 0..updatedGroups.lastIndex) {
|
||||||
|
val repoPlace = placesRepository.fetchPlace(updatedGroups[i].place.entityID)
|
||||||
|
if (repoPlace != null && repoPlace.name != ""){
|
||||||
|
updatedGroups[i] = MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao.findBookmarkLocation(repoPlace),
|
||||||
|
repoPlace
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
indicesToUpdate.add(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (indicesToUpdate.size < updatedGroups.size) {
|
||||||
|
schedulePlacesUpdate(updatedGroups)
|
||||||
|
}
|
||||||
|
// channel for lists of indices of places, each list to be fetched in a single request
|
||||||
|
val fetchPlacesChannel = Channel<List<Int>>(Channel.UNLIMITED)
|
||||||
|
var totalBatches = 0
|
||||||
|
for (i in indicesToUpdate.indices step LoadPlacesAsyncOptions.BATCH_SIZE) {
|
||||||
|
++totalBatches
|
||||||
|
fetchPlacesChannel.send(
|
||||||
|
indicesToUpdate.slice(
|
||||||
|
i until (i + LoadPlacesAsyncOptions.BATCH_SIZE).coerceAtMost(
|
||||||
|
indicesToUpdate.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fetchPlacesChannel.close()
|
||||||
|
val collectResults = Channel<List<Pair<Int, MarkerPlaceGroup>>>(totalBatches)
|
||||||
|
repeat(LoadPlacesAsyncOptions.CONNECTION_COUNT) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
for (indices in fetchPlacesChannel) {
|
||||||
|
ensureActive()
|
||||||
|
try {
|
||||||
|
val fetchedPlaces =
|
||||||
|
nearbyController.getPlaces(indices.map { updatedGroups[it].place })
|
||||||
|
collectResults.send(
|
||||||
|
fetchedPlaces.mapIndexed { index, place ->
|
||||||
|
Pair(indices[index], MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao.findBookmarkLocation(place),
|
||||||
|
place
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("NearbyPinDetails").e(e)
|
||||||
|
collectResults.send(indices.map { Pair(it, updatedGroups[it]) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var collectCount = 0
|
||||||
|
for (resultList in collectResults) {
|
||||||
|
for ((index, fetchedPlaceGroup) in resultList) {
|
||||||
|
val existingPlace = updatedGroups[index].place
|
||||||
|
val finalPlaceGroup = MarkerPlaceGroup(
|
||||||
|
fetchedPlaceGroup.isBookmarked,
|
||||||
|
fetchedPlaceGroup.place.apply {
|
||||||
|
location = existingPlace.location
|
||||||
|
distance = existingPlace.distance
|
||||||
|
isMonument = existingPlace.isMonument
|
||||||
|
}
|
||||||
|
)
|
||||||
|
updatedGroups[index] = finalPlaceGroup
|
||||||
|
placesRepository
|
||||||
|
.save(finalPlaceGroup.place)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
// handle any places clicked
|
||||||
|
if (clickedPlacesIndex < clickedPlaces.size) {
|
||||||
|
val clickedPlacesBacklog = hashMapOf<LatLng, Place>()
|
||||||
|
while (clickedPlacesIndex < clickedPlaces.size) {
|
||||||
|
clickedPlacesBacklog.put(
|
||||||
|
clickedPlaces[clickedPlacesIndex].location,
|
||||||
|
clickedPlaces[clickedPlacesIndex]
|
||||||
|
)
|
||||||
|
++clickedPlacesIndex
|
||||||
|
}
|
||||||
|
for ((index, group) in updatedGroups.withIndex()) {
|
||||||
|
if (clickedPlacesBacklog.containsKey(group.place.location)) {
|
||||||
|
updatedGroups[index] = MarkerPlaceGroup(
|
||||||
|
updatedGroups[index].isBookmarked,
|
||||||
|
clickedPlacesBacklog[group.place.location]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle any bookmarks toggled
|
||||||
|
if (bookmarkChangedPlacesIndex < bookmarkChangedPlaces.size) {
|
||||||
|
val bookmarkChangedPlacesBacklog = hashMapOf<LatLng, Place>()
|
||||||
|
while (bookmarkChangedPlacesIndex < bookmarkChangedPlaces.size) {
|
||||||
|
bookmarkChangedPlacesBacklog.put(
|
||||||
|
bookmarkChangedPlaces[bookmarkChangedPlacesIndex].location,
|
||||||
|
bookmarkChangedPlaces[bookmarkChangedPlacesIndex]
|
||||||
|
)
|
||||||
|
++bookmarkChangedPlacesIndex
|
||||||
|
}
|
||||||
|
for ((index, group) in updatedGroups.withIndex()) {
|
||||||
|
if (bookmarkChangedPlacesBacklog.containsKey(group.place.location)) {
|
||||||
|
updatedGroups[index] = MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao
|
||||||
|
.findBookmarkLocation(updatedGroups[index].place),
|
||||||
|
updatedGroups[index].place
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schedulePlacesUpdate(updatedGroups)
|
||||||
|
if (++collectCount == totalBatches) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectResults.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some centering task may need to wait for map to be ready, if they are requested before map is
|
||||||
|
* ready. So we will remember it when the map is ready
|
||||||
|
*/
|
||||||
|
private fun handleCenteringTaskIfAny() {
|
||||||
|
if (!placesLoadedOnce) {
|
||||||
|
placesLoadedOnce = true
|
||||||
|
nearbyParentFragmentView.centerMapToPlace(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWikidataEditSuccessful() {
|
||||||
|
updateMapAndList(LocationChangeType.MAP_UPDATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedSignificantly(latLng: LatLng) {
|
||||||
|
Timber.d("Location significantly changed")
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedSlightly(latLng: LatLng) {
|
||||||
|
Timber.d("Location significantly changed")
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocationChangedMedium(latLng: LatLng) {
|
||||||
|
Timber.d("Location changed medium")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filterByMarkerType(
|
||||||
|
selectedLabels: List<Label?>?, state: Int,
|
||||||
|
filterForPlaceState: Boolean, filterForAllNoneType: Boolean
|
||||||
|
) {
|
||||||
|
if (filterForAllNoneType) { // Means we will set labels based on states
|
||||||
|
when (state) {
|
||||||
|
CheckBoxTriStates.UNKNOWN -> {}
|
||||||
|
CheckBoxTriStates.UNCHECKED -> {
|
||||||
|
//TODO
|
||||||
|
nearbyParentFragmentView.filterOutAllMarkers()
|
||||||
|
nearbyParentFragmentView.setRecyclerViewAdapterItemsGreyedOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBoxTriStates.CHECKED -> {
|
||||||
|
// Despite showing all labels NearbyFilterState still should be applied
|
||||||
|
nearbyParentFragmentView.filterMarkersByLabels(
|
||||||
|
selectedLabels,
|
||||||
|
filterForPlaceState, false
|
||||||
|
)
|
||||||
|
nearbyParentFragmentView.setRecyclerViewAdapterAllSelected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nearbyParentFragmentView.filterMarkersByLabels(
|
||||||
|
selectedLabels,
|
||||||
|
filterForPlaceState, false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
override fun updateMapMarkersToController(baseMarkers: MutableList<BaseMarker>) {
|
||||||
|
NearbyController.markerLabelList.clear()
|
||||||
|
for (i in baseMarkers.indices) {
|
||||||
|
val nearbyBaseMarker = baseMarkers[i]
|
||||||
|
NearbyController.markerLabelList.add(
|
||||||
|
MarkerPlaceGroup(
|
||||||
|
bookmarkLocationDao.findBookmarkLocation(nearbyBaseMarker.place),
|
||||||
|
nearbyBaseMarker.place
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the supplied markerPlaceGroups to `NearbyController` and nearby list fragment,
|
||||||
|
* and tells nearby parent fragment to filter the updated values to be rendered as overlays
|
||||||
|
* on the map
|
||||||
|
*
|
||||||
|
* @param markerPlaceGroups the new/updated list of places along with their bookmarked status
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
private fun updatePlaceGroupsToControllerAndRender(markerPlaceGroups: List<MarkerPlaceGroup>) {
|
||||||
|
NearbyController.markerLabelList.clear()
|
||||||
|
NearbyController.markerLabelList.addAll(markerPlaceGroups)
|
||||||
|
nearbyParentFragmentView.setFilterState()
|
||||||
|
nearbyParentFragmentView.updateListFragment(markerPlaceGroups.map { it.place })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCheckboxUnknown() {
|
||||||
|
nearbyParentFragmentView.setCheckBoxState(CheckBoxTriStates.UNKNOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAdvancedQuery(query: String) {
|
||||||
|
this.customQuery = query
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchViewGainedFocus() {
|
||||||
|
if (nearbyParentFragmentView.isListBottomSheetExpanded()) {
|
||||||
|
// Back should first hide the bottom sheet if it is expanded
|
||||||
|
nearbyParentFragmentView.hideBottomSheet()
|
||||||
|
} else if (nearbyParentFragmentView.isDetailsBottomSheetVisible()) {
|
||||||
|
nearbyParentFragmentView.hideBottomDetailsSheet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a search for places within the area. Depending on whether the search
|
||||||
|
* is close to the current location, the map and list are updated
|
||||||
|
* accordingly.
|
||||||
|
*/
|
||||||
|
fun searchInTheArea() {
|
||||||
|
if (searchCloseToCurrentLocation()) {
|
||||||
|
updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|
||||||
|
} else {
|
||||||
|
updateMapAndList(LocationChangeType.SEARCH_CUSTOM_AREA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if search this area button is used around our current location, so that we can
|
||||||
|
* continue following our current location again
|
||||||
|
*
|
||||||
|
* @return Returns true if search this area button is used around our current location
|
||||||
|
*/
|
||||||
|
fun searchCloseToCurrentLocation(): Boolean {
|
||||||
|
if (null == nearbyParentFragmentView.getLastMapFocus()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
//TODO
|
||||||
|
val myLocation = Location("")
|
||||||
|
val destLocation = Location("")
|
||||||
|
destLocation.latitude = nearbyParentFragmentView.getMapFocus().latitude
|
||||||
|
destLocation.longitude = nearbyParentFragmentView.getMapFocus().longitude
|
||||||
|
myLocation.latitude = nearbyParentFragmentView.getLastMapFocus().latitude
|
||||||
|
myLocation.longitude = nearbyParentFragmentView.getLastMapFocus().longitude
|
||||||
|
val distance = myLocation.distanceTo(destLocation)
|
||||||
|
|
||||||
|
return (distance <= 2000.0 * 3 / 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMapReady() {
|
||||||
|
initializeMapOperations()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DUMMY = Proxy.newProxyInstance(
|
||||||
|
NearbyParentFragmentContract.View::class.java.getClassLoader(),
|
||||||
|
arrayOf<Class<*>>(NearbyParentFragmentContract.View::class.java),
|
||||||
|
InvocationHandler { proxy: Any?, method: Method?, args: Array<Any?>? ->
|
||||||
|
if (method!!.name == "onMyEvent") {
|
||||||
|
return@InvocationHandler null
|
||||||
|
} else if (String::class.java == method.returnType) {
|
||||||
|
return@InvocationHandler ""
|
||||||
|
} else if (Int::class.java == method.returnType) {
|
||||||
|
return@InvocationHandler 0
|
||||||
|
} else if (Int::class.javaPrimitiveType == method.returnType) {
|
||||||
|
return@InvocationHandler 0
|
||||||
|
} else if (Boolean::class.java == method.returnType) {
|
||||||
|
return@InvocationHandler java.lang.Boolean.FALSE
|
||||||
|
} else if (Boolean::class.javaPrimitiveType == method.returnType) {
|
||||||
|
return@InvocationHandler false
|
||||||
|
} else {
|
||||||
|
return@InvocationHandler null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as NearbyParentFragmentContract.View
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,12 @@ class NearbyParentFragmentPresenterTest {
|
||||||
@Mock
|
@Mock
|
||||||
internal lateinit var bookmarkLocationsDao: BookmarkLocationsDao
|
internal lateinit var bookmarkLocationsDao: BookmarkLocationsDao
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
internal lateinit var placesRepository: PlacesRepository
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
internal lateinit var nearbyController: NearbyController
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal lateinit var latestLocation: LatLng
|
internal lateinit var latestLocation: LatLng
|
||||||
|
|
||||||
|
|
@ -52,7 +58,8 @@ class NearbyParentFragmentPresenterTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.openMocks(this)
|
MockitoAnnotations.openMocks(this)
|
||||||
nearbyPresenter = NearbyParentFragmentPresenter(bookmarkLocationsDao)
|
nearbyPresenter =
|
||||||
|
NearbyParentFragmentPresenter(bookmarkLocationsDao, placesRepository, nearbyController)
|
||||||
nearbyPresenter.attachView(nearbyParentFragmentView)
|
nearbyPresenter.attachView(nearbyParentFragmentView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,7 +329,7 @@ class NearbyParentFragmentPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSetActionListeners() {
|
fun testSetActionListeners() {
|
||||||
nearbyPresenter.setActionListeners(any())
|
nearbyPresenter.setActionListeners(null)
|
||||||
verify(nearbyParentFragmentView).setFABPlusAction(any())
|
verify(nearbyParentFragmentView).setFABPlusAction(any())
|
||||||
verify(nearbyParentFragmentView).setFABRecenterAction(any())
|
verify(nearbyParentFragmentView).setFABRecenterAction(any())
|
||||||
}
|
}
|
||||||
|
|
@ -454,11 +461,11 @@ class NearbyParentFragmentPresenterTest {
|
||||||
nearbyPlacesInfo.boundaryCoordinates = arrayOf()
|
nearbyPlacesInfo.boundaryCoordinates = arrayOf()
|
||||||
nearbyPlacesInfo.currentLatLng = latestLocation
|
nearbyPlacesInfo.currentLatLng = latestLocation
|
||||||
nearbyPlacesInfo.searchLatLng = latestLocation
|
nearbyPlacesInfo.searchLatLng = latestLocation
|
||||||
nearbyPlacesInfo.placeList = null
|
nearbyPlacesInfo.placeList = emptyList<Place>()
|
||||||
|
|
||||||
whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList())
|
whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList())
|
||||||
nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, true)
|
nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null)
|
||||||
Mockito.verify(nearbyParentFragmentView).updateMapMarkers(any())
|
Mockito.verify(nearbyParentFragmentView).setFilterState()
|
||||||
Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false)
|
Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false)
|
||||||
Mockito.verify(nearbyParentFragmentView).updateListFragment(nearbyPlacesInfo.placeList)
|
Mockito.verify(nearbyParentFragmentView).updateListFragment(nearbyPlacesInfo.placeList)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue