Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions lib/border_patrol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,26 @@ def self.central_point(box)

def self.parse_kml_polygon_data(string, name = nil)
doc = Nokogiri::XML(string)
coordinates = doc.xpath('//coordinates').text.strip.split(/\s+/)
points = coordinates.map do |coord|
# "A Polygon is defined by an outer boundary and 0 or more inner boundaries."
outerboundary = doc.xpath('//outerBoundaryIs')
innerboundaries = doc.xpath('//innerBoundaryIs')
coordinates = outerboundary.xpath('.//coordinates').text.strip.split(/\s+/)
points = points_from_coordinates(coordinates)
if innerboundaries
inner_boundary_polygons = innerboundaries.map do |i|
BorderPatrol::Polygon.new(points_from_coordinates(i.xpath('.//coordinates').text.strip.split(/\s+/)))
end
BorderPatrol::Polygon.new(points).with_placemark_name(name).with_inner_boundaries(inner_boundary_polygons)
else
BorderPatrol::Polygon.new(points).with_placemark_name(name)
end
end

def self.points_from_coordinates c
c.map do |coord|
x, y, _ = coord.strip.split(',')
BorderPatrol::Point.new(x.to_f, y.to_f)
end
BorderPatrol::Polygon.new(points).with_placemark_name(name)
end

def self.placemark_name_for_polygon(p)
Expand Down
20 changes: 18 additions & 2 deletions lib/border_patrol/polygon.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
module BorderPatrol
class Polygon
attr_reader :placemark_name
attr_reader :placemark_name, :inner_boundaries
extend Forwardable

# Note @points is the outer boundary.
# A polygon may also have 1 or more inner boundaries. In order to not change the ctor signature,
# the inner boundaries are not settable at construction.
def initialize(*args)
args.flatten!
args.uniq!
fail InsufficientPointsToActuallyFormAPolygonError unless args.size > 2
@inner_boundaries = []
@points = Array.new(args)
end

def_delegators :@points, :size, :each, :first, :include?, :[], :index

def with_inner_boundaries(polygons)
@inner_boundaries = [polygons].flatten
self
end

def with_placemark_name(placemark)
@placemark_name ||= placemark
self
Expand All @@ -32,7 +42,8 @@ def ==(other)
index += direction
index = 0 if index == size
end
true
return true if @inner_boundaries.empty?
@inner_boundaries == other.inner_boundaries
end

# Quick and dirty hash function
Expand All @@ -54,6 +65,11 @@ def contains_point?(point)
end
j = i
end
return c if c == false
# Check if excluded by any of the inner boundaries
@inner_boundaries.each do |inner_boundary|
return false if inner_boundary.contains_point?(point)
end
c
end

Expand Down
86 changes: 64 additions & 22 deletions spec/lib/border_patrol/polygon_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@
expect(poly2).not_to eq(poly1)
expect(poly1).not_to eq(poly2)
end

it 'is true if polygons and their inner boundaries are congruent' do
poly1 = BorderPatrol::Polygon.new(BorderPatrol::Point.new(0, 0), BorderPatrol::Point.new(3, 0), BorderPatrol::Point.new(3,3), BorderPatrol::Point.new(0,3))
poly1.with_inner_boundaries(BorderPatrol::Polygon.new(BorderPatrol::Point.new(1,1), BorderPatrol::Point.new(2, 1), BorderPatrol::Point.new(2, 2), BorderPatrol::Point.new(1,2)))
poly2 = BorderPatrol::Polygon.new(BorderPatrol::Point.new(0, 0), BorderPatrol::Point.new(3, 0), BorderPatrol::Point.new(3,3), BorderPatrol::Point.new(0,3))
poly2.with_inner_boundaries(BorderPatrol::Polygon.new(BorderPatrol::Point.new(1,1), BorderPatrol::Point.new(2, 1), BorderPatrol::Point.new(2, 2), BorderPatrol::Point.new(1,2)))
expect(poly1).to eq(poly2)
end

it 'is false if polygons inner boundaries are not congruent' do
poly1 = BorderPatrol::Polygon.new(BorderPatrol::Point.new(0, 0), BorderPatrol::Point.new(3, 0), BorderPatrol::Point.new(3,3), BorderPatrol::Point.new(0,3))
poly1.with_inner_boundaries(BorderPatrol::Polygon.new(BorderPatrol::Point.new(1,1), BorderPatrol::Point.new(2, 1), BorderPatrol::Point.new(2, 2), BorderPatrol::Point.new(1,2)))
poly2 = BorderPatrol::Polygon.new(BorderPatrol::Point.new(0, 0), BorderPatrol::Point.new(3, 0), BorderPatrol::Point.new(3,3), BorderPatrol::Point.new(0,3))
poly2.with_inner_boundaries(BorderPatrol::Polygon.new(BorderPatrol::Point.new(1.1,1.1), BorderPatrol::Point.new(2.1, 1.1), BorderPatrol::Point.new(2.1, 2.1), BorderPatrol::Point.new(1.1,2.1)))
expect(poly1).not_to eq(poly2)
end
end

describe '#initialize' do
Expand Down Expand Up @@ -86,33 +102,59 @@
end

