Merge branch 'master' of github.com:mapbox/llmr-native

This commit is contained in:
Justin R. Miller
2014-03-25 14:39:43 -07:00
81 changed files with 33263 additions and 453 deletions

7
.gitignore vendored
View File

@@ -7,4 +7,9 @@
/ios/build
/config.gypi
/config.mk
/build
/build
/node_modules
/include/llmr/shader/shaders.hpp
/include/llmr/style/resources.hpp
/src/shader/shaders.cpp
/src/style/resources.cpp

View File

@@ -9,6 +9,10 @@ all: llmr
llmr: config.gypi src llmr.gyp
@if [ ! -f out ]; then echo 'please run ./configure first'; else $(MAKE) -C out BUILDTYPE=Release V=$(V) llmr-x86; fi;
gtest: config.gypi src llmr.gyp
deps/run_gyp llmr.gyp -Goutput_dir=./out/ --depth=. --generator-output=./build/gtest -f make
make -C build/gtest gtest V=$(V)
# build OS X app with pure make
app: config.gypi src macosx/llmr-app.gyp
deps/run_gyp macosx/llmr-app.gyp -Goutput_dir=./out/ --depth=. --generator-output=./build/macosx-make -f make
@@ -58,6 +62,13 @@ isim: config.gypi src ios/llmr-app.gyp
# does not work
#"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator" -SimulateApplication ios/build/Release-iphonesimulator/llmr.app/llmr
test: config.gypi src test/test.gyp
deps/run_gyp test/test.gyp -Goutput_dir=. --depth=. --generator-output=./build/test -f make
make -C build/test V=$(V)
@for FILE in build/test/Release/test_*; do \
$${FILE}; \
done
clean:
-rm -rf out
-rm -rf build
@@ -71,7 +82,4 @@ distclean:
-rm -rf macosx/llmr-app.xcodeproj
-rm -rf ios/llmr-app.xcodeproj
test: all
echo test
.PHONY: test linux

View File

@@ -4,12 +4,17 @@
var fs = require('fs');
var path = require('path');
module.exports = function() {
var glsl;
try { glsl = require('glsl-optimizer'); } catch(err) {}
module.exports = function(param) {
var name;
var shaders = {};
var shaderFiles = fs.readdirSync('src/shader');
// Load shaders
for (var i = 0; i < shaderFiles.length; i++) {
var parts = shaderFiles[i].match(/^(.+)\.(vertex|fragment)\.glsl$/);
if (parts) {
@@ -22,6 +27,48 @@ module.exports = function() {
}
}
// Optimize shader
if (glsl) {
var preamble = '';
var target = glsl.TARGET_OPENGL;
if (param == 'gles2') {
target = glsl.TARGET_OPENGLES20;
preamble = 'precision highp float;';
} else if (param == 'gles3') {
target = glsl.TARGET_OPENGLES30;
preamble = 'precision highp float;';
} else {
preamble = '#version 120';
}
var compiler = new glsl.Compiler(target);
for (name in shaders) {
var vertex_shader = new glsl.Shader(compiler, glsl.VERTEX_SHADER, preamble + '\n' + shaders[name].vertex);
if (vertex_shader.compiled()) {
shaders[name].vertex = vertex_shader.output();
} else {
console.warn('failed to optimize %s vertex shader', name);
process.exit(1);
}
vertex_shader.dispose();
var fragment_shader = new glsl.Shader(compiler, glsl.FRAGMENT_SHADER, preamble + '\n' + shaders[name].fragment);
if (fragment_shader.compiled()) {
shaders[name].fragment = fragment_shader.output();
} else {
console.warn('failed to optimize %s fragment shader', name);
process.exit(1);
}
fragment_shader.dispose();
}
compiler.dispose();
} else {
console.warn('Not optimizing shaders');
}
// Save to file
var lines = [];
var consts = [];
for (var name in shaders) {
@@ -69,6 +116,7 @@ module.exports = function() {
code += '\n};\n';
fs.writeFileSync('src/shader/shaders.cpp', code);
};
module.exports();
module.exports(process.argv[2]);

View File

@@ -87,10 +87,45 @@ module.exports = {
"value": ["Restaurant"],
"type": "point"
},
"country_label": {
"source": "streets",
"type": "text",
"layer": "country_label",
"feature_type": "point",
"padding": 10,
"text_field": "name",
"path": "horizontal",
"font": "Open Sans, Jomolhari, Siyam Rupali, Alef, Arial Unicode MS",
"fontSize": 16
},
"place_label": {
"source": "streets",
"type": "text",
"layer": "place_label",
"feature_type": "point",
"text_field": "name",
"path": "horizontal",
"font": "Open Sans, Jomolhari, Siyam Rupali, Alef, Arial Unicode MS",
"fontSize": 18,
"alwaysVisible": true
},
"road_label": {
"source": "streets",
"type": "text",
"layer": "road_label",
"feature_type": "line",
"text_field": "name",
"path": "curve",
"font": "Open Sans, Jomolhari, Siyam Rupali, Alef, Arial Unicode MS",
"fontSize": 12,
"textMinDistance": 250,
"maxAngleDelta": 1.04, // radians
},
},
"sprite": "img/maki-sprite",
"constants": {
"park": "#c8df9f",
"text": "#000000",
},
"structure": [
{ "name": "park", "bucket": "park" },
@@ -105,6 +140,9 @@ module.exports = {
{ "name": "embassy_poi", "bucket": "embassy_poi" },
{ "name": "park_poi", "bucket": "park_poi" },
{ "name": "restaurant_poi", "bucket": "restaurant_poi" },
{ "name": "country_label", "bucket": "country_label" },
{ "name": "place_label", "bucket": "place_label" },
{ "name": "road_label", "bucket": "road_label" },
],
"classes": [
{
@@ -156,8 +194,8 @@ module.exports = {
{ z: 0, val: 0.5 },
{ z: 13, val: 0.5 },
{ z: 16, val: 2 },
{ z: 20, val: 32 },
{ z: 30, val: 32 }
{ z: 20, val: 16 },
{ z: 30, val: 16 }
],
},
"road_large": {
@@ -169,8 +207,8 @@ module.exports = {
{ z: 11, val: 0.5 },
{ z: 13, val: 1 },
{ z: 16, val: 4 },
{ z: 20, val: 64 },
{ z: 30, val: 64 }
{ z: 20, val: 32 },
{ z: 30, val: 32 }
],
},
"alcohol_poi": {
@@ -208,6 +246,24 @@ module.exports = {
"image": "restaurant",
"opacity": [ "linear", 15, 0, 1.0, 0, 0.75 ]
},
"country_label": {
"type": "text",
"stroke": [ 1, 1, 1, 0.7 ],
"color": "text",
"size": 16
},
"place_label": {
"type": "text",
"stroke": [1,1,1,0.7],
"color": "text",
"size": 18
},
"road_label": {
"type": "text",
"color": "text",
"stroke": [1,1,1,0.7],
"size": ["exponential", 14, 8, 1, 8, 12]
},
}
}
]

View File

@@ -9,16 +9,19 @@
'cflags_cc': ['-std=c++11'],
'configurations': {
'Debug': {
'cflags': [ '-g', '-O0' ],
'cflags': [ '-g', '-O0', '-I<(boost_root)/include' ],
'defines': [ 'DEBUG' ],
'xcode_settings': {
'OTHER_CPLUSPLUSFLAGS': [ '-g']
'OTHER_CPLUSPLUSFLAGS': [ '-g', '-I<(boost_root)/include' ]
}
},
'Release': {
'cflags': [ '-O3'],
'defines': [ 'NDEBUG' ]
'cflags': [ '-O3', '-I<(boost_root)/include' ],
'defines': [ 'NDEBUG' ],
'xcode_settings': {
'OTHER_CPLUSPLUSFLAGS': [ '-I<(boost_root)/include' ]
}
}
}
}
}
}

View File

@@ -68,12 +68,10 @@ public:
glfwSetKeyCallback(window, key);
}
static void character(GLFWwindow *window, unsigned int codepoint) {
}
static void key(GLFWwindow *window, int key, int scancode, int action, int mods) {
MapView *mapView = (MapView *)glfwGetWindowUserPointer(window);
@@ -95,7 +93,6 @@ public:
}
}
static void scroll(GLFWwindow *window, double xoffset, double yoffset) {
MapView *mapView = (MapView *)glfwGetWindowUserPointer(window);
double delta = yoffset * 40;
@@ -234,3 +231,42 @@ public:
llmr::Settings& settings;
llmr::Map map;
};
namespace llmr {
namespace platform {
double time() {
return glfwGetTime();
}
void show_debug_image(std::string name, const char *data, size_t width, size_t height) {
static GLFWwindow *debug_window = nullptr;
if (!debug_window) {
debug_window = glfwCreateWindow(width, height, name.c_str(), nullptr, nullptr);
if (!debug_window) {
glfwTerminate();
fprintf(stderr, "Failed to initialize window\n");
exit(1);
}
}
GLFWwindow *current_window = glfwGetCurrentContext();
glfwSetWindowSize(debug_window, width, height);
glfwMakeContextCurrent(debug_window);
int fb_width, fb_height;
glfwGetFramebufferSize(debug_window, &fb_width, &fb_height);
float scale = (float)fb_width / (float)width;
glPixelZoom(scale, -scale);
glRasterPos2f(-1.0f, 1.0f);
glDrawPixels(width, height, GL_LUMINANCE, GL_UNSIGNED_BYTE, data);
glfwSwapBuffers(debug_window);
glfwMakeContextCurrent(current_window);
}
}
}

9
configure vendored
View File

