Explore nearby pictures (#4910)

* Add map fragment for explore and search

* Create structure of explore map by defining view and user action interfaces to presenter

* Add methods to map start, bottom sheet and permission

* Make the simple map visible

* Imitate methods from nearby map for permission needed and non needed map initialisation operations, however, needs to be tested and reactor

* a level of abstraction

* update media params to include coordinates

* Implement pageable presenter to explore

* Create Root fragment for map and media

* Iplement two presenter one for map operations, the other for pageable operations

* Construct general structure for both explore with search query and just by location

* fix injection issue

* Make default explore work with zoom level

* increase offscreen page limit with newly added fragment

* Make two distinct api calls for search with and without query

* Before trying to use same presenter for both search with and without query

* Add notes for Madhur

* Add Madhur's fixes for binding

* Call serch with and without query from the same spot

* partially solve zoom issue

* Make tab view unswipble while map is being used on search activity

* make viewpager unswipable while map is being used

* Code cleanup and reverting unnecessry edits

* Add search this area methods

* Implement search this area button functionality

* Fix search this area button, current location FAB and bottom attribution UI elements

* Add marker click action

* Solve bookmarkdao injection issue

* Make display bottom sheet details on marker click

* remove label and set bottom sheet behavior

* Remove irrelevan buttons like wikidata and article buttons

* Cancel bookmark feature for commons images for know, needs to be thought

* Add search this area button

* Add location off dialog

* Implement back button for explore map fragment while not on search activity

* Make thumbnails visible, they need some styling though

* Make gridle views even more beautiful

* Remove classes added to support query

* Remove query related code from Reach Activity

* Solve two progressbar issue

* Remove query related ekstra codes

* Remove not needed anymore callback

* Make medai details work

* Remove all old removed code dependencies

* Solve initial load takes too long issue

* Solve current position track

* Add placeholder for possible load issues

* Add red stroke to bitmap

* Add borders to rectangles

* Change media details text to details

* Fix file name extension anf File: prefix

* Fix some code style issues

* Fix some style issues

* Fix style issues

* Fix build issue

* Fix test about etMediaListFromSearch

* Fix test issue with Seacrh Activity

* Fix conflict mark
This commit is contained in:
neslihanturan 2022-04-14 11:28:17 +03:00 committed by GitHub
parent 7655562272
commit ee1bf4b5b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 2172 additions and 59 deletions

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import java.util.List;
public abstract class MapController {
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
public class NearbyPlacesInfo {
public List<Place> placeList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
public LatLng curLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places
public List<Media> mediaList; // Search location for finding this places
}
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
public class ExplorePlacesInfo {
public List<Place> explorePlaceList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
public LatLng curLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places
public List<Media> mediaList; // Search location for finding this places
}
}

View file

@ -175,8 +175,8 @@ public class BookmarkLocationsDao {
cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage());
cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription());
cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory());
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel().getText());
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel().getIcon());
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getText() : "");
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getIcon() : null);
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.getWikipediaLink().toString());
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.getWikidataLink().toString());
cv.put(BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.getCommonsLink().toString());

View file

