Merge pull request #4468 from mapnik/bounding-box-split

Split bounding box along (0, 0) before reprojecting  [WIP] [skip ci]
This commit is contained in:
Artem Pavlenko 2024-07-30 10:27:47 +01:00 committed by GitHub
commit 3348f97c76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 139 additions and 41 deletions

View file

@ -26,7 +26,7 @@
// mapnik // mapnik
#include <mapnik/config.hpp> #include <mapnik/config.hpp>
#include <mapnik/well_known_srs.hpp> #include <mapnik/well_known_srs.hpp>
#include <mapnik/geometry/box2d.hpp>
#include <mapnik/warning.hpp> #include <mapnik/warning.hpp>
MAPNIK_DISABLE_WARNING_PUSH MAPNIK_DISABLE_WARNING_PUSH
#include <mapnik/warning_ignore.hpp> #include <mapnik/warning_ignore.hpp>
@ -79,7 +79,8 @@ class MAPNIK_DECL projection
void inverse(double& x, double& y) const; void inverse(double& x, double& y) const;
std::string definition() const; std::string definition() const;
std::string description() const; std::string description() const;
void init_proj() const; void init_proj();
std::optional<box2d<double>> area_of_use() const;
private: private:
void swap(projection& rhs); void swap(projection& rhs);
@ -87,7 +88,8 @@ class MAPNIK_DECL projection
private: private:
std::string params_; std::string params_;
bool defer_proj_init_; bool defer_proj_init_;
mutable bool is_geographic_; bool is_geographic_;
std::optional<box2d<double>> area_of_use_;
mutable PJ* proj_; mutable PJ* proj_;
mutable PJ_CONTEXT* proj_ctx_; mutable PJ_CONTEXT* proj_ctx_;
}; };

View file

