Fix unaligned multi-line labels.
This forces offset lines to be aligned to the closest point to the anchor point on the original line, meaning that they are aligned where the offset line and original run parallel, or nearly so.
This commit is contained in:
parent
17626d5501
commit
944f34b3df
3 changed files with 299 additions and 3 deletions
|
@ -131,6 +131,8 @@ public:
|
|||
/** Go back to initial state. */
|
||||
void reset();
|
||||
|
||||
/** position on this line closest to the target position */
|
||||
double position_closest_to(pixel_position const &target_pos);
|
||||
|
||||
private:
|
||||
void rewind_subpath();
|
||||
|
|
|
@ -128,14 +128,70 @@ vertex_cache & vertex_cache::get_offseted(double offset, double region_width)
|
|||
}
|
||||
offseted_line->reset();
|
||||
offseted_line->next_subpath(); //TODO: Multiple subpath support
|
||||
double seek = (position_ + region_width/2.) * offseted_line->length() / length() - region_width/2.;
|
||||
if (seek < 0) seek = 0;
|
||||
if (seek > offseted_line->length()) seek = offseted_line->length();
|
||||
|
||||
// find the point on the offset line closest to the current position,
|
||||
// which we'll use to make the offset line aligned to this one.
|
||||
double seek = offseted_line->position_closest_to(current_position_);
|
||||
offseted_line->move(seek);
|
||||
|
||||
offseted_lines_[offset] = offseted_line;
|
||||
return *offseted_line;
|
||||
}
|
||||
|
||||
inline double dist_sq(pixel_position const &d) {
|
||||
return d.x*d.x + d.y*d.y;
|
||||
}
|
||||
|
||||
double vertex_cache::position_closest_to(pixel_position const &target_pos)
|
||||
{
|
||||
bool first = true;
|
||||
pixel_position old_pos, new_pos;
|
||||
double lin_pos = 0.0, min_pos = 0.0, min_dist_sq = std::numeric_limits<double>::max();
|
||||
|
||||
// find closest approach of each individual segment to the
|
||||
// target position. would be good if there were some kind
|
||||
// of prior, or fast test to avoid calculating on each
|
||||
// segment, but i can't think of one.
|
||||
for (segment const &seg : current_subpath_->vector) {
|
||||
if (first) {
|
||||
old_pos = seg.pos;
|
||||
min_pos = lin_pos;
|
||||
min_dist_sq = dist_sq(target_pos - old_pos);
|
||||
first = false;
|
||||
|
||||
} else {
|
||||
new_pos = seg.pos;
|
||||
|
||||
pixel_position d = new_pos - old_pos;
|
||||
if ((d.x != 0.0) || (d.y != 0)) {
|
||||
pixel_position c = target_pos - old_pos;
|
||||
double t = (c.x * d.x + c.y * d.y) / dist_sq(d);
|
||||
|
||||
if ((t >= 0.0) && (t <= 1.0)) {
|
||||
pixel_position pt = (d * t) + old_pos;
|
||||
double pt_dist_sq = dist_sq(target_pos - pt);
|
||||
|
||||
if (pt_dist_sq < min_dist_sq) {
|
||||
min_dist_sq = pt_dist_sq;
|
||||
min_pos = lin_pos + seg.length * t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
old_pos = new_pos;
|
||||
lin_pos += seg.length;
|
||||
|
||||
double end_dist_sq = dist_sq(target_pos - old_pos);
|
||||
if (end_dist_sq < min_dist_sq) {
|
||||
min_dist_sq = end_dist_sq;
|
||||
min_pos = lin_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min_pos;
|
||||
}
|
||||
|
||||
bool vertex_cache::forward(double length)
|
||||
{
|
||||
if (length < 0)
|
||||
|
|
238
tests/cpp_tests/line_offset_test.cpp
Normal file
238
tests/cpp_tests/line_offset_test.cpp
Normal file
|
@ -0,0 +1,238 @@
|
|||
|
||||
// mapnik
|
||||
#include <mapnik/coord.hpp>
|
||||
#include <mapnik/text/vertex_cache.hpp>
|
||||
|
||||
// boost
|
||||
#include <boost/detail/lightweight_test.hpp>
|
||||
#include <boost/version.hpp>
|
||||
|
||||
// stl
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
// test
|
||||
#include "utils.hpp"
|
||||
|
||||
struct fake_path
|
||||
{
|
||||
typedef boost::tuple<double, double, unsigned> coord_type;
|
||||
typedef std::vector<coord_type> cont_type;
|
||||
cont_type vertices_;
|
||||
cont_type::iterator itr_;
|
||||
|
||||
fake_path(std::initializer_list<double> l)
|
||||
: fake_path(l.begin(), l.size()) {
|
||||
}
|
||||
|
||||
fake_path(std::vector<double> const &v)
|
||||
: fake_path(v.begin(), v.size()) {
|
||||
}
|
||||
|
||||
template <typename Itr>
|
||||
fake_path(Itr itr, size_t sz) {
|
||||
size_t num_coords = sz >> 1;
|
||||
vertices_.reserve(num_coords);
|
||||
|
||||
for (size_t i = 0; i < num_coords; ++i) {
|
||||
double x = *itr++;
|
||||
double y = *itr++;
|
||||
unsigned cmd = (i == 0) ? agg::path_cmd_move_to : agg::path_cmd_line_to;
|
||||
vertices_.push_back(boost::make_tuple(x, y, cmd));
|
||||
}
|
||||
itr_ = vertices_.begin();
|
||||
}
|
||||
|
||||
unsigned vertex(double *x, double *y) {
|
||||
if (itr_ == vertices_.end()) {
|
||||
return agg::path_cmd_stop;
|
||||
}
|
||||
*x = itr_->get<0>();
|
||||
*y = itr_->get<1>();
|
||||
unsigned cmd = itr_->get<2>();
|
||||
++itr_;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
void rewind(unsigned) {
|
||||
itr_ = vertices_.begin();
|
||||
}
|
||||
};
|
||||
|
||||
double dist(mapnik::pixel_position const &a,
|
||||
mapnik::pixel_position const &b)
|
||||
{
|
||||
mapnik::pixel_position d = a - b;
|
||||
return std::sqrt(d.x*d.x + d.y*d.y);
|
||||
}
|
||||
|
||||
namespace boost { namespace detail {
|
||||
|
||||
template<class T, class U>
|
||||
inline void test_leq_impl(char const * expr1, char const * expr2,
|
||||
char const * file, int line, char const * function,
|
||||
T const & t, U const & u)
|
||||
{
|
||||
if( t > u )
|
||||
{
|
||||
BOOST_LIGHTWEIGHT_TEST_OSTREAM
|
||||
<< file << "(" << line << "): test '" << expr1 << " == " << expr2
|
||||
<< "' failed in function '" << function << "': "
|
||||
<< "'" << t << "' > '" << u << "'" << std::endl;
|
||||
++test_errors();
|
||||
}
|
||||
}
|
||||
|
||||
} }
|
||||
|
||||
#define BOOST_TEST_LEQ(expr1,expr2) ( ::boost::detail::test_leq_impl(#expr1, #expr2, __FILE__, __LINE__, BOOST_CURRENT_FUNCTION, expr1, expr2) )
|
||||
|
||||
void test_simple_segment(double const &offset)
|
||||
{
|
||||
const double dx = 0.01;
|
||||
fake_path path = {0, 0, 1, 0}, off_path = {0, offset, 1, offset};
|
||||
mapnik::vertex_cache vc(path), off_vc(off_path);
|
||||
|
||||
vc.reset(); vc.next_subpath();
|
||||
off_vc.reset(); off_vc.next_subpath();
|
||||
|
||||
while (vc.move(dx)) {
|
||||
double pos = vc.linear_position();
|
||||
double off_pos = off_vc.position_closest_to(vc.current_position());
|
||||
BOOST_TEST_LEQ(std::abs(pos - off_pos), 1.0e-6);
|
||||
}
|
||||
}
|
||||
|
||||
void test_straight_line(double const &offset) {
|
||||
const double dx = 0.01;
|
||||
fake_path path = {0, 0, 0.1, 0, 0.9, 0, 1, 0},
|
||||
off_path = {0, offset, 0.4, offset, 0.6, offset, 1, offset};
|
||||
mapnik::vertex_cache vc(path), off_vc(off_path);
|
||||
|
||||
vc.reset(); vc.next_subpath();
|
||||
off_vc.reset(); off_vc.next_subpath();
|
||||
|
||||
while (vc.move(dx)) {
|
||||
double pos = vc.linear_position();
|
||||
double off_pos = off_vc.position_closest_to(vc.current_position());
|
||||
BOOST_TEST_LEQ(std::abs(pos - off_pos), 1.0e-6);
|
||||
}
|
||||
}
|
||||
|
||||
void test_offset_curve(double const &offset) {
|
||||
const double dx = 0.01;
|
||||
const double r = (1.0 + offset);
|
||||
|
||||
std::vector<double> pos, off_pos;
|
||||
const size_t max_i = 1000;
|
||||
for (size_t i = 0; i <= max_i; ++i) {
|
||||
double x = M_PI * double(i) / max_i;
|
||||
pos.push_back(-std::cos(x)); pos.push_back(std::sin(x));
|
||||
off_pos.push_back(-r * std::cos(x)); off_pos.push_back(r * std::sin(x));
|
||||
}
|
||||
|
||||
fake_path path(pos), off_path(off_pos);
|
||||
mapnik::vertex_cache vc(path), off_vc(off_path);
|
||||
|
||||
vc.reset(); vc.next_subpath();
|
||||
off_vc.reset(); off_vc.next_subpath();
|
||||
|
||||
while (vc.move(dx)) {
|
||||
double pos = vc.linear_position();
|
||||
double off_pos = off_vc.position_closest_to(vc.current_position());
|
||||
{
|
||||
mapnik::vertex_cache::scoped_state s(off_vc);
|
||||
off_vc.move(off_pos);
|
||||
BOOST_TEST_LEQ(dist(vc.current_position(), off_vc.current_position()), (1.001 * offset));
|
||||
}
|
||||
BOOST_TEST_LEQ(std::abs((pos / vc.length()) - (off_pos / off_vc.length())), 1.0e-3);
|
||||
}
|
||||
}
|
||||
|
||||
void test_s_shaped_curve(double const &offset) {
|
||||
const double dx = 0.01;
|
||||
const double r = (1.0 + offset);
|
||||
const double r2 = (1.0 - offset);
|
||||
|
||||
std::vector<double> pos, off_pos;
|
||||
const size_t max_i = 1000;
|
||||
for (size_t i = 0; i <= max_i; ++i) {
|
||||
double x = M_PI * double(i) / max_i;
|
||||
pos.push_back(-std::cos(x) - 1); pos.push_back(std::sin(x));
|
||||
off_pos.push_back(-r * std::cos(x) - 1); off_pos.push_back(r * std::sin(x));
|
||||
}
|
||||
for (size_t i = 0; i <= max_i; ++i) {
|
||||
double x = M_PI * double(i) / max_i;
|
||||
pos.push_back(-std::cos(x) + 1); pos.push_back(-std::sin(x));
|
||||
off_pos.push_back(-r2 * std::cos(x) + 1); off_pos.push_back(-r2 * std::sin(x));
|
||||
}
|
||||
|
||||
fake_path path(pos), off_path(off_pos);
|
||||
mapnik::vertex_cache vc(path), off_vc(off_path);
|
||||
|
||||
vc.reset(); vc.next_subpath();
|
||||
off_vc.reset(); off_vc.next_subpath();
|
||||
|
||||
while (vc.move(dx)) {
|
||||
double off_pos = off_vc.position_closest_to(vc.current_position());
|
||||
{
|
||||
mapnik::vertex_cache::scoped_state s(off_vc);
|
||||
off_vc.move(off_pos);
|
||||
BOOST_TEST_LEQ(dist(vc.current_position(), off_vc.current_position()), (1.002 * offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
std::vector<std::string> args;
|
||||
for (int i=1;i<argc;++i)
|
||||
{
|
||||
args.push_back(argv[i]);
|
||||
}
|
||||
bool quiet = std::find(args.begin(), args.end(), "-q")!=args.end();
|
||||
|
||||
try {
|
||||
|
||||
BOOST_TEST(set_working_dir(args));
|
||||
|
||||
std::vector<double> offsets = { 0.01, 0.02, 0.1, 0.2 };
|
||||
for (double offset : offsets) {
|
||||
// test simple straight line segment - should be easy to
|
||||
// find the correspondance here.
|
||||
test_simple_segment(offset);
|
||||
|
||||
// test straight line consisting of more than one segment.
|
||||
test_straight_line(offset);
|
||||
|
||||
// test an offset outer curve
|
||||
test_offset_curve(offset);
|
||||
|
||||
// test an offset along an S-shaped curve, which is harder
|
||||
// because the positions along the offset are no longer
|
||||
// linearly related to the positions along the original
|
||||
// curve.
|
||||
test_s_shaped_curve(offset);
|
||||
}
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
std::cerr << ex.what() << "\n";
|
||||
}
|
||||
|
||||
if (!::boost::detail::test_errors())
|
||||
{
|
||||
if (quiet) std::clog << "\x1b[1;32m.\x1b[0m";
|
||||
else std::clog << "C++ line offset: \x1b[1;32m✓ \x1b[0m\n";
|
||||
#if BOOST_VERSION >= 104600
|
||||
::boost::detail::report_errors_remind().called_report_errors_function = true;
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
return ::boost::report_errors();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue