#3077 Hide disambiguation items from depiction search - when fetching depictions get the entity instead of the search result (#3741)

This commit is contained in:
Seán Mac Gillicuddy 2020-05-12 11:44:17 +01:00 committed by GitHub
parent 34f02499e4
commit 057d11a0e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 359 additions and 582 deletions

View file

@ -13,8 +13,6 @@ public interface SubDepictionListContract {
interface View { interface View {
void onImageUrlFetched(String response, int position);
void onSuccess(List<DepictedItem> mediaList); void onSuccess(List<DepictedItem> mediaList);
void initErrorView(); void initErrorView();
@ -23,15 +21,10 @@ public interface SubDepictionListContract {
void setIsLastPage(boolean b); void setIsLastPage(boolean b);
boolean isParentClass();
} }
interface UserActionListener extends BasePresenter<View> { interface UserActionListener extends BasePresenter<View> {
void saveQuery();
void fetchThumbnailForEntityId(String entityId, int position);
void initSubDepictionList(String qid, Boolean isParentClass) throws IOException; void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
String getQuery(); String getQuery();

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.depictions.subClass; package fr.free.nrw.commons.depictions.subClass;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -7,23 +10,14 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import dagger.android.support.DaggerFragment; import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
@ -32,9 +26,10 @@ import fr.free.nrw.commons.explore.depictions.SearchDepictionsRenderer;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import java.io.IOException;
import static android.view.View.GONE; import java.util.List;
import static android.view.View.VISIBLE; import java.util.Locale;
import javax.inject.Inject;
/** /**
* Fragment for parent classes and child classes of Depicted items in Explore * Fragment for parent classes and child classes of Depicted items in Explore
@ -54,10 +49,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
*/ */
private boolean isParentClass = false; private boolean isParentClass = false;
private RVRendererAdapter<DepictedItem> depictionsAdapter; private RVRendererAdapter<DepictedItem> depictionsAdapter;
/**
* Used by scroll state listener, when hasMoreImages is false scrolling does not fetches any more images
*/
private boolean hasMoreImages = true;
RecyclerView.LayoutManager layoutManager; RecyclerView.LayoutManager layoutManager;
/** /**
* Stores entityId for the depiction * Stores entityId for the depiction
@ -77,12 +68,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
getActivity().finish(); getActivity().finish();
WikidataItemDetailsActivity.startYourself(getContext(), item); WikidataItemDetailsActivity.startYourself(getContext(), item);
} }
@Override
public void fetchThumbnailUrlForEntity(String entityId, int position) {
presenter.fetchThumbnailForEntityId(entityId, position);
}
}); });
@Override @Override
@ -140,15 +125,8 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet); ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
} }
@Override
public void onImageUrlFetched(String response, int position) {
depictionsAdapter.getItem(position).setImageUrl(response);
depictionsAdapter.notifyItemChanged(position);
}
@Override @Override
public void onSuccess(List<DepictedItem> mediaList) { public void onSuccess(List<DepictedItem> mediaList) {
hasMoreImages = false;
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
depictionNotFound.setVisibility(GONE); depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE); bottomProgressBar.setVisibility(GONE);
@ -164,7 +142,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
@Override @Override
public void initErrorView() { public void initErrorView() {
hasMoreImages = false;
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE); bottomProgressBar.setVisibility(GONE);
depictionNotFound.setVisibility(VISIBLE); depictionNotFound.setVisibility(VISIBLE);
@ -180,11 +157,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
@Override @Override
public void setIsLastPage(boolean b) { public void setIsLastPage(boolean b) {
hasMoreImages = !b;
} }
@Override
public boolean isParentClass() {
return isParentClass;
}
} }

View file

@ -4,7 +4,6 @@ import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import fr.free.nrw.commons.explore.depictions.DepictsClient; import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
@ -13,7 +12,6 @@ import io.reactivex.disposables.CompositeDisposable;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -48,10 +46,6 @@ public class SubDepictionListPresenter implements SubDepictionListContract.UserA
DepictsClient depictsClient; DepictsClient depictsClient;
private List<DepictedItem> queryList = new ArrayList<>(); private List<DepictedItem> queryList = new ArrayList<>();
OkHttpJsonApiClient okHttpJsonApiClient; OkHttpJsonApiClient okHttpJsonApiClient;
/**
* variable used to record the number of API calls already made for fetching Thumbnails
*/
private int size = 0;
@Inject @Inject
public SubDepictionListPresenter(RecentSearchesDao recentSearchesDao, DepictsClient depictsClient, OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD) Scheduler ioScheduler, public SubDepictionListPresenter(RecentSearchesDao recentSearchesDao, DepictsClient depictsClient, OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD) Scheduler ioScheduler,
@ -72,38 +66,8 @@ public class SubDepictionListPresenter implements SubDepictionListContract.UserA
this.view = DUMMY; this.view = DUMMY;
} }
/**
* Store the current query in Recent searches
*/
@Override
public void saveQuery() {
RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearch = new RecentSearch(null, query, new Date());
} else {
recentSearch.setLastSearched(new Date());
}
recentSearchesDao.save(recentSearch);
}
/**
* Calls Wikibase APIs to fetch Thumbnail image for a given wikidata item
*/
@Override
public void fetchThumbnailForEntityId(String entityId, int position) {
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(response -> {
view.onImageUrlFetched(response,position);
}));
}
@Override @Override
public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException { public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException {
size = 0;
if (isParentClass) { if (isParentClass) {
compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid) compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid)
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
@ -137,9 +101,6 @@ public class SubDepictionListPresenter implements SubDepictionListContract.UserA
} else { } else {
this.queryList.addAll(mediaList); this.queryList.addAll(mediaList);
view.onSuccess(mediaList); view.onSuccess(mediaList);
for (DepictedItem m : mediaList) {
fetchThumbnailForEntityId(m.getId(), size++);
}
} }
} }

View file

@ -1,19 +1,6 @@
package fr.free.nrw.commons.depictions.subClass.models package fr.free.nrw.commons.depictions.subClass.models
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem data class SparqlResponse(val results: Result)
data class SparqlResponse(val results: Result) {
fun toDepictedItems() =
results.bindings.map {
DepictedItem(
it.itemLabel.value,
it.itemDescription?.value ?: "",
"",
false,
it.item.value.substringAfterLast("/")
)
}
}
data class Result(val bindings: List<Binding>) data class Result(val bindings: List<Binding>)
@ -21,6 +8,8 @@ data class Binding(
val item: SparqInfo, val item: SparqInfo,
val itemLabel: SparqInfo, val itemLabel: SparqInfo,
val itemDescription: SparqInfo? = null val itemDescription: SparqInfo? = null
) ) {
val id: String by lazy { item.value.substringAfterLast("/") }
}
data class SparqInfo(val type: String, val value: String) data class SparqInfo(val type: String, val value: String)

