Compare commits

...

6 Commits

Author SHA1 Message Date
Mihail Mitrofanov
d8ddf5a0ae [android]: Backup geo tags and tracks to local folder
This commit adds backup of user data to a local folder on the device.

Features:
* Turn on/off regular backup
* Choose new or existing folder for saving backup
* Set how often backup runs
* Set how many backups to keep
* Create backup manually

Signed-off-by: Mihail Mitrofanov <mk.mitrofanov@outlook.com>
2025-06-27 23:55:49 +07:00
Harry Bond
1e32df193d [Android] fix speed limit indicator activating when driving exactly at speed limit
the displayed speed is rounded, but the speedLimitExceeded check isn't, resulting in what appears to be incorrect activations. Fixed by rounding in speedLimitExceeded also.
fixes #544

Signed-off-by: Harry Bond <me@hbond.xyz>
2025-06-27 23:54:30 +07:00
Eivind Samseth
fca61732b7 [styles] Regenerate
Signed-off-by: Eivind Samseth <eisa01@gmail.com>
2025-06-27 23:52:16 +07:00
Eivind Samseth
8053d2f4a5 [styles] Remove path designated bicycle on level z14 as requested by review
Signed-off-by: Eivind Samseth <eisa01@gmail.com>
2025-06-27 23:51:39 +07:00
Eivind Samseth
7bfe507e57 [styles] Regenerate
Signed-off-by: Eivind Samseth <eisa01@gmail.com>
2025-06-27 23:51:35 +07:00
Eivind Samseth
1e06e46344 [styles] Render paths and track at same level as footway and cycleway
Signed-off-by: Eivind Samseth <eisa01@gmail.com>
2025-06-27 23:51:02 +07:00
23 changed files with 1501 additions and 472 deletions

View File

@@ -44,6 +44,7 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import app.organicmaps.api.Const;
import app.organicmaps.backup.PeriodicBackupRunner;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.base.OnBackPressListener;
import app.organicmaps.bookmarks.BookmarkCategoriesActivity;
@@ -139,6 +140,7 @@ import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_HELP_CODE;
import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_RECORD_TRACK_CODE;
import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_SETTINGS_CODE;
import static app.organicmaps.util.PowerManagment.POWER_MANAGEMENT_TAG;
import static app.organicmaps.util.concurrency.UiThread.runLater;
public class MwmActivity extends BaseMwmFragmentActivity
implements PlacePageActivationListener,
@@ -253,6 +255,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
@NonNull
private DisplayManager mDisplayManager;
private PeriodicBackupRunner backupRunner;
ManageRouteBottomSheet mManageRouteBottomSheet;
private boolean mRemoveDisplayListener = true;
@@ -607,6 +611,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
*/
if (Map.isEngineCreated())
onRenderingInitializationFinished();
backupRunner = new PeriodicBackupRunner(this);
}
private void onSettingsResult(ActivityResult activityResult)
@@ -1352,6 +1358,11 @@ public class MwmActivity extends BaseMwmFragmentActivity
final String backUrl = Framework.nativeGetParsedBackUrl();
if (!TextUtils.isEmpty(backUrl))
Utils.openUri(this, Uri.parse(backUrl), null);
if (backupRunner != null && !backupRunner.isAlreadyChecked() && backupRunner.isTimeToBackup())
{
backupRunner.doBackup();
}
}
@CallSuper

View File

