Skip to content

Commit

Permalink
Geos ccw analysis (rgeo#229)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
2 people authored and Quiwin committed Apr 7, 2021
1 parent beb4df4 commit 4c46b2b
Show file tree
Hide file tree
Showing 30 changed files with 527 additions and 137 deletions.
2 changes: 2 additions & 0 deletions History.md
Expand Up @@ -4,6 +4,8 @@
* Expand gemspec
* Drop Ruby 2.3 support
* Change ProjectedLinearRing #is_simple? method to be uniform across geos versions #228
* Add a simple fallback for `Polygon#contains?` (Quiwin) #224
* Add `ccw?` method for linear rings, with geos support #229

### 2.1.1 / 2019-8-26

Expand Down
4 changes: 0 additions & 4 deletions Rakefile
Expand Up @@ -4,10 +4,6 @@ require "rake/testtask"
require "rake/extensiontask"
require "bundler/gem_tasks"

gemspec = eval(File.read(Dir.glob("*.gemspec").first))
release_gemspec = eval(File.read(Dir.glob("*.gemspec").first))
release_gemspec.version = gemspec.version.to_s.sub(/\.nonrelease$/, "")

# Build tasks

if RUBY_DESCRIPTION.match(/^jruby/)
Expand Down
78 changes: 78 additions & 0 deletions ext/geos_c_impl/analysis.c
@@ -0,0 +1,78 @@
/*
Analysis methos for GEOS wrapper
*/

#include "preface.h"

#ifdef RGEO_GEOS_SUPPORTED

#include <ruby.h>
#include <geos_c.h>

#include "analysis.h"
#include "factory.h"
#include "errors.h"

RGEO_BEGIN_C

/*
* call-seq:
* RGeo::Geos::Analysis.ccw? -> true or false
*
* Checks direction for a ring, returns +true+ if counter-clockwise, +false+
* otherwise.
*/
#ifdef RGEO_GEOS_SUPPORTS_ISCCW
VALUE rgeo_geos_analysis_ccw_p(VALUE self, VALUE ring)
{

const RGeo_GeometryData* ring_data;
const GEOSCoordSequence* coord_seq;
char is_ccw;

rgeo_check_geos_object(ring);

ring_data = RGEO_GEOMETRY_DATA_PTR(ring);

coord_seq = GEOSGeom_getCoordSeq_r(ring_data->geos_context, ring_data->geom);
if (!coord_seq) { rb_raise(geos_error, "Could not retrieve CoordSeq from given ring."); }
if (!GEOSCoordSeq_isCCW_r(ring_data->geos_context, coord_seq, &is_ccw)) {
rb_raise(geos_error, "Could not determine if the CoordSeq is CCW.");
}

return is_ccw ? Qtrue : Qfalse;
};
#endif // RGEO_GEOS_SUPPORTS_ISCCW


/**
* call-seq:
* RGeo::Geos::Analysis.ccw_supported? -> true or false
*
* Checks if the RGEO_GEOS_SUPPORTS_ISCCW macro is defined, returns +true+
* if it is, +false+ otherwise
*/
VALUE rgeo_geos_analysis_supports_ccw(VALUE self)
{
#ifdef RGEO_GEOS_SUPPORTS_ISCCW
return Qtrue;
#else
return Qfalse;
#endif
}


void rgeo_init_geos_analysis(RGeo_Globals* globals)
{
VALUE geos_analysis_module;

geos_analysis_module = rb_define_module_under(globals->geos_module, "Analysis");
rb_define_singleton_method(geos_analysis_module, "ccw_supported?", rgeo_geos_analysis_supports_ccw, 0);
#ifdef RGEO_GEOS_SUPPORTS_ISCCW
rb_define_singleton_method(geos_analysis_module, "ccw?", rgeo_geos_analysis_ccw_p, 1);
#endif // RGEO_GEOS_SUPPORTS_ISCCW
}

RGEO_END_C

#endif
42 changes: 42 additions & 0 deletions ext/geos_c_impl/analysis.h
@@ -0,0 +1,42 @@
/*
Analysis methos for GEOS wrapper
*/

#ifndef RGEO_GEOS_ANALYSIS_INCLUDED
#define RGEO_GEOS_ANALYSIS_INCLUDED

#include <ruby.h>

#ifdef RGEO_GEOS_SUPPORTED

#include "factory.h"

RGEO_BEGIN_C

/*
* call-seq:
* RGeo::Geos::Analysis.ccw? -> true or false
*
* Checks direction for a ring, returns +true+ if counter-clockwise, +false+
* otherwise.
*/
#ifdef RGEO_GEOS_SUPPORTS_CCW
VALUE rgeo_geos_analysis_ccw_p(VALUE self, VALUE ring);
#endif // RGEO_GEOS_SUPPORTS_CCW

/**
* call-seq:
* RGeo::Geos::Analysis.ccw_supported? -> true or false
*
* Checks if the RGEO_GEOS_SUPPORTS_ISCCW macro is defined, returns +true+
* if it is, +false+ otherwise
*/
VALUE rgeo_geos_analysis_supports_ccw(VALUE self);

void rgeo_init_geos_analysis(RGeo_Globals* globals);

RGEO_END_C

#endif // RGEO_GEOS_SUPPORTED

#endif // RGEO_GEOS_ANALYSIS_INCLUDED
35 changes: 35 additions & 0 deletions ext/geos_c_impl/errors.c
@@ -0,0 +1,35 @@

#ifndef RGEO_GEOS_ERROS_INCLUDED
#define RGEO_GEOS_ERROS_INCLUDED

#include <ruby.h>

#include "preface.h"

#ifdef RGEO_GEOS_SUPPORTED

#include "errors.h"

RGEO_BEGIN_C

// Any error relative to RGeo.
VALUE rgeo_error;
// RGeo error specific to the GEOS implementation.
VALUE geos_error;


void rgeo_init_geos_errors() {
VALUE rgeo_module;
VALUE error_module;

rgeo_module = rb_define_module("RGeo");
error_module = rb_define_module_under(rgeo_module, "Error");
rgeo_error = rb_define_class_under(error_module, "RGeoError", rb_eRuntimeError);
geos_error = rb_define_class_under(error_module, "GeosError", rgeo_error);
}

RGEO_END_C

#endif // RGEO_GEOS_SUPPORTED

#endif // RGEO_GEOS_ERROS_INCLUDED
22 changes: 22 additions & 0 deletions ext/geos_c_impl/errors.h
@@ -0,0 +1,22 @@

#ifndef RGEO_GEOS_ERROS_INCLUDED
#define RGEO_GEOS_ERROS_INCLUDED

#include <ruby.h>

#ifdef RGEO_GEOS_SUPPORTED

RGEO_BEGIN_C

// Any error relative to RGeo.
extern VALUE rgeo_error;
// RGeo error specific to the GEOS implementation.
extern VALUE geos_error;

void rgeo_init_geos_errors();

RGEO_END_C

#endif // RGEO_GEOS_SUPPORTED

#endif // RGEO_GEOS_ERROS_INCLUDED
1 change: 1 addition & 0 deletions ext/geos_c_impl/extconf.rb
Expand Up @@ -31,6 +31,7 @@ def create_dummy_makefile
have_func("GEOSPreparedContains_r", "geos_c.h")
have_func("GEOSPreparedDisjoint_r", "geos_c.h")
have_func("GEOSUnaryUnion_r", "geos_c.h")
have_func("GEOSCoordSeq_isCCW_r", "geos_c.h")
have_func("rb_memhash", "ruby.h")
end

Expand Down
11 changes: 10 additions & 1 deletion ext/geos_c_impl/factory.c
Expand Up @@ -16,6 +16,7 @@
#include "line_string.h"
#include "polygon.h"
#include "geometry_collection.h"
#include "errors.h"

RGEO_BEGIN_C

Expand Down Expand Up @@ -576,10 +577,11 @@ RGeo_Globals* rgeo_init_geos_factory()
VALUE wrapped_globals;
VALUE feature_module;

rgeo_module = rb_define_module("RGeo");

globals = ALLOC(RGeo_Globals);

// Cache some modules so we don't have to look them up by name every time
rgeo_module = rb_define_module("RGeo");
feature_module = rb_define_module_under(rgeo_module, "Feature");
globals->feature_module = feature_module;
globals->geos_module = rb_define_module_under(rgeo_module, "Geos");
Expand Down Expand Up @@ -831,6 +833,13 @@ char rgeo_is_geos_object(VALUE obj)
return (TYPE(obj) == T_DATA && RDATA(obj)->dfree == (RUBY_DATA_FUNC)destroy_geometry_func) ? 1 : 0;
}

