summaryrefslogtreecommitdiff
path: root/test/sprite
diff options
context:
space:
mode:
authorJohn Firebaugh <john.firebaugh@gmail.com>2015-11-05 15:05:43 -0800
committerJohn Firebaugh <john.firebaugh@gmail.com>2015-11-12 13:03:14 -0800
commit1e350b7ea485117cadc413d4d41062cf3c3c43a1 (patch)
tree8dde92582eda347a7c88c0e655270cef82e44744 /test/sprite
parent0dc1519a1891dac6272f69dd1d1768f15908003c (diff)
downloadqtlocation-mapboxgl-1e350b7ea485117cadc413d4d41062cf3c3c43a1.tar.gz
[core] Reorganize sprite related files
Diffstat (limited to 'test/sprite')
-rw-r--r--test/sprite/custom_sprites.cpp61
-rw-r--r--test/sprite/sprite.cpp173
-rw-r--r--test/sprite/sprite_atlas.cpp173
-rw-r--r--test/sprite/sprite_image.cpp64
-rw-r--r--test/sprite/sprite_parser.cpp310
-rw-r--r--test/sprite/sprite_store.cpp143
6 files changed, 924 insertions, 0 deletions
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 <mbgl/map/map.hpp>
+#include <mbgl/map/still_image.hpp>
+#include <mbgl/util/image.hpp>
+#include <mbgl/sprite/sprite_image.hpp>
+
+#include <mbgl/util/io.hpp>
+
+#include <mbgl/platform/default/headless_view.hpp>
+#include <mbgl/platform/default/headless_display.hpp>
+#include <mbgl/storage/default_file_source.hpp>
+
+#include <future>
+
+using namespace mbgl;
+
+TEST(Sprite, CustomSpriteImages) {
+ FixtureLog log;
+
+ auto display = std::make_shared<mbgl::HeadlessDisplay>();
+ 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<SpriteImage>(12, 12, 1, std::string(12 * 12 * 4, '\xFF')));
+ std::promise<std::unique_ptr<const StillImage>> promise;
+ map.renderStill([&promise](std::exception_ptr error, std::unique_ptr<const StillImage> 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<const char*>(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 <mbgl/sprite/sprite.hpp>
+#include <mbgl/util/run_loop.hpp>
+#include <mbgl/util/thread.hpp>
+
+using namespace mbgl;
+
+using SpriteTestCallback = std::function<void(Sprite*, const Sprites&, std::exception_ptr)>;
+
+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> 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<uv::async>(loop.get(), [&] { loop.stop(); });
+ async_->unref();
+
+ const util::ThreadContext context = {"Map", util::ThreadType::Map, util::ThreadPriority::Regular};
+
+ util::Thread<SpriteThread> 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<uv::async> async_;
+};
+
+TEST_F(SpriteTest, LoadingSuccess) {
+ SpriteParams params = {
+ "test/fixtures/resources/sprite",
+ 1.0,
+ };
+
+ auto callback = [this, &params](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, &params](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 <mbgl/sprite/sprite_atlas.hpp>
+#include <mbgl/sprite/sprite_store.hpp>
+#include <mbgl/sprite/sprite_parser.hpp>
+#include <mbgl/util/io.hpp>
+#include <mbgl/util/image.hpp>
+
+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<Sprites>());
+
+ 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<const char*>(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<Sprites>());
+
+ 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<const char*>(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<SpriteImage>(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<const char*>(atlas.getData()), bytes);
+ EXPECT_EQ(0x0000000000000000u, hash) << std::hex << hash;
+
+ // Update sprite
+ auto newSprite = std::make_shared<SpriteImage>(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<const char*>(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<const char*>(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 <mbgl/sprite/sprite_image.hpp>
+#include <mbgl/util/exception.hpp>
+
+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 <mbgl/sprite/sprite_parser.hpp>
+#include <mbgl/sprite/sprite_image.hpp>
+#include <mbgl/util/image.hpp>
+#include <mbgl/util/io.hpp>
+
+#include <algorithm>
+
+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<Sprites>();
+
+ std::set<std::string> names;
+ std::transform(images.begin(), images.end(), std::inserter(names, names.begin()),
+ [](const auto& pair) { return pair.first; });
+
+ EXPECT_EQ(std::set<std::string>({ "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<std::string>();
+
+ 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<Sprites>();
+ 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<Sprites>();
+ 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<Sprites>();
+ 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<Sprites>();
+ 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<Sprites>();
+ 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 <mbgl/sprite/sprite_store.hpp>
+
+using namespace mbgl;
+
+TEST(Sprite, SpriteStore) {
+ FixtureLog log;
+
+ const auto sprite1 = std::make_shared<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+ const auto sprite2 = std::make_shared<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+ const auto sprite3 = std::make_shared<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+
+ using Sprites = std::map<std::string, std::shared_ptr<const SpriteImage>>;
+ 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<SpriteImage>(8, 8, 1, std::string(8 * 8 * 4, '\0'));
+
+ using Sprites = std::map<std::string, std::shared_ptr<const SpriteImage>>;
+ 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<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+ const auto sprite2 = std::make_shared<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+
+ using Sprites = std::map<std::string, std::shared_ptr<const SpriteImage>>;
+ 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<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+ const auto sprite2 = std::make_shared<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+
+ using Sprites = std::map<std::string, std::shared_ptr<const SpriteImage>>;
+ 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<SpriteImage>(8, 8, 2, std::string(16 * 16 * 4, '\0'));
+ const auto sprite2 = std::make_shared<SpriteImage>(9, 9, 2, std::string(18 * 18 * 4, '\0'));
+
+ using Sprites = std::map<std::string, std::shared_ptr<const SpriteImage>>;
+ 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());
+}