@@ -0,0 +1,114 @@
package app.organicmaps.backup;
import static app.organicmaps.settings.BackupSettingsFragment.MAX_BACKUPS_DEFAULT_COUNT;
import static app.organicmaps.settings.BackupSettingsFragment.MAX_BACKUPS_KEY;
import static app.organicmaps.util.StorageUtils.isFolderWritable;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import app.organicmaps.R;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.log.Logger;
public class BackupUtils
{
private static final String BACKUP_PREFIX = "backup_";
private static final String BACKUP_EXTENSION = ".kmz";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withLocale(Locale.US);
private static final String TAG = BackupUtils.class.getSimpleName();
public static CharSequence formatReadableFolderPath(Context context, @NonNull Uri uri)
{
String docId = DocumentsContract.getTreeDocumentId(uri);
String volumeId;
String subPath = "";
int colonIndex = docId.indexOf(':');
if (colonIndex >= 0)
{
volumeId = docId.substring(0, colonIndex);
subPath = docId.substring(colonIndex + 1);
}
else
{
volumeId = docId;
}
String volumeName;
if ("primary".equalsIgnoreCase(volumeId))
volumeName = context.getString(R.string.maps_storage_shared);
else
volumeName = context.getString(R.string.maps_storage_removable);
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append(volumeName + ": \n", new AbsoluteSizeSpan(UiUtils.dimen(context, R.dimen.text_size_body_3)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.append("/" + subPath, new AbsoluteSizeSpan(UiUtils.dimen(context, R.dimen.text_size_body_4)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return sb;
}
public static int getMaxBackups(SharedPreferences prefs)
{
String rawValue = prefs.getString(MAX_BACKUPS_KEY, String.valueOf(MAX_BACKUPS_DEFAULT_COUNT));
try
{
return Integer.parseInt(rawValue);
} catch (NumberFormatException e)
{
Logger.e(TAG, "Failed to parse max backups count, raw value: " + rawValue + " set to default: " + MAX_BACKUPS_DEFAULT_COUNT, e);
prefs.edit()
.putString(MAX_BACKUPS_KEY, String.valueOf(MAX_BACKUPS_DEFAULT_COUNT))
.apply();
return MAX_BACKUPS_DEFAULT_COUNT;
}
}
public static DocumentFile createUniqueBackupFolder(@NonNull DocumentFile parentDir, LocalDateTime backupTime)
{
String folderName = BACKUP_PREFIX + backupTime.format(DATE_FORMATTER);
return parentDir.createDirectory(folderName);
}
public static String getBackupName(LocalDateTime backupTime)
{
String formattedBackupTime = backupTime.format(DATE_FORMATTER);
return BACKUP_PREFIX + formattedBackupTime + BACKUP_EXTENSION;
}
public static DocumentFile[] getBackupFolders(DocumentFile parentDir)
{
List<DocumentFile> backupFolders = new ArrayList<>();
for (DocumentFile file : parentDir.listFiles())
{
if (file.isDirectory() && file.getName() != null && file.getName().startsWith(BACKUP_PREFIX))
backupFolders.add(file);
}
return backupFolders.toArray(new DocumentFile[0]);
}
public static boolean isBackupFolderAvailable(Context context, String storedFolderPath)
{
return !TextUtils.isEmpty(storedFolderPath) && isFolderWritable(context, storedFolderPath);
}
}

View File

@@ -0,0 +1,189 @@
package app.organicmaps.backup;
import static app.organicmaps.backup.BackupUtils.getBackupName;
import static app.organicmaps.backup.BackupUtils.getBackupFolders;
import static app.organicmaps.util.StorageUtils.copyFileToDocumentFile;
import static app.organicmaps.util.StorageUtils.deleteDirectoryRecursive;
import android.app.Activity;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.BookmarkSharingResult;
import app.organicmaps.bookmarks.data.KmlFileType;
import app.organicmaps.util.concurrency.ThreadPool;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
public class LocalBackupManager implements BookmarkManager.BookmarksSharingListener
{
public static final String TAG = LocalBackupManager.class.getSimpleName();
private final Activity activity;
private final String backupFolderPath;
private final int maxBackups;
private Listener listener;
public LocalBackupManager(@NonNull Activity activity, @NonNull String backupFolderPath, int maxBackups)
{
this.activity = activity;
this.backupFolderPath = backupFolderPath;
this.maxBackups = maxBackups;
}
public void doBackup()
{
BookmarkManager.INSTANCE.addSharingListener(this);
prepareBookmarkCategoriesForSharing();
if (listener != null)
listener.onBackupStarted();
}
public void setListener(@NonNull Listener listener)
{
this.listener = listener;
}
@Override
public void onPreparedFileForSharing(@NonNull BookmarkSharingResult result)
{
BookmarkManager.INSTANCE.removeSharingListener(this);
ThreadPool.getWorker().execute(() -> {
ErrorCode errorCode = null;
switch (result.getCode())
{
case BookmarkSharingResult.SUCCESS ->
{
if (!saveBackup(result))
{
Logger.e(TAG, "Failed to save backup. See system log above");
errorCode = ErrorCode.FILE_ERROR;
}
else
{
Logger.i(TAG, "Backup was created and saved successfully");
}
}
case BookmarkSharingResult.EMPTY_CATEGORY ->
{
errorCode = ErrorCode.EMPTY_CATEGORY;
Logger.e(TAG, "Failed to create backup. Category is empty");
}
case BookmarkSharingResult.ARCHIVE_ERROR ->
{
errorCode = ErrorCode.ARCHIVE_ERROR;
Logger.e(TAG, "Failed to create archive of bookmarks");
}
case BookmarkSharingResult.FILE_ERROR ->
{
errorCode = ErrorCode.FILE_ERROR;
Logger.e(TAG, "Failed create file for archive");
}
default ->
{
errorCode = ErrorCode.UNSUPPORTED;
Logger.e(TAG, "Failed to create backup. Unknown error");
}
}
ErrorCode finalErrorCode = errorCode;
UiThread.run(() -> {
if (listener != null)
{
if (finalErrorCode == null)
listener.onBackupFinished();
else
listener.onBackupFailed(finalErrorCode);
}
});
});
}
private boolean saveBackup(@NonNull BookmarkSharingResult result)
{
boolean isSuccess = false;
Uri folderUri = Uri.parse(backupFolderPath);
try
{
DocumentFile parentFolder = DocumentFile.fromTreeUri(activity, folderUri);
if (parentFolder != null && parentFolder.canWrite())
{
LocalDateTime now = LocalDateTime.now();
DocumentFile backupFolder = BackupUtils.createUniqueBackupFolder(parentFolder, now);
if (backupFolder != null)
{
String backupName = getBackupName(now);
DocumentFile backupFile = backupFolder.createFile(result.getMimeType(), backupName);
if (backupFile != null && copyFileToDocumentFile(activity, new File(result.getSharingPath()), backupFile))
{
Logger.i(TAG, "Backup saved to " + backupFile.getUri());
isSuccess = true;
}
}
else
{
Logger.e(TAG, "Failed to create backup folder");
}
}
cleanOldBackups(parentFolder);
} catch (Exception e)
{
Logger.e(TAG, "Failed to save backup", e);
}
return isSuccess;
}
public void cleanOldBackups(DocumentFile parentDir)
{
DocumentFile[] backupFolders = getBackupFolders(parentDir);
if (backupFolders.length > maxBackups)
{
Arrays.sort(backupFolders, Comparator.comparing(DocumentFile::getName));
for (int i = 0; i < backupFolders.length - maxBackups; i++)
{
Logger.i(TAG, "Delete old backup " + backupFolders[i].getUri());
deleteDirectoryRecursive(backupFolders[i]);
}
}
}
private void prepareBookmarkCategoriesForSharing()
{
List<BookmarkCategory> categories = BookmarkManager.INSTANCE.getCategories();
long[] categoryIds = new long[categories.size()];
for (int i = 0; i < categories.size(); i++)
categoryIds[i] = categories.get(i).getId();
BookmarkManager.INSTANCE.prepareCategoriesForSharing(categoryIds, KmlFileType.Text);
}
public interface Listener
{
void onBackupStarted();
void onBackupFinished();
void onBackupFailed(ErrorCode errorCode);
}
public enum ErrorCode
{
EMPTY_CATEGORY,
ARCHIVE_ERROR,
FILE_ERROR,
UNSUPPORTED,
}
}

View File

@@ -0,0 +1,104 @@
package app.organicmaps.backup;
import static app.organicmaps.backup.BackupUtils.getMaxBackups;
import static app.organicmaps.backup.BackupUtils.isBackupFolderAvailable;
import static app.organicmaps.settings.BackupSettingsFragment.BACKUP_FOLDER_PATH_KEY;
import static app.organicmaps.settings.BackupSettingsFragment.BACKUP_INTERVAL_KEY;
import static app.organicmaps.settings.BackupSettingsFragment.LAST_BACKUP_TIME_KEY;
import static app.organicmaps.util.StorageUtils.isFolderWritable;
import android.app.Activity;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import app.organicmaps.util.log.Logger;
public class PeriodicBackupRunner
{
private final Activity activity;
private static final String TAG = PeriodicBackupRunner.class.getSimpleName();
private final SharedPreferences prefs;
private boolean alreadyChecked = false;
public PeriodicBackupRunner(Activity activity)
{
this.activity = activity;
this.prefs = PreferenceManager.getDefaultSharedPreferences(activity);
}
public boolean isAlreadyChecked()
{
return alreadyChecked;
}
public boolean isTimeToBackup()
{
long intervalMs = getBackupIntervalMs();
if (intervalMs <= 0)
return false;
long lastBackupTime = prefs.getLong(LAST_BACKUP_TIME_KEY, 0);
long now = System.currentTimeMillis();
alreadyChecked = true;
return (now - lastBackupTime) >= intervalMs;
}
public void doBackup()
{
String storedFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
if (isBackupFolderAvailable(activity, storedFolderPath))
{
Logger.i(TAG, "Performing periodic backup");
performBackup(storedFolderPath, getMaxBackups(prefs));
}
else
{
Logger.w(TAG, "Backup folder is not writable, passed path: " + storedFolderPath);
}
}
private long getBackupIntervalMs()
{
String defaultValue = "0";
try
{
return Long.parseLong(prefs.getString(BACKUP_INTERVAL_KEY, defaultValue));
} catch (NumberFormatException e)
{
return 0;
}
}
private void performBackup(String backupFolderPath, int maxBackups)
{
LocalBackupManager backupManager = new LocalBackupManager(activity, backupFolderPath, maxBackups);
backupManager.setListener(new LocalBackupManager.Listener()
{
@Override
public void onBackupStarted()
{
Logger.i(TAG, "Periodic backup started");
}
@Override
public void onBackupFinished()
{
prefs.edit().putLong(LAST_BACKUP_TIME_KEY, System.currentTimeMillis()).apply();
Logger.i(TAG, "Periodic backup finished");
}
@Override
public void onBackupFailed(LocalBackupManager.ErrorCode errorCode)
{
Logger.e(TAG, "Periodic backup was failed with code: " + errorCode);
}
});
backupManager.doBackup();
}
}

View File

@@ -266,7 +266,8 @@ public class NavigationController implements TrafficManager.TrafficCallback,
mSpeedLimit.setSpeedLimit(0, false);
return;
}
final boolean speedLimitExceeded = info.speedLimitMps < location.getSpeed();
mSpeedLimit.setSpeedLimit(StringUtils.nativeFormatSpeed(info.speedLimitMps), speedLimitExceeded);
final int fSpeedLimit = StringUtils.nativeFormatSpeed(info.speedLimitMps);
final boolean speedLimitExceeded = fSpeedLimit < StringUtils.nativeFormatSpeed(location.getSpeed());
mSpeedLimit.setSpeedLimit(fSpeedLimit, speedLimitExceeded);
}
}

View File

@@ -0,0 +1,384 @@
package app.organicmaps.settings;
import static app.organicmaps.backup.BackupUtils.formatReadableFolderPath;
import static app.organicmaps.backup.BackupUtils.getMaxBackups;
import static app.organicmaps.backup.BackupUtils.isBackupFolderAvailable;
import static app.organicmaps.util.StorageUtils.isFolderWritable;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.text.DateFormat;
import app.organicmaps.R;
import app.organicmaps.backup.LocalBackupManager;
import app.organicmaps.util.log.Logger;
public class BackupSettingsFragment
extends BaseXmlSettingsFragment
{
private ActivityResultLauncher<Intent> folderPickerLauncher;
private static final String TAG = LocalBackupManager.class.getSimpleName();
public static final String BACKUP_FOLDER_PATH_KEY = "backup_location";
public static final String LAST_BACKUP_TIME_KEY = "last_backup_time";
private static final String BACKUP_NOW_KEY = "backup_now";
public static final String BACKUP_INTERVAL_KEY = "backup_history_interval";
public static final String MAX_BACKUPS_KEY = "backup_history_count";
public static final int MAX_BACKUPS_DEFAULT_COUNT = 10;
public static final String DEFAULT_BACKUP_INTERVAL = "86400000"; // 24 hours in ms
private LocalBackupManager mBackupManager;
private SharedPreferences prefs;
@Override
protected int getXmlResources()
{
return R.xml.prefs_backup;
}
@NonNull
@SuppressWarnings("NotNullFieldNotInitialized")
Preference backupLocationOption;
@NonNull
@SuppressWarnings("NotNullFieldNotInitialized")
ListPreference backupIntervalOption;
@NonNull
@SuppressWarnings("NotNullFieldNotInitialized")
Preference maxBackupsOption;
@NonNull
@SuppressWarnings("NotNullFieldNotInitialized")
Preference backupNowOption;
@NonNull
@SuppressWarnings("NotNullFieldNotInitialized")
Preference advancedCategory;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
folderPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
boolean isSuccess = false;
String lastFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
if (result.getResultCode() == Activity.RESULT_OK)
{
Intent data = result.getData();
Logger.i(TAG, "Folder selection result: " + data);
if (data == null)
return;
Uri uri = data.getData();
if (uri != null)
{
takePersistableUriPermission(uri);
Logger.i(TAG, "Backup location changed to " + uri);
prefs.edit().putString(BACKUP_FOLDER_PATH_KEY, uri.toString()).apply();
setFormattedBackupPath(uri);
runBackup();
isSuccess = true;
}
else
{
Logger.w(TAG, "Folder selection result is null");
}
}
else if (result.getResultCode() == Activity.RESULT_CANCELED)
{
Logger.w(TAG, "User canceled folder selection");
if (TextUtils.isEmpty(lastFolderPath))
{
prefs.edit().putString(BACKUP_FOLDER_PATH_KEY, null).apply();
Logger.i(TAG, "Backup settings reset");
initBackupLocationOption();
}
else if (isFolderWritable(requireActivity(), lastFolderPath))
{
Logger.i(TAG, "Backup location not changed, using previous value " + lastFolderPath);
isSuccess = true;
}
else
{
Logger.e(TAG, "Backup location not changed, but last folder is not writable: " + lastFolderPath);
}
}
resetLastBackupTime();
updateStatusSummaryOption();
Logger.i(TAG, "Folder selection result: " + isSuccess);
applyAdvancedSettings(isSuccess);
}
);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
{
super.onCreatePreferences(savedInstanceState, rootKey);
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
backupLocationOption = findPreference(BACKUP_FOLDER_PATH_KEY);
backupIntervalOption = findPreference(BACKUP_INTERVAL_KEY);
maxBackupsOption = findPreference(MAX_BACKUPS_KEY);
backupNowOption = findPreference(BACKUP_NOW_KEY);
initBackupLocationOption();
initBackupIntervalOption();
initMaxBackupsOption();
initBackupNowOption();
}
private void initBackupLocationOption()
{
String storedFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
boolean isEnabled = false;
if (!TextUtils.isEmpty(storedFolderPath))
{
if (isFolderWritable(requireContext(), storedFolderPath))
{
setFormattedBackupPath(Uri.parse(storedFolderPath));
isEnabled = true;
}
else
{
Logger.e(TAG, "Backup location is not available, path: " + storedFolderPath);
showBackupErrorAlertDialog(requireContext().getString(R.string.dialog_report_error_missing_folder));
backupLocationOption.setSummary(requireContext().getString(R.string.pref_backup_now_summary_folder_unavailable));
}
}
else
{
backupLocationOption.setSummary(requireContext().getString(R.string.pref_backup_location_summary_initial));
}
applyAdvancedSettings(isEnabled);
backupLocationOption.setOnPreferenceClickListener(preference -> {
launchFolderPicker();
return true;
});
}
private void setFormattedBackupPath(@NonNull Uri uri)
{
backupLocationOption.setSummary(formatReadableFolderPath(requireContext(), uri));
}
private void initBackupIntervalOption()
{
String backupInterval = prefs.getString(BACKUP_INTERVAL_KEY, DEFAULT_BACKUP_INTERVAL);
CharSequence entry = getEntryForValue(backupIntervalOption, backupInterval);
if (entry != null)
backupIntervalOption.setSummary(entry);
backupIntervalOption.setOnPreferenceChangeListener((preference, newValue) -> {
CharSequence newEntry = getEntryForValue(backupIntervalOption, newValue.toString());
Logger.i(TAG, "auto backup interval changed to " + newEntry);
if (newEntry != null)
backupIntervalOption.setSummary(newEntry);
return true;
});
}
private void initMaxBackupsOption()
{
maxBackupsOption.setSummary(String.valueOf(getMaxBackups(prefs)));
maxBackupsOption.setOnPreferenceChangeListener((preference, newValue) -> {
maxBackupsOption.setSummary(newValue.toString());
return true;
});
}
private void initBackupNowOption()
{
updateStatusSummaryOption();
backupNowOption.setOnPreferenceClickListener(preference -> {
runBackup();
return true;
});
}
private void updateStatusSummaryOption()
{
long lastBackupTime = prefs.getLong(LAST_BACKUP_TIME_KEY, 0L);
String summary;
if (lastBackupTime > 0)
{
String time = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(lastBackupTime);
summary = requireContext().getString(R.string.pref_backup_status_summary_success) + ": " + time;
}
else
{
summary = requireContext().getString(R.string.pref_backup_now_summary);
}
backupNowOption.setSummary(summary);
}
private void resetLastBackupTime()
{
prefs.edit().remove(LAST_BACKUP_TIME_KEY).apply();
}
private void applyAdvancedSettings(boolean isBackupEnabled)
{
backupIntervalOption.setVisible(isBackupEnabled);
maxBackupsOption.setVisible(isBackupEnabled);
backupNowOption.setVisible(isBackupEnabled);
}
private void runBackup()
{
String currentFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
if (!TextUtils.isEmpty(currentFolderPath))
{
if (isFolderWritable(requireContext(), currentFolderPath))
{
mBackupManager = new LocalBackupManager(requireActivity(), currentFolderPath, getMaxBackups(prefs));
mBackupManager.setListener(new LocalBackupManager.Listener()
{
@Override
public void onBackupStarted()
{
Logger.i(TAG, "Manual backup started");
backupNowOption.setEnabled(false);
backupNowOption.setSummary(R.string.pref_backup_now_summary_progress);
}
@Override
public void onBackupFinished()
{
Logger.i(TAG, "Manual backup successful");
backupNowOption.setEnabled(true);
backupNowOption.setSummary(R.string.pref_backup_now_summary_ok);
prefs.edit().putLong(LAST_BACKUP_TIME_KEY, System.currentTimeMillis()).apply();
}
@Override
public void onBackupFailed(LocalBackupManager.ErrorCode errorCode)
{
String errorMessage = switch (errorCode)
{
case EMPTY_CATEGORY -> requireContext().getString(R.string.pref_backup_now_summary_empty_lists);
default -> requireContext().getString(R.string.pref_backup_now_summary_failed);
};
Logger.e(TAG, "Manual backup was failed with code: " + errorCode);
backupNowOption.setEnabled(true);
backupNowOption.setSummary(errorMessage);
showBackupErrorAlertDialog(requireContext().getString(R.string.dialog_report_error_with_logs));
}
});
mBackupManager.doBackup();
}
else
{
backupNowOption.setSummary(R.string.pref_backup_now_summary_folder_unavailable);
showBackupErrorAlertDialog(requireContext().getString(R.string.dialog_report_error_missing_folder));
Logger.e(TAG, "Manual backup error: folder " + currentFolderPath + " unavailable");
}
}
else
{
backupNowOption.setSummary(R.string.pref_backup_now_summary_folder_unavailable);
Logger.e(TAG, "Manual backup error: no folder selected");
}
}
private void launchFolderPicker()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
PackageManager packageManager = requireActivity().getPackageManager();
if (intent.resolveActivity(packageManager) != null)
folderPickerLauncher.launch(intent);
else
showNoFileManagerError();
}
private void showNoFileManagerError()
{
new MaterialAlertDialogBuilder(requireActivity())
.setMessage(R.string.error_no_file_manager_app)
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
private void showBackupErrorAlertDialog(String message)
{
requireActivity().runOnUiThread(() -> {
new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.pref_backup_now_summary_failed)
.setMessage(message)
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
});
}
private void takePersistableUriPermission(Uri uri)
{
requireContext().getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
}
@Nullable
public static CharSequence getEntryForValue(@NonNull ListPreference listPref, @NonNull CharSequence value)
{
CharSequence[] entryValues = listPref.getEntryValues();
CharSequence[] entries = listPref.getEntries();
if (entryValues == null || entries == null)
return null;
for (int i = 0; i < entryValues.length; i++)
{
if (entryValues[i].equals(value))
return entries[i];
}
return null;
}
}

