diff options
author | John Firebaugh <john.firebaugh@gmail.com> | 2016-02-05 17:10:13 -0800 |
---|---|---|
committer | John Firebaugh <john.firebaugh@gmail.com> | 2016-02-10 15:40:20 -0800 |
commit | c3c4c7b9a695ad1dbebe57242ba071103fe9a567 (patch) | |
tree | e205ecdc6a2f6318c6ba6308b5aa8baacc42f481 /platform | |
parent | e9302c797f68c7e48b908b87b126045c8c5e5209 (diff) | |
download | qtlocation-mapboxgl-c3c4c7b9a695ad1dbebe57242ba071103fe9a567.tar.gz |
[core] Interface and implementation for offline
Diffstat (limited to 'platform')
-rw-r--r-- | platform/default/default_file_source.cpp | 98 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline.cpp | 122 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_database.cpp | 171 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_database.hpp | 23 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_download.cpp | 241 | ||||
-rw-r--r-- | platform/default/mbgl/storage/offline_download.hpp | 62 | ||||
-rw-r--r-- | platform/default/sqlite3.cpp | 41 | ||||
-rw-r--r-- | platform/default/sqlite3.hpp | 13 |
8 files changed, 755 insertions, 16 deletions
diff --git a/platform/default/default_file_source.cpp b/platform/default/default_file_source.cpp index efe893d49b..5bb171f7c2 100644 --- a/platform/default/default_file_source.cpp +++ b/platform/default/default_file_source.cpp @@ -2,6 +2,7 @@ #include <mbgl/storage/asset_file_source.hpp> #include <mbgl/storage/online_file_source.hpp> #include <mbgl/storage/offline_database.hpp> +#include <mbgl/storage/offline_download.hpp> #include <mbgl/platform/platform.hpp> #include <mbgl/util/url.hpp> @@ -61,6 +62,58 @@ public: return onlineFileSource.getAccessToken(); } + void listRegions(std::function<void (std::exception_ptr, optional<std::vector<OfflineRegion>>)> callback) { + try { + callback({}, offlineDatabase.listRegions()); + } catch (...) { + callback(std::current_exception(), {}); + } + } + + void createRegion(const OfflineRegionDefinition& definition, + const OfflineRegionMetadata& metadata, + std::function<void (std::exception_ptr, optional<OfflineRegion>)> callback) { + try { + callback({}, offlineDatabase.createRegion(definition, metadata)); + } catch (...) { + callback(std::current_exception(), {}); + } + } + + void getRegionStatus(int64_t regionID, std::function<void (std::exception_ptr, optional<OfflineRegionStatus>)> callback) { + try { + callback({}, getDownload(regionID).getStatus()); + } catch (...) { + callback(std::current_exception(), {}); + } + } + + void deleteRegion(OfflineRegion&& region, std::function<void (std::exception_ptr)> callback) { + try { + offlineDatabase.deleteRegion(std::move(region)); + callback({}); + } catch (...) { + callback(std::current_exception()); + } + } + + void removeUnusedOfflineResources(std::function<void (std::exception_ptr)> callback) { + try { + offlineDatabase.removeUnusedResources(); + callback({}); + } catch (...) { + callback(std::current_exception()); + } + } + + void setRegionObserver(int64_t regionID, std::unique_ptr<OfflineRegionObserver> observer) { + getDownload(regionID).setObserver(std::move(observer)); + } + + void setRegionDownloadState(int64_t regionID, OfflineRegionDownloadState state) { + getDownload(regionID).setState(state); + } + void add(FileRequest* req, Resource resource, Callback callback) { tasks[req] = std::make_unique<Task>(resource, callback, this); } @@ -77,15 +130,26 @@ public: offline = true; } +private: + OfflineDownload& getDownload(int64_t regionID) { + auto it = downloads.find(regionID); + if (it != downloads.end()) { + return *it->second; + } + return *downloads.emplace(regionID, + std::make_unique<OfflineDownload>(regionID, offlineDatabase.getRegionDefinition(regionID), offlineDatabase, onlineFileSource)).first->second; + } + OfflineDatabase offlineDatabase; OnlineFileSource onlineFileSource; std::unordered_map<FileRequest*, std::unique_ptr<Task>> tasks; + std::unordered_map<int64_t, std::unique_ptr<OfflineDownload>> downloads; bool offline = false; }; class DefaultFileRequest : public FileRequest { public: - DefaultFileRequest(Resource resource, FileSource::Callback callback, util::Thread<DefaultFileSource::Impl>& thread_) + DefaultFileRequest(Resource resource, FileSource::Callback callback, util::Thread<DefaultFileSource::Impl>& thread_) : thread(thread_), workRequest(thread.invokeWithCallback(&DefaultFileSource::Impl::add, callback, this, resource)) { } @@ -129,6 +193,38 @@ std::unique_ptr<FileRequest> DefaultFileSource::request(const Resource& resource } } +void DefaultFileSource::listOfflineRegions(std::function<void (std::exception_ptr, optional<std::vector<OfflineRegion>>)> callback) { + thread->invoke(&Impl::listRegions, callback); +} + +void DefaultFileSource::createOfflineRegion(const OfflineRegionDefinition& definition, + const OfflineRegionMetadata& metadata, + std::function<void (std::exception_ptr, optional<OfflineRegion>)> callback) { + thread->invoke(&Impl::createRegion, definition, metadata, callback); +} + +void DefaultFileSource::deleteOfflineRegion(OfflineRegion&& region, std::function<void (std::exception_ptr)> callback) { + thread->invoke(&Impl::deleteRegion, std::move(region), callback); +} + +void DefaultFileSource::setOfflineRegionObserver(OfflineRegion& region, std::unique_ptr<OfflineRegionObserver> observer) { + thread->invoke(&Impl::setRegionObserver, region.getID(), std::move(observer)); +} + +void DefaultFileSource::setOfflineRegionDownloadState(OfflineRegion& region, OfflineRegionDownloadState state) { + thread->invoke(&Impl::setRegionDownloadState, region.getID(), state); +} + +void DefaultFileSource::getOfflineRegionStatus(OfflineRegion& region, std::function<void (std::exception_ptr, optional<OfflineRegionStatus>)> callback) const { + thread->invoke(&Impl::getRegionStatus, region.getID(), callback); +} + +void DefaultFileSource::removeUnusedOfflineResources(std::function<void (std::exception_ptr)> callback) { + thread->invoke(&Impl::removeUnusedOfflineResources, callback); +} + +// For testing only: + void DefaultFileSource::put(const Resource& resource, const Response& response) { thread->invokeSync(&Impl::put, resource, response); } diff --git a/platform/default/mbgl/storage/offline.cpp b/platform/default/mbgl/storage/offline.cpp new file mode 100644 index 0000000000..7311474bcf --- /dev/null +++ b/platform/default/mbgl/storage/offline.cpp @@ -0,0 +1,122 @@ +#include <mbgl/storage/offline.hpp> +#include <mbgl/util/tile_cover.hpp> +#include <mbgl/map/source_info.hpp> + +#include <rapidjson/document.h> +#include <rapidjson/stringbuffer.h> +#include <rapidjson/writer.h> + +#include <cmath> + +namespace mbgl { + +OfflineTilePyramidRegionDefinition::OfflineTilePyramidRegionDefinition( + const std::string& styleURL_, const LatLngBounds& bounds_, double minZoom_, double maxZoom_, float pixelRatio_) + : styleURL(styleURL_), + bounds(bounds_), + minZoom(minZoom_), + maxZoom(maxZoom_), + pixelRatio(pixelRatio_) { + if (minZoom < 0 || maxZoom < 0 || maxZoom < minZoom || pixelRatio < 0 || + !std::isfinite(minZoom) || std::isnan(maxZoom) || !std::isfinite(pixelRatio)) { + throw std::invalid_argument("Invalid offline region definition"); + } +} + +std::vector<TileID> OfflineTilePyramidRegionDefinition::tileCover(SourceType type, uint16_t tileSize, const SourceInfo& info) const { + double minZ = std::max<double>(coveringZoomLevel(minZoom, type, tileSize), info.minZoom); + double maxZ = std::min<double>(coveringZoomLevel(maxZoom, type, tileSize), info.maxZoom); + + assert(minZ >= 0); + assert(maxZ >= 0); + assert(minZ < std::numeric_limits<uint8_t>::max()); + assert(maxZ < std::numeric_limits<uint8_t>::max()); + + std::vector<TileID> result; + + for (uint8_t z = minZ; z <= maxZ; z++) { + for (const auto& tile : mbgl::tileCover(bounds, z, z)) { + result.push_back(tile.normalized()); + } + } + + return result; +} + +OfflineRegionDefinition decodeOfflineRegionDefinition(const std::string& region) { + rapidjson::GenericDocument<rapidjson::UTF8<>, rapidjson::CrtAllocator> doc; + doc.Parse<0>(region.c_str()); + + if (doc.HasParseError() || + !doc.HasMember("style_url") || !doc["style_url"].IsString() || + !doc.HasMember("bounds") || !doc["bounds"].IsArray() || doc["bounds"].Size() != 4 || + !doc["bounds"][0].IsDouble() || !doc["bounds"][1].IsDouble() || + !doc["bounds"][2].IsDouble() || !doc["bounds"][3].IsDouble() || + !doc.HasMember("min_zoom") || !doc["min_zoom"].IsDouble() || + (doc.HasMember("max_zoom") && !doc["max_zoom"].IsDouble()) || + !doc.HasMember("pixel_ratio") || !doc["pixel_ratio"].IsDouble()) { + throw std::runtime_error("Malformed offline region definition"); + } + + std::string styleURL { doc["style_url"].GetString(), doc["style_url"].GetStringLength() }; + LatLngBounds bounds = LatLngBounds::hull( + LatLng(doc["bounds"][0].GetDouble(), doc["bounds"][1].GetDouble()), + LatLng(doc["bounds"][2].GetDouble(), doc["bounds"][3].GetDouble())); + double minZoom = doc["min_zoom"].GetDouble(); + double maxZoom = doc.HasMember("max_zoom") ? doc["max_zoom"].GetDouble() : INFINITY; + float pixelRatio = doc["pixel_ratio"].GetDouble(); + + return { styleURL, bounds, minZoom, maxZoom, pixelRatio }; +} + +std::string encodeOfflineRegionDefinition(const OfflineRegionDefinition& region) { + rapidjson::GenericDocument<rapidjson::UTF8<>, rapidjson::CrtAllocator> doc; + doc.SetObject(); + + doc.AddMember("style_url", rapidjson::StringRef(region.styleURL.data(), region.styleURL.length()), doc.GetAllocator()); + + rapidjson::GenericValue<rapidjson::UTF8<>, rapidjson::CrtAllocator> bounds(rapidjson::kArrayType); + bounds.PushBack(region.bounds.south(), doc.GetAllocator()); + bounds.PushBack(region.bounds.west(), doc.GetAllocator()); + bounds.PushBack(region.bounds.north(), doc.GetAllocator()); + bounds.PushBack(region.bounds.east(), doc.GetAllocator()); + doc.AddMember("bounds", bounds, doc.GetAllocator()); + + doc.AddMember("min_zoom", region.minZoom, doc.GetAllocator()); + if (std::isfinite(region.maxZoom)) { + doc.AddMember("max_zoom", region.maxZoom, doc.GetAllocator()); + } + + doc.AddMember("pixel_ratio", region.pixelRatio, doc.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + doc.Accept(writer); + + return buffer.GetString(); +} + +OfflineRegion::OfflineRegion(int64_t id_, + const OfflineRegionDefinition& definition_, + const OfflineRegionMetadata& metadata_) + : id(id_), + definition(definition_), + metadata(metadata_) { +} + +OfflineRegion::OfflineRegion(OfflineRegion&&) = default; +OfflineRegion::~OfflineRegion() = default; + +const OfflineRegionDefinition& OfflineRegion::getDefinition() const { + return definition; +} + +const OfflineRegionMetadata& OfflineRegion::getMetadata() const { + return metadata; +} + +int64_t OfflineRegion::getID() const { + return id; +} + +} // namespace mbgl diff --git a/platform/default/mbgl/storage/offline_database.cpp b/platform/default/mbgl/storage/offline_database.cpp index 67fcc435c6..c6510157bc 100644 --- a/platform/default/mbgl/storage/offline_database.cpp +++ b/platform/default/mbgl/storage/offline_database.cpp @@ -100,17 +100,17 @@ optional<Response> OfflineDatabase::get(const Resource& resource) { } } -void OfflineDatabase::put(const Resource& resource, const Response& response) { +uint64_t OfflineDatabase::put(const Resource& resource, const Response& response) { // Don't store errors in the cache. if (response.error) { - return; + return 0; } if (resource.kind == Resource::Kind::Tile) { assert(resource.tileData); - putTile(*resource.tileData, response); + return putTile(*resource.tileData, response); } else { - putResource(resource, response); + return putResource(resource, response); } } @@ -145,7 +145,7 @@ optional<Response> OfflineDatabase::getResource(const Resource& resource) { return response; } -void OfflineDatabase::putResource(const Resource& resource, const Response& response) { +uint64_t OfflineDatabase::putResource(const Resource& resource, const Response& response) { if (response.notModified) { mapbox::sqlite::Statement& stmt = getStatement( // 1 2 3 @@ -155,6 +155,7 @@ void OfflineDatabase::putResource(const Resource& resource, const Response& resp stmt.bind(2, response.expires); stmt.bind(3, resource.url); stmt.run(); + return 0; } else { mapbox::sqlite::Statement& stmt = getStatement( // 1 2 3 4 5 6 7 8 @@ -169,6 +170,7 @@ void OfflineDatabase::putResource(const Resource& resource, const Response& resp stmt.bind(6 /* accessed */, SystemClock::now()); std::string data; + uint64_t size = 0; if (response.noContent) { stmt.bind(7 /* data */, nullptr); @@ -176,15 +178,18 @@ void OfflineDatabase::putResource(const Resource& resource, const Response& resp } else { data = util::compress(*response.data); if (data.size() < response.data->size()) { - stmt.bind(7 /* data */, data, false); // do not retain the string internally. + size = data.size(); + stmt.bindBlob(7 /* data */, data.data(), size, false); stmt.bind(8 /* compressed */, true); } else { - stmt.bind(7 /* data */, *response.data, false); // do not retain the string internally. + size = response.data->size(); + stmt.bindBlob(7 /* data */, response.data->data(), size, false); stmt.bind(8 /* compressed */, false); } } stmt.run(); + return size; } } @@ -228,7 +233,7 @@ optional<Response> OfflineDatabase::getTile(const Resource::TileData& tile) { return response; } -void OfflineDatabase::putTile(const Resource::TileData& tile, const Response& response) { +uint64_t OfflineDatabase::putTile(const Resource::TileData& tile, const Response& response) { if (response.notModified) { mapbox::sqlite::Statement& stmt = getStatement( "UPDATE tiles SET accessed = ?1, expires = ?2 " @@ -248,6 +253,7 @@ void OfflineDatabase::putTile(const Resource::TileData& tile, const Response& re stmt.bind(6, tile.y); stmt.bind(7, tile.z); stmt.run(); + return 0; } else { mapbox::sqlite::Statement& stmt1 = getStatement( "REPLACE INTO tilesets (url_template, pixel_ratio) " @@ -275,6 +281,7 @@ void OfflineDatabase::putTile(const Resource::TileData& tile, const Response& re stmt2.bind(9 /* accessed */, SystemClock::now()); std::string data; + uint64_t size = 0; if (response.noContent) { stmt2.bind(10 /* data */, nullptr); @@ -282,16 +289,160 @@ void OfflineDatabase::putTile(const Resource::TileData& tile, const Response& re } else { data = util::compress(*response.data); if (data.size() < response.data->size()) { - stmt2.bind(10 /* data */, data, false); // do not retain the string internally. + size = data.size(); + stmt2.bindBlob(10 /* data */, data.data(), size, false); stmt2.bind(11 /* compressed */, true); } else { - stmt2.bind(10 /* data */, *response.data, false); // do not retain the string internally. + size = response.data->size(); + stmt2.bindBlob(10 /* data */, response.data->data(), size, false); stmt2.bind(11 /* compressed */, false); } } stmt2.run(); + return size; } } +std::vector<OfflineRegion> OfflineDatabase::listRegions() { + mapbox::sqlite::Statement& stmt = getStatement( + "SELECT id, definition, description FROM regions"); + + std::vector<OfflineRegion> result; + + while (stmt.run()) { + result.push_back(OfflineRegion( + stmt.get<int64_t>(0), + decodeOfflineRegionDefinition(stmt.get<std::string>(1)), + stmt.get<std::vector<uint8_t>>(2))); + } + + return std::move(result); +} + +OfflineRegion OfflineDatabase::createRegion(const OfflineRegionDefinition& definition, + const OfflineRegionMetadata& metadata) { + mapbox::sqlite::Statement& stmt = getStatement( + "INSERT INTO regions (definition, description) " + "VALUES (?1, ?2) "); + + stmt.bind(1, encodeOfflineRegionDefinition(definition)); + stmt.bindBlob(2, metadata); + stmt.run(); + + return OfflineRegion(db->lastInsertRowid(), definition, metadata); +} + +void OfflineDatabase::deleteRegion(OfflineRegion&& region) { + mapbox::sqlite::Statement& stmt = getStatement( + "DELETE FROM regions WHERE id = ?"); + + stmt.bind(1, region.getID()); + stmt.run(); +} + +optional<Response> OfflineDatabase::getRegionResource(int64_t regionID, const Resource& resource) { + auto response = get(resource); + + if (response) { + markUsed(regionID, resource); + } + + return response; +} + +uint64_t OfflineDatabase::putRegionResource(int64_t regionID, const Resource& resource, const Response& response) { + uint64_t result = put(resource, response); + markUsed(regionID, resource); + return result; +} + +void OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) { + if (resource.kind == Resource::Kind::Tile) { + mapbox::sqlite::Statement& stmt1 = getStatement( + "REPLACE INTO region_tiles (region_id, tileset_id, x, y, z) " + "SELECT ?1, tilesets.id, ?4, ?5, ?6 " + "FROM tilesets " + "WHERE url_template = ?2 " + "AND pixel_ratio = ?3 "); + + stmt1.bind(1, regionID); + stmt1.bind(2, (*resource.tileData).urlTemplate); + stmt1.bind(3, (*resource.tileData).pixelRatio); + stmt1.bind(4, (*resource.tileData).x); + stmt1.bind(5, (*resource.tileData).y); + stmt1.bind(6, (*resource.tileData).z); + stmt1.run(); + } else { + mapbox::sqlite::Statement& stmt1 = getStatement( + "REPLACE INTO region_resources (region_id, resource_url) " + "VALUES (?1, ?2) "); + + stmt1.bind(1, regionID); + stmt1.bind(2, resource.url); + stmt1.run(); + } +} + +OfflineRegionDefinition OfflineDatabase::getRegionDefinition(int64_t regionID) { + mapbox::sqlite::Statement& stmt = getStatement( + "SELECT definition FROM regions WHERE id = ?1"); + + stmt.bind(1, regionID); + stmt.run(); + + return decodeOfflineRegionDefinition(stmt.get<std::string>(0)); +} + +OfflineRegionStatus OfflineDatabase::getRegionCompletedStatus(int64_t regionID) { + OfflineRegionStatus result; + + mapbox::sqlite::Statement& stmt = getStatement( + "SELECT COUNT(*), SUM(size) FROM ( " + " SELECT LENGTH(data) as size " + " FROM region_resources, resources " + " WHERE region_id = ?1 " + " AND resources.url = region_resources.resource_url " + " UNION ALL " + " SELECT LENGTH(data) as size " + " FROM region_tiles, tiles " + " WHERE region_id = ?1 " + " AND tiles.tileset_id = region_tiles.tileset_id " + " AND tiles.z = region_tiles.z " + " AND tiles.x = region_tiles.x " + " AND tiles.y = region_tiles.y " + ") "); + + stmt.bind(1, regionID); + stmt.run(); + + result.completedResourceCount = stmt.get<int64_t>(0); + result.completedResourceSize = stmt.get<int64_t>(1); + + return result; +} + +void OfflineDatabase::removeUnusedResources() { + mapbox::sqlite::Statement& stmt1 = getStatement( + "DELETE FROM resources " + "WHERE ROWID NOT IN ( " + " SELECT resources.ROWID " + " FROM resources, region_resources " + " WHERE resources.url = region_resources.resource_url " + ") "); + stmt1.run(); + + mapbox::sqlite::Statement& stmt2 = getStatement( + "DELETE FROM tiles " + "WHERE ROWID NOT IN ( " + " SELECT tiles.ROWID " + " FROM tiles, region_tiles " + " AND tiles.tileset_id = region_tiles.tileset_id " + " AND tiles.z = region_tiles.z " + " AND tiles.x = region_tiles.x " + " AND tiles.y = region_tiles.y " + ") "); + stmt2.run(); +} + } // namespace mbgl diff --git a/platform/default/mbgl/storage/offline_database.hpp b/platform/default/mbgl/storage/offline_database.hpp index bc6f784d50..554bb16068 100644 --- a/platform/default/mbgl/storage/offline_database.hpp +++ b/platform/default/mbgl/storage/offline_database.hpp @@ -2,6 +2,7 @@ #define MBGL_OFFLINE_DATABASE #include <mbgl/storage/resource.hpp> +#include <mbgl/storage/offline.hpp> #include <mbgl/util/noncopyable.hpp> #include <mbgl/util/optional.hpp> @@ -27,7 +28,21 @@ public: ~OfflineDatabase(); optional<Response> get(const Resource&); - void put(const Resource&, const Response&); + uint64_t put(const Resource&, const Response&); + + std::vector<OfflineRegion> listRegions(); + + OfflineRegion createRegion(const OfflineRegionDefinition&, + const OfflineRegionMetadata&); + + void deleteRegion(OfflineRegion&&); + void removeUnusedResources(); + + optional<Response> getRegionResource(int64_t regionID, const Resource&); + uint64_t putRegionResource(int64_t regionID, const Resource&, const Response&); + + OfflineRegionDefinition getRegionDefinition(int64_t regionID); + OfflineRegionStatus getRegionCompletedStatus(int64_t regionID); private: void ensureSchema(); @@ -35,10 +50,12 @@ private: mapbox::sqlite::Statement& getStatement(const char *); optional<Response> getTile(const Resource::TileData&); - void putTile(const Resource::TileData&, const Response&); + uint64_t putTile(const Resource::TileData&, const Response&); optional<Response> getResource(const Resource&); - void putResource(const Resource&, const Response&); + uint64_t putResource(const Resource&, const Response&); + + void markUsed(int64_t regionID, const Resource&); const std::string path; std::unique_ptr<::mapbox::sqlite::Database> db; diff --git a/platform/default/mbgl/storage/offline_download.cpp b/platform/default/mbgl/storage/offline_download.cpp new file mode 100644 index 0000000000..1559895be2 --- /dev/null +++ b/platform/default/mbgl/storage/offline_download.cpp @@ -0,0 +1,241 @@ +#include <mbgl/storage/offline_download.hpp> +#include <mbgl/storage/offline_database.hpp> +#include <mbgl/storage/file_source.hpp> +#include <mbgl/storage/resource.hpp> +#include <mbgl/storage/response.hpp> +#include <mbgl/style/style_parser.hpp> +#include <mbgl/layer/symbol_layer.hpp> +#include <mbgl/text/glyph.hpp> +#include <mbgl/util/tile_cover.hpp> + +#include <set> + +namespace mbgl { + +OfflineDownload::OfflineDownload(int64_t id_, + OfflineRegionDefinition&& definition_, + OfflineDatabase& offlineDatabase_, + FileSource& onlineFileSource_) + : id(id_), + definition(definition_), + offlineDatabase(offlineDatabase_), + onlineFileSource(onlineFileSource_) { + setObserver(nullptr); +} + +OfflineDownload::~OfflineDownload() = default; + +void OfflineDownload::setObserver(std::unique_ptr<OfflineRegionObserver> observer_) { + observer = observer_ ? std::move(observer_) : std::make_unique<OfflineRegionObserver>(); +} + +void OfflineDownload::setState(OfflineRegionDownloadState state) { + if (status.downloadState == state) { + return; + } + + status.downloadState = state; + + if (status.downloadState == OfflineRegionDownloadState::Active) { + activateDownload(); + } else { + deactivateDownload(); + } +} + +std::vector<Resource> OfflineDownload::spriteResources(const StyleParser& parser) const { + std::vector<Resource> result; + + if (!parser.spriteURL.empty()) { + result.push_back(Resource::spriteImage(parser.spriteURL, definition.pixelRatio)); + result.push_back(Resource::spriteJSON(parser.spriteURL, definition.pixelRatio)); + } + + return result; +} + +std::vector<Resource> OfflineDownload::glyphResources(const StyleParser& parser) const { + std::vector<Resource> result; + + if (!parser.glyphURL.empty()) { + for (const auto& fontStack : parser.fontStacks()) { + for (uint32_t i = 0; i < 256; i++) { + result.push_back(Resource::glyphs(parser.glyphURL, fontStack, getGlyphRange(i * 256))); + } + } + } + + return result; +} + +std::vector<Resource> OfflineDownload::tileResources(SourceType type, uint16_t tileSize, const SourceInfo& info) const { + std::vector<Resource> result; + + for (const auto& tile : definition.tileCover(type, tileSize, info)) { + result.push_back(Resource::tile(info.tiles[0], definition.pixelRatio, tile.x, tile.y, tile.z)); + } + + return result; +} + +OfflineRegionStatus OfflineDownload::getStatus() const { + if (status.downloadState == OfflineRegionDownloadState::Active) { + return status; + } + + OfflineRegionStatus result = offlineDatabase.getRegionCompletedStatus(id); + + result.requiredResourceCount++; + optional<Response> styleResponse = offlineDatabase.get(Resource::style(definition.styleURL)); + if (!styleResponse) { + return result; + } + + StyleParser parser; + parser.parse(*styleResponse->data); + + result.requiredResourceCountIsIndeterminate = false; + + for (const auto& source : parser.sources) { + switch (source->type) { + case SourceType::Vector: + case SourceType::Raster: + if (source->getInfo()) { + result.requiredResourceCount += tileResources(source->type, source->tileSize, *source->getInfo()).size(); + } else { + result.requiredResourceCount += 1; + optional<Response> sourceResponse = offlineDatabase.get(Resource::source(source->url)); + if (sourceResponse) { + result.requiredResourceCount += tileResources(source->type, source->tileSize, + *StyleParser::parseTileJSON(*sourceResponse->data, source->url, source->type)).size(); + } else { + result.requiredResourceCountIsIndeterminate = true; + } + } + break; + + case SourceType::GeoJSON: + if (!source->url.empty()) { + result.requiredResourceCount += 1; + } + break; + + case SourceType::Video: + case SourceType::Annotations: + break; + } + } + + result.requiredResourceCount += spriteResources(parser).size(); + result.requiredResourceCount += glyphResources(parser).size(); + + return result; +} + +void OfflineDownload::activateDownload() { + status = offlineDatabase.getRegionCompletedStatus(id); + requiredSourceURLs.clear(); + + ensureResource(Resource::style(definition.styleURL), [&] (Response styleResponse) { + status.requiredResourceCountIsIndeterminate = false; + + StyleParser parser; + parser.parse(*styleResponse.data); + + for (const auto& source : parser.sources) { + SourceType type = source->type; + uint16_t tileSize = source->tileSize; + std::string url = source->url; + + switch (type) { + case SourceType::Vector: + case SourceType::Raster: + if (source->getInfo()) { + ensureTiles(type, tileSize, *source->getInfo()); + } else { + status.requiredResourceCountIsIndeterminate = true; + requiredSourceURLs.insert(url); + + ensureResource(Resource::source(url), [=] (Response sourceResponse) { + ensureTiles(type, tileSize, *StyleParser::parseTileJSON(*sourceResponse.data, url, type)); + + requiredSourceURLs.erase(url); + if (requiredSourceURLs.empty()) { + status.requiredResourceCountIsIndeterminate = false; + } + }); + } + break; + + case SourceType::GeoJSON: + if (!source->url.empty()) { + ensureResource(Resource::source(source->url)); + } + break; + + case SourceType::Video: + case SourceType::Annotations: + break; + } + } + + for (const auto& resource : spriteResources(parser)) { + ensureResource(resource); + } + + for (const auto& resource : glyphResources(parser)) { + ensureResource(resource); + } + }); + + // This will be the initial notification, after we've incremented requiredResourceCount + // to the reflect the extent to which required resources are already in the database. + observer->statusChanged(status); +} + +void OfflineDownload::deactivateDownload() { + requests.clear(); +} + +void OfflineDownload::ensureTiles(SourceType type, uint16_t tileSize, const SourceInfo& info) { + for (const auto& resource : tileResources(type, tileSize, info)) { + ensureResource(resource); + } +} + +void OfflineDownload::ensureResource(const Resource& resource, std::function<void (Response)> callback) { + status.requiredResourceCount++; + + optional<Response> offlineResponse = offlineDatabase.getRegionResource(id, resource); + if (offlineResponse) { + if (callback) { + callback(*offlineResponse); + } + + // Not incrementing status.completedResource{Size,Count} here because previously-existing + // resources are already accounted for by offlineDatabase.getRegionCompletedStatus(); + + return; + } + + auto it = requests.insert(requests.begin(), nullptr); + *it = onlineFileSource.request(resource, [=] (Response onlineResponse) { + requests.erase(it); + + if (onlineResponse.error) { + observer->responseError(*onlineResponse.error); + return; + } + + if (callback) { + callback(onlineResponse); + } + + status.completedResourceCount++; + status.completedResourceSize += offlineDatabase.putRegionResource(id, resource, onlineResponse); + + observer->statusChanged(status); + }); +} + +} // namespace mbgl diff --git a/platform/default/mbgl/storage/offline_download.hpp b/platform/default/mbgl/storage/offline_download.hpp new file mode 100644 index 0000000000..4200020487 --- /dev/null +++ b/platform/default/mbgl/storage/offline_download.hpp @@ -0,0 +1,62 @@ + #pragma once + +#include <mbgl/storage/offline.hpp> +#include <mbgl/style/types.hpp> + +#include <list> +#include <set> +#include <memory> + +namespace mbgl { + +class OfflineDatabase; +class FileSource; +class FileRequest; +class Resource; +class Response; +class SourceInfo; +class StyleParser; +class Source; + +/** + * Coordinates the request and storage of all resources for an offline region. + + * @private + */ +class OfflineDownload { +public: + OfflineDownload(int64_t id, OfflineRegionDefinition&&, OfflineDatabase& offline, FileSource& online); + ~OfflineDownload(); + + void setObserver(std::unique_ptr<OfflineRegionObserver>); + void setState(OfflineRegionDownloadState); + + OfflineRegionStatus getStatus() const; + +private: + void activateDownload(); + void deactivateDownload(); + + std::vector<Resource> spriteResources(const StyleParser&) const; + std::vector<Resource> glyphResources(const StyleParser&) const; + std::vector<Resource> tileResources(SourceType, uint16_t, const SourceInfo&) const; + + /* + * Ensure that the resource is stored in the database, requesting it if necessary. + * While the request is in progress, it is recorded in `requests`. If the download + * is deactivated, all in progress requests are cancelled. + */ + void ensureResource(const Resource&, std::function<void (Response)> = {}); + void ensureTiles(SourceType, uint16_t, const SourceInfo&); + + int64_t id; + OfflineRegionDefinition definition; + OfflineDatabase& offlineDatabase; + FileSource& onlineFileSource; + OfflineRegionStatus status; + std::unique_ptr<OfflineRegionObserver> observer; + std::list<std::unique_ptr<FileRequest>> requests; + std::set<std::string> requiredSourceURLs; +}; + +} // namespace mbgl diff --git a/platform/default/sqlite3.cpp b/platform/default/sqlite3.cpp index 09301bc4d9..2cc0e9f001 100644 --- a/platform/default/sqlite3.cpp +++ b/platform/default/sqlite3.cpp @@ -77,6 +77,11 @@ Statement Database::prepare(const char *query) { return Statement(db, query); } +int64_t Database::lastInsertRowid() const { + assert(db); + return sqlite3_last_insert_rowid(db); +} + Statement::Statement(sqlite3 *db, const char *sql) { const int err = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); if (err != SQLITE_OK) { @@ -170,12 +175,39 @@ template <> void Statement::bind(int offset, const char *value) { check(sqlite3_bind_text(stmt, offset, value, -1, SQLITE_STATIC)); } +// We currently cannot use sqlite3_bind_blob64 / sqlite3_bind_text64 because they +// was introduced in SQLite 3.8.7, and we need to support earlier versions: +// iOS 7.0: 3.7.13 +// iOS 8.2: 3.8.5 +// According to http://stackoverflow.com/questions/14288128/what-version-of-sqlite-does-ios-provide, +// the first iOS version with 3.8.7+ was 9.0, with 3.8.10.2. + +void Statement::bind(int offset, const char * value, std::size_t length, bool retain) { + assert(stmt); + if (length > std::numeric_limits<int>::max()) { + throw std::range_error("value too long for sqlite3_bind_text"); + } + check(sqlite3_bind_text(stmt, offset, value, int(length), + retain ? SQLITE_TRANSIENT : SQLITE_STATIC)); +} + void Statement::bind(int offset, const std::string& value, bool retain) { + bind(offset, value.data(), value.size(), retain); +} + +void Statement::bindBlob(int offset, const void * value, std::size_t length, bool retain) { assert(stmt); - check(sqlite3_bind_blob(stmt, offset, value.data(), int(value.size()), + if (length > std::numeric_limits<int>::max()) { + throw std::range_error("value too long for sqlite3_bind_text"); + } + check(sqlite3_bind_blob(stmt, offset, value, int(length), retain ? SQLITE_TRANSIENT : SQLITE_STATIC)); } +void Statement::bindBlob(int offset, const std::vector<uint8_t>& value, bool retain) { + bindBlob(offset, value.data(), value.size(), retain); +} + template <> void Statement::bind(int offset, std::chrono::system_clock::time_point value) { assert(stmt); check(sqlite3_bind_int64(stmt, offset, std::chrono::system_clock::to_time_t(value))); @@ -234,6 +266,13 @@ template <> std::string Statement::get(int offset) { }; } +template <> std::vector<uint8_t> Statement::get(int offset) { + assert(stmt); + const uint8_t* begin = reinterpret_cast<const uint8_t*>(sqlite3_column_blob(stmt, offset)); + const uint8_t* end = begin + sqlite3_column_bytes(stmt, offset); + return { begin, end }; +} + template <> std::chrono::system_clock::time_point Statement::get(int offset) { assert(stmt); return std::chrono::system_clock::from_time_t(sqlite3_column_int64(stmt, offset)); diff --git a/platform/default/sqlite3.hpp b/platform/default/sqlite3.hpp index 29e8967db3..cdfd5dc8de 100644 --- a/platform/default/sqlite3.hpp +++ b/platform/default/sqlite3.hpp @@ -1,6 +1,7 @@ #pragma once #include <string> +#include <vector> #include <stdexcept> typedef struct sqlite3 sqlite3; @@ -43,6 +44,8 @@ public: void exec(const std::string &sql); Statement prepare(const char *query); + int64_t lastInsertRowid() const; + private: sqlite3 *db = nullptr; }; @@ -63,7 +66,15 @@ public: operator bool() const; template <typename T> void bind(int offset, T value); - void bind(int offset, const std::string &value, bool retain = true); + + // Text + void bind(int offset, const char *, std::size_t length, bool retain = true); + void bind(int offset, const std::string&, bool retain = true); + + // Blob + void bindBlob(int offset, const void *, std::size_t length, bool retain = true); + void bindBlob(int offset, const std::vector<uint8_t>&, bool retain = true); + template <typename T> T get(int offset); bool run(); |