describe '#contains_point?' do
before do
points = [BorderPatrol::Point.new(-10, 0), BorderPatrol::Point.new(10, 0), BorderPatrol::Point.new(0, 10)]
@polygon = BorderPatrol::Polygon.new(points)
end
context 'when there is no inner boundary' do
before do
points = [BorderPatrol::Point.new(-10, 0), BorderPatrol::Point.new(10, 0), BorderPatrol::Point.new(0, 10)]
@polygon = BorderPatrol::Polygon.new(points)
end

it 'is true if the point is in the polygon' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(0.5, 0.5))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0, 5))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(-1, 3))).to be true
end
it 'is true if the point is in the polygon' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(0.5, 0.5))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0, 5))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(-1, 3))).to be true
end

it 'does not include points on the lines with slopes between vertices' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(5.0, 5.0))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(4.999999, 4.9999999))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0, 0))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0.000001, 0.000001))).to be true
end
it 'does not include points on the lines with slopes between vertices' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(5.0, 5.0))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(4.999999, 4.9999999))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0, 0))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0.000001, 0.000001))).to be true
end

it 'includes points at the vertices' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(-10, 0))).to be true
end

it 'includes points at the vertices' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(-10, 0))).to be true
it 'is false if the point is outside of the polygon' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(9, 5))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-5, 8))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-10, -1))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-20, -20))).to be false
end
end

it 'is false if the point is outside of the polygon' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(9, 5))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-5, 8))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-10, -1))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-20, -20))).to be false
context 'when there is an inner boundary' do
before do
@polygon = BorderPatrol::Polygon.new(BorderPatrol::Point.new(0, 0), BorderPatrol::Point.new(3, 0), BorderPatrol::Point.new(3,3), BorderPatrol::Point.new(0,3))
@polygon.with_inner_boundaries(BorderPatrol::Polygon.new(BorderPatrol::Point.new(1,1), BorderPatrol::Point.new(2, 1), BorderPatrol::Point.new(2, 2), BorderPatrol::Point.new(1,2)))
end

it 'is true if the point is in the polygon but not in the inner boundary' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(0.5, 1.5))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(0.5, 0.5))).to be true
expect(@polygon.contains_point?(BorderPatrol::Point.new(1.5, 0.5))).to be true
end

it 'is false if the point is outside the polygon' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(4, 0.5))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(2.5, 4))).to be false
expect(@polygon.contains_point?(BorderPatrol::Point.new(-1, 1.5))).to be false
end

it 'is false if the point is inside the inner boundary' do
expect(@polygon.contains_point?(BorderPatrol::Point.new(1.5, 1.5))).to be false
end

end
end

Expand Down
65 changes: 65 additions & 0 deletions spec/lib/border_patrol_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,69 @@
end
end
end


describe "KMLs with with holes" do
before(:each) do
@outside = [
BorderPatrol::Point.new(-74.0063, 40.72368),
BorderPatrol::Point.new(-74.00678, 40.71912),
BorderPatrol::Point.new(-74.00686, 40.71571),
BorderPatrol::Point.new(-74.00201, 40.71554),
BorderPatrol::Point.new(-73.99695, 40.71558),
]

@in_the_polygon = [
BorderPatrol::Point.new(-74.00193, 40.72299),
BorderPatrol::Point.new(-74.00188, 40.71951),
BorderPatrol::Point.new(-73.99669, 40.71942),
]

@in_the_hole = [
BorderPatrol::Point.new(-73.99618, 40.72338)
]
end

it 'correctly identifies points inside and outside polygon which has 1 hole' do
region = BorderPatrol.parse_kml(File.read('spec/support/polygon-with-hole-test1.kml'))
@outside.each do |p|
expect(region.contains_point?(p)).to be false
end
@in_the_polygon.each do |p|
expect(region.contains_point?(p)).to be true
end
@in_the_hole.each do |p|
expect(region.contains_point?(p)).to be false
end
end

it 'correctly identifies points inside and outside polygon which has 2 holes' do
region = BorderPatrol.parse_kml(File.read('spec/support/polygon-with-2-holes.kml'))
@outside.each do |p|
expect(region.contains_point?(p)).to be false
end
@in_the_polygon.each do |p|
expect(region.contains_point?(p)).to be true
end
@in_the_hole.each do |p|
expect(region.contains_point?(p)).to be false
end
end

it 'correctly identifies points inside and outside polygon with real-world polygon' do
region = BorderPatrol.parse_kml(File.read('spec/support/45.kml'))
in1 = BorderPatrol::Point.new(-73.80001, 40.87513)
in_hole1 = BorderPatrol::Point.new(-73.79593, 40.87487)
outside1 = BorderPatrol::Point.new(-73.77881, 40.87393)
way_out1 = BorderPatrol::Point.new(-73.76242, 40.88194)
in_another_polygon = BorderPatrol::Point.new(-73.77134, 40.8712)
expect(region.contains_point?(in1)).to be true
expect(region.contains_point?(in_hole1)).to be false
expect(region.contains_point?(outside1)).to be false
expect(region.contains_point?(way_out1)).to be false
expect(region.contains_point?(in_another_polygon)).to be true
end