View File

@@ -189,6 +189,10 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
LanguagesFragment langFragment = (LanguagesFragment)getSettingsActivity().stackFragment(LanguagesFragment.class, getString(R.string.change_map_locale), null);
langFragment.setListener(this);
}
else if (key.equals(getString(R.string.pref_backup)))
{
getSettingsActivity().stackFragment(BackupSettingsFragment.class, getString(R.string.pref_backup_title), null);
}
}
return super.onPreferenceTreeClick(preference);
}

View File

@@ -1,5 +1,6 @@
package app.organicmaps.util;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -10,10 +11,13 @@ import android.provider.DocumentsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile;
import app.organicmaps.BuildConfig;
import app.organicmaps.util.log.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
@@ -323,4 +327,76 @@ public class StorageUtils
}
}
}
public static boolean copyFileToDocumentFile(
@NonNull Activity activity,
@NonNull File sourceFile,
@NonNull DocumentFile targetFile
)
{
try (
InputStream in = new FileInputStream(sourceFile);
OutputStream out = activity.getContentResolver().openOutputStream(targetFile.getUri())
)
{
if (out == null)
{
Logger.e(TAG, "Failed to open output stream for " + targetFile.getUri());
return false;
}
byte[] buffer = new byte[8192];
int length;
while ((length = in.read(buffer)) > 0)
out.write(buffer, 0, length);
out.flush();
return true;
} catch (IOException e)
{
Logger.e(TAG, "Failed to copy file from " + sourceFile.getAbsolutePath() + " to " + targetFile.getUri(), e);
return false;
}
}
public static void deleteDirectoryRecursive(@NonNull DocumentFile dir)
{
try
{
for (DocumentFile file : dir.listFiles())
{
if (file.isDirectory())
deleteDirectoryRecursive(file);
else
file.delete();
}
dir.delete();
} catch (Exception e)
{
Logger.e(TAG, "Failed to delete directory: " + dir.getUri(), e);
}
}
public static boolean isFolderWritable(Context context, String folderPath)
{
try
{
Uri folderUri = Uri.parse(folderPath);
DocumentFile folder = DocumentFile.fromTreeUri(context, folderUri);
if (folder != null && folder.canWrite())
{
DocumentFile tempFile = folder.createFile("application/octet-stream", "temp_file");
if (tempFile != null)
{
tempFile.delete();
return true;
}
}
} catch (Exception e)
{
Logger.e(TAG, "Failed to check if folder is writable: " + folderPath, e);
}
return false;
}
}

