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 +7 -0
- data/.gitignore +5 -0
- data/.tool-versions +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +19 -0
- data/README.md +44 -0
- data/Rakefile +7 -0
- data/kml_contains.gemspec +21 -0
- data/lib/kml_contains/point.rb +32 -0
- data/lib/kml_contains/polygon.rb +120 -0
- data/lib/kml_contains/region.rb +31 -0
- data/lib/kml_contains/version.rb +3 -0
- data/lib/kml_contains.rb +90 -0
- data/script/benchmark.rb +21 -0
- data/spec/lib/kml_contains/polygon_spec.rb +238 -0
- data/spec/lib/kml_contains/region_spec.rb +50 -0
- data/spec/lib/kml_contains_spec.rb +350 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/45.kml +24 -0
- data/spec/support/colorado-test.kml +37 -0
- data/spec/support/elgin-opengis-ns-test.kml +201 -0
- data/spec/support/multi-polygon-test.kml +100 -0
- data/spec/support/polygon-with-2-holes.kml +72 -0
- data/spec/support/polygon-with-hole-test1.kml +66 -0
- metadata +119 -0
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/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 4.0.1
|
data/Gemfile
ADDED
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,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
|
data/lib/kml_contains.rb
ADDED
|
@@ -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
|
data/script/benchmark.rb
ADDED
|
@@ -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
|