@ -318,6 +318,8 @@ public class MainActivity extends BaseActivity
if (!exploreFragment.onBackPressed()) {
if (applicationKvStore.getBoolean("login_skipped")) {
super.onBackPressed();
} else {
setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
}
} else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) {

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.di
import android.app.Activity
import dagger.Module
import dagger.Provides
import fr.free.nrw.commons.explore.map.ExploreMapFragment
@Module
class ExploreMapFragmentModule{
@Provides
fun ExploreMapFragment.providesActivity(): Activity = activity!!
}

View file

@ -13,6 +13,8 @@ import fr.free.nrw.commons.customselector.ui.selector.FolderFragment;
import fr.free.nrw.commons.customselector.ui.selector.ImageFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.explore.ExploreListRootFragment;
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
@ -130,6 +132,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract ExploreListRootFragment bindExploreFeaturedRootFragment();
@ContributesAndroidInjector(modules = ExploreMapFragmentModule.class)
abstract ExploreMapFragment bindExploreNearbyUploadsFragment();
@ContributesAndroidInjector
abstract ExploreMapRootFragment bindExploreNearbyUploadsRootFragment();
@ContributesAndroidInjector
abstract BookmarkListRootFragment bindBookmarkListRootFragment();

View file

@ -10,6 +10,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
@ -29,6 +30,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
private static final String FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons";
private static final String MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android";
private static final String EXPLORE_MAP = "Map";
private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment";
@BindView(R.id.tab_layout)
@ -38,6 +40,7 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
ViewPagerAdapter viewPagerAdapter;
private ExploreListRootFragment featuredRootFragment;
private ExploreListRootFragment mobileRootFragment;
private ExploreMapRootFragment mapRootFragment;
@Inject
@Named("default_preferences")
public JsonKvStore applicationKvStore;
@ -68,6 +71,27 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
viewPager.setAdapter(viewPagerAdapter);
viewPager.setId(R.id.viewPager);
tabLayout.setupWithViewPager(viewPager);
viewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (position == 2) {
viewPager.setCanScroll(false);
} else {
viewPager.setCanScroll(true);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
setTabs();
setHasOptionsMenu(true);
return view;
@ -86,14 +110,21 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
Bundle mobileArguments = new Bundle();
mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY);
Bundle mapArguments = new Bundle();
mapArguments.putString("categoryName", EXPLORE_MAP);
featuredRootFragment = new ExploreListRootFragment(featuredArguments);
mobileRootFragment = new ExploreListRootFragment(mobileArguments);
mapRootFragment = new ExploreMapRootFragment(mapArguments);
fragmentList.add(featuredRootFragment);
titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase());
fragmentList.add(mobileRootFragment);
titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase());
fragmentList.add(mapRootFragment);
titleList.add(getString(R.string.explore_tab_title_map).toUpperCase());
((MainActivity)getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
@ -108,12 +139,18 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else {
} else if (tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
if (mobileRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else { //explore map fragment
if (mapRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
}
return false;
}

View file

@ -0,0 +1,216 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.navtab.NavTab;
public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
private ExploreMapFragment mapFragment;
@BindView(R.id.explore_container)
FrameLayout container;
public ExploreMapRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
@NonNull
public static ExploreMapRootFragment newInstance() {
ExploreMapRootFragment fragment = new ExploreMapRootFragment();
fragment.setRetainInstance(true);
return fragment;
}
public ExploreMapRootFragment(Bundle bundle) {
String title = bundle.getString("categoryName");
mapFragment = new ExploreMapFragment();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
mapFragment.setArguments(featuredArguments);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState == null) {
setFragment(mapFragment, mediaDetails);
}
}
public void setFragment(Fragment fragment, Fragment otherFragment) {
if (fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}
}
public void removeFragment(Fragment fragment) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public void onMediaClicked(int position) {
container.setVisibility(View.VISIBLE);
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true);
((ExploreFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, mapFragment);
mediaDetails.showImage(position);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (mapFragment != null && mapFragment.mediaList != null) {
return mapFragment.mediaList.get(i);
} else {
return null;
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (mapFragment != null && mapFragment.mediaList != null) {
return mapFragment.mediaList.size();
} else {
return 0;
}
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !mapFragment.isVisible()) {
removeFragment(mediaDetails);
onMediaClicked(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(mapFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
} if (mapFragment != null && mapFragment.isVisible()) {
if (mapFragment.backButtonClicked()) {
// Explore map fragment handled the event no further action required.
return true;
} else {
((MainActivity) getActivity()).showTabs();
return false;
}
} else {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
((MainActivity) getActivity()).showTabs();
return false;
}
}

View file

@ -140,7 +140,8 @@ public class SearchActivity extends BaseActivity
searchCategoryFragment.onQueryUpdated(query.toString());
}
} else {
}
else {
//Open RecentSearchesFragment
recentSearchesFragment.updateRecentSearches();
viewPager.setVisibility(View.GONE);
@ -155,8 +156,7 @@ public class SearchActivity extends BaseActivity
// Newly searched query...
if (recentSearch == null) {
recentSearchesDao.save(new RecentSearch(null, query, new Date()));
}
else {
} else {
recentSearch.setLastSearched(new Date());
recentSearchesDao.save(recentSearch);
}

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.explore.map;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.MediaClient;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ExploreMapCalls {
@Inject
MediaClient mediaClient;
@Inject
public ExploreMapCalls() {
}
/**
* Calls method to query Commons for uploads around a location
*
* @param curLatLng coordinates of search location
* @return list of places obtained
*/
List<Media> callCommonsQuery(final LatLng curLatLng) {
String coordinates = curLatLng.getLatitude() + "|" + curLatLng.getLongitude();
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
}
}

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.explore.map;
import android.content.Context;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.camera.CameraUpdate;
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.NearbyBaseMarker;
import fr.free.nrw.commons.nearby.Place;
import java.util.List;
public class ExploreMapContract {
interface View {
boolean isNetworkConnectionEstablished();
void populatePlaces(LatLng curlatLng,LatLng searchLatLng);
void checkPermissionsAndPerformAction();
void recenterMap(LatLng curLatLng);
void showLocationOffDialog();
void openLocationSettings();
void hideBottomDetailsSheet();
void displayBottomSheetWithInfo(Marker marker);
void addOnCameraMoveListener();
void addSearchThisAreaButtonAction();
void setSearchThisAreaButtonVisibility(boolean isVisible);
void setProgressBarVisibility(boolean isVisible);
boolean isDetailsBottomSheetVisible();
boolean isSearchThisAreaButtonVisible();
void addCurrentLocationMarker(LatLng curLatLng);
void updateMapToTrackPosition(LatLng curLatLng);
Context getContext();
LatLng getCameraTarget();
void centerMapToPlace(Place placeToCenter);
LatLng getLastLocation();
com.mapbox.mapboxsdk.geometry.LatLng getLastFocusLocation();
boolean isCurrentLocationMarkerVisible();
void setProjectorLatLngBounds();
void disableFABRecenter();
void enableFABRecenter();
void addNearbyMarkersToMapBoxMap(final List<NearbyBaseMarker> nearbyBaseMarkers, final Marker selectedMarker);
void setMapBoundaries(CameraUpdate cameaUpdate);
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
boolean backButtonClicked();
}
interface UserActions {
void updateMap(LocationServiceManager.LocationChangeType locationChangeType);
void lockUnlockNearby(boolean isNearbyLocked);
void attachView(View view);
void detachView();
void setActionListeners(JsonKvStore applicationKvStore);
boolean backButtonClicked();
void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng);
void markerUnselected();
void markerSelected(Marker marker);
}
}

View file

@ -0,0 +1,196 @@
package fr.free.nrw.commons.explore.map;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import com.mapbox.mapboxsdk.annotations.Marker;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.utils.PlaceUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import timber.log.Timber;
public class ExploreMapController extends MapController {
private final ExploreMapCalls exploreMapCalls;
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
public LatLng currentLocation; // current location of user
public double latestSearchRadius = 0; // Any last search radius
public double currentLocationSearchRadius = 0; // Search radius of only searches around current location
@Inject
public ExploreMapController(ExploreMapCalls explorePlaces) {
this.exploreMapCalls = explorePlaces;
}
/**
* Takes location as parameter and returns ExplorePlaces info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates
* @param curLatLng is current geolocation
* @param searchLatLng is the location that we want to search around
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around current location, false if another location
* @return explorePlacesInfo info that holds curLatLng, mediaList, explorePlaceList and boundaryCoordinates
*/
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean checkingAroundCurrentLocation) {
if (searchLatLng == null) {
Timber.d("Loading attractions explore map, but search is null");
return null;
}
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
try {
explorePlacesInfo.curLatLng = curLatLng;
latestSearchLocation = searchLatLng;
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south
mediaList.get(0).getCoordinates(), // north
mediaList.get(0).getCoordinates(), // west
mediaList.get(0).getCoordinates()};// east, init with a random location
if (searchLatLng != null) {
Timber.d("Sorting places by distance...");
final Map<Media, Double> distances = new HashMap<>();
for (Media media : mediaList) {
distances.put(media, computeDistanceBetween(media.getCoordinates(), searchLatLng));
// Find boundaries with basic find max approach
if (media.getCoordinates().getLatitude() < boundaryCoordinates[0].getLatitude()) {
boundaryCoordinates[0] = media.getCoordinates();
}
if (media.getCoordinates().getLatitude() > boundaryCoordinates[1].getLatitude()) {
boundaryCoordinates[1] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude() < boundaryCoordinates[2].getLongitude()) {
boundaryCoordinates[2] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude() > boundaryCoordinates[3].getLongitude()) {
boundaryCoordinates[3] = media.getCoordinates();
}
}
}
explorePlacesInfo.mediaList = mediaList;
explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList);
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates;
// Sets latestSearchRadius to maximum distance among boundaries and search location
for (LatLng bound : boundaryCoordinates) {
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(bound).distanceTo(LocationUtils.commonsLatLngToMapBoxLatLng(latestSearchLocation));
if (distance > latestSearchRadius) {
latestSearchRadius = distance;
}
}
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
if (checkingAroundCurrentLocation) {
currentLocationSearchRadius = latestSearchRadius;
currentLocation = curLatLng;
}
} catch (Exception e) {
e.printStackTrace();
}
return explorePlacesInfo;
}
/**
* Loads attractions from location for map view, we need to return places in Place data type
* @return baseMarkerOptions list that holds nearby places with their icons
*/
public static List<NearbyBaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
LatLng curLatLng,
final List<Place> placeList,
Context context,
NearbyBaseMarkerThumbCallback callback,
Marker selectedMarker,
boolean shouldTrackPosition,
ExplorePlacesInfo explorePlacesInfo) {
List<NearbyBaseMarker> baseMarkerOptions = new ArrayList<>();
if (placeList == null) {
return baseMarkerOptions;
}
VectorDrawableCompat vectorDrawable = null;
try {
vectorDrawable = VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme());
} catch (Resources.NotFoundException e) {
// ignore when running tests.
}
if (vectorDrawable != null) {
for (Place explorePlace : placeList) {
final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
String distance = formatDistanceBetween(curLatLng, explorePlace.location);
explorePlace.setDistance(distance);
nearbyBaseMarker.title(explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
explorePlace.location.getLatitude(),
explorePlace.location.getLongitude()));
nearbyBaseMarker.place(explorePlace);
Glide.with(context)
.asBitmap()
.load(explorePlace.getThumb())
.placeholder(R.drawable.image_placeholder_96)
.apply(new RequestOptions().override(96, 96).centerCrop())
.into(new CustomTarget<Bitmap>() {
// We add icons to markers when bitmaps are ready
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromBitmap(
ImageUtils.addRedBorder(resource, 6, context)));
baseMarkerOptions.add(nearbyBaseMarker);
if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition);
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
// We add thumbnail icon for images that couldn't be loaded
@Override
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
nearbyBaseMarker.setIcon(IconFactory.getInstance(context).fromResource(R.drawable.image_placeholder_96));
baseMarkerOptions.add(nearbyBaseMarker);
if (baseMarkerOptions.size() == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(baseMarkerOptions, explorePlacesInfo, selectedMarker, shouldTrackPosition);
}
}
});
}
}
return baseMarkerOptions;
}
interface NearbyBaseMarkerThumbCallback {
// Callback to notify thumbnails of explore markers are added as icons and ready
void onNearbyBaseMarkerThumbsReady(List<NearbyBaseMarker> baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition);
}
}

