diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index 9d8d99bdb..09a4230cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -268,6 +268,7 @@ public class LocationServiceManager implements LocationListener { LOCATION_NOT_CHANGED, PERMISSION_JUST_GRANTED, MAP_UPDATED, - SEARCH_CUSTOM_AREA + SEARCH_CUSTOM_AREA, + CUSTOM_QUERY } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 3c7bd1f97..d7b1f0954 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -7,6 +7,7 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.Gson; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.campaigns.CampaignResponseDTO; import fr.free.nrw.commons.explore.depictions.DepictsClient; import fr.free.nrw.commons.location.LatLng; @@ -265,14 +266,27 @@ public class OkHttpJsonApiClient { }); } + /** + * Make API Call to get Nearby Places + * + * @param cur Search lat long + * @param language Language + * @param radius Search Radius + * @param shouldQueryForMonuments : Should we query for monuments + * @return + * @throws Exception + */ @Nullable public List getNearbyPlaces(final LatLng cur, final String language, final double radius, - final boolean shouldQueryForMonuments) + final boolean shouldQueryForMonuments, final String customQuery) throws Exception { Timber.d("Fetching nearby items at radius %s", radius); + Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null)); final String wikidataQuery; - if (!shouldQueryForMonuments) { + if (customQuery != null) { + wikidataQuery = customQuery; + } else if (!shouldQueryForMonuments) { wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq"); } else { wikidataQuery = FileUtils.readFromResource("/queries/nearby_query_monuments.rq"); @@ -313,6 +327,23 @@ public class OkHttpJsonApiClient { throw new Exception(response.message()); } + /** + * Make API Call to get Nearby Places Implementation does not expects a custom query + * + * @param cur Search lat long + * @param language Language + * @param radius Search Radius + * @param shouldQueryForMonuments : Should we query for monuments + * @return + * @throws Exception + */ + @Nullable + public List getNearbyPlaces(final LatLng cur, final String language, final double radius, + final boolean shouldQueryForMonuments) + throws Exception { + return getNearbyPlaces(cur, language, radius, shouldQueryForMonuments, null); + } + /** * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: * bridge -> suspended bridge, aqueduct, etc diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index a534273e4..8493ffb57 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -5,6 +5,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import androidx.annotation.MainThread; +import androidx.annotation.Nullable; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import com.mapbox.mapboxsdk.annotations.IconFactory; @@ -54,12 +55,13 @@ public class NearbyController { * @param curLatLng current location for user * @param searchLatLng the location user wants to search around * @param returnClosestResult if this search is done to find closest result or all results + * @param customQuery if this search is done via an advanced query * @return NearbyPlacesInfo a variable holds Place list without distance information * and boundary coordinates of current Place List */ public NearbyPlacesInfo loadAttractionsFromLocation(final LatLng curLatLng, final LatLng searchLatLng, final boolean returnClosestResult, final boolean checkingAroundCurrentLocation, - final boolean shouldQueryForMonuments) throws Exception { + final boolean shouldQueryForMonuments, @Nullable final String customQuery) throws Exception { Timber.d("Loading attractions near %s", searchLatLng); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); @@ -70,7 +72,7 @@ public class NearbyController { } List places = nearbyPlaces .radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult, - shouldQueryForMonuments); + shouldQueryForMonuments, customQuery); if (null != places && places.size() > 0) { LatLng[] boundaryCoordinates = {places.get(0).location, // south @@ -128,10 +130,27 @@ public class NearbyController { return nearbyPlacesInfo; } else { - return null; + return nearbyPlacesInfo; } } + /** + * Prepares Place list to make their distance information update later. + * + * @param curLatLng current location for user + * @param searchLatLng the location user wants to search around + * @param returnClosestResult if this search is done to find closest result or all results + * @return NearbyPlacesInfo a variable holds Place list without distance information and + * boundary coordinates of current Place List + */ + public NearbyPlacesInfo loadAttractionsFromLocation(final LatLng curLatLng, + final LatLng searchLatLng, + final boolean returnClosestResult, final boolean checkingAroundCurrentLocation, + final boolean shouldQueryForMonuments) throws Exception { + return loadAttractionsFromLocation(curLatLng, searchLatLng, returnClosestResult, + checkingAroundCurrentLocation, shouldQueryForMonuments, null); + } + /** * Loads attractions from location for list view, we need to return Place data type. * diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index c7a3dc0fb..24e5e3810 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.nearby; +import androidx.annotation.Nullable; import java.io.IOException; import java.util.Collections; import java.util.List; @@ -38,10 +39,12 @@ public class NearbyPlaces { * @param curLatLng coordinates of search location * @param lang user's language * @param returnClosestResult true if only the nearest point is desired + * @param customQuery * @return list of places obtained */ - List radiusExpander(final LatLng curLatLng, final String lang, final boolean returnClosestResult - , final boolean shouldQueryForMonuments) throws Exception { + List radiusExpander(final LatLng curLatLng, final String lang, + final boolean returnClosestResult + , final boolean shouldQueryForMonuments, @Nullable final String customQuery) throws Exception { final int minResults; final double maxRadius; @@ -62,13 +65,12 @@ public class NearbyPlaces { // Increase the radius gradually to find a satisfactory number of nearby places while (radius <= maxRadius) { - places = getFromWikidataQuery(curLatLng, lang, radius, shouldQueryForMonuments); + places = getFromWikidataQuery(curLatLng, lang, radius, shouldQueryForMonuments, customQuery); Timber.d("%d results at radius: %f", places.size(), radius); if (places.size() >= minResults) { break; - } else { - radius *= RADIUS_MULTIPLIER; } + radius *= RADIUS_MULTIPLIER; } // make sure we will be able to send at least one request next time if (radius > maxRadius) { @@ -83,11 +85,14 @@ public class NearbyPlaces { * @param lang user's language * @param radius radius for search, as determined by radiusExpander() * @param shouldQueryForMonuments should the query include properites for monuments + * @param customQuery * @return list of places obtained * @throws IOException if query fails */ public List getFromWikidataQuery(final LatLng cur, final String lang, - final double radius, final boolean shouldQueryForMonuments) throws Exception { - return okHttpJsonApiClient.getNearbyPlaces(cur, lang, radius, shouldQueryForMonuments); + final double radius, final boolean shouldQueryForMonuments, + @Nullable final String customQuery) throws Exception { + return okHttpJsonApiClient + .getNearbyPlaces(cur, lang, radius, shouldQueryForMonuments, customQuery); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java index dc83bc996..1e071bd72 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java @@ -2,13 +2,14 @@ package fr.free.nrw.commons.nearby.contract; import android.content.Context; +import androidx.annotation.Nullable; import com.mapbox.mapboxsdk.annotations.Marker; +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import java.util.List; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.nearby.NearbyBaseMarker; import fr.free.nrw.commons.nearby.Place; @@ -19,6 +20,7 @@ public interface NearbyParentFragmentContract { boolean isNetworkConnectionEstablished(); void listOptionMenuItemClicked(); void populatePlaces(LatLng curlatLng); + void populatePlaces(LatLng curlatLng, String customQuery); boolean isListBottomSheetExpanded(); void checkPermissionsAndPerformAction(); void displayLoginSkippedWarning(); @@ -62,7 +64,7 @@ public interface NearbyParentFragmentContract { LatLng getCameraTarget(); - void centerMapToPlace(Place placeToCenter); + void centerMapToPlace(@Nullable Place placeToCenter); void updateListFragment(List placeList); @@ -72,6 +74,12 @@ public interface NearbyParentFragmentContract { boolean isCurrentLocationMarkerVisible(); void setProjectorLatLngBounds(); + + boolean isAdvancedQueryFragmentVisible(); + + void showHideAdvancedQueryFragment(boolean shouldShow); + + void centerMapToPosition(@Nullable LatLng searchLatLng); } interface NearbyListView { @@ -79,7 +87,7 @@ public interface NearbyParentFragmentContract { } interface UserActions { - void updateMapAndList(LocationServiceManager.LocationChangeType locationChangeType); + void updateMapAndList(LocationChangeType locationChangeType); void lockUnlockNearby(boolean isNearbyLocked); void attachView(View view); @@ -96,5 +104,7 @@ public interface NearbyParentFragmentContract { void searchViewGainedFocus(); void setCheckboxUnknown(); + + void setAdvancedQuery(String query); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/AdvanceQueryFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/AdvanceQueryFragment.kt new file mode 100644 index 000000000..3debf75ee --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/AdvanceQueryFragment.kt @@ -0,0 +1,70 @@ +package fr.free.nrw.commons.nearby.fragments + +import android.content.Context.INPUT_METHOD_SERVICE +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatEditText +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.R +import kotlinx.android.synthetic.main.fragment_advance_query.* + +class AdvanceQueryFragment : Fragment() { + + lateinit var originalQuery: String + lateinit var callback: Callback + lateinit var etQuery: AppCompatEditText + lateinit var btnApply: AppCompatButton + lateinit var btnReset: AppCompatButton + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_advance_query, container, false) + originalQuery = arguments?.getString("query")!! + setHasOptionsMenu(false) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + etQuery = view.findViewById(R.id.et_query) + btnApply = view.findViewById(R.id.btn_apply) + btnReset = view.findViewById(R.id.btn_reset) + + etQuery.setText(originalQuery) + btnReset.setOnClickListener { + btnReset.post { + etQuery.setText(originalQuery) + etQuery.clearFocus() + hideKeyBoard() + callback.reset() + } + } + + btnApply.setOnClickListener { + btnApply.post { + etQuery.clearFocus() + hideKeyBoard() + callback.apply(etQuery.text.toString()) + callback.close() + } + } + } + + fun hideKeyBoard() { + val inputMethodManager = + context?.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager? + inputMethodManager?.hideSoftInputFromWindow(view?.windowToken, 0) + } + + interface Callback { + fun reset() + fun apply(query: String) + fun close() + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 8e1c43d9f..c7ad89e08 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.nearby.fragments; +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; @@ -34,6 +35,7 @@ import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; @@ -44,6 +46,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatButton; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.AppCompatTextView; @@ -100,7 +103,9 @@ import fr.free.nrw.commons.nearby.NearbyFilterState; import fr.free.nrw.commons.nearby.NearbyMarker; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; +import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback; import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter; +import fr.free.nrw.commons.upload.FileUtils; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.ExecutorUtils; import fr.free.nrw.commons.utils.LayoutUtils; @@ -114,14 +119,12 @@ import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.wikidata.WikidataEditListener; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Named; @@ -180,6 +183,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment AppCompatImageView ivToggleChips; @BindView(R.id.chip_view) View llContainerChips; + @BindView(R.id.btn_advanced_options) + AppCompatButton btnAdvancedOptions; + @BindView(R.id.fl_container_nearby_children) + FrameLayout flConainerNearbyChildren; @Inject LocationServiceManager locationManager; @Inject NearbyController nearbyController; @@ -233,6 +240,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment private LatLngBounds latLngBounds; private PlaceAdapter adapter; private NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback; + private boolean isAdvancedQueryFragmentVisible = false; /** * Holds filtered markers that are to be shown @@ -342,6 +350,42 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); + + btnAdvancedOptions.setOnClickListener(v -> { + searchView.clearFocus(); + showHideAdvancedQueryFragment(true); + final AdvanceQueryFragment fragment = new AdvanceQueryFragment(); + final Bundle bundle=new Bundle(); + try { + bundle.putString("query", FileUtils.readFromResource("/queries/nearby_query.rq")); + } catch (IOException e) { + Timber.e(e); + } + fragment.setArguments(bundle); + fragment.callback = new Callback() { + @Override + public void close() { + showHideAdvancedQueryFragment(false); + } + + @Override + public void reset() { + presenter.setAdvancedQuery(null); + presenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED); + showHideAdvancedQueryFragment(false); + } + + @Override + public void apply(@NotNull final String query) { + presenter.setAdvancedQuery(query); + presenter.updateMapAndList(CUSTOM_QUERY); + showHideAdvancedQueryFragment(false); + } + }; + getChildFragmentManager().beginTransaction() + .replace(R.id.fl_container_nearby_children, fragment) + .commit(); + }); } /** @@ -600,7 +644,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment }); nearbyFilterList.getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(), 0.75); recyclerView.setAdapter(nearbyFilterSearchRecyclerViewAdapter); - LayoutUtils.setLayoutHeightAllignedToWidth(1, nearbyFilterList); + LayoutUtils.setLayoutHeightAllignedToWidth(1.25, nearbyFilterList); compositeDisposable.add(RxSearchView.queryTextChanges(searchView) .takeUntil(RxView.detaches(searchView)) @@ -821,7 +865,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment * @param place is new center of the map */ @Override - public void centerMapToPlace(final Place place) { + public void centerMapToPlace(@Nullable final Place place) { Timber.d("Map is centered to place"); final double cameraShift; if(null!=place){ @@ -846,6 +890,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment } } + @Override public void updateListFragment(final List placeList) { places = placeList; @@ -879,6 +924,32 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment latLngBounds = mapBox.getProjection().getVisibleRegion().latLngBounds; } + @Override + public boolean isAdvancedQueryFragmentVisible() { + return isAdvancedQueryFragmentVisible; + } + + @Override + public void showHideAdvancedQueryFragment(final boolean shouldShow) { + setHasOptionsMenu(!shouldShow); + flConainerNearbyChildren.setVisibility(shouldShow ? View.VISIBLE : View.GONE); + isAdvancedQueryFragmentVisible = shouldShow; + } + + @Override + public void centerMapToPosition(fr.free.nrw.commons.location.LatLng searchLatLng) { + final CameraPosition cameraPosition = mapBox.getCameraPosition(); + if (null != searchLatLng && !( + cameraPosition.target.getLatitude() == searchLatLng.getLatitude() + && cameraPosition.target.getLongitude() == searchLatLng.getLongitude())) { + final CameraPosition position = new CameraPosition.Builder() + .target(LocationUtils.commonsLatLngToMapBoxLatLng(searchLatLng)) + .zoom(ZOOM_LEVEL) // Same zoom level + .build(); + mapBox.setCameraPosition(position); + } + } + @Override public boolean isNetworkConnectionEstablished() { return NetworkUtils.isInternetConnectionEstablished(getActivity()); @@ -933,29 +1004,53 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment @Override public void populatePlaces(final fr.free.nrw.commons.location.LatLng curlatLng) { if (curlatLng.equals(lastFocusLocation) || lastFocusLocation == null || recenterToUserLocation) { // Means we are checking around current location - populatePlacesForCurrentLocation(lastKnownLocation, curlatLng); + populatePlacesForCurrentLocation(lastKnownLocation, curlatLng, null); } else { - populatePlacesForAnotherLocation(lastKnownLocation, curlatLng); + populatePlacesForAnotherLocation(lastKnownLocation, curlatLng, null); } if(recenterToUserLocation) { recenterToUserLocation = false; } } - private void populatePlacesForCurrentLocation(final fr.free.nrw.commons.location.LatLng curlatLng, - final fr.free.nrw.commons.location.LatLng searchLatLng){ + @Override + public void populatePlaces(final fr.free.nrw.commons.location.LatLng curlatLng, + @Nullable final String customQuery) { + if (customQuery == null || customQuery.isEmpty()) { + populatePlaces(curlatLng); + return; + } + + if (curlatLng.equals(lastFocusLocation) || lastFocusLocation == null + || recenterToUserLocation) { // Means we are checking around current location + populatePlacesForCurrentLocation(lastKnownLocation, curlatLng, customQuery); + } else { + populatePlacesForAnotherLocation(lastKnownLocation, curlatLng, customQuery); + } + if (recenterToUserLocation) { + recenterToUserLocation = false; + } + } + + private void populatePlacesForCurrentLocation( + final fr.free.nrw.commons.location.LatLng curlatLng, + final fr.free.nrw.commons.location.LatLng searchLatLng, @Nullable final String customQuery){ final Observable nearbyPlacesInfoObservable = Observable .fromCallable(() -> nearbyController .loadAttractionsFromLocation(curlatLng, searchLatLng, - false, true, Utils.isMonumentsEnabled(new Date()))); + false, true, Utils.isMonumentsEnabled(new Date()), customQuery)); compositeDisposable.add(nearbyPlacesInfoObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(nearbyPlacesInfo -> { - updateMapMarkers(nearbyPlacesInfo, true); - lastFocusLocation=searchLatLng; + if (nearbyPlacesInfo.placeList == null || nearbyPlacesInfo.placeList.isEmpty()) { + showErrorMessage(getString(R.string.no_nearby_places_around)); + } else { + updateMapMarkers(nearbyPlacesInfo, true); + lastFocusLocation = searchLatLng; + } }, throwable -> { Timber.d(throwable); @@ -966,20 +1061,24 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment })); } - private void populatePlacesForAnotherLocation(final fr.free.nrw.commons.location.LatLng curlatLng, - final fr.free.nrw.commons.location.LatLng searchLatLng){ - + private void populatePlacesForAnotherLocation( + final fr.free.nrw.commons.location.LatLng curlatLng, + final fr.free.nrw.commons.location.LatLng searchLatLng, @Nullable final String customQuery){ final Observable nearbyPlacesInfoObservable = Observable .fromCallable(() -> nearbyController .loadAttractionsFromLocation(curlatLng, searchLatLng, - false, true, Utils.isMonumentsEnabled(new Date()))); + false, true, Utils.isMonumentsEnabled(new Date()), customQuery)); compositeDisposable.add(nearbyPlacesInfoObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(nearbyPlacesInfo -> { - updateMapMarkers(nearbyPlacesInfo, false); - lastFocusLocation=searchLatLng; + if (nearbyPlacesInfo.placeList == null || nearbyPlacesInfo.placeList.isEmpty()) { + showErrorMessage(getString(R.string.no_nearby_places_around)); + } else { + updateMapMarkers(nearbyPlacesInfo, false); + lastFocusLocation = searchLatLng; + } }, throwable -> { Timber.e(throwable); @@ -1617,10 +1716,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment if (!fabPlus.isShown()) { showFABs(); } - getView().requestFocus(); - break; - case (BottomSheetBehavior.STATE_EXPANDED): - getView().requestFocus(); break; case (BottomSheetBehavior.STATE_HIDDEN): if (null != mapBox) { @@ -1630,9 +1725,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment transparentView.setAlpha(0); collapseFABs(isFABsExpanded); hideFABs(); - if (getView() != null) { - getView().requestFocus(); - } break; } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java index 0125bcfcf..edf314441 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java @@ -3,8 +3,10 @@ package fr.free.nrw.commons.nearby.presenter; import android.view.View; import androidx.annotation.MainThread; +import androidx.annotation.Nullable; import com.mapbox.mapboxsdk.annotations.Marker; +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.List; @@ -12,7 +14,6 @@ import java.util.List; 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; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.nearby.CheckBoxTriStates; import fr.free.nrw.commons.nearby.Label; @@ -25,6 +26,7 @@ import fr.free.nrw.commons.utils.LocationUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; import timber.log.Timber; +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; @@ -46,6 +48,8 @@ public class NearbyParentFragmentPresenter 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) -> { @@ -118,7 +122,11 @@ public class NearbyParentFragmentPresenter @Override public boolean backButtonClicked() { - if(nearbyParentFragmentView.isListBottomSheetExpanded()) { + 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; @@ -160,7 +168,7 @@ public class NearbyParentFragmentPresenter * @param locationChangeType defines if location changed significantly or slightly */ @Override - public void updateMapAndList(LocationServiceManager.LocationChangeType locationChangeType) { + public void updateMapAndList(LocationChangeType locationChangeType) { Timber.d("Presenter updates map and list"); if (isNearbyLocked) { Timber.d("Nearby is locked, so updateMapAndList returns"); @@ -184,7 +192,17 @@ public class NearbyParentFragmentPresenter * Significant changed - Markers and current location will be updated together * Slightly changed - Only current position marker will be updated */ - if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) + if(locationChangeType.equals(CUSTOM_QUERY)){ + Timber.d("ADVANCED_QUERY_SEARCH"); + lockUnlockNearby(true); + nearbyParentFragmentView.setProgressBarVisibility(true); + LatLng updatedLocationByUser = deriveUpdatedLocationFromSearchQuery(customQuery); + if (updatedLocationByUser == null) { + updatedLocationByUser = lastLocation; + } + nearbyParentFragmentView.populatePlaces(updatedLocationByUser, customQuery); + } + else if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) || locationChangeType.equals(MAP_UPDATED)) { Timber.d("LOCATION_SIGNIFICANTLY_CHANGED"); lockUnlockNearby(true); @@ -204,6 +222,38 @@ public class NearbyParentFragmentPresenter } } + private LatLng deriveUpdatedLocationFromSearchQuery(String customQuery) { + LatLng latLng = null; + final int indexOfPrefix = customQuery.indexOf("Point("); + if (indexOfPrefix == -1) { + Timber.e("Invalid prefix index - Seems like user has entered an invalid query"); + return latLng; + } + final int indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix); + if (indexOfSuffix == -1) { + Timber.e("Invalid suffix index - Seems like user has entered an invalid query"); + return latLng; + } + String latLngString = customQuery.substring(indexOfPrefix+"Point(".length(), indexOfSuffix); + if (latLngString.isEmpty()) { + return null; + } + + String latLngArray[] = latLngString.split(" "); + if (latLngArray.length != 2) { + return null; + } + + try { + latLng = new LatLng(Double.parseDouble(latLngArray[1].trim()), + Double.parseDouble(latLngArray[0].trim()), 1f); + }catch (Exception e){ + Timber.e("Error while parsing user entered lat long: %s", e); + } + + return latLng; + } + /** * Populates places for custom location, should be used for finding nearby places around a * location where you are not at. @@ -225,6 +275,7 @@ public class NearbyParentFragmentPresenter nearbyParentFragmentView.setProgressBarVisibility(false); nearbyParentFragmentView.updateListFragment(nearbyPlacesInfo.placeList); handleCenteringTaskIfAny(); + nearbyParentFragmentView.centerMapToPosition(nearbyPlacesInfo.searchLatLng); } } @@ -331,6 +382,11 @@ public class NearbyParentFragmentPresenter nearbyParentFragmentView.setCheckBoxState(CheckBoxTriStates.UNKNOWN); } + @Override + public void setAdvancedQuery(String query) { + this.customQuery = query; + } + @Override public void searchViewGainedFocus() { if(nearbyParentFragmentView.isListBottomSheetExpanded()) { diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index 843542de6..e51fb4969 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -19,7 +19,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import io.reactivex.Flowable; import io.reactivex.Observable; import io.reactivex.Single; -import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -288,7 +287,7 @@ public class UploadRepository { final List fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng( decLatitude, decLongitude, 0.0f), Locale.getDefault().getLanguage(), - NEARBY_RADIUS_IN_KILO_METERS, false); + NEARBY_RADIUS_IN_KILO_METERS, false, null); return (fromWikidataQuery != null && fromWikidataQuery.size() > 0) ? fromWikidataQuery .get(0) : null; }catch (final Exception e) { diff --git a/app/src/main/res/drawable/advanced_query_border.xml b/app/src/main/res/drawable/advanced_query_border.xml new file mode 100644 index 000000000..9ab002277 --- /dev/null +++ b/app/src/main/res/drawable/advanced_query_border.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_advance_query.xml b/app/src/main/res/layout/fragment_advance_query.xml new file mode 100644 index 000000000..5b6e2cf09 --- /dev/null +++ b/app/src/main/res/layout/fragment_advance_query.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nearby_parent.xml b/app/src/main/res/layout/fragment_nearby_parent.xml index 99b8d8ea9..2d61a3982 100644 --- a/app/src/main/res/layout/fragment_nearby_parent.xml +++ b/app/src/main/res/layout/fragment_nearby_parent.xml @@ -6,6 +6,9 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + diff --git a/app/src/main/res/layout/nearby_filter_list.xml b/app/src/main/res/layout/nearby_filter_list.xml index 2152087c6..e1362c286 100644 --- a/app/src/main/res/layout/nearby_filter_list.xml +++ b/app/src/main/res/layout/nearby_filter_list.xml @@ -4,7 +4,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:elevation="@dimen/activity_margin_horizontal" android:background="@color/white"> @@ -26,6 +26,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginVertical="@dimen/dimen_10" + android:paddingBottom="48dp" /> + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1561c6c37..a6582727b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,6 +326,7 @@ Share App Error fetching nearby places. + No nearby places around Error fetching nearby monuments. No recent searches Are you sure you want to clear your search history? @@ -673,4 +674,8 @@ Upload your first media by tapping on the add button. Contributions of User: %s Achievements of User: %s View user page + Advanced Options + You can customize the Nearby query. If you get errors, reset and apply. + Apply + Reset diff --git a/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt new file mode 100644 index 000000000..aa7a643d2 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt @@ -0,0 +1,89 @@ +package fr.free.nrw.commons + +import com.google.gson.Gson +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.verify +import fr.free.nrw.commons.explore.depictions.DepictsClient +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import okhttp3.Call +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Response +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import java.lang.Exception + +class OkHttpJsonApiClientTests { + @Mock + lateinit var okhttpClient: OkHttpClient + + @Mock + lateinit var depictsClient: DepictsClient + + @Mock + lateinit var wikiMediaToolforgeUrl: HttpUrl + + @Mock + lateinit var wikiMediaTestToolforgeUrl: HttpUrl + var sparqlQueryUrl: String = "https://www.testqparql.com" + var campaignsUrl: String = "https://www.testcampaignsurl.com" + + @Mock + lateinit var gson: Gson + + @Mock + lateinit var latLng: LatLng + private lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + @Mock + lateinit var call: Call + + @Mock + lateinit var response: Response + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + okHttpJsonApiClient = OkHttpJsonApiClient( + okhttpClient, + depictsClient, + wikiMediaToolforgeUrl, + wikiMediaTestToolforgeUrl, + sparqlQueryUrl, + campaignsUrl, + gson + ) + Mockito.`when`(okhttpClient.newCall(any())).thenReturn(call) + Mockito.`when`(call.execute()).thenReturn(response) + } + + @Test + fun testGetNearbyPlacesCustomQuery() { + Mockito.`when`(response.message()).thenReturn("test") + try { + okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, true, "test") + } catch (e: Exception) { + assert(e.message.equals("test")) + } + verify(okhttpClient).newCall(any()) + verify(call).execute() + + } + + @Test + fun testGetNearbyPlaces() { + Mockito.`when`(response.message()).thenReturn("test") + try { + okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, true) + } catch (e: Exception) { + assert(e.message.equals("test")) + } + verify(okhttpClient).newCall(any()) + verify(call).execute() + + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/AdvanceQueryFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/AdvanceQueryFragmentUnitTests.kt new file mode 100644 index 000000000..f30060a0d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/AdvanceQueryFragmentUnitTests.kt @@ -0,0 +1,129 @@ +package fr.free.nrw.commons.nearby + +import android.content.Context +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatEditText +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.verify +import fr.free.nrw.commons.R +import fr.free.nrw.commons.TestAppAdapter +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.wikipedia.AppAdapter + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AdvanceQueryFragmentUnitTests { + private lateinit var context: Context + private lateinit var activity: MainActivity + private lateinit var fragment: AdvanceQueryFragment + + private lateinit var viewGroup: ViewGroup + + @Mock + private lateinit var layoutInflater: LayoutInflater + + @Mock + private lateinit var callback: AdvanceQueryFragment.Callback + + @Mock + private lateinit var view: View + + @Mock + private lateinit var etQuery: AppCompatEditText + + @Mock + private lateinit var btnReset: AppCompatButton + + @Mock + private lateinit var btnApply: AppCompatButton + + @Mock + private lateinit var bundle: Bundle + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + AppAdapter.set(TestAppAdapter()) + context = RuntimeEnvironment.application.applicationContext + activity = Robolectric.buildActivity(MainActivity::class.java).create().get() + viewGroup = FrameLayout(context) + + Mockito.`when`(bundle.getString("query")).thenReturn("test") + + fragment = AdvanceQueryFragment() + fragment.callback = callback + fragment.arguments = bundle + + val fragmentManager: FragmentManager = activity.supportFragmentManager + val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commit() + + Mockito.`when`(layoutInflater.inflate(R.layout.fragment_advance_query, viewGroup, false)) + .thenReturn(view) + Mockito.`when`(view.findViewById(R.id.et_query)).thenReturn(etQuery) + Mockito.`when`(view.findViewById(R.id.btn_apply)).thenReturn(btnApply) + Mockito.`when`(view.findViewById(R.id.btn_reset)).thenReturn(btnReset) + } + + @Test + @Throws(Exception::class) + fun checkFragmentNotNull() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + Assert.assertNotNull(fragment) + } + + @Test + fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onCreateView(layoutInflater, viewGroup, bundle) + verify(layoutInflater).inflate(R.layout.fragment_advance_query, viewGroup, false) + } + + @Test + fun testOnViewCreated() { + fragment.onCreateView(layoutInflater, viewGroup, bundle) + fragment.onViewCreated(view, bundle) + verify(etQuery).setText("test") + + Mockito.`when`(btnReset.post(any())).thenAnswer { + it.getArgument(0, Runnable::class.java).run() + } + + Mockito.`when`(btnApply.post(any())).thenAnswer { + it.getArgument(0, Runnable::class.java).run() + } + btnReset.performClick() + btnApply.performClick() + } + + @Test + fun testHideKeyboard() { + fragment.hideKeyBoard() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyControllerTest.kt new file mode 100644 index 000000000..a9215a147 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyControllerTest.kt @@ -0,0 +1,74 @@ +package fr.free.nrw.commons.nearby + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.eq +import fr.free.nrw.commons.location.LatLng +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import java.util.* + +class NearbyControllerTest { + @Mock + private lateinit var nearbyPlaces: NearbyPlaces + + @Mock + lateinit var searchLatLong: LatLng + + @Mock + lateinit var currentLatLng: LatLng + var customQuery: String = "test" + private lateinit var nearbyController: NearbyController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + nearbyController = NearbyController(nearbyPlaces) + } + + @Test + fun testLoadAttractionsForLocationTest() { + Mockito.`when`(nearbyPlaces.radiusExpander(any(), any(), any(), any(), any())) + .thenReturn(Collections.emptyList()) + nearbyController.loadAttractionsFromLocation( + searchLatLong, + currentLatLng, + false, + false, + true, + customQuery + ) + Mockito.verify(nearbyPlaces).radiusExpander( + eq(currentLatLng), + any(String::class.java), + eq(false), + eq(true), + eq(customQuery) + ) + } + + @Test + fun testLoadAttractionsForLocationTestNoQuery() { + Mockito.`when`(nearbyPlaces.radiusExpander(any(), any(), any(), any(), anyOrNull())) + .thenReturn(Collections.emptyList()) + nearbyController.loadAttractionsFromLocation( + searchLatLong, + currentLatLng, + false, + false, + true + ) + Mockito.verify(nearbyPlaces).radiusExpander( + eq(currentLatLng), + any(String::class.java), + eq(false), + eq(true), + eq(null) + ) + } + + fun any(type: Class): T = Mockito.any(type) +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt index 7aaba1998..20108fd8b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt @@ -15,6 +15,7 @@ import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +import java.util.* /** * The unit test class for NearbyParentFragmentPresenter @@ -32,6 +33,8 @@ class NearbyParentFragmentPresenterTest { internal lateinit var selectedLabels: List