diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 1f5a86a9c..abc2b45ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -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, diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 3f272a41a..3c7bd1f97 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -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,107 +265,54 @@ public class OkHttpJsonApiClient { }); } - public Observable> getNearbyPlaces(LatLng cur, String language, double radius) + @Nullable + public List 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 + final String wikidataQuery; + if (!shouldQueryForMonuments) { + wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq"); + } else { + wikidataQuery = FileUtils.readFromResource("/queries/nearby_query_monuments.rq"); + } + 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(); - 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<>(); + final Response response = okHttpClient.newCall(request).execute(); + if (response.body() != null && response.isSuccessful()) { + final String json = response.body().string(); + final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); + final List bindings = nearbyResponse.getResults().getBindings(); + final List places = new ArrayList<>(); + for (final NearbyResultItem item : bindings) { + final Place placeFromNearbyItem = Place.from(item); + if (shouldQueryForMonuments && item.getMonument() != null) { + placeFromNearbyItem.setMonument(true); + } else { + placeFromNearbyItem.setMonument(false); } - NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - List bindings = nearbyResponse.getResults().getBindings(); - List places = new ArrayList<>(); - for (NearbyResultItem item : bindings) { - places.add(Place.from(item)); - } - return places; + places.add(placeFromNearbyItem); } - return new ArrayList<>(); - }); + return places; + } + throw new Exception(response.message()); } - /** - * 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> 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"; - } - 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(); - - 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()) { - final String json = response.body().string(); - if (json == null) { - return new ArrayList<>(); - } - - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place place = Place.from(item); - place.setMonument(true); - places.add(place); - } - return places; - } - return new ArrayList<>(); - }); - } catch (final IOException e) { - e.printStackTrace(); - return Observable.error(e); - } - } - /** * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: * bridge -> suspended bridge, aqueduct, etc diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index bd7a61d24..a534273e4 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -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 places = nearbyPlaces.radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult); + List 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> 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. * diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index 1ba3219b1..bd634f934 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -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 radiusExpander(LatLng curLatLng, String lang, boolean returnClosestResult) throws IOException { + List 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 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 getFromWikidataQuery(LatLng cur, String lang, double radius) throws Exception { - return okHttpJsonApiClient.getNearbyPlaces(cur, lang, radius).blockingSingle(); - } - - public Observable> queryWikiDataForMonuments( - LatLng latLng, String language) { - return okHttpJsonApiClient - .getNearbyMonuments(latLng, language, radius); + public List getFromWikidataQuery(final LatLng cur, final String lang, + final double radius, final boolean shouldQueryForMonuments) throws Exception { + return okHttpJsonApiClient.getNearbyPlaces(cur, lang, radius, shouldQueryForMonuments); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index b007b5f5d..1fc43ea38 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -883,29 +883,11 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment final Observable nearbyPlacesInfoObservable = Observable .fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curlatLng, searchLatLng, false, true)); + .loadAttractionsFromLocation(curlatLng, searchLatLng, + false, true, Utils.isMonumentsEnabled(new Date(), applicationKvStore))); - Observable> 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 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 nearbyPlacesInfoObservable = Observable .fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curlatLng, searchLatLng, false, false)); + .loadAttractionsFromLocation(curlatLng, searchLatLng, + false, true, Utils.isMonumentsEnabled(new Date(), applicationKvStore))); - Observable> 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 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 mergeNearbyPlacesAndMonuments(List nearbyPlaces, List monuments){ - List 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 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 { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt index fcb6a013b..4da8740cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index 98a1e9535..c30d5bd12 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -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 fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng( + final List 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; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index ff3f63eb8..fb8cf3713 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -199,11 +199,14 @@ class FileProcessor @Inject constructor( private fun suggestNearbyDepictions(imageCoordinates: ImageCoordinates): Disposable { return Observable.fromIterable(radiiProgressionInMetres.map { it / 1000.0 }) .concatMap { - okHttpJsonApiClient.getNearbyPlaces( - imageCoordinates.latLng, - Locale.getDefault().language, - it - ) + Observable.fromCallable { + okHttpJsonApiClient.getNearbyPlaces( + imageCoordinates.latLng, + Locale.getDefault().language, + it, + false + ) + } } .subscribeOn(Schedulers.io()) .filter { it.size >= MIN_NEARBY_RESULTS } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java index e0e410171..fe588534b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java @@ -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) diff --git a/app/src/main/resources/queries/nearby_query_monuments.rq b/app/src/main/resources/queries/nearby_query_monuments.rq new file mode 100644 index 000000000..420d23591 --- /dev/null +++ b/app/src/main/resources/queries/nearby_query_monuments.rq @@ -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 . + } + OPTIONAL { + ?wikipediaArticle schema:about ?item ; + schema:isPartOf . + SERVICE wikibase:label { bd:serviceParam wikibase:language "en" } + } + + OPTIONAL { + ?commonsArticle schema:about ?item ; + schema:isPartOf . + SERVICE wikibase:label { bd:serviceParam wikibase:language "en" } + } + } + } + GROUP BY ?item ?wikipediaArticle ?commonsArticle \ No newline at end of file