View file

@ -0,0 +1,807 @@
package fr.free.nrw.commons.explore.map;
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.utils.MapUtils.CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE;
import static fr.free.nrw.commons.utils.MapUtils.CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT;
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Html;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.mapbox.mapboxsdk.annotations.Icon;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
import com.mapbox.mapboxsdk.annotations.Polygon;
import com.mapbox.mapboxsdk.annotations.PolygonOptions;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdate;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.maps.UiSettings;
import com.mapbox.pluginscalebar.ScaleBarOptions;
import com.mapbox.pluginscalebar.ScaleBarPlugin;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
import fr.free.nrw.commons.explore.paging.LiveDataConverter;
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.media.MediaClient;
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
import fr.free.nrw.commons.nearby.NearbyMarker;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ExecutorUtils;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.utils.MapUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import fr.free.nrw.commons.utils.UiUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class ExploreMapFragment extends CommonsDaggerSupportFragment
implements ExploreMapContract.View, LocationUpdateListener {
private BottomSheetBehavior bottomSheetDetailsBehavior;
private BroadcastReceiver broadcastReceiver;
private boolean isNetworkErrorOccurred;
private Snackbar snackbar;
private boolean isDarkTheme;
private boolean isPermissionDenied;
private fr.free.nrw.commons.location.LatLng lastKnownLocation; // lask location of user
private fr.free.nrw.commons.location.LatLng lastFocusLocation; // last location that map is focused
public List<Media> mediaList;
private boolean recenterToUserLocation; // true is recenter is needed (ie. when current location is in visible map boundaries)
private MapboxMap.OnCameraMoveListener cameraMoveListener;
private MapboxMap mapBox;
private Place lastPlaceToCenter; // the last place that we centered the map
private boolean isMapBoxReady;
private Marker selectedMarker; // the marker that user selected
private LatLngBounds projectorLatLngBounds; // current windows borders
private Marker currentLocationMarker;
private Polygon currentLocationPolygon;
IntentFilter intentFilter = new IntentFilter(MapUtils.NETWORK_INTENT_ACTION);
@Inject
LiveDataConverter liveDataConverter;
@Inject
MediaClient mediaClient;
@Inject
LocationServiceManager locationManager;
@Inject
ExploreMapController exploreMapController;
@Inject @Named("default_preferences")
JsonKvStore applicationKvStore;
@Inject
BookmarkLocationsDao bookmarkLocationDao; // May be needed in future if we want to integrate bookmarking explore places
@Inject
SystemThemeUtils systemThemeUtils;
private ExploreMapPresenter presenter;
@BindView(R.id.map_view) MapView mapView;
@BindView(R.id.bottom_sheet_details) View bottomSheetDetails;
@BindView(R.id.map_progress_bar) ProgressBar progressBar;
@BindView(R.id.fab_recenter) FloatingActionButton fabRecenter;
@BindView(R.id.search_this_area_button) Button searchThisAreaButton;
@BindView(R.id.tv_attribution) AppCompatTextView tvAttribution;
@BindView(R.id.directionsButton) LinearLayout directionsButton;
@BindView(R.id.commonsButton) LinearLayout commonsButton;
@BindView(R.id.mediaDetailsButton) LinearLayout mediaDetailsButton;
@BindView(R.id.description) TextView description;
@BindView(R.id.title) TextView title;
@BindView(R.id.category) TextView distance;
@NonNull
public static ExploreMapFragment newInstance() {
ExploreMapFragment fragment = new ExploreMapFragment();
fragment.setRetainInstance(true);
return fragment;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState
) {
View v = inflater.inflate(R.layout.fragment_explore_map, container, false);
ButterKnife.bind(this, v);
return v;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mapView.onStart();
setSearchThisAreaButtonVisibility(false);
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
initNetworkBroadCastReceiver();
if (presenter == null) {
presenter = new ExploreMapPresenter(bookmarkLocationDao);
}
setHasOptionsMenu(true);
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
isPermissionDenied = false;
cameraMoveListener= () -> presenter.onCameraMove(mapBox.getCameraPosition().target);
presenter.attachView(this);
recenterToUserLocation = false;
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(mapBoxMap -> {
mapBox = mapBoxMap;
initViews();
presenter.setActionListeners(applicationKvStore);
mapBoxMap.setStyle(isDarkTheme? Style.DARK:Style.OUTDOORS, style -> {
final UiSettings uiSettings = mapBoxMap.getUiSettings();
uiSettings.setCompassGravity(Gravity.BOTTOM | Gravity.LEFT);
uiSettings.setCompassMargins(12, 0, 0, 24);
uiSettings.setLogoEnabled(false);
uiSettings.setAttributionEnabled(false);
uiSettings.setRotateGesturesEnabled(false);
isMapBoxReady = true;
performMapReadyActions();
final CameraPosition cameraPosition = new CameraPosition.Builder()
.target(new com.mapbox.mapboxsdk.geometry.LatLng(51.50550, -0.07520))
.zoom(MapUtils.ZOOM_OUT)
.build();
mapBoxMap.setCameraPosition(cameraPosition);
final ScaleBarPlugin scaleBarPlugin = new ScaleBarPlugin(mapView, mapBoxMap);
final int color = isDarkTheme ? R.color.bottom_bar_light : R.color.bottom_bar_dark;
final ScaleBarOptions scaleBarOptions = new ScaleBarOptions(getContext())
.setTextColor(color)
.setTextSize(R.dimen.description_text_size)
.setBarHeight(R.dimen.tiny_gap)
.setBorderWidth(R.dimen.miniscule_margin)
.setMarginTop(R.dimen.tiny_padding)
.setMarginLeft(R.dimen.tiny_padding)
.setTextBarMargin(R.dimen.tiny_padding);
scaleBarPlugin.create(scaleBarOptions);
});
});
}
@Override
public void onResume() {
super.onResume();
mapView.onResume();
presenter.attachView(this);
registerNetworkReceiver();
if (isResumed()) {
if (!isPermissionDenied && !applicationKvStore
.getBoolean("doNotAskForLocationPermission", false)) {
startTheMap();
} else {
startMapWithoutPermission();
}
}
}
private void startTheMap() {
mapView.onStart();
performMapReadyActions();
}
private void startMapWithoutPermission() {
mapView.onStart();
applicationKvStore.putBoolean("doNotAskForLocationPermission", true);
lastKnownLocation = MapUtils.defaultLatLng;
MapUtils.centerMapToDefaultLatLng(mapBox);
if (mapBox != null) {
addOnCameraMoveListener();
}
presenter.onMapReady(exploreMapController);
}
private void registerNetworkReceiver() {
if (getActivity() != null) {
getActivity().registerReceiver(broadcastReceiver, intentFilter);
}
}
private void performMapReadyActions() {
if (isMapBoxReady) {
if(!applicationKvStore.getBoolean("doNotAskForLocationPermission", false) ||
PermissionUtils.hasPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)){
checkPermissionsAndPerformAction();
}else{
isPermissionDenied = true;
addOnCameraMoveListener();
}
}
}
private void initViews() {
Timber.d("init views called");
initBottomSheets();
setBottomSheetCallbacks();
}
/**
* a) Creates bottom sheet behaviours from bottom sheet, sets initial states and visibility
* b) Gets the touch event on the map to perform following actions:
* if bottom sheet details are expanded or collapsed hide the bottom sheet details.
*/
@SuppressLint("ClickableViewAccessibility")
private void initBottomSheets() {
bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetails.setVisibility(View.VISIBLE);
mapView.setOnTouchListener((v, event) -> {
// Motion event is triggered two times on a touch event, one as ACTION_UP
// and other as ACTION_DOWN, we only want one trigger per touch event.
if(event.getAction() == MotionEvent.ACTION_DOWN) {
if (bottomSheetDetailsBehavior.getState()
== BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else if (bottomSheetDetailsBehavior.getState()
== BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
return false;
});
}
/**
* Defines how bottom sheets will act on click
*/
private void setBottomSheetCallbacks() {
bottomSheetDetails.setOnClickListener(v -> {
if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
} else if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
});
}
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
Timber.d("Location significantly changed");
if (isMapBoxReady && latLng != null &&!isUserBrowsing()) {
handleLocationUpdate(latLng,LOCATION_SIGNIFICANTLY_CHANGED);
}
}
private boolean isUserBrowsing() {
final boolean isUserBrowsing = lastKnownLocation!=null && !presenter.areLocationsClose(getCameraTarget(), lastKnownLocation);
return isUserBrowsing;
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
Timber.d("Location slightly changed");
if (isMapBoxReady && latLng != null &&!isUserBrowsing()) {//If the map has never ever shown the current location, lets do it know
handleLocationUpdate(latLng,LOCATION_SLIGHTLY_CHANGED);
}
}
private void handleLocationUpdate(final fr.free.nrw.commons.location.LatLng latLng, final LocationServiceManager.LocationChangeType locationChangeType){
lastKnownLocation = latLng;
exploreMapController.currentLocation = lastKnownLocation;
presenter.updateMap(locationChangeType);
}
@Override
public void onLocationChangedMedium(LatLng latLng) {
}
@Override
public boolean isNetworkConnectionEstablished() {
return NetworkUtils.isInternetConnectionEstablished(getActivity());
}
@Override
public void populatePlaces(LatLng curLatLng, LatLng searchLatLng) {
final Observable<MapController.ExplorePlacesInfo> nearbyPlacesInfoObservable;
if (curLatLng == null) {
checkPermissionsAndPerformAction();
return;
}
if (searchLatLng.equals(lastFocusLocation) || lastFocusLocation == null || recenterToUserLocation) { // Means we are checking around current location
nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(curLatLng, searchLatLng, true);
} else {
nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(curLatLng, searchLatLng, false);
}
compositeDisposable.add(nearbyPlacesInfoObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(explorePlacesInfo -> {
updateMapMarkers(explorePlacesInfo, isCurrentLocationMarkerVisible());
mediaList = explorePlacesInfo.mediaList;
lastFocusLocation = searchLatLng;
},
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places)+throwable.getLocalizedMessage());
setProgressBarVisibility(false);
presenter.lockUnlockNearby(false);
}));
if(recenterToUserLocation) {
recenterToUserLocation = false;
}
}
/**
* Updates map markers according to latest situation
* @param explorePlacesInfo holds several information as current location, marker list etc.
*/
private void updateMapMarkers(final MapController.ExplorePlacesInfo explorePlacesInfo, final boolean shouldTrackPosition) {
presenter.updateMapMarkers(explorePlacesInfo, selectedMarker,shouldTrackPosition);
}
private void showErrorMessage(final String message) {
ViewUtil.showLongToast(getActivity(), message);
}
@Override
public void checkPermissionsAndPerformAction() {
Timber.d("Checking permission and perfoming action");
PermissionUtils.checkPermissionsAndPerformAction(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION,
() -> locationPermissionGranted(),
() -> isPermissionDenied = true,
R.string.location_permission_title,
R.string.location_permission_rationale_nearby);
}
private void locationPermissionGranted() {
isPermissionDenied = false;
applicationKvStore.putBoolean("doNotAskForLocationPermission", false);
lastKnownLocation = locationManager.getLastLocation();
fr.free.nrw.commons.location.LatLng target=lastFocusLocation;
if(null == lastFocusLocation){
target = lastKnownLocation;
}
if (lastKnownLocation != null) {
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(target)) // Sets the new camera position
.zoom(ZOOM_LEVEL) // Same zoom level
.build();
mapBox.moveCamera(CameraUpdateFactory.newCameraPosition(position));
}
else if(locationManager.isGPSProviderEnabled() || locationManager.isNetworkProviderEnabled()){
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER);
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
setProgressBarVisibility(true);
}
else {
Toast.makeText(getContext(), getString(R.string.nearby_location_not_available), Toast.LENGTH_LONG).show();
}
presenter.onMapReady(exploreMapController);
registerUnregisterLocationListener(false);
addOnCameraMoveListener();
}
public void registerUnregisterLocationListener(final boolean removeLocationListener) {
MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this);
}
@Override
public void recenterMap(LatLng curLatLng) {
if (isPermissionDenied || curLatLng == null) {
recenterToUserLocation = true;
checkPermissionsAndPerformAction();
if (!isPermissionDenied && !(locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled())) {
showLocationOffDialog();
}
return;
}
addCurrentLocationMarker(curLatLng);
final CameraPosition position;
position = new CameraPosition.Builder()
.target(new com.mapbox.mapboxsdk.geometry.LatLng(curLatLng.getLatitude(), curLatLng.getLongitude(), 0)) // Sets the new camera position
.zoom(mapBox.getCameraPosition().zoom) // Same zoom level
.build();
mapBox.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
}
@Override
public void showLocationOffDialog() {
// This creates a dialog box that prompts the user to enable location
DialogUtil
.showAlertDialog(getActivity(), getString(R.string.ask_to_turn_location_on), getString(R.string.nearby_needs_location),
getString(R.string.yes), getString(R.string.no), this::openLocationSettings, null);
}
@Override
public void openLocationSettings() {
// This method opens the location settings of the device along with a followup toast.
final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
final PackageManager packageManager = getActivity().getPackageManager();
if (intent.resolveActivity(packageManager)!= null) {
startActivity(intent);
Toast.makeText(getContext(), R.string.recommend_high_accuracy_mode, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.cannot_open_location_settings, Toast.LENGTH_LONG).show();
}
}
@Override
public void hideBottomDetailsSheet() {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
@Override
public void displayBottomSheetWithInfo(final Marker marker) {
selectedMarker = marker;
final NearbyMarker nearbyMarker = (NearbyMarker) marker;
final Place place = nearbyMarker.getNearbyBaseMarker().getPlace();
passInfoToSheet(place);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
/**
* Same bottom sheet carries information for all nearby places, so we need to pass information
* (title, description, distance and links) to view on nearby marker click
* @param place Place of clicked nearby marker
*/
private void passInfoToSheet(final Place place) {
directionsButton.setOnClickListener(view -> Utils.handleGeoCoordinates(getActivity(),
place.getLocation()));
commonsButton.setVisibility(place.hasCommonsLink()?View.VISIBLE:View.GONE);
commonsButton.setOnClickListener(view -> Utils.handleWebUrl(getContext(), place.siteLinks.getCommonsLink()));
int index = 0;
for (Media media : mediaList) {
if (media.getFilename().equals(place.name)) {
int finalIndex = index;
mediaDetailsButton.setOnClickListener(view -> {
((ExploreMapRootFragment) getParentFragment()).onMediaClicked(finalIndex);
});
}
index ++;
}
title.setText(place.name.substring(5, place.name.lastIndexOf(".")));
distance.setText(place.distance);
// Remove label since it is double information
String descriptionText = place.getLongDescription()
.replace(place.getName() + " (","");
descriptionText = (descriptionText.equals(place.getLongDescription()) ? descriptionText : descriptionText.replaceFirst(".$",""));
// Set the short description after we remove place name from long description
description.setText(descriptionText);
}
@Override
public void addOnCameraMoveListener() {
mapBox.addOnCameraMoveListener(cameraMoveListener);
}
@Override
public void addSearchThisAreaButtonAction() {
searchThisAreaButton.setOnClickListener(presenter.onSearchThisAreaClicked());
}
@Override
public void setSearchThisAreaButtonVisibility(boolean isVisible) {
if (isVisible) {
searchThisAreaButton.setVisibility(View.VISIBLE);
} else {
searchThisAreaButton.setVisibility(View.GONE);
}
}
@Override
public void setProgressBarVisibility(boolean isVisible) {
if (isVisible) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
}
@Override
public boolean isDetailsBottomSheetVisible() {
if (bottomSheetDetails.getVisibility() == View.VISIBLE) {
return true;
} else {
return false;
}
}
@Override
public boolean isSearchThisAreaButtonVisible() {
if (searchThisAreaButton.getVisibility() == View.VISIBLE) {
return true;
} else {
return false;
}
}
/**
* Removes old current location marker and adds a new one to display current location
* @param curLatLng current location of user
*/
@Override
public void addCurrentLocationMarker(LatLng curLatLng) {
if (null != curLatLng && !isPermissionDenied) {
ExecutorUtils.get().submit(() -> {
mapView.post(() -> removeCurrentLocationMarker());
Timber.d("Adds current location marker");
final Icon icon = IconFactory.getInstance(getContext())
.fromResource(R.drawable.current_location_marker);
final MarkerOptions currentLocationMarkerOptions = new MarkerOptions()
.position(new com.mapbox.mapboxsdk.geometry.LatLng(curLatLng.getLatitude(),
curLatLng.getLongitude()));
currentLocationMarkerOptions.setIcon(icon); // Set custom icon
mapView.post(
() -> currentLocationMarker = mapBox.addMarker(currentLocationMarkerOptions));
final List<com.mapbox.mapboxsdk.geometry.LatLng> circle = UiUtils
.createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
curLatLng.getAccuracy() * 2, 100);
final PolygonOptions currentLocationPolygonOptions = new PolygonOptions()
.addAll(circle)
.strokeColor(getResources().getColor(R.color.current_marker_stroke))
.fillColor(getResources().getColor(R.color.current_marker_fill));
mapView.post(
() -> currentLocationPolygon = mapBox
.addPolygon(currentLocationPolygonOptions));
});
} else {
Timber.d("not adding current location marker..current location is null");
}
}
@Override
public boolean isCurrentLocationMarkerVisible() {
if (projectorLatLngBounds == null || currentLocationMarker == null) {
Timber.d("Map projection bounds are null");
return false;
} else {
Timber.d("Current location marker %s" , projectorLatLngBounds.contains(currentLocationMarker.getPosition()) ? "visible" : "invisible");
return projectorLatLngBounds.contains(currentLocationMarker.getPosition());
}
}
/**
* Sets boundaries of visible region in terms of geolocation
*/
@Override
public void setProjectorLatLngBounds() {
projectorLatLngBounds = mapBox.getProjection().getVisibleRegion().latLngBounds;
}
/**
* Removes old current location marker
*/
private void removeCurrentLocationMarker() {
if (currentLocationMarker != null && mapBox!=null) {
mapBox.removeMarker(currentLocationMarker);
if (currentLocationPolygon != null) {
mapBox.removePolygon(currentLocationPolygon);
}
}
}
/**
* Update map camera to trac users current position
* @param curLatLng
*/
@Override
public void updateMapToTrackPosition(LatLng curLatLng) {
Timber.d("Updates map camera to track user position");
final CameraPosition cameraPosition;
if(isPermissionDenied){
cameraPosition = new CameraPosition.Builder().target
(LocationUtils.commonsLatLngToMapBoxLatLng(curLatLng)).build();
}else{
cameraPosition = new CameraPosition.Builder().target
(LocationUtils.commonsLatLngToMapBoxLatLng(curLatLng)).build();
}
if(null!=mapBox) {
mapBox.setCameraPosition(cameraPosition);
mapBox.animateCamera(CameraUpdateFactory
.newCameraPosition(cameraPosition), 1000);
}
}
@Override
public LatLng getCameraTarget() {
return mapBox == null ? null : LocationUtils.mapBoxLatLngToCommonsLatLng(mapBox.getCameraPosition().target);
}
/**
* Centers map to a given place
* @param place place to center
*/
@Override
public void centerMapToPlace(Place place) {
MapUtils.centerMapToPlace(place, mapBox, lastPlaceToCenter, getActivity());
Timber.d("Map is centered to place");
final double cameraShift;
if (null != place) {
lastPlaceToCenter = place;
}
if (null != lastPlaceToCenter) {
final Configuration configuration = getActivity().getResources().getConfiguration();
if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
cameraShift = CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT;
} else {
cameraShift = CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE;
}
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(
new fr.free.nrw.commons.location.LatLng(
lastPlaceToCenter.location.getLatitude() - cameraShift,
lastPlaceToCenter.getLocation().getLongitude(),
0))) // Sets the new camera position
.zoom(mapBox.getCameraPosition().zoom) // Same zoom level
.build();
mapBox.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
}
}
@Override
public LatLng getLastLocation() {
if (lastKnownLocation == null) {
lastKnownLocation = locationManager.getLastLocation();
}
return lastKnownLocation;
}
@Override
public com.mapbox.mapboxsdk.geometry.LatLng getLastFocusLocation() {
return lastFocusLocation == null? null : LocationUtils.commonsLatLngToMapBoxLatLng(lastFocusLocation);
}
@Override
public void disableFABRecenter() {
fabRecenter.setEnabled(false);
}
@Override
public void enableFABRecenter() {
fabRecenter.setEnabled(true);
}
@Override
public void addNearbyMarkersToMapBoxMap(List<NearbyBaseMarker> nearbyBaseMarkers, Marker selectedMarker) {
mapBox.clear();
if (isMapBoxReady && mapBox != null) {
mapBox.addMarkers(nearbyBaseMarkers);
setMapMarkerActions(selectedMarker);
}
}
private void setMapMarkerActions(final Marker selectedMarker) {
if (mapBox != null) {
mapBox.setOnInfoWindowCloseListener(marker -> {
if (marker == selectedMarker) {
presenter.markerUnselected();
}
});
mapBox.setOnMarkerClickListener(marker -> {
if (marker instanceof NearbyMarker) {
presenter.markerSelected(marker);
}
return false;
});
}
}
@Override
public void setMapBoundaries(CameraUpdate cameaUpdate) {
mapBox.easeCamera(cameaUpdate);
}
@Override
public void setFABRecenterAction(OnClickListener onClickListener) {
fabRecenter.setOnClickListener(onClickListener);
}
@Override
public boolean backButtonClicked() {
if (!(bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN)) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
return true;
} else {
return false;
}
}
/**
* Adds network broadcast receiver to recognize connection established
*/
private void initNetworkBroadCastReceiver() {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
if (getActivity() != null) {
if (NetworkUtils.isInternetConnectionEstablished(getActivity())) {
if (isNetworkErrorOccurred) {
presenter.updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
isNetworkErrorOccurred = false;
}
if (snackbar != null) {
snackbar.dismiss();
snackbar = null;
}
} else {
if (snackbar == null) {
snackbar = Snackbar.make(getView(), R.string.no_internet, Snackbar.LENGTH_INDEFINITE);
setSearchThisAreaButtonVisibility(false);
setProgressBarVisibility(false);
}
isNetworkErrorOccurred = true;
snackbar.show();
}
}
}
};
}
}