View file

@ -9,6 +9,7 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.PageEditInterface; import fr.free.nrw.commons.actions.PageEditInterface;
import fr.free.nrw.commons.category.CategoryInterface; import fr.free.nrw.commons.category.CategoryInterface;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaDetailInterface; import fr.free.nrw.commons.media.MediaDetailInterface;
import fr.free.nrw.commons.media.MediaInterface; import fr.free.nrw.commons.media.MediaInterface;
@ -77,10 +78,12 @@ public class NetworkingModule {
@Provides @Provides
@Singleton @Singleton
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
@Named("tools_forge") HttpUrl toolsForgeUrl, @Named("tools_forge") HttpUrl toolsForgeUrl,
@Named("default_preferences") JsonKvStore defaultKvStore, @Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) { Gson gson) {
return new OkHttpJsonApiClient(okHttpClient, return new OkHttpJsonApiClient(okHttpClient,
depictsClient,
toolsForgeUrl, toolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL, WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,

View file

@ -1,175 +0,0 @@
package fr.free.nrw.commons.explore.depictions;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.depictions.models.Search;
import fr.free.nrw.commons.media.MediaInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataProperties;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.wikipedia.wikidata.DataValue.DataValueString;
import org.wikipedia.wikidata.Statement_partial;
/**
* Depicts Client to handle custom calls to Commons Wikibase APIs
*/
@Singleton
public class DepictsClient {
private final DepictsInterface depictsInterface;
private final MediaInterface mediaInterface;
public static final String NO_DEPICTED_IMAGE = "No Image for Depiction";
@Inject
public DepictsClient(DepictsInterface depictsInterface, MediaInterface mediaInterface) {
this.depictsInterface = depictsInterface;
this.mediaInterface = mediaInterface;
}
/**
* Search for depictions using the search item
* @return list of depicted items
*/
public Observable<DepictedItem> searchForDepictions(String query, int limit, int offset) {
return depictsInterface.searchForDepicts(
query,
String.valueOf(limit),
Locale.getDefault().getLanguage(),
Locale.getDefault().getLanguage(),
String.valueOf(offset)
)
.toObservable()
.flatMap( depictSearchResponse ->
Observable.fromIterable(depictSearchResponse.getSearch()))
.map(DepictedItem::new);
}
/**
* Get URL for image using image name
* Ex: title = Guion Bluford
* Url = https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Guion_Bluford.jpg/70px-Guion_Bluford.jpg
*/
private String getThumbnailUrl(String title) {
String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
title = title.replace(" ", "_");
String MD5Hash = getMd5(title);
/**
* We use 70 pixels as the size of our Thumbnail (as it is the perfect fits our UI)
*/
return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/70px-" + title;
}
/**
* Ex: entityId = Q357458
* value returned = Elgin Baylor Night program.jpeg
*/
public Single<String> getP18ForItem(String entityId) {
return depictsInterface.getImageForEntity(entityId)
.map(claimsResponse -> {
final List<Statement_partial> imageClaim = claimsResponse.getClaims()
.get(WikidataProperties.IMAGE.getPropertyName());
final DataValueString dataValue = (DataValueString) imageClaim
.get(0)
.getMainSnak()
.getDataValue();
return getThumbnailUrl((dataValue.getValue()));
})
.onErrorReturn(throwable -> NO_DEPICTED_IMAGE)
.singleOrError();
}
/**
* @return list of images for a particular depict entity
*/
public Observable<List<Media>> fetchImagesForDepictedItem(String query, int sroffset) {
return mediaInterface.fetchImagesForDepictedItem("haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query, String.valueOf(sroffset))
.map(mwQueryResponse -> {
List<Media> mediaList = new ArrayList<>();
for (Search s: mwQueryResponse.getQuery().getSearch()) {
Media media = new Media(null,
getUrl(s.getTitle()),
s.getTitle(),
"",
0,
safeParseDate(s.getTimestamp()),
safeParseDate(s.getTimestamp()),
""
);
mediaList.add(media);
}
return mediaList;
});
}
/**
* Get url for the image from media of depictions
* Ex: Tiger_Woods
* Value: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Tiger_Woods.jpg/70px-Tiger_Woods.jpg
*/
private String getUrl(String title) {
String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
title = title.substring(title.indexOf(':')+1);
title = title.replace(" ", "_");
String MD5Hash = getMd5(title);
return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/640px-" + title;
}
/**
* Generates MD5 hash for the filename
*/
public String getMd5(String input)
{
try {
// Static getInstance method is called with hashing MD5
MessageDigest md = MessageDigest.getInstance("MD5");
// digest() method is called to calculate message digest
// of an input digest() return array of byte
byte[] messageDigest = md.digest(input.getBytes());
// Convert byte array into signum representation
BigInteger no = new BigInteger(1, messageDigest);
// Convert message digest into hex value
String hashtext = no.toString(16);
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
return hashtext;
}
// For specifying wrong message digest algorithms
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Parse the date string into the required format
* @param dateStr
* @return date in the required format
*/
@Nullable
private static Date safeParseDate(String dateStr) {
try {
return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr);
} catch (ParseException e) {
return null;
}
}
}

View file

@ -0,0 +1,145 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.models.DepictionResponse
import fr.free.nrw.commons.depictions.subClass.models.Binding
import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse
import fr.free.nrw.commons.media.MediaInterface
import fr.free.nrw.commons.upload.depicts.DepictsInterface
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Observable
import io.reactivex.Single
import org.wikipedia.wikidata.Entities
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.text.ParseException
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
const val LARGE_IMAGE_SIZE="640px"
const val THUMB_IMAGE_SIZE="70px"
/**
* Depicts Client to handle custom calls to Commons Wikibase APIs
*/
@Singleton
class DepictsClient @Inject constructor(
private val depictsInterface: DepictsInterface,
private val mediaInterface: MediaInterface
) {
/**
* Search for depictions using the search item
* @return list of depicted items
*/
fun searchForDepictions(query: String?, limit: Int, offset: Int): Single<List<DepictedItem>> {
val language = Locale.getDefault().language
return depictsInterface.searchForDepicts(query, "$limit", language, language, "$offset")
.map { it.search.joinToString("|") { searchItem -> searchItem.id } }
.flatMap(::getEntities)
.map { it.entities()?.values?.map(::DepictedItem) ?: emptyList() }
}
/**
* @return list of images for a particular depict entity
*/
fun fetchImagesForDepictedItem(query: String, sroffset: Int): Observable<List<Media>> {
return mediaInterface.fetchImagesForDepictedItem(
"haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query,
sroffset.toString()
)
.map { mwQueryResponse: DepictionResponse ->
mwQueryResponse.query
.search
.map {
Media(
null,
getUrl(it.title),
it.title,
"",
0,
safeParseDate(it.timestamp),
safeParseDate(it.timestamp),
""
)
}
}
}
private fun getUrl(title: String): String {
return getImageUrl(title, LARGE_IMAGE_SIZE)
}
fun getEntities(ids: String): Single<Entities> {
return depictsInterface.getEntities(ids, Locale.getDefault().language)
}
fun toDepictions(sparqlResponse: Observable<SparqlResponse>): Observable<List<DepictedItem>> {
return sparqlResponse.map { it.results.bindings.joinToString("|", transform = Binding::id) }
.flatMap { getEntities(it).toObservable() }
.map { it.entities()?.values?.map(::DepictedItem) ?: emptyList() }
}
companion object {
/**
* Get url for the image from media of depictions
* Ex: Tiger_Woods
* Value: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Tiger_Woods.jpg/70px-Tiger_Woods.jpg
*/
fun getImageUrl(title: String, size: String): String {
return title.substringAfter(":")
.replace(" ", "_")
.let {
val MD5Hash = getMd5(it)
"https://upload.wikimedia.org/wikipedia/commons/thumb/${MD5Hash[0]}/${MD5Hash[0]}${MD5Hash[1]}/$it/$size-$it"
}
}
/**
* Generates MD5 hash for the filename
*/
fun getMd5(input: String): String {
return try {
// Static getInstance method is called with hashing MD5
val md = MessageDigest.getInstance("MD5")
// digest() method is called to calculate message digest
// of an input digest() return array of byte
val messageDigest = md.digest(input.toByteArray())
// Convert byte array into signum representation
val no = BigInteger(1, messageDigest)
// Convert message digest into hex value
var hashtext = no.toString(16)
while (hashtext.length < 32) {
hashtext = "0$hashtext"
}
hashtext
} // For specifying wrong message digest algorithms
catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
}
/**
* Parse the date string into the required format
* @param dateStr
* @return date in the required format
*/
private fun safeParseDate(dateStr: String): Date? {
return try {
CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr)
} catch (e: ParseException) {
null
}
}
}
}

View file

@ -53,15 +53,6 @@ public class SearchDepictionsFragment extends CommonsDaggerSupportFragment imple
WikidataItemDetailsActivity.startYourself(getContext(), item); WikidataItemDetailsActivity.startYourself(getContext(), item);
presenter.saveQuery(); presenter.saveQuery();
} }
/**
*fetch thumbnail image for all the depicted items (if available)
*/
@Override
public void fetchThumbnailUrlForEntity(String entityId, int position) {
presenter.fetchThumbnailForEntityId(entityId,position);
}
}); });
private RVRendererAdapter<DepictedItem> depictionsAdapter; private RVRendererAdapter<DepictedItem> depictionsAdapter;
private boolean isLastPage; private boolean isLastPage;
@ -218,12 +209,6 @@ public class SearchDepictionsFragment extends CommonsDaggerSupportFragment imple
return depictionsAdapter; return depictionsAdapter;
} }
@Override
public void onImageUrlFetched(String response, int position) {
depictionsAdapter.getItem(position).setImageUrl(response);
depictionsAdapter.notifyItemChanged(position);
}
/** /**
* Inform the view that there are no more items to be loaded for this search query * Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query * or reset the isLastPage for the current query

View file

@ -49,8 +49,6 @@ public interface SearchDepictionsFragmentContract {
*/ */
RVRendererAdapter<DepictedItem> getAdapter(); RVRendererAdapter<DepictedItem> getAdapter();
void onImageUrlFetched(String response, int position);
/** /**
* Inform the view that there are no more items to be loaded for this search query * Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query * or reset the isLastPage for the current query
@ -85,10 +83,5 @@ public interface SearchDepictionsFragmentContract {
* @return query * @return query
*/ */
String getQuery(); String getQuery();
/**
* After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
*/
void fetchThumbnailForEntityId(String entityId,int position);
} }
} }

