To access the contents of vector tiles with vtzero create a vector_tile
object first with the data of the tile as first argument:
#include <vtzero/vector_tile.hpp> // always needed when reading vector tiles
std::string vt_data = ...;
vtzero::vector_tile tile{vt_data};
Instead of a string, you can also initialize the vector_tile
using a
vtzero::data_view
. This class contains only a pointer and size referencing
some data similar to the C++17 std::string_view
class. It is typedef'd from
the protozero::data_view
. See the protozero
doc
for more details.
vtzero::data_view vt_data = ...;
vtzero::vector_tile tile{vt_data};
In both cases the vector_tile
object contains references to the original
tile data. You have to make sure this data stays available through the whole
lifetime of the vector_tile
object and all the other objects we'll create
in this tutorial for accessing parts of the vector tile. The data is not
copied by vtzero when accessing vector tiles.
You can think of the vector_tile
class as a "proxy" class giving you access
to the decoded data, similarly the classes layer
, feature
, and
property
described in the next chapters are "proxy" classes, too.
Vector tiles consist of a list of layers. The list of layers can be empty
in which case tile.empty()
will return true.
The simplest and fasted way to access the layers is through the next_layer()
function:
while (auto layer = tile.next_layer()) {
...
}
Note that this creates new layer objects on the fly referencing the layer you
are currently looking at. Once you have iterated over all the layers,
next_layer()
will return the "invalid" (default constructed) layer object
which converts to false in an boolean context.
You can reset the layer iterator to the beginning again if you need to go over the layers again:
tile.reset_layer();
Instead of using this external iterator, you can use a different function with
an internal iterator that calls a function defined by you for each layer. Your
function must take a layer&&
as parameter and return true
if the iteration
should continue and false
otherwise:
tile.for_each_layer([&](layer&& l) {
// do something with layer
return true;
});
Both the external and internal iteration do basically the same and have the same performance characteristics.
You can also access layers through their index or name:
tile.get_layer(3);
will give you the 4th layer in the tile. With
tile.get_layer_by_name("foobar");
you'll get the layer with the specified name. Both will return the invalid layer if that layer doesn't exist.
Note that accessing layers by index or name is less efficient than iterating
over them using next_layer()
if you are accessing several layers. So usually
you should only use those function if you want to access one specific layer
only.
If you need the number of layers, you can call tile.count_layers()
. This
function still has to iterate over the layers internally decoding some of the
data, so it is not cheap.
Once you have a layer as described in the previous chapter you can access the metadata of this layer easily:
- The version is available with
layer.version()
. Only version 1 and 2 are currently supported by this library. - The extent of the tile is available through
layer.extent()
. This is usually 4096. - The function
layer.name()
returns the name of the layer asdata_view
. This does not include a final 0-byte! - The number of features is returned by the
layer.num_features()
function. If it doesn't contain any featureslayer.empty()
will return true. (Different then thevector_tile::count_layers()
, thelayer::num_features()
function isO(1)
).
To access the features call the next_feature()
function until it returns
the invalid (default constructed) feature:
while (auto feature = layer.next_feature()) {
...
}
Use reset_feature()
to restart the feature iterator from the beginning.
Instead of using this external iterator, you can use a different function with
an internal iterator that calls a function defined by you for each feature.
Your function must take a feature&&
as parameter and return true
if the
iteration should continue and false
otherwise:
layer.for_each_feature([&](feature&& f) {
// do something with the feature
return true;
});
Both the external and internal iteration do basically the same and have the same performance characteristics.
If you know the ID of a feature you can get the feature using
get_feature_by_id()
, but note that this will do a linear search through
all the features in the layer, decoding each one until it finds the right ID.
This is almost always not what you want.
Note that the feature returned by next_feature()
or get_feature_by_id()
will internally contain a pointer to the layer it came from. The layer has to
stay valid as long as the feature is used.
You get features from the layer as described in the previous chapter. The
feature
class gives you access to the ID, the geometry and the properties
of the feature. Access the ID using the id()
method which will return 0
if no ID is set. You can ask for the existence of the ID using has_id()
:
auto feature = layer...;
if (feature.has_id()) {
cout << feature.id() << '\n';
}
The geometry()
method returns an object of the geometry
class. It contains
the geometry type and a reference to the (un-decoded) geometry data. See a
later chapter on the details of decoding this geometry. You can also directly
add this geometry to a new feature you are writing.
The number of properties in the feature is returned by the
feature::num_properties()
function. If the feature doesn't contain any
properties feature.empty()
will return true. (Different then the
vector_tile::count_layers()
, the feature::num_properties()
function is
O(1)
).
To access the properties call the next_property()
function until it returns
the invalid (default constructed) property:
while (auto property = feature.next_property()) {
...
}
Use reset_property()
to restart the property iterator from the beginning.
Instead of using this external iterator, you can use a different function with
an internal iterator that calls a function defined by you for each property.
Your function must take a property&&
as parameter and return true
if the
iteration should continue and false
otherwise:
feature.for_each_property([&](property&& p) {
...
return true;
});
Both the external and internal iteration do basically the same and have the same performance characteristics.
Each property you get from the feature is an object of the property
class. It
contains a view of the property key and value. The key is always a string
encoded in a vtzero::data_view
, the value can be of different types but is
always encapsulated in a property_value
type, a variant type that can be
converted into whatever type the value really has.
auto property = ...;
auto pkey = std::string(property.key()); // returns a vtzero::data_view which can
// be converted to std::string
property_value pvalue = property.value();
To get the type of the property value, call type()
:
const auto type = pvalue.type();
If the property value is an int, for instance, you can get it like this:
if (pvalue.type() == property_value_type::int_value)
int64_t v = pvalue.int_value();
}
Instead of accessing the values this way, you'll often use the visitor
interface. Here is an example where the print_visitor
struct is used to print
out the values. In this case one overload is used for all primitive types
(double
, float
, int
, uint
, bool
), one overload is used for the string_value
type which is encoded in a data_view
. You must make sure your visitor handles
all those types.
struct print_visitor {
template <typename T>
void operator()(T value) {
std::cout << value;
}
void operator()(vtzero::data_view value) {
std::cout << std::string(value);
}
};
vtzero::apply_visitor(print_visitor{}, pvalue));
All call operators of your visitor class have to return the same type. In the
case above this was void
, but it can be something else. That return type
will be the return type of the apply_visitor
function. This can be used,
for instance, to convert the values into one type:
struct to_string_visitor {
template <typename T>
std::string operator()(T value) {
return std::to_string(value);
}
std::string operator()(vtzero::data_view value) {
return std::string(value);
}
};
std::string v = vtzero::apply_visitor(to_string_visitor{}, pvalue);
Sometimes you want to convert the property_value
type into your own variant
type. You can use the vtzero::convert_property_value()
free function for
this.
Lets say you are using boost
and this is your variant:
using variant_type = boost::variant<std::string, float, double, int64_t, uint64_t, bool>;
You can then use the following line to convert the data:
variant_type v = vtzero::convert_property_value<variant_type>(pvalue);
Your variant type must be constructible from all the types std::string
,
float
, double
, int64_t
, uint64_t
, and bool
. If it is not, you can
define a mapping between those types and the types you use in your variant
class.
using variant_type = boost::variant<mystring, double, int64_t, uint64_t, bool>;
struct mapping : vtzero::property_value_mapping {
using string_type = mystring; // use your own string type which must be
// convertible from data_view
using float_type = double; // no float in variant, so convert to double
};
variant_type v = vtzero::convert_property_value<variant_type, mapping>(pvalue);
This linear access to the properties with lazy decoding of each property only
when it is accessed saves memory allocations, especially if you are only
interested in very few properties. But sometimes it is easier to create a
mapping (based on std::unordered_map
for instance) between keys and values. This is where
the vtzero::create_properties_map()
templated free function comes in. It
needs the map type as template parameter:
using key_type = std::string; // must be something that can be converted from data_view
using value_type = boost::variant<std::string, float, double, int64_t, uint64_t, bool>;
using map_type = std::map<key_type, value_type>;
auto feature = ...;
auto map = create_properties_map<map_type>(feature);
Both std::map
and std::unordered_map
are supported as map type, but this
should also work with any other map type that has an emplace()
method.
Features must contain a geometry of type UNKNOWN, POINT, LINESTRING, or POLYGON. The UNKNOWN type is not further specified by the vector tile spec, this library doesn't allow you to do anything with this type. Note that multipoint, multilinestring, and multipolygon geometries are also possible, they don't have special types.
You can get the geometry type with feature.geometry_type()
, but usually
you'll get the geometry with feature.geometry()
. This will return an object
of type vtzero::geometry
which contains the geometry type and a view of
the raw geometry data. To decode the data you have to call one of the decoder
free functions decode_geometry()
, decode_point_geometry()
,
decode_linestring_geometry()
, or decode_polygon_geometry()
. The first of
these functions can decode any point, linestring, or polygon geometry. The
others must be called with a geometry of the specified type and will only
decode that type.
For all the decoder functions the first parameter is the geometry (as returned
by feature.geometry()
), the second parameter is a handler object that you
must implement. The decoder function will call certain callbacks on this object
that give you part of the geometry data which allows you to use this data in
any way you like.
The handler for decode_point_geometry()
must implement the following
functions:
void points_begin(uint32_t count)
: This is called once at the beginning with the number of points. For a point geometry, this will be 1, for multipoint geometries this will be larger.void points_point(vtzero::point point)
: This is called once for each point.void points_end()
: This is called once at the end.
The handler for decode_linestring_geometry()
must implement the following
functions:
void linestring_begin(uint32_t count)
: This is called at the beginning of each linestring with the number of points in this linestring. For a simple linestring this function will only be called once, for a multilinestring it will be called several times.void linestring_point(vtzero::point point)
: This is called once for each point.void linestring_end()
: This is called at the end of each linestring.
The handler for decode_polygon_geometry
must implement the following
functions:
void ring_begin(uint32_t count)
: This is called at the beginning of each ring with the number of points in this ring. For a simple polygon with only one outer ring, this function will only be called once, if there are inner rings or if this is a multipolygon, it will be called several times.void ring_point(vtzero::point point)
: This is called once for each point.void ring_end(vtzero::ring_type)
: This is called at the end of each ring. The parameter tells you whether the ring is an outer or inner ring or whether the ring was invalid (if the area is 0).
The handler for decode_geometry()
must implement all of the functions
mentioned above for the different types. It is guaranteed that only one
set of functions will be called depending on the geometry type.
If your handler implements the result()
method, the decode functions will
have the return type of the result()
method and will return whatever
result returns. If the result()
method is not available, the decode functions
return void.
Here is a typical implementation of a linestring handler:
struct linestring_handler {
using linestring = std::vector<my_point_type>;
linestring points;
void linestring_begin(uint32_t count) {
points.reserve(count);
}
void linestring_point(vtzero::point point) noexcept {
points.push_back(convert_to_my_point(point));
}
void linestring_end() const noexcept {
}
linestring result() {
return std::move(points);
}
};
Note that the count
given to the linestring_begin()
method is used here to
reserve memory. This is potentially problematic if the count is large. Please
keep this in mind.
Vector tile layers contain two tables with all the property keys and all property values used in the features in that layer. Vtzero usually handles those table lookups internally without you noticing. But sometimes it might be necessary to access this data directly.
From the layer object you can get references to the tables:
vtzero::layer layer = ...;
const auto& kt = layer.key_table();
const auto& vt = layer.value_table();
Instead you can also lookup keys and values using methods on the layer object:
vtzero::layer layer = ...;
const vtzero::data_view k = layer.key(17);
const vtzero::property_value_view v = layer.value(42);
As usual in vtzero you only get views back, so you need to keep the layer object around as long as you are accessing the results of those methods.
Note that the lookup tables are created on first access from the layer data. As long as you are not accessing those tables directly or by looking up any properties in a feature, the tables are not created and no extra memory is used.