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:
Tanmay Gupta 2024-12-20 18:00:53 +05:30 committed by GitHub
parent 70b4f78a5d
commit c891c2b0df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 676 additions and 679 deletions

View file

@ -20,8 +20,8 @@ import timber.log.Timber;
public class NearbyController extends MapController {
private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
public static final int MAX_RESULTS = 1000;
public static double currentLocationSearchRadius = 10.0; //in kilometers
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

View file

@ -2,11 +2,13 @@ package fr.free.nrw.commons.nearby.contract;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleCoroutineScope;
import fr.free.nrw.commons.BaseMarker;
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.nearby.Label;
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
import fr.free.nrw.commons.nearby.Place;
import java.util.List;
@ -68,7 +70,7 @@ public interface NearbyParentFragmentContract {
Context getContext();
void updateMapMarkers(List<BaseMarker> BaseMarkers);
void replaceMarkerOverlays(List<MarkerPlaceGroup> markerPlaceGroups);
void filterOutAllMarkers();
@ -127,5 +129,7 @@ public interface NearbyParentFragmentContract {
void setCheckboxUnknown();
void setAdvancedQuery(String query);
void toggleBookmarkedStatus(Place place);
}
}

View file

@ -23,8 +23,6 @@ import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@ -56,6 +54,7 @@ import androidx.appcompat.app.AlertDialog.Builder;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.lifecycle.LifecycleOwnerKt;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -110,16 +109,12 @@ import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@ -155,6 +150,29 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
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
LocationServiceManager locationManager;
@Inject
@ -217,7 +235,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private Runnable searchRunnable;
private static final long SCROLL_DELAY = 800; // Delay for debounce of onscroll, in milliseconds.
private List<Place> updatedPlacesList;
private LatLng updatedLatLng;
private boolean searchable;
@ -308,10 +325,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
* WLM URL
*/
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
public static NearbyParentFragment newInstance() {
@ -327,7 +340,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
view = binding.getRoot();
initNetworkBroadCastReceiver();
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao);
presenter = new NearbyParentFragmentPresenter(bookmarkLocationDao, placesRepository, nearbyController);
progressDialog = new ProgressDialog(getActivity());
progressDialog.setCancelable(false);
progressDialog.setMessage("Saving in progress...");
@ -452,28 +465,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
binding.map.getOverlays().add(scaleBarOverlay);
binding.map.getZoomController().setVisibility(Visibility.NEVER);
binding.map.getController().setZoom(ZOOM_LEVEL);
binding.map.getOverlays().add(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;
}
}));
binding.map.getOverlays().add(mapEventsOverlay);
binding.map.addMapListener(new MapListener() {
@Override
@ -605,8 +597,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
return Unit.INSTANCE;
},
(place, isBookmarked) -> {
updateMarker(isBookmarked, place, null);
binding.map.invalidate();
presenter.toggleBookmarkedStatus(place);
return Unit.INSTANCE;
},
commonPlaceClickActions,
@ -670,19 +661,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
registerNetworkReceiver();
if (isResumed() && ((MainActivity) getActivity()).activeFragment == ActiveFragment.NEARBY) {
if (locationPermissionsHelper.checkLocationPermission(getActivity())) {
if (lastFocusLocation == null && lastKnownLocation == null) {
locationPermissionGranted();
} else{
if (updatedPlacesList != null) {
if (!updatedPlacesList.isEmpty()) {
loadPlacesDataAsync(updatedPlacesList, updatedLatLng);
} else {
updateMapMarkers(updatedPlacesList, getLastMapFocus(), false);
}
}else {
locationPermissionGranted();
}
}
} else {
startMapWithoutPermission();
}
@ -973,7 +952,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void updateListFragment(final List<Place> placeList) {
places = placeList;
adapter.clear();
adapter.setItems(placeList);
binding.bottomSheetNearby.noResultsMessage.setVisibility(
placeList.isEmpty() ? View.VISIBLE : View.GONE);
@ -1367,13 +1346,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
? getTextBetweenParentheses(
updatedPlace.getLongDescription()) : updatedPlace.getLongDescription());
marker.showInfoWindow();
for (int i = 0; i < updatedPlacesList.size(); i++) {
Place pl = updatedPlacesList.get(i);
if (pl.location == updatedPlace.location) {
updatedPlacesList.set(i, updatedPlace);
presenter.handlePinClicked(updatedPlace);
savePlaceToDatabase(place);
}
}
Drawable icon = ContextCompat.getDrawable(getContext(),
getIconFor(updatedPlace, isBookMarked));
marker.setIcon(icon);
@ -1412,12 +1386,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
setProgressBarVisibility(false);
presenter.lockUnlockNearby(false);
} else {
updateMapMarkers(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng,
true);
updateMapMarkers(nearbyPlacesInfo.placeList, searchLatLng, true);
lastFocusLocation = searchLatLng;
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
searchLatLng.getLongitude());
loadPlacesDataAsync(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng);
}
},
throwable -> {
@ -1457,12 +1429,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
// curLatLng is used to calculate distance from the current location to the place
// and distance is later on populated to the place
updateMapMarkers(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng,
false);
updateMapMarkers(nearbyPlacesInfo.placeList, searchLatLng, false);
lastMapFocus = new GeoPoint(searchLatLng.getLatitude(),
searchLatLng.getLongitude());
stopQuery();
loadPlacesDataAsync(nearbyPlacesInfo.placeList, nearbyPlacesInfo.currentLatLng);
}
},
throwable -> {
@ -1475,167 +1445,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
}));
}
public void loadPlacesDataAsync(List<Place> placeList, LatLng curLatLng) {
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) {
public void savePlaceToDatabase(Place place) {
compositeDisposable.add(placesRepository
.save(place)
.subscribeOn(Schedulers.io())
@ -1661,8 +1471,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
*/
private void updateMapMarkers(final List<Place> nearbyPlaces, final LatLng curLatLng,
final boolean shouldUpdateSelectedMarker) {
presenter.updateMapMarkers(nearbyPlaces, curLatLng, shouldUpdateSelectedMarker);
setFilterState();
presenter.updateMapMarkers(nearbyPlaces, curLatLng,
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
public void filterOutAllMarkers() {
clearAllMarkers();
@ -1925,8 +1728,11 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
final boolean displayExists = false;
final boolean displayNeedsPhoto= false;
final boolean displayWlm = false;
// Remove the previous markers before updating them
clearAllMarkers();
if (selectedLabels == null || selectedLabels.size() == 0) {
replaceMarkerOverlays(NearbyController.markerLabelList);
return;
}
final ArrayList<MarkerPlaceGroup> placeGroupsToShow = new ArrayList<>();
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
final Place place = markerPlaceGroup.getPlace();
// When label filter is engaged
@ -1967,19 +1773,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
}
if (shouldUpdateMarker) {
updateMarker(markerPlaceGroup.getIsBookmarked(), place,
NearbyController.currentLocation);
placeGroupsToShow.add(
new MarkerPlaceGroup(markerPlaceGroup.getIsBookmarked(), place)
);
}
}
if (selectedLabels == null || selectedLabels.size() == 0) {
ArrayList<BaseMarker> markerArrayList = new ArrayList<>();
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
BaseMarker nearbyBaseMarker = new BaseMarker();
nearbyBaseMarker.setPlace(markerPlaceGroup.getPlace());
markerArrayList.add(nearbyBaseMarker);
}
addMarkersToMap(markerArrayList);
}
replaceMarkerOverlays(placeGroupsToShow);
}
@Override
@ -1987,18 +1786,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
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
*
@ -2052,13 +1839,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
);
}
/**
* 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) {
public Marker convertToMarker(Place place, Boolean isBookMarked) {
Drawable icon = ContextCompat.getDrawable(getContext(), getIconFor(place, isBookMarked));
GeoPoint point = new GeoPoint(place.location.getLatitude(), place.location.getLongitude());
Marker marker = new Marker(binding.map);
@ -2094,21 +1875,26 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
return true;
});
binding.map.getOverlays().add(marker);
return marker;
}
/**
* Adds multiple markers representing places to the map and handles item gestures.
*
* @param nearbyBaseMarkers The list of Place objects containing information about the
* locations.
* @param markerPlaceGroups The list of marker place groups containing the places and
* their bookmarked status
*/
private void addMarkersToMap(List<BaseMarker> nearbyBaseMarkers) {
@Override
public void replaceMarkerOverlays(final List<MarkerPlaceGroup> markerPlaceGroups) {
ArrayList<Marker> newMarkers = new ArrayList<>(markerPlaceGroups.size());
for (MarkerPlaceGroup markerPlaceGroup : markerPlaceGroups) {
newMarkers.add(
convertToMarker(markerPlaceGroup.getPlace(), markerPlaceGroup.getIsBookmarked()));
}
clearAllMarkers();
binding.map.getOverlays().addAll(newMarkers);
}
for(int i = 0; i< nearbyBaseMarkers.size(); i++){
addMarkerToMap(nearbyBaseMarkers.get(i).getPlace(), false);
}
}
/**
* Extracts text between the first occurrence of '(' and its corresponding ')' in the input
@ -2400,7 +2186,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
binding.map.invalidate();
GeoPoint geoPoint = mapCenter;
if (geoPoint != null) {
List<Overlay> overlays = binding.map.getOverlays();
ScaleDiskOverlay diskOverlay =
new ScaleDiskOverlay(this.getContext(),
geoPoint, 2000, UnitOfMeasure.foot);
@ -2434,28 +2219,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
scaleBarOverlay.setBackgroundPaint(barPaint);
scaleBarOverlay.enableScaleBar();
binding.map.getOverlays().add(scaleBarOverlay);
binding.map.getOverlays().add(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;
}
}));
binding.map.getOverlays().add(mapEventsOverlay);
binding.map.setMultiTouchControls(true);
}
@ -2510,21 +2274,14 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void onBottomSheetItemClick(@Nullable View view, int position) {
BottomSheetItem item = dataList.get(position);
boolean isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
switch (item.getImageResourceId()) {
case R.drawable.ic_round_star_border_24px:
bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
presenter.toggleBookmarkedStatus(selectedPlace);
updateBookmarkButtonImage(selectedPlace);
isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
binding.map.invalidate();
break;
case R.drawable.ic_round_star_filled_24px:
bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
presenter.toggleBookmarkedStatus(selectedPlace);
updateBookmarkButtonImage(selectedPlace);
isBookmarked = bookmarkLocationDao.findBookmarkLocation(selectedPlace);
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
binding.map.invalidate();
break;
case R.drawable.ic_directions_black_24dp:
Utils.handleGeoCoordinates(this.getContext(), selectedPlace.getLocation());

View file

@ -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();
}
}
}

View file

@ -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
}
}

View file

@ -31,6 +31,12 @@ class NearbyParentFragmentPresenterTest {
@Mock
internal lateinit var bookmarkLocationsDao: BookmarkLocationsDao
@Mock
internal lateinit var placesRepository: PlacesRepository
@Mock
internal lateinit var nearbyController: NearbyController
@Mock
internal lateinit var latestLocation: LatLng
@ -52,7 +58,8 @@ class NearbyParentFragmentPresenterTest {
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.openMocks(this)
nearbyPresenter = NearbyParentFragmentPresenter(bookmarkLocationsDao)
nearbyPresenter =
NearbyParentFragmentPresenter(bookmarkLocationsDao, placesRepository, nearbyController)
nearbyPresenter.attachView(nearbyParentFragmentView)
}
@ -322,7 +329,7 @@ class NearbyParentFragmentPresenterTest {
@Test
fun testSetActionListeners() {
nearbyPresenter.setActionListeners(any())
nearbyPresenter.setActionListeners(null)
verify(nearbyParentFragmentView).setFABPlusAction(any())
verify(nearbyParentFragmentView).setFABRecenterAction(any())
}
@ -454,11 +461,11 @@ class NearbyParentFragmentPresenterTest {
nearbyPlacesInfo.boundaryCoordinates = arrayOf()
nearbyPlacesInfo.currentLatLng = latestLocation
nearbyPlacesInfo.searchLatLng = latestLocation
nearbyPlacesInfo.placeList = null
nearbyPlacesInfo.placeList = emptyList<Place>()
whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList())
nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, true)
Mockito.verify(nearbyParentFragmentView).updateMapMarkers(any())
nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null)
Mockito.verify(nearbyParentFragmentView).setFilterState()
Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false)
Mockito.verify(nearbyParentFragmentView).updateListFragment(nearbyPlacesInfo.placeList)
}