View file

@ -90,7 +90,6 @@ public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragm
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.doOnSubscribe(disposable -> saveQuery()) .doOnSubscribe(disposable -> saveQuery())
.collect(ArrayList<DepictedItem>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError)); .subscribe(this::handleSuccess, this::handleError));
} }
@ -154,23 +153,7 @@ public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragm
this.queryList.addAll(mediaList); this.queryList.addAll(mediaList);
view.onSuccess(mediaList); view.onSuccess(mediaList);
offset=queryList.size(); offset=queryList.size();
for (DepictedItem m : mediaList) {
fetchThumbnailForEntityId(m.getId(), size++);
}
} }
} }
/**
* After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
*/
@Override
public void fetchThumbnailForEntityId(String entityId,int position) {
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(response -> {
view.onImageUrlFetched(response,position);
}));
}
} }

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.explore.depictions; package fr.free.nrw.commons.explore.depictions;
import static fr.free.nrw.commons.explore.depictions.DepictsClient.NO_DEPICTED_IMAGE;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
@ -10,12 +9,9 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference; import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource; import com.facebook.datasource.DataSource;
@ -26,10 +22,8 @@ import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.pedrogomez.renderers.Renderer; import com.pedrogomez.renderers.Renderer;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import timber.log.Timber;
/** /**
* Renderer for DepictedItem * Renderer for DepictedItem
@ -81,10 +75,7 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
tvDepictionDesc.setText(item.getDescription()); tvDepictionDesc.setText(item.getDescription());
imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp)); imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp));
Timber.e("line86"+item.getImageUrl());
if (!TextUtils.isEmpty(item.getImageUrl())) { if (!TextUtils.isEmpty(item.getImageUrl())) {
if (!item.getImageUrl().equals(NO_DEPICTED_IMAGE) && !item.getImageUrl().equals(""))
{
ImageRequest imageRequest = ImageRequestBuilder ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(item.getImageUrl())) .newBuilderWithSource(Uri.parse(item.getImageUrl()))
.setAutoRotateEnabled(true) .setAutoRotateEnabled(true)
@ -99,7 +90,6 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
@Override @Override
public void onNewResultImpl(@Nullable Bitmap bitmap) { public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null) { if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", item.getImageUrl());
//imageView.setImageBitmap(Bitmap.createBitmap(bitmap)); //imageView.setImageBitmap(Bitmap.createBitmap(bitmap));
imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap))); imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
dataSource.close(); dataSource.close();
@ -108,19 +98,15 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
@Override @Override
public void onFailureImpl(DataSource dataSource) { public void onFailureImpl(DataSource dataSource) {
Timber.d("Error getting bitmap from image url %s", item.getImageUrl());
if (dataSource != null) { if (dataSource != null) {
dataSource.close(); dataSource.close();
} }
} }
}, CallerThreadExecutor.getInstance()); }, CallerThreadExecutor.getInstance());
}
} }
} }
public interface DepictCallback { public interface DepictCallback {
void depictsClicked(DepictedItem item); void depictsClicked(DepictedItem item);
void fetchThumbnailUrlForEntity(String entityId,int position);
} }
} }

View file

@ -4,7 +4,7 @@ import android.os.Parcelable
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.DataValue.DataValueEntityId import org.wikipedia.wikidata.DataValue.EntityId
import org.wikipedia.wikidata.Entities import org.wikipedia.wikidata.Entities
import java.util.* import java.util.*
@ -18,7 +18,7 @@ data class Depictions(val depictions: List<IdAndLabel>) : Parcelable {
entities.first?.statements entities.first?.statements
?.getOrElse(DEPICTS.propertyName, { emptyList() }) ?.getOrElse(DEPICTS.propertyName, { emptyList() })
?.map { statement -> ?.map { statement ->
(statement.mainSnak.dataValue as DataValueEntityId).value.id (statement.mainSnak.dataValue as EntityId).value.id
} }
?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) } ?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) }
?: emptyList() ?: emptyList()

View file

@ -602,10 +602,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
textView.setText(depictionName); textView.setText(depictionName);
if (depictionLoaded) { if (depictionLoaded) {
item.setOnClickListener(view -> { item.setOnClickListener(view -> {
DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId);
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class); Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictedItem.getName()); intent.putExtra("wikidataItemName", depictionName);
intent.putExtra("entityId", depictedItem.getId()); intent.putExtra("entityId", entityId);
getContext().startActivity(intent); getContext().startActivity(intent);
}); });
} }

View file

@ -7,6 +7,7 @@ import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse; import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO; import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse; import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse; import fr.free.nrw.commons.nearby.model.NearbyResponse;
@ -38,6 +39,7 @@ import timber.log.Timber;
public class OkHttpJsonApiClient { public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient; private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl; private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl; private final String sparqlQueryUrl;
private final String campaignsUrl; private final String campaignsUrl;
@ -46,11 +48,13 @@ public class OkHttpJsonApiClient {
@Inject @Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient, public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl, HttpUrl wikiMediaToolforgeUrl,
String sparqlQueryUrl, String sparqlQueryUrl,
String campaignsUrl, String campaignsUrl,
Gson gson) { Gson gson) {
this.okHttpClient = okHttpClient; this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl; this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl; this.campaignsUrl = campaignsUrl;
@ -219,14 +223,11 @@ public class OkHttpJsonApiClient {
} }
private Observable<List<DepictedItem>> depictedItemsFrom(Request request) { private Observable<List<DepictedItem>> depictedItemsFrom(Request request) {
return Observable.fromCallable(() -> { return depictsClient.toDepictions(Observable.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) { try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
return gson.fromJson(body.string(), SparqlResponse.class).toDepictedItems(); return gson.fromJson(body.string(), SparqlResponse.class);
}catch (Exception e) {
Timber.e(e);
return new ArrayList<DepictedItem>();
} }
}).doOnError(Timber::e); }).doOnError(Timber::e));
} }
@NotNull @NotNull

View file

@ -30,7 +30,8 @@ public class Place implements Parcelable {
public final Sitelinks siteLinks; public final Sitelinks siteLinks;
public Place(String name, Label label, String longDescription, LatLng location, String category, Sitelinks siteLinks, String pic, String destroyed) { this.name = name; public Place(String name, Label label, String longDescription, LatLng location, String category, Sitelinks siteLinks, String pic, String destroyed) {
this.name = name;
this.label = label; this.label = label;
this.longDescription = longDescription; this.longDescription = longDescription;
this.location = location; this.location = location;

View file

@ -79,7 +79,6 @@ public class UploadDepictsRenderer extends Renderer<DepictedItem> {
final String imageUrl = item.getImageUrl(); final String imageUrl = item.getImageUrl();
if (TextUtils.isEmpty(imageUrl)) { if (TextUtils.isEmpty(imageUrl)) {
imageView.setImageURI(UriUtil.getUriForResourceId(R.drawable.ic_wikidata_logo_24dp)); imageView.setImageURI(UriUtil.getUriForResourceId(R.drawable.ic_wikidata_logo_24dp));
listener.fetchThumbnailUrlForEntity(item);
} else { } else {
imageView.setImageURI(Uri.parse(imageUrl)); imageView.setImageURI(Uri.parse(imageUrl));
} }

View file

@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData;
import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.List; import java.util.List;
import org.jetbrains.annotations.NotNull;
/** /**
* The contract with which DepictsFragment and its presenter would talk to each other * The contract with which DepictsFragment and its presenter would talk to each other
@ -41,9 +40,6 @@ public interface DepictsContract {
* add depictions to list * add depictions to list
*/ */
void setDepictsList(List<DepictedItem> depictedItemList); void setDepictsList(List<DepictedItem> depictedItemList);
void onUrlFetched(@NotNull DepictedItem depictedItem, @NotNull String url);
} }
interface UserActionListener extends BasePresenter<View> { interface UserActionListener extends BasePresenter<View> {
@ -71,7 +67,5 @@ public interface DepictsContract {
void verifyDepictions(); void verifyDepictions();
LiveData<List<DepictedItem>> getDepictedItems(); LiveData<List<DepictedItem>> getDepictedItems();
void fetchThumbnailForEntityId(DepictedItem depictedItem);
} }
} }

View file

@ -144,15 +144,6 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
} }
} }
@Override
public void onUrlFetched(@NotNull DepictedItem depictedItem, @NotNull String url) {
final Pair<DepictedItem, Integer> itemAndPosition = returnItemAndPosition(depictedItem);
if (itemAndPosition != null) {
itemAndPosition.first.setImageUrl(url);
adapter.notifyItemChanged(itemAndPosition.second);
}
}
@Nullable @Nullable
private Pair<DepictedItem,Integer> returnItemAndPosition(@NotNull DepictedItem depictedItem) { private Pair<DepictedItem,Integer> returnItemAndPosition(@NotNull DepictedItem depictedItem) {
for (int i = 0; i < adapter.getItemCount(); i++) { for (int i = 0; i < adapter.getItemCount(); i++) {
@ -179,14 +170,6 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
presenter.onDepictItemClicked(item); presenter.onDepictItemClicked(item);
} }
/**
* Fetch thumbnail for the given entityId at the given position
*/
@Override
public void fetchThumbnailUrlForEntity(DepictedItem depictedItem) {
presenter.fetchThumbnailForEntityId(depictedItem);
}
/** /**
* Text change listener for the edit text view of depicts * Text change listener for the edit text view of depicts
*/ */

View file

@ -1,9 +1,8 @@
package fr.free.nrw.commons.upload.depicts; package fr.free.nrw.commons.upload.depicts;
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse; import fr.free.nrw.commons.wikidata.model.DepictSearchResponse;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import org.wikipedia.wikidata.ClaimsResponse; import org.wikipedia.wikidata.Entities;
import retrofit2.http.GET; import retrofit2.http.GET;
import retrofit2.http.Query; import retrofit2.http.Query;
@ -24,6 +23,6 @@ public interface DepictsInterface {
@GET("/w/api.php?action=wbsearchentities&format=json&type=item&uselang=en") @GET("/w/api.php?action=wbsearchentities&format=json&type=item&uselang=en")
Single<DepictSearchResponse> searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset); Single<DepictSearchResponse> searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset);
@GET("/w/api.php?action=wbgetclaims&format=json&property=P18") @GET("/w/api.php?format=json&action=wbgetentities")
Observable<ClaimsResponse> getImageForEntity(@Query("entity") String entityId); Single<Entities> getEntities(@Query("ids")String ids, @Query("languages")String language);
} }

