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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -68,7 +68,7 @@
android:layout_below="@id/toolbar_layout"
/>
<androidx.viewpager.widget.ViewPager
<fr.free.nrw.commons.explore.ParentViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="?attr/mainBackground"
app:layout_behavior="@string/bottom_sheet_behavior"
app:behavior_peekHeight="@dimen/large_height"
app:behavior_hideable="true"
android:visibility="visible"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/large_height"
android:layout_marginVertical="@dimen/activity_margin_horizontal"
android:gravity="center_vertical"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginRight="50dp"
android:maxLines="2"
android:ellipsize="end"
/>
<TextView
android:id="@+id/category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/tiny_height"
android:layout_marginTop="@dimen/small_height"
android:layout_marginBottom="@dimen/activity_margin_horizontal"
android:background="@android:color/darker_gray"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<LinearLayout
android:id="@+id/directionsButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_directions_black_24dp"
android:tint="?attr/rowButtonColor"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/directionsButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/nearby_directions"
android:textAllCaps="true"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/commonsButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_commons_icon_vector"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/commonsButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/nearby_commons"
android:textAllCaps="true"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/mediaDetailsButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_search_blue_24dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/mediaDetailsButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/explore_map_details"
android:textAllCaps="true"
/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/tiny_height"
android:layout_marginTop="@dimen/small_height"
android:layout_marginBottom="@dimen/activity_margin_horizontal"
android:background="@android:color/darker_gray"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_height"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginBottom="@dimen/standard_gap"
android:textSize="16sp" />
</LinearLayout>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- I have done this intentionally, the mapview because of some elevation or something,
sometimes hangs over the drawer layout and sometimes draws its onPaused state over the contributions, this seems to be the probable fix -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/container">
<com.mapbox.mapboxsdk.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</com.mapbox.mapboxsdk.maps.MapView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_recenter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:clickable="true"
android:focusable="true"
android:visibility="visible"
app:backgroundTint="@color/main_background_light"
app:elevation="@dimen/dimen_6"
app:fabSize="normal"
app:srcCompat="@drawable/ic_my_location_black_24dp"
app:useCompatPadding="true" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_attribution"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="@string/map_attribution"
android:textAlignment="center"
android:textSize="10sp" />
</RelativeLayout>
<Button
android:id="@+id/search_this_area_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_gravity="center_horizontal"
android:layout_margin="@dimen/activity_margin_horizontal"
android:background="@color/white"
android:padding="@dimen/activity_margin_horizontal"
android:singleLine="true"
android:text="@string/search_this_area"
android:textColor="@color/status_bar_blue"
android:visibility="gone"
app:elevation="@dimen/dimen_6"
/>
<include
android:id="@+id/bottom_sheet_details"
layout="@layout/bottom_sheet_details_explore" />
<ProgressBar
android:id="@+id/map_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:elevation="@dimen/dimen_6"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -302,6 +302,7 @@
<string name="explore_tab_title_featured">Featured</string>
<string name="explore_tab_title_mobile">Uploaded via mobile</string>
<string name="explore_tab_title_map">Map</string>
<string name="successful_wikidata_edit">Image added to %1$s on Wikidata!</string>
<string name="wikidata_edit_failure">Failed to update corresponding Wikidata entity!</string>
<string name="menu_set_wallpaper">Set as wallpaper</string>
@ -701,6 +702,7 @@ Upload your first media by tapping on the add button.</string>
<string name="no_location_found_message">How about adding the place where this image was taken?\nLocation data helps Wiki editors find your picture, making it much more useful.\nThank you!</string>
<string name="add_location">Add location</string>
<string name="feedback_sharing_data_alert">Please remove from this email any information that you are not comfortable sharing publicly. Also, please be aware that your email address with which you are posting, and the associated name and profile picture, will be visible publicly.</string>
<string name="explore_map_details">Details</string>
<string name="achievements_unavailable_beta">Achievements are only available in the prod flavor, please check the developer documentation.</string>
<string name="leaderboard_unavailable_beta">The leaderboard is only available in the prod flavor, please check the developer documentation.</string>
<string name="copyright_popup">Please only upload pictures you have taken by yourself. Uploaders of copyrighted images will be blocked. This applies to the beta flavor too. Thank you for testing the app!</string>