Add grid placement for text and shield symbolizer

This commit is contained in:
Jiri Drbalek 2015-05-22 14:12:50 +00:00
parent 1bb070c842
commit 098fd27291
9 changed files with 493 additions and 21 deletions

View file

@ -0,0 +1,252 @@
/*****************************************************************************
*
* This file is part of Mapnik (c++ mapping toolkit)
*
* Copyright (C) 2015 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_GRID_ADAPTERS_HPP
#define MAPNIK_GRID_ADAPTERS_HPP
// mapnik
#include <mapnik/vertex.hpp>
#include <mapnik/image.hpp>
#include <mapnik/image_util.hpp>
#include <mapnik/geom_util.hpp>
#include <mapnik/geometry/polygon_vertex_processor.hpp>
#include <mapnik/geometry_envelope.hpp>
#include <mapnik/geometry/interior.hpp>
#include <mapnik/view_strategy.hpp>
#include <mapnik/vertex_adapters.hpp>
// agg
#include "agg_rendering_buffer.h"
#include "agg_pixfmt_gray.h"
#include "agg_renderer_base.h"
#include "agg_renderer_scanline.h"
#include "agg_rasterizer_scanline_aa.h"
#include "agg_scanline_bin.h"
#include "agg_conv_transform.h"
namespace mapnik { namespace geometry {
// Generates integer coordinates of a spiral similar to the Ulam spiral
// around [0, 0], bounded by size.
class spiral_iterator
{
public:
spiral_iterator(unsigned size)
: end_(size * size),
i_(0),
x_(0), y_(0)
{
}
bool vertex(int * x, int * y)
{
if (i_ < end_)
{
*x = x_;
*y = y_;
if (std::abs(x_) <= std::abs(y_) && (x_ != y_ || x_ >= 0))
{
x_ += ((y_ >= 0) ? 1 : -1);
}
else
{
y_ += ((x_ >= 0) ? -1 : 1);
}
++i_;
return true;
}
return false;
}
void rewind()
{
i_ = x_ = y_ = 0;
}
private:
const unsigned end_;
unsigned i_;
int x_, y_;
};
struct view_transform_agg_adapter
{
void transform(double * x, double * y) const
{
vt.forward(x, y);
}
view_transform const& vt;
};
// Generates grid of points laying inside a polygon.
template <typename PathType, typename T, bool Alternating = false>
struct grid_vertex_converter
{
grid_vertex_converter(PathType & path, T dx, T dy, double scale_factor)
: grid_vertex_converter(cache_path(path), dx, dy, scale_factor)
{
}
void rewind(unsigned)
{
si_.rewind();
}
unsigned vertex(T * x, T * y)
{
int spiral_x, spiral_y;
while (si_.vertex(&spiral_x, &spiral_y))
{
T pix_x = interior_.x + spiral_x * dx_;
T pix_y = interior_.y + spiral_y * dy_;
if (Alternating && spiral_y % 2 != 0)
{
// Every odd line is shifted by dx/2.
pix_x += this->dx_ / 2.0;
}
if (pix_x >= 0 && static_cast<std::size_t>(pix_x) < hit_bitmap_.width() &&
pix_y >= 0 && static_cast<std::size_t>(pix_y) < hit_bitmap_.height() &&
get_pixel<image_gray8::pixel_type>(hit_bitmap_, pix_x, pix_y))
{
*x = pix_x;
*y = pix_y;
vt_.backward(x, y);
return mapnik::SEG_MOVETO;
}
}
return mapnik::SEG_END;
}
geometry_types type() const
{
return geometry_types::MultiPoint;
}
private:
grid_vertex_converter(polygon<T> const& poly, T dx, T dy, double scale_factor)
: grid_vertex_converter(poly, dx, dy, scale_factor, mapnik::geometry::envelope(poly))
{
}
grid_vertex_converter(polygon<T> const& poly, T dx, T dy, double scale_factor, box2d<T> const& envelope)
: hit_bitmap_scale_(get_hit_bitmap_scale(envelope)),
dx_(dx * hit_bitmap_scale_),
dy_(dy * hit_bitmap_scale_),
vt_(envelope.valid() ? (envelope.width() * hit_bitmap_scale_) : 0,
envelope.valid() ? (envelope.height() * hit_bitmap_scale_) : 0, envelope),
hit_bitmap_(create_hit_bitmap(poly)),
interior_(interior(poly, envelope, scale_factor)),
si_(std::max(std::ceil((hit_bitmap_.width() + std::abs((hit_bitmap_.width() / 2.0) - interior_.x) * 2.0) / dx_),
std::ceil((hit_bitmap_.height() + std::abs((hit_bitmap_.height() / 2.0) - interior_.y) * 2.0) / dy_)))
{
}
double get_hit_bitmap_scale(box2d<T> const& envelope) const
{
T size = envelope.width() * envelope.height();
// Polygon with huge area can lead to excessive memory allocation.
// This is more or less arbitrarily chosen limit for the maximum bitmap resolution.
// Bitmap bigger than this limit is scaled down to fit into this resolution.
const std::size_t max_size = 8192 * 8192;
if (size > max_size)
{
return std::sqrt(max_size / size);
}
return 1;
}
// The polygon is rendered to a bitmap for fast hit-testing.
image_gray8 create_hit_bitmap(polygon<T> const& poly) const
{
polygon_vertex_adapter<T> va(poly);
view_transform_agg_adapter vta{ vt_ };
agg::conv_transform<polygon_vertex_adapter<T>, view_transform_agg_adapter> tp(va, vta);
tp.rewind(0);
agg::rasterizer_scanline_aa<> ras;
ras.add_path(tp);
image_gray8 hit_bitmap(vt_.width(), vt_.height());
agg::rendering_buffer buf(hit_bitmap.data(),
hit_bitmap.width(),
hit_bitmap.height(),
hit_bitmap.row_size());
agg::pixfmt_gray8 pixfmt(buf);
using renderer_base = agg::renderer_base<agg::pixfmt_gray8>;
using renderer_bin = agg::renderer_scanline_bin_solid<renderer_base>;
renderer_base rb(pixfmt);
renderer_bin ren_bin(rb);
ren_bin.color(agg::gray8(1));
agg::scanline_bin sl_bin;
agg::render_scanlines(ras, sl_bin, ren_bin);
return hit_bitmap;
}
mapnik::geometry::point<T> interior(polygon<T> const& poly,
box2d<T> const& envelope,
double scale_factor) const
{
mapnik::geometry::point<T> interior;
if (!mapnik::geometry::interior(poly, scale_factor, interior))
{
auto center = envelope.center();
interior.x = center.x;
interior.y = center.y;
}
vt_.forward(&interior.x, &interior.y);
return interior;
}
polygon<T> cache_path(PathType & path) const
{
mapnik::geometry::polygon_vertex_processor<T> vertex_processor;
path.rewind(0);
vertex_processor.add_path(path);
return vertex_processor.polygon_;
}
const double hit_bitmap_scale_;
const T dx_, dy_;
const view_transform vt_;
const image_gray8 hit_bitmap_;
const mapnik::geometry::point<T> interior_;
spiral_iterator si_;
};
template <typename PathType, typename T>
using regular_grid_vertex_converter = grid_vertex_converter<PathType, T, false>;
template <typename PathType, typename T>
using alternating_grid_vertex_converter = grid_vertex_converter<PathType, T, true>;
}
}
#endif //MAPNIK_GRID_ADAPTERS_HPP

View file

@ -138,6 +138,8 @@ enum label_placement_enum : std::uint8_t
LINE_PLACEMENT,
VERTEX_PLACEMENT,
INTERIOR_PLACEMENT,
GRID_PLACEMENT,
ALTERNATING_GRID_PLACEMENT,
label_placement_enum_MAX
};

View file

@ -59,7 +59,13 @@ struct placement_finder_adapter
};
using vertex_converter_type = vertex_converter<clip_line_tag, transform_tag, affine_transform_tag, simplify_tag, smooth_tag>;
using vertex_converter_type = vertex_converter<clip_line_tag,
clip_poly_tag,
transform_tag,
affine_transform_tag,
simplify_tag,
smooth_tag,
offset_transform_tag>;
class base_symbolizer_helper
{
@ -147,6 +153,10 @@ public:
// Return all placements.
placements_list const& get() const;
protected:
void init_converters();
void initialize_points() const;
template <template <typename, typename> typename GridAdapter>
void initialize_grid_points() const;
bool next_point_placement() const;
bool next_line_placement() const;

View file

@ -80,6 +80,8 @@ struct evaluated_text_properties : util::noncopyable
bool allow_overlap;
bool largest_bbox_only;
text_upright_e upright;
double grid_cell_width;
double grid_cell_height;
};
}
@ -172,6 +174,8 @@ struct text_properties_expressions
symbolizer_base::value_type allow_overlap = false;
symbolizer_base::value_type largest_bbox_only = true;
symbolizer_base::value_type upright = enumeration_wrapper(UPRIGHT_AUTO);
symbolizer_base::value_type grid_cell_width = 0.0;
symbolizer_base::value_type grid_cell_height = 0.0;
};
// Contains all text symbolizer properties which are not directly related to text formatting and layout.

View file

@ -1134,6 +1134,7 @@ void map_parser::parse_text_symbolizer(rule & rule, xml_node const& node)
set_symbolizer_property<symbolizer_base,composite_mode_e>(sym, keys::halo_comp_op, node);
set_symbolizer_property<symbolizer_base,halo_rasterizer_enum>(sym, keys::halo_rasterizer, node);
set_symbolizer_property<symbolizer_base,transform_type>(sym, keys::halo_transform, node);
set_symbolizer_property<symbolizer_base,value_double>(sym, keys::offset, node);
rule.append(std::move(sym));
}
}
@ -1175,6 +1176,7 @@ void map_parser::parse_shield_symbolizer(rule & rule, xml_node const& node)
set_symbolizer_property<symbolizer_base,double>(sym, keys::shield_dy, node);
set_symbolizer_property<symbolizer_base,double>(sym, keys::opacity, node);
set_symbolizer_property<symbolizer_base,value_bool>(sym, keys::unlock_image, node);
set_symbolizer_property<symbolizer_base,value_double>(sym, keys::offset, node);
std::string file = node.get_attr<std::string>("file");
if (file.empty())

View file

@ -116,6 +116,8 @@ static const char * label_placement_strings[] = {
"line",
"vertex",
"interior",
"grid",
"alternating-grid",
""
};

View file

@ -33,7 +33,6 @@
#include <mapnik/geometry_centroid.hpp>
#include <mapnik/geometry/interior.hpp>
#include <mapnik/vertex_processor.hpp>
#include <mapnik/geom_util.hpp>
#include <mapnik/parse_path.hpp>
#include <mapnik/debug.hpp>
#include <mapnik/symbolizer.hpp>
@ -43,6 +42,7 @@
#include <mapnik/text/placements/dummy.hpp>
#include <mapnik/geometry_transform.hpp>
#include <mapnik/geometry_strategy.hpp>
#include <mapnik/grid_vertex_converter.hpp>
#include <mapnik/proj_strategy.hpp>
#include <mapnik/view_strategy.hpp>
@ -108,6 +108,31 @@ struct apply_vertex_placement
proj_transform const& prj_trans_;
};
template <template <typename, typename> typename GridAdapter, typename T, typename Points>
struct grid_placement_finder_adapter
{
grid_placement_finder_adapter(T dx, T dy, Points & points, double scale_factor)
: dx_(dx), dy_(dy),
points_(points),
scale_factor_(scale_factor) {}
template <typename PathT>
void add_path(PathT & path) const
{
GridAdapter<PathT, T> gpa(path, dx_, dy_, scale_factor_);
gpa.rewind(0);
double label_x, label_y;
for (unsigned cmd; (cmd = gpa.vertex(&label_x, &label_y)) != SEG_END; )
{
points_.emplace_back(label_x, label_y);
}
}
T dx_, dy_;
Points & points_;
double scale_factor_;
};
template <typename T>
struct split_multi_geometries
{
@ -244,13 +269,19 @@ void base_symbolizer_helper::initialize_geometries() const
void base_symbolizer_helper::initialize_points() const
{
label_placement_enum how_placed = text_props_->label_placement;
if (how_placed == LINE_PLACEMENT)
switch (how_placed)
{
case LINE_PLACEMENT:
point_placement_ = false;
return;
}
else
{
case GRID_PLACEMENT:
case ALTERNATING_GRID_PLACEMENT:
point_placement_ = true;
// Points for grid placement are generated in text_symbolizer_helper
// because base_symbolizer_helper doesn't have the vertex converter.
return;
default:
point_placement_ = true;
}
@ -337,19 +368,40 @@ text_symbolizer_helper::text_symbolizer_helper(
adapter_(finder_,false),
converter_(query_extent_, sym_, t, prj_trans, affine_trans, feature, vars, scale_factor)
{
init_converters();
if (geometries_to_process_.size())
{
text_symbolizer_helper::initialize_points();
finder_.next_position();
}
}
void text_symbolizer_helper::init_converters()
{
// setup vertex converter
value_bool clip = mapnik::get<value_bool, keys::clip>(sym_, feature_, vars_);
value_double simplify_tolerance = mapnik::get<value_double, keys::simplify_tolerance>(sym_, feature_, vars_);
value_double smooth = mapnik::get<value_double, keys::smooth>(sym_, feature_, vars_);
boost::optional<value_double> offset = get_optional<value_double>(sym_, keys::offset, feature_, vars_);
if (clip) converter_.template set<clip_line_tag>();
if (clip)
{
label_placement_enum how_placed = text_props_->label_placement;
if (how_placed == GRID_PLACEMENT || how_placed == ALTERNATING_GRID_PLACEMENT)
{
converter_.template set<clip_poly_tag>();
}
else
{
converter_.template set<clip_line_tag>();
}
}
converter_.template set<transform_tag>(); //always transform
converter_.template set<affine_transform_tag>();
if (simplify_tolerance > 0.0) converter_.template set<simplify_tag>(); // optional simplify converter
if (smooth > 0.0) converter_.template set<smooth_tag>(); // optional smooth converter
if (geometries_to_process_.size()) finder_.next_position();
if (offset) converter_.template set<offset_transform_tag>(); // optional offset converter
}
placements_list const& text_symbolizer_helper::get() const
@ -463,18 +515,11 @@ text_symbolizer_helper::text_symbolizer_helper(
adapter_(finder_,true),
converter_(query_extent_, sym_, t, prj_trans, affine_trans, feature, vars, scale_factor)
{
// setup vertex converter
value_bool clip = mapnik::get<value_bool, keys::clip>(sym_, feature_, vars_);
value_double simplify_tolerance = mapnik::get<value_double, keys::simplify_tolerance>(sym_, feature_, vars_);
value_double smooth = mapnik::get<value_double, keys::smooth>(sym_, feature_, vars_);
init_converters();
if (clip) converter_.template set<clip_line_tag>();
converter_.template set<transform_tag>(); //always transform
converter_.template set<affine_transform_tag>();
if (simplify_tolerance > 0.0) converter_.template set<simplify_tag>(); // optional simplify converter
if (smooth > 0.0) converter_.template set<smooth_tag>(); // optional smooth converter
if (geometries_to_process_.size())
{
text_symbolizer_helper::initialize_points();
init_marker();
finder_.next_position();
}
@ -515,6 +560,44 @@ void text_symbolizer_helper::init_marker() const
finder_.set_marker(std::make_shared<marker_info>(marker, trans), bbox, unlock_image, marker_displacement);
}
template <template <typename, typename> typename GridAdapter>
void text_symbolizer_helper::initialize_grid_points() const
{
for (auto const& geom : geometries_to_process_)
{
auto type = geometry::geometry_type(geom);
if (type != geometry::geometry_types::Polygon)
{
continue;
}
using adapter_type = detail::grid_placement_finder_adapter<
GridAdapter, double, std::list<pixel_position>>;
adapter_type ga(text_props_->grid_cell_width,
text_props_->grid_cell_height,
points_,
scale_factor_);
auto const& poly = mapnik::util::get<geometry::polygon<double>>(geom);
geometry::polygon_vertex_adapter<double> va(poly);
converter_.apply(va, ga);
}
}
void text_symbolizer_helper::initialize_points() const
{
label_placement_enum how_placed = text_props_->label_placement;
if (how_placed == GRID_PLACEMENT)
{
initialize_grid_points<geometry::regular_grid_vertex_converter>();
}
else if (how_placed == ALTERNATING_GRID_PLACEMENT)
{
initialize_grid_points<geometry::alternating_grid_vertex_converter>();
}
point_itr_ = points_.begin();
}
template text_symbolizer_helper::text_symbolizer_helper(
text_symbolizer const& sym,
feature_impl const& feature,

View file

@ -61,6 +61,8 @@ evaluated_text_properties_ptr evaluate_text_properties(text_symbolizer_propertie
prop->allow_overlap = util::apply_visitor(extract_value<value_bool>(feature,attrs), text_prop.expressions.allow_overlap);
prop->largest_bbox_only = util::apply_visitor(extract_value<value_bool>(feature,attrs), text_prop.expressions.largest_bbox_only);
prop->upright = util::apply_visitor(extract_value<text_upright_enum>(feature,attrs), text_prop.expressions.upright);
prop->grid_cell_width = util::apply_visitor(extract_value<value_double>(feature,attrs), text_prop.expressions.grid_cell_width);
prop->grid_cell_height = util::apply_visitor(extract_value<value_double>(feature,attrs), text_prop.expressions.grid_cell_height);
return prop;
}
@ -108,6 +110,8 @@ void text_symbolizer_properties::text_properties_from_xml(xml_node const& node)
set_property_from_xml<value_bool>(expressions.largest_bbox_only, "largest-bbox-only", node);
set_property_from_xml<value_double>(expressions.max_char_angle_delta, "max-char-angle-delta", node);
set_property_from_xml<text_upright_e>(expressions.upright, "upright", node);
set_property_from_xml<value_double>(expressions.grid_cell_width, "grid-cell-width", node);
set_property_from_xml<value_double>(expressions.grid_cell_height, "grid-cell-height", node);
}
void text_symbolizer_properties::from_xml(xml_node const& node, fontset_map const& fontsets, bool is_shield)
@ -175,6 +179,14 @@ void text_symbolizer_properties::to_xml(boost::property_tree::ptree &node,
{
serialize_property("upright", expressions.upright, node);
}
if (!(expressions.grid_cell_width == dfl.expressions.grid_cell_width) || explicit_defaults)
{
serialize_property("grid-cell-width", expressions.grid_cell_width, node);
}
if (!(expressions.grid_cell_height == dfl.expressions.grid_cell_height) || explicit_defaults)
{
serialize_property("grid-cell-height", expressions.grid_cell_height, node);
}
layout_defaults.to_xml(node, explicit_defaults, dfl.layout_defaults);
format_defaults.to_xml(node, explicit_defaults, dfl.format_defaults);
@ -197,6 +209,8 @@ void text_symbolizer_properties::add_expressions(expression_set & output) const
if (is_expression(expressions.allow_overlap)) output.insert(util::get<expression_ptr>(expressions.allow_overlap));
if (is_expression(expressions.largest_bbox_only)) output.insert(util::get<expression_ptr>(expressions.largest_bbox_only));
if (is_expression(expressions.upright)) output.insert(util::get<expression_ptr>(expressions.upright));
if (is_expression(expressions.grid_cell_width)) output.insert(util::get<expression_ptr>(expressions.grid_cell_width));
if (is_expression(expressions.grid_cell_height)) output.insert(util::get<expression_ptr>(expressions.grid_cell_height));
layout_defaults.add_expressions(output);
format_defaults.add_expressions(output);

View file

@ -0,0 +1,103 @@
#include "catch.hpp"
#include <mapnik/grid_vertex_converter.hpp>
TEST_CASE("spiral_iterator") {
SECTION("sprial 3x3") {
mapnik::geometry::spiral_iterator si(3);
const mapnik::geometry::point<int> points[] = {
{ 0, 0 }, { 1, 0 }, { 1, -1 },
{ 0, -1 }, { -1, -1 }, { -1, 0 },
{ -1, 1 }, { 0, 1 }, { 1, 1 } };
const std::size_t points_size = std::extent<decltype(points)>::value;
int x, y;
std::size_t index = 0;
while (si.vertex(&x, &y))
{
REQUIRE(index < points_size);
CHECK(x == points[index].x);
CHECK(y == points[index].y);
index++;
}
CHECK(index == points_size);
}
}
TEST_CASE("grid_vertex_converter") {
SECTION("empty polygon") {
mapnik::geometry::polygon<double> poly;
using path_type = mapnik::geometry::polygon_vertex_adapter<double>;
path_type path(poly);
using converter_type = mapnik::geometry::grid_vertex_converter<path_type, double>;
converter_type gvc(path, 10.0, 10.0, 1.0);
double x, y;
unsigned cmd = gvc.vertex(&x, &y);
CHECK(cmd == mapnik::SEG_END);
}
SECTION("grid of a square") {
mapnik::geometry::polygon<double> poly;
auto & exterior_ring = poly.exterior_ring;
exterior_ring.emplace_back(-10, -10);
exterior_ring.emplace_back( 10, -10);
exterior_ring.emplace_back( 10, 10);
exterior_ring.emplace_back(-10, 10);
exterior_ring.emplace_back(-10, -10);
using path_type = mapnik::geometry::polygon_vertex_adapter<double>;
path_type path(poly);
using converter_type = mapnik::geometry::grid_vertex_converter<path_type, double>;
converter_type gvc(path, 3.0, 3.0, 1.0);
const mapnik::geometry::point<double> points[] = {
{ 0, 0 }, { 3, 0 }, { 3, 3 }, { 0, 3 },
{ -3, 3 }, { -3, 0 }, { -3, -3 }, { 0, -3 },
{ 3, -3 }, { 6, -3 }, { 6, 0 }, { 6, 3 },
{ 6, 6 }, { 3, 6 }, { 0, 6 }, { -3, 6 },
{ -6, 6 }, { -6, 3 }, { -6, 0 }, { -6, -3 },
{ -6, -6 }, { -3, -6 }, { 0, -6 }, { 3, -6 },
{ 6, -6 }, { 9, -6 }, { 9, -3 }, { 9, 0 },
{ 9, 3 }, { 9, 6 }, { 9, 9 }, { 6, 9 },
{ 3, 9 }, { 0, 9 }, { -3, 9 }, { -6, 9 },
{ -9, 9 }, { -9, 6 }, { -9, 3 }, { -9, 0 },
{ -9, -3 }, { -9, -6 }, { -9, -9 }, { -6, -9 },
{ -3, -9 }, { 0, -9 }, { 3, -9 }, { 6, -9 },
{ 9, -9 } };
const std::size_t points_size = std::extent<decltype(points)>::value;
double x, y;
unsigned cmd = mapnik::SEG_END;
std::size_t index = 0;
while ((cmd = gvc.vertex(&x, &y)) != mapnik::SEG_END)
{
REQUIRE(index < points_size);
CHECK(cmd == mapnik::SEG_MOVETO);
CHECK(x == Approx(points[index].x));
CHECK(y == Approx(points[index].y));
index++;
}
CHECK(index == points_size);
CHECK(cmd == mapnik::SEG_END);
}
}