View file

@ -3,13 +3,11 @@ package fr.free.nrw.commons.upload.depicts
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import fr.free.nrw.commons.di.CommonsApplicationModule import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.explore.depictions.DepictsClient.NO_DEPICTED_IMAGE
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.Scheduler import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.processors.PublishProcessor import io.reactivex.processors.PublishProcessor
@ -26,8 +24,7 @@ import javax.inject.Singleton
class DepictsPresenter @Inject constructor( class DepictsPresenter @Inject constructor(
private val repository: UploadRepository, private val repository: UploadRepository,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler
private val depictsClient: DepictsClient
) : DepictsContract.UserActionListener { ) : DepictsContract.UserActionListener {
companion object { companion object {
@ -38,7 +35,6 @@ class DepictsPresenter @Inject constructor(
private val compositeDisposable: CompositeDisposable = CompositeDisposable() private val compositeDisposable: CompositeDisposable = CompositeDisposable()
private val searchTerm: PublishProcessor<String> = PublishProcessor.create() private val searchTerm: PublishProcessor<String> = PublishProcessor.create()
private val depictedItems: MutableLiveData<List<DepictedItem>> = MutableLiveData() private val depictedItems: MutableLiveData<List<DepictedItem>> = MutableLiveData()
private val idsToImageUrls = mutableMapOf<String, String>()
override fun onAttachView(view: DepictsContract.View) { override fun onAttachView(view: DepictsContract.View) {
this.view = view this.view = view
@ -75,19 +71,14 @@ class DepictsPresenter @Inject constructor(
return repository.searchAllEntities(it) return repository.searchAllEntities(it)
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.map { repository.selectedDepictions + it } .map { repository.selectedDepictions + it }
.map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
.map { it.distinctBy(DepictedItem::id) } .map { it.distinctBy(DepictedItem::id) }
.map(::addImageUrlsFromCache)
} }
private fun addImageUrlsFromCache(depictions: List<DepictedItem>) =
depictions.map { item ->
idsToImageUrls[item.id]?.let { item.copy(imageUrl = it) } ?: item
}
override fun onDetachView() { override fun onDetachView() {
view = DUMMY view = DUMMY
compositeDisposable.clear() compositeDisposable.clear()
idsToImageUrls.clear()
} }
override fun onPreviousButtonClicked() { override fun onPreviousButtonClicked() {
@ -122,30 +113,6 @@ class DepictsPresenter @Inject constructor(
} }
} }
/**
* Fetch thumbnail for the Wikidata Item
* @param entityId entityId of the item
* @param position position of the item
*/
override fun fetchThumbnailForEntityId(depictedItem: DepictedItem) {
compositeDisposable.add(
imageUrlFromNetworkOrCache(depictedItem)
.observeOn(mainThreadScheduler)
.filter { it != NO_DEPICTED_IMAGE }
.subscribe(
{ view.onUrlFetched(depictedItem, it) },
{ Timber.e(it) }
)
)
}
private fun imageUrlFromNetworkOrCache(depictedItem: DepictedItem): Single<String> =
if (idsToImageUrls.containsKey(depictedItem.id))
Single.just(idsToImageUrls[depictedItem.id])
else
depictsClient.getP18ForItem(depictedItem.id)
.subscribeOn(ioScheduler)
.doOnSuccess { idsToImageUrls[depictedItem.id] = it }
} }
inline fun <reified T> proxy() = Proxy inline fun <reified T> proxy() = Proxy

View file

@ -1,10 +1,9 @@
package fr.free.nrw.commons.upload.structure.depictions package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.depicts.DepictsInterface
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.processors.BehaviorProcessor import io.reactivex.processors.BehaviorProcessor
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -12,10 +11,11 @@ import javax.inject.Singleton
* The model class for depictions in upload * The model class for depictions in upload
*/ */
@Singleton @Singleton
class DepictModel @Inject constructor(private val depictsInterface: DepictsInterface) { class DepictModel @Inject constructor(private val depictsClient: DepictsClient) {
var nearbyPlaces: BehaviorProcessor<List<Place>> = BehaviorProcessor.createDefault(emptyList()) var nearbyPlaces: BehaviorProcessor<List<Place>> = BehaviorProcessor.createDefault(emptyList())
companion object { companion object {
private const val SEARCH_DEPICTS_LIMIT = 25 private const val SEARCH_DEPICTS_LIMIT = 25
} }
@ -25,16 +25,22 @@ class DepictModel @Inject constructor(private val depictsInterface: DepictsInter
*/ */
fun searchAllEntities(query: String): Flowable<List<DepictedItem>> { fun searchAllEntities(query: String): Flowable<List<DepictedItem>> {
if (query.isBlank()) { if (query.isBlank()) {
return nearbyPlaces.map { it.map(::DepictedItem) } return nearbyPlaces.switchMap { places: List<Place> ->
depictsClient.getEntities(
places.mapNotNull { it.wikiDataEntityId }.joinToString("|")
)
.map {
it.entities()!!.values.mapIndexed { index, entity ->
DepictedItem(entity, places[index])
}
}.toFlowable()
}
} }
return networkItems(query) return networkItems(query)
} }
private fun networkItems(query: String): Flowable<List<DepictedItem>> { private fun networkItems(query: String): Flowable<List<DepictedItem>> {
val language = Locale.getDefault().language return depictsClient.searchForDepictions(query, SEARCH_DEPICTS_LIMIT, 0)
return depictsInterface
.searchForDepicts(query, "$SEARCH_DEPICTS_LIMIT", language, language, "0")
.map { it.search.map(::DepictedItem) }
.toFlowable() .toFlowable()
} }

View file

@ -1,8 +1,13 @@
package fr.free.nrw.commons.upload.structure.depictions package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.explore.depictions.DepictsClient.Companion.getImageUrl
import fr.free.nrw.commons.explore.depictions.THUMB_IMAGE_SIZE
import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.model.DepictSearchItem import fr.free.nrw.commons.wikidata.WikidataProperties
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import org.wikipedia.wikidata.Statement_partial
/** /**
* Model class for Depicted Item in Upload and Explore * Model class for Depicted Item in Upload and Explore
@ -10,36 +15,57 @@ import fr.free.nrw.commons.wikidata.model.DepictSearchItem
data class DepictedItem constructor( data class DepictedItem constructor(
override val name: String, override val name: String,
val description: String?, val description: String?,
var imageUrl: String, val imageUrl: String?,
val instanceOfs: List<String>,
var isSelected: Boolean, var isSelected: Boolean,
override val id: String override val id: String
) : WikidataItem { ) : WikidataItem {
constructor(depictSearchItem: DepictSearchItem) : this(
depictSearchItem.label, constructor(entity: Entities.Entity) : this(
depictSearchItem.description, entity,
"", entity.labels().values.firstOrNull()?.value() ?: "",
false, entity.descriptions().values.firstOrNull()?.value() ?: ""
depictSearchItem.id
) )
constructor(place: Place) : this( constructor(entity: Entities.Entity, place: Place) : this(
entity,
place.name, place.name,
place.longDescription, place.longDescription
"",
false,
place.wikiDataEntityId!!
) )
var position = 0 constructor(entity: Entities.Entity, name: String, description: String) : this(
name,
description,
entity[WikidataProperties.IMAGE].primaryImageValue?.let {
getImageUrl(it.value, THUMB_IMAGE_SIZE)
},
entity[WikidataProperties.INSTANCE_OF].toIds(),
false,
entity.id()
)
override fun equals(o: Any?) = when { override fun equals(other: Any?) = when {
this === o -> true this === other -> true
o is DepictedItem -> name == o.name other is DepictedItem -> name == other.name
else -> false else -> false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return name?.hashCode() ?: 0 return name.hashCode()
} }
} }
private fun List<Statement_partial>?.toIds(): List<String> {
return this?.map { it.mainSnak.dataValue }
?.filterIsInstance<DataValue.EntityId>()
?.map { it.value.id }
?: emptyList()
}
private val List<Statement_partial>?.primaryImageValue: DataValue.ValueString?
get() = this?.first()?.mainSnak?.dataValue as? DataValue.ValueString
operator fun Entities.Entity.get(property: WikidataProperties) =
statements?.get(property.propertyName)

View file

@ -5,6 +5,4 @@ package fr.free.nrw.commons.upload.structure.depictions;
*/ */
public interface UploadDepictsCallback { public interface UploadDepictsCallback {
void depictsClicked(DepictedItem item); void depictsClicked(DepictedItem item);
void fetchThumbnailUrlForEntity(DepictedItem depictedItem);
} }

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.wikidata
enum class WikidataDisambiguationItems(val id: String) {
DISAMBIGUATION_PAGE("Q4167410"), INTERNAL_ITEM("Q17442446"), CATEGORY("Q4167836");
companion object {
fun isDisambiguationItem(ids: List<String>) =
values().any { disambiguationItem: WikidataDisambiguationItems ->
ids.any { id -> disambiguationItem.id == id }
}
}
}

View file

@ -3,6 +3,6 @@ package fr.free.nrw.commons.wikidata
import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.BuildConfig
enum class WikidataProperties(val propertyName: String) { enum class WikidataProperties(val propertyName: String) {
IMAGE("P18"), DEPICTS(BuildConfig.DEPICTS_PROPERTY); IMAGE("P18"), DEPICTS(BuildConfig.DEPICTS_PROPERTY), INSTANCE_OF("P31");
} }

View file

@ -52,15 +52,6 @@ class SubDepictionListPresenterTest {
subDepictionListPresenter?.onAttachView(view) subDepictionListPresenter?.onAttachView(view)
} }
@Test
fun fetchThumbnailForEntityId() {
val singleString: Single<String> = Single.just(String())
Mockito.`when`(depictsClient?.getP18ForItem(ArgumentMatchers.anyString())).thenReturn(singleString)
subDepictionListPresenter?.fetchThumbnailForEntityId("Q9394", 0)
testScheduler?.triggerActions()
view?.onImageUrlFetched("url", 0)
}
@Test @Test
fun initSubDepictionListForParentClass() { fun initSubDepictionListForParentClass() {
Mockito.`when`(okHttpJsonApiClient?.getParentQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable) Mockito.`when`(okHttpJsonApiClient?.getParentQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)

View file

@ -1,68 +1,60 @@
package fr.free.nrw.commons.explore.depictions package fr.free.nrw.commons.explore.depictions
import org.mockito.Mockito.verify import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.depictedItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
class SearchDepictionsPresenterTest { class SearchDepictionsPresenterTest {
@Mock @Mock
internal var view: SearchDepictionsFragmentContract.View? = null internal lateinit var view: SearchDepictionsFragmentContract.View
var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter? = null private lateinit var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter
var testScheduler: TestScheduler? = null private lateinit var testScheduler: TestScheduler
var jsonKvStore: JsonKvStore? = null
//var mediaWikiApi: MediaWikiApi? = null
@Mock @Mock
var recentSearchesDao: RecentSearchesDao? = null private lateinit var jsonKvStore: JsonKvStore
@Mock @Mock
var depictsClient: DepictsClient? = null lateinit var recentSearchesDao: RecentSearchesDao
var testObservable: Observable<DepictedItem>? = null @Mock
lateinit var depictsClient: DepictsClient
var mediaList: ArrayList<DepictedItem> = ArrayList()
@Before @Before
@Throws(Exception::class) @Throws(Exception::class)
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler() testScheduler = TestScheduler()
val depictedItem: DepictedItem = DepictedItem("label", "description", "url", false, "Q9394") val depictedItem: DepictedItem = depictedItem(instanceOfs = listOf())
mediaList.add(depictedItem) searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
testObservable = Observable.just(depictedItem) jsonKvStore,
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(jsonKvStore, recentSearchesDao, depictsClient, testScheduler, testScheduler) recentSearchesDao,
searchDepictionsFragmentPresenter?.onAttachView(view) depictsClient,
testScheduler,
testScheduler
)
searchDepictionsFragmentPresenter.onAttachView(view)
} }
@Test @Test
fun updateDepictionList() { fun updateDepictionList() {
Mockito.`when`(depictsClient?.searchForDepictions(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())).thenReturn(testObservable) val expectedList = listOf(depictedItem())
searchDepictionsFragmentPresenter?.updateDepictionList("rabbit", 25, false) whenever(depictsClient.searchForDepictions("rabbit", 25, 0))
testScheduler?.triggerActions() .thenReturn(Single.just(expectedList))
verify(view)?.onSuccess(mediaList) searchDepictionsFragmentPresenter.updateDepictionList("rabbit", 25, false)
testScheduler.triggerActions()
verify(view)?.onSuccess(expectedList)
} }
@Test }
fun fetchThumbnailForEntityId() {
val singleString: Single<String> = Single.just(String())
Mockito.`when`(depictsClient?.getP18ForItem(ArgumentMatchers.anyString())).thenReturn(singleString)
searchDepictionsFragmentPresenter?.fetchThumbnailForEntityId("Q9394", 0)
testScheduler?.triggerActions()
verify(view)?.onImageUrlFetched("", 0)
}
}

View file

@ -2,18 +2,15 @@ package fr.free.nrw.commons.upload
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.jraska.livedata.test import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.explore.depictions.DepictsClient import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.explore.depictions.DepictsClient.NO_DEPICTED_IMAGE
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.DepictsContract import fr.free.nrw.commons.upload.depicts.DepictsContract
import fr.free.nrw.commons.upload.depicts.DepictsPresenter import fr.free.nrw.commons.upload.depicts.DepictsPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -48,7 +45,7 @@ class DepictsPresenterTest {
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler() testScheduler = TestScheduler()
depictsPresenter = DepictsPresenter(repository, testScheduler, testScheduler, depictsClient) depictsPresenter = DepictsPresenter(repository, testScheduler, testScheduler)
depictsPresenter.onAttachView(view) depictsPresenter.onAttachView(view)
} }
@ -60,8 +57,15 @@ class DepictsPresenterTest {
} }
@Test @Test
fun `search results emission returns distinct results + selected items`() { fun `search results emission returns distinct results + selected items without disambiguations`() {
val searchResults = listOf(depictedItem(), depictedItem()) val searchResults = listOf(
depictedItem(id="nonUnique"),
depictedItem(id="nonUnique"),
depictedItem(
id = "unique",
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id)
)
)
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults)) whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
val selectedItem = depictedItem(id = "selected") val selectedItem = depictedItem(id = "selected")
whenever(repository.selectedDepictions).thenReturn(listOf(selectedItem)) whenever(repository.selectedDepictions).thenReturn(listOf(selectedItem))
@ -71,22 +75,7 @@ class DepictsPresenterTest {
verify(view).showError(false) verify(view).showError(false)
depictsPresenter.depictedItems depictsPresenter.depictedItems
.test() .test()
.assertValue(listOf(selectedItem, depictedItem())) .assertValue(listOf(selectedItem, depictedItem(id="nonUnique")))
}
@Test
fun `searchResults retrieve imageUrls from cache`() {
val depictedItem = depictedItem()
whenever(depictsClient.getP18ForItem(depictedItem.id)).thenReturn(Single.just("url"))
depictsPresenter.fetchThumbnailForEntityId(depictedItem)
testScheduler.triggerActions()
val searchResults = listOf(depictedItem(), depictedItem())
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
depictsPresenter.searchForDepictions("")
testScheduler.triggerActions()
depictsPresenter.depictedItems
.test()
.assertValue(listOf(depictedItem(imageUrl = "url")))
} }
@Test @Test
@ -149,42 +138,14 @@ class DepictsPresenterTest {
verify(view).noDepictionSelected() verify(view).noDepictionSelected()
} }
@Test
fun `image urls fetched from network update the view`() {
val depictedItem = depictedItem()
whenever(depictsClient.getP18ForItem(depictedItem.id)).thenReturn(Single.just("url"))
depictsPresenter.fetchThumbnailForEntityId(depictedItem)
testScheduler.triggerActions()
verify(view).onUrlFetched(depictedItem, "url")
}
@Test
fun `image urls fetched from network filter NO_DEPICTED_IMAGE`() {
val depictedItem = depictedItem()
whenever(depictsClient.getP18ForItem(depictedItem.id))
.thenReturn(Single.just(NO_DEPICTED_IMAGE))
depictsPresenter.fetchThumbnailForEntityId(depictedItem)
testScheduler.triggerActions()
verify(view, never()).onUrlFetched(depictedItem, NO_DEPICTED_IMAGE)
}
@Test
fun `successive image urls fetched from cache`() {
val depictedItem = depictedItem()
whenever(depictsClient.getP18ForItem(depictedItem.id)).thenReturn(Single.just("url"))
depictsPresenter.fetchThumbnailForEntityId(depictedItem)
testScheduler.triggerActions()
verify(view).onUrlFetched(depictedItem, "url")
depictsPresenter.fetchThumbnailForEntityId(depictedItem)
testScheduler.triggerActions()
verify(view, times(2)).onUrlFetched(depictedItem, "url")
}
} }
fun depictedItem( fun depictedItem(
name: String = "label", name: String = "label",
description: String = "desc", description: String = "desc",
imageUrl: String = "", imageUrl: String = "",
instanceOfs: List<String> = listOf(),
isSelected: Boolean = false, isSelected: Boolean = false,
id: String = "entityId" id: String = "entityId"
) = DepictedItem(name, description, imageUrl, isSelected, id) ) = DepictedItem(name, description, imageUrl, instanceOfs, isSelected, id)

