From 8826e42b3f40e75a654db9a4937d6a35f2d62dbf Mon Sep 17 00:00:00 2001 From: Jay Ravaliya Date: Fri, 1 May 2026 21:11:40 -0400 Subject: [PATCH] fix: skip self-intersecting polygons in coverage checks (#11) AIRMET (and any source returning hand-drawn polygons) can produce self-intersecting rings that pass construction but raise RGeo::Error::InvalidGeometry on contains?. The error surfaced in every Skywatch.brief call as a partial_failures entry for the whole AIRMET source. Rescue InvalidGeometry per-product in the two .contains? call sites (AdverseFilter.covers? and Outlook#covers?), returning false. Bad polygons are skipped silently; the rest of the source continues. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/skywatch/brief/analysis/adverse_filter.rb | 2 ++ lib/skywatch/nimbus/models/outlook.rb | 2 ++ spec/brief/analysis/adverse_filter_spec.rb | 13 +++++++++++++ spec/nimbus/models/outlook_spec.rb | 14 ++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/lib/skywatch/brief/analysis/adverse_filter.rb b/lib/skywatch/brief/analysis/adverse_filter.rb index b83d3ef..6955e32 100644 --- a/lib/skywatch/brief/analysis/adverse_filter.rb +++ b/lib/skywatch/brief/analysis/adverse_filter.rb @@ -9,6 +9,8 @@ def self.covers?(product, lat, lon) return false if polygon.nil? polygon.contains?(Skywatch::Shared::Geometry.point(lat, lon)) + rescue RGeo::Error::InvalidGeometry + false end def self.within(items, lat:, lon:, radius_nm:) diff --git a/lib/skywatch/nimbus/models/outlook.rb b/lib/skywatch/nimbus/models/outlook.rb index 2b6324b..911189d 100644 --- a/lib/skywatch/nimbus/models/outlook.rb +++ b/lib/skywatch/nimbus/models/outlook.rb @@ -70,6 +70,8 @@ def covers?(lat:, lon:) return false if geometry.nil? geometry.contains?(FACTORY.point(lon, lat)) + rescue RGeo::Error::InvalidGeometry + false end def to_h # rubocop:disable Metrics/MethodLength diff --git a/spec/brief/analysis/adverse_filter_spec.rb b/spec/brief/analysis/adverse_filter_spec.rb index 2fe89f3..90be894 100644 --- a/spec/brief/analysis/adverse_filter_spec.rb +++ b/spec/brief/analysis/adverse_filter_spec.rb @@ -27,6 +27,19 @@ degenerate = Skywatch::Briefer::Models::Sigmet.new(coords: []) expect(described_class.covers?(degenerate, 40.875, -74.282)).to be false end + + it 'returns false when the polygon is self-intersecting (RGeo InvalidGeometry)' do + # Bowtie ring: edges cross — RGeo raises Self-intersection on contains? + bowtie = Skywatch::Briefer::Models::Sigmet.new(coords: [ + Skywatch::Shared::Position.new(lat: 40.0, lon: -74.0), + Skywatch::Shared::Position.new(lat: 41.0, lon: -73.0), + Skywatch::Shared::Position.new(lat: 40.0, lon: -73.0), + Skywatch::Shared::Position.new(lat: 41.0, lon: -74.0), + Skywatch::Shared::Position.new(lat: 40.0, lon: -74.0) + ]) + expect { described_class.covers?(bowtie, 40.5, -73.5) }.not_to raise_error + expect(described_class.covers?(bowtie, 40.5, -73.5)).to be false + end end describe '.within' do diff --git a/spec/nimbus/models/outlook_spec.rb b/spec/nimbus/models/outlook_spec.rb index eb103a3..5ab703c 100644 --- a/spec/nimbus/models/outlook_spec.rb +++ b/spec/nimbus/models/outlook_spec.rb @@ -93,6 +93,20 @@ expect(mrgl.covers?(lat: 41.4, lon: -74.9)).to be(true) expect(slgt.covers?(lat: 41.4, lon: -74.9)).to be(false) end + + it 'returns false when the geometry is self-intersecting (RGeo InvalidGeometry)' do + factory = described_class::FACTORY + bowtie_ring = factory.linear_ring([ + factory.point(-74.0, 40.0), + factory.point(-73.0, 41.0), + factory.point(-73.0, 40.0), + factory.point(-74.0, 41.0), + factory.point(-74.0, 40.0) + ]) + bad = described_class.new(**attrs, geometry: factory.polygon(bowtie_ring)) + expect { bad.covers?(lat: 40.5, lon: -73.5) }.not_to raise_error + expect(bad.covers?(lat: 40.5, lon: -73.5)).to be(false) + end end describe '#to_h' do