diff --git a/bin/style.js b/bin/style.js index c89ff81e8..47cdcc1f9 100644 --- a/bin/style.js +++ b/bin/style.js @@ -2,6 +2,10 @@ module.exports = { "buckets": { + "satellite": { + "source": "satellite", + "type": "raster" + }, "admin_maritime": { "source": "mapbox streets", "layer": "admin", @@ -590,6 +594,9 @@ module.exports = { "structure": [{ "name": "background", "bucket": "background" + }, { + "name": "satellite", + "bucket": "satellite" }, { "name": "waterway_other", "bucket": "waterway_other" @@ -2054,7 +2061,11 @@ module.exports = { "color": "maki", "image": "beer-12", "translate": "point_translate", - } + }, + "satellite": { + "type": "raster", + "opacity": 0.25 + }, } }], "sprite": "/img/maki-sprite" diff --git a/include/llmr/map/map.hpp b/include/llmr/map/map.hpp index 72e955a93..a9bdf53e5 100644 --- a/include/llmr/map/map.hpp +++ b/include/llmr/map/map.hpp @@ -94,6 +94,7 @@ public: inline GlyphAtlas &getGlyphAtlas() { return glyphAtlas; } inline uv_loop_t *getLoop() { return loop; } inline time getAnimationTime() const { return animationTime; } + inline Texturepool &getTexturepool() { return texturepool; } private: // uv async callbacks @@ -106,6 +107,7 @@ private: void loadStyle(const uint8_t *const data, uint32_t bytes); void updateTiles(); + void updateClippingIDs(); // Prepares a map render by updating the tiles we need for the current view, as well as updating // the stylesheet. diff --git a/include/llmr/map/raster_tile_data.hpp b/include/llmr/map/raster_tile_data.hpp new file mode 100644 index 000000000..33243334b --- /dev/null +++ b/include/llmr/map/raster_tile_data.hpp @@ -0,0 +1,27 @@ +#ifndef LLMR_MAP_RASTER_TILE_DATA +#define LLMR_MAP_RASTER_TILE_DATA + +#include + +#include + + +namespace llmr { + +class RasterTileData : public TileData { + friend class TileParser; + +public: + RasterTileData(Tile::ID id, Map &map, const std::string url); + ~RasterTileData(); + + virtual void parse(); + virtual void render(Painter &painter, const LayerDescription& layer_desc); + +protected: + RasterBucket bucket; +}; + +} + +#endif diff --git a/include/llmr/map/source.hpp b/include/llmr/map/source.hpp index 14770d9d2..4da8e6149 100644 --- a/include/llmr/map/source.hpp +++ b/include/llmr/map/source.hpp @@ -6,10 +6,12 @@ #include #include +#include #include #include #include #include +#include namespace llmr { @@ -26,17 +28,18 @@ public: }; public: - Source(Map &map, Painter &painter, Texturepool &texturepool, + Source(Map &map, Painter &painter, const char *url = "", Type type = Type::vector, std::vector zooms = {0}, uint32_t tile_size = 512, uint32_t min_zoom = 0, uint32_t max_zoom = 14, bool enabled = true); - bool update(); - void prepare_render(const TransformState &transform, bool is_baselayer = false); - void render(time animationTime); + size_t prepareRender(const TransformState &transform); + void render(); + std::forward_list getIDs() const; + void updateClipIDs(const std::map &mapping); public: bool enabled; @@ -56,7 +59,6 @@ private: private: Map& map; Painter& painter; - Texturepool& texturepool; const Type type; const std::vector zooms; diff --git a/include/llmr/map/tile.hpp b/include/llmr/map/tile.hpp index 0857cf17b..76a01e04e 100644 --- a/include/llmr/map/tile.hpp +++ b/include/llmr/map/tile.hpp @@ -7,45 +7,64 @@ #include #include +#include +#include namespace llmr { class TileData; +struct ClipID { + explicit ClipID() {} + explicit ClipID(const std::bitset<8> &mask, uint8_t length) : mask(mask), length(length) {} + explicit ClipID(const std::string &mask, uint8_t length) : mask(mask), length(length) {} + std::bitset<8> mask; + uint8_t length = 0; +}; + class Tile : private util::noncopyable { public: struct ID { - int8_t z = 0; - int32_t x = 0, y = 0; + const int16_t w = 0; + const int8_t z = 0; + const int32_t x = 0, y = 0; - inline ID(int8_t z, int32_t x, int32_t y) - : z(z), x(x), y(y) { + inline explicit ID(int8_t z, int32_t x, int32_t y) + : w((x < 0 ? x - (1 << z) + 1 : x) / (1 << z)), z(z), x(x), y(y) {} + + inline uint64_t to_uint64() const { + return ((pow(2, z) * y + x) * 32) + z; } - inline operator uint64_t() const { - uint32_t dim = 1 << z; - return ((dim * y + x) * 32) + z; + inline operator std::string() const { + return std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y); } inline bool operator==(const ID& rhs) const { - return z == rhs.z && x == rhs.x && y == rhs.y; + return w == rhs.w && z == rhs.z && x == rhs.x && y == rhs.y; + } + + inline bool operator<(const ID &rhs) const { + if (w != rhs.w) return w < rhs.w; + if (z != rhs.z) return z < rhs.z; + if (x != rhs.x) return x < rhs.x; + return y < rhs.y; } ID parent(int8_t z) const; - void normalize(); ID normalized() const; std::forward_list children(int32_t z) const; + bool isChildOf(const Tile::ID &id) const; }; public: - Tile(const ID& id); + explicit Tile(const ID& id); public: const Tile::ID id; - uint8_t clip_id; + ClipID clip; mat4 matrix; std::shared_ptr data; - }; } diff --git a/include/llmr/map/tile_data.hpp b/include/llmr/map/tile_data.hpp index fb9eceedf..dde48e9c4 100644 --- a/include/llmr/map/tile_data.hpp +++ b/include/llmr/map/tile_data.hpp @@ -2,39 +2,21 @@ #define LLMR_MAP_TILE_DATA #include -#include -#include #include -#include -#include -#include #include - -#include - -#include -#include -#include -#include -#include -#include +#include +#include #include -#include #include -#include -#include #include #include namespace llmr { class Map; -class Raster; +class Painter; class LayerDescription; -class BucketDescription; - -class PlainShader; class TileData : public std::enable_shared_from_this, private util::noncopyable { @@ -55,47 +37,36 @@ public: }; public: - TileData(Tile::ID id, Map &map, const std::string url, const bool is_raster); + TileData(Tile::ID id, Map &map, const std::string url); ~TileData(); void request(); - void scheduleParsing(); - void parse(); void cancel(); - + void reparse(); const std::string toString() const; + // Override this in the child class. + virtual void parse() = 0; + virtual void render(Painter &painter, const LayerDescription& layer_desc) = 0; + + public: const Tile::ID id; std::atomic state; - std::shared_ptr raster; - - // Holds the actual geometries in this tile. - DebugFontBuffer debugFontBuffer; - VertexArrayObject debugFontArray; - - FillVertexBuffer fillVertexBuffer; - LineVertexBuffer lineVertexBuffer; - PointVertexBuffer pointVertexBuffer; - TextVertexBuffer textVertexBuffer; - - TriangleElementsBuffer triangleElementsBuffer; - LineElementsBuffer lineElementsBuffer; - PointElementsBuffer pointElementsBuffer; - - // Holds the buckets of this tile. - // They contain the location offsets in the buffers stored above - std::map> buckets; - -private: - const std::string url; - const bool is_raster = false; - std::string data; +protected: Map ↦ - // Stores a request that is in progress. + // Request-related information. + const std::string url; std::weak_ptr req; + std::string data; + + // Contains the tile ID string for painting debug information. + DebugFontBuffer debugFontBuffer; + +public: + DebugBucket debugBucket; }; } diff --git a/include/llmr/map/tile_parser.hpp b/include/llmr/map/tile_parser.hpp index 013d650de..269ab3ba9 100644 --- a/include/llmr/map/tile_parser.hpp +++ b/include/llmr/map/tile_parser.hpp @@ -1,7 +1,8 @@ #ifndef LLMR_MAP_TILE_PARSER #define LLMR_MAP_TILE_PARSER -#include +#include +#include #include namespace llmr { @@ -9,9 +10,12 @@ namespace llmr { class Style; class GlyphAtlas; +class LayerDescription; +class Bucket; + class TileParser { public: - TileParser(const std::string& data, TileData& tile, const Style& style, GlyphAtlas& glyphAtlas, bool is_raster = false); + TileParser(const std::string& data, VectorTileData& tile, const Style& style, GlyphAtlas& glyphAtlas); private: bool obsolete() const; @@ -22,13 +26,11 @@ private: std::unique_ptr createLineBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc); std::unique_ptr createPointBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc); std::unique_ptr createTextBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc); - std::unique_ptr createRasterBucket(const BucketDescription& bucket_desc); template void addBucketFeatures(Bucket& bucket, const VectorTileLayer& layer, const BucketDescription& bucket_desc); private: const VectorTile vector_data; - const std::string raster_data; - TileData& tile; + VectorTileData& tile; const Style& style; GlyphAtlas& glyphAtlas; Faces faces; diff --git a/include/llmr/map/vector_tile_data.hpp b/include/llmr/map/vector_tile_data.hpp new file mode 100644 index 000000000..6deae7bd7 --- /dev/null +++ b/include/llmr/map/vector_tile_data.hpp @@ -0,0 +1,50 @@ +#ifndef LLMR_MAP_VECTOR_TILE_DATA +#define LLMR_MAP_VECTOR_TILE_DATA + +#include + + +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace llmr { + + +class VectorTileData : public TileData { + friend class TileParser; + +public: + VectorTileData(Tile::ID id, Map &map, const std::string url); + ~VectorTileData(); + + virtual void parse(); + virtual void render(Painter &painter, const LayerDescription& layer_desc); + +protected: + // Holds the actual geometries in this tile. + FillVertexBuffer fillVertexBuffer; + LineVertexBuffer lineVertexBuffer; + PointVertexBuffer pointVertexBuffer; + TextVertexBuffer textVertexBuffer; + + TriangleElementsBuffer triangleElementsBuffer; + LineElementsBuffer lineElementsBuffer; + PointElementsBuffer pointElementsBuffer; + + // Holds the buckets of this tile. + // They contain the location offsets in the buffers stored above + std::map> buckets; + +}; + +} + +#endif diff --git a/include/llmr/renderer/debug_bucket.hpp b/include/llmr/renderer/debug_bucket.hpp new file mode 100644 index 000000000..f1a7d151f --- /dev/null +++ b/include/llmr/renderer/debug_bucket.hpp @@ -0,0 +1,36 @@ +#ifndef LLMR_RENDERER_DEBUGBUCKET +#define LLMR_RENDERER_DEBUGBUCKET + +#include +#include +#include +#include + +#include +#include + +#ifndef BUFFER_OFFSET +#define BUFFER_OFFSET(i) ((char *)nullptr + (i)) +#endif + +namespace llmr { + +class PlainShader; + +class DebugBucket : public Bucket { +public: + DebugBucket(DebugFontBuffer& fontBuffer); + + virtual void render(Painter& painter, const std::string& layer_name, const Tile::ID& id); + + void drawLines(PlainShader& shader); + void drawPoints(PlainShader& shader); + +private: + DebugFontBuffer& fontBuffer; + VertexArrayObject array; +}; + +} + +#endif diff --git a/include/llmr/renderer/painter.hpp b/include/llmr/renderer/painter.hpp index 03d894597..a5455ba62 100644 --- a/include/llmr/renderer/painter.hpp +++ b/include/llmr/renderer/painter.hpp @@ -33,6 +33,9 @@ class PointBucket; class TextBucket; class RasterBucket; +class LayerDescription; +class RasterTileData; + class Painter : private util::noncopyable { public: Painter(Map &map); @@ -43,11 +46,15 @@ public: void changeMatrix(); void render(const Tile& tile); void renderMatte(); + + void renderDebugFrame(); + void renderDebugText(DebugBucket& bucket); + void renderDebugText(const std::vector &strings); void renderFill(FillBucket& bucket, const std::string& layer_name, const Tile::ID& id); void renderLine(LineBucket& bucket, const std::string& layer_name, const Tile::ID& id); void renderPoint(PointBucket& bucket, const std::string& layer_name, const Tile::ID& id); void renderText(TextBucket& bucket, const std::string& layer_name, const Tile::ID& id); - void renderRaster(const std::string& layer_name, const std::shared_ptr& tile_data); + void renderRaster(RasterBucket& bucket, const std::string& layer_name, const Tile::ID& id); void resize(); @@ -55,16 +62,17 @@ public: void setDebug(bool enabled); void prepareClippingMask(); - void drawClippingMask(const mat4& matrix, uint8_t clip_id); + void drawClippingMask(const mat4& matrix, const ClipID& clip); void finishClippingMask(); + void setCurrentSourceName(const std::string &name); + bool needsAnimation() const; private: void setupShaders(); void renderLayers(const std::shared_ptr& tile, const std::vector& layers); void renderLayer(const std::shared_ptr& tile_data, const LayerDescription& layer_desc); void translateLayer(std::array translation, bool reverse = false); - void renderDebug(const std::shared_ptr& tile); void useProgram(uint32_t program); void lineWidth(float lineWidth); @@ -90,6 +98,7 @@ private: float strata = 0; const float strata_epsilon = 1.0f / (1 << 16); enum { Opaque, Translucent } pass = Opaque; + std::string currentSourceName; std::unique_ptr plainShader; std::unique_ptr outlineShader; diff --git a/include/llmr/renderer/raster_bucket.hpp b/include/llmr/renderer/raster_bucket.hpp index 88bb3bd84..bac87cc72 100644 --- a/include/llmr/renderer/raster_bucket.hpp +++ b/include/llmr/renderer/raster_bucket.hpp @@ -3,16 +3,26 @@ #include #include +#include namespace llmr { -class BucketDescription; +class RasterShader; +class VertexBuffer; +class VertexArrayObject; class RasterBucket : public Bucket { public: - RasterBucket(const BucketDescription& bucket_desc); + RasterBucket(Texturepool &texturepool); virtual void render(Painter& painter, const std::string& layer_name, const Tile::ID& id); + + bool setImage(const std::string &data); + + void drawRaster(RasterShader& shader, VertexBuffer &vertices, VertexArrayObject &array); + +private: + Raster raster; }; } diff --git a/include/llmr/style/sprite.hpp b/include/llmr/style/sprite.hpp index ea1a29dff..40da8c4a8 100644 --- a/include/llmr/style/sprite.hpp +++ b/include/llmr/style/sprite.hpp @@ -45,7 +45,7 @@ public: public: const float pixelRatio; - std::shared_ptr raster; + Raster raster; private: void asyncParseJSON(); @@ -57,9 +57,9 @@ private: private: Map ↦ - mutable std::mutex mtx; std::string body; - bool loaded = false; + std::string image; + std::atomic loaded; std::map pos; }; diff --git a/include/llmr/util/clip_ids.hpp b/include/llmr/util/clip_ids.hpp new file mode 100644 index 000000000..4e4d0abe3 --- /dev/null +++ b/include/llmr/util/clip_ids.hpp @@ -0,0 +1,19 @@ +#ifndef LLMR_UTIL_CLIP_IDS +#define LLMR_UTIL_CLIP_IDS + +#include +#include +#include +#include + +namespace llmr { + +static constexpr uint8_t clipMask[9] { 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF }; + +void updateClipIDs(const std::list &array); + +std::map computeClipIDs(std::forward_list array); + +} + +#endif diff --git a/include/llmr/util/raster.hpp b/include/llmr/util/raster.hpp index 0bca3a736..ccdffeef3 100644 --- a/include/llmr/util/raster.hpp +++ b/include/llmr/util/raster.hpp @@ -15,13 +15,11 @@ namespace llmr { class Raster : public std::enable_shared_from_this { public: + Raster(Texturepool &texturepool); ~Raster(); // load image data - void load(); - - // set shared texture pool - void setTexturepool(Texturepool* texturepool); + bool load(const std::string &img); // bind current texture void bind(bool linear = false); @@ -34,8 +32,6 @@ public: bool needsTransition() const; void updateTransitions(time now); - inline void setData(const std::string &img) { data = img; } - public: // loaded image dimensions uint32_t width = 0, height = 0; @@ -60,14 +56,11 @@ private: bool loaded = false; // shared texture pool - Texturepool* texturepool = nullptr; + Texturepool &texturepool; // min/mag filter uint32_t filter = 0; - // the encoded image data. - std::string data; - // the raw pixels char *img = nullptr; diff --git a/src/map/map.cpp b/src/map/map.cpp index 35d845858..dc707ee8f 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #include @@ -138,7 +141,6 @@ void Map::setup() { sources.emplace("mapbox streets", std::unique_ptr(new Source(*this, painter, - texturepool, "http://a.gl-api-us-east-1.tilestream.net/v3/mapbox.mapbox-streets-v4/%d/%d/%d.gl.pbf", Source::Type::vector, {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }}, @@ -150,7 +152,6 @@ void Map::setup() { sources.emplace("satellite", std::unique_ptr(new Source(*this, painter, - texturepool, "https://a.tiles.mapbox.com/v3/justin.hh0gkdfm/%d/%d/%d%s.png256", Source::Type::raster, {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 }}, @@ -331,7 +332,11 @@ void Map::toggleRaster() { if (satellite_source.enabled) { satellite_source.enabled = false; - style.appliedClasses.erase(style.appliedClasses.find("satellite")); + + auto style_class = style.appliedClasses.find("satellite"); + if (style_class != style.appliedClasses.end()) { + style.appliedClasses.erase(style_class); + } } else { satellite_source.enabled = true; style.appliedClasses.insert("satellite"); @@ -350,6 +355,28 @@ void Map::updateTiles() { } } + + +void Map::updateClippingIDs() { + std::forward_list ids; + + for (auto &pair : sources) { + const std::unique_ptr &source = pair.second; + if (source->enabled) { + ids.splice_after(ids.before_begin(), source->getIDs()); + } + } + + const std::map clipIDs = computeClipIDs(ids); + + for (auto &pair : sources) { + const std::unique_ptr &source = pair.second; + if (source->enabled) { + source->updateClipIDs(clipIDs); + } + } +} + void Map::prepare() { view.make_active(); @@ -366,14 +393,14 @@ void Map::prepare() { style.sprite->load(kSpriteURL); } - if (style.sprite && style.sprite->raster->isLoaded() && !style.sprite->raster->textured) { - style.sprite->raster->setTexturepool(&texturepool); - } - updateTiles(); + + updateClippingIDs(); } void Map::render() { + std::vector debug; + painter.clear(); painter.resize(); @@ -386,26 +413,30 @@ void Map::render() { painter.prepareClippingMask(); - bool is_baselayer = true; - + size_t source_id = 0; for (auto &pair : sources) { Source &source = *pair.second; - source.prepare_render(state, is_baselayer); - is_baselayer = source.enabled ? false : (true && is_baselayer); + size_t count = source.prepareRender(state); + debug.emplace_back(util::sprintf<100>("source %d: %d\n", source_id, count)); + source_id++; } painter.finishClippingMask(); + // Then, render each source's tiles. TODO: handle more than one // vector source. for (auto &pair : sources) { Source &source = *pair.second; - source.render(animationTime); + painter.setCurrentSourceName(pair.first); + source.render(); } painter.renderMatte(); + painter.renderDebugText(debug); + // Schedule another rerender when we definitely need a next frame. if (transform.needsTransition()) { update(); diff --git a/src/map/raster_tile_data.cpp b/src/map/raster_tile_data.cpp new file mode 100644 index 000000000..bf3999637 --- /dev/null +++ b/src/map/raster_tile_data.cpp @@ -0,0 +1,30 @@ +#include +#include +#include + +using namespace llmr; + + +RasterTileData::RasterTileData(Tile::ID id, Map &map, const std::string url) + : TileData(id, map, url), + bucket(map.getTexturepool()) { +} + +RasterTileData::~RasterTileData() { +} + +void RasterTileData::parse() { + if (state != State::loaded) { + return; + } + + if (bucket.setImage(data)) { + state = State::parsed; + } else { + state = State::invalid; + } +} + +void RasterTileData::render(Painter &painter, const LayerDescription& layer_desc) { + bucket.render(painter, layer_desc.name, id); +} diff --git a/src/map/source.cpp b/src/map/source.cpp index a83d06f88..91e9d7cb5 100644 --- a/src/map/source.cpp +++ b/src/map/source.cpp @@ -9,15 +9,18 @@ #include #include + +#include +#include + using namespace llmr; -Source::Source(Map &map, Painter &painter, Texturepool &texturepool, +Source::Source(Map &map, Painter &painter, const char *url, Source::Type type, std::vector zooms, uint32_t tile_size, uint32_t min_zoom, uint32_t max_zoom, bool enabled) : enabled(enabled), map(map), painter(painter), - texturepool(texturepool), type(type), zooms(zooms), url(url), @@ -29,43 +32,49 @@ bool Source::update() { return updateTiles(); } -void Source::prepare_render(const TransformState &transform, bool is_baselayer) { - if (!enabled) return; - - uint8_t i = 1; - - for (Tile& tile : tiles) { - if (tile.data && tile.data->state == TileData::State::parsed) { - transform.matrixFor(tile.matrix, tile.id); - matrix::multiply(tile.matrix, painter.projMatrix, tile.matrix); - tile.clip_id = i++; - if (is_baselayer) { - painter.drawClippingMask(tile.matrix, tile.clip_id); - } +void Source::updateClipIDs(const std::map &mapping) { + std::for_each(tiles.begin(), tiles.end(), [&mapping](Tile &tile) { + auto it = mapping.find(tile.id); + if (it != mapping.end()) { + tile.clip = it->second; + } else { + tile.clip = ClipID {}; } - } + }); } -void Source::render(time animationTime) { +size_t Source::prepareRender(const TransformState &transform) { + if (!enabled) return 0; + + size_t masks = 0; + for (Tile& tile : tiles) { + transform.matrixFor(tile.matrix, tile.id); + matrix::multiply(tile.matrix, painter.projMatrix, tile.matrix); + painter.drawClippingMask(tile.matrix, tile.clip); + masks++; + } + + return masks; +} + +void Source::render() { if (!enabled) return; for (const Tile& tile : tiles) { if (tile.data && tile.data->state == TileData::State::parsed) { - if (type == Type::raster) { - std::shared_ptr raster = tile.data->raster; - if (raster && !raster->textured) { - raster->setTexturepool(&texturepool); - raster->beginFadeInTransition(); - } - if (raster && raster->needsTransition()) { - raster->updateTransitions(animationTime); - } - } painter.render(tile); } } } +std::forward_list Source::getIDs() const { + std::forward_list ptrs; + std::transform(tiles.begin(), tiles.end(), std::front_inserter(ptrs), [](const Tile &tile) { + return tile.id; + }); + return ptrs; +} + TileData::State Source::hasTile(const Tile::ID& id) { for (const Tile& tile : tiles) { if (tile.id == id && tile.data) { @@ -111,11 +120,12 @@ TileData::State Source::addTile(const Tile::ID& id) { if (type == Source::Type::vector) { formed_url = util::sprintf<256>(url, normalized_id.z, normalized_id.x, normalized_id.y); + new_tile.data = std::make_shared(normalized_id, map, formed_url); } else { formed_url = util::sprintf<256>(url, normalized_id.z, normalized_id.x, normalized_id.y, (map.getState().getPixelRatio() > 1.0 ? "@2x" : "")); + new_tile.data = std::make_shared(normalized_id, map, formed_url); } - new_tile.data = std::make_shared(normalized_id, map, formed_url, (type == Source::Type::raster)); new_tile.data->request(); tile_data.push_front(new_tile.data); } @@ -245,7 +255,7 @@ bool Source::updateTiles() { // We're painting front-to-back, so we want to draw more detailed tiles first // before filling in other parts with lower zoom levels. tiles.sort([](const Tile & a, const Tile & b) { - return a.id.z > b.id.z; + return a.id.z < b.id.z; }); // Remove all the expired pointers from the list. diff --git a/src/map/tile.cpp b/src/map/tile.cpp index 6ec05beca..5e22dd0bb 100644 --- a/src/map/tile.cpp +++ b/src/map/tile.cpp @@ -4,6 +4,8 @@ using namespace llmr; +#include + Tile::Tile(const ID& id) : id(id) { } @@ -31,14 +33,19 @@ std::forward_list Tile::ID::children(int32_t child_z) const { return children; } -void Tile::ID::normalize() { +Tile::ID Tile::ID::normalized() const { int32_t dim = pow(2, z); - while (x < 0) x += dim; - while (x >= dim) x -= dim; + int32_t nx = x, ny = y; + while (nx < 0) nx += dim; + while (nx >= dim) nx -= dim; + return ID { z, nx, ny }; } -Tile::ID Tile::ID::normalized() const { - ID pos(z, x, y); - pos.normalize(); - return pos; +bool Tile::ID::isChildOf(const Tile::ID &parent) const { + if (parent.z >= z || parent.w != w) { + return false; + } + int32_t scale = pow(2, z - parent.z); + return parent.x == ((x < 0 ? x - scale + 1 : x) / scale) && + parent.y == y / scale; } diff --git a/src/map/tile_data.cpp b/src/map/tile_data.cpp index 5cfada7ff..9ad752526 100644 --- a/src/map/tile_data.cpp +++ b/src/map/tile_data.cpp @@ -1,33 +1,22 @@ #include -#include #include -#include -#include -#include -#include -#include -#include #include -#include - using namespace llmr; -TileData::TileData(Tile::ID id, Map &map, const std::string url, const bool is_raster) +TileData::TileData(Tile::ID id, Map &map, const std::string url) : id(id), state(State::initial), - raster(), + map(map), url(url), - is_raster(is_raster), - map(map) { + debugBucket(debugFontBuffer) { // Initialize tile debug coordinates const std::string str = util::sprintf<32>("%d/%d/%d", id.z, id.x, id.y); debugFontBuffer.addText(str.c_str(), 50, 200, 5); } TileData::~TileData() { - map.getGlyphAtlas().removeGlyphs((uint64_t)id); cancel(); } @@ -51,7 +40,7 @@ void TileData::request() { tile->data.swap(res->body); // Schedule tile parsing in another thread - tile->scheduleParsing(); + tile->reparse(); } else { fprintf(stderr, "[%s] tile loading failed: %d, %s\n", tile->url.c_str(), res->code, res->error_message.c_str()); } @@ -65,7 +54,7 @@ void TileData::cancel() { } } -void TileData::scheduleParsing() { +void TileData::reparse() { // We're creating a new work request. The work request deletes itself after it executed // the after work handler new uv::work>( @@ -74,24 +63,3 @@ void TileData::scheduleParsing() { [](std::shared_ptr &tile) { tile->map.update(); }, shared_from_this()); } - -void TileData::parse() { - if (state != State::loaded) { - return; - } - - try { - // Parsing creates state that is encapsulated in TileParser. While parsing, - // the TileParser object writes results into this objects. All other state - // is going to be discarded afterwards. - TileParser parser(data, *this, map.getStyle(), map.getGlyphAtlas(), is_raster); - } catch (const std::exception& ex) { - fprintf(stderr, "[%p] exception [%d/%d/%d]... failed: %s\n", this, id.z, id.x, id.y, ex.what()); - cancel(); - return; - } - - if (state != State::obsolete) { - state = State::parsed; - } -} diff --git a/src/map/tile_parser.cpp b/src/map/tile_parser.cpp index 26af27f67..793ef353b 100644 --- a/src/map/tile_parser.cpp +++ b/src/map/tile_parser.cpp @@ -14,14 +14,13 @@ using namespace llmr; -TileParser::TileParser(const std::string& data, TileData& tile, const Style& style, GlyphAtlas& glyphAtlas, bool is_raster) - : vector_data(is_raster ? pbf(0, 0) : pbf((const uint8_t *)data.data(), data.size())), - raster_data(is_raster ? data : ""), +TileParser::TileParser(const std::string& data, VectorTileData& tile, const Style& style, GlyphAtlas& glyphAtlas) + : vector_data(pbf((const uint8_t *)data.data(), data.size())), tile(tile), style(style), glyphAtlas(glyphAtlas), - placement(is_raster ? 0 : tile.id.z) { - if (!is_raster) parseGlyphs(); + placement(tile.id.z) { + parseGlyphs(); parseStyleLayers(style.layers); } @@ -37,7 +36,7 @@ void TileParser::parseGlyphs() { GlyphPositions &glyphs = faces[name]; for (const VectorTileGlyph &glyph : face.glyphs) { const Rect rect = - glyphAtlas.addGlyph((uint64_t)tile.id, name, glyph); + glyphAtlas.addGlyph(tile.id.to_uint64(), name, glyph); glyphs.emplace(glyph.id, Glyph{rect, glyph.metrics}); } } @@ -85,31 +84,22 @@ void TileParser::parseStyleLayers(const std::vector& layers) { } std::unique_ptr TileParser::createBucket(const BucketDescription& bucket_desc) { - if (bucket_desc.type == BucketType::Raster) { - if (raster_data.length()) { - tile.raster = std::make_shared(); - tile.raster->setData(raster_data); - tile.raster->load(); - } - return createRasterBucket(bucket_desc); - } else { - auto layer_it = vector_data.layers.find(bucket_desc.source_layer); - if (layer_it != vector_data.layers.end()) { - const VectorTileLayer& layer = layer_it->second; - if (bucket_desc.type == BucketType::Fill) { - return createFillBucket(layer, bucket_desc); - } else if (bucket_desc.type == BucketType::Line) { - return createLineBucket(layer, bucket_desc); - } else if (bucket_desc.type == BucketType::Point) { - return createPointBucket(layer, bucket_desc); - } else if (bucket_desc.type == BucketType::Text) { - return createTextBucket(layer, bucket_desc); - } else { - throw std::runtime_error("unknown bucket type"); - } + auto layer_it = vector_data.layers.find(bucket_desc.source_layer); + if (layer_it != vector_data.layers.end()) { + const VectorTileLayer& layer = layer_it->second; + if (bucket_desc.type == BucketType::Fill) { + return createFillBucket(layer, bucket_desc); + } else if (bucket_desc.type == BucketType::Line) { + return createLineBucket(layer, bucket_desc); + } else if (bucket_desc.type == BucketType::Point) { + return createPointBucket(layer, bucket_desc); + } else if (bucket_desc.type == BucketType::Text) { + return createTextBucket(layer, bucket_desc); } else { - // The layer specified in the bucket does not exist. Do nothing. + throw std::runtime_error("unknown bucket type"); } + } else { + // The layer specified in the bucket does not exist. Do nothing. } return nullptr; @@ -152,6 +142,8 @@ std::unique_ptr TileParser::createPointBucket(const VectorTileLayer& lay } std::unique_ptr TileParser::createTextBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) { + return nullptr; + // Determine the correct text stack. if (!layer.shaping.size()) { return nullptr; @@ -183,10 +175,3 @@ std::unique_ptr TileParser::createTextBucket(const VectorTileLayer& laye return std::move(bucket); } - -std::unique_ptr TileParser::createRasterBucket(const BucketDescription& bucket_desc) { - std::unique_ptr bucket = std::make_unique(bucket_desc); - // Raster buckets are just empty dummies so that they behave - // similarly to vector buckets in styling configurations. - return obsolete() ? nullptr : std::move(bucket); -} diff --git a/src/map/vector_tile_data.cpp b/src/map/vector_tile_data.cpp new file mode 100644 index 000000000..c5d3a23ec --- /dev/null +++ b/src/map/vector_tile_data.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include + +using namespace llmr; + +VectorTileData::VectorTileData(Tile::ID id, Map &map, const std::string url) + : TileData(id, map, url) { +} + +VectorTileData::~VectorTileData() { + map.getGlyphAtlas().removeGlyphs(id.to_uint64()); +} + +void VectorTileData::parse() { + if (state != State::loaded) { + return; + } + + try { + // Parsing creates state that is encapsulated in TileParser. While parsing, + // the TileParser object writes results into this objects. All other state + // is going to be discarded afterwards. + TileParser parser(data, *this, map.getStyle(), map.getGlyphAtlas()); + } catch (const std::exception& ex) { + fprintf(stderr, "[%p] exception [%d/%d/%d]... failed: %s\n", this, id.z, id.x, id.y, ex.what()); + cancel(); + return; + } + + if (state != State::obsolete) { + state = State::parsed; + } +} + +void VectorTileData::render(Painter &painter, const LayerDescription& layer_desc) { + auto databucket_it = buckets.find(layer_desc.bucket_name); + if (databucket_it != buckets.end()) { + assert(databucket_it->second); + databucket_it->second->render(painter, layer_desc.name, id); + } +} \ No newline at end of file diff --git a/src/renderer/debug_bucket.cpp b/src/renderer/debug_bucket.cpp new file mode 100644 index 000000000..2cdec50d3 --- /dev/null +++ b/src/renderer/debug_bucket.cpp @@ -0,0 +1,29 @@ +#include + +#include + +#include + +#include + +struct geometry_too_long_exception : std::exception {}; + +using namespace llmr; + +DebugBucket::DebugBucket(DebugFontBuffer& fontBuffer) + : fontBuffer(fontBuffer) { +} + +void DebugBucket::render(Painter& painter, const std::string& /*layer_name*/, const Tile::ID& /*id*/) { + painter.renderDebugText(*this); +} + +void DebugBucket::drawLines(PlainShader& shader) { + array.bind(shader, fontBuffer, BUFFER_OFFSET(0)); + glDrawArrays(GL_LINES, 0, (GLsizei)(fontBuffer.index())); +} + +void DebugBucket::drawPoints(PlainShader& shader) { + array.bind(shader, fontBuffer, BUFFER_OFFSET(0)); + glDrawArrays(GL_POINTS, 0, (GLsizei)(fontBuffer.index())); +} diff --git a/src/renderer/painter.cpp b/src/renderer/painter.cpp index 0915e790f..5ab9c91d3 100644 --- a/src/renderer/painter.cpp +++ b/src/renderer/painter.cpp @@ -22,6 +22,7 @@ #include #include #include +#include using namespace llmr; @@ -139,10 +140,15 @@ void Painter::prepareClippingMask() { coveringPlainArray.bind(*plainShader, tileStencilBuffer, BUFFER_OFFSET(0)); } -void Painter::drawClippingMask(const mat4& matrix, uint8_t clip_id) { +void Painter::drawClippingMask(const mat4& matrix, const ClipID &clip) { gl::group group("mask"); plainShader->setMatrix(matrix); + GLint id = clip.mask.to_ulong(); + GLuint mask = clipMask[clip.length]; + glStencilFunc(GL_ALWAYS, id, mask); + glStencilMask(0xFF); + Color background = map.getStyle().computed.background.color; background[0] *= map.getStyle().computed.background.opacity; background[1] *= map.getStyle().computed.background.opacity; @@ -151,7 +157,6 @@ void Painter::drawClippingMask(const mat4& matrix, uint8_t clip_id) { plainShader->setColor(background); - glStencilFunc(GL_ALWAYS, clip_id, 0xFF); glDrawArrays(GL_TRIANGLES, 0, (GLsizei)tileStencilBuffer.index()); } @@ -162,6 +167,11 @@ void Painter::finishClippingMask() { gl::end_group(); } +void Painter::setCurrentSourceName(const std::string &name) { + currentSourceName = name; +} + + void Painter::clear() { gl::group group("clear"); glStencilMask(0xFF); @@ -179,19 +189,23 @@ void Painter::render(const Tile& tile) { gl::group group(util::sprintf<32>("render %d/%d/%d", tile.id.z, tile.id.y, tile.id.z)); assert(tile.data); - if (tile.data->state != TileData::State::parsed) { - return; + if (tile.data->state == TileData::State::parsed) { + matrix = tile.matrix; + GLint id = tile.clip.mask.to_ulong(); + GLuint mask = clipMask[tile.clip.length]; + + glStencilFunc(GL_EQUAL, id, mask); + + renderLayers(tile.data, map.getStyle().layers); + } else { + fprintf(stderr, "tile not parsed yet\n"); } frameHistory.record(map.getAnimationTime(), map.getState().getNormalizedZoom()); - matrix = tile.matrix; - glStencilFunc(GL_EQUAL, tile.clip_id, 0xFF); - - renderLayers(tile.data, map.getStyle().layers); - if (debug) { - renderDebug(tile.data); + renderDebugText(tile.data->debugBucket); + renderDebugFrame(); } } @@ -242,16 +256,10 @@ void Painter::renderLayer(const std::shared_ptr& tile_data, const Laye // this layer and render it. auto bucket_it = map.getStyle().buckets.find(layer_desc.bucket_name); if (bucket_it != map.getStyle().buckets.end()) { - if (bucket_it->second.type == BucketType::Raster) { - if (tile_data && tile_data->raster) { - renderRaster(layer_desc.name, tile_data); - } - } else { - auto databucket_it = tile_data->buckets.find(layer_desc.bucket_name); - if (databucket_it != tile_data->buckets.end()) { - assert(databucket_it->second); - databucket_it->second->render(*this, layer_desc.name, tile_data->id); - } + const BucketDescription &bucket = bucket_it->second; + // Only try to render the bucket if it draws data from the current source. + if (bucket.source_name == currentSourceName) { + tile_data->render(*this, layer_desc); } } } @@ -271,8 +279,8 @@ void Painter::translateLayer(std::array translation, bool reverse) { } } -void Painter::renderRaster(const std::string& layer_name, const std::shared_ptr& tile_data) { - if (pass == Opaque) return; +void Painter::renderRaster(RasterBucket& bucket, const std::string& layer_name, const Tile::ID& /*id*/) { + if (pass == Translucent) return; auto raster_properties = map.getStyle().computed.rasters; auto raster_properties_it = raster_properties.find(layer_name); @@ -283,15 +291,17 @@ void Painter::renderRaster(const std::string& layer_name, const std::shared_ptr< gl::group group(layer_name + " (raster)"); + depthMask(false); + useProgram(rasterShader->program); rasterShader->setMatrix(matrix); - rasterShader->setImage(0); - rasterShader->setOpacity(properties.opacity * tile_data->raster->opacity); - tile_data->raster->bind(true); + rasterShader->setOpacity(1); + // rasterShader->setOpacity(properties.opacity * tile_data->raster->opacity); - coveringRasterArray.bind(*rasterShader, tileStencilBuffer, BUFFER_OFFSET(0)); - glDepthRange(strata, 1.0f); - glDrawArrays(GL_TRIANGLES, 0, (GLsizei)tileStencilBuffer.index()); + glDepthRange(strata + strata_epsilon, 1.0f); + bucket.drawRaster(*rasterShader, tileStencilBuffer, coveringRasterArray); + + depthMask(true); } void Painter::renderFill(FillBucket& bucket, const std::string& layer_name, const Tile::ID& id) { @@ -390,7 +400,7 @@ void Painter::renderFill(FillBucket& bucket, const std::string& layer_name, cons patternShader->setPatternBottomRight({{ imagePos.br.x, imagePos.br.y }}); patternShader->setColor(fill_color); patternShader->setMix(mix); - sprite->raster->bind(true); + sprite->raster.bind(true); // Draw the actual triangles into the color & stencil buffer. glDepthRange(strata + strata_epsilon, 1.0f); @@ -579,7 +589,7 @@ void Painter::renderPoint(PointBucket& bucket, const std::string& layer_name, co pointShader->setPointTopLeft({{ imagePos.tl.x, imagePos.tl.y }}); pointShader->setPointBottomRight({{ imagePos.br.x, imagePos.br.y }}); - sprite->raster->bind(map.getState().isChanging()); + sprite->raster.bind(map.getState().isChanging()); const float pointSize = (properties.size ? properties.size : (imagePos.size.x / map.getState().getPixelRatio())) * 1.4142135623730951 * map.getState().getPixelRatio(); #if defined(GL_ES_VERSION_2_0) @@ -707,8 +717,35 @@ void Painter::renderText(TextBucket& bucket, const std::string& layer_name, cons translateLayer(properties.translate, true); } -void Painter::renderDebug(const TileData::Ptr& tile_data) { - gl::group group("debug"); +void Painter::renderDebugText(DebugBucket& bucket) { + gl::group group("debug text"); + + glDisable(GL_DEPTH_TEST); + + useProgram(plainShader->program); + plainShader->setMatrix(matrix); + + // Draw white outline + plainShader->setColor(1.0f, 1.0f, 1.0f, 1.0f); + lineWidth(4.0f * map.getState().getPixelRatio()); + bucket.drawLines(*plainShader); + +#ifndef GL_ES_VERSION_2_0 + // Draw line "end caps" + glPointSize(2); + bucket.drawPoints(*plainShader); +#endif + + // Draw black text. + plainShader->setColor(0.0f, 0.0f, 0.0f, 1.0f); + lineWidth(2.0f * map.getState().getPixelRatio()); + bucket.drawLines(*plainShader); + + glEnable(GL_DEPTH_TEST); +} + +void Painter::renderDebugFrame() { + gl::group group("debug frame"); // Disable depth test and don't count this towards the depth buffer, // but *don't* disable stencil test, as we want to clip the red tile border @@ -724,15 +761,6 @@ void Painter::renderDebug(const TileData::Ptr& tile_data) { lineWidth(4.0f * map.getState().getPixelRatio()); glDrawArrays(GL_LINE_STRIP, 0, (GLsizei)tileBorderBuffer.index()); - // draw debug info - tile_data->debugFontArray.bind(*plainShader, tile_data->debugFontBuffer, BUFFER_OFFSET(0)); - plainShader->setColor(1.0f, 1.0f, 1.0f, 1.0f); - lineWidth(4.0f * map.getState().getPixelRatio()); - glDrawArrays(GL_LINES, 0, (GLsizei)tile_data->debugFontBuffer.index()); - plainShader->setColor(0.0f, 0.0f, 0.0f, 1.0f); - lineWidth(2.0f * map.getState().getPixelRatio()); - glDrawArrays(GL_LINES, 0, (GLsizei)tile_data->debugFontBuffer.index()); - glEnable(GL_DEPTH_TEST); } @@ -741,7 +769,8 @@ void Painter::renderMatte() { glDisable(GL_DEPTH_TEST); glStencilFunc(GL_EQUAL, 0x0, 0xFF); - Color white = {{ 0.9, 0.9, 0.9, 1 }}; + // Color white = {{ 0.9, 0.9, 0.9, 1 }}; + Color white = {{ 1, 1, 0, 1 }}; useProgram(plainShader->program); plainShader->setMatrix(nativeMatrix); @@ -753,3 +782,38 @@ void Painter::renderMatte() { glEnable(GL_DEPTH_TEST); } + + +void Painter::renderDebugText(const std::vector &strings) { + gl::group group("debug text"); + + glDisable(GL_DEPTH_TEST); + glStencilFunc(GL_ALWAYS, 0xFF, 0xFF); + + useProgram(plainShader->program); + plainShader->setMatrix(nativeMatrix); + + DebugFontBuffer debugFontBuffer; + int line = 25; + for (const std::string &str : strings) { + debugFontBuffer.addText(str.c_str(), 10, line, 0.75); + line += 20; + } + + // draw debug info + VertexArrayObject debugFontArray; + debugFontArray.bind(*plainShader, debugFontBuffer, BUFFER_OFFSET(0)); + plainShader->setColor(1.0f, 1.0f, 1.0f, 1.0f); + lineWidth(4.0f * map.getState().getPixelRatio()); + glDrawArrays(GL_LINES, 0, (GLsizei)debugFontBuffer.index()); +#ifndef GL_ES_VERSION_2_0 + glPointSize(2); + glDrawArrays(GL_POINTS, 0, (GLsizei)debugFontBuffer.index()); +#endif + plainShader->setColor(0.0f, 0.0f, 0.0f, 1.0f); + lineWidth(2.0f * map.getState().getPixelRatio()); + glDrawArrays(GL_LINES, 0, (GLsizei)debugFontBuffer.index()); + + + glEnable(GL_DEPTH_TEST); +} \ No newline at end of file diff --git a/src/renderer/raster_bucket.cpp b/src/renderer/raster_bucket.cpp index f8c656b2c..ca173680a 100644 --- a/src/renderer/raster_bucket.cpp +++ b/src/renderer/raster_bucket.cpp @@ -3,12 +3,21 @@ using namespace llmr; -RasterBucket::RasterBucket(const BucketDescription &) { +RasterBucket::RasterBucket(Texturepool &texturepool) + : raster(texturepool) { } -void RasterBucket::render(Painter &, const std::string &, const Tile::ID &) { - // The painter renders rasters directly. This function is a no-op - // and exists just to satisfy the abstract superclass requirements. - // That way, we can avoid having raster buckets pass around - // references to tile data and/or rasters directly. +void RasterBucket::render(Painter &painter, const std::string &layer_name, const Tile::ID &id) { + painter.renderRaster(*this, layer_name, id); +} + +bool RasterBucket::setImage(const std::string &data) { + return raster.load(data); +} + +void RasterBucket::drawRaster(RasterShader& shader, VertexBuffer &vertices, VertexArrayObject &array) { + raster.bind(true); + shader.setImage(0); + array.bind(shader, vertices, BUFFER_OFFSET(0)); + glDrawArrays(GL_TRIANGLES, 0, (GLsizei)vertices.index()); } diff --git a/src/renderer/text_bucket.cpp b/src/renderer/text_bucket.cpp index a2a88e9a7..5b75ca99f 100644 --- a/src/renderer/text_bucket.cpp +++ b/src/renderer/text_bucket.cpp @@ -14,8 +14,6 @@ #include -#define BUFFER_OFFSET(i) ((char *) nullptr + (i)) - #include using namespace llmr; diff --git a/src/style/sprite.cpp b/src/style/sprite.cpp index cf8f3ffed..e91a24c49 100644 --- a/src/style/sprite.cpp +++ b/src/style/sprite.cpp @@ -15,14 +15,20 @@ SpritePosition::SpritePosition(uint16_t x, uint16_t y, uint16_t width, uint16_t y(y), width(width), height(height), - pixelRatio(pixelRatio) {} + pixelRatio(pixelRatio) { +} ImagePosition::ImagePosition(const vec2& size, vec2 tl, vec2 br) : size(size), tl(tl), br(br) {} -Sprite::Sprite(Map &map, float pixelRatio) : pixelRatio(pixelRatio), raster(std::make_shared()), map(map) {} +Sprite::Sprite(Map &map, float pixelRatio) + : pixelRatio(pixelRatio), + raster(map.getTexturepool()), + map(map), + loaded(false) { +} void Sprite::load(const std::string& base_url) { std::shared_ptr sprite = shared_from_this(); @@ -40,7 +46,7 @@ void Sprite::load(const std::string& base_url) { platform::request_http(base_url + suffix + ".png", [sprite](platform::Response *res) { if (res->code == 200) { - sprite->raster->setData(res->body); + sprite->image.swap(res->body); sprite->asyncParseImage(); } else { fprintf(stderr, "failed to load sprite image\n"); @@ -49,8 +55,7 @@ void Sprite::load(const std::string& base_url) { } void Sprite::complete(std::shared_ptr &sprite) { - std::lock_guard lock(sprite->mtx); - if (sprite->raster && sprite->raster->isLoaded() && sprite->pos.size()) { + if (sprite->raster.isLoaded() && sprite->pos.size()) { sprite->loaded = true; sprite->map.update(); fprintf(stderr, "sprite loaded\n"); @@ -58,7 +63,6 @@ void Sprite::complete(std::shared_ptr &sprite) { } bool Sprite::isLoaded() const { - std::lock_guard lock(mtx); return loaded; } @@ -71,14 +75,14 @@ void Sprite::asyncParseJSON() { } void Sprite::parseImage(std::shared_ptr &sprite) { - sprite->raster->load(); + sprite->raster.load(sprite->image); + sprite->image.clear(); } void Sprite::parseJSON(std::shared_ptr &sprite) { - std::lock_guard lock(sprite->mtx); - rapidjson::Document d; d.Parse<0>(sprite->body.c_str()); + sprite->body.clear(); if (d.IsObject()) { for (rapidjson::Value::ConstMemberIterator itr = d.MemberBegin(); itr != d.MemberEnd(); ++itr) { @@ -105,7 +109,7 @@ void Sprite::parseJSON(std::shared_ptr &sprite) { } ImagePosition Sprite::getPosition(const std::string& name, bool repeating) { - if (!this->raster || !this->raster->isLoaded()) return {}; + if (!isLoaded()) return {}; // `repeating` indicates that the image will be used in a repeating pattern // repeating pattern images are assumed to have a 1px padding that mirrors the opposite edge @@ -122,12 +126,12 @@ ImagePosition Sprite::getPosition(const std::string& name, bool repeating) { pos.height }, { - (float)(pos.x + offset) / raster->width, - (float)(pos.y + offset) / raster->height + (float)(pos.x + offset) / raster.width, + (float)(pos.y + offset) / raster.height }, { - (float)(pos.x + pos.width - 2 * offset) / raster->width, - (float)(pos.y + pos.height - 2 * offset) / raster->height + (float)(pos.x + pos.width - 2 * offset) / raster.width, + (float)(pos.y + pos.height - 2 * offset) / raster.height } }; } diff --git a/src/util/clip_ids.cpp b/src/util/clip_ids.cpp new file mode 100644 index 000000000..457d6ddb3 --- /dev/null +++ b/src/util/clip_ids.cpp @@ -0,0 +1,194 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace llmr { + +struct TileHierarchy { + TileHierarchy(Tile::ID id, std::list &&children) + : id(id), children(std::move(children)) {} + + const Tile::ID id; + ClipID clip; + std::list children; +}; + +std::list partition(std::forward_list &&array) { + if (array.empty()) { + // We don't have to update the clipping mask because there are no tiles + // anyway. + return {}; + } + + int8_t minZ = array.begin()->z; + + std::list result; + std::forward_list remainder; + auto remainder_it = remainder.before_begin(); + + while (!array.empty()) { + const Tile::ID id = array.front(); + array.pop_front(); + if (id.z == minZ) { + std::forward_list children; + auto children_it = children.before_begin(); + + array.remove_if([&id, &children, &children_it](const Tile::ID &child) { + if (child.isChildOf(id)) { + children_it = children.insert_after(children_it, child); + return true; + } else { + return false; + } + }); + + result.emplace_back(id, partition(std::move(children))); + } else { + remainder_it = remainder.insert_after(remainder_it, id); + } + } + + // Concatenate the remainder. + if (!array.empty()) { + result.splice(result.begin(), partition(std::move(remainder))); + } + + return result; +} + +uint8_t prefix(std::list &array, TileHierarchy *parent = nullptr) { + if (array.empty()) { + return 0; + } + + bool all_children_are_immediate = true; + uint8_t max_child_prefix_length = 0; + + struct Huffman { + explicit Huffman(int prefix_length, TileHierarchy *item) + : prefix_length(prefix_length), children(1, item) {} + uint8_t prefix_length; + std::vector children; + }; + + // Create a temporary structure that we use for sorting the prefix tree. + std::list huffman; + std::transform(array.begin(), array.end(), std::back_inserter(huffman), [parent, &all_children_are_immediate, &max_child_prefix_length](TileHierarchy &item) { + uint8_t prefix_length = prefix(item.children, &item); + + if (prefix_length > max_child_prefix_length) { + max_child_prefix_length = prefix_length; + } + + if (!parent || item.id.z != parent->id.z + 1) { + all_children_are_immediate = false; + } + + return Huffman { prefix_length + 1, &item }; + }); + + while (huffman.size() > 1) { + huffman.sort([](const Huffman &a, const Huffman &b) { + return a.prefix_length < b.prefix_length; + }); + + Huffman &first = *huffman.begin(); + Huffman &second = *(++huffman.begin()); + + assert(&first != &second); + + // Prefix with 0 + std::for_each(first.children.begin(), first.children.end(), [](TileHierarchy *child) { + child->clip.mask >>= 1; + child->clip.mask.set(7, false); // noop + child->clip.length++; + }); + first.prefix_length++; + + // Prefix with 1 + std::for_each(second.children.begin(), second.children.end(), [](TileHierarchy *child) { + child->clip.mask >>= 1; + child->clip.mask.set(7, true); + child->clip.length++; + }); + second.prefix_length++; + + second.children.insert(second.children.end(), first.children.begin(), first.children.end()); + second.prefix_length = first.prefix_length + second.prefix_length; + + // Remove the first child as we've just merged it into the second version. + huffman.erase(huffman.begin()); + } + + uint8_t prefix_length = 0; + + // Filter out all-zero bits + bool filter_zero = !all_children_are_immediate || array.size() != 4; + + for (TileHierarchy &item : array) { + if (filter_zero && !item.clip.mask.any()) { + // Make sure we don't have a prefix that is all zeros. + // item.clip.mask |= (0x80 >> item.length); + item.clip.mask.set(7 - item.clip.length); + item.clip.length++; + } + + if (item.clip.length > prefix_length) { + prefix_length = item.clip.length; + } + } + + return max_child_prefix_length + prefix_length; +} + +void propagate(std::map &mapping, std::list &array, const ClipID &parent = ClipID{}) { + for (auto &item : array) { + item.clip.mask >>= parent.length; + item.clip.mask |= parent.mask; + item.clip.length += parent.length; + auto result = mapping.emplace(item.id, item.clip); + assert("Tried to insert a duplicate item" && result.second == true); + propagate(mapping, item.children, const_cast(item.clip)); + }; +} + +void updateClipIDs(const std::list &array) { + std::forward_list ids; + std::transform(array.begin(), array.end(), std::front_inserter(ids), [](Tile *item) { + return item->id; + }); + + const std::map mapping = computeClipIDs(ids); + + std::for_each(array.begin(), array.end(), [&mapping](Tile *item) { + auto it = mapping.find(item->id); + if (it != mapping.end()) { + item->clip = it->second; + } else { + item->clip = ClipID {}; + } + }); +} + +std::map computeClipIDs(std::forward_list array) { + // Sort by zoom level and make sure that we don't have duplicate elements. + array.sort(); + array.unique(); + + std::list hierarchy = partition(std::move(array)); + prefix(hierarchy); + + std::map mapping; + propagate(mapping, hierarchy); + return mapping; +} + +} diff --git a/src/util/raster.cpp b/src/util/raster.cpp index e7cb89e42..90268c09b 100644 --- a/src/util/raster.cpp +++ b/src/util/raster.cpp @@ -13,11 +13,15 @@ using namespace llmr; +Raster::Raster(Texturepool &texturepool) + : texturepool(texturepool) +{} + Raster::~Raster() { if (img && !textured) { free(img); } else if (textured) { - texturepool->removeTextureID(texture); + texturepool.removeTextureID(texture); } } @@ -26,17 +30,14 @@ bool Raster::isLoaded() const { return loaded; } -void Raster::load() { +bool Raster::load(const std::string &data) { loadImage(data); - { - std::lock_guard lock(mtx); - if (img) { - loaded = true; - } + std::lock_guard lock(mtx); + if (img) { + loaded = true; } - - data.clear(); + return loaded; } struct Buffer { @@ -154,9 +155,6 @@ void Raster::loadImage(const std::string& data) { } } -void Raster::setTexturepool(Texturepool* new_texturepool) { - texturepool = new_texturepool; -} void Raster::bind(bool linear) { if (!width || !height) { @@ -164,8 +162,8 @@ void Raster::bind(bool linear) { return; } - if (img && !textured && texturepool) { - texture = texturepool->getTextureID(); + if (img && !textured) { + texture = texturepool.getTextureID(); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); diff --git a/src/util/texturepool.cpp b/src/util/texturepool.cpp index a1966b700..9feadb3db 100644 --- a/src/util/texturepool.cpp +++ b/src/util/texturepool.cpp @@ -25,7 +25,7 @@ GLuint Texturepool::getTextureID() { return id; } - + void Texturepool::removeTextureID(GLuint texture_id) { bool needs_clear = false; diff --git a/test/clip_ids.cpp b/test/clip_ids.cpp new file mode 100644 index 000000000..3fd9ba062 --- /dev/null +++ b/test/clip_ids.cpp @@ -0,0 +1,234 @@ +#include +#include "gtest/gtest.h" + +#include + +#include +#include + +using namespace llmr; + +TEST(ClipIDs, ClipMasks) { + ASSERT_EQ(0, clipMask[0]); + ASSERT_EQ(0x80, clipMask[1]); + ASSERT_EQ(0xC0, clipMask[2]); + ASSERT_EQ(0xE0, clipMask[3]); + ASSERT_EQ(0xF0, clipMask[4]); + ASSERT_EQ(0xF8, clipMask[5]); + ASSERT_EQ(0xFC, clipMask[6]); + ASSERT_EQ(0xFE, clipMask[7]); + ASSERT_EQ(0xFF, clipMask[8]); +} + + +TEST(ClipIDs, ParentAndFourChildren) { + std::array, 5> tiles {{ + std::make_unique(Tile::ID { 1, 0, 0 }), // 1/0/0: 11000000 (3) + std::make_unique(Tile::ID { 1, 0, 1 }), // 1/0/1: 11100000 (3) + std::make_unique(Tile::ID { 1, 1, 0 }), // 1/1/0: 10000000 (3) + std::make_unique(Tile::ID { 1, 1, 1 }), // 1/1/1: 10100000 (3) + std::make_unique(Tile::ID { 0, 0, 0 }), // 0/0/0: 10000000 (1) + }}; + + std::list tile_ptrs; + std::transform(tiles.begin(), tiles.end(), std::back_inserter(tile_ptrs), [](std::unique_ptr &tile) { return tile.get(); }); + + updateClipIDs(tile_ptrs); + + // for (const auto &it : tiles) { + // std::cout << std::string(it->id) << ": " << it->clip.mask << " (" << (int)it->clip.length << ")" << std::endl; + // } + + ASSERT_EQ(std::bitset<8>("11000000"), tiles[0]->clip.mask); ASSERT_EQ(3, tiles[0]->clip.length); + ASSERT_EQ(std::bitset<8>("11100000"), tiles[1]->clip.mask); ASSERT_EQ(3, tiles[1]->clip.length); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[2]->clip.mask); ASSERT_EQ(3, tiles[2]->clip.length); + ASSERT_EQ(std::bitset<8>("10100000"), tiles[3]->clip.mask); ASSERT_EQ(3, tiles[3]->clip.length); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[4]->clip.mask); ASSERT_EQ(1, tiles[4]->clip.length); +} + +TEST(ClipIDs, ParentAndFourChildrenNegative) { + std::array, 5> tiles {{ + std::make_unique(Tile::ID { 1, -2, 0 }), // 1/0/0: 11000000 (3) + std::make_unique(Tile::ID { 1, -2, 1 }), // 1/0/1: 11100000 (3) + std::make_unique(Tile::ID { 1, -1, 0 }), // 1/1/0: 10000000 (3) + std::make_unique(Tile::ID { 1, -1, 1 }), // 1/1/1: 10100000 (3) + std::make_unique(Tile::ID { 0, -1, 0 }), // 0/0/0: 10000000 (1) + }}; + + std::list tile_ptrs; + std::transform(tiles.begin(), tiles.end(), std::back_inserter(tile_ptrs), [](std::unique_ptr &tile) { return tile.get(); }); + + updateClipIDs(tile_ptrs); + + // for (const auto &it : tiles) { + // std::cout << std::string(it->id) << ": " << it->clip.mask << " (" << (int)it->clip.length << ")" << std::endl; + // } + + ASSERT_EQ(std::bitset<8>("11000000"), tiles[0]->clip.mask); ASSERT_EQ(3, tiles[0]->clip.length); + ASSERT_EQ(std::bitset<8>("11100000"), tiles[1]->clip.mask); ASSERT_EQ(3, tiles[1]->clip.length); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[2]->clip.mask); ASSERT_EQ(3, tiles[2]->clip.length); + ASSERT_EQ(std::bitset<8>("10100000"), tiles[3]->clip.mask); ASSERT_EQ(3, tiles[3]->clip.length); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[4]->clip.mask); ASSERT_EQ(1, tiles[4]->clip.length); +} + +TEST(ClipIDs, NegativeParentAndMissingLevel) { + std::array, 5> tiles {{ + std::make_unique(Tile::ID { 1, -1, 0 }), // 1/-1/0: 10000000 (1) + std::make_unique(Tile::ID { 2, -1, 0 }), // 2/-1/0: 10000000 (3) + std::make_unique(Tile::ID { 2, -2, 1 }), // 2/-2/1: 11100000 (3) + std::make_unique(Tile::ID { 2, -1, 1 }), // 2/-1/1: 10100000 (3) + std::make_unique(Tile::ID { 2, -2, 0 }), // 2/-2/0: 11000000 (3) + }}; + + std::list tile_ptrs; + std::transform(tiles.begin(), tiles.end(), std::back_inserter(tile_ptrs), [](std::unique_ptr &tile) { return tile.get(); }); + + updateClipIDs(tile_ptrs); + + // for (const auto &it : tiles) { + // std::cout << std::string(it->id) << ": " << it->clip.mask << " (" << (int)it->clip.length << ")" << std::endl; + // } + + ASSERT_EQ(std::bitset<8>("10000000"), tiles[0]->clip.mask); ASSERT_EQ(1, tiles[0]->clip.length); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[1]->clip.mask); ASSERT_EQ(3, tiles[1]->clip.length); + ASSERT_EQ(std::bitset<8>("11100000"), tiles[2]->clip.mask); ASSERT_EQ(3, tiles[2]->clip.length); + ASSERT_EQ(std::bitset<8>("10100000"), tiles[3]->clip.mask); ASSERT_EQ(3, tiles[3]->clip.length); + ASSERT_EQ(std::bitset<8>("11000000"), tiles[4]->clip.mask); ASSERT_EQ(3, tiles[4]->clip.length); +} + + +TEST(ClipIDs, SevenOnSameLevel) { + std::array, 7> tiles {{ + std::make_unique(Tile::ID { 2, 0, 0 }), // 2/0/0: 11000000 (3) + std::make_unique(Tile::ID { 2, 0, 1 }), // 2/0/1: 11100000 (3) + std::make_unique(Tile::ID { 2, 0, 2 }), // 2/0/2: 10000000 (3) + std::make_unique(Tile::ID { 2, 1, 0 }), // 2/1/0: 10100000 (3) + std::make_unique(Tile::ID { 2, 1, 1 }), // 2/1/1: 01000000 (3) + std::make_unique(Tile::ID { 2, 1, 2 }), // 2/1/2: 01100000 (3) + std::make_unique(Tile::ID { 2, 2, 0 }), // 2/2/0: 00100000 (3) + }}; + + std::list tile_ptrs; + std::transform(tiles.begin(), tiles.end(), std::back_inserter(tile_ptrs), [](std::unique_ptr &tile) { return tile.get(); }); + + updateClipIDs(tile_ptrs); + + // for (const auto &it : tiles) { + // std::cout << std::string(it->id) << ": " << it->clip.mask << " (" << (int)it->clip.length << ")" << std::endl; + // } + + ASSERT_EQ(std::bitset<8>("11000000"), tiles[0]->clip.mask); ASSERT_EQ(3, tiles[0]->clip.length); + ASSERT_EQ(std::bitset<8>("11100000"), tiles[1]->clip.mask); ASSERT_EQ(3, tiles[1]->clip.length); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[2]->clip.mask); ASSERT_EQ(3, tiles[2]->clip.length); + ASSERT_EQ(std::bitset<8>("10100000"), tiles[3]->clip.mask); ASSERT_EQ(3, tiles[3]->clip.length); + ASSERT_EQ(std::bitset<8>("01000000"), tiles[4]->clip.mask); ASSERT_EQ(3, tiles[4]->clip.length); + ASSERT_EQ(std::bitset<8>("01100000"), tiles[5]->clip.mask); ASSERT_EQ(3, tiles[5]->clip.length); + ASSERT_EQ(std::bitset<8>("00100000"), tiles[6]->clip.mask); ASSERT_EQ(3, tiles[6]->clip.length); +} + +TEST(ClipIDs, MultipleLevels) { + std::array, 12> tiles {{ + std::make_unique(Tile::ID { 2, 0, 0 }), // 2/0/0: 10000000 (1) + std::make_unique(Tile::ID { 3, 0, 0 }), // 3/0/0: 10000000 (3) + std::make_unique(Tile::ID { 3, 0, 1 }), // 3/0/1: 11100000 (3) + std::make_unique(Tile::ID { 4, 0, 2 }), // 4/0/2: 11110000 (5) + std::make_unique(Tile::ID { 4, 1, 2 }), // 4/0/3: 11111000 (5) + std::make_unique(Tile::ID { 4, 0, 3 }), // 4/1/2: 11100000 (5) + std::make_unique(Tile::ID { 4, 1, 3 }), // 4/1/3: 11101000 (5) + std::make_unique(Tile::ID { 3, 1, 0 }), // 3/1/0: 10100000 (3) + std::make_unique(Tile::ID { 3, 1, 1 }), // 3/1/1: 11000000 (3) + std::make_unique(Tile::ID { 2, 1, 0 }), // 2/1/0: 01000000 (2) + std::make_unique(Tile::ID { 3, 2, 0 }), // 3/2/0: 01010000 (4) + std::make_unique(Tile::ID { 3, 2, 1 }), // 3/2/1: 01100000 (3) + }}; + + // Use a random order to verify that the result of this algorithm is independent of the + std::random_shuffle(tiles.begin(), tiles.end()); + + std::list tile_ptrs; + std::transform(tiles.begin(), tiles.end(), std::back_inserter(tile_ptrs), [](std::unique_ptr &tile) { return tile.get(); }); + + updateClipIDs(tile_ptrs); + + // Sort them by tile ID so that we know what order we have to test in. + std::sort(tiles.begin(), tiles.end(), [](const std::unique_ptr &a, const std::unique_ptr &b) { + return a->id < b->id; + }); + + // for (const auto &it : tiles) { + // std::cout << std::string(it->id) << ": " << it->clip.mask << " (" << (int)it->clip.length << ")" << std::endl; + // } + + ASSERT_EQ(std::string("2/0/0"), std::string(tiles[0]->id)); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[0]->clip.mask); + ASSERT_EQ(1, tiles[0]->clip.length); + + ASSERT_EQ(std::string("2/1/0"), std::string(tiles[1]->id)); + ASSERT_EQ(std::bitset<8>("01000000"), tiles[1]->clip.mask); + ASSERT_EQ(2, tiles[1]->clip.length); + + ASSERT_EQ(std::string("3/0/0"), std::string(tiles[2]->id)); + ASSERT_EQ(std::bitset<8>("10000000"), tiles[2]->clip.mask); + ASSERT_EQ(3, tiles[2]->clip.length); + + ASSERT_EQ(std::string("3/0/1"), std::string(tiles[3]->id)); + ASSERT_EQ(std::bitset<8>("11100000"), tiles[3]->clip.mask); + ASSERT_EQ(3, tiles[3]->clip.length); + + ASSERT_EQ(std::string("3/1/0"), std::string(tiles[4]->id)); + ASSERT_EQ(std::bitset<8>("10100000"), tiles[4]->clip.mask); + ASSERT_EQ(3, tiles[4]->clip.length); + + ASSERT_EQ(std::string("3/1/1"), std::string(tiles[5]->id)); + ASSERT_EQ(std::bitset<8>("11000000"), tiles[5]->clip.mask); + ASSERT_EQ(3, tiles[5]->clip.length); + + ASSERT_EQ(std::string("3/2/0"), std::string(tiles[6]->id)); + ASSERT_EQ(std::bitset<8>("01010000"), tiles[6]->clip.mask); + ASSERT_EQ(4, tiles[6]->clip.length); + + ASSERT_EQ(std::string("3/2/1"), std::string(tiles[7]->id)); + ASSERT_EQ(std::bitset<8>("01100000"), tiles[7]->clip.mask); + ASSERT_EQ(3, tiles[7]->clip.length); + + ASSERT_EQ(std::string("4/0/2"), std::string(tiles[8]->id)); + ASSERT_EQ(std::bitset<8>("11110000"), tiles[8]->clip.mask); + ASSERT_EQ(5, tiles[8]->clip.length); + + ASSERT_EQ(std::string("4/0/3"), std::string(tiles[9]->id)); + ASSERT_EQ(std::bitset<8>("11111000"), tiles[9]->clip.mask); + ASSERT_EQ(5, tiles[9]->clip.length); + + ASSERT_EQ(std::string("4/1/2"), std::string(tiles[10]->id)); + ASSERT_EQ(std::bitset<8>("11100000"), tiles[10]->clip.mask); + ASSERT_EQ(5, tiles[10]->clip.length); + + ASSERT_EQ(std::string("4/1/3"), std::string(tiles[11]->id)); + ASSERT_EQ(std::bitset<8>("11101000"), tiles[11]->clip.mask); + ASSERT_EQ(5, tiles[11]->clip.length); +} + +TEST(ClipIDs, DuplicateIDs) { + + std::forward_list tiles {{ + Tile::ID { 2, 0, 0 }, + Tile::ID { 2, 0, 1 }, + Tile::ID { 2, 0, 0 }, + Tile::ID { 2, 0, 1 }, + Tile::ID { 2, 0, 1 }, + }}; + + std::map mapping = computeClipIDs(tiles); + + ASSERT_EQ(2ull, mapping.size()); + + // for (const auto &it : mapping) { + // std::cout << std::string(it.first) << "(" << it.first << ")" << ": " << it.second.mask << " (" << (int)it.second.length << ")" << std::endl; + // } + + ASSERT_EQ(std::bitset<8>("01000000"), mapping[Tile::ID(2, 0, 0)].mask); + ASSERT_EQ(2, mapping[Tile::ID(2, 0, 0)].length); + + ASSERT_EQ(std::bitset<8>("10000000"), mapping[Tile::ID(2, 0, 1)].mask); + ASSERT_EQ(1, mapping[Tile::ID(2, 0, 1)].length); +} diff --git a/test/test.gyp b/test/test.gyp index 3e5b7c88d..9cb44d854 100644 --- a/test/test.gyp +++ b/test/test.gyp @@ -64,6 +64,22 @@ "../llmr.gyp:llmr-x86" ] }, + { + "target_name": "clip_ids", + "product_name": "test_clip_ids", + "type": "executable", + "libraries": [ + "-lpthread", + ], + "sources": [ + "./main.cpp", + "./clip_ids.cpp", + ], + "dependencies": [ + "../deps/gtest/gtest.gyp:gtest", + "../llmr.gyp:llmr-x86" + ] + }, { "target_name": "variant", "product_name": "test_variant", @@ -80,6 +96,22 @@ "../llmr.gyp:llmr-x86" ] }, + { + "target_name": "tile", + "product_name": "test_tile", + "type": "executable", + "libraries": [ + "-lpthread", + ], + "sources": [ + "./main.cpp", + "./tile.cpp", + ], + "dependencies": [ + "../deps/gtest/gtest.gyp:gtest", + "../llmr.gyp:llmr-x86" + ] + }, { "target_name": "headless", "product_name": "test_headless", @@ -105,7 +137,9 @@ "type": "none", "dependencies": [ "rotation_range", + "clip_ids", "variant", + "tile", ], } ] diff --git a/test/tile.cpp b/test/tile.cpp new file mode 100644 index 000000000..9e36fbecf --- /dev/null +++ b/test/tile.cpp @@ -0,0 +1,50 @@ +#include +#include "gtest/gtest.h" + +#include + +using namespace llmr; + + +TEST(Variant, isChild) { + ASSERT_TRUE(Tile::ID(1, 0, 0).isChildOf(Tile::ID(0, 0, 0))); + ASSERT_TRUE(Tile::ID(1, 1, 0).isChildOf(Tile::ID(0, 0, 0))); + ASSERT_TRUE(Tile::ID(1, 2, 0).isChildOf(Tile::ID(0, 1, 0))); + ASSERT_TRUE(Tile::ID(1, 3, 0).isChildOf(Tile::ID(0, 1, 0))); + ASSERT_TRUE(Tile::ID(1, 4, 0).isChildOf(Tile::ID(0, 2, 0))); + ASSERT_TRUE(Tile::ID(1, 5, 0).isChildOf(Tile::ID(0, 2, 0))); + ASSERT_TRUE(Tile::ID(2, 0, 0).isChildOf(Tile::ID(0, 0, 0))); + + ASSERT_TRUE(Tile::ID(2, 8, 0).isChildOf(Tile::ID(0, 2, 0))); + ASSERT_TRUE(Tile::ID(2, 9, 0).isChildOf(Tile::ID(0, 2, 0))); + ASSERT_TRUE(Tile::ID(2, 10, 0).isChildOf(Tile::ID(0, 2, 0))); + ASSERT_TRUE(Tile::ID(2, 11, 0).isChildOf(Tile::ID(0, 2, 0))); + ASSERT_TRUE(Tile::ID(2, 12, 0).isChildOf(Tile::ID(0, 3, 0))); + ASSERT_TRUE(Tile::ID(2, 13, 0).isChildOf(Tile::ID(0, 3, 0))); + + ASSERT_TRUE(Tile::ID(1, -1, 0).isChildOf(Tile::ID(0, -1, 0))); + ASSERT_TRUE(Tile::ID(1, -2, 0).isChildOf(Tile::ID(0, -1, 0))); + ASSERT_TRUE(Tile::ID(1, -3, 0).isChildOf(Tile::ID(0, -2, 0))); + ASSERT_TRUE(Tile::ID(1, -4, 0).isChildOf(Tile::ID(0, -2, 0))); + ASSERT_TRUE(Tile::ID(2, -1, 0).isChildOf(Tile::ID(0, -1, 0))); + ASSERT_TRUE(Tile::ID(2, -2, 0).isChildOf(Tile::ID(0, -1, 0))); + ASSERT_TRUE(Tile::ID(2, -3, 0).isChildOf(Tile::ID(0, -1, 0))); + ASSERT_TRUE(Tile::ID(2, -4, 0).isChildOf(Tile::ID(0, -1, 0))); + ASSERT_TRUE(Tile::ID(2, -5, 0).isChildOf(Tile::ID(0, -2, 0))); + ASSERT_TRUE(Tile::ID(2, -6, 0).isChildOf(Tile::ID(0, -2, 0))); + ASSERT_TRUE(Tile::ID(2, -7, 0).isChildOf(Tile::ID(0, -2, 0))); + ASSERT_TRUE(Tile::ID(2, -8, 0).isChildOf(Tile::ID(0, -2, 0))); + + ASSERT_FALSE(Tile::ID(4, -16, 0).isChildOf(Tile::ID(0, -2, 0))); + ASSERT_TRUE(Tile::ID(4, -17, 0).isChildOf(Tile::ID(0, -2, 0))); + + ASSERT_TRUE(Tile::ID(2, -1, 0).isChildOf(Tile::ID(1, -1, 0))); + ASSERT_TRUE(Tile::ID(2, -2, 0).isChildOf(Tile::ID(1, -1, 0))); + ASSERT_TRUE(Tile::ID(2, -3, 0).isChildOf(Tile::ID(1, -2, 0))); + ASSERT_TRUE(Tile::ID(2, -4, 0).isChildOf(Tile::ID(1, -2, 0))); + ASSERT_TRUE(Tile::ID(3, -1, 0).isChildOf(Tile::ID(1, -1, 0))); + ASSERT_TRUE(Tile::ID(3, -2, 0).isChildOf(Tile::ID(1, -1, 0))); + ASSERT_TRUE(Tile::ID(3, -3, 0).isChildOf(Tile::ID(1, -1, 0))); + ASSERT_TRUE(Tile::ID(3, -4, 0).isChildOf(Tile::ID(1, -1, 0))); + ASSERT_TRUE(Tile::ID(3, -5, 0).isChildOf(Tile::ID(1, -2, 0))); +}