View file

@ -2,83 +2,94 @@ package org.wikipedia.wikidata
import org.wikipedia.json.RuntimeTypeAdapterFactory import org.wikipedia.json.RuntimeTypeAdapterFactory
/*"datavalue": {
"value": {
"entity-type": "item",
"id": "Q30",
"numeric-id": 30
},
"type": "wikibase-entityid"
}
OR
"datavalue": {
"value": "SomePicture.jpg",
"type": "string"
}
OR
"datavalue": {
"value": {
"latitude": 37.7733,
"longitude": -122.412255,
"altitude": null,
"precision": 1.0e-6,
"globe": "http://www.wikidata.org/entity/Q2"
},
"type": "globecoordinate"
}
OR
"datavalue": {
"value": {
"time": "+2019-12-03T00:00:00Z",
"timezone": 0,
"before": 0,
"after": 0,
"precision": 11,
"calendarmodel": "http://www.wikidata.org/entity/Q1985727"
},
"type": "time"
}
*/
sealed class DataValue(val type: String) { sealed class DataValue(val type: String) {
companion object { companion object {
@JvmStatic @JvmStatic
val polymorphicTypeAdapter = val polymorphicTypeAdapter =
RuntimeTypeAdapterFactory.of(DataValue::class.java, DataValue::type.name) RuntimeTypeAdapterFactory.of(DataValue::class.java, DataValue::type.name)
.registerSubtype(DataValueEntityId::class.java, DataValueEntityId.TYPE) .registerSubtype(EntityId::class.java, EntityId.TYPE)
.registerSubtype(DataValueString::class.java, DataValueString.TYPE) .registerSubtype(ValueString::class.java, ValueString.TYPE)
.registerSubtype( .registerSubtype(GlobeCoordinate_partial::class.java, GlobeCoordinate_partial.TYPE)
DataValueGlobeCoordinate_partial::class.java, .registerSubtype(Time_partial::class.java, Time_partial.TYPE)
DataValueGlobeCoordinate_partial.TYPE .registerSubtype(Quantity_partial::class.java, Quantity_partial.TYPE)
) .registerSubtype(MonoLingualText_partial::class.java, MonoLingualText_partial.TYPE)
.registerSubtype(
DataValueTime_partial::class.java,
DataValueTime_partial.TYPE
)
} }
data class DataValueEntityId(val value: WikiBaseEntityValue) : // "value": {
DataValue(TYPE) { // "entity-type": "item",
// "id": "Q30",
// "numeric-id": 30
// },
// "type": "wikibase-entityid"
// }
data class EntityId(val value: WikiBaseEntityValue) : DataValue(TYPE) {
companion object { companion object {
const val TYPE = "wikibase-entityid" const val TYPE = "wikibase-entityid"
} }
} }
data class DataValueString(val value: String) : DataValue(TYPE) { // {
// "value": "SomePicture.jpg",
// "type": "string"
// }
data class ValueString(val value: String) : DataValue(TYPE) {
companion object { companion object {
const val TYPE = "string" const val TYPE = "string"
} }
} }
class DataValueGlobeCoordinate_partial() : // "value": {
DataValue(TYPE) { // "latitude": 37.7733,
// "longitude": -122.412255,
// "altitude": null,
// "precision": 1.0e-6,
// "globe": "http://www.wikidata.org/entity/Q2"
// },
// "type": "globecoordinate"
// }
class GlobeCoordinate_partial() : DataValue(TYPE) {
companion object { companion object {
const val TYPE = "globecoordinate" const val TYPE = "globecoordinate"
} }
} }
class DataValueTime_partial() : DataValue(TYPE) { // "value": {
// "time": "+2019-12-03T00:00:00Z",
// "timezone": 0,
// "before": 0,
// "after": 0,
// "precision": 11,
// "calendarmodel": "http://www.wikidata.org/entity/Q1985727"
// },
// "type": "time"
// }
class Time_partial() : DataValue(TYPE) {
companion object { companion object {
const val TYPE = "time" const val TYPE = "time"
} }
} }
// {
// "value": {
// "amount": "+587",
// "unit": "http://www.wikidata.org/entity/Q828224"
// }
// }
class Quantity_partial() : DataValue(TYPE) {
companion object {
const val TYPE = "quantity"
}
}
// {
// "value": {
// "text": "활",
// "language": "ko"
// }
// }
class MonoLingualText_partial() : DataValue(TYPE) {
companion object {
const val TYPE = "monolingualtext"
}
}
} }

