From 1e350b7ea485117cadc413d4d41062cf3c3c43a1 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 5 Nov 2015 15:05:43 -0800 Subject: [core] Reorganize sprite related files --- include/mbgl/annotation/sprite_image.hpp | 40 ---- include/mbgl/sprite/sprite_image.hpp | 40 ++++ platform/android/jni.cpp | 2 +- platform/default/glfw_view.cpp | 2 +- platform/ios/MGLMapView.mm | 2 +- src/mbgl/annotation/sprite_image.cpp | 29 --- src/mbgl/annotation/sprite_parser.cpp | 148 --------------- src/mbgl/annotation/sprite_parser.hpp | 46 ----- src/mbgl/annotation/sprite_store.cpp | 71 ------- src/mbgl/annotation/sprite_store.hpp | 53 ------ src/mbgl/geometry/sprite_atlas.cpp | 270 --------------------------- src/mbgl/geometry/sprite_atlas.hpp | 103 ---------- src/mbgl/map/map_context.cpp | 6 +- src/mbgl/map/sprite.cpp | 120 ------------ src/mbgl/map/sprite.hpp | 57 ------ src/mbgl/map/tile_worker.cpp | 2 +- src/mbgl/renderer/painter.cpp | 2 +- src/mbgl/renderer/painter_circle.cpp | 1 - src/mbgl/renderer/painter_fill.cpp | 3 +- src/mbgl/renderer/painter_line.cpp | 3 +- src/mbgl/renderer/painter_symbol.cpp | 2 +- src/mbgl/renderer/symbol_bucket.cpp | 6 +- src/mbgl/sprite/sprite.cpp | 119 ++++++++++++ src/mbgl/sprite/sprite.hpp | 57 ++++++ src/mbgl/sprite/sprite_atlas.cpp | 270 +++++++++++++++++++++++++++ src/mbgl/sprite/sprite_atlas.hpp | 103 ++++++++++ src/mbgl/sprite/sprite_image.cpp | 29 +++ src/mbgl/sprite/sprite_parser.cpp | 148 +++++++++++++++ src/mbgl/sprite/sprite_parser.hpp | 46 +++++ src/mbgl/sprite/sprite_store.cpp | 71 +++++++ src/mbgl/sprite/sprite_store.hpp | 53 ++++++ src/mbgl/style/style.cpp | 6 +- src/mbgl/style/style.hpp | 2 +- test/annotations/sprite_atlas.cpp | 173 ----------------- test/annotations/sprite_image.cpp | 64 ------- test/annotations/sprite_parser.cpp | 310 ------------------------------- test/annotations/sprite_store.cpp | 143 -------------- test/miscellaneous/custom_sprites.cpp | 61 ------ test/sprite/custom_sprites.cpp | 61 ++++++ test/sprite/sprite.cpp | 173 +++++++++++++++++ test/sprite/sprite_atlas.cpp | 173 +++++++++++++++++ test/sprite/sprite_image.cpp | 64 +++++++ test/sprite/sprite_parser.cpp | 310 +++++++++++++++++++++++++++++++ test/sprite/sprite_store.cpp | 143 ++++++++++++++ test/style/sprite.cpp | 173 ----------------- test/test.gypi | 14 +- 46 files changed, 1885 insertions(+), 1889 deletions(-) delete mode 100644 include/mbgl/annotation/sprite_image.hpp create mode 100644 include/mbgl/sprite/sprite_image.hpp delete mode 100644 src/mbgl/annotation/sprite_image.cpp delete mode 100644 src/mbgl/annotation/sprite_parser.cpp delete mode 100644 src/mbgl/annotation/sprite_parser.hpp delete mode 100644 src/mbgl/annotation/sprite_store.cpp delete mode 100644 src/mbgl/annotation/sprite_store.hpp delete mode 100644 src/mbgl/geometry/sprite_atlas.cpp delete mode 100644 src/mbgl/geometry/sprite_atlas.hpp delete mode 100644 src/mbgl/map/sprite.cpp delete mode 100644 src/mbgl/map/sprite.hpp create mode 100644 src/mbgl/sprite/sprite.cpp create mode 100644 src/mbgl/sprite/sprite.hpp create mode 100644 src/mbgl/sprite/sprite_atlas.cpp create mode 100644 src/mbgl/sprite/sprite_atlas.hpp create mode 100644 src/mbgl/sprite/sprite_image.cpp create mode 100644 src/mbgl/sprite/sprite_parser.cpp create mode 100644 src/mbgl/sprite/sprite_parser.hpp create mode 100644 src/mbgl/sprite/sprite_store.cpp create mode 100644 src/mbgl/sprite/sprite_store.hpp delete mode 100644 test/annotations/sprite_atlas.cpp delete mode 100644 test/annotations/sprite_image.cpp delete mode 100644 test/annotations/sprite_parser.cpp delete mode 100644 test/annotations/sprite_store.cpp delete mode 100644 test/miscellaneous/custom_sprites.cpp create mode 100644 test/sprite/custom_sprites.cpp create mode 100644 test/sprite/sprite.cpp create mode 100644 test/sprite/sprite_atlas.cpp create mode 100644 test/sprite/sprite_image.cpp create mode 100644 test/sprite/sprite_parser.cpp create mode 100644 test/sprite/sprite_store.cpp delete mode 100644 test/style/sprite.cpp diff --git a/include/mbgl/annotation/sprite_image.hpp b/include/mbgl/annotation/sprite_image.hpp deleted file mode 100644 index 7d8ea0501c..0000000000 --- a/include/mbgl/annotation/sprite_image.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef MBGL_ANNOTATIONS_SPRITE_IMAGE -#define MBGL_ANNOTATIONS_SPRITE_IMAGE - -#include -#include - -#include -#include -#include - -namespace mbgl { - -class SpriteImage : private util::noncopyable { -public: - SpriteImage( - uint16_t width, uint16_t height, float pixelRatio, std::string&& data, bool sdf = false); - - // Logical dimensions of the sprite image. - const uint16_t width; - const uint16_t height; - - // Pixel ratio of the sprite image. - const float pixelRatio; - - // Physical dimensions of the sprite image. - const uint16_t pixelWidth; - const uint16_t pixelHeight; - - // A string of an RGBA8 representation of the sprite. It must have exactly - // (width * ratio) * (height * ratio) * 4 (RGBA) bytes. The scan lines may - // not have gaps between them (i.e. stride == 0). - const std::string data; - - // Whether this image should be interpreted as a signed distance field icon. - const bool sdf; -}; - -} - -#endif diff --git a/include/mbgl/sprite/sprite_image.hpp b/include/mbgl/sprite/sprite_image.hpp new file mode 100644 index 0000000000..f36ec5e286 --- /dev/null +++ b/include/mbgl/sprite/sprite_image.hpp @@ -0,0 +1,40 @@ +#ifndef MBGL_SPRITE_IMAGE +#define MBGL_SPRITE_IMAGE + +#include +#include + +#include +#include +#include + +namespace mbgl { + +class SpriteImage : private util::noncopyable { +public: + SpriteImage( + uint16_t width, uint16_t height, float pixelRatio, std::string&& data, bool sdf = false); + + // Logical dimensions of the sprite image. + const uint16_t width; + const uint16_t height; + + // Pixel ratio of the sprite image. + const float pixelRatio; + + // Physical dimensions of the sprite image. + const uint16_t pixelWidth; + const uint16_t pixelHeight; + + // A string of an RGBA8 representation of the sprite. It must have exactly + // (width * ratio) * (height * ratio) * 4 (RGBA) bytes. The scan lines may + // not have gaps between them (i.e. stride == 0). + const std::string data; + + // Whether this image should be interpreted as a signed distance field icon. + const bool sdf; +}; + +} + +#endif diff --git a/platform/android/jni.cpp b/platform/android/jni.cpp index 707b0ac9e2..1e4c4c05f3 100644 --- a/platform/android/jni.cpp +++ b/platform/android/jni.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/platform/default/glfw_view.cpp b/platform/default/glfw_view.cpp index 6a5524d2c9..71c808ddb1 100644 --- a/platform/default/glfw_view.cpp +++ b/platform/default/glfw_view.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include #include diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index a097e4932e..750f3fabbb 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/mbgl/annotation/sprite_image.cpp b/src/mbgl/annotation/sprite_image.cpp deleted file mode 100644 index e482d8f13f..0000000000 --- a/src/mbgl/annotation/sprite_image.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include - -#include - -#include - -namespace mbgl { - -SpriteImage::SpriteImage(const uint16_t width_, - const uint16_t height_, - const float pixelRatio_, - std::string&& data_, - bool sdf_) - : width(width_), - height(height_), - pixelRatio(pixelRatio_), - pixelWidth(std::ceil(width * pixelRatio)), - pixelHeight(std::ceil(height * pixelRatio)), - data(std::move(data_)), - sdf(sdf_) { - const size_t size = pixelWidth * pixelHeight * 4; - if (size == 0) { - throw util::SpriteImageException("Sprite image dimensions may not be zero"); - } else if (size != data.size()) { - throw util::SpriteImageException("Sprite image pixel count mismatch"); - } -} - -} // namespace mbgl diff --git a/src/mbgl/annotation/sprite_parser.cpp b/src/mbgl/annotation/sprite_parser.cpp deleted file mode 100644 index acd93b47ce..0000000000 --- a/src/mbgl/annotation/sprite_parser.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include -#include - -#include - -#include - -#include -#include - -#include -#include -#include - -namespace mbgl { - -SpriteImagePtr createSpriteImage(const util::Image& image, - const uint16_t srcX, - const uint16_t srcY, - const uint16_t srcWidth, - const uint16_t srcHeight, - const double ratio, - const bool sdf) { - // Disallow invalid parameter configurations. - if (srcWidth == 0 || srcHeight == 0 || ratio <= 0 || ratio > 10 || srcWidth > 1024 || - srcHeight > 1024) { - Log::Warning(Event::Sprite, "Can't create sprite with invalid metrics"); - return nullptr; - } - - const uint16_t width = std::ceil(double(srcWidth) / ratio); - const uint16_t dstWidth = std::ceil(width * ratio); - assert(dstWidth >= srcWidth); - const uint16_t height = std::ceil(double(srcHeight) / ratio); - const uint16_t dstHeight = std::ceil(height * ratio); - assert(dstHeight >= srcHeight); - - std::string data(dstWidth * dstHeight * 4, '\0'); - - auto srcData = reinterpret_cast(image.getData()); - auto dstData = reinterpret_cast(const_cast(data.data())); - - const int32_t maxX = std::min(image.getWidth(), uint32_t(srcWidth + srcX)) - srcX; - assert(maxX <= int32_t(image.getWidth())); - const int32_t maxY = std::min(image.getHeight(), uint32_t(srcHeight + srcY)) - srcY; - assert(maxY <= int32_t(image.getHeight())); - - // Copy from the source image into our individual sprite image - for (uint16_t y = 0; y < maxY; ++y) { - const auto dstRow = y * dstWidth; - const auto srcRow = (y + srcY) * image.getWidth() + srcX; - for (uint16_t x = 0; x < maxX; ++x) { - dstData[dstRow + x] = srcData[srcRow + x]; - } - } - - return std::make_unique(width, height, ratio, std::move(data), sdf); -} - -namespace { - -inline uint16_t getUInt16(const rapidjson::Value& value, const char* name, const uint16_t def = 0) { - if (value.HasMember(name)) { - auto& v = value[name]; - if (v.IsUint() && v.GetUint() <= std::numeric_limits::max()) { - return v.GetUint(); - } else { - Log::Warning(Event::Sprite, "Value of '%s' must be an integer between 0 and 65535", - name); - } - } - - return def; -} - -inline double getDouble(const rapidjson::Value& value, const char* name, const double def = 0) { - if (value.HasMember(name)) { - auto& v = value[name]; - if (v.IsNumber()) { - return v.GetDouble(); - } else { - Log::Warning(Event::Sprite, "Value of '%s' must be a number", name); - } - } - - return def; -} - -inline bool getBoolean(const rapidjson::Value& value, const char* name, const bool def = false) { - if (value.HasMember(name)) { - auto& v = value[name]; - if (v.IsBool()) { - return v.GetBool(); - } else { - Log::Warning(Event::Sprite, "Value of '%s' must be a boolean", name); - } - } - - return def; -} - -} // namespace - -SpriteParseResult parseSprite(const std::string& image, const std::string& json) { - using namespace rapidjson; - - Sprites sprites; - - // Parse the sprite image. - const util::Image raster(image); - if (!raster) { - return std::string("Could not parse sprite image"); - } - - Document doc; - doc.Parse<0>(json.c_str()); - - if (doc.HasParseError()) { - std::stringstream message; - message << "Failed to parse JSON: " << rapidjson::GetParseError_En(doc.GetParseError()) << " at offset " << doc.GetErrorOffset(); - return message.str(); - } else if (!doc.IsObject()) { - return std::string("Sprite JSON root must be an object"); - } else { - for (Value::ConstMemberIterator itr = doc.MemberBegin(); itr != doc.MemberEnd(); ++itr) { - const std::string name = { itr->name.GetString(), itr->name.GetStringLength() }; - const Value& value = itr->value; - - if (value.IsObject()) { - const uint16_t x = getUInt16(value, "x", 0); - const uint16_t y = getUInt16(value, "y", 0); - const uint16_t width = getUInt16(value, "width", 0); - const uint16_t height = getUInt16(value, "height", 0); - const double pixelRatio = getDouble(value, "pixelRatio", 1); - const bool sdf = getBoolean(value, "sdf", false); - - auto sprite = createSpriteImage(raster, x, y, width, height, pixelRatio, sdf); - if (sprite) { - sprites.emplace(name, sprite); - } - } - } - } - - return sprites; -} - -} // namespace mbgl diff --git a/src/mbgl/annotation/sprite_parser.hpp b/src/mbgl/annotation/sprite_parser.hpp deleted file mode 100644 index c5856ebbc7..0000000000 --- a/src/mbgl/annotation/sprite_parser.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef MBGL_ANNOTATIONS_SPRITE_PARSER -#define MBGL_ANNOTATIONS_SPRITE_PARSER - -#include - -#include -#include - -#include -#include -#include - -namespace mbgl { - -namespace util { - -class Image; - -} // namespace util - -class SpriteImage; - -using SpriteImagePtr = std::shared_ptr; - -// Extracts an individual image from a spritesheet from the given location. -SpriteImagePtr createSpriteImage(const util::Image& image, - uint16_t srcX, - uint16_t srcY, - uint16_t srcWidth, - uint16_t srcHeight, - double ratio, - bool sdf); - -using Sprites = std::map; - - -using SpriteParseResult = mapbox::util::variant< - Sprites, // success - std::string>; // error - -// Parses an image and an associated JSON file and returns the sprite objects. -SpriteParseResult parseSprite(const std::string& image, const std::string& json); - -} // namespace mbgl - -#endif diff --git a/src/mbgl/annotation/sprite_store.cpp b/src/mbgl/annotation/sprite_store.cpp deleted file mode 100644 index 8d2231a2b7..0000000000 --- a/src/mbgl/annotation/sprite_store.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include - -#include - -namespace mbgl { - -void SpriteStore::setSprite(const std::string& name, std::shared_ptr sprite) { - std::lock_guard lock(mutex); - _setSprite(name, sprite); -} - -void SpriteStore::_setSprite(const std::string& name, - const std::shared_ptr& sprite) { - if (sprite) { - auto it = sprites.find(name); - if (it != sprites.end()) { - // There is already a sprite with that name in our store. - if ((it->second->width != sprite->width || it->second->height != sprite->height)) { - Log::Warning(Event::Sprite, "Can't change sprite dimensions for '%s'", name.c_str()); - return; - } - it->second = sprite; - } else { - sprites.emplace(name, sprite); - } - - // Always add/replace the value in the dirty list. - auto dirty_it = dirty.find(name); - if (dirty_it != dirty.end()) { - dirty_it->second = sprite; - } else { - dirty.emplace(name, sprite); - } - } else if (sprites.erase(name) > 0) { - dirty.emplace(name, nullptr); - } -} - -void SpriteStore::setSprites(const Sprites& newSprites) { - std::lock_guard lock(mutex); - for (const auto& pair : newSprites) { - _setSprite(pair.first, pair.second); - } -} - -void SpriteStore::removeSprite(const std::string& name) { - std::lock_guard lock(mutex); - _setSprite(name); -} - -std::shared_ptr SpriteStore::getSprite(const std::string& name) { - std::lock_guard lock(mutex); - const auto it = sprites.find(name); - if (it != sprites.end()) { - return it->second; - } else { - if (!sprites.empty()) { - Log::Info(Event::Sprite, "Can't find sprite named '%s'", name.c_str()); - } - return nullptr; - } -} - -SpriteStore::Sprites SpriteStore::getDirty() { - Sprites result; - std::lock_guard lock(mutex); - dirty.swap(result); - return result; -} - -} // namespace mbgl diff --git a/src/mbgl/annotation/sprite_store.hpp b/src/mbgl/annotation/sprite_store.hpp deleted file mode 100644 index 78e02ac695..0000000000 --- a/src/mbgl/annotation/sprite_store.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef MBGL_ANNOTATION_SPRITE_STORE -#define MBGL_ANNOTATION_SPRITE_STORE - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace mbgl { - -// The SpriteStore object holds Sprite images. -class SpriteStore : private util::noncopyable { - using Sprites = std::map>; - -public: - // Adds/replaces a Sprite image. - void setSprite(const std::string&, std::shared_ptr = nullptr); - - // Adds/replaces mutliple Sprite images. - void setSprites(const Sprites& sprites); - - // Removes a Sprite. - void removeSprite(const std::string&); - - // Obtains a Sprite image. - std::shared_ptr getSprite(const std::string&); - - // Returns Sprite images that changed since the last invocation of this function. - Sprites getDirty(); - -private: - void _setSprite(const std::string&, const std::shared_ptr& = nullptr); - - // Lock for sprites and dirty maps. - std::mutex mutex; - - // Stores all current sprites. - Sprites sprites; - - // Stores all Sprite IDs that changed since the last invocation. - Sprites dirty; -}; - -} // namespace mbgl - -#endif diff --git a/src/mbgl/geometry/sprite_atlas.cpp b/src/mbgl/geometry/sprite_atlas.cpp deleted file mode 100644 index 743848d5f2..0000000000 --- a/src/mbgl/geometry/sprite_atlas.cpp +++ /dev/null @@ -1,270 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - - -using namespace mbgl; - -SpriteAtlas::SpriteAtlas(dimension width_, dimension height_, float pixelRatio_, SpriteStore& store_) - : width(width_), - height(height_), - pixelWidth(std::ceil(width * pixelRatio_)), - pixelHeight(std::ceil(height * pixelRatio_)), - pixelRatio(pixelRatio_), - store(store_), - bin(width_, height_), - data(std::make_unique(pixelWidth * pixelHeight)), - dirty(true) { - std::fill(data.get(), data.get() + pixelWidth * pixelHeight, 0); -} - -Rect SpriteAtlas::allocateImage(const size_t pixel_width, const size_t pixel_height) { - // Increase to next number divisible by 4, but at least 1. - // This is so we can scale down the texture coordinates and pack them - // into 2 bytes rather than 4 bytes. - const uint16_t pack_width = (pixel_width + 1) + (4 - (pixel_width + 1) % 4); - const uint16_t pack_height = (pixel_height + 1) + (4 - (pixel_height + 1) % 4); - - // We have to allocate a new area in the bin, and store an empty image in it. - // Add a 1px border around every image. - Rect rect = bin.allocate(pack_width, pack_height); - if (rect.w == 0) { - return rect; - } - - rect.originalW = pixel_width; - rect.originalH = pixel_height; - - return rect; -} - -SpriteAtlasElement SpriteAtlas::getImage(const std::string& name, const bool wrap) { - std::lock_guard lock(mtx); - - auto rect_it = images.find({ name, wrap }); - if (rect_it != images.end()) { - return { rect_it->second.pos, rect_it->second.texture }; - } - - auto sprite = store.getSprite(name); - if (!sprite) { - return { Rect { 0, 0, 0, 0 }, nullptr }; - } - - Rect rect = allocateImage(sprite->width, sprite->height); - if (rect.w == 0) { - if (debug::spriteWarnings) { - Log::Warning(Event::Sprite, "sprite atlas bitmap overflow"); - } - return { Rect { 0, 0, 0, 0 }, nullptr }; - } - - const Holder& holder = images.emplace(Key{ name, wrap }, Holder{ sprite, rect }).first->second; - copy(holder, wrap); - - return { rect, sprite }; -} - -SpriteAtlasPosition SpriteAtlas::getPosition(const std::string& name, bool repeating) { - std::lock_guard lock(mtx); - - auto rect = getImage(name, repeating).pos; - if (repeating) { - // When the image is repeating, get the correct position of the image, rather than the - // one rounded up to 4 pixels. - // TODO: Can't we just use originalW/originalH? - auto sprite = store.getSprite(name); - if (!sprite) { - return SpriteAtlasPosition {}; - } - - rect.w = sprite->width; - rect.h = sprite->height; - } - - const float padding = 1; - - return SpriteAtlasPosition { - {{ float(rect.w), float(rect.h) }}, - {{ float(rect.x + padding) / width, float(rect.y + padding) / height }}, - {{ float(rect.x + padding + rect.w) / width, float(rect.y + padding + rect.h) / height }} - }; -} - -void SpriteAtlas::copy(const Holder& holder, const bool wrap) { - const uint32_t *srcData = reinterpret_cast(holder.texture->data.data()); - if (!srcData) return; - const vec2 srcSize { holder.texture->pixelWidth, holder.texture->pixelHeight }; - const Rect srcPos { 0, 0, srcSize.x, srcSize.y }; - const auto& dst = holder.pos; - - const int offset = 1; - - uint32_t *const dstData = data.get(); - const vec2 dstSize{ pixelWidth, pixelHeight }; - const Rect dstPos{ static_cast((offset + dst.x) * pixelRatio), - static_cast((offset + dst.y) * pixelRatio), - static_cast(dst.originalW * pixelRatio), - static_cast(dst.originalH * pixelRatio) }; - - util::bilinearScale(srcData, srcSize, srcPos, dstData, dstSize, dstPos, wrap); - - // Add borders around the copied image if required. - if (wrap) { - // We're copying from the same image so we don't have to scale again. - const uint32_t border = 1; - const uint32_t borderX = dstPos.x != 0 ? border : 0; - const uint32_t borderY = dstPos.y != 0 ? border : 0; - - // Left border - util::nearestNeighborScale( - dstData, dstSize, { dstPos.x + dstPos.w - borderX, dstPos.y, borderX, dstPos.h }, - dstData, dstSize, { dstPos.x - borderX, dstPos.y, borderX, dstPos.h }); - - // Right border - util::nearestNeighborScale(dstData, dstSize, { dstPos.x, dstPos.y, border, dstPos.h }, - dstData, dstSize, - { dstPos.x + dstPos.w, dstPos.y, border, dstPos.h }); - - // Top border - util::nearestNeighborScale( - dstData, dstSize, { dstPos.x - borderX, dstPos.y + dstPos.h - borderY, - dstPos.w + border + borderX, borderY }, - dstData, dstSize, - { dstPos.x - borderX, dstPos.y - borderY, dstPos.w + 2 * borderX, borderY }); - - // Bottom border - util::nearestNeighborScale( - dstData, dstSize, { dstPos.x - borderX, dstPos.y, dstPos.w + 2 * borderX, border }, - dstData, dstSize, - { dstPos.x - borderX, dstPos.y + dstPos.h, dstPos.w + border + borderX, border }); - } - - dirty = true; -} - -void SpriteAtlas::upload() { - if (dirty) { - bind(); - } -} - -void SpriteAtlas::updateDirty() { - auto dirtySprites = store.getDirty(); - if (dirtySprites.empty()) { - return; - } - - std::lock_guard lock(mtx); - - auto imageIterator = images.begin(); - auto spriteIterator = dirtySprites.begin(); - while (imageIterator != images.end() && spriteIterator != dirtySprites.end()) { - if (imageIterator->first.first < spriteIterator->first) { - ++imageIterator; - } else if (spriteIterator->first < imageIterator->first.first) { - ++spriteIterator; - } else { - // The two names match; - Holder& holder = imageIterator->second; - holder.texture = spriteIterator->second; - copy(holder, imageIterator->first.second); - - ++imageIterator; - // Don't advance the spriteIterator because there might be another sprite with the same - // name, but a different wrap value. - } - } -} - -void SpriteAtlas::bind(bool linear) { - if (!texture) { - MBGL_CHECK_ERROR(glGenTextures(1, &texture)); - MBGL_CHECK_ERROR(glBindTexture(GL_TEXTURE_2D, texture)); -#ifndef GL_ES_VERSION_2_0 - MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); -#endif - // We are using clamp to edge here since OpenGL ES doesn't allow GL_REPEAT on NPOT textures. - // We use those when the pixelRatio isn't a power of two, e.g. on iPhone 6 Plus. - MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); - MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); - fullUploadRequired = true; - } else { - MBGL_CHECK_ERROR(glBindTexture(GL_TEXTURE_2D, texture)); - } - - GLuint filter_val = linear ? GL_LINEAR : GL_NEAREST; - if (filter_val != filter) { - MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter_val)); - MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter_val)); - filter = filter_val; - } - - if (dirty) { - std::lock_guard lock(mtx); - - if (fullUploadRequired) { - MBGL_CHECK_ERROR(glTexImage2D( - GL_TEXTURE_2D, // GLenum target - 0, // GLint level - GL_RGBA, // GLint internalformat - pixelWidth, // GLsizei width - pixelHeight, // GLsizei height - 0, // GLint border - GL_RGBA, // GLenum format - GL_UNSIGNED_BYTE, // GLenum type - data.get() // const GLvoid * data - )); - fullUploadRequired = false; - } else { - MBGL_CHECK_ERROR(glTexSubImage2D( - GL_TEXTURE_2D, // GLenum target - 0, // GLint level - 0, // GLint xoffset - 0, // GLint yoffset - pixelWidth, // GLsizei width - pixelHeight, // GLsizei height - GL_RGBA, // GLenum format - GL_UNSIGNED_BYTE, // GLenum type - data.get() // const GLvoid *pixels - )); - } - - dirty = false; - -#ifndef GL_ES_VERSION_2_0 - // platform::showColorDebugImage("Sprite Atlas", reinterpret_cast(data.get()), - // pixelWidth, pixelHeight, pixelWidth, pixelHeight); -#endif - } -}; - -SpriteAtlas::~SpriteAtlas() { - std::lock_guard lock(mtx); - if (texture) { - mbgl::util::ThreadContext::getGLObjectStore()->abandonTexture(texture); - texture = 0; - } -} - -SpriteAtlas::Holder::Holder(const std::shared_ptr& texture_, - const Rect& pos_) - : texture(texture_), pos(pos_) { -} - -SpriteAtlas::Holder::Holder(Holder&& h) : texture(std::move(h.texture)), pos(h.pos) { -} diff --git a/src/mbgl/geometry/sprite_atlas.hpp b/src/mbgl/geometry/sprite_atlas.hpp deleted file mode 100644 index 2e794f651b..0000000000 --- a/src/mbgl/geometry/sprite_atlas.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef MBGL_GEOMETRY_SPRITE_ATLAS -#define MBGL_GEOMETRY_SPRITE_ATLAS - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace mbgl { - -class SpriteStore; -class SpriteImage; -class SpritePosition; - -struct SpriteAtlasPosition { - inline SpriteAtlasPosition(const std::array size_ = {{0, 0}}, - const std::array tl_ = {{0, 0}}, - const std::array br_ = {{0, 0}}) - : size(size_), tl(tl_), br(br_) {} - std::array size; - std::array tl; - std::array br; -}; - -struct SpriteAtlasElement { - const Rect pos; - const std::shared_ptr texture; -}; - -class SpriteAtlas : public util::noncopyable { -public: - typedef uint16_t dimension; - - SpriteAtlas(dimension width, dimension height, float pixelRatio, SpriteStore& store); - ~SpriteAtlas(); - - // Returns the coordinates of an image that is sourced from the sprite image. - // This getter attempts to read the image from the sprite if it is already loaded. - // In that case, it copies it into the sprite atlas and returns the dimensions. - // Otherwise, it returns a 0/0/0/0 rect. - // This function is used during bucket creation. - SpriteAtlasElement getImage(const std::string& name, const bool wrap); - - // This function is used for getting the position during render time. - SpriteAtlasPosition getPosition(const std::string& name, bool repeating = false); - - // Binds the atlas texture to the GPU, and uploads data if it is out of date. - void bind(bool linear = false); - - // Updates sprites in the atlas texture that may have changed in the source SpriteStore object. - void updateDirty(); - - // Uploads the texture to the GPU to be available when we need it. This is a lazy operation; - // the texture is only bound when the data is out of date (=dirty). - void upload(); - - inline dimension getWidth() const { return width; } - inline dimension getHeight() const { return height; } - inline dimension getTextureWidth() const { return pixelWidth; } - inline dimension getTextureHeight() const { return pixelHeight; } - inline float getPixelRatio() const { return pixelRatio; } - inline const uint32_t* getData() const { return data.get(); } - -private: - const GLsizei width, height; - const dimension pixelWidth, pixelHeight; - const float pixelRatio; - - struct Holder : private util::noncopyable { - inline Holder(const std::shared_ptr&, const Rect&); - inline Holder(Holder&&); - std::shared_ptr texture; - const Rect pos; - }; - - using Key = std::pair; - - Rect allocateImage(size_t width, size_t height); - void copy(const Holder& holder, const bool wrap); - - std::recursive_mutex mtx; - SpriteStore& store; - BinPack bin; - std::map images; - std::set uninitialized; - const std::unique_ptr data; - std::atomic dirty; - bool fullUploadRequired = true; - GLuint texture = 0; - uint32_t filter = 0; - static const int buffer = 1; -}; - -}; - -#endif diff --git a/src/mbgl/map/map_context.cpp b/src/mbgl/map/map_context.cpp index 75f195a3f9..dedfff02d8 100644 --- a/src/mbgl/map/map_context.cpp +++ b/src/mbgl/map/map_context.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include @@ -12,10 +11,11 @@ #include #include -#include - #include +#include +#include + #include #include #include diff --git a/src/mbgl/map/sprite.cpp b/src/mbgl/map/sprite.cpp deleted file mode 100644 index 518979c5f0..0000000000 --- a/src/mbgl/map/sprite.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -namespace mbgl { - -struct Sprite::Loader { - std::shared_ptr image; - std::shared_ptr json; - RequestHolder jsonRequest; - RequestHolder spriteRequest; -}; - -Sprite::Sprite(const std::string& baseUrl, float pixelRatio_) - : pixelRatio(pixelRatio_ > 1 ? 2 : 1) { - if (baseUrl.empty()) { - // Treat a non-existent sprite as a successfully loaded empty sprite. - loaded = true; - return; - } - - std::string spriteURL(baseUrl + (pixelRatio_ > 1 ? "@2x" : "") + ".png"); - std::string jsonURL(baseUrl + (pixelRatio_ > 1 ? "@2x" : "") + ".json"); - - loader = std::make_unique(); - - FileSource* fs = util::ThreadContext::getFileSource(); - loader->jsonRequest = fs->request({ Resource::Kind::SpriteJSON, jsonURL }, util::RunLoop::getLoop(), - [this, jsonURL](const Response& res) { - if (res.stale) { - // Only handle fresh responses. - return; - } - loader->jsonRequest = nullptr; - - if (res.error) { - std::stringstream message; - message << "Failed to load [" << jsonURL << "]: " << res.error->message; - emitSpriteLoadingFailed(message.str()); - return; - } else { - loader->json = res.data; - } - emitSpriteLoadedIfComplete(); - }); - - loader->spriteRequest = - fs->request({ Resource::Kind::SpriteImage, spriteURL }, util::RunLoop::getLoop(), - [this, spriteURL](const Response& res) { - if (res.stale) { - // Only handle fresh responses. - return; - } - loader->spriteRequest = nullptr; - - if (res.error) { - std::stringstream message; - message << "Failed to load [" << spriteURL << "]: " << res.error->message; - emitSpriteLoadingFailed(message.str()); - return; - } else { - loader->image = res.data; - } - emitSpriteLoadedIfComplete(); - }); -} - -Sprite::~Sprite() { -} - -void Sprite::emitSpriteLoadedIfComplete() { - assert(loader); - - if (!loader->image || !loader->json || !observer) { - return; - } - - auto local = std::move(loader); - auto result = parseSprite(*local->image, *local->json); - if (result.is()) { - loaded = true; - observer->onSpriteLoaded(result.get()); - } else { - emitSpriteLoadingFailed(result.get()); - } -} - -void Sprite::emitSpriteLoadingFailed(const std::string& message) { - if (!observer) { - return; - } - - auto error = std::make_exception_ptr(util::SpriteLoadingException(message)); - observer->onSpriteLoadingFailed(error); -} - -void Sprite::setObserver(Observer* observer_) { - observer = observer_; -} - -void Sprite::dumpDebugLogs() const { - Log::Info(Event::General, "Sprite::loaded: %d", loaded); -} - -} // namespace mbgl diff --git a/src/mbgl/map/sprite.hpp b/src/mbgl/map/sprite.hpp deleted file mode 100644 index cd82460a12..0000000000 --- a/src/mbgl/map/sprite.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef MBGL_STYLE_SPRITE -#define MBGL_STYLE_SPRITE - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace mbgl { - -class Request; - -class Sprite : private util::noncopyable { -public: - class Observer { - public: - virtual ~Observer() = default; - - virtual void onSpriteLoaded(const Sprites& sprites) = 0; - virtual void onSpriteLoadingFailed(std::exception_ptr error) = 0; - }; - - Sprite(const std::string& baseUrl, float pixelRatio); - ~Sprite(); - - inline bool isLoaded() const { - return loaded; - } - - void dumpDebugLogs() const; - - const float pixelRatio; - - void setObserver(Observer* observer); - -private: - void emitSpriteLoadedIfComplete(); - void emitSpriteLoadingFailed(const std::string& message); - - struct Loader; - std::unique_ptr loader; - - bool loaded = false; - - Observer* observer = nullptr; -}; - -} // namespace mbgl - -#endif diff --git a/src/mbgl/map/tile_worker.cpp b/src/mbgl/map/tile_worker.cpp index 8c1115170d..effb90a73d 100644 --- a/src/mbgl/map/tile_worker.cpp +++ b/src/mbgl/map/tile_worker.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/mbgl/renderer/painter.cpp b/src/mbgl/renderer/painter.cpp index 140ab2991c..284bed3705 100644 --- a/src/mbgl/renderer/painter.cpp +++ b/src/mbgl/renderer/painter.cpp @@ -13,7 +13,7 @@ #include -#include +#include #include #include diff --git a/src/mbgl/renderer/painter_circle.cpp b/src/mbgl/renderer/painter_circle.cpp index 708064bf89..67c23f8615 100644 --- a/src/mbgl/renderer/painter_circle.cpp +++ b/src/mbgl/renderer/painter_circle.cpp @@ -3,7 +3,6 @@ #include -#include #include #include diff --git a/src/mbgl/renderer/painter_fill.cpp b/src/mbgl/renderer/painter_fill.cpp index d9e069d4e8..4bee5b6d1e 100644 --- a/src/mbgl/renderer/painter_fill.cpp +++ b/src/mbgl/renderer/painter_fill.cpp @@ -1,9 +1,8 @@ #include #include #include -#include #include -#include +#include #include #include #include diff --git a/src/mbgl/renderer/painter_line.cpp b/src/mbgl/renderer/painter_line.cpp index cfc85fcb48..e33864622e 100644 --- a/src/mbgl/renderer/painter_line.cpp +++ b/src/mbgl/renderer/painter_line.cpp @@ -1,13 +1,12 @@ #include #include #include -#include #include #include #include #include #include -#include +#include #include #include diff --git a/src/mbgl/renderer/painter_symbol.cpp b/src/mbgl/renderer/painter_symbol.cpp index 343c9d5233..f46a9c8454 100644 --- a/src/mbgl/renderer/painter_symbol.cpp +++ b/src/mbgl/renderer/painter_symbol.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/mbgl/renderer/symbol_bucket.cpp b/src/mbgl/renderer/symbol_bucket.cpp index ee42d7f47e..1071fdcce9 100644 --- a/src/mbgl/renderer/symbol_bucket.cpp +++ b/src/mbgl/renderer/symbol_bucket.cpp @@ -1,11 +1,12 @@ #include #include #include -#include +#include +#include +#include #include #include #include -#include #include #include #include @@ -16,7 +17,6 @@ #include #include #include -#include #include #include diff --git a/src/mbgl/sprite/sprite.cpp b/src/mbgl/sprite/sprite.cpp new file mode 100644 index 0000000000..881a6ff8ed --- /dev/null +++ b/src/mbgl/sprite/sprite.cpp @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace mbgl { + +struct Sprite::Loader { + std::shared_ptr image; + std::shared_ptr json; + RequestHolder jsonRequest; + RequestHolder spriteRequest; +}; + +Sprite::Sprite(const std::string& baseUrl, float pixelRatio_) + : pixelRatio(pixelRatio_ > 1 ? 2 : 1) { + if (baseUrl.empty()) { + // Treat a non-existent sprite as a successfully loaded empty sprite. + loaded = true; + return; + } + + std::string spriteURL(baseUrl + (pixelRatio_ > 1 ? "@2x" : "") + ".png"); + std::string jsonURL(baseUrl + (pixelRatio_ > 1 ? "@2x" : "") + ".json"); + + loader = std::make_unique(); + + FileSource* fs = util::ThreadContext::getFileSource(); + loader->jsonRequest = fs->request({ Resource::Kind::SpriteJSON, jsonURL }, util::RunLoop::getLoop(), + [this, jsonURL](const Response& res) { + if (res.stale) { + // Only handle fresh responses. + return; + } + loader->jsonRequest = nullptr; + + if (res.error) { + std::stringstream message; + message << "Failed to load [" << jsonURL << "]: " << res.error->message; + emitSpriteLoadingFailed(message.str()); + return; + } else { + loader->json = res.data; + } + emitSpriteLoadedIfComplete(); + }); + + loader->spriteRequest = + fs->request({ Resource::Kind::SpriteImage, spriteURL }, util::RunLoop::getLoop(), + [this, spriteURL](const Response& res) { + if (res.stale) { + // Only handle fresh responses. + return; + } + loader->spriteRequest = nullptr; + + if (res.error) { + std::stringstream message; + message << "Failed to load [" << spriteURL << "]: " << res.error->message; + emitSpriteLoadingFailed(message.str()); + return; + } else { + loader->image = res.data; + } + emitSpriteLoadedIfComplete(); + }); +} + +Sprite::~Sprite() { +} + +void Sprite::emitSpriteLoadedIfComplete() { + assert(loader); + + if (!loader->image || !loader->json || !observer) { + return; + } + + auto local = std::move(loader); + auto result = parseSprite(*local->image, *local->json); + if (result.is()) { + loaded = true; + observer->onSpriteLoaded(result.get()); + } else { + emitSpriteLoadingFailed(result.get()); + } +} + +void Sprite::emitSpriteLoadingFailed(const std::string& message) { + if (!observer) { + return; + } + + auto error = std::make_exception_ptr(util::SpriteLoadingException(message)); + observer->onSpriteLoadingFailed(error); +} + +void Sprite::setObserver(Observer* observer_) { + observer = observer_; +} + +void Sprite::dumpDebugLogs() const { + Log::Info(Event::General, "Sprite::loaded: %d", loaded); +} + +} // namespace mbgl diff --git a/src/mbgl/sprite/sprite.hpp b/src/mbgl/sprite/sprite.hpp new file mode 100644 index 0000000000..d204b42e85 --- /dev/null +++ b/src/mbgl/sprite/sprite.hpp @@ -0,0 +1,57 @@ +#ifndef MBGL_SPRITE +#define MBGL_SPRITE + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mbgl { + +class Request; + +class Sprite : private util::noncopyable { +public: + class Observer { + public: + virtual ~Observer() = default; + + virtual void onSpriteLoaded(const Sprites& sprites) = 0; + virtual void onSpriteLoadingFailed(std::exception_ptr error) = 0; + }; + + Sprite(const std::string& baseUrl, float pixelRatio); + ~Sprite(); + + inline bool isLoaded() const { + return loaded; + } + + void dumpDebugLogs() const; + + const float pixelRatio; + + void setObserver(Observer* observer); + +private: + void emitSpriteLoadedIfComplete(); + void emitSpriteLoadingFailed(const std::string& message); + + struct Loader; + std::unique_ptr loader; + + bool loaded = false; + + Observer* observer = nullptr; +}; + +} // namespace mbgl + +#endif diff --git a/src/mbgl/sprite/sprite_atlas.cpp b/src/mbgl/sprite/sprite_atlas.cpp new file mode 100644 index 0000000000..22c2ba8b95 --- /dev/null +++ b/src/mbgl/sprite/sprite_atlas.cpp @@ -0,0 +1,270 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + + +using namespace mbgl; + +SpriteAtlas::SpriteAtlas(dimension width_, dimension height_, float pixelRatio_, SpriteStore& store_) + : width(width_), + height(height_), + pixelWidth(std::ceil(width * pixelRatio_)), + pixelHeight(std::ceil(height * pixelRatio_)), + pixelRatio(pixelRatio_), + store(store_), + bin(width_, height_), + data(std::make_unique(pixelWidth * pixelHeight)), + dirty(true) { + std::fill(data.get(), data.get() + pixelWidth * pixelHeight, 0); +} + +Rect SpriteAtlas::allocateImage(const size_t pixel_width, const size_t pixel_height) { + // Increase to next number divisible by 4, but at least 1. + // This is so we can scale down the texture coordinates and pack them + // into 2 bytes rather than 4 bytes. + const uint16_t pack_width = (pixel_width + 1) + (4 - (pixel_width + 1) % 4); + const uint16_t pack_height = (pixel_height + 1) + (4 - (pixel_height + 1) % 4); + + // We have to allocate a new area in the bin, and store an empty image in it. + // Add a 1px border around every image. + Rect rect = bin.allocate(pack_width, pack_height); + if (rect.w == 0) { + return rect; + } + + rect.originalW = pixel_width; + rect.originalH = pixel_height; + + return rect; +} + +SpriteAtlasElement SpriteAtlas::getImage(const std::string& name, const bool wrap) { + std::lock_guard lock(mtx); + + auto rect_it = images.find({ name, wrap }); + if (rect_it != images.end()) { + return { rect_it->second.pos, rect_it->second.texture }; + } + + auto sprite = store.getSprite(name); + if (!sprite) { + return { Rect { 0, 0, 0, 0 }, nullptr }; + } + + Rect rect = allocateImage(sprite->width, sprite->height); + if (rect.w == 0) { + if (debug::spriteWarnings) { + Log::Warning(Event::Sprite, "sprite atlas bitmap overflow"); + } + return { Rect { 0, 0, 0, 0 }, nullptr }; + } + + const Holder& holder = images.emplace(Key{ name, wrap }, Holder{ sprite, rect }).first->second; + copy(holder, wrap); + + return { rect, sprite }; +} + +SpriteAtlasPosition SpriteAtlas::getPosition(const std::string& name, bool repeating) { + std::lock_guard lock(mtx); + + auto rect = getImage(name, repeating).pos; + if (repeating) { + // When the image is repeating, get the correct position of the image, rather than the + // one rounded up to 4 pixels. + // TODO: Can't we just use originalW/originalH? + auto sprite = store.getSprite(name); + if (!sprite) { + return SpriteAtlasPosition {}; + } + + rect.w = sprite->width; + rect.h = sprite->height; + } + + const float padding = 1; + + return SpriteAtlasPosition { + {{ float(rect.w), float(rect.h) }}, + {{ float(rect.x + padding) / width, float(rect.y + padding) / height }}, + {{ float(rect.x + padding + rect.w) / width, float(rect.y + padding + rect.h) / height }} + }; +} + +void SpriteAtlas::copy(const Holder& holder, const bool wrap) { + const uint32_t *srcData = reinterpret_cast(holder.texture->data.data()); + if (!srcData) return; + const vec2 srcSize { holder.texture->pixelWidth, holder.texture->pixelHeight }; + const Rect srcPos { 0, 0, srcSize.x, srcSize.y }; + const auto& dst = holder.pos; + + const int offset = 1; + + uint32_t *const dstData = data.get(); + const vec2 dstSize{ pixelWidth, pixelHeight }; + const Rect dstPos{ static_cast((offset + dst.x) * pixelRatio), + static_cast((offset + dst.y) * pixelRatio), + static_cast(dst.originalW * pixelRatio), + static_cast(dst.originalH * pixelRatio) }; + + util::bilinearScale(srcData, srcSize, srcPos, dstData, dstSize, dstPos, wrap); + + // Add borders around the copied image if required. + if (wrap) { + // We're copying from the same image so we don't have to scale again. + const uint32_t border = 1; + const uint32_t borderX = dstPos.x != 0 ? border : 0; + const uint32_t borderY = dstPos.y != 0 ? border : 0; + + // Left border + util::nearestNeighborScale( + dstData, dstSize, { dstPos.x + dstPos.w - borderX, dstPos.y, borderX, dstPos.h }, + dstData, dstSize, { dstPos.x - borderX, dstPos.y, borderX, dstPos.h }); + + // Right border + util::nearestNeighborScale(dstData, dstSize, { dstPos.x, dstPos.y, border, dstPos.h }, + dstData, dstSize, + { dstPos.x + dstPos.w, dstPos.y, border, dstPos.h }); + + // Top border + util::nearestNeighborScale( + dstData, dstSize, { dstPos.x - borderX, dstPos.y + dstPos.h - borderY, + dstPos.w + border + borderX, borderY }, + dstData, dstSize, + { dstPos.x - borderX, dstPos.y - borderY, dstPos.w + 2 * borderX, borderY }); + + // Bottom border + util::nearestNeighborScale( + dstData, dstSize, { dstPos.x - borderX, dstPos.y, dstPos.w + 2 * borderX, border }, + dstData, dstSize, + { dstPos.x - borderX, dstPos.y + dstPos.h, dstPos.w + border + borderX, border }); + } + + dirty = true; +} + +void SpriteAtlas::upload() { + if (dirty) { + bind(); + } +} + +void SpriteAtlas::updateDirty() { + auto dirtySprites = store.getDirty(); + if (dirtySprites.empty()) { + return; + } + + std::lock_guard lock(mtx); + + auto imageIterator = images.begin(); + auto spriteIterator = dirtySprites.begin(); + while (imageIterator != images.end() && spriteIterator != dirtySprites.end()) { + if (imageIterator->first.first < spriteIterator->first) { + ++imageIterator; + } else if (spriteIterator->first < imageIterator->first.first) { + ++spriteIterator; + } else { + // The two names match; + Holder& holder = imageIterator->second; + holder.texture = spriteIterator->second; + copy(holder, imageIterator->first.second); + + ++imageIterator; + // Don't advance the spriteIterator because there might be another sprite with the same + // name, but a different wrap value. + } + } +} + +void SpriteAtlas::bind(bool linear) { + if (!texture) { + MBGL_CHECK_ERROR(glGenTextures(1, &texture)); + MBGL_CHECK_ERROR(glBindTexture(GL_TEXTURE_2D, texture)); +#ifndef GL_ES_VERSION_2_0 + MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); +#endif + // We are using clamp to edge here since OpenGL ES doesn't allow GL_REPEAT on NPOT textures. + // We use those when the pixelRatio isn't a power of two, e.g. on iPhone 6 Plus. + MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + fullUploadRequired = true; + } else { + MBGL_CHECK_ERROR(glBindTexture(GL_TEXTURE_2D, texture)); + } + + GLuint filter_val = linear ? GL_LINEAR : GL_NEAREST; + if (filter_val != filter) { + MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter_val)); + MBGL_CHECK_ERROR(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter_val)); + filter = filter_val; + } + + if (dirty) { + std::lock_guard lock(mtx); + + if (fullUploadRequired) { + MBGL_CHECK_ERROR(glTexImage2D( + GL_TEXTURE_2D, // GLenum target + 0, // GLint level + GL_RGBA, // GLint internalformat + pixelWidth, // GLsizei width + pixelHeight, // GLsizei height + 0, // GLint border + GL_RGBA, // GLenum format + GL_UNSIGNED_BYTE, // GLenum type + data.get() // const GLvoid * data + )); + fullUploadRequired = false; + } else { + MBGL_CHECK_ERROR(glTexSubImage2D( + GL_TEXTURE_2D, // GLenum target + 0, // GLint level + 0, // GLint xoffset + 0, // GLint yoffset + pixelWidth, // GLsizei width + pixelHeight, // GLsizei height + GL_RGBA, // GLenum format + GL_UNSIGNED_BYTE, // GLenum type + data.get() // const GLvoid *pixels + )); + } + + dirty = false; + +#ifndef GL_ES_VERSION_2_0 + // platform::showColorDebugImage("Sprite Atlas", reinterpret_cast(data.get()), + // pixelWidth, pixelHeight, pixelWidth, pixelHeight); +#endif + } +}; + +SpriteAtlas::~SpriteAtlas() { + std::lock_guard lock(mtx); + if (texture) { + mbgl::util::ThreadContext::getGLObjectStore()->abandonTexture(texture); + texture = 0; + } +} + +SpriteAtlas::Holder::Holder(const std::shared_ptr& texture_, + const Rect& pos_) + : texture(texture_), pos(pos_) { +} + +SpriteAtlas::Holder::Holder(Holder&& h) : texture(std::move(h.texture)), pos(h.pos) { +} diff --git a/src/mbgl/sprite/sprite_atlas.hpp b/src/mbgl/sprite/sprite_atlas.hpp new file mode 100644 index 0000000000..0d86279e2d --- /dev/null +++ b/src/mbgl/sprite/sprite_atlas.hpp @@ -0,0 +1,103 @@ +#ifndef MBGL_SPRITE_ATLAS +#define MBGL_SPRITE_ATLAS + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +class SpriteStore; +class SpriteImage; +class SpritePosition; + +struct SpriteAtlasPosition { + inline SpriteAtlasPosition(const std::array size_ = {{0, 0}}, + const std::array tl_ = {{0, 0}}, + const std::array br_ = {{0, 0}}) + : size(size_), tl(tl_), br(br_) {} + std::array size; + std::array tl; + std::array br; +}; + +struct SpriteAtlasElement { + const Rect pos; + const std::shared_ptr texture; +}; + +class SpriteAtlas : public util::noncopyable { +public: + typedef uint16_t dimension; + + SpriteAtlas(dimension width, dimension height, float pixelRatio, SpriteStore& store); + ~SpriteAtlas(); + + // Returns the coordinates of an image that is sourced from the sprite image. + // This getter attempts to read the image from the sprite if it is already loaded. + // In that case, it copies it into the sprite atlas and returns the dimensions. + // Otherwise, it returns a 0/0/0/0 rect. + // This function is used during bucket creation. + SpriteAtlasElement getImage(const std::string& name, const bool wrap); + + // This function is used for getting the position during render time. + SpriteAtlasPosition getPosition(const std::string& name, bool repeating = false); + + // Binds the atlas texture to the GPU, and uploads data if it is out of date. + void bind(bool linear = false); + + // Updates sprites in the atlas texture that may have changed in the source SpriteStore object. + void updateDirty(); + + // Uploads the texture to the GPU to be available when we need it. This is a lazy operation; + // the texture is only bound when the data is out of date (=dirty). + void upload(); + + inline dimension getWidth() const { return width; } + inline dimension getHeight() const { return height; } + inline dimension getTextureWidth() const { return pixelWidth; } + inline dimension getTextureHeight() const { return pixelHeight; } + inline float getPixelRatio() const { return pixelRatio; } + inline const uint32_t* getData() const { return data.get(); } + +private: + const GLsizei width, height; + const dimension pixelWidth, pixelHeight; + const float pixelRatio; + + struct Holder : private util::noncopyable { + inline Holder(const std::shared_ptr&, const Rect&); + inline Holder(Holder&&); + std::shared_ptr texture; + const Rect pos; + }; + + using Key = std::pair; + + Rect allocateImage(size_t width, size_t height); + void copy(const Holder& holder, const bool wrap); + + std::recursive_mutex mtx; + SpriteStore& store; + BinPack bin; + std::map images; + std::set uninitialized; + const std::unique_ptr data; + std::atomic dirty; + bool fullUploadRequired = true; + GLuint texture = 0; + uint32_t filter = 0; + static const int buffer = 1; +}; + +}; + +#endif diff --git a/src/mbgl/sprite/sprite_image.cpp b/src/mbgl/sprite/sprite_image.cpp new file mode 100644 index 0000000000..d5e4a7828e --- /dev/null +++ b/src/mbgl/sprite/sprite_image.cpp @@ -0,0 +1,29 @@ +#include + +#include + +#include + +namespace mbgl { + +SpriteImage::SpriteImage(const uint16_t width_, + const uint16_t height_, + const float pixelRatio_, + std::string&& data_, + bool sdf_) + : width(width_), + height(height_), + pixelRatio(pixelRatio_), + pixelWidth(std::ceil(width * pixelRatio)), + pixelHeight(std::ceil(height * pixelRatio)), + data(std::move(data_)), + sdf(sdf_) { + const size_t size = pixelWidth * pixelHeight * 4; + if (size == 0) { + throw util::SpriteImageException("Sprite image dimensions may not be zero"); + } else if (size != data.size()) { + throw util::SpriteImageException("Sprite image pixel count mismatch"); + } +} + +} // namespace mbgl diff --git a/src/mbgl/sprite/sprite_parser.cpp b/src/mbgl/sprite/sprite_parser.cpp new file mode 100644 index 0000000000..a8ed4f0e12 --- /dev/null +++ b/src/mbgl/sprite/sprite_parser.cpp @@ -0,0 +1,148 @@ +#include +#include + +#include + +#include + +#include +#include + +#include +#include +#include + +namespace mbgl { + +SpriteImagePtr createSpriteImage(const util::Image& image, + const uint16_t srcX, + const uint16_t srcY, + const uint16_t srcWidth, + const uint16_t srcHeight, + const double ratio, + const bool sdf) { + // Disallow invalid parameter configurations. + if (srcWidth == 0 || srcHeight == 0 || ratio <= 0 || ratio > 10 || srcWidth > 1024 || + srcHeight > 1024) { + Log::Warning(Event::Sprite, "Can't create sprite with invalid metrics"); + return nullptr; + } + + const uint16_t width = std::ceil(double(srcWidth) / ratio); + const uint16_t dstWidth = std::ceil(width * ratio); + assert(dstWidth >= srcWidth); + const uint16_t height = std::ceil(double(srcHeight) / ratio); + const uint16_t dstHeight = std::ceil(height * ratio); + assert(dstHeight >= srcHeight); + + std::string data(dstWidth * dstHeight * 4, '\0'); + + auto srcData = reinterpret_cast(image.getData()); + auto dstData = reinterpret_cast(const_cast(data.data())); + + const int32_t maxX = std::min(image.getWidth(), uint32_t(srcWidth + srcX)) - srcX; + assert(maxX <= int32_t(image.getWidth())); + const int32_t maxY = std::min(image.getHeight(), uint32_t(srcHeight + srcY)) - srcY; + assert(maxY <= int32_t(image.getHeight())); + + // Copy from the source image into our individual sprite image + for (uint16_t y = 0; y < maxY; ++y) { + const auto dstRow = y * dstWidth; + const auto srcRow = (y + srcY) * image.getWidth() + srcX; + for (uint16_t x = 0; x < maxX; ++x) { + dstData[dstRow + x] = srcData[srcRow + x]; + } + } + + return std::make_unique(width, height, ratio, std::move(data), sdf); +} + +namespace { + +inline uint16_t getUInt16(const rapidjson::Value& value, const char* name, const uint16_t def = 0) { + if (value.HasMember(name)) { + auto& v = value[name]; + if (v.IsUint() && v.GetUint() <= std::numeric_limits::max()) { + return v.GetUint(); + } else { + Log::Warning(Event::Sprite, "Value of '%s' must be an integer between 0 and 65535", + name); + } + } + + return def; +} + +inline double getDouble(const rapidjson::Value& value, const char* name, const double def = 0) { + if (value.HasMember(name)) { + auto& v = value[name]; + if (v.IsNumber()) { + return v.GetDouble(); + } else { + Log::Warning(Event::Sprite, "Value of '%s' must be a number", name); + } + } + + return def; +} + +inline bool getBoolean(const rapidjson::Value& value, const char* name, const bool def = false) { + if (value.HasMember(name)) { + auto& v = value[name]; + if (v.IsBool()) { + return v.GetBool(); + } else { + Log::Warning(Event::Sprite, "Value of '%s' must be a boolean", name); + } + } + + return def; +} + +} // namespace + +SpriteParseResult parseSprite(const std::string& image, const std::string& json) { + using namespace rapidjson; + + Sprites sprites; + + // Parse the sprite image. + const util::Image raster(image); + if (!raster) { + return std::string("Could not parse sprite image"); + } + + Document doc; + doc.Parse<0>(json.c_str()); + + if (doc.HasParseError()) { + std::stringstream message; + message << "Failed to parse JSON: " << rapidjson::GetParseError_En(doc.GetParseError()) << " at offset " << doc.GetErrorOffset(); + return message.str(); + } else if (!doc.IsObject()) { + return std::string("Sprite JSON root must be an object"); + } else { + for (Value::ConstMemberIterator itr = doc.MemberBegin(); itr != doc.MemberEnd(); ++itr) { + const std::string name = { itr->name.GetString(), itr->name.GetStringLength() }; + const Value& value = itr->value; + + if (value.IsObject()) { + const uint16_t x = getUInt16(value, "x", 0); + const uint16_t y = getUInt16(value, "y", 0); + const uint16_t width = getUInt16(value, "width", 0); + const uint16_t height = getUInt16(value, "height", 0); + const double pixelRatio = getDouble(value, "pixelRatio", 1); + const bool sdf = getBoolean(value, "sdf", false); + + auto sprite = createSpriteImage(raster, x, y, width, height, pixelRatio, sdf); + if (sprite) { + sprites.emplace(name, sprite); + } + } + } + } + + return sprites; +} + +} // namespace mbgl diff --git a/src/mbgl/sprite/sprite_parser.hpp b/src/mbgl/sprite/sprite_parser.hpp new file mode 100644 index 0000000000..d385d77aea --- /dev/null +++ b/src/mbgl/sprite/sprite_parser.hpp @@ -0,0 +1,46 @@ +#ifndef MBGL_SPRITE_PARSER +#define MBGL_SPRITE_PARSER + +#include + +#include +#include + +#include +#include +#include + +namespace mbgl { + +namespace util { + +class Image; + +} // namespace util + +class SpriteImage; + +using SpriteImagePtr = std::shared_ptr; + +// Extracts an individual image from a spritesheet from the given location. +SpriteImagePtr createSpriteImage(const util::Image& image, + uint16_t srcX, + uint16_t srcY, + uint16_t srcWidth, + uint16_t srcHeight, + double ratio, + bool sdf); + +using Sprites = std::map; + + +using SpriteParseResult = mapbox::util::variant< + Sprites, // success + std::string>; // error + +// Parses an image and an associated JSON file and returns the sprite objects. +SpriteParseResult parseSprite(const std::string& image, const std::string& json); + +} // namespace mbgl + +#endif diff --git a/src/mbgl/sprite/sprite_store.cpp b/src/mbgl/sprite/sprite_store.cpp new file mode 100644 index 0000000000..fb6833114e --- /dev/null +++ b/src/mbgl/sprite/sprite_store.cpp @@ -0,0 +1,71 @@ +#include + +#include + +namespace mbgl { + +void SpriteStore::setSprite(const std::string& name, std::shared_ptr sprite) { + std::lock_guard lock(mutex); + _setSprite(name, sprite); +} + +void SpriteStore::_setSprite(const std::string& name, + const std::shared_ptr& sprite) { + if (sprite) { + auto it = sprites.find(name); + if (it != sprites.end()) { + // There is already a sprite with that name in our store. + if ((it->second->width != sprite->width || it->second->height != sprite->height)) { + Log::Warning(Event::Sprite, "Can't change sprite dimensions for '%s'", name.c_str()); + return; + } + it->second = sprite; + } else { + sprites.emplace(name, sprite); + } + + // Always add/replace the value in the dirty list. + auto dirty_it = dirty.find(name); + if (dirty_it != dirty.end()) { + dirty_it->second = sprite; + } else { + dirty.emplace(name, sprite); + } + } else if (sprites.erase(name) > 0) { + dirty.emplace(name, nullptr); + } +} + +void SpriteStore::setSprites(const Sprites& newSprites) { + std::lock_guard lock(mutex); + for (const auto& pair : newSprites) { + _setSprite(pair.first, pair.second); + } +} + +void SpriteStore::removeSprite(const std::string& name) { + std::lock_guard lock(mutex); + _setSprite(name); +} + +std::shared_ptr SpriteStore::getSprite(const std::string& name) { + std::lock_guard lock(mutex); + const auto it = sprites.find(name); + if (it != sprites.end()) { + return it->second; + } else { + if (!sprites.empty()) { + Log::Info(Event::Sprite, "Can't find sprite named '%s'", name.c_str()); + } + return nullptr; + } +} + +SpriteStore::Sprites SpriteStore::getDirty() { + Sprites result; + std::lock_guard lock(mutex); + dirty.swap(result); + return result; +} + +} // namespace mbgl diff --git a/src/mbgl/sprite/sprite_store.hpp b/src/mbgl/sprite/sprite_store.hpp new file mode 100644 index 0000000000..ed903f074b --- /dev/null +++ b/src/mbgl/sprite/sprite_store.hpp @@ -0,0 +1,53 @@ +#ifndef MBGL_SPRITE_STORE +#define MBGL_SPRITE_STORE + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +// The SpriteStore object holds Sprite images. +class SpriteStore : private util::noncopyable { + using Sprites = std::map>; + +public: + // Adds/replaces a Sprite image. + void setSprite(const std::string&, std::shared_ptr = nullptr); + + // Adds/replaces mutliple Sprite images. + void setSprites(const Sprites& sprites); + + // Removes a Sprite. + void removeSprite(const std::string&); + + // Obtains a Sprite image. + std::shared_ptr getSprite(const std::string&); + + // Returns Sprite images that changed since the last invocation of this function. + Sprites getDirty(); + +private: + void _setSprite(const std::string&, const std::shared_ptr& = nullptr); + + // Lock for sprites and dirty maps. + std::mutex mutex; + + // Stores all current sprites. + Sprites sprites; + + // Stores all Sprite IDs that changed since the last invocation. + Sprites dirty; +}; + +} // namespace mbgl + +#endif diff --git a/src/mbgl/style/style.cpp b/src/mbgl/style/style.cpp index 5509447275..acf4752e07 100644 --- a/src/mbgl/style/style.cpp +++ b/src/mbgl/style/style.cpp @@ -1,9 +1,10 @@ #include -#include #include #include #include -#include +#include +#include +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include diff --git a/src/mbgl/style/style.hpp b/src/mbgl/style/style.hpp index 412107b737..837703e264 100644 --- a/src/mbgl/style/style.hpp +++ b/src/mbgl/style/style.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include #include diff --git a/test/annotations/sprite_atlas.cpp b/test/annotations/sprite_atlas.cpp deleted file mode 100644 index 0eece6c47c..0000000000 --- a/test/annotations/sprite_atlas.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "../fixtures/util.hpp" -#include "../fixtures/fixture_log_observer.hpp" - -#include -#include -#include -#include -#include - -using namespace mbgl; - -TEST(Annotations, SpriteAtlas) { - FixtureLog log; - - auto spriteParseResult = parseSprite(util::read_file("test/fixtures/annotations/emerald.png"), - util::read_file("test/fixtures/annotations/emerald.json")); - - SpriteStore store; - store.setSprites(spriteParseResult.get()); - - SpriteAtlas atlas(63, 112, 1, store); - - EXPECT_EQ(1.0f, atlas.getPixelRatio()); - EXPECT_EQ(63, atlas.getWidth()); - EXPECT_EQ(112, atlas.getHeight()); - EXPECT_EQ(63, atlas.getTextureWidth()); - EXPECT_EQ(112, atlas.getTextureHeight()); - - // Image hasn't been created yet. - EXPECT_TRUE(atlas.getData()); - - auto metro = atlas.getImage("metro", false); - EXPECT_EQ(0, metro.pos.x); - EXPECT_EQ(0, metro.pos.y); - EXPECT_EQ(20, metro.pos.w); - EXPECT_EQ(20, metro.pos.h); - EXPECT_EQ(18, metro.pos.originalW); - EXPECT_EQ(18, metro.pos.originalH); - EXPECT_EQ(18, metro.texture->width); - EXPECT_EQ(18, metro.texture->height); - EXPECT_EQ(18, metro.texture->pixelWidth); - EXPECT_EQ(18, metro.texture->pixelHeight); - EXPECT_EQ(1.0f, metro.texture->pixelRatio); - - auto pos = atlas.getPosition("metro", false); - EXPECT_DOUBLE_EQ(20, pos.size[0]); - EXPECT_DOUBLE_EQ(20, pos.size[1]); - EXPECT_DOUBLE_EQ(1.0f / 63, pos.tl[0]); - EXPECT_DOUBLE_EQ(1.0f / 112, pos.tl[1]); - EXPECT_DOUBLE_EQ(21.0f / 63, pos.br[0]); - EXPECT_DOUBLE_EQ(21.0f / 112, pos.br[1]); - - - auto missing = atlas.getImage("doesnotexist", false); - EXPECT_FALSE(missing.pos.hasArea()); - EXPECT_EQ(0, missing.pos.x); - EXPECT_EQ(0, missing.pos.y); - EXPECT_EQ(0, missing.pos.w); - EXPECT_EQ(0, missing.pos.h); - EXPECT_EQ(0, missing.pos.originalW); - EXPECT_EQ(0, missing.pos.originalH); - EXPECT_FALSE(missing.texture); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Info, - Event::Sprite, - int64_t(-1), - "Can't find sprite named 'doesnotexist'", - })); - - // Different wrapping mode produces different image. - auto metro2 = atlas.getImage("metro", true); - EXPECT_EQ(20, metro2.pos.x); - EXPECT_EQ(0, metro2.pos.y); - EXPECT_EQ(20, metro2.pos.w); - EXPECT_EQ(20, metro2.pos.h); - EXPECT_EQ(18, metro2.pos.originalW); - EXPECT_EQ(18, metro2.pos.originalH); - - const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; - const auto hash = test::crc64(reinterpret_cast(atlas.getData()), bytes); - EXPECT_EQ(0x9875FC0595489A9Fu, hash) << std::hex << hash; - - // util::write_file( - // "test/fixtures/annotations/atlas1.png", - // util::compress_png(atlas.getTextureWidth(), atlas.getTextureHeight(), atlas.getData())); -} - -TEST(Annotations, SpriteAtlasSize) { - auto spriteParseResult = parseSprite(util::read_file("test/fixtures/annotations/emerald.png"), - util::read_file("test/fixtures/annotations/emerald.json")); - - SpriteStore store; - store.setSprites(spriteParseResult.get()); - - SpriteAtlas atlas(63, 112, 1.4, store); - - EXPECT_DOUBLE_EQ(1.4f, atlas.getPixelRatio()); - EXPECT_EQ(63, atlas.getWidth()); - EXPECT_EQ(112, atlas.getHeight()); - EXPECT_EQ(89, atlas.getTextureWidth()); - EXPECT_EQ(157, atlas.getTextureHeight()); - - auto metro = atlas.getImage("metro", false); - EXPECT_EQ(0, metro.pos.x); - EXPECT_EQ(0, metro.pos.y); - EXPECT_EQ(20, metro.pos.w); - EXPECT_EQ(20, metro.pos.h); - EXPECT_EQ(18, metro.pos.originalW); - EXPECT_EQ(18, metro.pos.originalH); - EXPECT_EQ(18, metro.texture->width); - EXPECT_EQ(18, metro.texture->height); - EXPECT_EQ(18, metro.texture->pixelWidth); - EXPECT_EQ(18, metro.texture->pixelHeight); - EXPECT_EQ(1.0f, metro.texture->pixelRatio); - - const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; - const auto hash = test::crc64(reinterpret_cast(atlas.getData()), bytes); - EXPECT_EQ(0x2CDDA7DBB04D116Du, hash) << std::hex << hash; - - // util::write_file( - // "test/fixtures/annotations/atlas2.png", - // util::compress_png(atlas.getTextureWidth(), atlas.getTextureHeight(), atlas.getData())); -} - -TEST(Annotations, SpriteAtlasUpdates) { - SpriteStore store; - - SpriteAtlas atlas(32, 32, 1, store); - - EXPECT_EQ(1.0f, atlas.getPixelRatio()); - EXPECT_EQ(32, atlas.getWidth()); - EXPECT_EQ(32, atlas.getHeight()); - EXPECT_EQ(32, atlas.getTextureWidth()); - EXPECT_EQ(32, atlas.getTextureHeight()); - - store.setSprite("one", std::make_shared(16, 12, 1, std::string(16 * 12 * 4, '\x00'))); - auto one = atlas.getImage("one", false); - EXPECT_EQ(0, one.pos.x); - EXPECT_EQ(0, one.pos.y); - EXPECT_EQ(20, one.pos.w); - EXPECT_EQ(16, one.pos.h); - EXPECT_EQ(16, one.pos.originalW); - EXPECT_EQ(12, one.pos.originalH); - EXPECT_EQ(16, one.texture->width); - EXPECT_EQ(12, one.texture->height); - EXPECT_EQ(16, one.texture->pixelWidth); - EXPECT_EQ(12, one.texture->pixelHeight); - EXPECT_EQ(1.0f, one.texture->pixelRatio); - - const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; - const auto hash = test::crc64(reinterpret_cast(atlas.getData()), bytes); - EXPECT_EQ(0x0000000000000000u, hash) << std::hex << hash; - - // Update sprite - auto newSprite = std::make_shared(16, 12, 1, std::string(16 * 12 * 4, '\xFF')); - store.setSprite("one", newSprite); - ASSERT_EQ(newSprite, store.getSprite("one")); - - // Atlas texture hasn't changed yet. - const auto hash2 = test::crc64(reinterpret_cast(atlas.getData()), bytes); - EXPECT_EQ(0x0000000000000000u, hash2) << std::hex << hash2; - - atlas.updateDirty(); - - // Now the atlas texture has changed. - const auto hash3 = test::crc64(reinterpret_cast(atlas.getData()), bytes); - EXPECT_EQ(0x4E6D4900CD2D9149u, hash3) << std::hex << hash3; - - // util::write_file( - // "test/fixtures/annotations/atlas3.png", - // util::compress_png(atlas.getTextureWidth(), atlas.getTextureHeight(), atlas.getData())); -} diff --git a/test/annotations/sprite_image.cpp b/test/annotations/sprite_image.cpp deleted file mode 100644 index 9c5ca79ac9..0000000000 --- a/test/annotations/sprite_image.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "../fixtures/util.hpp" - -#include -#include - -using namespace mbgl; - -TEST(Annotations, SpriteImageZeroWidth) { - try { - SpriteImage(0, 16, 2, ""); - FAIL() << "Expected exception"; - } catch (util::SpriteImageException& ex) { - EXPECT_STREQ("Sprite image dimensions may not be zero", ex.what()); - } -} - -TEST(Annotations, SpriteImageZeroHeight) { - try { - SpriteImage(16, 0, 2, ""); - FAIL() << "Expected exception"; - } catch (util::SpriteImageException& ex) { - EXPECT_STREQ("Sprite image dimensions may not be zero", ex.what()); - } -} - -TEST(Annotations, SpriteImageZeroRatio) { - try { - SpriteImage(16, 16, 0, ""); - FAIL() << "Expected exception"; - } catch (util::SpriteImageException& ex) { - EXPECT_STREQ("Sprite image dimensions may not be zero", ex.what()); - } -} - -TEST(Annotations, SpriteImageMismatchedData) { - try { - SpriteImage(16, 16, 2, ""); - FAIL() << "Expected exception"; - } catch (util::SpriteImageException& ex) { - EXPECT_STREQ("Sprite image pixel count mismatch", ex.what()); - } -} - -TEST(Annotations, SpriteImage) { - std::string pixels(32 * 24 * 4, '\0'); - SpriteImage sprite(16, 12, 2, std::move(pixels)); - EXPECT_EQ(16, sprite.width); - EXPECT_EQ(32, sprite.pixelWidth); - EXPECT_EQ(12, sprite.height); - EXPECT_EQ(24, sprite.pixelHeight); - EXPECT_EQ(2, sprite.pixelRatio); - EXPECT_EQ(32u * 24 * 4, sprite.data.size()); -} - -TEST(Annotations, SpriteImageFractionalRatio) { - std::string pixels(20 * 12 * 4, '\0'); - SpriteImage sprite(13, 8, 1.5, std::move(pixels)); - EXPECT_EQ(13, sprite.width); - EXPECT_EQ(20, sprite.pixelWidth); - EXPECT_EQ(8, sprite.height); - EXPECT_EQ(12, sprite.pixelHeight); - EXPECT_EQ(1.5, sprite.pixelRatio); - EXPECT_EQ(20u * 12 * 4, sprite.data.size()); -} diff --git a/test/annotations/sprite_parser.cpp b/test/annotations/sprite_parser.cpp deleted file mode 100644 index d4c3f96d6a..0000000000 --- a/test/annotations/sprite_parser.cpp +++ /dev/null @@ -1,310 +0,0 @@ -#include "../fixtures/util.hpp" -#include "../fixtures/fixture_log_observer.hpp" - -#include -#include -#include -#include - -#include - -using namespace mbgl; - -TEST(Annotations, SpriteImageCreationInvalid) { - FixtureLog log; - - const util::Image image_1x(util::read_file("test/fixtures/annotations/emerald.png")); - ASSERT_TRUE(image_1x); - ASSERT_EQ(200u, image_1x.getWidth()); - ASSERT_EQ(299u, image_1x.getHeight()); - - ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 0, 16, 1, false)); // width == 0 - ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 16, 0, 1, false)); // height == 0 - ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 1, 1, 0, false)); // ratio == 0 - ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 1, 1, 23, false)); // ratio too large - ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 2048, 16, 1, false)); // too wide - ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 16, 1025, 1, false)); // too tall - - EXPECT_EQ(6u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Can't create sprite with invalid metrics", - })); -} - -TEST(Annotations, SpriteImageCreation1x) { - const util::Image image_1x(util::read_file("test/fixtures/annotations/emerald.png")); - ASSERT_TRUE(image_1x); - ASSERT_EQ(200u, image_1x.getWidth()); - ASSERT_EQ(299u, image_1x.getHeight()); - - { // "museum_icon":{"x":177,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false} - const auto sprite = createSpriteImage(image_1x, 177, 187, 18, 18, 1, false); - ASSERT_TRUE(sprite.get()); - EXPECT_EQ(18, sprite->width); - EXPECT_EQ(18, sprite->height); - EXPECT_EQ(18, sprite->pixelWidth); - EXPECT_EQ(18, sprite->pixelHeight); - EXPECT_EQ(1, sprite->pixelRatio); - EXPECT_EQ(0x7FCC5F263D1FFE16u, test::crc64(sprite->data)); - } - - { // outside image == blank - const auto sprite = createSpriteImage(image_1x, 200, 0, 16, 16, 1, false); - ASSERT_TRUE(sprite.get()); - EXPECT_EQ(16, sprite->width); - EXPECT_EQ(16, sprite->height); - EXPECT_EQ(16, sprite->pixelWidth); - EXPECT_EQ(16, sprite->pixelHeight); - EXPECT_EQ(1, sprite->pixelRatio); - EXPECT_EQ(0x0000000000000000u, test::crc64(sprite->data)) << std::hex << test::crc64(sprite->data); - } - - { // outside image == blank - const auto sprite = createSpriteImage(image_1x, 0, 300, 16, 16, 1, false); - ASSERT_TRUE(sprite.get()); - EXPECT_EQ(16, sprite->width); - EXPECT_EQ(16, sprite->height); - EXPECT_EQ(16, sprite->pixelWidth); - EXPECT_EQ(16, sprite->pixelHeight); - EXPECT_EQ(1, sprite->pixelRatio); - EXPECT_EQ(0x0000000000000000u, test::crc64(sprite->data)) << std::hex << test::crc64(sprite->data); - } -} - -TEST(Annotations, SpriteImageCreation2x) { - const util::Image image_2x(util::read_file("test/fixtures/annotations/emerald@2x.png")); - ASSERT_TRUE(image_2x); - - // "museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false} - const auto sprite = createSpriteImage(image_2x, 354, 374, 36, 36, 2, false); - ASSERT_TRUE(sprite.get()); - EXPECT_EQ(18, sprite->width); - EXPECT_EQ(18, sprite->height); - EXPECT_EQ(36, sprite->pixelWidth); - EXPECT_EQ(36, sprite->pixelHeight); - EXPECT_EQ(2, sprite->pixelRatio); - EXPECT_EQ(0x85F345098DD4F9E3u, test::crc64(sprite->data)); -} - -TEST(Annotations, SpriteImageCreation1_5x) { - const util::Image image_2x(util::read_file("test/fixtures/annotations/emerald@2x.png")); - ASSERT_TRUE(image_2x); - - // "museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false} - const auto sprite = createSpriteImage(image_2x, 354, 374, 36, 36, 1.5, false); - ASSERT_TRUE(sprite.get()); - EXPECT_EQ(24, sprite->width); - EXPECT_EQ(24, sprite->height); - EXPECT_EQ(36, sprite->pixelWidth); - EXPECT_EQ(36, sprite->pixelHeight); - EXPECT_EQ(1.5, sprite->pixelRatio); - EXPECT_EQ(0x85F345098DD4F9E3u, test::crc64(sprite->data)); - - // "hospital_icon":{"x":314,"y":518,"width":36,"height":36,"pixelRatio":2,"sdf":false} - const auto sprite2 = createSpriteImage(image_2x, 314, 518, 35, 35, 1.5, false); - ASSERT_TRUE(sprite2.get()); - EXPECT_EQ(24, sprite2->width); - EXPECT_EQ(24, sprite2->height); - EXPECT_EQ(36, sprite2->pixelWidth); - EXPECT_EQ(36, sprite2->pixelHeight); - EXPECT_EQ(1.5, sprite2->pixelRatio); - EXPECT_EQ(0x134A530C742DD141u, test::crc64(sprite2->data)); -} - -TEST(Annotations, SpriteParsing) { - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = util::read_file("test/fixtures/annotations/emerald.json"); - - const auto images = parseSprite(image_1x, json_1x).get(); - - std::set names; - std::transform(images.begin(), images.end(), std::inserter(names, names.begin()), - [](const auto& pair) { return pair.first; }); - - EXPECT_EQ(std::set({ "airfield_icon", - "airport_icon", - "background", - "cemetery_icon", - "college_icon", - "default_1", - "default_2", - "default_3", - "default_4", - "default_5", - "default_6", - "default_marker", - "dlr", - "dlr.london-overground.london-underground.national-rail", - "dlr.london-underground", - "dlr.london-underground.national-rail", - "dlr.national-rail", - "dot", - "embassy_icon", - "fire-station_icon", - "generic-metro", - "generic-rail", - "generic_icon", - "golf_icon", - "government_icon", - "grass_pattern", - "harbor_icon", - "hospital_icon", - "hospital_striped", - "interstate_1", - "interstate_2", - "interstate_3", - "library_icon", - "london-overground", - "london-overground.london-underground", - "london-overground.london-underground.national-rail", - "london-overground.national-rail", - "london-underground", - "london-underground.national-rail", - "marker_icon", - "metro", - "metro.rer", - "monument_icon", - "moscow-metro", - "museum_icon", - "national-rail", - "oneway_motorway", - "oneway_road", - "park_icon", - "police_icon", - "post_icon", - "prison_icon", - "religious-christian_icon", - "religious-jewish_icon", - "religious-muslim_icon", - "rer", - "rer.transilien", - "s-bahn", - "s-bahn.u-bahn", - "sand_noise", - "school_icon", - "school_striped", - "secondary_marker", - "u-bahn", - "us_highway_1", - "us_highway_2", - "us_highway_3", - "us_state_1", - "us_state_2", - "us_state_3", - "washington-metro", - "wiener-linien", - "zoo_icon" }), - names); - - { - auto sprite = images.find("generic-metro")->second; - EXPECT_EQ(18, sprite->width); - EXPECT_EQ(18, sprite->height); - EXPECT_EQ(18, sprite->pixelWidth); - EXPECT_EQ(18, sprite->pixelHeight); - EXPECT_EQ(1, sprite->pixelRatio); - EXPECT_EQ(0xFF56F5F48F707147u, test::crc64(sprite->data)); - } -} - -TEST(Annotations, SpriteParsingInvalidJSON) { - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = R"JSON({ "image": " })JSON"; - - const auto error = parseSprite(image_1x, json_1x).get(); - - EXPECT_EQ(error, - std::string("Failed to parse JSON: Missing a closing quotation mark in string. at offset 13")); -} - -TEST(Annotations, SpriteParsingEmptyImage) { - FixtureLog log; - - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = R"JSON({ "image": {} })JSON"; - - const auto images = parseSprite(image_1x, json_1x).get(); - EXPECT_EQ(0u, images.size()); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Can't create sprite with invalid metrics", - })); -} - -TEST(Annotations, SpriteParsingSimpleWidthHeight) { - FixtureLog log; - - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = R"JSON({ "image": { "width": 32, "height": 32 } })JSON"; - - const auto images = parseSprite(image_1x, json_1x).get(); - EXPECT_EQ(1u, images.size()); -} - -TEST(Annotations, SpriteParsingWidthTooBig) { - FixtureLog log; - - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = R"JSON({ "image": { "width": 65536, "height": 32 } })JSON"; - - const auto images = parseSprite(image_1x, json_1x).get(); - EXPECT_EQ(0u, images.size()); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Value of 'width' must be an integer between 0 and 65535", - })); - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Can't create sprite with invalid metrics", - })); -} - -TEST(Annotations, SpriteParsingNegativeWidth) { - FixtureLog log; - - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = R"JSON({ "image": { "width": -1, "height": 32 } })JSON"; - - const auto images = parseSprite(image_1x, json_1x).get(); - EXPECT_EQ(0u, images.size()); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Value of 'width' must be an integer between 0 and 65535", - })); - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Can't create sprite with invalid metrics", - })); -} - -TEST(Annotations, SpriteParsingNullRatio) { - FixtureLog log; - - const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); - const auto json_1x = R"JSON({ "image": { "width": 32, "height": 32, "pixelRatio": 0 } })JSON"; - - const auto images = parseSprite(image_1x, json_1x).get(); - EXPECT_EQ(0u, images.size()); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Can't create sprite with invalid metrics", - })); -} diff --git a/test/annotations/sprite_store.cpp b/test/annotations/sprite_store.cpp deleted file mode 100644 index 16eaad2499..0000000000 --- a/test/annotations/sprite_store.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "../fixtures/util.hpp" -#include "../fixtures/fixture_log_observer.hpp" - -#include - -using namespace mbgl; - -TEST(Annotations, SpriteStore) { - FixtureLog log; - - const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - const auto sprite2 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - const auto sprite3 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - - using Sprites = std::map>; - SpriteStore store; - - // Adding single - store.setSprite("one", sprite1); - EXPECT_EQ(Sprites({ - { "one", sprite1 }, - }), - store.getDirty()); - EXPECT_EQ(Sprites(), store.getDirty()); - - // Adding multiple - store.setSprite("two", sprite2); - store.setSprite("three", sprite3); - EXPECT_EQ(Sprites({ - { "two", sprite2 }, { "three", sprite3 }, - }), - store.getDirty()); - EXPECT_EQ(Sprites(), store.getDirty()); - - // Removing - store.removeSprite("one"); - store.removeSprite("two"); - EXPECT_EQ(Sprites({ - { "one", nullptr }, { "two", nullptr }, - }), - store.getDirty()); - EXPECT_EQ(Sprites(), store.getDirty()); - - // Accessing - EXPECT_EQ(sprite3, store.getSprite("three")); - - EXPECT_TRUE(log.empty()); - - EXPECT_EQ(nullptr, store.getSprite("two")); - EXPECT_EQ(nullptr, store.getSprite("four")); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Info, - Event::Sprite, - int64_t(-1), - "Can't find sprite named 'two'", - })); - EXPECT_EQ(1u, log.count({ - EventSeverity::Info, - Event::Sprite, - int64_t(-1), - "Can't find sprite named 'four'", - })); - - // Overwriting - store.setSprite("three", sprite1); - EXPECT_EQ(Sprites({ - { "three", sprite1 }, - }), - store.getDirty()); - EXPECT_EQ(Sprites(), store.getDirty()); -} - -TEST(Annotations, SpriteStoreOtherPixelRatio) { - FixtureLog log; - - const auto sprite1 = std::make_shared(8, 8, 1, std::string(8 * 8 * 4, '\0')); - - using Sprites = std::map>; - SpriteStore store; - - // Adding mismatched sprite image - store.setSprite("one", sprite1); - EXPECT_EQ(Sprites({ { "one", sprite1 } }), store.getDirty()); -} - -TEST(Annotations, SpriteStoreMultiple) { - const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - const auto sprite2 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - - using Sprites = std::map>; - SpriteStore store; - - store.setSprites({ - { "one", sprite1 }, { "two", sprite2 }, - }); - EXPECT_EQ(Sprites({ - { "one", sprite1 }, { "two", sprite2 }, - }), - store.getDirty()); - EXPECT_EQ(Sprites(), store.getDirty()); -} - -TEST(Annotations, SpriteStoreReplace) { - FixtureLog log; - - const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - const auto sprite2 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - - using Sprites = std::map>; - SpriteStore store; - - store.setSprite("sprite", sprite1); - EXPECT_EQ(sprite1, store.getSprite("sprite")); - store.setSprite("sprite", sprite2); - EXPECT_EQ(sprite2, store.getSprite("sprite")); - - EXPECT_EQ(Sprites({ { "sprite", sprite2 } }), store.getDirty()); -} - -TEST(Annotations, SpriteStoreReplaceWithDifferentDimensions) { - FixtureLog log; - - const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); - const auto sprite2 = std::make_shared(9, 9, 2, std::string(18 * 18 * 4, '\0')); - - using Sprites = std::map>; - SpriteStore store; - - store.setSprite("sprite", sprite1); - store.setSprite("sprite", sprite2); - - EXPECT_EQ(1u, log.count({ - EventSeverity::Warning, - Event::Sprite, - int64_t(-1), - "Can't change sprite dimensions for 'sprite'", - })); - - EXPECT_EQ(sprite1, store.getSprite("sprite")); - - EXPECT_EQ(Sprites({ { "sprite", sprite1 } }), store.getDirty()); -} diff --git a/test/miscellaneous/custom_sprites.cpp b/test/miscellaneous/custom_sprites.cpp deleted file mode 100644 index a72119ea61..0000000000 --- a/test/miscellaneous/custom_sprites.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "../fixtures/util.hpp" -#include "../fixtures/fixture_log_observer.hpp" - -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include - -using namespace mbgl; - -TEST(Headless, CustomSpriteImages) { - FixtureLog log; - - auto display = std::make_shared(); - HeadlessView view(display, 1); - view.resize(256, 256); - DefaultFileSource fileSource(nullptr); - - const auto style = util::read_file("test/fixtures/headless/pois.json"); - - Map map(view, fileSource, MapMode::Still); - - map.setLatLngZoom(LatLng{ 52.499167, 13.418056 }, 15); - - map.setStyleJSON(style, ""); - map.setSprite("cafe", - std::make_shared(12, 12, 1, std::string(12 * 12 * 4, '\xFF'))); - std::promise> promise; - map.renderStill([&promise](std::exception_ptr error, std::unique_ptr image) { - if (error) { - promise.set_exception(error); - } else { - promise.set_value(std::move(image)); - } - }); - auto result = promise.get_future().get(); - ASSERT_EQ(256, result->width); - ASSERT_EQ(256, result->height); - - EXPECT_EQ( - 21u, - log.count({ - EventSeverity::Info, Event::Sprite, int64_t(-1), "Can't find sprite named 'bakery'", - })); - - // const size_t bytes = result->width * result->height * 4; - // const auto hash = test::crc64(reinterpret_cast(result->pixels.get()), bytes); - // EXPECT_EQ(0xC40A4BCD76AEC564u, hash) << std::hex << hash; - - // const std::string png = util::compress_png(result->width, result->height, - // result->pixels.get()); - // util::write_file("test/fixtures/headless/1.actual.png", png); -} diff --git a/test/sprite/custom_sprites.cpp b/test/sprite/custom_sprites.cpp new file mode 100644 index 0000000000..98d173ea2d --- /dev/null +++ b/test/sprite/custom_sprites.cpp @@ -0,0 +1,61 @@ +#include "../fixtures/util.hpp" +#include "../fixtures/fixture_log_observer.hpp" + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +using namespace mbgl; + +TEST(Sprite, CustomSpriteImages) { + FixtureLog log; + + auto display = std::make_shared(); + HeadlessView view(display, 1); + view.resize(256, 256); + DefaultFileSource fileSource(nullptr); + + const auto style = util::read_file("test/fixtures/headless/pois.json"); + + Map map(view, fileSource, MapMode::Still); + + map.setLatLngZoom(LatLng{ 52.499167, 13.418056 }, 15); + + map.setStyleJSON(style, ""); + map.setSprite("cafe", + std::make_shared(12, 12, 1, std::string(12 * 12 * 4, '\xFF'))); + std::promise> promise; + map.renderStill([&promise](std::exception_ptr error, std::unique_ptr image) { + if (error) { + promise.set_exception(error); + } else { + promise.set_value(std::move(image)); + } + }); + auto result = promise.get_future().get(); + ASSERT_EQ(256, result->width); + ASSERT_EQ(256, result->height); + + EXPECT_EQ( + 21u, + log.count({ + EventSeverity::Info, Event::Sprite, int64_t(-1), "Can't find sprite named 'bakery'", + })); + + // const size_t bytes = result->width * result->height * 4; + // const auto hash = test::crc64(reinterpret_cast(result->pixels.get()), bytes); + // EXPECT_EQ(0xC40A4BCD76AEC564u, hash) << std::hex << hash; + + // const std::string png = util::compress_png(result->width, result->height, + // result->pixels.get()); + // util::write_file("test/fixtures/headless/1.actual.png", png); +} diff --git a/test/sprite/sprite.cpp b/test/sprite/sprite.cpp new file mode 100644 index 0000000000..7575cb3c9a --- /dev/null +++ b/test/sprite/sprite.cpp @@ -0,0 +1,173 @@ +#include "../fixtures/fixture_log_observer.hpp" +#include "../fixtures/mock_file_source.hpp" +#include "../fixtures/util.hpp" + +#include +#include +#include + +using namespace mbgl; + +using SpriteTestCallback = std::function; + +struct SpriteParams { + const std::string baseUrl; + const float pixelRatio; +}; + +class SpriteThread : public Sprite::Observer { +public: + SpriteThread(FileSource* fileSource, SpriteTestCallback callback) : callback_(callback) { + util::ThreadContext::setFileSource(fileSource); + } + + void loadSprite(const SpriteParams& params) { + sprite_.reset(new Sprite(params.baseUrl, params.pixelRatio)); + sprite_->setObserver(this); + } + + void unloadSprite() { + sprite_->setObserver(nullptr); + sprite_.reset(); + } + + void onSpriteLoaded(const Sprites& sprites) override { + callback_(sprite_.get(), sprites, nullptr); + } + + void onSpriteLoadingFailed(std::exception_ptr error) override { + callback_(sprite_.get(), Sprites(), error); + } + +private: + std::unique_ptr sprite_; + SpriteTestCallback callback_; +}; + +class SpriteTest : public testing::Test { +protected: + void runTest(const SpriteParams& params, FileSource* fileSource, SpriteTestCallback callback) { + util::RunLoop loop(uv_default_loop()); + + async_ = std::make_unique(loop.get(), [&] { loop.stop(); }); + async_->unref(); + + const util::ThreadContext context = {"Map", util::ThreadType::Map, util::ThreadPriority::Regular}; + + util::Thread tester(context, fileSource, callback); + tester.invoke(&SpriteThread::loadSprite, params); + + uv_run(loop.get(), UV_RUN_DEFAULT); + + tester.invoke(&SpriteThread::unloadSprite); + } + + void stopTest() { + async_->send(); + } + +private: + std::unique_ptr async_; +}; + +TEST_F(SpriteTest, LoadingSuccess) { + SpriteParams params = { + "test/fixtures/resources/sprite", + 1.0, + }; + + auto callback = [this, ¶ms](Sprite* sprite, const Sprites& sprites, std::exception_ptr error) { + ASSERT_TRUE(util::ThreadContext::currentlyOn(util::ThreadType::Map)); + + ASSERT_TRUE(error == nullptr); + + ASSERT_TRUE(!sprites.empty()); + + ASSERT_EQ(sprite->pixelRatio, params.pixelRatio); + ASSERT_NE(sprite->pixelRatio, 1.5); + ASSERT_NE(sprite->pixelRatio, 2.0); + + ASSERT_TRUE(sprite->isLoaded()); + + stopTest(); + }; + + MockFileSource fileSource(MockFileSource::Success, ""); + runTest(params, &fileSource, callback); +} + +TEST_F(SpriteTest, LoadingFail) { + SpriteParams params = { + "test/fixtures/resources/sprite", + 1.0, + }; + + auto callback = [this, ¶ms](Sprite* sprite, const Sprites&, std::exception_ptr error) { + ASSERT_TRUE(util::ThreadContext::currentlyOn(util::ThreadType::Map)); + + ASSERT_TRUE(error != nullptr); + + ASSERT_EQ(sprite->pixelRatio, params.pixelRatio); + ASSERT_NE(sprite->pixelRatio, 1.5); + ASSERT_NE(sprite->pixelRatio, 2.0); + + ASSERT_FALSE(sprite->isLoaded()); + + stopTest(); + }; + + MockFileSource fileSourceFailSpriteJSON(MockFileSource::RequestFail, "sprite.json"); + runTest(params, &fileSourceFailSpriteJSON, callback); + + MockFileSource fileSourceFailSpriteImage(MockFileSource::RequestFail, "sprite.png"); + runTest(params, &fileSourceFailSpriteImage, callback); + + MockFileSource fileSourceCorruptedSpriteJSON(MockFileSource::RequestWithCorruptedData, "sprite.json"); + runTest(params, &fileSourceCorruptedSpriteJSON, callback); + + MockFileSource fileSourceCorruptedSpriteImage(MockFileSource::RequestWithCorruptedData, "sprite.png"); + runTest(params, &fileSourceCorruptedSpriteImage, callback); +} + +TEST_F(SpriteTest, LoadingCancel) { + SpriteParams params = { + "test/fixtures/resources/sprite", + 1.0, + }; + + auto callback = [this](Sprite*, const Sprites&, std::exception_ptr) { + FAIL() << "Should never be called"; + }; + + MockFileSource fileSourceDelaySpriteJSON(MockFileSource::SuccessWithDelay, "sprite.json"); + fileSourceDelaySpriteJSON.setOnRequestDelayedCallback([this]{ + stopTest(); + }); + runTest(params, &fileSourceDelaySpriteJSON, callback); + + MockFileSource fileSourceDelaySpriteImage(MockFileSource::SuccessWithDelay, "sprite.png"); + fileSourceDelaySpriteImage.setOnRequestDelayedCallback([this]{ + stopTest(); + }); + runTest(params, &fileSourceDelaySpriteImage, callback); +} + +TEST_F(SpriteTest, InvalidURL) { + SpriteParams params = { + "foo bar", + 1.0, + }; + + auto callback = [this](Sprite* sprite, const Sprites&, std::exception_ptr error) { + ASSERT_TRUE(util::ThreadContext::currentlyOn(util::ThreadType::Map)); + + ASSERT_TRUE(error != nullptr); + + ASSERT_EQ(sprite->isLoaded(), false); + + stopTest(); + }; + + MockFileSource fileSource(MockFileSource::Success, ""); + runTest(params, &fileSource, callback); +} diff --git a/test/sprite/sprite_atlas.cpp b/test/sprite/sprite_atlas.cpp new file mode 100644 index 0000000000..554ef16da9 --- /dev/null +++ b/test/sprite/sprite_atlas.cpp @@ -0,0 +1,173 @@ +#include "../fixtures/util.hpp" +#include "../fixtures/fixture_log_observer.hpp" + +#include +#include +#include +#include +#include + +using namespace mbgl; + +TEST(Sprite, SpriteAtlas) { + FixtureLog log; + + auto spriteParseResult = parseSprite(util::read_file("test/fixtures/annotations/emerald.png"), + util::read_file("test/fixtures/annotations/emerald.json")); + + SpriteStore store; + store.setSprites(spriteParseResult.get()); + + SpriteAtlas atlas(63, 112, 1, store); + + EXPECT_EQ(1.0f, atlas.getPixelRatio()); + EXPECT_EQ(63, atlas.getWidth()); + EXPECT_EQ(112, atlas.getHeight()); + EXPECT_EQ(63, atlas.getTextureWidth()); + EXPECT_EQ(112, atlas.getTextureHeight()); + + // Image hasn't been created yet. + EXPECT_TRUE(atlas.getData()); + + auto metro = atlas.getImage("metro", false); + EXPECT_EQ(0, metro.pos.x); + EXPECT_EQ(0, metro.pos.y); + EXPECT_EQ(20, metro.pos.w); + EXPECT_EQ(20, metro.pos.h); + EXPECT_EQ(18, metro.pos.originalW); + EXPECT_EQ(18, metro.pos.originalH); + EXPECT_EQ(18, metro.texture->width); + EXPECT_EQ(18, metro.texture->height); + EXPECT_EQ(18, metro.texture->pixelWidth); + EXPECT_EQ(18, metro.texture->pixelHeight); + EXPECT_EQ(1.0f, metro.texture->pixelRatio); + + auto pos = atlas.getPosition("metro", false); + EXPECT_DOUBLE_EQ(20, pos.size[0]); + EXPECT_DOUBLE_EQ(20, pos.size[1]); + EXPECT_DOUBLE_EQ(1.0f / 63, pos.tl[0]); + EXPECT_DOUBLE_EQ(1.0f / 112, pos.tl[1]); + EXPECT_DOUBLE_EQ(21.0f / 63, pos.br[0]); + EXPECT_DOUBLE_EQ(21.0f / 112, pos.br[1]); + + + auto missing = atlas.getImage("doesnotexist", false); + EXPECT_FALSE(missing.pos.hasArea()); + EXPECT_EQ(0, missing.pos.x); + EXPECT_EQ(0, missing.pos.y); + EXPECT_EQ(0, missing.pos.w); + EXPECT_EQ(0, missing.pos.h); + EXPECT_EQ(0, missing.pos.originalW); + EXPECT_EQ(0, missing.pos.originalH); + EXPECT_FALSE(missing.texture); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Info, + Event::Sprite, + int64_t(-1), + "Can't find sprite named 'doesnotexist'", + })); + + // Different wrapping mode produces different image. + auto metro2 = atlas.getImage("metro", true); + EXPECT_EQ(20, metro2.pos.x); + EXPECT_EQ(0, metro2.pos.y); + EXPECT_EQ(20, metro2.pos.w); + EXPECT_EQ(20, metro2.pos.h); + EXPECT_EQ(18, metro2.pos.originalW); + EXPECT_EQ(18, metro2.pos.originalH); + + const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; + const auto hash = test::crc64(reinterpret_cast(atlas.getData()), bytes); + EXPECT_EQ(0x9875FC0595489A9Fu, hash) << std::hex << hash; + + // util::write_file( + // "test/fixtures/annotations/atlas1.png", + // util::compress_png(atlas.getTextureWidth(), atlas.getTextureHeight(), atlas.getData())); +} + +TEST(Sprite, SpriteAtlasSize) { + auto spriteParseResult = parseSprite(util::read_file("test/fixtures/annotations/emerald.png"), + util::read_file("test/fixtures/annotations/emerald.json")); + + SpriteStore store; + store.setSprites(spriteParseResult.get()); + + SpriteAtlas atlas(63, 112, 1.4, store); + + EXPECT_DOUBLE_EQ(1.4f, atlas.getPixelRatio()); + EXPECT_EQ(63, atlas.getWidth()); + EXPECT_EQ(112, atlas.getHeight()); + EXPECT_EQ(89, atlas.getTextureWidth()); + EXPECT_EQ(157, atlas.getTextureHeight()); + + auto metro = atlas.getImage("metro", false); + EXPECT_EQ(0, metro.pos.x); + EXPECT_EQ(0, metro.pos.y); + EXPECT_EQ(20, metro.pos.w); + EXPECT_EQ(20, metro.pos.h); + EXPECT_EQ(18, metro.pos.originalW); + EXPECT_EQ(18, metro.pos.originalH); + EXPECT_EQ(18, metro.texture->width); + EXPECT_EQ(18, metro.texture->height); + EXPECT_EQ(18, metro.texture->pixelWidth); + EXPECT_EQ(18, metro.texture->pixelHeight); + EXPECT_EQ(1.0f, metro.texture->pixelRatio); + + const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; + const auto hash = test::crc64(reinterpret_cast(atlas.getData()), bytes); + EXPECT_EQ(0x2CDDA7DBB04D116Du, hash) << std::hex << hash; + + // util::write_file( + // "test/fixtures/annotations/atlas2.png", + // util::compress_png(atlas.getTextureWidth(), atlas.getTextureHeight(), atlas.getData())); +} + +TEST(Sprite, SpriteAtlasUpdates) { + SpriteStore store; + + SpriteAtlas atlas(32, 32, 1, store); + + EXPECT_EQ(1.0f, atlas.getPixelRatio()); + EXPECT_EQ(32, atlas.getWidth()); + EXPECT_EQ(32, atlas.getHeight()); + EXPECT_EQ(32, atlas.getTextureWidth()); + EXPECT_EQ(32, atlas.getTextureHeight()); + + store.setSprite("one", std::make_shared(16, 12, 1, std::string(16 * 12 * 4, '\x00'))); + auto one = atlas.getImage("one", false); + EXPECT_EQ(0, one.pos.x); + EXPECT_EQ(0, one.pos.y); + EXPECT_EQ(20, one.pos.w); + EXPECT_EQ(16, one.pos.h); + EXPECT_EQ(16, one.pos.originalW); + EXPECT_EQ(12, one.pos.originalH); + EXPECT_EQ(16, one.texture->width); + EXPECT_EQ(12, one.texture->height); + EXPECT_EQ(16, one.texture->pixelWidth); + EXPECT_EQ(12, one.texture->pixelHeight); + EXPECT_EQ(1.0f, one.texture->pixelRatio); + + const size_t bytes = atlas.getTextureWidth() * atlas.getTextureHeight() * 4; + const auto hash = test::crc64(reinterpret_cast(atlas.getData()), bytes); + EXPECT_EQ(0x0000000000000000u, hash) << std::hex << hash; + + // Update sprite + auto newSprite = std::make_shared(16, 12, 1, std::string(16 * 12 * 4, '\xFF')); + store.setSprite("one", newSprite); + ASSERT_EQ(newSprite, store.getSprite("one")); + + // Atlas texture hasn't changed yet. + const auto hash2 = test::crc64(reinterpret_cast(atlas.getData()), bytes); + EXPECT_EQ(0x0000000000000000u, hash2) << std::hex << hash2; + + atlas.updateDirty(); + + // Now the atlas texture has changed. + const auto hash3 = test::crc64(reinterpret_cast(atlas.getData()), bytes); + EXPECT_EQ(0x4E6D4900CD2D9149u, hash3) << std::hex << hash3; + + // util::write_file( + // "test/fixtures/annotations/atlas3.png", + // util::compress_png(atlas.getTextureWidth(), atlas.getTextureHeight(), atlas.getData())); +} diff --git a/test/sprite/sprite_image.cpp b/test/sprite/sprite_image.cpp new file mode 100644 index 0000000000..8bc88fcc84 --- /dev/null +++ b/test/sprite/sprite_image.cpp @@ -0,0 +1,64 @@ +#include "../fixtures/util.hpp" + +#include +#include + +using namespace mbgl; + +TEST(Sprite, SpriteImageZeroWidth) { + try { + SpriteImage(0, 16, 2, ""); + FAIL() << "Expected exception"; + } catch (util::SpriteImageException& ex) { + EXPECT_STREQ("Sprite image dimensions may not be zero", ex.what()); + } +} + +TEST(Sprite, SpriteImageZeroHeight) { + try { + SpriteImage(16, 0, 2, ""); + FAIL() << "Expected exception"; + } catch (util::SpriteImageException& ex) { + EXPECT_STREQ("Sprite image dimensions may not be zero", ex.what()); + } +} + +TEST(Sprite, SpriteImageZeroRatio) { + try { + SpriteImage(16, 16, 0, ""); + FAIL() << "Expected exception"; + } catch (util::SpriteImageException& ex) { + EXPECT_STREQ("Sprite image dimensions may not be zero", ex.what()); + } +} + +TEST(Sprite, SpriteImageMismatchedData) { + try { + SpriteImage(16, 16, 2, ""); + FAIL() << "Expected exception"; + } catch (util::SpriteImageException& ex) { + EXPECT_STREQ("Sprite image pixel count mismatch", ex.what()); + } +} + +TEST(Sprite, SpriteImage) { + std::string pixels(32 * 24 * 4, '\0'); + SpriteImage sprite(16, 12, 2, std::move(pixels)); + EXPECT_EQ(16, sprite.width); + EXPECT_EQ(32, sprite.pixelWidth); + EXPECT_EQ(12, sprite.height); + EXPECT_EQ(24, sprite.pixelHeight); + EXPECT_EQ(2, sprite.pixelRatio); + EXPECT_EQ(32u * 24 * 4, sprite.data.size()); +} + +TEST(Sprite, SpriteImageFractionalRatio) { + std::string pixels(20 * 12 * 4, '\0'); + SpriteImage sprite(13, 8, 1.5, std::move(pixels)); + EXPECT_EQ(13, sprite.width); + EXPECT_EQ(20, sprite.pixelWidth); + EXPECT_EQ(8, sprite.height); + EXPECT_EQ(12, sprite.pixelHeight); + EXPECT_EQ(1.5, sprite.pixelRatio); + EXPECT_EQ(20u * 12 * 4, sprite.data.size()); +} diff --git a/test/sprite/sprite_parser.cpp b/test/sprite/sprite_parser.cpp new file mode 100644 index 0000000000..c2e4df3c58 --- /dev/null +++ b/test/sprite/sprite_parser.cpp @@ -0,0 +1,310 @@ +#include "../fixtures/util.hpp" +#include "../fixtures/fixture_log_observer.hpp" + +#include +#include +#include +#include + +#include + +using namespace mbgl; + +TEST(Sprite, SpriteImageCreationInvalid) { + FixtureLog log; + + const util::Image image_1x(util::read_file("test/fixtures/annotations/emerald.png")); + ASSERT_TRUE(image_1x); + ASSERT_EQ(200u, image_1x.getWidth()); + ASSERT_EQ(299u, image_1x.getHeight()); + + ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 0, 16, 1, false)); // width == 0 + ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 16, 0, 1, false)); // height == 0 + ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 1, 1, 0, false)); // ratio == 0 + ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 1, 1, 23, false)); // ratio too large + ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 2048, 16, 1, false)); // too wide + ASSERT_EQ(nullptr, createSpriteImage(image_1x, 0, 0, 16, 1025, 1, false)); // too tall + + EXPECT_EQ(6u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't create sprite with invalid metrics", + })); +} + +TEST(Sprite, SpriteImageCreation1x) { + const util::Image image_1x(util::read_file("test/fixtures/annotations/emerald.png")); + ASSERT_TRUE(image_1x); + ASSERT_EQ(200u, image_1x.getWidth()); + ASSERT_EQ(299u, image_1x.getHeight()); + + { // "museum_icon":{"x":177,"y":187,"width":18,"height":18,"pixelRatio":1,"sdf":false} + const auto sprite = createSpriteImage(image_1x, 177, 187, 18, 18, 1, false); + ASSERT_TRUE(sprite.get()); + EXPECT_EQ(18, sprite->width); + EXPECT_EQ(18, sprite->height); + EXPECT_EQ(18, sprite->pixelWidth); + EXPECT_EQ(18, sprite->pixelHeight); + EXPECT_EQ(1, sprite->pixelRatio); + EXPECT_EQ(0x7FCC5F263D1FFE16u, test::crc64(sprite->data)); + } + + { // outside image == blank + const auto sprite = createSpriteImage(image_1x, 200, 0, 16, 16, 1, false); + ASSERT_TRUE(sprite.get()); + EXPECT_EQ(16, sprite->width); + EXPECT_EQ(16, sprite->height); + EXPECT_EQ(16, sprite->pixelWidth); + EXPECT_EQ(16, sprite->pixelHeight); + EXPECT_EQ(1, sprite->pixelRatio); + EXPECT_EQ(0x0000000000000000u, test::crc64(sprite->data)) << std::hex << test::crc64(sprite->data); + } + + { // outside image == blank + const auto sprite = createSpriteImage(image_1x, 0, 300, 16, 16, 1, false); + ASSERT_TRUE(sprite.get()); + EXPECT_EQ(16, sprite->width); + EXPECT_EQ(16, sprite->height); + EXPECT_EQ(16, sprite->pixelWidth); + EXPECT_EQ(16, sprite->pixelHeight); + EXPECT_EQ(1, sprite->pixelRatio); + EXPECT_EQ(0x0000000000000000u, test::crc64(sprite->data)) << std::hex << test::crc64(sprite->data); + } +} + +TEST(Sprite, SpriteImageCreation2x) { + const util::Image image_2x(util::read_file("test/fixtures/annotations/emerald@2x.png")); + ASSERT_TRUE(image_2x); + + // "museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false} + const auto sprite = createSpriteImage(image_2x, 354, 374, 36, 36, 2, false); + ASSERT_TRUE(sprite.get()); + EXPECT_EQ(18, sprite->width); + EXPECT_EQ(18, sprite->height); + EXPECT_EQ(36, sprite->pixelWidth); + EXPECT_EQ(36, sprite->pixelHeight); + EXPECT_EQ(2, sprite->pixelRatio); + EXPECT_EQ(0x85F345098DD4F9E3u, test::crc64(sprite->data)); +} + +TEST(Sprite, SpriteImageCreation1_5x) { + const util::Image image_2x(util::read_file("test/fixtures/annotations/emerald@2x.png")); + ASSERT_TRUE(image_2x); + + // "museum_icon":{"x":354,"y":374,"width":36,"height":36,"pixelRatio":2,"sdf":false} + const auto sprite = createSpriteImage(image_2x, 354, 374, 36, 36, 1.5, false); + ASSERT_TRUE(sprite.get()); + EXPECT_EQ(24, sprite->width); + EXPECT_EQ(24, sprite->height); + EXPECT_EQ(36, sprite->pixelWidth); + EXPECT_EQ(36, sprite->pixelHeight); + EXPECT_EQ(1.5, sprite->pixelRatio); + EXPECT_EQ(0x85F345098DD4F9E3u, test::crc64(sprite->data)); + + // "hospital_icon":{"x":314,"y":518,"width":36,"height":36,"pixelRatio":2,"sdf":false} + const auto sprite2 = createSpriteImage(image_2x, 314, 518, 35, 35, 1.5, false); + ASSERT_TRUE(sprite2.get()); + EXPECT_EQ(24, sprite2->width); + EXPECT_EQ(24, sprite2->height); + EXPECT_EQ(36, sprite2->pixelWidth); + EXPECT_EQ(36, sprite2->pixelHeight); + EXPECT_EQ(1.5, sprite2->pixelRatio); + EXPECT_EQ(0x134A530C742DD141u, test::crc64(sprite2->data)); +} + +TEST(Sprite, SpriteParsing) { + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = util::read_file("test/fixtures/annotations/emerald.json"); + + const auto images = parseSprite(image_1x, json_1x).get(); + + std::set names; + std::transform(images.begin(), images.end(), std::inserter(names, names.begin()), + [](const auto& pair) { return pair.first; }); + + EXPECT_EQ(std::set({ "airfield_icon", + "airport_icon", + "background", + "cemetery_icon", + "college_icon", + "default_1", + "default_2", + "default_3", + "default_4", + "default_5", + "default_6", + "default_marker", + "dlr", + "dlr.london-overground.london-underground.national-rail", + "dlr.london-underground", + "dlr.london-underground.national-rail", + "dlr.national-rail", + "dot", + "embassy_icon", + "fire-station_icon", + "generic-metro", + "generic-rail", + "generic_icon", + "golf_icon", + "government_icon", + "grass_pattern", + "harbor_icon", + "hospital_icon", + "hospital_striped", + "interstate_1", + "interstate_2", + "interstate_3", + "library_icon", + "london-overground", + "london-overground.london-underground", + "london-overground.london-underground.national-rail", + "london-overground.national-rail", + "london-underground", + "london-underground.national-rail", + "marker_icon", + "metro", + "metro.rer", + "monument_icon", + "moscow-metro", + "museum_icon", + "national-rail", + "oneway_motorway", + "oneway_road", + "park_icon", + "police_icon", + "post_icon", + "prison_icon", + "religious-christian_icon", + "religious-jewish_icon", + "religious-muslim_icon", + "rer", + "rer.transilien", + "s-bahn", + "s-bahn.u-bahn", + "sand_noise", + "school_icon", + "school_striped", + "secondary_marker", + "u-bahn", + "us_highway_1", + "us_highway_2", + "us_highway_3", + "us_state_1", + "us_state_2", + "us_state_3", + "washington-metro", + "wiener-linien", + "zoo_icon" }), + names); + + { + auto sprite = images.find("generic-metro")->second; + EXPECT_EQ(18, sprite->width); + EXPECT_EQ(18, sprite->height); + EXPECT_EQ(18, sprite->pixelWidth); + EXPECT_EQ(18, sprite->pixelHeight); + EXPECT_EQ(1, sprite->pixelRatio); + EXPECT_EQ(0xFF56F5F48F707147u, test::crc64(sprite->data)); + } +} + +TEST(Sprite, SpriteParsingInvalidJSON) { + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = R"JSON({ "image": " })JSON"; + + const auto error = parseSprite(image_1x, json_1x).get(); + + EXPECT_EQ(error, + std::string("Failed to parse JSON: Missing a closing quotation mark in string. at offset 13")); +} + +TEST(Sprite, SpriteParsingEmptyImage) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = R"JSON({ "image": {} })JSON"; + + const auto images = parseSprite(image_1x, json_1x).get(); + EXPECT_EQ(0u, images.size()); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't create sprite with invalid metrics", + })); +} + +TEST(Sprite, SpriteParsingSimpleWidthHeight) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = R"JSON({ "image": { "width": 32, "height": 32 } })JSON"; + + const auto images = parseSprite(image_1x, json_1x).get(); + EXPECT_EQ(1u, images.size()); +} + +TEST(Sprite, SpriteParsingWidthTooBig) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = R"JSON({ "image": { "width": 65536, "height": 32 } })JSON"; + + const auto images = parseSprite(image_1x, json_1x).get(); + EXPECT_EQ(0u, images.size()); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Value of 'width' must be an integer between 0 and 65535", + })); + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't create sprite with invalid metrics", + })); +} + +TEST(Sprite, SpriteParsingNegativeWidth) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = R"JSON({ "image": { "width": -1, "height": 32 } })JSON"; + + const auto images = parseSprite(image_1x, json_1x).get(); + EXPECT_EQ(0u, images.size()); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Value of 'width' must be an integer between 0 and 65535", + })); + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't create sprite with invalid metrics", + })); +} + +TEST(Sprite, SpriteParsingNullRatio) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + const auto json_1x = R"JSON({ "image": { "width": 32, "height": 32, "pixelRatio": 0 } })JSON"; + + const auto images = parseSprite(image_1x, json_1x).get(); + EXPECT_EQ(0u, images.size()); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't create sprite with invalid metrics", + })); +} diff --git a/test/sprite/sprite_store.cpp b/test/sprite/sprite_store.cpp new file mode 100644 index 0000000000..17fce62a2b --- /dev/null +++ b/test/sprite/sprite_store.cpp @@ -0,0 +1,143 @@ +#include "../fixtures/util.hpp" +#include "../fixtures/fixture_log_observer.hpp" + +#include + +using namespace mbgl; + +TEST(Sprite, SpriteStore) { + FixtureLog log; + + const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + const auto sprite2 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + const auto sprite3 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + + using Sprites = std::map>; + SpriteStore store; + + // Adding single + store.setSprite("one", sprite1); + EXPECT_EQ(Sprites({ + { "one", sprite1 }, + }), + store.getDirty()); + EXPECT_EQ(Sprites(), store.getDirty()); + + // Adding multiple + store.setSprite("two", sprite2); + store.setSprite("three", sprite3); + EXPECT_EQ(Sprites({ + { "two", sprite2 }, { "three", sprite3 }, + }), + store.getDirty()); + EXPECT_EQ(Sprites(), store.getDirty()); + + // Removing + store.removeSprite("one"); + store.removeSprite("two"); + EXPECT_EQ(Sprites({ + { "one", nullptr }, { "two", nullptr }, + }), + store.getDirty()); + EXPECT_EQ(Sprites(), store.getDirty()); + + // Accessing + EXPECT_EQ(sprite3, store.getSprite("three")); + + EXPECT_TRUE(log.empty()); + + EXPECT_EQ(nullptr, store.getSprite("two")); + EXPECT_EQ(nullptr, store.getSprite("four")); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Info, + Event::Sprite, + int64_t(-1), + "Can't find sprite named 'two'", + })); + EXPECT_EQ(1u, log.count({ + EventSeverity::Info, + Event::Sprite, + int64_t(-1), + "Can't find sprite named 'four'", + })); + + // Overwriting + store.setSprite("three", sprite1); + EXPECT_EQ(Sprites({ + { "three", sprite1 }, + }), + store.getDirty()); + EXPECT_EQ(Sprites(), store.getDirty()); +} + +TEST(Sprite, SpriteStoreOtherPixelRatio) { + FixtureLog log; + + const auto sprite1 = std::make_shared(8, 8, 1, std::string(8 * 8 * 4, '\0')); + + using Sprites = std::map>; + SpriteStore store; + + // Adding mismatched sprite image + store.setSprite("one", sprite1); + EXPECT_EQ(Sprites({ { "one", sprite1 } }), store.getDirty()); +} + +TEST(Sprite, SpriteStoreMultiple) { + const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + const auto sprite2 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + + using Sprites = std::map>; + SpriteStore store; + + store.setSprites({ + { "one", sprite1 }, { "two", sprite2 }, + }); + EXPECT_EQ(Sprites({ + { "one", sprite1 }, { "two", sprite2 }, + }), + store.getDirty()); + EXPECT_EQ(Sprites(), store.getDirty()); +} + +TEST(Sprite, SpriteStoreReplace) { + FixtureLog log; + + const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + const auto sprite2 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + + using Sprites = std::map>; + SpriteStore store; + + store.setSprite("sprite", sprite1); + EXPECT_EQ(sprite1, store.getSprite("sprite")); + store.setSprite("sprite", sprite2); + EXPECT_EQ(sprite2, store.getSprite("sprite")); + + EXPECT_EQ(Sprites({ { "sprite", sprite2 } }), store.getDirty()); +} + +TEST(Sprite, SpriteStoreReplaceWithDifferentDimensions) { + FixtureLog log; + + const auto sprite1 = std::make_shared(8, 8, 2, std::string(16 * 16 * 4, '\0')); + const auto sprite2 = std::make_shared(9, 9, 2, std::string(18 * 18 * 4, '\0')); + + using Sprites = std::map>; + SpriteStore store; + + store.setSprite("sprite", sprite1); + store.setSprite("sprite", sprite2); + + EXPECT_EQ(1u, log.count({ + EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Can't change sprite dimensions for 'sprite'", + })); + + EXPECT_EQ(sprite1, store.getSprite("sprite")); + + EXPECT_EQ(Sprites({ { "sprite", sprite1 } }), store.getDirty()); +} diff --git a/test/style/sprite.cpp b/test/style/sprite.cpp deleted file mode 100644 index 1c3f9b2271..0000000000 --- a/test/style/sprite.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "../fixtures/fixture_log_observer.hpp" -#include "../fixtures/mock_file_source.hpp" -#include "../fixtures/util.hpp" - -#include -#include -#include - -using namespace mbgl; - -using SpriteTestCallback = std::function; - -struct SpriteParams { - const std::string baseUrl; - const float pixelRatio; -}; - -class SpriteThread : public Sprite::Observer { -public: - SpriteThread(FileSource* fileSource, SpriteTestCallback callback) : callback_(callback) { - util::ThreadContext::setFileSource(fileSource); - } - - void loadSprite(const SpriteParams& params) { - sprite_.reset(new Sprite(params.baseUrl, params.pixelRatio)); - sprite_->setObserver(this); - } - - void unloadSprite() { - sprite_->setObserver(nullptr); - sprite_.reset(); - } - - void onSpriteLoaded(const Sprites& sprites) override { - callback_(sprite_.get(), sprites, nullptr); - } - - void onSpriteLoadingFailed(std::exception_ptr error) override { - callback_(sprite_.get(), Sprites(), error); - } - -private: - std::unique_ptr sprite_; - SpriteTestCallback callback_; -}; - -class SpriteTest : public testing::Test { -protected: - void runTest(const SpriteParams& params, FileSource* fileSource, SpriteTestCallback callback) { - util::RunLoop loop(uv_default_loop()); - - async_ = std::make_unique(loop.get(), [&] { loop.stop(); }); - async_->unref(); - - const util::ThreadContext context = {"Map", util::ThreadType::Map, util::ThreadPriority::Regular}; - - util::Thread tester(context, fileSource, callback); - tester.invoke(&SpriteThread::loadSprite, params); - - uv_run(loop.get(), UV_RUN_DEFAULT); - - tester.invoke(&SpriteThread::unloadSprite); - } - - void stopTest() { - async_->send(); - } - -private: - std::unique_ptr async_; -}; - -TEST_F(SpriteTest, LoadingSuccess) { - SpriteParams params = { - "test/fixtures/resources/sprite", - 1.0, - }; - - auto callback = [this, ¶ms](Sprite* sprite, const Sprites& sprites, std::exception_ptr error) { - ASSERT_TRUE(util::ThreadContext::currentlyOn(util::ThreadType::Map)); - - ASSERT_TRUE(error == nullptr); - - ASSERT_TRUE(!sprites.empty()); - - ASSERT_EQ(sprite->pixelRatio, params.pixelRatio); - ASSERT_NE(sprite->pixelRatio, 1.5); - ASSERT_NE(sprite->pixelRatio, 2.0); - - ASSERT_TRUE(sprite->isLoaded()); - - stopTest(); - }; - - MockFileSource fileSource(MockFileSource::Success, ""); - runTest(params, &fileSource, callback); -} - -TEST_F(SpriteTest, LoadingFail) { - SpriteParams params = { - "test/fixtures/resources/sprite", - 1.0, - }; - - auto callback = [this, ¶ms](Sprite* sprite, const Sprites&, std::exception_ptr error) { - ASSERT_TRUE(util::ThreadContext::currentlyOn(util::ThreadType::Map)); - - ASSERT_TRUE(error != nullptr); - - ASSERT_EQ(sprite->pixelRatio, params.pixelRatio); - ASSERT_NE(sprite->pixelRatio, 1.5); - ASSERT_NE(sprite->pixelRatio, 2.0); - - ASSERT_FALSE(sprite->isLoaded()); - - stopTest(); - }; - - MockFileSource fileSourceFailSpriteJSON(MockFileSource::RequestFail, "sprite.json"); - runTest(params, &fileSourceFailSpriteJSON, callback); - - MockFileSource fileSourceFailSpriteImage(MockFileSource::RequestFail, "sprite.png"); - runTest(params, &fileSourceFailSpriteImage, callback); - - MockFileSource fileSourceCorruptedSpriteJSON(MockFileSource::RequestWithCorruptedData, "sprite.json"); - runTest(params, &fileSourceCorruptedSpriteJSON, callback); - - MockFileSource fileSourceCorruptedSpriteImage(MockFileSource::RequestWithCorruptedData, "sprite.png"); - runTest(params, &fileSourceCorruptedSpriteImage, callback); -} - -TEST_F(SpriteTest, LoadingCancel) { - SpriteParams params = { - "test/fixtures/resources/sprite", - 1.0, - }; - - auto callback = [this](Sprite*, const Sprites&, std::exception_ptr) { - FAIL() << "Should never be called"; - }; - - MockFileSource fileSourceDelaySpriteJSON(MockFileSource::SuccessWithDelay, "sprite.json"); - fileSourceDelaySpriteJSON.setOnRequestDelayedCallback([this]{ - stopTest(); - }); - runTest(params, &fileSourceDelaySpriteJSON, callback); - - MockFileSource fileSourceDelaySpriteImage(MockFileSource::SuccessWithDelay, "sprite.png"); - fileSourceDelaySpriteImage.setOnRequestDelayedCallback([this]{ - stopTest(); - }); - runTest(params, &fileSourceDelaySpriteImage, callback); -} - -TEST_F(SpriteTest, InvalidURL) { - SpriteParams params = { - "foo bar", - 1.0, - }; - - auto callback = [this](Sprite* sprite, const Sprites&, std::exception_ptr error) { - ASSERT_TRUE(util::ThreadContext::currentlyOn(util::ThreadType::Map)); - - ASSERT_TRUE(error != nullptr); - - ASSERT_EQ(sprite->isLoaded(), false); - - stopTest(); - }; - - MockFileSource fileSource(MockFileSource::Success, ""); - runTest(params, &fileSource, callback); -} diff --git a/test/test.gypi b/test/test.gypi index 7c0b0d6c23..e9678b0754 100644 --- a/test/test.gypi +++ b/test/test.gypi @@ -39,11 +39,6 @@ 'miscellaneous/assert.cpp', - 'annotations/sprite_atlas.cpp', - 'annotations/sprite_image.cpp', - 'annotations/sprite_store.cpp', - 'annotations/sprite_parser.cpp', - 'api/annotations.cpp', 'api/api_misuse.cpp', 'api/repeated_render.cpp', @@ -54,7 +49,6 @@ 'miscellaneous/binpack.cpp', 'miscellaneous/bilinear.cpp', 'miscellaneous/comparisons.cpp', - 'miscellaneous/custom_sprites.cpp', 'miscellaneous/enums.cpp', 'miscellaneous/functions.cpp', 'miscellaneous/geo.cpp', @@ -91,7 +85,13 @@ 'style/glyph_store.cpp', 'style/pending_resources.cpp', 'style/resource_loading.cpp', - 'style/sprite.cpp', + + 'sprite/sprite.cpp', + 'sprite/sprite_atlas.cpp', + 'sprite/sprite_image.cpp', + 'sprite/sprite_parser.cpp', + 'sprite/sprite_store.cpp', + 'sprite/custom_sprites.cpp', ], 'libraries': [ '<@(gtest_static_libs)', -- cgit v1.2.1