-
Notifications
You must be signed in to change notification settings - Fork 161
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
Geos ccw analysis #229
Geos ccw analysis #229
Conversation
Add a `RGeo::Geos::Analysis.ccw?` method which itself uses the GEOSCoordSeq_isCCW_r to determine if a geometry is counter-clockwise. Also add an error.h header to help throwing errors when there is an issue in the c extension. And add a `RGeo::GeosError` for GEOS related errors.
This helps a lot a developer which has a global version installed and tries to contribute to the library, since it will strictly look for related files...
test/geos_capi/analysis_test.rb
Outdated
def setup | ||
skip "Needs GEOS." unless RGeo::Geos.capi_supported? | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other c_api tests add the if RGeo::Geos.capi_supported?
statement after the closure to the test class. I like this approach, so at some point, we could change all of them to this format. We can save that for another PR though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I saw the if RGeo::Geos.capi_supported?
and the WARNING, but this is hard to see IMHO. I'm all for changing that. I won't be able to work on it until next week. If you have time and you want to add a commit to this PR I'd be OK. Or I'll open an issue and we'll add it to our low priority queue :)
This looks great! I just have 2 other things to note about it.
Edit: I forgot that |
Definitely, we're in ruby, convenience should always be the main focus!
Yes, adding a |
…nore the function completely if the GEOS version is too low.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice ! I've added one last commit, which unfortunately is a test that fails. When a ring has only colinear points, the result is not the same whether we're using geos or a simple factory...
What's your opinion on that one ? IMHO, we could remove the test and add a note about colinear points, but I don't know if there is a spec on that somewhere.
We should either remove it or add some sort of handling for invalid rings. The JTS description of the I think we should just leave it as is and leave a note about it. It could be annoying to raise an error everytime there's an issue and I think the handling of invalid geoms needs to be re-addressed in v3 anyways. |
@keithdoggett After digging a bit more into that, I think this is an issue. Since require_relative "lib/rgeo"
factory = RGeo::Geos.factory
p1 = factory.point(1, 1)
p2 = factory.point(2, 1)
p3 = factory.point(5, 1)
l = factory.linear_ring([p1, p2, p3, p1])
# Both those assertions should not be true:
l.is_ring? == false
factory.property(:uses_lenient_assertions) == nil IMO, we should add a notice saying that only if you use Force linear_string validationdiff --git a/ext/geos_c_impl/factory.h b/ext/geos_c_impl/factory.h
index 91ab26c..96b5b04 100644
--- a/ext/geos_c_impl/factory.h
+++ b/ext/geos_c_impl/factory.h
@@ -82,12 +82,12 @@ typedef struct {
int buffer_resolution;
} RGeo_FactoryData;
-#define RGEO_FACTORYFLAGS_LENIENT_MULTIPOLYGON 1
-#define RGEO_FACTORYFLAGS_SUPPORTS_Z 2
-#define RGEO_FACTORYFLAGS_SUPPORTS_M 4
-#define RGEO_FACTORYFLAGS_SUPPORTS_Z_OR_M 6
-#define RGEO_FACTORYFLAGS_PREPARE_HEURISTIC 8
-
+#define RGEO_FACTORYFLAGS_LENIENT_MULTIPOLYGON 0b00001
+#define RGEO_FACTORYFLAGS_SUPPORTS_Z 0b00010
+#define RGEO_FACTORYFLAGS_SUPPORTS_M 0b00100
+#define RGEO_FACTORYFLAGS_SUPPORTS_Z_OR_M 0b00110
+#define RGEO_FACTORYFLAGS_PREPARE_HEURISTIC 0b01000
+#define RGEO_FACTORYFLAGS_LENIENT_ASSERTIONS 0b10000
/*
Wrapped structure for Geometry objects.
diff --git a/ext/geos_c_impl/geometry.c b/ext/geos_c_impl/geometry.c
index 3a34f24..dda69b0 100644
--- a/ext/geos_c_impl/geometry.c
+++ b/ext/geos_c_impl/geometry.c
@@ -1047,6 +1047,7 @@ static VALUE method_geometry_invalid_reason(VALUE self)
str = GEOSisValidReason_r(self_data->geos_context, self_geom);
if (str) {
result = rb_str_new2(str);
+ GEOSFree_r(self_data->geos_context, str);
}
else {
result = rb_str_new2("Exception");
diff --git a/ext/geos_c_impl/line_string.c b/ext/geos_c_impl/line_string.c
index 0f5bc64..1d491d6 100644
--- a/ext/geos_c_impl/line_string.c
+++ b/ext/geos_c_impl/line_string.c
@@ -17,6 +17,7 @@
#include "line_string.h"
#include "coordinates.h"
+#include "errors.h"
RGEO_BEGIN_C
@@ -500,21 +501,29 @@ static VALUE cmethod_create_line_string(VALUE module, VALUE factory, VALUE array
static VALUE cmethod_create_linear_ring(VALUE module, VALUE factory, VALUE array)
{
- VALUE result;
GEOSCoordSequence* coord_seq;
RGeo_FactoryData* factory_data;
GEOSGeometry* geom;
+ char* invalid_reason;
- result = Qnil;
coord_seq = coord_seq_from_array(factory, array, 1);
- if (coord_seq) {
- factory_data = RGEO_FACTORY_DATA_PTR(factory);
- geom = GEOSGeom_createLinearRing_r(factory_data->geos_context, coord_seq);
- if (geom) {
- result = rgeo_wrap_geos_geometry(factory, geom, factory_data->globals->geos_linear_ring);
- }
+ if (!coord_seq) { return Qnil; }
+
+ factory_data = RGEO_FACTORY_DATA_PTR(factory);
+ geom = GEOSGeom_createLinearRing_r(factory_data->geos_context, coord_seq);
+ if (!geom) { return Qnil; }
+
+ if ((factory_data->flags & RGEO_FACTORYFLAGS_LENIENT_ASSERTIONS) || GEOSisValid_r(factory_data->geos_context, geom)) {
+ return rgeo_wrap_geos_geometry(factory, geom, factory_data->globals->geos_linear_ring);
+ }
+ invalid_reason = GEOSisValidReason_r(factory_data->geos_context, geom);
+ if (invalid_reason) {
+ // TODO: This is bad, looking for a canonical solution https://stackoverflow.com/questions/64834575
+ GEOSFree_r(factory_data->geos_context, invalid_reason);
+ rb_raise(geos_error, "%s", invalid_reason);
+ } else {
+ rb_raise(rgeo_error, "Unknown error.");
}
- return result;
}
diff --git a/ext/geos_c_impl/preface.h b/ext/geos_c_impl/preface.h
index 002c924..e59115b 100644
--- a/ext/geos_c_impl/preface.h
+++ b/ext/geos_c_impl/preface.h
@@ -2,6 +2,8 @@
Preface header for GEOS wrapper
*/
+// GEOS Header to ensure we can only use the thread-safe API.
+#define GEOS_USE_ONLY_R_API
#ifdef HAVE_GEOS_C_H
#ifdef HAVE_GEOSSETSRID_R
diff --git a/lib/rgeo/geos/capi_factory.rb b/lib/rgeo/geos/capi_factory.rb
index a9c36c8..431f8cd 100644
--- a/lib/rgeo/geos/capi_factory.rb
+++ b/lib/rgeo/geos/capi_factory.rb
@@ -33,6 +33,7 @@ module RGeo
raise Error::UnsupportedOperation, "GEOS cannot support both Z and M coordinates at the same time."
end
flags |= 8 unless opts_[:auto_prepare] == :disabled
+ flags |= 16 if opts_[:uses_lenient_assertions]
# Buffer resolution
buffer_resolution_ = opts_[:buffer_resolution].to_i
@@ -256,8 +257,12 @@ module RGeo
_buffer_resolution
end
- # Returns true if this factory is lenient with MultiPolygon assertions
+ def lenient_assertions?
+ _flags & 0x10 != 0
+ end
+ # Returns true if this factory is lenient with MultiPolygon assertions
+ # TODO: This should be defined in the c code, using the header RGEO_FACTORYFLAGS_LENIENT_MULTIPOLYGON
def lenient_multi_polygon_assertions?
_flags & 0x1 != 0
end
diff --git a/test/cartesian_analysis_test.rb b/test/cartesian_analysis_test.rb
index 1cdbc11..086fd0d 100644
--- a/test/cartesian_analysis_test.rb
+++ b/test/cartesian_analysis_test.rb
@@ -15,10 +15,15 @@ class CartesianAnalysisTest < Minitest::Test # :nodoc:
end
def line
- p1 = @factory.point(1, 1)
- p2 = @factory.point(2, 1)
- p3 = @factory.point(5, 1)
- @factory.line_string([p1, p2, p3, p1])
+require_relative "lib/rgeo"
+
+factory = RGeo::Geos.factory(uses_lenient_assertions: true)
+p1 = factory.point(1, 1)
+p2 = factory.point(2, 1)
+p3 = factory.point(5, 1)
+l = factory.linear_ring([p1, p2, p3, p1])
+l.is_ring? == false
+factory.property(:uses_lenient_assertions) == nil
end
def clockwise_triangle |
@BuonOmo you raise a good point here. The We need to figure out how we want the validation method to work for v3, but I'm of the opinion that allowing instantiation of invalid geometries is fine, but a validity check should be done before certain operations (area, ccw?, etc.) to prevent weird results unless |
@keithdoggett, ok correct this should not be part of this PR. I'll open an issue soon. I think we should discuss about planning for v3 in our next monthly :) I've added a note to the PR, I'll let you review and I think we'll be good to go ! |
@BuonOmo this looks good now. I think the last thing we should do is remove the |
@keithdoggett merged ! I think we could try and merge #230 and then make a minor. Does it look or for you ? |
Add a `RGeo::Geos::Analysis.ccw?` method which itself uses the GEOSCoordSeq_isCCW_r to determine if a geometry is counter-clockwise. This method is used by `RGeo::Cartesian::Analysis.ccw?` when possible, and there is a new `LinearRing#ccw?` method as well! This geos version is only valid for the geos capi, and libgeos must be 3.7+. Some refactoring was also done: Add an error.h header to help throwing errors when there is an issue in the c extension. And add a `RGeo::GeosError` for GEOS related errors. Change lots of `require` to `require_relative`. This helps a lot for local development and clarifies usage. Co-authored-by: Keith Doggett <keith.doggett887@gmail.com>
Add a `RGeo::Geos::Analysis.ccw?` method which itself uses the GEOSCoordSeq_isCCW_r to determine if a geometry is counter-clockwise. This method is used by `RGeo::Cartesian::Analysis.ccw?` when possible, and there is a new `LinearRing#ccw?` method as well! This geos version is only valid for the geos capi, and libgeos must be 3.7+. Some refactoring was also done: Add an error.h header to help throwing errors when there is an issue in the c extension. And add a `RGeo::GeosError` for GEOS related errors. Change lots of `require` to `require_relative`. This helps a lot for local development and clarifies usage. Co-authored-by: Keith Doggett <keith.doggett887@gmail.com>
Summary
We needed to avoid performance loss when computing ring direction for rgeo/rgeo-geojson#47. This led me to this implementation of a
ccw?
method for GEOS C API, which is way faster than the ruby implementation (see rgeo/rgeo-geojson#47 (comment))Commit details
Add clockwise analysis for GEOS_CAPI 8b640b0
Add a
RGeo::Geos::Analysis.ccw?
method which itself uses theGEOSCoordSeq_isCCW_r to determine if a geometry is counter-clockwise.
Also add an error.h header to help throwing errors when there is an
issue in the c extension. And add a
RGeo::GeosError
for GEOS relatederrors.
Change from require to require_relative 163a68f
This helps a lot a developer which has a global version installed and
tries to contribute to the library, since it will strictly look for
related files...
For review
I did some opiniated changes, I'd be glad to reverse some if you do not agree on those :)