View file

@ -1,6 +1,6 @@
package org.wikipedia.wikidata package org.wikipedia.wikidata
import org.wikipedia.wikidata.DataValue.DataValueEntityId import org.wikipedia.wikidata.DataValue.EntityId
data class EditClaim(val claims: List<Statement_partial>) { data class EditClaim(val claims: List<Statement_partial>) {
@ -14,7 +14,7 @@ data class EditClaim(val claims: List<Statement_partial>) {
Snak_partial( Snak_partial(
"value", "value",
propertyName, propertyName,
DataValueEntityId( EntityId(
WikiBaseEntityValue( WikiBaseEntityValue(
"item", "item",
entityId, entityId,

View file

@ -2,6 +2,7 @@ package org.wikipedia.wikidata;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -42,7 +43,7 @@ public class Entities extends MwResponse implements PostProcessingTypeAdapter.Po
@Nullable private Map<String, Label> labels; @Nullable private Map<String, Label> labels;
@Nullable private Map<String, Label> descriptions; @Nullable private Map<String, Label> descriptions;
@Nullable private Map<String, SiteLink> sitelinks; @Nullable private Map<String, SiteLink> sitelinks;
@Nullable private Map<String, List<Statement_partial>> statements; @Nullable @SerializedName(value = "statements", alternate = "claims") private Map<String, List<Statement_partial>> statements;
@Nullable private String missing; @Nullable private String missing;
@NonNull public String id() { @NonNull public String id() {