View File

@@ -904,4 +904,29 @@
<string name="editor_building_levels">Этажей (вкл. подвалы искл. крышу)</string>
<string name="editor_level">Этаж (0 это первый этаж)</string>
<string name="error_enter_correct_level">Введите правильный номер этажа</string>
<!-- Settings "Backup" category: "Backup" title -->
<string name="pref_backup_title">Резервное копирование меток и треков</string>
<string name="pref_backup_summary">Автоматически сохранять в папку на устройстве</string>
<string name="pref_backup_now_title">Создать резервную копию</string>
<string name="pref_backup_now_summary">Запустить резервное копирование вручную</string>
<string name="pref_backup_now_summary_progress">Идёт резервное копирование…</string>
<string name="pref_backup_now_summary_ok">Копирование успешно завершено</string>
<string name="pref_backup_now_summary_empty_lists">Нет данных для копирования</string>
<string name="pref_backup_now_summary_failed">Ошибка при копировании</string>
<string name="pref_backup_now_summary_folder_unavailable">Папка для копий недоступна</string>
<string name="pref_backup_status_summary_success">Последнее успешное копирование</string>
<string name="pref_backup_location_title">Папка для резервных копий</string>
<string name="pref_backup_location_summary_initial">Сначала выберите папку и дайте доступ</string>
<string name="pref_backup_history_title">Хранить количество копий</string>
<string name="pref_backup_interval_title">Автозапуск</string>
<string name="backup_interval_every_day">Каждый день</string>
<string name="backup_interval_every_week">Каждую неделю</string>
<string name="backup_interval_manual_only">Выключено (только вручную)</string>
<string name="dialog_report_error_missing_folder">Выбранная папка для резервного копирования недоступна или нет права записи в неё. Пожалуйста, выберите другую папку</string>
<string name="dialog_report_error_with_logs">Пожалуйста, отправьте нам отчет об ошибке:\n
- Включите \"Запись логов\" в настройках\n
- воспроизведите проблему\n
- на экране \"Справка\" нажмите кнопку \"Сообщить о проблеме\" и отправьте нам отчет по почте или в чат\n
- отключите логирование
</string>
</resources>

