Skip to content

Define CS-specific behavior of algorithms only with strategy (part 1). #533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Dec 16, 2018

Conversation

awulkiew
Copy link
Member

In Boost.Geometry if a strategy is passed into the algorithm the coordinate system of geometries are ignored. The CS-specific behavior of a strategy is used. At least this is the case at the "highest-level". Internally still in many places the information about the CS defined by strategy is not propagated, default strategies are created from geometry types and algorithms dispatched by CS in algorithms instead of in strategies. To fix this:

  • proper strategies has to be "derived" from "higher-level" strategy and propagated down to each underlying algorithm,
  • no default strategy should be created internally,
  • to be in line with the library design all CS specific code (dispatched by CS) should be moved to strategies directory.

This PR is the first part of changes addressing the above problems. I decided to propose it because it is big enough already and I want to know if the direction I'm moving in is correct.

In order to find places where change is needed semi-automatically I added new coordinate system cs::undefined for which default strategy is not defined nor any algorithm dispatched (besides places where some CS is assumed by default which has to be found manually). Next I modified the following:

  • move CS-specific normalization utility to strategies namespace strategy::normalize
  • move CS-specific parts of expand and envelope to strategies, replacing algorithm dispatched by CS with proper strategies,
  • make equals/disjoint for P/P clean, i.e. strategy now contains CS-specific code and algorithm calls this strategy (internal utility equals_point_point and disjoint_point_point requires strategy now). Also create CS-specific ones. Keep the agnostic one using one of the CS specific default strategies based on geometries.
  • move CS-specific parts of disjoint for Box/Box into strategies, create proper strategy etc.
  • move CS-specific parts of disjoint for Segment/Box into strategies.
  • move CS-specific parts of within Point/Box strategy from agnostic strategies to CS-specific, keep agnostic strategy using default CS-specific strategy based on CS defined by geometries.
  • point_on_border Midpoint parameter is removed because it was cartesian-only and only was hiding problems (if there was a problem in algorithm and Midpoint used then one could have create rings touching at midpoint and get the same result as in the case of rings touching at endpoint but without Midpoint)
  • wkt now internally creates default disjoint strategy, I was reluctant to also add Strategy argument to wkt because I thought that stream manipulators should have only 1 argument but dsv has more. Still passing disjoint strategy to wkt feels wierd.
  • azimuth, normalize, expand, envelope, disjoint, etc. strategies are derived from "higher-level" strategies and passed internally.
  • overlapping interiors condition fixed in is_valid.
  • simplify within concept check parameters

The removal of Midpoint in point_on_border exposed a bug in is_valid erroreously returning a false-positive reported here: #515. So at this point several sym_difference tests fail, which is correct.

This work is also why I was postponing fixing #466. I'm going bottom-up with the changes and only now approaching the level of envelope for Polygon. Fixing this issue requires to have a different algorithm for cartesian and non-cartesian Rings and Single-Geometries contained in Multi-Geometries. So at least parts of the algorithm would have to be moved to strategies.

Remove is_radian and units typedef from non-cartesian cs structs.
- use CS-specific normalization strategies instead of algorithm.
- add lower-level strategies getters (point in point, envelope, expand).
- pass strategy to equals_point_point()
…artesian-only hack.

If using this parameter changes the result it means that a different
method should be used. Using it does not solve the real problem, it only
hides it. Consider a polygon in another, touching at the first vertex.
Checking the midpoint of the first segment of contained polygon could
result in finding out that the polygon is inside. However if the segment
was collinear to the containing polygon's segment or this polygon had
vertex exactly at the checked midpoint the result would be the same as
using the first point of the contained polygon.
- use equals P/P in equals_point_point extracted from strategy passed to
  algorithm
- remove Midpoint of point_on_border (this exposes an error in is_valid)
- use envelope and expand strategies extracted from strategy passed to
  algorithm
- change union's default strategy (relate v.s. intersection).
…trategies and use these strategies in algorithms.
Shadowing of template parameters, missing typename keywords, missing
includes.
…om algorithms details to strategies.

This fixes circular dependencies.
…his also makes template keywords unnecessary.
…egies. Change P/B within strategy concept (no struct template parameters).
@awulkiew awulkiew added this to the 1.70 milestone Nov 29, 2018
Copy link
Collaborator

@barendgehrels barendgehrels left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. I went to most of it and it looks consistent and a lot cleaner. Looks like a lot of code could be deleted.
I'm OK with merging. We both touched a little bit of the same areas so I don't hope that there are too many conflicts. As far as I could judge, mostly the touched lines are different so I think there are not many.

But... is it OK if I merge first?

@@ -310,23 +317,27 @@ class multi_point_multi_geometry
}
};

