diff --git a/include/mbgl/storage/database_file_source.hpp b/include/mbgl/storage/database_file_source.hpp index 3c3bcbeea3e0..2e7b33f82d80 100644 --- a/include/mbgl/storage/database_file_source.hpp +++ b/include/mbgl/storage/database_file_source.hpp @@ -103,18 +103,13 @@ class DatabaseFileSource : public FileSource { virtual void clearAmbientCache(std::function); /** - * Sets the maximum size in bytes for the ambient cache. + * Sets the maximum size (number of resources) for the ambient cache. * * This call is potentially expensive because it will try * to trim the data in case the database is larger than the * size defined. The size of offline regions are not affected * by this settings, but the ambient cache will always try - * to not exceed the maximum size defined, taking into account - * the current size for the offline regions. - * - * If the maximum size is set to 50 MB and 40 MB are already - * used by offline regions, the cache size will be effectively - * 10 MB. + * to not exceed the maximum size defined. * * Setting the size to 0 will disable the cache if there is no * offline region on the database. @@ -202,15 +197,28 @@ class DatabaseFileSource : public FileSource { * Only resources and tiles that belong to a region will be copied over. Identical * regions will be flattened into a single new region in the main database. * - * Invokes the callback with a `MapboxOfflineTileCountExceededException` error if - * the merge operation would result in the offline tile count limit being exceeded. - * * Merged regions may not be in a completed status if the secondary database * does not contain all the tiles or resources required by the region definition. */ virtual void mergeOfflineRegions(const std::string& sideDatabasePath, std::function)>); + /* + * Merge a tilepack offline database (OA format) into the main offline database. + * + * When the database merge is completed, the provided callback will be + * executed on the database thread; it is the responsibility of the SDK bindings + * to re-execute a user-provided callback on the main thread. + * + * Resources and tiles will be copied over to the given region. + * + * Merged regions may not be in a completed status if the tilepack database + * does not contain all the tiles or resources required by the region definition. + */ + virtual void mergeTilepack(const std::string& sideDatabasePath, + const int64_t regionID, + std::function)>); + /** * Remove an offline region from the database and perform any resources * evictions necessary as a result. @@ -241,12 +249,6 @@ class DatabaseFileSource : public FileSource { */ virtual void invalidateOfflineRegion(const OfflineRegion&, std::function); - /** - * Changing or bypassing this limit without permission from Mapbox is - * prohibited by the Mapbox Terms of Service. - */ - virtual void setOfflineMapboxTileCountLimit(uint64_t) const; - void setResourceOptions(ResourceOptions) override; ResourceOptions getResourceOptions() override; diff --git a/include/mbgl/storage/offline.hpp b/include/mbgl/storage/offline.hpp index b136ee7fe20f..8f9bfc543c92 100644 --- a/include/mbgl/storage/offline.hpp +++ b/include/mbgl/storage/offline.hpp @@ -196,21 +196,6 @@ class OfflineRegionObserver { * that re-executes the user-provided implementation on the main thread. */ virtual void responseError(Response::Error) {} // NOLINT(performance-unnecessary-value-param) - - /* - * Implement this method to be notified when the limit on the number of - * Mapbox tiles stored for offline regions has been reached. - * - * Once the limit has been reached, the SDK will not download further - * offline tiles from Mapbox APIs until existing tiles have been removed. - * - * This limit does not apply to non-Mapbox tile sources. - * - * Note that this method will be executed on the database thread; it is the - * responsibility of the SDK bindings to wrap this object in an interface - * that re-executes the user-provided implementation on the main thread. - */ - virtual void mapboxTileCountLimitExceeded(uint64_t /* limit */) {} }; class OfflineRegion { diff --git a/include/mbgl/storage/resource_options.hpp b/include/mbgl/storage/resource_options.hpp index 7cf6b9087ac9..055a0102c25e 100644 --- a/include/mbgl/storage/resource_options.hpp +++ b/include/mbgl/storage/resource_options.hpp @@ -89,7 +89,7 @@ class ResourceOptions final { /** * @brief Sets the maximum cache size. * - * @param size Cache maximum size in bytes. + * @param size Cache maximum size (number of resources). * @return reference to ResourceOptions for chaining options together. */ ResourceOptions& withMaximumCacheSize(uint64_t size); @@ -97,7 +97,7 @@ class ResourceOptions final { /** * @brief Gets the previously set (or default) maximum allowed cache size. * - * @return maximum allowed cache database size in bytes. + * @return maximum allowed cache database size (number of resources). */ uint64_t maximumCacheSize() const; diff --git a/include/mbgl/util/constants.hpp b/include/mbgl/util/constants.hpp index 847c72a25486..8eec771f2804 100644 --- a/include/mbgl/util/constants.hpp +++ b/include/mbgl/util/constants.hpp @@ -44,7 +44,7 @@ constexpr float ONE_EM = 24.0f; constexpr uint8_t DEFAULT_PREFETCH_ZOOM_DELTA = 4; -constexpr uint64_t DEFAULT_MAX_CACHE_SIZE = 50 * 1024 * 1024; +constexpr uint64_t DEFAULT_MAX_CACHE_SIZE = 1000; // Default ImageManager's cache size for images added via onStyleImageMissing API. // Average sprite size with 1.0 pixel ratio is ~2kB, 8kB for pixel ratio of 2.0. diff --git a/include/mbgl/util/tile_server_options.hpp b/include/mbgl/util/tile_server_options.hpp index 2182f34d9f2b..345d22a30578 100644 --- a/include/mbgl/util/tile_server_options.hpp +++ b/include/mbgl/util/tile_server_options.hpp @@ -251,6 +251,13 @@ class TileServerOptions final { * @return const bool true if API key is required */ bool requiresApiKey() const; + + /** + * @brief Initialize the offline db with journal_mode = WAL or not + */ + TileServerOptions& setUseWalJournal(bool useWalJournal); + bool useWalJournal() const; + /** * @brief Gets the default styles. */ @@ -288,11 +295,6 @@ class TileServerOptions final { */ static TileServerOptions MapLibreConfiguration(); - /** - * @brief Get the tile server options configured for Mapbox. - */ - static TileServerOptions MapboxConfiguration(); - /** * @brief Get the tile server options configured for MapTiler. */ diff --git a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp index f008f381511f..777190efc956 100644 --- a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp @@ -36,10 +36,6 @@ OfflineManager::OfflineManager(jni::JNIEnv& env, const jni::Object& OfflineManager::~OfflineManager() {} -void OfflineManager::setOfflineMapboxTileCountLimit(jni::JNIEnv&, jni::jlong limit) { - fileSource->setOfflineMapboxTileCountLimit(limit); -} - void OfflineManager::listOfflineRegions(jni::JNIEnv& env_, const jni::Object& jFileSource_, const jni::Object& callback_) { @@ -158,6 +154,31 @@ void OfflineManager::mergeOfflineRegions(jni::JNIEnv& env_, }); } +void OfflineManager::mergeTilepack(jni::JNIEnv& env_, const jni::Object& jFileSource_, + const jni::String& jString_, const jni::jlong regionID_, + const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + auto globalFilesource = jni::NewGlobal(env_, jFileSource_); + + auto path = jni::Make(env_, jString_); + fileSource->mergeTilepack(path, regionID_, [ + //Keep a shared ptr to a global reference of the callback and file source so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)), + jFileSource = std::make_shared(std::move(globalFilesource)) + ](mbgl::expected regions) mutable { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (regions) { + OfflineManager::MergeOfflineRegionsCallback::onMerge( + *env, *jFileSource, *callback, *regions); + } else { + OfflineManager::MergeOfflineRegionsCallback::onError( + *env, *callback, regions.error()); + } + }); +} + void OfflineManager::resetDatabase(jni::JNIEnv& env_, const jni::Object& callback_) { auto globalCallback = jni::NewGlobal(env_, callback_); @@ -252,11 +273,11 @@ void OfflineManager::registerNative(jni::JNIEnv& env) { jni::MakePeer&>, "initialize", "finalize", - METHOD(&OfflineManager::setOfflineMapboxTileCountLimit, "setOfflineMapboxTileCountLimit"), METHOD(&OfflineManager::listOfflineRegions, "listOfflineRegions"), METHOD(&OfflineManager::getOfflineRegion, "getOfflineRegion"), METHOD(&OfflineManager::createOfflineRegion, "createOfflineRegion"), METHOD(&OfflineManager::mergeOfflineRegions, "mergeOfflineRegions"), + METHOD(&OfflineManager::mergeTilepack, "mergeTilepack"), METHOD(&OfflineManager::resetDatabase, "nativeResetDatabase"), METHOD(&OfflineManager::packDatabase, "nativePackDatabase"), METHOD(&OfflineManager::invalidateAmbientCache, "nativeInvalidateAmbientCache"), diff --git a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp index 9b5f5299f6fb..540adf6bec19 100644 --- a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp @@ -105,8 +105,6 @@ class OfflineManager { OfflineManager(jni::JNIEnv&, const jni::Object&); ~OfflineManager(); - void setOfflineMapboxTileCountLimit(jni::JNIEnv&, jni::jlong limit); - void listOfflineRegions(jni::JNIEnv&, const jni::Object&, const jni::Object& callback); @@ -127,6 +125,12 @@ class OfflineManager { const jni::String&, const jni::Object&); + void mergeTilepack(jni::JNIEnv&, + const jni::Object&, + const jni::String&, + const jni::jlong regionID, + const jni::Object&); + void putResourceWithUrl(jni::JNIEnv&, const jni::String& url, const jni::Array& data, diff --git a/platform/android/MapLibreAndroid/src/cpp/offline/offline_region.cpp b/platform/android/MapLibreAndroid/src/cpp/offline/offline_region.cpp index 20fa12ee8d78..776faf863d7b 100644 --- a/platform/android/MapLibreAndroid/src/cpp/offline/offline_region.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/offline/offline_region.cpp @@ -59,16 +59,6 @@ void OfflineRegion::setOfflineRegionObserver(jni::JNIEnv& env_, callback.Call(*env, method, OfflineRegionError::New(*env, error)); } - void mapboxTileCountLimitExceeded(uint64_t limit) override { - // Reattach, the callback comes from a different thread - android::UniqueEnv env = android::AttachEnv(); - - static auto& javaClass = jni::Class::Singleton(*env); - static auto method = javaClass.GetMethod(*env, "mapboxTileCountLimitExceeded"); - - callback.Call(*env, method, jlong(limit)); - } - jni::Global, jni::EnvAttachingDeleter> callback; }; diff --git a/platform/android/MapLibreAndroid/src/cpp/util/tile_server_options.cpp b/platform/android/MapLibreAndroid/src/cpp/util/tile_server_options.cpp index 08b42cff3faa..7a780e5291af 100644 --- a/platform/android/MapLibreAndroid/src/cpp/util/tile_server_options.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/util/tile_server_options.cpp @@ -25,6 +25,7 @@ jni::Local> TileServerOptions::New(jni::JNIEnv& e jni::String, jni::String, jni::jboolean, + jni::jboolean, jni::String, jni::Array>>(env); @@ -56,6 +57,7 @@ jni::Local> TileServerOptions::New(jni::JNIEnv& e tileVersionPrefixValue ? jni::Make(env, *tileVersionPrefixValue) : jni::Local(), jni::Make(env, tileServerOptions.apiKeyParameterName()), jni::jboolean(tileServerOptions.requiresApiKey()), + jni::jboolean(tileServerOptions.useWalJournal()), jni::Make(env, tileServerOptions.defaultStyle()), TileServerOptions::NewStyles(env, tileServerOptions.defaultStyles())); } @@ -77,12 +79,6 @@ jni::Local> TileServerOptions::DefaultConfigurati return TileServerOptions::New(env, options); } -jni::Local> TileServerOptions::MapboxConfiguration( - jni::JNIEnv& env, const jni::Class& jOptions) { - auto options = mbgl::TileServerOptions::MapboxConfiguration(); - return TileServerOptions::New(env, options); -} - jni::Local> TileServerOptions::MapTilerConfiguration( jni::JNIEnv& env, const jni::Class& jOptions) { auto options = mbgl::TileServerOptions::MapTilerConfiguration(); @@ -127,6 +123,8 @@ mbgl::TileServerOptions TileServerOptions::getTileServerOptions(jni::JNIEnv& env static auto apiKeyParameterNameField = javaClass.GetField(env, "apiKeyParameterName"); static auto apiKeyRequiredField = javaClass.GetField(env, "apiKeyRequired"); + static auto useWalJournalField = javaClass.GetField(env, "useWalJournal"); + static auto defaultStyleField = javaClass.GetField(env, "defaultStyle"); static auto defaultStylesField = javaClass.GetField>>(env, "defaultStyles"); @@ -137,7 +135,8 @@ mbgl::TileServerOptions TileServerOptions::getTileServerOptions(jni::JNIEnv& env .withBaseURL(jni::Make(env, options.Get(env, baseURLField))) .withUriSchemeAlias(jni::Make(env, options.Get(env, uriSchemeAliasField))) .withApiKeyParameterName(jni::Make(env, options.Get(env, apiKeyParameterNameField))) - .setRequiresApiKey(options.Get(env, apiKeyRequiredField)); + .setRequiresApiKey(options.Get(env, apiKeyRequiredField)) + .setUseWalJournal(options.Get(env, useWalJournalField)); auto sourcePrefixValue = options.Get(env, sourceVersionPrefixField); retVal.withSourceTemplate( @@ -194,8 +193,6 @@ void TileServerOptions::registerNative(jni::JNIEnv& env) { *javaClass, jni::MakeNativeMethod("defaultConfiguration"), - jni::MakeNativeMethod("mapboxConfiguration"), jni::MakeNativeMethod("mapTilerConfiguration"), jni::MakeNativeMethod> DefaultConfiguration(jni::JNIEnv&, const jni::Class&); - static jni::Local> MapboxConfiguration(jni::JNIEnv&, - const jni::Class&); static jni::Local> MapTilerConfiguration(jni::JNIEnv&, const jni::Class&); static jni::Local> MapLibreConfiguration(jni::JNIEnv&, diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java index 62c19e42678a..7215ba55704e 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/MapLibre.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import org.maplibre.android.util.TileServerOptionsConfigurator; import timber.log.Timber; import org.maplibre.android.constants.MapLibreConstants; @@ -52,7 +53,7 @@ public final class MapLibre { */ @UiThread @NonNull - public static synchronized MapLibre getInstance(@NonNull Context context) { + public static synchronized MapLibre getInstance(@NonNull Context context, @Nullable TileServerOptionsConfigurator tileServerOptionsConfigurator) { ThreadUtils.init(context); ThreadUtils.checkThread(TAG); if (INSTANCE == null) { @@ -63,6 +64,9 @@ public static synchronized MapLibre getInstance(@NonNull Context context) { } TileServerOptions tileServerOptions = TileServerOptions.get(WellKnownTileServer.MapLibre); + if (tileServerOptionsConfigurator != null) { + tileServerOptions = tileServerOptionsConfigurator.configure(tileServerOptions); + } INSTANCE.tileServerOptions = tileServerOptions; INSTANCE.apiKey = null; FileSource fileSource = FileSource.getInstance(context); @@ -89,7 +93,8 @@ public static synchronized MapLibre getInstance(@NonNull Context context) { @UiThread @NonNull public static synchronized MapLibre getInstance(@NonNull Context context, @Nullable String apiKey, - WellKnownTileServer tileServer) { + WellKnownTileServer tileServer, + @Nullable TileServerOptionsConfigurator tileServerOptionsConfigurator) { ThreadUtils.init(context); ThreadUtils.checkThread(TAG); if (INSTANCE == null) { @@ -103,6 +108,9 @@ public static synchronized MapLibre getInstance(@NonNull Context context, @Nulla } TileServerOptions tileServerOptions = TileServerOptions.get(tileServer); + if (tileServerOptionsConfigurator != null) { + tileServerOptions = tileServerOptionsConfigurator.configure(tileServerOptions); + } INSTANCE.tileServerOptions = tileServerOptions; FileSource fileSource = FileSource.getInstance(context); fileSource.setTileServerOptions(tileServerOptions); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/WellKnownTileServer.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/WellKnownTileServer.java index be41d908545f..b8e73bd1e64d 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/WellKnownTileServer.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/WellKnownTileServer.java @@ -1,7 +1,6 @@ package org.maplibre.android; public enum WellKnownTileServer { - Mapbox, MapTiler, MapLibre; } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequestUrl.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequestUrl.java index 28bb459f93f2..121c619902c3 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequestUrl.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/http/HttpRequestUrl.java @@ -20,7 +20,7 @@ private HttpRequestUrl() { * @return the adapted resource url */ public static String buildResourceUrl(@NonNull String host, String resourceUrl, int querySize, boolean offline) { - if (isValidMapboxEndpoint(host)) { + if (isValidOutdooractiveEndpoint(host)) { if (querySize == 0) { resourceUrl = resourceUrl + "?"; } else { @@ -40,10 +40,10 @@ public static String buildResourceUrl(@NonNull String host, String resourceUrl, * @param host the host used as endpoint * @return true if a valid MapLibre endpoint */ - private static boolean isValidMapboxEndpoint(String host) { - return host.equals("mapbox.com") - || host.endsWith(".mapbox.com") - || host.equals("mapbox.cn") - || host.endsWith(".mapbox.cn"); + private static boolean isValidOutdooractiveEndpoint(String host) { + return host.equals("outdooractive.com") + || host.endsWith(".outdooractive.com") + || host.equals("oastatic.com") + || host.endsWith(".oastatic.com"); } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LatLngEvaluator.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LatLngEvaluator.java index 7642c0ccde34..3148de7083e3 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LatLngEvaluator.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/LatLngEvaluator.java @@ -13,6 +13,7 @@ class LatLngEvaluator implements TypeEvaluator { @NonNull @Override public LatLng evaluate(float fraction, @NonNull LatLng startValue, @NonNull LatLng endValue) { + fraction = Double.isNaN(fraction) ? 0f : fraction; latLng.setLatitude(startValue.getLatitude() + ((endValue.getLatitude() - startValue.getLatitude()) * fraction)); latLng.setLongitude(startValue.getLongitude() diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java index a647b7534813..182b4da2ea9b 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/location/SymbolLocationLayerRenderer.java @@ -46,6 +46,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import org.maplibre.android.log.Logger; import org.maplibre.geojson.Feature; import org.maplibre.geojson.Point; @@ -60,6 +61,9 @@ import java.util.Set; final class SymbolLocationLayerRenderer implements LocationLayerRenderer { + + private static final String TAG = "mlgl-locationSymbol"; + private Style style; private final LayerSourceProvider layerSourceProvider; @@ -180,6 +184,11 @@ public void setAccuracyRadius(Float accuracy) { @Override public void styleScaling(Expression scaleExpression) { + if (!style.isFullyLoaded()) { + Logger.w(TAG, "Style is not fully loaded, not able to get layer!"); + return; + } + for (String layerId : layerSet) { Layer layer = style.getLayer(layerId); if (layer instanceof SymbolLayer) { @@ -246,6 +255,7 @@ private void updateForegroundBearing(float bearing) { private void setLayerVisibility(@NonNull String layerId, boolean visible) { if (!style.isFullyLoaded()) { + Logger.w(TAG, "Style is not fully loaded, not able to get layer!"); return; } @@ -271,6 +281,11 @@ public void adjustPulsingCircleLayerVisibility(boolean visible) { */ @Override public void stylePulsingCircle(LocationComponentOptions options) { + if (!style.isFullyLoaded()) { + Logger.w(TAG, "Style is not fully loaded, not able to get layer!"); + return; + } + if (style.getLayer(PULSING_CIRCLE_LAYER) != null) { setLayerVisibility(PULSING_CIRCLE_LAYER, true); style.getLayer(PULSING_CIRCLE_LAYER).setProperties( @@ -326,8 +341,10 @@ private void refreshSource() { // prevents exception when other style has been set with an update in flight // https://github.com/maplibre/maplibre-native/issues/3348 if (!style.isFullyLoaded()) { + Logger.w(TAG, "Style is not fully loaded, not able to get source!"); return; } + GeoJsonSource source = style.getSourceAs(LOCATION_SOURCE); if (source != null) { locationSource.setGeoJson(locationFeature); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt index 5d021809b99e..64a8522db37c 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt @@ -225,9 +225,6 @@ class OfflineManager private constructor(context: Context) { * Only resources and tiles that belong to a region will be copied over. Identical * regions will be flattened into a single new region in the main database. * - * The operation will be aborted and [MergeOfflineRegionsCallback.onError] with an appropriate message - * will be invoked if the merge would result in the offline tile count limit being exceeded. - * * Merged regions may not be in a completed status if the secondary database * does not contain all the tiles or resources required by the region definition. * @@ -265,6 +262,59 @@ class OfflineManager private constructor(context: Context) { }.start() } + /** + * Merge an offline tilepack from a secondary database into the main offline database. + * + * When the merge is completed, or fails, the {@link MergeOfflineRegionsCallback} will be invoked on the main thread. + * The callback reference is strongly kept throughout the process, + * so it needs to be wrapped in a weak reference or released on the client side if necessary. + * + * The secondary database may need to be upgraded to the latest schema. + * This is done in-place and requires write-access to the provided path. + * If the app's process doesn't have write-access to the provided path, + * the file will be copied to the temporary, internal directory for the duration of the merge. + * + * Only resources and tiles that belong to a region will be copied over. Identical + * regions will be flattened into a single new region in the main database. + * + * Merged regions may not be in a completed status if the secondary database + * does not contain all the tiles or resources required by the region definition. + * + * @param path secondary database writable path + * @param regionID region id + * @param callback completion/error callback + */ + fun mergeTilepack(path: String, regionID: Long, callback: MergeOfflineRegionsCallback) { + val src = File(path) + Thread { + var errorMessage: String? = null + if (src.canWrite()) { + handler.post { // path writable, merge and update schema in place if necessary + mergeTilepackFiles(src, regionID, callback, false) + } + } else if (src.canRead()) { + // path not writable, copy the the file to temp directory + val dst = File(FileSource.getInternalCachePath(context), src.name) + try { + copyTempDatabaseFile(src, dst) + handler.post { // merge and update schema using the copy + mergeTilepackFiles(dst, regionID, callback, true) + } + } catch (ex: IOException) { + ex.printStackTrace() + errorMessage = ex.message + } + } else { + // path not readable, abort + errorMessage = "Secondary database needs to be located in a readable path." + } + if (errorMessage != null) { + val finalErrorMessage: String = errorMessage + handler.post { callback.onError(finalErrorMessage) } + } + }.start() + } + /** * Delete existing database and re-initialize. * @@ -390,27 +440,25 @@ class OfflineManager private constructor(context: Context) { } /** - * Sets the maximum size in bytes for the ambient cache. + * Sets the maximum size (number of resources) for the ambient cache. * * This call is potentially expensive because it will try * to trim the data in case the database is larger than the * size defined. The size of offline regions are not affected * by this settings, but the ambient cache will always try - * to not exceed the maximum size defined, taking into account - * the current size for the offline regions. + * to not exceed the maximum size defined. * * Note that if you use the SDK's offline functionality, your ability to set the ambient cache size will be limited. * Space that offline regions take up detract from the space available for ambient caching, and the ambient cache - * size does not block offline downloads. For example: if the maximum cache size is set to 50 MB and 40 MB are - * already used by offline regions, the ambient cache size will effectively be 10 MB. + * size does not block offline downloads. * * Setting the size to 0 will disable the cache if there is no * offline region on the database. * * This method should always be called at the start of an app, before setting the style and loading a map. - * Otherwise, the map will instantiate with the default cache size of 50 MB. + * Otherwise, the map will instantiate with the default cache size of 1000 resources. * - * @param size the maximum size of the ambient cache + * @param size the maximum number of resources of the ambient cache * @param callback the callback to be invoked when the the maximum size has been set or when the operation erred. */ fun setMaximumAmbientCacheSize(size: Long, callback: FileSourceCallback?) { @@ -483,6 +531,36 @@ class OfflineManager private constructor(context: Context) { ) } + private fun mergeTilepackFiles(file: File, regionID: Long, callback: MergeOfflineRegionsCallback, isTemporaryFile: Boolean) { + fileSource.activate() + mergeTilepack( + fileSource, + file.absolutePath, + regionID, + object : MergeOfflineRegionsCallback { + override fun onMerge(offlineRegions: Array?) { + if (isTemporaryFile) { + file.delete() + } + handler.post { + fileSource.deactivate() + callback.onMerge(offlineRegions) + } + } + + override fun onError(error: String) { + if (isTemporaryFile) { + file.delete() + } + handler.post { + fileSource.deactivate() + callback.onError(error) + } + } + } + ) + } + /** * Creates an offline region in the database by downloading the resources needed to use * the given region offline. @@ -539,19 +617,6 @@ class OfflineManager private constructor(context: Context) { return world().contains(definition.bounds!!) } - /** - * Sets the maximum number of tiles that may be downloaded and stored on the current device. - * By default, the limit is set to 6,000. - * - * Once this limit is reached, [OfflineRegion.OfflineRegionObserver.mapboxTileCountLimitExceeded] - * fires every additional attempt to download additional tiles until already downloaded tiles are removed - * by calling [OfflineRegion.delete]. - * - * @param limit the maximum number of tiles allowed to be downloaded - */ - @Keep - external fun setOfflineMapboxTileCountLimit(limit: Long) - /** * Sets whether database file packing occurs automatically. * By default, the automatic database file packing is enabled. @@ -566,7 +631,6 @@ class OfflineManager private constructor(context: Context) { * * @param autopack flag setting the automatic database file packing. */ - @Keep external fun runPackDatabaseAutomatically(autopack: Boolean) @@ -589,6 +653,9 @@ class OfflineManager private constructor(context: Context) { @Keep private external fun mergeOfflineRegions(fileSource: FileSource, path: String, callback: MergeOfflineRegionsCallback) + @Keep + private external fun mergeTilepack(fileSource: FileSource, path: String, regionID: Long, callback: MergeOfflineRegionsCallback) + @Keep private external fun nativeResetDatabase(callback: FileSourceCallback?) diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineRegion.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineRegion.kt index 4727636e59f4..6fdb4e5fc1b3 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineRegion.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineRegion.kt @@ -80,19 +80,6 @@ class OfflineRegion @Keep private constructor(offlineRegionPtr: Long, fileSource * @param error the offline region error message */ fun onError(error: OfflineRegionError) - - /* - * Implement this method to be notified when the limit on the number of MapLibre - * tiles stored for offline regions has been reached. - * - * Once the limit has been reached, the SDK will not download further offline - * tiles from MapLibre APIs until existing tiles have been removed. - * - * This limit does not apply to non-MapLibre tile sources. - * - * This method will be executed on the main thread. - */ - fun mapboxTileCountLimitExceeded(limit: Long) } /** @@ -255,12 +242,6 @@ class OfflineRegion @Keep private constructor(offlineRegionPtr: Long, fileSource handler.post { observer?.onError(error) } } } - - override fun mapboxTileCountLimitExceeded(limit: Long) { - if (deliverMessages()) { - handler.post { observer?.mapboxTileCountLimitExceeded(limit) } - } - } }) } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptions.java index c144bf4db60b..4df83d1a3cc4 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptions.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptions.java @@ -72,6 +72,8 @@ public TileServerOptions[] newArray(int size) { @Keep private boolean apiKeyRequired; @Keep + private boolean useWalJournal; + @Keep private String defaultStyle; @Keep private DefaultStyle[] defaultStyles; @@ -98,6 +100,7 @@ public TileServerOptions[] newArray(int size) { * @param tileVersionPrefix the tile version prefix * @param apiKeyParameterName the name of api key parameter * @param apiKeyRequired indicates if API key is required + * @param useWalJournal indicates if the offline db should be initialized with journal_mode = WAL * @param defaultStyle the name of the default style * @param defaultStyles the list of default styles */ @@ -122,6 +125,7 @@ public TileServerOptions( @Nullable String tileVersionPrefix, String apiKeyParameterName, boolean apiKeyRequired, + boolean useWalJournal, String defaultStyle, DefaultStyle[] defaultStyles ) { @@ -146,6 +150,7 @@ public TileServerOptions( setDefaultStyles(defaultStyles); setDefaultStyle(defaultStyle); setApiKeyRequired(apiKeyRequired); + setUseWalJournal(useWalJournal); } public void setBaseURL(String baseURL) { @@ -300,6 +305,14 @@ public boolean getApiKeyRequired() { return this.apiKeyRequired; } + public void setUseWalJournal(boolean useWalJournal) { + this.useWalJournal = useWalJournal; + } + + public boolean useWalJournal() { + return this.useWalJournal; + } + public void setDefaultStyles(DefaultStyle[] defaultStyles) { this.defaultStyles = defaultStyles; } @@ -351,6 +364,7 @@ protected TileServerOptions(Parcel in) { setTileVersionPrefix(in.readString()); setApiKeyParameterName(in.readString()); setApiKeyRequired(in.readByte() != 0); + setUseWalJournal(in.readByte() != 0); setDefaultStyle(in.readString()); in.createTypedArray(DefaultStyle.CREATOR); } @@ -382,14 +396,13 @@ public void writeToParcel(@NonNull Parcel out, int flags) { out.writeString(tileVersionPrefix); out.writeString(apiKeyParameterName); out.writeByte((byte) (apiKeyRequired ? 1 : 0)); + out.writeByte((byte) (useWalJournal ? 1 : 0)); out.writeString(defaultStyle); out.writeTypedArray(defaultStyles, 0); } public static TileServerOptions get(WellKnownTileServer tileServer) { switch (tileServer) { - case Mapbox: - return mapboxConfiguration(); case MapTiler: return mapTilerConfiguration(); case MapLibre: @@ -403,10 +416,6 @@ public static TileServerOptions get(WellKnownTileServer tileServer) { @NonNull private static native TileServerOptions defaultConfiguration(); - @Keep - @NonNull - private static native TileServerOptions mapboxConfiguration(); - @Keep @NonNull private static native TileServerOptions mapTilerConfiguration(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptionsConfigurator.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptionsConfigurator.java new file mode 100644 index 000000000000..0e105ce3d9b5 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/util/TileServerOptionsConfigurator.java @@ -0,0 +1,5 @@ +package org.maplibre.android.util; + +public interface TileServerOptionsConfigurator { + TileServerOptions configure(TileServerOptions tileServerOptions); +} diff --git a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java index 95dc270d375d..ad78096e1412 100644 --- a/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java +++ b/platform/android/MapLibreAndroid/src/opengl/java/org/maplibre/android/maps/renderer/surfaceview/MapLibreGLSurfaceView.java @@ -205,6 +205,10 @@ boolean createSurface() { Log.e(TAG, "mEglConfig not initialized"); return false; } + if (mEglContext == null) { + Log.e(TAG, "mEglContext not initialized"); + return false; + } /* * The window size has changed, so we need to create a new diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/offline/OfflineDownloadTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/offline/OfflineDownloadTest.kt index d742ed86b4c8..d36077189ca2 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/offline/OfflineDownloadTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/offline/OfflineDownloadTest.kt @@ -64,10 +64,6 @@ class OfflineDownloadTest : OfflineRegion.OfflineRegionObserver { Logger.e(TAG, "Error while downloading offline region: $error") } - override fun mapboxTileCountLimitExceeded(limit: Long) { - Logger.e(TAG, "Tile count limited exceeded: $limit") - } - private fun createTestRegionDefinition(): OfflineRegionDefinition { return OfflineGeometryRegionDefinition( TestStyles.getPredefinedStyleWithFallback("Streets"), diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/DownloadRegionActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/DownloadRegionActivity.kt index 35b96d70180a..1685fcd75f74 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/DownloadRegionActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/DownloadRegionActivity.kt @@ -44,7 +44,6 @@ class DownloadRegionActivity : AppCompatActivity(), OfflineRegion.OfflineRegionO setContentView(binding.root) offlineManager = OfflineManager.getInstance(this) - offlineManager.setOfflineMapboxTileCountLimit(Long.MAX_VALUE) initUi() deleteOldOfflineRegions { @@ -170,10 +169,6 @@ class DownloadRegionActivity : AppCompatActivity(), OfflineRegion.OfflineRegionO logMessage("Error: $error") } - override fun mapboxTileCountLimitExceeded(limit: Long) { - logMessage("Error: tile count limit exceeded") - } - protected fun logMessage(message: String) { Timber.d(message) logView.append(message) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/OfflineActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/OfflineActivity.kt index c915f4960be6..d639b49f74b0 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/OfflineActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/offline/OfflineActivity.kt @@ -262,11 +262,6 @@ class OfflineActivity : AppCompatActivity(), DownloadRegionDialogListener { Timber.e("onError: %s, %s", error.reason, error.message) offlineRegion!!.setDownloadState(OfflineRegion.STATE_INACTIVE) } - - override fun mapboxTileCountLimitExceeded(limit: Long) { - Timber.e("MapLibre tile count limit exceeded: %s", limit) - offlineRegion!!.setDownloadState(OfflineRegion.STATE_INACTIVE) - } }) // Change the region state diff --git a/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts b/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts index 79aa9f611c1f..1324f223f554 100644 --- a/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts +++ b/platform/android/buildSrc/src/main/kotlin/maplibre.gradle-publish.gradle.kts @@ -133,3 +133,16 @@ afterEvaluate { } } } + +// Note: The following was left after a rebase and might be unnecessary/break things +signing { + sign(publishing.publications) +} + +/// OA Changes +tasks { + withType { + onlyIf { !project.hasProperty("skip.signing") } + } +} +/// diff --git a/platform/darwin/core/http_file_source.mm b/platform/darwin/core/http_file_source.mm index 41e400da06f4..08e7beea2983 100644 --- a/platform/darwin/core/http_file_source.mm +++ b/platform/darwin/core/http_file_source.mm @@ -212,20 +212,19 @@ void cancel() { HTTPFileSource::~HTTPFileSource() = default; MLN_APPLE_EXPORT -BOOL isValidMapboxEndpoint(NSURL *url) { - return ([url.host isEqualToString:@"mapbox.com"] || - [url.host hasSuffix:@".mapbox.com"] || - [url.host isEqualToString:@"mapbox.cn"] || - [url.host hasSuffix:@".mapbox.cn"]); +BOOL isValidOutdooractiveEndpoint(NSURL *url) { + return ([url.host isEqualToString:@"outdooractive.com"] || + [url.host hasSuffix:@".outdooractive.com"] || + [url.host isEqualToString:@"oastatic.com"] || + [url.host hasSuffix:@".oastatic.com"]); } MLN_APPLE_EXPORT NSURL *resourceURL(const Resource& resource) { - NSURL *url = [NSURL URLWithString:@(resource.url.c_str())]; #if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR - if (isValidMapboxEndpoint(url)) { + if (isValidOutdooractiveEndpoint(url)) { NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; NSMutableArray *queryItems = [NSMutableArray array]; @@ -268,8 +267,6 @@ BOOL isValidMapboxEndpoint(NSURL *url) { [req setValue:rangeHeader forHTTPHeaderField:@"Range"]; } - [req addValue:impl->userAgent forHTTPHeaderField:@"User-Agent"]; - const bool isTile = resource.kind == mbgl::Resource::Kind::Tile; if (isTile) { @@ -294,6 +291,9 @@ BOOL isValidMapboxEndpoint(NSURL *url) { if ([networkManager.delegate respondsToSelector:@selector(willSendRequest:)]) { req = [networkManager.delegate willSendRequest:req]; } + if (session.configuration.HTTPAdditionalHeaders[@"User-Agent"] == nil) { + [req addValue:impl->userAgent forHTTPHeaderField:@"User-Agent"]; + } request->task = [session dataTaskWithRequest:req diff --git a/platform/darwin/src/MLNFeature.mm b/platform/darwin/src/MLNFeature.mm index 84b89b5ed584..9cf49c1a82e3 100644 --- a/platform/darwin/src/MLNFeature.mm +++ b/platform/darwin/src/MLNFeature.mm @@ -336,7 +336,9 @@ - (NSDictionary *)geoJSONDictionary { featureCollection.reserve(self.shapes.count); for (MLNShape *feature in self.shapes) { auto geoJSONObject = feature.geoJSONObject; - MLNAssert(geoJSONObject.is(), @"Feature collection must only contain features."); + if (!geoJSONObject.is()) { + continue; + } featureCollection.push_back(geoJSONObject.get()); } return featureCollection; diff --git a/platform/darwin/src/MLNMapSnapshotter.mm b/platform/darwin/src/MLNMapSnapshotter.mm index b6fc746e2ee8..289e49c53cad 100644 --- a/platform/darwin/src/MLNMapSnapshotter.mm +++ b/platform/darwin/src/MLNMapSnapshotter.mm @@ -806,11 +806,9 @@ - (void)configureWithOptions:(MLNMapSnapshotOptions *)options { } // Create the snapshotter - auto localFontFamilyName = config.localFontFamilyName ? std::string(config.localFontFamilyName.UTF8String) : nullptr; + std::optional localFontFamilyName = config.localFontFamilyName ? std::optional(std::string(config.localFontFamilyName.UTF8String)) : std::nullopt; _delegateHost = std::make_unique(self); - _mbglMapSnapshotter = std::make_unique( - size, pixelRatio, resourceOptions, clientOptions, *_delegateHost, localFontFamilyName); - + _mbglMapSnapshotter = std::make_unique(size, pixelRatio, resourceOptions, clientOptions, *_delegateHost, localFontFamilyName); _mbglMapSnapshotter->setStyleURL(std::string(options.styleURL.absoluteString.UTF8String)); // Camera options diff --git a/platform/darwin/src/MLNNetworkConfiguration.h b/platform/darwin/src/MLNNetworkConfiguration.h index 19fc298e22b4..0eb0335f4ef8 100644 --- a/platform/darwin/src/MLNNetworkConfiguration.h +++ b/platform/darwin/src/MLNNetworkConfiguration.h @@ -68,6 +68,17 @@ MLN_EXPORT */ @property (atomic, strong, null_resettable) NSURLSessionConfiguration *sessionConfiguration; +/** + A Boolean value indicating whether the current `NSURLSessionConfiguration` stops + making network requests. + + When this property is set to `NO` `MGLMapView` will rely solely on pre-cached + tiles. + + The default value of this property is `YES`. + */ +@property (atomic, assign) BOOL connected; + @end NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNNetworkConfiguration.mm b/platform/darwin/src/MLNNetworkConfiguration.mm index dbc4816e09e7..6cace5b0ff3d 100644 --- a/platform/darwin/src/MLNNetworkConfiguration.mm +++ b/platform/darwin/src/MLNNetworkConfiguration.mm @@ -4,6 +4,8 @@ #import "MLNSettings_Private.h" #endif +#include + #import "MLNReachability.h" static NSString * const MLNStartTime = @"start_time"; @@ -64,6 +66,19 @@ + (NSURLSessionConfiguration *)defaultSessionConfiguration { return sessionConfiguration; } +- (void)setConnected:(BOOL)connected { + if (!connected) { + mbgl::NetworkStatus::Set(mbgl::NetworkStatus::Status::Offline); + } else { + mbgl::NetworkStatus::Set(mbgl::NetworkStatus::Status::Online); + } +} + +- (BOOL)connected { + auto status = mbgl::NetworkStatus::Get(); + return status == mbgl::NetworkStatus::Status::Online; +} + // MARK: - MLNNativeNetworkDelegate - (NSURLSession *)sessionForNetworkManager:(MLNNativeNetworkManager *)networkManager { diff --git a/platform/darwin/src/MLNOfflinePack.mm b/platform/darwin/src/MLNOfflinePack.mm index b68cd556255c..e18929c54735 100644 --- a/platform/darwin/src/MLNOfflinePack.mm +++ b/platform/darwin/src/MLNOfflinePack.mm @@ -45,7 +45,6 @@ @interface MLNShapeOfflineRegion () mbglDatabaseFileSource; @property (nonatomic) std::shared_ptr mbglOnlineFileSource; @property (nonatomic) std::shared_ptr mbglFileSource; -@property (nonatomic, getter=isPaused) BOOL paused; @end @implementation MLNOfflineStorage { @@ -59,10 +57,6 @@ + (instancetype)sharedOfflineStorage { static MLNOfflineStorage *sharedOfflineStorage; dispatch_once(&onceToken, ^{ sharedOfflineStorage = [[self alloc] init]; -#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR - [[NSNotificationCenter defaultCenter] addObserver:sharedOfflineStorage selector:@selector(unpauseFileSource:) name:UIApplicationWillEnterForegroundNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:sharedOfflineStorage selector:@selector(pauseFileSource:) name:UIApplicationDidEnterBackgroundNotification object:nil]; -#endif [sharedOfflineStorage reloadPacks]; }); @@ -76,28 +70,6 @@ + (instancetype)sharedOfflineStorage { return sharedOfflineStorage; } -#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR -- (void)pauseFileSource:(__unused NSNotification *)notification { - if (self.isPaused) { - return; - } - - _mbglOnlineFileSource->pause(); - _mbglDatabaseFileSource->pause(); - self.paused = YES; -} - -- (void)unpauseFileSource:(__unused NSNotification *)notification { - if (!self.isPaused) { - return; - } - - _mbglOnlineFileSource->resume(); - _mbglDatabaseFileSource->resume(); - self.paused = NO; -} -#endif - - (void)setDelegate:(id)newValue { MLNLogDebug(@"Setting delegate: %@", newValue); _delegate = newValue; @@ -322,7 +294,7 @@ + (NSString *)legacyDatabasePath { } - (void)addContentsOfFile:(NSString *)filePath withCompletionHandler:(MLNBatchedOfflinePackAdditionCompletionHandler)completion { - MLNLogDebug(@"Adding contentsOfFile: %@ completionHandler: %@", filePath, completion); + MLNLogInfo(@"Adding contentsOfFile: %@ completionHandler: %@", filePath, completion); NSURL *fileURL = [NSURL fileURLWithPath:filePath]; [self addContentsOfURL:fileURL withCompletionHandler:completion]; @@ -330,7 +302,7 @@ - (void)addContentsOfFile:(NSString *)filePath withCompletionHandler:(MLNBatched } - (void)addContentsOfURL:(NSURL *)fileURL withCompletionHandler:(MLNBatchedOfflinePackAdditionCompletionHandler)completion { - MLNLogDebug(@"Adding contentsOfURL: %@ completionHandler: %@", fileURL, completion); + MLNLogInfo(@"Adding contentsOfURL: %@ completionHandler: %@", fileURL, completion); NSFileManager *fileManager = [NSFileManager defaultManager]; if (!fileURL.isFileURL) { @@ -404,10 +376,89 @@ - (void)_addContentsOfFile:(NSString *)filePath withCompletionHandler:(void (^)( }); } +- (void)addContentsOfTilepack:(NSURL *)fileURL + toOfflinePack:(MLNOfflinePack *)offlinePack + withCompletionHandler:(MLNBatchedOfflinePackAdditionCompletionHandler)completion +{ + MLNLogInfo(@"Adding addContentsOfTilepack: %@ completionHandler: %@", fileURL, completion); + + if (!fileURL.isFileURL) { + [NSException raise:NSInvalidArgumentException format:@"%@ must be a valid file path", fileURL.absoluteString]; + } + + __weak MLNOfflineStorage *weakSelf = self; + [self _addContentsOfTilepack:fileURL.path + toOfflinePack:offlinePack + withCompletionHandler:^(NSArray * _Nullable packs, NSError * _Nullable error) + { + if (packs) { + NSMutableDictionary *packsByIdentifier = [NSMutableDictionary dictionary]; + + MLNOfflineStorage *strongSelf = weakSelf; + for (MLNOfflinePack *pack in packs) { + [packsByIdentifier setObject:pack forKey:@(pack.mbglOfflineRegion->getID())]; + } + + id mutablePacks = [strongSelf mutableArrayValueForKey:@"packs"]; + NSMutableIndexSet *replaceIndexSet = [NSMutableIndexSet indexSet]; + NSMutableArray *replacePacksArray = [NSMutableArray array]; + [strongSelf.packs enumerateObjectsUsingBlock:^(MLNOfflinePack * _Nonnull pack, NSUInteger idx, BOOL * _Nonnull stop) { + MLNOfflinePack *newPack = packsByIdentifier[@(pack.mbglOfflineRegion->getID())]; + if (newPack) { + MLNOfflinePack *previousPack = [mutablePacks objectAtIndex:idx]; + [previousPack invalidate]; + [replaceIndexSet addIndex:idx]; + [replacePacksArray addObject:[packsByIdentifier objectForKey:@(newPack.mbglOfflineRegion->getID())]]; + [packsByIdentifier removeObjectForKey:@(newPack.mbglOfflineRegion->getID())]; + } + }]; + + if (replaceIndexSet.count > 0) { + [mutablePacks replaceObjectsAtIndexes:replaceIndexSet withObjects:replacePacksArray]; + } + + [mutablePacks addObjectsFromArray:packsByIdentifier.allValues]; + } + if (completion) { + completion(fileURL, packs, error); + } + }]; +} + +- (void)_addContentsOfTilepack:(NSString *)filePath + toOfflinePack:(MLNOfflinePack* )offlinePack + withCompletionHandler:(void (^)(NSArray * _Nullable packs, NSError * _Nullable error))completion +{ + _mbglDatabaseFileSource->mergeTilepack(std::string(static_cast([filePath UTF8String])), offlinePack.mbglOfflineRegion->getID(), [&, completion, filePath](mbgl::expected result) { + NSError *error; + NSMutableArray *packs; + if (!result) { + NSString *description = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ADD_FILE_CONTENTS_FAILED_DESC", @"Foundation", nil, @"Unable to add offline packs from the file at %@.", @"User-friendly error description"), filePath]; + error = [NSError errorWithDomain:MLNErrorDomain code:MLNErrorCodeModifyingOfflineStorageFailed + userInfo:@{ + NSLocalizedDescriptionKey: description, + NSLocalizedFailureReasonErrorKey: @(mbgl::util::toString(result.error()).c_str()) + }]; + } else { + auto& regions = result.value(); + packs = [NSMutableArray arrayWithCapacity:regions.size()]; + for (auto ®ion : regions) { + MLNOfflinePack *pack = [[MLNOfflinePack alloc] initWithMBGLRegion:new mbgl::OfflineRegion(std::move(region))]; + [packs addObject:pack]; + } + } + if (completion) { + dispatch_async(dispatch_get_main_queue(), [&, completion, error, packs](void) { + completion(packs, error); + }); + } + }); +} + // MARK: Pack management methods - (void)addPackForRegion:(id )region withContext:(NSData *)context completionHandler:(MLNOfflinePackAdditionCompletionHandler)completion { - MLNLogDebug(@"Adding packForRegion: %@ contextLength: %lu completionHandler: %@", region, (unsigned long)context.length, completion); + MLNLogInfo(@"Adding packForRegion: %@ contextLength: %lu completionHandler: %@", region, (unsigned long)context.length, completion); __weak MLNOfflineStorage *weakSelf = self; [self _addPackForRegion:region withContext:context completionHandler:^(MLNOfflinePack * _Nullable pack, NSError * _Nullable error) { pack.state = MLNOfflinePackStateInactive; @@ -447,7 +498,7 @@ - (void)_addPackForRegion:(id )region withContext:(NSData *)co } - (void)removePack:(MLNOfflinePack *)pack withCompletionHandler:(MLNOfflinePackRemovalCompletionHandler)completion { - MLNLogDebug(@"Removing pack: %@ completionHandler: %@", pack, completion); + MLNLogInfo(@"Removing pack: %@ completionHandler: %@", pack, completion); [[self mutableArrayValueForKey:@"packs"] removeObject:pack]; [self _removePack:pack withCompletionHandler:^(NSError * _Nullable error) { if (completion) { @@ -539,11 +590,6 @@ - (void)getPacksWithCompletionHandler:(void (^)(NSArray *packs }); } -- (void)setMaximumAllowedMapboxTiles:(uint64_t)maximumCount { - MLNLogDebug(@"Setting maximumAllowedMapboxTiles: %lu", (unsigned long)maximumCount); - _mbglDatabaseFileSource->setOfflineMapboxTileCountLimit(maximumCount); -} - // MARK: - Ambient cache management - (void)setMaximumAmbientCacheSize:(NSUInteger)cacheSize withCompletionHandler:(void (^)(NSError * _Nullable))completion { @@ -610,6 +656,11 @@ - (void)resetDatabaseWithCompletionHandler:(void (^)(NSError *_Nullable error))c } }); } + +- (void)runPackDatabaseAutomatically:(BOOL)autopack { + _mbglDatabaseFileSource->runPackDatabaseAutomatically(autopack); +} + // MARK: - - (unsigned long long)countOfBytesCompleted { diff --git a/platform/darwin/src/MLNSettings.h b/platform/darwin/src/MLNSettings.h index 72a6289832b5..4eb8757769a9 100644 --- a/platform/darwin/src/MLNSettings.h +++ b/platform/darwin/src/MLNSettings.h @@ -16,11 +16,7 @@ typedef NS_ENUM(NSUInteger, MLNWellKnownTileServer) { /** MapLibre */ - MLNMapLibre, - /** - Mapbox - */ - MLNMapbox + MLNMapLibre }; /** @@ -57,6 +53,11 @@ MLN_EXPORT */ + (void)useWellKnownTileServer:(MLNWellKnownTileServer)tileServer; +/** + Use the WAL journal mode for new offline databases + */ ++ (void)setUseWalJournal:(BOOL)useWalJournal; + @end NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNSettings.mm b/platform/darwin/src/MLNSettings.mm index b0d6d8038b7d..d39568114877 100644 --- a/platform/darwin/src/MLNSettings.mm +++ b/platform/darwin/src/MLNSettings.mm @@ -116,9 +116,6 @@ + (void)useWellKnownTileServer:(MLNWellKnownTileServer)tileServer { case MLNMapLibre: [MLNSettings setTileServerOptionsInternal:mbgl::TileServerOptions::MapLibreConfiguration()]; break; - case MLNMapbox: - [MLNSettings setTileServerOptionsInternal:mbgl::TileServerOptions::MapboxConfiguration()]; - break; default: [MLNSettings setTileServerOptionsInternal:mbgl::TileServerOptions::DefaultConfiguration()]; } @@ -229,4 +226,10 @@ + (MLNTileServerOptions*)tileServerOptions { } ++ (void)setUseWalJournal:(BOOL)useWalJournal { + auto tileServerOptions = [MLNSettings sharedSettings].tileServerOptionsInternal; + + [MLNSettings sharedSettings].tileServerOptionsInternal = &tileServerOptions->setUseWalJournal(useWalJournal); +} + @end diff --git a/platform/darwin/src/MLNShapeOfflineRegion.mm b/platform/darwin/src/MLNShapeOfflineRegion.mm index 8e24920809eb..beec58d83095 100644 --- a/platform/darwin/src/MLNShapeOfflineRegion.mm +++ b/platform/darwin/src/MLNShapeOfflineRegion.mm @@ -42,7 +42,7 @@ - (instancetype)init { } - (instancetype)initWithStyleURL:(NSURL *)styleURL shape:(MLNShape *)shape fromZoomLevel:(double)minimumZoomLevel toZoomLevel:(double)maximumZoomLevel { - MLNLogDebug(@"Initializing styleURL: %@ shape: %@ fromZoomLevel: %f toZoomLevel: %f", styleURL, shape, minimumZoomLevel, maximumZoomLevel); + MLNLogInfo(@"Initializing styleURL: %@ shape: %@ fromZoomLevel: %f toZoomLevel: %f", styleURL, shape, minimumZoomLevel, maximumZoomLevel); if (self = [super init]) { if (!styleURL) { styleURL = [MLNStyle defaultStyleURL]; diff --git a/platform/darwin/src/MLNStyle.mm b/platform/darwin/src/MLNStyle.mm index 3f4d211ff6a7..60c82c9ce331 100644 --- a/platform/darwin/src/MLNStyle.mm +++ b/platform/darwin/src/MLNStyle.mm @@ -162,7 +162,7 @@ - (void)setStyleJSON:(NSString *)styleJSON { } - (void)setSources:(NSSet<__kindof MLNSource *> *)sources { - MLNLogDebug(@"Setting: %lu sources", sources.count); + MLNLogInfo(@"Setting: %lu sources", sources.count); for (MLNSource *source in self.sources) { [self removeSource:source]; } @@ -211,7 +211,7 @@ - (MLNSource *)sourceFromMBGLSource:(mbgl::style::Source *)rawSource { - (void)addSource:(MLNSource *)source { - MLNLogDebug(@"Adding source: %@", source); + MLNLogInfo(@"Adding source: %@", source); if (!source.rawSource) { [NSException raise:NSInvalidArgumentException format: @"The source %@ cannot be added to the style. " @@ -232,7 +232,7 @@ - (void)removeSource:(MLNSource *)source } - (BOOL)removeSource:(MLNSource *)source error:(NSError * __nullable * __nullable)outError { - MLNLogDebug(@"Removing source: %@", source); + MLNLogInfo(@"Removing source: %@", source); if (!source.rawSource) { NSString *errorMessage = [NSString stringWithFormat: @@ -288,7 +288,7 @@ - (BOOL)removeSource:(MLNSource *)source error:(NSError * __nullable * __nullabl } - (void)setLayers:(NSArray<__kindof MLNStyleLayer *> *)layers { - MLNLogDebug(@"Setting: %lu layers", layers.count); + MLNLogInfo(@"Setting: %lu layers", layers.count); for (MLNStyleLayer *layer in self.layers) { [self removeLayer:layer]; } @@ -389,7 +389,7 @@ - (MLNStyleLayer *)layerWithIdentifier:(NSString *)identifier - (void)removeLayer:(MLNStyleLayer *)layer { - MLNLogDebug(@"Removing layer: %@", layer); + MLNLogInfo(@"Removing layer: %@", layer); if (!layer.rawLayer) { [NSException raise:NSInvalidArgumentException format: @"The style layer %@ cannot be removed from the style. " @@ -403,7 +403,7 @@ - (void)removeLayer:(MLNStyleLayer *)layer - (void)addLayer:(MLNStyleLayer *)layer { - MLNLogDebug(@"Adding layer: %@", layer); + MLNLogInfo(@"Adding layer: %@", layer); if (!layer.rawLayer) { [NSException raise:NSInvalidArgumentException format: @"The style layer %@ cannot be added to the style. " @@ -425,7 +425,7 @@ - (void)insertLayer:(MLNStyleLayer *)layer atIndex:(NSUInteger)index { - (void)insertLayer:(MLNStyleLayer *)layer belowLayer:(MLNStyleLayer *)sibling { - MLNLogDebug(@"Inseting layer: %@ belowLayer: %@", layer, sibling); + MLNLogInfo(@"Inserting layer: %@ belowLayer: %@", layer, sibling); if (!layer.rawLayer) { [NSException raise:NSInvalidArgumentException format: @@ -450,7 +450,7 @@ - (void)insertLayer:(MLNStyleLayer *)layer belowLayer:(MLNStyleLayer *)sibling } - (void)insertLayer:(MLNStyleLayer *)layer aboveLayer:(MLNStyleLayer *)sibling { - MLNLogDebug(@"Inseting layer: %@ aboveLayer: %@", layer, sibling); + MLNLogInfo(@"Inserting layer: %@ aboveLayer: %@", layer, sibling); if (!layer.rawLayer) { [NSException raise:NSInvalidArgumentException format: @@ -504,7 +504,7 @@ - (void)insertLayer:(MLNStyleLayer *)layer aboveLayer:(MLNStyleLayer *)sibling { - (void)setImage:(MLNImage *)image forName:(NSString *)name { - MLNLogDebug(@"Setting image: %@ forName: %@", image, name); + MLNLogInfo(@"Setting image: %@ forName: %@", image, name); if (!image) { [NSException raise:NSInvalidArgumentException format:@"Cannot assign nil image to “%@”.", name]; @@ -519,7 +519,7 @@ - (void)setImage:(MLNImage *)image forName:(NSString *)name - (void)removeImageForName:(NSString *)name { - MLNLogDebug(@"Removing imageForName: %@", name); + MLNLogInfo(@"Removing imageForName: %@", name); if (!name) { [NSException raise:NSInvalidArgumentException format:@"Cannot remove image with nil name."]; diff --git a/platform/darwin/src/MLNTilePyramidOfflineRegion.mm b/platform/darwin/src/MLNTilePyramidOfflineRegion.mm index e83d9d5958fd..b031005f6b20 100644 --- a/platform/darwin/src/MLNTilePyramidOfflineRegion.mm +++ b/platform/darwin/src/MLNTilePyramidOfflineRegion.mm @@ -38,7 +38,7 @@ - (instancetype)init { } - (instancetype)initWithStyleURL:(NSURL *)styleURL bounds:(MLNCoordinateBounds)bounds fromZoomLevel:(double)minimumZoomLevel toZoomLevel:(double)maximumZoomLevel { - MLNLogDebug(@"Initializing styleURL: %@ bounds: %@ fromZoomLevel: %f toZoomLevel: %f", styleURL, MLNStringFromCoordinateBounds(bounds), minimumZoomLevel, maximumZoomLevel); + MLNLogInfo(@"Initializing styleURL: %@ bounds: %@ fromZoomLevel: %f toZoomLevel: %f", styleURL, MLNStringFromCoordinateBounds(bounds), minimumZoomLevel, maximumZoomLevel); if (self = [super init]) { if (!styleURL) { styleURL = [MLNStyle defaultStyleURL]; diff --git a/platform/darwin/src/NSExpression+MLNAdditions.h b/platform/darwin/src/NSExpression+MLNAdditions.h index 265183687c7a..0ca455540f47 100644 --- a/platform/darwin/src/NSExpression+MLNAdditions.h +++ b/platform/darwin/src/NSExpression+MLNAdditions.h @@ -225,7 +225,7 @@ FOUNDATION_EXTERN MLN_EXPORT const MLNExpressionInterpolationMode @return An initialized expression equivalent to `object`, suitable for use as the value of a style layer attribute. */ -+ (instancetype)expressionWithMLNJSONObject:(id)object NS_SWIFT_NAME(init(mglJSONObject:)); ++ (instancetype)expressionWithMLNJSONObject:(id)object NS_SWIFT_NAME(init(mlnJSONObject:)); /** An equivalent Foundation object that can be serialized as JSON. diff --git a/platform/darwin/src/NSPredicate+MLNAdditions.h b/platform/darwin/src/NSPredicate+MLNAdditions.h index 7e5a9a0b5630..4f30b2c522e2 100644 --- a/platform/darwin/src/NSPredicate+MLNAdditions.h +++ b/platform/darwin/src/NSPredicate+MLNAdditions.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @return An initialized predicate equivalent to `object`, suitable for use with the ``MLNVectorStyleLayer/predicate`` property. */ -+ (instancetype)predicateWithMLNJSONObject:(id)object NS_SWIFT_NAME(init(mglJSONObject:)); ++ (instancetype)predicateWithMLNJSONObject:(id)object NS_SWIFT_NAME(init(mlnJSONObject:)); /** An equivalent Foundation object that can be serialized as JSON. diff --git a/platform/darwin/test/MLNResourceTests.mm b/platform/darwin/test/MLNResourceTests.mm index 581c6fd7bebe..a8cbb8db894d 100644 --- a/platform/darwin/test/MLNResourceTests.mm +++ b/platform/darwin/test/MLNResourceTests.mm @@ -4,7 +4,6 @@ namespace mbgl { extern NSURL *resourceURL(const Resource& resource); - extern BOOL isValidMapboxEndpoint(NSURL *url); } @interface MLNResourceTests : XCTestCase @@ -12,20 +11,6 @@ @interface MLNResourceTests : XCTestCase @implementation MLNResourceTests -- (void)testValidEndpoints { - using namespace mbgl; - - XCTAssertTrue(isValidMapboxEndpoint([NSURL URLWithString:@"https://mapbox.com"])); - XCTAssertTrue(isValidMapboxEndpoint([NSURL URLWithString:@"https://mapbox.cn"])); - XCTAssertTrue(isValidMapboxEndpoint([NSURL URLWithString:@"https://example.mapbox.com"])); - XCTAssertTrue(isValidMapboxEndpoint([NSURL URLWithString:@"https://example.mapbox.cn"])); - - XCTAssertFalse(isValidMapboxEndpoint([NSURL URLWithString:@"https://example.com"])); - XCTAssertFalse(isValidMapboxEndpoint([NSURL URLWithString:@"https://example.cn"])); - XCTAssertFalse(isValidMapboxEndpoint([NSURL URLWithString:@"https://examplemapbox.com"])); - XCTAssertFalse(isValidMapboxEndpoint([NSURL URLWithString:@"https://examplemapbox.cn"])); -} - - (void)internalTestOfflineQueryParameterIsAddedForOfflineResource:(std::string)testURL { using namespace mbgl; @@ -77,12 +62,7 @@ - (void)internalTestOfflineQueryParameterIsAddedForOfflineResource:(std::string) } - (void)testOfflineQueryParameterIsAddedForOfflineResource { - std::string testURL = "test://mapbox.com/testing_offline_query?a=one&b=two"; - [self internalTestOfflineQueryParameterIsAddedForOfflineResource:testURL]; -} - -- (void)testOfflineQueryParameterIsAddedForOfflineResourceForChina { - std::string testURL = "test://mapbox.cn/testing_offline_query?a=one&b=two"; + std::string testURL = "test://outdooractive.com/testing_offline_query?a=one&b=two"; [self internalTestOfflineQueryParameterIsAddedForOfflineResource:testURL]; } diff --git a/platform/default/BUILD.bazel b/platform/default/BUILD.bazel index e22c12db9526..574df5e7fc0c 100644 --- a/platform/default/BUILD.bazel +++ b/platform/default/BUILD.bazel @@ -100,6 +100,7 @@ cc_library( "include/mbgl/storage/file_source_request.hpp", "include/mbgl/storage/local_file_request.hpp", "include/mbgl/storage/merge_sideloaded.hpp", + "include/mbgl/storage/merge_tilepack.hpp", "include/mbgl/storage/offline_database.hpp", "include/mbgl/storage/offline_download.hpp", "include/mbgl/storage/offline_schema.hpp", diff --git a/platform/default/include/mbgl/storage/merge_tilepack.hpp b/platform/default/include/mbgl/storage/merge_tilepack.hpp new file mode 100644 index 000000000000..ce9d0b7e58dd --- /dev/null +++ b/platform/default/include/mbgl/storage/merge_tilepack.hpp @@ -0,0 +1,48 @@ +#pragma once + +// THIS IS A GENERATED FILE; EDIT merge_tilepack.sql INSTEAD +// To regenerate, run `node platform/default/include/mbgl/storage/merge_tilepack.js` + +namespace mbgl { + +static constexpr const char* mergeTilepackDatabaseSQL = +"INSERT INTO regions\n" +" SELECT DISTINCT NULL, sr.definition, sr.description\n" +" FROM side.regions sr \n" +" LEFT JOIN regions r ON sr.definition = r.definition AND sr.description IS r.description\n" +" WHERE r.definition IS NULL;\n" +"REPLACE INTO tiles\n" +" SELECT t.id,\n" +" st.url_template, st.pixel_ratio, st.z, st.x, st.y,\n" +" st.expires, st.modified, st.etag, st.data, st.compressed, st.accessed, st.must_revalidate\n" +" FROM (SELECT DISTINCT sti.* FROM side.region_tiles srt JOIN side.tiles sti ON srt.tile_id = sti.id)\n" +" AS st\n" +" LEFT JOIN tiles t ON st.url_template = t.url_template AND st.pixel_ratio = t.pixel_ratio AND st.z = t.z AND st.x = t.x AND st.y = t.y\n" +" WHERE t.id IS NULL\n" +" OR st.modified > t.modified;\n" +"INSERT OR IGNORE INTO region_tiles\n" +" SELECT rm.main_region_id, sti.id\n" +" FROM side.region_tiles srt\n" +" JOIN region_mapping rm ON srt.region_id = rm.side_region_id\n" +" JOIN (SELECT t.id, st.id AS side_tile_id FROM side.tiles st\n" +" JOIN tiles t ON st.url_template = t.url_template AND st.pixel_ratio = t.pixel_ratio AND st.z = t.z AND st.x = t.x AND st.y = t.y\n" +" ) AS sti ON srt.tile_id = sti.side_tile_id;\n" +"REPLACE INTO resources\n" +" SELECT r.id, \n" +" sr.url, sr.kind, sr.expires, sr.modified, sr.etag,\n" +" sr.data, sr.compressed, sr.accessed, sr.must_revalidate\n" +" FROM side.region_resources srr JOIN side.resources sr ON srr.resource_id = sr.id\n" +" LEFT JOIN resources r ON sr.url = r.url\n" +" WHERE r.id IS NULL\n" +" OR sr.modified > r.modified;\n" +"INSERT OR IGNORE INTO region_resources\n" +" SELECT rm.main_region_id, sri.id\n" +" FROM side.region_resources srr\n" +" JOIN region_mapping rm ON srr.region_id = rm.side_region_id\n" +" JOIN (SELECT r.id, sr.id AS side_resource_id FROM side.resources sr\n" +" JOIN resources r ON sr.url = r.url) AS sri ON srr.resource_id = sri.side_resource_id;\n" +" \n" +"DROP TABLE region_mapping;\n" +; + +} // namespace mbgl diff --git a/platform/default/include/mbgl/storage/merge_tilepack.js b/platform/default/include/mbgl/storage/merge_tilepack.js new file mode 100644 index 000000000000..e88a1d22beee --- /dev/null +++ b/platform/default/include/mbgl/storage/merge_tilepack.js @@ -0,0 +1,21 @@ +var fs = require('fs'); +fs.writeFileSync('platform/default/include/mbgl/storage/merge_tilepack.hpp', `#pragma once + +// THIS IS A GENERATED FILE; EDIT merge_tilepack.sql INSTEAD +// To regenerate, run \`node platform/default/include/mbgl/storage/merge_tilepack.js\` + +namespace mbgl { + +static constexpr const char* mergeTilepackDatabaseSQL = +${fs.readFileSync('platform/default/include/mbgl/storage/merge_tilepack.sql', 'utf8') + .replace(/ *--.*/g, '') + .split('\n') + .filter(a => a) + .map(line => '"' + line + '\\n"') + .join('\n') +} +; + +} // namespace mbgl +`); + diff --git a/platform/default/include/mbgl/storage/merge_tilepack.sql b/platform/default/include/mbgl/storage/merge_tilepack.sql new file mode 100644 index 000000000000..e8cf30a648fa --- /dev/null +++ b/platform/default/include/mbgl/storage/merge_tilepack.sql @@ -0,0 +1,45 @@ +INSERT INTO regions + SELECT DISTINCT NULL, sr.definition, sr.description -- Merge duplicate regions + FROM side.regions sr + LEFT JOIN regions r ON sr.definition = r.definition AND sr.description IS r.description + WHERE r.definition IS NULL; + +--Insert /Update tiles +REPLACE INTO tiles + SELECT t.id, -- use the old ID in case we run a REPLACE. If it doesn't exist yet, it'll be NULL which will auto-assign a new ID. + st.url_template, st.pixel_ratio, st.z, st.x, st.y, + st.expires, st.modified, st.etag, st.data, st.compressed, st.accessed, st.must_revalidate + FROM (SELECT DISTINCT sti.* FROM side.region_tiles srt JOIN side.tiles sti ON srt.tile_id = sti.id) -- ensure that we're only considering region tiles, and not ambient tiles. + AS st + LEFT JOIN tiles t ON st.url_template = t.url_template AND st.pixel_ratio = t.pixel_ratio AND st.z = t.z AND st.x = t.x AND st.y = t.y + WHERE t.id IS NULL -- only consider tiles that don't exist yet in the original database. + OR st.modified > t.modified; -- ...or tiles that are newer in the side loaded DB. + +-- Update region_tiles usage +INSERT OR IGNORE INTO region_tiles + SELECT rm.main_region_id, sti.id + FROM side.region_tiles srt + JOIN region_mapping rm ON srt.region_id = rm.side_region_id + JOIN (SELECT t.id, st.id AS side_tile_id FROM side.tiles st + JOIN tiles t ON st.url_template = t.url_template AND st.pixel_ratio = t.pixel_ratio AND st.z = t.z AND st.x = t.x AND st.y = t.y + ) AS sti ON srt.tile_id = sti.side_tile_id; + +-- copy over resources +REPLACE INTO resources + SELECT r.id, + sr.url, sr.kind, sr.expires, sr.modified, sr.etag, + sr.data, sr.compressed, sr.accessed, sr.must_revalidate + FROM side.region_resources srr JOIN side.resources sr ON srr.resource_id = sr.id --only consider region resources, and not ambient resources. + LEFT JOIN resources r ON sr.url = r.url + WHERE r.id IS NULL -- only consider resources that don't exist yet in the main database + OR sr.modified > r.modified; -- ...or resources that are newer in the side loaded DB. + +-- Update region_resources usage +INSERT OR IGNORE INTO region_resources + SELECT rm.main_region_id, sri.id + FROM side.region_resources srr + JOIN region_mapping rm ON srr.region_id = rm.side_region_id + JOIN (SELECT r.id, sr.id AS side_resource_id FROM side.resources sr + JOIN resources r ON sr.url = r.url) AS sri ON srr.resource_id = sri.side_resource_id; + +DROP TABLE region_mapping; diff --git a/platform/default/include/mbgl/storage/offline_database.hpp b/platform/default/include/mbgl/storage/offline_database.hpp index 7c3af65f3a38..0035a19d3604 100644 --- a/platform/default/include/mbgl/storage/offline_database.hpp +++ b/platform/default/include/mbgl/storage/offline_database.hpp @@ -32,11 +32,6 @@ namespace util { struct IOException; } // namespace util -struct MapboxTileLimitExceededException : util::Exception { - MapboxTileLimitExceededException() - : util::Exception("Mapbox tile limit exceeded") {} -}; - class OfflineDatabase { public: OfflineDatabase(std::string path, const TileServerOptions& options); @@ -71,6 +66,8 @@ class OfflineDatabase { expected mergeDatabase(const std::string& sideDatabasePath); + expected mergeTilepack(const std::string& sideDatabasePath, const int64_t regionID); + expected updateMetadata(int64_t regionID, const OfflineRegionMetadata&); std::exception_ptr deleteRegion(OfflineRegion&&); @@ -86,11 +83,6 @@ class OfflineDatabase { expected getRegionCompletedStatus(int64_t regionID); std::exception_ptr setMaximumAmbientCacheSize(uint64_t); - void setOfflineMapboxTileCountLimit(uint64_t); - uint64_t getOfflineMapboxTileCountLimit(); - bool offlineMapboxTileCountLimitExceeded(); - uint64_t getOfflineMapboxTileCount(); - bool exceedsOfflineMapboxTileCountLimit(const Resource&); void markUsedResources(int64_t regionID, const std::list&); std::exception_ptr pack(); void runPackDatabaseAutomatically(bool autopack_) { autopack = autopack_; } @@ -98,8 +90,6 @@ class OfflineDatabase { void reopenDatabaseReadOnly(bool readOnly); private: - class DatabaseSizeChangeStats; - void initialize(); void handleError(const mapbox::sqlite::Exception&, const char* action); void handleError(const util::IOException&, const char* action); @@ -112,6 +102,7 @@ class OfflineDatabase { void migrateToVersion5(); void migrateToVersion3(); void migrateToVersion6(); + void migrateToVersion7(); void cleanup(); bool disabled(); void vacuum(); @@ -147,42 +138,14 @@ class OfflineDatabase { T getPragma(const char*); uint64_t maximumAmbientCacheSize = util::DEFAULT_MAX_CACHE_SIZE; - uint64_t offlineMapboxTileCountLimit = util::mapbox::DEFAULT_OFFLINE_TILE_COUNT_LIMIT; - - std::optional offlineMapboxTileCount; - bool evict(uint64_t neededFreeSize, DatabaseSizeChangeStats& stats); + bool evict(uint64_t neededFreeSize, bool limitNumberOfResourcesToDelete); TileServerOptions tileServerOptions; - class DatabaseSizeChangeStats { - public: - explicit DatabaseSizeChangeStats(OfflineDatabase*); - - // Returns difference between current database size and - // database size at the time of creation of this object. - int64_t diff() const; - - // Returns how many bytes were released comparing to a database - // size at the time of creation of this object. - uint64_t bytesReleased() const; - - // Returns page size for the database. - uint64_t pageSize() const; - - private: - uint64_t pageSize_ = 0u; - uint64_t pageCount_ = 0u; - uint64_t initialSize_ = 0u; - OfflineDatabase* db = nullptr; - }; - - friend class DatabaseSizeChangeStats; - // Lazily initializes currentAmbientCacheSize. std::exception_ptr initAmbientCacheSize(); std::optional currentAmbientCacheSize; - void updateAmbientCacheSize(DatabaseSizeChangeStats&); bool autopack = true; bool readOnly = false; diff --git a/platform/default/include/mbgl/storage/offline_download.hpp b/platform/default/include/mbgl/storage/offline_download.hpp index 42bd9d5b8228..7411bb907feb 100644 --- a/platform/default/include/mbgl/storage/offline_download.hpp +++ b/platform/default/include/mbgl/storage/offline_download.hpp @@ -49,8 +49,6 @@ class OfflineDownload { */ void ensureResource(Resource&&, std::function = {}); - void onMapboxTileCountLimitExceeded(); - int64_t id; OfflineRegionDefinition definition; OfflineDatabase& offlineDatabase; diff --git a/platform/default/src/mbgl/storage/database_file_source.cpp b/platform/default/src/mbgl/storage/database_file_source.cpp index 884c02f247d2..5bb93ffacbbd 100644 --- a/platform/default/src/mbgl/storage/database_file_source.cpp +++ b/platform/default/src/mbgl/storage/database_file_source.cpp @@ -26,14 +26,36 @@ class DatabaseFileSourceThread { std::optional offlineResponse = (resource.storagePolicy != Resource::StoragePolicy::Volatile) ? db->get(resource) : std::nullopt; + if (!offlineResponse && resource.storagePolicy != Resource::StoragePolicy::Volatile) { + std::string url = resource.url; + size_t webpPos = url.rfind(".webp"); + if (webpPos != std::string::npos) { + Resource alternateResource = resource; + alternateResource.url = url.substr(0, webpPos) + ".png" + url.substr(webpPos + 5); + offlineResponse = db->get(alternateResource); + } + } if (!offlineResponse) { offlineResponse.emplace(); offlineResponse->noContent = true; offlineResponse->error = std::make_unique(Response::Error::Reason::NotFound, "Not found in offline database"); - } else if (!offlineResponse->isUsable()) { - offlineResponse->error = std::make_unique(Response::Error::Reason::NotFound, - "Cached resource is unusable"); + } + else if (!offlineResponse->isUsable()) { + // OA improvement: We never want to not use available resources in the database + // unless they are required to revalidate + // TODO: Check how resources get updated + if (offlineResponse->mustRevalidate) { + Log::Info(Event::Database, "Cached resource is marked as must-revalidate -> not using it, expires=" + (offlineResponse->expires ? util::iso8601(*offlineResponse->expires) : std::string("n/a")) + " UTC, url=" + resource.url); + offlineResponse->error = std::make_unique(Response::Error::Reason::NotFound, + "Cached resource is unusable"); + } + else { + Log::Info(Event::Database, "Cached resource is marked as unusable/expired, but using it anyway, expires=" + (offlineResponse->expires ? util::iso8601(*offlineResponse->expires) : std::string("n/a")) + " UTC, url=" + resource.url); + } + } + else { + Log::Info(Event::Database, "Cached resource is usable, expires=" + (offlineResponse->expires ? util::iso8601(*offlineResponse->expires) : std::string("n/a")) + " UTC, url=" + resource.url); } req.invoke(&FileSourceRequest::setResponse, *offlineResponse); } @@ -92,6 +114,12 @@ class DatabaseFileSourceThread { callback(db->mergeDatabase(sideDatabasePath)); } + void mergeTilepack(const std::string& sideDatabasePath, + const int64_t regionID, + const std::function)>& callback) { + callback(db->mergeTilepack(sideDatabasePath, regionID)); + } + void updateMetadata(const int64_t regionID, const OfflineRegionMetadata& metadata, const std::function)>& callback) { @@ -128,8 +156,6 @@ class DatabaseFileSourceThread { } } - void setOfflineMapboxTileCountLimit(uint64_t limit) { db->setOfflineMapboxTileCountLimit(limit); } - void reopenDatabaseReadOnly(bool readOnly) { db->reopenDatabaseReadOnly(readOnly); } private: @@ -287,6 +313,14 @@ void DatabaseFileSource::mergeOfflineRegions( impl->actor().invoke(&DatabaseFileSourceThread::mergeOfflineRegions, sideDatabasePath, std::move(callback)); } +void DatabaseFileSource::mergeTilepack( + const std::string& sideDatabasePath, + const int64_t regionID, + std::function)> callback) +{ + impl->actor().invoke(&DatabaseFileSourceThread::mergeTilepack, sideDatabasePath, regionID, std::move(callback)); +} + void DatabaseFileSource::updateOfflineMetadata( const int64_t regionID, const OfflineRegionMetadata& metadata, @@ -319,10 +353,6 @@ void DatabaseFileSource::getOfflineRegionStatus( impl->actor().invoke(&DatabaseFileSourceThread::getRegionStatus, region.getID(), std::move(callback)); } -void DatabaseFileSource::setOfflineMapboxTileCountLimit(uint64_t limit) const { - impl->actor().invoke(&DatabaseFileSourceThread::setOfflineMapboxTileCountLimit, limit); -} - void DatabaseFileSource::setProperty(const std::string& key, const mapbox::base::Value& value) { if (key == READ_ONLY_MODE_KEY && value.getBool()) { impl->actor().invoke(&DatabaseFileSourceThread::reopenDatabaseReadOnly, *value.getBool()); diff --git a/platform/default/src/mbgl/storage/local_file_request.cpp b/platform/default/src/mbgl/storage/local_file_request.cpp index 8c6cf43d808f..9e928264bd87 100644 --- a/platform/default/src/mbgl/storage/local_file_request.cpp +++ b/platform/default/src/mbgl/storage/local_file_request.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -19,15 +20,18 @@ void requestLocalFile(const std::string& path, int result = stat(path.c_str(), &buf); if (result == 0 && (S_IFDIR & buf.st_mode)) { + Log::Info(Event::HttpRequest, "File resource is directory, url=" + path); response.error = std::make_unique(Response::Error::Reason::NotFound); } else if (result == -1 && errno == ENOENT) { response.error = std::make_unique(Response::Error::Reason::NotFound); } else { auto data = util::readFile(path, dataRange); if (!data) { + Log::Info(Event::HttpRequest, "Cannot read file resource, url=" + path); response.error = std::make_unique(Response::Error::Reason::Other, std::string("Cannot read file ") + path); } else { + Log::Info(Event::HttpRequest, "File resource loaded, url=" + path); response.data = std::make_shared(std::move(*data)); } } diff --git a/platform/default/src/mbgl/storage/main_resource_loader.cpp b/platform/default/src/mbgl/storage/main_resource_loader.cpp index 063443387729..685f700d226c 100644 --- a/platform/default/src/mbgl/storage/main_resource_loader.cpp +++ b/platform/default/src/mbgl/storage/main_resource_loader.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -44,11 +45,16 @@ class MainResourceLoaderThread { // Keep parent request alive while chained request is being processed. std::shared_ptr parentKeepAlive = std::move(parent); + Log::Info(Event::HttpRequest, "Fetching resource from network, url=" + res.url); + MBGL_TIMING_START(watch); return onlineFileSource->request(res, [=, ptr = parentKeepAlive, this](const Response& response) { if (databaseFileSource) { databaseFileSource->forward(res, response, nullptr); } + + Log::Info(Event::HttpRequest, "Network response, size=" + std::to_string(response.data != nullptr ? response.data->size() : 0) + ", noContent=" + (response.noContent ? std::string("true") : std::string("false")) + ", notModified=" + (response.notModified ? std::string("true") : std::string("false")) + ", must-revalidate=" + (response.mustRevalidate ? std::string("true") : std::string("false")) + ", expires=" + (response.expires ? util::iso8601(*response.expires) : std::string("n/a")) + " UTC, url=" + res.url); + if (res.kind == Resource::Kind::Tile) { // onlineResponse.data will be null if data not modified MBGL_TIMING_FINISH(watch, @@ -69,31 +75,52 @@ class MainResourceLoaderThread { if (assetFileSource && assetFileSource->canRequest(resource)) { // Asset request tasks[req] = assetFileSource->request(resource, callback); + Log::Info(Event::HttpRequest, "Fetching resource from assets, url=" + resource.url); } else if (mbtilesFileSource && mbtilesFileSource->canRequest(resource)) { // Local file request tasks[req] = mbtilesFileSource->request(resource, callback); + Log::Info(Event::HttpRequest, "Fetching resource from mbtiles, url=" + resource.url); } else if (pmtilesFileSource && pmtilesFileSource->canRequest(resource)) { // Local file request tasks[req] = pmtilesFileSource->request(resource, callback); + Log::Info(Event::HttpRequest, "Fetching resource from pmtiles, url=" + resource.url); } else if (localFileSource && localFileSource->canRequest(resource)) { // Local file request tasks[req] = localFileSource->request(resource, callback); + Log::Info(Event::HttpRequest, "Fetching resource from local file, url=" + resource.url); } else if (databaseFileSource && databaseFileSource->canRequest(resource)) { // Try cache only request if needed. if (resource.loadingMethod == Resource::LoadingMethod::CacheOnly) { tasks[req] = databaseFileSource->request(resource, callback); + Log::Info(Event::HttpRequest, "Fetching resource from database, url=" + resource.url); } else { // Cache request with fallback to network with cache control tasks[req] = databaseFileSource->request(resource, [=, this](const Response& response) { Resource res = resource; - // Resource is in the cache - if (!response.noContent) { + // Resource is in the cache? + if (response.noContent) { + Log::Info(Event::HttpRequest, "Not in database - fallback to network, url=" + resource.url); + } + else { + Log::Info(Event::HttpRequest, "Got resource from database, isUsable=" + (response.isUsable() ? std::string("true") : std::string("false")) + ", must-revalidate=" + (response.mustRevalidate ? std::string("true") : std::string("false")) + ", expires=" + (response.expires ? util::iso8601(*response.expires) : std::string("n/a")) + " UTC, url=" + resource.url); + if (response.isUsable()) { + // OA update: Always use offline resources if available callback(response); + // Set the priority of existing resource to low if it's expired but usable. res.setPriority(Resource::Priority::Low); - } else { + + // OA update: Early exit when the resource exists, is usable + // and not expired. + // Tiles automatically use the database first and exit if available + // (see tile_loader_impl.hpp) + if (response.isFresh()) { + return; + } + } + else { // Set prior data only if it was not returned to // the requester. Once we get 304 response from // the network, we will forward response to the diff --git a/platform/default/src/mbgl/storage/offline_database.cpp b/platform/default/src/mbgl/storage/offline_database.cpp index ec42912449b5..fa59b363d9d8 100644 --- a/platform/default/src/mbgl/storage/offline_database.cpp +++ b/platform/default/src/mbgl/storage/offline_database.cpp @@ -10,6 +10,9 @@ #include #include +#include + +#include namespace mbgl { @@ -67,6 +70,9 @@ void OfflineDatabase::initialize() { migrateToVersion6(); // fall through case 6: + migrateToVersion7(); + // fall through + case 7: // Happy path; we're done return; default: @@ -77,7 +83,7 @@ void OfflineDatabase::initialize() { } void OfflineDatabase::changePath(const std::string& path_) { - Log::Info(Event::Database, "Changing the database path."); + Log::Info(Event::Database, "Changing the database path to " + path_); cleanup(); path = path_; initialize(); @@ -137,8 +143,6 @@ void OfflineDatabase::handleError(const char* action) { handleError(ex, action); } catch (const util::IOException& ex) { handleError(ex, action); - } catch (const MapboxTileLimitExceededException&) { - throw; // This is ours and must be handled on client side. } catch (const std::runtime_error& ex) { handleError(ex, action); } catch (...) { @@ -148,7 +152,7 @@ void OfflineDatabase::handleError(const char* action) { } void OfflineDatabase::removeExisting() { - Log::Warning(Event::Database, "Removing existing incompatible offline database"); + Log::Warning(Event::Database, "Removing existing offline database"); statements.clear(); db.reset(); @@ -169,11 +173,25 @@ void OfflineDatabase::createSchema() { checkFlags(); vacuum(); - db->exec("PRAGMA journal_mode = DELETE"); - db->exec("PRAGMA synchronous = FULL"); + + if (tileServerOptions.useWalJournal()) { + Log::Info(Event::Database, "Using journal_mode=WAL in the offline db"); + db->exec("PRAGMA journal_mode = WAL"); + db->exec("PRAGMA synchronous = NORMAL"); + db->exec("PRAGMA temp_store = MEMORY"); + } + else { + Log::Info(Event::Database, "Using journal_mode=DELETE in the offline db"); + db->exec("PRAGMA journal_mode = DELETE"); + db->exec("PRAGMA synchronous = FULL"); + } + + db->exec("PRAGMA auto_vacuum = INCREMENTAL"); + db->exec("PRAGMA page_size = 4096"); // Actually the default + mapbox::sqlite::Transaction transaction(*db); db->exec(offlineDatabaseSchema); - db->exec("PRAGMA user_version = 6"); + db->exec("PRAGMA user_version = 7"); transaction.commit(); } @@ -215,6 +233,16 @@ void OfflineDatabase::migrateToVersion6() { transaction.commit(); } +void OfflineDatabase::migrateToVersion7() { + assert(db); + checkFlags(); + + // There is no evident change between v6 and v7 + mapbox::sqlite::Transaction transaction(*db); + db->exec("PRAGMA user_version = 7"); + transaction.commit(); +} + void OfflineDatabase::vacuum() { assert(db); checkFlags(); @@ -225,6 +253,9 @@ void OfflineDatabase::vacuum() { } else { db->exec("PRAGMA incremental_vacuum"); } + + db->exec("PRAGMA wal_checkpoint(TRUNCATE)"); // Does nothing if WAL is not enabled + db->exec("PRAGMA optimize"); } void OfflineDatabase::checkFlags() { @@ -313,10 +344,8 @@ std::pair OfflineDatabase::putInternal(const Resource& resource, size = compressed ? compressedData.size() : response.data->size(); } - std::optional stats; if (evict_) { - stats = DatabaseSizeChangeStats(this); - if (!evict(size, *stats)) { + if (!evict(1, true)) { Log::Info(Event::Database, "Unable to make space for entry"); return {false, 0}; } @@ -341,8 +370,8 @@ std::pair OfflineDatabase::putInternal(const Resource& resource, compressed); } - if (stats) { - updateAmbientCacheSize(*stats); + if (inserted && currentAmbientCacheSize) { + *currentAmbientCacheSize += 1; } return {inserted, size}; @@ -746,8 +775,10 @@ std::exception_ptr OfflineDatabase::clearAmbientCache() try { // clang-format off mapbox::sqlite::Query tileQuery{ getStatement( "DELETE FROM tiles " - "WHERE id NOT IN (" - " SELECT tile_id FROM region_tiles" + "WHERE id IN (" + " SELECT id FROM tiles" + " EXCEPT" + " SELECT tile_id FROM region_tiles" ")" ) }; // clang-format on @@ -757,14 +788,18 @@ std::exception_ptr OfflineDatabase::clearAmbientCache() try { // clang-format off mapbox::sqlite::Query resourceQuery{ getStatement( "DELETE FROM resources " - "WHERE id NOT IN (" - " SELECT resource_id FROM region_resources" + "WHERE id IN (" + " SELECT id FROM resources" + " EXCEPT" + " SELECT resource_id FROM region_resources" ")" ) }; // clang-format on resourceQuery.run(); + currentAmbientCacheSize.reset(); + if (autopack) vacuum(); return nullptr; @@ -892,38 +927,15 @@ expected OfflineDatabase::mergeDatabase(cons return unexpected(std::current_exception()); } try { - // Support sideloaded databases at user_version = 6. Future schema + // Support sideloaded databases at user_version = 7. Future schema // version changes will need to implement migration paths for sideloaded - // databases at version 6. + // databases at version 7. auto sideUserVersion = static_cast(getPragma("PRAGMA side.user_version")); const auto mainUserVersion = getPragma("PRAGMA user_version"); - if (sideUserVersion < 6 || sideUserVersion != mainUserVersion) { + if (sideUserVersion < 7 || sideUserVersion != mainUserVersion) { throw std::runtime_error("Merge database has incorrect user_version"); } - auto currentTileCount = getOfflineMapboxTileCount(); - // clang-format off - mapbox::sqlite::Query queryTiles{ getStatement( - "SELECT COUNT(DISTINCT st.id) " - "FROM side.tiles st " - //only consider region tiles, and not ambient tiles. - "JOIN side.region_tiles srt ON srt.tile_id = st.id " - "LEFT JOIN tiles t ON st.url_template = t.url_template AND " - "st.pixel_ratio = t.pixel_ratio AND " - "st.z = t.z AND " - "st.x = t.x AND " - "st.y = t.y " - "WHERE t.id IS NULL " - "AND st.url_template LIKE ?1 || '%'") }; - // clang-format on - queryTiles.bind(1, tileServerOptions.uriSchemeAlias() + "://"); - queryTiles.run(); - auto countOfTilesToMerge = queryTiles.get(0); - if ((countOfTilesToMerge + currentTileCount) > offlineMapboxTileCountLimit) { - throw MapboxTileLimitExceededException(); - } - queryTiles.reset(); - mapbox::sqlite::Transaction transaction(*db); db->exec(mergeSideloadedDatabaseSQL); transaction.commit(); @@ -955,6 +967,82 @@ expected OfflineDatabase::mergeDatabase(cons return {}; } +expected +OfflineDatabase::mergeTilepack(const std::string& sideDatabasePath, const int64_t regionID) { + checkFlags(); + + try { + // clang-format off + mapbox::sqlite::Query query{ getStatement("ATTACH DATABASE ?1 AS side") }; + // clang-format on + + query.bind(1, sideDatabasePath); + query.run(); + } catch (const mapbox::sqlite::Exception& ex) { + Log::Error(Event::Database, static_cast(ex.code), "Can't attach database (" + sideDatabasePath + ") for merge: " + ex.what()); + return unexpected(std::current_exception()); + } + + // Tilepacks always have one region containing all resources/tiles. + try { + // clang-format off + mapbox::sqlite::Query query{ getStatement( + "CREATE TEMPORARY TABLE region_mapping AS " + " SELECT 1 AS side_region_id, ?1 AS main_region_id" + )}; + // clang-format on + + query.bind(1, regionID); + query.run(); + } catch (const mapbox::sqlite::Exception& ex) { + Log::Error(Event::Database, static_cast(ex.code), std::string("Can't create a necessary temp table for merge: ") + ex.what()); + return unexpected(std::current_exception()); + } + + try { + // Support sideloaded databases at user_version = 7. Future schema version + // changes will need to implement migration paths for sideloaded databases at + // version 7. + auto sideUserVersion = static_cast(getPragma("PRAGMA side.user_version")); + const auto mainUserVersion = getPragma("PRAGMA user_version"); + if (sideUserVersion < 7 || sideUserVersion != mainUserVersion) { + throw std::runtime_error("Tilepack has incorrect user_version"); + } + + mapbox::sqlite::Transaction transaction(*db); + db->exec(mergeTilepackDatabaseSQL); + transaction.commit(); + + // clang-format off + mapbox::sqlite::Query queryRegions{ getStatement( + "SELECT DISTINCT r.id, r.definition, r.description " + "FROM side.regions sr " + "JOIN regions r ON sr.definition = r.definition AND sr.description IS r.description" + )}; + // clang-format on + + OfflineRegions result; + while (queryRegions.run()) { + // Construct, then move because this constructor is private. + OfflineRegion region(queryRegions.get(0), + decodeOfflineRegionDefinition(queryRegions.get(1)), + queryRegions.get>(2)); + result.emplace_back(std::move(region)); + } + + db->exec("DETACH DATABASE side"); + // Explicit move to avoid triggering the copy constructor. + return { std::move(result) }; + } catch (const std::runtime_error& ex) { + db->exec("DETACH DATABASE side"); + Log::Error(Event::Database, ex.what()); + + return unexpected(std::current_exception()); + } + + return {}; +} + expected OfflineDatabase::updateMetadata( const int64_t regionID, const OfflineRegionMetadata& metadata) try { checkFlags(); @@ -983,14 +1071,12 @@ std::exception_ptr OfflineDatabase::deleteRegion(OfflineRegion&& region) try { query.run(); } - DatabaseSizeChangeStats stats(this); - evict(0, stats); + currentAmbientCacheSize.reset(); + evict(0, true); + assert(db); if (autopack) vacuum(); - updateAmbientCacheSize(stats); - // Ensure that the cached offlineTileCount value is recalculated. - offlineMapboxTileCount = std::nullopt; return nullptr; } catch (...) { handleError("delete region"); @@ -1048,18 +1134,12 @@ void OfflineDatabase::putRegionResources(int64_t regionID, const auto& resource = std::get<0>(elem); const auto& response = std::get<1>(elem); - try { - uint64_t resourceSize = putRegionResourceInternal(regionID, resource, response); - completedResourceCount++; - completedResourceSize += resourceSize; - if (resource.kind == Resource::Kind::Tile) { - completedTileCount += 1; - completedTileSize += resourceSize; - } - } catch (const MapboxTileLimitExceededException&) { - // Commit the rest of the batch and rethrow - transaction.commit(); - throw; + uint64_t resourceSize = putRegionResourceInternal(regionID, resource, response); + completedResourceCount++; + completedResourceSize += resourceSize; + if (resource.kind == Resource::Kind::Tile) { + completedTileCount += 1; + completedTileSize += resourceSize; } } @@ -1080,16 +1160,7 @@ uint64_t OfflineDatabase::putRegionResourceInternal(int64_t regionID, checkFlags(); uint64_t size = putInternal(resource, response, false).second; - bool previouslyUnused = markUsed(regionID, resource); - - if (previouslyUnused && exceedsOfflineMapboxTileCountLimit(resource)) { - throw MapboxTileLimitExceededException(); - } - - if (offlineMapboxTileCount && resource.kind == Resource::Kind::Tile && - util::mapbox::isCanonicalURL(tileServerOptions, resource.url) && previouslyUnused) { - *offlineMapboxTileCount += 1; - } + markUsed(regionID, resource); return size; } @@ -1236,23 +1307,23 @@ T OfflineDatabase::getPragma(const char* sql) { return query.get(0); } -// Remove least-recently used resources and tiles until the used database size, -// as calculated by multiplying the number of in-use pages by the page size, is +// Remove least-recently used resources and tiles until the used size is // less than the maximum cache size. Returns false if this condition cannot be // satisfied. -// -// SQLite database never shrinks in size unless we call VACUUM. We here -// are monitoring the soft limit (i.e. number of free pages in the file) -// and as it approaches to the hard limit (i.e. the actual file size) we -// delete an arbitrary number of old cache entries. The free pages approach -// saves us from calling VACUUM or keeping a running total, which can be costly. -bool OfflineDatabase::evict(uint64_t neededFreeSize, DatabaseSizeChangeStats& stats) { +bool OfflineDatabase::evict(uint64_t neededFreeSize, bool limitNumberOfResourcesToDelete) { checkFlags(); uint64_t ambientCacheSize = (initAmbientCacheSize() == nullptr) ? *currentAmbientCacheSize : maximumAmbientCacheSize; - uint64_t newAmbientCacheSize = ambientCacheSize + neededFreeSize + stats.pageSize(); + uint64_t newAmbientCacheSize = ambientCacheSize + neededFreeSize; + + if (newAmbientCacheSize > maximumAmbientCacheSize) { + uint64_t resourcesToDelete = (newAmbientCacheSize - maximumAmbientCacheSize) + 100; + if (limitNumberOfResourcesToDelete) { + // Don't delete more than 200 elements to prevent hangs + resourcesToDelete = std::min(uint64_t(200), resourcesToDelete); + } + Log::Info(Event::Database, "evict ambient cache, new = " + std::to_string(newAmbientCacheSize) + ", maximum = " + std::to_string(maximumAmbientCacheSize) + ", removing " + std::to_string(resourcesToDelete)); - while (newAmbientCacheSize > maximumAmbientCacheSize) { // clang-format off mapbox::sqlite::Query accessedQuery{ getStatement( "SELECT max(accessed) " @@ -1271,7 +1342,7 @@ bool OfflineDatabase::evict(uint64_t neededFreeSize, DatabaseSizeChangeStats& st " ORDER BY accessed ASC LIMIT ?1 " ") " ) }; - accessedQuery.bind(1, 50); + accessedQuery.bind(1, int(resourcesToDelete)); // clang-format on if (!accessedQuery.run()) { return false; @@ -1308,15 +1379,15 @@ bool OfflineDatabase::evict(uint64_t neededFreeSize, DatabaseSizeChangeStats& st tileQuery.run(); const uint64_t tileChanges = tileQuery.changes(); - // Update current ambient cache size, based on how many bytes were released. - newAmbientCacheSize = std::max( - static_cast(newAmbientCacheSize) - static_cast(stats.bytesReleased()), 0u); + Log::Info(Event::Database, "evict ambient cache, removed " + std::to_string(resourceChanges) + " resources, " + std::to_string(tileChanges) + " tiles"); // The cached value of offlineTileCount does not need to be updated // here because only non-offline tiles can be removed by eviction. if (resourceChanges == 0 && tileChanges == 0) { return false; } + + currentAmbientCacheSize.reset(); } return true; @@ -1327,38 +1398,15 @@ std::exception_ptr OfflineDatabase::initAmbientCacheSize() { try { // clang-format off mapbox::sqlite::Query query{ getStatement( - "SELECT SUM(data) " + "SELECT SUM(count) " "FROM ( " - " SELECT SUM(IFNULL(LENGTH(data), 0) " - " + IFNULL(LENGTH(id), 0) " - " + IFNULL(LENGTH(url_template), 0) " - " + IFNULL(LENGTH(pixel_ratio), 0) " - " + IFNULL(LENGTH(x), 0) " - " + IFNULL(LENGTH(y), 0) " - " + IFNULL(LENGTH(z), 0) " - " + IFNULL(LENGTH(expires), 0) " - " + IFNULL(LENGTH(modified), 0) " - " + IFNULL(LENGTH(etag), 0) " - " + IFNULL(LENGTH(compressed), 0) " - " + IFNULL(LENGTH(accessed), 0) " - " + IFNULL(LENGTH(must_revalidate), 0) " - " ) as data " + " SELECT count(*) AS count " " FROM tiles " " LEFT JOIN region_tiles " " ON tile_id = tiles.id " " WHERE tile_id IS NULL " " UNION ALL " - " SELECT SUM(IFNULL(LENGTH(data), 0) " - " + IFNULL(LENGTH(id), 0) " - " + IFNULL(LENGTH(url), 0) " - " + IFNULL(LENGTH(kind), 0) " - " + IFNULL(LENGTH(expires), 0) " - " + IFNULL(LENGTH(modified), 0) " - " + IFNULL(LENGTH(etag), 0) " - " + IFNULL(LENGTH(compressed), 0) " - " + IFNULL(LENGTH(accessed), 0) " - " + IFNULL(LENGTH(must_revalidate), 0) " - " ) as data " + " SELECT count(*) AS count " " FROM resources " " LEFT JOIN region_resources " " ON resource_id = resources.id " @@ -1387,10 +1435,8 @@ std::exception_ptr OfflineDatabase::setMaximumAmbientCacheSize(uint64_t size) { maximumAmbientCacheSize = size; if (*currentAmbientCacheSize > maximumAmbientCacheSize) { - DatabaseSizeChangeStats stats(this); - evict(0, stats); + evict(0, false); if (autopack) vacuum(); - updateAmbientCacheSize(stats); } return nullptr; @@ -1401,50 +1447,6 @@ std::exception_ptr OfflineDatabase::setMaximumAmbientCacheSize(uint64_t size) { } } -void OfflineDatabase::setOfflineMapboxTileCountLimit(uint64_t limit) { - offlineMapboxTileCountLimit = limit; -} - -uint64_t OfflineDatabase::getOfflineMapboxTileCountLimit() { - return offlineMapboxTileCountLimit; -} - -bool OfflineDatabase::offlineMapboxTileCountLimitExceeded() { - return getOfflineMapboxTileCount() >= offlineMapboxTileCountLimit; -} - -uint64_t OfflineDatabase::getOfflineMapboxTileCount() try { - // Calculating this on every call would be much simpler than caching and - // manually updating the value, but it would make offline downloads an O(n²) - // operation, because the database query below involves an index scan of - // region_tiles. - - if (offlineMapboxTileCount) { - return *offlineMapboxTileCount; - } - - // clang-format off - mapbox::sqlite::Query query{ getStatement( - "SELECT COUNT(DISTINCT id) " - "FROM region_tiles, tiles " - "WHERE tile_id = tiles.id " - "AND url_template LIKE ?1 || '%'") }; - // clang-format on - query.bind(1, tileServerOptions.uriSchemeAlias() + "://"); - query.run(); - - offlineMapboxTileCount = query.get(0); - return *offlineMapboxTileCount; -} catch (...) { - handleError("get offline Mapbox tile count"); - return std::numeric_limits::max(); -} - -bool OfflineDatabase::exceedsOfflineMapboxTileCountLimit(const Resource& resource) { - return resource.kind == Resource::Kind::Tile && util::mapbox::isCanonicalURL(tileServerOptions, resource.url) && - offlineMapboxTileCountLimitExceeded(); -} - void OfflineDatabase::markUsedResources(int64_t regionID, const std::list& resources) try { if (!db) { initialize(); @@ -1487,34 +1489,4 @@ void OfflineDatabase::reopenDatabaseReadOnly(bool readOnly_) { } } -OfflineDatabase::DatabaseSizeChangeStats::DatabaseSizeChangeStats(OfflineDatabase* db_) - : db(db_) { - assert(db); - pageSize_ = db->getPragma("PRAGMA page_size"); - pageCount_ = db->getPragma("PRAGMA page_count"); - initialSize_ = pageSize_ * (pageCount_ - db->getPragma("PRAGMA freelist_count")); -} - -uint64_t OfflineDatabase::DatabaseSizeChangeStats::pageSize() const { - return pageSize_; -} - -int64_t OfflineDatabase::DatabaseSizeChangeStats::diff() const { - const int64_t currentSize = static_cast(pageSize_) * (db->getPragma("PRAGMA page_count") - - db->getPragma("PRAGMA freelist_count")); - return currentSize - static_cast(initialSize_); -} - -uint64_t OfflineDatabase::DatabaseSizeChangeStats::bytesReleased() const { - uint64_t currentSize = pageSize_ * (pageCount_ - db->getPragma("PRAGMA freelist_count")); - return std::max(initialSize_ - currentSize, 0u); -} - -void OfflineDatabase::updateAmbientCacheSize(DatabaseSizeChangeStats& stats) { - assert(currentAmbientCacheSize); - if (currentAmbientCacheSize) { - *currentAmbientCacheSize = std::max(static_cast(*currentAmbientCacheSize) + stats.diff(), 0u); - } -} - } // namespace mbgl diff --git a/platform/default/src/mbgl/storage/offline_download.cpp b/platform/default/src/mbgl/storage/offline_download.cpp index 4a48c584c638..1ebbc0403480 100644 --- a/platform/default/src/mbgl/storage/offline_download.cpp +++ b/platform/default/src/mbgl/storage/offline_download.cpp @@ -426,15 +426,10 @@ void OfflineDownload::deactivateDownload() { bool OfflineDownload::flushResourcesBuffer() { if (buffer.empty()) return true; - try { - offlineDatabase.putRegionResources(id, buffer, status); - buffer.clear(); - observer->statusChanged(status); - return true; - } catch (const MapboxTileLimitExceededException&) { - onMapboxTileCountLimitExceeded(); - return false; - } + offlineDatabase.putRegionResources(id, buffer, status); + buffer.clear(); + observer->statusChanged(status); + return true; } void OfflineDownload::queueResource(Resource&& resource) { @@ -510,11 +505,6 @@ void OfflineDownload::ensureResource(Resource&& resource, std::functionmapboxTileCountLimitExceeded(offlineDatabase.getOfflineMapboxTileCountLimit()); - setState(OfflineRegionDownloadState::Inactive); -} - } // namespace mbgl diff --git a/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift b/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift index da75dd59f9b7..2d35fff6cc9f 100644 --- a/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift +++ b/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift @@ -35,7 +35,7 @@ class DDSCircleLayerExample: UIViewController, MLNMapViewDelegate { layer.predicate = NSPredicate(format: "class == %@", "shop") // Style the circle layer color based on the rank - layer.circleColor = NSExpression(mglJSONObject: ["step", ["get", "rank"], 0, "red", 10, "green", 20, "blue", 30, "purple", 40, "yellow"] as [Any]) + layer.circleColor = NSExpression(mlnJSONObject: ["step", ["get", "rank"], 0, "red", 10, "green", 20, "blue", 30, "purple", 40, "yellow"] as [Any]) layer.circleRadius = NSExpression(forConstantValue: 3) style.addLayer(layer) diff --git a/platform/ios/app-swift/Sources/MultipleImagesExample.swift b/platform/ios/app-swift/Sources/MultipleImagesExample.swift index bc077e3670e1..27e2f3c1c4dc 100644 --- a/platform/ios/app-swift/Sources/MultipleImagesExample.swift +++ b/platform/ios/app-swift/Sources/MultipleImagesExample.swift @@ -47,7 +47,7 @@ class MultipleImagesExample: UIViewController, MLNMapViewDelegate { let imageLayer = MLNSymbolStyleLayer(identifier: "npc-poi-images", source: source) imageLayer.sourceLayerIdentifier = "pois" - imageLayer.iconImageName = NSExpression(mglJSONObject: [ + imageLayer.iconImageName = NSExpression(mlnJSONObject: [ "match", ["get", "POITYPE"], "Restroom", "restrooms", "Trailhead", "trailhead", diff --git a/platform/ios/app-swift/Sources/OfflinePackExample.swift b/platform/ios/app-swift/Sources/OfflinePackExample.swift index 0c6ba73cc8f8..7ef01f71f2e5 100644 --- a/platform/ios/app-swift/Sources/OfflinePackExample.swift +++ b/platform/ios/app-swift/Sources/OfflinePackExample.swift @@ -26,7 +26,6 @@ class OfflinePackExample: UIViewController, MLNMapViewDelegate { // Setup offline pack notification handlers. NotificationCenter.default.addObserver(self, selector: #selector(offlinePackProgressDidChange), name: NSNotification.Name.MLNOfflinePackProgressChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(offlinePackDidReceiveError), name: NSNotification.Name.MLNOfflinePackError, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(offlinePackDidReceiveMaximumAllowedMapboxTiles), name: NSNotification.Name.MLNOfflinePackMaximumMapboxTilesReached, object: nil) } func mapViewDidFinishLoadingMap(_: MLNMapView) { @@ -119,15 +118,6 @@ class OfflinePackExample: UIViewController, MLNMapViewDelegate { print("Offline pack “\(userInfo.name)” received error: \(error.localizedFailureReason ?? "unknown error")") } } - - @objc func offlinePackDidReceiveMaximumAllowedMapboxTiles(notification: NSNotification) { - if let pack = notification.object as? MLNOfflinePack, - let userInfo = try? jsonDecoder.decode(UserData.self, from: pack.context), - let maximumCount = (notification.userInfo?[MLNOfflinePackUserInfoKey.maximumCount] as AnyObject).uint64Value - { - print("Offline pack “\(userInfo.name)” reached limit of \(maximumCount) tiles.") - } - } } // #-end-example-code diff --git a/platform/ios/app-swift/Sources/POIAlongRouteExample.swift b/platform/ios/app-swift/Sources/POIAlongRouteExample.swift index 7959828f62f9..2d5dddf180b9 100644 --- a/platform/ios/app-swift/Sources/POIAlongRouteExample.swift +++ b/platform/ios/app-swift/Sources/POIAlongRouteExample.swift @@ -79,7 +79,7 @@ class POIAlongRouteExample: UIViewController, MLNMapViewDelegate { layer.lineColor = NSExpression(forConstantValue: UIColor(red: 59 / 255, green: 178 / 255, blue: 208 / 255, alpha: 1)) // Use expression to smoothly adjust the line width from 2pt to 20pt between zoom levels 14 and 18. - layer.lineWidth = NSExpression(mglJSONObject: ["interpolate", ["linear"], ["zoom"], 14, 2, 18, 20]) + layer.lineWidth = NSExpression(mlnJSONObject: ["interpolate", ["linear"], ["zoom"], 14, 2, 18, 20]) // We can also add a second layer that will draw a stroke around the original line. let casingLayer = MLNLineStyleLayer(identifier: "polyline-case", source: source) @@ -91,7 +91,7 @@ class POIAlongRouteExample: UIViewController, MLNMapViewDelegate { // Stroke color slightly darker than the line color. casingLayer.lineColor = NSExpression(forConstantValue: UIColor(red: 41 / 255, green: 145 / 255, blue: 171 / 255, alpha: 1)) // Use expression to gradually increase the stroke width between zoom levels 14 and 18. - casingLayer.lineWidth = NSExpression(mglJSONObject: ["interpolate", ["linear"], ["zoom"], 14, 1, 18, 4]) + casingLayer.lineWidth = NSExpression(mlnJSONObject: ["interpolate", ["linear"], ["zoom"], 14, 1, 18, 4]) // Just for fun, let’s add another copy of the line with a dash pattern. let dashedLayer = MLNLineStyleLayer(identifier: "polyline-dash", source: source) diff --git a/platform/ios/app/MBXOfflinePacksTableViewController.m b/platform/ios/app/MBXOfflinePacksTableViewController.m index 3568332c52c6..9715244605ba 100644 --- a/platform/ios/app/MBXOfflinePacksTableViewController.m +++ b/platform/ios/app/MBXOfflinePacksTableViewController.m @@ -38,7 +38,6 @@ - (void)viewDidLoad { [[MLNOfflineStorage sharedOfflineStorage] addObserver:self forKeyPath:@"packs" options:NSKeyValueObservingOptionInitial context:NULL]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MLNOfflinePackProgressChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveError:) name:MLNOfflinePackErrorNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveMaximumAllowedMapboxTiles:) name:MLNOfflinePackMaximumMapboxTilesReachedNotification object:nil]; } - (void)dealloc { @@ -273,12 +272,4 @@ - (void)offlinePackDidReceiveError:(NSNotification *)notification { } } -- (void)offlinePackDidReceiveMaximumAllowedMapboxTiles:(NSNotification *)notification { - MLNOfflinePack *pack = notification.object; - NSAssert([pack isKindOfClass:[MLNOfflinePack class]], @"MLNOfflineStorage notification has a non-pack object."); - - uint64_t maximumCount = [notification.userInfo[MLNOfflinePackUserInfoKeyMaximumCount] unsignedLongLongValue]; - NSLog(@"Offline pack “%@” reached limit of %llu tiles.", pack.name, maximumCount); -} - @end diff --git a/platform/ios/src/MLNFaux3DUserLocationAnnotationView.mm b/platform/ios/src/MLNFaux3DUserLocationAnnotationView.mm index 72691ed2688d..be68abb938ec 100644 --- a/platform/ios/src/MLNFaux3DUserLocationAnnotationView.mm +++ b/platform/ios/src/MLNFaux3DUserLocationAnnotationView.mm @@ -222,7 +222,7 @@ - (void)drawPuck if ( ! _puckDot) { _puckDot = [self circleLayerWithSize:MLNUserLocationAnnotationPuckSize]; - _puckDot.backgroundColor = [[UIColor whiteColor] CGColor]; + _puckDot.backgroundColor = [[UIColor colorWithWhite:1.0 alpha:0.8] CGColor]; _puckDot.shadowColor = [puckShadowColor CGColor]; _puckDot.shadowOpacity = shadowOpacity; _puckDot.shadowPath = [[UIBezierPath bezierPathWithOvalInRect:_puckDot.bounds] CGPath]; diff --git a/platform/ios/src/MLNMapView.h b/platform/ios/src/MLNMapView.h index bf7acae4a2aa..88cf8f38e006 100644 --- a/platform/ios/src/MLNMapView.h +++ b/platform/ios/src/MLNMapView.h @@ -2322,6 +2322,24 @@ of north, the map will automatically snap to exact north. */ - (void)addPluginLayerType:(Class)pluginLayerClass; +#pragma mark - Outdooractive private + +/// Dormant means there is no underlying GL view (typically in the background) +@property (nonatomic, getter=isDormant) BOOL dormant; + +@property (nonatomic, nullable) UIImage *lastSnapshotImage; + +- (void)validateLocationServices; + +- (void)validateUserHeadingUpdating; + +- (void)locationManager:(id)manager didUpdateLocations:(NSArray *)locations; +- (void)locationManager:(__unused id)manager didUpdateLocations:(NSArray *)locations animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion; + +- (void)locationManager:(__unused id)manager didUpdateHeading:(CLHeading *)newHeading; + +- (MLNMapCamera *)cameraAtCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel; + @end NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MLNMapView.mm b/platform/ios/src/MLNMapView.mm index 7bbe3c1bb217..6f78e66cf200 100644 --- a/platform/ios/src/MLNMapView.mm +++ b/platform/ios/src/MLNMapView.mm @@ -275,7 +275,7 @@ typedef NS_ENUM(NSUInteger, MLNUserTrackingState) { /// Duration of an animation due to a user location update, typically chosen to /// match a typical interval between user location updates. -const NSTimeInterval MLNUserLocationAnimationDuration = 1.0; +const NSTimeInterval MLNUserLocationAnimationDuration = 0.3; /// Distance between the map view’s edge and that of the user location /// annotation view. @@ -288,7 +288,10 @@ typedef NS_ENUM(NSUInteger, MLNUserTrackingState) { const double MLNMinimumZoomLevelForUserTracking = 10.5; /// Initial zoom level when entering user tracking mode from a low zoom level. -const double MLNDefaultZoomLevelForUserTracking = 14.0; +const double MLNDefaultZoomLevelForUserTracking = 15.0; + +/// Tolerance for snapping to true north, measured in degrees in either direction. +const CLLocationDirection MLNToleranceForSnappingToNorth = 7; /// Distance threshold to stop the camera while animating. const CLLocationDistance MLNDistanceThresholdForCameraPause = 500; @@ -407,8 +410,6 @@ @interface MLNMapView () )annotation - (void)addAnnotation:(id )annotation { - MLNLogDebug(@"Adding annotation: %@", annotation); + MLNLogInfo(@"Adding annotation: %@", annotation); if ( ! annotation) return; // The core bulk add API is efficient with respect to indexing and @@ -4835,7 +4843,7 @@ - (void)addAnnotation:(id )annotation - (void)addAnnotations:(NSArray> *)annotations { - MLNLogDebug(@"Adding: %lu annotations", annotations.count); + MLNLogInfo(@"Adding %lu annotations", annotations.count); if ( ! annotations) return; [self willChangeValueForKey:@"annotations"]; @@ -5140,7 +5148,7 @@ - (void)removeAnnotation:(id )annotation - (void)removeAnnotations:(NSArray> *)annotations { - MLNLogDebug(@"Removing: %lu annotations", annotations.count); + MLNLogInfo(@"Removing %lu annotations", annotations.count); if ( ! annotations) return; [self willChangeValueForKey:@"annotations"]; @@ -5225,13 +5233,13 @@ - (void)removeAnnotations:(NSArray> *)annotations - (void)addOverlay:(id )overlay { - MLNLogDebug(@"Adding overlay: %@", overlay); + MLNLogInfo(@"Adding overlay: %@", overlay); [self addOverlays:@[ overlay ]]; } - (void)addOverlays:(NSArray> *)overlays { - MLNLogDebug(@"Adding: %lu overlays", overlays.count); + MLNLogInfo(@"Adding %lu overlays", overlays.count); #if DEBUG for (id overlay in overlays) { @@ -5244,13 +5252,13 @@ - (void)addOverlays:(NSArray> *)overlays - (void)removeOverlay:(id )overlay { - MLNLogDebug(@"Removing overlay: %@", overlay); + MLNLogInfo(@"Removing overlay: %@", overlay); [self removeOverlays:@[ overlay ]]; } - (void)removeOverlays:(NSArray> *)overlays { - MLNLogDebug(@"Removing: %lu overlays", overlays.count); + MLNLogInfo(@"Removing %lu overlays", overlays.count); #if DEBUG for (id overlay in overlays) { @@ -5944,7 +5952,7 @@ - (void)showAnnotations:(NSArray> *)annotations edgePadding:( - (void)showAnnotations:(NSArray> *)annotations edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion { - MLNLogDebug(@"Showing: %lu annotations edgePadding: %@ animated: %@", annotations.count, NSStringFromUIEdgeInsets(insets), MLNStringFromBOOL(animated)); + MLNLogDebug(@"Showing %lu annotations, edgePadding: %@, animated: %@", annotations.count, NSStringFromUIEdgeInsets(insets), MLNStringFromBOOL(animated)); if ( ! annotations.count) { if (completion) { @@ -6191,7 +6199,7 @@ - (void)setUserTrackingMode:(MLNUserTrackingMode)mode animated:(BOOL)animated - (void)setUserTrackingMode:(MLNUserTrackingMode)mode animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion { - MLNLogDebug(@"Setting userTrackingMode: %lu animated: %@", mode, MLNStringFromBOOL(animated)); + MLNLogInfo(@"Setting userTrackingMode: %lu, animated: %@", mode, MLNStringFromBOOL(animated)); if (mode == _userTrackingMode) { if (completion) @@ -6292,7 +6300,7 @@ - (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(B - (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion { - MLNLogDebug(@"Setting targetCoordinate: %@ animated: %@", MLNStringFromCLLocationCoordinate2D(targetCoordinate), MLNStringFromBOOL(animated)); + MLNLogDebug(@"Setting targetCoordinate: %@, animated: %@", MLNStringFromCLLocationCoordinate2D(targetCoordinate), MLNStringFromBOOL(animated)); BOOL isSynchronous = YES; if (targetCoordinate.latitude != self.targetCoordinate.latitude || targetCoordinate.longitude != self.targetCoordinate.longitude) @@ -6344,7 +6352,9 @@ - (void)validateUserHeadingUpdating - (void)locationManager:(id)manager didUpdateLocations:(NSArray *)locations { - [self locationManager:manager didUpdateLocations:locations animated:YES completionHandler:nil]; + BOOL animated = (self.userTrackingMode != MLNUserTrackingModeFollowWithHeading); + + [self locationManager:manager didUpdateLocations:locations animated:animated completionHandler:nil]; } - (void)locationManager:(__unused id)manager didUpdateLocations:(NSArray *)locations animated:(BOOL)animated completionHandler:(nullable void (^)(void))completion @@ -7485,8 +7495,21 @@ - (void)updateUserLocationAnnotationViewAnimatedWithDuration:(NSTimeInterval)dur annotationView.center = userPoint; } - if (CGRectContainsPoint(CGRectInset(self.bounds, -MLNAnnotationUpdateViewportOutset.width, - -MLNAnnotationUpdateViewportOutset.height), userPoint)) + CGRect annotationViewportRect = CGRectInset(self.bounds, -MLNAnnotationUpdateViewportOutset.width, + -MLNAnnotationUpdateViewportOutset.height); + BOOL annotationVisible = CGRectContainsPoint(annotationViewportRect, userPoint); + if (@available(iOS 14.0, *)) { + if ([self.locationManager respondsToSelector:@selector(accuracyAuthorization)] && self.locationManager.accuracyAuthorization == CLAccuracyAuthorizationReducedAccuracy) { + CGFloat accuracyRingRadius = round(self.userLocation.location.horizontalAccuracy / [self metersPerPointAtLatitude:self.userLocation.coordinate.latitude zoomLevel:self.zoomLevel]); + CGRect accuracyRingRect = CGRectMake(userPoint.x - accuracyRingRadius, + userPoint.y - accuracyRingRadius, + accuracyRingRadius * 2, + accuracyRingRadius * 2); + annotationVisible = CGRectIntersectsRect(annotationViewportRect, accuracyRingRect); + } + } + + if (annotationVisible) { // Smoothly move the user location annotation view and callout view to // the new location. diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 8b641267c28c..c3d83886b18e 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -310,7 +310,7 @@ void Transform::flyTo(const CameraOptions& inputCamera, duration = *animation.duration; } else { /// V: Average velocity, measured in ρ-screenfuls per second. - double velocity = 1.2; + double velocity = 3; if (animation.velocity) { velocity = *animation.velocity / rho; } diff --git a/src/mbgl/tile/tile_loader_impl.hpp b/src/mbgl/tile/tile_loader_impl.hpp index dd465fc94cb2..918b127e523c 100644 --- a/src/mbgl/tile/tile_loader_impl.hpp +++ b/src/mbgl/tile/tile_loader_impl.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -118,6 +119,8 @@ void TileLoader::loadFromCache() { tile.setTriedCache(); if (res.error && res.error->reason == Response::Error::Reason::NotFound) { + Log::Info(Event::HttpRequest, "TileLoader: tile not found, url=" + std::string(resource.url)); + // When the cache-only request could not be satisfied, don't treat // it as an error. A cache lookup could still return data, _and_ an // error, in particular when we were able to find the data, but it @@ -129,10 +132,12 @@ void TileLoader::loadFromCache() { resource.priorEtag = res.etag; resource.priorData = res.data; } else { + Log::Info(Event::HttpRequest, "TileLoader: found, isUsable=" + (res.isUsable() ? std::string("true") : std::string("false")) + ", must-revalidate=" + (res.mustRevalidate ? std::string("true") : std::string("false")) + ", expires=" + (res.expires ? util::iso8601(*res.expires) : std::string("n/a")) + " UTC, url=" + std::string(resource.url)); loadedData(res, Resource::LoadingMethod::CacheOnly); } if (necessity == TileNecessity::Required) { + Log::Info(Event::HttpRequest, "TileLoader: tile required -> network, url=" + std::string(resource.url)); loadFromNetwork(); } break; diff --git a/src/mbgl/util/tile_server_options.cpp b/src/mbgl/util/tile_server_options.cpp index 61a050152e90..4b6dd8487bdc 100644 --- a/src/mbgl/util/tile_server_options.cpp +++ b/src/mbgl/util/tile_server_options.cpp @@ -31,6 +31,8 @@ class TileServerOptions::Impl { std::string apiKeyParameterName; bool apiKeyRequired; + bool useWalJournal; + std::vector defaultStyles; std::string defaultStyle; }; @@ -192,6 +194,15 @@ bool TileServerOptions::requiresApiKey() const { return impl_->apiKeyRequired; } +TileServerOptions& TileServerOptions::setUseWalJournal(bool useWalJournal) { + impl_->useWalJournal = useWalJournal; + return *this; +} + +bool TileServerOptions::useWalJournal() const { + return impl_->useWalJournal; +} + const std::vector TileServerOptions::defaultStyles() const { return impl_->defaultStyles; } @@ -229,34 +240,8 @@ TileServerOptions TileServerOptions::MapLibreConfiguration() { .withTileTemplate("/{path}", "tiles", {}) .withDefaultStyles(styles) .withDefaultStyle("Basic") - .setRequiresApiKey(false); - return options; -} - -// - -TileServerOptions TileServerOptions::MapboxConfiguration() { - std::vector styles{ - mbgl::util::DefaultStyle("mapbox://styles/mapbox/streets-v11", "Streets", 11), - mbgl::util::DefaultStyle("mapbox://styles/mapbox/outdoors-v11", "Outdoors", 11), - mbgl::util::DefaultStyle("mapbox://styles/mapbox/light-v10", "Light", 10), - mbgl::util::DefaultStyle("mapbox://styles/mapbox/dark-v10", "Dark", 10), - mbgl::util::DefaultStyle("mapbox://styles/mapbox/satellite-v9", "Satellite", 9), - mbgl::util::DefaultStyle("mapbox://styles/mapbox/satellite-streets-v11", "Satellite Streets", 11)}; - - TileServerOptions options = TileServerOptions() - .withBaseURL("https://api.mapbox.com") - .withUriSchemeAlias("mapbox") - .withApiKeyParameterName("access_token") - .withSourceTemplate("/{domain}.json", "", {"/v4"}) - .withStyleTemplate("/styles/v1{path}", "styles", {}) - .withSpritesTemplate( - "/styles/v1{directory}{filename}/sprite{extension}", "sprites", {}) - .withGlyphsTemplate("/fonts/v1{path}", "fonts", {}) - .withTileTemplate("{path}", "tiles", {"/v4"}) - .withDefaultStyles(styles) - .withDefaultStyle("Streets") - .setRequiresApiKey(true); + .setRequiresApiKey(false) + .setUseWalJournal(false); return options; } @@ -281,7 +266,8 @@ TileServerOptions TileServerOptions::MapTilerConfiguration() { .withTileTemplate("{path}", "tiles", {}) .withDefaultStyles(styles) .withDefaultStyle("Streets") - .setRequiresApiKey(true); + .setRequiresApiKey(true) + .setUseWalJournal(false); return options; } diff --git a/test/storage/offline_database.test.cpp b/test/storage/offline_database.test.cpp index abf4485e265d..1dba9dbf6568 100644 --- a/test/storage/offline_database.test.cpp +++ b/test/storage/offline_database.test.cpp @@ -822,108 +822,6 @@ TEST(OfflineDatabase, TEST_REQUIRES_WRITE(Pack)) { EXPECT_EQ(0u, log.uncheckedCount()); } -TEST(OfflineDatabase, MapboxTileLimitExceeded) { - FixtureLog log; - - uint64_t limit = 60; - - OfflineDatabase db(":memory:", fixture::tileServerOptions); - db.setOfflineMapboxTileCountLimit(limit); - - Response response; - response.data = randomString(4096); - - auto insertAmbientTile = [&](unsigned i) { - const Resource ambientTile = Resource::tile( - "maptiler://tiles/tiles/ambient_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ); - db.put(ambientTile, response); - }; - - auto insertRegionTile = [&](int64_t regionID, uint64_t i) { - const Resource tile = Resource::tile( - "maptiler://tiles/tiles/region_tile_" + std::to_string(i), 1, 0, 0, 0, Tileset::Scheme::XYZ); - db.putRegionResource(regionID, tile, response); - }; - - OfflineTilePyramidRegionDefinition definition1{ - "maptiler://maps/style1", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true}; - OfflineRegionMetadata metadata1{{1, 2, 3}}; - - OfflineTilePyramidRegionDefinition definition2{ - "maptiler://maps/style2", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true}; - OfflineRegionMetadata metadata2{{1, 2, 3}}; - - auto region1 = db.createRegion(definition1, metadata1); - auto region2 = db.createRegion(definition2, metadata2); - - // Fine because tile limit only affects offline region. - for (unsigned i = 0; i < limit * 2; ++i) { - insertAmbientTile(i); - } - - ASSERT_EQ(db.getOfflineMapboxTileCount(), 0); - - // Fine because this region is under the tile limit. - for (uint64_t i = 0; i < limit - 10; ++i) { - insertRegionTile(region1->getID(), i); - } - - ASSERT_EQ(db.getOfflineMapboxTileCount(), limit - 10); - - // Fine because this region + the previous is at the limit. - for (uint64_t i = limit; i < limit + 10; ++i) { - insertRegionTile(region2->getID(), i); - } - - ASSERT_EQ(db.getOfflineMapboxTileCount(), limit); - - // Full. - ASSERT_THROW(insertRegionTile(region1->getID(), 200), MapboxTileLimitExceededException); - ASSERT_THROW(insertRegionTile(region2->getID(), 201), MapboxTileLimitExceededException); - - // These tiles are already on respective - // regions. - insertRegionTile(region1->getID(), 0); - insertRegionTile(region2->getID(), 60); - - // Should be fine, ambient tile. - insertAmbientTile(333); - - // Also fine, not Mapbox. - const Resource notMapboxTile = Resource::tile("foobar://region_tile", 1, 0, 0, 0, Tileset::Scheme::XYZ); - db.putRegionResource(region1->getID(), notMapboxTile, response); - - // These tiles are not on the region they are - // being added to, but exist on another region, - // so they do not add to the total size. - insertRegionTile(region2->getID(), 0); - insertRegionTile(region1->getID(), 60); - - ASSERT_EQ(db.getOfflineMapboxTileCount(), limit); - - // The tile 1 belongs to two regions and will - // still count as resource. - db.deleteRegion(std::move(*region2)); - - ASSERT_EQ(db.getOfflineMapboxTileCount(), 51); - - // Add new tiles to the region 1. We are adding - // 10, which would blow up the limit if it wasn't - // for the fact that tile 60 is already on the - // database and will not count. - for (uint64_t i = limit; i < limit + 10; ++i) { - insertRegionTile(region1->getID(), i); - } - - // Full again. - ASSERT_THROW(insertRegionTile(region1->getID(), 202), MapboxTileLimitExceededException); - - db.deleteRegion(std::move(*region1)); - - ASSERT_EQ(0u, db.listRegions().value().size()); - ASSERT_EQ(0u, log.uncheckedCount()); -} - TEST(OfflineDatabase, Invalidate) { using namespace std::chrono_literals; @@ -1325,67 +1223,6 @@ TEST(OfflineDatabase, HasRegionResourceTile) { EXPECT_EQ(0u, log.uncheckedCount()); } -TEST(OfflineDatabase, OfflineMapboxTileCount) { - FixtureLog log; - OfflineDatabase db(":memory:", fixture::tileServerOptions); - OfflineTilePyramidRegionDefinition definition{ - "http://example.com/style", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, true}; - OfflineRegionMetadata metadata; - - auto region1 = db.createRegion(definition, metadata); - ASSERT_TRUE(region1); - auto region2 = db.createRegion(definition, metadata); - ASSERT_TRUE(region2); - - Resource nonMapboxTile = Resource::tile("http://example.com/", 1.0, 0, 0, 0, Tileset::Scheme::XYZ); - Resource mapboxTile1 = Resource::tile("maptiler://tiles/tiles/1", 1.0, 0, 0, 0, Tileset::Scheme::XYZ); - Resource mapboxTile2 = Resource::tile("maptiler://tiles/tiles/2", 1.0, 0, 0, 1, Tileset::Scheme::XYZ); - - Response response; - response.data = std::make_shared("data"); - - // Count is initially zero. - EXPECT_EQ(0u, db.getOfflineMapboxTileCount()); - - // Count stays the same after putting a non-tile resource. - db.putRegionResource(region1->getID(), Resource::style("http://example.com/"), response); - EXPECT_EQ(0u, db.getOfflineMapboxTileCount()); - - // Count stays the same after putting a non-Mapbox tile. - db.putRegionResource(region1->getID(), nonMapboxTile, response); - EXPECT_EQ(0u, db.getOfflineMapboxTileCount()); - - // Count increases after putting a Mapbox tile not used by another region. - db.putRegionResource(region1->getID(), mapboxTile1, response); - EXPECT_EQ(1u, db.getOfflineMapboxTileCount()); - - // Count stays the same after putting a Mapbox tile used by another region. - db.putRegionResource(region2->getID(), mapboxTile1, response); - EXPECT_EQ(1u, db.getOfflineMapboxTileCount()); - - // Count stays the same after putting a Mapbox tile used by the same region. - db.putRegionResource(region2->getID(), mapboxTile1, response); - EXPECT_EQ(1u, db.getOfflineMapboxTileCount()); - - // Count stays the same after deleting a region when the tile is still used by another region. - db.deleteRegion(std::move(*region2)); - EXPECT_EQ(1u, db.getOfflineMapboxTileCount()); - - // Count stays the same after the putting a non-offline Mapbox tile. - db.put(mapboxTile2, response); - EXPECT_EQ(1u, db.getOfflineMapboxTileCount()); - - // Count increases after putting a pre-existing, but non-offline Mapbox tile. - db.putRegionResource(region1->getID(), mapboxTile2, response); - EXPECT_EQ(2u, db.getOfflineMapboxTileCount()); - - // Count decreases after deleting a region when the tiles are not used by other regions. - db.deleteRegion(std::move(*region1)); - EXPECT_EQ(0u, db.getOfflineMapboxTileCount()); - - EXPECT_EQ(0u, log.uncheckedCount()); -} - TEST(OfflineDatabase, BatchInsertion) { FixtureLog log; OfflineDatabase db(":memory:", fixture::tileServerOptions); @@ -1413,41 +1250,6 @@ TEST(OfflineDatabase, BatchInsertion) { EXPECT_EQ(0u, log.uncheckedCount()); } -TEST(OfflineDatabase, BatchInsertionMapboxTileCountExceeded) { - FixtureLog log; - OfflineDatabase db(":memory:", fixture::tileServerOptions); - db.setOfflineMapboxTileCountLimit(1); - db.setMaximumAmbientCacheSize(1024 * 100); - - OfflineTilePyramidRegionDefinition definition{"", LatLngBounds::world(), 0, INFINITY, 1.0, false}; - auto region = db.createRegion(definition, OfflineRegionMetadata()); - ASSERT_TRUE(region); - - Response response; - response.data = randomString(1024); - std::list> resources; - - resources.emplace_back(Resource::style("http://example.com/"), response); - resources.emplace_back(Resource::tile("maptiler://tiles/1", 1.0, 0, 0, 0, Tileset::Scheme::XYZ), response); - resources.emplace_back(Resource::tile("maptiler://tiles/2", 1.0, 0, 0, 0, Tileset::Scheme::XYZ), response); - - OfflineRegionStatus status; - try { - db.putRegionResources(region->getID(), resources, status); - EXPECT_FALSE(true); - } catch (const MapboxTileLimitExceededException&) { - // Expected - } - - EXPECT_EQ(0u, status.completedTileCount); - EXPECT_EQ(0u, status.completedResourceCount); - const auto completedStatus = db.getRegionCompletedStatus(region->getID()).value(); - EXPECT_EQ(1u, completedStatus.completedTileCount); - EXPECT_EQ(2u, completedStatus.completedResourceCount); - - EXPECT_EQ(0u, log.uncheckedCount()); -} - TEST(OfflineDatabase, MigrateFromV2Schema) { // v2.db is a v2 database containing a single offline region with a small number of resources. FixtureLog log; @@ -1745,10 +1547,6 @@ TEST(OfflineDatabase, TEST_REQUIRES_WRITE(DisallowedIO)) { EXPECT_EQ(1u, log.count(warning(ResultCode::Auth, "Can't delete region: authorization denied"))); EXPECT_EQ(0u, log.uncheckedCount()); - EXPECT_EQ(std::numeric_limits::max(), db.getOfflineMapboxTileCount()); - EXPECT_EQ(1u, log.count(warning(ResultCode::Auth, "Can't get offline Mapbox tile count: authorization denied"))); - EXPECT_EQ(0u, log.uncheckedCount()); - fs.reset(); } #endif // __QT__ @@ -1911,37 +1709,6 @@ TEST(OfflineDatabase, MergeDatabaseWithMultipleRegionsWithOverlap) { } } -TEST(OfflineDatabase, MergeDatabaseWithSingleRegionTooManyNewTiles) { - FixtureLog log; - util::deleteFile(filename_sideload); - util::copyFile(filename_sideload, "test/fixtures/offline_database/sideload_sat_multiple.db"); - - OfflineDatabase db(":memory:", fixture::tileServerOptions); - db.setOfflineMapboxTileCountLimit(1); - - auto result = db.mergeDatabase(filename_sideload); - EXPECT_FALSE(result); - EXPECT_EQ(1u, log.count({EventSeverity::Error, Event::Database, -1, "Mapbox tile limit exceeded"})); - EXPECT_EQ(0u, log.uncheckedCount()); -} - -TEST(OfflineDatabase, MergeDatabaseWithSingleRegionTooManyExistingTiles) { - FixtureLog log; - deleteDatabaseFiles(); - util::deleteFile(filename_sideload); - util::copyFile(filename, "test/fixtures/offline_database/sideload_sat_multiple.db"); - util::copyFile(filename_sideload, "test/fixtures/offline_database/satellite_test.db"); - - OfflineDatabase db(filename, fixture::tileServerOptions); - db.setOfflineMapboxTileCountLimit(2); - - auto result = db.mergeDatabase(filename_sideload); - EXPECT_THROW(std::rethrow_exception(result.error()), MapboxTileLimitExceededException); - - EXPECT_EQ(1u, log.count({EventSeverity::Error, Event::Database, -1, "Mapbox tile limit exceeded"})); - EXPECT_EQ(0u, log.uncheckedCount()); -} - #ifndef WIN32 // Windows cannot copy a folder as a file TEST(OfflineDatabase, MergeDatabaseWithInvalidPath) { FixtureLog log; diff --git a/test/storage/offline_download.test.cpp b/test/storage/offline_download.test.cpp index 50fb6404e5f2..786acaba531a 100644 --- a/test/storage/offline_download.test.cpp +++ b/test/storage/offline_download.test.cpp @@ -48,13 +48,8 @@ class MockObserver : public OfflineRegionObserver { if (responseErrorFn) responseErrorFn(error); } - void mapboxTileCountLimitExceeded(uint64_t limit) override { - if (mapboxTileCountLimitExceededFn) mapboxTileCountLimitExceededFn(limit); - } - std::function statusChangedFn; std::function responseErrorFn; - std::function mapboxTileCountLimitExceededFn; }; class OfflineTest { @@ -534,110 +529,6 @@ TEST(OfflineDownload, RequestErrorsAreRetried) { test.loop.run(); } -TEST(OfflineDownload, TileCountLimitExceededNoTileResponse) { - OfflineTest test; - auto region = test.createRegion(); - ASSERT_TRUE(region); - OfflineDownload download(region->getID(), - OfflineTilePyramidRegionDefinition( - "http://127.0.0.1:3000/style.json", LatLngBounds::world(), 0.0, 0.0, 1.0, false), - test.db, - test.fileSource); - - uint64_t tileLimit = 0; - - test.db.setOfflineMapboxTileCountLimit(tileLimit); - - test.fileSource.styleResponse = [&](const Resource& resource) { - EXPECT_EQ("http://127.0.0.1:3000/style.json", resource.url); - return test.response("mapbox_source.style.json"); - }; - // test.fileSource.tileResponse = [&] (const Resource& resource) { - // EXPECT_EQ("maptiler://0-0-0.vector.pbf", resource.url); - // return test.response("0-0-0.vector.pbf"); - // }; - - auto observer = std::make_unique(); - bool mapboxTileCountLimitExceededCalled = false; - - observer->mapboxTileCountLimitExceededFn = [&](uint64_t limit) { - EXPECT_FALSE(mapboxTileCountLimitExceededCalled); - EXPECT_EQ(tileLimit, limit); - mapboxTileCountLimitExceededCalled = true; - }; - - observer->statusChangedFn = [&](OfflineRegionStatus status) { - if (!mapboxTileCountLimitExceededCalled) { - EXPECT_FALSE(status.complete()); - EXPECT_EQ(OfflineRegionDownloadState::Active, status.downloadState); - } else { - EXPECT_EQ(OfflineRegionDownloadState::Inactive, status.downloadState); - test.loop.stop(); - } - }; - - download.setObserver(std::move(observer)); - download.setState(OfflineRegionDownloadState::Active); - - test.loop.run(); -} - -TEST(OfflineDownload, TileCountLimitExceededWithTileResponse) { - OfflineTest test; - auto region = test.createRegion(); - ASSERT_TRUE(region); - OfflineDownload download(region->getID(), - OfflineTilePyramidRegionDefinition( - "http://127.0.0.1:3000/style.json", LatLngBounds::world(), 0.0, 0.0, 1.0, true), - test.db, - test.fileSource); - - uint64_t tileLimit = 1; - - test.db.setOfflineMapboxTileCountLimit(tileLimit); - - test.fileSource.styleResponse = [&](const Resource& resource) { - EXPECT_EQ("http://127.0.0.1:3000/style.json", resource.url); - return test.response("mapbox_source.style.json"); - }; - - test.fileSource.tileResponse = [&](const Resource& resource) { - const Resource::TileData& tile = *resource.tileData; - EXPECT_EQ("maptiler://{z}-{x}-{y}.vector.pbf", tile.urlTemplate); - EXPECT_EQ(1, tile.pixelRatio); - EXPECT_EQ(0, tile.x); - EXPECT_EQ(0, tile.y); - EXPECT_EQ(0, tile.z); - return test.response("0-0-0.vector.pbf"); - }; - - auto observer = std::make_unique(); - bool mapboxTileCountLimitExceededCalled = false; - - observer->mapboxTileCountLimitExceededFn = [&](uint64_t limit) { - EXPECT_FALSE(mapboxTileCountLimitExceededCalled); - EXPECT_EQ(tileLimit, limit); - mapboxTileCountLimitExceededCalled = true; - }; - - observer->statusChangedFn = [&](OfflineRegionStatus status) { - if (!mapboxTileCountLimitExceededCalled) { - EXPECT_EQ(OfflineRegionDownloadState::Active, status.downloadState); - } else { - EXPECT_EQ(OfflineRegionDownloadState::Inactive, status.downloadState); - test.loop.stop(); - } - if (status.completedResourceCount > tileLimit) { - test.loop.stop(); - } - }; - - download.setObserver(std::move(observer)); - download.setState(OfflineRegionDownloadState::Active); - - test.loop.run(); -} - TEST(OfflineDownload, WithPreviouslyExistingTile) { OfflineTest test; auto region = test.createRegion(); diff --git a/test/util/mapbox.test.cpp b/test/util/mapbox.test.cpp index 2e0d8bf37afa..4dd34c20009a 100644 --- a/test/util/mapbox.test.cpp +++ b/test/util/mapbox.test.cpp @@ -12,297 +12,10 @@ using namespace mbgl; using SourceType = mbgl::style::SourceType; namespace mapboxFixture { -const TileServerOptions mapboxTileServerOptions = TileServerOptions::MapboxConfiguration(); const TileServerOptions mapLibreTileServerOptions = TileServerOptions::MapLibreConfiguration(); const TileServerOptions mapTilerTileServerOptions = TileServerOptions::MapTilerConfiguration(); } // namespace mapboxFixture -TEST(Mapbox, SourceURL) { - EXPECT_EQ( - "https://api.mapbox.com/v4/user.map.json?access_token=key&secure", - mbgl::util::mapbox::normalizeSourceURL(mapboxFixture::mapboxTileServerOptions, "mapbox://user.map", "key")); - EXPECT_EQ("https://api.example.com/v4/user.map.json?access_token=key&secure", - mbgl::util::mapbox::normalizeSourceURL( - TileServerOptions(mapboxFixture::mapboxTileServerOptions).withBaseURL("https://api.example.com"), - "mapbox://user.map", - "key")); - EXPECT_EQ( - "https://api.mapbox.com/v4/" - "user.map.json?access_token=key&secure&style=mapbox://styles/mapbox/" - "streets-v9@0", - mbgl::util::mapbox::normalizeSourceURL(mapboxFixture::mapboxTileServerOptions, - "mapbox://user.map?style=mapbox://styles/mapbox/streets-v9@0", - "key")); - EXPECT_EQ( - "https://api.mapbox.com/v4/user.map.json?access_token=key&secure", - mbgl::util::mapbox::normalizeSourceURL(mapboxFixture::mapboxTileServerOptions, "mapbox://user.map?", "key")); - EXPECT_EQ("http://path", - mbgl::util::mapbox::normalizeSourceURL(mapboxFixture::mapboxTileServerOptions, "http://path", "key")); - EXPECT_THROW( - mbgl::util::mapbox::normalizeSourceURL(mapboxFixture::mapboxTileServerOptions, "mapbox://user.map", ""), - std::runtime_error); -} - -TEST(Mapbox, GlyphsURL) { - EXPECT_EQ( - "https://api.mapbox.com/fonts/v1/boxmap/Comic%20Sans/" - "0-255.pbf?access_token=key", - mbgl::util::mapbox::normalizeGlyphsURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://fonts/boxmap/Comic%20Sans/0-255.pbf", "key")); - EXPECT_EQ( - "https://api.example.com/fonts/v1/boxmap/Comic%20Sans/" - "0-255.pbf?access_token=key", - mbgl::util::mapbox::normalizeGlyphsURL( - TileServerOptions(mapboxFixture::mapboxTileServerOptions).withBaseURL("https://api.example.com"), - "mapbox://fonts/boxmap/Comic%20Sans/0-255.pbf", - "key")); - EXPECT_EQ( - "https://api.mapbox.com/fonts/v1/boxmap/{fontstack}/" - "{range}.pbf?access_token=key", - mbgl::util::mapbox::normalizeGlyphsURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://fonts/boxmap/{fontstack}/{range}.pbf", "key")); - EXPECT_EQ("http://path", - mbgl::util::mapbox::normalizeGlyphsURL(mapboxFixture::mapboxTileServerOptions, "http://path", "key")); - EXPECT_EQ("mapbox://path", - mbgl::util::mapbox::normalizeGlyphsURL(mapboxFixture::mapboxTileServerOptions, "mapbox://path", "key")); -} - -TEST(Mapbox, StyleURL) { - EXPECT_EQ("mapbox://foo", - mbgl::util::mapbox::normalizeStyleURL(mapboxFixture::mapboxTileServerOptions, "mapbox://foo", "key")); - EXPECT_EQ("https://api.mapbox.com/styles/v1/user/style?access_token=key", - mbgl::util::mapbox::normalizeStyleURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://styles/user/style", "key")); - EXPECT_EQ("https://api.example.com/styles/v1/user/style?access_token=key", - mbgl::util::mapbox::normalizeStyleURL( - TileServerOptions(mapboxFixture::mapboxTileServerOptions).withBaseURL("https://api.example.com"), - "mapbox://styles/user/style", - "key")); - EXPECT_EQ("https://api.mapbox.com/styles/v1/user/style/draft?access_token=key", - mbgl::util::mapbox::normalizeStyleURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://styles/user/style/draft", "key")); - EXPECT_EQ( - "https://api.mapbox.com/styles/v1/user/" - "style?access_token=key&shave=true", - mbgl::util::mapbox::normalizeStyleURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://styles/user/style?shave=true", "key")); - EXPECT_EQ("https://api.mapbox.com/styles/v1/user/style?access_token=key", - mbgl::util::mapbox::normalizeStyleURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://styles/user/style?", "key")); - EXPECT_EQ("http://path", - mbgl::util::mapbox::normalizeStyleURL(mapboxFixture::mapboxTileServerOptions, "http://path", "key")); -} - -TEST(Mapbox, SpriteURL) { - EXPECT_EQ("map/box/sprites@2x.json", - mbgl::util::mapbox::normalizeSpriteURL( - mapboxFixture::mapboxTileServerOptions, "map/box/sprites@2x.json", "key")); - EXPECT_EQ("mapbox://foo", - mbgl::util::mapbox::normalizeSpriteURL(mapboxFixture::mapboxTileServerOptions, "mapbox://foo", "key")); - EXPECT_EQ( - "https://api.mapbox.com/styles/v1/mapbox/streets-v8/" - "sprite.json?access_token=key", - mbgl::util::mapbox::normalizeSpriteURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://sprites/mapbox/streets-v8.json", "key")); - EXPECT_EQ( - "https://api.example.com/styles/v1/mapbox/streets-v8/" - "sprite.json?access_token=key", - mbgl::util::mapbox::normalizeSpriteURL( - TileServerOptions(mapboxFixture::mapboxTileServerOptions).withBaseURL("https://api.example.com"), - "mapbox://sprites/mapbox/streets-v8.json", - "key")); - EXPECT_EQ( - "https://api.mapbox.com/styles/v1/mapbox/streets-v8/" - "sprite@2x.png?access_token=key", - mbgl::util::mapbox::normalizeSpriteURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://sprites/mapbox/streets-v8@2x.png", "key")); - EXPECT_EQ( - "https://api.mapbox.com/styles/v1/mapbox/streets-v8/draft/" - "sprite@2x.png?access_token=key", - mbgl::util::mapbox::normalizeSpriteURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://sprites/mapbox/streets-v8/draft@2x.png", "key")); - EXPECT_EQ( - "https://api.mapbox.com/styles/v1/mapbox/streets-v11/" - "sprite?access_token=key&fresh=true.png", - mbgl::util::mapbox::normalizeSpriteURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://sprites/mapbox/streets-v11?fresh=true.png", "key")); - EXPECT_EQ("mapbox://////", - mbgl::util::mapbox::normalizeSpriteURL(mapboxFixture::mapboxTileServerOptions, "mapbox://////", "key")); -} - -TEST(Mapbox, TileURL) { - EXPECT_EQ("https://api.mapbox.com/v4/a.b/0/0/0.pbf?access_token=key", - mbgl::util::mapbox::normalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://tiles/a.b/0/0/0.pbf", "key")); - EXPECT_EQ( - "https://api.mapbox.com/v4/a.b/0/0/" - "0.pbf?access_token=key&style=mapbox://styles/mapbox/streets-v9@0", - mbgl::util::mapbox::normalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "mapbox://tiles/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/" - "streets-v9@0", - "key")); - EXPECT_EQ("https://api.mapbox.com/v4/a.b/0/0/0.pbf?access_token=key", - mbgl::util::mapbox::normalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://tiles/a.b/0/0/0.pbf?", "key")); - EXPECT_EQ("https://api.mapbox.com/v4/a.b/0/0/0.png?access_token=key", - mbgl::util::mapbox::normalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://tiles/a.b/0/0/0.png", "key")); - EXPECT_EQ("https://api.example.com/v4/a.b/0/0/0.png?access_token=key", - mbgl::util::mapbox::normalizeTileURL( - TileServerOptions(mapboxFixture::mapboxTileServerOptions).withBaseURL("https://api.example.com"), - "mapbox://tiles/a.b/0/0/0.png", - "key")); - EXPECT_EQ("https://api.mapbox.com/v4/a.b/0/0/0@2x.png?access_token=key", - mbgl::util::mapbox::normalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://tiles/a.b/0/0/0@2x.png", "key")); - EXPECT_EQ("https://api.mapbox.com/v4/a.b,c.d/0/0/0.pbf?access_token=key", - mbgl::util::mapbox::normalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "mapbox://tiles/a.b,c.d/0/0/0.pbf", "key")); - EXPECT_EQ("http://path", - mbgl::util::mapbox::normalizeSpriteURL(mapboxFixture::mapboxTileServerOptions, "http://path", "key")); -} - -TEST(Mapbox, CanonicalURL) { - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://b.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/" - "{y}.vector.pbf?access_token=key", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "https://api.mapbox.cn/v4/a.b/{z}/{x}/" - "{y}.vector.pbf?access_token=key", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b,c.d/{z}/{x}/{y}.vector.pbf", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b,c.d/{z}/{x}/" - "{y}.vector.pbf?access_token=key", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/" - "{y}.vector.pbf?access_token=key&custom=parameter", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/" - "{y}.vector.pbf?custom=parameter&access_token=key", - SourceType::Vector, - 512)); - EXPECT_EQ( - "mapbox://tiles/a.b/{z}/{x}/" - "{y}.vector.pbf?custom=parameter&second=param", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/" - "{y}.vector.pbf?custom=parameter&access_token=key&second=param", - SourceType::Vector, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}{ratio}.jpg", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key", - SourceType::Raster, - 256)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}{ratio}.jpg70", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key", - SourceType::Raster, - 256)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}@2x.jpg", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key", - SourceType::Raster, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}@2x.jpg70", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key", - SourceType::Raster, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}{ratio}.png", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png", - SourceType::Raster, - 256)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}{ratio}.png", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key", - SourceType::Raster, - 256)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}@2x.png", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png", - SourceType::Raster, - 512)); - EXPECT_EQ("mapbox://tiles/a.b/{z}/{x}/{y}@2x.png", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key", - SourceType::Raster, - 512)); - - // We don't ever expect to see these inputs, but be safe anyway. - EXPECT_EQ( - "", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, "", SourceType::Raster, 256)); - EXPECT_EQ("http://path", - mbgl::util::mapbox::canonicalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "http://path", SourceType::Raster, 256)); - EXPECT_EQ("http://api.mapbox.com/v4/", - mbgl::util::mapbox::canonicalizeTileURL( - mapboxFixture::mapboxTileServerOptions, "http://api.mapbox.com/v4/", SourceType::Raster, 256)); - EXPECT_EQ("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.", - SourceType::Raster, - 256)); - EXPECT_EQ("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}/.", - mbgl::util::mapbox::canonicalizeTileURL(mapboxFixture::mapboxTileServerOptions, - "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}/.", - SourceType::Raster, - 256)); -} - -TEST(Mapbox, CanonicalizeRasterTileset) { - mbgl::Tileset tileset; - tileset.tiles = { - "http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/" - "{y}.png?access_token=key"}; - - mbgl::util::mapbox::canonicalizeTileset( - mapboxFixture::mapboxTileServerOptions, tileset, "mapbox://mapbox.satellite", SourceType::Raster, 256); - - EXPECT_EQ("mapbox://tiles/mapbox.satellite/{z}/{x}/{y}{ratio}.png", tileset.tiles[0]); -} - -TEST(Mapbox, CanonicalizeVectorTileset) { - mbgl::Tileset tileset; - tileset.tiles = { - "http://a.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/" - "{y}.vector.pbf?access_token=key"}; - - mbgl::util::mapbox::canonicalizeTileset( - mapboxFixture::mapboxTileServerOptions, tileset, "mapbox://mapbox.streets", SourceType::Vector, 512); - - EXPECT_EQ("mapbox://tiles/mapbox.streets/{z}/{x}/{y}.vector.pbf", tileset.tiles[0]); -} - // MapLibre tests TEST(MapLibre, CanonicalURL) { EXPECT_EQ( diff --git a/test/util/tile_server_options.test.cpp b/test/util/tile_server_options.test.cpp index f0f2c4f1d29b..89dfcfea8c4b 100644 --- a/test/util/tile_server_options.test.cpp +++ b/test/util/tile_server_options.test.cpp @@ -3,7 +3,7 @@ #include TEST(TileServerOptions, CopyAssignment) { - mbgl::TileServerOptions options = mbgl::TileServerOptions::MapboxConfiguration(); + mbgl::TileServerOptions options = mbgl::TileServerOptions::MapLibreConfiguration(); mbgl::TileServerOptions optionsCopy = options; EXPECT_FALSE(&optionsCopy == &options); @@ -11,7 +11,7 @@ TEST(TileServerOptions, CopyAssignment) { } TEST(TileServerOptions, CopyConstructor) { - mbgl::TileServerOptions options = mbgl::TileServerOptions::MapboxConfiguration(); + mbgl::TileServerOptions options = mbgl::TileServerOptions::MapLibreConfiguration(); mbgl::TileServerOptions optionsCopy = mbgl::TileServerOptions(options); EXPECT_FALSE(&optionsCopy == &options);