This is an automated email from the git hooks/post-receive script. sebastic pushed a commit to branch master in repository mapnik-vector-tile.
commit 49f807b4efdb6975697456d92e0a30be2ee7e1bf Author: Bas Couwenberg <sebas...@xs4all.nl> Date: Fri Sep 11 22:54:58 2015 +0200 Imported Upstream version 0.8.1+dfsg --- CHANGELOG.md | 10 + Makefile | 14 +- README.md | 2 +- bootstrap.sh | 2 +- examples/c++/tileinfo.cpp | 3 +- gyp/build.gyp | 36 +- package.json | 2 +- src/vector_tile_backend_pbf.ipp | 2 + src/vector_tile_datasource.ipp | 3 +- src/vector_tile_datasource_pbf.cpp | 2 + src/vector_tile_datasource_pbf.hpp | 59 +++ src/vector_tile_datasource_pbf.ipp | 413 +++++++++++++++++ src/vector_tile_geometry_decoder.hpp | 105 ++++- src/vector_tile_geometry_encoder.hpp | 12 +- src/vector_tile_processor.ipp | 63 +-- src/vector_tile_strategy.hpp | 88 ++++ test/clipper_test.cpp | 143 ++++++ test/data/natural_earth.tif | Bin 3299605 -> 0 bytes test/data/tile_with_extra_feature_field.pbf | 2 + test/data/tile_with_extra_field.pbf | 2 + test/data/tile_with_extra_layer_fields.pbf | 2 + test/data/tile_with_invalid_layer_value_type.pbf | 2 + test/data/tile_with_unexpected_geomtype.pbf | 3 + test/encoding_util.hpp | 3 +- test/geometry_encoding.cpp | 22 +- test/test_main.cpp | 9 +- test/test_utils.cpp | 16 +- test/vector_tile.cpp | 84 ++-- test/vector_tile_pbf.cpp | 557 +++++++++++++++++++++++ 29 files changed, 1543 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c1c12..da1f497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.8.1 + + - Added `tile_datasource_pbf` - It should be used in places where you need to plug + in a `mapnik::datasource` to read from a binary encoded .pbf buffer. (@danpat #114) + - Updated bundled clipper to https://github.com/mapnik/clipper/commit/bfad32ec4b41783497d076c2ec44c7cbf4ebe56b + - Clipper is now patched to avoid abort on out of range coordinates (#111) + - Fixed handling of geometry collections (#106) + - Added mapnik vector tile strategy for transform + - Updated test cases + ## 0.8.0 - Now using `boost::geometry` to clip lines and `ClipperLib` to clip polygons diff --git a/Makefile b/Makefile index 71b405d..a518b5a 100755 --- a/Makefile +++ b/Makefile @@ -1,12 +1,22 @@ MAPNIK_PLUGINDIR := $(shell mapnik-config --input-plugins) BUILDTYPE ?= Release +CLIPPER_REVISION=bfad32e +PBF_REVISION=1df6453 +GYP_REVISION=3464008 + all: libvtile ./deps/gyp: - git clone https://chromium.googlesource.com/external/gyp.git ./deps/gyp && cd ./deps/gyp && git checkout 3464008 + git clone https://chromium.googlesource.com/external/gyp.git ./deps/gyp && cd ./deps/gyp && git checkout $(GYP_REVISION) + +./deps/pbf: + git clone https://github.com/mapbox/pbf.hpp.git ./deps/pbf && cd ./deps/pbf && git checkout $(PBF_REVISION) + +./deps/clipper: + git clone https://github.com/mapnik/clipper.git -b r493-mapnik ./deps/clipper && cd ./deps/clipper && git checkout $(CLIPPER_REVISION) && ./cpp/fix_members.sh -build/Makefile: ./deps/gyp gyp/build.gyp test/*cpp +build/Makefile: ./deps/gyp ./deps/clipper ./deps/pbf gyp/build.gyp test/*cpp deps/gyp/gyp gyp/build.gyp --depth=. -DMAPNIK_PLUGINDIR=\"$(MAPNIK_PLUGINDIR)\" -Goutput_dir=. --generator-output=./build -f make libvtile: build/Makefile Makefile diff --git a/README.md b/README.md index f47e297..5a1c2b2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Provides C++ headers that support rendering geodata into vector tiles and render ## Depends - - mapnik-vector-tile 0.7.x depends on Mapnik v3.0.x (until 3.0.0 is released this means latest mapnik HEAD) + - mapnik-vector-tile >=0.7.x depends on Mapnik v3.0.x (until 3.0.0 is released this means latest mapnik HEAD) - mapnik-vector-tile 0.6.x and previous work with Mapnik v2.2.x or v2.3.x - You will need `libmapnik` and `mapnik-config` available - Protobuf: `libprotobuf` and `protoc` diff --git a/bootstrap.sh b/bootstrap.sh index d98f431..6e47b2f 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -22,7 +22,7 @@ function install() { } function install_mason_deps() { - install mapnik latest + install mapnik 3.0.0-rc3 install protobuf 2.6.1 install freetype 2.5.5 install harfbuzz 0.9.40 diff --git a/examples/c++/tileinfo.cpp b/examples/c++/tileinfo.cpp index 397ffbb..7a9ddca 100644 --- a/examples/c++/tileinfo.cpp +++ b/examples/c++/tileinfo.cpp @@ -104,7 +104,6 @@ int main(int argc, char** argv) { vector_tile::Tile_Feature const & f = layer.features(j); total_repeated += f.geometry_size(); - eGeomType g_type = static_cast<eGeomType>(f.type()); int cmd = -1; const int cmd_bits = 3; unsigned length = 0; @@ -117,7 +116,6 @@ int main(int argc, char** argv) length = cmd_length >> cmd_bits; if (length <= 0) num_empty++; num_commands++; - g_length = 0; } if (length > 0) { length--; @@ -138,6 +136,7 @@ int main(int argc, char** argv) else if (cmd == (SEG_CLOSE & ((1 << cmd_bits) - 1))) { if (g_length <= 2) degenerate++; + g_length = 0; num_close++; } else diff --git a/gyp/build.gyp b/gyp/build.gyp index 3c145d3..42a4b71 100644 --- a/gyp/build.gyp +++ b/gyp/build.gyp @@ -3,7 +3,14 @@ "common.gypi" ], 'variables': { - 'MAPNIK_PLUGINDIR%': '' + 'MAPNIK_PLUGINDIR%': '', + 'common_defines' : [ + 'MAPNIK_VECTOR_TILE_LIBRARY=1', + 'CLIPPER_INTPOINT_IMPL=mapnik::geometry::point<cInt>', + 'CLIPPER_PATH_IMPL=mapnik::geometry::line_string<cInt>', + 'CLIPPER_PATHS_IMPL=mapnik::geometry::multi_line_string<cInt>', + 'CLIPPER_IMPL_INCLUDE=<mapnik/geometry.hpp>' + ] }, "targets": [ { @@ -34,7 +41,8 @@ "<(SHARED_INTERMEDIATE_DIR)/vector_tile.pb.cc" ], 'include_dirs': [ - '<(SHARED_INTERMEDIATE_DIR)/' + '<(SHARED_INTERMEDIATE_DIR)/', + '../deps/pbf' ], 'cflags_cc' : [ '-D_THREAD_SAFE', @@ -48,7 +56,8 @@ }, 'direct_dependent_settings': { 'include_dirs': [ - '<(SHARED_INTERMEDIATE_DIR)/' + '<(SHARED_INTERMEDIATE_DIR)/', + '../deps/pbf' ], 'libraries':[ '-lprotobuf-lite' @@ -69,14 +78,19 @@ 'hard_dependency': 1, "type": "static_library", "sources": [ - "<!@(find ../src/ -name '*.cpp')" + "<!@(find ../src/ -name '*.cpp')", + "../deps/clipper/cpp/clipper.cpp" ], 'defines' : [ - 'MAPNIK_VECTOR_TILE_LIBRARY=1' + "<@(common_defines)" ], 'cflags_cc' : [ '<!@(mapnik-config --cflags)' ], + 'include_dirs': [ + '../deps/pbf', + '../deps/clipper/cpp' + ], 'xcode_settings': { 'OTHER_CPLUSPLUSFLAGS':[ '<!@(mapnik-config --cflags)' @@ -84,10 +98,12 @@ }, 'direct_dependent_settings': { 'include_dirs': [ - '<(SHARED_INTERMEDIATE_DIR)/' + '<(SHARED_INTERMEDIATE_DIR)/', + '../deps/pbf', + '../deps/clipper/cpp' ], 'defines' : [ - 'MAPNIK_VECTOR_TILE_LIBRARY=1' + "<@(common_defines)" ], 'cflags_cc' : [ '<!@(mapnik-config --cflags)' @@ -113,13 +129,15 @@ 'dependencies': [ 'mapnik_vector_tile_impl' ], "type": "executable", "defines": [ + "<@(common_defines)", "MAPNIK_PLUGINDIR=<(MAPNIK_PLUGINDIR)" ], "sources": [ "<!@(find ../test/ -name '*.cpp')" ], "include_dirs": [ - "../src" + "../src", + '../deps/pbf' ] }, { @@ -150,4 +168,4 @@ } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index d7c65ca..515480b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapnik-vector-tile", - "version": "0.8.0", + "version": "0.8.1", "description": "Mapnik vector tile API", "main": "./package.json", "repository" : { diff --git a/src/vector_tile_backend_pbf.ipp b/src/vector_tile_backend_pbf.ipp index dbc07a0..8b1792d 100644 --- a/src/vector_tile_backend_pbf.ipp +++ b/src/vector_tile_backend_pbf.ipp @@ -59,6 +59,8 @@ backend_pbf::backend_pbf(vector_tile::Tile & _tile, : tile_(_tile), path_multiplier_(path_multiplier), current_layer_(NULL), + x_(0), + y_(0), current_feature_(NULL) { } diff --git a/src/vector_tile_datasource.ipp b/src/vector_tile_datasource.ipp index 1176b71..3ee0dc4 100644 --- a/src/vector_tile_datasource.ipp +++ b/src/vector_tile_datasource.ipp @@ -190,7 +190,8 @@ namespace mapnik { namespace vector_tile_impl { { continue; } - mapnik::geometry::geometry<double> geom = decode_geometry(f,tile_x_,tile_y_,scale_,-1*scale_); + mapnik::vector_tile_impl::Geometry geoms(f,tile_x_, tile_y_, scale_, -1*scale_); + mapnik::geometry::geometry<double> geom = decode_geometry(geoms, f.type()); if (geom.is<mapnik::geometry::geometry_empty>()) { continue; diff --git a/src/vector_tile_datasource_pbf.cpp b/src/vector_tile_datasource_pbf.cpp new file mode 100644 index 0000000..9a13992 --- /dev/null +++ b/src/vector_tile_datasource_pbf.cpp @@ -0,0 +1,2 @@ +#include "vector_tile_datasource_pbf.hpp" +#include "vector_tile_datasource_pbf.ipp" diff --git a/src/vector_tile_datasource_pbf.hpp b/src/vector_tile_datasource_pbf.hpp new file mode 100644 index 0000000..5e778df --- /dev/null +++ b/src/vector_tile_datasource_pbf.hpp @@ -0,0 +1,59 @@ +#ifndef __MAPNIK_VECTOR_TILE_DATASOURCE_PBF_H__ +#define __MAPNIK_VECTOR_TILE_DATASOURCE_PBF_H__ + +#include <mapnik/datasource.hpp> +#include <mapnik/box2d.hpp> +#include "pbf_reader.hpp" + +namespace mapnik { namespace vector_tile_impl { + + // TODO: consider using mapnik::value here instead + using pbf_attr_value_type = mapnik::util::variant<std::string, float, double, int64_t, uint64_t, bool>; + using layer_pbf_attr_type = std::vector<pbf_attr_value_type>; + + class tile_datasource_pbf : public datasource + { + public: + tile_datasource_pbf(mapbox::util::pbf const& layer, + unsigned x, + unsigned y, + unsigned z, + unsigned tile_size); + virtual ~tile_datasource_pbf(); + datasource::datasource_t type() const; + featureset_ptr features(query const& q) const; + featureset_ptr features_at_point(coord2d const& pt, double tol = 0) const; + void set_envelope(box2d<double> const& bbox); + box2d<double> get_tile_extent() const; + box2d<double> envelope() const; + boost::optional<datasource_geometry_t> get_geometry_type() const; + layer_descriptor get_descriptor() const; + std::string const& get_name() { return name_; } + private: + mutable mapnik::layer_descriptor desc_; + mutable bool attributes_added_; + mapbox::util::pbf layer_; + unsigned x_; + unsigned y_; + unsigned z_; + unsigned tile_size_; + mutable bool extent_initialized_; + mutable mapnik::box2d<double> extent_; + double tile_x_; + double tile_y_; + double scale_; + uint32_t layer_extent_; + + std::string name_; + std::vector<mapbox::util::pbf> features_; + std::vector<std::string> layer_keys_; + layer_pbf_attr_type layer_values_; + }; + +}} // end ns + +#if !defined(MAPNIK_VECTOR_TILE_LIBRARY) +#include "vector_tile_datasource_pbf.ipp" +#endif + +#endif // __MAPNIK_VECTOR_TILE_DATASOURCE_PBF_H__ diff --git a/src/vector_tile_datasource_pbf.ipp b/src/vector_tile_datasource_pbf.ipp new file mode 100644 index 0000000..8976b4b --- /dev/null +++ b/src/vector_tile_datasource_pbf.ipp @@ -0,0 +1,413 @@ +#include "vector_tile_projection.hpp" +#include "vector_tile_geometry_decoder.hpp" + +#include <mapnik/box2d.hpp> +#include <mapnik/coord.hpp> +#include <mapnik/feature_layer_desc.hpp> +#include <mapnik/geometry.hpp> +#include <mapnik/params.hpp> +#include <mapnik/query.hpp> +#include <mapnik/unicode.hpp> +#include <mapnik/version.hpp> +#include <mapnik/value_types.hpp> +#include <mapnik/well_known_srs.hpp> +#include <mapnik/version.hpp> +#include <mapnik/vertex.hpp> +#include <mapnik/datasource.hpp> +#include <mapnik/feature.hpp> +#include <mapnik/feature_factory.hpp> +#include <mapnik/geom_util.hpp> +#include <mapnik/image.hpp> +#include <mapnik/image_reader.hpp> +#include <mapnik/raster.hpp> +#include <mapnik/view_transform.hpp> +#include <mapnik/util/variant.hpp> + +#include <memory> +#include <stdexcept> +#include <string> + +#include <boost/optional.hpp> +#include <unicode/unistr.h> + +namespace mapnik { namespace vector_tile_impl { + + template <typename Filter> + class tile_featureset_pbf : public Featureset + { + public: + tile_featureset_pbf(Filter const& filter, + mapnik::box2d<double> const& tile_extent, + mapnik::box2d<double> const& unbuffered_query, + std::set<std::string> const& attribute_names, + std::vector<mapbox::util::pbf> const& features, + double tile_x, + double tile_y, + double scale, + std::vector<std::string> const& layer_keys, + layer_pbf_attr_type const& layer_values) + : filter_(filter), + tile_extent_(tile_extent), + unbuffered_query_(unbuffered_query), + features_(features), + layer_keys_(layer_keys), + layer_values_(layer_values), + tile_x_(tile_x), + tile_y_(tile_y), + scale_(scale), + itr_(0), + tr_("utf-8"), + ctx_(std::make_shared<mapnik::context_type>()) + { + std::set<std::string>::const_iterator pos = attribute_names.begin(); + std::set<std::string>::const_iterator end = attribute_names.end(); + for ( ;pos !=end; ++pos) + { + for (auto const& key : layer_keys_) + { + if (key == *pos) + { + ctx_->push(*pos); + break; + } + } + } + } + + virtual ~tile_featureset_pbf() {} + + feature_ptr next() + { + while ( itr_ < features_.size() ) + { + mapbox::util::pbf f = features_.at(itr_); + // TODO: auto-increment feature id counter here + mapnik::feature_ptr feature = mapnik::feature_factory::create(ctx_,itr_); + pbf_attr_value_type val; + + ++itr_; + int tagcount=0; + uint32_t key_idx, val_idx; + int32_t geometry_type = 0; // vector_tile::Tile_GeomType_UNKNOWN + while (f.next()) + { + switch(f.tag()) + { + case 1: + feature->set_id(f.get_uint64()); + break; + case 2: + { + auto tag_iterator = f.packed_uint32(); + + for (auto _i = tag_iterator.first; _i != tag_iterator.second; ++_i) + { + if (tagcount % 2 == 0) + { + key_idx = *_i; + ++tagcount; + } + else + { + val_idx = *_i; + val = layer_values_.at(val_idx); + std::string name = layer_keys_.at(key_idx); + if (feature->has_key(name)) + { + if (val.is<std::string>()) + { + feature->put(name, tr_.transcode(val.get<std::string>().data(), val.get<std::string>().length())); + } + else if (val.is<bool>()) + { + feature->put(name, static_cast<mapnik::value_bool>(val.get<bool>())); + } + else if (val.is<int64_t>()) + { + feature->put(name, static_cast<mapnik::value_integer>(val.get<int64_t>())); + } + else if (val.is<uint64_t>()) + { + feature->put(name, static_cast<mapnik::value_integer>(val.get<uint64_t>())); + } + else if (val.is<double>()) + { + feature->put(name, static_cast<mapnik::value_double>(val.get<double>())); + } + else if (val.is<float>()) + { + feature->put(name, static_cast<mapnik::value_double>(val.get<float>())); + } else { + throw std::runtime_error("unknown attribute type while reading feature"); + } + ++tagcount; + } + } + } + } + break; + case 3: + geometry_type = f.get_enum(); + switch (geometry_type) + { + case 1: //vector_tile::Tile_GeomType_POINT + case 2: // vector_tile::Tile_GeomType_LINESTRING + case 3: // vector_tile::Tile_GeomType_POLYGON + break; + default: // vector_tile::Tile_GeomType_UNKNOWN or any other value + throw std::runtime_error("unknown geometry type " + std::to_string(geometry_type) + " in feature"); + } + break; + case 5: + { + std::string const& image_buffer = f.get_bytes(); + std::unique_ptr<mapnik::image_reader> reader(mapnik::get_image_reader(image_buffer.data(),image_buffer.size())); + if (reader.get()) + { + int image_width = reader->width(); + int image_height = reader->height(); + if (image_width > 0 && image_height > 0) + { + mapnik::view_transform t(image_width, image_height, tile_extent_, 0, 0); + box2d<double> intersect = tile_extent_.intersect(unbuffered_query_); + box2d<double> ext = t.forward(intersect); + if (ext.width() > 0.5 && ext.height() > 0.5 ) + { + // select minimum raster containing whole ext + int x_off = static_cast<int>(std::floor(ext.minx() +.5)); + int y_off = static_cast<int>(std::floor(ext.miny() +.5)); + int end_x = static_cast<int>(std::floor(ext.maxx() +.5)); + int end_y = static_cast<int>(std::floor(ext.maxy() +.5)); + + // clip to available data + if (x_off < 0) + x_off = 0; + if (y_off < 0) + y_off = 0; + if (end_x > image_width) + end_x = image_width; + if (end_y > image_height) + end_y = image_height; + int width = end_x - x_off; + int height = end_y - y_off; + box2d<double> feature_raster_extent(x_off, + y_off, + x_off + width, + y_off + height); + intersect = t.backward(feature_raster_extent); + double filter_factor = 1.0; + mapnik::image_any data = reader->read(x_off, y_off, width, height); + mapnik::raster_ptr raster = std::make_shared<mapnik::raster>(intersect, + data, + filter_factor + ); + feature->set_raster(raster); + return feature; + } + } + } + } + break; + case 4: + { + auto geom_itr = f.packed_uint32(); + mapnik::vector_tile_impl::GeometryPBF geoms(geom_itr, tile_x_,tile_y_,scale_,-1*scale_); + mapnik::geometry::geometry<double> geom = decode_geometry(geoms, geometry_type); + if (geom.is<mapnik::geometry::geometry_empty>()) + { + continue; + } + mapnik::box2d<double> envelope = mapnik::geometry::envelope(geom); + if (!filter_.pass(envelope)) + { + continue; + } + feature->set_geometry(std::move(geom)); + return feature; + } + break; + default: + // NOTE: The vector_tile.proto file technically allows for extension fields + // of values 16 to max here. Technically, we should just skip() those + // fields. + // However, if we're fed a corrupt file (or random data), we don't + // want to just blindly follow the bytes, so we have made the decision + // to abort cleanly, rather than doing GIGO. + throw std::runtime_error("unknown field type " + std::to_string(f.tag()) +" in feature"); + + } + } + } + return feature_ptr(); + } + + private: + Filter filter_; + mapnik::box2d<double> tile_extent_; + mapnik::box2d<double> unbuffered_query_; + std::vector<mapbox::util::pbf> const& features_; + std::vector<std::string> const& layer_keys_; + layer_pbf_attr_type const& layer_values_; + + double tile_x_; + double tile_y_; + double scale_; + unsigned itr_; + mapnik::transcoder tr_; + mapnik::context_ptr ctx_; + + }; + + // tile_datasource impl + tile_datasource_pbf::tile_datasource_pbf(mapbox::util::pbf const& layer, + unsigned x, + unsigned y, + unsigned z, + unsigned tile_size) + : datasource(parameters()), + desc_("in-memory PBF encoded datasource","utf-8"), + attributes_added_(false), + layer_(layer), + x_(x), + y_(y), + z_(z), + tile_size_(tile_size), + extent_initialized_(false), + tile_x_(0.0), + tile_y_(0.0), + scale_(0.0), + layer_extent_(0) + { + double resolution = mapnik::EARTH_CIRCUMFERENCE/(1 << z_); + tile_x_ = -0.5 * mapnik::EARTH_CIRCUMFERENCE + x_ * resolution; + tile_y_ = 0.5 * mapnik::EARTH_CIRCUMFERENCE - y_ * resolution; + + mapbox::util::pbf val_msg; + + while (layer_.next()) + { + switch(layer_.tag()) + { + case 1: + name_ = layer_.get_string(); + break; + case 2: + features_.push_back(layer_.get_message()); + break; + case 3: + layer_keys_.push_back(layer_.get_string()); + break; + case 4: + val_msg = layer_.get_message(); + while (val_msg.next()) + { + switch(val_msg.tag()) { + case 1: + layer_values_.push_back(val_msg.get_string()); + break; + case 2: + layer_values_.push_back(val_msg.get_float()); + break; + case 3: + layer_values_.push_back(val_msg.get_double()); + break; + case 4: + layer_values_.push_back(val_msg.get_int64()); + break; + case 5: + layer_values_.push_back(val_msg.get_uint64()); + break; + case 6: + layer_values_.push_back(val_msg.get_sint64()); + break; + case 7: + layer_values_.push_back(val_msg.get_bool()); + break; + default: + throw std::runtime_error("unknown Value type " + std::to_string(layer_.tag()) + " in layer.values"); + } + } + break; + case 5: + layer_extent_ = layer_.get_uint32(); + break; + case 15: + layer_.skip(); + break; + default: + throw std::runtime_error("unknown field type " + std::to_string(layer_.tag()) + " in layer"); + } + } + scale_ = (static_cast<double>(layer_extent_) / tile_size_) * tile_size_/resolution; + } + + tile_datasource_pbf::~tile_datasource_pbf() {} + + datasource::datasource_t tile_datasource_pbf::type() const + { + return datasource::Vector; + } + + featureset_ptr tile_datasource_pbf::features(query const& q) const + { + mapnik::filter_in_box filter(q.get_bbox()); + return std::make_shared<tile_featureset_pbf<mapnik::filter_in_box> > + (filter, get_tile_extent(), q.get_unbuffered_bbox(), q.property_names(), features_, tile_x_, tile_y_, scale_, layer_keys_, layer_values_); + } + + featureset_ptr tile_datasource_pbf::features_at_point(coord2d const& pt, double tol) const + { + mapnik::filter_at_point filter(pt,tol); + std::set<std::string> names; + for (auto const& key : layer_keys_) + { + names.insert(key); + } + return std::make_shared<tile_featureset_pbf<filter_at_point> > + (filter, get_tile_extent(), get_tile_extent(), names, features_, tile_x_, tile_y_, scale_, layer_keys_, layer_values_); + } + + void tile_datasource_pbf::set_envelope(box2d<double> const& bbox) + { + extent_initialized_ = true; + extent_ = bbox; + } + + box2d<double> tile_datasource_pbf::get_tile_extent() const + { + spherical_mercator merc(tile_size_); + double minx,miny,maxx,maxy; + merc.xyz(x_,y_,z_,minx,miny,maxx,maxy); + return box2d<double>(minx,miny,maxx,maxy); + } + + box2d<double> tile_datasource_pbf::envelope() const + { + if (!extent_initialized_) + { + extent_ = get_tile_extent(); + extent_initialized_ = true; + } + return extent_; + } + + boost::optional<mapnik::datasource_geometry_t> tile_datasource_pbf::get_geometry_type() const + { + return mapnik::datasource_geometry_t::Collection; + } + + layer_descriptor tile_datasource_pbf::get_descriptor() const + { + if (!attributes_added_) + { + for (auto const& key : layer_keys_) + { + // Object type here because we don't know the precise value until features are unpacked + desc_.add_descriptor(attribute_descriptor(key, Object)); + } + attributes_added_ = true; + } + return desc_; + } + + }} // end ns diff --git a/src/vector_tile_geometry_decoder.hpp b/src/vector_tile_geometry_decoder.hpp index 938d2e4..c0f1510 100644 --- a/src/vector_tile_geometry_decoder.hpp +++ b/src/vector_tile_geometry_decoder.hpp @@ -2,6 +2,7 @@ #define __MAPNIK_VECTOR_TILE_GEOMETRY_DECODER_H__ #include "vector_tile.pb.h" +#include "pbf_reader.hpp" #include <mapnik/util/is_clockwise.hpp> @@ -10,6 +11,8 @@ namespace mapnik { namespace vector_tile_impl { +// NOTE: this object is for one-time use. Once you've progressed to the end +// by calling next(), to re-iterate, you must construct a new object class Geometry { public: @@ -38,6 +41,34 @@ private: double ox, oy; }; +// NOTE: this object is for one-time use. Once you've progressed to the end +// by calling next(), to re-iterate, you must construct a new object +class GeometryPBF { + +public: + inline explicit GeometryPBF(std::pair< mapbox::util::pbf::const_uint32_iterator, mapbox::util::pbf::const_uint32_iterator > const& geo_iterator, + double tile_x, double tile_y, + double scale_x, double scale_y); + + enum command : uint8_t { + end = 0, + move_to = 1, + line_to = 2, + close = 7 + }; + + inline command next(double& rx, double& ry); + +private: + std::pair< mapbox::util::pbf::const_uint32_iterator, mapbox::util::pbf::const_uint32_iterator > geo_iterator_; + double scale_x_; + double scale_y_; + uint8_t cmd; + uint32_t length; + double x, y; + double ox, oy; +}; + Geometry::Geometry(vector_tile::Tile_Feature const& f, double tile_x, double tile_y, double scale_x, double scale_y) @@ -90,22 +121,70 @@ Geometry::command Geometry::next(double& rx, double& ry) { } } -inline mapnik::geometry::geometry<double> decode_geometry(vector_tile::Tile_Feature const& f, - double tile_x, double tile_y, - double scale_x, double scale_y, +GeometryPBF::GeometryPBF(std::pair<mapbox::util::pbf::const_uint32_iterator, mapbox::util::pbf::const_uint32_iterator > const& geo_iterator, + double tile_x, double tile_y, + double scale_x, double scale_y) + : geo_iterator_(geo_iterator), + scale_x_(scale_x), + scale_y_(scale_y), + cmd(1), + length(0), + x(tile_x), y(tile_y), + ox(0), oy(0) {} + +GeometryPBF::command GeometryPBF::next(double& rx, double& ry) { + if (geo_iterator_.first != geo_iterator_.second) { + if (length == 0) { + uint32_t cmd_length = static_cast<uint32_t>(*geo_iterator_.first++); + cmd = cmd_length & 0x7; + length = cmd_length >> 3; + } + + --length; + + if (cmd == move_to || cmd == line_to) { + int32_t dx = *geo_iterator_.first++; + int32_t dy = *geo_iterator_.first++; + dx = ((dx >> 1) ^ (-(dx & 1))); + dy = ((dy >> 1) ^ (-(dy & 1))); + x += (static_cast<double>(dx) / scale_x_); + y += (static_cast<double>(dy) / scale_y_); + rx = x; + ry = y; + if (cmd == move_to) { + ox = x; + oy = y; + return move_to; + } else { + return line_to; + } + } else if (cmd == close) { + rx = ox; + ry = oy; + return close; + } else { + fprintf(stderr, "unknown command: %d\n", cmd); + return end; + } + } else { + return end; + } +} + +template <typename T> +inline mapnik::geometry::geometry<double> decode_geometry(T & geoms, int32_t geom_type, bool treat_all_rings_as_exterior=false) { - Geometry::command cmd; - Geometry geoms(f,tile_x,tile_y,scale_x,scale_y); + typename T::command cmd; double x1, y1; mapnik::geometry::geometry<double> geom; // output geometry - switch (f.type()) + switch (geom_type) { case vector_tile::Tile_GeomType_POINT: { mapnik::geometry::multi_point<double> mp; - while ((cmd = geoms.next(x1, y1)) != Geometry::end) + while ((cmd = geoms.next(x1, y1)) != T::end) { mp.emplace_back(mapnik::geometry::point<double>(x1,y1)); } @@ -129,9 +208,9 @@ inline mapnik::geometry::geometry<double> decode_geometry(vector_tile::Tile_Feat mapnik::geometry::multi_line_string<double> multi_line; multi_line.emplace_back(); bool first = true; - while ((cmd = geoms.next(x1, y1)) != Geometry::end) + while ((cmd = geoms.next(x1, y1)) != T::end) { - if (cmd == Geometry::move_to) + if (cmd == T::move_to) { if (first) { @@ -173,9 +252,9 @@ inline mapnik::geometry::geometry<double> decode_geometry(vector_tile::Tile_Feat rings.emplace_back(); double x2,y2; bool first = true; - while ((cmd = geoms.next(x1, y1)) != Geometry::end) + while ((cmd = geoms.next(x1, y1)) != T::end) { - if (cmd == Geometry::move_to) + if (cmd == T::move_to) { x2 = x1; y2 = y1; @@ -188,7 +267,7 @@ inline mapnik::geometry::geometry<double> decode_geometry(vector_tile::Tile_Feat rings.emplace_back(); } } - else if (cmd == Geometry::close) + else if (cmd == T::close) { rings.back().add_coord(x2,y2); continue; @@ -302,7 +381,7 @@ inline mapnik::geometry::geometry<double> decode_geometry(vector_tile::Tile_Feat else if (num_poly == 1) { auto itr = std::make_move_iterator(multi_poly.begin()); - geom = std::move(mapnik::geometry::polygon<double>(std::move(*itr))); + geom = mapnik::geometry::polygon<double>(std::move(*itr)); return geom; } else diff --git a/src/vector_tile_geometry_encoder.hpp b/src/vector_tile_geometry_encoder.hpp index f14f543..dd95f8e 100644 --- a/src/vector_tile_geometry_encoder.hpp +++ b/src/vector_tile_geometry_encoder.hpp @@ -21,8 +21,8 @@ inline unsigned encode_geometry(mapnik::geometry::point<std::int64_t> const& pt, int32_t dx = pt.x - start_x; int32_t dy = pt.y - start_y; // Manual zigzag encoding. - current_feature.add_geometry((dx << 1) ^ (dx >> 31)); - current_feature.add_geometry((dy << 1) ^ (dy >> 31)); + current_feature.add_geometry((static_cast<unsigned>(dx) << 1) ^ (dx >> 31)); + current_feature.add_geometry((static_cast<unsigned>(dy) << 1) ^ (dy >> 31)); start_x = pt.x; start_y = pt.y; return 1; @@ -57,8 +57,8 @@ inline unsigned encode_geometry(mapnik::geometry::line_string<std::int64_t> cons int32_t dx = pt.x - start_x; int32_t dy = pt.y - start_y; // Manual zigzag encoding. - current_feature.add_geometry((dx << 1) ^ (dx >> 31)); - current_feature.add_geometry((dy << 1) ^ (dy >> 31)); + current_feature.add_geometry((static_cast<unsigned>(dx) << 1) ^ (dx >> 31)); + current_feature.add_geometry((static_cast<unsigned>(dy) << 1) ^ (dy >> 31)); start_x = pt.x; start_y = pt.y; } @@ -109,8 +109,8 @@ inline unsigned encode_geometry(mapnik::geometry::linear_ring<std::int64_t> cons int32_t dx = pt.x - start_x; int32_t dy = pt.y - start_y; // Manual zigzag encoding. - current_feature.add_geometry((dx << 1) ^ (dx >> 31)); - current_feature.add_geometry((dy << 1) ^ (dy >> 31)); + current_feature.add_geometry((static_cast<unsigned>(dx) << 1) ^ (dx >> 31)); + current_feature.add_geometry((static_cast<unsigned>(dy) << 1) ^ (dy >> 31)); start_x = pt.x; start_y = pt.y; ++count; diff --git a/src/vector_tile_processor.ipp b/src/vector_tile_processor.ipp index 6d639a3..8851fd3 100644 --- a/src/vector_tile_processor.ipp +++ b/src/vector_tile_processor.ipp @@ -8,7 +8,6 @@ #include <mapnik/datasource.hpp> #include <mapnik/projection.hpp> #include <mapnik/proj_transform.hpp> -#include <mapnik/proj_strategy.hpp> #include <mapnik/scale_denominator.hpp> #include <mapnik/attribute_descriptor.hpp> #include <mapnik/feature_layer_desc.hpp> @@ -22,13 +21,11 @@ #include <mapnik/image_scaling.hpp> #include <mapnik/image_compositing.hpp> #include <mapnik/view_transform.hpp> -#include <mapnik/view_strategy.hpp> #include <mapnik/util/noncopyable.hpp> #include <mapnik/transform_path_adapter.hpp> #include <mapnik/geometry_is_empty.hpp> #include <mapnik/geometry_envelope.hpp> #include <mapnik/geometry_adapters.hpp> -#include <mapnik/geometry_strategy.hpp> #include <mapnik/geometry_transform.hpp> // agg @@ -50,6 +47,7 @@ #include <string> #include <stdexcept> +#include "vector_tile_strategy.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" @@ -796,29 +794,12 @@ void processor<T>::apply_to_layer(mapnik::layer const& lay, feature = features->next(); continue; } - if (geom.is<mapnik::geometry::geometry_collection<double> >()) + if (handle_geometry(*feature, + geom, + prj_trans, + buffered_query_ext) > 0) { - auto const& collection = mapnik::util::get<mapnik::geometry::geometry_collection<double> >(geom); - for (auto const& part : collection) - { - if (handle_geometry(*feature, - part, - prj_trans, - buffered_query_ext) > 0) - { - painted_ = true; - } - } - } - else - { - if (handle_geometry(*feature, - geom, - prj_trans, - buffered_query_ext) > 0) - { - painted_ = true; - } + painted_ = true; } feature = features->next(); } @@ -885,10 +866,14 @@ struct encoder_visitor { return 0; } - unsigned operator() (mapnik::geometry::geometry_collection<std::int64_t> const& geom) + unsigned operator() (mapnik::geometry::geometry_collection<std::int64_t> & geom) { - //throw std::runtime_error("geometry_collections not supported in encoder_visitor"); - return 0; + unsigned count = 0; + for (auto & g : geom) + { + count += mapnik::util::apply_visitor((*this), g); + } + return count; } unsigned operator() (mapnik::geometry::point<std::int64_t> const& geom) @@ -1229,8 +1214,12 @@ struct simplify_visitor { unsigned operator() (mapnik::geometry::geometry_collection<std::int64_t> const& geom) { - //throw std::runtime_error("geometry_collection not supported in simplify_visitor"); - return 0; + unsigned count = 0; + for (auto const& g : geom) + { + count += mapnik::util::apply_visitor((*this), g); + } + return count; } unsigned operator() (mapnik::geometry::geometry_empty const& geom) @@ -1249,19 +1238,13 @@ unsigned processor<T>::handle_geometry(mapnik::feature_impl const& feature, mapnik::proj_transform const& prj_trans, mapnik::box2d<double> const& buffered_query_ext) { - mapnik::proj_backward_strategy proj_strat(prj_trans); - mapnik::view_strategy view_strat(t_); - mapnik::geometry::scale_strategy scale_strat(backend_.get_path_multiplier(), 0.5); - using sg_type = mapnik::geometry::strategy_group<mapnik::proj_backward_strategy, - mapnik::view_strategy, - mapnik::geometry::scale_strategy >; - sg_type sg(proj_strat, view_strat, scale_strat); + vector_tile_strategy vs(prj_trans, t_, backend_.get_path_multiplier()); mapnik::geometry::point<double> p1_min(buffered_query_ext.minx(), buffered_query_ext.miny()); mapnik::geometry::point<double> p1_max(buffered_query_ext.maxx(), buffered_query_ext.maxy()); - mapnik::geometry::point<std::int64_t> p2_min = mapnik::geometry::transform<std::int64_t>(p1_min, sg); - mapnik::geometry::point<std::int64_t> p2_max = mapnik::geometry::transform<std::int64_t>(p1_max, sg); + mapnik::geometry::point<std::int64_t> p2_min = mapnik::geometry::transform<std::int64_t>(p1_min, vs); + mapnik::geometry::point<std::int64_t> p2_max = mapnik::geometry::transform<std::int64_t>(p1_max, vs); box2d<int> bbox(p2_min.x, p2_min.y, p2_max.x, p2_max.y); - mapnik::geometry::geometry<std::int64_t> new_geom = mapnik::geometry::transform<std::int64_t>(geom, sg); + mapnik::geometry::geometry<std::int64_t> new_geom = mapnik::geometry::transform<std::int64_t>(geom, vs); encoder_visitor<T> encoder(backend_,feature,bbox, area_threshold_); if (simplify_distance_ > 0) { diff --git a/src/vector_tile_strategy.hpp b/src/vector_tile_strategy.hpp new file mode 100644 index 0000000..1f8c045 --- /dev/null +++ b/src/vector_tile_strategy.hpp @@ -0,0 +1,88 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2014 Artem Pavlenko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + *****************************************************************************/ + +#ifndef MAPNIK_VECTOR_TILE_STRATEGY_HPP +#define MAPNIK_VECTOR_TILE_STRATEGY_HPP + +// mapnik +#include <mapnik/config.hpp> +#include <mapnik/util/noncopyable.hpp> +#include <mapnik/proj_transform.hpp> +#include <mapnik/view_transform.hpp> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-local-typedef" +#include <boost/geometry/core/coordinate_type.hpp> +#include <boost/geometry/core/access.hpp> +#include <boost/numeric/conversion/cast.hpp> +#pragma GCC diagnostic pop + + +namespace mapnik { + +namespace vector_tile_impl { + +struct vector_tile_strategy +{ + vector_tile_strategy(proj_transform const& prj_trans, + view_transform const& tr, + double scaling) + : prj_trans_(prj_trans), + tr_(tr), + scaling_(scaling) {} + + template <typename P1, typename P2> + inline bool apply(P1 const& p1, P2 & p2) const + { + using p2_type = typename boost::geometry::coordinate_type<P2>::type; + double x = boost::geometry::get<0>(p1); + double y = boost::geometry::get<1>(p1); + double z = 0.0; + if (!prj_trans_.backward(x, y, z)) return false; + tr_.forward(&x,&y); + x = x * scaling_; + y = y * scaling_; + x = std::round(x); + y = std::round(y); + boost::geometry::set<0>(p2, static_cast<p2_type>(x)); + boost::geometry::set<1>(p2, static_cast<p2_type>(y)); + return true; + } + + template <typename P1, typename P2> + inline P2 execute(P1 const& p1, bool & status) const + { + P2 p2; + status = apply(p1, p2); + return p2; + } + + proj_transform const& prj_trans_; + view_transform const& tr_; + double const scaling_; +}; + +} +} + +#endif // MAPNIK_VECTOR_TILE_STRATEGY_HPP diff --git a/test/clipper_test.cpp b/test/clipper_test.cpp new file mode 100644 index 0000000..d06b2bc --- /dev/null +++ b/test/clipper_test.cpp @@ -0,0 +1,143 @@ +#include <limits> +#include <iostream> +#include <mapnik/projection.hpp> +#include <mapnik/geometry_transform.hpp> +//#include <mapnik/util/geometry_to_geojson.hpp> + +#include "vector_tile_strategy.hpp" +#include "vector_tile_projection.hpp" + +#include "catch.hpp" +#include "clipper.hpp" + +TEST_CASE( "vector_tile_strategy", "should not overflow" ) { + mapnik::projection merc("+init=epsg:3857",true); + mapnik::proj_transform prj_trans(merc,merc); // no-op + unsigned tile_size = 256; + mapnik::vector_tile_impl::spherical_mercator merc_tiler(tile_size); + double minx,miny,maxx,maxy; + merc_tiler.xyz(9664,20435,15,minx,miny,maxx,maxy); + mapnik::box2d<double> z15_extent(minx,miny,maxx,maxy); + mapnik::view_transform tr(tile_size,tile_size,z15_extent,0,0); + { + mapnik::vector_tile_impl::vector_tile_strategy vs(prj_trans, tr, 16); + // even an invalid point is not expected to result in values beyond hirange + mapnik::geometry::point<std::int64_t> g(-20037508.342789*2,-20037508.342789*2); + mapnik::geometry::geometry<std::int64_t> new_geom = mapnik::geometry::transform<std::int64_t>(g, vs); + REQUIRE( new_geom.is<mapnik::geometry::point<std::int64_t>>() ); + auto const& pt = mapnik::util::get<mapnik::geometry::point<std::int64_t>>(new_geom); + REQUIRE( (pt.x < ClipperLib::hiRange) ); + REQUIRE( (pt.y < ClipperLib::hiRange) ); + REQUIRE( (-pt.x < ClipperLib::hiRange) ); + REQUIRE( (-pt.y < ClipperLib::hiRange) ); + } + merc_tiler.xyz(0,0,0,minx,miny,maxx,maxy); + mapnik::geometry::polygon<std::int64_t> g; + g.exterior_ring.add_coord(minx,miny); + g.exterior_ring.add_coord(maxx,miny); + g.exterior_ring.add_coord(maxx,maxy); + g.exterior_ring.add_coord(minx,maxy); + g.exterior_ring.add_coord(minx,miny); + { + // absurdly large but still should not result in values beyond hirange + double path_multiplier = 100000000000.0; + mapnik::vector_tile_impl::vector_tile_strategy vs(prj_trans, tr, path_multiplier); + mapnik::geometry::geometry<std::int64_t> new_geom = mapnik::geometry::transform<std::int64_t>(g, vs); + REQUIRE( new_geom.is<mapnik::geometry::polygon<std::int64_t>>() ); + auto const& poly = mapnik::util::get<mapnik::geometry::polygon<std::int64_t>>(new_geom); + for (auto const& pt : poly.exterior_ring) + { + INFO( pt.x ) + INFO( ClipperLib::hiRange ) + REQUIRE( (pt.x < ClipperLib::hiRange) ); + REQUIRE( (pt.y < ClipperLib::hiRange) ); + REQUIRE( (-pt.x < ClipperLib::hiRange) ); + REQUIRE( (-pt.y < ClipperLib::hiRange) ); + } + } + { + // expected to trigger values above hirange + double path_multiplier = 1000000000000.0; + mapnik::vector_tile_impl::vector_tile_strategy vs(prj_trans, tr, path_multiplier); + mapnik::geometry::geometry<std::int64_t> new_geom = mapnik::geometry::transform<std::int64_t>(g, vs); + REQUIRE( new_geom.is<mapnik::geometry::polygon<std::int64_t>>() ); + auto const& poly = mapnik::util::get<mapnik::geometry::polygon<std::int64_t>>(new_geom); + for (auto const& pt : poly.exterior_ring) + { + INFO( pt.x ) + INFO( ClipperLib::hiRange ) + REQUIRE(( (pt.x > ClipperLib::hiRange) || + (pt.y > ClipperLib::hiRange) || + (-pt.x < ClipperLib::hiRange) || + (-pt.y < ClipperLib::hiRange) + )); + } + } + +} + +TEST_CASE( "clipper IntPoint", "should accept 64bit values" ) { + std::int64_t x = 4611686018427387903; + std::int64_t y = 4611686018427387903; + auto x0 = std::numeric_limits<std::int64_t>::max(); + auto y0 = std::numeric_limits<std::int64_t>::max(); + REQUIRE( x == ClipperLib::hiRange ); + REQUIRE( y == ClipperLib::hiRange ); + REQUIRE( (x0/2) == ClipperLib::hiRange ); + REQUIRE( (y0/2) == ClipperLib::hiRange ); + auto pt = ClipperLib::IntPoint(x,y); + CHECK( pt.x == x ); + CHECK( pt.y == y ); + auto pt2 = ClipperLib::IntPoint(x,y); + CHECK( (pt == pt2) ); + CHECK( !(pt != pt2) ); + // this is invalid when passed to RangeTest but should + // still be able to be created + auto pt3 = ClipperLib::IntPoint(x0,y0); + REQUIRE( pt3.x == x0 ); + REQUIRE( pt3.y == y0 ); + REQUIRE( (pt3.x/2) == ClipperLib::hiRange ); + REQUIRE( (pt3.y/2) == ClipperLib::hiRange ); + CHECK( (pt != pt3) ); +} + +TEST_CASE( "clipper AddPath 1", "should not throw within range coords" ) { + ClipperLib::Clipper clipper; + ClipperLib::Path clip_box; // actually mapnik::geometry::line_string<std::int64_t> + // values that should just barely work since they are one below the + // threshold + auto x0 = (std::numeric_limits<std::int64_t>::min()/2)+1; + auto y0 = (std::numeric_limits<std::int64_t>::min()/2)+1; + auto x1 = (std::numeric_limits<std::int64_t>::max()/2); + auto y1 = (std::numeric_limits<std::int64_t>::max()/2); + clip_box.emplace_back(x0,y0); + clip_box.emplace_back(x1,y0); + clip_box.emplace_back(x1,y1); + clip_box.emplace_back(x0,y1); + clip_box.emplace_back(x0,y0); + CHECK( clipper.AddPath(clip_box,ClipperLib::ptClip,true) ); +} + +TEST_CASE( "clipper AddPath 2", "should throw on out of range coords" ) { + ClipperLib::Clipper clipper; + ClipperLib::Path clip_box; // actually mapnik::geometry::line_string<std::int64_t> + auto x0 = std::numeric_limits<std::int64_t>::min()+1; + auto y0 = std::numeric_limits<std::int64_t>::min()+1; + auto x1 = std::numeric_limits<std::int64_t>::max()-1; + auto y1 = std::numeric_limits<std::int64_t>::max()-1; + clip_box.emplace_back(x0,y0); + clip_box.emplace_back(x1,y0); + clip_box.emplace_back(x1,y1); + clip_box.emplace_back(x0,y1); + clip_box.emplace_back(x0,y0); + try + { + clipper.AddPath(clip_box,ClipperLib::ptClip,true); + FAIL( "expected exception" ); + } + catch(std::exception const& ex) + { + REQUIRE(std::string(ex.what()) == "Coordinate outside allowed range: -9223372036854775807 -9223372036854775807 9223372036854775807 9223372036854775807"); + } +} + diff --git a/test/data/natural_earth.tif b/test/data/natural_earth.tif deleted file mode 100644 index 43500b0..0000000 Binary files a/test/data/natural_earth.tif and /dev/null differ diff --git a/test/data/tile_with_extra_feature_field.pbf b/test/data/tile_with_extra_feature_field.pbf new file mode 100644 index 0000000..484e656 --- /dev/null +++ b/test/data/tile_with_extra_feature_field.pbf @@ -0,0 +1,2 @@ + +this is the name0c(��x \ No newline at end of file diff --git a/test/data/tile_with_extra_field.pbf b/test/data/tile_with_extra_field.pbf new file mode 100644 index 0000000..0c08869 --- /dev/null +++ b/test/data/tile_with_extra_field.pbf @@ -0,0 +1,2 @@ + +this is the name(��x c \ No newline at end of file diff --git a/test/data/tile_with_extra_layer_fields.pbf b/test/data/tile_with_extra_layer_fields.pbf new file mode 100644 index 0000000..8dc5537 --- /dev/null +++ b/test/data/tile_with_extra_layer_fields.pbf @@ -0,0 +1,2 @@ + +this is the name(��0cx�c \ No newline at end of file diff --git a/test/data/tile_with_invalid_layer_value_type.pbf b/test/data/tile_with_invalid_layer_value_type.pbf new file mode 100644 index 0000000..8c8b9ef --- /dev/null +++ b/test/data/tile_with_invalid_layer_value_type.pbf @@ -0,0 +1,2 @@ + +this is the name"@c(��x \ No newline at end of file diff --git a/test/data/tile_with_unexpected_geomtype.pbf b/test/data/tile_with_unexpected_geomtype.pbf new file mode 100644 index 0000000..f718cd2 --- /dev/null +++ b/test/data/tile_with_unexpected_geomtype.pbf @@ -0,0 +1,3 @@ + +this is the name(��x" +this is the name"cccc(��x \ No newline at end of file diff --git a/test/encoding_util.hpp b/test/encoding_util.hpp index cf10bde..5568c48 100644 --- a/test/encoding_util.hpp +++ b/test/encoding_util.hpp @@ -150,6 +150,7 @@ template <typename T> std::string compare(mapnik::geometry::geometry<T> const& g) { vector_tile::Tile_Feature feature = geometry_to_feature(g); - auto g2 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms(feature,0.0,0.0,1.0,1.0); + auto g2 = mapnik::vector_tile_impl::decode_geometry(geoms,feature.type()); return decode_to_path_string(g2); } diff --git a/test/geometry_encoding.cpp b/test/geometry_encoding.cpp index 31a8805..b599580 100644 --- a/test/geometry_encoding.cpp +++ b/test/geometry_encoding.cpp @@ -180,7 +180,8 @@ TEST_CASE( "polygon with degenerate exterior ring ", "should be culled" ) { vector_tile::Tile_Feature feature = geometry_to_feature<std::int64_t>(p0); // since first ring is degenerate the whole polygon should be culled - auto p1 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms(feature,0.0,0.0,1.0,1.0); + auto p1 = mapnik::vector_tile_impl::decode_geometry(geoms, feature.type()); CHECK( p1.is<mapnik::geometry::geometry_empty>() ); } @@ -206,7 +207,8 @@ TEST_CASE( "polygon with degenerate exterior ring ", "should be culled" ) { vector_tile::Tile_Feature feature = geometry_to_feature<std::int64_t>(p0); // since first ring is degenerate the whole polygon should be culled - auto p1 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms(feature,0.0,0.0,1.0,1.0); + auto p1 = mapnik::vector_tile_impl::decode_geometry(geoms, feature.type()); CHECK( p1.is<mapnik::geometry::geometry_empty>() ); }*/ @@ -230,7 +232,8 @@ TEST_CASE( "polygon with valid exterior ring but degenerate interior ring", "sho CHECK( wkt0 == expected_wkt0); vector_tile::Tile_Feature feature = geometry_to_feature<std::int64_t>(p0); - auto p1 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms(feature,0.0,0.0,1.0,1.0); + auto p1 = mapnik::vector_tile_impl::decode_geometry(geoms, feature.type()); CHECK( p1.is<mapnik::geometry::polygon<double> >() ); auto const& poly = mapnik::util::get<mapnik::geometry::polygon<double> >(p1); // since interior ring is degenerate it should have been culled when decoded @@ -270,7 +273,8 @@ TEST_CASE( "polygon with valid exterior ring but one degenerate interior ring of CHECK( wkt0 == expected_wkt0); vector_tile::Tile_Feature feature = geometry_to_feature<std::int64_t>(p0); - auto p1 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms(feature,0.0,0.0,1.0,1.0); + auto p1 = mapnik::vector_tile_impl::decode_geometry(geoms, feature.type()); CHECK( p1.is<mapnik::geometry::polygon<double> >() ); auto const& poly = mapnik::util::get<mapnik::geometry::polygon<double> >(p1); // since first interior ring is degenerate it should have been culled when decoded @@ -311,7 +315,8 @@ TEST_CASE( "(multi)polygon with hole", "should round trip without changes" ) { CHECK( wkt0 == expected_wkt0); vector_tile::Tile_Feature feature = geometry_to_feature<std::int64_t>(p0); - auto p1 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms(feature,0.0,0.0,1.0,1.0); + auto p1 = mapnik::vector_tile_impl::decode_geometry(geoms, feature.type()); CHECK( p1.is<mapnik::geometry::polygon<double> >() ); CHECK( extent == mapnik::geometry::envelope(p1) ); @@ -323,7 +328,9 @@ TEST_CASE( "(multi)polygon with hole", "should round trip without changes" ) { // for polygons rings that were encoded correctly in vtiles (CCW exterior, CW interior) // then this should be unneeded, but for rings with incorrect order then this style of // decoding should allow them still to be queried correctly using the current mapnik hit_test algos - auto _p1 = mapnik::vector_tile_impl::decode_geometry(feature,0.0,0.0,1.0,1.0,true); + // Note, we need a new Geometry here, the old object can't be rewound. + mapnik::vector_tile_impl::Geometry geoms2(feature,0.0,0.0,1.0,1.0); + auto _p1 = mapnik::vector_tile_impl::decode_geometry(geoms2, feature.type(), true); wkt0.clear(); CHECK( mapnik::util::to_wkt(wkt0,_p1) ); CHECK( _p1.is<mapnik::geometry::multi_polygon<double> >() ); @@ -378,7 +385,8 @@ TEST_CASE( "(multi)polygon with hole", "should round trip without changes" ) { mapnik::box2d<double> multi_extent = mapnik::geometry::envelope(multi_poly); vector_tile::Tile_Feature feature1 = geometry_to_feature<std::int64_t>(multi_poly); - auto mp = mapnik::vector_tile_impl::decode_geometry(feature1,0.0,0.0,1.0,1.0); + mapnik::vector_tile_impl::Geometry geoms1(feature1,0.0,0.0,1.0,1.0); + auto mp = mapnik::vector_tile_impl::decode_geometry(geoms1, feature1.type()); CHECK( mp.is<mapnik::geometry::multi_polygon<double> >() ); CHECK( multi_extent == mapnik::geometry::envelope(mp) ); diff --git a/test/test_main.cpp b/test/test_main.cpp index 8d5a3a8..e12617c 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -6,7 +6,14 @@ int main (int argc, char* const argv[]) { - GOOGLE_PROTOBUF_VERIFY_VERSION; + try + { + GOOGLE_PROTOBUF_VERIFY_VERSION; + } + catch (std::exception const& ex) { + std::clog << ex.what() << "\n"; + return -1; + } int result = Catch::Session().run( argc, argv ); if (!result) printf("\x1b[1;32m ✓ \x1b[0m\n"); google::protobuf::ShutdownProtobufLibrary(); diff --git a/test/test_utils.cpp b/test/test_utils.cpp index 7378d5e..b156662 100644 --- a/test/test_utils.cpp +++ b/test/test_utils.cpp @@ -30,6 +30,14 @@ std::shared_ptr<mapnik::memory_datasource> build_ds(double x,double y, bool seco mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx,1)); mapnik::transcoder tr("utf-8"); feature->put("name",tr.transcode("null island")); + // NOTE: all types below that are not part of mapnik::value + // are likely getting converted, e.g. float -> double + feature->put_new("int",static_cast<int64_t>(-73)); + feature->put_new("uint",static_cast<uint64_t>(37)); + feature->put_new("float",static_cast<float>(99.2)); + feature->put_new("double",static_cast<double>(83.4)); + feature->put_new("bool",true); + feature->put_new("boolf",false); feature->set_geometry(mapnik::geometry::point<double>(x,y)); ds->push(feature); if (second) { @@ -46,13 +54,17 @@ std::shared_ptr<mapnik::memory_datasource> build_ds(double x,double y, bool seco std::shared_ptr<mapnik::memory_datasource> build_geojson_ds(std::string const& geojson_file) { mapnik::util::file input(geojson_file); - auto json = input.data(); + if (!input.open()) + { + throw std::runtime_error("failed to open geojson"); + } mapnik::geometry::geometry<double> geom; - std::string json_string(json.get()); + std::string json_string(input.data().get(), input.size()); if (!mapnik::json::from_geojson(json_string, geom)) { throw std::runtime_error("failed to parse geojson"); } + mapnik::geometry::correct(geom); mapnik::parameters params; params["type"] = "memory"; std::shared_ptr<mapnik::memory_datasource> ds = std::make_shared<mapnik::memory_datasource>(params); diff --git a/test/vector_tile.cpp b/test/vector_tile.cpp index 04ecb1c..3a15986 100644 --- a/test/vector_tile.cpp +++ b/test/vector_tile.cpp @@ -126,10 +126,10 @@ TEST_CASE( "vector tile output 1", "should create vector tile with two points" ) CHECK(9 == f.geometry(0)); CHECK(4096 == f.geometry(1)); CHECK(4096 == f.geometry(2)); - CHECK(95 == tile.ByteSize()); + CHECK(194 == tile.ByteSize()); std::string buffer; CHECK(tile.SerializeToString(&buffer)); - CHECK(95 == buffer.size()); + CHECK(194 == buffer.size()); } TEST_CASE( "vector tile output 2", "adding empty layers should result in empty tile" ) { @@ -232,7 +232,7 @@ TEST_CASE( "vector tile input", "should be able to parse message and render poin // serialize to message std::string buffer; CHECK(tile.SerializeToString(&buffer)); - CHECK(52 == buffer.size()); + CHECK(151 == buffer.size()); // now create new objects mapnik::Map map2(tile_size,tile_size,"+init=epsg:3857"); tile_type tile2; @@ -254,7 +254,13 @@ TEST_CASE( "vector tile input", "should be able to parse message and render poin CHECK( ds->get_geometry_type() == mapnik::datasource_geometry_t::Collection ); mapnik::layer_descriptor lay_desc = ds->get_descriptor(); std::vector<std::string> expected_names; + expected_names.push_back("bool"); + expected_names.push_back("boolf"); + expected_names.push_back("double"); + expected_names.push_back("float"); + expected_names.push_back("int"); expected_names.push_back("name"); + expected_names.push_back("uint"); std::vector<std::string> names; for (auto const& desc : lay_desc.get_descriptors()) { @@ -540,25 +546,11 @@ TEST_CASE( "encoding single line 1", "should maintain start/end vertex" ) { // ported from shapefile test in tilelive-bridge (a:should render a (1.0.1)) TEST_CASE( "encoding single line 2", "should maintain start/end vertex" ) { unsigned path_multiplier = 16; - //unsigned tolerance = 5; vector_tile::Tile tile; mapnik::vector_tile_impl::backend_pbf backend(tile,path_multiplier); backend.start_tile_layer("layer"); mapnik::feature_ptr feature(mapnik::feature_factory::create(std::make_shared<mapnik::context_type>(),1)); backend.start_tile_feature(*feature); - /* - std::unique_ptr<mapnik::geometry_type> g(new mapnik::geometry_type(mapnik::geometry_type::types::Polygon)); - g->move_to(168.267850,-24.576888); - g->line_to(167.982618,-24.697145); - g->line_to(168.114561,-24.783548); - g->line_to(168.267850,-24.576888); - g->line_to(168.267850,-24.576888); - g->close_path(); - //g->push_vertex(256.000000,-0.00000, mapnik::SEG_CLOSE); - // todo - why does shape_io result in on-zero close path x,y? - mapnik::vertex_adapter va(*g); - backend.add_path(va, tolerance, g->type()); - */ mapnik::geometry::polygon<double> geom; { mapnik::geometry::linear_ring<double> ring; @@ -569,7 +561,7 @@ TEST_CASE( "encoding single line 2", "should maintain start/end vertex" ) { ring.add_coord(168.267850,-24.576888); geom.set_exterior_ring(std::move(ring)); } - mapnik::geometry::scale_strategy scale_strat(backend.get_path_multiplier(), 0.5); + mapnik::geometry::scale_rounding_strategy scale_strat(backend.get_path_multiplier()); mapnik::geometry::polygon<std::int64_t> poly = mapnik::geometry::transform<std::int64_t>(geom, scale_strat); std::string foo; mapnik::util::to_wkt(foo, poly); @@ -627,13 +619,44 @@ mapnik::geometry::geometry<double> round_trip(mapnik::geometry::geometry<double> } vector_tile::Tile_Feature const& f = layer.features(0); double scale = (double)path_multiplier; - return mapnik::vector_tile_impl::decode_geometry(f,0,0,scale,-1*scale); + + mapnik::vector_tile_impl::Geometry geoms(f,0,0,scale,-1*scale); + return mapnik::vector_tile_impl::decode_geometry(geoms, f.type()); } TEST_CASE( "vector tile point encoding", "should create vector tile with data" ) { mapnik::geometry::point<double> geom(0,0); mapnik::geometry::geometry<double> new_geom = round_trip(geom); CHECK( !mapnik::geometry::is_empty(new_geom) ); + std::string wkt; + CHECK( mapnik::util::to_wkt(wkt, new_geom) ); + CHECK( wkt == "POINT(128 -128)" ); + CHECK( new_geom.is<mapnik::geometry::point<double> >() ); +} + +TEST_CASE( "vector tile geometry collection encoding", "should create vector tile with data" ) { + mapnik::geometry::point<double> geom_p(0,0); + mapnik::geometry::geometry_collection<double> geom; + geom.push_back(geom_p); + mapnik::geometry::geometry<double> new_geom = round_trip(geom); + CHECK( !mapnik::geometry::is_empty(new_geom) ); + std::string wkt; + CHECK( mapnik::util::to_wkt(wkt, new_geom) ); + CHECK( wkt == "POINT(128 -128)" ); + CHECK( new_geom.is<mapnik::geometry::point<double> >() ); +} + +TEST_CASE( "vector tile geometry collection encoding x2", "should create vector tile with data" ) { + mapnik::geometry::point<double> geom_p(0,0); + mapnik::geometry::geometry_collection<double> geom_t; + geom_t.push_back(geom_p); + mapnik::geometry::geometry_collection<double> geom; + geom.push_back(std::move(geom_t)); + mapnik::geometry::geometry<double> new_geom = round_trip(geom); + CHECK( !mapnik::geometry::is_empty(new_geom) ); + std::string wkt; + CHECK( mapnik::util::to_wkt(wkt, new_geom) ); + CHECK( wkt == "POINT(128 -128)" ); CHECK( new_geom.is<mapnik::geometry::point<double> >() ); } @@ -667,7 +690,7 @@ TEST_CASE( "vector tile line_string encoding", "should create vector tile with d mapnik::geometry::geometry<double> new_geom = round_trip(geom); std::string wkt; CHECK( mapnik::util::to_wkt(wkt, new_geom) ); - CHECK( wkt == "LINESTRING(128 -128,192.001 0)" ); + CHECK( wkt == "LINESTRING(128 -128,192 0)" ); CHECK( !mapnik::geometry::is_empty(new_geom) ); CHECK( new_geom.is<mapnik::geometry::line_string<double> >() ); } @@ -681,7 +704,7 @@ TEST_CASE( "vector tile multi_line_string encoding of single line_string", "shou mapnik::geometry::geometry<double> new_geom = round_trip(geom); std::string wkt; CHECK( mapnik::util::to_wkt(wkt, new_geom) ); - CHECK( wkt == "LINESTRING(128 -128,192.001 0)" ); + CHECK( wkt == "LINESTRING(128 -128,192 0)" ); CHECK( !mapnik::geometry::is_empty(new_geom) ); CHECK( new_geom.is<mapnik::geometry::line_string<double> >() ); } @@ -699,7 +722,7 @@ TEST_CASE( "vector tile multi_line_string encoding of actual multi_line_string", mapnik::geometry::geometry<double> new_geom = round_trip(geom); std::string wkt; CHECK( mapnik::util::to_wkt(wkt, new_geom) ); - CHECK( wkt == "MULTILINESTRING((128 -128,192.001 0),(120.889 -128,63.288 -256))" ); + CHECK( wkt == "MULTILINESTRING((128 -128,192 0),(120.889 -128,63.288 -256))" ); CHECK( !mapnik::geometry::is_empty(new_geom) ); CHECK( new_geom.is<mapnik::geometry::multi_line_string<double> >() ); } @@ -808,7 +831,7 @@ TEST_CASE( "vector tile line_string is simplified", "should create vector tile w mapnik::geometry::geometry<double> new_geom = round_trip(line,500); std::string wkt; CHECK( mapnik::util::to_wkt(wkt, new_geom) ); - CHECK( wkt == "LINESTRING(128 -128,192.001 0)" ); + CHECK( wkt == "LINESTRING(128 -128,192 0)" ); CHECK( !mapnik::geometry::is_empty(new_geom) ); CHECK( new_geom.is<mapnik::geometry::line_string<double> >() ); auto const& line2 = mapnik::util::get<mapnik::geometry::line_string<double> >(new_geom); @@ -826,7 +849,7 @@ TEST_CASE( "vector tile multi_line_string is simplified", "should create vector mapnik::geometry::geometry<double> new_geom = round_trip(geom,500); std::string wkt; CHECK( mapnik::util::to_wkt(wkt, new_geom) ); - CHECK( wkt == "LINESTRING(128 -128,192.001 0)" ); + CHECK( wkt == "LINESTRING(128 -128,192 0)" ); CHECK( !mapnik::geometry::is_empty(new_geom) ); CHECK( new_geom.is<mapnik::geometry::line_string<double> >() ); auto const& line2 = mapnik::util::get<mapnik::geometry::line_string<double> >(new_geom); @@ -917,10 +940,7 @@ TEST_CASE( "vector tile from simplified geojson", "should create vector tile wit mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); mapnik::Map map(tile_size,tile_size,"+init=epsg:3857"); mapnik::layer lyr("layer","+init=epsg:4326"); - // create a datasource with a feature outside the map std::shared_ptr<mapnik::memory_datasource> ds = testing::build_geojson_ds("./test/data/poly.geojson"); - // but fake the overall envelope to ensure the layer is still processed - // and then removed given no intersecting features will be added ds->set_envelope(mapnik::box2d<double>(160.147311,11.047284,160.662858,11.423830)); lyr.set_datasource(ds); map.add_layer(lyr); @@ -941,7 +961,8 @@ TEST_CASE( "vector tile from simplified geojson", "should create vector tile wit double tile_x = -0.5 * mapnik::EARTH_CIRCUMFERENCE + x * resolution; double tile_y = 0.5 * mapnik::EARTH_CIRCUMFERENCE - y * resolution; double scale = (static_cast<double>(layer.extent()) / tile_size) * tile_size/resolution; - auto geom = mapnik::vector_tile_impl::decode_geometry(f,tile_x,tile_y,scale,-1*scale); + mapnik::vector_tile_impl::Geometry geoms(f,tile_x, tile_y,scale,-1*scale); + auto geom = mapnik::vector_tile_impl::decode_geometry(geoms,f.type()); unsigned int n_err = 0; mapnik::projection wgs84("+init=epsg:4326",true); @@ -951,7 +972,7 @@ TEST_CASE( "vector tile from simplified geojson", "should create vector tile wit CHECK( n_err == 0 ); std::string geojson_string; CHECK( mapnik::util::to_geojson(geojson_string,projected_geom) ); - CHECK( geojson_string == "{\"type\":\"MultiPolygon\",\"coordinates\":[[[[160.42640625,11.4238608092025],[160.41375,11.404562686369],[160.3996875,11.3949131331061],[160.3996875,11.3990486960562],[160.39265625,11.4031841988239],[160.3940625,11.3976701817588],[160.38703125,11.3838846711709],[160.39265625,11.3825060833676],[160.39125,11.3618264654176],[160.3378125,11.3397665531013],[160.3434375,11.3604477708622],[160.26609375,11.3094313929343],[160.28296875,11.3011576095711],[160.29,11.2 [...] + CHECK( geojson_string == "{\"type\":\"Polygon\",\"coordinates\":[[[160.42359375,11.422482415387],[160.40671875,11.3976701817587],[160.396875,11.3935345987523],[160.39828125,11.4018057045895],[160.39265625,11.4004272036667],[160.38984375,11.3811274888866],[160.3940625,11.3838846711709],[160.3771875,11.3521754635814],[160.33921875,11.3590690696413],[160.35046875,11.3645838345287],[160.3575,11.3645838345287],[160.3575,11.3756130442004],[160.28859375,11.3480392200085],[160.295625,11.3287 [...] } mapnik::geometry::geometry<double> round_trip2(mapnik::geometry::geometry<double> const& geom, @@ -1000,7 +1021,8 @@ mapnik::geometry::geometry<double> round_trip2(mapnik::geometry::geometry<double double tile_x = -0.5 * mapnik::EARTH_CIRCUMFERENCE + x * resolution; double tile_y = 0.5 * mapnik::EARTH_CIRCUMFERENCE - y * resolution; double scale = (static_cast<double>(layer.extent()) / tile_size) * tile_size/resolution; - return mapnik::vector_tile_impl::decode_geometry(f,tile_x,tile_y,scale,-1*scale); + mapnik::vector_tile_impl::Geometry geoms(f,tile_x, tile_y,scale,-1*scale); + return mapnik::vector_tile_impl::decode_geometry(geoms,f.type()); } TEST_CASE( "vector tile line_string is verify direction", "should line string with proper directions" ) { @@ -1028,7 +1050,7 @@ TEST_CASE( "vector tile line_string is verify direction", "should line string wi mapnik::geometry::geometry<double> xgeom = mapnik::geometry::transform<double>(new_geom, proj_strat); std::string wkt; mapnik::util::to_wkt(wkt, xgeom); - CHECK( wkt == "MULTILINESTRING((0 1.99992945603165,2.00006103515625 1.99992945603165,2.00006103515625 0),(7.99996948242188 0,7.99996948242188 1.99992945603165,59.9999084472656 1.99992945603165,59.9999084472656 7.99994115658818,7.99996948242188 7.99994115658818,7.99996948242188 59.9998101102059,2.00006103515625 59.9998101102059,2.00006103515625 7.99994115658817,0.0000000000000005 7.99994115658817))" ); + CHECK( wkt == "MULTILINESTRING((0 1.99992945603165,2.00006103515625 1.99992945603165,2.00006103515625 0),(7.99996948242188 0,7.99996948242188 1.99992945603165,59.9999084472656 1.99992945603165,59.9999084472656 7.99994115658818,7.99996948242188 7.99994115658818,7.99996948242188 59.9999474398107,2.00006103515625 59.9999474398107,2.00006103515625 7.99994115658818,0.0000000000000005 7.99994115658818))" ); REQUIRE( !mapnik::geometry::is_empty(xgeom) ); REQUIRE( new_geom.is<mapnik::geometry::multi_line_string<double> >() ); auto const& line2 = mapnik::util::get<mapnik::geometry::multi_line_string<double> >(new_geom); diff --git a/test/vector_tile_pbf.cpp b/test/vector_tile_pbf.cpp new file mode 100644 index 0000000..25482e5 --- /dev/null +++ b/test/vector_tile_pbf.cpp @@ -0,0 +1,557 @@ +#include "catch.hpp" + +// test utils +#include "test_utils.hpp" +#include <mapnik/memory_datasource.hpp> +#include <mapnik/util/fs.hpp> +#include <mapnik/agg_renderer.hpp> +#include <mapnik/feature_factory.hpp> +#include <mapnik/load_map.hpp> +#include <mapnik/image_util.hpp> +#include <mapnik/vertex_adapters.hpp> +#include <mapnik/projection.hpp> +#include <mapnik/proj_transform.hpp> +#include <mapnik/geometry_is_empty.hpp> +#include <mapnik/util/geometry_to_geojson.hpp> +#include <mapnik/util/geometry_to_wkt.hpp> +#include <mapnik/geometry_reprojection.hpp> +#include <mapnik/geometry_transform.hpp> +#include <mapnik/geometry_strategy.hpp> +#include <mapnik/proj_strategy.hpp> +#include <mapnik/geometry.hpp> +#include <mapnik/datasource_cache.hpp> + +#include <boost/optional/optional_io.hpp> + +// vector output api +#include "vector_tile_compression.hpp" +#include "vector_tile_processor.hpp" +#include "vector_tile_backend_pbf.hpp" +#include "vector_tile_util.hpp" +#include "vector_tile_projection.hpp" +#include "vector_tile_geometry_decoder.hpp" + +// vector input api +#include "vector_tile_datasource.hpp" +#include "vector_tile_datasource_pbf.hpp" +#include "pbf_reader.hpp" + +#include <string> +#include <fstream> +#include <streambuf> + +TEST_CASE( "pbf vector tile input", "should be able to parse message and render point" ) { + typedef mapnik::vector_tile_impl::backend_pbf backend_type; + typedef mapnik::vector_tile_impl::processor<backend_type> renderer_type; + typedef vector_tile::Tile tile_type; + tile_type tile; + backend_type backend(tile,16); + unsigned tile_size = 256; + mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); + mapnik::Map map(tile_size,tile_size,"+init=epsg:3857"); + mapnik::layer lyr("layer",map.srs()); + lyr.set_datasource(testing::build_ds(0,0)); + map.add_layer(lyr); + map.zoom_to_box(bbox); + mapnik::request m_req(map.width(),map.height(),map.get_current_extent()); + renderer_type ren(backend,map,m_req); + ren.apply(); + // serialize to message + std::string buffer; + CHECK(tile.SerializeToString(&buffer)); + CHECK(151 == buffer.size()); + // now create new objects + mapnik::Map map2(tile_size,tile_size,"+init=epsg:3857"); + tile_type tile2; + CHECK(tile2.ParseFromString(buffer)); + std::string key(""); + CHECK(false == mapnik::vector_tile_impl::is_solid_extent(tile2,key)); + CHECK("" == key); + CHECK(1 == tile2.layers_size()); + vector_tile::Tile_Layer const& layer2 = tile2.layers(0); + CHECK(std::string("layer") == layer2.name()); + CHECK(1 == layer2.features_size()); + + mapnik::layer lyr2("layer",map.srs()); + + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer3 = pbf_tile.get_message(); + + std::shared_ptr<mapnik::vector_tile_impl::tile_datasource_pbf> ds = std::make_shared< + mapnik::vector_tile_impl::tile_datasource_pbf>( + layer3,0,0,0,map2.width()); + CHECK(ds->get_name() == "layer"); + ds->set_envelope(bbox); + CHECK( ds->type() == mapnik::datasource::Vector ); + CHECK( ds->get_geometry_type() == mapnik::datasource_geometry_t::Collection ); + mapnik::layer_descriptor lay_desc = ds->get_descriptor(); + std::vector<std::string> expected_names; + expected_names.push_back("bool"); + expected_names.push_back("boolf"); + expected_names.push_back("double"); + expected_names.push_back("float"); + expected_names.push_back("int"); + expected_names.push_back("name"); + expected_names.push_back("uint"); + std::vector<std::string> names; + for (auto const& desc : lay_desc.get_descriptors()) + { + names.push_back(desc.get_name()); + } + + CHECK(names == expected_names); + lyr2.set_datasource(ds); + lyr2.add_style("style"); + map2.add_layer(lyr2); + mapnik::load_map(map2,"test/data/style.xml"); + //std::clog << mapnik::save_map_to_string(map2) << "\n"; + map2.zoom_to_box(bbox); + mapnik::image_rgba8 im(map2.width(),map2.height()); + mapnik::agg_renderer<mapnik::image_rgba8> ren2(map2,im); + ren2.apply(); + if (!mapnik::util::exists("test/fixtures/expected-1.png")) { + mapnik::save_to_file(im,"test/fixtures/expected-1.png","png32"); + } + unsigned diff = testing::compare_images(im,"test/fixtures/expected-1.png"); + CHECK(0 == diff); + if (diff > 0) { + mapnik::save_to_file(im,"test/fixtures/actual-1.png","png32"); + } +} + + +TEST_CASE( "pbf vector tile datasource", "should filter features outside extent" ) { + typedef mapnik::vector_tile_impl::backend_pbf backend_type; + typedef mapnik::vector_tile_impl::processor<backend_type> renderer_type; + typedef vector_tile::Tile tile_type; + tile_type tile; + backend_type backend(tile,16); + unsigned tile_size = 256; + mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); + mapnik::Map map(tile_size,tile_size,"+init=epsg:3857"); + mapnik::layer lyr("layer",map.srs()); + lyr.set_datasource(testing::build_ds(0,0)); + map.add_layer(lyr); + mapnik::request m_req(tile_size,tile_size,bbox); + renderer_type ren(backend,map,m_req); + ren.apply(); + std::string key(""); + CHECK(false == mapnik::vector_tile_impl::is_solid_extent(tile,key)); + CHECK("" == key); + CHECK(1 == tile.layers_size()); + vector_tile::Tile_Layer const& layer = tile.layers(0); + CHECK(std::string("layer") == layer.name()); + CHECK(1 == layer.features_size()); + vector_tile::Tile_Feature const& f = layer.features(0); + CHECK(static_cast<mapnik::value_integer>(1) == static_cast<mapnik::value_integer>(f.id())); + CHECK(3 == f.geometry_size()); + CHECK(9 == f.geometry(0)); + CHECK(4096 == f.geometry(1)); + CHECK(4096 == f.geometry(2)); + + std::string buffer; + tile.SerializeToString(&buffer); + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer2 = pbf_tile.get_message(); + + // now actually start the meat of the test + mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0,tile_size); + mapnik::featureset_ptr fs; + + // ensure we can query single feature + fs = ds.features(mapnik::query(bbox)); + mapnik::feature_ptr feat = fs->next(); + CHECK(feat != mapnik::feature_ptr()); + CHECK(feat->size() == 0); + CHECK(fs->next() == mapnik::feature_ptr()); + mapnik::query qq = mapnik::query(mapnik::box2d<double>(-1,-1,1,1)); + qq.add_property_name("name"); + fs = ds.features(qq); + feat = fs->next(); + CHECK(feat != mapnik::feature_ptr()); + CHECK(feat->size() == 1); +// CHECK(feat->get("name") == "null island"); + + // now check that datasource api throws out feature which is outside extent + fs = ds.features(mapnik::query(mapnik::box2d<double>(-10,-10,-10,-10))); + CHECK(fs->next() == mapnik::feature_ptr()); + + // ensure same behavior for feature_at_point + fs = ds.features_at_point(mapnik::coord2d(0.0,0.0),0.0001); + CHECK(fs->next() != mapnik::feature_ptr()); + + fs = ds.features_at_point(mapnik::coord2d(1.0,1.0),1.0001); + CHECK(fs->next() != mapnik::feature_ptr()); + + fs = ds.features_at_point(mapnik::coord2d(-10,-10),0); + CHECK(fs->next() == mapnik::feature_ptr()); + + // finally, make sure attributes are also filtered + mapnik::feature_ptr f_ptr; + fs = ds.features(mapnik::query(bbox)); + f_ptr = fs->next(); + CHECK(f_ptr != mapnik::feature_ptr()); + // no attributes + CHECK(f_ptr->context()->size() == 0); + + mapnik::query q(bbox); + q.add_property_name("name"); + fs = ds.features(q); + f_ptr = fs->next(); + CHECK(f_ptr != mapnik::feature_ptr()); + // one attribute + CHECK(f_ptr->context()->size() == 1); +} + +// NOTE: encoding multiple lines as one path is technically incorrect +// because in Mapnik the protocol is to split geometry parts into separate paths. +// However this case should still be supported because keeping a single flat array is an +// important optimization in the case that lines do not need to be labeled in custom ways +// or represented as GeoJSON +TEST_CASE( "pbf encoding multi line as one path", "should maintain second move_to command" ) { + // Options + // here we use a multiplier of 1 to avoid rounding numbers + // and stay in integer space for simplity + unsigned path_multiplier = 1; + // here we use an extreme tolerance to prove that all vertices are maintained no matter + // the tolerance because we never want to drop a move_to or the first line_to + //unsigned tolerance = 2000000; + // now create the testing data + vector_tile::Tile tile; + unsigned tile_size = 256; + mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); + mapnik::vector_tile_impl::backend_pbf backend(tile,path_multiplier); + backend.start_tile_layer("layer"); + mapnik::feature_ptr feature(mapnik::feature_factory::create(std::make_shared<mapnik::context_type>(),1)); + backend.start_tile_feature(*feature); + mapnik::geometry::multi_line_string<std::int64_t> geom; + { + mapnik::geometry::linear_ring<std::int64_t> ring; + ring.add_coord(0,0); + ring.add_coord(2,2); + geom.emplace_back(std::move(ring)); + } + { + mapnik::geometry::linear_ring<std::int64_t> ring; + ring.add_coord(1,1); + ring.add_coord(2,2); + geom.emplace_back(std::move(ring)); + } + /* + g->move_to(0,0); // takes 3 geoms: command length,x,y + g->line_to(2,2); // new command, so again takes 3 geoms: command length,x,y | total 6 + g->move_to(1,1); // takes 3 geoms: command length,x,y + g->line_to(2,2); // new command, so again takes 3 geoms: command length,x,y | total 6 + */ + backend.current_feature_->set_type(vector_tile::Tile_GeomType_LINESTRING); + for (auto const& line : geom) + { + backend.add_path(line); + } + backend.stop_tile_feature(); + backend.stop_tile_layer(); + // done encoding single feature/geometry + std::string key(""); + CHECK(false == mapnik::vector_tile_impl::is_solid_extent(tile,key)); + CHECK("" == key); + CHECK(1 == tile.layers_size()); + vector_tile::Tile_Layer const& layer = tile.layers(0); + CHECK(1 == layer.features_size()); + vector_tile::Tile_Feature const& f = layer.features(0); + CHECK(12 == f.geometry_size()); + CHECK(9 == f.geometry(0)); // 1 move_to + CHECK(0 == f.geometry(1)); // x:0 + CHECK(0 == f.geometry(2)); // y:0 + CHECK(10 == f.geometry(3)); // 1 line_to + CHECK(4 == f.geometry(4)); // x:2 + CHECK(4 == f.geometry(5)); // y:2 + CHECK(9 == f.geometry(6)); // 1 move_to + CHECK(1 == f.geometry(7)); // x:1 + CHECK(1 == f.geometry(8)); // y:1 + CHECK(10 == f.geometry(9)); // 1 line_to + CHECK(2 == f.geometry(10)); // x:2 + CHECK(2 == f.geometry(11)); // y:2 + + mapnik::featureset_ptr fs; + mapnik::feature_ptr f_ptr; + + + std::string buffer; + tile.SerializeToString(&buffer); + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer2 = pbf_tile.get_message(); + + + mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0,tile_size); + fs = ds.features(mapnik::query(bbox)); + f_ptr = fs->next(); + CHECK(f_ptr != mapnik::feature_ptr()); + // no attributes + CHECK(f_ptr->context()->size() == 0); + + CHECK(f_ptr->get_geometry().is<mapnik::geometry::multi_line_string<double> >()); +} + + +// NOTE: encoding multiple lines as one path is technically incorrect +// because in Mapnik the protocol is to split geometry parts into separate paths. +// However this case should still be supported because keeping a single flat array is an +// important optimization in the case that lines do not need to be labeled in custom ways +// or represented as GeoJSON + +TEST_CASE( "pbf decoding empty buffer", "should throw exception" ) { + std::string buffer; + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer2; + REQUIRE_THROWS(layer2 = pbf_tile.get_message()); +} + +TEST_CASE( "pbf decoding garbage buffer", "should throw exception" ) { + std::string buffer("daufyglwi3h7fseuhfas8w3h,dksufasdf"); + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer2; + REQUIRE_THROWS(layer2 = pbf_tile.get_message()); +} + + +TEST_CASE( "pbf decoding some truncated buffers", "should throw exception" ) { + + typedef mapnik::vector_tile_impl::backend_pbf backend_type; + typedef mapnik::vector_tile_impl::processor<backend_type> renderer_type; + typedef vector_tile::Tile tile_type; + tile_type tile; + backend_type backend(tile,16); + unsigned tile_size = 256; + mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); + mapnik::Map map(tile_size,tile_size,"+init=epsg:3857"); + mapnik::layer lyr("layer",map.srs()); + lyr.set_datasource(testing::build_ds(0,0)); + map.add_layer(lyr); + mapnik::request m_req(tile_size,tile_size,bbox); + renderer_type ren(backend,map,m_req); + ren.apply(); + std::string key(""); + CHECK(false == mapnik::vector_tile_impl::is_solid_extent(tile,key)); + CHECK("" == key); + CHECK(1 == tile.layers_size()); + vector_tile::Tile_Layer const& layer = tile.layers(0); + CHECK(std::string("layer") == layer.name()); + CHECK(1 == layer.features_size()); + vector_tile::Tile_Feature const& f = layer.features(0); + CHECK(static_cast<mapnik::value_integer>(1) == static_cast<mapnik::value_integer>(f.id())); + CHECK(3 == f.geometry_size()); + CHECK(9 == f.geometry(0)); + CHECK(4096 == f.geometry(1)); + CHECK(4096 == f.geometry(2)); + + + // We will test truncating the generated protobuf at every increment. + // Most cases should fail, except for the lucky bites where we chop + // it off at a point that would be valid anyway. + std::string buffer; + tile.SerializeToString(&buffer); + for (int i=1; i< buffer.size(); i++) + { + CHECK_THROWS({ + mapbox::util::pbf pbf_tile(buffer.c_str(), i); + pbf_tile.next(); + mapbox::util::pbf layer2 = pbf_tile.get_message(); + mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0,tile_size); + mapnik::featureset_ptr fs; + mapnik::feature_ptr f_ptr; + fs = ds.features(mapnik::query(bbox)); + f_ptr = fs->next(); + while (f_ptr != mapnik::feature_ptr()) { + f_ptr = fs->next(); + } + }); + } +} + +TEST_CASE( "pbf vector tile from simplified geojson", "should create vector tile with data" ) { + typedef mapnik::vector_tile_impl::backend_pbf backend_type; + typedef mapnik::vector_tile_impl::processor<backend_type> renderer_type; + typedef vector_tile::Tile tile_type; + tile_type tile; + backend_type backend(tile,1000); + unsigned tile_size = 256; + mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); + mapnik::Map map(tile_size,tile_size,"+init=epsg:3857"); + mapnik::layer lyr("layer","+init=epsg:4326"); + std::shared_ptr<mapnik::memory_datasource> ds = testing::build_geojson_ds("./test/data/poly.geojson"); + ds->set_envelope(mapnik::box2d<double>(160.147311,11.047284,160.662858,11.423830)); + lyr.set_datasource(ds); + map.add_layer(lyr); + map.zoom_to_box(bbox); + mapnik::request m_req(tile_size,tile_size,bbox); + renderer_type ren(backend,map,m_req); + ren.apply(); + CHECK( ren.painted() == true ); + CHECK(1 == tile.layers_size()); + vector_tile::Tile_Layer const& layer = tile.layers(0); + CHECK(std::string("layer") == layer.name()); + CHECK(1 == layer.features_size()); + vector_tile::Tile_Feature const& f = layer.features(0); + unsigned z = 0; + unsigned x = 0; + unsigned y = 0; + double resolution = mapnik::EARTH_CIRCUMFERENCE/(1 << z); + double tile_x = -0.5 * mapnik::EARTH_CIRCUMFERENCE + x * resolution; + double tile_y = 0.5 * mapnik::EARTH_CIRCUMFERENCE - y * resolution; + double scale = (static_cast<double>(layer.extent()) / tile_size) * tile_size/resolution; + + std::string buffer; + tile.SerializeToString(&buffer); + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf pbf_layer = pbf_tile.get_message(); + // Need to loop because they could be encoded in any order + bool found = false; + while (!found && pbf_layer.next()) { + // But there will be only one in our tile, so we'll break out of loop once we find it + if (pbf_layer.tag() == 2) { + mapbox::util::pbf pbf_feature = pbf_layer.get_message(); + while (!found && pbf_feature.next()) { + if (pbf_feature.tag() == 4) { + found = true; + std::pair< mapbox::util::pbf::const_uint32_iterator, mapbox::util::pbf::const_uint32_iterator > geom_itr = pbf_feature.packed_uint32(); + mapnik::vector_tile_impl::GeometryPBF geoms(geom_itr, tile_x,tile_y,scale,-1*scale); + auto geom = mapnik::vector_tile_impl::decode_geometry(geoms, f.type()); + + unsigned int n_err = 0; + mapnik::projection wgs84("+init=epsg:4326",true); + mapnik::projection merc("+init=epsg:3857",true); + mapnik::proj_transform prj_trans(merc,wgs84); + mapnik::geometry::geometry<double> projected_geom = mapnik::geometry::reproject_copy(geom,prj_trans,n_err); + CHECK( n_err == 0 ); + std::string geojson_string; + CHECK( mapnik::util::to_geojson(geojson_string,projected_geom) ); + CHECK( geojson_string == "{\"type\":\"Polygon\",\"coordinates\":[[[160.42359375,11.422482415387],[160.40671875,11.3976701817587],[160.396875,11.3935345987523],[160.39828125,11.4018057045895],[160.39265625,11.4004272036667],[160.38984375,11.3811274888866],[160.3940625,11.3838846711709],[160.3771875,11.3521754635814],[160.33921875,11.3590690696413],[160.35046875,11.3645838345287],[160.3575,11.3645838345287],[160.3575,11.3756130442004],[160.28859375,11.3480392200085],[160. [...] + break; + } + } + } + } + REQUIRE( found ); +} + + +TEST_CASE( "pbf raster tile output", "should be able to overzoom raster" ) { + mapnik::datasource_cache::instance().register_datasources(MAPNIK_PLUGINDIR); + typedef vector_tile::Tile tile_type; + tile_type tile; + { + typedef mapnik::vector_tile_impl::backend_pbf backend_type; + typedef mapnik::vector_tile_impl::processor<backend_type> renderer_type; + double minx,miny,maxx,maxy; + mapnik::vector_tile_impl::spherical_mercator merc(256); + merc.xyz(0,0,0,minx,miny,maxx,maxy); + mapnik::box2d<double> bbox(minx,miny,maxx,maxy); + backend_type backend(tile,16); + mapnik::Map map(256,256,"+init=epsg:3857"); + map.set_buffer_size(1024); + mapnik::layer lyr("layer",map.srs()); + mapnik::parameters params; + params["type"] = "gdal"; + std::ostringstream s; + s << std::fixed << std::setprecision(16) + << bbox.minx() << ',' << bbox.miny() << ',' + << bbox.maxx() << ',' << bbox.maxy(); + params["extent"] = s.str(); + params["file"] = "test/data/256x256.png"; + std::shared_ptr<mapnik::datasource> ds = + mapnik::datasource_cache::instance().create(params); + lyr.set_datasource(ds); + map.add_layer(lyr); + mapnik::request m_req(256,256,bbox); + m_req.set_buffer_size(map.buffer_size()); + renderer_type ren(backend,map,m_req,1.0,0,0,1,"jpeg",mapnik::SCALING_BILINEAR); + ren.apply(); + } + // Done creating test data, now test created tile + CHECK(1 == tile.layers_size()); + vector_tile::Tile_Layer const& layer = tile.layers(0); + CHECK(std::string("layer") == layer.name()); + CHECK(1 == layer.features_size()); + vector_tile::Tile_Feature const& f = layer.features(0); + CHECK(static_cast<mapnik::value_integer>(1) == static_cast<mapnik::value_integer>(f.id())); + CHECK(0 == f.geometry_size()); + CHECK(f.has_raster()); + std::string const& ras_buffer = f.raster(); + CHECK(!ras_buffer.empty()); + + // confirm tile looks correct as encoded + std::string buffer; + CHECK(tile.SerializeToString(&buffer)); + + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer2 = pbf_tile.get_message(); + + // now read back and render image at larger size + // and zoomed in + double minx,miny,maxx,maxy; + mapnik::vector_tile_impl::spherical_mercator merc(256); + // 2/0/1.png + merc.xyz(0,1,2,minx,miny,maxx,maxy); + mapnik::box2d<double> bbox(minx,miny,maxx,maxy); + mapnik::Map map2(256,256,"+init=epsg:3857"); + map2.set_buffer_size(1024); + mapnik::layer lyr2("layer",map2.srs()); + std::shared_ptr<mapnik::vector_tile_impl::tile_datasource_pbf> ds2 = std::make_shared< + mapnik::vector_tile_impl::tile_datasource_pbf>( + layer2,0,0,0,256); + lyr2.set_datasource(ds2); + lyr2.add_style("style"); + map2.add_layer(lyr2); + mapnik::load_map(map2,"test/data/raster_style.xml"); + map2.zoom_to_box(bbox); + mapnik::image_rgba8 im(map2.width(),map2.height()); + mapnik::agg_renderer<mapnik::image_rgba8> ren2(map2,im); + ren2.apply(); + if (!mapnik::util::exists("test/fixtures/expected-3.png")) { + mapnik::save_to_file(im,"test/fixtures/expected-3.png","png32"); + } + unsigned diff = testing::compare_images(im,"test/fixtures/expected-3.png"); + CHECK(0 == diff); + if (diff > 0) { + mapnik::save_to_file(im,"test/fixtures/actual-3.png","png32"); + } +} + +TEST_CASE("Check that we throw on various valid-but-we-don't-handle PBF encoded files","Should be throwing exceptions") +{ + std::vector<std::string> filenames = {"test/data/tile_with_extra_feature_field.pbf", + "test/data/tile_with_extra_layer_fields.pbf", + "test/data/tile_with_invalid_layer_value_type.pbf", + "test/data/tile_with_unexpected_geomtype.pbf"}; + + for (auto const& f : filenames) { + + CHECK_THROWS({ + std::ifstream t(f); + std::string buffer((std::istreambuf_iterator<char>(t)), + std::istreambuf_iterator<char>()); + + mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789); + unsigned tile_size = 256; + mapbox::util::pbf pbf_tile(buffer.c_str(), buffer.size()); + pbf_tile.next(); + mapbox::util::pbf layer2 = pbf_tile.get_message(); + mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0,tile_size); + mapnik::featureset_ptr fs; + mapnik::feature_ptr f_ptr; + fs = ds.features(mapnik::query(bbox)); + f_ptr = fs->next(); + while (f_ptr != mapnik::feature_ptr()) { + f_ptr = fs->next(); + } + }); + } + +} -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/mapnik-vector-tile.git _______________________________________________ Pkg-grass-devel mailing list Pkg-grass-devel@lists.alioth.debian.org http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel