This commit is contained in:
Saifuddin Adenwala 2025-03-17 05:18:46 +00:00 committed by GitHub
commit fe0a51f65b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1747 additions and 1852 deletions

View file

@ -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()
}

View file

@ -144,8 +144,8 @@ public class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
*/
@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 class ExploreMapRootFragment extends CommonsDaggerSupportFragment impleme
*/
@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;
}

View file

@ -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<Fragment> fragmentList = new ArrayList<>();
List<String> 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);
}
}

View file

@ -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<Fragment>()
val titleList = mutableListOf<String>()
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)
}
}

View file

@ -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<Media> callCommonsQuery(final LatLng currentLatLng) {
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
}
}

View file

@ -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<Media> {
val coordinates = "${currentLatLng.latitude}|${currentLatLng.longitude}"
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet()
}
}

View file

@ -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<BaseMarker> 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();
}
}

View file

@ -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<BaseMarker>)
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
}
}

View file

@ -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<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south
mediaList.get(0).getCoordinates(), // north
mediaList.get(0).getCoordinates(), // west
mediaList.get(0).getCoordinates()};// east, init with a random location
if (searchLatLng != null) {
Timber.d("Sorting places by distance...");
final Map<Media, Double> distances = new HashMap<>();
for (Media media : mediaList) {
distances.put(media,
computeDistanceBetween(media.getCoordinates(), searchLatLng));
// Find boundaries with basic find max approach
if (media.getCoordinates().getLatitude()
< boundaryCoordinates[0].getLatitude()) {
boundaryCoordinates[0] = media.getCoordinates();
}
if (media.getCoordinates().getLatitude()
> boundaryCoordinates[1].getLatitude()) {
boundaryCoordinates[1] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude()
< boundaryCoordinates[2].getLongitude()) {
boundaryCoordinates[2] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude()
> boundaryCoordinates[3].getLongitude()) {
boundaryCoordinates[3] = media.getCoordinates();
}
}
}
explorePlacesInfo.mediaList = mediaList;
explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList);
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates;
// Sets latestSearchRadius to maximum distance among boundaries and search location
for (LatLng bound : boundaryCoordinates) {
double distance = LocationUtils.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<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
LatLng currentLatLng,
final List<Place> placeList,
Context context,
NearbyBaseMarkerThumbCallback callback,
ExplorePlacesInfo explorePlacesInfo) {
List<BaseMarker> 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<Bitmap>() {
// We add icons to markers when bitmaps are ready
@Override
public void onResourceReady(@NonNull Bitmap resource,
@Nullable Transition<? super Bitmap> 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<BaseMarker> baseMarkers,
ExplorePlacesInfo explorePlacesInfo);
}
}

View file

@ -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<Media, Double>()
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<Place>?,
context: Context,
callback: NearbyBaseMarkerThumbCallback,
explorePlacesInfo: ExplorePlacesInfo
): List<BaseMarker> {
val baseMarkerList = mutableListOf<BaseMarker>()
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<Bitmap>() {
// We add icons to markers when bitmaps are ready
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
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<BaseMarker>,
explorePlacesInfo: ExplorePlacesInfo
)
}
}

View file

@ -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<Media>? = 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<MapController.ExplorePlacesInfo> =
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<BaseMarker>) {
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<OverlayItem>()
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<OverlayItem> {
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<OverlayItem>
for (i in overlays.indices) {
if (overlays[i] is ItemizedOverlayWithFocus<*>) {
item = overlays[i] as ItemizedOverlayWithFocus<OverlayItem>
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() {}
}

View file

@ -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<ExplorePlacesInfo> 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<BaseMarker> 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;
}
}
}

View file

@ -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<ExplorePlacesInfo> {
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<BaseMarker>,
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
}
}

View file

@ -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?)
}

View file

@ -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)

View file

@ -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")
}