View file

@ -0,0 +1,293 @@
package fr.free.nrw.commons.explore.map;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
import android.view.View;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.MapController.ExplorePlacesInfo;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback;
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.NearbyBaseMarker;
import fr.free.nrw.commons.utils.LocationUtils;
import io.reactivex.Observable;
import java.lang.reflect.Proxy;
import java.util.List;
import timber.log.Timber;
public class ExploreMapPresenter
implements ExploreMapContract.UserActions,
NearbyBaseMarkerThumbCallback {
BookmarkLocationsDao bookmarkLocationDao;
private boolean isNearbyLocked;
private boolean placesLoadedOnce;
private LatLng curLatLng;
private ExploreMapController exploreMapController;
private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy
.newProxyInstance(
ExploreMapContract.View.class.getClassLoader(),
new Class[]{ExploreMapContract.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 ExploreMapContract.View exploreMapFragmentView = DUMMY;
public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao){
this.bookmarkLocationDao = bookmarkLocationDao;
}
@Override
public void updateMap(LocationChangeType locationChangeType) {
Timber.d("Presenter updates map and list" + locationChangeType.toString());
if (isNearbyLocked) {
Timber.d("Nearby is locked, so updateMapAndList returns");
return;
}
if (!exploreMapFragmentView.isNetworkConnectionEstablished()) {
Timber.d("Network connection is not established");
return;
}
LatLng lastLocation = exploreMapFragmentView.getLastLocation();
curLatLng = lastLocation;
if (curLatLng == 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(LOCATION_SIGNIFICANTLY_CHANGED)) {
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED");
lockUnlockNearby(true);
exploreMapFragmentView.setProgressBarVisibility(true);
exploreMapFragmentView.populatePlaces(curLatLng, lastLocation);
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
Timber.d("SEARCH_CUSTOM_AREA");
lockUnlockNearby(true);
exploreMapFragmentView.setProgressBarVisibility(true);
exploreMapFragmentView.populatePlaces(curLatLng, exploreMapFragmentView.getCameraTarget());
} else { // Means location changed slightly, ie user is walking or driving.
Timber.d("Means location changed slightly");
if (exploreMapFragmentView.isCurrentLocationMarkerVisible()){ // Means user wants to see their live location
exploreMapFragmentView.recenterMap(curLatLng);
}
}
}
/**
* 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) {
exploreMapFragmentView.disableFABRecenter();
} else {
exploreMapFragmentView.enableFABRecenter();
}
}
@Override
public void attachView(ExploreMapContract.View view) {
exploreMapFragmentView = view;
}
@Override
public void detachView() {
exploreMapFragmentView = DUMMY;
}
/**
* Sets click listener of FAB
*/
@Override
public void setActionListeners(JsonKvStore applicationKvStore) {
exploreMapFragmentView.setFABRecenterAction(v -> {
exploreMapFragmentView.recenterMap(curLatLng);
});
}
@Override
public boolean backButtonClicked() {
return exploreMapFragmentView.backButtonClicked();
}
@Override
public void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng) {
exploreMapFragmentView.setProjectorLatLngBounds();
// If our nearby markers are calculated at least once
if (exploreMapController.latestSearchLocation != null) {
double distance = latLng.distanceTo
(LocationUtils.commonsLatLngToMapBoxLatLng(exploreMapController.latestSearchLocation));
if (exploreMapFragmentView.isNetworkConnectionEstablished()) {
if (distance > exploreMapController.latestSearchRadius && exploreMapController.latestSearchRadius != 0) {
exploreMapFragmentView.setSearchThisAreaButtonVisibility(true);
} else {
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
}
}
} else {
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
}
}
public void onMapReady(ExploreMapController exploreMapController) {
this.exploreMapController = exploreMapController;
exploreMapFragmentView.addSearchThisAreaButtonAction();
if(null != exploreMapFragmentView) {
exploreMapFragmentView.addSearchThisAreaButtonAction();
initializeMapOperations();
}
}
public void initializeMapOperations() {
lockUnlockNearby(false);
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
exploreMapFragmentView.addSearchThisAreaButtonAction();
}
public Observable<ExplorePlacesInfo> loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean checkingAroundCurrent) {
return Observable
.fromCallable(() -> exploreMapController
.loadAttractionsFromLocation(curLatLng, searchLatLng,checkingAroundCurrent));
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
* @param explorePlacesInfo This variable has placeToCenter list information and distances.
*/
public void updateMapMarkers(
MapController.ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition) {
exploreMapFragmentView.setMapBoundaries(CameraUpdateFactory.newLatLngBounds(getLatLngBounds(explorePlacesInfo.boundaryCoordinates), 50));
prepareNearbyBaseMarkers(explorePlacesInfo, selectedMarker, shouldTrackPosition);
}
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition) {
exploreMapController
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.curLatLng, // Curlatlang will be used to calculate distances
explorePlacesInfo.explorePlaceList,
exploreMapFragmentView.getContext(),
this,
selectedMarker,
shouldTrackPosition,
explorePlacesInfo);
}
@Override
public void onNearbyBaseMarkerThumbsReady(List<NearbyBaseMarker> baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition) {
if(null != exploreMapFragmentView) {
exploreMapFragmentView.addNearbyMarkersToMapBoxMap(baseMarkers, selectedMarker);
exploreMapFragmentView.addCurrentLocationMarker(explorePlacesInfo.curLatLng);
if(shouldTrackPosition){
exploreMapFragmentView.updateMapToTrackPosition(explorePlacesInfo.curLatLng);
}
lockUnlockNearby(false); // So that new location updates wont come
exploreMapFragmentView.setProgressBarVisibility(false);
handleCenteringTaskIfAny();
}
}
private LatLngBounds getLatLngBounds(LatLng[] boundaries) {
LatLngBounds latLngBounds = new LatLngBounds.Builder()
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[0]))
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[1]))
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[2]))
.include(LocationUtils.commonsLatLngToMapBoxLatLng(boundaries[3]))
.build();
return latLngBounds;
}
/**
* 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;
exploreMapFragmentView.centerMapToPlace(null);
}
}
public View.OnClickListener onSearchThisAreaClicked() {
return v -> {
// Lock map operations during search this area operation
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
if (searchCloseToCurrentLocation()){
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
updateMap(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 == exploreMapFragmentView.getLastFocusLocation() || exploreMapController.latestSearchRadius == 0) {
return true;
}
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(exploreMapFragmentView.getCameraTarget())
.distanceTo(exploreMapFragmentView.getLastFocusLocation());
if (distance > exploreMapController.currentLocationSearchRadius * 3 / 4) {
return false;
} else {
return true;
}
}
@Override
public void markerUnselected() {
exploreMapFragmentView.hideBottomDetailsSheet();
}
@Override
public void markerSelected(Marker marker) {
exploreMapFragmentView.displayBottomSheetWithInfo(marker);
}
public boolean areLocationsClose(LatLng cameraTarget, LatLng lastKnownLocation) {
double distance = LocationUtils.commonsLatLngToMapBoxLatLng(cameraTarget)
.distanceTo(LocationUtils.commonsLatLngToMapBoxLatLng(lastKnownLocation));
if (distance > exploreMapController.currentLocationSearchRadius * 3 / 4) {
return false;
} else {
return true;
}
}
}

View file

@ -15,6 +15,8 @@ import javax.inject.Singleton
const val PAGE_ID_PREFIX = "M"
const val CATEGORY_CONTINUATION_PREFIX = "category_"
const val LIMIT = 30
const val RADIUS = 10000
/**
* Media Client to handle custom calls to Commons MediaWiki APIs
@ -90,6 +92,16 @@ class MediaClient @Inject constructor(
fun getMediaListFromSearch(keyword: String?, limit: Int, offset: Int) =
responseMapper(mediaInterface.getMediaListFromSearch(keyword, limit, offset))
/**
* This method takes coordinate as input and returns a list of Media objects.
* It uses the generator query API to get the images searched using a query.
*
* @param coordinate coordinate
* @return
*/
fun getMediaListFromGeoSearch(coordinate: String?) =
responseMapper(mediaInterface.getMediaListFromGeoSearch(coordinate, LIMIT, RADIUS))
/**
* @return list of images for a particular depict entity
*/
@ -179,9 +191,9 @@ class MediaClient @Inject constructor(
}
private fun mediaFromPageAndEntity(pages: List<MwQueryPage>): Single<List<Media>> {
return if (pages.isEmpty())
return if (pages.isEmpty()) {
Single.just(emptyList())
else
} else {
getEntities(pages.map { "$PAGE_ID_PREFIX${it.pageId()}" })
.map {
pages.zip(it.entities().values)
@ -191,5 +203,7 @@ class MediaClient @Inject constructor(
}
}
}
}
}
}

