Added functionality to export location of nearby missing pictures to GPX file and KML file (#5645)

* Fixed Grey empty screen at Upload wizard caption step after denying files permission

* Empty commit

* Fixed loop issue

* Created docs for earlier commits

* Fixed javadoc

* Fixed spaces

* Added added basic features to OSM Maps

* Added search location feature

* Added filter to Open Street Maps

* Fixed chipGroup in Open Street Maps

* Removed mapBox code

* Removed mapBox's code

* Reformat code

* Reformatted code

* Removed rotation feature to map

* Removed rotation files and Fixed Marker click problem

* Ignored failing tests

* Added voice input feature

* Fixed test cases

* Changed caption and description text

* Replaced mapbox to osmdroid in upload activity

* Fixed Unit Tests

* Made selected marker to be fixed on map

* Changed color of map marker

* Fixes #5439 by capitalizing first letter of voice input

* Removed mapbox code1

* Removed mapbox code2

* Fixed failing tests

* Fixed failing due to merging

* Added feature to save nearby places as GPX and KML

* Fixed error caused by null
This commit is contained in:
Kanahia 2024-03-25 19:52:17 +05:30 committed by GitHub
parent dae1f2557e
commit c41940241b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 443 additions and 0 deletions

View file

@ -11,6 +11,8 @@ import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.explore.depictions.DepictsClient; import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; 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.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem; import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.profile.achievements.FeaturedImages; import fr.free.nrw.commons.profile.achievements.FeaturedImages;
@ -27,6 +29,8 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
@ -393,6 +397,196 @@ public class OkHttpJsonApiClient {
throw new Exception(response.message()); 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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
"<!--Created by wikidata-missing-pictures-offline -->\n" +
"<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n" +
" <Document>";
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> 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("&", "&amp;");
String itemLatitude = latStr;
String itemLongitude = longStr;
String itemClass = item.getClas().getValue();
String formattedItemName =
!itemClass.isEmpty() ? itemName + " (" + itemClass + ")"
: itemName;
String kmlEntry = "\n <Placemark>\n" +
" <name>" + formattedItemName + "</name>\n" +
" <description>" + itemUrl + "</description>\n" +
" <Point>\n" +
" <coordinates>" + itemLongitude + ","
+ itemLatitude
+ "</coordinates>\n" +
" </Point>\n" +
" </Placemark>";
kmlString = kmlString + kmlEntry;
} else {
Timber.e("No match found");
}
}
}
}
latitude += increment;
}
longitude += increment;
}
kmlString = kmlString + "\n </Document>\n" +
"</kml>\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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
"<gpx\n" +
" version=\"1.0\"\n" +
" creator=\"ExpertGPS 1.1 - https://www.topografix.com\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xmlns=\"http://www.topografix.com/GPX/1/0\"\n" +
" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">"
+ "\n<bounds minlat=\"$MIN_LATITUDE\" minlon=\"$MIN_LONGITUDE\" maxlat=\"$MAX_LATITUDE\" maxlon=\"$MAX_LONGITUDE\"/>";
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> 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("&", "&amp;");
String itemLatitude = latStr;
String itemLongitude = longStr;
String itemClass = item.getClas().getValue();
String formattedItemName =
!itemClass.isEmpty() ? itemName + " (" + itemClass + ")"
: itemName;
String gpxEntry =
"\n <wpt lat=\"" + itemLatitude + "\" lon=\"" + itemLongitude
+ "\">\n" +
" <name>" + itemName + "</name>\n" +
" <url>" + itemUrl + "</url>\n" +
" </wpt>";
gpxString = gpxString + gpxEntry;
} else {
Timber.e("No match found");
}
}
}
}
latitude += increment;
}
longitude += increment;
}
gpxString = gpxString + "\n</gpx>";
return gpxString;
}
private List<PlaceBindings> 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 * Make API Call to get Nearby Places Implementation does not expects a custom query
* *

View file

@ -118,6 +118,14 @@ public class NearbyController extends MapController {
return nearbyPlacesInfo; 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. * Prepares Place list to make their distance information update later.
* *

View file

@ -119,4 +119,27 @@ public class NearbyPlaces {
.getNearbyPlaces(screenTopRight, screenBottomLeft, lang, shouldQueryForMonuments, .getNearbyPlaces(screenTopRight, screenBottomLeft, lang, shouldQueryForMonuments,
customQuery); 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);
}
} }

View file

@ -22,6 +22,7 @@ import android.location.Location;
import android.location.LocationManager; import android.location.LocationManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.text.Html; 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.contributions.MainActivity.ActiveFragment;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore; 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.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.nearby.CheckBoxTriStates; 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.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
@ -357,6 +363,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@NonNull final MenuInflater inflater) { @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.nearby_fragment_menu, menu); inflater.inflate(R.menu.nearby_fragment_menu, menu);
MenuItem listMenu = menu.findItem(R.id.list_sheet); 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() { listMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override @Override
public boolean onMenuItemClick(MenuItem item) { public boolean onMenuItemClick(MenuItem item) {
@ -364,6 +372,44 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
return false; 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 @Override
@ -1198,6 +1244,102 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
} }
} }
private void savePlacesAsKML(LatLng latLng, LatLng nextlatLng) {
final Observable<String> 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<String> 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( private void populatePlacesForCurrentLocation(
final fr.free.nrw.commons.location.LatLng currentLatLng, final fr.free.nrw.commons.location.LatLng currentLatLng,
final fr.free.nrw.commons.location.LatLng screenTopRight, final fr.free.nrw.commons.location.LatLng screenTopRight,

View file

@ -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<PlaceBindings>
)
data class Item(
@SerializedName("type") val type: String,
@SerializedName("value") val value: String
)
data class Head(
@SerializedName("vars") val vars: List<String>
)
data class Clas(
@SerializedName("type") val type: String,
@SerializedName("value") val value: String
)

View file

@ -6,4 +6,12 @@
app:showAsAction="ifRoom|withText" app:showAsAction="ifRoom|withText"
android:icon="@drawable/ic_list_white_24dp" android:icon="@drawable/ic_list_white_24dp"
/> />
<item android:id="@+id/list_item_gpx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:title="Save as GPX file" />
<item android:id="@+id/list_item_kml"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:title="Save as KML file" />
</menu> </menu>

View file

@ -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