end

end
24 changes: 24 additions & 0 deletions spec/support/45.kml

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions spec/support/polygon-with-2-holes.kml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version='1.0' encoding='UTF-8'?>
<kml xmlns='http://www.opengis.net/kml/2.2'>
<Document>
<name>polygon-with-2-holes</name>
<description><![CDATA[]]></description>
<Folder>
<name>polygon-with-2-holes</name>
<Placemark>
<name>Outer 1</name>
<styleUrl>#poly-000000-1-77-nodesc</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<tessellate>1</tessellate>
<coordinates>-74.0041208,40.72948650000001,0.0 -74.00425,40.717924,0.0 -73.983135,40.71818400000001,0.0 -73.983393,40.729763,0.0 -74.0041208,40.72948650000001,0.0</coordinates>
</LinearRing>
</outerBoundaryIs>
<innerBoundaryIs>
<LinearRing>
<tessellate>1</tessellate>
<coordinates>-73.999872,40.726186,0.0 -73.999872,40.721567,0.0 -73.9946795,40.72156710000001,0.0 -73.9947009,40.7262994,0.0 -73.999872,40.726186,0.0</coordinates>
</LinearRing>
</innerBoundaryIs>
<innerBoundaryIs>
<LinearRing>
<tessellate>1</tessellate>
<coordinates>-73.9924049,40.7263481,0.0 -73.9922547,40.721567099999994,0.0 -73.9868259,40.7215671,0.0 -73.9871049,40.72642940000001,0.0 -73.9924049,40.7263481,0.0</coordinates>
</LinearRing>
</innerBoundaryIs>
</Polygon>
</Placemark>
</Folder>
<Style id='poly-000000-1-77-nodesc-normal'>
<LineStyle>
<color>ff000000</color>
<width>1</width>
</LineStyle>
<PolyStyle>
<color>4D000000</color>
<fill>1</fill>
<outline>1</outline>
</PolyStyle>
<BalloonStyle>
<text><![CDATA[<h3>$[name]</h3>]]></text>
</BalloonStyle>
</Style>
<Style id='poly-000000-1-77-nodesc-highlight'>
<LineStyle>
<color>ff000000</color>
<width>2.0</width>
</LineStyle>
<PolyStyle>
<color>4D000000</color>
<fill>1</fill>
<outline>1</outline>
</PolyStyle>
<BalloonStyle>
<text><![CDATA[<h3>$[name]</h3>]]></text>
</BalloonStyle>
</Style>
<StyleMap id='poly-000000-1-77-nodesc'>
<Pair>
<key>normal</key>
<styleUrl>#poly-000000-1-77-nodesc-normal</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#poly-000000-1-77-nodesc-highlight</styleUrl>
</Pair>
</StyleMap>
</Document>
</kml>
66 changes: 66 additions & 0 deletions spec/support/polygon-with-hole-test1.kml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version='1.0' encoding='UTF-8'?>
<kml xmlns='http://www.opengis.net/kml/2.2'>
<Document>
<name>Polygon with hole test 1</name>
<description><![CDATA[]]></description>
<Folder>
<name>Polygon with hole test</name>
<Placemark>
<name>Outer 1</name>
<styleUrl>#poly-000000-1-77-nodesc</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<tessellate>1</tessellate>
<coordinates>-74.0039921,40.7295028,0.0 -74.0042496,40.7179242,0.0 -73.9831352,40.7181844,0.0 -73.9833927,40.7297629,0.0 -74.0039921,40.7295028,0.0</coordinates>
</LinearRing>
</outerBoundaryIs>
<innerBoundaryIs>
<LinearRing>
<tessellate>1</tessellate>
<coordinates>-73.9998722,40.7261855,0.0 -73.9998722,40.7215671,0.0 -73.9895725,40.7215671,0.0 -73.9897442,40.7263807,0.0 -73.9998722,40.7261855,0.0</coordinates>
</LinearRing>
</innerBoundaryIs>
</Polygon>
</Placemark>
</Folder>
<Style id='poly-000000-1-77-nodesc-normal'>
<LineStyle>
<color>ff000000</color>
<width>1</width>
</LineStyle>
<PolyStyle>
<color>4D000000</color>
<fill>1</fill>
<outline>1</outline>
</PolyStyle>
<BalloonStyle>
<text><![CDATA[<h3>$[name]</h3>]]></text>
</BalloonStyle>
</Style>
<Style id='poly-000000-1-77-nodesc-highlight'>
<LineStyle>
<color>ff000000</color>
<width>2.0</width>
</LineStyle>
<PolyStyle>
<color>4D000000</color>
<fill>1</fill>
<outline>1</outline>
</PolyStyle>
<BalloonStyle>
<text><![CDATA[<h3>$[name]</h3>]]></text>
</BalloonStyle>
</Style>
<StyleMap id='poly-000000-1-77-nodesc'>
<Pair>
<key>normal</key>
<styleUrl>#poly-000000-1-77-nodesc-normal</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#poly-000000-1-77-nodesc-highlight</styleUrl>
</Pair>
</StyleMap>
</Document>
</kml>