summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
authorJohn Firebaugh <john.firebaugh@gmail.com>2016-02-05 17:10:13 -0800
committerJohn Firebaugh <john.firebaugh@gmail.com>2016-02-10 15:40:20 -0800
commitc3c4c7b9a695ad1dbebe57242ba071103fe9a567 (patch)
treee205ecdc6a2f6318c6ba6308b5aa8baacc42f481 /platform
parente9302c797f68c7e48b908b87b126045c8c5e5209 (diff)
downloadqtlocation-mapboxgl-c3c4c7b9a695ad1dbebe57242ba071103fe9a567.tar.gz
[core] Interface and implementation for offline
Diffstat (limited to 'platform')
-rw-r--r--platform/default/default_file_source.cpp98
-rw-r--r--platform/default/mbgl/storage/offline.cpp122
-rw-r--r--platform/default/mbgl/storage/offline_database.cpp171
-rw-r--r--platform/default/mbgl/storage/offline_database.hpp23
-rw-r--r--platform/default/mbgl/storage/offline_download.cpp241
-rw-r--r--platform/default/mbgl/storage/offline_download.hpp62
-rw-r--r--platform/default/sqlite3.cpp41
-rw-r--r--platform/default/sqlite3.hpp13
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();