diff --git a/bin/render.cpp b/bin/render.cpp index 33e216f52..740d3dffe 100644 --- a/bin/render.cpp +++ b/bin/render.cpp @@ -111,7 +111,16 @@ int main(int argc, char *argv[]) { util::write_file(output, png); }); - map.renderStill([async](std::unique_ptr image) { + map.renderStill([async](std::exception_ptr error, std::unique_ptr image) { + try { + if (error) { + std::rethrow_exception(error); + } + } catch(std::exception& e) { + std::cout << "Error: " << e.what() << std::endl; + exit(1); + } + async->data = const_cast(image.release()); uv_async_send(async); }); diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index e71c5f5b4..a6aac08b3 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -42,7 +42,7 @@ public: // Register a callback that will get called (on the render thread) when all resources have // been loaded and a complete render occurs. - using StillImageCallback = std::function)>; + using StillImageCallback = std::function)>; void renderStill(StillImageCallback callback); // Triggers a synchronous or asynchronous render. diff --git a/include/mbgl/platform/event.hpp b/include/mbgl/platform/event.hpp index 5fd64119c..8cdd1d50a 100644 --- a/include/mbgl/platform/event.hpp +++ b/include/mbgl/platform/event.hpp @@ -29,6 +29,7 @@ enum class Event : uint8_t { ParseStyle, ParseTile, Render, + ResourceLoader, Database, HttpRequest, Sprite, @@ -46,6 +47,7 @@ MBGL_DEFINE_ENUM_CLASS(EventClass, Event, { { Event::ParseStyle, "ParseStyle" }, { Event::ParseTile, "ParseTile" }, { Event::Render, "Render" }, + { Event::ResourceLoader, "ResourceLoader" }, { Event::Database, "Database" }, { Event::HttpRequest, "HttpRequest" }, { Event::Sprite, "Sprite" }, diff --git a/include/mbgl/util/exception.hpp b/include/mbgl/util/exception.hpp index 0b4403270..da61aa482 100644 --- a/include/mbgl/util/exception.hpp +++ b/include/mbgl/util/exception.hpp @@ -11,6 +11,11 @@ struct Exception : std::runtime_error { inline Exception(const std::string &msg) : std::runtime_error(msg) {} }; +struct GlyphRangeLoadingException : Exception { + inline GlyphRangeLoadingException(const char *msg) : Exception(msg) {} + inline GlyphRangeLoadingException(const std::string &msg) : Exception(msg) {} +}; + struct MisuseException : Exception { inline MisuseException(const char *msg) : Exception(msg) {} inline MisuseException(const std::string &msg) : Exception(msg) {} @@ -21,6 +26,21 @@ struct ShaderException : Exception { inline ShaderException(const std::string &msg) : Exception(msg) {} }; +struct SourceLoadingException : Exception { + inline SourceLoadingException(const char *msg) : Exception(msg) {} + inline SourceLoadingException(const std::string &msg) : Exception(msg) {} +}; + +struct SpriteLoadingException : Exception { + inline SpriteLoadingException(const char *msg) : Exception(msg) {} + inline SpriteLoadingException(const std::string &msg) : Exception(msg) {} +}; + +struct TileLoadingException : Exception { + inline TileLoadingException(const char *msg) : Exception(msg) {} + inline TileLoadingException(const std::string &msg) : Exception(msg) {} +}; + } } diff --git a/src/mbgl/map/map_context.cpp b/src/mbgl/map/map_context.cpp index f373274eb..28bd8903e 100644 --- a/src/mbgl/map/map_context.cpp +++ b/src/mbgl/map/map_context.cpp @@ -232,7 +232,7 @@ void MapContext::render() { painter->render(*style, transformState, data.getAnimationTime()); if (data.mode == MapMode::Still) { - callback(view.readStillImage()); + callback(nullptr, view.readStillImage()); callback = nullptr; } @@ -274,4 +274,13 @@ void MapContext::onTileDataChanged() { triggerUpdate(); } +void MapContext::onResourceLoadingFailed(std::exception_ptr error) { + assert(Environment::currentlyOn(ThreadType::Map)); + + if (data.mode == MapMode::Still && callback) { + callback(error, nullptr); + callback = nullptr; + } +} + } diff --git a/src/mbgl/map/map_context.hpp b/src/mbgl/map/map_context.hpp index c84245484..fb9fdb4d4 100644 --- a/src/mbgl/map/map_context.hpp +++ b/src/mbgl/map/map_context.hpp @@ -43,7 +43,7 @@ public: void resize(uint16_t width, uint16_t height, float ratio); - using StillImageCallback = std::function)>; + using StillImageCallback = std::function)>; void renderStill(StillImageCallback callback); void triggerUpdate(Update = Update::Nothing); @@ -61,6 +61,7 @@ public: // ResourceLoader::Observer implementation. void onTileDataChanged() override; + void onResourceLoadingFailed(std::exception_ptr error) override; private: void updateTiles(); diff --git a/src/mbgl/map/resource_loader.cpp b/src/mbgl/map/resource_loader.cpp index 2be86dc5e..5aa714595 100644 --- a/src/mbgl/map/resource_loader.cpp +++ b/src/mbgl/map/resource_loader.cpp @@ -104,10 +104,18 @@ void ResourceLoader::onGlyphRangeLoaded() { emitTileDataChanged(); } +void ResourceLoader::onGlyphRangeLoadingFailed(std::exception_ptr error) { + emitResourceLoadingFailed(error); +} + void ResourceLoader::onSourceLoaded() { emitTileDataChanged(); } +void ResourceLoader::onSourceLoadingFailed(std::exception_ptr error) { + emitResourceLoadingFailed(error); +} + void ResourceLoader::onTileLoaded(bool isNewTile) { if (isNewTile) { shouldReparsePartialTiles_ = true; @@ -116,12 +124,20 @@ void ResourceLoader::onTileLoaded(bool isNewTile) { emitTileDataChanged(); } +void ResourceLoader::onTileLoadingFailed(std::exception_ptr error) { + emitResourceLoadingFailed(error); +} + void ResourceLoader::onSpriteLoaded() { shouldReparsePartialTiles_ = true; emitTileDataChanged(); } +void ResourceLoader::onSpriteLoadingFailed(std::exception_ptr error) { + emitResourceLoadingFailed(error); +} + void ResourceLoader::emitTileDataChanged() { assert(Environment::currentlyOn(ThreadType::Map)); @@ -130,4 +146,20 @@ void ResourceLoader::emitTileDataChanged() { } } +void ResourceLoader::emitResourceLoadingFailed(std::exception_ptr error) { + assert(Environment::currentlyOn(ThreadType::Map)); + + try { + if (error) { + std::rethrow_exception(error); + } + } catch(const std::exception& e) { + Log::Error(Event::ResourceLoader, e.what()); + } + + if (observer_) { + observer_->onResourceLoadingFailed(error); + } +} + } diff --git a/src/mbgl/map/resource_loader.hpp b/src/mbgl/map/resource_loader.hpp index 379444135..33949aa48 100644 --- a/src/mbgl/map/resource_loader.hpp +++ b/src/mbgl/map/resource_loader.hpp @@ -32,6 +32,7 @@ public: virtual ~Observer() = default; virtual void onTileDataChanged() = 0; + virtual void onResourceLoadingFailed(std::exception_ptr error) = 0; }; ResourceLoader(); @@ -62,16 +63,21 @@ public: // GlyphStore::Observer implementation. void onGlyphRangeLoaded() override; + void onGlyphRangeLoadingFailed(std::exception_ptr error) override; // Source::Observer implementation. void onSourceLoaded() override; + void onSourceLoadingFailed(std::exception_ptr error) override; void onTileLoaded(bool isNewTile) override; + void onTileLoadingFailed(std::exception_ptr error) override; // Sprite::Observer implementation. void onSpriteLoaded() override; + void onSpriteLoadingFailed(std::exception_ptr error) override; private: void emitTileDataChanged(); + void emitResourceLoadingFailed(std::exception_ptr error); bool shouldReparsePartialTiles_ = false; diff --git a/src/mbgl/map/source.cpp b/src/mbgl/map/source.cpp index e8dc3468d..b0c4e22f5 100644 --- a/src/mbgl/map/source.cpp +++ b/src/mbgl/map/source.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -150,7 +151,9 @@ void Source::load(const std::string& accessToken) { req = nullptr; if (res.status != Response::Successful) { - Log::Warning(Event::General, "Failed to load source TileJSON: %s", res.message.c_str()); + std::stringstream message; + message << "Failed to load [" << info.url << "]: " << res.message; + emitSourceLoadingFailed(message.str()); return; } @@ -158,7 +161,9 @@ void Source::load(const std::string& accessToken) { d.Parse<0>(res.data.c_str()); if (d.HasParseError()) { - Log::Error(Event::General, "Invalid source TileJSON; Parse Error at %d: %s", d.GetErrorOffset(), d.GetParseError()); + std::stringstream message; + message << "Failed to parse [" << info.url << "]: " << d.GetErrorOffset() << " - " << d.GetParseError(); + emitSourceLoadingFailed(message.str()); return; } @@ -282,22 +287,26 @@ TileData::State Source::addTile(MapData& data, new_tile.data = cache.get(normalized_id.to_uint64()); } - auto callback = std::bind(&Source::emitTileLoaded, this, true); + auto successCallback = std::bind(&Source::emitTileLoaded, this, true); + auto failureCallback = std::bind(&Source::emitTileLoadingFailed, this, std::placeholders::_1); + if (!new_tile.data) { // If we don't find working tile data, we're just going to load it. if (info.type == SourceType::Vector) { new_tile.data = std::make_shared(normalized_id, data.transform.getMaxZoom(), style, glyphAtlas, glyphStore, spriteAtlas, sprite, info); - new_tile.data->request(style.workers, transformState.getPixelRatio(), callback); + new_tile.data->request( + style.workers, transformState.getPixelRatio(), successCallback, failureCallback); } else if (info.type == SourceType::Raster) { new_tile.data = std::make_shared(normalized_id, texturePool, info); - new_tile.data->request(style.workers, transformState.getPixelRatio(), callback); + new_tile.data->request( + style.workers, transformState.getPixelRatio(), successCallback, failureCallback); } else if (info.type == SourceType::Annotations) { new_tile.data = std::make_shared(normalized_id, data.annotationManager, data.transform.getMaxZoom(), style, glyphAtlas, glyphStore, spriteAtlas, sprite, info); - new_tile.data->reparse(style.workers, callback); + new_tile.data->reparse(style.workers, successCallback); } else { throw std::runtime_error("source type not implemented"); } @@ -536,10 +545,28 @@ void Source::emitSourceLoaded() { } } +void Source::emitSourceLoadingFailed(const std::string& message) { + if (!observer_) { + return; + } + + auto error = std::make_exception_ptr(util::SourceLoadingException(message)); + observer_->onSourceLoadingFailed(error); +} + void Source::emitTileLoaded(bool isNewTile) { if (observer_) { observer_->onTileLoaded(isNewTile); } } +void Source::emitTileLoadingFailed(const std::string& message) { + if (!observer_) { + return; + } + + auto error = std::make_exception_ptr(util::TileLoadingException(message)); + observer_->onTileLoadingFailed(error); +} + } diff --git a/src/mbgl/map/source.hpp b/src/mbgl/map/source.hpp index 9d3de81b7..303d4d024 100644 --- a/src/mbgl/map/source.hpp +++ b/src/mbgl/map/source.hpp @@ -58,7 +58,10 @@ public: virtual ~Observer() = default; virtual void onSourceLoaded() = 0; + virtual void onSourceLoadingFailed(std::exception_ptr error) = 0; + virtual void onTileLoaded(bool isNewTile) = 0; + virtual void onTileLoadingFailed(std::exception_ptr error) = 0; }; Source(); @@ -102,7 +105,9 @@ public: private: void emitSourceLoaded(); + void emitSourceLoadingFailed(const std::string& message); void emitTileLoaded(bool isNewTile); + void emitTileLoadingFailed(const std::string& message); bool handlePartialTile(const TileID &id, Worker &worker); bool findLoadedChildren(const TileID& id, int32_t maxCoveringZoom, std::forward_list& retain); diff --git a/src/mbgl/map/sprite.cpp b/src/mbgl/map/sprite.cpp index cfc097457..d8cadbeb3 100644 --- a/src/mbgl/map/sprite.cpp +++ b/src/mbgl/map/sprite.cpp @@ -1,17 +1,20 @@ #include -#include -#include -#include -#include #include +#include +#include #include #include -#include +#include +#include #include +#include #include +#include +#include + using namespace mbgl; SpritePosition::SpritePosition(uint16_t x_, uint16_t y_, uint16_t width_, uint16_t height_, float pixelRatio_, bool sdf_) @@ -39,25 +42,31 @@ Sprite::Sprite(const std::string& baseUrl, float pixelRatio_) std::string spriteURL(baseUrl + (pixelRatio_ > 1 ? "@2x" : "") + ".png"); std::string jsonURL(baseUrl + (pixelRatio_ > 1 ? "@2x" : "") + ".json"); - jsonRequest = env.request({ Resource::Kind::JSON, jsonURL }, [this](const Response &res) { + jsonRequest = env.request({ Resource::Kind::JSON, jsonURL }, [this, jsonURL](const Response &res) { jsonRequest = nullptr; if (res.status == Response::Successful) { body = res.data; parseJSON(); } else { - Log::Warning(Event::Sprite, "Failed to load sprite info: %s", res.message.c_str()); + std::stringstream message; + message << "Failed to load [" << jsonURL << "]: " << res.message; + emitSpriteLoadingFailed(message.str()); + return; } loadedJSON = true; emitSpriteLoadedIfComplete(); }); - spriteRequest = env.request({ Resource::Kind::Image, spriteURL }, [this](const Response &res) { + spriteRequest = env.request({ Resource::Kind::Image, spriteURL }, [this, spriteURL](const Response &res) { spriteRequest = nullptr; if (res.status == Response::Successful) { image = res.data; parseImage(); } else { - Log::Warning(Event::Sprite, "Failed to load sprite image: %s", res.message.c_str()); + std::stringstream message; + message << "Failed to load [" << spriteURL << "]: " << res.message; + emitSpriteLoadingFailed(message.str()); + return; } loadedImage = true; emitSpriteLoadedIfComplete(); @@ -80,6 +89,15 @@ void Sprite::emitSpriteLoadedIfComplete() { } } +void Sprite::emitSpriteLoadingFailed(const std::string& message) { + if (!observer) { + return; + } + + auto error = std::make_exception_ptr(util::SpriteLoadingException(message)); + observer->onSpriteLoadingFailed(error); +} + bool Sprite::isLoaded() const { return loadedImage && loadedJSON; } diff --git a/src/mbgl/map/sprite.hpp b/src/mbgl/map/sprite.hpp index ccf256161..13cfbe2d8 100644 --- a/src/mbgl/map/sprite.hpp +++ b/src/mbgl/map/sprite.hpp @@ -39,6 +39,7 @@ public: virtual ~Observer() = default; virtual void onSpriteLoaded() = 0; + virtual void onSpriteLoadingFailed(std::exception_ptr error) = 0; }; Sprite(const std::string& baseUrl, float pixelRatio); @@ -56,6 +57,7 @@ public: void setObserver(Observer* observer); private: void emitSpriteLoadedIfComplete(); + void emitSpriteLoadingFailed(const std::string& message); void parseJSON(); void parseImage(); diff --git a/src/mbgl/map/tile_data.cpp b/src/mbgl/map/tile_data.cpp index e9084bdff..be0d7d32f 100644 --- a/src/mbgl/map/tile_data.cpp +++ b/src/mbgl/map/tile_data.cpp @@ -1,11 +1,13 @@ #include + #include #include - -#include -#include -#include #include +#include +#include +#include + +#include using namespace mbgl; @@ -34,15 +36,20 @@ void TileData::setState(const State& state_) { state = state_; } -void TileData::request(Worker& worker, float pixelRatio, std::function callback) { +void TileData::request(Worker& worker, + float pixelRatio, + const std::function& successCallback, + const std::function& failureCallback) { std::string url = source.tileURL(id, pixelRatio); state = State::loading; - req = env.request({ Resource::Kind::Tile, url }, [url, callback, &worker, this](const Response &res) { + req = env.request({ Resource::Kind::Tile, url }, [url, successCallback, failureCallback, &worker, this](const Response &res) { req = nullptr; if (res.status != Response::Successful) { - Log::Error(Event::HttpRequest, "[%s] tile loading failed: %s", url.c_str(), res.message.c_str()); + std::stringstream message; + message << "Failed to load [" << url << "]: " << res.message; + failureCallback(message.str()); return; } @@ -50,7 +57,7 @@ void TileData::request(Worker& worker, float pixelRatio, std::function c data = res.data; // Schedule tile parsing in another thread - reparse(worker, callback); + reparse(worker, successCallback); }); } diff --git a/src/mbgl/map/tile_data.hpp b/src/mbgl/map/tile_data.hpp index 3945a3214..0d34b2df7 100644 --- a/src/mbgl/map/tile_data.hpp +++ b/src/mbgl/map/tile_data.hpp @@ -44,7 +44,10 @@ public: TileData(const TileID&, const SourceInfo&); ~TileData(); - void request(Worker&, float pixelRatio, std::function callback); + void request(Worker&, + float pixelRatio, + const std::function& successCallback, + const std::function& failureCallback); // Schedule a tile reparse on a worker thread and call the callback on // completion. It will return true if the work was schedule or false it was diff --git a/src/mbgl/text/glyph_store.cpp b/src/mbgl/text/glyph_store.cpp index 4ee853b1e..72c7b16a8 100644 --- a/src/mbgl/text/glyph_store.cpp +++ b/src/mbgl/text/glyph_store.cpp @@ -1,20 +1,23 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + #include +#include namespace mbgl { @@ -139,7 +142,8 @@ GlyphPBF::GlyphPBF(const std::string& glyphURL, const std::string& fontStack, GlyphRange glyphRange, Environment& env_, - const GlyphLoadedCallback& callback) + const GlyphLoadedCallback& successCallback, + const GlyphLoadingFailedCallback& failureCallback) : parsed(false), env(env_) { // Load the glyph set URL std::string url = util::replaceTokens(glyphURL, [&](const std::string &name) -> std::string { @@ -149,20 +153,20 @@ GlyphPBF::GlyphPBF(const std::string& glyphURL, }); // The prepare call jumps back to the main thread. - req = env.request({ Resource::Kind::Glyphs, url }, [&, url, callback](const Response &res) { + req = env.request({ Resource::Kind::Glyphs, url }, [&, url, successCallback, failureCallback](const Response &res) { req = nullptr; if (res.status != Response::Successful) { - // Something went wrong with loading the glyph pbf. - const std::string msg = std::string { "[ERROR] failed to load glyphs: " } + url + " message: " + res.message; - Log::Error(Event::HttpRequest, msg); + std::stringstream message; + message << "Failed to load [" << url << "]: " << res.message; + failureCallback(message.str()); } else { // Transfer the data to the GlyphSet and signal its availability. // Once it is available, the caller will need to call parse() to actually // parse the data we received. We are not doing this here since this callback is being // called from another (unknown) thread. data = res.data; - callback(this); + successCallback(this); } }); } @@ -234,8 +238,10 @@ bool GlyphPBF::isParsed() const { GlyphStore::GlyphStore(uv_loop_t* loop, Environment& env_) : env(env_), asyncEmitGlyphRangeLoaded(util::make_unique(loop, [this] { emitGlyphRangeLoaded(); })), + asyncEmitGlyphRangeLoadedingFailed(util::make_unique(loop, [this] { emitGlyphRangeLoadingFailed(); })), observer(nullptr) { asyncEmitGlyphRangeLoaded->unref(); + asyncEmitGlyphRangeLoadedingFailed->unref(); } GlyphStore::~GlyphStore() { @@ -254,19 +260,26 @@ bool GlyphStore::requestGlyphRangesIfNeeded(const std::string& fontStackName, return requestIsNeeded; } - auto callback = [this, fontStackName](GlyphPBF* glyph) { + auto successCallback = [this, fontStackName](GlyphPBF* glyph) { auto fontStack = createFontStack(fontStackName); glyph->parse(**fontStack); asyncEmitGlyphRangeLoaded->send(); }; + auto failureCallback = [this](const std::string& message) { + std::lock_guard lock(errorMessageMutex); + errorMessage = message; + asyncEmitGlyphRangeLoadedingFailed->send(); + }; + std::lock_guard lock(rangesMutex); auto& rangeSets = ranges[fontStackName]; for (const auto& range : glyphRanges) { const auto& rangeSets_it = rangeSets.find(range); if (rangeSets_it == rangeSets.end()) { - auto glyph = util::make_unique(glyphURL, fontStackName, range, env, callback); + auto glyph = util::make_unique(glyphURL, fontStackName, range, env, + successCallback, failureCallback); rangeSets.emplace(range, std::move(glyph)); requestIsNeeded = true; continue; @@ -312,4 +325,14 @@ void GlyphStore::emitGlyphRangeLoaded() { } } +void GlyphStore::emitGlyphRangeLoadingFailed() { + if (!observer) { + return; + } + + std::lock_guard lock(errorMessageMutex); + auto error = std::make_exception_ptr(util::GlyphRangeLoadingException(errorMessage)); + observer->onGlyphRangeLoadingFailed(error); +} + } diff --git a/src/mbgl/text/glyph_store.hpp b/src/mbgl/text/glyph_store.hpp index e13ed1f1d..77452a305 100644 --- a/src/mbgl/text/glyph_store.hpp +++ b/src/mbgl/text/glyph_store.hpp @@ -59,12 +59,14 @@ private: class GlyphPBF { public: using GlyphLoadedCallback = std::function; + using GlyphLoadingFailedCallback = std::function; GlyphPBF(const std::string &glyphURL, const std::string &fontStack, GlyphRange glyphRange, Environment &env, - const GlyphLoadedCallback& callback); + const GlyphLoadedCallback& successCallback, + const GlyphLoadingFailedCallback& failureCallback); ~GlyphPBF(); void parse(FontStack &stack); @@ -91,6 +93,7 @@ public: virtual ~Observer() = default; virtual void onGlyphRangeLoaded() = 0; + virtual void onGlyphRangeLoadingFailed(std::exception_ptr error) = 0; }; GlyphStore(uv_loop_t* loop, Environment &); @@ -110,6 +113,7 @@ public: private: void emitGlyphRangeLoaded(); + void emitGlyphRangeLoadingFailed(); util::exclusive createFontStack(const std::string &fontStack); @@ -122,7 +126,11 @@ private: std::unordered_map> stacks; std::mutex stacksMutex; + std::string errorMessage; + std::mutex errorMessageMutex; + std::unique_ptr asyncEmitGlyphRangeLoaded; + std::unique_ptr asyncEmitGlyphRangeLoadedingFailed; Observer* observer; }; diff --git a/test/api/repeated_render.cpp b/test/api/repeated_render.cpp index ce7e6cbb0..f563b0007 100644 --- a/test/api/repeated_render.cpp +++ b/test/api/repeated_render.cpp @@ -28,7 +28,7 @@ TEST(API, RepeatedRender) { map.resize(128, 512, 1); map.setStyleJSON(style, "test/suite"); std::promise> promise; - map.renderStill([&promise](std::unique_ptr image) { + map.renderStill([&promise](std::exception_ptr, std::unique_ptr image) { promise.set_value(std::move(image)); }); auto result = promise.get_future().get(); @@ -42,7 +42,7 @@ TEST(API, RepeatedRender) { map.resize(512, 512, 2); map.setStyleJSON(style, "TEST_DATA/suite"); std::promise> promise; - map.renderStill([&promise](std::unique_ptr image) { + map.renderStill([&promise](std::exception_ptr, std::unique_ptr image) { promise.set_value(std::move(image)); }); auto result = promise.get_future().get(); diff --git a/test/fixtures/resources/glyphs.pbf b/test/fixtures/resources/glyphs.pbf new file mode 100644 index 000000000..0d160f789 Binary files /dev/null and b/test/fixtures/resources/glyphs.pbf differ diff --git a/test/fixtures/resources/source.json b/test/fixtures/resources/source.json new file mode 100644 index 000000000..db516f9f9 --- /dev/null +++ b/test/fixtures/resources/source.json @@ -0,0 +1 @@ +{"attribution":"© Mapbox © OpenStreetMap Improve this map","bounds":[-180,-85.0511,180,85.0511],"center":[0,0,0],"format":"pbf","maxzoom":15,"minzoom":0,"name":"Mapbox Streets V6 + Vector Terrain V2","scheme":"xyz","tilejson":"2.0.0","tiles":["test/fixtures/resources/vector.pbf"],"vector_layers":[{"description":"Generalized landcover classification","fields":{"class":"One of: wood, scrub, grass, crop, snow"},"id":"landcover","maxzoom":22,"minzoom":0},{"description":"","fields":{"class":"One of: shadow, highlight","level":"Brightness %. One of: 94, 90, 89, 78, 67, 56"},"id":"hillshade","maxzoom":22,"minzoom":0},{"description":"Elevation contour polygons","fields":{"ele":"Integer. The elevation of the contour in meters.","index":"Indicator for every 2nd, 5th, or 10th contour. Coastlines are given -1. One of: 2, 5, 10, -1, null"},"id":"contour","maxzoom":22,"minzoom":0},{"description":"","fields":{"class":"One of: cemetery, hospital, industrial, park, parking, pitch, sand, school, wood","osm_id":"Unique OSM ID number","type":"OSM tag, more specific than class"},"id":"landuse"},{"description":"","fields":{"class":"One of: river, canal, stream, stream_intermittent, ditch, drain","osm_id":"Unique OSM ID number","type":"One of: river, canal, stream, ditch, drain"},"id":"waterway"},{"description":"","fields":{"osm_id":"Unique OSM ID number"},"id":"water"},{"description":"","fields":{"osm_id":"Unique OSM ID number","type":"One of: runway, taxiway, apron"},"id":"aeroway"},{"description":"","fields":{"class":"One of: fence, hedge, cliff, gate","osm_id":"Unique OSM ID number"},"id":"barrier_line"},{"description":"","fields":{"osm_id":"Unique OSM ID number"},"id":"building"},{"description":"","fields":{"class":"One of: wetland, wetland_noveg","osm_id":"Unique OSM ID number"},"id":"landuse_overlay"},{"description":"","fields":{"class":"One of: motorway, motorway_link, main, street, street_limited, service, driveway, path, major_rail, minor_rail, aerialway","layer":"Number used for ordering overlapping tunnels","oneway":"Number. Oneway roads are 1, all others are 0.","osm_id":"Unique OSM ID number","type":"The value of the tunnel's highway tag"},"id":"tunnel"},{"description":"","fields":{"class":"One of: motorway, motorway_link, main, street, street_limited, service, driveway, path, major_rail, minor_rail, aerialway","oneway":"Number. Oneway roads are 1, all others are 0.","osm_id":"Unique OSM ID number","type":"The value of the road's highway tag"},"id":"road"},{"description":"","fields":{"class":"One of: motorway, motorway_link, main, street, street_limited, service, driveway, path, major_rail, minor_rail, aerialway","layer":"Number used for ordering overlapping bridges","oneway":"Number. Oneway bridges are 1, all others are 0.","osm_id":"Unique OSM ID number","type":"The value of the bridge's highway tag"},"id":"bridge"},{"description":"","fields":{"admin_level":"The OSM administrative level of the boundary","disputed":"Number. Disputed boundaries are 1, all others are 0.","maritime":"Number. Maritime boundaries are 1, all others are 0."},"id":"admin"},{"description":"","fields":{"code":"ISO 3166-1 Alpha-2 code","name":"Local name of the country","name_de":"German name of the country","name_en":"English name of the country","name_es":"Spanish name of the country","name_fr":"French name of the country","name_ru":"Russian name of the country","name_zh":"Chinese name of the country","osm_id":"Unique OSM ID number","parent":"ISO 3166-1 Alpha-2 code of the administering/parent state, if any","scalerank":"Number, 1-6. Useful for styling text sizes.","type":"One of: country, territory, disputed territory, sar"},"id":"country_label"},{"description":"","fields":{"labelrank":"Number, 1-6. Useful for styling text sizes.","name":"Local or international name of the water body","name_de":"German name of the water body","name_en":"English name of the water body","name_es":"Spanish name of the water body","name_fr":"French name of the water body","name_ru":"Russian name of the water body","name_zh":"Chinese name of the water body","placement":"One of: point, line"},"id":"marine_label"},{"description":"","fields":{"abbr":"Abbreviated state name","area":"The area of the state in kilometers²","name":"Local name of the state","name_de":"German name of the state","name_en":"English name of the state","name_es":"Spanish name of the state","name_fr":"French name of the state","name_ru":"Russian name of the state","name_zh":"Chinese name of the state","osm_id":"Unique OSM ID number"},"id":"state_label"},{"description":"","fields":{"capital":"Admin level the city is a capital of, if any. One of: 2, 3, 4, null","ldir":"A hint for label placement at low zoom levels. One of: N, E, S, W, NE, SE, SW, NW, null","localrank":"Number. Priority relative to nearby places. Useful for limiting label density.","name":"Local name of the place","name_de":"German name of the place","name_en":"English name of the place","name_es":"Spanish name of the place","name_fr":"French name of the place","name_ru":"Russian name of the place","name_zh":"Chinese name of the place","osm_id":"Unique OSM ID number","scalerank":"Number, 0-9 or null. Useful for styling text & marker sizes.","type":"One of: city, town, village, hamlet, suburb, neighbourhood"},"id":"place_label"},{"description":"","fields":{"area":"The area of the water polygon in Mercator meters²","name":"Local name of the water body","name_de":"German name of the water body","name_en":"English name of the water body","name_es":"Spanish name of the water body","name_fr":"French name of the water body","name_ru":"Russian name of the water body","name_zh":"Chinese name of the water body","osm_id":"Unique OSM ID number"},"id":"water_label"},{"description":"","fields":{"address":"Street address of the POI","localrank":"Number. Priority relative to nearby POIs. Useful for limiting label density.","maki":"The name of the Maki icon that should be used for the POI","name":"Local name of the POI","name_de":"German name of the POI","name_en":"English name of the POI","name_es":"Spanish name of the POI","name_fr":"French name of the POI","name_ru":"Russian name of the POI","name_zh":"Chinese name of the POI","network":"For rail stations, the network(s) that the station serves. Useful for icon styling.","osm_id":"Unique OSM ID number","ref":"Short reference code, if any","scalerank":"Number. 1-4. Useful for styling icon sizes and minimum zoom levels.","type":"The original OSM tag value","website":"URL of the POI"},"id":"poi_label"},{"description":"","fields":{"class":"One of: motorway, motorway_link, main, street, street_limited, service, driveway, path","len":"Number. Length of the road segment in Mercator meters.","localrank":"Number. Used for shield points only. Priority relative to nearby shields. Useful for limiting shield density.","name":"Local name of the road","name_de":"German name of the road","name_en":"English name of the road","name_es":"Spanish name of the road","name_fr":"French name of the road","name_ru":"Russian name of the road","name_zh":"Chinese name of the road","osm_id":"Unique OSM ID number","ref":"Route number of the road","reflen":"Number. How many characters long the ref tag is. Useful for shield styling.","shield":"The shield style to use. One of: default, mx-federal, mx-state, us-highway, us-highway-alternate, us-highway-business, us-highway-duplex, us-interstate, us-interstate-business, us-interstate-duplex, us-interstate-truck, us-state"},"id":"road_label"},{"description":"","fields":{"class":"One of: river, canal, stream, stream_intermittent","name":"Local name of the waterway","name_de":"German name of the waterway","name_en":"English name of the waterway","name_es":"Spanish name of the waterway","name_fr":"French name of the waterway","name_ru":"Russian name of the waterway","name_zh":"Chinese name of the waterway","osm_id":"Unique OSM ID number","type":"One of: river, canal, stream"},"id":"waterway_label"},{"description":"","fields":{"house_num":"House number","osm_id":"Unique OSM ID number"},"id":"housenum_label"}]} diff --git a/test/fixtures/resources/sprite.json b/test/fixtures/resources/sprite.json new file mode 100644 index 000000000..dcc2b4808 --- /dev/null +++ b/test/fixtures/resources/sprite.json @@ -0,0 +1 @@ +{"background":{"x":0,"y":20,"width":50,"height":50,"pixelRatio":1,"sdf":false},"grass_pattern":{"x":100,"y":80,"width":50,"height":50,"pixelRatio":1,"sdf":false},"interstate_1":{"x":0,"y":100,"width":41,"height":40,"pixelRatio":1,"sdf":false},"interstate_2":{"x":0,"y":100,"width":41,"height":40,"pixelRatio":1,"sdf":false},"interstate_3":{"x":41,"y":100,"width":48,"height":39,"pixelRatio":1,"sdf":false},"us_state_1":{"x":0,"y":73,"width":29,"height":24,"pixelRatio":1,"sdf":false},"us_state_2":{"x":0,"y":73,"width":29,"height":24,"pixelRatio":1,"sdf":false},"us_state_3":{"x":30,"y":73,"width":32,"height":24,"pixelRatio":1,"sdf":false},"us_highway_1":{"x":0,"y":142,"width":29,"height":29,"pixelRatio":1,"sdf":false},"us_highway_2":{"x":30,"y":142,"width":33,"height":29,"pixelRatio":1,"sdf":false},"us_highway_3":{"x":64,"y":142,"width":36,"height":29,"pixelRatio":1,"sdf":false},"default_1":{"x":0,"y":0,"width":17,"height":16,"pixelRatio":1,"sdf":false},"default_2":{"x":17,"y":0,"width":22,"height":16,"pixelRatio":1,"sdf":false},"default_3":{"x":39,"y":0,"width":27,"height":16,"pixelRatio":1,"sdf":false},"default_4":{"x":66,"y":0,"width":32,"height":16,"pixelRatio":1,"sdf":false},"default_5":{"x":98,"y":0,"width":37,"height":16,"pixelRatio":1,"sdf":false},"default_6":{"x":135,"y":0,"width":42,"height":16,"pixelRatio":1,"sdf":false},"london-overground":{"x":70,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"london-underground":{"x":88,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"national-rail":{"x":106,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"dlr":{"x":106,"y":25,"width":18,"height":18,"pixelRatio":1,"sdf":false},"dlr.london-overground.london-underground.national-rail":{"x":70,"y":25,"width":72,"height":18,"pixelRatio":1,"sdf":false},"dlr.london-underground":{"x":88,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"dlr.london-underground.national-rail":{"x":88,"y":25,"width":54,"height":18,"pixelRatio":1,"sdf":false},"dlr.national-rail":{"x":106,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"london-overground.london-underground":{"x":70,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"london-overground.london-underground.national-rail":{"x":124,"y":25,"width":54,"height":18,"pixelRatio":1,"sdf":false},"london-overground.national-rail":{"x":124,"y":25,"width":36,"height":18,"pixelRatio":1,"sdf":false},"london-underground.national-rail":{"x":124,"y":43,"width":36,"height":18,"pixelRatio":1,"sdf":false},"metro":{"x":71,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"rer":{"x":87,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"metro.rer":{"x":71,"y":43,"width":34,"height":18,"pixelRatio":1,"sdf":false},"rer.transilien":{"x":87,"y":43,"width":36,"height":18,"pixelRatio":1,"sdf":false},"u-bahn":{"x":70,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"s-bahn":{"x":88,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"s-bahn.u-bahn":{"x":70,"y":62,"width":36,"height":18,"pixelRatio":1,"sdf":false},"washington-metro":{"x":106,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"wiener-linien":{"x":124,"y":62,"width":18,"height":18,"pixelRatio":1,"sdf":false},"moscow-metro":{"x":142,"y":61,"width":21,"height":18,"pixelRatio":1,"sdf":false},"generic-metro":{"x":160,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"generic-rail":{"x":178,"y":43,"width":18,"height":18,"pixelRatio":1,"sdf":false},"dot":{"x":166,"y":63,"width":13,"height":13,"pixelRatio":1,"sdf":false},"default_marker":{"x":0,"y":175,"width":33,"height":86,"pixelRatio":1,"sdf":false},"secondary_marker":{"x":33,"y":175,"width":33,"height":86,"pixelRatio":1,"sdf":false},"oneway_motorway":{"x":178,"y":24,"width":21,"height":19,"pixelRatio":1,"sdf":false},"oneway_road":{"x":178,"y":62,"width":21,"height":19,"pixelRatio":1,"sdf":false},"hospital_icon":{"x":157,"y":259,"width":18,"height":18,"pixelRatio":1,"sdf":false},"fire-station_icon":{"x":157,"y":241,"width":18,"height":18,"pixelRatio":1,"sdf":false},"cemetery_icon":{"x":157,"y":79,"width":18,"height":18,"pixelRatio":1,"sdf":false},"zoo_icon":{"x":177,"y":79,"width":18,"height":18,"pixelRatio":1,"sdf":false},"park_icon":{"x":177,"y":97,"width":18,"height":18,"pixelRatio":1,"sdf":false},"golf_icon":{"x":177,"y":115,"width":18,"height":18,"pixelRatio":1,"sdf":false},"school_icon":{"x":177,"y":133,"width":18,"height":18,"pixelRatio":1,"sdf":false},"monument_icon":{"x":177,"y":151,"width":18,"height":18,"pixelRatio":1,"sdf":false},"library_icon":{"x":177,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"museum_icon":{"x":177,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"college_icon":{"x":177,"y":205,"width":18,"height":18,"pixelRatio":1,"sdf":false},"religious-christian_icon":{"x":157,"y":115,"width":18,"height":18,"pixelRatio":1,"sdf":false},"religious-jewish_icon":{"x":157,"y":133,"width":18,"height":18,"pixelRatio":1,"sdf":false},"religious-muslim_icon":{"x":157,"y":151,"width":18,"height":18,"pixelRatio":1,"sdf":false},"government_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"post_icon":{"x":157,"y":205,"width":18,"height":18,"pixelRatio":1,"sdf":false},"embassy_icon":{"x":157,"y":223,"width":18,"height":18,"pixelRatio":1,"sdf":false},"police_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"marker_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"prison_icon":{"x":157,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"airfield_icon":{"x":157,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"airport_icon":{"x":157,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"harbor_icon":{"x":139,"y":169,"width":18,"height":18,"pixelRatio":1,"sdf":false},"generic_icon":{"x":139,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false},"hospital_striped":{"x":117,"y":135,"width":3,"height":3,"pixelRatio":1,"sdf":false},"school_striped":{"x":114,"y":135,"width":3,"height":3,"pixelRatio":1,"sdf":false},"sand_noise":{"x":75,"y":174,"width":50,"height":50,"pixelRatio":1,"sdf":false}} \ No newline at end of file diff --git a/test/fixtures/resources/sprite.png b/test/fixtures/resources/sprite.png new file mode 100644 index 000000000..967f2e76a Binary files /dev/null and b/test/fixtures/resources/sprite.png differ diff --git a/test/fixtures/resources/style.json b/test/fixtures/resources/style.json new file mode 100644 index 000000000..698f3fb1e --- /dev/null +++ b/test/fixtures/resources/style.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "name": "Test", + "sources": { + "compositedsource": { + "url": "test/fixtures/resources/source.json", + "type": "vector" + } + }, + "sprite": "test/fixtures/resources/sprite", + "glyphs": "test/fixtures/resources/glyphs.pbf", + "layers": [{ + "id": "road", + "type": "symbol", + "source": "compositedsource", + "source-layer": "road_label", + "layout": { + "text-font": "Open Sans Regular, Arial Unicode MS Regular", + "text-field": "{name_en}" + } + }, { + "id": "poi", + "type": "symbol", + "source": "compositedsource", + "source-layer": "poi_label", + "layout": { + "text-font": "Open Sans Regular, Arial Unicode MS Regular", + "icon-image": "{maki}_icon" + } + }] +} diff --git a/test/fixtures/resources/vector.pbf b/test/fixtures/resources/vector.pbf new file mode 100644 index 000000000..cea355eef Binary files /dev/null and b/test/fixtures/resources/vector.pbf differ diff --git a/test/headless/headless.cpp b/test/headless/headless.cpp index a02062dbd..8dfed0383 100644 --- a/test/headless/headless.cpp +++ b/test/headless/headless.cpp @@ -152,7 +152,7 @@ TEST_P(HeadlessTest, render) { map.setLatLngZoom(mbgl::LatLng(latitude, longitude), zoom); map.setBearing(bearing); - map.renderStill([&](std::unique_ptr image) { + map.renderStill([&](std::exception_ptr, std::unique_ptr image) { const std::string png = util::compress_png(image->width, image->height, image->pixels.get()); util::write_file("test/suite/tests/" + base + "/" + name + "/actual.png", png); promise.set_value(); diff --git a/test/resources/mock_file_source.cpp b/test/resources/mock_file_source.cpp new file mode 100644 index 000000000..b6ff99c74 --- /dev/null +++ b/test/resources/mock_file_source.cpp @@ -0,0 +1,47 @@ +#include "mock_file_source.hpp" + +#include +#include +#include + +namespace mbgl { + +class MockFileSource::Impl { +public: + Impl(uv_loop_t*, const std::string& matchFail) : matchFail_(matchFail) {} + + void handleRequest(Request* req) const; + +private: + std::string matchFail_; +}; + +void MockFileSource::Impl::handleRequest(Request* req) const { + const Resource& resource = req->resource; + + std::shared_ptr response = std::make_shared(); + if (matchFail_.empty() || resource.url.find(matchFail_) == std::string::npos) { + response->status = Response::Status::Successful; + response->data = util::read_file(resource.url.c_str()); + } else { + response->message = "Failed by the test case"; + } + + req->notify(response); +} + +MockFileSource::MockFileSource(const std::string& matchFail) + : thread_(util::make_unique>("FileSource", util::ThreadPriority::Low, matchFail)) { +} + +Request* MockFileSource::request(const Resource& resource, uv_loop_t* loop, Callback callback) { + Request* req = new Request(resource, loop, std::move(callback)); + thread_->invoke(&Impl::handleRequest, req); + + return req; +} + +void MockFileSource::cancel(Request*) { +} + +} diff --git a/test/resources/mock_file_source.hpp b/test/resources/mock_file_source.hpp new file mode 100644 index 000000000..8c76bd539 --- /dev/null +++ b/test/resources/mock_file_source.hpp @@ -0,0 +1,33 @@ +#ifndef TEST_RESOURCES_MOCK_FILE_SOURCE +#define TEST_RESOURCES_MOCK_FILE_SOURCE + +#include + +#include + +namespace mbgl { + +namespace util { +template class Thread; +} + +// This mock FileSource will read data from the disk and will fail +// the request if the URL matches the 'matchFail' string. +class MockFileSource : public FileSource { +public: + class Impl; + + MockFileSource(const std::string& matchFail); + ~MockFileSource() override = default; + + // FileSource implementation. + Request* request(const Resource&, uv_loop_t*, Callback) override; + void cancel(Request*) override; + +private: + const std::unique_ptr> thread_; +}; + +} + +#endif diff --git a/test/resources/mock_view.hpp b/test/resources/mock_view.hpp new file mode 100644 index 000000000..865cd2da5 --- /dev/null +++ b/test/resources/mock_view.hpp @@ -0,0 +1,21 @@ +#ifndef TEST_RESOURCES_MOCK_VIEW +#define TEST_RESOURCES_MOCK_VIEW + +#include + +namespace mbgl { + +class MockView : public View { +public: + MockView() = default; + + // View implementation. + void activate() override {}; + void deactivate() override {}; + void notify() override {}; + void invalidate(std::function) override {}; +}; + +} + +#endif diff --git a/test/resources/resource_loader.cpp b/test/resources/resource_loader.cpp new file mode 100644 index 000000000..4203755f8 --- /dev/null +++ b/test/resources/resource_loader.cpp @@ -0,0 +1,154 @@ +#include "../fixtures/util.hpp" +#include "mock_file_source.hpp" +#include "mock_view.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mbgl; + +namespace { + +class MockMapContext : public ResourceLoader::Observer { +public: + MockMapContext(uv_loop_t* loop, + View& view, + FileSource& fileSource, + const std::function& callback) + : env_(fileSource), + envScope_(env_, ThreadType::Map, "Map"), + data_(view, MapMode::Still), + glyphStore_(util::make_unique(loop, env_)), + glyphAtlas_(util::make_unique(1024, 1024)), + spriteAtlas_(util::make_unique(512, 512)), + texturePool_(util::make_unique()), + style_(util::make_unique