template <typename DisjointPointBoxStrategy>
struct overlaps_box_point
{
template <typename Box, typename Point>
static inline bool apply(Box const& box, Point const& point)
{
// The default strategy is enough for Point/Box
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this comment can be removed now (is it still the default strategy? It can now be specified)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should have been removed.

@@ -134,7 +148,8 @@ struct disjoint_segment_box_sphere_or_spheroid

if (lon1 > lon2)
{
swap(lon1, lat1, lon2, lat2);
std::swap(lon1, lon2);
std::swap(lat1, lat2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more clear indeed

}
}
}

template <typename Range, typename Box, typename Strategy>
static inline void apply(Range const& range, Box& mbr, Strategy const& strategy)
{
return apply(boost::begin(range), boost::end(range), mbr, strategy);
return apply(Strategy::begin(range), Strategy::end(range), mbr, strategy);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is surprising. What is this for kind of strategy? So it should implement range like features?

Copy link
Member Author

@awulkiew awulkiew Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with envelope is that in cartesian we can process points but in spherical and geographic we have to analyse segments because geodesic's vertex may have different latitude (higher or lower on northern or southern hemisphere respectively) than the endpoints. So the strategy feeds the algorithm either with points or with segments. Previously the dispatching by CS was done in the algorithm. I decided to do it this way to allow the strategy to define the smallest Cs-specific portions of the algorithm. Otherwise I'd have to implement the whole calculation of envelope of a range as strategy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@@ -306,6 +306,17 @@ struct stateful_range_appender<Geometry, open>
}

private:
static inline bool disjoint(point_type const& p1, point_type const& p2)
{
// TODO: pass strategy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK for me, PR is large enough

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I wanted to postpone it because I didn't want to force the user to pass within point strategy into read_wkt(). We should probably have separate wkt strategy for that.

{
typedef geometry::degree units;
};

template <>
struct coordinate_system_units<geometry::radian>
struct define_angular_units<geometry::radian>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see. We don't get backward compatibility issues with this?

OK, I see that it is part of core_detail - then it should be no problem

>::units units;
};
: core_detail::define_angular_units<DegreeOrRadian>
{};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason to derive instead of defining it inside?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less lines of code. AFAIU it's a typical way of defining MPL-like meta-functions.

\brief Undefined coordinate system
\ingroup cs
*/
struct undefined {};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

typename Iterator, typename RangeIterator,
typename Section,
typename IntersectionStrategy, typename RobustPolicy
>
static inline void advance_to_non_duplicate_next(Iterator& next,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this. So this will give a merge conflict later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, I'll resolve them.

@awulkiew
Copy link
Member Author

No problem with merging your PR first.

Copy link
Member

@vissarion vissarion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Adam, it makes more sense now and much clearer. I reviewed it and added some minor comments. In general I am ok with merging and looking forward to part II.

Additionally, since it seems the right thread to ask: if strategies define the CS-specific computation of an algorithm what is an agnostic strategy? Should just be part of the corresponding algorithm?

}
}
}

template <typename Range, typename Box, typename Strategy>
static inline void apply(Range const& range, Box& mbr, Strategy const& strategy)
{
return apply(boost::begin(range), boost::end(range), mbr, strategy);
return apply(Strategy::begin(range), Strategy::end(range), mbr, strategy);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

{
return ! detail::disjoint::disjoint_point_point(point1, point2);
return Strategy::apply(point1, point2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need a special strategy for checking if 2 points are equal? Is it the same for all CS? Unless if you consider normalization, but in this case how normalization strategy is passes here? What am I missing...

Copy link
Member Author

@awulkiew awulkiew Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general yes, this could be done the same for all coordinate systems. For this we'd have to agree what's the valid range of coordinates, e.g. if lon_deg = -180+eps/2 is valid, since this would be true: bg::math::equals(lon_deg + 360, 180), this coordinate would be ambiguous and cartesian-like comparison would fail. Currently we have this normalization in place which even if the user represents the coordinates ambiguously (-180 vs 180 or even as something outside valid range) the relation operations still works. In order for this to work we need separate strategies for various CSes even for points.

Copy link
Member

@vissarion vissarion Dec 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have a strong opinion here, since I cannot predict the user needs, but what about fixing a range e.g. (-180,180], then if the user compare invalid coordinates the result is "unexpected" as with invalid geometries. In any case the user could be use normalization before comparing and have an always correct result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, this should be done in a new PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the normalization could also be a part of correct. However I think that assuming that -180 is correct is common practice and if we changed the existing behavior users could get unexpected results.

@@ -482,11 +492,13 @@ class get_turns_generic

typename IntersectionStrategy::envelope_strategy_type const
envelope_strategy = intersection_strategy.get_envelope_strategy();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it needed for intersection strategy to contain both envelope and expand strategies? Why not passing them independently as in the strategies in disjoint_segment_box?

Copy link
Member Author

@awulkiew awulkiew Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sectionalize uses 2 policies. And even if we wanted to calculate boxes and expand spherical boxes by other boxes instead of segments, spherical boxes are expanded in different way than cartesian ones so we'd also need second strategy here (expand box/box).

@@ -70,13 +70,7 @@ struct within
Geometry2 const& geometry2,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. why no dispatching as in area and distance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I answered that before but also here, dispatching based on function templates doesn't force us to explicitly state the types. I've done it there only because I was forced to do it, not because I wanted to do it. ;)

@@ -1,6 +1,6 @@
// Boost.Geometry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now there is strategy named envelope_segment the name of this file should also change

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

@awulkiew
Copy link
Member Author

Merged develop with recently merged #531 and #540.

As a reminder, note that some sym_difference tests fails due to this: #515

Additionally, since it seems the right thread to ask: if strategies define the CS-specific computation of an algorithm what is an agnostic strategy? Should just be part of the corresponding algorithm?

@vissarion I'm not sure. Maybe it indeed should simply be a different algorithm with distinguished name, like bg::algorithm_variant1, bg::algorithm_variant2, bg::algorithm_variant3 and the default bg::algorithm. In that case I'm not sure if the name "Strategy" is a good name since it's more general, maybe it would then be "CSStrategy". But anyway, I'm not sure if we can change that now because of causing problems to the users. Thought If there is only 1 agnostic strategy for algorithm I doubt anyone is using it explicitly. Maybe besides buffer's strategies which are probably used heavily because they are something between Strategies and Parameters. And on top of this we might decide to move to C++11 like some other libraries and this could be a good opportunity to redefine some aspects of the library. IF we want to talk about all of it I suggest to create an Issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants