kml_contains 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2fbcf0a4ed8ab0c472a4b71a29d6dd44701d4137ebac81c4ca7738b3a5ca03ec
4
+ data.tar.gz: 9079e91ee5602095d782e0641c619bebf631685d2f9b9ddaa60469efc79888df
5
+ SHA512:
6
+ metadata.gz: 63f084937feb4043fa6044ffff06e3bb31add9d3f20035b8b7041ca53f2d907343a8632c95cbb93af1f5569feadaceae71f9da08c833a6292f88a5c082f22bb4
7
+ data.tar.gz: c525d102e0dc0b551601fac286a18dbabfd8e1be000853660407d5b88552fd7b8acf0c223c2613b7d7e9b4fb36d13be01e9b02136a6b5bbc5430a234c5938d9c
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *~
2
+ #*
3
+ *.sw*
4
+ .bundle
5
+ Gemfile.lock
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 4.0.1
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright © 2009 Square, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the “Software”), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # KmlContains
2
+
3
+ Parse a KML file and check if geographic points fall inside the polygons it defines. Supports multiple polygons and polygons with holes (inner boundaries).
4
+
5
+ ## Usage
6
+
7
+ Using [NSW Local Government Areas](https://data.gov.au/data/dataset/nsw-local-government-areas) from data.gov.au as an example — download the ESRI Shapefile (GDA94) and convert it to KML with [GDAL](https://gdal.org):
8
+
9
+ ```bash
10
+ ogr2ogr -f KML nsw-lga.kml nsw_lga.shp
11
+ ```
12
+
13
+ Then in Ruby:
14
+
15
+ ```ruby
16
+ region = KmlContains.parse_kml(File.read('nsw-lga.kml'))
17
+
18
+ sydney = KmlContains::Point.new(151.21, -33.87)
19
+ region.contains_point?(sydney) # => true
20
+ region.contains_point?(151.21, -33.87) # also works
21
+
22
+ melbourne = KmlContains::Point.new(144.96, -37.81)
23
+ region.contains_point?(melbourne) # => false
24
+
25
+ # find which polygon contains a point and access its ExtendedData
26
+ lga = region.find { |p| p.contains_point?(sydney) }
27
+ lga.extended_data['LGA_NAME'] # => "Council of the City of Sydney"
28
+ ```
29
+
30
+ Points are `(longitude, latitude)`. You can pass a `KmlContains::Point`, a longitude/latitude pair, or any object that responds to `x` and `y`.
31
+
32
+ ## Dependencies
33
+
34
+ * [Nokogiri](https://nokogiri.org)
35
+
36
+ ## Credits
37
+
38
+ Originally created as `border_patrol` at Square by **Zach Brock** and **Matt Wilson**.
39
+
40
+ Contributors: **Scott Gonyea**, **Rob Olson**, **Omar Qazi**, **Denis Haskin**, **Tamir Duberstein**, **Erica Kwan**.
41
+
42
+ ## License
43
+
44
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -0,0 +1,21 @@
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
+ require 'kml_contains/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'kml_contains'
6
+ s.version = KmlContains::VERSION
7
+ s.authors = ['Zach Brock', 'Matt Wilson']
8
+ s.description = 'Check if points are inside or outside the region polygons in an imported KML file.'
9
+ s.summary = 'Import and query KML regions'
10
+ s.homepage = 'https://github.com/qapn/kml_contains'
11
+ s.license = 'MIT'
12
+ s.required_ruby_version = '>= 3.0'
13
+
14
+ s.require_paths = ['lib']
15
+ s.files = `git ls-files`.split("\n")
16
+ s.add_runtime_dependency('nokogiri')
17
+
18
+ s.add_development_dependency('benchmark')
19
+ s.add_development_dependency('rake')
20
+ s.add_development_dependency('rspec')
21
+ end
@@ -0,0 +1,32 @@
1
+ module KmlContains
2
+ Point = Struct.new(:x, :y) unless defined?(::KmlContains::Point)
3
+
4
+ class Point
5
+ alias_method :latitude, :y
6
+ alias_method :latitude=, :y=
7
+ alias_method :lat, :y
8
+ alias_method :lat=, :y=
9
+
10
+ alias_method :longitude, :x
11
+ alias_method :longitude=, :x=
12
+ alias_method :lng, :x
13
+ alias_method :lng=, :x=
14
+ alias_method :lon, :x
15
+ alias_method :lon=, :x=
16
+
17
+ # Lots of Map APIs want the coordinates in lat-lng order
18
+ def latlng
19
+ [lat, lon]
20
+ end
21
+ alias_method :coords, :latlng
22
+
23
+ def inspect
24
+ self.class.inspect_string % latlng
25
+ end
26
+
27
+ # IE: #<KmlContains::Point(lat, lng) = (-25.363882, 131.044922)>
28
+ def self.inspect_string
29
+ @inspect_string ||= "#<#{name}(lat, lng) = (%p, %p)>"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,120 @@
1
+ module KmlContains
2
+ class Polygon
3
+ attr_reader :placemark_name, :inner_boundaries, :extended_data
4
+ extend Forwardable
5
+
6
+ # Note @points is the outer boundary.
7
+ # A polygon may also have 1 or more inner boundaries. In order to not change the ctor signature,
8
+ # the inner boundaries are not settable at construction.
9
+ def initialize(*args)
10
+ args.flatten!
11
+ args.uniq!
12
+ raise InsufficientPointsToActuallyFormAPolygonError unless args.size > 2
13
+ @inner_boundaries = []
14
+ @extended_data = {}
15
+ @points = args.dup
16
+ precompute_normalised_bounds
17
+ end
18
+
19
+ def_delegators :@points, :size, :each, :first, :include?, :[], :index
20
+
21
+ def with_inner_boundaries(polygons)
22
+ @inner_boundaries = [polygons].flatten
23
+ self
24
+ end
25
+
26
+ def with_placemark_name(placemark)
27
+ @placemark_name ||= placemark
28
+ self
29
+ end
30
+
31
+ def with_extended_data(data)
32
+ @extended_data = data
33
+ self
34
+ end
35
+
36
+ def ==(other)
37
+ # Do we have the right number of points?
38
+ return false unless other.size == size
39
+
40
+ # Are the points in the right order?
41
+ first, second = first(2)
42
+ index = other.index(first)
43
+ return false unless index
44
+ direction = (other[index - 1] == second ? -1 : 1)
45
+ # Check if the two polygons have the same edges and the same points
46
+ # i.e. [point1, point2, point3] is the same as [point2, point3, point1] is the same as [point3, point2, point1]
47
+ each do |i|
48
+ return false unless i == other[index]
49
+ index += direction
50
+ index = 0 if index == size
51
+ end
52
+ return true if @inner_boundaries.empty?
53
+ @inner_boundaries == other.inner_boundaries
54
+ end
55
+
56
+ # Quick and dirty hash function
57
+ def hash
58
+ @points.map { |point| point.x + point.y }.reduce(&:+).to_i
59
+ end
60
+
61
+ def contains_point?(point)
62
+ return false unless inside_bounding_box?(point)
63
+ px = normalise_lng(point.x)
64
+ c = false
65
+ i = -1
66
+ j = size - 1
67
+ while (i += 1) < size
68
+ iy = self[i].y
69
+ jy = self[j].y
70
+ if (iy <= point.y && point.y < jy) ||
71
+ (jy <= point.y && point.y < iy)
72
+ if px < (@norm_xs[j] - @norm_xs[i]) * (point.y - iy) / (jy - iy) + @norm_xs[i]
73
+ c = !c
74
+ end
75
+ end
76
+ j = i
77
+ end
78
+ return c if c == false
79
+ # Check if excluded by any of the inner boundaries
80
+ @inner_boundaries.each do |inner_boundary|
81
+ return false if inner_boundary.contains_point?(point)
82
+ end
83
+ c
84
+ end
85
+
86
+ def inside_bounding_box?(point)
87
+ px = normalise_lng(point.x)
88
+ !(px < @bb_min_x || px > @bb_max_x ||
89
+ point.y < @bb_min_y || point.y > @bb_max_y)
90
+ end
91
+
92
+ def bounding_box
93
+ [KmlContains::Point.new(@bb_min_x, @bb_max_y),
94
+ KmlContains::Point.new(@bb_max_x, @bb_min_y)]
95
+ end
96
+
97
+ def central_point
98
+ KmlContains.central_point(bounding_box)
99
+ end
100
+
101
+ private
102
+
103
+ def normalise_lng(lng)
104
+ diff = lng - @ref_x
105
+ diff -= 360 while diff > 180
106
+ diff += 360 while diff < -180
107
+ @ref_x + diff
108
+ end
109
+
110
+ def precompute_normalised_bounds
111
+ @ref_x = @points.first.x
112
+ @norm_xs = @points.map { |p| normalise_lng(p.x) }
113
+ ys = @points.map(&:y)
114
+ @bb_min_x = @norm_xs.min
115
+ @bb_max_x = @norm_xs.max
116
+ @bb_min_y = ys.min
117
+ @bb_max_y = ys.max
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,31 @@
1
+ module KmlContains
2
+ class Region < Set
3
+ def contains_point?(*point)
4
+ point = case point.length
5
+ when 1
6
+ point.first
7
+ when 2
8
+ KmlContains::Point.new(point[0], point[1])
9
+ else
10
+ raise ArgumentError, "#{point} is invalid. Arguments can either be an object, or a longitude,lattitude pair."
11
+ end
12
+ any? { |polygon| polygon.contains_point?(point) }
13
+ end
14
+
15
+ # The below are some general helper methods
16
+ def bounding_boxes
17
+ map(&:bounding_box)
18
+ end
19
+
20
+ def bounding_box
21
+ boxes = bounding_boxes
22
+ boxes.flatten!
23
+
24
+ KmlContains.bounding_box(boxes)
25
+ end
26
+
27
+ def central_point
28
+ KmlContains.central_point(bounding_box)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module KmlContains
2
+ VERSION = '0.3.0'
3
+ end
@@ -0,0 +1,90 @@
1
+ require 'set'
2
+ require 'forwardable'
3
+ require 'nokogiri'
4
+ require 'kml_contains/version'
5
+ require 'kml_contains/point'
6
+ require 'kml_contains/polygon'
7
+ require 'kml_contains/region'
8
+
9
+ module KmlContains
10
+ class InsufficientPointsToActuallyFormAPolygonError < ArgumentError; end
11
+
12
+ def self.parse_kml(string)
13
+ doc = Nokogiri::XML(string)
14
+
15
+ polygons = doc.search('Polygon').map do |polygon_kml|
16
+ placemark_name = placemark_name_for_polygon(polygon_kml)
17
+ extended_data = extended_data_for_polygon(polygon_kml)
18
+ parse_kml_polygon_data(polygon_kml.to_s, placemark_name, extended_data)
19
+ end
20
+ KmlContains::Region.new(polygons)
21
+ end
22
+
23
+ def self.bounding_box(points)
24
+ xs = points.map(&:x)
25
+ ys = points.map(&:y)
26
+ [Point.new(xs.min, ys.max), Point.new(xs.max, ys.min)]
27
+ end
28
+
29
+ def self.central_point(box)
30
+ point1, point2 = box
31
+
32
+ x = (point1.x + point2.x) / 2
33
+ y = (point1.y + point2.y) / 2
34
+
35
+ Point.new(x, y)
36
+ end
37
+
38
+ private
39
+
40
+ def self.parse_kml_polygon_data(string, name = nil, extended_data = {})
41
+ doc = Nokogiri::XML(string)
42
+ # "A Polygon is defined by an outer boundary and 0 or more inner boundaries."
43
+ outerboundary = doc.xpath('//outerBoundaryIs')
44
+ innerboundaries = doc.xpath('//innerBoundaryIs')
45
+ coordinates = outerboundary.xpath('.//coordinates').text.strip.split(/\s+/)
46
+ points = points_from_coordinates(coordinates)
47
+ if innerboundaries
48
+ inner_boundary_polygons = innerboundaries.map do |i|
49
+ KmlContains::Polygon.new(points_from_coordinates(i.xpath('.//coordinates').text.strip.split(/\s+/)))
50
+ end
51
+ KmlContains::Polygon.new(points).with_placemark_name(name).with_extended_data(extended_data).with_inner_boundaries(inner_boundary_polygons)
52
+ else
53
+ KmlContains::Polygon.new(points).with_placemark_name(name).with_extended_data(extended_data)
54
+ end
55
+ end
56
+
57
+ def self.points_from_coordinates c
58
+ c.map do |coord|
59
+ x, y, _ = coord.strip.split(',')
60
+ KmlContains::Point.new(x.to_f, y.to_f)
61
+ end
62
+ end
63
+
64
+ def self.placemark_for_polygon(p)
65
+ # A polygon can be contained by a MultiGeometry or Placemark
66
+ parent = p.parent
67
+ parent = parent.parent if parent.name == 'MultiGeometry'
68
+
69
+ return nil unless parent.name == 'Placemark'
70
+
71
+ parent
72
+ end
73
+
74
+ def self.placemark_name_for_polygon(p)
75
+ placemark = placemark_for_polygon(p)
76
+ return nil unless placemark
77
+
78
+ placemark.search('name').text
79
+ end
80
+
81
+ def self.extended_data_for_polygon(p)
82
+ placemark = placemark_for_polygon(p)
83
+ return {} unless placemark
84
+
85
+ data = {}
86
+ placemark.search('Data').each { |d| data[d['name']] = d.search('value').text }
87
+ placemark.search('SimpleData').each { |sd| data[sd['name']] = sd.text }
88
+ data
89
+ end
90
+ end
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby -w
2
+ require 'kml_contains'
3
+ require 'benchmark'
4
+
5
+ colorado_region = KmlContains.parse_kml(File.read('spec/support/colorado-test.kml'))
6
+ multi_polygon_region = KmlContains.parse_kml(File.read('spec/support/multi-polygon-test.kml'))
7
+ Benchmark.bm(20) do |x|
8
+ x.report('colorado region') do
9
+ 10_000.times do |_i|
10
+ multiple = (rand(2) == 1 ? -1 : 1)
11
+ colorado_region.contains_point?(rand * 180 * multiple, rand * 180 * multiple)
12
+ end
13
+ end
14
+
15
+ x.report('multi polygon region') do
16
+ 10_000.times do |_i|
17
+ multiple = (rand(2) == 1 ? -1 : 1)
18
+ multi_polygon_region.contains_point?(rand * 180 * multiple, rand * 180 * multiple)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,238 @@
1
+ require 'spec_helper'
2
+
3
+ describe KmlContains::Polygon do
4
+ describe '==' do
5
+ it 'is true if polygons are congruent' do
6
+ points = [KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0)]
7
+ poly1 = KmlContains::Polygon.new(points)
8
+ poly2 = KmlContains::Polygon.new(points.unshift(points.pop))
9
+
10
+ expect(poly1).to eq(poly2)
11
+ expect(poly2).to eq(poly1)
12
+ poly3 = KmlContains::Polygon.new(points.reverse)
13
+ expect(poly1).to eq(poly3)
14
+ expect(poly3).to eq(poly1)
15
+
16
+ end
17
+
18
+ it 'cares about order of points' do
19
+ points = [KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(5, 5), KmlContains::Point.new(0, 0)]
20
+ poly1 = KmlContains::Polygon.new(points)
21
+ points = [KmlContains::Point.new(5, 5), KmlContains::Point.new(1, 2), KmlContains::Point.new(0, 0), KmlContains::Point.new(3, 4)]
22
+ poly2 = KmlContains::Polygon.new(points)
23
+
24
+ expect(poly1).not_to eq(poly2)
25
+ expect(poly2).not_to eq(poly1)
26
+
27
+ end
28
+
29
+ it 'is false if one polygon is a subset' do
30
+ poly1 = KmlContains::Polygon.new(KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0))
31
+ poly2 = KmlContains::Polygon.new(KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0), KmlContains::Point.new(4, 4))
32
+ expect(poly2).not_to eq(poly1)
33
+ expect(poly1).not_to eq(poly2)
34
+ end
35
+
36
+ it 'is false if the polygons are not congruent' do
37
+ poly1 = KmlContains::Polygon.new(KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0))
38
+ poly2 = KmlContains::Polygon.new(KmlContains::Point.new(2, 1), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0))
39
+ expect(poly2).not_to eq(poly1)
40
+ expect(poly1).not_to eq(poly2)
41
+ end
42
+
43
+ it 'is true if polygons and their inner boundaries are congruent' do
44
+ poly1 = KmlContains::Polygon.new(KmlContains::Point.new(0, 0), KmlContains::Point.new(3, 0), KmlContains::Point.new(3,3), KmlContains::Point.new(0,3))
45
+ poly1.with_inner_boundaries(KmlContains::Polygon.new(KmlContains::Point.new(1,1), KmlContains::Point.new(2, 1), KmlContains::Point.new(2, 2), KmlContains::Point.new(1,2)))
46
+ poly2 = KmlContains::Polygon.new(KmlContains::Point.new(0, 0), KmlContains::Point.new(3, 0), KmlContains::Point.new(3,3), KmlContains::Point.new(0,3))
47
+ poly2.with_inner_boundaries(KmlContains::Polygon.new(KmlContains::Point.new(1,1), KmlContains::Point.new(2, 1), KmlContains::Point.new(2, 2), KmlContains::Point.new(1,2)))
48
+ expect(poly1).to eq(poly2)
49
+ end
50
+
51
+ it 'is false if polygons inner boundaries are not congruent' do
52
+ poly1 = KmlContains::Polygon.new(KmlContains::Point.new(0, 0), KmlContains::Point.new(3, 0), KmlContains::Point.new(3,3), KmlContains::Point.new(0,3))
53
+ poly1.with_inner_boundaries(KmlContains::Polygon.new(KmlContains::Point.new(1,1), KmlContains::Point.new(2, 1), KmlContains::Point.new(2, 2), KmlContains::Point.new(1,2)))
54
+ poly2 = KmlContains::Polygon.new(KmlContains::Point.new(0, 0), KmlContains::Point.new(3, 0), KmlContains::Point.new(3,3), KmlContains::Point.new(0,3))
55
+ poly2.with_inner_boundaries(KmlContains::Polygon.new(KmlContains::Point.new(1.1,1.1), KmlContains::Point.new(2.1, 1.1), KmlContains::Point.new(2.1, 2.1), KmlContains::Point.new(1.1,2.1)))
56
+ expect(poly1).not_to eq(poly2)
57
+ end
58
+ end
59
+
60
+ describe '#initialize' do
61
+ it 'stores a list of points' do
62
+ points = [KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0)]
63
+ polygon = KmlContains::Polygon.new(points)
64
+ points.each do |point|
65
+ expect(polygon).to include point
66
+ end
67
+ end
68
+
69
+ it 'can be instantiated with a arbitrary argument list' do
70
+ points = [KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0)]
71
+ poly1 = KmlContains::Polygon.new(* points)
72
+ poly2 = KmlContains::Polygon.new(points)
73
+ expect(poly1).to eq(poly2)
74
+ end
75
+
76
+ it 'raises if less than 3 points are given' do
77
+ points = [KmlContains::Point.new(1, 2), KmlContains::Point.new(2, 3)]
78
+ expect { KmlContains::Polygon.new(points) }.to raise_exception(KmlContains::InsufficientPointsToActuallyFormAPolygonError)
79
+ points = [KmlContains::Point.new(1, 2)]
80
+ expect { KmlContains::Polygon.new(points) }.to raise_exception(KmlContains::InsufficientPointsToActuallyFormAPolygonError)
81
+ points = []
82
+ expect { KmlContains::Polygon.new(points) }.to raise_exception(KmlContains::InsufficientPointsToActuallyFormAPolygonError)
83
+ end
84
+
85
+ it "doesn't store duplicated points" do
86
+ points = [KmlContains::Point.new(1, 2), KmlContains::Point.new(3, 4), KmlContains::Point.new(0, 0)]
87
+ duplicate_point = [KmlContains::Point.new(1, 2)]
88
+ polygon = KmlContains::Polygon.new(points + duplicate_point)
89
+ expect(polygon.size).to eq(3)
90
+ points.each do |point|
91
+ expect(polygon).to include point
92
+ end
93
+ end
94
+ end
95
+
96
+ describe '#bounding_box' do
97
+ it 'returns the (max top, max left), (max bottom, max right) as points' do
98
+ points = [KmlContains::Point.new(-1, 3), KmlContains::Point.new(4, -3), KmlContains::Point.new(10, 4), KmlContains::Point.new(0, 12)]
99
+ polygon = KmlContains::Polygon.new(points)
100
+ expect(polygon.bounding_box).to eq([KmlContains::Point.new(-1, 12), KmlContains::Point.new(10, -3)])
101
+ end
102
+ end
103
+
104
+ describe '#contains_point?' do
105
+ context 'when there is no inner boundary' do
106
+ before do
107
+ points = [KmlContains::Point.new(-10, 0), KmlContains::Point.new(10, 0), KmlContains::Point.new(0, 10)]
108
+ @polygon = KmlContains::Polygon.new(points)
109
+ end
110
+
111
+ it 'is true if the point is in the polygon' do
112
+ expect(@polygon.contains_point?(KmlContains::Point.new(0.5, 0.5))).to be true
113
+ expect(@polygon.contains_point?(KmlContains::Point.new(0, 5))).to be true
114
+ expect(@polygon.contains_point?(KmlContains::Point.new(-1, 3))).to be true
115
+ end
116
+
117
+ it 'does not include points on the lines with slopes between vertices' do
118
+ expect(@polygon.contains_point?(KmlContains::Point.new(5.0, 5.0))).to be false
119
+ expect(@polygon.contains_point?(KmlContains::Point.new(4.999999, 4.9999999))).to be true
120
+ expect(@polygon.contains_point?(KmlContains::Point.new(0, 0))).to be true
121
+ expect(@polygon.contains_point?(KmlContains::Point.new(0.000001, 0.000001))).to be true
122
+ end
123
+
124
+ it 'includes points at the vertices' do
125
+ expect(@polygon.contains_point?(KmlContains::Point.new(-10, 0))).to be true
126
+ end
127
+
128
+ it 'is false if the point is outside of the polygon' do
129
+ expect(@polygon.contains_point?(KmlContains::Point.new(9, 5))).to be false
130
+ expect(@polygon.contains_point?(KmlContains::Point.new(-5, 8))).to be false
131
+ expect(@polygon.contains_point?(KmlContains::Point.new(-10, -1))).to be false
132
+ expect(@polygon.contains_point?(KmlContains::Point.new(-20, -20))).to be false
133
+ end
134
+ end
135
+
136
+ context 'when the polygon crosses the international date line' do
137
+ before do
138
+ points = [
139
+ KmlContains::Point.new(170, 10),
140
+ KmlContains::Point.new(-170, 10),
141
+ KmlContains::Point.new(-170, -10),
142
+ KmlContains::Point.new(170, -10),
143
+ ]
144
+ @polygon = KmlContains::Polygon.new(points)
145
+ end
146
+
147
+ it 'is true for a point inside the polygon (between 170 and 180)' do
148
+ expect(@polygon.contains_point?(KmlContains::Point.new(175, 0))).to be true
149
+ end
150
+
151
+ it 'is true for a point inside the polygon (between -180 and -170)' do
152
+ expect(@polygon.contains_point?(KmlContains::Point.new(-175, 0))).to be true
153
+ end
154
+
155
+ it 'is false for a point outside the polygon' do
156
+ expect(@polygon.contains_point?(KmlContains::Point.new(0, 0))).to be false
157
+ expect(@polygon.contains_point?(KmlContains::Point.new(160, 0))).to be false
158
+ expect(@polygon.contains_point?(KmlContains::Point.new(-160, 0))).to be false
159
+ end
160
+
161
+ it 'is false for a point outside the latitude range' do
162
+ expect(@polygon.contains_point?(KmlContains::Point.new(175, 15))).to be false
163
+ end
164
+ end
165
+
166
+ context 'when there is an inner boundary' do
167
+ before do
168
+ @polygon = KmlContains::Polygon.new(KmlContains::Point.new(0, 0), KmlContains::Point.new(3, 0), KmlContains::Point.new(3,3), KmlContains::Point.new(0,3))
169
+ @polygon.with_inner_boundaries(KmlContains::Polygon.new(KmlContains::Point.new(1,1), KmlContains::Point.new(2, 1), KmlContains::Point.new(2, 2), KmlContains::Point.new(1,2)))
170
+ end
171
+
172
+ it 'is true if the point is in the polygon but not in the inner boundary' do
173
+ expect(@polygon.contains_point?(KmlContains::Point.new(0.5, 1.5))).to be true
174
+ expect(@polygon.contains_point?(KmlContains::Point.new(0.5, 0.5))).to be true
175
+ expect(@polygon.contains_point?(KmlContains::Point.new(1.5, 0.5))).to be true
176
+ end
177
+
178
+ it 'is false if the point is outside the polygon' do
179
+ expect(@polygon.contains_point?(KmlContains::Point.new(4, 0.5))).to be false
180
+ expect(@polygon.contains_point?(KmlContains::Point.new(2.5, 4))).to be false
181
+ expect(@polygon.contains_point?(KmlContains::Point.new(-1, 1.5))).to be false
182
+ end
183
+
184
+ it 'is false if the point is inside the inner boundary' do
185
+ expect(@polygon.contains_point?(KmlContains::Point.new(1.5, 1.5))).to be false
186
+ end
187
+
188
+ end
189
+ end
190
+
191
+ describe '#inside_bounding_box?' do
192
+ before do
193
+ points = [KmlContains::Point.new(-10, 0), KmlContains::Point.new(10, 0), KmlContains::Point.new(0, 10)]
194
+ @polygon = KmlContains::Polygon.new(points)
195
+ end
196
+
197
+ it 'is false if it is outside the bounding box' do
198
+ expect(@polygon.inside_bounding_box?(KmlContains::Point.new(-10, -1))).to be false
199
+ expect(@polygon.inside_bounding_box?(KmlContains::Point.new(-20, -20))).to be false
200
+ expect(@polygon.inside_bounding_box?(KmlContains::Point.new(1, 20))).to be false
201
+ end
202
+
203
+ it 'returns true if it is inside the bounding box' do
204
+ expect(@polygon.inside_bounding_box?(KmlContains::Point.new(9, 5))).to be true
205
+ expect(@polygon.inside_bounding_box?(KmlContains::Point.new(-5, 8))).to be true
206
+ expect(@polygon.inside_bounding_box?(KmlContains::Point.new(1, 1))).to be true
207
+ end
208
+
209
+ end
210
+
211
+ describe '#with_placemark_name' do
212
+ before(:each) do
213
+ points = [KmlContains::Point.new(-10, 0), KmlContains::Point.new(10, 0), KmlContains::Point.new(0, 10)]
214
+ @polygon = KmlContains::Polygon.new(points)
215
+ end
216
+
217
+ it 'adds a placemark name to a polygon' do
218
+ expect(@polygon.placemark_name).to be_nil
219
+
220
+ @polygon.with_placemark_name('Twin Peaks, San Francisco')
221
+ expect(@polygon.placemark_name).to eq('Twin Peaks, San Francisco')
222
+ end
223
+
224
+ it 'returns the Polygon object' do
225
+ expect(@polygon.with_placemark_name('Silverlake, Los Angeles')).to equal @polygon
226
+ end
227
+
228
+ it 'only allows the placemark name to be set once' do
229
+ expect(@polygon.placemark_name).to be_nil
230
+
231
+ @polygon.with_placemark_name('Santa Clara, California')
232
+ expect(@polygon.placemark_name).to eq('Santa Clara, California')
233
+
234
+ @polygon.with_placemark_name('Santa Cruz, California')
235
+ expect(@polygon.placemark_name).to eq('Santa Clara, California')
236
+ end
237
+ end
238
+ end