Skip to content

Commit 369e79b

Browse files
Nearby: No longer keeps loading until timeout when map is zoomed out (commons-app#6070)
* Nearby: Search for actual map center * Add query syntax and methods * Nearby: Added binary search for loading pins * Add NearbyQueryParams and refactor * Add unit tests and complete implementation * Nearby: Increase max radius from 100km to 300km * Nearby: Centermost pins now appear on top * getNearbyItemCount: Added javadoc --------- Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
1 parent c963cd9 commit 369e79b

11 files changed

+303
-37
lines changed

app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt

+115-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package fr.free.nrw.commons.mwapi
22

33
import android.text.TextUtils
44
import com.google.gson.Gson
5+
import com.google.gson.JsonParser
56
import fr.free.nrw.commons.BuildConfig
67
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
78
import fr.free.nrw.commons.explore.depictions.DepictsClient
@@ -10,6 +11,7 @@ import fr.free.nrw.commons.fileusages.GlobalFileUsagesResponse
1011
import fr.free.nrw.commons.location.LatLng
1112
import fr.free.nrw.commons.nearby.Place
1213
import fr.free.nrw.commons.nearby.model.ItemsClass
14+
import fr.free.nrw.commons.nearby.model.NearbyQueryParams
1315
import fr.free.nrw.commons.nearby.model.NearbyResponse
1416
import fr.free.nrw.commons.nearby.model.PlaceBindings
1517
import fr.free.nrw.commons.profile.achievements.FeaturedImages
@@ -330,36 +332,130 @@ class OkHttpJsonApiClient @Inject constructor(
330332
throw Exception(response.message)
331333
}
332334

335+
/**
336+
* Returns the count of items in the specified area by querying Wikidata.
337+
*
338+
* @param queryParams: a `NearbyQueryParam` specifying the geographical area.
339+
* @return The count of items in the specified area.
340+
*/
341+
@Throws(Exception::class)
342+
fun getNearbyItemCount(
343+
queryParams: NearbyQueryParams
344+
): Int {
345+
val wikidataQuery: String = when (queryParams) {
346+
is NearbyQueryParams.Rectangular -> {
347+
val westCornerLat = queryParams.screenTopRight.latitude
348+
val westCornerLong = queryParams.screenTopRight.longitude
349+
val eastCornerLat = queryParams.screenBottomLeft.latitude
350+
val eastCornerLong = queryParams.screenBottomLeft.longitude
351+
FileUtils.readFromResource("/queries/rectangle_query_for_item_count.rq")
352+
.replace("\${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat))
353+
.replace("\${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong))
354+
.replace("\${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat))
355+
.replace("\${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong))
356+
}
357+
358+
is NearbyQueryParams.Radial -> {
359+
FileUtils.readFromResource("/queries/radius_query_for_item_count.rq")
360+
.replace(
361+
"\${LAT}",
362+
String.format(Locale.ROOT, "%.4f", queryParams.center.latitude)
363+
)
364+
.replace(
365+
"\${LONG}",
366+
String.format(Locale.ROOT, "%.4f", queryParams.center.longitude)
367+
)
368+
.replace("\${RAD}", String.format(Locale.ROOT, "%.2f", queryParams.radiusInKm))
369+
}
370+
}
371+
372+
val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!!
373+
.newBuilder()
374+
.addQueryParameter("query", wikidataQuery)
375+
.addQueryParameter("format", "json")
376+
377+
val request: Request = Request.Builder()
378+
.url(urlBuilder.build())
379+
.build()
380+
381+
val response = okHttpClient.newCall(request).execute()
382+
if (response.body != null && response.isSuccessful) {
383+
val json = response.body!!.string()
384+
return JsonParser.parseString(json).getAsJsonObject().getAsJsonObject("results")
385+
.getAsJsonArray("bindings").get(0).getAsJsonObject().getAsJsonObject("itemCount")
386+
.get("value").asInt
387+
}
388+
throw Exception(response.message)
389+
}
390+
333391
@Throws(Exception::class)
334392
fun getNearbyPlaces(
335-
screenTopRight: LatLng,
336-
screenBottomLeft: LatLng, language: String,
393+
queryParams: NearbyQueryParams, language: String,
337394
shouldQueryForMonuments: Boolean, customQuery: String?
338395
): List<Place>? {
339396
Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString())
340397

398+
val locale = Locale.ROOT;
341399
val wikidataQuery: String = if (customQuery != null) {
342-
customQuery
343-
} else if (!shouldQueryForMonuments) {
344-
FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq")
345-
} else {
346-
FileUtils.readFromResource("/queries/rectangle_query_for_nearby_monuments.rq")
347-
}
400+
when (queryParams) {
401+
is NearbyQueryParams.Rectangular -> {
402+
val westCornerLat = queryParams.screenTopRight.latitude
403+
val westCornerLong = queryParams.screenTopRight.longitude
404+
val eastCornerLat = queryParams.screenBottomLeft.latitude
405+
val eastCornerLong = queryParams.screenBottomLeft.longitude
406+
customQuery
407+
.replace("\${LAT_WEST}", String.format(locale, "%.4f", westCornerLat))
408+
.replace("\${LONG_WEST}", String.format(locale, "%.4f", westCornerLong))
409+
.replace("\${LAT_EAST}", String.format(locale, "%.4f", eastCornerLat))
410+
.replace("\${LONG_EAST}", String.format(locale, "%.4f", eastCornerLong))
411+
.replace("\${LANG}", language)
412+
}
413+
is NearbyQueryParams.Radial -> {
414+
Timber.e(
415+
"%s%s",
416+
"okHttpJsonApiClient.getNearbyPlaces invoked with custom query",
417+
"and radial coordinates. This is currently not supported."
418+
)
419+
""
420+
}
421+
}
422+
} else when (queryParams) {
423+
is NearbyQueryParams.Radial -> {
424+
val placeHolderQuery: String = if (!shouldQueryForMonuments) {
425+
FileUtils.readFromResource("/queries/radius_query_for_nearby.rq")
426+
} else {
427+
FileUtils.readFromResource("/queries/radius_query_for_nearby_monuments.rq")
428+
}
429+
placeHolderQuery.replace(
430+
"\${LAT}", String.format(locale, "%.4f", queryParams.center.latitude)
431+
).replace(
432+
"\${LONG}", String.format(locale, "%.4f", queryParams.center.longitude)
433+
)
434+
.replace("\${RAD}", String.format(locale, "%.2f", queryParams.radiusInKm))
435+
}
348436

349-
val westCornerLat = screenTopRight.latitude
350-
val westCornerLong = screenTopRight.longitude
351-
val eastCornerLat = screenBottomLeft.latitude
352-
val eastCornerLong = screenBottomLeft.longitude
437+
is NearbyQueryParams.Rectangular -> {
438+
val placeHolderQuery: String = if (!shouldQueryForMonuments) {
439+
FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq")
440+
} else {
441+
FileUtils.readFromResource("/queries/rectangle_query_for_nearby_monuments.rq")
442+
}
443+
val westCornerLat = queryParams.screenTopRight.latitude
444+
val westCornerLong = queryParams.screenTopRight.longitude
445+
val eastCornerLat = queryParams.screenBottomLeft.latitude
446+
val eastCornerLong = queryParams.screenBottomLeft.longitude
447+
placeHolderQuery
448+
.replace("\${LAT_WEST}", String.format(locale, "%.4f", westCornerLat))
449+
.replace("\${LONG_WEST}", String.format(locale, "%.4f", westCornerLong))
450+
.replace("\${LAT_EAST}", String.format(locale, "%.4f", eastCornerLat))
451+
.replace("\${LONG_EAST}", String.format(locale, "%.4f", eastCornerLong))
452+
.replace("\${LANG}", language)
453+
}
454+
}
353455

354-
val query = wikidataQuery
355-
.replace("\${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat))
356-
.replace("\${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong))
357-
.replace("\${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat))
358-
.replace("\${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong))
359-
.replace("\${LANG}", language)
360456
val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!!
361457
.newBuilder()
362-
.addQueryParameter("query", query)
458+
.addQueryParameter("query", wikidataQuery)
363459
.addQueryParameter("format", "json")
364460

365461
val request: Request = Request.Builder()

app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ public NearbyPlacesInfo loadAttractionsFromLocation(final LatLng currentLatLng,
196196
return null;
197197
}
198198

199-
List<Place> places = nearbyPlaces.getFromWikidataQuery(screenTopRight, screenBottomLeft,
200-
Locale.getDefault().getLanguage(), shouldQueryForMonuments, customQuery);
199+
List<Place> places = nearbyPlaces.getFromWikidataQuery(currentLatLng, screenTopRight,
200+
screenBottomLeft, Locale.getDefault().getLanguage(), shouldQueryForMonuments,
201+
customQuery);
201202

202203
if (null != places && places.size() > 0) {
203204
LatLng[] boundaryCoordinates = {

app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java

+63-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package fr.free.nrw.commons.nearby;
22

3+
import android.location.Location;
34
import androidx.annotation.Nullable;
5+
import fr.free.nrw.commons.nearby.model.NearbyQueryParams;
46
import java.io.IOException;
57
import java.util.Collections;
68
import java.util.List;
@@ -101,6 +103,7 @@ public List<Place> getFromWikidataQuery(final LatLng cur, final String lang,
101103
* Retrieves a list of places from a Wikidata query based on screen coordinates and optional
102104
* parameters.
103105
*
106+
* @param centerPoint The center of the map, used for radius queries if required.
104107
* @param screenTopRight The top right corner of the screen (latitude, longitude).
105108
* @param screenBottomLeft The bottom left corner of the screen (latitude, longitude).
106109
* @param lang The language for the query.
@@ -111,13 +114,70 @@ public List<Place> getFromWikidataQuery(final LatLng cur, final String lang,
111114
* @throws Exception If an error occurs during the retrieval process.
112115
*/
113116
public List<Place> getFromWikidataQuery(
117+
final fr.free.nrw.commons.location.LatLng centerPoint,
114118
final fr.free.nrw.commons.location.LatLng screenTopRight,
115119
final fr.free.nrw.commons.location.LatLng screenBottomLeft, final String lang,
116120
final boolean shouldQueryForMonuments,
117121
@Nullable final String customQuery) throws Exception {
118-
return okHttpJsonApiClient
119-
.getNearbyPlaces(screenTopRight, screenBottomLeft, lang, shouldQueryForMonuments,
120-
customQuery);
122+
if (customQuery != null) {
123+
return okHttpJsonApiClient
124+
.getNearbyPlaces(
125+
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
126+
shouldQueryForMonuments,
127+
customQuery);
128+
}
129+
130+
final int lowerLimit = 1000, upperLimit = 1500;
131+
132+
final float[] results = new float[1];
133+
Location.distanceBetween(centerPoint.getLatitude(), screenTopRight.getLongitude(),
134+
centerPoint.getLatitude(), screenBottomLeft.getLongitude(), results);
135+
final float longGap = results[0] / 1000f;
136+
Location.distanceBetween(screenTopRight.getLatitude(), centerPoint.getLongitude(),
137+
screenBottomLeft.getLatitude(), centerPoint.getLongitude(), results);
138+
final float latGap = results[0] / 1000f;
139+
140+
if (Math.max(longGap, latGap) < 100f) {
141+
final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
142+
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft));
143+
if (itemCount < upperLimit) {
144+
return okHttpJsonApiClient.getNearbyPlaces(
145+
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
146+
shouldQueryForMonuments, null);
147+
}
148+
}
149+
150+
// minRadius, targetRadius and maxRadius are radii in decameters
151+
// unlike other radii here, which are in kilometers, to avoid looping over
152+
// floating point values
153+
int minRadius = 0, maxRadius = Math.round(Math.min(300f, Math.min(longGap, latGap))) * 100;
154+
int targetRadius = maxRadius / 2;
155+
while (minRadius < maxRadius) {
156+
targetRadius = minRadius + (maxRadius - minRadius + 1) / 2;
157+
final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
158+
new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f));
159+
if (itemCount >= lowerLimit && itemCount < upperLimit) {
160+
break;
161+
}
162+
if (targetRadius > maxRadius / 2 && itemCount < lowerLimit / 5) { // fast forward
163+
minRadius = targetRadius;
164+
targetRadius = minRadius + (maxRadius - minRadius + 1) / 2;
165+
minRadius = targetRadius;
166+
if (itemCount < lowerLimit / 10 && minRadius < maxRadius) { // fast forward again
167+
targetRadius = minRadius + (maxRadius - minRadius + 1) / 2;
168+
minRadius = targetRadius;
169+
}
170+
continue;
171+
}
172+
if (itemCount < upperLimit) {
173+
minRadius = targetRadius;
174+
} else {
175+
maxRadius = targetRadius - 1;
176+
}
177+
}
178+
return okHttpJsonApiClient.getNearbyPlaces(
179+
new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
180+
null);
121181
}
122182

123183
/**

app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -1101,19 +1101,19 @@ public void populatePlaces(final LatLng currentLatLng) {
11011101
eastCornerLong, 0);
11021102
if (currentLatLng.equals(
11031103
getLastMapFocus())) { // Means we are checking around current location
1104-
populatePlacesForCurrentLocation(getLastMapFocus(), screenTopRightLatLng,
1104+
populatePlacesForCurrentLocation(getMapFocus(), screenTopRightLatLng,
11051105
screenBottomLeftLatLng, currentLatLng, null);
11061106
} else {
1107-
populatePlacesForAnotherLocation(getLastMapFocus(), screenTopRightLatLng,
1107+
populatePlacesForAnotherLocation(getMapFocus(), screenTopRightLatLng,
11081108
screenBottomLeftLatLng, currentLatLng, null);
11091109
}
11101110
} else {
11111111
if (currentLatLng.equals(
11121112
getLastMapFocus())) { // Means we are checking around current location
1113-
populatePlacesForCurrentLocation(getLastMapFocus(), screenTopRightLatLng,
1113+
populatePlacesForCurrentLocation(getMapFocus(), screenTopRightLatLng,
11141114
screenBottomLeftLatLng, currentLatLng, null);
11151115
} else {
1116-
populatePlacesForAnotherLocation(getLastMapFocus(), screenTopRightLatLng,
1116+
populatePlacesForAnotherLocation(getMapFocus(), screenTopRightLatLng,
11171117
screenBottomLeftLatLng, currentLatLng, null);
11181118
}
11191119
}
@@ -1887,9 +1887,12 @@ public Marker convertToMarker(Place place, boolean isBookMarked) {
18871887
@Override
18881888
public void replaceMarkerOverlays(final List<MarkerPlaceGroup> markerPlaceGroups) {
18891889
ArrayList<Marker> newMarkers = new ArrayList<>(markerPlaceGroups.size());
1890-
for (MarkerPlaceGroup markerPlaceGroup : markerPlaceGroups) {
1890+
// iterate in reverse so that the nearest pins get rendered on top
1891+
for (int i = markerPlaceGroups.size() - 1; i >= 0; i--) {
18911892
newMarkers.add(
1892-
convertToMarker(markerPlaceGroup.getPlace(), markerPlaceGroup.getIsBookmarked()));
1893+
convertToMarker(markerPlaceGroups.get(i).getPlace(),
1894+
markerPlaceGroups.get(i).getIsBookmarked())
1895+
);
18931896
}
18941897
clearAllMarkers();
18951898
binding.map.getOverlays().addAll(newMarkers);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fr.free.nrw.commons.nearby.model
2+
3+
import fr.free.nrw.commons.location.LatLng
4+
5+
sealed class NearbyQueryParams {
6+
class Rectangular(val screenTopRight: LatLng, val screenBottomLeft: LatLng) :
7+
NearbyQueryParams()
8+
9+
class Radial(val center: LatLng, val radiusInKm: Float) : NearbyQueryParams()
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
SELECT (COUNT(?item) AS ?itemCount)
2+
WHERE {
3+
# Around given location.
4+
SERVICE wikibase:around {
5+
?item wdt:P625 ?location.
6+
bd:serviceParam wikibase:center "Point(${LONG} ${LAT})"^^geo:wktLiteral.
7+
bd:serviceParam wikibase:radius "${RAD}" . # Radius in kilometers.
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SELECT
2+
?item
3+
(SAMPLE(?location) as ?location)
4+
WHERE {
5+
# Around given location.
6+
SERVICE wikibase:around {
7+
?item wdt:P625 ?location.
8+
bd:serviceParam wikibase:center "Point(${LONG} ${LAT})"^^geo:wktLiteral.
9+
bd:serviceParam wikibase:radius "${RAD}" . # Radius in kilometers.
10+
}
11+
}
12+
GROUP BY ?item
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
SELECT
2+
?item
3+
(SAMPLE(?location) as ?location)
4+
(SAMPLE(?monument) AS ?monument)
5+
WHERE {
6+
# Around given location.
7+
SERVICE wikibase:around {
8+
?item wdt:P625 ?location.
9+
bd:serviceParam wikibase:center "Point(${LONG} ${LAT})"^^geo:wktLiteral.
10+
bd:serviceParam wikibase:radius "${RAD}" . # Radius in kilometers.
11+
}
12+
13+
# Wiki Loves Monuments
14+
OPTIONAL {?item p:P1435 ?monument}
15+
OPTIONAL {?item p:P2186 ?monument}
16+
OPTIONAL {?item p:P1459 ?monument}
17+
OPTIONAL {?item p:P1460 ?monument}
18+
OPTIONAL {?item p:P1216 ?monument}
19+
OPTIONAL {?item p:P709 ?monument}
20+
OPTIONAL {?item p:P718 ?monument}
21+
OPTIONAL {?item p:P5694 ?monument}
22+
OPTIONAL {?item p:P3426 ?monument}
23+
24+
}
25+
GROUP BY ?item
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
SELECT (COUNT(?item) AS ?itemCount)
2+
WHERE {
3+
SERVICE wikibase:box {
4+
?item wdt:P625 ?location.
5+
bd:serviceParam wikibase:cornerWest "Point(${LONG_WEST} ${LAT_WEST})"^^geo:wktLiteral.
6+
bd:serviceParam wikibase:cornerEast "Point(${LONG_EAST} ${LAT_EAST})"^^geo:wktLiteral.
7+
}
8+
}

app/src/main/resources/queries/rectangle_query_for_nearby.rq

-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ WHERE {
88
bd:serviceParam wikibase:cornerWest "Point(${LONG_WEST} ${LAT_WEST})"^^geo:wktLiteral.
99
bd:serviceParam wikibase:cornerEast "Point(${LONG_EAST} ${LAT_EAST})"^^geo:wktLiteral.
1010
}
11-
1211
}
1312
GROUP BY ?item

0 commit comments

Comments
 (0)