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 dc567088c..e2da22533 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
@@ -11,6 +11,8 @@ import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
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.PlaceBindings;
+import fr.free.nrw.commons.nearby.model.ItemsClass;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.profile.achievements.FeaturedImages;
@@ -27,6 +29,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.HttpUrl;
@@ -393,6 +397,196 @@ public class OkHttpJsonApiClient {
throw new Exception(response.message());
}
+ /**
+ * Make API Call to get Places
+ *
+ * @param leftLatLng Left lat long
+ * @param rightLatLng Right lat long
+ * @return
+ * @throws Exception
+ */
+ @Nullable
+ public String getPlacesAsKML(final LatLng leftLatLng, final LatLng rightLatLng)
+ throws Exception {
+ String kmlString = "\n" +
+ "\n" +
+ "\n" +
+ " ";
+
+ int increment = 1;
+ double longitude = leftLatLng.getLongitude();
+
+ while (longitude <= rightLatLng.getLongitude()) {
+ double NEXT_LONGITUDE =
+ (increment + longitude) >= 0.0 && (increment + longitude) <= 1.0 ? 0.0
+ : increment + longitude;
+
+ double latitude = leftLatLng.getLatitude();
+
+ while (latitude <= rightLatLng.getLatitude()) {
+ double NEXT_LATITUDE =
+ (increment + latitude) >= 0.0 && (increment + latitude) <= 1.0 ? 0.0
+ : increment + latitude;
+ List placeBindings = runQuery(new LatLng(latitude, longitude, 0),
+ new LatLng(NEXT_LATITUDE, NEXT_LONGITUDE, 0));
+ if (placeBindings != null) {
+ for (PlaceBindings item : placeBindings) {
+ if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) {
+ String input = item.getLocation().getValue();
+ Pattern pattern = Pattern.compile(
+ "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)");
+ Matcher matcher = pattern.matcher(input);
+
+ if (matcher.find()) {
+ String longStr = matcher.group(1);
+ String latStr = matcher.group(2);
+ String itemUrl = item.getItem().getValue();
+ String itemName = item.getLabel().getValue().replace("&", "&");
+ String itemLatitude = latStr;
+ String itemLongitude = longStr;
+ String itemClass = item.getClas().getValue();
+
+ String formattedItemName =
+ !itemClass.isEmpty() ? itemName + " (" + itemClass + ")"
+ : itemName;
+
+ String kmlEntry = "\n \n" +
+ " " + formattedItemName + "\n" +
+ " " + itemUrl + "\n" +
+ " \n" +
+ " " + itemLongitude + ","
+ + itemLatitude
+ + "\n" +
+ " \n" +
+ " ";
+ kmlString = kmlString + kmlEntry;
+ } else {
+ Timber.e("No match found");
+ }
+ }
+ }
+ }
+ latitude += increment;
+ }
+ longitude += increment;
+ }
+ kmlString = kmlString + "\n \n" +
+ "\n";
+ return kmlString;
+ }
+
+ /**
+ * Make API Call to get Places
+ *
+ * @param leftLatLng Left lat long
+ * @param rightLatLng Right lat long
+ * @return
+ * @throws Exception
+ */
+ @Nullable
+ public String getPlacesAsGPX(final LatLng leftLatLng, final LatLng rightLatLng)
+ throws Exception {
+ String gpxString = "\n" +
+ ""
+ + "\n";
+
+ int increment = 1;
+ double longitude = leftLatLng.getLongitude();
+
+ while (longitude <= rightLatLng.getLongitude()) {
+ double NEXT_LONGITUDE =
+ (increment + longitude) >= 0.0 && (increment + longitude) <= 1.0 ? 0.0
+ : increment + longitude;
+
+ double latitude = leftLatLng.getLatitude();
+
+ while (latitude <= rightLatLng.getLatitude()) {
+ double NEXT_LATITUDE =
+ (increment + latitude) >= 0.0 && (increment + latitude) <= 1.0 ? 0.0
+ : increment + latitude;
+ List placeBindings = runQuery(new LatLng(latitude, longitude, 0),
+ new LatLng(NEXT_LATITUDE, NEXT_LONGITUDE, 0));
+ if (placeBindings != null) {
+ for (PlaceBindings item : placeBindings) {
+ if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) {
+ String input = item.getLocation().getValue();
+ Pattern pattern = Pattern.compile(
+ "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)");
+ Matcher matcher = pattern.matcher(input);
+
+ if (matcher.find()) {
+ String longStr = matcher.group(1);
+ String latStr = matcher.group(2);
+ String itemUrl = item.getItem().getValue();
+ String itemName = item.getLabel().getValue().replace("&", "&");
+ String itemLatitude = latStr;
+ String itemLongitude = longStr;
+ String itemClass = item.getClas().getValue();
+
+ String formattedItemName =
+ !itemClass.isEmpty() ? itemName + " (" + itemClass + ")"
+ : itemName;
+
+ String gpxEntry =
+ "\n \n" +
+ " " + itemName + "\n" +
+ " " + itemUrl + "\n" +
+ " ";
+ gpxString = gpxString + gpxEntry;
+
+ } else {
+ Timber.e("No match found");
+ }
+ }
+ }
+ }
+ latitude += increment;
+ }
+ longitude += increment;
+ }
+ gpxString = gpxString + "\n";
+ return gpxString;
+ }
+
+ private List runQuery(final LatLng currentLatLng, final LatLng nextLatLng)
+ throws IOException {
+
+ final String wikidataQuery = FileUtils.readFromResource("/queries/places_query.rq");
+ final String query = wikidataQuery
+ .replace("${LONGITUDE}",
+ String.format(Locale.ROOT, "%.2f", currentLatLng.getLongitude()))
+ .replace("${LATITUDE}", String.format(Locale.ROOT, "%.4f", currentLatLng.getLatitude()))
+ .replace("${NEXT_LONGITUDE}",
+ String.format(Locale.ROOT, "%.4f", nextLatLng.getLongitude()))
+ .replace("${NEXT_LATITUDE}",
+ String.format(Locale.ROOT, "%.4f", nextLatLng.getLatitude()));
+
+ final HttpUrl.Builder urlBuilder = HttpUrl
+ .parse(sparqlQueryUrl)
+ .newBuilder()
+ .addQueryParameter("query", query)
+ .addQueryParameter("format", "json");
+
+ final Request request = new Request.Builder()
+ .url(urlBuilder.build())
+ .build();
+
+ final Response response = okHttpClient.newCall(request).execute();
+ if (response.body() != null && response.isSuccessful()) {
+ final String json = response.body().string();
+ final ItemsClass item = gson.fromJson(json, ItemsClass.class);
+ return item.getResults().getBindings();
+ } else {
+ return null;
+ }
+ }
+
/**
* Make API Call to get Nearby Places Implementation does not expects a custom query
*
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 12bc641b8..61d749147 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
@@ -118,6 +118,14 @@ public class NearbyController extends MapController {
return nearbyPlacesInfo;
}
+ public String getPlacesAsKML(LatLng leftLatLng, LatLng rightLatLng) throws Exception {
+ return nearbyPlaces.getPlacesAsKML(leftLatLng, rightLatLng);
+ }
+
+ public String getPlacesAsGPX(LatLng leftLatLng, LatLng rightLatLng) throws Exception {
+ return nearbyPlaces.getPlacesAsGPX(leftLatLng, rightLatLng);
+ }
+
/**
* Prepares Place list to make their distance information update later.
*
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 07b7f53fd..787cc35e2 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
@@ -119,4 +119,27 @@ public class NearbyPlaces {
.getNearbyPlaces(screenTopRight, screenBottomLeft, lang, shouldQueryForMonuments,
customQuery);
}
+
+ /**
+ * Runs the Wikidata query to retrieve the KML String
+ *
+ * @param leftLatLng coordinates of Left Most position
+ * @param rightLatLng coordinates of Right Most position
+ * @throws IOException if query fails
+ */
+ public String getPlacesAsKML(LatLng leftLatLng, LatLng rightLatLng) throws Exception {
+ return okHttpJsonApiClient.getPlacesAsKML(leftLatLng, rightLatLng);
+ }
+
+ /**
+ * Runs the Wikidata query to retrieve the GPX String
+ *
+ * @param leftLatLng coordinates of Left Most position
+ * @param rightLatLng coordinates of Right Most position
+ * @throws IOException if query fails
+ */
+ public String getPlacesAsGPX(LatLng leftLatLng, LatLng rightLatLng) throws Exception {
+ return okHttpJsonApiClient.getPlacesAsGPX(leftLatLng, rightLatLng);
+ }
+
}
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 f84334db5..6a16fccf2 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
@@ -22,6 +22,7 @@ import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Environment;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.text.Html;
@@ -80,6 +81,7 @@ import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
+import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.nearby.CheckBoxTriStates;
@@ -105,11 +107,15 @@ import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@@ -357,6 +363,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.nearby_fragment_menu, menu);
MenuItem listMenu = menu.findItem(R.id.list_sheet);
+ MenuItem saveAsGPXButton = menu.findItem(R.id.list_item_gpx);
+ MenuItem saveAsKMLButton = menu.findItem(R.id.list_item_kml);
listMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
@@ -364,6 +372,44 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
return false;
}
});
+ saveAsGPXButton.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(@NonNull MenuItem item) {
+ try {
+ IGeoPoint screenTopRight = mapView.getProjection().fromPixels(mapView.getWidth(), 0);
+ IGeoPoint screenBottomLeft = mapView.getProjection().fromPixels(0, mapView.getHeight());
+ fr.free.nrw.commons.location.LatLng screenTopRightLatLng = new fr.free.nrw.commons.location.LatLng(
+ screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
+ fr.free.nrw.commons.location.LatLng screenBottomLeftLatLng = new fr.free.nrw.commons.location.LatLng(
+ screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
+ setProgressBarVisibility(true);
+ savePlacesAsGPX(screenTopRightLatLng,screenBottomLeftLatLng);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return false;
+ }
+ });
+ saveAsKMLButton.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(@NonNull MenuItem item) {
+ try {
+ IGeoPoint screenTopRight = mapView.getProjection().fromPixels(mapView.getWidth(), 0);
+ IGeoPoint screenBottomLeft = mapView.getProjection().fromPixels(0, mapView.getHeight());
+ fr.free.nrw.commons.location.LatLng screenTopRightLatLng = new fr.free.nrw.commons.location.LatLng(
+ screenBottomLeft.getLatitude(), screenBottomLeft.getLongitude(), 0);
+ fr.free.nrw.commons.location.LatLng screenBottomLeftLatLng = new fr.free.nrw.commons.location.LatLng(
+ screenTopRight.getLatitude(), screenTopRight.getLongitude(), 0);
+ setProgressBarVisibility(true);
+ savePlacesAsKML(screenTopRightLatLng,screenBottomLeftLatLng);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return false;
+ }
+ });
}
@Override
@@ -1198,6 +1244,102 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
}
}
+ private void savePlacesAsKML(LatLng latLng, LatLng nextlatLng) {
+ final Observable savePlacesObservable = Observable
+ .fromCallable(() -> nearbyController
+ .getPlacesAsKML(latLng, nextlatLng));
+ compositeDisposable.add(savePlacesObservable
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(kmlString -> {
+ if (kmlString != null) {
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss",
+ Locale.getDefault()).format(new Date());
+ String fileName =
+ "KML_" + timeStamp + "_" + System.currentTimeMillis() + ".kml";
+ boolean saved = saveFile(kmlString, fileName);
+ setProgressBarVisibility(false);
+ if (saved) {
+ Toast.makeText(this.getContext(),
+ "KML file saved successfully at /Downloads/" + fileName,
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this.getContext(), "Failed to save KML file.",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ },
+ throwable -> {
+ Timber.d(throwable);
+ showErrorMessage(getString(R.string.error_fetching_nearby_places)
+ + throwable.getLocalizedMessage());
+ setProgressBarVisibility(false);
+ presenter.lockUnlockNearby(false);
+ setFilterState();
+ }));
+ }
+
+ private void savePlacesAsGPX(LatLng latLng, LatLng nextlatLng) {
+ final Observable savePlacesObservable = Observable
+ .fromCallable(() -> nearbyController
+ .getPlacesAsGPX(latLng, nextlatLng));
+ compositeDisposable.add(savePlacesObservable
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(gpxString -> {
+ if (gpxString != null) {
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss",
+ Locale.getDefault()).format(new Date());
+ String fileName =
+ "GPX_" + timeStamp + "_" + System.currentTimeMillis() + ".gpx";
+ boolean saved = saveFile(gpxString, fileName);
+ setProgressBarVisibility(false);
+ if (saved) {
+ Toast.makeText(this.getContext(),
+ "GPX file saved successfully at /Downloads/" + fileName,
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this.getContext(), "Failed to save KML file.",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ },
+ throwable -> {
+ Timber.d(throwable);
+ showErrorMessage(getString(R.string.error_fetching_nearby_places)
+ + throwable.getLocalizedMessage());
+ setProgressBarVisibility(false);
+ presenter.lockUnlockNearby(false);
+ setFilterState();
+ }));
+ }
+
+ public static boolean saveFile(String string, String fileName) {
+
+ if (!isExternalStorageWritable()) {
+ return false;
+ }
+
+ File downloadsDir = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS);
+ File kmlFile = new File(downloadsDir, fileName);
+
+ try {
+ FileOutputStream fos = new FileOutputStream(kmlFile);
+ fos.write(string.getBytes());
+ fos.close();
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private static boolean isExternalStorageWritable() {
+ String state = Environment.getExternalStorageState();
+ return Environment.MEDIA_MOUNTED.equals(state);
+ }
+
private void populatePlacesForCurrentLocation(
final fr.free.nrw.commons.location.LatLng currentLatLng,
final fr.free.nrw.commons.location.LatLng screenTopRight,
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/PlaceBindings.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/PlaceBindings.kt
new file mode 100644
index 000000000..ccbdd156c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/PlaceBindings.kt
@@ -0,0 +1,46 @@
+package fr.free.nrw.commons.nearby.model
+
+import com.google.gson.annotations.SerializedName
+
+data class PlaceBindings(
+ @SerializedName("item") val item: Item,
+ @SerializedName("label") val label: Label,
+ @SerializedName("location") val location: Location,
+ @SerializedName("class") val clas: Clas
+)
+
+data class ItemsClass(
+ @SerializedName("head") val head: Head,
+ @SerializedName("results") val results: Results
+)
+
+data class Label(
+ @SerializedName("xml:lang") val xml: String,
+ @SerializedName("type") val type: String,
+ @SerializedName("value") val value: String
+)
+
+data class Location(
+ @SerializedName("datatype") val datatype: String,
+ @SerializedName("type") val type: String,
+ @SerializedName("value") val value: String
+)
+
+data class Results(
+ @SerializedName("bindings") val bindings: List
+)
+
+data class Item(
+ @SerializedName("type") val type: String,
+ @SerializedName("value") val value: String
+)
+
+data class Head(
+ @SerializedName("vars") val vars: List
+)
+
+
+data class Clas(
+ @SerializedName("type") val type: String,
+ @SerializedName("value") val value: String
+)
\ No newline at end of file
diff --git a/app/src/main/res/menu/nearby_fragment_menu.xml b/app/src/main/res/menu/nearby_fragment_menu.xml
index b9df1aa0a..30b5c9dd5 100644
--- a/app/src/main/res/menu/nearby_fragment_menu.xml
+++ b/app/src/main/res/menu/nearby_fragment_menu.xml
@@ -6,4 +6,12 @@
app:showAsAction="ifRoom|withText"
android:icon="@drawable/ic_list_white_24dp"
/>
+
+
diff --git a/app/src/main/resources/queries/places_query.rq b/app/src/main/resources/queries/places_query.rq
new file mode 100644
index 000000000..fea399d40
--- /dev/null
+++ b/app/src/main/resources/queries/places_query.rq
@@ -0,0 +1,22 @@
+SELECT
+ ?item
+ (SAMPLE(COALESCE(?en_label, ?fr_label, ?id_label, ?item_label)) as ?label)
+ (SAMPLE(?location) as ?location)
+ (GROUP_CONCAT(DISTINCT ?class_label ; separator=",") as ?class)
+WHERE {
+ SERVICE wikibase:box {
+ ?item wdt:P625 ?location .
+ bd:serviceParam wikibase:cornerSouthWest "Point(${LONGITUDE} ${LATITUDE})"^^geo:wktLiteral .
+ bd:serviceParam wikibase:cornerNorthEast "Point(${NEXT_LONGITUDE} ${NEXT_LATITUDE})"^^geo:wktLiteral .
+ }
+ MINUS {?item wdt:P18 ?image}
+ MINUS {?item wdt:P582 ?endtime.}
+ MINUS {?item wdt:P582 ?dissolvedOrAbolished.}
+ MINUS {?item p:P31 ?instanceStatement. ?instanceStatement pq:P582 ?endtimeQualifier.}
+ OPTIONAL {?item rdfs:label ?en_label . FILTER(LANG(?en_label) = "en")}
+ OPTIONAL {?item rdfs:label ?fr_label . FILTER(LANG(?fr_label) = "fr")}
+ OPTIONAL {?item rdfs:label ?vn_label . FILTER(LANG(?id_label) = "id")}
+ OPTIONAL {?item rdfs:label ?item_label}
+ OPTIONAL {?item wdt:P31 ?class. ?class rdfs:label ?class_label. FILTER(LANG(?class_label) = "en")}
+}
+GROUP BY ?item
\ No newline at end of file