mirror of
https://codeberg.org/comaps/comaps
synced 2026-01-22 02:53:59 +00:00
Compare commits
2 Commits
jb_server_
...
zy-mwm-int
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b4e0fa8cc | ||
|
|
e56e54f994 |
@@ -346,6 +346,60 @@
|
||||
<data android:mimeType="text/xml" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Custom MWM map files -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<!-- See http://stackoverflow.com/questions/3400072/pathpattern-to-match-file-extension-does-not-work-if-a-period-exists-elsewhere-i -->
|
||||
<data android:pathPattern="/.*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Duplicate without mimeType for MWM files -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern="/.*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.mwm" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.MWM" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
@@ -331,6 +331,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
||||
}
|
||||
|
||||
final IntentProcessor[] mIntentProcessors = {
|
||||
new Factory.MwmFileProcessor(),
|
||||
new Factory.UrlProcessor(),
|
||||
new Factory.KmzKmlProcessor(),
|
||||
};
|
||||
|
||||
@@ -473,7 +473,16 @@ class DownloaderAdapter extends RecyclerView.Adapter<DownloaderAdapter.ViewHolde
|
||||
{
|
||||
mName.setText(mItem.name);
|
||||
if (!mItem.isExpandable())
|
||||
UiUtils.setTextAndHideIfEmpty(mSubtitle, mItem.description);
|
||||
{
|
||||
// Show version info for downloaded maps (in "My Maps" mode)
|
||||
String subtitle = mItem.description;
|
||||
if (mMyMapsMode && mItem.present && mItem.localVersion > 0)
|
||||
{
|
||||
String versionStr = formatVersion(mItem.localVersion);
|
||||
subtitle = TextUtils.isEmpty(subtitle) ? "v" + versionStr : subtitle + " • v" + versionStr;
|
||||
}
|
||||
UiUtils.setTextAndHideIfEmpty(mSubtitle, subtitle);
|
||||
}
|
||||
}
|
||||
|
||||
if (mItem.isExpandable())
|
||||
@@ -509,6 +518,21 @@ class DownloaderAdapter extends RecyclerView.Adapter<DownloaderAdapter.ViewHolde
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a version number (YYMMDD) into a readable date string (YY.MM.DD).
|
||||
*/
|
||||
private String formatVersion(long version)
|
||||
{
|
||||
if (version <= 0)
|
||||
return "";
|
||||
String v = String.valueOf(version);
|
||||
// Pad with leading zeros if needed
|
||||
while (v.length() < 6)
|
||||
v = "0" + v;
|
||||
// Format as YY.MM.DD
|
||||
return v.substring(0, 2) + "." + v.substring(2, 4) + "." + v.substring(4, 6);
|
||||
}
|
||||
}
|
||||
|
||||
static class HeaderViewHolder extends BaseInnerViewHolder<String>
|
||||
|
||||
@@ -23,10 +23,12 @@ import app.organicmaps.sdk.routing.RoutingController;
|
||||
import app.organicmaps.sdk.search.SearchEngine;
|
||||
import app.organicmaps.sdk.util.StorageUtils;
|
||||
import app.organicmaps.sdk.util.concurrency.ThreadPool;
|
||||
import app.organicmaps.sdk.downloader.CustomMwmManager;
|
||||
import app.organicmaps.search.SearchActivity;
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Factory
|
||||
{
|
||||
@@ -65,6 +67,92 @@ public class Factory
|
||||
}
|
||||
}
|
||||
|
||||
public static class MwmFileProcessor implements IntentProcessor
|
||||
{
|
||||
private static final String MWM_EXTENSION = ".mwm";
|
||||
|
||||
@Override
|
||||
public boolean process(@NonNull Intent intent, @NonNull MwmActivity activity)
|
||||
{
|
||||
if (!Intent.ACTION_VIEW.equals(intent.getAction()))
|
||||
return false;
|
||||
|
||||
final Uri uri = intent.getData();
|
||||
if (uri == null)
|
||||
return false;
|
||||
|
||||
// Check if this is an MWM file
|
||||
if (!isMwmFile(activity, uri))
|
||||
return false;
|
||||
|
||||
// Import the MWM file on a background thread
|
||||
ThreadPool.getStorage().execute(() -> {
|
||||
CustomMwmManager.ImportResult result = CustomMwmManager.importMwmFile(activity, uri);
|
||||
|
||||
// Show result on UI thread
|
||||
activity.runOnUiThread(() -> {
|
||||
switch (result)
|
||||
{
|
||||
case SUCCESS:
|
||||
android.widget.Toast.makeText(activity,
|
||||
activity.getString(app.organicmaps.R.string.custom_mwm_import_success),
|
||||
android.widget.Toast.LENGTH_LONG).show();
|
||||
// Reload maps to include the new custom map
|
||||
Framework.nativeReloadWorldMaps();
|
||||
break;
|
||||
case ERROR_INVALID_FILE:
|
||||
android.widget.Toast.makeText(activity,
|
||||
activity.getString(app.organicmaps.R.string.custom_mwm_import_invalid),
|
||||
android.widget.Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case ERROR_IO:
|
||||
case ERROR_STORAGE:
|
||||
android.widget.Toast.makeText(activity,
|
||||
activity.getString(app.organicmaps.R.string.custom_mwm_import_error),
|
||||
android.widget.Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isMwmFile(@NonNull MwmActivity activity, @NonNull Uri uri)
|
||||
{
|
||||
String fileName = null;
|
||||
|
||||
// Try to get filename from content resolver
|
||||
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()))
|
||||
{
|
||||
try (android.database.Cursor cursor = activity.getContentResolver().query(
|
||||
uri, new String[]{android.provider.OpenableColumns.DISPLAY_NAME}, null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst())
|
||||
{
|
||||
int nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex >= 0)
|
||||
fileName = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Fallback to URI path
|
||||
if (fileName == null)
|
||||
{
|
||||
String path = uri.getPath();
|
||||
if (path != null)
|
||||
{
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
fileName = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
||||
}
|
||||
}
|
||||
|
||||
return fileName != null && fileName.toLowerCase(Locale.US).endsWith(MWM_EXTENSION);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UrlProcessor implements IntentProcessor
|
||||
{
|
||||
private static final int SEARCH_IN_VIEWPORT_ZOOM = 16;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:background="@color/bg_editor"
|
||||
android:background="?colorSurfaceContainerLow"
|
||||
tools:context=".editor.EditorActivity">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/frameLayout"
|
||||
android:background="@color/bg_editor"
|
||||
android:background="?colorSurfaceContainerLow"
|
||||
android:layout_marginBottom="@dimen/margin_quarter">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
style="@style/MwmWidget.FrameLayout.Elevation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_editor"
|
||||
android:background="?colorSurfaceContainerLow"
|
||||
android:layout_above="@+id/tv__mode_switch"
|
||||
android:layout_below="@id/toolbar"/>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/tv__mode_switch"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@color/bg_editor"/>
|
||||
android:background="?colorSurfaceContainerLow"/>
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tv__mode_switch"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_editor">
|
||||
android:background="?colorSurfaceContainerLow">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_editor"
|
||||
android:background="?colorSurfaceContainerLow"
|
||||
android:paddingStart="@dimen/margin_half"
|
||||
android:paddingEnd="@dimen/margin_half"
|
||||
android:scrollbars="vertical"/>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingEnd="@dimen/margin_base"
|
||||
android:paddingStart="@dimen/margin_base"
|
||||
android:background="@color/fg_editor"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
@@ -25,7 +24,6 @@
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/fg_editor"
|
||||
android:scrollbars="vertical"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
<color name="bg_panel">@color/bg_window</color>
|
||||
<color name="bg_primary_dark">#FF588157</color>
|
||||
<color name="bg_app">#10140F</color>
|
||||
<color name="bg_editor">#161b14</color>
|
||||
<color name="fg_editor">#282e25</color>
|
||||
|
||||
<color name="bg_menu">#CC2D3237</color>
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
<color name="bg_panel">@color/bg_window</color>
|
||||
<color name="bg_primary_dark">#37653F</color> <!-- secondary dark -->
|
||||
<color name="bg_app">@android:color/white</color>
|
||||
<color name="bg_editor">#ebefe4</color>
|
||||
<color name="fg_editor">#f9faf2</color>
|
||||
|
||||
<color name="bg_dialog_translucent">#BB000000</color>
|
||||
<color name="bg_text_translucent">#99FFFFFF</color>
|
||||
|
||||
@@ -975,4 +975,8 @@
|
||||
<string name="download_resources_custom_url_message">Override the default map download server used for map downloads. Leave empty to use CoMaps default server.</string>
|
||||
<string name="download_resources_custom_url_summary_none">Not set</string>
|
||||
<string name="download_resources_custom_url_error_scheme">Please enter a URL starting with http:// or https://</string>
|
||||
<!-- Custom MWM file import messages -->
|
||||
<string name="custom_mwm_import_success">Custom map imported successfully. Restart the app to load it.</string>
|
||||
<string name="custom_mwm_import_invalid">Invalid MWM file. Please select a valid map file.</string>
|
||||
<string name="custom_mwm_import_error">Failed to import the map file. Please try again.</string>
|
||||
</resources>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginBottom">@dimen/margin_half</item>
|
||||
<item name="cardBackgroundColor">@color/fg_editor</item>
|
||||
<item name="cardBackgroundColor">?appBackground</item>
|
||||
<item name="android:padding">@dimen/margin_base</item>
|
||||
<item name="cardPreventCornerOverlap">false</item>
|
||||
</style>
|
||||
|
||||
@@ -67,8 +67,8 @@ struct CountryItemBuilder
|
||||
jclass m_class;
|
||||
jmethodID m_ctor;
|
||||
jfieldID m_Id, m_Name, m_DirectParentId, m_TopmostParentId, m_DirectParentName, m_TopmostParentName, m_Description,
|
||||
m_Size, m_EnqueuedSize, m_TotalSize, m_ChildCount, m_TotalChildCount, m_Present, m_Progress, m_DownloadedBytes,
|
||||
m_BytesToDownload, m_Category, m_Status, m_ErrorCode;
|
||||
m_Size, m_EnqueuedSize, m_TotalSize, m_LocalVersion, m_ChildCount, m_TotalChildCount, m_Present, m_Progress,
|
||||
m_DownloadedBytes, m_BytesToDownload, m_Category, m_Status, m_ErrorCode;
|
||||
|
||||
CountryItemBuilder(JNIEnv * env)
|
||||
{
|
||||
@@ -85,6 +85,7 @@ struct CountryItemBuilder
|
||||
m_Size = env->GetFieldID(m_class, "size", "J");
|
||||
m_EnqueuedSize = env->GetFieldID(m_class, "enqueuedSize", "J");
|
||||
m_TotalSize = env->GetFieldID(m_class, "totalSize", "J");
|
||||
m_LocalVersion = env->GetFieldID(m_class, "localVersion", "J");
|
||||
m_ChildCount = env->GetFieldID(m_class, "childCount", "I");
|
||||
m_TotalChildCount = env->GetFieldID(m_class, "totalChildCount", "I");
|
||||
m_Present = env->GetFieldID(m_class, "present", "Z");
|
||||
@@ -221,6 +222,9 @@ static void UpdateItem(JNIEnv * env, jobject item, storage::NodeAttrs const & at
|
||||
env->SetLongField(item, ciBuilder.m_EnqueuedSize, attrs.m_downloadingMwmSize);
|
||||
env->SetLongField(item, ciBuilder.m_TotalSize, attrs.m_mwmSize);
|
||||
|
||||
// Local version (YYMMDD format)
|
||||
env->SetLongField(item, ciBuilder.m_LocalVersion, attrs.m_localMwmVersion);
|
||||
|
||||
// Child counts
|
||||
env->SetIntField(item, ciBuilder.m_ChildCount, attrs.m_downloadingMwmCounter);
|
||||
env->SetIntField(item, ciBuilder.m_TotalChildCount, attrs.m_mwmCounter);
|
||||
|
||||
@@ -53,6 +53,9 @@ public final class CountryItem implements Comparable<CountryItem>
|
||||
public long enqueuedSize;
|
||||
public long totalSize;
|
||||
|
||||
// Local MWM file version (YYMMDD format, e.g. 251231). 0 if not downloaded.
|
||||
public long localVersion;
|
||||
|
||||
public int childCount;
|
||||
public int totalChildCount;
|
||||
|
||||
@@ -155,7 +158,7 @@ public final class CountryItem implements Comparable<CountryItem>
|
||||
+ "\", category: \"" + category + "\", name: \"" + name + "\", directParentName: \"" + directParentName
|
||||
+ "\", topmostParentName: \"" + topmostParentName + "\", present: " + present + ", status: " + status
|
||||
+ ", errorCode: " + errorCode + ", headerId: " + headerId + ", size: " + size + ", enqueuedSize: " + enqueuedSize
|
||||
+ ", totalSize: " + totalSize + ", childCount: " + childCount + ", totalChildCount: " + totalChildCount
|
||||
+ ", progress: " + StringUtils.formatUsingUsLocale("%.2f", progress) + "% }";
|
||||
+ ", totalSize: " + totalSize + ", localVersion: " + localVersion + ", childCount: " + childCount
|
||||
+ ", totalChildCount: " + totalChildCount + ", progress: " + StringUtils.formatUsingUsLocale("%.2f", progress) + "% }";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
package app.organicmaps.sdk.downloader;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.organicmaps.sdk.Framework;
|
||||
import app.organicmaps.sdk.util.StorageUtils;
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages custom MWM map files that are manually imported by the user.
|
||||
* Custom maps are stored in dated folders (YYMMDD format) and take precedence
|
||||
* over downloaded maps when loading.
|
||||
*/
|
||||
public class CustomMwmManager
|
||||
{
|
||||
private static final String TAG = CustomMwmManager.class.getSimpleName();
|
||||
private static final String CUSTOM_MAPS_DIR = "custom_maps";
|
||||
private static final String MWM_EXTENSION = ".mwm";
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd", Locale.US);
|
||||
|
||||
public enum ImportResult
|
||||
{
|
||||
SUCCESS,
|
||||
ERROR_INVALID_FILE,
|
||||
ERROR_IO,
|
||||
ERROR_STORAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom MWM file with its metadata.
|
||||
*/
|
||||
public static class CustomMwmFile
|
||||
{
|
||||
public final String name; // e.g., "Portland"
|
||||
public final String path; // Full path to the file
|
||||
public final long version; // Date version as YYMMDD number
|
||||
public final long fileSize;
|
||||
|
||||
public CustomMwmFile(String name, String path, long version, long fileSize)
|
||||
{
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
this.version = version;
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom maps root directory path.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getCustomMapsDir(@NonNull Context context)
|
||||
{
|
||||
String writableDir = Framework.nativeGetWritableDir();
|
||||
return StorageUtils.addTrailingSeparator(writableDir) + CUSTOM_MAPS_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates the custom maps directory for today's date.
|
||||
* @return The directory path, or null if creation failed.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getTodayCustomMapsDir(@NonNull Context context)
|
||||
{
|
||||
String customMapsDir = getCustomMapsDir(context);
|
||||
String today = DATE_FORMAT.format(new Date());
|
||||
String todayDir = StorageUtils.addTrailingSeparator(customMapsDir) + today;
|
||||
|
||||
File dir = new File(todayDir);
|
||||
if (!dir.exists() && !dir.mkdirs())
|
||||
{
|
||||
Logger.e(TAG, "Failed to create custom maps directory: " + todayDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
return todayDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports an MWM file from a content URI into the custom maps directory.
|
||||
* The file is saved in a dated folder based on today's date.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param uri Content URI of the MWM file
|
||||
* @return ImportResult indicating success or the type of error
|
||||
*/
|
||||
@NonNull
|
||||
public static ImportResult importMwmFile(@NonNull Context context, @NonNull Uri uri)
|
||||
{
|
||||
String fileName = getFileNameFromUri(context, uri);
|
||||
if (fileName == null || !fileName.toLowerCase(Locale.US).endsWith(MWM_EXTENSION))
|
||||
{
|
||||
Logger.e(TAG, "Invalid file name or not an MWM file: " + fileName);
|
||||
return ImportResult.ERROR_INVALID_FILE;
|
||||
}
|
||||
|
||||
String destDir = getTodayCustomMapsDir(context);
|
||||
if (destDir == null)
|
||||
{
|
||||
return ImportResult.ERROR_STORAGE;
|
||||
}
|
||||
|
||||
File destFile = new File(destDir, fileName);
|
||||
Logger.i(TAG, "Importing MWM file to: " + destFile.getAbsolutePath());
|
||||
|
||||
try
|
||||
{
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
if (!StorageUtils.copyFile(resolver, uri, destFile))
|
||||
{
|
||||
Logger.e(TAG, "Failed to copy MWM file");
|
||||
return ImportResult.ERROR_IO;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Successfully imported MWM file: " + fileName);
|
||||
return ImportResult.SUCCESS;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "IOException while importing MWM file", e);
|
||||
return ImportResult.ERROR_IO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file name from a content URI.
|
||||
*/
|
||||
@Nullable
|
||||
private static String getFileNameFromUri(@NonNull Context context, @NonNull Uri uri)
|
||||
{
|
||||
String fileName = null;
|
||||
|
||||
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()))
|
||||
{
|
||||
try (android.database.Cursor cursor = context.getContentResolver().query(
|
||||
uri, new String[]{android.provider.OpenableColumns.DISPLAY_NAME}, null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst())
|
||||
{
|
||||
int nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex >= 0)
|
||||
{
|
||||
fileName = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to get file name from URI", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName == null)
|
||||
{
|
||||
// Try to get from path
|
||||
String path = uri.getPath();
|
||||
if (path != null)
|
||||
{
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
fileName = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
||||
}
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all custom MWM files, grouped by map name with the newest version taking precedence.
|
||||
* @return Map from country name to CustomMwmFile (newest version)
|
||||
*/
|
||||
@NonNull
|
||||
public static Map<String, CustomMwmFile> getCustomMwmFiles(@NonNull Context context)
|
||||
{
|
||||
Map<String, CustomMwmFile> result = new HashMap<>();
|
||||
String customMapsDir = getCustomMapsDir(context);
|
||||
File rootDir = new File(customMapsDir);
|
||||
|
||||
if (!rootDir.exists() || !rootDir.isDirectory())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
File[] versionDirs = rootDir.listFiles(File::isDirectory);
|
||||
if (versionDirs == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Sort by version descending (newest first)
|
||||
Arrays.sort(versionDirs, (a, b) -> {
|
||||
long versionA = parseVersion(a.getName());
|
||||
long versionB = parseVersion(b.getName());
|
||||
return Long.compare(versionB, versionA);
|
||||
});
|
||||
|
||||
for (File versionDir : versionDirs)
|
||||
{
|
||||
long version = parseVersion(versionDir.getName());
|
||||
if (version <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
File[] mwmFiles = versionDir.listFiles((dir, name) ->
|
||||
name.toLowerCase(Locale.US).endsWith(MWM_EXTENSION));
|
||||
|
||||
if (mwmFiles == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (File mwmFile : mwmFiles)
|
||||
{
|
||||
String name = mwmFile.getName();
|
||||
// Remove .mwm extension to get country name
|
||||
String countryName = name.substring(0, name.length() - MWM_EXTENSION.length());
|
||||
|
||||
// Only add if we don't already have a newer version
|
||||
if (!result.containsKey(countryName))
|
||||
{
|
||||
result.put(countryName, new CustomMwmFile(
|
||||
countryName,
|
||||
mwmFile.getAbsolutePath(),
|
||||
version,
|
||||
mwmFile.length()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all version directories in the custom maps folder, sorted by version descending.
|
||||
* @return List of version directory paths
|
||||
*/
|
||||
@NonNull
|
||||
public static List<String> getCustomMapVersionDirs(@NonNull Context context)
|
||||
{
|
||||
List<String> result = new ArrayList<>();
|
||||
String customMapsDir = getCustomMapsDir(context);
|
||||
File rootDir = new File(customMapsDir);
|
||||
|
||||
if (!rootDir.exists() || !rootDir.isDirectory())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
File[] versionDirs = rootDir.listFiles(File::isDirectory);
|
||||
if (versionDirs == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Sort by version descending (newest first)
|
||||
Arrays.sort(versionDirs, (a, b) -> {
|
||||
long versionA = parseVersion(a.getName());
|
||||
long versionB = parseVersion(b.getName());
|
||||
return Long.compare(versionB, versionA);
|
||||
});
|
||||
|
||||
for (File versionDir : versionDirs)
|
||||
{
|
||||
if (parseVersion(versionDir.getName()) > 0)
|
||||
{
|
||||
result.add(StorageUtils.addTrailingSeparator(versionDir.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a custom version of a map exists.
|
||||
* @param countryName The country/map name (without .mwm extension)
|
||||
* @return The CustomMwmFile if found, null otherwise
|
||||
*/
|
||||
@Nullable
|
||||
public static CustomMwmFile getCustomMwmFile(@NonNull Context context, @NonNull String countryName)
|
||||
{
|
||||
Map<String, CustomMwmFile> customFiles = getCustomMwmFiles(context);
|
||||
return customFiles.get(countryName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a custom MWM file.
|
||||
* @param file The CustomMwmFile to delete
|
||||
* @return true if deletion was successful
|
||||
*/
|
||||
public static boolean deleteCustomMwmFile(@NonNull CustomMwmFile file)
|
||||
{
|
||||
File f = new File(file.path);
|
||||
boolean deleted = f.delete();
|
||||
|
||||
if (deleted)
|
||||
{
|
||||
Logger.i(TAG, "Deleted custom MWM file: " + file.path);
|
||||
|
||||
// Clean up empty version directories
|
||||
File parentDir = f.getParentFile();
|
||||
if (parentDir != null)
|
||||
{
|
||||
String[] remaining = parentDir.list();
|
||||
if (remaining != null && remaining.length == 0)
|
||||
{
|
||||
if (parentDir.delete())
|
||||
{
|
||||
Logger.i(TAG, "Deleted empty version directory: " + parentDir.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.e(TAG, "Failed to delete custom MWM file: " + file.path);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version string (YYMMDD format) to a long.
|
||||
* @return The version number, or 0 if parsing failed
|
||||
*/
|
||||
private static long parseVersion(String versionStr)
|
||||
{
|
||||
if (versionStr == null || versionStr.length() != 6)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Long.parseLong(versionStr);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# Deploy your maps files server
|
||||
|
||||
This doc explain how to deploy your own instance of a CoMaps server with files from official CDNs (We are working to be able to download maps files without hardcoded countries.txt file embedded in the app)
|
||||
We explain how to deploy with minimal config, but each tools have differents options to change server port or choose maps files that you want to download.
|
||||
|
||||
## Deploy the server
|
||||
Our community has developped different tools to deploy easily an instance of a CoMaps server:
|
||||
- [comaps-map-distributor](https://codeberg.org/gedankenstuecke/comaps-map-distributor)
|
||||
- [comaps-server](https://github.com/myanesp/comaps-server)
|
||||
|
||||
### Deploy comaps-map-distributor
|
||||
|
||||
Prerequisites
|
||||
- python3 and pip
|
||||
|
||||
- Launch your terminal
|
||||
- Run `pip install comaps-map-distributor`
|
||||
- Launch the tool with this command `comaps-map-distributor download-maps`
|
||||
- Choose maps files you want to download from official CDNs
|
||||
- Run `comaps-map-distributor serve-maps`
|
||||
- Go to your mobile device -> CoMaps -> settings -> Advanced -> Custom Maps server
|
||||
- Edit URL with your URL server and enjoy
|
||||
|
||||
|
||||
### Deploy comaps-server
|
||||
|
||||
Prerequisites
|
||||
- Docker
|
||||
- Your server is accessible from your network
|
||||
|
||||
|
||||
#### Docker
|
||||
|
||||
- Launch your terminal
|
||||
- Run ``` docker run -d \
|
||||
--name comaps-server \
|
||||
--restart unless-stopped \
|
||||
-e MAPS=all \
|
||||
-e OUTPUT_DIR=/maps \
|
||||
-p "80:80" \
|
||||
ghcr.io/myanesp/comaps-server:latest```
|
||||
- Go to your mobile device -> CoMaps -> settings -> Advanced -> Custom Maps server
|
||||
- Edit URL with your URL server and enjoy
|
||||
|
||||
#### Docker compose
|
||||
- Launch your terminal
|
||||
- Create a `compose.yml` file with this config:
|
||||
|
||||
```services:
|
||||
maps-server:
|
||||
image: ghcr.io/myanesp/comaps-server
|
||||
container_name: comaps-server
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
- MAPS=World,WorldCoasts,Spain
|
||||
- OUTPUT_DIR=/maps
|
||||
volumes:
|
||||
- ./maps:/maps
|
||||
- TZ=Europe/Madrid```
|
||||
|
||||
- Execute `docker compose up`
|
||||
- Go to your mobile device -> CoMaps -> settings -> Advanced -> Custom Maps server
|
||||
- Edit URL with your URL server and enjoy
|
||||
|
||||
You can find more details in the [FAQ articles](https://www.comaps.app/support/how-can-i-host-a-custom-map-server-for-downloads/) to deploy your own HTTP maps server and find more details [here](https://www.comaps.app/support/how-can-i-set-a-custom-map-server-for-downloads/) about restrictions.
|
||||
@@ -33,6 +33,7 @@ namespace
|
||||
char constexpr kBitsExt[] = ".bftsegbits";
|
||||
char constexpr kNodesExt[] = ".bftsegnodes";
|
||||
char constexpr kOffsetsExt[] = ".offsets";
|
||||
char constexpr kCustomMapsDir[] = "custom_maps";
|
||||
|
||||
string GetAdditionalWorldScope()
|
||||
{
|
||||
@@ -205,6 +206,44 @@ void FindAllDiffs(std::string const & dataDir, std::vector<LocalCountryFile> & d
|
||||
FindAllDiffsInDirectory(base::JoinPath(dir, fwt.first /* subdir */), diffs);
|
||||
}
|
||||
|
||||
void FindAllCustomMaps(string const & dataDir, std::vector<LocalCountryFile> & localFiles)
|
||||
{
|
||||
string const customMapsPath = base::JoinPath(GetDataDirFullPath(dataDir), kCustomMapsDir);
|
||||
|
||||
if (!Platform::IsFileExistsByFullPath(customMapsPath) || !Platform::IsDirectory(customMapsPath))
|
||||
return;
|
||||
|
||||
Platform::TFilesWithType versionDirs;
|
||||
Platform::GetFilesByType(customMapsPath, Platform::EFileType::Directory, versionDirs);
|
||||
|
||||
for (auto const & versionDir : versionDirs)
|
||||
{
|
||||
string const & subdir = versionDir.first;
|
||||
int64_t version;
|
||||
// Custom maps use YYMMDD format for version directories
|
||||
if (!ParseVersion(subdir, version))
|
||||
continue;
|
||||
|
||||
string const versionPath = base::JoinPath(customMapsPath, subdir);
|
||||
|
||||
Platform::TFilesWithType files;
|
||||
Platform::GetFilesByType(versionPath, Platform::EFileType::Regular, files);
|
||||
|
||||
for (auto const & file : files)
|
||||
{
|
||||
string name = file.first;
|
||||
if (!name.ends_with(DATA_FILE_EXTENSION))
|
||||
continue;
|
||||
|
||||
// Remove DATA_FILE_EXTENSION and use base name as a country file name.
|
||||
base::GetNameWithoutExt(name);
|
||||
localFiles.emplace_back(versionPath, CountryFile(std::move(name)), version);
|
||||
}
|
||||
}
|
||||
|
||||
LOG(LINFO, ("Found", localFiles.size(), "custom maps in", customMapsPath));
|
||||
}
|
||||
|
||||
void FindAllLocalMapsAndCleanup(int64_t latestVersion, std::vector<LocalCountryFile> & localFiles)
|
||||
{
|
||||
FindAllLocalMapsAndCleanup(latestVersion, string(), localFiles);
|
||||
@@ -223,6 +262,10 @@ void FindAllLocalMapsAndCleanup(int64_t latestVersion, string const & dataDir,
|
||||
for (auto const & fwt : fwts)
|
||||
{
|
||||
string const & subdir = fwt.first;
|
||||
// Skip the custom_maps directory - it's processed separately
|
||||
if (subdir == kCustomMapsDir)
|
||||
continue;
|
||||
|
||||
int64_t version;
|
||||
if (!ParseVersion(subdir, version) || version > latestVersion)
|
||||
continue;
|
||||
@@ -236,6 +279,11 @@ void FindAllLocalMapsAndCleanup(int64_t latestVersion, string const & dataDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Find custom maps in the custom_maps directory.
|
||||
// Custom maps have higher priority and use future date versions (YYMMDD format).
|
||||
// The storage will prefer the newest version when multiple versions exist.
|
||||
FindAllCustomMaps(dataDir, localFiles);
|
||||
|
||||
// Check for World and WorldCoasts in app bundle or in resources.
|
||||
Platform & platform = GetPlatform();
|
||||
string const world(WORLD_FILE_NAME);
|
||||
|
||||
@@ -46,6 +46,10 @@ void FindAllLocalMapsAndCleanup(int64_t latestVersion, std::string const & dataD
|
||||
|
||||
void FindAllDiffs(std::string const & dataDir, std::vector<LocalCountryFile> & diffs);
|
||||
|
||||
// Finds custom MWM files in the custom_maps/YYMMDD/ directories.
|
||||
// Custom maps override downloaded maps when they have a newer version date.
|
||||
void FindAllCustomMaps(std::string const & dataDir, std::vector<LocalCountryFile> & localFiles);
|
||||
|
||||
// This method removes:
|
||||
// * partially downloaded non-latest maps (with version less than |latestVersion|)
|
||||
// * empty directories
|
||||
|
||||
@@ -1666,6 +1666,9 @@ void Storage::GetNodeAttrs(CountryId const & countryId, NodeAttrs & nodeAttrs) c
|
||||
|
||||
nodeAttrs.m_localMwmCounter += 1;
|
||||
nodeAttrs.m_localMwmSize += localFile->GetSize(MapFileType::Map);
|
||||
// For leaf nodes, store the local file version
|
||||
if (d.ChildrenCount() == 0)
|
||||
nodeAttrs.m_localMwmVersion = localFile->GetVersion();
|
||||
});
|
||||
nodeAttrs.m_present = m_localFiles.find(countryId) != m_localFiles.end();
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ struct NodeAttrs
|
||||
, m_mwmSize(0)
|
||||
, m_localMwmSize(0)
|
||||
, m_downloadingMwmSize(0)
|
||||
, m_localMwmVersion(0)
|
||||
, m_status(NodeStatus::Undefined)
|
||||
, m_error(NodeErrorCode::NoError)
|
||||
, m_present(false)
|
||||
@@ -88,6 +89,10 @@ struct NodeAttrs
|
||||
/// \note The size of leaves is the size is written in countries.txt.
|
||||
MwmSize m_downloadingMwmSize;
|
||||
|
||||
/// Version of the local mwm file (YYMMDD format, e.g. 251231).
|
||||
/// 0 if not downloaded.
|
||||
int64_t m_localMwmVersion;
|
||||
|
||||
/// The name of the node in a local language. That means the language dependent on
|
||||
/// a device locale.
|
||||
std::string m_nodeLocalName;
|
||||
|
||||
Reference in New Issue
Block a user