summaryrefslogtreecommitdiff
path: root/platform/default/mbgl
diff options
context:
space:
mode:
Diffstat (limited to 'platform/default/mbgl')
-rw-r--r--platform/default/mbgl/storage/offline.cpp122
-rw-r--r--platform/default/mbgl/storage/offline_database.cpp523
-rw-r--r--platform/default/mbgl/storage/offline_database.hpp94
-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/mbgl/storage/offline_schema.cpp.include53
-rw-r--r--platform/default/mbgl/storage/offline_schema.js24
-rw-r--r--platform/default/mbgl/storage/offline_schema.sql62
8 files changed, 1181 insertions, 0 deletions
diff --git a/platform/default/mbgl/storage/offline.cpp b/platform/default/mbgl/storage/offline.cpp
new file mode 100644
index 0000000000..931e079771
--- /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/source/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
new file mode 100644
index 0000000000..78f0272fef
--- /dev/null
+++ b/platform/default/mbgl/storage/offline_database.cpp
@@ -0,0 +1,523 @@
+#include <mbgl/storage/offline_database.hpp>
+#include <mbgl/storage/response.hpp>
+#include <mbgl/util/compression.hpp>
+#include <mbgl/util/io.hpp>
+#include <mbgl/util/string.hpp>
+#include <mbgl/util/chrono.hpp>
+#include <mbgl/map/tile_id.hpp>
+#include <mbgl/platform/log.hpp>
+
+#include "sqlite3.hpp"
+#include <sqlite3.h>
+
+namespace mbgl {
+
+using namespace mapbox::sqlite;
+
+// If you change the schema you must write a migration from the previous version.
+static const uint32_t schemaVersion = 2;
+
+OfflineDatabase::Statement::~Statement() {
+ stmt.reset();
+ stmt.clearBindings();
+}
+
+OfflineDatabase::OfflineDatabase(const std::string& path_, uint64_t maximumCacheSize_)
+ : path(path_),
+ maximumCacheSize(maximumCacheSize_) {
+ ensureSchema();
+}
+
+OfflineDatabase::~OfflineDatabase() {
+ // Deleting these SQLite objects may result in exceptions, but we're in a destructor, so we
+ // can't throw anything.
+ try {
+ statements.clear();
+ db.reset();
+ } catch (mapbox::sqlite::Exception& ex) {
+ Log::Error(Event::Database, ex.code, ex.what());
+ }
+}
+
+void OfflineDatabase::ensureSchema() {
+ if (path != ":memory:") {
+ try {
+ db = std::make_unique<Database>(path.c_str(), ReadWrite);
+ db->setBusyTimeout(Milliseconds::max());
+
+ {
+ auto userVersionStmt = db->prepare("PRAGMA user_version");
+ userVersionStmt.run();
+ switch (userVersionStmt.get<int>(0)) {
+ case 0: break; // cache-only database; ok to delete
+ case 1: break; // cache-only database; ok to delete
+ case 2: return;
+ default: throw std::runtime_error("unknown schema version");
+ }
+ }
+
+ removeExisting();
+ db = std::make_unique<Database>(path.c_str(), ReadWrite | Create);
+ db->setBusyTimeout(Milliseconds::max());
+ } catch (mapbox::sqlite::Exception& ex) {
+ if (ex.code == SQLITE_CANTOPEN) {
+ db = std::make_unique<Database>(path.c_str(), ReadWrite | Create);
+ db->setBusyTimeout(Milliseconds::max());
+ } else if (ex.code == SQLITE_NOTADB) {
+ removeExisting();
+ db = std::make_unique<Database>(path.c_str(), ReadWrite | Create);
+ db->setBusyTimeout(Milliseconds::max());
+ }
+ }
+ }
+
+ #include "offline_schema.cpp.include"
+
+ db = std::make_unique<Database>(path.c_str(), ReadWrite | Create);
+ db->setBusyTimeout(Milliseconds::max());
+ db->exec(schema);
+ db->exec("PRAGMA user_version = " + util::toString(schemaVersion));
+}
+
+void OfflineDatabase::removeExisting() {
+ Log::Warning(Event::Database, "Removing existing incompatible offline database");
+
+ db.reset();
+
+ try {
+ util::deleteFile(path);
+ } catch (util::IOException& ex) {
+ Log::Error(Event::Database, ex.code, ex.what());
+ }
+}
+
+OfflineDatabase::Statement OfflineDatabase::getStatement(const char * sql) {
+ auto it = statements.find(sql);
+
+ if (it != statements.end()) {
+ return Statement(*it->second);
+ }
+
+ return Statement(*statements.emplace(sql, std::make_unique<mapbox::sqlite::Statement>(db->prepare(sql))).first->second);
+}
+
+optional<Response> OfflineDatabase::get(const Resource& resource) {
+ if (resource.kind == Resource::Kind::Tile) {
+ assert(resource.tileData);
+ return getTile(*resource.tileData);
+ } else {
+ return getResource(resource);
+ }
+}
+
+uint64_t OfflineDatabase::put(const Resource& resource, const Response& response) {
+ return putInternal(resource, response, true);
+}
+
+uint64_t OfflineDatabase::putInternal(const Resource& resource, const Response& response, bool evict_) {
+ if (response.error) {
+ return 0;
+ }
+
+ std::string compressedData;
+ bool compressed = false;
+ uint64_t size = 0;
+
+ if (response.data) {
+ compressedData = util::compress(*response.data);
+ compressed = compressedData.size() < response.data->size();
+ size = compressed ? compressedData.size() : response.data->size();
+ }
+
+ if (evict_ && !evict(size)) {
+ Log::Warning(Event::Database, "Unable to make space for entry");
+ return 0;
+ }
+
+ if (resource.kind == Resource::Kind::Tile) {
+ assert(resource.tileData);
+ putTile(*resource.tileData, response,
+ compressed ? compressedData : *response.data,
+ compressed);
+ } else {
+ putResource(resource, response,
+ compressed ? compressedData : *response.data,
+ compressed);
+ }
+
+ return size;
+}
+
+optional<Response> OfflineDatabase::getResource(const Resource& resource) {
+ Statement accessedStmt = getStatement(
+ "UPDATE resources SET accessed = ?1 WHERE url = ?2");
+
+ accessedStmt->bind(1, SystemClock::now());
+ accessedStmt->bind(2, resource.url);
+ accessedStmt->run();
+
+ Statement stmt = getStatement(
+ // 0 1 2 3 4
+ "SELECT etag, expires, modified, data, compressed "
+ "FROM resources "
+ "WHERE url = ?");
+
+ stmt->bind(1, resource.url);
+
+ if (!stmt->run()) {
+ return {};
+ }
+
+ Response response;
+
+ response.etag = stmt->get<optional<std::string>>(0);
+ response.expires = stmt->get<optional<SystemTimePoint>>(1);
+ response.modified = stmt->get<optional<SystemTimePoint>>(2);
+
+ optional<std::string> data = stmt->get<optional<std::string>>(3);
+ if (!data) {
+ response.noContent = true;
+ } else if (stmt->get<int>(4)) {
+ response.data = std::make_shared<std::string>(util::decompress(*data));
+ } else {
+ response.data = std::make_shared<std::string>(*data);
+ }
+
+ return response;
+}
+
+void OfflineDatabase::putResource(const Resource& resource,
+ const Response& response,
+ const std::string& data,
+ bool compressed) {
+ if (response.notModified) {
+ Statement stmt = getStatement(
+ "UPDATE resources "
+ "SET accessed = ?1, "
+ " expires = ?2 "
+ "WHERE url = ?3 ");
+
+ stmt->bind(1, SystemClock::now());
+ stmt->bind(2, response.expires);
+ stmt->bind(3, resource.url);
+ stmt->run();
+ } else {
+ Statement stmt = getStatement(
+ "REPLACE INTO resources (url, kind, etag, expires, modified, accessed, data, compressed) "
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ");
+
+ stmt->bind(1, resource.url);
+ stmt->bind(2, int(resource.kind));
+ stmt->bind(3, response.etag);
+ stmt->bind(4, response.expires);
+ stmt->bind(5, response.modified);
+ stmt->bind(6, SystemClock::now());
+
+ if (response.noContent) {
+ stmt->bind(7, nullptr);
+ stmt->bind(8, false);
+ } else {
+ stmt->bindBlob(7, data.data(), data.size(), false);
+ stmt->bind(8, compressed);
+ }
+
+ stmt->run();
+ }
+}
+
+optional<Response> OfflineDatabase::getTile(const Resource::TileData& tile) {
+ Statement accessedStmt = getStatement(
+ "UPDATE tiles "
+ "SET accessed = ?1 "
+ "WHERE url_template = ?2 "
+ " AND pixel_ratio = ?3 "
+ " AND x = ?4 "
+ " AND y = ?5 "
+ " AND z = ?6 ");
+
+ accessedStmt->bind(1, SystemClock::now());
+ accessedStmt->bind(2, tile.urlTemplate);
+ accessedStmt->bind(3, tile.pixelRatio);
+ accessedStmt->bind(4, tile.x);
+ accessedStmt->bind(5, tile.y);
+ accessedStmt->bind(6, tile.z);
+ accessedStmt->run();
+
+ Statement stmt = getStatement(
+ // 0 1 2 3 4
+ "SELECT etag, expires, modified, data, compressed "
+ "FROM tiles "
+ "WHERE url_template = ?1 "
+ " AND pixel_ratio = ?2 "
+ " AND x = ?3 "
+ " AND y = ?4 "
+ " AND z = ?5 ");
+
+ stmt->bind(1, tile.urlTemplate);
+ stmt->bind(2, tile.pixelRatio);
+ stmt->bind(3, tile.x);
+ stmt->bind(4, tile.y);
+ stmt->bind(5, tile.z);
+
+ if (!stmt->run()) {
+ return {};
+ }
+
+ Response response;
+
+ response.etag = stmt->get<optional<std::string>>(0);
+ response.expires = stmt->get<optional<SystemTimePoint>>(1);
+ response.modified = stmt->get<optional<SystemTimePoint>>(2);
+
+ optional<std::string> data = stmt->get<optional<std::string>>(3);
+ if (!data) {
+ response.noContent = true;
+ } else if (stmt->get<int>(4)) {
+ response.data = std::make_shared<std::string>(util::decompress(*data));
+ } else {
+ response.data = std::make_shared<std::string>(*data);
+ }
+
+ return response;
+}
+
+void OfflineDatabase::putTile(const Resource::TileData& tile,
+ const Response& response,
+ const std::string& data,
+ bool compressed) {
+ if (response.notModified) {
+ Statement stmt = getStatement(
+ "UPDATE tiles "
+ "SET accessed = ?1, "
+ " expires = ?2 "
+ "WHERE url_template = ?3 "
+ " AND pixel_ratio = ?4 "
+ " AND x = ?5 "
+ " AND y = ?6 "
+ " AND z = ?7 ");
+
+ stmt->bind(1, SystemClock::now());
+ stmt->bind(2, response.expires);
+ stmt->bind(3, tile.urlTemplate);
+ stmt->bind(4, tile.pixelRatio);
+ stmt->bind(5, tile.x);
+ stmt->bind(6, tile.y);
+ stmt->bind(7, tile.z);
+ stmt->run();
+ } else {
+ Statement stmt2 = getStatement(
+ "REPLACE INTO tiles (url_template, pixel_ratio, x, y, z, modified, etag, expires, accessed, data, compressed) "
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ");
+
+ stmt2->bind(1, tile.urlTemplate);
+ stmt2->bind(2, tile.pixelRatio);
+ stmt2->bind(3, tile.x);
+ stmt2->bind(4, tile.y);
+ stmt2->bind(5, tile.z);
+ stmt2->bind(6, response.modified);
+ stmt2->bind(7, response.etag);
+ stmt2->bind(8, response.expires);
+ stmt2->bind(9, SystemClock::now());
+
+ if (response.noContent) {
+ stmt2->bind(10, nullptr);
+ stmt2->bind(11, false);
+ } else {
+ stmt2->bindBlob(10, data.data(), data.size(), false);
+ stmt2->bind(11, compressed);
+ }
+
+ stmt2->run();
+ }
+}
+
+std::vector<OfflineRegion> OfflineDatabase::listRegions() {
+ 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 result;
+}
+
+OfflineRegion OfflineDatabase::createRegion(const OfflineRegionDefinition& definition,
+ const OfflineRegionMetadata& metadata) {
+ 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) {
+ Statement stmt = getStatement(
+ "DELETE FROM regions WHERE id = ?");
+
+ stmt->bind(1, region.getID());
+ stmt->run();
+
+ evict(0);
+}
+
+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 = putInternal(resource, response, false);
+ markUsed(regionID, resource);
+ return result;
+}
+
+void OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
+ if (resource.kind == Resource::Kind::Tile) {
+ Statement stmt = getStatement(
+ "REPLACE INTO region_tiles (region_id, tile_id) "
+ "SELECT ?1, tiles.id "
+ "FROM tiles "
+ "WHERE url_template = ?2 "
+ " AND pixel_ratio = ?3 "
+ " AND x = ?4 "
+ " AND y = ?5 "
+ " 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();
+ } else {
+ Statement stmt = getStatement(
+ "REPLACE 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();
+ }
+}
+
+OfflineRegionDefinition OfflineDatabase::getRegionDefinition(int64_t regionID) {
+ 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;
+
+ Statement stmt = getStatement(
+ "SELECT COUNT(*), SUM(size) FROM ( "
+ " SELECT LENGTH(data) as size "
+ " FROM region_resources, resources "
+ " WHERE region_id = ?1 "
+ " AND resource_id = resources.id "
+ " UNION ALL "
+ " SELECT LENGTH(data) as size "
+ " FROM region_tiles, tiles "
+ " WHERE region_id = ?1 "
+ " AND tile_id = tiles.id "
+ ") ");
+
+ stmt->bind(1, regionID);
+ stmt->run();
+
+ result.completedResourceCount = stmt->get<int64_t>(0);
+ result.completedResourceSize = stmt->get<int64_t>(1);
+
+ return result;
+}
+
+template <class T>
+T OfflineDatabase::getPragma(const char * sql) {
+ Statement stmt = getStatement(sql);
+ stmt->run();
+ return stmt->get<T>(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
+// less than the maximum cache size. Returns false if this condition cannot be
+// satisfied.
+//
+// SQLite database never shrinks in size unless we call VACCUM. 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 VACCUM or keeping a running total, which can be costly.
+bool OfflineDatabase::evict(uint64_t neededFreeSize) {
+ uint64_t pageSize = getPragma<int64_t>("PRAGMA page_size");
+ uint64_t pageCount = getPragma<int64_t>("PRAGMA page_count");
+
+ if (pageSize * pageCount > maximumCacheSize) {
+ Log::Warning(mbgl::Event::Database, "Current size is larger than the maximum size. Database won't get truncated.");
+ }
+
+ auto usedSize = [&] {
+ return pageSize * (pageCount - getPragma<int64_t>("PRAGMA freelist_count"));
+ };
+
+ // The addition of pageSize is a fudge factor to account for non `data` column
+ // size, and because pages can get fragmented on the database.
+ while (usedSize() + neededFreeSize + pageSize > maximumCacheSize) {
+ Statement stmt1 = getStatement(
+ "DELETE FROM resources "
+ "WHERE id IN ( "
+ " SELECT id FROM resources "
+ " LEFT JOIN region_resources "
+ " ON resource_id = resources.id "
+ " WHERE resource_id IS NULL "
+ " ORDER BY accessed ASC LIMIT ?1 "
+ ") ");
+ stmt1->bind(1, 50);
+ stmt1->run();
+ uint64_t changes1 = db->changes();
+
+ Statement stmt2 = getStatement(
+ "DELETE FROM tiles "
+ "WHERE id IN ( "
+ " SELECT id FROM tiles "
+ " LEFT JOIN region_tiles "
+ " ON tile_id = tiles.id "
+ " WHERE tile_id IS NULL "
+ " ORDER BY accessed ASC LIMIT ?1 "
+ ") ");
+ stmt2->bind(1, 50);
+ stmt2->run();
+ uint64_t changes2 = db->changes();
+
+ if (changes1 == 0 && changes2 == 0) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace mbgl
diff --git a/platform/default/mbgl/storage/offline_database.hpp b/platform/default/mbgl/storage/offline_database.hpp
new file mode 100644
index 0000000000..854ebdb47d
--- /dev/null
+++ b/platform/default/mbgl/storage/offline_database.hpp
@@ -0,0 +1,94 @@
+#ifndef MBGL_OFFLINE_DATABASE
+#define MBGL_OFFLINE_DATABASE
+
+#include <mbgl/storage/resource.hpp>
+#include <mbgl/storage/offline.hpp>
+#include <mbgl/util/noncopyable.hpp>
+#include <mbgl/util/optional.hpp>
+#include <mbgl/util/constants.hpp>
+
+#include <unordered_map>
+#include <memory>
+#include <string>
+
+namespace mapbox {
+namespace sqlite {
+class Database;
+class Statement;
+}
+}
+
+namespace mbgl {
+
+class Response;
+class TileID;
+
+class OfflineDatabase : private util::noncopyable {
+public:
+ // Limits affect ambient caching (put) only; resources required by offline
+ // regions are exempt.
+ OfflineDatabase(const std::string& path,
+ uint64_t maximumCacheSize = util::DEFAULT_MAX_CACHE_SIZE);
+ ~OfflineDatabase();
+
+ optional<Response> get(const Resource&);
+ uint64_t put(const Resource&, const Response&);
+
+ std::vector<OfflineRegion> listRegions();
+
+ OfflineRegion createRegion(const OfflineRegionDefinition&,
+ const OfflineRegionMetadata&);
+
+ void deleteRegion(OfflineRegion&&);
+
+ 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();
+ void removeExisting();
+
+ class Statement {
+ public:
+ explicit Statement(mapbox::sqlite::Statement& stmt_) : stmt(stmt_) {}
+ Statement(Statement&&) = default;
+ Statement(const Statement&) = delete;
+ ~Statement();
+
+ mapbox::sqlite::Statement* operator->() { return &stmt; };
+
+ private:
+ mapbox::sqlite::Statement& stmt;
+ };
+
+ Statement getStatement(const char *);
+
+ optional<Response> getTile(const Resource::TileData&);
+ void putTile(const Resource::TileData&, const Response&,
+ const std::string&, bool compressed);
+
+ optional<Response> getResource(const Resource&);
+ void putResource(const Resource&, const Response&,
+ const std::string&, bool compressed);
+
+ uint64_t putInternal(const Resource&, const Response&, bool evict);
+ void markUsed(int64_t regionID, const Resource&);
+
+ const std::string path;
+ std::unique_ptr<::mapbox::sqlite::Database> db;
+ std::unordered_map<const char *, std::unique_ptr<::mapbox::sqlite::Statement>> statements;
+
+ template <class T>
+ T getPragma(const char *);
+
+ uint64_t maximumCacheSize;
+
+ bool evict(uint64_t neededFreeSize);
+};
+
+} // namespace mbgl
+
+#endif
diff --git a/platform/default/mbgl/storage/offline_download.cpp b/platform/default/mbgl/storage/offline_download.cpp
new file mode 100644
index 0000000000..27bf3b8c5b
--- /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, source->tileSize)).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, tileSize));
+
+ 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) {
+ if (onlineResponse.error) {
+ observer->responseError(*onlineResponse.error);
+ return;
+ }
+
+ requests.erase(it);
+
+ 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/mbgl/storage/offline_schema.cpp.include b/platform/default/mbgl/storage/offline_schema.cpp.include
new file mode 100644
index 0000000000..3068dadfe0
--- /dev/null
+++ b/platform/default/mbgl/storage/offline_schema.cpp.include
@@ -0,0 +1,53 @@
+/* THIS IS A GENERATED FILE; EDIT offline_schema.sql INSTEAD */
+static const char * schema =
+"CREATE TABLE resources (\n"
+" id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n"
+" url TEXT NOT NULL,\n"
+" kind INTEGER NOT NULL,\n"
+" expires INTEGER,\n"
+" modified INTEGER,\n"
+" etag TEXT,\n"
+" data BLOB,\n"
+" compressed INTEGER NOT NULL DEFAULT 0,\n"
+" accessed INTEGER NOT NULL,\n"
+" UNIQUE (url)\n"
+");\n"
+"CREATE TABLE tiles (\n"
+" id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n"
+" url_template TEXT NOT NULL,\n"
+" pixel_ratio INTEGER NOT NULL,\n"
+" z INTEGER NOT NULL,\n"
+" x INTEGER NOT NULL,\n"
+" y INTEGER NOT NULL,\n"
+" expires INTEGER,\n"
+" modified INTEGER,\n"
+" etag TEXT,\n"
+" data BLOB,\n"
+" compressed INTEGER NOT NULL DEFAULT 0,\n"
+" accessed INTEGER NOT NULL,\n"
+" UNIQUE (url_template, pixel_ratio, z, x, y)\n"
+");\n"
+"CREATE TABLE regions (\n"
+" id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n"
+" definition TEXT NOT NULL,\n"
+" description BLOB\n"
+");\n"
+"CREATE TABLE region_resources (\n"
+" region_id INTEGER NOT NULL REFERENCES regions(id),\n"
+" resource_id INTEGER NOT NULL REFERENCES resources(id),\n"
+" UNIQUE (region_id, resource_id)\n"
+");\n"
+"CREATE TABLE region_tiles (\n"
+" region_id INTEGER NOT NULL REFERENCES regions(id),\n"
+" tile_id INTEGER NOT NULL REFERENCES tiles(id),\n"
+" UNIQUE (region_id, tile_id)\n"
+");\n"
+"CREATE INDEX resources_accessed\n"
+"ON resources (accessed);\n"
+"CREATE INDEX tiles_accessed\n"
+"ON tiles (accessed);\n"
+"CREATE INDEX region_resources_resource_id\n"
+"ON region_resources (resource_id);\n"
+"CREATE INDEX region_tiles_tile_id\n"
+"ON region_tiles (tile_id);\n"
+;
diff --git a/platform/default/mbgl/storage/offline_schema.js b/platform/default/mbgl/storage/offline_schema.js
new file mode 100644
index 0000000000..153ba34e38
--- /dev/null
+++ b/platform/default/mbgl/storage/offline_schema.js
@@ -0,0 +1,24 @@
+// To regenerate:
+// (cd platform/default/mbgl/storage && node offline_schema.js)
+
+var fs = require('fs');
+var readline = require('readline');
+
+var lineReader = readline.createInterface({
+ input: fs.createReadStream('offline_schema.sql')
+});
+
+var lines = [
+ "/* THIS IS A GENERATED FILE; EDIT offline_schema.sql INSTEAD */",
+ "static const char * schema = ",
+];
+
+lineReader
+ .on('line', function (line) {
+ line = line.replace(/ *--.*/, '');
+ if (line) lines.push('"' + line + '\\n"');
+ })
+ .on('close', function () {
+ lines.push(';\n');
+ fs.writeFileSync('offline_schema.cpp.include', lines.join('\n'));
+ });
diff --git a/platform/default/mbgl/storage/offline_schema.sql b/platform/default/mbgl/storage/offline_schema.sql
new file mode 100644
index 0000000000..cba922f3f7
--- /dev/null
+++ b/platform/default/mbgl/storage/offline_schema.sql
@@ -0,0 +1,62 @@
+CREATE TABLE resources ( -- Generic table for style, source, sprite, and glyph resources.
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ url TEXT NOT NULL,
+ kind INTEGER NOT NULL,
+ expires INTEGER,
+ modified INTEGER,
+ etag TEXT,
+ data BLOB,
+ compressed INTEGER NOT NULL DEFAULT 0,
+ accessed INTEGER NOT NULL,
+ UNIQUE (url)
+);
+
+CREATE TABLE tiles (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ url_template TEXT NOT NULL,
+ pixel_ratio INTEGER NOT NULL,
+ z INTEGER NOT NULL,
+ x INTEGER NOT NULL,
+ y INTEGER NOT NULL,
+ expires INTEGER,
+ modified INTEGER,
+ etag TEXT,
+ data BLOB,
+ compressed INTEGER NOT NULL DEFAULT 0,
+ accessed INTEGER NOT NULL,
+ UNIQUE (url_template, pixel_ratio, z, x, y)
+);
+
+CREATE TABLE regions (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ definition TEXT NOT NULL, -- JSON formatted definition of region. Regions may be of variant types:
+ -- e.g. bbox and zoom range, route path, flyTo parameters, etc. Note that
+ -- the set of tiles required for a region may span multiple sources.
+ description BLOB -- User provided data in user-defined format
+);
+
+CREATE TABLE region_resources (
+ region_id INTEGER NOT NULL REFERENCES regions(id),
+ resource_id INTEGER NOT NULL REFERENCES resources(id),
+ UNIQUE (region_id, resource_id)
+);
+
+CREATE TABLE region_tiles (
+ region_id INTEGER NOT NULL REFERENCES regions(id),
+ tile_id INTEGER NOT NULL REFERENCES tiles(id),
+ UNIQUE (region_id, tile_id)
+);
+
+-- Indexes for efficient eviction queries
+
+CREATE INDEX resources_accessed
+ON resources (accessed);
+
+CREATE INDEX tiles_accessed
+ON tiles (accessed);
+
+CREATE INDEX region_resources_resource_id
+ON region_resources (resource_id);
+
+CREATE INDEX region_tiles_tile_id
+ON region_tiles (tile_id);