mirror of
https://github.com/russok/FitoTrack.git
synced 2025-10-29 00:32:11 -07:00
#24 Upload workouts to OpenStreetMap as GPS-Trace
This commit is contained in:
parent
7a61b94696
commit
d35c8a7428
@ -74,8 +74,14 @@ dependencies {
|
||||
implementation 'stax:stax-api:1.0.1'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
|
||||
|
||||
implementation ('de.westnordost:osmapi-traces:1.0')
|
||||
configurations {
|
||||
compile.exclude group: 'net.sf.kxml', module: 'kxml2' // already included in Android
|
||||
}
|
||||
implementation 'oauth.signpost:signpost-commonshttp4:1.2.1.2'
|
||||
|
||||
// Android Room
|
||||
def room_version = "2.2.0-beta01"
|
||||
def room_version = "2.2.0"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
package de.tadris.fitness.activity;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
@ -28,21 +30,31 @@ import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import de.tadris.fitness.Instance;
|
||||
import de.tadris.fitness.R;
|
||||
import de.tadris.fitness.data.WorkoutSample;
|
||||
import de.tadris.fitness.osm.OAuthAuthentication;
|
||||
import de.tadris.fitness.osm.OsmTraceUploader;
|
||||
import de.tadris.fitness.util.DialogUtils;
|
||||
import de.tadris.fitness.util.gpx.GpxExporter;
|
||||
import de.tadris.fitness.util.unit.UnitUtils;
|
||||
import de.tadris.fitness.view.ProgressDialogController;
|
||||
import de.westnordost.osmapi.traces.GpsTraceDetails;
|
||||
import oauth.signpost.OAuthConsumer;
|
||||
|
||||
public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.WorkoutDeleter {
|
||||
|
||||
@ -211,7 +223,55 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
|
||||
}).start();
|
||||
}
|
||||
|
||||
private OAuthConsumer oAuthConsumer= null;
|
||||
private void prepareUpload(){
|
||||
OAuthAuthentication authentication= new OAuthAuthentication(mHandler, this, new OAuthAuthentication.OAuthAuthenticationListener() {
|
||||
@Override
|
||||
public void authenticationFailed() {
|
||||
new AlertDialog.Builder(ShowWorkoutActivity.this)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.authenticationFailed)
|
||||
.setPositiveButton(R.string.okay, null)
|
||||
.create().show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationComplete(OAuthConsumer consumer) {
|
||||
oAuthConsumer= consumer;
|
||||
showUploadOptions();
|
||||
}
|
||||
});
|
||||
|
||||
authentication.authenticateIfNecessary();
|
||||
}
|
||||
|
||||
AlertDialog dialog= null;
|
||||
private void showUploadOptions(){
|
||||
dialog= new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.actionUploadToOSM)
|
||||
.setView(R.layout.dialog_upload_osm)
|
||||
.setPositiveButton(R.string.upload, (dialogInterface, i) -> {
|
||||
CheckBox checkBox= dialog.findViewById(R.id.uploadCutting);
|
||||
Spinner spinner= dialog.findViewById(R.id.uploadVisibility);
|
||||
EditText descriptionEdit= dialog.findViewById(R.id.uploadDescription);
|
||||
String description= descriptionEdit.getText().toString().trim();
|
||||
GpsTraceDetails.Visibility visibility;
|
||||
switch (spinner.getSelectedItemPosition()){
|
||||
case 0: visibility= GpsTraceDetails.Visibility.IDENTIFIABLE; break;
|
||||
default:
|
||||
case 1: visibility= GpsTraceDetails.Visibility.TRACKABLE; break;
|
||||
case 2: visibility= GpsTraceDetails.Visibility.PRIVATE; break;
|
||||
}
|
||||
uploadToOsm(checkBox.isChecked(), visibility, description);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void uploadToOsm(boolean cut, GpsTraceDetails.Visibility visibility, String description){
|
||||
List<WorkoutSample> samples = new ArrayList<>(this.samples);
|
||||
new OsmTraceUploader(this, mHandler, workout, samples, visibility, oAuthConsumer, cut, description).upload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
@ -223,6 +283,9 @@ public class ShowWorkoutActivity extends WorkoutActivity implements DialogUtils.
|
||||
case R.id.actionExportGpx:
|
||||
exportToGpx();
|
||||
return true;
|
||||
case R.id.actionUploadOSM:
|
||||
prepareUpload();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@ -80,9 +80,13 @@ public class Workout{
|
||||
if(comment.length() > 2){
|
||||
return comment;
|
||||
}else{
|
||||
return getDateString();
|
||||
}
|
||||
}
|
||||
|
||||
public String getDateString(){
|
||||
return SimpleDateFormat.getDateTimeInstance().format(new Date(start));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
29
app/src/main/java/de/tadris/fitness/osm/GpsTraceLatLong.java
Normal file
29
app/src/main/java/de/tadris/fitness/osm/GpsTraceLatLong.java
Normal file
@ -0,0 +1,29 @@
|
||||
package de.tadris.fitness.osm;
|
||||
|
||||
import de.tadris.fitness.data.WorkoutSample;
|
||||
import de.westnordost.osmapi.map.data.LatLon;
|
||||
|
||||
public class GpsTraceLatLong implements LatLon {
|
||||
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
|
||||
public GpsTraceLatLong(double latitude, double longitude) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public GpsTraceLatLong(WorkoutSample sample) {
|
||||
this(sample.lat, sample.lon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
}
|
||||
132
app/src/main/java/de/tadris/fitness/osm/OAuthAuthentication.java
Normal file
132
app/src/main/java/de/tadris/fitness/osm/OAuthAuthentication.java
Normal file
@ -0,0 +1,132 @@
|
||||
package de.tadris.fitness.osm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.provider.Browser;
|
||||
import android.text.InputType;
|
||||
import android.widget.EditText;
|
||||
|
||||
import de.tadris.fitness.R;
|
||||
import de.tadris.fitness.view.ProgressDialogController;
|
||||
import oauth.signpost.OAuth;
|
||||
import oauth.signpost.OAuthConsumer;
|
||||
import oauth.signpost.OAuthProvider;
|
||||
import oauth.signpost.exception.OAuthException;
|
||||
|
||||
public class OAuthAuthentication {
|
||||
|
||||
private OAuthConsumer oAuthConsumer= OAuthUrlProvider.getDefaultConsumer();
|
||||
private OAuthProvider oAuthProvider= OAuthUrlProvider.getDefaultProvider();
|
||||
|
||||
private Handler handler;
|
||||
private Activity activity;
|
||||
private ProgressDialogController dialogController;
|
||||
private SharedPreferences preferences;
|
||||
private OAuthAuthenticationListener listener;
|
||||
|
||||
public OAuthAuthentication(Handler handler, Activity activity, OAuthAuthenticationListener listener) {
|
||||
this.handler = handler;
|
||||
this.activity = activity;
|
||||
dialogController= new ProgressDialogController(activity, activity.getString(R.string.uploading));
|
||||
this.preferences= activity.getSharedPreferences("osm_oauth", Context.MODE_PRIVATE);
|
||||
this.listener= listener;
|
||||
}
|
||||
|
||||
public void authenticateIfNecessary(){
|
||||
if(isAuthenticated()){
|
||||
loadAccessToken();
|
||||
listener.authenticationComplete(oAuthConsumer);
|
||||
}else{
|
||||
retrieveRequestToken();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthenticated(){
|
||||
return preferences.getBoolean("authenticated", false);
|
||||
}
|
||||
|
||||
private void retrieveRequestToken(){
|
||||
dialogController.show();
|
||||
dialogController.setIndeterminate(true);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String authUrl = oAuthProvider.retrieveRequestToken(oAuthConsumer, OAuth.OUT_OF_BAND);
|
||||
handler.post(() -> {
|
||||
Intent intent= new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl));
|
||||
intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
activity.startActivity(intent);
|
||||
showEnterVerificationCodeDialog();
|
||||
dialogController.cancel();
|
||||
});
|
||||
} catch (OAuthException e) {
|
||||
e.printStackTrace();
|
||||
handler.post(() -> {
|
||||
dialogController.cancel();
|
||||
listener.authenticationFailed();
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void showEnterVerificationCodeDialog(){
|
||||
EditText editText= new EditText(activity);
|
||||
editText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
editText.setSingleLine(true);
|
||||
|
||||
AlertDialog dialog= new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.enterVerificationCode).setView(editText).setPositiveButton(R.string.okay, (dialogInterface, i) -> {
|
||||
new Thread(() -> retrieveAccessToken(editText.getText().toString().trim())).start();
|
||||
dialogInterface.cancel();
|
||||
}).setNegativeButton(R.string.cancel, null).setCancelable(false).create();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void loadAccessToken(){
|
||||
oAuthConsumer.setTokenWithSecret(preferences.getString("accessToken", ""),
|
||||
preferences.getString("tokenSecret", ""));
|
||||
}
|
||||
|
||||
private void saveAccessToken(){
|
||||
preferences.edit()
|
||||
.putString("accessToken", oAuthConsumer.getToken())
|
||||
.putString("tokenSecret", oAuthConsumer.getTokenSecret())
|
||||
.putBoolean("authenticated", true).apply();
|
||||
}
|
||||
|
||||
public void clearAccessToken(){
|
||||
preferences.edit().putBoolean("authenticated", false).apply();
|
||||
}
|
||||
|
||||
private void retrieveAccessToken(String verificationCode){
|
||||
handler.post(() -> dialogController.show());
|
||||
try{
|
||||
oAuthProvider.retrieveAccessToken(oAuthConsumer, verificationCode);
|
||||
handler.post(() -> {
|
||||
dialogController.cancel();
|
||||
saveAccessToken();
|
||||
listener.authenticationComplete(oAuthConsumer);
|
||||
});
|
||||
}catch (OAuthException e){
|
||||
handler.post(() -> {
|
||||
dialogController.cancel();
|
||||
listener.authenticationFailed();
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OAuthAuthenticationListener{
|
||||
|
||||
void authenticationFailed();
|
||||
|
||||
void authenticationComplete(OAuthConsumer consumer);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package de.tadris.fitness.osm;
|
||||
|
||||
import oauth.signpost.OAuthConsumer;
|
||||
import oauth.signpost.OAuthProvider;
|
||||
import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer;
|
||||
import oauth.signpost.commonshttp.CommonsHttpOAuthProvider;
|
||||
|
||||
public class OAuthUrlProvider {
|
||||
|
||||
static private final String CONSUMER_KEY= "jFL9grFmAo5ZS720YDDRXdSOb7F0IZQf9lnY1PHq";
|
||||
static private final String CONSUMER_SECRET= "oH969vYW60fZLco6E09UQl3uFXqjl4siQbOL0q9q";
|
||||
|
||||
static OAuthConsumer getDefaultConsumer(){
|
||||
return new CommonsHttpOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
|
||||
}
|
||||
|
||||
static OAuthProvider getDefaultProvider(){
|
||||
return new CommonsHttpOAuthProvider(URL_TOKEN_REQUEST, URL_TOKEN_ACCESS, URL_AUTHORIZE);
|
||||
}
|
||||
|
||||
static private final String URL_TOKEN_REQUEST= "https://www.openstreetmap.org/oauth/request_token";
|
||||
static private final String URL_TOKEN_ACCESS= "https://www.openstreetmap.org/oauth/access_token";
|
||||
static private final String URL_AUTHORIZE= "https://www.openstreetmap.org/oauth/authorize";
|
||||
|
||||
|
||||
}
|
||||
116
app/src/main/java/de/tadris/fitness/osm/OsmTraceUploader.java
Normal file
116
app/src/main/java/de/tadris/fitness/osm/OsmTraceUploader.java
Normal file
@ -0,0 +1,116 @@
|
||||
package de.tadris.fitness.osm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import de.tadris.fitness.R;
|
||||
import de.tadris.fitness.data.Workout;
|
||||
import de.tadris.fitness.data.WorkoutSample;
|
||||
import de.tadris.fitness.view.ProgressDialogController;
|
||||
import de.westnordost.osmapi.OsmConnection;
|
||||
import de.westnordost.osmapi.traces.GpsTraceDetails;
|
||||
import de.westnordost.osmapi.traces.GpsTracesDao;
|
||||
import de.westnordost.osmapi.traces.GpsTrackpoint;
|
||||
import oauth.signpost.OAuthConsumer;
|
||||
|
||||
public class OsmTraceUploader {
|
||||
|
||||
private static final int CUT_DISTANCE= 300;
|
||||
|
||||
private Activity activity;
|
||||
private Handler handler;
|
||||
private Workout workout;
|
||||
private List<WorkoutSample> samples;
|
||||
private GpsTraceDetails.Visibility visibility;
|
||||
private OAuthConsumer consumer;
|
||||
private boolean cut;
|
||||
private ProgressDialogController dialogController;
|
||||
private String description;
|
||||
|
||||
public OsmTraceUploader(Activity activity, Handler handler, Workout workout, List<WorkoutSample> samples, GpsTraceDetails.Visibility visibility, OAuthConsumer consumer, boolean cut, String description) {
|
||||
this.activity = activity;
|
||||
this.handler = handler;
|
||||
this.workout = workout;
|
||||
this.samples = samples;
|
||||
this.visibility = visibility;
|
||||
this.consumer = consumer;
|
||||
this.cut = cut;
|
||||
this.description= description;
|
||||
this.dialogController= new ProgressDialogController(activity, activity.getString(R.string.uploading));
|
||||
}
|
||||
|
||||
private void cut(){
|
||||
cut(false);
|
||||
cut(true);
|
||||
}
|
||||
|
||||
private void cut(boolean last){
|
||||
double distance= 0;
|
||||
int count= 0;
|
||||
WorkoutSample lastSample= samples.remove(last ? samples.size()-1 : 0);
|
||||
while(distance < CUT_DISTANCE){
|
||||
WorkoutSample currentSample= samples.remove(last ? samples.size()-1 : 0);
|
||||
distance+= lastSample.toLatLong().sphericalDistance(currentSample.toLatLong());
|
||||
count++;
|
||||
lastSample= currentSample;
|
||||
}
|
||||
Log.d("Uploader", "Cutted " + (last ? "last" : "first") + " " + count + " Samples (" + distance + " meters)");
|
||||
}
|
||||
|
||||
public void upload(){
|
||||
new Thread(() -> {
|
||||
try{
|
||||
executeTask();
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
handler.post(() -> {
|
||||
Toast.makeText(activity, R.string.uploadFailed, Toast.LENGTH_LONG).show();
|
||||
dialogController.cancel();
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void executeTask(){
|
||||
handler.post(() -> dialogController.show());
|
||||
setProgress(0);
|
||||
if(cut){ cut(); }
|
||||
setProgress(20);
|
||||
OsmConnection osm = new OsmConnection(
|
||||
"https://api.openstreetmap.org/api/0.6/", "FitoTrack", consumer);
|
||||
|
||||
List<GpsTrackpoint> trackpoints= new ArrayList<>();
|
||||
|
||||
for(WorkoutSample sample : samples){
|
||||
GpsTrackpoint trackpoint= new GpsTrackpoint(new GpsTraceLatLong(sample));
|
||||
trackpoint.time= new Date(sample.absoluteTime);
|
||||
trackpoint.elevation= (float)sample.elevation;
|
||||
trackpoints.add(trackpoint);
|
||||
}
|
||||
setProgress(25);
|
||||
new GpsTracesDao(osm).create(workout.getDateString(), visibility, description, Collections.singletonList("FitoTrack"), trackpoints);
|
||||
setProgress(100);
|
||||
handler.post(() -> {
|
||||
Toast.makeText(activity, R.string.uploadSuccessful, Toast.LENGTH_LONG).show();
|
||||
dialogController.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private void setProgress(int progress){
|
||||
handler.post(() -> dialogController.setProgress(progress));
|
||||
}
|
||||
|
||||
}
|
||||
64
app/src/main/res/layout/dialog_upload_osm.xml
Normal file
64
app/src/main/res/layout/dialog_upload_osm.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TableLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/description" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/uploadDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ems="10"
|
||||
android:inputType="textShortMessage|textAutoComplete"
|
||||
android:singleLine="true" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trackVisibilityPref" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/uploadVisibility"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:dropDownWidth="match_parent"
|
||||
android:entries="@array/osm_track_visibility"
|
||||
android:spinnerMode="dropdown" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</TableLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/uploadCutting"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/cut" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -23,6 +23,9 @@
|
||||
<item
|
||||
android:id="@+id/actionExportGpx"
|
||||
android:title="@string/exportAsGpxFile" />
|
||||
<item
|
||||
android:id="@+id/actionUploadOSM"
|
||||
android:title="@string/actionUploadToOSM" />
|
||||
<item
|
||||
android:id="@+id/actionDeleteWorkout"
|
||||
android:title="@string/delete" />
|
||||
|
||||
8
app/src/main/res/values/osm_track_visibility.xml
Normal file
8
app/src/main/res/values/osm_track_visibility.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="osm_track_visibility">
|
||||
<item>Identifiable</item>
|
||||
<item>Trackable</item>
|
||||
<item>Private</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@ -62,6 +62,12 @@
|
||||
<string name="workoutBurnedEnergy">Burned Energy</string>
|
||||
<string name="workoutTotalEnergy">Total Energy</string>
|
||||
<string name="workoutEnergyConsumption">Energy Consumption</string>
|
||||
<string name="uploading">Uploading</string>
|
||||
<string name="enterVerificationCode">Enter Verification Code</string>
|
||||
<string name="authenticationFailed">Authentication failed.</string>
|
||||
<string name="upload">Upload</string>
|
||||
<string name="uploadSuccessful">Upload Successful</string>
|
||||
<string name="uploadFailed">Upload Failed</string>
|
||||
|
||||
<string name="workoutAscent">Ascent</string>
|
||||
<string name="workoutDescent">Descent</string>
|
||||
@ -115,4 +121,8 @@
|
||||
<string name="data">Data</string>
|
||||
<string name="mapStyle">Map Style</string>
|
||||
<string name="waiting_gps">Waiting for GPS</string>
|
||||
<string name="actionUploadToOSM">Upload to OSM</string>
|
||||
<string name="cut">Cut the first/last 300 Meters</string>
|
||||
<string name="trackVisibilityPref">Track Visibility</string>
|
||||
<string name="description">Description</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user