From 64b874ecb13e2458835b0629a6feeee2d13981ff Mon Sep 17 00:00:00 2001 From: Jiri Drbalek Date: Sun, 8 Oct 2017 14:32:14 +0000 Subject: [PATCH] New interior algorithm --- include/mapnik/geom_util.hpp | 74 ------ include/mapnik/geometry/interior.hpp | 35 +++ .../geometry/polygon_vertex_processor.hpp | 71 ++++++ include/mapnik/markers_placements/basic.hpp | 1 + .../mapnik/markers_placements/interior.hpp | 14 +- .../process_point_symbolizer.hpp | 5 +- src/build.py | 1 + src/geometry/interior.cpp | 234 ++++++++++++++++++ .../render_markers_symbolizer.cpp | 3 +- src/text/symbolizer_helpers.cpp | 18 +- 10 files changed, 370 insertions(+), 86 deletions(-) create mode 100644 include/mapnik/geometry/interior.hpp create mode 100644 include/mapnik/geometry/polygon_vertex_processor.hpp create mode 100644 src/geometry/interior.cpp diff --git a/include/mapnik/geom_util.hpp b/include/mapnik/geom_util.hpp index d16c42eee..32283601f 100644 --- a/include/mapnik/geom_util.hpp +++ b/include/mapnik/geom_util.hpp @@ -518,80 +518,6 @@ bool hit_test(PathType & path, double x, double y, double tol) return inside; } -template -bool interior_position(PathType & path, double & x, double & y) -{ - // start with the centroid - if (!label::centroid(path, x,y)) - return false; - - // otherwise we find a horizontal line across the polygon and then return the - // center of the widest intersection between the polygon and the line. - - std::vector intersections; // only need to store the X as we know the y - geometry::point p0, p1, move_to; - unsigned command = SEG_END; - - path.rewind(0); - - while (SEG_END != (command = path.vertex(&p0.x, &p0.y))) - { - switch (command) - { - case SEG_MOVETO: - move_to = p0; - break; - case SEG_CLOSE: - p0 = move_to; - case SEG_LINETO: - // if the segments overlap - if (p0.y == p1.y) - { - if (p0.y == y) - { - double xi = (p0.x + p1.x) / 2.0; - intersections.push_back(xi); - } - } - // if the path segment crosses the bisector - else if ((p0.y <= y && p1.y >= y) || - (p0.y >= y && p1.y <= y)) - { - // then calculate the intersection - double xi = p0.x; - if (p0.x != p1.x) - { - double m = (p1.y - p0.y) / (p1.x - p0.x); - double c = p0.y - m * p0.x; - xi = (y - c) / m; - } - - intersections.push_back(xi); - } - break; - } - p1 = p0; - } - - // no intersections we just return the default - if (intersections.empty()) - return true; - std::sort(intersections.begin(), intersections.end()); - double max_width = 0; - for (unsigned ii = 1; ii < intersections.size(); ii += 2) - { - double xlow = intersections[ii-1]; - double xhigh = intersections[ii]; - double width = xhigh - xlow; - if (width > max_width) - { - x = (xlow + xhigh) / 2.0; - max_width = width; - } - } - return true; -} - }} #endif // MAPNIK_GEOM_UTIL_HPP diff --git a/include/mapnik/geometry/interior.hpp b/include/mapnik/geometry/interior.hpp new file mode 100644 index 000000000..50378d163 --- /dev/null +++ b/include/mapnik/geometry/interior.hpp @@ -0,0 +1,35 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2017 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_GEOMETRY_INTERIOR_HPP +#define MAPNIK_GEOMETRY_INTERIOR_HPP + +#include + +namespace mapnik { namespace geometry { + +template +point interior(polygon const& polygon, double scale_factor); + +} } + +#endif // MAPNIK_GEOMETRY_INTERIOR_HPP diff --git a/include/mapnik/geometry/polygon_vertex_processor.hpp b/include/mapnik/geometry/polygon_vertex_processor.hpp new file mode 100644 index 000000000..46fe0cdcc --- /dev/null +++ b/include/mapnik/geometry/polygon_vertex_processor.hpp @@ -0,0 +1,71 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2017 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_GEOMETRY_POLYGON_VERTEX_PROCESSOR_HPP +#define MAPNIK_GEOMETRY_POLYGON_VERTEX_PROCESSOR_HPP + +// geometry +#include + +namespace mapnik { namespace geometry { + +template +struct polygon_vertex_processor +{ + template + void add_path(Path & path) + { + point p; + unsigned cmd; + linear_ring ring; + bool exterior = true; + while ((cmd = path.vertex(&p.x, &p.y)) != SEG_END) + { + switch (cmd) + { + case SEG_MOVETO: + case SEG_LINETO: + ring.emplace_back(p); + break; + case SEG_CLOSE: + ring.emplace_back(ring.front()); + if (exterior) + { + polygon_.exterior_ring = std::move(ring); + exterior = false; + } + else + { + polygon_.interior_rings.emplace_back(std::move(ring)); + } + ring = linear_ring(); + break; + } + } + } + + polygon polygon_; +}; + +} } + +#endif // MAPNIK_GEOMETRY_POLYGON_VERTEX_PROCESSOR_HPP diff --git a/include/mapnik/markers_placements/basic.hpp b/include/mapnik/markers_placements/basic.hpp index abcef5ea6..de3e18e10 100644 --- a/include/mapnik/markers_placements/basic.hpp +++ b/include/mapnik/markers_placements/basic.hpp @@ -46,6 +46,7 @@ struct markers_placement_params bool allow_overlap; bool avoid_edges; direction_enum direction; + double scale_factor; }; class markers_basic_placement : util::noncopyable diff --git a/include/mapnik/markers_placements/interior.hpp b/include/mapnik/markers_placements/interior.hpp index 6a2705e15..e0bbd69a1 100644 --- a/include/mapnik/markers_placements/interior.hpp +++ b/include/mapnik/markers_placements/interior.hpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include namespace mapnik { @@ -58,11 +60,13 @@ public: } else { - if (!label::interior_position(this->locator_, x, y)) - { - this->done_ = true; - return false; - } + geometry::polygon_vertex_processor vertex_processor; + vertex_processor.add_path(this->locator_); + geometry::point placement = geometry::interior(vertex_processor.polygon_, + this->params_.scale_factor); + + x = placement.x; + y = placement.y; } angle = 0; diff --git a/include/mapnik/renderer_common/process_point_symbolizer.hpp b/include/mapnik/renderer_common/process_point_symbolizer.hpp index ef0863fd6..fda6f8efc 100644 --- a/include/mapnik/renderer_common/process_point_symbolizer.hpp +++ b/include/mapnik/renderer_common/process_point_symbolizer.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -79,9 +80,7 @@ void render_point_symbolizer(point_symbolizer const &sym, else if (type == mapnik::geometry::geometry_types::Polygon) { auto const& poly = mapnik::util::get >(geometry); - geometry::polygon_vertex_adapter va(poly); - if (!label::interior_position(va ,pt.x, pt.y)) - return; + pt = geometry::interior(poly, common.scale_factor_); } else { diff --git a/src/build.py b/src/build.py index c5889dccf..009f4d0de 100644 --- a/src/build.py +++ b/src/build.py @@ -169,6 +169,7 @@ source = Split( datasource_cache_static.cpp debug.cpp geometry_reprojection.cpp + geometry/interior.cpp expression_node.cpp expression_string.cpp expression.cpp diff --git a/src/geometry/interior.cpp b/src/geometry/interior.cpp new file mode 100644 index 000000000..ceab6ae1a --- /dev/null +++ b/src/geometry/interior.cpp @@ -0,0 +1,234 @@ +/***************************************************************************** + * + * This file is part of Mapnik (c++ mapping toolkit) + * + * Copyright (C) 2017 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 + * + *****************************************************************************/ + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mapnik { namespace geometry { + +// Interior algorithm is realized as a modification of Polylabel algorithm +// from https://github.com/mapbox/polylabel. +// The modification aims to improve visual output by prefering +// placements closer to centroid. + +namespace detail { + +// get squared distance from a point to a segment +template +T segment_dist_sq(const point& p, + const point& a, + const point& b) +{ + auto x = a.x; + auto y = a.y; + auto dx = b.x - x; + auto dy = b.y - y; + + if (dx != 0 || dy != 0) { + + auto t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy); + + if (t > 1) { + x = b.x; + y = b.y; + + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.x - x; + dy = p.y - y; + + return dx * dx + dy * dy; +} + +template +void point_to_ring_dist(point const& point, linear_ring const& ring, + bool & inside, double & min_dist_sq) +{ + for (std::size_t i = 0, len = ring.size(), j = len - 1; i < len; j = i++) + { + const auto& a = ring[i]; + const auto& b = ring[j]; + + if ((a.y > point.y) != (b.y > point.y) && + (point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x)) inside = !inside; + + min_dist_sq = std::min(min_dist_sq, segment_dist_sq(point, a, b)); + } +} + +// signed distance from point to polygon outline (negative if point is outside) +template +double point_to_polygon_dist(const point& point, const polygon& polygon) +{ + bool inside = false; + double min_dist_sq = std::numeric_limits::infinity(); + + point_to_ring_dist(point, polygon.exterior_ring, inside, min_dist_sq); + + for (const auto& ring : polygon.interior_rings) + { + point_to_ring_dist(point, ring, inside, min_dist_sq); + } + + return (inside ? 1 : -1) * std::sqrt(min_dist_sq); +} + +template +struct fitness_functor +{ + fitness_functor(point const& centroid, point const& polygon_size) + : centroid(centroid), + max_size(std::max(polygon_size.x, polygon_size.y)) + {} + + T operator()(point const& cell_center, T distance_polygon) const + { + if (distance_polygon <= 0) + { + return distance_polygon; + } + point d(cell_center.x - centroid.x, cell_center.y - centroid.y); + double distance_centroid = std::sqrt(d.x * d.x + d.y * d.y); + return distance_polygon * (1 - distance_centroid / max_size); + } + + point centroid; + T max_size; +}; + +template +struct cell +{ + template + cell(const point& c_, T h_, + const polygon& polygon, + const FitnessFunc& ff) + : c(c_), + h(h_), + d(point_to_polygon_dist(c, polygon)), + fitness(ff(c, d)), + max_fitness(ff(c, d + h * std::sqrt(2))) + {} + + point c; // cell center + T h; // half the cell size + T d; // distance from cell center to polygon + T fitness; // fitness of the cell center + T max_fitness; // a "potential" of the cell calculated from max distance to polygon within the cell +}; + +template +point polylabel(const polygon& polygon, T precision = 1) +{ + // find the bounding box of the outer ring + const box2d bbox = envelope(polygon.exterior_ring); + const point size { bbox.width(), bbox.height() }; + + const T cell_size = std::min(size.x, size.y); + T h = cell_size / 2; + + // a priority queue of cells in order of their "potential" (max distance to polygon) + auto compare_func = [] (const cell& a, const cell& b) + { + return a.max_fitness < b.max_fitness; + }; + using Queue = std::priority_queue, std::vector>, decltype(compare_func)>; + Queue queue(compare_func); + + if (cell_size == 0) + { + return { bbox.minx(), bbox.miny() }; + } + + point centroid; + if (!mapnik::geometry::centroid(polygon, centroid)) + { + auto center = bbox.center(); + return { center.x, center.y }; + } + + fitness_functor fitness_func(centroid, size); + + // cover polygon with initial cells + for (T x = bbox.minx(); x < bbox.maxx(); x += cell_size) + { + for (T y = bbox.miny(); y < bbox.maxy(); y += cell_size) + { + queue.push(cell({x + h, y + h}, h, polygon, fitness_func)); + } + } + + // take centroid as the first best guess + auto best_cell = cell(centroid, 0, polygon, fitness_func); + + while (!queue.empty()) + { + // pick the most promising cell from the queue + auto current_cell = queue.top(); + queue.pop(); + + // update the best cell if we found a better one + if (current_cell.fitness > best_cell.fitness) + { + best_cell = current_cell; + } + + // do not drill down further if there's no chance of a better solution + if (current_cell.max_fitness - best_cell.fitness <= precision) continue; + + // split the cell into four cells + h = current_cell.h / 2; + queue.push(cell({current_cell.c.x - h, current_cell.c.y - h}, h, polygon, fitness_func)); + queue.push(cell({current_cell.c.x + h, current_cell.c.y - h}, h, polygon, fitness_func)); + queue.push(cell({current_cell.c.x - h, current_cell.c.y + h}, h, polygon, fitness_func)); + queue.push(cell({current_cell.c.x + h, current_cell.c.y + h}, h, polygon, fitness_func)); + } + + return best_cell.c; +} + +} // namespace detail + +template +point interior(polygon const& polygon, double scale_factor) +{ + // This precision has been chosen to work well in the map (viewport) coordinates. + double precision = 10.0 * scale_factor; + return detail::polylabel(polygon, precision); +} + +template +point interior(polygon const& polygon, double scale_factor); + +} } + diff --git a/src/renderer_common/render_markers_symbolizer.cpp b/src/renderer_common/render_markers_symbolizer.cpp index 6706cb1cd..16b863c9c 100644 --- a/src/renderer_common/render_markers_symbolizer.cpp +++ b/src/renderer_common/render_markers_symbolizer.cpp @@ -228,7 +228,8 @@ markers_dispatch_params::markers_dispatch_params(box2d const& size, get(sym, feature, vars), get(sym, feature, vars), get(sym, feature, vars), - get(sym, feature, vars)} + get(sym, feature, vars), + scale} , placement_method(get(sym, feature, vars)) , ignore_placement(get(sym, feature, vars)) , snap_to_pixels(snap) diff --git a/src/text/symbolizer_helpers.cpp b/src/text/symbolizer_helpers.cpp index 8064e6c6f..f77707547 100644 --- a/src/text/symbolizer_helpers.cpp +++ b/src/text/symbolizer_helpers.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,10 @@ #include #include #include +#include +#include +#include +#include namespace mapnik { namespace geometry { @@ -288,9 +293,16 @@ void base_symbolizer_helper::initialize_points() const } else if (how_placed == INTERIOR_PLACEMENT && type == geometry::geometry_types::Polygon) { - auto const& poly = mapnik::util::get >(geom); - geometry::polygon_vertex_adapter va(poly); - success = label::interior_position(va, label_x, label_y); + auto const& poly = util::get>(geom); + proj_transform backwart_transform(prj_trans_.dest(), prj_trans_.source()); + view_strategy vs(t_); + proj_strategy ps(backwart_transform); + using transform_group_type = geometry::strategy_group; + transform_group_type transform_group(ps, vs); + geometry::polygon tranformed_poly(geometry::transform(poly, transform_group)); + geometry::point pt = geometry::interior(tranformed_poly, scale_factor_); + points_.emplace_back(pt.x, pt.y); + continue; } else {