geo_graf 1.0.2
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 +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/Guardfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +90 -0
- data/Rakefile +9 -0
- data/geo_graf.gemspec +20 -0
- data/lib/geo_graf.rb +10 -0
- data/lib/geo_graf/intersection_calculator.rb +45 -0
- data/lib/geo_graf/polygon.rb +83 -0
- data/spec/geo_graf/intersection_calculator_spec.rb +215 -0
- data/spec/geo_graf/polygon_spec.rb +146 -0
- data/spec/geo_graf_spec.rb +22 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b2efd6937e6852e7b2fc4d422629843b005acbf8
|
4
|
+
data.tar.gz: 3a832e48ff664b270f751f33fd070098189301cd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 47a2b6cfa2e69e6a60985f654fc52f2fea73db5da92d1364500c99af7233ffea763044271009f1bb3481f351add9b69fe5745ef58920a6c54327f9dd22d3bc04
|
7
|
+
data.tar.gz: c2f0a08bf4bb9f17a7bfb8a46ff4f5a4e59eaf840762e721349b4a377c9fb9f8d2c947b4d5aed429b28980915785557aff8dd9c8990bcd0fe44603f58bb4040e
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
geo_graf (1.0.2)
|
5
|
+
rgeo
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
celluloid (0.15.2)
|
11
|
+
timers (~> 1.1.0)
|
12
|
+
coderay (1.0.9)
|
13
|
+
diff-lcs (1.2.4)
|
14
|
+
ffi (1.9.0)
|
15
|
+
formatador (0.2.4)
|
16
|
+
guard (2.0.3)
|
17
|
+
formatador (>= 0.2.4)
|
18
|
+
listen (~> 2.0)
|
19
|
+
lumberjack (~> 1.0)
|
20
|
+
pry (>= 0.9.12)
|
21
|
+
thor (>= 0.18.1)
|
22
|
+
guard-rspec (3.1.0)
|
23
|
+
guard (>= 1.8)
|
24
|
+
rspec (~> 2.13)
|
25
|
+
listen (2.0.3)
|
26
|
+
celluloid (>= 0.15.2)
|
27
|
+
rb-fsevent (>= 0.9.3)
|
28
|
+
rb-inotify (>= 0.9)
|
29
|
+
lumberjack (1.0.4)
|
30
|
+
method_source (0.8.2)
|
31
|
+
pry (0.9.12.2)
|
32
|
+
coderay (~> 1.0.5)
|
33
|
+
method_source (~> 0.8)
|
34
|
+
slop (~> 3.4)
|
35
|
+
rake (10.1.0)
|
36
|
+
rb-fsevent (0.9.3)
|
37
|
+
rb-inotify (0.9.2)
|
38
|
+
ffi (>= 0.5.0)
|
39
|
+
rgeo (0.3.20)
|
40
|
+
rspec (2.14.1)
|
41
|
+
rspec-core (~> 2.14.0)
|
42
|
+
rspec-expectations (~> 2.14.0)
|
43
|
+
rspec-mocks (~> 2.14.0)
|
44
|
+
rspec-core (2.14.5)
|
45
|
+
rspec-expectations (2.14.3)
|
46
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
47
|
+
rspec-mocks (2.14.3)
|
48
|
+
slop (3.4.6)
|
49
|
+
thor (0.18.1)
|
50
|
+
timers (1.1.0)
|
51
|
+
|
52
|
+
PLATFORMS
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
bundler (~> 1.3)
|
57
|
+
geo_graf!
|
58
|
+
guard-rspec
|
59
|
+
rake
|
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Wimdu GmbH
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# GeoGraf
|
2
|
+
|
3
|
+
[](https://travis-ci.org/wimdu/geo_graf)
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
This gem computes the relation between multiple polygons (do they overlap and
|
8
|
+
by how much?), such as Wimdu's GeoLocations.
|
9
|
+
|
10
|
+
GeoGraf uses [RGeo](http://dazuma.github.io/rgeo/rdoc/) to perform the
|
11
|
+
calculations. It uses the
|
12
|
+
[Mercator projection](http://en.wikipedia.org/wiki/Mercator_projection), which
|
13
|
+
projects the surface of earth onto a flat surface. This is inaccurate, as
|
14
|
+
areas appear bigger when they are further away from the equator (have a look at
|
15
|
+
Greenland and the DR Congo on Google Maps, they both are roughly 2 million
|
16
|
+
square kilometers big). However, the assumption is that the error of areas that
|
17
|
+
relate to each other is rather small, as the overlapping area is at the same
|
18
|
+
latitude. And we're not interested in the correct area calculation, we just need
|
19
|
+
to know if they overlap, which is the bigger one and roughly how much they
|
20
|
+
overlap.
|
21
|
+
|
22
|
+
The input is expected to be an array of polygon descriptions. Each polygon
|
23
|
+
description has to be a hash with an `:id` key and `:polygon_coords`, which
|
24
|
+
itself is an array of coordinates describing the polygon. Each coordinate is
|
25
|
+
an array of it's latitude and longitude (in this order).
|
26
|
+
|
27
|
+
Example:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'geo_graf'
|
31
|
+
|
32
|
+
GeoGraf.intersections_for(
|
33
|
+
[
|
34
|
+
{
|
35
|
+
id: 1,
|
36
|
+
polygon_coords: [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]]
|
37
|
+
},
|
38
|
+
{
|
39
|
+
id: 2,
|
40
|
+
polygon_coords: [[-3, -3], [-1, -3], [-1, -1], [-3, -1], [-3, -3]]
|
41
|
+
}
|
42
|
+
]
|
43
|
+
)
|
44
|
+
```
|
45
|
+
|
46
|
+
The output is an array of relation descriptions. Each description is a hash in
|
47
|
+
the following format:
|
48
|
+
* `:id`: The ID of the polygon. It's always the ID of the smaller polygon
|
49
|
+
(by area).
|
50
|
+
* `:contained_area_percentage`: How much of the area of the smaller polygon
|
51
|
+
is contained in the larger one.
|
52
|
+
* `:container_id`: The ID of the larger polygon.
|
53
|
+
|
54
|
+
If two polygons don't overlap, no relation exists and there will be no
|
55
|
+
description returned for these two.
|
56
|
+
|
57
|
+
For the example above, the following result will be returned:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
[
|
61
|
+
{
|
62
|
+
id: 2,
|
63
|
+
contained_area_percentage: 25,
|
64
|
+
container_id: 1
|
65
|
+
}
|
66
|
+
]
|
67
|
+
```
|
68
|
+
|
69
|
+
This means that 25% of the area of polygon 2 overlap with the area of polygon
|
70
|
+
1.
|
71
|
+
|
72
|
+
## Installation
|
73
|
+
|
74
|
+
GeoGraf depends on RGeo with [GEOS](http://trac.osgeo.org/geos/) >= 3.3.3. You
|
75
|
+
need to install it before installing RGeo.
|
76
|
+
|
77
|
+
### MacOs with homebrew
|
78
|
+
|
79
|
+
```sh
|
80
|
+
brew install geos
|
81
|
+
```
|
82
|
+
|
83
|
+
### Ubuntu/Debian
|
84
|
+
|
85
|
+
```sh
|
86
|
+
sudo apt-get install libgeos++-dev
|
87
|
+
```
|
88
|
+
|
89
|
+
## License
|
90
|
+
Copyright (c) 2013 Wimdu GmbH (MIT License). See LICENSE.txt for details.
|
data/Rakefile
ADDED
data/geo_graf.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'geo_graf'
|
3
|
+
spec.version = '1.0.2'
|
4
|
+
spec.date = Time.now.strftime('%Y-%m-%d')
|
5
|
+
spec.authors = ['Marek Nowak', 'Johannes Barre']
|
6
|
+
spec.email = ['marek.nowak@wimdu.com', 'johannes.barre@wimdu.com']
|
7
|
+
spec.description = %q{Calculates the relations between overlapping polygons}
|
8
|
+
spec.summary = %q{Calculates the relations between overlapping polygons}
|
9
|
+
spec.homepage = 'https://github.com/wimdu/geo_graf'
|
10
|
+
|
11
|
+
spec.files = `git ls-files`.split($/)
|
12
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
13
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
14
|
+
spec.require_paths = ['lib']
|
15
|
+
|
16
|
+
spec.add_dependency 'rgeo'
|
17
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
18
|
+
spec.add_development_dependency 'guard-rspec'
|
19
|
+
spec.add_development_dependency 'rake'
|
20
|
+
end
|
data/lib/geo_graf.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rgeo'
|
2
|
+
require 'geo_graf/polygon'
|
3
|
+
|
4
|
+
module GeoGraf
|
5
|
+
class GeosNotInstalledError < StandardError; end
|
6
|
+
|
7
|
+
class IntersectionCalculator
|
8
|
+
def initialize(input_geodata)
|
9
|
+
@geodata ||= input_geodata
|
10
|
+
.map { |geodatum| {id: geodatum[:id], polygon: polygon_from_coords(geodatum[:polygon_coords])} }
|
11
|
+
.sort_by { |geodatum| geodatum[:polygon].area } # always have smaller first
|
12
|
+
end
|
13
|
+
|
14
|
+
def intersections
|
15
|
+
intersections = []
|
16
|
+
|
17
|
+
geodata.combination(2) do |smaller, bigger|
|
18
|
+
intersection_area = smaller[:polygon].intersection_area(bigger[:polygon])
|
19
|
+
|
20
|
+
unless intersection_area.zero?
|
21
|
+
intersections << {
|
22
|
+
id: smaller[:id],
|
23
|
+
contained_area_percentage: (intersection_area.to_f / smaller[:polygon].area * 100).round,
|
24
|
+
container_id: bigger[:id]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
intersections
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :geodata
|
35
|
+
|
36
|
+
def polygon_from_coords(coords)
|
37
|
+
Polygon.new(coords, rgeo_factory)
|
38
|
+
end
|
39
|
+
|
40
|
+
def rgeo_factory
|
41
|
+
raise(GeosNotInstalledError, 'The Geos library needs to be installed (see README.md).') unless RGeo::Geos.supported?
|
42
|
+
@factory ||= RGeo::Geographic.simple_mercator_factory
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module GeoGraf
|
2
|
+
class Polygon
|
3
|
+
def initialize(coordinates, factory)
|
4
|
+
@rgeo_polygons = [create_polygon(coordinates, factory)]
|
5
|
+
|
6
|
+
create_shadow_polygon_if_required(coordinates, factory)
|
7
|
+
end
|
8
|
+
|
9
|
+
def intersection_area(other)
|
10
|
+
intersection = intersection(other)
|
11
|
+
|
12
|
+
if intersection.respond_to?(:area)
|
13
|
+
intersection.area
|
14
|
+
elsif intersection.respond_to?(:map)
|
15
|
+
sum_of_areas_in(intersection)
|
16
|
+
else
|
17
|
+
0.0
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def area
|
22
|
+
rgeo_polygons.first.area
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
attr_reader :rgeo_polygons
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def intersection(other)
|
32
|
+
intersection = nil
|
33
|
+
|
34
|
+
# If you wonder why for a seemingly simple intersection of two
|
35
|
+
# polygons we are actually intersecting multiple rgeo_polygons
|
36
|
+
# with other's rgeo_polygons, check out the comment above
|
37
|
+
# #create_shadow_polygon_if_required. Peace.
|
38
|
+
|
39
|
+
rgeo_polygons.each do |polygon|
|
40
|
+
other.rgeo_polygons.each do |other_polygon|
|
41
|
+
intersection = polygon.intersection(other_polygon)
|
42
|
+
return intersection if intersection && !intersection.is_empty?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
intersection
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_polygon(coordinates, factory, longitude_offset: 0)
|
49
|
+
points = coordinates.map { |c| factory.point(c[1] + longitude_offset, c[0]) }
|
50
|
+
ring = factory.linear_ring(points)
|
51
|
+
factory.polygon(ring)
|
52
|
+
end
|
53
|
+
|
54
|
+
def sum_of_areas_in(collection)
|
55
|
+
collection
|
56
|
+
.select { |i| i.respond_to?(:area) }
|
57
|
+
.map(&:area)
|
58
|
+
.inject(0, :+)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Ah, the shadow polygon... We use it because RGeo is kind of retarded
|
62
|
+
# and doesn't deal with situations in which shapes wrap around
|
63
|
+
# the 180th meridian correctly. Therefore we create a fake shape with
|
64
|
+
# an offset of either 360 or -360 degrees to ensure that we catch
|
65
|
+
# the intersection if there is one. And, the name is awesome, right?
|
66
|
+
|
67
|
+
def create_shadow_polygon_if_required(coordinates, factory)
|
68
|
+
if line_180(factory).intersects?(rgeo_polygons.first)
|
69
|
+
rgeo_polygons << create_polygon(coordinates, factory, longitude_offset: -360)
|
70
|
+
elsif line_minus_180(factory).intersects?(rgeo_polygons.first)
|
71
|
+
rgeo_polygons << create_polygon(coordinates, factory, longitude_offset: 360)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def line_180(factory)
|
76
|
+
factory.line(factory.point(180, 90), factory.point(180, -90))
|
77
|
+
end
|
78
|
+
|
79
|
+
def line_minus_180(factory)
|
80
|
+
factory.line(factory.point(-180, 90), factory.point(-180, -90))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
$: << '../lib'
|
2
|
+
|
3
|
+
require 'geo_graf/intersection_calculator'
|
4
|
+
|
5
|
+
describe GeoGraf::IntersectionCalculator do
|
6
|
+
describe "#intersections" do
|
7
|
+
subject(:intersections) { described_class.new(input).intersections }
|
8
|
+
|
9
|
+
context "given no shape" do
|
10
|
+
let(:input) { [] }
|
11
|
+
|
12
|
+
it "returns no intersections" do
|
13
|
+
expect(intersections).to be_empty
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "given only one shape" do
|
18
|
+
let(:input) {
|
19
|
+
[{
|
20
|
+
id: 1,
|
21
|
+
polygon_coords: [[-2, -2], [2, -2], [-2, 1], [-2, -2]]
|
22
|
+
}]
|
23
|
+
}
|
24
|
+
|
25
|
+
it "returns no intersections" do
|
26
|
+
expect(intersections).to be_empty
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "given two nonintersecting shapes" do
|
31
|
+
let(:input) {
|
32
|
+
[
|
33
|
+
{
|
34
|
+
id: 1,
|
35
|
+
polygon_coords: [[-2, -2], [2, -2], [-2, 1], [-2, -2]]
|
36
|
+
},
|
37
|
+
{
|
38
|
+
id: 2,
|
39
|
+
polygon_coords: [[100, 100], [102, 100], [100, 101], [100, 100]]
|
40
|
+
}
|
41
|
+
]
|
42
|
+
}
|
43
|
+
|
44
|
+
it "returns no intersections" do
|
45
|
+
expect(intersections).to be_empty
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "given two identical shapes" do
|
50
|
+
let(:input) {
|
51
|
+
[
|
52
|
+
{
|
53
|
+
id: 1,
|
54
|
+
polygon_coords: [[-2, -2], [2, -2], [-2, 1], [-2, -2]]
|
55
|
+
},
|
56
|
+
{
|
57
|
+
id: 2,
|
58
|
+
polygon_coords: [[-2, -2], [2, -2], [-2, 1], [-2, -2]]
|
59
|
+
}
|
60
|
+
]
|
61
|
+
}
|
62
|
+
|
63
|
+
it "returns an intersection of the second one containing the first one 100%" do
|
64
|
+
expect(intersections).to match_array([{id: 1, contained_area_percentage: 100, container_id: 2}])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "given one square inside the other" do
|
69
|
+
let(:input) {
|
70
|
+
[
|
71
|
+
{
|
72
|
+
id: 1,
|
73
|
+
polygon_coords: [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]]
|
74
|
+
},
|
75
|
+
{
|
76
|
+
id: 2,
|
77
|
+
polygon_coords: [[-2, -2], [0, -2], [0, 0], [-2, 0], [-2, -2]]
|
78
|
+
}
|
79
|
+
]
|
80
|
+
}
|
81
|
+
|
82
|
+
it "returns an intersection of the first one containing the second one 100%" do
|
83
|
+
expect(intersections).to match_array([{id: 2, contained_area_percentage: 100, container_id: 1}])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "given one square intersecting the other 25%" do
|
88
|
+
let(:input) {
|
89
|
+
[
|
90
|
+
{
|
91
|
+
id: 1,
|
92
|
+
polygon_coords: [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]]
|
93
|
+
},
|
94
|
+
{
|
95
|
+
id: 2,
|
96
|
+
polygon_coords: [[-3, -3], [-1, -3], [-1, -1], [-3, -1], [-3, -3]]
|
97
|
+
}
|
98
|
+
]
|
99
|
+
}
|
100
|
+
|
101
|
+
it "returns an intersection of the first one containing the second one 25%" do
|
102
|
+
expect(intersections).to match_array([{id: 2, contained_area_percentage: 25, container_id: 1}])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "given one square intersecting the other square of the same size 50%" do
|
107
|
+
let(:input) {
|
108
|
+
[
|
109
|
+
{
|
110
|
+
id: 1,
|
111
|
+
polygon_coords: [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]]
|
112
|
+
},
|
113
|
+
{
|
114
|
+
id: 2,
|
115
|
+
polygon_coords: [[-4, -2], [0, -2], [0, 2], [-4, 2], [-4, -2]]
|
116
|
+
}
|
117
|
+
]
|
118
|
+
}
|
119
|
+
|
120
|
+
it "returns an intersection of the second one containing the first one 50%" do
|
121
|
+
expect(intersections).to match_array([{id: 1, contained_area_percentage: 50, container_id: 2}])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "given multiple shapes of which some intersect" do
|
126
|
+
let(:input) {
|
127
|
+
[
|
128
|
+
{
|
129
|
+
id: 1,
|
130
|
+
polygon_coords: [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]]
|
131
|
+
},
|
132
|
+
{
|
133
|
+
id: 2,
|
134
|
+
polygon_coords: [[-1, 1], [-4, 1], [-4, -2], [-1, -2], [-1, 1]]
|
135
|
+
},
|
136
|
+
{
|
137
|
+
id: 3,
|
138
|
+
polygon_coords: [[-3, -3], [-1, -3], [-1, -1], [-3, -1], [-3, -3]]
|
139
|
+
},
|
140
|
+
{
|
141
|
+
id: 4,
|
142
|
+
polygon_coords: [[-1, 8], [-3, 8], [-3, 6], [-1, 6], [-1, 8]]
|
143
|
+
},
|
144
|
+
{
|
145
|
+
id: 5,
|
146
|
+
polygon_coords: [[11, 6], [11, 0], [2, 6], [11, 6]]
|
147
|
+
},
|
148
|
+
{
|
149
|
+
id: 6,
|
150
|
+
polygon_coords: [[5, 4], [8, 4], [8, 2], [5, 2], [5, 4]]
|
151
|
+
},
|
152
|
+
{
|
153
|
+
id: 7,
|
154
|
+
polygon_coords: [[3, -3], [1, -3], [1, -1], [3, -1], [3, -3]]
|
155
|
+
}
|
156
|
+
]
|
157
|
+
}
|
158
|
+
|
159
|
+
it "returns 4 intersections" do
|
160
|
+
expect(intersections).to match_array([
|
161
|
+
{id: 2, contained_area_percentage: 33, container_id: 1},
|
162
|
+
{id: 3, contained_area_percentage: 50, container_id: 2},
|
163
|
+
{id: 3, contained_area_percentage: 25, container_id: 1},
|
164
|
+
{id: 6, contained_area_percentage: 49, container_id: 5}, # 49% because of it's not a plane but a sphere
|
165
|
+
{id: 7, contained_area_percentage: 25, container_id: 1}
|
166
|
+
])
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "given multiple shapes on the both sides of 180 degrees line of which some intersect" do
|
171
|
+
let(:input) {
|
172
|
+
[
|
173
|
+
{
|
174
|
+
id: 1,
|
175
|
+
polygon_coords: [[-2, 178], [2, 178], [2, -178], [-2, -178], [-2, 178]]
|
176
|
+
},
|
177
|
+
{
|
178
|
+
id: 2,
|
179
|
+
polygon_coords: [[-1, -179], [-4, -179], [-4, 178], [-1, 178], [-1, -179]]
|
180
|
+
},
|
181
|
+
{
|
182
|
+
id: 3,
|
183
|
+
polygon_coords: [[-3, 177], [-1, 177], [-1, 179], [-3, 179], [-3, 177]]
|
184
|
+
},
|
185
|
+
{
|
186
|
+
id: 4,
|
187
|
+
polygon_coords: [[-1, -172], [-3, -172], [-3, -174], [-1, -174], [-1, -172]]
|
188
|
+
},
|
189
|
+
{
|
190
|
+
id: 5,
|
191
|
+
polygon_coords: [[11, -174], [11, 180], [2, -174], [11, -174]]
|
192
|
+
},
|
193
|
+
{
|
194
|
+
id: 6,
|
195
|
+
polygon_coords: [[5, -176], [8, -176], [8, -178], [5, -178], [5, -176]]
|
196
|
+
},
|
197
|
+
{
|
198
|
+
id: 7,
|
199
|
+
polygon_coords: [[3, 177], [1, 177], [1, 179], [3, 179], [3, 177]]
|
200
|
+
}
|
201
|
+
]
|
202
|
+
}
|
203
|
+
|
204
|
+
it "returns 4 intersections" do
|
205
|
+
expect(intersections).to match_array([
|
206
|
+
{id: 2, contained_area_percentage: 33, container_id: 1},
|
207
|
+
{id: 3, contained_area_percentage: 50, container_id: 2},
|
208
|
+
{id: 3, contained_area_percentage: 25, container_id: 1},
|
209
|
+
{id: 6, contained_area_percentage: 49, container_id: 5}, # 49% because of it's not a plane but a sphere
|
210
|
+
{id: 7, contained_area_percentage: 25, container_id: 1}
|
211
|
+
])
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
$: << '../lib'
|
2
|
+
|
3
|
+
require 'geo_graf/polygon'
|
4
|
+
|
5
|
+
describe GeoGraf::Polygon do
|
6
|
+
let(:polygon) { described_class.new(input, factory) }
|
7
|
+
let(:factory) { double("Factory", polygon: rgeo_polygon, point: rgeo_point, linear_ring: rgeo_linear_ring, line: line_180) }
|
8
|
+
let(:rgeo_polygon) { double("RGeoPolygon") }
|
9
|
+
let(:rgeo_point) { double("RGeoPoint") }
|
10
|
+
let(:rgeo_linear_ring) { double("LinearRing") }
|
11
|
+
let(:line_180) { double("Line", intersects?: false) }
|
12
|
+
let(:input) { [[-2, -2], [2, -2], [-2, 1], [-2, -2]] }
|
13
|
+
|
14
|
+
describe ".new" do
|
15
|
+
subject(:create_new_polygon) { polygon }
|
16
|
+
|
17
|
+
context "when the polygon doesn't span across 180th meridian" do
|
18
|
+
it "creates a point for each coordinate" do
|
19
|
+
factory.should_receive(:point).exactly(8).times # 4 + 4 used for creation of 180 and -180 lines
|
20
|
+
|
21
|
+
create_new_polygon
|
22
|
+
end
|
23
|
+
|
24
|
+
it "creates a linear ring out of the points" do
|
25
|
+
factory.should_receive(:linear_ring).with([rgeo_point] * 4)
|
26
|
+
|
27
|
+
create_new_polygon
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates the polygon using the linear ring" do
|
31
|
+
factory.should_receive(:polygon).with(rgeo_linear_ring)
|
32
|
+
|
33
|
+
create_new_polygon
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when the polygon spans across 180th meridian" do
|
38
|
+
let(:line_180) { double("Line", intersects?: true) }
|
39
|
+
|
40
|
+
it "creates two polygons using the linear rings" do
|
41
|
+
factory.should_receive(:polygon).with(rgeo_linear_ring).twice
|
42
|
+
|
43
|
+
create_new_polygon
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#area" do
|
49
|
+
subject(:get_area) { polygon.area }
|
50
|
+
|
51
|
+
it "delegates to the internal RGeo polygon" do
|
52
|
+
rgeo_polygon.should_receive(:area)
|
53
|
+
|
54
|
+
get_area
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#intersection_area" do
|
59
|
+
let(:other_polygon) { double('OtherPolygon', rgeo_polygons: [others_rgeo_polygon_1]) }
|
60
|
+
let(:others_rgeo_polygon_1) { double('OtherRgeoPolygon1') }
|
61
|
+
let(:intersection) { double('Intersection', is_empty?: false, area: 1234.5) }
|
62
|
+
let(:no_intersection) { double('NoIntersection', is_empty?: true) }
|
63
|
+
|
64
|
+
context "when the polygon doesn't span across 180th meridian" do
|
65
|
+
context "when it intersects the other polygon" do
|
66
|
+
before do
|
67
|
+
rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_1).and_return(intersection)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "returns the area of the intersection" do
|
71
|
+
expect(polygon.intersection_area(other_polygon)).to eq(1234.5)
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when the intersection spans over multiple polygons and points" do
|
75
|
+
let(:intersection) { [double('Point'), double('Polygon', area: 123.67), double('Polygon', area: 3.12)] }
|
76
|
+
|
77
|
+
before do
|
78
|
+
intersection.stub(:is_empty?).and_return(false)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "returns the sum of areas of the intersection" do
|
82
|
+
expect(polygon.intersection_area(other_polygon)).to eq(126.79)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when it doesn't intersect the other polygon" do
|
88
|
+
before do
|
89
|
+
rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_1).and_return(no_intersection)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "returns 0.0" do
|
93
|
+
expect(polygon.intersection_area(other_polygon)).to eq(0.0)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "when the polygon spans across 180th meridian" do
|
99
|
+
let(:other_polygon) {
|
100
|
+
double('OtherPolygon', rgeo_polygons: [others_rgeo_polygon_1, others_rgeo_polygon_2])
|
101
|
+
}
|
102
|
+
let(:others_rgeo_polygon_2) { double('OtherRgeoPolygon2') }
|
103
|
+
let(:shadow_rgeo_polygon) { double("ShadowRgeoPolygon") }
|
104
|
+
let(:line_180) { double("Line", intersects?: true) }
|
105
|
+
|
106
|
+
before do
|
107
|
+
factory.stub(:polygon).and_return(rgeo_polygon, shadow_rgeo_polygon)
|
108
|
+
rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_1).and_return(no_intersection)
|
109
|
+
rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_2).and_return(no_intersection)
|
110
|
+
shadow_rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_2).and_return(no_intersection)
|
111
|
+
end
|
112
|
+
|
113
|
+
context "when it intersects the other polygon" do
|
114
|
+
before do
|
115
|
+
shadow_rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_1).and_return(intersection)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "returns the area of the intersection" do
|
119
|
+
expect(polygon.intersection_area(other_polygon)).to eq(1234.5)
|
120
|
+
end
|
121
|
+
|
122
|
+
context "when the intersection spans over multiple polygon" do
|
123
|
+
let(:intersection) { [double('Point'), double('Polygon', area: 123.67), double('Polygon', area: 3.12)] }
|
124
|
+
|
125
|
+
before do
|
126
|
+
intersection.stub(:is_empty?).and_return(false)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "returns the sum of areas of the intersection" do
|
130
|
+
expect(polygon.intersection_area(other_polygon)).to eq(126.79)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "when it doesn't intersect the other polygon" do
|
136
|
+
before do
|
137
|
+
shadow_rgeo_polygon.stub(:intersection).with(others_rgeo_polygon_1).and_return(no_intersection)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "returns 0.0" do
|
141
|
+
expect(polygon.intersection_area(other_polygon)).to eq(0.0)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$: << '../lib'
|
2
|
+
|
3
|
+
require 'geo_graf'
|
4
|
+
|
5
|
+
describe GeoGraf do
|
6
|
+
describe "#intersections_for" do
|
7
|
+
subject(:intersections_for_input) { described_class.intersections_for(input) }
|
8
|
+
|
9
|
+
let(:calculator) { double('Calculator') }
|
10
|
+
let(:input) { double('Input') }
|
11
|
+
let(:intersection_calculator_output) { double('Output') }
|
12
|
+
|
13
|
+
before do
|
14
|
+
allow(GeoGraf::IntersectionCalculator).to receive(:new).with(input).and_return(calculator)
|
15
|
+
allow(calculator).to receive(:intersections).and_return(intersection_calculator_output)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "runs intersections on a new instance of IntersectionCalculator" do
|
19
|
+
expect(intersections_for_input).to eq(intersection_calculator_output)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geo_graf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marek Nowak
|
8
|
+
- Johannes Barre
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rgeo
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bundler
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.3'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.3'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: guard-rspec
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rake
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: Calculates the relations between overlapping polygons
|
71
|
+
email:
|
72
|
+
- marek.nowak@wimdu.com
|
73
|
+
- johannes.barre@wimdu.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- .gitignore
|
79
|
+
- .travis.yml
|
80
|
+
- Gemfile
|
81
|
+
- Gemfile.lock
|
82
|
+
- Guardfile
|
83
|
+
- LICENSE.txt
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- geo_graf.gemspec
|
87
|
+
- lib/geo_graf.rb
|
88
|
+
- lib/geo_graf/intersection_calculator.rb
|
89
|
+
- lib/geo_graf/polygon.rb
|
90
|
+
- spec/geo_graf/intersection_calculator_spec.rb
|
91
|
+
- spec/geo_graf/polygon_spec.rb
|
92
|
+
- spec/geo_graf_spec.rb
|
93
|
+
homepage: https://github.com/wimdu/geo_graf
|
94
|
+
licenses: []
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.0.3
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Calculates the relations between overlapping polygons
|
116
|
+
test_files:
|
117
|
+
- spec/geo_graf/intersection_calculator_spec.rb
|
118
|
+
- spec/geo_graf/polygon_spec.rb
|
119
|
+
- spec/geo_graf_spec.rb
|