@ -42,7 +42,6 @@
#include <stdexcept> #include <stdexcept>
namespace mapnik { namespace mapnik {
namespace { // (local) namespace { // (local)
// Returns points in clockwise order. This allows us to do anti-meridian checks. // Returns points in clockwise order. This allows us to do anti-meridian checks.
@ -90,6 +89,36 @@ auto envelope_points(box2d<T> const& env, std::size_t num_points) -> geometry::m
return coords; return coords;
} }
std::vector<box2d<double>> split_bounding_box(box2d<double> const& bbox)
{
std::vector<box2d<double>> output;
if (bbox.minx() < 0 && bbox.maxx() > 0)
{
if (bbox.miny() < 0 && bbox.maxy() > 0)
{
output.emplace_back(bbox.minx(), bbox.miny(), 0, 0);
output.emplace_back(0, bbox.miny(), bbox.maxx(), 0);
output.emplace_back(0, 0, bbox.maxx(), bbox.maxy());
output.emplace_back(bbox.minx(), 0, 0, bbox.maxy());
}
else
{
output.emplace_back(bbox.minx(), bbox.miny(), 0, bbox.maxy());
output.emplace_back(0, bbox.miny(), bbox.maxx(), bbox.maxy());
}
}
else if (bbox.miny() < 0 && bbox.maxy() > 0)
{
output.emplace_back(bbox.minx(), bbox.miny(), bbox.maxx(), 0);
output.emplace_back(bbox.minx(), 0, bbox.maxx(), bbox.maxy());
}
else
{
output.push_back(bbox);
}
return output;
}
} // namespace } // namespace
proj_transform::proj_transform(projection const& source, projection const& dest) proj_transform::proj_transform(projection const& source, projection const& dest)
@ -398,7 +427,7 @@ bool proj_transform::backward(box2d<double>& box) const
} }
// More robust, but expensive, bbox transform // More robust, but expensive, bbox transform
// in the face of proj4 out of bounds conditions. // in the face of libproj out of bounds conditions.
// Can result in 20 -> 10 r/s performance hit. // Can result in 20 -> 10 r/s performance hit.
// Alternative is to provide proper clipping box // Alternative is to provide proper clipping box
// in the target srs by setting map 'maximum-extent' // in the target srs by setting map 'maximum-extent'
@ -413,27 +442,34 @@ bool proj_transform::backward(box2d<double>& env, std::size_t points) const
return backward(env); return backward(env);
} }
auto coords = envelope_points(env, points); // this is always clockwise
for (auto& p : coords)
{
double z = 0;
if (!backward(p.x, p.y, z))
return false;
}
box2d<double> result; box2d<double> result;
boost::geometry::envelope(coords, result); for (auto b : split_bounding_box(env))
if (is_source_longlat_ && !util::is_clockwise(coords))
{ {
// we've gone to a geographic CS, and our clockwise envelope has auto coords = envelope_points(b, points); // this is always clockwise
// changed into an anticlockwise one. This means we've crossed the antimeridian, and for (auto& p : coords)
// need to expand the X direction to +/-180 to include all the data. Once we can deal {
// with multiple bboxes in queries we can improve. double z = 0;
double miny = result.miny(); if (!backward(p.x, p.y, z))
result.expand_to_include(-180.0, miny); return false;
result.expand_to_include(180.0, miny); }
if (!result.valid())
boost::geometry::envelope(coords, result);
else
{
box2d<double> bb;
boost::geometry::envelope(coords, bb);
result.expand_to_include(bb);
}
if (is_source_longlat_ && !util::is_clockwise(coords))
{
// we've gone to a geographic CS, and our clockwise envelope has
// changed into an anticlockwise one. This means we've crossed the antimeridian, and
// need to expand the X direction to +/-180 to include all the data. Once we can deal
// with multiple bboxes in queries we can improve.
double miny = result.miny();
result.expand_to_include(-180.0, miny);
result.expand_to_include(180.0, miny);
}
} }
env.re_center(result.center().x, result.center().y); env.re_center(result.center().x, result.center().y);
@ -453,32 +489,45 @@ bool proj_transform::forward(box2d<double>& env, std::size_t points) const
} }
auto coords = envelope_points(env, points); // this is always clockwise auto coords = envelope_points(env, points); // this is always clockwise
double z = 0;
for (auto& p : coords) for (auto& p : coords)
{ {
double z = 0;
if (!forward(p.x, p.y, z)) if (!forward(p.x, p.y, z))
return false; return false;
} }
box2d<double> result; box2d<double> result;
boost::geometry::envelope(coords, result); for (auto b : split_bounding_box(env))
if (is_dest_longlat_ && !util::is_clockwise(coords))
{ {
// we've gone to a geographic CS, and our clockwise envelope has auto coords = envelope_points(b, points); // this is always clockwise
// changed into an anticlockwise one. This means we've crossed the antimeridian, and for (auto& p : coords)
// need to expand the X direction to +/-180 to include all the data. Once we can deal {
// with multiple bboxes in queries we can improve. double z = 0;
double miny = result.miny(); if (!forward(p.x, p.y, z))
result.expand_to_include(-180.0, miny); return false;
result.expand_to_include(180.0, miny); }
if (!result.valid())
boost::geometry::envelope(coords, result);
else
{
box2d<double> bb;
boost::geometry::envelope(coords, bb);
result.expand_to_include(bb);
}
if (is_dest_longlat_ && !util::is_clockwise(coords))
{
// we've gone to a geographic CS, and our clockwise envelope has
// changed into an anticlockwise one. This means we've crossed the antimeridian, and
// need to expand the X direction to +/-180 to include all the data. Once we can deal
// with multiple bboxes in queries we can improve.
double miny = result.miny();
result.expand_to_include(-180.0, miny);
result.expand_to_include(180.0, miny);
}
} }
env.re_center(result.center().x, result.center().y); env.re_center(result.center().x, result.center().y);
env.height(result.height()); env.height(result.height());
env.width(result.width()); env.width(result.width());
return true; return true;
} }

View file

@ -94,7 +94,7 @@ bool projection::operator!=(const projection& other) const
return !(*this == other); return !(*this == other);
} }
void projection::init_proj() const void projection::init_proj()
{ {
#ifdef MAPNIK_USE_PROJ #ifdef MAPNIK_USE_PROJ
if (!proj_) if (!proj_)
@ -117,6 +117,11 @@ void projection::init_proj() const
} }
PJ_TYPE type = proj_get_type(proj_); PJ_TYPE type = proj_get_type(proj_);
is_geographic_ = (type == PJ_TYPE_GEOGRAPHIC_2D_CRS || type == PJ_TYPE_GEOGRAPHIC_3D_CRS) ? true : false; is_geographic_ = (type == PJ_TYPE_GEOGRAPHIC_2D_CRS || type == PJ_TYPE_GEOGRAPHIC_3D_CRS) ? true : false;
double west_lon, south_lat, east_lon, north_lat;
if (proj_get_area_of_use(proj_ctx_, proj_, &west_lon, &south_lat, &east_lon, &north_lat, nullptr))
{
area_of_use_ = box2d<double>{west_lon, south_lat, east_lon, north_lat};
}
} }
#endif #endif
} }
@ -131,6 +136,11 @@ bool projection::is_geographic() const
return is_geographic_; return is_geographic_;
} }
std::optional<box2d<double>> projection::area_of_use() const
{
return area_of_use_;
}
std::optional<well_known_srs_e> projection::well_known() const std::optional<well_known_srs_e> projection::well_known() const
{ {
return is_well_known_srs(params_); return is_well_known_srs(params_);

View file

@ -16,6 +16,8 @@ TEST_CASE("projection transform")
CHECK(proj_4326.is_geographic()); CHECK(proj_4326.is_geographic());
CHECK(!proj_3857.is_geographic()); CHECK(!proj_3857.is_geographic());
CHECK(*proj_4326.area_of_use() == mapnik::box2d<double>(-180, -90, 180, 90));
CHECK(*proj_3857.area_of_use() == mapnik::box2d<double>(-180, -85.06, 180, 85.06));
mapnik::proj_transform prj_trans(proj_4326, proj_3857); mapnik::proj_transform prj_trans(proj_4326, proj_3857);
double minx = -45.0; double minx = -45.0;
@ -48,6 +50,7 @@ TEST_CASE("projection transform")
{ {
mapnik::projection proj_4269("epsg:4269"); mapnik::projection proj_4269("epsg:4269");
mapnik::projection proj_3857("epsg:3857"); mapnik::projection proj_3857("epsg:3857");
mapnik::proj_transform prj_trans(proj_4269, proj_3857); mapnik::proj_transform prj_trans(proj_4269, proj_3857);
mapnik::proj_transform prj_trans2(proj_3857, proj_4269); mapnik::proj_transform prj_trans2(proj_3857, proj_4269);
@ -200,7 +203,42 @@ TEST_CASE("projection transform")
} }
} }
} }
#endif SECTION("Test proj north/south poles bbox")
{
// WGS 84 / Arctic Polar Stereographic
mapnik::projection prj_geog("epsg:4326");
mapnik::projection prj_proj("epsg:3995");
mapnik::proj_transform prj_trans_fwd(prj_proj, prj_geog);
mapnik::proj_transform prj_trans_rev(prj_geog, prj_proj);
// bounds
const mapnik::box2d<double> bounds{-180.0, 60.0, 180.0, 90.0};
{
// projected bounds
mapnik::box2d<double> projected_bounds{-3299207.53, -3333134.03, 3299207.53, 3333134.03};
CHECKED_IF(prj_trans_fwd.forward(projected_bounds, PROJ_ENVELOPE_POINTS))
{
CHECK(projected_bounds.minx() == Approx(bounds.minx()));
CHECK(projected_bounds.miny() == Approx(48.65640)); // this is expected
CHECK(projected_bounds.maxx() == Approx(bounds.maxx()));
CHECK(projected_bounds.maxy() == Approx(bounds.maxy()));
}
}
{
// check the same logic works for .backward()
mapnik::box2d<double> projected_bounds{-3299207.53, -3333134.03, 3299207.53, 3333134.03};
CHECKED_IF(prj_trans_rev.backward(projected_bounds, PROJ_ENVELOPE_POINTS))
{
CHECK(projected_bounds.minx() == Approx(bounds.minx()));
CHECK(projected_bounds.miny() == Approx(48.65640)); // this is expected
CHECK(projected_bounds.maxx() == Approx(bounds.maxx()));
CHECK(projected_bounds.maxy() == Approx(bounds.maxy()));
}
}
}
SECTION("proj_transform of coordinate arrays with stride > 1") SECTION("proj_transform of coordinate arrays with stride > 1")
{ {
@ -237,7 +275,6 @@ TEST_CASE("projection transform")
} }
} }
#ifdef MAPNIK_USE_PROJ
SECTION("lonlat <-> New Zealand Transverse Mercator 2000") SECTION("lonlat <-> New Zealand Transverse Mercator 2000")
{ {
mapnik::projection const proj_2193("epsg:2193"); mapnik::projection const proj_2193("epsg:2193");