From ee1bf4b5b6d01e8d638ebad73fe40b6d9518058b Mon Sep 17 00:00:00 2001 From: neslihanturan Date: Thu, 14 Apr 2022 11:28:17 +0300 Subject: [PATCH] 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 --- .../fr/free/nrw/commons/MapController.java | 30 + .../locations/BookmarkLocationsDao.java | 4 +- .../commons/contributions/MainActivity.java | 2 + .../commons/di/ExploreMapFragmentModule.kt | 13 + .../nrw/commons/di/FragmentBuilderModule.java | 8 + .../nrw/commons/explore/ExploreFragment.java | 39 +- .../explore/ExploreMapRootFragment.java | 216 +++++ .../nrw/commons/explore/SearchActivity.java | 6 +- .../commons/explore/map/ExploreMapCalls.java | 31 + .../explore/map/ExploreMapContract.java | 59 ++ .../explore/map/ExploreMapController.java | 196 +++++ .../explore/map/ExploreMapFragment.java | 807 ++++++++++++++++++ .../explore/map/ExploreMapPresenter.java | 293 +++++++ .../fr/free/nrw/commons/media/MediaClient.kt | 18 +- .../nrw/commons/media/MediaInterface.java | 17 +- .../nrw/commons/nearby/NearbyController.java | 18 +- .../fr/free/nrw/commons/nearby/Place.java | 35 +- .../fragments/NearbyParentFragment.java | 5 +- .../NearbyParentFragmentPresenter.java | 34 +- .../fr/free/nrw/commons/utils/ImageUtils.java | 17 + .../free/nrw/commons/utils/LocationUtils.java | 33 + .../fr/free/nrw/commons/utils/MapUtils.java | 73 ++ .../fr/free/nrw/commons/utils/PlaceUtils.java | 28 + .../res/drawable/image_placeholder_96.png | Bin 0 -> 1129 bytes app/src/main/res/layout/activity_search.xml | 2 +- .../layout/bottom_sheet_details_explore.xml | 162 ++++ .../main/res/layout/fragment_explore_map.xml | 83 ++ app/src/main/res/values/strings.xml | 2 + 28 files changed, 2172 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/MapController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ExploreMapFragmentModule.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java create mode 100644 app/src/main/res/drawable/image_placeholder_96.png create mode 100644 app/src/main/res/layout/bottom_sheet_details_explore.xml create mode 100644 app/src/main/res/layout/fragment_explore_map.xml diff --git a/app/src/main/java/fr/free/nrw/commons/MapController.java b/app/src/main/java/fr/free/nrw/commons/MapController.java new file mode 100644 index 000000000..0cf66ad8f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/MapController.java @@ -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 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 mediaList; // Search location for finding this places + } + + /** + * We pass this variable as a group of placeList and boundaryCoordinates + */ + public class ExplorePlacesInfo { + public List 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 mediaList; // Search location for finding this places + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java index b3fcfeebe..a55ae5e0d 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java @@ -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()); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index afa377c2f..46ecc1bb8 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -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) { diff --git a/app/src/main/java/fr/free/nrw/commons/di/ExploreMapFragmentModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ExploreMapFragmentModule.kt new file mode 100644 index 000000000..fe037d3e2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ExploreMapFragmentModule.kt @@ -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!! +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 09ae84207..5c2b1af4d 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java index c1c22ca66..470aea3df 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java @@ -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; } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java new file mode 100644 index 000000000..385cd886a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index a4527f866..1f07c39f6 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -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); } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java new file mode 100644 index 000000000..0d8b1de57 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java @@ -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 callCommonsQuery(final LatLng curLatLng) { + String coordinates = curLatLng.getLatitude() + "|" + curLatLng.getLongitude(); + return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java new file mode 100644 index 000000000..4a9adba48 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java @@ -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 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); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java new file mode 100644 index 000000000..72a4b216b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java @@ -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 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 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 loadAttractionsFromLocationToBaseMarkerOptions( + LatLng curLatLng, + final List placeList, + Context context, + NearbyBaseMarkerThumbCallback callback, + Marker selectedMarker, + boolean shouldTrackPosition, + ExplorePlacesInfo explorePlacesInfo) { + List 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() { + // We add icons to markers when bitmaps are ready + @Override + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition 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 baseMarkers, ExplorePlacesInfo explorePlacesInfo, Marker selectedMarker, boolean shouldTrackPosition); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java new file mode 100644 index 000000000..83a2d14dd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java @@ -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 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 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 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 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(); + } + } + } + }; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java new file mode 100644 index 000000000..fc20bdbfe --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java @@ -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 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 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; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt index b4ab6ec01..6a86bcdaf 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt @@ -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): Single> { - 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( } } } + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java index e56c3733a..ee66c41ae 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java @@ -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 getMediaListFromSearch(@Query("gsrsearch") String keyword, @Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset); + Single 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 getMediaListFromGeoSearch(@Query("ggscoord") String location, @Query("ggslimit") int itemLimit, @Query("ggsradius") int radius); /** * Fetches Media object from the imageInfo API diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 71d4fc207..edacd5d8c 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -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 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 diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index b0b3a14e0..8972d505e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -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; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index e1a5aa106..6fd89a989 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -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 nearbyPlacesInfoObservable = Observable + final Observable nearbyPlacesInfoObservable = Observable .fromCallable(() -> nearbyController .loadAttractionsFromLocation(curlatLng, searchLatLng, false, true, Utils.isMonumentsEnabled(new Date()), customQuery)); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java index edf314441..4019aa9fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java @@ -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. diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 17ec83412..3bf1847d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -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; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java index 38cf88ca1..bc861f008 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java @@ -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; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java new file mode 100644 index 000000000..08078a8d5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java @@ -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 + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java index 8c67b3917..b9da196fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java @@ -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 mediaToExplorePlace( List mediaList) { + List 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; + } } diff --git a/app/src/main/res/drawable/image_placeholder_96.png b/app/src/main/res/drawable/image_placeholder_96.png new file mode 100644 index 0000000000000000000000000000000000000000..bd72ac18ab650e4349a7ab0b9c6e07b5c373a54f GIT binary patch literal 1129 zcmeAS@N?(olHy`uVBq!ia0vp^jX<2j!3HE3Y6d3)DVAa<&kznEsNqQI0P;BtJR*x3 z82D2`m@)j2LN!oOvcxr_Bsf2(yEr+qAXP8FD1G)j8z}|`=4YNR zjv*Cu-rl{L_b5W7^S$9|a=JN^H>StTo z%|5k!lVOw2>&^G-nwpviHFmdUUO(I1rf}NmN#(!& z@%uzv1=S{VUNSlQ`n7i1?zw9(=Gkd;$jQk~cwV{IUfNYqPF|j|K|p6|U82NsL$Pkx z$EByAe!6bTeA!p_V2;`DDy|>Vfg*`!vo(E==monLl$W=g_(rdv@1rKc^FuGp`s)6N z7c;J8){3}xJic+kWNV}*$c z@9t?@P4D05zxn?A+UxS&viANPCj2sW3=~e+uuugGZ{T~3KgWizds9)Ilk{{8#0 z*Z1z-tFTSb)PBF;Y&I`L!p4X{72h|k(yD7nvCPgtWImgB-@bhY^UqIIIeE2zQum?* zFH1I=iZwrd&gr*Y_}Xhzd99B+>f0Zm+H~ak^X1)ZZ^}QCb#`Ov;gINPXoL|90(0eN z@G2^;xLIO#^z!9keMJGs-es3HpMADG_P7wpJIK$;;HmKCW#Es0dxTo`7(E_;lvsUr zmASyLk6I$T*c@I7w|t0ibh2lhz~c1cI)_U^U@SwB{5ruyb7K}RV_0&*pEuEOZ9p_b z&#n@CnnUg`&Qz%&=hj~Ix`-7_?(-~BR1!5Q{)U-V~ z>vEmJBUWJto5D}7gWZ{RObS~YA8<3AbmusyzLMJ^i|L>_!=!wHIer&K8ba8c)-$Ng zSFo79K$qcVzCaDT!cV4yC*Q>~dek=wa50=@=eSmG#_I4()+vKQ<+{L{cUe*mQ&c$T z&7a@QqOh|iA=h1(;ZlJ`i1oI$F^(HH%{FVWd&<=G%a7%P&29c=E8aOVyp`^CyQZ+> ztpmf|&8&`3U$7+TNelGI&ty2WocG|1Ku!mJUyfsa9t^)S9Sw?1m^MsjYdPnx#PEA& z+l7}JED!W13q;8`GTce$a$0?r<$=GNf=Qew!@sMdPK!fX9^}U=T#0jJ_;+2*Y3=&` zCpYm_&fEU0&N*$nMc>A#wHxBrXV)(e(Pg>)R_ylMZI=tq+`nW0<=h;1Q1l~bM27zi Z(;n^nX0cw-7+5SYc)I$ztaD0e0sv>2(uV*5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index c47cd6094..d24c4689f 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -68,7 +68,7 @@ android:layout_below="@id/toolbar_layout" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_explore_map.xml b/app/src/main/res/layout/fragment_explore_map.xml new file mode 100644 index 000000000..9d1246f80 --- /dev/null +++ b/app/src/main/res/layout/fragment_explore_map.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + +