diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt index f293ccc440..6d25db39aa 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt @@ -696,12 +696,12 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On } } - override fun onLocationChangedSignificantly(latLng: LatLng) { + override fun onLocationChangedSignificantly(latLng: LatLng?) { // Will be called if location changed more than 1000 meter updateClosestNearbyCardViewInfo() } - override fun onLocationChangedSlightly(latLng: LatLng) { + override fun onLocationChangedSlightly(latLng: LatLng?) { /* Update closest nearby notification card onLocationChangedSlightly */ try { @@ -711,7 +711,7 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On } } - override fun onLocationChangedMedium(latLng: LatLng) { + override fun onLocationChangedMedium(latLng: LatLng?) { // Update closest nearby card view if location changed more than 500 meters updateClosestNearbyCardViewInfo() } 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 index abf02758d5..bf37d019fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java @@ -144,8 +144,8 @@ public void onMediaClicked(int position) { */ @Override public Media getMediaAtPosition(int i) { - if (mapFragment != null && mapFragment.mediaList != null) { - return mapFragment.mediaList.get(i); + if (mapFragment != null && mapFragment.getMediaList() != null) { + return mapFragment.getMediaList().get(i); } else { return null; } @@ -159,8 +159,8 @@ public Media getMediaAtPosition(int i) { */ @Override public int getTotalMediaCount() { - if (mapFragment != null && mapFragment.mediaList != null) { - return mapFragment.mediaList.size(); + if (mapFragment != null && mapFragment.getMediaList() != null) { + return mapFragment.getMediaList().size(); } else { return 0; } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java deleted file mode 100644 index cf72691232..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java +++ /dev/null @@ -1,309 +0,0 @@ -package fr.free.nrw.commons.explore.depictions; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.FrameLayout; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.viewpager.widget.ViewPager; -import com.google.android.material.snackbar.Snackbar; -import com.google.android.material.tabs.TabLayout; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding; -import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; -import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.structure.depictions.DepictModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import fr.free.nrw.commons.wikidata.WikidataConstants; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; - -/** - * Activity to show depiction media, parent classes and child classes of depicted items in Explore - */ -public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailPagerFragment.MediaDetailProvider, - CategoryImagesCallback { - private FragmentManager supportFragmentManager; - private DepictedImagesFragment depictionImagesListFragment; - private MediaDetailPagerFragment mediaDetailPagerFragment; - - /** - * Name of the depicted item - * Ex: Rabbit - */ - - @Inject BookmarkItemsDao bookmarkItemsDao; - private CompositeDisposable compositeDisposable; - @Inject - DepictModel depictModel; - private String wikidataItemName; - private ActivityWikidataItemDetailsBinding binding; - - ViewPagerAdapter viewPagerAdapter; - private DepictedItem wikidataItem; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - compositeDisposable = new CompositeDisposable(); - supportFragmentManager = getSupportFragmentManager(); - viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setOffscreenPageLimit(2); - binding.tabLayout.setupWithViewPager(binding.viewPager); - - final DepictedItem depictedItem = getIntent().getParcelableExtra( - WikidataConstants.BOOKMARKS_ITEMS); - wikidataItem = depictedItem; - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTabs(); - setPageTitle(); - } - - /** - * Gets the passed wikidataItemName from the intents and displays it as the page title - */ - private void setPageTitle() { - if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) { - setTitle(getIntent().getStringExtra("wikidataItemName")); - } - } - - /** - * This method is called on success of API call for featured Images. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetailPagerFragment !=null){ - mediaDetailPagerFragment.notifyDataSetChanged(); - } - } - - /** - * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab, - * Set the fragments according to the tab selected in the viewPager. - */ - private void setTabs() { - List fragmentList = new ArrayList<>(); - List titleList = new ArrayList<>(); - depictionImagesListFragment = new DepictedImagesFragment(); - ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment(); - ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment(); - wikidataItemName = getIntent().getStringExtra("wikidataItemName"); - String entityId = getIntent().getStringExtra("entityId"); - if (getIntent() != null && wikidataItemName != null) { - Bundle arguments = new Bundle(); - arguments.putString("wikidataItemName", wikidataItemName); - arguments.putString("entityId", entityId); - depictionImagesListFragment.setArguments(arguments); - parentDepictionsFragment.setArguments(arguments); - childDepictionsFragment.setArguments(arguments); - } - fragmentList.add(depictionImagesListFragment); - titleList.add(getResources().getString(R.string.title_for_media)); - fragmentList.add(childDepictionsFragment); - titleList.add(getResources().getString(R.string.title_for_child_classes)); - fragmentList.add(parentDepictionsFragment); - titleList.add(getResources().getString(R.string.title_for_parent_classes)); - viewPagerAdapter.setTabData(fragmentList, titleList); - binding.viewPager.setOffscreenPageLimit(2); - viewPagerAdapter.notifyDataSetChanged(); - - } - - - /** - * Shows media detail fragment when user clicks on any image in the list - */ - @Override - public void onMediaClicked(int position) { - binding.tabLayout.setVisibility(View.GONE); - binding.viewPager.setVisibility(View.GONE); - binding.mediaContainer.setVisibility(View.VISIBLE); - if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); - FragmentManager supportFragmentManager = getSupportFragmentManager(); - supportFragmentManager - .beginTransaction() - .replace(R.id.mediaContainer, mediaDetailPagerFragment) - .addToBackStack(null) - .commit(); - supportFragmentManager.executePendingTransactions(); - } - mediaDetailPagerFragment.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) { - return depictionImagesListFragment.getMediaAtPosition(i); - } - - /** - * This method is called on backPressed of anyFragment in the activity. - * If condition is called when mediaDetailFragment is opened. - */ - @Override - public void onBackPressed() { - if (supportFragmentManager.getBackStackEntryCount() == 1){ - binding.tabLayout.setVisibility(View.VISIBLE); - binding.viewPager.setVisibility(View.VISIBLE); - binding.mediaContainer.setVisibility(View.GONE); - } - super.onBackPressed(); - } - - /** - * 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() { - return depictionImagesListFragment.getTotalMediaCount(); - } - - @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 (getSupportFragmentManager().getBackStackEntryCount() == 1) { - onBackPressed(); - onMediaClicked(index); - } - } - - /** - * Consumers should be simply using this method to use this activity. - * - * @param context A Context of the application package implementing this class. - * @param depictedItem Name of the depicts for displaying its details - */ - public static void startYourself(Context context, DepictedItem depictedItem) { - Intent intent = new Intent(context, WikidataItemDetailsActivity.class); - intent.putExtra("wikidataItemName", depictedItem.getName()); - intent.putExtra("entityId", depictedItem.getId()); - intent.putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem); - context.startActivity(intent); - } - - /** - * This function inflates the menu - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater menuInflater=getMenuInflater(); - menuInflater.inflate(R.menu.menu_wikidata_item,menu); - - updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item)); - - return super.onCreateOptionsMenu(menu); - } - - /** - * This method handles the logic on item select in toolbar menu - * Currently only 1 choice is available to open Wikidata item details page in browser - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()){ - case R.id.browser_actions_menu_items: - String entityId=getIntent().getStringExtra("entityId"); - Uri uri = Uri.parse("https://www.wikidata.org/wiki/" + entityId); - Utils.handleWebUrl(this, uri); - return true; - case R.id.menu_bookmark_current_item: - - if(getIntent().getStringExtra("fragment") != null) { - compositeDisposable.add(depictModel.getDepictions( - getIntent().getStringExtra("entityId") - ).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(depictedItems -> { - final boolean bookmarkExists = bookmarkItemsDao.updateBookmarkItem( - depictedItems.get(0)); - final Snackbar snackbar - = bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.add_bookmark, Snackbar.LENGTH_LONG) - : Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.remove_bookmark, - Snackbar.LENGTH_LONG); - - snackbar.show(); - updateBookmarkState(item); - })); - - } else { - final boolean bookmarkExists - = bookmarkItemsDao.updateBookmarkItem(wikidataItem); - final Snackbar snackbar - = bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.add_bookmark, Snackbar.LENGTH_LONG) - : Snackbar.make(findViewById(R.id.toolbar_layout), R.string.remove_bookmark, - Snackbar.LENGTH_LONG); - - snackbar.show(); - updateBookmarkState(item); - } - return true; - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void updateBookmarkState(final MenuItem item) { - final boolean isBookmarked; - if(getIntent().getStringExtra("fragment") != null) { - isBookmarked - = bookmarkItemsDao.findBookmarkItem(getIntent().getStringExtra("entityId")); - } else { - isBookmarked = bookmarkItemsDao.findBookmarkItem(wikidataItem.getId()); - } - final int icon - = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px - : R.drawable.menu_ic_round_star_border_24px; - item.setIcon(icon); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt new file mode 100644 index 0000000000..109f7e199a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt @@ -0,0 +1,303 @@ +package fr.free.nrw.commons.explore.depictions + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding +import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment +import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment +import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.upload.structure.depictions.DepictModel +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.wikidata.WikidataConstants +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +/** + * Activity to show depiction media, parent classes and child classes of depicted items in Explore + */ +class WikidataItemDetailsActivity : BaseActivity(), + MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { + + private lateinit var supportFragmentManager: FragmentManager + private lateinit var depictionImagesListFragment: DepictedImagesFragment + private var mediaDetailPagerFragment: MediaDetailPagerFragment? = null + + /** + * Name of the depicted item + * Ex: Rabbit + */ + @Inject + lateinit var bookmarkItemsDao: BookmarkItemsDao + + @Inject + lateinit var depictModel: DepictModel + private var wikidataItemName: String? = null + private lateinit var binding: ActivityWikidataItemDetailsBinding + + private lateinit var viewPagerAdapter: ViewPagerAdapter + private var wikidataItem: DepictedItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityWikidataItemDetailsBinding.inflate(layoutInflater) + setContentView(binding.root) + + supportFragmentManager = getSupportFragmentManager() + viewPagerAdapter = ViewPagerAdapter(supportFragmentManager) + binding.viewPager.adapter = viewPagerAdapter + binding.viewPager.offscreenPageLimit = 2 + binding.tabLayout.setupWithViewPager(binding.viewPager) + + wikidataItem = intent.getParcelableExtra(WikidataConstants.BOOKMARKS_ITEMS) + setSupportActionBar(binding.toolbarBinding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + setTabs() + setPageTitle() + } + + /** + * Gets the passed wikidataItemName from the intent and displays it as the page title + */ + private fun setPageTitle() { + intent.getStringExtra("wikidataItemName")?.let { + title = it + } + } + + /** + * This method is called on success of API call for featured images. + * The ViewPager will be notified that the number of items has changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetailPagerFragment?.notifyDataSetChanged() + } + + /** + * This activity contains 3 tabs and a ViewPager. + * This method is used to set the titles of tabs and the fragments according to the selected tab + */ + private fun setTabs() { + val fragmentList = mutableListOf() + val titleList = mutableListOf() + + depictionImagesListFragment = DepictedImagesFragment() + val childDepictionsFragment = ChildDepictionsFragment() + val parentDepictionsFragment = ParentDepictionsFragment() + + wikidataItemName = intent.getStringExtra("wikidataItemName") + val entityId = intent.getStringExtra("entityId") + + if (!wikidataItemName.isNullOrEmpty()) { + val arguments = Bundle().apply { + putString("wikidataItemName", wikidataItemName) + putString("entityId", entityId) + } + depictionImagesListFragment.arguments = arguments + parentDepictionsFragment.arguments = arguments + childDepictionsFragment.arguments = arguments + } + + fragmentList.apply { + add(depictionImagesListFragment) + add(childDepictionsFragment) + add(parentDepictionsFragment) + } + + titleList.apply { + add(getString(R.string.title_for_media)) + add(getString(R.string.title_for_child_classes)) + add(getString(R.string.title_for_parent_classes)) + } + + viewPagerAdapter.setTabData(fragmentList, titleList) + binding.viewPager.offscreenPageLimit = 2 + viewPagerAdapter.notifyDataSetChanged() + } + + /** + * Shows media detail fragment when user clicks on any image in the list + */ + override fun onMediaClicked(position: Int) { + binding.apply { + tabLayout.visibility = View.GONE + viewPager.visibility = View.GONE + mediaContainer.visibility = View.VISIBLE + } + + if (mediaDetailPagerFragment == null || mediaDetailPagerFragment?.isVisible == false) { + mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true) + supportFragmentManager = getSupportFragmentManager() + supportFragmentManager.beginTransaction() + .replace(R.id.mediaContainer, mediaDetailPagerFragment!!) + .addToBackStack(null) + .commit() + supportFragmentManager.executePendingTransactions() + } + + mediaDetailPagerFragment?.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 fun getMediaAtPosition(i: Int): Media? { + return depictionImagesListFragment.getMediaAtPosition(i) + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + override fun onBackPressed() { + if (supportFragmentManager.backStackEntryCount == 1) { + binding.apply { + tabLayout.visibility = View.VISIBLE + viewPager.visibility = View.VISIBLE + mediaContainer.visibility = View.GONE + } + } + super.onBackPressed() + } + + /** + * 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 fun getTotalMediaCount(): Int { + return depictionImagesListFragment.getTotalMediaCount() + } + + override fun getContributionStateAt(position: Int): Int? { + return null + } + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (supportFragmentManager.backStackEntryCount == 1) { + onBackPressed() + onMediaClicked(index) + } + } + + companion object { + /** + * Consumers should be simply using this method to use this activity. + * + * @param context a Context of the application package implementing this class. + * @param depictedItem Name of the depicts for displaying its details + */ + fun startYourself(context: Context, depictedItem: DepictedItem) { + val intent = Intent(context, WikidataItemDetailsActivity::class.java).apply { + putExtra("wikidataItemName", depictedItem.name) + putExtra("entityId", depictedItem.id) + putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem) + } + context.startActivity(intent) + } + } + + /** + * Inflates the menu + */ + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_wikidata_item, menu) + updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item)) + return super.onCreateOptionsMenu(menu) + } + + /** + * This method handles the logic on item select in toolbar menu + * Currently only 1 choice is available to open Wikidata item details page in browser + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.browser_actions_menu_items -> { + val entityId = intent.getStringExtra("entityId") + val uri = Uri.parse("https://www.wikidata.org/wiki/$entityId") + Utils.handleWebUrl(this, uri) + return true + } + R.id.menu_bookmark_current_item -> { + val entityId = intent.getStringExtra("entityId") + + if (intent.getStringExtra("fragment") != null) { + compositeDisposable.add( + depictModel.getDepictions(entityId!!) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { depictedItems -> + val bookmarkExists = bookmarkItemsDao + .updateBookmarkItem(depictedItems[0]) + val snackbarText = if (bookmarkExists) + R.string.add_bookmark + else + R.string.remove_bookmark + Snackbar.make( + findViewById(R.id.toolbar_layout), + snackbarText, + Snackbar.LENGTH_LONG + ).show() + updateBookmarkState(item) + } + ) + } else { + val bookmarkExists = bookmarkItemsDao.updateBookmarkItem(wikidataItem!!) + val snackbarText = if (bookmarkExists) + R.string.add_bookmark + else + R.string.remove_bookmark + Snackbar.make( + findViewById(R.id.toolbar_layout), + snackbarText, + Snackbar.LENGTH_LONG + ).show() + updateBookmarkState(item) + } + return true + } + android.R.id.home -> { + onBackPressed() + return true + } + else -> return super.onOptionsItemSelected(item) + } + } + + private fun updateBookmarkState(item: MenuItem) { + val isBookmarked = bookmarkItemsDao.findBookmarkItem( + intent.getStringExtra("entityId") ?: wikidataItem?.id + ) + val icon = if (isBookmarked) + R.drawable.menu_ic_round_star_filled_24px + else + R.drawable.menu_ic_round_star_border_24px + item.setIcon(icon) + } +} 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 deleted file mode 100644 index ec426942c3..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java +++ /dev/null @@ -1,31 +0,0 @@ -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 currentLatLng coordinates of search location - * @return list of places obtained - */ - List callCommonsQuery(final LatLng currentLatLng) { - String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude(); - return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt new file mode 100644 index 0000000000..0738f1899f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt @@ -0,0 +1,25 @@ +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 javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ExploreMapCalls @Inject constructor() { + + @Inject + lateinit var mediaClient: MediaClient + + /** + * Calls method to query Commons for uploads around a location + * + * @param currentLatLng coordinates of search location + * @return list of places obtained + */ + fun callCommonsQuery(currentLatLng: LatLng): List { + val coordinates = "${currentLatLng.latitude}|${currentLatLng.longitude}" + 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 deleted file mode 100644 index feb66bf55b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java +++ /dev/null @@ -1,45 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import android.content.Context; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; -import java.util.List; - -public class ExploreMapContract { - - interface View { - boolean isNetworkConnectionEstablished(); - void populatePlaces(LatLng curlatLng); - void askForLocationPermission(); - void recenterMap(LatLng curLatLng); - void hideBottomDetailsSheet(); - LatLng getMapCenter(); - LatLng getMapFocus(); - LatLng getLastMapFocus(); - void addMarkersToMap(final List nearbyBaseMarkers); - void clearAllMarkers(); - void addSearchThisAreaButtonAction(); - void setSearchThisAreaButtonVisibility(boolean isVisible); - void setProgressBarVisibility(boolean isVisible); - boolean isDetailsBottomSheetVisible(); - boolean isSearchThisAreaButtonVisible(); - Context getContext(); - LatLng getLastLocation(); - void disableFABRecenter(); - void enableFABRecenter(); - 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(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt new file mode 100644 index 0000000000..256e621e95 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.explore.map + +import android.content.Context +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager + +interface ExploreMapContract { + + interface View { + fun isNetworkConnectionEstablished(): Boolean + fun populatePlaces(curLatLng: LatLng?) + fun askForLocationPermission() + fun recenterMap(curLatLng: LatLng?) + fun hideBottomDetailsSheet() + fun getMapCenter(): LatLng + fun getMapFocus(): LatLng + fun getLastMapFocus(): LatLng + fun addMarkersToMap(nearbyBaseMarkers: List) + fun clearAllMarkers() + fun addSearchThisAreaButtonAction() + fun setSearchThisAreaButtonVisibility(isVisible: Boolean) + fun setProgressBarVisibility(isVisible: Boolean) + fun isDetailsBottomSheetVisible(): Boolean + fun isSearchThisAreaButtonVisible(): Boolean + fun getContext(): Context + fun getLastLocation(): LatLng + fun disableFABRecenter() + fun enableFABRecenter() + fun setFABRecenterAction(onClickListener: android.view.View.OnClickListener) + fun backButtonClicked(): Boolean + } + + interface UserActions { + fun updateMap(locationChangeType: LocationServiceManager.LocationChangeType) + fun lockUnlockNearby(isNearbyLocked: Boolean) + fun attachView(view: View) + fun detachView() + fun setActionListeners(applicationKvStore: JsonKvStore) + fun backButtonClicked(): Boolean + } +} 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 deleted file mode 100644 index c944f75a11..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java +++ /dev/null @@ -1,213 +0,0 @@ -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 fr.free.nrw.commons.BaseMarker; -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.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 currentLatLng, mediaList, - * explorePlaceList and boundaryCoordinates - * - * @param currentLatLng 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 currentLatLng, mediaList, explorePlaceList and - * boundaryCoordinates - */ - public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, 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.currentLatLng = currentLatLng; - 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.calculateDistance(bound.getLatitude(), - bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude()); - 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 = currentLatLng; - } - } 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 currentLatLng, - final List placeList, - Context context, - NearbyBaseMarkerThumbCallback callback, - ExplorePlacesInfo explorePlacesInfo) { - List baseMarkerList = new ArrayList<>(); - - if (placeList == null) { - return baseMarkerList; - } - - VectorDrawableCompat vectorDrawable = null; - try { - vectorDrawable = VectorDrawableCompat.create( - context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme()); - - } catch (Resources.NotFoundException e) { - // ignore when running tests. - } - if (vectorDrawable != null) { - for (Place explorePlace : placeList) { - final BaseMarker baseMarker = new BaseMarker(); - String distance = formatDistanceBetween(currentLatLng, explorePlace.location); - explorePlace.setDistance(distance); - - baseMarker.setTitle( - explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); - baseMarker.setPosition( - new fr.free.nrw.commons.location.LatLng( - explorePlace.location.getLatitude(), - explorePlace.location.getLongitude(), 0)); - baseMarker.setPlace(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) { - baseMarker.setIcon( - ImageUtils.addRedBorder(resource, 6, context)); - baseMarkerList.add(baseMarker); - if (baseMarkerList.size() - == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback - callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, - explorePlacesInfo); - } - } - - @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); - baseMarker.fromResource(context, R.drawable.image_placeholder_96); - baseMarkerList.add(baseMarker); - if (baseMarkerList.size() - == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback - callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, - explorePlacesInfo); - } - } - }); - } - } - return baseMarkerList; - } - - interface NearbyBaseMarkerThumbCallback { - - // Callback to notify thumbnails of explore markers are added as icons and ready - void onNearbyBaseMarkerThumbsReady(List baseMarkers, - ExplorePlacesInfo explorePlacesInfo); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt new file mode 100644 index 0000000000..391d9d5c52 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt @@ -0,0 +1,217 @@ +package fr.free.nrw.commons.explore.map + +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +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 fr.free.nrw.commons.BaseMarker +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.Place +import fr.free.nrw.commons.utils.ImageUtils +import fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween +import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween +import fr.free.nrw.commons.utils.LocationUtils +import fr.free.nrw.commons.utils.PlaceUtils +import timber.log.Timber +import javax.inject.Inject + +class ExploreMapController @Inject constructor( + private val exploreMapCalls: ExploreMapCalls +) : MapController() { + + var latestSearchLocation: LatLng? = null // Can be current and camera target when search this + // area button is used + var currentLocation: LatLng? = null // Current location of user + var latestSearchRadius: Double = 0.0 // Any last search radius + var currentLocationSearchRadius: Double = 0.0 // Search radius of only searches around current + // location + + /** + * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, + * mediaList, explorePlaceList and boundaryCoordinates + * + * @param currentLatLng 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 currentLatLng, mediaList, explorePlaceList and + * boundaryCoordinates + */ + fun loadAttractionsFromLocation( + currentLatLng: LatLng, + searchLatLng: LatLng?, + checkingAroundCurrentLocation: Boolean + ): ExplorePlacesInfo? { + + if (searchLatLng == null) { + Timber.d("Loading attractions explore map, but search is null") + return null + } + + val explorePlacesInfo = ExplorePlacesInfo() + try { + explorePlacesInfo.currentLatLng = currentLatLng + latestSearchLocation = searchLatLng + + val mediaList = exploreMapCalls.callCommonsQuery(searchLatLng) + val boundaryCoordinates = arrayOf( + mediaList[0].coordinates, // south + mediaList[0].coordinates, // north + mediaList[0].coordinates, // west + mediaList[0].coordinates // east, init with a random location + ) + + Timber.d("Sorting places by distance...") + val distances = mutableMapOf() + + for (media in mediaList) { + distances[media] = computeDistanceBetween(media.coordinates!!, searchLatLng) + + // Find boundaries with basic find max approach + if (media.coordinates!!.latitude < boundaryCoordinates[0]?.latitude!!) { + boundaryCoordinates[0] = media.coordinates + } + if (media.coordinates!!.latitude > boundaryCoordinates[1]?.latitude!!) { + boundaryCoordinates[1] = media.coordinates + } + if (media.coordinates!!.longitude < boundaryCoordinates[2]?.longitude!!) { + boundaryCoordinates[2] = media.coordinates + } + if (media.coordinates!!.longitude > boundaryCoordinates[3]?.longitude!!) { + boundaryCoordinates[3] = media.coordinates + } + } + + explorePlacesInfo.mediaList = mediaList + explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList) + explorePlacesInfo.boundaryCoordinates = boundaryCoordinates + + // Sets latestSearchRadius to maximum distance among boundaries and search location + for (bound in boundaryCoordinates) { + val distance = LocationUtils.calculateDistance( + bound?.latitude!!, bound.longitude, + searchLatLng.latitude, searchLatLng.longitude + ) + 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 = currentLatLng + } + } catch (e: Exception) { + 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 + */ + companion object { + fun loadAttractionsFromLocationToBaseMarkerOptions( + currentLatLng: LatLng, + placeList: List?, + context: Context, + callback: NearbyBaseMarkerThumbCallback, + explorePlacesInfo: ExplorePlacesInfo + ): List { + val baseMarkerList = mutableListOf() + + if (placeList == null) { + return baseMarkerList + } + + var vectorDrawable: VectorDrawableCompat? = null + try { + vectorDrawable = VectorDrawableCompat.create( + context.resources, R.drawable.ic_custom_map_marker_dark, context.theme + ) + } catch (e: Resources.NotFoundException) { + // Ignore when running tests + } + + vectorDrawable?.let { + for (explorePlace in placeList) { + val baseMarker = BaseMarker() + val distance = formatDistanceBetween(currentLatLng, explorePlace.location) + explorePlace.distance = distance + + baseMarker.title = explorePlace.name.substring( + 5, + explorePlace.name.lastIndexOf(".") + ) + baseMarker.position = LatLng( + explorePlace.location.latitude, + explorePlace.location.longitude, 0.0f + ) + baseMarker.place = explorePlace + + Glide.with(context) + .asBitmap() + .load(explorePlace.thumb) + .placeholder(R.drawable.image_placeholder_96) + .apply(RequestOptions().override(96, 96).centerCrop()) + .into(object : CustomTarget() { + // We add icons to markers when bitmaps are ready + override fun onResourceReady( + resource: Bitmap, + transition: Transition? + ) { + baseMarker.icon = ImageUtils.addRedBorder(resource, 6, context) + baseMarkerList.add(baseMarker) + if (baseMarkerList.size == placeList.size) { + // If true, we added all markers to list and can trigger thumbs + // ready callback + callback.onNearbyBaseMarkerThumbsReady( + baseMarkerList, + explorePlacesInfo + ) + } + } + + override fun onLoadCleared(placeholder: Drawable?) {} + + // We add thumbnail icon for images that couldn't be loaded + override fun onLoadFailed(errorDrawable: Drawable?) { + super.onLoadFailed(errorDrawable) + baseMarker.fromResource(context, R.drawable.image_placeholder_96) + baseMarkerList.add(baseMarker) + if (baseMarkerList.size == placeList.size) { + // If true, we added all markers to list and can trigger thumbs + // ready callback + callback.onNearbyBaseMarkerThumbsReady( + baseMarkerList, + explorePlacesInfo + ) + } + } + }) + } + } + return baseMarkerList + } + } + + interface NearbyBaseMarkerThumbCallback { + // Callback to notify thumbnails of explore markers are added as icons and ready + fun onNearbyBaseMarkerThumbsReady( + baseMarkers: List, + explorePlacesInfo: ExplorePlacesInfo + ) + } +} 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 deleted file mode 100644 index 1b16591828..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ /dev/null @@ -1,1011 +0,0 @@ -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.ZOOM_LEVEL; - -import android.Manifest.permission; -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.location.Location; -import android.location.LocationManager; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.BaseMarker; -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.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentExploreMapBinding; -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.LocationPermissionsHelper; -import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; -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.Place; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.MapUtils; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.SystemThemeUtils; -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.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import org.osmdroid.events.MapEventsReceiver; -import org.osmdroid.events.MapListener; -import org.osmdroid.events.ScrollEvent; -import org.osmdroid.events.ZoomEvent; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.util.constants.GeoConstants; -import org.osmdroid.views.CustomZoomButtonsController; -import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener; -import org.osmdroid.views.overlay.ItemizedOverlayWithFocus; -import org.osmdroid.views.overlay.MapEventsOverlay; -import org.osmdroid.views.overlay.Overlay; -import org.osmdroid.views.overlay.OverlayItem; -import org.osmdroid.views.overlay.ScaleBarOverlay; -import org.osmdroid.views.overlay.ScaleDiskOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import timber.log.Timber; - -public class ExploreMapFragment extends CommonsDaggerSupportFragment - implements ExploreMapContract.View, LocationUpdateListener, LocationPermissionCallback { - - 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; // last 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 BaseMarker clickedMarker; - private GeoPoint mapCenter; - private GeoPoint lastMapFocus; - 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; - LocationPermissionsHelper locationPermissionsHelper; - - // Nearby map state (if we came from Nearby) - private double prevZoom; - private double prevLatitude; - private double prevLongitude; - - private ExploreMapPresenter presenter; - - public FragmentExploreMapBinding binding; - - private ActivityResultLauncher activityResultLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - locationPermissionGranted(); - } else { - if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - DialogUtil.showAlertDialog(getActivity(), - getActivity().getString(R.string.location_permission_title), - getActivity().getString(R.string.location_permission_rationale_explore), - getActivity().getString(android.R.string.ok), - getActivity().getString(android.R.string.cancel), - () -> { - askForLocationPermission(); - }, - null, - null - ); - } else { - if (isPermissionDenied) { - locationPermissionsHelper.showAppSettingsDialog(getActivity(), - R.string.explore_map_needs_location); - } - Timber.d("The user checked 'Don't ask again' or denied the permission twice"); - isPermissionDenied = true; - } - } - }); - - @NonNull - public static ExploreMapFragment newInstance() { - ExploreMapFragment fragment = new ExploreMapFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState - ) { - loadNearbyMapData(); - binding = FragmentExploreMapBinding.inflate(getLayoutInflater()); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setSearchThisAreaButtonVisibility(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.tvAttribution.setText( - Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY)); - } else { - binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); - } - initNetworkBroadCastReceiver(); - locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager, - this); - if (presenter == null) { - presenter = new ExploreMapPresenter(bookmarkLocationDao); - } - setHasOptionsMenu(true); - - isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - isPermissionDenied = false; - presenter.attachView(this); - - initViews(); - presenter.setActionListeners(applicationKvStore); - - org.osmdroid.config.Configuration.getInstance().load(this.getContext(), - PreferenceManager.getDefaultSharedPreferences(this.getContext())); - - binding.mapView.setTileSource(TileSourceFactory.WIKIMEDIA); - binding.mapView.setTilesScaledToDpi(true); - - org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( - "Referer", "http://maps.wikimedia.org/" - ); - - ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView); - scaleBarOverlay.setScaleBarOffset(15, 25); - Paint barPaint = new Paint(); - barPaint.setARGB(200, 255, 250, 250); - scaleBarOverlay.setBackgroundPaint(barPaint); - scaleBarOverlay.enableScaleBar(); - binding.mapView.getOverlays().add(scaleBarOverlay); - binding.mapView.getZoomController() - .setVisibility(CustomZoomButtonsController.Visibility.NEVER); - binding.mapView.setMultiTouchControls(true); - - if (!isCameFromNearbyMap()) { - binding.mapView.getController().setZoom(ZOOM_LEVEL); - } - - performMapReadyActions(); - - binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - binding.mapView.invalidate(); - } else { - Timber.e("CLICKED MARKER IS NULL"); - } - if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { - // Back should first hide the bottom sheet if it is expanded - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } else if (isDetailsBottomSheetVisible()) { - hideBottomDetailsSheet(); - } - return true; - } - - @Override - public boolean longPressHelper(GeoPoint p) { - return false; - } - })); - - binding.mapView.addMapListener(new MapListener() { - @Override - public boolean onScroll(ScrollEvent event) { - if (getLastMapFocus() != null) { - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(binding.mapView.getMapCenter().getLatitude()); - dest_location.setLongitude(binding.mapView.getMapCenter().getLongitude()); - mylocation.setLatitude(getLastMapFocus().getLatitude()); - mylocation.setLongitude(getLastMapFocus().getLongitude()); - Float distance = mylocation.distanceTo(dest_location);//in meters - if (getLastMapFocus() != null) { - if (isNetworkConnectionEstablished() && (event.getX() > 0 - || event.getY() > 0)) { - if (distance > 2000.0) { - setSearchThisAreaButtonVisibility(true); - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } else { - setSearchThisAreaButtonVisibility(false); - } - } - - return true; - } - - @Override - public boolean onZoom(ZoomEvent event) { - return false; - } - - }); - if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { - askForLocationPermission(); - } - } - - @Override - public void onResume() { - super.onResume(); - binding.mapView.onResume(); - presenter.attachView(this); - registerNetworkReceiver(); - if (isResumed()) { - if (locationPermissionsHelper.checkLocationPermission(getActivity())) { - performMapReadyActions(); - } else { - startMapWithoutPermission(); - } - } - } - - @Override - public void onPause() { - super.onPause(); - // unregistering the broadcastReceiver, as it was causing an exception and a potential crash - unregisterNetworkReceiver(); - } - - - /** - * Unregisters the networkReceiver - */ - private void unregisterNetworkReceiver() { - if (getActivity() != null) { - getActivity().unregisterReceiver(broadcastReceiver); - } - } - - private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.getDefaultLatLng(); - moveCameraToPosition( - new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); - presenter.onMapReady(exploreMapController); - } - - private void registerNetworkReceiver() { - if (getActivity() != null) { - getActivity().registerReceiver(broadcastReceiver, intentFilter); - } - } - - private void performMapReadyActions() { - if (isDarkTheme) { - binding.mapView.getOverlayManager().getTilesOverlay() - .setColorFilter(TilesOverlay.INVERT_COLORS); - } - if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && - !locationPermissionsHelper.checkLocationPermission(getActivity())) { - isPermissionDenied = true; - } - lastKnownLocation = MapUtils.getDefaultLatLng(); - - // if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom - if (isCameFromNearbyMap()) { - moveCameraToPosition( - new GeoPoint(prevLatitude, prevLongitude), - prevZoom, - 1L - ); - } else { - moveCameraToPosition( - new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); - } - presenter.onMapReady(exploreMapController); - } - - /** - * Fetch Nearby map camera data from fragment arguments if any. - */ - public void loadNearbyMapData() { - // get fragment arguments - if (getArguments() != null) { - prevZoom = getArguments().getDouble("prev_zoom"); - prevLatitude = getArguments().getDouble("prev_latitude"); - prevLongitude = getArguments().getDouble("prev_longitude"); - } - } - - /** - * Checks if fragment arguments contain data from Nearby map, indicating that the user navigated - * from Nearby using 'Show in Explore'. - * - * @return true if user navigated from Nearby map - **/ - public boolean isCameFromNearbyMap() { - return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0; - } - - public void loadNearbyMapFromExplore() { - ((MainActivity) getContext()).loadNearbyMapFromExplore( - binding.mapView.getZoomLevelDouble(), - binding.mapView.getMapCenter().getLatitude(), - binding.mapView.getMapCenter().getLongitude() - ); - } - - 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( - binding.bottomSheetDetailsBinding.getRoot()); - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - binding.bottomSheetDetailsBinding.getRoot().setVisibility(View.VISIBLE); - } - - /** - * Defines how bottom sheets will act on click - */ - private void setBottomSheetCallbacks() { - binding.bottomSheetDetailsBinding.getRoot().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 (latLng != null) { - handleLocationUpdate(latLng, LOCATION_SIGNIFICANTLY_CHANGED); - } - } - - @Override - public void onLocationChangedSlightly(LatLng latLng) { - Timber.d("Location slightly changed"); - if (latLng != null) {//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 currentLatLng) { - final Observable nearbyPlacesInfoObservable; - if (currentLatLng == null) { - return; - } - if (currentLatLng.equals( - getLastMapFocus())) { // Means we are checking around current location - nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(currentLatLng, - getLastMapFocus(), true); - } else { - nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(getLastMapFocus(), - currentLatLng, false); - } - getCompositeDisposable().add(nearbyPlacesInfoObservable - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(explorePlacesInfo -> { - mediaList = explorePlacesInfo.mediaList; - if (mediaList == null) { - showResponseMessage(getString(R.string.no_pictures_in_this_area)); - } - updateMapMarkers(explorePlacesInfo); - lastMapFocus = new GeoPoint(currentLatLng.getLatitude(), - currentLatLng.getLongitude()); - }, - throwable -> { - Timber.d(throwable); - // Not showing the user, throwable localizedErrorMessage - showErrorMessage(getString(R.string.error_fetching_nearby_places)); - - 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) { - presenter.updateMapMarkers(explorePlacesInfo); - } - - private void showErrorMessage(final String message) { - ViewUtil.showLongToast(getActivity(), message); - } - - private void showResponseMessage(final String message) { - ViewUtil.showLongSnackbar(getView(), message); - } - - @Override - public void askForLocationPermission() { - Timber.d("Asking for location permission"); - activityResultLauncher.launch(permission.ACCESS_FINE_LOCATION); - } - - private void locationPermissionGranted() { - isPermissionDenied = false; - applicationKvStore.putBoolean("doNotAskForLocationPermission", false); - lastKnownLocation = locationManager.getLastLocation(); - fr.free.nrw.commons.location.LatLng target = lastKnownLocation; - if (lastKnownLocation != null) { - GeoPoint targetP = new GeoPoint(target.getLatitude(), target.getLongitude()); - mapCenter = targetP; - binding.mapView.getController().setCenter(targetP); - recenterMarkerToPosition(targetP); - moveCameraToPosition(targetP); - } else if (locationManager.isGPSProviderEnabled() - || locationManager.isNetworkProviderEnabled()) { - locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER); - locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - setProgressBarVisibility(true); - } else { - locationPermissionsHelper.showLocationOffDialog(getActivity(), - R.string.ask_to_turn_location_on_text); - } - presenter.onMapReady(exploreMapController); - registerUnregisterLocationListener(false); - } - - public void registerUnregisterLocationListener(final boolean removeLocationListener) { - MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this); - } - - @Override - public void recenterMap(LatLng currentLatLng) { - // if user has denied permission twice, then show dialog - if (isPermissionDenied) { - if (locationPermissionsHelper.checkLocationPermission(getActivity())) { - // this will run when user has given permission by opening app's settings - isPermissionDenied = false; - recenterMap(currentLatLng); - } else { - askForLocationPermission(); - } - } else { - if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { - askForLocationPermission(); - } else { - locationPermissionGranted(); - } - } - if (currentLatLng == null) { - recenterToUserLocation = true; - return; - } - recenterMarkerToPosition( - new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude())); - binding.mapView.getController() - .animateTo(new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude())); - if (lastMapFocus != null) { - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(binding.mapView.getMapCenter().getLatitude()); - dest_location.setLongitude(binding.mapView.getMapCenter().getLongitude()); - mylocation.setLatitude(lastMapFocus.getLatitude()); - mylocation.setLongitude(lastMapFocus.getLongitude()); - Float distance = mylocation.distanceTo(dest_location);//in meters - if (lastMapFocus != null) { - if (isNetworkConnectionEstablished()) { - if (distance > 2000.0) { - setSearchThisAreaButtonVisibility(true); - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } - - @Override - public void hideBottomDetailsSheet() { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } - - /** - * 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) { - binding.bottomSheetDetailsBinding.directionsButton.setOnClickListener( - view -> Utils.handleGeoCoordinates(getActivity(), - place.getLocation(), binding.mapView.getZoomLevelDouble())); - - binding.bottomSheetDetailsBinding.commonsButton.setVisibility( - place.hasCommonsLink() ? View.VISIBLE : View.GONE); - binding.bottomSheetDetailsBinding.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; - binding.bottomSheetDetailsBinding.mediaDetailsButton.setOnClickListener(view -> { - ((ExploreMapRootFragment) getParentFragment()).onMediaClicked(finalIndex); - }); - } - index++; - } - binding.bottomSheetDetailsBinding.title.setText( - place.name.substring(5, place.name.lastIndexOf("."))); - binding.bottomSheetDetailsBinding.category.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 - binding.bottomSheetDetailsBinding.description.setText(descriptionText); - } - - @Override - public void addSearchThisAreaButtonAction() { - binding.searchThisAreaButton.setOnClickListener(presenter.onSearchThisAreaClicked()); - } - - @Override - public void setSearchThisAreaButtonVisibility(boolean isVisible) { - binding.searchThisAreaButton.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - - @Override - public void setProgressBarVisibility(boolean isVisible) { - binding.mapProgressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - - @Override - public boolean isDetailsBottomSheetVisible() { - if (binding.bottomSheetDetailsBinding.getRoot().getVisibility() == View.VISIBLE) { - return true; - } else { - return false; - } - } - - @Override - public boolean isSearchThisAreaButtonVisible() { - return binding.bottomSheetDetailsBinding.getRoot().getVisibility() == View.VISIBLE; - } - - @Override - public LatLng getLastLocation() { - if (lastKnownLocation == null) { - lastKnownLocation = locationManager.getLastLocation(); - } - return lastKnownLocation; - } - - @Override - public void disableFABRecenter() { - binding.fabRecenter.setEnabled(false); - } - - @Override - public void enableFABRecenter() { - binding.fabRecenter.setEnabled(true); - } - - /** - * Adds a markers to the map based on the list of NearbyBaseMarker. - * - * @param nearbyBaseMarkers The NearbyBaseMarker object representing the markers to be added. - */ - @Override - public void addMarkersToMap(List nearbyBaseMarkers) { - clearAllMarkers(); - for (int i = 0; i < nearbyBaseMarkers.size(); i++) { - addMarkerToMap(nearbyBaseMarkers.get(i)); - } - binding.mapView.invalidate(); - } - - /** - * Adds a marker to the map based on the specified NearbyBaseMarker. - * - * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added. - */ - private void addMarkerToMap(BaseMarker nearbyBaseMarker) { - if (isAttachedToActivity()) { - ArrayList items = new ArrayList<>(); - Bitmap icon = nearbyBaseMarker.getIcon(); - Drawable d = new BitmapDrawable(getResources(), icon); - GeoPoint point = new GeoPoint( - nearbyBaseMarker.getPlace().location.getLatitude(), - nearbyBaseMarker.getPlace().location.getLongitude()); - OverlayItem item = new OverlayItem(nearbyBaseMarker.getPlace().name, null, - point); - item.setMarker(d); - items.add(item); - ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items, - new OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(int index, OverlayItem item) { - final Place place = nearbyBaseMarker.getPlace(); - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetDetailsBehavior.setState( - BottomSheetBehavior.STATE_COLLAPSED); - } - clickedMarker = nearbyBaseMarker; - passInfoToSheet(place); - return true; - } - - @Override - public boolean onItemLongPress(int index, OverlayItem item) { - return false; - } - }, getContext()); - - overlay.setFocusItemsOnTap(true); - binding.mapView.getOverlays().add(overlay); // Add the overlay to the map - } - } - - /** - * Removes a marker from the map based on the specified NearbyBaseMarker. - * - * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be removed. - */ - private void removeMarker(BaseMarker nearbyBaseMarker) { - Place place = nearbyBaseMarker.getPlace(); - List overlays = binding.mapView.getOverlays(); - ItemizedOverlayWithFocus item; - - for (int i = 0; i < overlays.size(); i++) { - if (overlays.get(i) instanceof ItemizedOverlayWithFocus) { - item = (ItemizedOverlayWithFocus) overlays.get(i); - OverlayItem overlayItem = item.getItem(0); - - if (place.location.getLatitude() == overlayItem.getPoint().getLatitude() - && place.location.getLongitude() == overlayItem.getPoint().getLongitude()) { - binding.mapView.getOverlays().remove(i); - binding.mapView.invalidate(); - break; - } - } - } - } - - /** - * Clears all markers from the map and resets certain map overlays and gestures. After clearing - * markers, it re-adds a scale bar overlay and rotation gesture overlay to the map. - */ - @Override - public void clearAllMarkers() { - if (isAttachedToActivity()) { - binding.mapView.getOverlayManager().clear(); - GeoPoint geoPoint = mapCenter; - if (geoPoint != null) { - List overlays = binding.mapView.getOverlays(); - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this.getContext(), - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - binding.mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - binding.mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this.getContext(), - R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - binding.mapView.getOverlays().add(startMarker); - } - ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView); - scaleBarOverlay.setScaleBarOffset(15, 25); - Paint barPaint = new Paint(); - barPaint.setARGB(200, 255, 250, 250); - scaleBarOverlay.setBackgroundPaint(barPaint); - scaleBarOverlay.enableScaleBar(); - binding.mapView.getOverlays().add(scaleBarOverlay); - binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - binding.mapView.invalidate(); - } else { - Timber.e("CLICKED MARKER IS NULL"); - } - if (bottomSheetDetailsBehavior.getState() - == BottomSheetBehavior.STATE_EXPANDED) { - // Back should first hide the bottom sheet if it is expanded - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } else if (isDetailsBottomSheetVisible()) { - hideBottomDetailsSheet(); - } - return true; - } - - @Override - public boolean longPressHelper(GeoPoint p) { - return false; - } - })); - binding.mapView.setMultiTouchControls(true); - } - } - - /** - * Recenters the map view to the specified GeoPoint and updates the marker to indicate the new - * position. - * - * @param geoPoint The GeoPoint representing the new center position for the map. - */ - private void recenterMarkerToPosition(GeoPoint geoPoint) { - if (geoPoint != null) { - binding.mapView.getController().setCenter(geoPoint); - List overlays = binding.mapView.getOverlays(); - for (int i = 0; i < overlays.size(); i++) { - if (overlays.get(i) instanceof org.osmdroid.views.overlay.Marker) { - binding.mapView.getOverlays().remove(i); - } else if (overlays.get(i) instanceof ScaleDiskOverlay) { - binding.mapView.getOverlays().remove(i); - } - } - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this.getContext(), - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - binding.mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - binding.mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this.getContext(), R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - binding.mapView.getOverlays().add(startMarker); - } - } - - /** - * Moves the camera of the map view to the specified GeoPoint using an animation. - * - * @param geoPoint The GeoPoint representing the new camera position for the map. - */ - private void moveCameraToPosition(GeoPoint geoPoint) { - binding.mapView.getController().animateTo(geoPoint); - } - - /** - * Moves the camera of the map view to the specified GeoPoint at specified zoom level and speed - * using an animation. - * - * @param geoPoint The GeoPoint representing the new camera position for the map. - * @param zoom Zoom level of the map camera - * @param speed Speed of animation - */ - private void moveCameraToPosition(GeoPoint geoPoint, double zoom, long speed) { - binding.mapView.getController().animateTo(geoPoint, zoom, speed); - } - - @Override - public fr.free.nrw.commons.location.LatLng getLastMapFocus() { - return lastMapFocus == null ? getMapCenter() : new fr.free.nrw.commons.location.LatLng( - lastMapFocus.getLatitude(), lastMapFocus.getLongitude(), 100); - } - - @Override - public fr.free.nrw.commons.location.LatLng getMapCenter() { - fr.free.nrw.commons.location.LatLng latLnge = null; - if (mapCenter != null) { - latLnge = new fr.free.nrw.commons.location.LatLng( - mapCenter.getLatitude(), mapCenter.getLongitude(), 100); - } else { - if (applicationKvStore.getString("LastLocation") != null) { - final String[] locationLatLng - = applicationKvStore.getString("LastLocation").split(","); - lastKnownLocation - = new fr.free.nrw.commons.location.LatLng(Double.parseDouble(locationLatLng[0]), - Double.parseDouble(locationLatLng[1]), 1f); - latLnge = lastKnownLocation; - } else { - latLnge = new fr.free.nrw.commons.location.LatLng(51.506255446947776, - -0.07483536015053005, 1f); - } - } - if (!isCameFromNearbyMap()) { - moveCameraToPosition(new GeoPoint(latLnge.getLatitude(), latLnge.getLongitude())); - } - return latLnge; - } - - @Override - public fr.free.nrw.commons.location.LatLng getMapFocus() { - fr.free.nrw.commons.location.LatLng mapFocusedLatLng = new fr.free.nrw.commons.location.LatLng( - binding.mapView.getMapCenter().getLatitude(), - binding.mapView.getMapCenter().getLongitude(), 100); - return mapFocusedLatLng; - } - - @Override - public void setFABRecenterAction(OnClickListener onClickListener) { - binding.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(); - } - } - } - }; - } - - /** - * helper function to confirm that this fragment has been attached. - **/ - public boolean isAttachedToActivity() { - boolean attached = isVisible() && getActivity() != null; - return attached; - } - - @Override - public void onLocationPermissionDenied(String toastMessage) { - } - - @Override - public void onLocationPermissionGranted() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt new file mode 100644 index 0000000000..9daa1dbfb8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt @@ -0,0 +1,937 @@ +package fr.free.nrw.commons.explore.map + +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.graphics.Color +import android.graphics.Paint +import android.graphics.drawable.BitmapDrawable +import android.location.Location +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.Html +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.BaseMarker +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.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentExploreMapBinding +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.LocationPermissionsHelper +import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback +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.Place +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.MapUtils +import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL +import fr.free.nrw.commons.utils.NetworkUtils +import fr.free.nrw.commons.utils.SystemThemeUtils +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.osmdroid.events.MapEventsReceiver +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.util.constants.GeoConstants +import org.osmdroid.views.CustomZoomButtonsController +import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener +import org.osmdroid.views.overlay.ItemizedOverlayWithFocus +import org.osmdroid.views.overlay.MapEventsOverlay +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.ScaleBarOverlay +import org.osmdroid.views.overlay.ScaleDiskOverlay +import org.osmdroid.views.overlay.TilesOverlay +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Named + +class ExploreMapFragment : CommonsDaggerSupportFragment(), + ExploreMapContract.View, LocationUpdateListener, LocationPermissionCallback { + + private lateinit var bottomSheetDetailsBehavior: BottomSheetBehavior<*> + private var broadcastReceiver: BroadcastReceiver? = null + private var isNetworkErrorOccurred = false + private var snackbar: Snackbar? = null + private var isDarkTheme = false + private var isPermissionDenied = false + private var lastKnownLocation: LatLng? = null // last location of user + private var lastFocusLocation: LatLng? = null // last focused location of the map + var mediaList: List? = null + private var recenterToUserLocation = false // true if recentering is needed + private var clickedMarker: BaseMarker? = null + private var mapCenter: GeoPoint? = null + private var lastMapFocus: GeoPoint? = null + private val intentFilter = IntentFilter(MapUtils.NETWORK_INTENT_ACTION) + + @Inject + lateinit var liveDataConverter: LiveDataConverter + + @Inject + lateinit var mediaClient: MediaClient + + @Inject + lateinit var locationManager: LocationServiceManager + + @Inject + lateinit var exploreMapController: ExploreMapController + + @Inject + @Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + @Inject + lateinit var bookmarkLocationDao: BookmarkLocationsDao // Future use for bookmarking explore places + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + private lateinit var locationPermissionsHelper: LocationPermissionsHelper + + // Nearby map state (if we came from Nearby) + private var prevZoom = 0.0 + private var prevLatitude = 0.0 + private var prevLongitude = 0.0 + + private var presenter: ExploreMapPresenter? = null + + private lateinit var binding: FragmentExploreMapBinding + + private val activityResultLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + locationPermissionGranted() + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + activity?.let { + DialogUtil.showAlertDialog( + it, + getString(R.string.location_permission_title), + getString(R.string.location_permission_rationale_explore), + getString(android.R.string.ok), + getString(android.R.string.cancel), + { + askForLocationPermission() + }, + null, + null + ) + } + } else { + if (isPermissionDenied) { + locationPermissionsHelper.showAppSettingsDialog( + requireActivity(), + R.string.explore_map_needs_location + ) + } + Timber.d("The user checked 'Don't ask again' or denied the permission twice") + isPermissionDenied = true + } + } + } + + companion object { + @JvmStatic + fun newInstance(): ExploreMapFragment { + return ExploreMapFragment().apply { retainInstance = true } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + loadNearbyMapData() + binding = FragmentExploreMapBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setSearchThisAreaButtonVisibility(false) + + binding.tvAttribution.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY) + } else { + Html.fromHtml(getString(R.string.map_attribution)) + } + + initNetworkBroadCastReceiver() + locationPermissionsHelper = LocationPermissionsHelper( + requireActivity(), + locationManager, + this + ) + + if (presenter == null) { + presenter = ExploreMapPresenter(bookmarkLocationDao) + } + setHasOptionsMenu(true) + + isDarkTheme = systemThemeUtils.isDeviceInNightMode() + isPermissionDenied = false + presenter?.attachView(this) + + initViews() + presenter?.setActionListeners(applicationKvStore) + + org.osmdroid.config.Configuration.getInstance().load( + requireContext(), + PreferenceManager.getDefaultSharedPreferences(requireContext()) + ) + + binding.mapView.apply { + setTileSource(TileSourceFactory.WIKIMEDIA) + setTilesScaledToDpi(true) + org.osmdroid.config.Configuration.getInstance() + .additionalHttpRequestProperties["Referer"] = "http://maps.wikimedia.org/" + + val scaleBarOverlay = ScaleBarOverlay(this).apply { + setScaleBarOffset(15, 25) + setBackgroundPaint( + Paint().apply { + setARGB(200, 255, 250, 250) + } + ) + enableScaleBar() + } + overlays.add(scaleBarOverlay) + + zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) + setMultiTouchControls(true) + + if (!isCameFromNearbyMap()) { + controller.setZoom(ZOOM_LEVEL.toDouble()) + } + } + + performMapReadyActions() + + binding.mapView.overlays.add( + MapEventsOverlay(object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean { + clickedMarker?.let { + removeMarker(it) + addMarkerToMap(it) + binding.mapView.invalidate() + } ?: Timber.e("CLICKED MARKER IS NULL") + + if (bottomSheetDetailsBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet() + } + return true + } + + override fun longPressHelper(p: GeoPoint?): Boolean = false + }) + ) + + + binding.mapView.addMapListener(object : MapListener { + override fun onScroll(event: ScrollEvent?): Boolean { + lastMapFocus?.let { + val myLocation = Location("").apply { + latitude = it.latitude + longitude = it.longitude + } + + val destLocation = Location("").apply { + latitude = binding.mapView.mapCenter.latitude + longitude = binding.mapView.mapCenter.longitude + } + + val distance = myLocation.distanceTo(destLocation) + + if ( + isNetworkConnectionEstablished() + && + (event?.x!! > 0 || event.y > 0) + ) { + setSearchThisAreaButtonVisibility(distance > 2000.0) + } + } ?: setSearchThisAreaButtonVisibility(false) + + return true + } + + override fun onZoom(event: ZoomEvent?): Boolean = false + }) + + if (!locationPermissionsHelper.checkLocationPermission(requireActivity())) { + askForLocationPermission() + } + } + + override fun onResume() { + super.onResume() + binding.mapView.onResume() + presenter?.attachView(this) + registerNetworkReceiver() + + if (isResumed) { + if (activity?.let { locationPermissionsHelper.checkLocationPermission(it) } == true) { + performMapReadyActions() + } else { + startMapWithoutPermission() + } + } + } + + override fun onPause() { + super.onPause() + // Unregistering the broadcastReceiver to prevent crashes + unregisterNetworkReceiver() + } + + /** + * Unregisters the networkReceiver + */ + private fun unregisterNetworkReceiver() { + activity?.unregisterReceiver(broadcastReceiver) + } + + private fun startMapWithoutPermission() { + lastKnownLocation = MapUtils.defaultLatLng + moveCameraToPosition(GeoPoint(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude)) + presenter?.onMapReady(exploreMapController) + } + + private fun registerNetworkReceiver() { + activity?.registerReceiver(broadcastReceiver, intentFilter) + } + + private fun performMapReadyActions() { + if (isDarkTheme) { + binding.mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) + } + if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && + !locationPermissionsHelper.checkLocationPermission(requireActivity()) + ) { + isPermissionDenied = true + } + + lastKnownLocation = MapUtils.defaultLatLng + + // If user came from 'Show in Explore' in Nearby, load saved map center and zoom + if (isCameFromNearbyMap()) { + moveCameraToPosition(GeoPoint(prevLatitude, prevLongitude), prevZoom, 1L) + } else { + moveCameraToPosition( + GeoPoint(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude) + ) + } + + presenter?.onMapReady(exploreMapController) + } + + /** + * Fetch Nearby map camera data from fragment arguments if available. + */ + fun loadNearbyMapData() { + arguments?.let { + prevZoom = it.getDouble("prev_zoom") + prevLatitude = it.getDouble("prev_latitude") + prevLongitude = it.getDouble("prev_longitude") + } + } + + /** + * Checks if fragment arguments contain data from the Nearby map, + * indicating that the user navigated from Nearby using 'Show in Explore'. + * + * @return true if user navigated from Nearby map + */ + fun isCameFromNearbyMap(): Boolean { + return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0 + } + + fun loadNearbyMapFromExplore() { + (requireContext() as MainActivity).loadNearbyMapFromExplore( + binding.mapView.zoomLevelDouble, + binding.mapView.mapCenter.latitude, + binding.mapView.mapCenter.longitude + ) + } + + private fun initViews() { + Timber.d("init views called") + initBottomSheets() + setBottomSheetCallbacks() + } + + /** + * a) Creates bottom sheet behaviors 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 fun initBottomSheets() { + bottomSheetDetailsBehavior = BottomSheetBehavior.from(binding.bottomSheetDetailsBinding.root) + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + binding.bottomSheetDetailsBinding.root.visibility = View.VISIBLE + } + + /** + * Defines how bottom sheets will act on click + */ + private fun setBottomSheetCallbacks() { + binding.bottomSheetDetailsBinding.root.setOnClickListener { + bottomSheetDetailsBehavior.state = when (bottomSheetDetailsBehavior.state) { + BottomSheetBehavior.STATE_COLLAPSED -> BottomSheetBehavior.STATE_EXPANDED + BottomSheetBehavior.STATE_EXPANDED -> BottomSheetBehavior.STATE_COLLAPSED + else -> bottomSheetDetailsBehavior.state + } + } + } + + override fun onLocationChangedSignificantly(latLng: LatLng?) { + Timber.d("Location significantly changed") + latLng?.let { handleLocationUpdate(it, LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) } + } + + override fun onLocationChangedSlightly(latLng: LatLng?) { + Timber.d("Location slightly changed") + latLng?.let { handleLocationUpdate(it, LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED) } + } + + private fun handleLocationUpdate( + latLng: LatLng, + locationChangeType: LocationServiceManager.LocationChangeType + ) { + lastKnownLocation = latLng + exploreMapController.currentLocation = lastKnownLocation + presenter?.updateMap(locationChangeType) + } + + override fun onLocationChangedMedium(latLng: LatLng?) { + // No implementation required + } + + override fun isNetworkConnectionEstablished(): Boolean { + return NetworkUtils.isInternetConnectionEstablished(activity) + } + + override fun populatePlaces(curLatLng: LatLng?) { + if (curLatLng == null) return + + val nearbyPlacesInfoObservable: Observable = + if (curLatLng == getLastMapFocus()) { + // Checking around current location + presenter!!.loadAttractionsFromLocation(curLatLng, getLastMapFocus(), true) + } else { + presenter!!.loadAttractionsFromLocation(getLastMapFocus(), curLatLng, false) + } + + compositeDisposable.add( + nearbyPlacesInfoObservable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ explorePlacesInfo -> + mediaList = explorePlacesInfo.mediaList + if (mediaList.isNullOrEmpty()) { + showResponseMessage(getString(R.string.no_pictures_in_this_area)) + } + updateMapMarkers(explorePlacesInfo) + lastMapFocus = GeoPoint(curLatLng.latitude, curLatLng.longitude) + }, { throwable -> + Timber.d(throwable) + showErrorMessage(getString(R.string.error_fetching_nearby_places)) + 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 fun updateMapMarkers(explorePlacesInfo: MapController.ExplorePlacesInfo) { + presenter?.updateMapMarkers(explorePlacesInfo) + } + + private fun showErrorMessage(message: String) { + ViewUtil.showLongToast(requireActivity(), message) + } + + private fun showResponseMessage(message: String) { + ViewUtil.showLongSnackbar(requireView(), message) + } + + override fun askForLocationPermission() { + Timber.d("Asking for location permission") + activityResultLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) + } + + private fun locationPermissionGranted() { + isPermissionDenied = false + applicationKvStore.putBoolean("doNotAskForLocationPermission", false) + lastKnownLocation = locationManager.getLastLocation() + val target = lastKnownLocation + + if (lastKnownLocation != null) { + val targetP = GeoPoint(target!!.latitude, target.longitude) + mapCenter = targetP + binding.mapView.controller.setCenter(targetP) + recenterMarkerToPosition(targetP) + moveCameraToPosition(targetP) + } else if ( + locationManager.isGPSProviderEnabled() || locationManager.isNetworkProviderEnabled() + ) { + locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + setProgressBarVisibility(true) + } else { + locationPermissionsHelper.showLocationOffDialog( + requireActivity(), + R.string.ask_to_turn_location_on_text + ) + } + presenter?.onMapReady(exploreMapController) + registerUnregisterLocationListener(false) + } + + fun registerUnregisterLocationListener(removeLocationListener: Boolean) { + MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this) + } + + override fun recenterMap(currentLatLng: LatLng?) { + if (isPermissionDenied) { + if (locationPermissionsHelper.checkLocationPermission(requireActivity())) { + isPermissionDenied = false + recenterMap(currentLatLng) + } else { + askForLocationPermission() + } + } else { + if (!locationPermissionsHelper.checkLocationPermission(requireActivity())) { + askForLocationPermission() + } else { + locationPermissionGranted() + } + } + + if (currentLatLng == null) { + recenterToUserLocation = true + return + } + + recenterMarkerToPosition(GeoPoint(currentLatLng.latitude, currentLatLng.longitude)) + binding.mapView.controller.animateTo( + GeoPoint(currentLatLng.latitude, currentLatLng.longitude) + ) + + lastMapFocus?.let { + val myLocation = Location("").apply { + latitude = it.latitude + longitude = it.longitude + } + val destLocation = Location("").apply { + latitude = binding.mapView.mapCenter.latitude + longitude = binding.mapView.mapCenter.longitude + } + val distance = myLocation.distanceTo(destLocation) + + if (isNetworkConnectionEstablished()) { + setSearchThisAreaButtonVisibility(distance > 2000.0) + } else { + setSearchThisAreaButtonVisibility(false) + } + } ?: setSearchThisAreaButtonVisibility(false) + } + + override fun hideBottomDetailsSheet() { + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + /** + * 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 fun passInfoToSheet(place: Place) { + binding.bottomSheetDetailsBinding.directionsButton.setOnClickListener { + Utils.handleGeoCoordinates(activity, place.location, binding.mapView.zoomLevelDouble) + } + + binding.bottomSheetDetailsBinding.commonsButton.visibility = + if (place.hasCommonsLink()) View.VISIBLE else View.GONE + + binding.bottomSheetDetailsBinding.commonsButton.setOnClickListener { + Utils.handleWebUrl(context, place.siteLinks.commonsLink) + } + + mediaList?.indexOfFirst { it.filename == place.name }.takeIf { it!! >= 0 }?.let { index -> + binding.bottomSheetDetailsBinding.mediaDetailsButton.setOnClickListener { + (parentFragment as? ExploreMapRootFragment)?.onMediaClicked(index) + } + } + + binding.bottomSheetDetailsBinding.title.text = place.name.substring( + 5, + place.name.lastIndexOf(".") + ) + binding.bottomSheetDetailsBinding.category.text = place.distance + + var descriptionText = place.longDescription.replace("${place.name} (", "") + descriptionText = if (descriptionText == place.longDescription) descriptionText + else descriptionText.dropLast(1) + + binding.bottomSheetDetailsBinding.description.text = descriptionText + } + + override fun addSearchThisAreaButtonAction() { + binding.searchThisAreaButton.setOnClickListener(presenter?.onSearchThisAreaClicked()) + } + + override fun setSearchThisAreaButtonVisibility(isVisible: Boolean) { + binding.searchThisAreaButton.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + override fun setProgressBarVisibility(isVisible: Boolean) { + binding.mapProgressBar.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + override fun isDetailsBottomSheetVisible(): Boolean { + return binding.bottomSheetDetailsBinding.root.visibility == View.VISIBLE + } + + override fun isSearchThisAreaButtonVisible(): Boolean { + return binding.bottomSheetDetailsBinding.root.visibility == View.VISIBLE + } + + override fun getLastLocation(): LatLng { + if (lastKnownLocation == null) { + lastKnownLocation = locationManager.getLastLocation() + } + return lastKnownLocation!! + } + + override fun disableFABRecenter() { + binding.fabRecenter.isEnabled = false + } + + override fun enableFABRecenter() { + binding.fabRecenter.isEnabled = true + } + + /** + * Adds markers to the map based on the list of NearbyBaseMarker. + * + * @param nearbyBaseMarkers The NearbyBaseMarker object representing the markers to be added. + */ + override fun addMarkersToMap(nearbyBaseMarkers: List) { + clearAllMarkers() + nearbyBaseMarkers.forEach { addMarkerToMap(it) } + binding.mapView.invalidate() + } + + /** + * Adds a marker to the map based on the specified NearbyBaseMarker. + * + * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added. + */ + private fun addMarkerToMap(nearbyBaseMarker: BaseMarker) { + if (isAttachedToActivity()) { + val items = ArrayList() + val icon = nearbyBaseMarker.icon + val drawable = BitmapDrawable(resources, icon) + val point = GeoPoint( + nearbyBaseMarker.place.location.latitude, + nearbyBaseMarker.place.location.longitude + ) + val item = OverlayItem(nearbyBaseMarker.place.name, null, point).apply { + setMarker(drawable) + } + items.add(item) + + val overlay = ItemizedOverlayWithFocus(items, object : OnItemGestureListener { + override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { + val place = nearbyBaseMarker.place + clickedMarker?.let { + removeMarker(it) + addMarkerToMap(it) + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + clickedMarker = nearbyBaseMarker + passInfoToSheet(place) + return true + } + + override fun onItemLongPress(index: Int, item: OverlayItem): Boolean { + return false + } + }, context) + + overlay.setFocusItemsOnTap(true) + binding.mapView.overlays.add(overlay) // Add the overlay to the map + } + } + + private fun removeMarker(nearbyBaseMarker: BaseMarker) { + val place = nearbyBaseMarker.place + val overlays = binding.mapView.overlays + var item: ItemizedOverlayWithFocus + + for (i in overlays.indices) { + if (overlays[i] is ItemizedOverlayWithFocus<*>) { + item = overlays[i] as ItemizedOverlayWithFocus + val overlayItem = item.getItem(0) + + if (place.location.latitude == overlayItem.point.latitude && + place.location.longitude == overlayItem.point.longitude + ) { + binding.mapView.overlays.removeAt(i) + binding.mapView.invalidate() + break + } + } + } + } + + override fun clearAllMarkers() { + if (isAttachedToActivity()) { + binding.mapView.overlayManager.clear() + mapCenter?.let { geoPoint -> + val overlays = binding.mapView.overlays + val diskOverlay = ScaleDiskOverlay( + context, geoPoint, 2000, GeoConstants.UnitOfMeasure.foot + ).apply { + val circlePaint = Paint().apply { + color = Color.rgb(128, 128, 128) + style = Paint.Style.STROKE + strokeWidth = 2f + } + setCirclePaint2(circlePaint) + + val diskPaint = Paint().apply { + color = Color.argb(40, 128, 128, 128) + style = Paint.Style.FILL_AND_STROKE + } + setCirclePaint1(diskPaint) + + setDisplaySizeMin(900) + setDisplaySizeMax(1700) + } + binding.mapView.overlays.add(diskOverlay) + + val startMarker = Marker(binding.mapView).apply { + position = geoPoint + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.current_location_marker + ) + title = "Your Location" + textLabelFontSize = 24 + } + binding.mapView.overlays.add(startMarker) + } + + val scaleBarOverlay = ScaleBarOverlay(binding.mapView).apply { + setScaleBarOffset(15, 25) + setBackgroundPaint( + Paint().apply { + setARGB(200, 255, 250, 250) + } + ) + enableScaleBar() + } + binding.mapView.overlays.add(scaleBarOverlay) + + binding.mapView.overlays.add(object : MapEventsOverlay(object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean { + clickedMarker?.let { + removeMarker(it) + addMarkerToMap(it) + binding.mapView.invalidate() + } ?: Timber.e("CLICKED MARKER IS NULL") + + if (bottomSheetDetailsBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet() + } + return true + } + + override fun longPressHelper(p: GeoPoint?): Boolean { + return false + } + }) {}) + + binding.mapView.setMultiTouchControls(true) + } + } + + private fun recenterMarkerToPosition(geoPoint: GeoPoint?) { + geoPoint?.let { + binding.mapView.controller.setCenter(it) + val overlays = binding.mapView.overlays + overlays.removeAll { overlay -> overlay is Marker || overlay is ScaleDiskOverlay } + + val diskOverlay = ScaleDiskOverlay( + context, it, 2000, GeoConstants.UnitOfMeasure.foot + ).apply { + val circlePaint = Paint().apply { + color = Color.rgb(128, 128, 128) + style = Paint.Style.STROKE + strokeWidth = 2f + } + setCirclePaint2(circlePaint) + + val diskPaint = Paint().apply { + color = Color.argb(40, 128, 128, 128) + style = Paint.Style.FILL_AND_STROKE + } + setCirclePaint1(diskPaint) + + setDisplaySizeMin(900) + setDisplaySizeMax(1700) + } + binding.mapView.overlays.add(diskOverlay) + + val startMarker = Marker(binding.mapView).apply { + position = it + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.current_location_marker + ) + title = "Your Location" + textLabelFontSize = 24 + } + binding.mapView.overlays.add(startMarker) + } + } + + private fun moveCameraToPosition(geoPoint: GeoPoint) { + binding.mapView.controller.animateTo(geoPoint) + } + + private fun moveCameraToPosition(geoPoint: GeoPoint, zoom: Double, speed: Long) { + binding.mapView.controller.animateTo(geoPoint, zoom, speed) + } + + override fun getLastMapFocus(): LatLng { + return lastMapFocus?.let { + LatLng(it.latitude, it.longitude, 100f) + } ?: getMapCenter() + } + + override fun getMapCenter(): LatLng { + var latLng: LatLng? = null + + if (mapCenter != null) { + latLng = LatLng(mapCenter!!.latitude, mapCenter!!.longitude, 100f) + } else { + applicationKvStore.getString("LastLocation")?.let { lastLocation -> + val locationLatLng = lastLocation.split(",").map { it.toDouble() } + lastKnownLocation = LatLng(locationLatLng[0], locationLatLng[1], 1f) + latLng = lastKnownLocation + } ?: run { + latLng = LatLng(51.506255446947776, -0.07483536015053005, 1f) + } + } + + if (!isCameFromNearbyMap()) { + moveCameraToPosition(GeoPoint(latLng?.latitude!!, latLng?.longitude!!)) + } + return latLng!! + } + + override fun getMapFocus(): LatLng { + return LatLng( + binding.mapView.mapCenter.latitude, + binding.mapView.mapCenter.longitude, + 100f + ) + } + + override fun setFABRecenterAction(onClickListener: View.OnClickListener) { + binding.fabRecenter.setOnClickListener(onClickListener) + } + + override fun backButtonClicked(): Boolean { + return if (bottomSheetDetailsBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetDetailsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + true + } else { + false + } + } + + private fun initNetworkBroadCastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + activity?.let { + if (NetworkUtils.isInternetConnectionEstablished(it)) { + if (isNetworkErrorOccurred) { + presenter?.updateMap( + LocationServiceManager + .LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + ) + isNetworkErrorOccurred = false + } + snackbar?.dismiss() + snackbar = null + } else { + if (snackbar == null) { + snackbar = Snackbar.make( + requireView(), + R.string.no_internet, + Snackbar.LENGTH_INDEFINITE + ) + setSearchThisAreaButtonVisibility(false) + setProgressBarVisibility(false) + } + isNetworkErrorOccurred = true + snackbar?.show() + } + } + } + } + } + + fun isAttachedToActivity(): Boolean { + return isVisible && activity != null + } + + override fun onLocationPermissionDenied(toastMessage: String) {} + + override fun onLocationPermissionGranted() {} +} 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 deleted file mode 100644 index 94b9cf5ad3..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java +++ /dev/null @@ -1,227 +0,0 @@ -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.location.Location; -import android.view.View; -import fr.free.nrw.commons.BaseMarker; -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 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 LatLng currentLatLng; - 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; - } - - /** - * 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(exploreMapFragmentView.getMapCenter()); - } else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) { - Timber.d("SEARCH_CUSTOM_AREA"); - lockUnlockNearby(true); - exploreMapFragmentView.setProgressBarVisibility(true); - exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus()); - } else { // Means location changed slightly, ie user is walking or driving. - Timber.d("Means location changed slightly"); - } - } - - /** - * 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(currentLatLng); - }); - - } - - @Override - public boolean backButtonClicked() { - return exploreMapFragmentView.backButtonClicked(); - } - - public void onMapReady(ExploreMapController exploreMapController) { - this.exploreMapController = exploreMapController; - if (null != exploreMapFragmentView) { - exploreMapFragmentView.addSearchThisAreaButtonAction(); - initializeMapOperations(); - } - } - - public void initializeMapOperations() { - lockUnlockNearby(false); - updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - } - - public Observable loadAttractionsFromLocation(LatLng currentLatLng, - LatLng searchLatLng, boolean checkingAroundCurrent) { - return Observable - .fromCallable(() -> exploreMapController - .loadAttractionsFromLocation(currentLatLng, 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) { - if (explorePlacesInfo.mediaList != null) { - prepareNearbyBaseMarkers(explorePlacesInfo); - } else { - lockUnlockNearby(false); // So that new location updates wont come - exploreMapFragmentView.setProgressBarVisibility(false); - } - } - - void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) { - exploreMapController - .loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng, - // Curlatlang will be used to calculate distances - explorePlacesInfo.explorePlaceList, - exploreMapFragmentView.getContext(), - this, - explorePlacesInfo); - } - - @Override - public void onNearbyBaseMarkerThumbsReady(List baseMarkers, - ExplorePlacesInfo explorePlacesInfo) { - if (null != exploreMapFragmentView) { - exploreMapFragmentView.addMarkersToMap(baseMarkers); - lockUnlockNearby(false); // So that new location updates wont come - exploreMapFragmentView.setProgressBarVisibility(false); - } - } - - 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.getLastMapFocus()) { - return true; - } - - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(exploreMapFragmentView.getMapFocus().getLatitude()); - dest_location.setLongitude(exploreMapFragmentView.getMapFocus().getLongitude()); - mylocation.setLatitude(exploreMapFragmentView.getLastMapFocus().getLatitude()); - mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude()); - Float distance = mylocation.distanceTo(dest_location); - - if (distance > 2000.0 * 3 / 4) { - return false; - } else { - return true; - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt new file mode 100644 index 0000000000..00aaa55f68 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt @@ -0,0 +1,206 @@ +package fr.free.nrw.commons.explore.map + +import android.location.Location +import android.view.View +import fr.free.nrw.commons.BaseMarker +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.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType +import io.reactivex.Observable +import timber.log.Timber +import java.lang.reflect.Proxy + +class ExploreMapPresenter( + private val bookmarkLocationDao: BookmarkLocationsDao +) : ExploreMapContract.UserActions, ExploreMapController.NearbyBaseMarkerThumbCallback { + + private var isNearbyLocked: Boolean = false + private var currentLatLng: LatLng? = null + private var exploreMapController: ExploreMapController? = null + + companion object { + private val DUMMY: ExploreMapContract.View = Proxy.newProxyInstance( + ExploreMapContract.View::class.java.classLoader, + arrayOf(ExploreMapContract.View::class.java) + ) { _, method, _ -> + when (method.returnType) { + String::class.java -> "" + Integer::class.java -> 0 + Int::class.java -> 0 + Boolean::class.java -> false + Boolean::class.javaPrimitiveType -> false + else -> null + } + } as ExploreMapContract.View + } + + private var exploreMapFragmentView: ExploreMapContract.View = DUMMY + + override fun updateMap(locationChangeType: LocationChangeType) { + Timber.d("Presenter updates map and list $locationChangeType") + if (isNearbyLocked) { + Timber.d("Nearby is locked, so updateMapAndList returns") + return + } + + if (!exploreMapFragmentView.isNetworkConnectionEstablished()) { + Timber.d("Network connection is not established") + return + } + + /** + * Significant changed - Markers and current location will be updated together + * Slightly changed - Only current position marker will be updated + */ + when (locationChangeType) { + LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED -> { + Timber.d("LOCATION_SIGNIFICANTLY_CHANGED") + lockUnlockNearby(true) + exploreMapFragmentView.setProgressBarVisibility(true) + exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapCenter()) + } + + LocationChangeType.SEARCH_CUSTOM_AREA -> { + Timber.d("SEARCH_CUSTOM_AREA") + lockUnlockNearby(true) + exploreMapFragmentView.setProgressBarVisibility(true) + exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus()) + } + + else -> { + Timber.d("Means location changed slightly") + } + } + } + + /** + * Nearby updates take time since they are network operations. During update time, we don't + * want to get any other calls from the user. So locking nearby. + * + * @param isNearbyLocked true means lock, false means unlock + */ + override fun lockUnlockNearby(isNearbyLocked: Boolean) { + this.isNearbyLocked = isNearbyLocked + if (isNearbyLocked) { + exploreMapFragmentView.disableFABRecenter() + } else { + exploreMapFragmentView.enableFABRecenter() + } + } + + override fun attachView(view: ExploreMapContract.View) { + exploreMapFragmentView = view + } + + override fun detachView() { + exploreMapFragmentView = DUMMY + } + + /** + * Sets click listener of FAB + */ + override fun setActionListeners(applicationKvStore: JsonKvStore) { + exploreMapFragmentView.setFABRecenterAction { + currentLatLng?.let { it1 -> exploreMapFragmentView.recenterMap(it1) } + } + } + + override fun backButtonClicked(): Boolean { + return exploreMapFragmentView.backButtonClicked() + } + + fun onMapReady(exploreMapController: ExploreMapController) { + this.exploreMapController = exploreMapController + exploreMapFragmentView.addSearchThisAreaButtonAction() + initializeMapOperations() + } + + fun initializeMapOperations() { + lockUnlockNearby(false) + updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + } + + fun loadAttractionsFromLocation( + currentLatLng: LatLng, + searchLatLng: LatLng?, + checkingAroundCurrent: Boolean + ): Observable { + return Observable.fromCallable { + exploreMapController?.loadAttractionsFromLocation( + currentLatLng, 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. + */ + fun updateMapMarkers(explorePlacesInfo: MapController.ExplorePlacesInfo) { + if (explorePlacesInfo.mediaList != null) { + prepareNearbyBaseMarkers(explorePlacesInfo) + } else { + lockUnlockNearby(false) // So that new location updates won't come + exploreMapFragmentView.setProgressBarVisibility(false) + } + } + + private fun prepareNearbyBaseMarkers(explorePlacesInfo: MapController.ExplorePlacesInfo) { + ExploreMapController.loadAttractionsFromLocationToBaseMarkerOptions( + explorePlacesInfo.currentLatLng, + explorePlacesInfo.explorePlaceList, + exploreMapFragmentView.getContext(), + this, + explorePlacesInfo + ) + } + + override fun onNearbyBaseMarkerThumbsReady( + baseMarkers: List, + explorePlacesInfo: ExplorePlacesInfo + ) { + exploreMapFragmentView.addMarkersToMap(baseMarkers) + lockUnlockNearby(false) // So that new location updates won't come + exploreMapFragmentView.setProgressBarVisibility(false) + } + + fun onSearchThisAreaClicked(): View.OnClickListener { + return View.OnClickListener { + // Lock map operations during search this area operation + exploreMapFragmentView.setSearchThisAreaButtonVisibility(false) + + if (searchCloseToCurrentLocation()) { + updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + } else { + updateMap(LocationChangeType.SEARCH_CUSTOM_AREA) + } + } + } + + /** + * Returns true if search this area button is used around our current location, so that we can + * continue following our current location again + * + * @return Returns true if search this area button is used around our current location + */ + fun searchCloseToCurrentLocation(): Boolean { + val lastMapFocus = exploreMapFragmentView.getLastMapFocus() ?: return true + + val myLocation = Location("").apply { + latitude = lastMapFocus.latitude + longitude = lastMapFocus.longitude + } + + val destLocation = Location("").apply { + latitude = exploreMapFragmentView.getMapFocus().latitude + longitude = exploreMapFragmentView.getMapFocus().longitude + } + + return myLocation.distanceTo(destLocation) <= 2000.0 * 3 / 4 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt index e90cc12241..1dd3420f20 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt @@ -2,11 +2,11 @@ package fr.free.nrw.commons.location interface LocationUpdateListener { // Will be used to update all nearby markers on the map - fun onLocationChangedSignificantly(latLng: LatLng) + fun onLocationChangedSignificantly(latLng: LatLng?) // Will be used to track users motion - fun onLocationChangedSlightly(latLng: LatLng) + fun onLocationChangedSlightly(latLng: LatLng?) // Will be used updating nearby card view notification - fun onLocationChangedMedium(latLng: LatLng) + fun onLocationChangedMedium(latLng: LatLng?) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index 25baf3a92e..62ef04badc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -1864,21 +1864,21 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), presenter!!.updateMapAndList(locationChangeType) } - override fun onLocationChangedSignificantly(latLng: LatLng) { + override fun onLocationChangedSignificantly(latLng: LatLng?) { Timber.d("Location significantly changed") if (latLng != null) { handleLocationUpdate(latLng, LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) } } - override fun onLocationChangedSlightly(latLng: LatLng) { + override fun onLocationChangedSlightly(latLng: LatLng?) { Timber.d("Location slightly changed") if (latLng != null) { //If the map has never ever shown the current location, lets do it know handleLocationUpdate(latLng, LocationChangeType.LOCATION_SLIGHTLY_CHANGED) } } - override fun onLocationChangedMedium(latLng: LatLng) { + override fun onLocationChangedMedium(latLng: LatLng?) { Timber.d("Location changed medium") if (latLng != null) { //If the map has never ever shown the current location, lets do it know handleLocationUpdate(latLng, LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt index b4639b14ae..23813c8aed 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt @@ -495,17 +495,17 @@ class NearbyParentFragmentPresenter updateMapAndList(LocationChangeType.MAP_UPDATED) } - override fun onLocationChangedSignificantly(latLng: LatLng) { + override fun onLocationChangedSignificantly(latLng: LatLng?) { Timber.d("Location significantly changed") updateMapAndList(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) } - override fun onLocationChangedSlightly(latLng: LatLng) { + override fun onLocationChangedSlightly(latLng: LatLng?) { Timber.d("Location significantly changed") updateMapAndList(LocationChangeType.LOCATION_SLIGHTLY_CHANGED) } - override fun onLocationChangedMedium(latLng: LatLng) { + override fun onLocationChangedMedium(latLng: LatLng?) { Timber.d("Location changed medium") }