View File

@@ -40,6 +40,7 @@
<string name="pref_keep_screen_on" translatable="false">KeepScreenOn</string>
<string name="pref_show_on_lock_screen" translatable="false">ShowOnLockScreen</string>
<string name="pref_map_locale" translatable="false">MapLanguage</string>
<string name="pref_backup" translatable="false">Backup</string>
<string name="pref_left_button" translatable="false">LeftButton</string>
<string name="notification_ticker_ltr" translatable="false">%1$s: %2$s</string>

View File

@@ -23,7 +23,29 @@
<item>0</item>
<item>1</item>
</string-array>
<string-array name="backup_interval_entries">
<item>@string/backup_interval_every_day</item>
<item>@string/backup_interval_every_week</item>
<item>@string/backup_interval_manual_only</item>
</string-array>
<string-array name="backup_interval_values">
<item>86400000</item> <!-- Every day -->
<item>604800000</item> <!-- Every week -->
<item>0</item> <!-- Manual only -->
</string-array>
<string-array name="backup_history_entries">
<item>3</item>
<item>10</item>
</string-array>
<string-array name="backup_history_values">
<item>3</item>
<item>10</item>
</string-array>
<string-array name="map_style">
<item>@string/off</item>
<item>@string/on</item>

View File

@@ -936,6 +936,32 @@
<string name="codeberg">Codeberg</string>
<string name="pref_left_button_title">Left button setup</string>
<string name="pref_left_button_disable">Disable</string>
<!-- Settings "Backup" category: "Backup" title -->
<string name="pref_backup_title">Bookmarks and tracks backup</string>
<string name="pref_backup_summary">Automatically backup to a folder on your device</string>
<string name="pref_backup_now_title">Backup now</string>
<string name="pref_backup_now_summary">Create a backup immediately</string>
<string name="pref_backup_now_summary_progress">Backup in progress…</string>
<string name="pref_backup_now_summary_ok">Backup completed successfully</string>
<string name="pref_backup_now_summary_empty_lists">Nothing to back up</string>
<string name="pref_backup_now_summary_failed">Backup failed</string>
<string name="pref_backup_now_summary_folder_unavailable">The backup folder is not available</string>
<string name="pref_backup_status_summary_success">Last successful backup</string>
<string name="pref_backup_location_title">Backup location</string>
<string name="pref_backup_location_summary_initial">Please select a folder first and grant permission</string>
<string name="pref_backup_history_title">Number of backups to keep</string>
<string name="pref_backup_interval_title">Automatic backup</string>
<string name="backup_interval_every_day">Daily</string>
<string name="backup_interval_every_week">Weekly</string>
<string name="backup_interval_manual_only">Off (manual only)</string>
<string name="dialog_report_error_missing_folder">The selected backup location is not available or writable. Select a different location, please.</string>
<string name="dialog_report_error_with_logs">Please send us an error report:\n
- \"Enable logging\" in the settings\n
- reproduce the problem\n
- in the \"Help/About\" screen press a \"Report a bug\" button and send it to us via email or chat\n
- disable logging
</string>
<string name="clear">Clear</string>
<string name="route_type">Route type</string>
<string name="vehicle">Vehicle</string>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="backup_location"
android:summary="@string/pref_backup_location_summary_initial"
android:title="@string/pref_backup_location_title" />
<Preference
android:key="backup_now"
android:summary="@string/pref_backup_now_summary"
android:title="@string/pref_backup_now_title" />
<ListPreference
android:defaultValue="86400000"
android:entries="@array/backup_interval_entries"
android:entryValues="@array/backup_interval_values"
android:key="backup_history_interval"
android:title="@string/pref_backup_interval_title" />
<ListPreference
android:defaultValue="10"
android:entries="@array/backup_history_entries"
android:entryValues="@array/backup_history_values"
android:key="backup_history_count"
android:title="@string/pref_backup_history_title" />
</PreferenceScreen>

View File