@@ -19,6 +19,11 @@ parser.add_option("--debug",
dest="debug",
help="Also build debug build")
parser.add_option("--boost",
action="store",
dest="boost_root",
help="Path to boost (defaults to /usr/local)")
parser.add_option("--glfw3",
action="store",
dest="glfw3",
@@ -55,6 +60,10 @@ def pkg_config(pkg):
return (libs, cflags)
def configure_llmr(o):
if options.boost_root:
o['variables']['boost_root'] = options.boost_root
else:
o['variables']['boost_root'] = '/usr/local'
o['target_defaults']['default_configuration'] = 'Debug' if options.debug else 'Release'
def configure_glfw3(o):

9592
deps/gtest/gtest-all.cc vendored Normal file

File diff suppressed because it is too large Load Diff

14
deps/gtest/gtest.gyp vendored Normal file
View File

@@ -0,0 +1,14 @@
{
'targets': [{
'target_name': 'gtest',
'type': 'static_library',
'sources': [
'gtest-all.cc'
],
'direct_dependent_settings': {
'include_dirs': [
'.',
],
},
}]
}

20061
deps/gtest/gtest/gtest.h vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
#ifndef LLMR_GEOMETRY_ANCHOR
#define LLMR_GEOMETRY_ANCHOR
#include <vector>
namespace llmr {
struct Anchor {
float x = 0.0f;
float y = 0.0f;
float angle = 0.0f;
float scale = 0.0f;
int segment = -1;
explicit Anchor(float x, float y, float angle, float scale)
: x(x), y(y), angle(angle), scale(scale) {}
explicit Anchor(float x, float y, float angle, float scale, int segment)
: x(x), y(y), angle(angle), scale(scale), segment(segment) {}
};
typedef std::vector<Anchor> Anchors;
}
#endif

View File

@@ -2,23 +2,17 @@
#define LLMR_GEOMETRY_BINPACK
#include <llmr/util/noncopyable.hpp>
#include <llmr/util/rect.hpp>
#include <cstdint>
#include <list>
namespace llmr {
template <typename T>
struct Rect {
Rect(T x, T y, T w, T h) : x(x), y(y), w(w), h(h) {}
T x = 0, y = 0;
T w = 0, h = 0;
};
template <typename T>
class BinPack : private noncopyable {
class BinPack : private util::noncopyable {
public:
BinPack(T width, T height)
: free(1, { 0, 0, width, height }) {}
: free(1, Rect<uint16_t>{ 0, 0, width, height }) {}
public:
Rect<T> allocate(T width, T height) {
// Find the smallest free rect angle
@@ -35,7 +29,7 @@ public:
if (smallest == free.end()) {
// There's no space left for this char.
return { 0, 0, 0, 0 };
return Rect<uint16_t>{ 0, 0, 0, 0 };
} else {
Rect<T> rect = *smallest;
free.erase(smallest);
@@ -60,7 +54,7 @@ public:
if (rect.h > height) free.emplace_back(rect.x, rect.y + height, width, rect.h - height);
}
return { rect.x, rect.y, width, height };
return Rect<uint16_t>{ rect.x, rect.y, width, height };
}
}

View File

@@ -4,6 +4,7 @@
#include <cstdlib>
#include <cassert>
#include <llmr/platform/gl.hpp>
#include <llmr/util/noncopyable.hpp>
namespace llmr {
@@ -12,7 +13,7 @@ template <
int bufferType = GL_ARRAY_BUFFER,
size_t defaultLength = 8192
>
class Buffer {
class Buffer : private util::noncopyable {
public:
~Buffer() {
if (array) {
@@ -47,7 +48,7 @@ public:
protected:
// increase the buffer size by at least /required/ bytes.
void *addElement() {
inline void *addElement() {
assert("Buffer is already bound to GPU" && buffer == 0);
if (length < pos + itemSize) {
while (length < pos + itemSize) length += defaultLength;

View File

@@ -0,0 +1,52 @@
#ifndef LLMR_GEOMETRY_GLYPH_ATLAS
#define LLMR_GEOMETRY_GLYPH_ATLAS
#include <llmr/geometry/binpack.hpp>
#include <string>
#include <set>
#include <map>
#include <mutex>
#include <atomic>
namespace llmr {
class VectorTileGlyph;
class GlyphAtlas {
public:
private:
struct GlyphValue {
GlyphValue(const Rect<uint16_t>& rect, uint64_t id)
: rect(rect), ids({ id }) {}
Rect<uint16_t> rect;
std::set<uint64_t> ids;
};
public:
GlyphAtlas(uint16_t width, uint16_t height);
~GlyphAtlas();
Rect<uint16_t> addGlyph(uint64_t tile_id, const std::string& face_name,
const VectorTileGlyph& glyph);
void removeGlyphs(uint64_t tile_id);
void bind();
public:
const uint16_t width = 0;
const uint16_t height = 0;
private:
std::mutex mtx;
BinPack<uint16_t> bin;
std::map<std::string, std::map<uint32_t, GlyphValue>> index;
char *const data = nullptr;
std::atomic<bool> dirty;
uint32_t texture = 0;
};
};
#endif

View File

@@ -0,0 +1,13 @@
#ifndef LLMR_GEOMETRY_INTERPOLATE
#define LLMR_GEOMETRY_INTERPOLATE
#include <llmr/geometry/anchor.hpp>
#include <llmr/util/math.hpp>
namespace llmr {
Anchors interpolate(const std::vector<Coordinate> &vertices, float spacing,
float minScale = 0.0f, int start = 0);
}
#endif

View File

@@ -0,0 +1,25 @@
#ifndef LLMR_GEOMETRY_TEXT_BUFFER
#define LLMR_GEOMETRY_TEXT_BUFFER
#include "buffer.hpp"
#include <array>
namespace llmr {
class TextVertexBuffer : public Buffer <
16,
GL_ARRAY_BUFFER,
32768
> {
public:
typedef int16_t vertex_type;
static const double angleFactor;
size_t add(int16_t x, int16_t y, float ox, float oy, uint16_t tx, uint16_t ty, float angle, float minzoom, std::array<float, 2> range, float maxzoom, float labelminzoom);
};
}
#endif

View File

@@ -5,6 +5,8 @@
#include <llmr/map/tile_data.hpp>
#include <llmr/map/transform.hpp>
#include <llmr/style/style.hpp>
#include <llmr/style/style.hpp>
#include <llmr/geometry/glyph_atlas.hpp>
#include <llmr/renderer/painter.hpp>
#include <llmr/util/noncopyable.hpp>
#include <llmr/util/texturepool.hpp>
@@ -76,6 +78,7 @@ private:
Transform transform;
Texturepool texturepool;
Style style;
GlyphAtlas glyphAtlas;
Painter painter;
int32_t min_zoom;

View File

@@ -5,10 +5,21 @@
#include <llmr/util/vec.hpp>
#include <llmr/util/mat4.hpp>
#include <llmr/util/noncopyable.hpp>
#include <llmr/geometry/glyph_atlas.hpp>
#include <llmr/geometry/debug_font_buffer.hpp>
#include <llmr/geometry/vao.hpp>
#include <llmr/map/vector_tile.hpp>
#include <llmr/platform/platform.hpp>
#include <llmr/renderer/bucket.hpp>
#include <llmr/geometry/vertex_buffer.hpp>
#include <llmr/geometry/elements_buffer.hpp>
#include <llmr/geometry/fill_buffer.hpp>
#include <llmr/geometry/line_buffer.hpp>
#include <llmr/geometry/point_buffer.hpp>
#include <llmr/geometry/text_buffer.hpp>
#include <cstdint>
#include <forward_list>
#include <string>
@@ -21,17 +32,10 @@ namespace llmr {
class Style;
class Raster;
class Bucket;
class LayerDescription;
class BucketDescription;
class VectorTile;
class VectorTileLayer;
class FillVertexBuffer;
class LineVertexBuffer;
class PointVertexBuffer;
class TriangleElementsBuffer;
class LineElementsBuffer;
class PointElementsBuffer;
class PlainShader;
class TileData : public std::enable_shared_from_this<TileData>,
@@ -53,20 +57,11 @@ public:
};
public:
TileData(Tile::ID id, const Style& style, const bool use_raster = false, const bool use_retina = false);
TileData(Tile::ID id, const Style& style, GlyphAtlas& glyphAtlas, const bool use_raster = false, const bool use_retina = false);
~TileData();
// Start loading the tile.
void request();
// Other functions
bool parse();
void parseStyleLayers(const VectorTile& tile, const std::vector<LayerDescription>& layers);
std::shared_ptr<Bucket> createBucket(const VectorTile& tile, const BucketDescription& bucket_desc);
std::shared_ptr<Bucket> createFillBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
std::shared_ptr<Bucket> createLineBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
std::shared_ptr<Bucket> createPointBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
void cancel();
const std::string toString() const;
@@ -82,21 +77,24 @@ public:
DebugFontBuffer debugFontBuffer;
VertexArrayObject debugFontArray;
std::shared_ptr<FillVertexBuffer> fillVertexBuffer;
std::shared_ptr<LineVertexBuffer> lineVertexBuffer;
std::shared_ptr<PointVertexBuffer> pointVertexBuffer;
FillVertexBuffer fillVertexBuffer;
LineVertexBuffer lineVertexBuffer;
PointVertexBuffer pointVertexBuffer;
TextVertexBuffer textVertexBuffer;
std::shared_ptr<TriangleElementsBuffer> triangleElementsBuffer;
std::shared_ptr<LineElementsBuffer> lineElementsBuffer;
std::shared_ptr<PointElementsBuffer> pointElementsBuffer;
TriangleElementsBuffer triangleElementsBuffer;
LineElementsBuffer lineElementsBuffer;
PointElementsBuffer pointElementsBuffer;
// Holds the buckets of this tile.
// They contain the location offsets in the buffers stored above
std::map<std::string, std::shared_ptr<Bucket>> buckets;
std::map<std::string, std::unique_ptr<Bucket>> buckets;
private:
// Source data
std::string data;
const Style& style;
GlyphAtlas& glyphAtlas;
platform::Request *req = nullptr;
};

View File

@@ -0,0 +1,35 @@
#ifndef LLMR_MAP_TILE_PARSER
#define LLMR_MAP_TILE_PARSER
#include <llmr/map/tile_data.hpp>
#include <llmr/text/placement.hpp>
namespace llmr {
class TileParser {
public:
TileParser(const std::string& data, TileData& tile, const Style& style, GlyphAtlas& glyphAtlas);
private:
bool obsolete() const;
void parseGlyphs();
void parseStyleLayers(const std::vector<LayerDescription>& layers);
std::unique_ptr<Bucket> createBucket(const BucketDescription& bucket_desc);
std::unique_ptr<Bucket> createFillBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
std::unique_ptr<Bucket> createLineBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
std::unique_ptr<Bucket> createPointBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
std::unique_ptr<Bucket> createTextBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc);
template <class Bucket> void addBucketFeatures(Bucket& bucket, const VectorTileLayer& layer, const BucketDescription& bucket_desc);
private:
const VectorTile data;
TileData& tile;
const Style& style;
GlyphAtlas& glyphAtlas;
Faces faces;
Placement placement;
};
}
#endif

View File

@@ -37,6 +37,7 @@ public:
// Getters
void matrixFor(mat4& matrix, const Tile::ID& id) const;
float getZoom() const;
float getNormalizedZoom() const;
int32_t getIntegerZoom() const;
double getScale() const;
double getAngle() const;

View File

@@ -2,7 +2,9 @@
#define LLMR_MAP_VECTOR_TILE
#include <llmr/util/pbf.hpp>
#include <llmr/util/vec.hpp>
#include <llmr/style/value.hpp>
#include <llmr/text/glyph.hpp>
#include <vector>
#include <map>
#include <set>
@@ -15,24 +17,27 @@ class VectorTileLayer;
struct pbf;
struct Coordinate {
struct null {};
Coordinate() : x(0), y(0) {}
Coordinate(int16_t x, int16_t y) : x(x), y(y) {}
Coordinate(null) : x(std::numeric_limits<int16_t>::min()), y(std::numeric_limits<int16_t>::min()) {}
int16_t x;
int16_t y;
inline bool operator==(const Coordinate& other) const {
return x == other.x && y == other.y;
}
inline operator bool() const {
return x != std::numeric_limits<int16_t>::min() && y != std::numeric_limits<int16_t>::min();
}
enum class FeatureType {
Unknown = 0,
Point = 1,
LineString = 2,
Polygon = 3
};
std::ostream& operator<<(std::ostream&, const FeatureType& type);
class VectorTileFeature {
public:
VectorTileFeature(pbf feature, const VectorTileLayer& layer);
uint64_t id = 0;
FeatureType type = FeatureType::Unknown;
std::map<std::string, Value> properties;
pbf geometry;
};
std::ostream& operator<<(std::ostream&, const VectorTileFeature& feature);
/*
* Allows iterating over the features of a VectorTileLayer using a
* BucketDescription as filter. Only features matching the descriptions will
@@ -67,6 +72,8 @@ private:
std::set<uint32_t> values;
};
std::ostream& operator<<(std::ostream&, const GlyphPlacement& placement);
class VectorTileLayer {
public:
VectorTileLayer(pbf data);
@@ -76,8 +83,38 @@ public:
uint32_t extent = 4096;
std::vector<std::string> keys;
std::vector<Value> values;
std::vector<std::string> faces;
std::map<std::string, std::map<Value, Shaping>> shaping;
};
class VectorTileGlyph {
public:
VectorTileGlyph();
VectorTileGlyph(pbf data);
uint32_t id = 0;
// A signed distance field of the glyph with a border of 3 pixels.
std::string bitmap;
// Glyph metrics
GlyphMetrics metrics;
};
std::ostream& operator<<(std::ostream&, const VectorTileGlyph& glyph);
class VectorTileFace {
public:
VectorTileFace(pbf data);
std::string name;
std::string family;
std::string style;
std::vector<VectorTileGlyph> glyphs;
};
std::ostream& operator<<(std::ostream&, const VectorTileFace& face);
class VectorTile {
public:
VectorTile();
@@ -85,8 +122,11 @@ public:
VectorTile& operator=(VectorTile&& other);
std::map<std::string, const VectorTileLayer> layers;
std::map<std::string, const VectorTileFace> faces;
};
}
#endif

View File

@@ -37,6 +37,9 @@ double time();
// Performs routine cleanup operation and is called on every loop iteration.
void cleanup();
// Shows an RGBA image with the specified dimensions in a named window.
void show_debug_image(std::string name, const char *data, size_t width, size_t height);
}
}

View File

@@ -26,7 +26,6 @@ class BucketDescription;
class OutlineShader;
class PlainShader;
class PatternShader;
struct Coordinate;
struct pbf;
class FillBucket : public Bucket {
@@ -39,9 +38,9 @@ class FillBucket : public Bucket {
typedef ElementGroup triangle_group_type;
typedef ElementGroup line_group_type;
public:
FillBucket(const std::shared_ptr<FillVertexBuffer>& vertexBuffer,
const std::shared_ptr<TriangleElementsBuffer>& triangleElementsBuffer,
const std::shared_ptr<LineElementsBuffer>& lineElementsBuffer,
FillBucket(FillVertexBuffer& vertexBuffer,
TriangleElementsBuffer& triangleElementsBuffer,
LineElementsBuffer& lineElementsBuffer,
const BucketDescription& bucket_desc);
~FillBucket();
@@ -63,9 +62,9 @@ private:
TESStesselator *tesselator;
ClipperLib::Clipper clipper;
std::shared_ptr<FillVertexBuffer> vertexBuffer;
std::shared_ptr<TriangleElementsBuffer> triangleElementsBuffer;
std::shared_ptr<LineElementsBuffer> lineElementsBuffer;
FillVertexBuffer& vertexBuffer;
TriangleElementsBuffer& triangleElementsBuffer;
LineElementsBuffer& lineElementsBuffer;
// hold information on where the vertices are located in the FillBuffer
const size_t vertex_start;

View File

@@ -0,0 +1,31 @@
#ifndef LLMR_RENDERER_FRAME_HISTORY
#define LLMR_RENDERER_FRAME_HISTORY
#include <deque>
#include <cassert>
#include <cmath>
#include <llmr/platform/platform.hpp>
namespace llmr {
struct FrameSnapshot {
explicit inline FrameSnapshot(float time, float z) : time(time), z(z) {}
float time;
float z;
};
class FrameHistory {
public:
// Record frame history that will be used to calculate fading params
void record(float zoom);
bool needsAnimation(float duration) const;
public:
std::deque<FrameSnapshot> history;
};
}
#endif

View File

@@ -5,6 +5,7 @@
#include <llmr/style/bucket_description.hpp>
#include <llmr/geometry/vao.hpp>
#include <llmr/geometry/elements_buffer.hpp>
#include <llmr/geometry/line_buffer.hpp>
#include <memory>
#include <vector>
@@ -15,16 +16,15 @@ class LineVertexBuffer;
class TriangleElementsBuffer;
class LineShader;
class LinejoinShader;
struct Coordinate;
struct pbf;
class LineBucket : public Bucket {
typedef ElementGroup triangle_group_type;
typedef ElementGroup point_group_type;
public:
LineBucket(const std::shared_ptr<LineVertexBuffer>& vertexBuffer,
const std::shared_ptr<TriangleElementsBuffer>& triangleElementsBuffer,
const std::shared_ptr<PointElementsBuffer>& pointElementsBuffer,
LineBucket(LineVertexBuffer& vertexBuffer,
TriangleElementsBuffer& triangleElementsBuffer,
PointElementsBuffer& pointElementsBuffer,
const BucketDescription& bucket_desc);
virtual void render(Painter& painter, const std::string& layer_name, const Tile::ID& id);
@@ -43,9 +43,9 @@ public:
private:
std::shared_ptr<LineVertexBuffer> vertexBuffer;
std::shared_ptr<TriangleElementsBuffer> triangleElementsBuffer;
std::shared_ptr<PointElementsBuffer> pointElementsBuffer;
LineVertexBuffer& vertexBuffer;
TriangleElementsBuffer& triangleElementsBuffer;
PointElementsBuffer& pointElementsBuffer;
const size_t vertex_start;
const size_t triangle_elements_start;

View File

@@ -6,6 +6,7 @@
#include <llmr/geometry/vertex_buffer.hpp>
#include <llmr/util/mat4.hpp>
#include <llmr/util/noncopyable.hpp>
#include <llmr/renderer/frame_history.hpp>
#include <llmr/shader/plain_shader.hpp>
#include <llmr/shader/outline_shader.hpp>
@@ -14,6 +15,7 @@
#include <llmr/shader/linejoin_shader.hpp>
#include <llmr/shader/point_shader.hpp>
#include <llmr/shader/raster_shader.hpp>
#include <llmr/shader/text_shader.hpp>
namespace llmr {
@@ -25,10 +27,11 @@ class Tile;
class FillBucket;
class LineBucket;
class PointBucket;
class TextBucket;
class Painter : private util::noncopyable {
public:
Painter(Transform& transform, Settings& settings, Style& style);
Painter(Transform& transform, Settings& settings, Style& style, GlyphAtlas& glyphAtlas);
void setup();
void clear();
@@ -38,12 +41,15 @@ public:
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 resize(int width, int height);
void prepareClippingMask();
void drawClippingMask(const mat4& matrix, uint8_t clip_id, bool opaque = true);
void finishClippingMask();
bool needsAnimation() const;
private:
void setupShaders();
void renderRaster(const std::shared_ptr<TileData>& tile);
@@ -65,8 +71,9 @@ private:
Transform& transform;
Settings& settings;
Style& style;
GlyphAtlas& glyphAtlas;
FrameHistory frameHistory;
uint32_t gl_program = 0;
float gl_lineWidth = 0;
@@ -82,6 +89,7 @@ private:
std::unique_ptr<PatternShader> patternShader;
std::unique_ptr<PointShader> pointShader;
std::unique_ptr<RasterShader> rasterShader;
std::unique_ptr<TextShader> textShader;
// Set up the stencil quad we're using to generate the stencil mask.
VertexBuffer tileStencilBuffer = {

View File

@@ -19,12 +19,11 @@ class Style;
class PointVertexBuffer;
class BucketDescription;
class PointShader;
struct Coordinate;
struct pbf;
class PointBucket : public Bucket {
public:
PointBucket(const std::shared_ptr<PointVertexBuffer>& vertexBuffer,
PointBucket(PointVertexBuffer& vertexBuffer,
const BucketDescription& bucket_desc);
virtual void render(Painter& painter, const std::string& layer_name, const Tile::ID& id);
@@ -39,7 +38,8 @@ public:
const BucketGeometryDescription geometry;
private:
std::shared_ptr<PointVertexBuffer> vertexBuffer;
PointVertexBuffer& vertexBuffer;
VertexArrayObject array;
const size_t vertex_start;

View File

@@ -0,0 +1,61 @@
#ifndef LLMR_RENDERER_TEXTBUCKET
#define LLMR_RENDERER_TEXTBUCKET
#include "bucket.hpp"
#include <llmr/style/bucket_description.hpp>
#include <llmr/geometry/vao.hpp>
#include <llmr/geometry/elements_buffer.hpp>
#include <llmr/map/vector_tile.hpp>
#include <llmr/text/types.hpp>
#include <memory>
#include <map>
#include <vector>
namespace llmr {
class Style;
class TextVertexBuffer;
class TriangleElementsBuffer;
class TextShader;
class Placement;
struct pbf;
class TextBucket : public Bucket {
typedef ElementGroup triangle_group_type;
public:
TextBucket(
TextVertexBuffer &vertexBuffer,
TriangleElementsBuffer &triangleElementsBuffer,
const BucketDescription &bucket_desc, Placement &placement);
virtual void render(Painter &painter, const std::string &layer_name,
const Tile::ID &id);
void addGlyphs(const PlacedGlyphs &glyphs, float placementZoom,
PlacementRange placementRange, float zoom);
void addFeature(const VectorTileFeature &feature,
const IndexedFaces &faces,
const std::map<Value, Shaping> &shapings);
bool empty() const;
void drawGlyphs(TextShader &shader);
public:
const BucketGeometryDescription &geom_desc;
private:
TextVertexBuffer& vertexBuffer;
TriangleElementsBuffer& triangleElementsBuffer;
Placement &placement;
const size_t vertex_start;
const size_t triangle_elements_start;
std::vector<triangle_group_type> triangleGroups;
};
}
#endif

View File

@@ -1,28 +0,0 @@
// NOTE: DO NOT CHANGE THIS FILE. IT IS AUTOMATICALLY GENERATED.
#ifndef LLMR_SHADER_SHADERS
#define LLMR_SHADER_SHADERS
namespace llmr {
struct shader_source {
const char *vertex;
const char *fragment;
};
enum {
LINE_SHADER,
LINEJOIN_SHADER,
OUTLINE_SHADER,
PATTERN_SHADER,
PLAIN_SHADER,
POINT_SHADER,
RASTER_SHADER,
SHADER_COUNT
};
extern const shader_source shaders[SHADER_COUNT];
}
#endif

View File

@@ -0,0 +1,72 @@
#ifndef LLMR_SHADER_TEXT_SHADER
#define LLMR_SHADER_TEXT_SHADER
#include "shader.hpp"
namespace llmr {
class TextShader : public Shader {
public:
TextShader();
void bind(char *offset);
void setColor(float r, float g, float b, float a);
void setColor(const std::array<float, 4> &color);
void setBuffer(float buffer);
void setGamma(float gamma);
void setExtrudeMatrix(const std::array<float, 16> &exmatrix);
void setAngle(float angle);
void setZoom(float zoom);
void setFlip(float flip);
void setFadeDist(float fadedist);
void setMinFadeZoom(float minfadezoom);
void setMaxFadeZoom(float maxfadezoom);
void setFadeZoom(float fadezoom);
void setTextureSize(const std::array<float, 2> &texsize);
private:
int32_t a_pos = -1;
int32_t a_offset = -1;
int32_t a_data1 = -1;
int32_t a_data2 = -1;
std::array<float, 4> color = {{}};
int32_t u_color = -1;
float buffer = 0.0f;
int32_t u_buffer = -1;
float gamma = 0.0f;
int32_t u_gamma = -1;
std::array<float, 16> exmatrix = {{}};
int32_t u_exmatrix = -1;
float angle = 0.0f;
int32_t u_angle = -1;
float zoom = 0.0f;
int32_t u_zoom = -1;
float flip = 0.0f;
int32_t u_flip = -1;
float fadedist = 0.0f;
int32_t u_fadedist = -1;
float minfadezoom = 0.0f;
int32_t u_minfadezoom = -1;
float maxfadezoom = 0.0f;
int32_t u_maxfadezoom = -1;
float fadezoom = 0.0f;
int32_t u_fadezoom = -1;
std::array<float, 2> texsize = {{}};
int32_t u_texsize = -1;
};
}
#endif

View File

@@ -3,6 +3,7 @@
#include <string>
#include <vector>
#include <cmath>
#include "value.hpp"
@@ -12,7 +13,8 @@ enum class BucketType {
None = 0,
Fill = 1,
Line = 2,
Point = 3
Point = 3,
Text = 4
};
enum class CapType {
@@ -29,11 +31,17 @@ enum class JoinType {
Round = 3
};
enum class TextPathType {
Horizontal = 0,
Curve = 1
};
inline BucketType bucketType(const std::string& type) {
if (type == "fill") return BucketType::Fill;
else if (type == "line") return BucketType::Line;
else if (type == "point") return BucketType::Point;
else if (type == "text") return BucketType::Text;
else return BucketType::None;
}
@@ -52,18 +60,32 @@ inline JoinType joinType(const std::string& join) {
else return JoinType::None;
}
inline TextPathType textPathType(const std::string& path) {
if (path == "horizontal") return TextPathType::Horizontal;
else if (path == "curve") return TextPathType::Curve;
else return TextPathType::Horizontal;
};
class BucketGeometryDescription {
public:
CapType cap = CapType::None;
JoinType join = JoinType::None;
std::string font;
std::string text_field;
float font_size = 0.0f;
float miter_limit = 2.0f;
float round_limit = 1.0f;
TextPathType path = TextPathType::Horizontal;
float padding = 2.0f;
float textMinDistance = 250.0f;
float rotate = 0.0f; // what is this?
float maxAngleDelta = M_PI;
bool alwaysVisible = false;
};
class BucketDescription {
public:
BucketType feature_type = BucketType::None;
BucketType type = BucketType::None;
// Specify what data to pull into this bucket

View File

@@ -14,6 +14,7 @@ public:
std::map<std::string, FillClass> fill;
std::map<std::string, LineClass> line;
std::map<std::string, PointClass> point;
std::map<std::string, TextClass> text;
};

View File

@@ -45,6 +45,9 @@ bool stops(float z, const std::vector<bool>& values);
float linear(float z, const std::vector<float>& values);
bool linear(float z, const std::vector<bool>& values);
float exponential(float z, const std::vector<float>& values);
bool exponential(float z, const std::vector<bool>& values);
}
@@ -61,7 +64,7 @@ struct FunctionProperty {
};
struct PointClass {
FunctionProperty<bool> hidden;
FunctionProperty<bool> enabled = true;
FunctionProperty<float> size;
Color color = {{ 0, 0, 0, 1 }};
FunctionProperty<float> opacity = 1;
@@ -69,7 +72,7 @@ struct PointClass {
};
struct PointProperties {
bool hidden = false;
bool enabled = true;
float size = 0;
Color color = {{ 0, 0, 0, 1 }};
float opacity = 1.0;
@@ -77,7 +80,7 @@ struct PointProperties {
};
struct LineClass {
FunctionProperty<bool> hidden;
FunctionProperty<bool> enabled = true;
FunctionProperty<float> width;
FunctionProperty<float> offset;
Color color = {{ 0, 0, 0, 1 }};
@@ -85,7 +88,7 @@ struct LineClass {
};
struct LineProperties {
bool hidden = false;
bool enabled = true;
float width = 0;
float offset = 0;
Color color = {{ 0, 0, 0, 1 }};
@@ -93,7 +96,7 @@ struct LineProperties {
};
struct FillClass {
FunctionProperty<bool> hidden;
FunctionProperty<bool> enabled = true;
Winding winding = Winding::NonZero;
FunctionProperty<bool> antialias = true;
Color fill_color = {{ 0, 0, 0, 1 }};
@@ -103,7 +106,7 @@ struct FillClass {
};
struct FillProperties {
bool hidden = false;
bool enabled = true;
Winding winding = Winding::NonZero;
bool antialias = true;
Color fill_color = {{ 0, 0, 0, 1 }};
@@ -112,6 +115,26 @@ struct FillProperties {
std::string image;
};
struct TextClass {
FunctionProperty<bool> enabled = true;
Color color = {{ 0, 0, 0, 1 }};
Color halo = {{ 1, 1, 1, 0.75 }};
FunctionProperty<float> haloRadius = 0.25f;
FunctionProperty<float> size = 12.0f;
FunctionProperty<float> rotate = 0.0f;
FunctionProperty<bool> alwaysVisible = false;
};
struct TextProperties {
bool enabled = true;
Color color = {{ 0, 0, 0, 1 }};
Color halo = {{ 1, 1, 1, 0.75 }};
float haloRadius = 0.25f;
float size = 12.0f;
float rotate = 0.0f;
bool alwaysVisible = false;
};
struct BackgroundClass {
Color color = {{ 1, 1, 1, 1 }};
};

View File

@@ -1,15 +0,0 @@
// NOTE: DO NOT CHANGE THIS FILE. IT IS AUTOMATICALLY GENERATED.
#ifndef LLMR_STYLE_RESOURCES
#define LLMR_STYLE_RESOURCES
namespace llmr {
namespace resources {
extern const unsigned char style[];
extern const unsigned long style_size;
}
}
#endif

View File

@@ -56,6 +56,7 @@ public:
std::map<std::string, FillProperties> fills;
std::map<std::string, LineProperties> lines;
std::map<std::string, PointProperties> points;
std::map<std::string, TextProperties> texts;
} computed;
};

View File

@@ -23,6 +23,7 @@ private:
FillClass parseFillClass(JSVal value);
LineClass parseLineClass(JSVal value);
PointClass parsePointClass(JSVal value);
TextClass parseTextClass(JSVal value);
BackgroundClass parseBackgroundClass(JSVal value);
bool parseBoolean(JSVal value);

View File

@@ -6,7 +6,9 @@
namespace llmr {
typedef ::util::variant<bool, int64_t, uint64_t, double, std::string> Value;
typedef util::variant<bool, int64_t, uint64_t, double, std::string> Value;
std::string toString(const Value& value);
Value parseValue(pbf data);

View File

@@ -0,0 +1,31 @@
#ifndef LLMR_TEXT_COLLISION
#define LLMR_TEXT_COLLISION
#include <llmr/text/types.hpp>
namespace llmr {
class Collision {
public:
Collision();
~Collision();
PlacementProperty place(const PlacedGlyphs &boxes,
const CollisionAnchor &anchor,
float minPlacementScale, float maxPlacementScale,
float padding, bool horizontal, bool alwaysVisible);
float getPlacementScale(const GlyphBoxes &glyphs, float minPlacementScale,
float maxPlacementScale, float pad);
PlacementRange getPlacementRange(const GlyphBoxes &glyphs,
float placementScale);
void insert(const GlyphBoxes &glyphs, const CollisionAnchor &anchor,
float placementScale, const PlacementRange &placementRange,
bool horizontal, float padding);
private:
void *tree;
};
}
#endif

View File

@@ -0,0 +1,58 @@
#ifndef LLMR_TEXT_GLYPH
#define LLMR_TEXT_GLYPH
#include <llmr/util/rect.hpp>
#include <cstdint>
#include <vector>
#include <map>
namespace llmr {
struct GlyphMetrics {
operator bool() const {
return width == 0 && height == 0 && advance == 0;
}
// Glyph metrics.
uint32_t width = 0;
uint32_t height = 0;
int32_t left = 0;
int32_t top = 0;
uint32_t advance = 0;
};
struct Glyph {
inline explicit Glyph() : rect(0, 0, 0, 0), metrics() {}
inline explicit Glyph(const Rect<uint16_t> &rect,
const GlyphMetrics &metrics)
: rect(rect), metrics(metrics) {}
operator bool() const {
return !metrics && !rect;
}
const Rect<uint16_t> rect;
const GlyphMetrics metrics;
};
typedef std::map<uint32_t, Glyph> GlyphPositions;
typedef std::map<std::string, GlyphPositions> Faces;
typedef std::vector<const GlyphPositions *> IndexedFaces;
class GlyphPlacement {
public:
inline explicit GlyphPlacement(uint32_t face, uint32_t glyph, uint32_t x, uint32_t y)
: face(face), glyph(glyph), x(x), y(y) {}
uint32_t face = 0;
uint32_t glyph = 0;
uint32_t x = 0;
uint32_t y = 0;
};
typedef std::vector<GlyphPlacement> Shaping;
}
#endif

View File

@@ -0,0 +1,39 @@
#ifndef LLMR_TEXT_PLACEMENT
#define LLMR_TEXT_PLACEMENT
#include <llmr/text/collision.hpp>
#include <llmr/geometry/geometry.hpp>
#include <llmr/util/vec.hpp>
#include <llmr/text/glyph.hpp>
#include <vector>
namespace llmr {
class TextBucket;
class BucketGeometryDescription;
class Placement {
public:
Placement(int8_t zoom);
void addFeature(TextBucket &bucket, const std::vector<Coordinate> &line,
const BucketGeometryDescription &info,
const IndexedFaces &faces,
const Shaping &shaping);
float measureText(const IndexedFaces &faces,
const Shaping &shaping);
private:
const int8_t zoom;
Collision collision;
const float zOffset;
const float maxPlacementScale;
public:
static const int tileExtent;
static const int glyphSize;
static const float minScale;
};
}
#endif

View File

@@ -0,0 +1,54 @@
#ifndef LLMR_TEXT_ROTATION_RANGE
#define LLMR_TEXT_ROTATION_RANGE
#include <llmr/util/math.hpp>
#include <llmr/text/types.hpp>
#include <vector>
#include <cassert>
namespace llmr {
/*
* Combine an array of collision ranges to form a continuous
* range that includes 0. Collisions within the ignoreRange are ignored
*/
CollisionRange mergeCollisions(const CollisionList &collisions,
PlacementRange ignoreRange);
/*
* Calculate collision ranges for two rotating boxes.e
*/
CollisionList rotatingRotatingCollisions(const CollisionRect &a,
const CollisionRect &b,
const CollisionAnchor &anchorToAnchor);
/*
* Return the intersection points of a circle and a line segment;
*/
void circleEdgeCollisions(std::back_insert_iterator<CollisionAngles> angles,
const CollisionPoint &corner, float radius,
const CollisionPoint &p1, const CollisionPoint &p2);
/*
* Calculate the ranges for which the corner,
* rotatated around the anchor, is within the box;
*/
void cornerBoxCollisions(std::back_insert_iterator<CollisionList> collisions,
const CollisionPoint &corner,
const CollisionCorners &boxCorners, bool flip = false);
/*
* Calculate collision ranges for a rotating box and a fixed box;
*/
CollisionList rotatingFixedCollisions(const CollisionRect &rotating,
const CollisionRect &fixed);
/*
* Calculate the range a box conflicts with a second box
*/
CollisionRange rotationRange(const GlyphBox &inserting,
const PlacementBox &blocker, float scale);
}
#endif

113
include/llmr/text/types.hpp Normal file
View File

@@ -0,0 +1,113 @@
#ifndef LLMR_TEXT_TYPES
#define LLMR_TEXT_TYPES
#include <llmr/util/vec.hpp>
#include <llmr/util/rect.hpp>
#include <array>
#include <vector>
namespace llmr {
typedef vec2<float> CollisionPoint;
typedef vec2<float> CollisionAnchor;
typedef std::array<float, 2> PlacementRange;
typedef float CollisionAngle;
typedef std::vector<CollisionAngle> CollisionAngles;
typedef std::array<CollisionAngle, 2> CollisionRange;
typedef std::vector<CollisionRange> CollisionList;
typedef std::array<CollisionPoint, 4> CollisionCorners;
struct CollisionRect {
CollisionPoint tl;
CollisionPoint br;
inline explicit CollisionRect() {}
inline explicit CollisionRect(CollisionPoint::Type ax,
CollisionPoint::Type ay,
CollisionPoint::Type bx,
CollisionPoint::Type by)
: tl(ax, ay), br(bx, by) {}
inline explicit CollisionRect(const CollisionPoint &tl,
const CollisionPoint &br)
: tl(tl), br(br) {}
};
// These are the glyph boxes that we want to have placed.
struct GlyphBox {
explicit GlyphBox() {}
explicit GlyphBox(const CollisionRect &bbox, const CollisionRect &box,
float minScale)
: bbox(bbox), box(box), minScale(minScale) {}
explicit GlyphBox(const CollisionRect &box, float minScale, float maxScale,
const CollisionAnchor &anchor, bool rotate)
: anchor(anchor),
box(box),
rotate(rotate),
minScale(minScale),
maxScale(maxScale) {}
CollisionAnchor anchor;
CollisionRect bbox;
CollisionRect box;
bool rotate = false;
float minScale = 0.0f;
float maxScale = std::numeric_limits<float>::infinity();
};
typedef std::vector<GlyphBox> GlyphBoxes;
// TODO: Transform the vec2<float>s to vec2<int16_t> to save bytes
struct PlacedGlyph {
explicit PlacedGlyph(const vec2<float> &tl, const vec2<float> &tr,
const vec2<float> &bl, const vec2<float> &br,
const Rect<uint16_t> &tex, uint32_t width,
uint32_t height, float angle, const GlyphBox &glyphBox)
: tl(tl),
tr(tr),
bl(bl),
br(br),
tex(tex),
width(width),
height(height),
angle(angle),
glyphBox(glyphBox) {}
vec2<float> tl, tr, bl, br;
Rect<uint16_t> tex;
uint32_t width, height;
float angle;
GlyphBox glyphBox;
};
typedef std::vector<PlacedGlyph> PlacedGlyphs;
// These are the placed boxes contained in the rtree.
struct PlacementBox {
CollisionAnchor anchor;
CollisionRect bbox;
CollisionRect box;
bool rotate = false;
PlacementRange placementRange = {{0.0f, 0.0f}};
float placementScale = 0.0f;
float maxScale = std::numeric_limits<float>::infinity();
float padding = 0.0f;
};
struct PlacementProperty {
explicit PlacementProperty() {}
explicit PlacementProperty(float zoom, const PlacementRange &rotationRange)
: zoom(zoom), rotationRange(rotationRange) {}
inline operator bool() const {
return !isnan(zoom) && zoom != std::numeric_limits<float>::infinity() &&
rotationRange[0] != rotationRange[1];
}
float zoom = std::numeric_limits<float>::infinity();
PlacementRange rotationRange = {{0.0f, 0.0f}};
};
}
#endif

View File

@@ -2,16 +2,75 @@
#define LLMR_UTIL_MATH
#include <cmath>
#include <array>
#include "vec.hpp"
namespace llmr {
namespace util {
inline double angle_between(double ax, double ay, double bx, double by) {
template <typename T>
inline T max(T a, T b) {
return b > a ? b : a;
}
template <typename T>
inline T max(T a, T b, T c) {
return max(max(a, b), c);
}
template <typename T>
inline T max(T a, T b, T c, T d) {
return max(max(a, b), max(c, d));
}
template <typename T>
inline T min(T a, T b) {
return b < a ? b : a;
}
template <typename T>
inline T min(T a, T b, T c) {
return min(min(a, b), c);
}
template <typename T>
inline T min(T a, T b, T c, T d) {
return min(min(a, b), min(c, d));
}
// Find the angle of the two vectors, solving the formula for the cross product
// a x b = |a||b|sin(θ) for θ.
template <typename T = double, typename S>
inline T angle_between(S ax, S ay, S bx, S by) {
return atan2((ax * by - ay * bx), ax * bx + ay * by);
}
template <typename T = double, typename S>
inline T angle_between(const vec2<S>& a, const vec2<S>& b) {
return angle_between(a.x, a.y, b.x, b.y);
}
template <typename T = double, typename S>
inline T angle_to(const vec2<S>& a, const vec2<S>& b) {
return atan2(a.y - b.y, a.x - b.x);
}
template <typename T, typename S1, typename S2>
inline T interp(S1 a, S2 b, T t) {
return (a * ((T)1 - t)) + (b * t);
}
// Reflect an angle around 0 degrees
template <typename T>
inline std::array<T, 2> flip(const std::array<T, 2>& c) {
return {{
static_cast<T>(2 * M_PI - c[0]),
static_cast<T>(2 * M_PI - c[1])
}};
}
template <typename T, typename S1, typename S2>
inline vec2<T> normal(const S1& a, const S2& b) {
T dx = b.x - a.x;
@@ -28,6 +87,12 @@ inline T dist(const S1& a, const S2& b) {
return c;
}
// Take the magnitude of vector a.
template <typename T = double, typename S>
inline T mag(const S& a) {
return sqrt(a.x * a.x + a.y * a.y);
}
}
}

View File

@@ -0,0 +1,16 @@
#ifndef LLMR_UTIL_RECT
#define LLMR_UTIL_RECT
namespace llmr {
template <typename T>
struct Rect {
explicit Rect(T x, T y, T w, T h) : x(x), y(y), w(w), h(h) {}
T x = 0, y = 0;
T w = 0, h = 0;
operator bool() const { return w == 0 || h == 0; }
};
}
#endif

View File

@@ -16,6 +16,8 @@
#define VARIANT_INLINE __attribute__((noinline))
#endif
namespace llmr {
namespace util {
namespace detail {
@@ -273,12 +275,12 @@ public:
}
template<typename T>
VARIANT_INLINE void is() const
VARIANT_INLINE bool is() const
{
return (type_id == detail::type_traits<T, Types...>::id);
}
VARIANT_INLINE void valid() const
VARIANT_INLINE bool valid() const
{
return (type_id != detail::invalid_value);
}
@@ -377,4 +379,6 @@ operator<< (std::basic_ostream<charT,traits>& out, Variant const& rhs)
}
}
#endif // UTIL_VARIANT_HPP

View File

@@ -4,28 +4,75 @@
#include <limits>
#include <type_traits>
#include <cmath>
#include <cstdint>
namespace llmr {
template <typename T = double>
struct vec2 {
struct null {};
typedef T Type;
T x, y;
inline vec2() {}
template<typename U = T, typename std::enable_if<std::numeric_limits<U>::has_quiet_NaN, int>::type = 0>
inline vec2(null) : x(std::numeric_limits<T>::quiet_NaN()), y(std::numeric_limits<T>::quiet_NaN()) {}
template<typename U = T, typename std::enable_if<!std::numeric_limits<U>::has_quiet_NaN, int>::type = 0>
inline vec2(null) : x(std::numeric_limits<T>::min()), y(std::numeric_limits<T>::min()) {}
inline vec2(const vec2& o) : x(o.x), y(o.y) {}
template<typename U>
inline vec2(const U& u) : x(u.x), y(u.y) {}
inline vec2(T x, T y) : x(x), y(y) {}
inline bool operator==(const vec2& rhs) const {
return x == rhs.x && y == rhs.y;
}
template <typename O>
inline typename std::enable_if<std::is_arithmetic<O>::value, vec2>::type
operator*(O o) const {
return {x * o, y * o};
}
template <typename O>
inline typename std::enable_if<std::is_arithmetic<O>::value, vec2>::type &
operator*=(O o) {
x *= o;
y *= o;
}
template <typename O>
inline typename std::enable_if<std::is_arithmetic<O>::value, vec2>::type
operator-(O o) const {
return {x - o, y - o};
}
template <typename O>
inline typename std::enable_if<!std::is_arithmetic<O>::value, vec2>::type
operator-(const O &o) const {
return {x - o.x, y - o.y};
}
template <typename M>
inline vec2 matMul(const M &m) const {
return {m[0] * x + m[1] * y, m[2] * x + m[3] * y};
}
template<typename U = T, typename std::enable_if<std::numeric_limits<U>::has_quiet_NaN, int>::type = 0>
inline operator bool() const {
return !isnan(x) && !isnan(y);
}
template<typename U = T, typename std::enable_if<!std::numeric_limits<U>::has_quiet_NaN, int>::type = 0>
inline operator bool() const {
return x != std::numeric_limits<T>::min() && y != std::numeric_limits<T>::min();
}
};
template <typename T = double>
@@ -58,6 +105,8 @@ struct box {
vec2<double> center;
};
typedef vec2<int16_t> Coordinate;
}
#endif

View File

@@ -110,9 +110,5 @@ void cancel_request_http(Request *request) {
}
}
double time() {
return glfwGetTime();
}
}
}

View File

@@ -1,12 +1,12 @@
{
'includes': [
'./common.gypi',
'./config.gypi'
'./config.gypi',
],
'targets': [
{
'target_name': 'before_build_actions',
'type':'none',
'target_name': 'shaders_gl',
'type': 'none',
'actions': [
{
'action_name': 'Build Shaders',
@@ -17,8 +17,31 @@
'include/llmr/shader/shaders.hpp',
'src/shader/shaders.cpp'
],
'action': ['bin/build-shaders.js'],
},
'action': ['bin/build-shaders.js', 'gl'],
}
]
},
{
'target_name': 'shaders_gles2',
'type': 'none',
'actions': [
{
'action_name': 'Build Shaders',
'inputs': [
'<!@(find src -name "*.glsl")'
],
'outputs': [
'include/llmr/shader/shaders.hpp',
'src/shader/shaders.cpp'
],
'action': ['bin/build-shaders.js', 'gles2'],
}
]
},
{
'target_name': 'build_stylesheet',
'type': 'none',
'actions': [
{
'action_name': 'Build Stylesheet',
'inputs': [
@@ -37,7 +60,8 @@
'product_name': 'llmr-x86',
'type': 'static_library',
'dependencies': [
'before_build_actions'
'build_stylesheet',
'shaders_gl',
],
'sources': [
'<!@(find src -name "*.cpp")',
@@ -95,7 +119,8 @@
'product_name': 'llmr-ios',
'type': 'static_library',
'dependencies': [
'before_build_actions'
'build_stylesheet',
'shaders_gles2',
],
'sources': [
'<!@(find src -name "*.cpp")',

View File

@@ -84,9 +84,5 @@ void cancel_request_http(Request *request)
}];
}
double time() {
return glfwGetTime();
}
}
}

7
package.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "llmr-native",
"version": "0.0.1",
"dependencies": {
"glsl-optimizer": "https://github.com/kkaefer/glsl-optimizer/archive/amalgamation.zip"
}
}

View File

@@ -0,0 +1,142 @@
#include <llmr/geometry/glyph_atlas.hpp>
#include <llmr/map/vector_tile.hpp>
#include <llmr/platform/gl.hpp>
#include <llmr/platform/platform.hpp>
#include <cassert>
#include <algorithm>
using namespace llmr;
GlyphAtlas::GlyphAtlas(uint16_t width, uint16_t height)
: width(width),
height(height),
bin(width, height),
data(new char[width *height]),
dirty(true) {
}
GlyphAtlas::~GlyphAtlas() {
delete[] data;
}
Rect<uint16_t> GlyphAtlas::addGlyph(uint64_t tile_id, const std::string& face_name,
const VectorTileGlyph& glyph) {
std::lock_guard<std::mutex> lock(mtx);
// Use constant value for now.
const uint8_t buffer = 3;
std::map<uint32_t, GlyphValue>& face = index[face_name];
std::map<uint32_t, GlyphValue>::iterator it = face.find(glyph.id);
// The glyph is already in this texture.
if (it != face.end()) {
GlyphValue& value = it->second;
value.ids.insert(tile_id);
return value.rect;
}
// The glyph bitmap has zero width.
if (!glyph.bitmap.size()) {
return Rect<uint16_t>{ 0, 0, 0, 0 };
}
uint16_t buffered_width = glyph.metrics.width + buffer * 2;
uint16_t buffered_height = glyph.metrics.height + buffer * 2;
// Add a 1px border around every image.
uint16_t pack_width = buffered_width;
uint16_t pack_height = buffered_height;
// Increase to next number divisible by 4, but at least 1.
// This is so we can scale down the texture coordinates and pack them
// into 2 bytes rather than 4 bytes.
pack_width += (4 - pack_width % 4);
pack_height += (4 - pack_height % 4);
Rect<uint16_t> rect = bin.allocate(pack_width, pack_height);
if (rect.w == 0) {
fprintf(stderr, "glyph bitmap overflow");
return rect;
}
assert(rect.x + rect.w <= width);
assert(rect.y + rect.h <= height);
face.emplace(glyph.id, GlyphValue { rect, tile_id });
// Copy the bitmap
char *target = data;
const char *source = glyph.bitmap.data();
for (uint32_t y = 0; y < buffered_height; y++) {
uint32_t y1 = width * (rect.y + y) + rect.x;
uint32_t y2 = buffered_width * y;
for (uint32_t x = 0; x < buffered_width; x++) {
target[y1 + x] = source[y2 + x];
}
}
dirty = true;
return rect;
}
void GlyphAtlas::removeGlyphs(uint64_t tile_id) {
std::lock_guard<std::mutex> lock(mtx);
for (auto& faces : index) {
std::map<uint32_t, GlyphValue>& face = faces.second;
for (auto it = face.begin(); it != face.end(); /* we advance in the body */) {
GlyphValue& value = it->second;
value.ids.erase(tile_id);
if (!value.ids.size()) {
const Rect<uint16_t>& rect = value.rect;
// Clear out the bitmap.
char *target = data;
for (uint32_t y = 0; y < rect.h; y++) {
uint32_t y1 = width * (rect.y + y) + rect.x;
for (uint32_t x = 0; x < rect.w; x++) {
target[y1 + x] = 0;
}
}
dirty = true;
bin.release(rect);
// Make sure to post-increment the iterator: This will return the
// current iterator, but will go to the next position before we
// erase the element from the map. That way, the iterator stays
// valid.
face.erase(it++);
} else {
++it;
}
}
}
}
void GlyphAtlas::bind() {
if (!texture) {
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
} else {
glBindTexture(GL_TEXTURE_2D, texture);
}
if (dirty) {
std::lock_guard<std::mutex> lock(mtx);
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, data);
dirty = false;
// platform::show_debug_image("Glyph Atlas", data, width, height);
}
};

View File

@@ -0,0 +1,42 @@
#include <llmr/geometry/interpolate.hpp>
namespace llmr {
Anchors interpolate(const std::vector<Coordinate> &vertices, float spacing,
float minScale, int start) {
float distance = 0.0f;
float markedDistance = 0.0f;
int added = start;
Anchors points;
auto end = vertices.end() - 1;
int i = 0;
for (auto it = vertices.begin(); it != end; it++, i++) {
const Coordinate &a = *(it), b = *(it + 1);
float segmentDist = util::dist<float>(a, b);
float angle = util::angle_to(b, a);
while (markedDistance + spacing < distance + segmentDist) {
markedDistance += spacing;
float t = (markedDistance - distance) / segmentDist,
x = util::interp(a.x, b.x, t), y = util::interp(a.y, b.y, t),
s = added % 8 == 0 ? minScale : added % 4 == 0
? 2
: added % 2 == 0 ? 4 : 8;
if (x >= 0 && x < 4096 && y >= 0 && y < 4096) {
points.emplace_back(x, y, angle, s, i);
}
added++;
}
distance += segmentDist;
}
return points;
}
}

View File

@@ -0,0 +1,35 @@
#include <llmr/geometry/text_buffer.hpp>
#include <llmr/platform/gl.hpp>
#include <llmr/util/math.hpp>
#include <cmath>
using namespace llmr;
const double TextVertexBuffer::angleFactor = 128.0 / M_PI;
size_t TextVertexBuffer::add(int16_t x, int16_t y, float ox, float oy, uint16_t tx, uint16_t ty, float angle, float minzoom, std::array<float, 2> range, float maxzoom, float labelminzoom) {
size_t idx = index();
void *data = addElement();
int16_t *shorts = static_cast<int16_t *>(data);
shorts[0] = x;
shorts[1] = y;
shorts[2] = round(ox * 64); // use 1/64 pixels for placement
shorts[3] = round(oy * 64);
uint8_t *ubytes = static_cast<uint8_t *>(data);
// a_data1
ubytes[8] = tx / 4;
ubytes[9] = ty / 4;
ubytes[10] = labelminzoom * 10;
ubytes[11] = (int16_t)round(angle * angleFactor) % 256;
// a_data2
ubytes[12] = minzoom * 10; // 1/10 zoom levels: z16 == 160.
ubytes[13] = fmin(maxzoom, 25) * 10; // 1/10 zoom levels: z16 == 160.
ubytes[14] = util::max((int16_t)round(range[0] * angleFactor), (int16_t)0) % 256;
ubytes[15] = util::min((int16_t)round(range[1] * angleFactor), (int16_t)255) % 256;
return idx;
}

View File

@@ -13,9 +13,10 @@ using namespace llmr;
Map::Map(Settings& settings)
: settings(settings),
transform(),
style(),
painter(transform, settings, style),
texturepool(),
style(),
glyphAtlas(1024, 1024),
painter(transform, settings, style, glyphAtlas),
min_zoom(0),
max_zoom((use_raster ? kTileRasterMaxZoom : kTileVectorMaxZoom)) {
}
@@ -45,7 +46,7 @@ void Map::loadSettings() {
transform.setAngle(settings.angle);
transform.setScale(settings.scale);
transform.setLonLat(settings.longitude, settings.latitude);
style.cascade(transform.getZoom());
style.cascade(transform.getNormalizedZoom());
update();
}
@@ -86,7 +87,7 @@ void Map::stopPanning() {
void Map::scaleBy(double ds, double cx, double cy, double duration) {
transform.scaleBy(ds, cx, cy, duration);
style.cascade(transform.getZoom());
style.cascade(transform.getNormalizedZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
@@ -136,7 +137,7 @@ void Map::getLonLat(double& lon, double& lat) const {
void Map::setLonLatZoom(double lon, double lat, double zoom, double duration) {
transform.setLonLatZoom(lon, lat, zoom, duration);
style.cascade(transform.getZoom());
style.cascade(transform.getNormalizedZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
@@ -150,7 +151,7 @@ void Map::getLonLatZoom(double& lon, double& lat, double& zoom) const {
void Map::setScale(double scale, double cx, double cy, double duration) {
transform.setScale(scale, cx, cy, duration);
style.cascade(transform.getZoom());
style.cascade(transform.getNormalizedZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
@@ -160,7 +161,7 @@ void Map::setScale(double scale, double cx, double cy, double duration) {
void Map::setZoom(double zoom, double duration) {
transform.setZoom(zoom, duration);
style.cascade(transform.getZoom());
style.cascade(transform.getNormalizedZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
@@ -214,7 +215,7 @@ void Map::resetPosition() {
transform.setAngle(0);
transform.setLonLat(0, 0);
transform.setZoom(0);
style.cascade(transform.getZoom());
style.cascade(transform.getNormalizedZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
@@ -284,7 +285,7 @@ TileData::State Map::addTile(const Tile::ID& id) {
if (!new_tile.data) {
// If we don't find working tile data, we're just going to load it.
new_tile.data = std::make_shared<TileData>(normalized_id, style, use_raster, (pixel_ratio > 1.0));
new_tile.data = std::make_shared<TileData>(normalized_id, style, glyphAtlas, use_raster, (pixel_ratio > 1.0));
new_tile.data->request();
tile_data.push_front(new_tile.data);
}
@@ -482,5 +483,5 @@ bool Map::render() {
painter.renderMatte();
return changed || transform.needsAnimation();
return changed || transform.needsAnimation() || painter.needsAnimation();
}

View File

@@ -1,35 +1,24 @@
#include <llmr/map/tile_data.hpp>
#include <llmr/map/tile_parser.hpp>
#include <llmr/style/style.hpp>
#include <llmr/map/vector_tile.hpp>
#include <llmr/geometry/fill_buffer.hpp>
#include <llmr/geometry/line_buffer.hpp>
#include <llmr/geometry/point_buffer.hpp>
#include <llmr/geometry/text_buffer.hpp>
#include <llmr/geometry/elements_buffer.hpp>
#include <llmr/renderer/fill_bucket.hpp>
#include <llmr/renderer/line_bucket.hpp>
#include <llmr/renderer/point_bucket.hpp>
#include <llmr/util/pbf.hpp>
#include <llmr/util/raster.hpp>
#include <llmr/util/string.hpp>
#include <chrono>
using namespace llmr;
TileData::TileData(Tile::ID id, const Style& style, const bool use_raster, const bool use_retina)
TileData::TileData(Tile::ID id, const Style& style, GlyphAtlas& glyphAtlas, const bool use_raster, const bool use_retina)
: id(id),
state(State::initial),
use_raster(use_raster),
use_retina(use_retina),
state(State::initial),
raster(),
fillVertexBuffer(std::make_shared<FillVertexBuffer>()),
lineVertexBuffer(std::make_shared<LineVertexBuffer>()),
pointVertexBuffer(std::make_shared<PointVertexBuffer>()),
triangleElementsBuffer(std::make_shared<TriangleElementsBuffer>()),
lineElementsBuffer(std::make_shared<LineElementsBuffer>()),
pointElementsBuffer(std::make_shared<PointElementsBuffer>()),
style(style) {
style(style),
glyphAtlas(glyphAtlas) {
// Initialize tile debug coordinates
char coord[32];
@@ -38,6 +27,7 @@ TileData::TileData(Tile::ID id, const Style& style, const bool use_raster, const
}
TileData::~TileData() {
glyphAtlas.removeGlyphs((uint64_t)id);
cancel();
}
@@ -96,15 +86,10 @@ bool TileData::parse() {
}
try {
double parse_time_start = platform::time();
VectorTile tile(pbf((const uint8_t *)data.data(), data.size()));
parseStyleLayers(tile, style.layers);
double parse_time = (platform::time() - parse_time_start) * 1000.0;
if (state == State::obsolete) {
// fprintf(stderr, "[%p] parsing [%d/%d/%d] cancelled after %8.3fms\n", this, id.z, id.x, id.y, parse_time);
} else {
// fprintf(stderr, "[%p] parsing [%d/%d/%d] took %8.3fms\n", this, id.z, id.x, id.y, parse_time);
}
// 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, style, glyphAtlas);
} 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();
@@ -119,115 +104,3 @@ bool TileData::parse() {
return true;
}
void TileData::parseStyleLayers(const VectorTile& tile, const std::vector<LayerDescription>& layers) {
for (const LayerDescription& layer_desc : layers) {
// Cancel early when parsing.
if (state == State::obsolete) {
return;
}
if (layer_desc.child_layer.size()) {
// This is a layer group.
// TODO: create framebuffer
parseStyleLayers(tile, layer_desc.child_layer);
// TODO: render framebuffer on previous framebuffer
} else {
// This is a singular layer. Check if this bucket already exists. If not,
// parse this bucket.
auto bucket_it = buckets.find(layer_desc.bucket_name);
if (bucket_it == buckets.end()) {
auto bucket_it = style.buckets.find(layer_desc.bucket_name);
if (bucket_it != style.buckets.end()) {
// Only create the new bucket if we have an actual specification
// for it.
std::shared_ptr<Bucket> bucket = createBucket(tile, bucket_it->second);
if (bucket) {
// Bucket creation might fail because the data tile may not
// contain any data that falls into this bucket.
buckets[layer_desc.bucket_name] = bucket;
}
} else {
// There is no proper specification for this bucket, even though
// it is referenced in the stylesheet.
fprintf(stderr, "Stylesheet specifies bucket %s, but it is not defined\n", layer_desc.bucket_name.c_str());
}
}
}
}
}
std::shared_ptr<Bucket> TileData::createBucket(const VectorTile& tile, const BucketDescription& bucket_desc) {
auto layer_it = tile.layers.find(bucket_desc.source_layer);
if (layer_it != tile.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 {
// TODO: create other bucket types.
}
} else {
// The layer specified in the bucket does not exist. Do nothing.
}
return nullptr;
}
std::shared_ptr<Bucket> TileData::createFillBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
std::shared_ptr<FillBucket> bucket = std::make_shared<FillBucket>(fillVertexBuffer, triangleElementsBuffer, lineElementsBuffer, bucket_desc);
FilteredVectorTileLayer filtered_layer(layer, bucket_desc);
for (pbf feature : filtered_layer) {
if (state == State::obsolete) return nullptr;
while (feature.next(4)) { // geometry
pbf geometry_pbf = feature.message();
if (geometry_pbf) {
bucket->addGeometry(geometry_pbf);
bucket->tessellate();
}
}
}
return bucket;
}
std::shared_ptr<Bucket> TileData::createLineBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
std::shared_ptr<LineBucket> bucket = std::make_shared<LineBucket>(lineVertexBuffer, triangleElementsBuffer, pointElementsBuffer, bucket_desc);
FilteredVectorTileLayer filtered_layer(layer, bucket_desc);
for (pbf feature : filtered_layer) {
if (state == State::obsolete) return nullptr;
while (feature.next(4)) { // geometry
pbf geometry_pbf = feature.message();
if (geometry_pbf) {
bucket->addGeometry(geometry_pbf);
}
}
}
return bucket;
}
std::shared_ptr<Bucket> TileData::createPointBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
std::shared_ptr<PointBucket> bucket = std::make_shared<PointBucket>(pointVertexBuffer, bucket_desc);
FilteredVectorTileLayer filtered_layer(layer, bucket_desc);
for (pbf feature : filtered_layer) {
if (state == State::obsolete) return nullptr;
while (feature.next(4)) { // geometry
pbf geometry_pbf = feature.message();
if (geometry_pbf) {
bucket->addGeometry(geometry_pbf);
}
}
}
return bucket;
}

168
src/map/tile_parser.cpp Normal file
View File

@@ -0,0 +1,168 @@
#include <llmr/map/tile_parser.hpp>
#include <llmr/style/style.hpp>
#include <llmr/renderer/fill_bucket.hpp>
#include <llmr/renderer/line_bucket.hpp>
#include <llmr/renderer/point_bucket.hpp>
#include <llmr/renderer/text_bucket.hpp>
#include <llmr/util/std.hpp>
using namespace llmr;
TileParser::TileParser(const std::string& data, TileData& tile, const Style& style, GlyphAtlas& glyphAtlas)
: data(pbf((const uint8_t *)data.data(), data.size())),
tile(tile),
style(style),
glyphAtlas(glyphAtlas),
placement(tile.id.z) {
parseGlyphs();
parseStyleLayers(style.layers);
}
bool TileParser::obsolete() const {
return tile.state == TileData::State::obsolete;
}
void TileParser::parseGlyphs() {
for (const std::pair<std::string, const VectorTileFace> pair : data.faces) {
const std::string &name = pair.first;
const VectorTileFace &face = pair.second;
GlyphPositions &glyphs = faces[name];
for (const VectorTileGlyph &glyph : face.glyphs) {
const Rect<uint16_t> rect =
glyphAtlas.addGlyph((uint64_t)tile.id, name, glyph);
glyphs.emplace(glyph.id, Glyph{rect, glyph.metrics});
}
}
}
void TileParser::parseStyleLayers(const std::vector<LayerDescription>& layers) {
for (const LayerDescription& layer_desc : layers) {
// Cancel early when parsing.
if (obsolete()) {
return;
}
if (layer_desc.child_layer.size()) {
// This is a layer group.
// TODO: create framebuffer
parseStyleLayers(layer_desc.child_layer);
// TODO: render framebuffer on previous framebuffer
} else {
// This is a singular layer. Check if this bucket already exists. If not,
// parse this bucket.
auto bucket_it = tile.buckets.find(layer_desc.bucket_name);
if (bucket_it == tile.buckets.end()) {
auto bucket_it = style.buckets.find(layer_desc.bucket_name);
if (bucket_it != style.buckets.end()) {
// Only create the new bucket if we have an actual specification
// for it.
std::unique_ptr<Bucket> bucket = createBucket(bucket_it->second);
if (bucket) {
// Bucket creation might fail because the data tile may not
// contain any data that falls into this bucket.
tile.buckets[layer_desc.bucket_name] = std::move(bucket);
}
} else {
// There is no proper specification for this bucket, even though
// it is referenced in the stylesheet.
fprintf(stderr, "Stylesheet specifies bucket %s, but it is not defined\n", layer_desc.bucket_name.c_str());
}
}
}
}
}
std::unique_ptr<Bucket> TileParser::createBucket(const BucketDescription& bucket_desc) {
auto layer_it = data.layers.find(bucket_desc.source_layer);
if (layer_it != 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");
}
} else {
// The layer specified in the bucket does not exist. Do nothing.
}
return nullptr;
}
template <class Bucket>
void TileParser::addBucketFeatures(Bucket& bucket, const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
FilteredVectorTileLayer filtered_layer(layer, bucket_desc);
for (pbf feature : filtered_layer) {
if (obsolete()) return;
while (feature.next(4)) { // geometry
pbf geometry_pbf = feature.message();
if (geometry_pbf) {
bucket->addGeometry(geometry_pbf);
}
}
}
}
std::unique_ptr<Bucket> TileParser::createFillBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
std::unique_ptr<FillBucket> bucket = std::make_unique<FillBucket>(
tile.fillVertexBuffer, tile.triangleElementsBuffer, tile.lineElementsBuffer, bucket_desc);
addBucketFeatures(bucket, layer, bucket_desc);
return obsolete() ? nullptr : std::move(bucket);
}
std::unique_ptr<Bucket> TileParser::createLineBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
std::unique_ptr<LineBucket> bucket = std::make_unique<LineBucket>(
tile.lineVertexBuffer, tile.triangleElementsBuffer, tile.pointElementsBuffer, bucket_desc);
addBucketFeatures(bucket, layer, bucket_desc);
return obsolete() ? nullptr : std::move(bucket);
}
std::unique_ptr<Bucket> TileParser::createPointBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
std::unique_ptr<PointBucket> bucket = std::make_unique<PointBucket>(
tile.pointVertexBuffer, bucket_desc);
addBucketFeatures(bucket, layer, bucket_desc);
return obsolete() ? nullptr : std::move(bucket);
}
std::unique_ptr<Bucket> TileParser::createTextBucket(const VectorTileLayer& layer, const BucketDescription& bucket_desc) {
// Determine the correct text stack.
if (!layer.shaping.size()) {
return nullptr;
}
// TODO: currently hardcoded to use the first font stack.
const std::map<Value, Shaping>& shaping = layer.shaping.begin()->second;
const Faces& const_faces = faces;
IndexedFaces faces;
for (const std::string& face : layer.faces) {
auto it = const_faces.find(face);
if (it == const_faces.end()) {
// This layer references an unknown face.
return nullptr;
}
faces.push_back(&it->second);
}
std::unique_ptr<TextBucket> bucket = std::make_unique<TextBucket>(
tile.textVertexBuffer, tile.triangleElementsBuffer, bucket_desc, placement);
FilteredVectorTileLayer filtered_layer(layer, bucket_desc);
for (const pbf& feature_pbf : filtered_layer) {
if (obsolete()) return nullptr;
bucket->addFeature({ feature_pbf, layer }, faces, shaping);
}
return std::move(bucket);
}

View File

@@ -256,6 +256,10 @@ float Transform::getZoom() const {
return log(scale) / M_LN2;
}
float Transform::getNormalizedZoom() const {
return log(scale * util::tileSize / 256.0f) / M_LN2;
}
int32_t Transform::getIntegerZoom() const {
return floor(log(scale) / M_LN2);
}

View File

@@ -2,9 +2,72 @@
#include <llmr/style/bucket_description.hpp>
#include <algorithm>
#include <iostream>
using namespace llmr;
std::ostream& llmr::operator<<(std::ostream& os, const FeatureType& type) {
switch (type) {
case FeatureType::Unknown: return os << "Unknown";
case FeatureType::Point: return os << "Point";
case FeatureType::LineString: return os << "LineString";
case FeatureType::Polygon: return os << "Polygon";
default: return os << "Invalid";
}
}
VectorTileFeature::VectorTileFeature(pbf feature, const VectorTileLayer& layer) {
while (feature.next()) {
if (feature.tag == 1) { // id
id = feature.varint<uint64_t>();
} else if (feature.tag == 2) { // tags
// tags are packed varints. They should have an even length.
pbf tags = feature.message();
while (tags) {
uint32_t tag_key = tags.varint();
if (layer.keys.size() <= tag_key) {
// TODO: Throw correct exception
fprintf(stderr, "feature referenced out of range key\n");
break;
}
if (tags) {
uint32_t tag_val = tags.varint();
if (layer.values.size() <= tag_val) {
// TODO: Throw correct exception
fprintf(stderr, "feature referenced out of range value\n");
break;
}
properties.emplace(layer.keys[tag_key], layer.values[tag_val]);
} else {
// TODO: Throw correct exception
fprintf(stderr, "uneven number of feature tag ids\n");
break;
}
}
} else if (feature.tag == 3) { // type
type = (FeatureType)feature.varint();
} else if (feature.tag == 4) { // geometry
geometry = feature.message();
} else {
feature.skip();
}
}
}
std::ostream& llmr::operator<<(std::ostream& os, const VectorTileFeature& feature) {
os << "Feature(" << feature.id << "): " << feature.type << std::endl;
for (const auto& prop : feature.properties) {
os << " - " << prop.first << ": " << prop.second << std::endl;
}
return os;
}
VectorTile::VectorTile() {}
@@ -13,6 +76,9 @@ VectorTile::VectorTile(pbf tile) {
if (tile.tag == 3) { // layer
VectorTileLayer layer(tile.message());
layers.emplace(layer.name, std::forward<VectorTileLayer>(layer));
} else if (tile.tag == 4) { // face
VectorTileFace face(tile.message());
faces.emplace(face.name, std::forward<VectorTileFace>(face));
} else {
tile.skip();
}
@@ -26,8 +92,101 @@ VectorTile& VectorTile::operator=(VectorTile && other) {
return *this;
}
VectorTileFace::VectorTileFace(pbf face) {
while (face.next()) {
if (face.tag == 1) { // family
family = face.string();
} else if (face.tag == 2) { // style
style = face.string();
} else if (face.tag == 5) { // glyphs
glyphs.emplace_back(face.message());
} else {
face.skip();
}
}
name = family + " " + style;
}
std::ostream& llmr::operator<<(std::ostream& os, const VectorTileFace& face) {
os << "Face: " << face.name << std::endl;
for (const VectorTileGlyph& glyph : face.glyphs) {
os << " - " << glyph;
}
return os;
}
// -1 overflows to maximum value
VectorTileGlyph::VectorTileGlyph() {}
VectorTileGlyph::VectorTileGlyph(pbf glyph) {
while (glyph.next()) {
if (glyph.tag == 1) { // id
id = glyph.varint();
} else if (glyph.tag == 2) { // bitmap
bitmap = glyph.string();
} else if (glyph.tag == 3) { // width
metrics.width = glyph.varint();
} else if (glyph.tag == 4) { // height
metrics.height = glyph.varint();
} else if (glyph.tag == 5) { // left
metrics.left = glyph.svarint();
} else if (glyph.tag == 6) { // top
metrics.top = glyph.svarint();
} else if (glyph.tag == 7) { // advance
metrics.advance = glyph.varint();
} else {
glyph.skip();
}
}
}
std::ostream& llmr::operator<<(std::ostream& os, const VectorTileGlyph& glyph) {
os << "Glyph " << glyph.id << ": " << glyph.metrics.width << "x" << glyph.metrics.height <<
" (" << glyph.metrics.left << "/" << glyph.metrics.top << ") +" << glyph.metrics.advance << std::endl;
return os;
}
class VectorTileLabel {
public:
VectorTileLabel(pbf data);
uint32_t text = 0;
uint32_t stack = 0;
std::vector<GlyphPlacement> placements;
};
VectorTileLabel::VectorTileLabel(pbf label) {
pbf faces, glyphs, x, y;
while (label.next()) {
if (label.tag == 1) { // text
text = label.varint();
} else if (label.tag == 2) { // stack
stack = label.varint();
} else if (label.tag == 3) { // faces
faces = label.message();
} else if (label.tag == 4) { // glyphs
glyphs = label.message();
} else if (label.tag == 5) { // x
x = label.message();
} else if (label.tag == 6) { // y
y = label.message();
} else {
label.skip();
}
}
while (faces && glyphs && x && y) {
placements.emplace_back(faces.varint(), glyphs.varint(), x.varint(), y.varint());
}
}
VectorTileLayer::VectorTileLayer(pbf layer) : data(layer) {
std::vector<std::string> stacks;
std::vector<VectorTileLabel> labels;
while (layer.next()) {
if (layer.tag == 1) { // name
name = layer.string();
@@ -37,10 +196,34 @@ VectorTileLayer::VectorTileLayer(pbf layer) : data(layer) {
values.emplace_back(std::move(parseValue(layer.message())));
} else if (layer.tag == 5) { // extent
extent = layer.varint();
} else if (layer.tag == 7) { // faces
faces.emplace_back(layer.string());
} else if (layer.tag == 8) { // labels
labels.emplace_back(layer.message());
} else if (layer.tag == 9) { // stacks
stacks.emplace_back(layer.string());
} else {
layer.skip();
}
}
// Build index of [stack][text] => shaping information
for (VectorTileLabel& label : labels) {
if (values.size() <= label.text || stacks.size() <= label.stack) {
// The indices are out of range. Skip this label.
continue;
}
const Value& text = values[label.text];
const std::string& stack = stacks[label.stack];
shaping[stack][text].swap(label.placements);
}
}
std::ostream& llmr::operator<<(std::ostream& os, const GlyphPlacement& placement) {
os << "Glyph " << placement.face << "/" << placement.glyph << ": " << placement.x << "/" << placement.y << std::endl;
return os;
}
FilteredVectorTileLayer::FilteredVectorTileLayer(const VectorTileLayer& layer, const BucketDescription& bucket_desc)
@@ -129,7 +312,7 @@ void FilteredVectorTileLayer::iterator::operator++() {
default: type = BucketType::None; break;
}
if (type != filter.bucket_desc.type) {
if (type != filter.bucket_desc.feature_type) {
break; // feature_pbf loop early
}
} else {
@@ -137,7 +320,7 @@ void FilteredVectorTileLayer::iterator::operator++() {
}
}
if (!skip && type == filter.bucket_desc.type) {
if (!skip && type == filter.bucket_desc.feature_type) {
valid = true;
return; // data loop
} else {

View File

@@ -5,4 +5,4 @@ const char *llmr::kTileRasterURL = "https://a.tiles.mapbox.com/v3/justin.map-pgy
const char *llmr::kSpriteURL = "https://dl.dropboxusercontent.com/u/575564/sprite";
const int32_t llmr::kTileVectorMaxZoom = 14;
const int32_t llmr::kTileRasterMaxZoom = 21;
const int32_t llmr::kTileRasterMaxZoom = 21;

View File

@@ -30,9 +30,9 @@ void FillBucket::free(void *, void *ptr) {
::free(ptr);
}
FillBucket::FillBucket(const std::shared_ptr<FillVertexBuffer>& vertexBuffer,
const std::shared_ptr<TriangleElementsBuffer>& triangleElementsBuffer,
const std::shared_ptr<LineElementsBuffer>& lineElementsBuffer,
FillBucket::FillBucket(FillVertexBuffer& vertexBuffer,
TriangleElementsBuffer& triangleElementsBuffer,
LineElementsBuffer& lineElementsBuffer,
const BucketDescription& bucket_desc)
: geom_desc(bucket_desc.geometry),
allocator(new TESSalloc {
@@ -51,9 +51,9 @@ tesselator(tessNewTess(allocator)),
vertexBuffer(vertexBuffer),
triangleElementsBuffer(triangleElementsBuffer),
lineElementsBuffer(lineElementsBuffer),
vertex_start(vertexBuffer->index()),
triangle_elements_start(triangleElementsBuffer->index()),
line_elements_start(lineElementsBuffer->index()) {
vertex_start(vertexBuffer.index()),
triangle_elements_start(triangleElementsBuffer.index()),
line_elements_start(lineElementsBuffer.index()) {
assert(tesselator);
}
@@ -88,6 +88,8 @@ void FillBucket::addGeometry(pbf& geom) {
line.clear();
hasVertices = true;
}
tessellate();
}
void FillBucket::tessellate() {
@@ -129,12 +131,12 @@ void FillBucket::tessellate() {
for (const ClipperLib::IntPoint& pt : polygon) {
line.push_back(pt.X);
line.push_back(pt.Y);
vertexBuffer->add(pt.X, pt.Y);
vertexBuffer.add(pt.X, pt.Y);
}
for (size_t i = 0; i < group_count; i++) {
const size_t prev_i = (i == 0 ? group_count : i) - 1;
lineElementsBuffer->add(lineIndex + prev_i, lineIndex + i);
lineElementsBuffer.add(lineIndex + prev_i, lineIndex + i);
}
lineIndex += group_count;
@@ -153,7 +155,7 @@ void FillBucket::tessellate() {
for (size_t i = 0; i < vertex_count; ++i) {
if (vertex_indices[i] == TESS_UNDEF) {
vertexBuffer->add(round(vertices[i * 2]), round(vertices[i * 2 + 1]));
vertexBuffer.add(round(vertices[i * 2]), round(vertices[i * 2 + 1]));
vertex_indices[i] = (TESSindex)total_vertex_count;
total_vertex_count++;
}
@@ -178,7 +180,7 @@ void FillBucket::tessellate() {
const TESSindex c = vertex_indices[element_group[2]];
if (a != TESS_UNDEF && b != TESS_UNDEF && c != TESS_UNDEF) {
triangleElementsBuffer->add(triangleIndex + a, triangleIndex + b, triangleIndex + c);
triangleElementsBuffer.add(triangleIndex + a, triangleIndex + b, triangleIndex + c);
} else {
// TODO: We're missing a vertex that was not part of the line.
fprintf(stderr, "undefined element buffer\n");
@@ -209,34 +211,34 @@ bool FillBucket::empty() const {
}
void FillBucket::drawElements(PlainShader& shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer->itemSize);
char *elements_index = BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer->itemSize);
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
char *elements_index = BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer.itemSize);
for (triangle_group_type& group : triangleGroups) {
group.array.bind(shader, *vertexBuffer, *triangleElementsBuffer, vertex_index);
group.array.bind(shader, vertexBuffer, triangleElementsBuffer, vertex_index);
glDrawElements(GL_TRIANGLES, group.elements_length * 3, GL_UNSIGNED_SHORT, elements_index);
vertex_index += group.vertex_length * vertexBuffer->itemSize;
elements_index += group.elements_length * triangleElementsBuffer->itemSize;
vertex_index += group.vertex_length * vertexBuffer.itemSize;
elements_index += group.elements_length * triangleElementsBuffer.itemSize;
}
}
void FillBucket::drawElements(PatternShader& shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer->itemSize);
char *elements_index = BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer->itemSize);
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
char *elements_index = BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer.itemSize);
for (triangle_group_type& group : triangleGroups) {
group.array.bind(shader, *vertexBuffer, *triangleElementsBuffer, vertex_index);
group.array.bind(shader, vertexBuffer, triangleElementsBuffer, vertex_index);
glDrawElements(GL_TRIANGLES, group.elements_length * 3, GL_UNSIGNED_SHORT, elements_index);
vertex_index += group.vertex_length * vertexBuffer->itemSize;
elements_index += group.elements_length * triangleElementsBuffer->itemSize;
vertex_index += group.vertex_length * vertexBuffer.itemSize;
elements_index += group.elements_length * triangleElementsBuffer.itemSize;
}
}
void FillBucket::drawVertices(OutlineShader& shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer->itemSize);
char *elements_index = BUFFER_OFFSET(line_elements_start * lineElementsBuffer->itemSize);
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
char *elements_index = BUFFER_OFFSET(line_elements_start * lineElementsBuffer.itemSize);
for (line_group_type& group : lineGroups) {
group.array.bind(shader, *vertexBuffer, *lineElementsBuffer, vertex_index);
group.array.bind(shader, vertexBuffer, lineElementsBuffer, vertex_index);
glDrawElements(GL_LINES, group.elements_length * 2, GL_UNSIGNED_SHORT, elements_index);
vertex_index += group.vertex_length * vertexBuffer->itemSize;
elements_index += group.elements_length * lineElementsBuffer->itemSize;
vertex_index += group.vertex_length * vertexBuffer.itemSize;
elements_index += group.elements_length * lineElementsBuffer.itemSize;
}
}

View File

@@ -0,0 +1,47 @@
#include <llmr/renderer/frame_history.hpp>
using namespace llmr;
// Record frame history that will be used to calculate fading params
void FrameHistory::record(float zoom) {
// first frame ever
if (!history.size()) {
history.emplace_back(FrameSnapshot{0.0f, zoom});
history.emplace_back(FrameSnapshot{0.0f, zoom});
}
if (history.size() > 0 || history.back().z != zoom) {
history.emplace_back(FrameSnapshot{static_cast<float>(platform::time() * 1000), zoom});
}
}
bool FrameHistory::needsAnimation(const float duration) const {
if (!history.size()) return false;
// If we have a value that is older than duration and whose z value is the
// same as the most current z value, and if all values inbetween have the
// same z value, we don't need animation, otherwise we probably do.
const FrameSnapshot &pivot = history.back();
int i = -1;
while (history.size() > i + 1 &&
history[i + 1].time + duration < pivot.time) {
i++;
}
if (i < 0) {
// There is no frame that is older than the duration time, so we need to
// check all frames.
i = 0;
}
// Make sure that all subsequent snapshots have the same zoom as the last
// pivot element.
for (; history.size() > i; i++) {
if (history[i].z != pivot.z) {
return true;
}
}
return false;
}

View File

@@ -1,5 +1,4 @@
#include <llmr/renderer/line_bucket.hpp>
#include <llmr/geometry/line_buffer.hpp>
#include <llmr/geometry/elements_buffer.hpp>
#include <llmr/geometry/geometry.hpp>
@@ -18,17 +17,17 @@ struct geometry_too_long_exception : std::exception {};
using namespace llmr;
LineBucket::LineBucket(const std::shared_ptr<LineVertexBuffer>& vertexBuffer,
const std::shared_ptr<TriangleElementsBuffer>& triangleElementsBuffer,
const std::shared_ptr<PointElementsBuffer>& pointElementsBuffer,
LineBucket::LineBucket(LineVertexBuffer& vertexBuffer,
TriangleElementsBuffer& triangleElementsBuffer,
PointElementsBuffer& pointElementsBuffer,
const BucketDescription& bucket_desc)
: geometry(bucket_desc.geometry),
vertexBuffer(vertexBuffer),
triangleElementsBuffer(triangleElementsBuffer),
pointElementsBuffer(pointElementsBuffer),
vertex_start(vertexBuffer->index()),
triangle_elements_start(triangleElementsBuffer->index()),
point_elements_start(pointElementsBuffer->index())
vertex_start(vertexBuffer.index()),
triangle_elements_start(triangleElementsBuffer.index()),
point_elements_start(pointElementsBuffer.index())
{
}
@@ -101,7 +100,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
nextNormal = util::normal<double>(currentVertex, lastVertex);
}
int32_t start_vertex = (int32_t)vertexBuffer->index();
int32_t start_vertex = (int32_t)vertexBuffer.index();
std::vector<TriangleElement> triangle_store;
std::vector<PointElement> point_store;
@@ -172,7 +171,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
// Add offset square begin cap.
if (!prevVertex && beginCap == CapType::Square) {
// Add first vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
flip * (prevNormal.x + prevNormal.y), flip * (-prevNormal.x + prevNormal.y), // extrude normal
0, 0, distance) - start_vertex; // texture normal
@@ -180,7 +179,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
e1 = e2; e2 = e3;
// Add second vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
flip * (prevNormal.x - prevNormal.y), flip * (prevNormal.x + prevNormal.y), // extrude normal
0, 1, distance) - start_vertex; // texture normal
@@ -191,7 +190,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
// Add offset square end cap.
else if (!nextVertex && endCap == CapType::Square) {
// Add first vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
nextNormal.x - flip * nextNormal.y, flip * nextNormal.x + nextNormal.y, // extrude normal
0, 0, distance) - start_vertex; // texture normal
@@ -199,7 +198,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
e1 = e2; e2 = e3;
// Add second vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
nextNormal.x + flip * nextNormal.y, -flip * nextNormal.x + nextNormal.y, // extrude normal
0, 1, distance) - start_vertex; // texture normal
@@ -225,7 +224,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
}
// Add first vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
flip * joinNormal.x, flip * joinNormal.y, // extrude normal
0, 0, distance) - start_vertex; // texture normal
@@ -233,7 +232,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
e1 = e2; e2 = e3;
// Add second vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
-flip * joinNormal.x, -flip * joinNormal.y, // extrude normal
0, 1, distance) - start_vertex; // texture normal
@@ -249,7 +248,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
else {
// Close up the previous line
// Add first vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
flip * prevNormal.y, -flip * prevNormal.x, // extrude normal
0, 0, distance) - start_vertex; // texture normal
@@ -257,7 +256,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
e1 = e2; e2 = e3;
// Add second vertex.
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
-flip * prevNormal.y, flip * prevNormal.x, // extrude normal
0, 1, distance) - start_vertex; // texture normal
@@ -287,7 +286,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
// Start the new quad.
// Add first vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
-flip * nextNormal.y, flip * nextNormal.x, // extrude normal
0, 0, distance) - start_vertex; // texture normal
@@ -295,7 +294,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
e1 = e2; e2 = e3;
// Add second vertex
e3 = (int32_t)vertexBuffer->add(currentVertex.x, currentVertex.y, // vertex pos
e3 = (int32_t)vertexBuffer.add(currentVertex.x, currentVertex.y, // vertex pos
flip * nextNormal.y, -flip * nextNormal.x, // extrude normal
0, 1, distance) - start_vertex; // texture normal
@@ -304,7 +303,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
}
}
size_t end_vertex = vertexBuffer->index();
size_t end_vertex = vertexBuffer.index();
size_t vertex_count = end_vertex - start_vertex;
// Store the triangle/line groups.
@@ -316,7 +315,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
triangle_group_type& group = triangleGroups.back();
for (const TriangleElement& triangle : triangle_store) {
triangleElementsBuffer->add(
triangleElementsBuffer.add(
group.vertex_length + triangle.a,
group.vertex_length + triangle.b,
group.vertex_length + triangle.c
@@ -336,7 +335,7 @@ void LineBucket::addGeometry(const std::vector<Coordinate>& vertices) {
point_group_type& group = pointGroups.back();
for (PointElement point : point_store) {
pointElementsBuffer->add(group.vertex_length + point);
pointElementsBuffer.add(group.vertex_length + point);
}
group.vertex_length += vertex_count;
@@ -357,23 +356,23 @@ bool LineBucket::hasPoints() const {
}
void LineBucket::drawLines(LineShader& shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer->itemSize);
char *elements_index = BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer->itemSize);
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
char *elements_index = BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer.itemSize);
for (triangle_group_type& group : triangleGroups) {
group.array.bind(shader, *vertexBuffer, *triangleElementsBuffer, vertex_index);
group.array.bind(shader, vertexBuffer, triangleElementsBuffer, vertex_index);
glDrawElements(GL_TRIANGLES, group.elements_length * 3, GL_UNSIGNED_SHORT, elements_index);
vertex_index += group.vertex_length * vertexBuffer->itemSize;
elements_index += group.elements_length * triangleElementsBuffer->itemSize;
vertex_index += group.vertex_length * vertexBuffer.itemSize;
elements_index += group.elements_length * triangleElementsBuffer.itemSize;
}
}
void LineBucket::drawPoints(LinejoinShader& shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer->itemSize);
char *elements_index = BUFFER_OFFSET(point_elements_start * pointElementsBuffer->itemSize);
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
char *elements_index = BUFFER_OFFSET(point_elements_start * pointElementsBuffer.itemSize);
for (point_group_type& group : pointGroups) {
group.array.bind(shader, *vertexBuffer, *pointElementsBuffer, vertex_index);
group.array.bind(shader, vertexBuffer, pointElementsBuffer, vertex_index);
glDrawElements(GL_POINTS, group.elements_length, GL_UNSIGNED_SHORT, elements_index);
vertex_index += group.vertex_length * vertexBuffer->itemSize;
elements_index += group.elements_length * pointElementsBuffer->itemSize;
vertex_index += group.vertex_length * vertexBuffer.itemSize;
elements_index += group.elements_length * pointElementsBuffer.itemSize;
}
}

View File

@@ -8,6 +8,7 @@
#include <llmr/renderer/fill_bucket.hpp>
#include <llmr/renderer/line_bucket.hpp>
#include <llmr/renderer/point_bucket.hpp>
#include <llmr/renderer/text_bucket.hpp>
#include <llmr/map/transform.hpp>
#include <llmr/map/settings.hpp>
@@ -21,10 +22,15 @@ using namespace llmr;
#define BUFFER_OFFSET(i) ((char *)nullptr + (i))
Painter::Painter(Transform& transform, Settings& settings, Style& style)
Painter::Painter(Transform& transform, Settings& settings, Style& style, GlyphAtlas& glyphAtlas)
: transform(transform),
settings(settings),
style(style) {
style(style),
glyphAtlas(glyphAtlas) {
}
bool Painter::needsAnimation() const {
return frameHistory.needsAnimation(300);
}
void Painter::setup() {
@@ -37,6 +43,7 @@ void Painter::setup() {
assert(linejoinShader);
assert(patternShader);
assert(rasterShader);
assert(textShader);
// Blending
@@ -64,6 +71,7 @@ void Painter::setupShaders() {
patternShader = std::make_unique<PatternShader>();
pointShader = std::make_unique<PointShader>();
rasterShader = std::make_unique<RasterShader>();
textShader = std::make_unique<TextShader>();
}
void Painter::resize(int width, int height) {
@@ -149,6 +157,8 @@ void Painter::render(const Tile& tile) {
return;
}
frameHistory.record(transform.getNormalizedZoom());
matrix = tile.matrix;
glStencilFunc(GL_EQUAL, tile.clip_id, 0xFF);
@@ -229,7 +239,7 @@ void Painter::renderFill(FillBucket& bucket, const std::string& layer_name, cons
if (bucket.empty()) return;
const FillProperties& properties = style.computed.fills[layer_name];
if (properties.hidden) return;
if (!properties.enabled) return;
Color fill_color = properties.fill_color;
fill_color[0] *= properties.opacity;
@@ -356,7 +366,7 @@ void Painter::renderLine(LineBucket& bucket, const std::string& layer_name, cons
if (bucket.empty()) return;
const LineProperties& properties = style.computed.lines[layer_name];
if (properties.hidden) return;
if (!properties.enabled) return;
float width = properties.width;
float offset = properties.offset / 2;
@@ -437,7 +447,7 @@ void Painter::renderPoint(PointBucket& bucket, const std::string& layer_name, co
if (pass == Opaque) return;
const PointProperties& properties = style.computed.points[layer_name];
if (properties.hidden) return;
if (!properties.enabled) return;
Color color = properties.color;
color[0] *= properties.opacity;
@@ -473,6 +483,106 @@ void Painter::renderPoint(PointBucket& bucket, const std::string& layer_name, co
bucket.drawPoints(*pointShader);
}
void Painter::renderText(TextBucket& bucket, const std::string& layer_name, const Tile::ID& id) {
// Abort early.
if (pass == Opaque) return;
if (bucket.empty()) return;
const TextProperties& properties = style.computed.texts[layer_name];
if (!properties.enabled) return;
mat4 exMatrix;
matrix::copy(exMatrix, projMatrix);
if (bucket.geom_desc.path == TextPathType::Curve) {
matrix::rotate_z(exMatrix, exMatrix, transform.getAngle());
}
const float rotate = properties.rotate;
if (rotate != 0.0f) {
matrix::rotate_z(exMatrix, exMatrix, rotate);
}
// If layerStyle.size > bucket.info.fontSize then labels may collide
float fontSize = fmin(properties.size, bucket.geom_desc.font_size);
matrix::scale(exMatrix, exMatrix, fontSize / 24.0f, fontSize / 24.0f, 1.0f);
useProgram(textShader->program);
textShader->setMatrix(matrix);
textShader->setExtrudeMatrix(exMatrix);
glyphAtlas.bind();
textShader->setTextureSize({{ static_cast<float>(glyphAtlas.width), static_cast<float>(glyphAtlas.height) }});
textShader->setGamma(2.5f / fontSize / transform.pixelRatio);
// Convert the -pi..pi to an int8 range.
float angle = round((transform.getAngle() + rotate) / M_PI * 128);
// adjust min/max zooms for variable font sies
float zoomAdjust = log(fontSize / bucket.geom_desc.font_size) / log(2);
textShader->setAngle((int32_t)(angle + 256) % 256);
textShader->setFlip(bucket.geom_desc.path == TextPathType::Curve ? 1 : 0);
textShader->setZoom((transform.getNormalizedZoom() - zoomAdjust) * 10); // current zoom level
// Label fading
const float duration = 300.0f;
const float currentTime = platform::time() * 1000;
std::deque<FrameSnapshot> &history = frameHistory.history;
// Remove frames until only one is outside the duration, or until there are only three
while (history.size() > 3 && history[1].time + duration < currentTime) {
history.pop_front();
}
if (history[1].time + duration < currentTime) {
history[0].z = history[1].z;
}
size_t frameLen = history.size();
assert("there should never be less than three frames in the history" && frameLen >= 3);
// Find the range of zoom levels we want to fade between
float startingZ = history.front().z;
const FrameSnapshot lastFrame = history.back();
float endingZ = lastFrame.z;
float lowZ = fmin(startingZ, endingZ);
float highZ = fmax(startingZ, endingZ);
// Calculate the speed of zooming, and how far it would zoom in terms of zoom levels in one duration
float zoomDiff = endingZ - history[1].z,
timeDiff = lastFrame.time - history[1].time;
if (timeDiff > duration) timeDiff = 1;
float fadedist = zoomDiff / (timeDiff / duration);
if (isnan(fadedist)) fprintf(stderr, "fadedist should never be NaN");
// At end of a zoom when the zoom stops changing continue pretending to zoom at that speed
// bump is how much farther it would have been if it had continued zooming at the same rate
float bump = (currentTime - lastFrame.time) / duration * fadedist;
textShader->setFadeDist(fadedist * 10);
textShader->setMinFadeZoom(floor(lowZ * 10));
textShader->setMaxFadeZoom(floor(highZ * 10));
textShader->setFadeZoom((transform.getZoom() + bump) * 10);
// We're drawing in the translucent pass which is bottom-to-top, so we need
// to draw the halo first.
if (properties.halo[3] > 0.0f) {
textShader->setColor(properties.halo);
textShader->setBuffer(properties.haloRadius);
glDepthRange(strata, 1.0f);
bucket.drawGlyphs(*textShader);
}
// Then, we draw the text over the halo
textShader->setColor(properties.color);
textShader->setBuffer((256.0f - 64.0f) / 256.0f);
glDepthRange(strata + strata_epsilon, 1.0f);
bucket.drawGlyphs(*textShader);
}
void Painter::renderDebug(const TileData::Ptr& tile_data) {
// 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

View File

@@ -15,11 +15,11 @@ struct geometry_too_long_exception : std::exception {};
using namespace llmr;
PointBucket::PointBucket(const std::shared_ptr<PointVertexBuffer>& vertexBuffer,
PointBucket::PointBucket(PointVertexBuffer& vertexBuffer,
const BucketDescription& bucket_desc)
: geometry(bucket_desc.geometry),
vertexBuffer(vertexBuffer),
vertex_start(vertexBuffer->index()) {
vertex_start(vertexBuffer.index()) {
}
void PointBucket::addGeometry(pbf& geom) {
@@ -28,13 +28,13 @@ void PointBucket::addGeometry(pbf& geom) {
int32_t x, y;
while ((cmd = geometry.next(x, y)) != Geometry::end) {
if (cmd == Geometry::move_to) {
vertexBuffer->add(x, y);
vertexBuffer.add(x, y);
} else {
fprintf(stderr, "other command than move_to in point geometry\n");
}
}
vertex_end = vertexBuffer->index();
vertex_end = vertexBuffer.index();
}
void PointBucket::render(Painter& painter, const std::string& layer_name, const Tile::ID& id) {
@@ -46,7 +46,7 @@ bool PointBucket::hasPoints() const {
}
void PointBucket::drawPoints(PointShader& shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer->itemSize);
array.bind(shader, *vertexBuffer, vertex_index);
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
array.bind(shader, vertexBuffer, vertex_index);
glDrawArrays(GL_POINTS, 0, (GLsizei)(vertex_end - vertex_start));
}

View File

@@ -0,0 +1,163 @@
#include <llmr/renderer/text_bucket.hpp>
#include <llmr/geometry/text_buffer.hpp>
#include <llmr/geometry/elements_buffer.hpp>
#include <llmr/geometry/geometry.hpp>
#include <llmr/renderer/painter.hpp>
#include <llmr/shader/text_shader.hpp>
#include <llmr/style/style.hpp>
#include <llmr/map/vector_tile.hpp>
#include <llmr/text/placement.hpp>
#include <llmr/util/math.hpp>
#include <llmr/platform/gl.hpp>
#include <iostream>
#define BUFFER_OFFSET(i) ((char *) nullptr + (i))
#include <cassert>
using namespace llmr;
TextBucket::TextBucket(
TextVertexBuffer &vertexBuffer,
TriangleElementsBuffer &triangleElementsBuffer,
const BucketDescription &bucket_desc, Placement &placement)
: geom_desc(bucket_desc.geometry),
vertexBuffer(vertexBuffer),
triangleElementsBuffer(triangleElementsBuffer),
placement(placement),
vertex_start(vertexBuffer.index()),
triangle_elements_start(triangleElementsBuffer.index()) {}
void TextBucket::addGlyphs(const PlacedGlyphs &glyphs, float placementZoom,
PlacementRange placementRange, float zoom) {
placementZoom += zoom;
for (const PlacedGlyph &glyph : glyphs) {
const auto &tl = glyph.tl;
const auto &tr = glyph.tr;
const auto &bl = glyph.bl;
const auto &br = glyph.br;
const auto &tex = glyph.tex;
const auto &width = glyph.width;
const auto &height = glyph.height;
const auto &angle = glyph.angle;
float minZoom = util::max(
static_cast<float>(zoom + log(glyph.glyphBox.minScale) / log(2)),
placementZoom);
float maxZoom = util::min(
static_cast<float>(zoom + log(glyph.glyphBox.maxScale) / log(2)),
25.0f);
const auto &glyphAnchor = glyph.glyphBox.anchor;
if (maxZoom <= minZoom)
continue;
// Lower min zoom so that while fading out the label
// it can be shown outside of collision-free zoom levels
if (minZoom == placementZoom) {
minZoom = 0;
}
const int glyph_vertex_length = 4;
if (!triangleGroups.size() ||
(triangleGroups.back().vertex_length + glyph_vertex_length >
65535)) {
// Move to a new group because the old one can't hold the geometry.
triangleGroups.emplace_back();
}
// We're generating triangle fans, so we always start with the first
// coordinate in this polygon.
triangle_group_type &triangleGroup = triangleGroups.back();
uint32_t triangleIndex = triangleGroup.vertex_length;
// coordinates (2 triangles)
vertexBuffer.add(glyphAnchor.x, glyphAnchor.y, tl.x, tl.y, tex.x,
tex.y, angle, minZoom, placementRange, maxZoom,
placementZoom);
vertexBuffer.add(glyphAnchor.x, glyphAnchor.y, tr.x, tr.y,
tex.x + width, tex.y, angle, minZoom, placementRange,
maxZoom, placementZoom);
vertexBuffer.add(glyphAnchor.x, glyphAnchor.y, bl.x, bl.y, tex.x,
tex.y + height, angle, minZoom, placementRange,
maxZoom, placementZoom);
vertexBuffer.add(glyphAnchor.x, glyphAnchor.y, br.x, br.y,
tex.x + width, tex.y + height, angle, minZoom,
placementRange, maxZoom, placementZoom);
// add the two triangles, referencing the four coordinates we just
// inserted.
triangleElementsBuffer.add(triangleIndex + 0, triangleIndex + 1,
triangleIndex + 2);
triangleElementsBuffer.add(triangleIndex + 1, triangleIndex + 2,
triangleIndex + 3);
triangleGroup.vertex_length += glyph_vertex_length;
triangleGroup.elements_length += 2;
}
};
void TextBucket::addFeature(const VectorTileFeature &feature,
const IndexedFaces &faces,
const std::map<Value, Shaping> &shapings) {
auto it_prop = feature.properties.find(geom_desc.text_field);
if (it_prop == feature.properties.end()) {
// feature does not have the correct property
return;
}
const Value &value = it_prop->second;
auto it_shaping = shapings.find(value);
if (it_shaping == shapings.end()) {
// we lack shaping information for this label
return;
}
const Shaping &shaping = it_shaping->second;
// std::cerr << "we have shaping for " << value << std::endl;
// Decode all lines.
std::vector<Coordinate> line;
Geometry::command cmd;
Coordinate coord;
pbf geom = feature.geometry;
Geometry geometry(geom);
int32_t x, y;
while ((cmd = geometry.next(x, y)) != Geometry::end) {
if (cmd == Geometry::move_to) {
if (!line.empty()) {
placement.addFeature(*this, line, geom_desc, faces, shaping);
line.clear();
}
}
line.emplace_back(x, y);
}
if (line.size()) {
placement.addFeature(*this, line, geom_desc, faces, shaping);
}
}
void TextBucket::render(Painter &painter, const std::string &layer_name,
const Tile::ID &id) {
painter.renderText(*this, layer_name, id);
}
bool TextBucket::empty() const { return triangleGroups.empty(); }
void TextBucket::drawGlyphs(TextShader &shader) {
char *vertex_index = BUFFER_OFFSET(vertex_start * vertexBuffer.itemSize);
char *elements_index =
BUFFER_OFFSET(triangle_elements_start * triangleElementsBuffer.itemSize);
for (triangle_group_type &group : triangleGroups) {
group.array.bind(shader, vertexBuffer, triangleElementsBuffer, vertex_index);
glDrawElements(GL_TRIANGLES, group.elements_length * 3, GL_UNSIGNED_SHORT, elements_index);
vertex_index += group.vertex_length * vertexBuffer.itemSize;
elements_index += group.elements_length * triangleElementsBuffer.itemSize;
}
}

View File

@@ -102,20 +102,9 @@ bool Shader::compileShader(GLuint *shader, GLenum type, const GLchar *source) {
GLint status;
*shader = glCreateShader(type);
#if defined(GL_ES_VERSION_2_0)
// Add OpenGL ES precision premable
const GLchar *preamble = "precision highp float;\n\n";
#else
// Desktop GLSL
const GLchar *preamble = "#version 120\n\n";
#endif
const GLchar *strings[] = { preamble, source };
const GLint lengths[] = { (GLint)strlen(preamble), (GLint)strlen(source) };
glShaderSource(*shader, 2, strings, lengths);
const GLchar *strings[] = { source };
const GLint lengths[] = { (GLint)strlen(source) };
glShaderSource(*shader, 1, strings, lengths);
glCompileShader(*shader);

View File

@@ -1,35 +0,0 @@
// NOTE: DO NOT CHANGE THIS FILE. IT IS AUTOMATICALLY GENERATED.
#include <llmr/shader/shaders.hpp>
using namespace llmr;
const shader_source llmr::shaders[SHADER_COUNT] = {
{
"// floor(127 / 2) == 63.0\n// the maximum allowed miter limit is 2.0 at the moment. the extrude normal is\n// stored in a byte (-128..127). we scale regular normals up to length 63, but\n// there are also \"special\" normals that have a bigger length (of up to 126 in\n// this case).\n// #define scale 63.0\n#define scale 0.015873016\n\nattribute vec2 a_pos;\nattribute vec2 a_extrude;\n// attribute float a_linesofar;\n\n// posmatrix is for the vertex position, exmatrix is for rotating and projecting\n// the extrusion vector.\nuniform mat4 u_matrix;\nuniform mat4 u_exmatrix;\n\n// shared\nuniform float u_ratio;\nuniform vec2 u_linewidth;\nuniform vec4 u_color;\n\nvarying vec2 v_normal;\n// varying float v_linesofar;\n\nvoid main() {\n // We store the texture normals in the most insignificant bit\n // transform y so that 0 => -1 and 1 => 1\n // In the texture normal, x is 0 if the normal points straight up/down and 1 if it's a round cap\n // y is 1 if the normal points up, and -1 if it points down\n vec2 normal = mod(a_pos, 2.0);\n normal.y = sign(normal.y - 0.5);\n v_normal = normal;\n\n // Scale the extrusion vector down to a normal and then up by the line width\n // of this vertex.\n vec4 dist = vec4(u_linewidth.s * a_extrude * scale, 0.0, 0.0);\n\n // Remove the texture normal bit of the position before scaling it with the\n // model/view matrix. Add the extrusion vector *after* the model/view matrix\n // because we're extruding the line in pixel space, regardless of the current\n // tile's zoom level.\n gl_Position = u_matrix * vec4(floor(a_pos * 0.5), 0.0, 1.0) + u_exmatrix * dist;\n // v_linesofar = a_linesofar * u_ratio;\n}",
"uniform vec2 u_linewidth;\nuniform vec4 u_color;\n\nuniform vec2 u_dasharray;\n\nvarying vec2 v_normal;\n// varying float v_linesofar;\n\nvoid main() {\n // Calculate the distance of the pixel from the line in pixels.\n float dist = length(v_normal) * u_linewidth.s;\n\n // Calculate the antialiasing fade factor. This is either when fading in\n // the line in case of an offset line (v_linewidth.t) or when fading out\n // (v_linewidth.s)\n float alpha = clamp(min(dist - (u_linewidth.t - 1.0), u_linewidth.s - dist), 0.0, 1.0);\n\n // Calculate the antialiasing fade factor based on distance to the dash.\n // Only affects alpha when line is dashed\n // float pos = mod(v_linesofar, u_dasharray.x + u_dasharray.y);\n // alpha *= max(step(0.0, -u_dasharray.y), clamp(min(pos, u_dasharray.x - pos), 0.0, 1.0));\n\n gl_FragColor = u_color * alpha;\n}",
},
{
"attribute vec2 a_pos;\n\nuniform mat4 u_matrix;\nuniform vec2 u_world;\nuniform float u_size;\n\nvarying vec2 v_pos;\n\nvoid main() {\n gl_Position = u_matrix * vec4(floor(a_pos / 2.0), 0.0, 1.0);\n v_pos = (gl_Position.xy + 1.0) * u_world;\n gl_PointSize = u_size;\n}\n",
"uniform vec4 u_color;\nuniform vec2 u_linewidth;\n\nvarying vec2 v_pos;\n\nvoid main() {\n float dist = length(v_pos - gl_FragCoord.xy);\n\n // Calculate the antialiasing fade factor. This is either when fading in\n // the line in case of an offset line (v_linewidth.t) or when fading out\n // (v_linewidth.s)\n float alpha = clamp(min(dist - (u_linewidth.t - 1.0), u_linewidth.s - dist), 0.0, 1.0);\n gl_FragColor = u_color * alpha;\n}\n",
},
{
"attribute vec2 a_pos;\nuniform mat4 u_matrix;\nuniform vec2 u_world;\n\nvarying vec2 v_pos;\n\nvoid main() {\n gl_Position = u_matrix * vec4(a_pos, 0, 1);\n v_pos = (gl_Position.xy + 1.0) / 2.0 * u_world;\n}\n",
"uniform vec4 u_color;\n\nvarying vec2 v_pos;\n\nvoid main() {\n float dist = length(v_pos - gl_FragCoord.xy);\n float alpha = smoothstep(1.0, 0.0, dist);\n gl_FragColor = u_color * alpha;\n}\n",
},
{
"uniform mat4 u_matrix;\n\nattribute vec2 a_pos;\n\nvarying vec2 v_pos;\n\nvoid main() {\n v_pos = a_pos;\n gl_Position = u_matrix * vec4(a_pos, 0, 1);\n}\n",
"uniform vec4 u_color;\n\nuniform vec2 u_offset;\nuniform vec2 u_pattern_size;\nuniform vec2 u_pattern_tl;\nuniform vec2 u_pattern_br;\nuniform float u_mix;\n\n\nuniform sampler2D u_image;\n\nvarying vec2 v_pos;\n\nvoid main() {\n\n vec2 imagecoord = mod((v_pos + u_offset) / u_pattern_size, 1.0);\n vec2 pos = mix(u_pattern_tl, u_pattern_br, imagecoord);\n vec4 color1 = texture2D(u_image, pos);\n\n vec2 imagecoord2 = mod(imagecoord * 2.0, 1.0);\n vec2 pos2 = mix(u_pattern_tl, u_pattern_br, imagecoord2);\n vec4 color2 = texture2D(u_image, pos2);\n\n vec4 color = mix(color1, color2, u_mix);\n gl_FragColor = color + u_color * (1.0 - color.a);\n}\n",
},
{
"attribute vec2 a_pos;\n\nuniform mat4 u_matrix;\n\nvoid main() {\n gl_Position = u_matrix * vec4(a_pos, 0, 1);\n}\n",
"uniform vec4 u_color;\n\nvoid main() {\n gl_FragColor = u_color;\n}\n",
},
{
"attribute vec2 a_pos;\n\nuniform mat4 u_matrix;\nuniform float u_size;\n\nvoid main() {\n gl_Position = u_matrix * vec4(a_pos, 0, 1);\n gl_PointSize = u_size;\n}\n",
"#define root2 1.4142135623730951\n\nuniform sampler2D u_image;\nuniform vec2 u_tl;\nuniform vec2 u_br;\nuniform vec4 u_color;\n\nuniform vec2 pos;\nuniform float inbounds;\nuniform vec4 color;\n\nvoid main() {\n vec2 pos = (gl_PointCoord * 2.0 - 1.0) * root2 / 2.0 + 0.5;\n\n float inbounds = step(0.0, pos.x) * step(0.0, pos.y) *\n (1.0 - step(1.0, pos.x)) * (1.0 - step(1.0, pos.y));\n\n vec4 color = texture2D(u_image, mix(u_tl, u_br, pos)) * inbounds;\n\n gl_FragColor = color * u_color;\n}\n",
},
{
"uniform mat4 u_matrix;\n\nattribute vec2 a_pos;\n\nvarying vec2 v_pos;\n\nvoid main() {\n gl_Position = u_matrix * vec4(a_pos, 0, 1);\n v_pos = a_pos / 4096.0;\n}\n",
"uniform sampler2D u_image;\nuniform float u_opacity;\n\nvarying vec2 v_pos;\n\nvoid main() {\n gl_FragColor = texture2D(u_image, v_pos) * u_opacity;\n}\n",
}
};

View File

@@ -0,0 +1,13 @@
uniform sampler2D u_texture;
uniform vec4 u_color;
uniform float u_buffer;
uniform float u_gamma;
varying vec2 v_tex;
varying float v_alpha;
void main() {
float dist = texture2D(u_texture, v_tex).a;
float alpha = smoothstep(u_buffer - u_gamma, u_buffer + u_gamma, dist) * v_alpha;
gl_FragColor = u_color * alpha;
}

View File

@@ -0,0 +1,69 @@
attribute vec2 a_pos;
attribute vec2 a_offset;
attribute vec4 a_data1;
attribute vec4 a_data2;
// posmatrix is for the vertex position, exmatrix is for rotating and projecting
// the extrusion vector.
uniform mat4 u_matrix;
uniform mat4 u_exmatrix;
uniform float u_angle;
uniform float u_zoom;
uniform float u_flip;
uniform float u_fadedist;
uniform float u_minfadezoom;
uniform float u_maxfadezoom;
uniform float u_fadezoom;
uniform vec2 u_texsize;
varying vec2 v_tex;
varying float v_alpha;
void main() {
vec2 a_tex = a_data1.xy;
float a_labelminzoom = a_data1[2];
float a_angle = a_data1[3];
vec2 a_zoom = a_data2.st;
vec2 a_range = a_data2.pq;
float rev = 0.0;
// u_angle is angle of the map, -128..128 representing 0..2PI
// a_angle is angle of the label, 0..256 representing 0..2PI, where 0 is horizontal text
float rotated = mod(a_angle + u_angle, 256.0);
// if the label rotates with the map, and if the rotated label is upside down, hide it
if (u_flip > 0.0 && rotated >= 64.0 && rotated < 192.0) rev = 1.0;
// If the label should be invisible, we move the vertex outside
// of the view plane so that the triangle gets clipped. This makes it easier
// for us to create degenerate triangle strips.
// u_zoom is the current zoom level adjusted for the change in font size
float z = 2.0 - step(a_zoom[0], u_zoom) - (1.0 - step(a_zoom[1], u_zoom)) + rev;
// fade out labels
float alpha = clamp((u_fadezoom - a_labelminzoom) / u_fadedist, 0.0, 1.0);
if (u_fadedist >= 0.0) {
v_alpha = alpha;
} else {
v_alpha = 1.0 - alpha;
}
if (u_maxfadezoom < a_labelminzoom) {
v_alpha = 0.0;
}
if (u_minfadezoom >= a_labelminzoom) {
v_alpha = 1.0;
}
// if label has been faded out, clip it
z += step(v_alpha, 0.0);
// all the angles are 0..256 representing 0..2PI
// hide if (angle >= a_rangeend && angle < rangestart)
z += (1.0 - step(a_range[0], u_angle)) * step(a_range[1], u_angle);
gl_Position = u_matrix * vec4(a_pos, 0, 1) + u_exmatrix * vec4(a_offset / 64.0, z, 0);
v_tex = a_tex * 4.0 / u_texsize;
}

157
src/shader/text_shader.cpp Normal file
View File

@@ -0,0 +1,157 @@
#include <llmr/shader/text_shader.hpp>
#include <llmr/shader/shaders.hpp>
#include <llmr/platform/gl.hpp>
#include <cstdio>
using namespace llmr;
TextShader::TextShader()
: Shader(
shaders[TEXT_SHADER].vertex,
shaders[TEXT_SHADER].fragment
) {
if (!valid) {
fprintf(stderr, "invalid text shader\n");
return;
}
a_pos = glGetAttribLocation(program, "a_pos");
a_offset = glGetAttribLocation(program, "a_offset");
a_data1 = glGetAttribLocation(program, "a_data1");
a_data2 = glGetAttribLocation(program, "a_data2");
u_matrix = glGetUniformLocation(program, "u_matrix");
u_color = glGetUniformLocation(program, "u_color");
u_buffer = glGetUniformLocation(program, "u_buffer");
u_gamma = glGetUniformLocation(program, "u_gamma");
u_exmatrix = glGetUniformLocation(program, "u_exmatrix");
u_angle = glGetUniformLocation(program, "u_angle");
u_zoom = glGetUniformLocation(program, "u_zoom");
u_flip = glGetUniformLocation(program, "u_flip");
u_fadedist = glGetUniformLocation(program, "u_fadedist");
u_minfadezoom = glGetUniformLocation(program, "u_minfadezoom");
u_maxfadezoom = glGetUniformLocation(program, "u_maxfadezoom");
u_fadezoom = glGetUniformLocation(program, "u_fadezoom");
u_texsize = glGetUniformLocation(program, "u_texsize");
// fprintf(stderr, "TextShader:\n");
// fprintf(stderr, " - a_pos: %d\n", a_pos);
// fprintf(stderr, " - a_offset: %d\n", a_offset);
// fprintf(stderr, " - a_data1: %d\n", a_tex);
// fprintf(stderr, " - a_data2: %d\n", a_tex);
// fprintf(stderr, " - u_color: %d\n", u_color);
// fprintf(stderr, " - u_buffer: %d\n", u_buffer);
// fprintf(stderr, " - u_gamma: %d\n", u_gamma);
// fprintf(stderr, " - u_exmatrix: %d\n", u_exmatrix);
// fprintf(stderr, " - u_angle: %d\n", u_angle);
// fprintf(stderr, " - u_zoom: %d\n", u_zoom);
// fprintf(stderr, " - u_flip: %d\n", u_flip);
// fprintf(stderr, " - u_fadedist: %d\n", u_fadedist);
// fprintf(stderr, " - u_minfadezoom: %d\n", u_minfadezoom);
// fprintf(stderr, " - u_maxfadezoom: %d\n", u_maxfadezoom);
// fprintf(stderr, " - u_fadezoom: %d\n", u_fadezoom);
// fprintf(stderr, " - u_texsize: %d\n", u_texsize);
}
void TextShader::bind(char *offset) {
glEnableVertexAttribArray(a_pos);
glVertexAttribPointer(a_pos, 2, GL_SHORT, false, 16, offset + 0);
glEnableVertexAttribArray(a_offset);
glVertexAttribPointer(a_offset, 2, GL_SHORT, false, 16, offset + 4);
glEnableVertexAttribArray(a_data1);
glVertexAttribPointer(a_data1, 4, GL_UNSIGNED_BYTE, false, 16, offset + 8);
glEnableVertexAttribArray(a_data2);
glVertexAttribPointer(a_data2, 4, GL_UNSIGNED_BYTE, false, 16, offset + 12);
}
void TextShader::setColor(const std::array<float, 4>& new_color) {
if (color != new_color) {
glUniform4fv(u_color, 1, new_color.data());
color = new_color;
}
}
void TextShader::setColor(float r, float g, float b, float a) {
setColor({{ r, g, b, a }});
}
void TextShader::setBuffer(float new_buffer) {
if (buffer != new_buffer) {
glUniform1f(u_buffer, new_buffer);
buffer = new_buffer;
}
}
void TextShader::setGamma(float new_gamma) {
if (gamma != new_gamma) {
glUniform1f(u_gamma, new_gamma);
gamma = new_gamma;
}
}
void TextShader::setExtrudeMatrix(const std::array<float, 16> &new_exmatrix) {
if (exmatrix != new_exmatrix) {
glUniformMatrix4fv(u_exmatrix, 1, GL_FALSE, new_exmatrix.data());
exmatrix = new_exmatrix;
}
}
void TextShader::setAngle(float new_angle) {
if (angle != new_angle) {
glUniform1f(u_angle, new_angle);
angle = new_angle;
}
}
void TextShader::setZoom(float new_zoom) {
if (zoom != new_zoom) {
glUniform1f(u_zoom, new_zoom);
zoom = new_zoom;
}
}
void TextShader::setFlip(float new_flip) {
if (flip != new_flip) {
glUniform1f(u_flip, new_flip);
flip = new_flip;
}
}
void TextShader::setFadeDist(float new_fadedist) {
if (fadedist != new_fadedist) {
glUniform1f(u_fadedist, new_fadedist);
fadedist = new_fadedist;
}
}
void TextShader::setMinFadeZoom(float new_minfadezoom) {
if (minfadezoom != new_minfadezoom) {
glUniform1f(u_minfadezoom, new_minfadezoom);
minfadezoom = new_minfadezoom;
}
}
void TextShader::setMaxFadeZoom(float new_maxfadezoom) {
if (maxfadezoom != new_maxfadezoom) {
glUniform1f(u_maxfadezoom, new_maxfadezoom);
maxfadezoom = new_maxfadezoom;
}
}
void TextShader::setFadeZoom(float new_fadezoom) {
if (fadezoom != new_fadezoom) {
glUniform1f(u_fadezoom, new_fadezoom);
fadezoom = new_fadezoom;
}
}
void TextShader::setTextureSize(const std::array<float, 2> &new_texsize) {
if (texsize != new_texsize) {
glUniform2fv(u_texsize, 1, new_texsize.data());
texsize = new_texsize;
}
}

View File

@@ -89,3 +89,23 @@ bool functions::linear(float z, const std::vector<bool>&) {
// TODO
return false;
}
float functions::exponential(float z, const std::vector<float>& values) {
if (values.size() != 5) {
return 0;
}
const float z_base = values[0];
const float val = values[1];
const float slope = values[2];
const float min = values[3];
const float max = values[4];
return fmin(fmax(min, val + pow(1.75, (z - z_base)) * slope), max);
// return fmin(fmax(min, val + (z - z_base) * slope), max);
}
bool functions::exponential(float z, const std::vector<bool>&) {
// TODO
return false;
}

View File

@@ -1,7 +0,0 @@
// NOTE: DO NOT CHANGE THIS FILE. IT IS AUTOMATICALLY GENERATED.
#include <llmr/style/resources.hpp>
using namespace llmr;
const unsigned char resources::style[] = R"JSON({"buckets":{"water":{"source":"streets","layer":"water","type":"fill"},"road_large":{"source":"streets","layer":"road","field":"class","value":["motorway","main"],"type":"line","cap":"round","join":"round"},"road_regular":{"source":"streets","layer":"road","field":"class","value":"street","type":"line","cap":"round","join":"round"},"road_limited":{"source":"streets","layer":"road","field":"class","value":"street_limited","type":"line","cap":"round","join":"round"},"park":{"source":"streets","layer":"landuse","field":"class","value":"park","type":"fill"},"wood":{"source":"streets","layer":"landuse","field":"class","value":"wood","type":"fill"},"school":{"source":"streets","layer":"landuse","field":"class","value":"school","type":"fill"},"cemetery":{"source":"streets","layer":"landuse","field":"class","value":"cemetery","type":"fill"},"industrial":{"source":"streets","layer":"landuse","field":"class","value":"industrial","type":"fill"},"building":{"source":"streets","layer":"building","type":"fill"},"alcohol_poi":{"source":"streets","layer":"poi_label","field":"type","value":["Alcohol"],"type":"point"},"cafe_poi":{"source":"streets","layer":"poi_label","field":"type","value":["Cafe"],"type":"point"},"embassy_poi":{"source":"streets","layer":"poi_label","field":"type","value":["Embassy"],"type":"point"},"park_poi":{"source":"streets","layer":"poi_label","field":"type","value":["Park"],"type":"point"},"restaurant_poi":{"source":"streets","layer":"poi_label","field":"type","value":["Restaurant"],"type":"point"}},"sprite":"img/maki-sprite","constants":{"park":"#c8df9f"},"structure":[{"name":"park","bucket":"park"},{"name":"wood","bucket":"wood"},{"name":"water","bucket":"water"},{"name":"building","bucket":"building"},{"name":"road_limited","bucket":"road_limited"},{"name":"road_regular","bucket":"road_regular"},{"name":"road_large","bucket":"road_large"},{"name":"alcohol_poi","bucket":"alcohol_poi"},{"name":"cafe_poi","bucket":"cafe_poi"},{"name":"embassy_poi","bucket":"embassy_poi"},{"name":"park_poi","bucket":"park_poi"},{"name":"restaurant_poi","bucket":"restaurant_poi"}],"classes":[{"name":"default","layers":{"background":{"type":"background","color":"#FFFFFF"},"park":{"type":"fill","color":"park","antialias":true,"image":"park"},"wood":{"type":"fill","color":"#33AA66","antialias":true,"opacity":0.1},"water":{"type":"fill","color":"#bae0e9","stroke":"#73b6e6","antialias":true,"image":"water_etsy"},"building":{"type":"fill","color":"#000000","antialias":true,"opacity":["linear",13,0,0.1,0,0.1]},"road_limited":{"type":"line","color":"#BBBBBB","width":["stops",{"z":0,"val":1},{"z":30,"val":1}]},"road_regular":{"type":"line","color":"#999999","width":["stops",{"z":0,"val":0.5},{"z":13,"val":0.5},{"z":16,"val":2},{"z":20,"val":32},{"z":30,"val":32}]},"road_large":{"type":"line","color":"#666666","width":["stops",{"z":0,"val":0.5},{"z":11,"val":0.5},{"z":13,"val":1},{"z":16,"val":4},{"z":20,"val":64},{"z":30,"val":64}]},"alcohol_poi":{"type":"point","color":"#cccccc","size":18,"image":"alcohol-shop","opacity":["linear",15,0,1,0,0.75]},"cafe_poi":{"type":"point","color":"#cccccc","size":18,"image":"cafe","opacity":["linear",15,0,1,0,0.75]},"embassy_poi":{"type":"point","color":"#cccccc","size":18,"image":"embassy","opacity":["linear",15,0,1,0,0.75]},"park_poi":{"type":"point","color":"#cccccc","size":18,"image":"park","opacity":["linear",15,0,1,0,0.75]},"restaurant_poi":{"type":"point","color":"#cccccc","size":18,"image":"restaurant","opacity":["linear",15,0,1,0,0.75]}}}]})JSON";
const unsigned long resources::style_size = sizeof(resources::style);

View File

@@ -1,5 +1,6 @@
#include <llmr/style/style.hpp>
#include <llmr/style/style_parser.hpp>
#include <csscolorparser/csscolorparser.hpp>
#include <rapidjson/document.h>
@@ -36,7 +37,7 @@ void Style::cascade(float z) {
// TODO: This should be restricted to fill styles that have actual
// values so as to not override with default values.
llmr::FillProperties& fill = computed.fills[layer_name];
fill.hidden = layer.hidden(z);
fill.enabled = layer.enabled(z);
fill.winding = layer.winding;
fill.antialias = layer.antialias(z);
fill.fill_color = layer.fill_color;
@@ -53,7 +54,7 @@ void Style::cascade(float z) {
// TODO: This should be restricted to line styles that have actual
// values so as to not override with default values.
llmr::LineProperties& stroke = computed.lines[layer_name];
stroke.hidden = layer.hidden(z);
stroke.enabled = layer.enabled(z);
stroke.width = layer.width(z);
stroke.offset = layer.offset(z);
stroke.color = layer.color;
@@ -68,13 +69,29 @@ void Style::cascade(float z) {
// TODO: This should be restricted to point styles that have actual
// values so as to not override with default values.
llmr::PointProperties& point = computed.points[layer_name];
point.hidden = layer.hidden(z);
point.enabled = layer.enabled(z);
point.color = layer.color;
point.size = layer.size(z);
point.opacity = layer.opacity(z);
point.image = layer.image;
}
for (const auto& text_pair : sheetClass.text) {
const std::string& layer_name = text_pair.first;
const llmr::TextClass& layer = text_pair.second;
// TODO: This should be restricted to point styles that have actual
// values so as to not override with default values.
llmr::TextProperties& text = computed.texts[layer_name];
text.enabled = layer.enabled(z);
text.color = layer.color;
text.size = layer.size(z);
text.halo = layer.halo;
text.haloRadius = layer.haloRadius(z);
text.rotate = layer.rotate(z);
text.alwaysVisible = layer.alwaysVisible(z);
}
// Cascade background
computed.background.color = sheetClass.background.color;
}

View File

@@ -31,6 +31,12 @@ BucketDescription StyleParser::parseBucket(JSVal value) {
} else {
throw Style::exception("bucket type must be a string");
}
} else if (name == "feature_type") {
if (value.IsString()) {
bucket.feature_type = bucketType({ value.GetString(), value.GetStringLength() });
} else {
throw Style::exception("feature type must be a string");
}
} else if (name == "source") {
if (value.IsString()) {
bucket.source_name = { value.GetString(), value.GetStringLength() };
@@ -81,6 +87,18 @@ BucketDescription StyleParser::parseBucket(JSVal value) {
} else {
throw Style::exception("font size must be a number");
}
} else if (name == "text_field") {
if (value.IsString()) {
bucket.geometry.text_field = { value.GetString(), value.GetStringLength() };
} else {
throw Style::exception("text field must be a string");
}
} else if (name == "path") {
if (value.IsString()) {
bucket.geometry.path = textPathType({ value.GetString(), value.GetStringLength() });
} else {
throw Style::exception("curve must be a string");
}
} else if (name == "miterLimit") {
if (value.IsNumber()) {
bucket.geometry.miter_limit = value.GetDouble();
@@ -93,7 +111,24 @@ BucketDescription StyleParser::parseBucket(JSVal value) {
} else {
throw Style::exception("round limit must be a number");
}
} else if (name == "textMinDistance") {
if (value.IsNumber()) {
bucket.geometry.textMinDistance = value.GetDouble();
} else {
throw Style::exception("text min distance must be a number");
}
} else if (name == "maxAngleDelta") {
if (value.IsNumber()) {
bucket.geometry.maxAngleDelta = value.GetDouble();
} else {
throw Style::exception("max angle delta must be a number");
}
}
}
if (bucket.feature_type == BucketType::None) {
bucket.feature_type = bucket.type;
}
return bucket;
@@ -217,6 +252,8 @@ void StyleParser::parseClass(const std::string& name, JSVal value, ClassDescript
class_desc.line.insert({ name, std::forward<LineClass>(parseLineClass(value)) });
} else if (type_name == "point") {
class_desc.point.insert({ name, std::forward<PointClass>(parsePointClass(value)) });
} else if (type_name == "text") {
class_desc.text.insert({ name, std::forward<TextClass>(parseTextClass(value)) });
} else if (type_name == "background") {
class_desc.background = parseBackgroundClass(value);
} else {
@@ -250,24 +287,35 @@ std::string StyleParser::parseString(JSVal value) {
}
Color StyleParser::parseColor(JSVal value) {
if (!value.IsString()) {
if (value.IsArray()) {
// [ r, g, b, a] array
if (value.Size() != 4) {
throw Style::exception("color array must have four elements");
}
JSVal r = value[(rapidjson::SizeType)0], g = value[1], b = value[2], a = value[3];
if (!r.IsNumber() || !g.IsNumber() || !b.IsNumber() || !a.IsNumber()) {
throw Style::exception("color values must be numbers");
}
return {{static_cast<float>(r.GetDouble()),
static_cast<float>(g.GetDouble()),
static_cast<float>(b.GetDouble()),
static_cast<float>(a.GetDouble())}};
} else if (!value.IsString()) {
throw Style::exception("color value must be a string");
}
const std::string str { value.GetString(), value.GetStringLength() };
const std::string str{value.GetString(), value.GetStringLength()};
auto it = constants.find(str);
if (it != constants.end()) {
return parseColor(*it->second);
} else {
CSSColorParser::Color css_color = CSSColorParser::parse(str);
return {{
(float)css_color.r / 255,
(float)css_color.g / 255,
(float)css_color.b / 255,
css_color.a
}
};
return {{(float)css_color.r / 255, (float)css_color.g / 255,
(float)css_color.b / 255, css_color.a}};
}
}
@@ -281,6 +329,8 @@ typename FunctionProperty<T>::fn StyleParser::parseFunctionType(JSVal type) {
return &functions::linear;
} else if (t == "stops") {
return &functions::stops;
} else if (t == "exponential") {
return &functions::exponential;
} else {
throw Style::exception("unknown function type");
}
@@ -378,8 +428,8 @@ FunctionProperty<bool> StyleParser::parseBoolFunction(JSVal value) {
FillClass StyleParser::parseFillClass(JSVal value) {
FillClass klass;
if (value.HasMember("hidden")) {
klass.hidden = parseBoolFunction(value["hidden"]);
if (value.HasMember("enabled")) {
klass.enabled = parseBoolFunction(value["enabled"]);
}
if (value.HasMember("color")) {
@@ -410,8 +460,8 @@ FillClass StyleParser::parseFillClass(JSVal value) {
LineClass StyleParser::parseLineClass(JSVal value) {
LineClass klass;
if (value.HasMember("hidden")) {
klass.hidden = parseBoolFunction(value["hidden"]);
if (value.HasMember("enabled")) {
klass.enabled = parseBoolFunction(value["enabled"]);
}
if (value.HasMember("color")) {
@@ -432,8 +482,8 @@ LineClass StyleParser::parseLineClass(JSVal value) {
PointClass StyleParser::parsePointClass(JSVal value) {
PointClass klass;
if (value.HasMember("hidden")) {
klass.hidden = parseBoolFunction(value["hidden"]);
if (value.HasMember("enabled")) {
klass.enabled = parseBoolFunction(value["enabled"]);
}
if (value.HasMember("color")) {
@@ -455,6 +505,41 @@ PointClass StyleParser::parsePointClass(JSVal value) {
return klass;
}
TextClass StyleParser::parseTextClass(JSVal value) {
TextClass klass;
if (value.HasMember("enabled")) {
klass.enabled = parseBoolFunction(value["enabled"]);
}
if (value.HasMember("color")) {
klass.color = parseColor(value["color"]);
}
if (value.HasMember("stroke")) {
klass.halo = parseColor(value["stroke"]);
}
if (value.HasMember("strokeWidth")) {
klass.haloRadius = parseFloatFunction(value["strokeWidth"]);
}
if (value.HasMember("size")) {
klass.size = parseFloatFunction(value["size"]);
}
if (value.HasMember("rotate")) {
klass.rotate = parseFloatFunction(value["rotate"]);
}
if (value.HasMember("alwaysVisible")) {
klass.alwaysVisible = parseBoolFunction(value["alwaysVisible"]);
}
return klass;
}
BackgroundClass StyleParser::parseBackgroundClass(JSVal value) {
BackgroundClass klass;

View File

@@ -26,3 +26,12 @@ llmr::Value llmr::parseValue(pbf data) {
}
return false;
}
std::string llmr::toString(const llmr::Value& value) {
if (value.is<std::string>()) return value.get<std::string>();
else if (value.is<bool>()) return value.get<bool>() ? "true" : "false";
else if (value.is<int64_t>()) return std::to_string(value.get<int64_t>());
else if (value.is<uint64_t>()) return std::to_string(value.get<uint64_t>());
else if (value.is<double>()) return std::to_string(value.get<double>());
else return "null";
}

321
src/text/collision.cpp Normal file
View File

@@ -0,0 +1,321 @@
#include <llmr/text/collision.hpp>
#include <llmr/text/rotation_range.hpp>
#include <llmr/util/math.hpp>
#include <boost/geometry.hpp>
#include <boost/geometry/geometries/point.hpp>
#include <boost/geometry/geometries/box.hpp>
#include <boost/geometry/index/rtree.hpp>
namespace llmr {
namespace bg = boost::geometry;
namespace bgm = bg::model;
namespace bgi = bg::index;
typedef bgm::point<float, 2, bg::cs::cartesian> Point;
typedef bgm::box<Point> Box;
typedef std::pair<Box, PlacementBox> PlacementValue;
typedef bgi::rtree<PlacementValue, bgi::quadratic<16>> Tree;
}
using namespace llmr;
Collision::~Collision() {
delete ((Tree *)tree);
}
Collision::Collision() : tree(new Tree()) {
const float m = 4096;
// Hack to prevent cross-tile labels
insert(
{{GlyphBox{
CollisionRect{CollisionPoint{0, 0}, CollisionPoint{0, m * 8}},
CollisionRect{CollisionPoint{0, 0}, CollisionPoint{0, m * 8}}, 0},
GlyphBox{
CollisionRect{CollisionPoint{0, 0}, CollisionPoint{m * 8, 0}},
CollisionRect{CollisionPoint{0, 0}, CollisionPoint{m * 8, 0}},
0}}},
CollisionAnchor(0, 0), 1, {{M_PI * 2, 0}}, false, 2);
insert({{GlyphBox{
CollisionRect{CollisionPoint{-m * 8, 0}, CollisionPoint{0, 0}},
CollisionRect{CollisionPoint{-m * 8, 0}, CollisionPoint{0, 0}},
0},
GlyphBox{
CollisionRect{CollisionPoint{0, -m * 8}, CollisionPoint{0, 0}},
CollisionRect{CollisionPoint{0, -m * 8}, CollisionPoint{0, 0}},
0}}},
CollisionAnchor{m, m}, 1, {{M_PI * 2, 0}}, false, 2);
}
GlyphBox getMergedGlyphs(const PlacedGlyphs &placed_glyphs, bool horizontal,
const CollisionAnchor &anchor) {
GlyphBox mergedGlyphs;
const float inf = std::numeric_limits<float>::infinity();
mergedGlyphs.box = CollisionRect{{inf, inf}, {-inf, -inf}};
mergedGlyphs.rotate = horizontal;
mergedGlyphs.anchor = anchor;
CollisionRect &box = mergedGlyphs.box;
for (const PlacedGlyph &placed_glyph : placed_glyphs) {
const CollisionRect &gbox = placed_glyph.glyphBox.box;
box.tl.x = util::min(box.tl.x, gbox.tl.x);
box.tl.y = util::min(box.tl.y, gbox.tl.y);
box.br.x = util::max(box.br.x, gbox.br.x);
box.br.y = util::max(box.br.y, gbox.br.y);
mergedGlyphs.minScale =
util::max(mergedGlyphs.minScale, placed_glyph.glyphBox.minScale);
}
return mergedGlyphs;
}
PlacementProperty Collision::place(const PlacedGlyphs &placed_glyphs,
const CollisionAnchor &anchor,
float minPlacementScale,
float maxPlacementScale, float padding,
bool horizontal, bool alwaysVisible) {
float minScale = std::numeric_limits<float>::infinity();
for (const PlacedGlyph &placed_glyph : placed_glyphs) {
minScale = util::min(minScale, placed_glyph.glyphBox.minScale);
}
minPlacementScale = util::max(minPlacementScale, minScale);
// Collision checks between rotating and fixed labels are
// relatively expensive, so we use one box per label, not per glyph
// for horizontal labels.
GlyphBoxes glyphs;
if (horizontal) {
glyphs.push_back(getMergedGlyphs(placed_glyphs, horizontal, anchor));
} else {
for (const PlacedGlyph &placed_glyph : placed_glyphs) {
glyphs.push_back(placed_glyph.glyphBox);
}
}
// Calculate bboxes for all the glyphs
for (GlyphBox &glyph : glyphs) {
if (horizontal) {
const CollisionRect &box = glyph.box;
float x12 = box.tl.x * box.tl.x, y12 = box.tl.y * box.tl.y,
x22 = box.br.x * box.br.x, y22 = box.br.y * box.br.y,
diag = sqrt(
util::max(x12 + y12, x12 + y22, x22 + y12, x22 + y22));
glyph.bbox = CollisionRect{{-diag, -diag}, {diag, diag}};
} else {
glyph.bbox = glyph.box;
}
}
// Calculate the minimum scale the entire label can be shown without
// collisions
float scale = alwaysVisible ? minPlacementScale
: getPlacementScale(glyphs, minPlacementScale,
maxPlacementScale, padding);
// Return if the label can never be placed without collision
if (scale < 0 || scale == std::numeric_limits<float>::infinity()) {
return PlacementProperty{};
}
// Calculate the range it is safe to rotate all glyphs
PlacementRange rotationRange = getPlacementRange(glyphs, scale);
insert(glyphs, anchor, scale, rotationRange, horizontal, padding);
float zoom = log(scale) / log(2);
return PlacementProperty{zoom, rotationRange};
}
float Collision::getPlacementScale(const GlyphBoxes &glyphs,
float minPlacementScale,
float maxPlacementScale, float pad) {
for (const GlyphBox &glyph : glyphs) {
const CollisionRect &bbox = glyph.bbox;
const CollisionRect &box = glyph.box;
const CollisionAnchor &anchor = glyph.anchor;
if (anchor.x < 0 || anchor.x > 4096 || anchor.y < 0 ||
anchor.y > 4096) {
return -1;
}
float minScale = fmax(minPlacementScale, glyph.minScale);
float maxScale = glyph.maxScale;
if (minScale >= maxScale) {
continue;
}
// Compute the scaled bounding box of the unrotated glyph
float minPlacedX = anchor.x + bbox.tl.x / minScale;
float minPlacedY = anchor.y + bbox.tl.y / minScale;
float maxPlacedX = anchor.x + bbox.br.x / minScale;
float maxPlacedY = anchor.y + bbox.br.y / minScale;
Box query_box{Point{minPlacedX, minPlacedY},
Point{maxPlacedX, maxPlacedY}};
std::vector<PlacementValue> blocking;
((Tree *)tree)
->query(bgi::intersects(query_box), std::back_inserter(blocking));
if (blocking.size()) {
const CollisionAnchor &na = anchor; // new anchor
const CollisionRect &nb = box; // new box
for (const PlacementValue &value : blocking) {
const PlacementBox &placement = value.second;
const CollisionAnchor &oa = placement.anchor; // old anchor
const CollisionRect &ob = placement.box; // old box
// If anchors are identical, we're going to skip the label.
// NOTE: this isn't right because there can be glyphs with
// the same anchor but differing box offsets.
if (na == oa) {
return -1;
}
// todo: unhardcode the 8 = tileExtent/tileSize
float padding = fmax(pad, placement.padding) * 8.0f;
// Original algorithm:
float s1 = (ob.tl.x - nb.br.x - padding) /
(na.x - oa.x); // scale at which new box is to the
// left of old box
float s2 = (ob.br.x - nb.tl.x + padding) /
(na.x - oa.x); // scale at which new box is to the
// right of old box
float s3 = (ob.tl.y - nb.br.y - padding) /
(na.y - oa.y); // scale at which new box is to the
// top of old box
float s4 = (ob.br.y - nb.tl.y + padding) /
(na.y - oa.y); // scale at which new box is to the
// bottom of old box
if (isnan(s1) || isnan(s2)) {
s1 = s2 = 1;
}
if (isnan(s3) || isnan(s4)) {
s3 = s4 = 1;
}
float collisionFreeScale = fmin(fmax(s1, s2), fmax(s3, s4));
// Only update label's min scale if the glyph was
// restricted by a collision
if (collisionFreeScale > minPlacementScale &&
collisionFreeScale > minScale &&
collisionFreeScale < maxScale &&
collisionFreeScale < placement.maxScale) {
minPlacementScale = collisionFreeScale;
}
if (minPlacementScale > maxPlacementScale) {
return -1;
}
}
}
}
return minPlacementScale;
}
PlacementRange Collision::getPlacementRange(const GlyphBoxes &glyphs,
float placementScale) {
PlacementRange placementRange = {{2.0f * M_PI, 0}};
for (const GlyphBox &glyph : glyphs) {
const CollisionRect &bbox = glyph.bbox;
const CollisionAnchor &anchor = glyph.anchor;
float minPlacedX = anchor.x + bbox.tl.x / placementScale;
float minPlacedY = anchor.y + bbox.tl.y / placementScale;
float maxPlacedX = anchor.x + bbox.br.x / placementScale;
float maxPlacedY = anchor.y + bbox.br.y / placementScale;
Box query_box{Point{minPlacedX, minPlacedY},
Point{maxPlacedX, maxPlacedY}};
std::vector<PlacementValue> blocking;
((Tree *)tree)
->query(bgi::intersects(query_box), std::back_inserter(blocking));
for (const PlacementValue &value : blocking) {
const Box &s = value.first;
const PlacementBox &b = value.second;
float x1, x2, y1, y2, intersectX, intersectY;
// Adjust and compare bboxes to see if the glyphs might intersect
if (placementScale > b.placementScale) {
x1 = b.anchor.x + b.bbox.tl.x / placementScale;
y1 = b.anchor.y + b.bbox.tl.y / placementScale;
x2 = b.anchor.x + b.bbox.br.x / placementScale;
y2 = b.anchor.y + b.bbox.br.y / placementScale;
intersectX = x1 < maxPlacedX && x2 > minPlacedX;
intersectY = y1 < maxPlacedY && y2 > minPlacedY;
} else {
x1 = anchor.x + bbox.tl.x / b.placementScale;
y1 = anchor.y + bbox.tl.y / b.placementScale;
x2 = anchor.x + bbox.br.x / b.placementScale;
y2 = anchor.y + bbox.br.y / b.placementScale;
intersectX = x1 < s.max_corner().get<0>() &&
x2 > s.min_corner().get<0>();
intersectY = y1 < s.max_corner().get<1>() &&
y2 > s.min_corner().get<1>();
}
// If they can't intersect, skip more expensive rotation calculation
if (!(intersectX && intersectY))
continue;
float scale = fmax(placementScale, b.placementScale);
// TODO? glyph.box or glyph.bbox?
CollisionRange range = rotationRange(glyph, b, scale);
placementRange[0] = fmin(placementRange[0], range[0]);
placementRange[1] = fmax(placementRange[1], range[1]);
}
}
return placementRange;
};
void Collision::insert(const GlyphBoxes &glyphs, const CollisionAnchor &anchor,
float placementScale,
const PlacementRange &placementRange, bool horizontal,
float padding) {
assert(placementScale != std::numeric_limits<float>::infinity());
for (const GlyphBox &glyph : glyphs) {
const CollisionRect &bbox = glyph.bbox;
const CollisionRect &box = glyph.box;
float minScale = fmax(placementScale, glyph.minScale);
Box bounds{Point{anchor.x + bbox.tl.x / minScale,
anchor.y + bbox.tl.y / minScale},
Point{anchor.x + bbox.br.x / minScale,
anchor.y + bbox.br.y / minScale}};
PlacementBox placement;
placement.anchor = anchor;
placement.box = box;
placement.bbox = bbox;
placement.rotate = horizontal;
placement.placementRange = placementRange;
placement.placementScale = minScale;
placement.maxScale = glyph.maxScale;
placement.padding = padding;
assert(!isnan(bounds.min_corner().get<0>()) && !isnan(bounds.min_corner().get<1>()));
assert(!isnan(bounds.max_corner().get<0>()) && !isnan(bounds.max_corner().get<1>()));
((Tree *)tree)->insert({bounds, placement});
}
}

326
src/text/placement.cpp Normal file
View File

@@ -0,0 +1,326 @@
#include <llmr/text/placement.hpp>
#include <llmr/map/vector_tile.hpp>
#include <llmr/geometry/interpolate.hpp>
#include <llmr/style/bucket_description.hpp>
#include <llmr/renderer/text_bucket.hpp>
#include <llmr/util/math.hpp>
#include <llmr/util/constants.hpp>
using namespace llmr;
const int Placement::tileExtent = 4096;
const int Placement::glyphSize =
24; // size in pixels of this glyphs in the tile
const float Placement::minScale = 0.125; // underscale by 3 zoom levels
Placement::Placement(int8_t zoom)
: zoom(zoom),
zOffset(log(256.0 / util::tileSize) / log(2)),
// Calculate the maximum scale we can go down in our fake-3d rtree so that
// placement still makes sense. This is calculated so that the minimum
// placement zoom can be at most 25.5 (we use an unsigned integer x10 to
// store the minimum zoom).
//
// We don't want to place labels all the way to 25.5. This lets too many
// glyphs be placed, slowing down collision checking. Only place labels if
// they will show up within the intended zoom range of the tile.
// TODO make this not hardcoded to 3
maxPlacementScale(exp(log(2) * util::min((25.5 - zoom), 3.0))) {}
bool byScale(const Anchor &a, const Anchor &b) { return a.scale < b.scale; }
static const Glyph null_glyph;
inline const Glyph &getGlyph(const GlyphPlacement &placed,
const IndexedFaces &faces) {
if (placed.face < faces.size()) {
const GlyphPositions &face = *faces[placed.face];
if (&face) {
auto it = face.find(placed.glyph);
if (it != face.end()) {
return it->second;
} else {
fprintf(stderr, "glyph %d does not exist\n", placed.glyph);
}
} else {
fprintf(stderr, "face pointer is null\n");
}
} else {
fprintf(stderr, "face does not exist\n");
}
return null_glyph;
}
struct GlyphInstance {
explicit GlyphInstance(const vec2<float> &anchor) : anchor(anchor) {}
explicit GlyphInstance(const vec2<float> &anchor, float offset,
float minScale, float maxScale, float angle)
: anchor(anchor),
offset(offset),
minScale(minScale),
maxScale(maxScale),
angle(angle) {}
const vec2<float> anchor;
const float offset = 0.0f;
const float minScale = Placement::minScale;
const float maxScale = std::numeric_limits<float>::infinity();
const float angle = 0.0f;
};
typedef std::vector<GlyphInstance> GlyphInstances;
void getSegmentGlyphs(std::back_insert_iterator<GlyphInstances> glyphs,
Anchor &anchor, float offset,
const std::vector<Coordinate> &line, int segment,
int8_t direction, float maxAngleDelta) {
const bool upsideDown = direction < 0;
if (offset < 0)
direction *= -1;
if (direction > 0)
segment++;
vec2<float> newAnchor = anchor;
if (line.size() <= segment) {
return;
}
vec2<float> end = line[segment];
float prevscale = std::numeric_limits<float>::infinity();
float prevAngle = 0.0f;
offset = fabs(offset);
const float placementScale = anchor.scale;
while (true) {
const float dist = util::dist<float>(newAnchor, end);
const float scale = offset / dist;
float angle = -atan2(end.x - newAnchor.x, end.y - newAnchor.y) +
direction * M_PI / 2.0f;
if (upsideDown)
angle += M_PI;
// Don't place around sharp corners
float angleDiff = fmod((angle - prevAngle), (2.0f * M_PI));
if (prevAngle && angleDiff > maxAngleDelta) {
anchor.scale = prevscale;
break;
}
glyphs = GlyphInstance{
/* anchor */ newAnchor,
/* offset */ static_cast<float>(upsideDown ? M_PI : 0.0),
/* minScale */ scale,
/* maxScale */ prevscale,
/* angle */ static_cast<float>(
fmod((angle + 2.0 * M_PI), (2.0 * M_PI)))};
if (scale <= placementScale)
break;
newAnchor = end;
// skip duplicate nodes
while (newAnchor == end) {
segment += direction;
if (line.size() <= segment) {
anchor.scale = scale;
return;
}
end = line[segment];
}
vec2<float> normal = util::normal<float>(newAnchor, end) * dist;
newAnchor = newAnchor - normal;
prevscale = scale;
prevAngle = angle;
}
}
PlacedGlyphs getGlyphs(Anchor &anchor, float advance, const Shaping &shaping,
const IndexedFaces &faces, float fontScale,
bool horizontal, const std::vector<Coordinate> &line,
float maxAngleDelta, float rotate) {
// The total text advance is the width of this label.
// TODO: figure out correct ascender height.
vec2<float> origin{-advance / 2, -17};
// TODO: allow setting an alignment
// var alignment = 'center';
// if (alignment == 'center') {
// origin.x -= advance / 2;
// } else if (alignment == 'right') {
// origin.x -= advance;
// }
PlacedGlyphs glyphs;
const uint32_t buffer = 3;
for (const GlyphPlacement &placed : shaping) {
const Glyph &glyph = getGlyph(placed, faces);
if (!glyph) {
// This glyph is empty and doesn't have any pixels that we'd need to
// show.
continue;
}
uint32_t width = glyph.metrics.width;
uint32_t height = glyph.metrics.height;
width += buffer * 2;
height += buffer * 2;
// Increase to next number divisible by 4, but at least 1.
// This is so we can scale down the texture coordinates and pack them
// into 2 bytes rather than 4 bytes.
width += (4 - width % 4);
height += (4 - height % 4);
float x =
(origin.x + placed.x + glyph.metrics.left - buffer + width / 2) *
fontScale;
GlyphInstances glyphInstances;
if (anchor.segment >= 0) {
getSegmentGlyphs(std::back_inserter(glyphInstances), anchor, x,
line, anchor.segment, 1, maxAngleDelta);
getSegmentGlyphs(std::back_inserter(glyphInstances), anchor, x,
line, anchor.segment, -1, maxAngleDelta);
} else {
glyphInstances.emplace_back(GlyphInstance{anchor});
}
const float x1 = origin.x + placed.x + glyph.metrics.left - buffer;
const float y1 = origin.y + placed.y - glyph.metrics.top - buffer;
const float x2 = x1 + width;
const float y2 = y1 + height;
const vec2<float> otl{x1, y1};
const vec2<float> otr{x2, y1};
const vec2<float> obl{x1, y2};
const vec2<float> obr{x2, y2};
const CollisionRect obox{fontScale *x1, fontScale *y1,
fontScale *x2, fontScale *y2};
for (const GlyphInstance &instance : glyphInstances) {
vec2<float> tl = otl;
vec2<float> tr = otr;
vec2<float> bl = obl;
vec2<float> br = obr;
CollisionRect box = obox;
// Clamp to -90/+90 degrees
const float angle = instance.angle + rotate;
if (angle) {
// Compute the transformation matrix.
float angle_sin = sin(angle);
float angle_cos = cos(angle);
std::array<float, 4> matrix = {
{angle_cos, -angle_sin, angle_sin, angle_cos}};
tl = tl.matMul(matrix);
tr = tr.matMul(matrix);
bl = bl.matMul(matrix);
br = br.matMul(matrix);
// Calculate the rotated glyph's bounding box offsets from the
// anchor point.
box = CollisionRect{
fontScale * util::min(tl.x, tr.x, bl.x, br.x),
fontScale * util::min(tl.y, tr.y, bl.y, br.y),
fontScale * util::max(tl.x, tr.x, bl.x, br.x),
fontScale * util::max(tl.y, tr.y, bl.y, br.y)};
}
// Remember the glyph for later insertion.
glyphs.emplace_back(PlacedGlyph{
tl, tr, bl, br, glyph.rect, width, height,
static_cast<float>(
fmod((anchor.angle + rotate + instance.offset + 2 * M_PI),
(2 * M_PI))),
GlyphBox{box, instance.minScale, instance.maxScale,
instance.anchor, horizontal}});
}
}
// Prevent label from extending past the end of the line
for (PlacedGlyph &g : glyphs) {
g.glyphBox.minScale = util::max(g.glyphBox.minScale, anchor.scale);
}
return glyphs;
}
void Placement::addFeature(TextBucket& bucket,
const std::vector<Coordinate> &line,
const BucketGeometryDescription &info,
const IndexedFaces &faces,
const Shaping &shaping) {
const bool horizontal = info.path == TextPathType::Horizontal;
const float padding = info.padding;
const float maxAngleDelta = info.maxAngleDelta;
const float textMinDistance = info.textMinDistance;
const float rotate = info.rotate;
const float fontScale =
(tileExtent / util::tileSize) / (glyphSize / info.font_size);
const float advance = measureText(faces, shaping);
Anchors anchors;
// fprintf(stderr, "adding feature with advance %f\n", advance);
if (line.size() == 1) {
// Point labels
anchors = Anchors{{Anchor{
static_cast<float>(line[0].x), static_cast<float>(line[0].y),
0, minScale}}};
} else {
// Line labels
anchors = interpolate(line, textMinDistance, minScale);
// Sort anchors by segment so that we can start placement with
// the anchors that can be shown at the lowest zoom levels.
std::sort(anchors.begin(), anchors.end(), byScale);
}
for (Anchor anchor : anchors) {
PlacedGlyphs glyphs =
getGlyphs(anchor, advance, shaping, faces, fontScale, horizontal,
line, maxAngleDelta, rotate);
PlacementProperty place =
collision.place(glyphs, anchor, anchor.scale, maxPlacementScale,
padding, horizontal, info.alwaysVisible);
if (place) {
bucket.addGlyphs(glyphs, place.zoom, place.rotationRange,
zoom - zOffset);
}
}
}
float Placement::measureText(const IndexedFaces &faces,
const Shaping &shaping) {
float advance = 0;
// TODO: advance is not calculated correctly. we should instead use the
// bounding box of the glyph placement.
for (const GlyphPlacement &shape : shaping) {
advance += getGlyph(shape, faces).metrics.advance;
}
return advance;
}

260
src/text/rotation_range.cpp Normal file
View File

@@ -0,0 +1,260 @@
#include <llmr/text/rotation_range.hpp>
#include <cassert>
namespace llmr {
/*
* Combine an array of collision ranges to form a continuous
* range that includes 0. Collisions within the ignoreRange are ignored
*/
CollisionRange mergeCollisions(const CollisionList &collisions,
PlacementRange ignoreRange) {
// find continuous interval including 0 that doesn't have any collisions
float min = 2.0f * M_PI;
float max = 0.0f;
for (CollisionRange collision : collisions) {
bool entryOutside =
ignoreRange[0] <= collision[0] && collision[0] <= ignoreRange[1];
bool exitOutside =
ignoreRange[0] <= collision[1] && collision[1] <= ignoreRange[1];
if (entryOutside && exitOutside) {
// no collision, since blocker is out of range
} else if (entryOutside) {
min = util::min(min, ignoreRange[1]);
max = util::max(max, collision[1]);
} else if (exitOutside) {
min = util::min(min, collision[0]);
max = util::max(max, ignoreRange[0]);
} else {
min = util::min(min, collision[0]);
max = util::max(max, collision[1]);
}
}
return {{min, max}};
}
/*
* Calculate collision ranges for two rotating boxes.
*/
CollisionList
rotatingRotatingCollisions(const CollisionRect &a, const CollisionRect &b,
const CollisionAnchor &anchorToAnchor) {
const float d = util::mag<float>(anchorToAnchor);
const float d_sq = d * d;
const CollisionAnchor horizontal = {1, 0};
const float angleBetweenAnchors =
util::angle_between<float>(anchorToAnchor, horizontal);
// Calculate angles at which collisions may occur
const std::array<float, 8> c = {{
// top/bottom
/*[0]*/ static_cast<float>(asin((float)(a.br.y - b.tl.y) / d)),
/*[1]*/ static_cast<float>(asin((float)(a.br.y - b.tl.y) / d) + M_PI),
/*[2]*/ static_cast<float>(2 * M_PI -
asin((float)(-a.tl.y + b.br.y) / d)),
/*[3]*/ static_cast<float>(M_PI - asin((float)(-a.tl.y + b.br.y) / d)),
// left/right
/*[4]*/ static_cast<float>(2 * M_PI -
acos((float)(a.br.x - b.tl.x) / d)),
/*[5]*/ static_cast<float>(acos((float)(a.br.x - b.tl.x) / d)),
/*[6]*/ static_cast<float>(M_PI - acos((float)(-a.tl.x + b.br.x) / d)),
/*[7]*/ static_cast<float>(M_PI +
acos((float)(-a.tl.x + b.br.x) / d))}};
const float rl = a.br.x - b.tl.x;
const float lr = -a.tl.x + b.br.x;
const float tb = a.br.y - b.tl.y;
const float bt = -a.tl.y + b.br.y;
// Calculate the distance squared of the diagonal which will be used
// to check if the boxes are close enough for collisions to occur at each
// angle
// todo, triple check these
const std::array<float, 8> e = {{
// top/bottom
/*[0]*/ static_cast<float>(rl * rl + tb * tb),
/*[1]*/ static_cast<float>(lr * lr + tb * tb),
/*[2]*/ static_cast<float>(rl * rl + bt * bt),
/*[3]*/ static_cast<float>(lr * lr + bt * bt),
// left/right
/*[4]*/ static_cast<float>(rl * rl + tb * tb),
/*[5]*/ static_cast<float>(rl * rl + bt * bt),
/*[6]*/ static_cast<float>(lr * lr + bt * bt),
/*[7]*/ static_cast<float>(lr * lr + tb * tb)}};
std::vector<float> f;
for (size_t i = 0; i < c.size(); i++) {
// Check if they are close enough to collide
if (!isnan(c[i]) && d_sq <= e[i]) {
// So far, angles have been calulated as relative to the vector
// between anchors.
// Convert the angles to angles from north.
f.push_back(
fmod((c[i] + angleBetweenAnchors + 2 * M_PI), (2 * M_PI)));
}
}
assert(f.size() % 2 == 0);
// Group the collision angles by two
// each group represents a range where the two boxes collide
CollisionList collisions;
std::sort(f.begin(), f.end());
for (size_t k = 0; k < f.size(); k += 2) {
collisions.push_back({{f[k], f[k + 1]}});
}
return collisions;
}
double getAngle(const CollisionPoint &p1, const CollisionPoint &p2,
CollisionAngle d, const CollisionPoint &corner) {
return fmod(util::angle_between(util::interp(p1.x, p2.x, d),
util::interp(p1.y, p2.y, d), corner.x,
corner.y) +
2 * M_PI,
2 * M_PI);
}
/*
* Return the intersection points of a circle and a line segment;
*/
void circleEdgeCollisions(std::back_insert_iterator<CollisionAngles> angles,
const CollisionPoint &corner, float radius,
const CollisionPoint &p1, const CollisionPoint &p2) {
const CollisionPoint::Type edgeX = p2.x - p1.x;
const CollisionPoint::Type edgeY = p2.y - p1.y;
const CollisionAngle a = edgeX * edgeX + edgeY * edgeY;
const CollisionAngle b = (edgeX * p1.x + edgeY * p1.y) * 2;
const CollisionAngle c = p1.x * p1.x + p1.y * p1.y - radius * radius;
const CollisionAngle discriminant = b * b - 4 * a * c;
// a collision exists only if line intersects circle at two points
if (discriminant > 0) {
CollisionAngle x1 = (-b - sqrt(discriminant)) / (2 * a);
CollisionAngle x2 = (-b + sqrt(discriminant)) / (2 * a);
// only add points if within line segment
// hack to handle floating point representations of 0 and 1
if (0 < x1 && x1 < 1) {
angles = getAngle(p1, p2, x1, corner);
}
if (0 < x2 && x2 < 1) {
angles = getAngle(p1, p2, x2, corner);
}
}
}
/*
* Calculate the ranges for which the corner,
* rotatated around the anchor, is within the box;
*/
void cornerBoxCollisions(std::back_insert_iterator<CollisionList> collisions,
const CollisionPoint &corner,
const CollisionCorners &boxCorners, bool flip) {
float radius = util::mag<float>(corner);
CollisionAngles angles;
// Calculate the points at which the corners intersect with the edges
for (size_t i = 0, j = 3; i < 4; j = i++) {
circleEdgeCollisions(std::back_inserter(angles), corner, radius,
boxCorners[j], boxCorners[i]);
}
if (angles.size() % 2 != 0) {
// TODO fix
// This could get hit when a point intersects very close to a corner
// and floating point issues cause only one of the entry or exit to be
// counted
throw std::runtime_error("expecting an even number of intersections");
}
std::sort(angles.begin(), angles.end());
// Group by pairs, where each represents a range where a collision occurs
for (size_t k = 0; k < angles.size(); k += 2) {
CollisionRange range = {{angles[k], angles[k + 1]}};
if (flip) {
range = util::flip(range);
}
collisions = range;
}
}
CollisionCorners getCorners(const CollisionRect &a) {
return {{{a.tl.x, a.tl.y},
{a.tl.x, a.br.y},
{a.br.x, a.br.y},
{a.br.x, a.tl.y}}};
}
/*
* Calculate collision ranges for a rotating box and a fixed box;
*/
CollisionList rotatingFixedCollisions(const CollisionRect &rotating,
const CollisionRect &fixed) {
const auto cornersR = getCorners(rotating);
const auto cornersF = getCorners(fixed);
// A collision occurs when, and only at least one corner from one of the
// boxes is within the other box. Calculate these ranges for each corner.
CollisionList collisions;
for (size_t i = 0; i < 4; i++) {
cornerBoxCollisions(std::back_inserter(collisions), cornersR[i],
cornersF);
cornerBoxCollisions(std::back_inserter(collisions), cornersF[i],
cornersR, true);
}
return collisions;
}
/*
* Calculate the range a box conflicts with a second box
*/
CollisionRange rotationRange(const GlyphBox &inserting,
const PlacementBox &blocker, float scale) {
CollisionList collisions;
const GlyphBox &a = inserting;
const PlacementBox &b = blocker;
// Instead of scaling the boxes, we move the anchors
CollisionAnchor relativeAnchor{
static_cast<float>((b.anchor.x - a.anchor.x) * scale),
static_cast<float>((b.anchor.y - a.anchor.y) * scale)};
// Generate a list of collision interval
if (a.rotate && b.rotate) {
collisions = rotatingRotatingCollisions(a.box, b.box, relativeAnchor);
} else if (a.rotate) {
const CollisionRect box {
b.box.tl.x + relativeAnchor.x, b.box.tl.y + relativeAnchor.y,
b.box.br.x + relativeAnchor.x, b.box.br.y + relativeAnchor.y};
collisions = rotatingFixedCollisions(a.box, box);
} else if (b.rotate) {
const CollisionRect box {
a.box.tl.x - relativeAnchor.x, a.box.tl.y - relativeAnchor.y,
a.box.br.x - relativeAnchor.x, a.box.br.y - relativeAnchor.y};
collisions = rotatingFixedCollisions(b.box, box);
} else {
// collisions remains empty
}
// Find and return the continous are around 0 where there are no collisions
return mergeCollisions(collisions, blocker.placementRange);
}
}

6
test/main.cpp Normal file
View File

@@ -0,0 +1,6 @@
#include "gtest/gtest.h"
GTEST_API_ int main(int argc, char *argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

137
test/rotation_range.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include <iostream>
#include "gtest/gtest.h"
#include <llmr/text/rotation_range.hpp>
using namespace llmr;
double deg(double x) { return x / M_PI * 180.0; }
TEST(RotationRange, mergeCollisions) {
// merge overlapping ranges
EXPECT_EQ(
CollisionRange({{1.0 / 8.0 * M_PI, 6.0 / 8.0 * M_PI}}),
mergeCollisions(
CollisionList(
{{CollisionRange({{3.0 / 8.0 * M_PI, 5.0 / 8.0 * M_PI}}),
CollisionRange({{4.0 / 8.0 * M_PI, 6.0 / 8.0 * M_PI}}),
CollisionRange({{1.0 / 8.0 * M_PI, 2.0 / 8.0 * M_PI}})}}),
PlacementRange({{2.0 * M_PI, 0.0}})));
// ignore collision within ignore range
EXPECT_EQ(
CollisionRange({{5.0 / 4.0 * M_PI, 6.0 / 4.0 * M_PI}}),
mergeCollisions(CollisionList({{CollisionRange({{M_PI / 2, M_PI}}),
CollisionRange({{5.0 / 4.0 * M_PI,
6.0 / 4.0 * M_PI}})}}),
PlacementRange({{0, M_PI}})));
// crop collision that ends within ignore range
EXPECT_EQ(CollisionRange({{1.0 / 2.0 * M_PI, 3.0 / 4.0 * M_PI}}),
mergeCollisions(
CollisionList({{CollisionRange({{1.0 / 2.0 * M_PI, M_PI}})}}),
PlacementRange({{3.0 / 4.0 * M_PI, 3.0 / 2.0 * M_PI}})));
// crop collision that starts within ignore range
EXPECT_EQ(CollisionRange({{3.0 / 4.0 * M_PI, M_PI}}),
mergeCollisions(
CollisionList({{CollisionRange({{1.0 / 2.0 * M_PI, M_PI}})}}),
PlacementRange({{1.0 / 4.0 * M_PI, 3.0 / 4.0 * M_PI}})));
}
TEST(RotationRange, rotatingFixedCollisions) {
// returns collisions
auto collisions = rotatingFixedCollisions(
CollisionRect{CollisionPoint{-1, 0}, CollisionPoint{0, 1}},
CollisionRect{CollisionPoint{1.4142, -10}, CollisionPoint{10, 10}});
EXPECT_EQ(1, collisions.size());
EXPECT_EQ(135, round(deg(collisions.front()[0])));
EXPECT_EQ(135, round(deg(collisions.front()[1])));
}
TEST(RotationRange, cornerBoxCollisions) {
{
// returns intersections in sorted order as angles 0..2PI
CollisionList list;
cornerBoxCollisions(
std::back_inserter(list), CollisionPoint{1, 1},
CollisionCorners{{CollisionPoint{0, 0}, CollisionPoint{0, 10},
CollisionPoint{10, 10}, CollisionPoint{10, 0}}});
EXPECT_EQ(1, list.size());
EXPECT_EQ((CollisionRange{{M_PI / 4.0, M_PI * 7.0 / 4.0}}), list[0]);
}
{
// handles no intersections
CollisionList list;
cornerBoxCollisions(
std::back_inserter(list), CollisionPoint{200, 200},
CollisionCorners{{CollisionPoint{1, 1}, CollisionPoint{1, 10},
CollisionPoint{10, 10}, CollisionPoint{10, 1}}});
EXPECT_EQ(0, list.size());
}
}
TEST(RotationRange, circleEdgeCollisions) {
{
// handles two intersection points
CollisionAngles list;
circleEdgeCollisions(std::back_inserter(list), CollisionPoint{0, 1}, 1,
CollisionPoint{-10, 0}, CollisionPoint{10, 0});
std::sort(list.begin(), list.end());
EXPECT_EQ(2, list.size());
EXPECT_EQ(static_cast<float>(M_PI / 2), list[0]);
EXPECT_EQ(static_cast<float>(M_PI * 3.0 / 2.0), list[1]);
}
{
// handles one intersection point
CollisionAngles list;
circleEdgeCollisions(std::back_inserter(list), CollisionPoint{0, 1}, 1,
CollisionPoint{0, 0}, CollisionPoint{10, 0});
EXPECT_EQ(1, list.size());
EXPECT_EQ(static_cast<float>(M_PI / 2), list[0]);
}
{
// only returns intersections within the line segment
CollisionAngles list;
circleEdgeCollisions(std::back_inserter(list), CollisionPoint{0, 1}, 1,
CollisionPoint{3, 1}, CollisionPoint{30, 1});
EXPECT_EQ(0, list.size());
}
{
// doesnt count tangetial intersections as collisions
CollisionAngles list;
circleEdgeCollisions(std::back_inserter(list), CollisionPoint{0, 1}, 1,
CollisionPoint{-10, 1}, CollisionPoint{10, 1});
EXPECT_EQ(0, list.size());
}
}
TEST(RotationRange, rotatingRotatingCollisions) {
{
// basically works
CollisionList c = rotatingRotatingCollisions(
CollisionRect{{-1, 0}, {1, 0}}, CollisionRect{{-1, 0}, {1, 0}},
CollisionAnchor{1, 1});
EXPECT_EQ(2, c.size());
EXPECT_EQ(135, round(deg(c[0][0])));
EXPECT_EQ(135, round(deg(c[0][1])));
EXPECT_EQ(315, round(deg(c[1][0])));
EXPECT_EQ(315, round(deg(c[1][1])));
}
{
// checks if the two boxes are close enough to collide at that angle
CollisionList c = rotatingRotatingCollisions(
CollisionRect{{-1, 0}, {1, 0}}, CollisionRect{{-1, 0}, {1, 0}},
CollisionAnchor{2, 2});
EXPECT_EQ(0, c.size());
}
}

21
test/test.gyp Normal file
View File

@@ -0,0 +1,21 @@
{
'includes': [
'../common.gypi',
'../config.gypi'
],
'targets': [
{
"target_name": "rotation_range",
"product_name": "test_rotation_range",
"type": "executable",
"sources": [
"./main.cpp",
"./rotation_range.cpp",
],
"dependencies": [
"../deps/gtest/gtest.gyp:gtest",
"../llmr.gyp:llmr-x86"
]
},
]
}