View file

@ -11,7 +11,7 @@ import retrofit2.http.QueryMap;
* Interface for interacting with Commons media related APIs
*/
public interface MediaInterface {
String MEDIA_PARAMS="&prop=imageinfo&iiprop=url|extmetadata|user&&iiurlwidth=640" +
String MEDIA_PARAMS="&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlwidth=640" +
"&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
"|Artist|LicenseShortName|LicenseUrl";
@ -78,7 +78,20 @@ public interface MediaInterface {
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=search&gsrwhat=text&gsrnamespace=6" + //Search parameters
MEDIA_PARAMS)
Single<MwQueryResponse> getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset);
Single<MwQueryResponse> getMediaListFromSearch(@Query("gsrsearch") String keyword,
@Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset);
/**
* This method retrieves a list of Media objects filtered using list geosearch query. Example: https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2&generator=geosearch&ggsnamespace=6&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl&ggscoord=37.45579%7C-122.31369&ggslimit=30&ggsradius=10000
*
* @param location the search location
* @param itemLimit how many images are returned
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=geosearch&ggsnamespace=6" + //Search parameters
MEDIA_PARAMS)
Single<MwQueryResponse> getMediaListFromGeoSearch(@Query("ggscoord") String location, @Query("ggslimit") int itemLimit, @Query("ggsradius") int radius);
/**
* Fetches Media object from the imageInfo API

View file

@ -11,6 +11,8 @@ import androidx.annotation.Nullable;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import com.mapbox.mapboxsdk.annotations.Marker;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UiUtils;
@ -24,7 +26,11 @@ import java.util.Map;
import javax.inject.Inject;
import timber.log.Timber;
public class NearbyController {
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class NearbyController extends MapController {
private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
public static double currentLocationSearchRadius = 10.0; //in kilometers
@ -223,16 +229,6 @@ public class NearbyController {
return baseMarkerOptions;
}
/**
* We pass this variable as a group of placeList and boundaryCoordinates
*/
public class NearbyPlacesInfo {
public List<Place> placeList; // List of nearby places
public LatLng[] boundaryCoordinates; // Corners of nearby area
public LatLng curLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places
}
/**
* Updates makerLabelList item isBookmarked value
* @param place place which is bookmarked

View file

@ -7,7 +7,6 @@ import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.nearby.NearbyController.NearbyPlacesInfo;
import org.apache.commons.lang3.StringUtils;
import fr.free.nrw.commons.location.LatLng;
@ -34,7 +33,19 @@ public class Place implements Parcelable {
public String distance;
public final Sitelinks siteLinks;
private boolean isMonument;
private String thumb;
public Place() {
language = null;
name = null;
label = null;
longDescription = null;
location = null;
category = null;
pic = null;
exists = null;
siteLinks = null;
}
public Place(String language,String name, Label label, String longDescription, LatLng location, String category, Sitelinks siteLinks, String pic, Boolean exists) {
this.language = language;
@ -47,6 +58,20 @@ public class Place implements Parcelable {
this.pic = (pic == null) ? "":pic;
this.exists = exists;
}
public Place(String name, String longDescription, LatLng location, String category, Sitelinks siteLinks, String pic, String thumb) {
this.name = name;
this.longDescription = longDescription;
this.location = location;
this.category = category;
this.siteLinks = siteLinks;
this.pic = (pic == null) ? "":pic;
this.thumb = thumb;
this.language = null;
this.label = null;
this.exists = true;
}
public Place(Parcel in) {
this.language = in.readString();
this.name = in.readString();
@ -269,4 +294,12 @@ public class Place implements Parcelable {
return new Place[size];
}
};
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
}

View file

@ -81,6 +81,7 @@ import com.mapbox.mapboxsdk.maps.UiSettings;
import com.mapbox.pluginscalebar.ScaleBarOptions;
import com.mapbox.pluginscalebar.ScaleBarPlugin;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MapController.NearbyPlacesInfo;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;
@ -97,7 +98,6 @@ import fr.free.nrw.commons.nearby.Label;
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
import fr.free.nrw.commons.nearby.NearbyBaseMarker;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyController.NearbyPlacesInfo;
import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter;
import fr.free.nrw.commons.nearby.NearbyFilterState;
import fr.free.nrw.commons.nearby.NearbyMarker;
@ -710,7 +710,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void setFilterState() {
Log.d("deneme5","setfilterState");
chipNeedsPhoto.setChecked(NearbyFilterState.getInstance().isNeedPhotoSelected());
chipExists.setChecked(NearbyFilterState.getInstance().isExistsSelected());
chipWlm.setChecked(NearbyFilterState.getInstance().isWlmSelected());
@ -1077,7 +1076,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
final fr.free.nrw.commons.location.LatLng curlatLng,
final fr.free.nrw.commons.location.LatLng searchLatLng, @Nullable final String customQuery){
final Observable<NearbyPlacesInfo> nearbyPlacesInfoObservable = Observable
final Observable<NearbyController.NearbyPlacesInfo> nearbyPlacesInfoObservable = Observable
.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curlatLng, searchLatLng,
false, true, Utils.isMonumentsEnabled(new Date()), customQuery));

View file

@ -196,7 +196,7 @@ public class NearbyParentFragmentPresenter
Timber.d("ADVANCED_QUERY_SEARCH");
lockUnlockNearby(true);
nearbyParentFragmentView.setProgressBarVisibility(true);
LatLng updatedLocationByUser = deriveUpdatedLocationFromSearchQuery(customQuery);
LatLng updatedLocationByUser = LocationUtils.deriveUpdatedLocationFromSearchQuery(customQuery);
if (updatedLocationByUser == null) {
updatedLocationByUser = lastLocation;
}
@ -222,38 +222,6 @@ 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.

View file

@ -5,10 +5,12 @@ import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.net.Uri;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.exifinterface.media.ExifInterface;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
@ -340,4 +342,19 @@ public class ImageUtils {
return errorMessage.toString();
}
/**
* Adds red border to a bitmap
* @param bitmap
* @param borderSize
* @param context
* @return
*/
public static Bitmap addRedBorder(Bitmap bitmap, int borderSize, Context context) {
Bitmap bmpWithBorder = Bitmap.createBitmap(bitmap.getWidth() + borderSize * 2, bitmap.getHeight() + borderSize * 2, bitmap.getConfig());
Canvas canvas = new Canvas(bmpWithBorder);
canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed));
canvas.drawBitmap(bitmap, borderSize, borderSize, null);
return bmpWithBorder;
}
}

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.utils;
import fr.free.nrw.commons.location.LatLng;
import timber.log.Timber;
public class LocationUtils {
public static LatLng mapBoxLatLngToCommonsLatLng(com.mapbox.mapboxsdk.geometry.LatLng mapBoxLatLng) {
@ -10,4 +11,36 @@ public class LocationUtils {
public static com.mapbox.mapboxsdk.geometry.LatLng commonsLatLngToMapBoxLatLng(LatLng commonsLatLng) {
return new com.mapbox.mapboxsdk.geometry.LatLng(commonsLatLng.getLatitude(), commonsLatLng.getLongitude());
}
public static 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;
}
}

View file

@ -0,0 +1,73 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.content.res.Configuration;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.maps.MapboxMap;
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.Place;
import timber.log.Timber;
public class MapUtils {
public static final float ZOOM_LEVEL = 14f;
public static final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005;
public static final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004;
public static final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
public static final float ZOOM_OUT = 0f;
public static final LatLng defaultLatLng = new fr.free.nrw.commons.location.LatLng(51.50550,-0.07520,1f);
public static void centerMapToPlace(Place placeToCenter, MapboxMap mapBox, Place lastPlaceToCenter, Context context) {
Timber.d("Map is centered to place");
final double cameraShift;
if(null != placeToCenter){
lastPlaceToCenter = placeToCenter;
}
if (null != lastPlaceToCenter) {
final Configuration configuration = context.getResources().getConfiguration();
if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
cameraShift = CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT;
} else {
cameraShift = CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE;
}
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(
new fr.free.nrw.commons.location.LatLng(lastPlaceToCenter.location.getLatitude() - cameraShift,
lastPlaceToCenter.getLocation().getLongitude(),
0))) // Sets the new camera position
.zoom(ZOOM_LEVEL) // Same zoom level
.build();
mapBox.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
}
}
public static void centerMapToDefaultLatLng(MapboxMap mapBox) {
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(defaultLatLng))
.zoom(MapUtils.ZOOM_OUT)
.build();
if(mapBox != null){
mapBox.moveCamera(CameraUpdateFactory.newCameraPosition(position));
}
}
public static void registerUnregisterLocationListener(final boolean removeLocationListener, LocationServiceManager locationManager, LocationUpdateListener locationUpdateListener) {
try {
if (removeLocationListener) {
locationManager.unregisterLocationManager();
locationManager.removeLocationListener(locationUpdateListener);
Timber.d("Location service manager unregistered and removed");
} else {
locationManager.addLocationListener(locationUpdateListener);
locationManager.registerLocationManager();
Timber.d("Location service manager added and registered");
}
}catch (final Exception e){
Timber.e(e);
//Broadcasts are tricky, should be catchedonR
}
}
}

View file

@ -1,5 +1,10 @@
package fr.free.nrw.commons.utils;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.Sitelinks;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -23,4 +28,27 @@ public class PlaceUtils {
return new LatLng(latitude, longitude, 0);
}
/**
* Turns a Media list to a Place list by creating a new list in Place type
* @param mediaList
* @return
*/
public static List<Place> mediaToExplorePlace( List<Media> mediaList) {
List<Place> explorePlaceList = new ArrayList<>();
for (Media media :mediaList) {
explorePlaceList.add(new Place(media.getFilename(),
media.getFallbackDescription(),
media.getCoordinates(),
media.getCategories().toString(),
new Sitelinks.Builder()
.setCommonsLink(media.getPageTitle().getCanonicalUri())
.setWikipediaLink("") // we don't necessarily have them, can be fetched later
.setWikidataLink("") // we don't necessarily have them, can be fetched later
.build(),
media.getImageUrl(),
media.getThumbUrl()));
}
return explorePlaceList;
}
}