void rgeo_check_geos_object(VALUE obj)
{
if (!rgeo_is_geos_object(obj)) {
rb_raise(rgeo_error, "Not a GEOS Geometry object.");
}
}


const GEOSGeometry* rgeo_get_geos_geometry_safe(VALUE obj)
{
Expand Down
6 changes: 5 additions & 1 deletion ext/geos_c_impl/factory.h
Expand Up @@ -10,7 +10,6 @@

RGEO_BEGIN_C


/*
Per-interpreter globals.
Most of these are cached references to commonly used classes, modules,
Expand Down Expand Up @@ -187,6 +186,11 @@ GEOSGeometry* rgeo_convert_to_detached_geos_geometry(VALUE obj, VALUE factory, V
*/
char rgeo_is_geos_object(VALUE obj);

/*
Raises a rgeo error if the object is not a GEOS Geometry implementation.
*/
void rgeo_check_geos_object(VALUE obj);

/*
Gets the underlying GEOS geometry for a given ruby object. Returns NULL
if the given ruby object is not a GEOS geometry wrapper.
Expand Down
7 changes: 5 additions & 2 deletions ext/geos_c_impl/main.c
Expand Up @@ -2,26 +2,27 @@
Main initializer for GEOS wrapper
*/


#include "preface.h"

#ifdef RGEO_GEOS_SUPPORTED

#include <ruby.h>
#include <geos_c.h>

#include "errors.h"

#include "factory.h"
#include "geometry.h"
#include "point.h"
#include "line_string.h"
#include "polygon.h"
#include "geometry_collection.h"
#include "analysis.h"

#endif

RGEO_BEGIN_C


void Init_geos_c_impl()
{
#ifdef RGEO_GEOS_SUPPORTED
Expand All @@ -33,6 +34,8 @@ void Init_geos_c_impl()
rgeo_init_geos_line_string(globals);
rgeo_init_geos_polygon(globals);
rgeo_init_geos_geometry_collection(globals);
rgeo_init_geos_analysis(globals);
rgeo_init_geos_errors();
#endif
}

Expand Down
3 changes: 3 additions & 0 deletions ext/geos_c_impl/preface.h
Expand Up @@ -21,6 +21,9 @@
#ifdef HAVE_GEOSUNARYUNION_R
#define RGEO_GEOS_SUPPORTS_UNARYUNION
#endif
#ifdef HAVE_GEOSCOORDSEQ_ISCCW_R
#define RGEO_GEOS_SUPPORTS_ISCCW
#endif
#ifdef HAVE_RB_MEMHASH
#define RGEO_SUPPORTS_NEW_HASHING
#endif
Expand Down
18 changes: 9 additions & 9 deletions lib/rgeo.rb
Expand Up @@ -75,12 +75,12 @@
# database, and based on the postgresql adapter. Available as the
# activerecord-postgis-adapter gem.

require "rgeo/version"
require "rgeo/error"
require "rgeo/feature"
require "rgeo/coord_sys"
require "rgeo/impl_helper"
require "rgeo/wkrep"
require "rgeo/geos"
require "rgeo/cartesian"
require "rgeo/geographic"
require_relative "rgeo/version"
require_relative "rgeo/error"
require_relative "rgeo/feature"
require_relative "rgeo/coord_sys"
require_relative "rgeo/impl_helper"
require_relative "rgeo/wkrep"
require_relative "rgeo/geos"
require_relative "rgeo/cartesian"
require_relative "rgeo/geographic"
14 changes: 7 additions & 7 deletions lib/rgeo/cartesian.rb
Expand Up @@ -6,10 +6,10 @@
# the simple Cartesian implementation. It also provides a namespace
# for Cartesian-specific analysis tools.

require "rgeo/cartesian/calculations"
require "rgeo/cartesian/feature_methods"
require "rgeo/cartesian/feature_classes"
require "rgeo/cartesian/factory"
require "rgeo/cartesian/interface"
require "rgeo/cartesian/bounding_box"
require "rgeo/cartesian/analysis"
require_relative "cartesian/calculations"
require_relative "cartesian/feature_methods"
require_relative "cartesian/feature_classes"
require_relative "cartesian/factory"
require_relative "cartesian/interface"
require_relative "cartesian/bounding_box"
require_relative "cartesian/analysis"
22 changes: 22 additions & 0 deletions lib/rgeo/cartesian/analysis.rb
Expand Up @@ -13,6 +13,28 @@ module Cartesian

module Analysis
class << self
# Check orientation of a ring, returns `true` if it is counter-clockwise
# and false otherwise.
#
# If the factory used is GEOS based, use the GEOS implementation to
# check that. Otherwise, this methods falls back to `ring_direction`.
#
# == Note
#
# This method does not ensure a correct result for an invalid geometry.
# You should make sure your ring is valid beforehand using `is_ring?`
# if you are using a LineString, or directly `valid?` for a
# `linear_ring?`.
# This will be subject to changes in v3.
def ccw?(ring)
if RGeo::Geos.is_capi_geos?(ring) && RGeo::Geos::Analysis.ccw_supported?
RGeo::Geos::Analysis.ccw?(ring)
else
RGeo::Cartesian::Analysis.ring_direction(ring) == 1
end
end
alias counter_clockwise? ccw?

# Given a LineString, which must be a ring, determine whether the
# ring proceeds clockwise or counterclockwise.
# Returns 1 for counterclockwise, or -1 for clockwise.
Expand Down

0 comments on commit 4c46b2b

Please sign in to comment.