diff --git a/History.md b/History.md index 5f0297b0..0c551eed 100644 --- a/History.md +++ b/History.md @@ -2,6 +2,8 @@ * Add SphericalPolygonMethods#centroid #208 (allknowingfrog) * Expand gemspec * Drop Ruby 2.3 support +* 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 diff --git a/Rakefile b/Rakefile index c292855f..ec5005fe 100644 --- a/Rakefile +++ b/Rakefile @@ -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/) diff --git a/ext/geos_c_impl/analysis.c b/ext/geos_c_impl/analysis.c new file mode 100644 index 00000000..0e58ccc5 --- /dev/null +++ b/ext/geos_c_impl/analysis.c @@ -0,0 +1,78 @@ +/* + Analysis methos for GEOS wrapper +*/ + +#include "preface.h" + +#ifdef RGEO_GEOS_SUPPORTED + +#include +#include + +#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 diff --git a/ext/geos_c_impl/analysis.h b/ext/geos_c_impl/analysis.h new file mode 100644 index 00000000..a726bfbd --- /dev/null +++ b/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 + +#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 diff --git a/ext/geos_c_impl/errors.c b/ext/geos_c_impl/errors.c new file mode 100644 index 00000000..50cf4b04 --- /dev/null +++ b/ext/geos_c_impl/errors.c @@ -0,0 +1,35 @@ + +#ifndef RGEO_GEOS_ERROS_INCLUDED +#define RGEO_GEOS_ERROS_INCLUDED + +#include + +#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 diff --git a/ext/geos_c_impl/errors.h b/ext/geos_c_impl/errors.h new file mode 100644 index 00000000..b45ad8a8 --- /dev/null +++ b/ext/geos_c_impl/errors.h @@ -0,0 +1,22 @@ + +#ifndef RGEO_GEOS_ERROS_INCLUDED +#define RGEO_GEOS_ERROS_INCLUDED + +#include + +#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 diff --git a/ext/geos_c_impl/extconf.rb b/ext/geos_c_impl/extconf.rb index 4c4cb596..658e90bc 100644 --- a/ext/geos_c_impl/extconf.rb +++ b/ext/geos_c_impl/extconf.rb @@ -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 diff --git a/ext/geos_c_impl/factory.c b/ext/geos_c_impl/factory.c index 27510331..3c69688f 100644 --- a/ext/geos_c_impl/factory.c +++ b/ext/geos_c_impl/factory.c @@ -16,6 +16,7 @@ #include "line_string.h" #include "polygon.h" #include "geometry_collection.h" +#include "errors.h" RGEO_BEGIN_C @@ -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"); @@ -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) { diff --git a/ext/geos_c_impl/factory.h b/ext/geos_c_impl/factory.h index ee0a25f1..91ab26c3 100644 --- a/ext/geos_c_impl/factory.h +++ b/ext/geos_c_impl/factory.h @@ -10,7 +10,6 @@ RGEO_BEGIN_C - /* Per-interpreter globals. Most of these are cached references to commonly used classes, modules, @@ -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. diff --git a/ext/geos_c_impl/main.c b/ext/geos_c_impl/main.c index bd1dd15a..8d4102c1 100644 --- a/ext/geos_c_impl/main.c +++ b/ext/geos_c_impl/main.c @@ -2,7 +2,6 @@ Main initializer for GEOS wrapper */ - #include "preface.h" #ifdef RGEO_GEOS_SUPPORTED @@ -10,18 +9,20 @@ #include #include +#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 @@ -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 } diff --git a/ext/geos_c_impl/preface.h b/ext/geos_c_impl/preface.h index 4c1aeb4d..002c9248 100644 --- a/ext/geos_c_impl/preface.h +++ b/ext/geos_c_impl/preface.h @@ -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 diff --git a/lib/rgeo.rb b/lib/rgeo.rb index ddcd7ce5..0f2cf1f9 100644 --- a/lib/rgeo.rb +++ b/lib/rgeo.rb @@ -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" diff --git a/lib/rgeo/cartesian.rb b/lib/rgeo/cartesian.rb index edc91998..3bfc57f0 100644 --- a/lib/rgeo/cartesian.rb +++ b/lib/rgeo/cartesian.rb @@ -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" diff --git a/lib/rgeo/cartesian/analysis.rb b/lib/rgeo/cartesian/analysis.rb index feeeb552..40804c0b 100644 --- a/lib/rgeo/cartesian/analysis.rb +++ b/lib/rgeo/cartesian/analysis.rb @@ -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. diff --git a/lib/rgeo/coord_sys.rb b/lib/rgeo/coord_sys.rb index bcc24331..421f7340 100644 --- a/lib/rgeo/coord_sys.rb +++ b/lib/rgeo/coord_sys.rb @@ -6,12 +6,12 @@ # # ----------------------------------------------------------------------------- -require "rgeo/coord_sys/cs/factories" -require "rgeo/coord_sys/cs/entities" -require "rgeo/coord_sys/cs/wkt_parser" -require "rgeo/coord_sys/srs_database/entry" -require "rgeo/coord_sys/srs_database/url_reader" -require "rgeo/coord_sys/srs_database/sr_org" +require_relative "coord_sys/cs/factories" +require_relative "coord_sys/cs/entities" +require_relative "coord_sys/cs/wkt_parser" +require_relative "coord_sys/srs_database/entry" +require_relative "coord_sys/srs_database/url_reader" +require_relative "coord_sys/srs_database/sr_org" module RGeo # This module provides data structures and tools related to coordinate diff --git a/lib/rgeo/error.rb b/lib/rgeo/error.rb index a401b822..212c4756 100644 --- a/lib/rgeo/error.rb +++ b/lib/rgeo/error.rb @@ -14,6 +14,10 @@ module Error class RGeoError < RuntimeError end + # RGeo error specific to the GEOS implementation. + class GeosError < RGeoError + end + # The specified geometry is invalid class InvalidGeometry < RGeoError end diff --git a/lib/rgeo/feature.rb b/lib/rgeo/feature.rb index b8526600..e659b54e 100644 --- a/lib/rgeo/feature.rb +++ b/lib/rgeo/feature.rb @@ -24,20 +24,20 @@ # itself. The implementation should separately document any such # extensions that it may provide. -require "rgeo/feature/factory" -require "rgeo/feature/types" -require "rgeo/feature/geometry" -require "rgeo/feature/point" -require "rgeo/feature/curve" -require "rgeo/feature/line_string" -require "rgeo/feature/linear_ring" -require "rgeo/feature/line" -require "rgeo/feature/surface" -require "rgeo/feature/polygon" -require "rgeo/feature/geometry_collection" -require "rgeo/feature/multi_point" -require "rgeo/feature/multi_curve" -require "rgeo/feature/multi_line_string" -require "rgeo/feature/multi_surface" -require "rgeo/feature/multi_polygon" -require "rgeo/feature/factory_generator" +require_relative "feature/factory" +require_relative "feature/types" +require_relative "feature/geometry" +require_relative "feature/point" +require_relative "feature/curve" +require_relative "feature/line_string" +require_relative "feature/linear_ring" +require_relative "feature/line" +require_relative "feature/surface" +require_relative "feature/polygon" +require_relative "feature/geometry_collection" +require_relative "feature/multi_point" +require_relative "feature/multi_curve" +require_relative "feature/multi_line_string" +require_relative "feature/multi_surface" +require_relative "feature/multi_polygon" +require_relative "feature/factory_generator" diff --git a/lib/rgeo/feature/linear_ring.rb b/lib/rgeo/feature/linear_ring.rb index 9090aa26..f9bac283 100644 --- a/lib/rgeo/feature/linear_ring.rb +++ b/lib/rgeo/feature/linear_ring.rb @@ -23,6 +23,16 @@ module Feature module LinearRing include LineString extend Type + + # Returns +true+ if the ring is oriented in a counter clockwise direction + # otherwise returns +false+. + # + # == Notes + # + # Not a standard SFS method for linear rings, but added for convenience. + def ccw? + raise Error::UnsupportedOperation, "Method LinearRing#ccw? not defined." + end end end end diff --git a/lib/rgeo/geographic.rb b/lib/rgeo/geographic.rb index b9fb97fb..4b2c6db3 100644 --- a/lib/rgeo/geographic.rb +++ b/lib/rgeo/geographic.rb @@ -18,13 +18,13 @@ # See the various class methods of Geographic for more information on # the behaviors of the factories they generate. -require "rgeo/geographic/factory" -require "rgeo/geographic/projected_window" -require "rgeo/geographic/interface" -require "rgeo/geographic/spherical_math" -require "rgeo/geographic/spherical_feature_methods" -require "rgeo/geographic/spherical_feature_classes" -require "rgeo/geographic/proj4_projector" -require "rgeo/geographic/simple_mercator_projector" -require "rgeo/geographic/projected_feature_methods" -require "rgeo/geographic/projected_feature_classes" +require_relative "geographic/factory" +require_relative "geographic/projected_window" +require_relative "geographic/interface" +require_relative "geographic/spherical_math" +require_relative "geographic/spherical_feature_methods" +require_relative "geographic/spherical_feature_classes" +require_relative "geographic/proj4_projector" +require_relative "geographic/simple_mercator_projector" +require_relative "geographic/projected_feature_methods" +require_relative "geographic/projected_feature_classes" diff --git a/lib/rgeo/geos.rb b/lib/rgeo/geos.rb index 73c1e58a..b6a56fff 100644 --- a/lib/rgeo/geos.rb +++ b/lib/rgeo/geos.rb @@ -21,24 +21,24 @@ module RGeo module Geos - require "rgeo/geos/utils" - require "rgeo/geos/interface" + require_relative "geos/utils" + require_relative "geos/interface" begin - require "rgeo/geos/geos_c_impl" + require_relative "geos/geos_c_impl" rescue LoadError # continue end CAPI_SUPPORTED = RGeo::Geos.const_defined?(:CAPIGeometryMethods) if CAPI_SUPPORTED - require "rgeo/geos/capi_feature_classes" - require "rgeo/geos/capi_factory" + require_relative "geos/capi_feature_classes" + require_relative "geos/capi_factory" end - require "rgeo/geos/ffi_feature_methods" - require "rgeo/geos/ffi_feature_classes" - require "rgeo/geos/ffi_factory" - require "rgeo/geos/zm_feature_methods" - require "rgeo/geos/zm_feature_classes" - require "rgeo/geos/zm_factory" + require_relative "geos/ffi_feature_methods" + require_relative "geos/ffi_feature_classes" + require_relative "geos/ffi_factory" + require_relative "geos/zm_feature_methods" + require_relative "geos/zm_feature_classes" + require_relative "geos/zm_factory" # Determine ffi support. begin diff --git a/lib/rgeo/geos/capi_feature_classes.rb b/lib/rgeo/geos/capi_feature_classes.rb index 5cd893f7..8290a2b4 100644 --- a/lib/rgeo/geos/capi_feature_classes.rb +++ b/lib/rgeo/geos/capi_feature_classes.rb @@ -72,6 +72,10 @@ class CAPILinearRingImpl # :nodoc: include CAPIGeometryMethods include CAPILineStringMethods include CAPILinearRingMethods + + def ccw? + RGeo::Cartesian::Analysis.ccw?(self) + end end class CAPILineImpl # :nodoc: diff --git a/lib/rgeo/geos/ffi_feature_methods.rb b/lib/rgeo/geos/ffi_feature_methods.rb index 05e7e2cb..a223a042 100644 --- a/lib/rgeo/geos/ffi_feature_methods.rb +++ b/lib/rgeo/geos/ffi_feature_methods.rb @@ -393,6 +393,10 @@ module FFILinearRingMethods # :nodoc: def geometry_type Feature::LinearRing end + + def ccw? + RGeo::Cartesian::Analysis.ccw?(self) + end end module FFILineMethods # :nodoc: diff --git a/lib/rgeo/impl_helper.rb b/lib/rgeo/impl_helper.rb index fa48a36f..5b7af97e 100644 --- a/lib/rgeo/impl_helper.rb +++ b/lib/rgeo/impl_helper.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "rgeo/impl_helper/utils" -require "rgeo/impl_helper/math" -require "rgeo/impl_helper/basic_geometry_methods" -require "rgeo/impl_helper/basic_geometry_collection_methods" -require "rgeo/impl_helper/basic_point_methods" -require "rgeo/impl_helper/basic_line_string_methods" -require "rgeo/impl_helper/basic_polygon_methods" +require_relative "impl_helper/utils" +require_relative "impl_helper/math" +require_relative "impl_helper/basic_geometry_methods" +require_relative "impl_helper/basic_geometry_collection_methods" +require_relative "impl_helper/basic_point_methods" +require_relative "impl_helper/basic_line_string_methods" +require_relative "impl_helper/basic_polygon_methods" diff --git a/lib/rgeo/impl_helper/basic_line_string_methods.rb b/lib/rgeo/impl_helper/basic_line_string_methods.rb index 353c617b..84233dee 100644 --- a/lib/rgeo/impl_helper/basic_line_string_methods.rb +++ b/lib/rgeo/impl_helper/basic_line_string_methods.rb @@ -89,15 +89,13 @@ def coordinates def contains?(rhs) if Feature::Point === rhs - return contains_point?(rhs) + contains_point?(rhs) else raise(Error::UnsupportedOperation, - "Method LineString#contains? is only defined for Point" - ) + "Method LineString#contains? is only defined for Point") end end - private def contains_point?(point) @@ -174,6 +172,10 @@ def geometry_type Feature::LinearRing end + def ccw? + RGeo::Cartesian::Analysis.ccw?(self) + end + private def validate_geometry diff --git a/lib/rgeo/wkrep.rb b/lib/rgeo/wkrep.rb index 7a8ea94a..a967cdbe 100644 --- a/lib/rgeo/wkrep.rb +++ b/lib/rgeo/wkrep.rb @@ -21,7 +21,7 @@ # To parse a byte string in WKB (well-known binary) format back into a # geometry object, use the WKRep::WKBParser class. -require "rgeo/wkrep/wkt_parser" -require "rgeo/wkrep/wkt_generator" -require "rgeo/wkrep/wkb_parser" -require "rgeo/wkrep/wkb_generator" +require_relative "wkrep/wkt_parser" +require_relative "wkrep/wkt_generator" +require_relative "wkrep/wkb_parser" +require_relative "wkrep/wkb_generator" diff --git a/rgeo.gemspec b/rgeo.gemspec index f9b93845..4b455cec 100644 --- a/rgeo.gemspec +++ b/rgeo.gemspec @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "./lib/rgeo/version" +require_relative "lib/rgeo/version" Gem::Specification.new do |spec| spec.name = "rgeo" @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.4.0" spec.license = "BSD-3-Clause" - spec.files = Dir["lib/**/*.rb", "ext/**/*.{rb,c,h}", "LICENSE.txt"] + spec.files = Dir["lib/**/*.rb", "ext/**/*.{rb,c,h}", "LICENSE.txt", "README.md"] spec.extensions = Dir["ext/*/extconf.rb"] spec.add_development_dependency "ffi-geos", "~> 1.2" diff --git a/test/cartesian_analysis_test.rb b/test/cartesian_analysis_test.rb index 88c017d4..5cffa773 100644 --- a/test/cartesian_analysis_test.rb +++ b/test/cartesian_analysis_test.rb @@ -6,79 +6,172 @@ # # ----------------------------------------------------------------------------- -require "test_helper" +require_relative "test_helper" class CartesianAnalysisTest < Minitest::Test # :nodoc: + class Fixtures + def initialize(factory) + @factory = factory + 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]) + end + + def clockwise_triangle + p1 = @factory.point(1, 1) + p2 = @factory.point(2, 4) + p3 = @factory.point(5, 2) + @factory.line_string([p1, p2, p3, p1]) + end + + def counterclockwise_triangle + p1 = @factory.point(1, 1) + p2 = @factory.point(2, 4) + p3 = @factory.point(5, 2) + @factory.line_string([p1, p3, p2, p1]) + end + + def clockwise_puckered_quad + p1 = @factory.point(1, 1) + p2 = @factory.point(2, 6) + p3 = @factory.point(3, 3) + p4 = @factory.point(5, 2) + @factory.line_string([p1, p2, p3, p4, p1]) + end + + def counterclockwise_puckered_quad + p1 = @factory.point(1, 1) + p2 = @factory.point(2, 6) + p3 = @factory.point(3, 3) + p4 = @factory.point(5, 2) + @factory.line_string([p1, p4, p3, p2, p1]) + end + + def clockwise_hat + p1 = @factory.point(1, 2) + p2 = @factory.point(2, 3) + p3 = @factory.point(3, 2) + p4 = @factory.point(2, 1) + p5 = @factory.point(2, 0) + p6 = @factory.point(0, 2) + @factory.line_string([p1, p2, p3, p4, p5, p6, p1]) + end + + def counterclockwise_hat + p1 = @factory.point(2, 1) + p2 = @factory.point(3, 2) + p3 = @factory.point(2, 3) + p4 = @factory.point(1, 2) + p5 = @factory.point(0, 2) + p6 = @factory.point(2, 0) + @factory.line_string([p1, p2, p3, p4, p5, p6, p1]) + end + + def counterclockwise_near_circle + p1 = @factory.point(0, -3) + p2 = @factory.point(2, -2) + p3 = @factory.point(3, 0) + p4 = @factory.point(2, 2) + p5 = @factory.point(0, 3) + p6 = @factory.point(-2, 2) + p7 = @factory.point(-3, 0) + p8 = @factory.point(-2, -2) + @factory.line_string([p1, p2, p3, p4, p5, p6, p7, p8, p1]) + end + end + def setup - @factory = RGeo::Cartesian.simple_factory + @fixtures = Fixtures.new(RGeo::Cartesian.simple_factory) + end + + # --------------------------------------------- RGeo::Cartesian::Analysis.ccw? + + def test_ccw_p_clockwise_triangle + ring = @fixtures.clockwise_triangle + assert_equal( + false, + RGeo::Cartesian::Analysis.ccw?(ring), + "falls back to ring_direction" + ) + end + + def test_ccw_p_counterclockwise_triangle + ring = @fixtures.counterclockwise_triangle + assert_equal( + true, + RGeo::Cartesian::Analysis.ccw?(ring), + "falls back to ring_direction" + ) + end + + def test_ccw_p_clockwise_puckered_quad + ring = @fixtures.clockwise_puckered_quad + assert_equal(false, RGeo::Cartesian::Analysis.ccw?(ring)) + end + + def test_ccw_p_counterclockwise_puckered_quad + ring = @fixtures.counterclockwise_puckered_quad + assert_equal(true, RGeo::Cartesian::Analysis.ccw?(ring)) + end + + def test_ccw_p_clockwise_hat + ring = @fixtures.clockwise_hat + assert_equal(false, RGeo::Cartesian::Analysis.ccw?(ring)) + end + + def test_ccw_p_counterclockwise_hat + ring = @fixtures.counterclockwise_hat + assert_equal(true, RGeo::Cartesian::Analysis.ccw?(ring)) + end + + def test_ccw_p_counterclockwise_near_circle + ring = @fixtures.counterclockwise_near_circle + assert_equal(true, RGeo::Cartesian::Analysis.ccw?(ring)) + end + + # ----------------------------------- RGeo::Cartesian::Analysis.ring_direction + + def test_ring_direction_line + ring = @fixtures.line + assert_equal(0, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_clockwise_triangle - p1 = @factory.point(1, 1) - p2 = @factory.point(2, 4) - p3 = @factory.point(5, 2) - ring = @factory.line_string([p1, p2, p3, p1]) + ring = @fixtures.clockwise_triangle assert_equal(-1, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_counterclockwise_triangle - p1 = @factory.point(1, 1) - p2 = @factory.point(2, 4) - p3 = @factory.point(5, 2) - ring = @factory.line_string([p1, p3, p2, p1]) + ring = @fixtures.counterclockwise_triangle assert_equal(1, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_clockwise_puckered_quad - p1 = @factory.point(1, 1) - p2 = @factory.point(2, 6) - p3 = @factory.point(3, 3) - p4 = @factory.point(5, 2) - ring = @factory.line_string([p1, p2, p3, p4, p1]) + ring = @fixtures.clockwise_puckered_quad assert_equal(-1, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_counterclockwise_puckered_quad - p1 = @factory.point(1, 1) - p2 = @factory.point(2, 6) - p3 = @factory.point(3, 3) - p4 = @factory.point(5, 2) - ring = @factory.line_string([p1, p4, p3, p2, p1]) + ring = @fixtures.counterclockwise_puckered_quad assert_equal(1, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_clockwise_hat - p1 = @factory.point(1, 2) - p2 = @factory.point(2, 3) - p3 = @factory.point(3, 2) - p4 = @factory.point(2, 1) - p5 = @factory.point(2, 0) - p6 = @factory.point(0, 2) - ring = @factory.line_string([p1, p2, p3, p4, p5, p6, p1]) + ring = @fixtures.clockwise_hat assert_equal(-1, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_counterclockwise_hat - p1 = @factory.point(2, 1) - p2 = @factory.point(3, 2) - p3 = @factory.point(2, 3) - p4 = @factory.point(1, 2) - p5 = @factory.point(0, 2) - p6 = @factory.point(2, 0) - ring = @factory.line_string([p1, p2, p3, p4, p5, p6, p1]) + ring = @fixtures.counterclockwise_hat assert_equal(1, RGeo::Cartesian::Analysis.ring_direction(ring)) end def test_ring_direction_counterclockwise_near_circle - p1 = @factory.point(0, -3) - p2 = @factory.point(2, -2) - p3 = @factory.point(3, 0) - p4 = @factory.point(2, 2) - p5 = @factory.point(0, 3) - p6 = @factory.point(-2, 2) - p7 = @factory.point(-3, 0) - p8 = @factory.point(-2, -2) - ring = @factory.line_string([p1, p2, p3, p4, p5, p6, p7, p8, p1]) + ring = @fixtures.counterclockwise_near_circle assert_equal(1, RGeo::Cartesian::Analysis.ring_direction(ring)) end end diff --git a/test/common/line_string_tests.rb b/test/common/line_string_tests.rb index bedc4ff0..701aa22c 100644 --- a/test/common/line_string_tests.rb +++ b/test/common/line_string_tests.rb @@ -341,6 +341,17 @@ def test_contains_point assert_equal(true, line_string.contains?(@factory.point(0.5, 1))) assert_equal(false, line_string.contains?(@factory.point(-1, -1))) end + + def test_linear_ring_ccw + point1 = @factory.point(1, 1) + point2 = @factory.point(2, 0) + point3 = @factory.point(0, 0) + cw_ring = @factory.linear_ring([point1, point2, point3, point1]) + ccw_ring = @factory.linear_ring([point3, point2, point1, point3]) + + assert_equal(false, cw_ring.ccw?) + assert_equal(true, ccw_ring.ccw?) + end end end end diff --git a/test/common/multi_polygon_tests.rb b/test/common/multi_polygon_tests.rb index 61e07679..c0d99d99 100644 --- a/test/common/multi_polygon_tests.rb +++ b/test/common/multi_polygon_tests.rb @@ -200,7 +200,7 @@ def test_boundary parsed_coordinates.zip(boundary_coordinates).each do |parsed_line, boundary_line| parsed_line.zip(boundary_line).each do |p_coord, b_coord| p_coord.zip(b_coord).each do |p_val, b_val| - assert_in_delta(p_val, b_val, 0.00000000000001) + assert_in_delta(p_val, b_val, 1e-13) end end end diff --git a/test/geos_capi/analysis_test.rb b/test/geos_capi/analysis_test.rb new file mode 100644 index 00000000..bf3f217c --- /dev/null +++ b/test/geos_capi/analysis_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module GeosCapi + class AnalysisTest < CartesianAnalysisTest + def setup + skip "Needs GEOS." unless RGeo::Geos.capi_supported? + @fixtures = Fixtures.new(RGeo::Geos.factory) + end + + def test_ccw_p_raises_if_not_a_geos_object + skip "Needs GEOS 3.7+" unless RGeo::Geos::Analysis.ccw_supported? + factory = RGeo::Cartesian.simple_factory + pt1 = factory.point(1, 0) + pt2 = factory.point(2, 0) + pt3 = factory.point(2, 1) + ring = factory.line_string([pt1, pt2, pt3, pt1]) + assert_raises(RGeo::Error::RGeoError) { RGeo::Geos::Analysis.ccw?(ring) } + end + + def test_ccw_p_raises_if_no_coordseq + skip "Needs GEOS 3.7+" unless RGeo::Geos::Analysis.ccw_supported? + factory = RGeo::Geos.factory(native_interface: :capi) + point = factory.point(1, 2) + assert_raises(RGeo::Error::GeosError) { RGeo::Geos::Analysis.ccw?(point) } + end + + def test_ccw_p_returns_true_if_ccw + skip "Needs GEOS 3.7+" unless RGeo::Geos::Analysis.ccw_supported? + factory = RGeo::Geos.factory(native_interface: :capi) + pt1 = factory.point(1, 0) + pt2 = factory.point(2, 0) + pt3 = factory.point(2, 1) + sequence = [pt1, pt2, pt3, pt1] + ring_ccw = factory.line_string(sequence) + ring_cw = factory.line_string(sequence.reverse) + assert_equal(false, RGeo::Geos::Analysis.ccw?(ring_cw)) + assert_equal(true, RGeo::Geos::Analysis.ccw?(ring_ccw)) + end + + # no need to re-test the ruby implementation + methods.grep(/\Atest_ring_direction/).each { |meth| undef_method meth } + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 4157d111..da13b9e8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true require "minitest/autorun" -require "rgeo" +require_relative "../lib/rgeo" require "psych" -require "common/factory_tests" -require "common/geometry_collection_tests" -require "common/line_string_tests" -require "common/multi_line_string_tests" -require "common/multi_point_tests" -require "common/multi_polygon_tests" -require "common/point_tests" -require "common/polygon_tests" +require_relative "common/factory_tests" +require_relative "common/geometry_collection_tests" +require_relative "common/line_string_tests" +require_relative "common/multi_line_string_tests" +require_relative "common/multi_point_tests" +require_relative "common/multi_polygon_tests" +require_relative "common/point_tests" +require_relative "common/polygon_tests" require "pry-byebug" if ENV["BYEBUG"]