@@ -112,6 +112,13 @@
app:singleLineTitle="false"
android:persistent="false"
android:order="18"/>
<Preference
android:key="@string/pref_backup"
android:title="@string/pref_backup_title"
android:summary="@string/pref_backup_summary"
app:singleLineTitle="false"
android:persistent="false"
android:order="19"/>
</androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -26904,19 +26904,6 @@ cont {
}
cont {
name: "highway-path"
element {
scale: 14
lines {
width: 0.9
color: 1720994322
dashdot {
dd: 3.5
dd: 2.0
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27049,16 +27036,6 @@ cont {
priority: 219
cap: BUTTCAP
}
lines {
width: 0.9
color: 1720994322
dashdot {
dd: 3.5
dd: 2.7
}
priority: 220
cap: BUTTCAP
}
}
element {
scale: 15
@@ -27215,19 +27192,6 @@ cont {
}
cont {
name: "highway-path-bridge"
element {
scale: 14
lines {
width: 0.9
color: 1720994322
dashdot {
dd: 3.5
dd: 2.0
}
priority: 155
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27412,19 +27376,6 @@ cont {
}
cont {
name: "highway-path-difficult"
element {
scale: 14
lines {
width: 0.9
color: 1720994322
dashdot {
dd: 1.0
dd: 2.0
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27549,19 +27500,6 @@ cont {
}
cont {
name: "highway-path-expert"
element {
scale: 14
lines {
width: 0.9
color: 1716665907
dashdot {
dd: 1.0
dd: 4.0
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27686,19 +27624,6 @@ cont {
}
cont {
name: "highway-path-horse"
element {
scale: 14
lines {
width: 0.9
color: 1720994322
dashdot {
dd: 3.5
dd: 2.0
}
priority: 155
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27823,19 +27748,6 @@ cont {
}
cont {
name: "highway-path-tunnel"
element {
scale: 14
lines {
width: 0.9
color: 1720994322
dashdot {
dd: 3.5
dd: 2.0
}
priority: 155
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -35974,19 +35886,6 @@ cont {
}
cont {
name: "highway-track"
element {
scale: 14
lines {
width: 1.1
color: 1717523245
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36111,19 +36010,6 @@ cont {
}
cont {
name: "highway-track-area"
element {
scale: 14
lines {
width: 1.1
color: 1717523245
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36248,19 +36134,6 @@ cont {
}
cont {
name: "highway-track-bridge"
element {
scale: 14
lines {
width: 1.1
color: 1717523245
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36445,19 +36318,6 @@ cont {
}
cont {
name: "highway-track-no-access"
element {
scale: 14
lines {
width: 1.1
color: 1717523245
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36582,19 +36442,6 @@ cont {
}
cont {
name: "highway-track-tunnel"
element {
scale: 14
lines {
width: 1.1
color: 1717523245
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -63404,19 +63251,6 @@ cont {
}
cont {
name: "piste:type-hike"
element {
scale: 14
lines {
width: 0.9
color: 2573352319
dashdot {
dd: 3.5
dd: 2.0
}
priority: 120
cap: BUTTCAP
}
}
element {
scale: 15
lines {

Binary file not shown.

View File

@@ -26838,19 +26838,6 @@ cont {
}
cont {
name: "highway-path"
element {
scale: 14
lines {
width: 0.9
color: 1720602141
dashdot {
dd: 3.5
dd: 2.0
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -26983,16 +26970,6 @@ cont {
priority: 219
cap: BUTTCAP
}
lines {
width: 0.9
color: 1720602141
dashdot {
dd: 3.5
dd: 2.7
}
priority: 220
cap: BUTTCAP
}
}
element {
scale: 15
@@ -27149,19 +27126,6 @@ cont {
}
cont {
name: "highway-path-bridge"
element {
scale: 14
lines {
width: 0.9
color: 1720602141
dashdot {
dd: 3.5
dd: 2.0
}
priority: 155
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27346,19 +27310,6 @@ cont {
}
cont {
name: "highway-path-difficult"
element {
scale: 14
lines {
width: 0.9
color: 1720602141
dashdot {
dd: 1.0
dd: 2.0
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27483,19 +27434,6 @@ cont {
}
cont {
name: "highway-path-expert"
element {
scale: 14
lines {
width: 0.9
color: 1715283479
dashdot {
dd: 1.0
dd: 4.0
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27620,19 +27558,6 @@ cont {
}
cont {
name: "highway-path-horse"
element {
scale: 14
lines {
width: 0.9
color: 1720602141
dashdot {
dd: 3.5
dd: 2.0
}
priority: 155
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -27757,19 +27682,6 @@ cont {
}
cont {
name: "highway-path-tunnel"
element {
scale: 14
lines {
width: 0.9
color: 1720602141
dashdot {
dd: 3.5
dd: 2.0
}
priority: 155
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -35770,19 +35682,6 @@ cont {
}
cont {
name: "highway-track"
element {
scale: 14
lines {
width: 1.1
color: 1717786416
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -35907,19 +35806,6 @@ cont {
}
cont {
name: "highway-track-area"
element {
scale: 14
lines {
width: 1.1
color: 1717786416
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36044,19 +35930,6 @@ cont {
}
cont {
name: "highway-track-bridge"
element {
scale: 14
lines {
width: 1.1
color: 1717786416
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36241,19 +36114,6 @@ cont {
}
cont {
name: "highway-track-no-access"
element {
scale: 14
lines {
width: 1.1
color: 1717786416
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -36378,19 +36238,6 @@ cont {
}
cont {
name: "highway-track-tunnel"
element {
scale: 14
lines {
width: 1.1
color: 1717786416
dashdot {
dd: 6.0
dd: 2.5
}
priority: 180
cap: BUTTCAP
}
}
element {
scale: 15
lines {
@@ -63178,19 +63025,6 @@ cont {
}
cont {
name: "piste:type-hike"
element {
scale: 14
lines {
width: 0.9
color: 2579790591
dashdot {
dd: 3.5
dd: 2.0
}
priority: 120
cap: BUTTCAP
}
}
element {
scale: 15
lines {

View File

@@ -714,7 +714,7 @@ line|z17-[highway=steps][bridge?]::bridgeblack,
line|z16-[highway=road][bridge?]::bridgeblack,
line|z16-[highway=service][bridge?]::bridgeblack
{casing-linecap: butt;casing-color:@bridge_casing;casing-opacity: 0.7;}
line|z14-[highway=track],
line|z15-[highway=track],
{color: @track;opacity: 0.6;}
line|z14-[highway=raceway],
{color: @track;opacity: 0.7;}
@@ -728,11 +728,11 @@ line|z16-[highway=footway],
{opacity: 0.95;}
line|z15-[highway=steps],
{color: @residential;opacity: 1;}
line|z14-[highway=path],
line|z15-[highway=path],
{color: @path;opacity: 0.6;}
line|z14-[highway=path][_path_grade=expert],
line|z15-[highway=path][_path_grade=expert],
{color: @path_expert; opacity: 0.6;}
line|z14-[piste:type=hike],
line|z15-[piste:type=hike],
{color: @piste; opacity: 0.4;}
line|z17-[highway=footway][tunnel?]::tunnelBackground,
line|z17-[highway=cycleway][tunnel?]::tunnelBackground,
@@ -916,8 +916,6 @@ line|z19-[highway=raceway],
line|z19-[leisure=track][!area]
{width:4; opacity: 0.8;}
line|z14[highway=track],
{width: 1.1; dashes: 6,2.5;}
line|z15[highway=track],
{width: 1.4; dashes: 6,2.5;}
line|z16[highway=track],
@@ -929,9 +927,6 @@ line|z18[highway=track],
line|z19-[highway=track],
{width: 4.2; dashes: 12,3.5; opacity: 0.8;}
line|z14[highway=path],
line|z14[piste:type=hike],
{width: 0.9; dashes: 3.5,2;}
line|z15[highway=path],
line|z15[piste:type=hike],
{width: 1.1; dashes: 3.5,2;}
@@ -946,8 +941,6 @@ line|z18[highway=path],
line|z19-[highway=path],
{width: 3.7; dashes: 8,4.5; opacity: 0.8;}
line|z14[highway=path][bicycle=designated],
{width: 0.9; dashes: 3.5,2.7;}
line|z15[highway=path][bicycle=designated],
{width: 1.1; dashes: 3.5,2.7;}
line|z16[highway=path][bicycle=designated],
@@ -959,8 +952,6 @@ line|z18[highway=path][bicycle=designated],
line|z19-[highway=path][bicycle=designated],
{width: 3.7; dashes: 8,6.2; opacity: 0.8;}
line|z14[highway=path][_path_grade=difficult],
{width: 0.9; dashes: 1,2;}
line|z15[highway=path][_path_grade=difficult],
{width: 1.1; dashes: 1,2;}
line|z16[highway=path][_path_grade=difficult],
@@ -970,8 +961,6 @@ line|z17[highway=path][_path_grade=difficult],
line|z18-[highway=path][_path_grade=difficult],
{width: 2.8; dashes: 2.8,3.5;}
line|z14[highway=path][_path_grade=expert],
{width: 0.9; dashes: 1,4;}
line|z15[highway=path][_path_grade=expert],
{width: 1.1; dashes: 1,4;}
line|z16[highway=path][_path_grade=expert],

View File

@@ -224,11 +224,11 @@ railway-subway-bridge::dash # line::dash z16- (also has
=== 221
highway-footway-bicycle # line z15- (also has line::cycleline z15-, pathtext z15-)
highway-path-bicycle # line z14- (also has line::cycleline z14-, pathtext z15-)
highway-path-bicycle # line z15- (also has line::cycleline z14-, pathtext z15-)
=== 220
highway-footway-bicycle::cycleline # line::cycleline z15- (also has line z15-, pathtext z15-)
highway-path-bicycle::cycleline # line::cycleline z14- (also has line z14-, pathtext z15-)
highway-path-bicycle::cycleline # line::cycleline z14- (also has line z15-, pathtext z15-)
=== 219
highway-cycleway # line z13- (also has pathtext z15-)
@@ -276,15 +276,15 @@ highway-bridleway-tunnel # line z14- (also has line::
highway-footway # line z15- (also has pathtext z15-)
highway-footway-area # line z15- and area z14- (also has pathtext z15-)
highway-footway-crossing # line z16-
highway-path # line z14- (also has pathtext z15-)
highway-path-difficult # line z14- (also has pathtext z15-)
highway-path-expert # line z14- (also has pathtext z15-)
highway-path # line z15- (also has pathtext z15-)
highway-path-difficult # line z15- (also has pathtext z15-)
highway-path-expert # line z15- (also has pathtext z15-)
highway-raceway # line z14- (also has pathtext z16-)
highway-track # line z14- (also has pathtext z15-)
highway-track-area # line z14- (also has pathtext z15-)
highway-track-bridge # line z14- (also has line::bridgeblack z17-, line::bridgewhite z15-, pathtext z15-)
highway-track-no-access # line z14- (also has pathtext z15-)
highway-track-tunnel # line z14- (also has line::tunnelBackground z17-, line::tunnelCasing z17-, pathtext z15-)
highway-track # line z15- (also has pathtext z15-)
highway-track-area # line z15- (also has pathtext z15-)
highway-track-bridge # line z15- (also has line::bridgeblack z17-, line::bridgewhite z15-, pathtext z15-)
highway-track-no-access # line z15- (also has pathtext z15-)
highway-track-tunnel # line z15- (also has line::tunnelBackground z17-, line::tunnelCasing z17-, pathtext z15-)
=== 180
highway-construction # line z13- (also has pathtext z15-)
@@ -302,25 +302,25 @@ railway-preserved-tunnel # line z15-
highway-footway-bridge # line z15- (also has line::bridgeblack z17-, line::bridgewhite z15-, pathtext z15-)
highway-footway-sidewalk # line z16-
highway-footway-tunnel # line z15- (also has line::tunnelBackground z17-, line::tunnelCasing z17-, pathtext z15-)
highway-path-bridge # line z14- (also has line::bridgeblack z17-, line::bridgewhite z15-, pathtext z15-)
highway-path-horse # line z14- (also has pathtext z15-)
highway-path-tunnel # line z14- (also has line::tunnelBackground z17-, line::tunnelCasing z17-, pathtext z15-)
highway-path-bridge # line z15- (also has line::bridgeblack z17-, line::bridgewhite z15-, pathtext z15-)
highway-path-horse # line z15- (also has pathtext z15-)
highway-path-tunnel # line z15- (also has line::tunnelBackground z17-, line::tunnelCasing z17-, pathtext z15-)
=== 155
highway-bridleway-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z14-, line::tunnelCasing z17-, pathtext z15-)
highway-cycleway-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z13-, line::tunnelCasing z17-, pathtext z15-)
highway-footway-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z15-, line::tunnelCasing z17-, pathtext z15-)
highway-path-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z14-, line::tunnelCasing z17-, pathtext z15-)
highway-path-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z15-, line::tunnelCasing z17-, pathtext z15-)
highway-steps-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z15-, line::tunnelCasing z17-, pathtext z16-)
highway-track-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z14-, line::tunnelCasing z17-, pathtext z15-)
highway-track-tunnel::tunnelBackground # line::tunnelBackground z17- (also has line z15-, line::tunnelCasing z17-, pathtext z15-)
=== 154
highway-bridleway-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z14-, line::tunnelBackground z17-, pathtext z15-)
highway-cycleway-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z13-, line::tunnelBackground z17-, pathtext z15-)
highway-footway-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z15-, line::tunnelBackground z17-, pathtext z15-)
highway-path-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z14-, line::tunnelBackground z17-, pathtext z15-)
highway-path-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z15-, line::tunnelBackground z17-, pathtext z15-)
highway-steps-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z15-, line::tunnelBackground z17-, pathtext z16-)
highway-track-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z14-, line::tunnelBackground z17-, pathtext z15-)
highway-track-tunnel::tunnelCasing # line::tunnelCasing z17- (also has line z15-, line::tunnelBackground z17-, pathtext z15-)
=== 153
highway-bridleway-bridge::bridgewhite # line::bridgewhite z15- (also has line z14-, line::bridgeblack z17-, pathtext z15-)
@@ -328,7 +328,7 @@ highway-cycleway-bridge::bridgewhite # line::bridgewhite z15- (al
highway-footway-bridge::bridgewhite # line::bridgewhite z15- (also has line z15-, line::bridgeblack z17-, pathtext z15-)
highway-motorway-bridge::bridgewhite # line::bridgewhite z13- (also has line z6-, line::bridgeblack z13-, pathtext z10-, shield::shield z10-)
highway-motorway_link-bridge::bridgewhite # line::bridgewhite z14- (also has line z10-, line::bridgeblack z14-, pathtext z10-, shield::shield z10-)
highway-path-bridge::bridgewhite # line::bridgewhite z15- (also has line z14-, line::bridgeblack z17-, pathtext z15-)
highway-path-bridge::bridgewhite # line::bridgewhite z15- (also has line z15-, line::bridgeblack z17-, pathtext z15-)
highway-pedestrian-bridge::bridgewhite # line::bridgewhite z13- (also has line z13-, line::bridgeblack z14-, pathtext z14-)
highway-primary-bridge::bridgewhite # line::bridgewhite z14- (also has line z8-, line::bridgeblack z14-, pathtext z10-, shield::shield z10-)
highway-primary_link-bridge::bridgewhite # line::bridgewhite z14- (also has line z11-, line::bridgeblack z14-, pathtext z11-, shield::shield z11-)
@@ -340,7 +340,7 @@ highway-service-bridge::bridgewhite # line::bridgewhite z16- (al
highway-steps-bridge::bridgewhite # line::bridgewhite z15- (also has line z15-, line::bridgeblack z17-, pathtext z16-)
highway-tertiary-bridge::bridgewhite # line::bridgewhite z14- (also has line z11-, line::bridgeblack z14-, pathtext z12-, shield::shield z13-)
highway-tertiary_link-bridge::bridgewhite # line::bridgewhite z14- (also has line z14-, line::bridgeblack z14-, pathtext z18-)
highway-track-bridge::bridgewhite # line::bridgewhite z15- (also has line z14-, line::bridgeblack z17-, pathtext z15-)
highway-track-bridge::bridgewhite # line::bridgewhite z15- (also has line z15-, line::bridgeblack z17-, pathtext z15-)
highway-trunk-bridge::bridgewhite # line::bridgewhite z13- (also has line z6-, line::bridgeblack z13-, pathtext z10-, shield::shield z10-)
highway-trunk_link-bridge::bridgewhite # line::bridgewhite z14- (also has line z10-, line::bridgeblack z14-, pathtext z10-, shield::shield z10-)
highway-unclassified-bridge::bridgewhite # line::bridgewhite z14- (also has line z11-, line::bridgeblack z14-, pathtext z13-)
@@ -363,7 +363,7 @@ highway-cycleway-bridge::bridgeblack # line::bridgeblack z17- (al
highway-footway-bridge::bridgeblack # line::bridgeblack z17- (also has line z15-, line::bridgewhite z15-, pathtext z15-)
highway-motorway-bridge::bridgeblack # line::bridgeblack z13- (also has line z6-, line::bridgewhite z13-, pathtext z10-, shield::shield z10-)
highway-motorway_link-bridge::bridgeblack # line::bridgeblack z14- (also has line z10-, line::bridgewhite z14-, pathtext z10-, shield::shield z10-)
highway-path-bridge::bridgeblack # line::bridgeblack z17- (also has line z14-, line::bridgewhite z15-, pathtext z15-)
highway-path-bridge::bridgeblack # line::bridgeblack z17- (also has line z15-, line::bridgewhite z15-, pathtext z15-)
highway-pedestrian-bridge::bridgeblack # line::bridgeblack z14- (also has line z13-, line::bridgewhite z13-, pathtext z14-)
highway-primary-bridge::bridgeblack # line::bridgeblack z14- (also has line z8-, line::bridgewhite z14-, pathtext z10-, shield::shield z10-)
highway-primary_link-bridge::bridgeblack # line::bridgeblack z14- (also has line z11-, line::bridgewhite z14-, pathtext z11-, shield::shield z11-)
@@ -375,7 +375,7 @@ highway-service-bridge::bridgeblack # line::bridgeblack z16- (al
highway-steps-bridge::bridgeblack # line::bridgeblack z17- (also has line z15-, line::bridgewhite z15-, pathtext z16-)
highway-tertiary-bridge::bridgeblack # line::bridgeblack z14- (also has line z11-, line::bridgewhite z14-, pathtext z12-, shield::shield z13-)
highway-tertiary_link-bridge::bridgeblack # line::bridgeblack z14- (also has line z14-, line::bridgewhite z14-, pathtext z18-)
highway-track-bridge::bridgeblack # line::bridgeblack z17- (also has line z14-, line::bridgewhite z15-, pathtext z15-)
highway-track-bridge::bridgeblack # line::bridgeblack z17- (also has line z15-, line::bridgewhite z15-, pathtext z15-)
highway-trunk-bridge::bridgeblack # line::bridgeblack z13- (also has line z6-, line::bridgewhite z13-, pathtext z10-, shield::shield z10-)
highway-trunk_link-bridge::bridgeblack # line::bridgeblack z14- (also has line z10-, line::bridgewhite z14-, pathtext z10-, shield::shield z10-)
highway-unclassified-bridge::bridgeblack # line::bridgeblack z14- (also has line z11-, line::bridgewhite z14-, pathtext z13-)
@@ -407,7 +407,7 @@ piste:type-downhill-expert # line z14- (also has pathte
piste:type-downhill-freeride # line z14- (also has pathtext z15-)
piste:type-downhill-intermediate # line z14- (also has pathtext z15-)
piste:type-downhill-novice # line z14- (also has pathtext z15-)
piste:type-hike # line z14- (also has pathtext z15-)
piste:type-hike # line z15- (also has pathtext z15-)
piste:type-nordic # line z14- (also has pathtext z15-)
piste:type-skitour # line z14- (also has pathtext z15-)
piste:type-sled # line z14- (also has pathtext z15-)

View File

@@ -705,13 +705,13 @@ natural-rock # icon z17- (also has captio
highway-bridleway # pathtext z15- (also has line z14-)
highway-bridleway-bridge # pathtext z15- (also has line z14-, line::bridgeblack z17-, line::bridgewhite z15-)
highway-bridleway-tunnel # pathtext z15- (also has line z14-, line::tunnelBackground z17-, line::tunnelCasing z17-)
highway-path # pathtext z15- (also has line z14-)
highway-path-bicycle # pathtext z15- (also has line z14-, line::cycleline z14-)
highway-path-bridge # pathtext z15- (also has line z14-, line::bridgeblack z17-, line::bridgewhite z15-)
highway-path-difficult # pathtext z15- (also has line z14-)
highway-path-expert # pathtext z15- (also has line z14-)
highway-path-horse # pathtext z15- (also has line z14-)
highway-path-tunnel # pathtext z15- (also has line z14-, line::tunnelBackground z17-, line::tunnelCasing z17-)
highway-path # pathtext z15- (also has line z15-)
highway-path-bicycle # pathtext z15- (also has line z15-, line::cycleline z14-)
highway-path-bridge # pathtext z15- (also has line z15-, line::bridgeblack z17-, line::bridgewhite z15-)
highway-path-difficult # pathtext z15- (also has line z15-)
highway-path-expert # pathtext z15- (also has line z15-)
highway-path-horse # pathtext z15- (also has line z15-)
highway-path-tunnel # pathtext z15- (also has line z15-, line::tunnelBackground z17-, line::tunnelCasing z17-)
=== 2820
highway-steps # pathtext z16- (also has line z15-)
@@ -719,11 +719,11 @@ highway-steps-bridge # pathtext z16- (also has li
highway-steps-tunnel # pathtext z16- (also has line z15-, line::tunnelBackground z17-, line::tunnelCasing z17-)
=== 2810
highway-track # pathtext z15- (also has line z14-)
highway-track-area # pathtext z15- (also has line z14-)
highway-track-bridge # pathtext z15- (also has line z14-, line::bridgeblack z17-, line::bridgewhite z15-)
highway-track-no-access # pathtext z15- (also has line z14-)
highway-track-tunnel # pathtext z15- (also has line z14-, line::tunnelBackground z17-, line::tunnelCasing z17-)
highway-track # pathtext z15- (also has line z15-)
highway-track-area # pathtext z15- (also has line z15-)
highway-track-bridge # pathtext z15- (also has line z15-, line::bridgeblack z17-, line::bridgewhite z15-)
highway-track-no-access # pathtext z15- (also has line z15-)
highway-track-tunnel # pathtext z15- (also has line z15-, line::tunnelBackground z17-, line::tunnelCasing z17-)
=== 2780
highway-service # pathtext z16- (also has line z15-)
@@ -739,7 +739,7 @@ piste:type-downhill-expert # pathtext z15- (also has li
piste:type-downhill-freeride # pathtext z15- (also has line z14-)
piste:type-downhill-intermediate # pathtext z15- (also has line z14-)
piste:type-downhill-novice # pathtext z15- (also has line z14-)
piste:type-hike # pathtext z15- (also has line z14-)
piste:type-hike # pathtext z15- (also has line z15-)
piste:type-nordic # pathtext z15- (also has line z14-)
piste:type-skitour # pathtext z15- (also has line z14-)
piste:type-sled # pathtext z15- (also has line z14-)