From f52a0fa4f6a7bc893f20a066bb35886be71e35ca Mon Sep 17 00:00:00 2001 From: Jiri Drbalek Date: Wed, 29 Nov 2017 14:25:18 +0000 Subject: [PATCH] new interior algorithm --- include/mapnik/geom_util.hpp | 74 ------ include/mapnik/geometry/interior.hpp | 36 +++ .../mapnik/markers_placements/interior.hpp | 14 +- .../process_point_symbolizer.hpp | 5 +- src/build.py | 1 + src/geometry/interior.cpp | 226 ++++++++++++++++++ .../render_markers_symbolizer.cpp | 3 +- src/text/symbolizer_helpers.cpp | 8 +- .../marker_placement_vertex_last.cpp | 8 +- 9 files changed, 283 insertions(+), 92 deletions(-) create mode 100644 include/mapnik/geometry/interior.hpp create mode 100644 src/geometry/interior.cpp diff --git a/include/mapnik/geom_util.hpp b/include/mapnik/geom_util.hpp index 1a63d33fb6..49aa06a6a4 100755 --- a/include/mapnik/geom_util.hpp +++ b/include/mapnik/geom_util.hpp @@ -537,80 +537,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 0000000000..232ab33217 --- /dev/null +++ b/include/mapnik/geometry/interior.hpp @@ -0,0 +1,36 @@ +/***************************************************************************** + * + * 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 +#include + +namespace mapnik { namespace geometry { + +template +point interior(polygon const& polygon, double scale_factor); + +} } + +#endif // MAPNIK_GEOMETRY_INTERIOR_HPP diff --git a/include/mapnik/markers_placements/interior.hpp b/include/mapnik/markers_placements/interior.hpp index fd4d7f8665..69c4b2e073 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 @@ class markers_interior_placement : public markers_point_placementlocator_, 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 4b33b953f4..964d4f1225 100644 --- a/include/mapnik/renderer_common/process_point_symbolizer.hpp +++ b/include/mapnik/renderer_common/process_point_symbolizer.hpp @@ -30,6 +30,7 @@ #include #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 c73571204d..4a886d65e8 100644 --- a/src/build.py +++ b/src/build.py @@ -173,6 +173,7 @@ def ldconfig(*args,**kwargs): geometry/closest_point.cpp geometry/reprojection.cpp geometry/envelope.cpp + geometry/interior.cpp geometry/polylabel.cpp expression_node.cpp expression_string.cpp diff --git a/src/geometry/interior.cpp b/src/geometry/interior.cpp new file mode 100644 index 0000000000..d1a7c5aee1 --- /dev/null +++ b/src/geometry/interior.cpp @@ -0,0 +1,226 @@ +/***************************************************************************** + * + * 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 +#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; +} + +// signed distance from point to polygon outline (negative if point is outside) +template +auto point_to_polygon_dist(const point& point, const polygon& polygon) +{ + bool inside = false; + auto min_dist_sq = std::numeric_limits::infinity(); + + for (const auto& ring : polygon) + { + 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)); + } + } + + 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()(const point& cell_center, T distance_polygon) const + { + if (distance_polygon <= 0) + { + return distance_polygon; + } + point d = cell_center - centroid; + 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.at(0)); + 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 d76f89c3cf..15d9c7c8dc 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 47fa00b153..9d3f1880cc 100644 --- a/src/text/symbolizer_helpers.cpp +++ b/src/text/symbolizer_helpers.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -302,11 +303,8 @@ void base_symbolizer_helper::initialize_points() const geometry::polygon tranformed_poly(geometry::transform(poly, transform_group)); if (how_placed == INTERIOR_PLACEMENT) { - geometry::polygon_vertex_adapter va(tranformed_poly); - if (label::interior_position(va, label_x, label_y)) - { - points_.emplace_back(label_x, label_y); - } + geometry::point pt = geometry::interior(tranformed_poly, scale_factor_); + points_.emplace_back(pt.x, pt.y); } else if (how_placed == POLYLABEL_PLACEMENT) { diff --git a/test/unit/symbolizer/marker_placement_vertex_last.cpp b/test/unit/symbolizer/marker_placement_vertex_last.cpp index 611385c3ac..d7b19f25ad 100644 --- a/test/unit/symbolizer/marker_placement_vertex_last.cpp +++ b/test/unit/symbolizer/marker_placement_vertex_last.cpp @@ -23,7 +23,7 @@ SECTION("empty geometry") { mapnik::markers_placement_params params { mapnik::box2d(0, 0, 10, 10), agg::trans_affine(), - 0, 0, false, false, DIRECTION_AUTO }; + 0, 0, false, false, DIRECTION_AUTO, 1.0 }; placement_type placement(va, detector, params); @@ -45,7 +45,7 @@ SECTION("point") { mapnik::markers_placement_params params { mapnik::box2d(0, 0, 10, 10), agg::trans_affine(), - 0, 0, false, false, DIRECTION_AUTO }; + 0, 0, false, false, DIRECTION_AUTO, 1.0 }; placement_type placement(va, detector, params); @@ -75,7 +75,7 @@ SECTION("line string") { mapnik::markers_placement_params params { mapnik::box2d(0, 0, 10, 10), agg::trans_affine(), - 0, 0, false, false, DIRECTION_AUTO }; + 0, 0, false, false, DIRECTION_AUTO, 1.0 }; placement_type placement(va, detector, params); @@ -109,7 +109,7 @@ SECTION("polygon") { mapnik::markers_placement_params params { mapnik::box2d(0, 0, 10, 10), agg::trans_affine(), - 0, 0, false, false, DIRECTION_AUTO }; + 0, 0, false, false, DIRECTION_AUTO, 1.0 }; placement_type placement(va, detector, params);