Make Single Query for Nearby and WLM pins (#4573)

* Merge nearby and monument queries

* Bug Fix- query resource path change on shouldQueryForMonuments

* Bug Fixes
1. Propagate exceptions for nearby API calls to caller
2. Fix too much work on main thread exception in NearbyParentFragment
This commit is contained in:
Ashish 2021-08-24 16:55:07 +05:30 committed by GitHub
parent cba99ae5e3
commit 678bd33410
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 148 additions and 187 deletions

View file

@ -454,7 +454,7 @@ public class ContributionsFragment
private void updateClosestNearbyCardViewInfo() {
curLatLng = locationManager.getLastLocation();
compositeDisposable.add(Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng, curLatLng, true, false)) // thanks to boolean, it will only return closest result
.loadAttractionsFromLocation(curLatLng, curLatLng, true, false, false)) // thanks to boolean, it will only return closest result
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNearbyNotification,

View file

@ -5,6 +5,7 @@ import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.UPDAT
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
@ -264,105 +265,52 @@ public class OkHttpJsonApiClient {
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius)
@Nullable
public List<Place> getNearbyPlaces(final LatLng cur, final String language, final double radius,
final boolean shouldQueryForMonuments)
throws Exception {
Timber.d("Fetching nearby items at radius %s", radius);
String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", language);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new ArrayList<>();
}
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) {
places.add(Place.from(item));
}
return places;
}
return new ArrayList<>();
});
}
/**
* Wikidata query to fetch monuments
*
* @param cur : The current location coordinates
* @param language : The language
* @param radius : The radius around the current location within which we expect the results
* @return
* @throws IOException
*/
public Observable<List<Place>> getNearbyMonuments(LatLng cur, String language, final double radius){
Timber.d("Fetching monuments at radius %s", radius);
final String wikidataQuery;
try {
wikidataQuery = FileUtils.readFromResource("/queries/monuments_query.rq");
if (TextUtils.isEmpty(language)) {
language = "en";
if (!shouldQueryForMonuments) {
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
} else {
wikidataQuery = FileUtils.readFromResource("/queries/nearby_query_monuments.rq");
}
String query = wikidataQuery
final String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", language);
HttpUrl.Builder urlBuilder = HttpUrl
final HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
Request request = new Request.Builder()
final Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
Timber.d("Monuments URL: %s", request.url().toString());
return Observable.fromCallable(() -> {
final Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
if (response.body() != null && response.isSuccessful()) {
final String json = response.body().string();
if (json == null) {
return new ArrayList<>();
}
final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
final List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
final List<Place> places = new ArrayList<>();
for (final NearbyResultItem item : bindings) {
final Place place = Place.from(item);
place.setMonument(true);
places.add(place);
final Place placeFromNearbyItem = Place.from(item);
if (shouldQueryForMonuments && item.getMonument() != null) {
placeFromNearbyItem.setMonument(true);
} else {
placeFromNearbyItem.setMonument(false);
}
places.add(placeFromNearbyItem);
}
return places;
}
return new ArrayList<>();
});
} catch (final IOException e) {
e.printStackTrace();
return Observable.error(e);
}
throw new Exception(response.message());
}
/**

View file

@ -57,7 +57,9 @@ public class NearbyController {
* @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List
*/
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, LatLng searchLatLng, boolean returnClosestResult, boolean checkingAroundCurrentLocation) throws IOException {
public NearbyPlacesInfo loadAttractionsFromLocation(final LatLng curLatLng, final LatLng searchLatLng,
final boolean returnClosestResult, final boolean checkingAroundCurrentLocation,
final boolean shouldQueryForMonuments) throws Exception {
Timber.d("Loading attractions near %s", searchLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
@ -66,7 +68,9 @@ public class NearbyController {
Timber.d("Loading attractions nearby, but curLatLng is null");
return null;
}
List<Place> places = nearbyPlaces.radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult);
List<Place> places = nearbyPlaces
.radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult,
shouldQueryForMonuments);
if (null != places && places.size() > 0) {
LatLng[] boundaryCoordinates = {places.get(0).location, // south
@ -128,11 +132,6 @@ public class NearbyController {
}
}
public Observable<List<Place>> queryWikiDataForMonuments(
final LatLng latLng, final String language) {
return nearbyPlaces.queryWikiDataForMonuments(latLng, language);
}
/**
* Loads attractions from location for list view, we need to return Place data type.
*

View file

@ -1,8 +1,6 @@
package fr.free.nrw.commons.nearby;
import io.reactivex.Observable;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
@ -41,12 +39,12 @@ public class NearbyPlaces {
* @param lang user's language
* @param returnClosestResult true if only the nearest point is desired
* @return list of places obtained
* @throws IOException if query fails
*/
List<Place> radiusExpander(LatLng curLatLng, String lang, boolean returnClosestResult) throws IOException {
List<Place> radiusExpander(final LatLng curLatLng, final String lang, final boolean returnClosestResult
, final boolean shouldQueryForMonuments) throws Exception {
int minResults;
double maxRadius;
final int minResults;
final double maxRadius;
List<Place> places = Collections.emptyList();
@ -64,12 +62,7 @@ public class NearbyPlaces {
// Increase the radius gradually to find a satisfactory number of nearby places
while (radius <= maxRadius) {
try {
places = getFromWikidataQuery(curLatLng, lang, radius);
} catch (final Exception e) {
Timber.e(e, "Exception in fetching nearby places");
break;
}
places = getFromWikidataQuery(curLatLng, lang, radius, shouldQueryForMonuments);
Timber.d("%d results at radius: %f", places.size(), radius);
if (places.size() >= minResults) {
break;
@ -89,16 +82,12 @@ public class NearbyPlaces {
* @param cur coordinates of search location
* @param lang user's language
* @param radius radius for search, as determined by radiusExpander()
* @param shouldQueryForMonuments should the query include properites for monuments
* @return list of places obtained
* @throws IOException if query fails
*/
public List<Place> getFromWikidataQuery(LatLng cur, String lang, double radius) throws Exception {
return okHttpJsonApiClient.getNearbyPlaces(cur, lang, radius).blockingSingle();
}
public Observable<List<Place>> queryWikiDataForMonuments(
LatLng latLng, String language) {
return okHttpJsonApiClient
.getNearbyMonuments(latLng, language, radius);
public List<Place> getFromWikidataQuery(final LatLng cur, final String lang,
final double radius, final boolean shouldQueryForMonuments) throws Exception {
return okHttpJsonApiClient.getNearbyPlaces(cur, lang, radius, shouldQueryForMonuments);
}
}

View file

@ -883,29 +883,11 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
final Observable<NearbyPlacesInfo> nearbyPlacesInfoObservable = Observable
.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curlatLng, searchLatLng, false, true));
.loadAttractionsFromLocation(curlatLng, searchLatLng,
false, true, Utils.isMonumentsEnabled(new Date(), applicationKvStore)));
Observable<List<Place>> observableWikidataMonuments = Observable.empty();
if(Utils.isMonumentsEnabled(new Date(), applicationKvStore)){
observableWikidataMonuments =
nearbyController
.queryWikiDataForMonuments(searchLatLng, Locale.getDefault().getLanguage());
}
compositeDisposable.add(Observable.zip(nearbyPlacesInfoObservable
, observableWikidataMonuments.onErrorReturn(throwable -> {
showErrorMessage(getString(R.string.error_fetching_nearby_monuments) + throwable
.getLocalizedMessage());
return new ArrayList<>();
}),
(nearbyPlacesInfo, monuments) -> {
final List<Place> places = mergeNearbyPlacesAndMonuments(nearbyPlacesInfo.placeList,
monuments);
nearbyPlacesInfo.placeList.clear();
nearbyPlacesInfo.placeList.addAll(places);
return nearbyPlacesInfo;
}).subscribeOn(Schedulers.io())
compositeDisposable.add(nearbyPlacesInfoObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(nearbyPlacesInfo -> {
updateMapMarkers(nearbyPlacesInfo, true);
@ -925,28 +907,11 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
final Observable<NearbyPlacesInfo> nearbyPlacesInfoObservable = Observable
.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curlatLng, searchLatLng, false, false));
.loadAttractionsFromLocation(curlatLng, searchLatLng,
false, true, Utils.isMonumentsEnabled(new Date(), applicationKvStore)));
Observable<List<Place>> observableWikidataMonuments = Observable.empty();
if (Utils.isMonumentsEnabled(new Date(), applicationKvStore)) {
observableWikidataMonuments = nearbyController
.queryWikiDataForMonuments(searchLatLng, Locale.getDefault().getLanguage());
}
compositeDisposable.add(Observable.zip(nearbyPlacesInfoObservable
, observableWikidataMonuments.onErrorReturn(throwable -> {
showErrorMessage(getString(R.string.error_fetching_nearby_monuments) + throwable
.getLocalizedMessage());
return new ArrayList<>();
}),
(nearbyPlacesInfo, monuments) -> {
final List<Place> places = mergeNearbyPlacesAndMonuments(nearbyPlacesInfo.placeList,
monuments);
nearbyPlacesInfo.placeList.clear();
nearbyPlacesInfo.placeList.addAll(places);
return nearbyPlacesInfo;
}).subscribeOn(Schedulers.io())
compositeDisposable.add(nearbyPlacesInfoObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(nearbyPlacesInfo -> {
updateMapMarkers(nearbyPlacesInfo, false);
@ -961,25 +926,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
}));
}
/**
* If a nearby place happens to be a monument as well, don't make the Pin's overlap, instead
* show it as a monument
*
* @param nearbyPlaces
* @param monuments
* @return
*/
private List<Place> mergeNearbyPlacesAndMonuments(List<Place> nearbyPlaces, List<Place> monuments){
List<Place> allPlaces= new ArrayList<>();
allPlaces.addAll(monuments);
for (Place place : nearbyPlaces){
if(!allPlaces.contains(place)){
allPlaces.add(place);
}
}
return allPlaces;
}
/**
* Populates places for your location, should be used for finding nearby places around a
* location where you are.
@ -1238,7 +1184,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
public void addCurrentLocationMarker(final fr.free.nrw.commons.location.LatLng curLatLng) {
if (null != curLatLng && !isPermissionDenied) {
ExecutorUtils.get().submit(() -> {
mapView.post(() -> removeCurrentLocationMarker());
removeCurrentLocationMarker();
Timber.d("Adds current location marker");
final Icon icon = IconFactory.getInstance(getContext())
@ -1248,8 +1194,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
.position(new LatLng(curLatLng.getLatitude(),
curLatLng.getLongitude()));
currentLocationMarkerOptions.setIcon(icon); // Set custom icon
mapView.post(() -> currentLocationMarker = mapBox.addMarker(currentLocationMarkerOptions));
currentLocationMarker = mapBox.addMarker(currentLocationMarkerOptions);
final List<LatLng> circle = UiUtils
.createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(),
@ -1259,7 +1204,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
.addAll(circle)
.strokeColor(getResources().getColor(R.color.current_marker_stroke))
.fillColor(getResources().getColor(R.color.current_marker_fill));
mapView.post(() -> currentLocationPolygon = mapBox.addPolygon(currentLocationPolygonOptions));
currentLocationPolygon = mapBox.addPolygon(currentLocationPolygonOptions);
});
} else {

View file

@ -14,7 +14,8 @@ class NearbyResultItem(private val item: ResultTuple?,
@field:SerializedName("pic") private val pic: ResultTuple?,
@field:SerializedName("destroyed") private val destroyed: ResultTuple?,
@field:SerializedName("description") private val description: ResultTuple?,
@field:SerializedName("endTime") private val endTime: ResultTuple?) {
@field:SerializedName("endTime") private val endTime: ResultTuple?,
@field:SerializedName("monument") private val monument: ResultTuple?) {
fun getItem(): ResultTuple {
return item ?: ResultTuple()
@ -71,4 +72,8 @@ class NearbyResultItem(private val item: ResultTuple?,
fun getAddress(): String {
return address?.value?:""
}
fun getMonument():ResultTuple?{
return monument
}
}

View file

@ -283,13 +283,14 @@ public class UploadRepository {
* @return
*/
@Nullable
public Place checkNearbyPlaces(double decLatitude, double decLongitude) {
public Place checkNearbyPlaces(final double decLatitude, final double decLongitude) {
try {
List<Place> fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng(
final List<Place> fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng(
decLatitude, decLongitude, 0.0f),
Locale.getDefault().getLanguage(),
NEARBY_RADIUS_IN_KILO_METERS);
return fromWikidataQuery.size() > 0 ? fromWikidataQuery.get(0) : null;
NEARBY_RADIUS_IN_KILO_METERS, false);
return (fromWikidataQuery != null && fromWikidataQuery.size() > 0) ? fromWikidataQuery
.get(0) : null;
}catch (final Exception e) {
Timber.e("Error fetching nearby places: %s", e.getMessage());
return null;

View file

@ -199,12 +199,15 @@ class FileProcessor @Inject constructor(
private fun suggestNearbyDepictions(imageCoordinates: ImageCoordinates): Disposable {
return Observable.fromIterable(radiiProgressionInMetres.map { it / 1000.0 })
.concatMap {
Observable.fromCallable {
okHttpJsonApiClient.getNearbyPlaces(
imageCoordinates.latLng,
Locale.getDefault().language,
it
it,
false
)
}
}
.subscribeOn(Schedulers.io())
.filter { it.size >= MIN_NEARBY_RESULTS }
.take(1)

View file

@ -104,8 +104,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
* This method checks for the nearest location that needs images and suggests it to the user.
* @param uploadItem
*/
private void checkNearbyPlaces(UploadItem uploadItem) {
Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository
private void checkNearbyPlaces(final UploadItem uploadItem) {
final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository
.checkNearbyPlaces(uploadItem.getGpsCoords().getDecLatitude(),
uploadItem.getGpsCoords().getDecLongitude()))
.subscribeOn(ioScheduler)

View file

@ -0,0 +1,71 @@
SELECT
(SAMPLE(?location) as ?location)
?item
(SAMPLE(COALESCE(?itemLabelPreferredLanguage, ?itemLabelAnyLanguage)) as ?label)
(SAMPLE(COALESCE(?itemDescriptionPreferredLanguage, ?itemDescriptionAnyLanguage, "?")) as ?description)
(SAMPLE(?classId) as ?class)
(SAMPLE(COALESCE(?classLabelPreferredLanguage, ?classLabelAnyLanguage, "?")) as ?classLabel)
(SAMPLE(COALESCE(?icon0, ?icon1)) as ?icon)
?wikipediaArticle
?commonsArticle
(SAMPLE(?commonsCategory) as ?commonsCategory)
(SAMPLE(?pic) as ?pic)
(SAMPLE(?destroyed) as ?destroyed)
(SAMPLE(?endTime) as ?endTime)
(SAMPLE(?monument) as ?monument)
WHERE {
# Around given location...
SERVICE wikibase:around {
?item wdt:P625 ?location.
bd:serviceParam wikibase:center "Point(${LONG} ${LAT})"^^geo:wktLiteral.
bd:serviceParam wikibase:radius "${RAD}" . # Radius in kilometers.
}
OPTIONAL {
{ ?item p:P1435 ?monument } UNION { ?item p:P2186 ?monument } UNION { ?item p:P1459 ?monument } UNION { ?item p:P1460 ?monument } UNION { ?item p:P1216 ?monument } UNION { ?item p:P709 ?monument } UNION { ?item p:P718 ?monument } UNION { ?item p:P5694 ?monument }
}
# Get the label in the preferred language of the user, or any other language if no label is available in that language.
OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "${LANG}")}
OPTIONAL {?item rdfs:label ?itemLabelAnyLanguage}
# Get the description in the preferred language of the user, or any other language if no description is available in that language.
OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (lang(?itemDescriptionPreferredLanguage) = "${LANG}")}
OPTIONAL {?item schema:description ?itemDescriptionAnyLanguage }
# Get Commons category (P373)
OPTIONAL { ?item wdt:P373 ?commonsCategory. }
# Get (P18)
OPTIONAL { ?item wdt:P18 ?pic. }
# Get (P576)
OPTIONAL { ?item wdt:P576 ?destroyed. }
# Get (P582)
OPTIONAL { ?item wdt:P582 ?endTime. }
# Get the class label in the preferred language of the user, or any other language if no label is available in that language.
OPTIONAL {
?item p:P31/ps:P31 ?classId.
OPTIONAL {?classId rdfs:label ?classLabelPreferredLanguage. FILTER (lang(?classLabelPreferredLanguage) = "${LANG}")}
OPTIONAL {?classId rdfs:label ?classLabelAnyLanguage}
OPTIONAL {
?wikipediaArticle schema:about ?item ;
schema:isPartOf <https://${LANG}.wikipedia.org/> .
}
OPTIONAL {
?wikipediaArticle schema:about ?item ;
schema:isPartOf <https://en.wikipedia.org/> .
SERVICE wikibase:label { bd:serviceParam wikibase:language "en" }
}
OPTIONAL {
?commonsArticle schema:about ?item ;
schema:isPartOf <https://commons.wikimedia.org/> .
SERVICE wikibase:label { bd:serviceParam wikibase:language "en" }
}
}
}
GROUP BY ?item ?wikipediaArticle ?commonsArticle