diff options
-rw-r--r-- | include/mbgl/storage/default_file_source.hpp | 6 | ||||
-rw-r--r-- | include/mbgl/storage/offline.hpp | 16 | ||||
-rw-r--r-- | platform/default/default_file_source.cpp | 8 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_database.cpp | 114 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_database.hpp | 13 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_download.cpp | 8 | ||||
-rw-r--r-- | src/mbgl/util/mapbox.cpp | 2 | ||||
-rw-r--r-- | src/mbgl/util/mapbox.hpp | 2 | ||||
-rw-r--r-- | test/fixtures/offline/mapbox_source.style.json | 17 | ||||
-rw-r--r-- | test/storage/offline_database.cpp | 57 | ||||
-rw-r--r-- | test/storage/offline_download.cpp | 33 |
11 files changed, 261 insertions, 15 deletions
diff --git a/include/mbgl/storage/default_file_source.hpp b/include/mbgl/storage/default_file_source.hpp index 3e839bbc18..93d6297651 100644 --- a/include/mbgl/storage/default_file_source.hpp +++ b/include/mbgl/storage/default_file_source.hpp @@ -94,6 +94,12 @@ public: */ void deleteOfflineRegion(OfflineRegion&&, std::function<void (std::exception_ptr)>); + /* + * Changing or bypassing this limit without permission from Mapbox is prohibited + * by the Mapbox Terms of Service. + */ + void setOfflineMapboxTileCountLimit(uint64_t) const; + // For testing only. void put(const Resource&, const Response&); void goOffline(); diff --git a/include/mbgl/storage/offline.hpp b/include/mbgl/storage/offline.hpp index 2bc286dbab..a3153adc28 100644 --- a/include/mbgl/storage/offline.hpp +++ b/include/mbgl/storage/offline.hpp @@ -150,6 +150,22 @@ public: * re-executes the user-provided implementation on the main thread. */ virtual void responseError(Response::Error) {} + + /* + * 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. Contact your + * Mapbox sales representative to raise the limit. + * + * 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/platform/default/default_file_source.cpp b/platform/default/default_file_source.cpp index bdf02ee69a..24297374c9 100644 --- a/platform/default/default_file_source.cpp +++ b/platform/default/default_file_source.cpp @@ -113,6 +113,10 @@ public: tasks.erase(req); } + void setOfflineMapboxTileCountLimit(uint64_t limit) { + offlineDatabase.setOfflineMapboxTileCountLimit(limit); + } + void put(const Resource& resource, const Response& response) { offlineDatabase.put(resource, response); } @@ -205,6 +209,10 @@ void DefaultFileSource::getOfflineRegionStatus(OfflineRegion& region, std::funct thread->invoke(&Impl::getRegionStatus, region.getID(), callback); } +void DefaultFileSource::setOfflineMapboxTileCountLimit(uint64_t limit) const { + thread->invokeSync(&Impl::setOfflineMapboxTileCountLimit, limit); +} + // For testing only: void DefaultFileSource::put(const Resource& resource, const Response& response) { diff --git a/platform/default/mbgl/storage/offline_database.cpp b/platform/default/mbgl/storage/offline_database.cpp index 983b84a3b4..df3e3f4dc3 100644 --- a/platform/default/mbgl/storage/offline_database.cpp +++ b/platform/default/mbgl/storage/offline_database.cpp @@ -447,6 +447,9 @@ void OfflineDatabase::deleteRegion(OfflineRegion&& region) { stmt->run(); evict(0); + + // Ensure that the cached offlineTileCount value is recalculated. + offlineMapboxTileCount = {}; } optional<Response> OfflineDatabase::getRegionResource(int64_t regionID, const Resource& resource) { @@ -461,13 +464,21 @@ optional<Response> OfflineDatabase::getRegionResource(int64_t regionID, const Re uint64_t OfflineDatabase::putRegionResource(int64_t regionID, const Resource& resource, const Response& response) { uint64_t size = putInternal(resource, response, false).second; - markUsed(regionID, resource); + bool previouslyUnused = markUsed(regionID, resource); + + if (offlineMapboxTileCount + && resource.kind == Resource::Kind::Tile + && util::mapbox::isMapboxURL(resource.url) + && previouslyUnused) { + *offlineMapboxTileCount += 1; + } + return size; } -void OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) { +bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) { if (resource.kind == Resource::Kind::Tile) { - Statement stmt = getStatement( + Statement insert = getStatement( "INSERT OR IGNORE INTO region_tiles (region_id, tile_id) " "SELECT ?1, tiles.id " "FROM tiles " @@ -478,23 +489,61 @@ void OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) { " AND z = ?6 "); const Resource::TileData& tile = *resource.tileData; - stmt->bind(1, regionID); - stmt->bind(2, tile.urlTemplate); - stmt->bind(3, tile.pixelRatio); - stmt->bind(4, tile.x); - stmt->bind(5, tile.y); - stmt->bind(6, tile.z); - stmt->run(); + insert->bind(1, regionID); + insert->bind(2, tile.urlTemplate); + insert->bind(3, tile.pixelRatio); + insert->bind(4, tile.x); + insert->bind(5, tile.y); + insert->bind(6, tile.z); + insert->run(); + + if (db->changes() == 0) { + return false; + } + + Statement select = getStatement( + "SELECT region_id " + "FROM region_tiles, tiles " + "WHERE region_id != ?1 " + " AND url_template = ?2 " + " AND pixel_ratio = ?3 " + " AND x = ?4 " + " AND y = ?5 " + " AND z = ?6 " + "LIMIT 1 "); + + select->bind(1, regionID); + select->bind(2, tile.urlTemplate); + select->bind(3, tile.pixelRatio); + select->bind(4, tile.x); + select->bind(5, tile.y); + select->bind(6, tile.z); + return !select->run(); } else { - Statement stmt = getStatement( + Statement insert = getStatement( "INSERT OR IGNORE INTO region_resources (region_id, resource_id) " "SELECT ?1, resources.id " "FROM resources " "WHERE resources.url = ?2 "); - stmt->bind(1, regionID); - stmt->bind(2, resource.url); - stmt->run(); + insert->bind(1, regionID); + insert->bind(2, resource.url); + insert->run(); + + if (db->changes() == 0) { + return false; + } + + Statement select = getStatement( + "SELECT region_id " + "FROM region_resources, resources " + "WHERE region_id != ?1 " + " AND resources.url = ?2 " + "LIMIT 1 "); + + select->bind(1, regionID); + select->bind(2, resource.url); + return !select->run(); } } @@ -591,6 +640,9 @@ bool OfflineDatabase::evict(uint64_t neededFreeSize) { stmt2->run(); uint64_t changes2 = db->changes(); + // The cached value of offlineTileCount does not need to be updated + // here because only non-offline tiles can be removed by eviction. + if (changes1 == 0 && changes2 == 0) { return false; } @@ -599,4 +651,38 @@ bool OfflineDatabase::evict(uint64_t neededFreeSize) { return true; } +void OfflineDatabase::setOfflineMapboxTileCountLimit(uint64_t limit) { + offlineMapboxTileCountLimit = limit; +} + +uint64_t OfflineDatabase::getOfflineMapboxTileCountLimit() { + return offlineMapboxTileCountLimit; +} + +bool OfflineDatabase::offlineMapboxTileCountLimitExceeded() { + return getOfflineMapboxTileCount() >= offlineMapboxTileCountLimit; +} + +uint64_t OfflineDatabase::getOfflineMapboxTileCount() { + // 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; + } + + Statement stmt = getStatement( + "SELECT COUNT(DISTINCT id) " + "FROM region_tiles, tiles " + "WHERE tile_id = tiles.id " + "AND url_template LIKE 'mapbox://%' "); + + stmt->run(); + + offlineMapboxTileCount = stmt->get<int64_t>(0); + return *offlineMapboxTileCount; +} + } // namespace mbgl diff --git a/platform/default/mbgl/storage/offline_database.hpp b/platform/default/mbgl/storage/offline_database.hpp index f207ab406a..fc3f729bff 100644 --- a/platform/default/mbgl/storage/offline_database.hpp +++ b/platform/default/mbgl/storage/offline_database.hpp @@ -6,6 +6,7 @@ #include <mbgl/util/noncopyable.hpp> #include <mbgl/util/optional.hpp> #include <mbgl/util/constants.hpp> +#include <mbgl/util/mapbox.hpp> #include <unordered_map> #include <memory> @@ -49,6 +50,11 @@ public: OfflineRegionDefinition getRegionDefinition(int64_t regionID); OfflineRegionStatus getRegionCompletedStatus(int64_t regionID); + void setOfflineMapboxTileCountLimit(uint64_t); + uint64_t getOfflineMapboxTileCountLimit(); + bool offlineMapboxTileCountLimitExceeded(); + uint64_t getOfflineMapboxTileCount(); + private: void connect(int flags); void ensureSchema(); @@ -78,7 +84,9 @@ private: const std::string&, bool compressed); std::pair<bool, uint64_t> putInternal(const Resource&, const Response&, bool evict); - void markUsed(int64_t regionID, const Resource&); + + // Return value is true iff the resource was previously unused by any other regions. + bool markUsed(int64_t regionID, const Resource&); const std::string path; std::unique_ptr<::mapbox::sqlite::Database> db; @@ -89,6 +97,9 @@ private: uint64_t maximumCacheSize; + uint64_t offlineMapboxTileCountLimit = util::mapbox::DEFAULT_OFFLINE_TILE_COUNT_LIMIT; + optional<uint64_t> offlineMapboxTileCount; + bool evict(uint64_t neededFreeSize); }; diff --git a/platform/default/mbgl/storage/offline_download.cpp b/platform/default/mbgl/storage/offline_download.cpp index 228942f1f0..8aa58d6c34 100644 --- a/platform/default/mbgl/storage/offline_download.cpp +++ b/platform/default/mbgl/storage/offline_download.cpp @@ -7,6 +7,7 @@ #include <mbgl/layer/symbol_layer.hpp> #include <mbgl/text/glyph.hpp> #include <mbgl/util/tile_cover.hpp> +#include <mbgl/util/mapbox.hpp> #include <set> @@ -218,6 +219,13 @@ void OfflineDownload::ensureResource(const Resource& resource, std::function<voi return; } + if (resource.kind == Resource::Kind::Tile + && util::mapbox::isMapboxURL(resource.url) + && offlineDatabase.offlineMapboxTileCountLimitExceeded()) { + observer->mapboxTileCountLimitExceeded(offlineDatabase.getOfflineMapboxTileCountLimit()); + return; + } + auto it = requests.insert(requests.begin(), nullptr); *it = onlineFileSource.request(resource, [=] (Response onlineResponse) { if (onlineResponse.error) { diff --git a/src/mbgl/util/mapbox.cpp b/src/mbgl/util/mapbox.cpp index 7ee0f279b6..bcc7601446 100644 --- a/src/mbgl/util/mapbox.cpp +++ b/src/mbgl/util/mapbox.cpp @@ -169,6 +169,8 @@ std::string canonicalizeTileURL(const std::string& url, SourceType type, uint16_ return result; } +const uint64_t DEFAULT_OFFLINE_TILE_COUNT_LIMIT = std::numeric_limits<uint64_t>::max(); + } // end namespace mapbox } // end namespace util } // end namespace mbgl diff --git a/src/mbgl/util/mapbox.hpp b/src/mbgl/util/mapbox.hpp index bb0536cfa2..56c40df7ca 100644 --- a/src/mbgl/util/mapbox.hpp +++ b/src/mbgl/util/mapbox.hpp @@ -19,6 +19,8 @@ std::string normalizeTileURL(const std::string& url, const std::string& accessTo // Return a "mapbox://tiles/..." URL (suitable for normalizeTileURL) for the given Mapbox tile URL. std::string canonicalizeTileURL(const std::string& url, SourceType, uint16_t tileSize); +extern const uint64_t DEFAULT_OFFLINE_TILE_COUNT_LIMIT; + } // namespace mapbox } // namespace util } // namespace mbgl diff --git a/test/fixtures/offline/mapbox_source.style.json b/test/fixtures/offline/mapbox_source.style.json new file mode 100644 index 0000000000..75bfd6667d --- /dev/null +++ b/test/fixtures/offline/mapbox_source.style.json @@ -0,0 +1,17 @@ +{ + "version": 8, + "sources": { + "inline": { + "type": "vector", + "maxzoom": 15, + "minzoom": 0, + "tiles": [ "mapbox://{z}-{x}-{y}.vector.pbf" ] + } + }, + "layers": [{ + "id": "fill", + "type": "fill", + "source": "inline", + "source-layer": "water" + }] +} diff --git a/test/storage/offline_database.cpp b/test/storage/offline_database.cpp index d93945fb40..82e07c208a 100644 --- a/test/storage/offline_database.cpp +++ b/test/storage/offline_database.cpp @@ -603,3 +603,60 @@ TEST(OfflineDatabase, PutFailsWhenEvictionInsuffices) { auto flo = dynamic_cast<FixtureLogObserver*>(observer.get()); EXPECT_EQ(1ul, flo->count({ EventSeverity::Warning, Event::Database, -1, "Unable to make space for entry" })); } + +TEST(OfflineDatabase, OfflineMapboxTileCount) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + OfflineRegionDefinition definition { "http://example.com/style", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0 }; + OfflineRegionMetadata metadata; + + OfflineRegion region1 = db.createRegion(definition, metadata); + OfflineRegion region2 = db.createRegion(definition, metadata); + + Resource nonMapboxTile = Resource::tile("http://example.com/", 1.0, 0, 0, 0); + Resource mapboxTile1 = Resource::tile("mapbox://tiles/1", 1.0, 0, 0, 0); + Resource mapboxTile2 = Resource::tile("mapbox://tiles/2", 1.0, 0, 0, 1); + + Response response; + response.data = std::make_shared<std::string>("data"); + + // Count is initially zero. + EXPECT_EQ(0, db.getOfflineMapboxTileCount()); + + // Count stays the same after putting a non-tile resource. + db.putRegionResource(region1.getID(), Resource::style("http://example.com/"), response); + EXPECT_EQ(0, db.getOfflineMapboxTileCount()); + + // Count stays the same after putting a non-Mapbox tile. + db.putRegionResource(region1.getID(), nonMapboxTile, response); + EXPECT_EQ(0, db.getOfflineMapboxTileCount()); + + // Count increases after putting a Mapbox tile not used by another region. + db.putRegionResource(region1.getID(), mapboxTile1, response); + EXPECT_EQ(1, db.getOfflineMapboxTileCount()); + + // Count stays the same after putting a Mapbox tile used by another region. + db.putRegionResource(region2.getID(), mapboxTile1, response); + EXPECT_EQ(1, db.getOfflineMapboxTileCount()); + + // Count stays the same after putting a Mapbox tile used by the same region. + db.putRegionResource(region2.getID(), mapboxTile1, response); + EXPECT_EQ(1, 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(1, db.getOfflineMapboxTileCount()); + + // Count stays the same after the putting a non-offline Mapbox tile. + db.put(mapboxTile2, response); + EXPECT_EQ(1, db.getOfflineMapboxTileCount()); + + // Count increases after putting a pre-existing, but non-offline Mapbox tile. + db.putRegionResource(region1.getID(), mapboxTile2, response); + EXPECT_EQ(2, db.getOfflineMapboxTileCount()); + + // Count decreases after deleting a region when the tiles are not used by other regions. + db.deleteRegion(std::move(region1)); + EXPECT_EQ(0, db.getOfflineMapboxTileCount()); +} diff --git a/test/storage/offline_download.cpp b/test/storage/offline_download.cpp index 73a6f80fcc..fce081b8ab 100644 --- a/test/storage/offline_download.cpp +++ b/test/storage/offline_download.cpp @@ -24,8 +24,13 @@ public: if (responseErrorFn) responseErrorFn(error); } + void mapboxTileCountLimitExceeded(uint64_t limit) override { + if (mapboxTileCountLimitExceededFn) mapboxTileCountLimitExceededFn(limit); + } + std::function<void (OfflineRegionStatus)> statusChangedFn; std::function<void (Response::Error)> responseErrorFn; + std::function<void (uint64_t)> mapboxTileCountLimitExceededFn; }; class OfflineTest { @@ -347,3 +352,31 @@ TEST(OfflineDownload, RequestErrorsAreRetried) { test.loop.run(); } + +TEST(OfflineDownload, TileCountLimitExceeded) { + OfflineTest test; + OfflineRegion region = test.createRegion(); + OfflineDownload download( + region.getID(), + OfflineTilePyramidRegionDefinition("http://127.0.0.1:3000/offline/style.json", LatLngBounds::world(), 0.0, 0.0, 1.0), + test.db, test.fileSource); + + test.db.setOfflineMapboxTileCountLimit(0); + + test.fileSource.styleResponse = [&] (const Resource& resource) { + EXPECT_EQ("http://127.0.0.1:3000/offline/style.json", resource.url); + return test.response("offline/mapbox_source.style.json"); + }; + + auto observer = std::make_unique<MockObserver>(); + + observer->mapboxTileCountLimitExceededFn = [&] (uint64_t limit) { + EXPECT_EQ(0, limit); + test.loop.stop(); + }; + + download.setObserver(std::move(observer)); + download.setState(OfflineRegionDownloadState::Active); + + test.loop.run(); +} |