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

View file

@ -1,5 +1,8 @@
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.os.Bundle;
import android.view.LayoutInflater;
@ -7,23 +10,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
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.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
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.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
/**
* 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 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;
/**
* Stores entityId for the depiction
@ -77,12 +68,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
getActivity().finish();
WikidataItemDetailsActivity.startYourself(getContext(), item);
}
@Override
public void fetchThumbnailUrlForEntity(String entityId, int position) {
presenter.fetchThumbnailForEntityId(entityId, position);
}
});
@Override
@ -140,15 +125,8 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
}
@Override
public void onImageUrlFetched(String response, int position) {
depictionsAdapter.getItem(position).setImageUrl(response);
depictionsAdapter.notifyItemChanged(position);
}
@Override
public void onSuccess(List<DepictedItem> mediaList) {
hasMoreImages = false;
progressBar.setVisibility(View.GONE);
depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
@ -164,7 +142,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
@Override
public void initErrorView() {
hasMoreImages = false;
progressBar.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
depictionNotFound.setVisibility(VISIBLE);
@ -180,11 +157,6 @@ public class SubDepictionListFragment extends DaggerFragment implements SubDepic
@Override
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 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.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
@ -13,7 +12,6 @@ import io.reactivex.disposables.CompositeDisposable;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@ -48,10 +46,6 @@ public class SubDepictionListPresenter implements SubDepictionListContract.UserA
DepictsClient depictsClient;
private List<DepictedItem> queryList = new ArrayList<>();
OkHttpJsonApiClient okHttpJsonApiClient;
/**
* variable used to record the number of API calls already made for fetching Thumbnails
*/
private int size = 0;
@Inject
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;
}
/**
* 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
public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException {
size = 0;
if (isParentClass) {
compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid)
.subscribeOn(ioScheduler)
@ -137,9 +101,6 @@ public class SubDepictionListPresenter implements SubDepictionListContract.UserA
} else {
this.queryList.addAll(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
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
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 SparqlResponse(val results: Result)
data class Result(val bindings: List<Binding>)
@ -21,6 +8,8 @@ data class Binding(
val item: SparqInfo,
val itemLabel: SparqInfo,
val itemDescription: SparqInfo? = null
)
) {
val id: String by lazy { item.value.substringAfterLast("/") }
}
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.PageEditInterface;
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.media.MediaDetailInterface;
import fr.free.nrw.commons.media.MediaInterface;
@ -77,10 +78,12 @@ public class NetworkingModule {
@Provides
@Singleton
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
@Named("tools_forge") HttpUrl toolsForgeUrl,
@Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) {
return new OkHttpJsonApiClient(okHttpClient,
depictsClient,
toolsForgeUrl,
WIKIDATA_SPARQL_QUERY_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);
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 boolean isLastPage;
@ -218,12 +209,6 @@ public class SearchDepictionsFragment extends CommonsDaggerSupportFragment imple
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
* or reset the isLastPage for the current query

View file

@ -49,8 +49,6 @@ public interface SearchDepictionsFragmentContract {
*/
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
* or reset the isLastPage for the current query
@ -85,10 +83,5 @@ public interface SearchDepictionsFragmentContract {
* @return query
*/
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)
.observeOn(mainThreadScheduler)
.doOnSubscribe(disposable -> saveQuery())
.collect(ArrayList<DepictedItem>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}
@ -154,23 +153,7 @@ public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragm
this.queryList.addAll(mediaList);
view.onSuccess(mediaList);
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;
import static fr.free.nrw.commons.explore.depictions.DepictsClient.NO_DEPICTED_IMAGE;
import android.graphics.Bitmap;
import android.net.Uri;
@ -10,12 +9,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
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.ImageRequestBuilder;
import com.pedrogomez.renderers.Renderer;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import timber.log.Timber;
/**
* Renderer for DepictedItem
@ -81,10 +75,7 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
tvDepictionDesc.setText(item.getDescription());
imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp));
Timber.e("line86"+item.getImageUrl());
if (!TextUtils.isEmpty(item.getImageUrl())) {
if (!item.getImageUrl().equals(NO_DEPICTED_IMAGE) && !item.getImageUrl().equals(""))
{
ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(item.getImageUrl()))
.setAutoRotateEnabled(true)
@ -99,7 +90,6 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", item.getImageUrl());
//imageView.setImageBitmap(Bitmap.createBitmap(bitmap));
imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
dataSource.close();
@ -108,7 +98,6 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
@Override
public void onFailureImpl(DataSource dataSource) {
Timber.d("Error getting bitmap from image url %s", item.getImageUrl());
if (dataSource != null) {
dataSource.close();
}
@ -116,11 +105,8 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
}, CallerThreadExecutor.getInstance());
}
}
}
public interface DepictCallback {
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 fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS
import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.DataValue.DataValueEntityId
import org.wikipedia.wikidata.DataValue.EntityId
import org.wikipedia.wikidata.Entities
import java.util.*
@ -18,7 +18,7 @@ data class Depictions(val depictions: List<IdAndLabel>) : Parcelable {
entities.first?.statements
?.getOrElse(DEPICTS.propertyName, { emptyList() })
?.map { statement ->
(statement.mainSnak.dataValue as DataValueEntityId).value.id
(statement.mainSnak.dataValue as EntityId).value.id
}
?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) }
?: emptyList()

View file

@ -602,10 +602,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
textView.setText(depictionName);
if (depictionLoaded) {
item.setOnClickListener(view -> {
DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId);
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictedItem.getName());
intent.putExtra("entityId", depictedItem.getId());
intent.putExtra("wikidataItemName", depictionName);
intent.putExtra("entityId", entityId);
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.campaigns.CampaignResponseDTO;
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.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
@ -38,6 +39,7 @@ import timber.log.Timber;
public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
@ -46,11 +48,13 @@ public class OkHttpJsonApiClient {
@Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
@ -219,14 +223,11 @@ public class OkHttpJsonApiClient {
}
private Observable<List<DepictedItem>> depictedItemsFrom(Request request) {
return Observable.fromCallable(() -> {
return depictsClient.toDepictions(Observable.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
return gson.fromJson(body.string(), SparqlResponse.class).toDepictedItems();
}catch (Exception e) {
Timber.e(e);
return new ArrayList<DepictedItem>();
return gson.fromJson(body.string(), SparqlResponse.class);
}
}).doOnError(Timber::e);
}).doOnError(Timber::e));
}
@NotNull

View file

@ -30,7 +30,8 @@ public class Place implements Parcelable {
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.longDescription = longDescription;
this.location = location;

View file

@ -79,7 +79,6 @@ public class UploadDepictsRenderer extends Renderer<DepictedItem> {
final String imageUrl = item.getImageUrl();
if (TextUtils.isEmpty(imageUrl)) {
imageView.setImageURI(UriUtil.getUriForResourceId(R.drawable.ic_wikidata_logo_24dp));
listener.fetchThumbnailUrlForEntity(item);
} else {
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.upload.structure.depictions.DepictedItem;
import java.util.List;
import org.jetbrains.annotations.NotNull;
/**
* The contract with which DepictsFragment and its presenter would talk to each other
@ -41,9 +40,6 @@ public interface DepictsContract {
* add depictions to list
*/
void setDepictsList(List<DepictedItem> depictedItemList);
void onUrlFetched(@NotNull DepictedItem depictedItem, @NotNull String url);
}
interface UserActionListener extends BasePresenter<View> {
@ -71,7 +67,5 @@ public interface DepictsContract {
void verifyDepictions();
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
private Pair<DepictedItem,Integer> returnItemAndPosition(@NotNull DepictedItem depictedItem) {
for (int i = 0; i < adapter.getItemCount(); i++) {
@ -179,14 +170,6 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
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
*/

View file

@ -1,9 +1,8 @@
package fr.free.nrw.commons.upload.depicts;
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.wikipedia.wikidata.ClaimsResponse;
import org.wikipedia.wikidata.Entities;
import retrofit2.http.GET;
import retrofit2.http.Query;
@ -24,6 +23,6 @@ public interface DepictsInterface {
@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);
@GET("/w/api.php?action=wbgetclaims&format=json&property=P18")
Observable<ClaimsResponse> getImageForEntity(@Query("entity") String entityId);
@GET("/w/api.php?format=json&action=wbgetentities")
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.MutableLiveData
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.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable
import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction
import io.reactivex.processors.PublishProcessor
@ -26,8 +24,7 @@ import javax.inject.Singleton
class DepictsPresenter @Inject constructor(
private val repository: UploadRepository,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler,
private val depictsClient: DepictsClient
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler
) : DepictsContract.UserActionListener {
companion object {
@ -38,7 +35,6 @@ class DepictsPresenter @Inject constructor(
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
private val searchTerm: PublishProcessor<String> = PublishProcessor.create()
private val depictedItems: MutableLiveData<List<DepictedItem>> = MutableLiveData()
private val idsToImageUrls = mutableMapOf<String, String>()
override fun onAttachView(view: DepictsContract.View) {
this.view = view
@ -75,19 +71,14 @@ class DepictsPresenter @Inject constructor(
return repository.searchAllEntities(it)
.subscribeOn(ioScheduler)
.map { repository.selectedDepictions + it }
.map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
.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() {
view = DUMMY
compositeDisposable.clear()
idsToImageUrls.clear()
}
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

View file

@ -1,10 +1,9 @@
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.upload.depicts.DepictsInterface
import io.reactivex.Flowable
import io.reactivex.processors.BehaviorProcessor
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@ -12,10 +11,11 @@ import javax.inject.Singleton
* The model class for depictions in upload
*/
@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())
companion object {
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>> {
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)
}
private fun networkItems(query: String): Flowable<List<DepictedItem>> {
val language = Locale.getDefault().language
return depictsInterface
.searchForDepicts(query, "$SEARCH_DEPICTS_LIMIT", language, language, "0")
.map { it.search.map(::DepictedItem) }
return depictsClient.searchForDepictions(query, SEARCH_DEPICTS_LIMIT, 0)
.toFlowable()
}

View file

@ -1,8 +1,13 @@
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.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
@ -10,36 +15,57 @@ import fr.free.nrw.commons.wikidata.model.DepictSearchItem
data class DepictedItem constructor(
override val name: String,
val description: String?,
var imageUrl: String,
val imageUrl: String?,
val instanceOfs: List<String>,
var isSelected: Boolean,
override val id: String
) : WikidataItem {
constructor(depictSearchItem: DepictSearchItem) : this(
depictSearchItem.label,
depictSearchItem.description,
"",
false,
depictSearchItem.id
constructor(entity: Entities.Entity) : this(
entity,
entity.labels().values.firstOrNull()?.value() ?: "",
entity.descriptions().values.firstOrNull()?.value() ?: ""
)
constructor(place: Place) : this(
constructor(entity: Entities.Entity, place: Place) : this(
entity,
place.name,
place.longDescription,
"",
false,
place.wikiDataEntityId!!
place.longDescription
)
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 {
this === o -> true
o is DepictedItem -> name == o.name
override fun equals(other: Any?) = when {
this === other -> true
other is DepictedItem -> name == other.name
else -> false
}
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 {
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
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)
}
@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
fun initSubDepictionListForParentClass() {
Mockito.`when`(okHttpJsonApiClient?.getParentQIDs(ArgumentMatchers.anyString())).thenReturn(testObservable)

View file

@ -1,68 +1,60 @@
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.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.depictedItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
class SearchDepictionsPresenterTest {
@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
var jsonKvStore: JsonKvStore? = null
//var mediaWikiApi: MediaWikiApi? = null
private lateinit var testScheduler: TestScheduler
@Mock
var recentSearchesDao: RecentSearchesDao? = null
private lateinit var jsonKvStore: JsonKvStore
@Mock
var depictsClient: DepictsClient? = null
lateinit var recentSearchesDao: RecentSearchesDao
var testObservable: Observable<DepictedItem>? = null
var mediaList: ArrayList<DepictedItem> = ArrayList()
@Mock
lateinit var depictsClient: DepictsClient
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
val depictedItem: DepictedItem = DepictedItem("label", "description", "url", false, "Q9394")
mediaList.add(depictedItem)
testObservable = Observable.just(depictedItem)
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(jsonKvStore, recentSearchesDao, depictsClient, testScheduler, testScheduler)
searchDepictionsFragmentPresenter?.onAttachView(view)
val depictedItem: DepictedItem = depictedItem(instanceOfs = listOf())
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
jsonKvStore,
recentSearchesDao,
depictsClient,
testScheduler,
testScheduler
)
searchDepictionsFragmentPresenter.onAttachView(view)
}
@Test
fun updateDepictionList() {
Mockito.`when`(depictsClient?.searchForDepictions(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())).thenReturn(testObservable)
searchDepictionsFragmentPresenter?.updateDepictionList("rabbit", 25, false)
testScheduler?.triggerActions()
verify(view)?.onSuccess(mediaList)
val expectedList = listOf(depictedItem())
whenever(depictsClient.searchForDepictions("rabbit", 25, 0))
.thenReturn(Single.just(expectedList))
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 com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
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.upload.depicts.DepictsContract
import fr.free.nrw.commons.upload.depicts.DepictsPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Rule
@ -48,7 +45,7 @@ class DepictsPresenterTest {
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
depictsPresenter = DepictsPresenter(repository, testScheduler, testScheduler, depictsClient)
depictsPresenter = DepictsPresenter(repository, testScheduler, testScheduler)
depictsPresenter.onAttachView(view)
}
@ -60,8 +57,15 @@ class DepictsPresenterTest {
}
@Test
fun `search results emission returns distinct results + selected items`() {
val searchResults = listOf(depictedItem(), depictedItem())
fun `search results emission returns distinct results + selected items without disambiguations`() {
val searchResults = listOf(
depictedItem(id="nonUnique"),
depictedItem(id="nonUnique"),
depictedItem(
id = "unique",
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id)
)
)
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
val selectedItem = depictedItem(id = "selected")
whenever(repository.selectedDepictions).thenReturn(listOf(selectedItem))
@ -71,22 +75,7 @@ class DepictsPresenterTest {
verify(view).showError(false)
depictsPresenter.depictedItems
.test()
.assertValue(listOf(selectedItem, depictedItem()))
}
@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")))
.assertValue(listOf(selectedItem, depictedItem(id="nonUnique")))
}
@Test
@ -149,42 +138,14 @@ class DepictsPresenterTest {
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(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
isSelected: Boolean = false,
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
/*"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) {
companion object {
@JvmStatic
val polymorphicTypeAdapter =
RuntimeTypeAdapterFactory.of(DataValue::class.java, DataValue::type.name)
.registerSubtype(DataValueEntityId::class.java, DataValueEntityId.TYPE)
.registerSubtype(DataValueString::class.java, DataValueString.TYPE)
.registerSubtype(
DataValueGlobeCoordinate_partial::class.java,
DataValueGlobeCoordinate_partial.TYPE
)
.registerSubtype(
DataValueTime_partial::class.java,
DataValueTime_partial.TYPE
)
.registerSubtype(EntityId::class.java, EntityId.TYPE)
.registerSubtype(ValueString::class.java, ValueString.TYPE)
.registerSubtype(GlobeCoordinate_partial::class.java, GlobeCoordinate_partial.TYPE)
.registerSubtype(Time_partial::class.java, Time_partial.TYPE)
.registerSubtype(Quantity_partial::class.java, Quantity_partial.TYPE)
.registerSubtype(MonoLingualText_partial::class.java, MonoLingualText_partial.TYPE)
}
data class DataValueEntityId(val value: WikiBaseEntityValue) :
DataValue(TYPE) {
// "value": {
// "entity-type": "item",
// "id": "Q30",
// "numeric-id": 30
// },
// "type": "wikibase-entityid"
// }
data class EntityId(val value: WikiBaseEntityValue) : DataValue(TYPE) {
companion object {
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 {
const val TYPE = "string"
}
}
class DataValueGlobeCoordinate_partial() :
DataValue(TYPE) {
// "value": {
// "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 {
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 {
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
import org.wikipedia.wikidata.DataValue.DataValueEntityId
import org.wikipedia.wikidata.DataValue.EntityId
data class EditClaim(val claims: List<Statement_partial>) {
@ -14,7 +14,7 @@ data class EditClaim(val claims: List<Statement_partial>) {
Snak_partial(
"value",
propertyName,
DataValueEntityId(
EntityId(
WikiBaseEntityValue(
"item",
entityId,

View file

@ -2,6 +2,7 @@ package org.wikipedia.wikidata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
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> descriptions;
@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;
@NonNull public String id() {