simplify_rb 0.2.0 → 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 +4 -4
- data/README.md +32 -14
- data/Rakefile +2 -2
- data/lib/simplify_rb/douglas_peucker_simplifier.rb +82 -0
- data/lib/simplify_rb/point.rb +39 -0
- data/lib/simplify_rb/radial_distance_simplifier.rb +18 -0
- data/lib/simplify_rb/version.rb +1 -1
- data/lib/simplify_rb.rb +12 -93
- data/spec/simplify_rb/point_spec.rb +70 -0
- data/spec/simplify_rb_spec.rb +16 -0
- metadata +7 -5
- data/lib/simplify_rb/symbolizer.rb +0 -13
- data/spec/symbolizer_spec.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33cd91e84b17826cf512f3b17083e297b65c3268
|
4
|
+
data.tar.gz: 48d4337b44170364e78f3e206e5b19a90a84d567
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd402062db3e2892fd4a1ce5aa8223b47f03daf465be61d397b874c19630aaa7085269ade4ed93c1be6cb45f61d1e092075e962587c3f174383cfd27aaa12468
|
7
|
+
data.tar.gz: 9a4baf26c41e5d912767840a811847d1bd65b3ccac19e16b9b743a2a7ef5ac21d515f944c92631e8fbe55aa280b47c6029aec77d7c50a5f86234f46cc86be72a
|
data/README.md
CHANGED
@@ -6,20 +6,6 @@ SimplifyRb is a Ruby port of [simplify.js](https://github.com/mourner/simplify-j
|
|
6
6
|
|
7
7
|
You can use this gem to reduce the number of points in a complex polyline / polygon, making use of an optimized Douglas-Peucker algorithm.
|
8
8
|
|
9
|
-
## Installation
|
10
|
-
|
11
|
-
Add this line to your application's Gemfile:
|
12
|
-
|
13
|
-
gem 'simplify_rb'
|
14
|
-
|
15
|
-
And then execute:
|
16
|
-
|
17
|
-
$ bundle
|
18
|
-
|
19
|
-
Or install it yourself as:
|
20
|
-
|
21
|
-
$ gem install simplify_rb
|
22
|
-
|
23
9
|
## Usage
|
24
10
|
|
25
11
|
```ruby
|
@@ -40,3 +26,35 @@ SimplifyRb::Simplifier.new.process(points, tolerance, high_quality)
|
|
40
26
|
```tolerance```: (optional, 1 by default): Affects the amount of simplification that occurs (the smaller, the less simplification).
|
41
27
|
|
42
28
|
```high_quality```: (optional, False by default): Flag to exclude the distance pre-processing. Produces higher quality results when true is passed, but runs slower.
|
29
|
+
|
30
|
+
### Custom points
|
31
|
+
|
32
|
+
You can also use custom points, such as a struct or object which responds to `:x` and `:y`, rather than hashes:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
CustomPointStruct = Struct.new(:x, :y)
|
36
|
+
|
37
|
+
custom_points = [
|
38
|
+
CustomPointStruct.new(51.5256, -0.0875),
|
39
|
+
CustomPointStruct.new(51.7823, -0.0912)
|
40
|
+
]
|
41
|
+
|
42
|
+
tolerance = 1
|
43
|
+
high_quality = true
|
44
|
+
|
45
|
+
SimplifyRb::Simplifier.new.process(custom_points, tolerance, high_quality)
|
46
|
+
```
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
Add this line to your application's Gemfile:
|
51
|
+
|
52
|
+
gem 'simplify_rb'
|
53
|
+
|
54
|
+
And then execute:
|
55
|
+
|
56
|
+
$ bundle
|
57
|
+
|
58
|
+
Or install it yourself as:
|
59
|
+
|
60
|
+
$ gem install simplify_rb
|
data/Rakefile
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Optimized Douglas-Peucker algorithm
|
2
|
+
|
3
|
+
module SimplifyRb
|
4
|
+
class DouglasPeuckerSimplifier
|
5
|
+
def process(points, sq_tolerance)
|
6
|
+
points.first.keep = true
|
7
|
+
points.last.keep = true
|
8
|
+
|
9
|
+
simplify_douglas_peucker(points, sq_tolerance)
|
10
|
+
.select(&:keep)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
MaxSqDist = Struct.new(:max_sq_dist, :index)
|
16
|
+
|
17
|
+
def simplify_douglas_peucker(points, sq_tolerance)
|
18
|
+
first_i = 0
|
19
|
+
last_i = points.length - 1
|
20
|
+
index = nil
|
21
|
+
stack = []
|
22
|
+
|
23
|
+
while last_i
|
24
|
+
result = calc_max_sq_dist(first_i, last_i, points)
|
25
|
+
index = result.index
|
26
|
+
|
27
|
+
if result.max_sq_dist > sq_tolerance
|
28
|
+
points[index].keep = true
|
29
|
+
|
30
|
+
stack.push(first_i, index, index, last_i)
|
31
|
+
end
|
32
|
+
|
33
|
+
first_i, last_i = stack.pop(2)
|
34
|
+
end
|
35
|
+
|
36
|
+
points
|
37
|
+
end
|
38
|
+
|
39
|
+
def calc_max_sq_dist(first_i, last_i, points)
|
40
|
+
index = nil
|
41
|
+
max_sq_dist = 0
|
42
|
+
range = (first_i + 1)...last_i
|
43
|
+
|
44
|
+
range.each do |i|
|
45
|
+
sq_dist = get_sq_seg_dist(points[i], points[first_i], points[last_i])
|
46
|
+
|
47
|
+
if sq_dist > max_sq_dist
|
48
|
+
index = i
|
49
|
+
max_sq_dist = sq_dist
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
MaxSqDist.new(max_sq_dist, index)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Square distance from a point to a segment
|
57
|
+
def get_sq_seg_dist(point, point_1, point_2)
|
58
|
+
x = point_1.x
|
59
|
+
y = point_1.y
|
60
|
+
dx = point_2.x - x
|
61
|
+
dy = point_2.y - y
|
62
|
+
|
63
|
+
if dx != 0 || dy != 0
|
64
|
+
t = ((point.x - x) * dx + (point.y - y) * dy) / (dx * dx + dy * dy)
|
65
|
+
|
66
|
+
if t > 1
|
67
|
+
x = point_2.x
|
68
|
+
y = point_2.y
|
69
|
+
|
70
|
+
elsif t > 0
|
71
|
+
x += dx * t
|
72
|
+
y += dy * t
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
dx = point.x - x
|
77
|
+
dy = point.y - y
|
78
|
+
|
79
|
+
dx * dx + dy * dy
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SimplifyRb
|
2
|
+
class Point
|
3
|
+
attr_reader :x, :y, :original_entity
|
4
|
+
attr_accessor :keep
|
5
|
+
|
6
|
+
def initialize(raw_point)
|
7
|
+
@original_entity = raw_point
|
8
|
+
@x, @y = parse_x_y(raw_point)
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_sq_dist_to(other_point)
|
12
|
+
dx = x - other_point.x
|
13
|
+
dy = y - other_point.y
|
14
|
+
|
15
|
+
dx * dx + dy * dy
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse_x_y(raw_point)
|
21
|
+
x = nil
|
22
|
+
y = nil
|
23
|
+
|
24
|
+
if raw_point.kind_of? Hash
|
25
|
+
x = raw_point[:x] || raw_point['x']
|
26
|
+
y = raw_point[:y] || raw_point['y']
|
27
|
+
elsif raw_point.respond_to?(:x) && raw_point.respond_to?(:y)
|
28
|
+
x = raw_point.x
|
29
|
+
y = raw_point.y
|
30
|
+
end
|
31
|
+
|
32
|
+
if x.nil? || y.nil?
|
33
|
+
raise ArgumentError.new('Points must have :x and :y values')
|
34
|
+
end
|
35
|
+
|
36
|
+
[x, y]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Basic distance-based simplification
|
2
|
+
|
3
|
+
module SimplifyRb
|
4
|
+
class RadialDistanceSimplifier
|
5
|
+
def process(points, sq_tolerance)
|
6
|
+
new_points = [points.first]
|
7
|
+
|
8
|
+
points.each do |point|
|
9
|
+
sq_dist = point.get_sq_dist_to(new_points.last)
|
10
|
+
new_points << point if sq_dist > sq_tolerance
|
11
|
+
end
|
12
|
+
|
13
|
+
new_points << points.last unless new_points.last == points.last
|
14
|
+
|
15
|
+
new_points
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/simplify_rb/version.rb
CHANGED
data/lib/simplify_rb.rb
CHANGED
@@ -1,107 +1,26 @@
|
|
1
1
|
require 'simplify_rb/version'
|
2
|
-
require 'simplify_rb/
|
2
|
+
require 'simplify_rb/point'
|
3
|
+
require 'simplify_rb/radial_distance_simplifier'
|
4
|
+
require 'simplify_rb/douglas_peucker_simplifier'
|
3
5
|
|
4
6
|
module SimplifyRb
|
5
7
|
class Simplifier
|
6
|
-
def process(
|
7
|
-
raise ArgumentError.new('Points must be an array') unless
|
8
|
+
def process(raw_points, tolerance = 1, highest_quality = false)
|
9
|
+
raise ArgumentError.new('Points must be an array') unless raw_points.is_a? Array
|
8
10
|
|
9
|
-
return
|
10
|
-
|
11
|
-
symbolizer = Symbolizer.new
|
12
|
-
|
13
|
-
points = symbolizer.symbolize_keys(points) unless points.all? { |p| symbolizer.keys_are_symbols?(p.keys) }
|
11
|
+
return raw_points if raw_points.length <= 1
|
14
12
|
|
15
13
|
sq_tolerance = tolerance * tolerance
|
16
14
|
|
17
|
-
|
18
|
-
points = simplify_radial_dist(points, sq_tolerance) unless highest_quality
|
19
|
-
|
20
|
-
# Optimisation step 2
|
21
|
-
simplify_douglas_peucker(points, sq_tolerance)
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
# Basic distance-based simplification
|
27
|
-
def simplify_radial_dist(points, sq_tolerance)
|
28
|
-
new_points = [points.first]
|
29
|
-
|
30
|
-
points.each do |point|
|
31
|
-
new_points << point if (get_sq_dist(point, new_points.last) > sq_tolerance)
|
32
|
-
end
|
33
|
-
|
34
|
-
new_points << points.last unless new_points.last == points.last
|
35
|
-
|
36
|
-
new_points
|
37
|
-
end
|
38
|
-
|
39
|
-
# Simplification using optimized Douglas-Peucker algorithm with recursion elimination
|
40
|
-
def simplify_douglas_peucker(points, sq_tolerance)
|
41
|
-
first = 0
|
42
|
-
last = points.length - 1
|
43
|
-
index = nil
|
44
|
-
stack = []
|
45
|
-
|
46
|
-
points.first[:keep] = true
|
47
|
-
points.last[:keep] = true
|
48
|
-
|
49
|
-
while last
|
50
|
-
max_sq_dist = 0
|
15
|
+
points = raw_points.map { |p| Point.new(p) }
|
51
16
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
if sq_dist > max_sq_dist
|
56
|
-
index = i
|
57
|
-
max_sq_dist = sq_dist
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
if max_sq_dist > sq_tolerance
|
62
|
-
points[index][:keep] = true
|
63
|
-
|
64
|
-
stack.push(first, index, index, last)
|
65
|
-
end
|
66
|
-
|
67
|
-
first, last = stack.pop(2)
|
68
|
-
end # end while
|
69
|
-
|
70
|
-
points.select { |p| p[:keep] && p.delete(:keep) }
|
71
|
-
end
|
72
|
-
|
73
|
-
# Square distance between two points
|
74
|
-
def get_sq_dist(point_1, point_2)
|
75
|
-
dx = point_1[:x] - point_2[:x]
|
76
|
-
dy = point_1[:y] - point_2[:y]
|
77
|
-
|
78
|
-
dx * dx + dy * dy
|
79
|
-
end
|
80
|
-
|
81
|
-
# Square distance from a point to a segment
|
82
|
-
def get_sq_seg_dist(point, point_1, point_2)
|
83
|
-
x = point_1[:x]
|
84
|
-
y = point_1[:y]
|
85
|
-
dx = point_2[:x] - x
|
86
|
-
dy = point_2[:y] - y
|
87
|
-
|
88
|
-
if dx != 0 || dy != 0
|
89
|
-
t = ((point[:x] - x) * dx + (point[:y] - y) * dy) / (dx * dx + dy * dy)
|
90
|
-
|
91
|
-
if t > 1
|
92
|
-
x = point_2[:x]
|
93
|
-
y = point_2[:y]
|
94
|
-
|
95
|
-
elsif t > 0
|
96
|
-
x += dx * t
|
97
|
-
y += dy * t
|
98
|
-
end
|
17
|
+
unless highest_quality
|
18
|
+
points = RadialDistanceSimplifier.new.process(points, sq_tolerance)
|
99
19
|
end
|
100
20
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
dx * dx + dy * dy
|
21
|
+
DouglasPeuckerSimplifier.new
|
22
|
+
.process(points, sq_tolerance)
|
23
|
+
.map(&:original_entity)
|
105
24
|
end
|
106
25
|
end
|
107
26
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'simplify_rb/point'
|
3
|
+
|
4
|
+
describe SimplifyRb::Point do
|
5
|
+
describe 'parsing hashes with string keys' do
|
6
|
+
it 'determines the :x, :y value' do
|
7
|
+
raw_point = { "x" => 51.5256, "y" => -0.0875 }
|
8
|
+
point = described_class.new(raw_point)
|
9
|
+
|
10
|
+
expect(point.x).to eq(51.5256)
|
11
|
+
expect(point.y).to eq(-0.0875)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'parsing structs' do
|
16
|
+
it 'determines the :x, :y value' do
|
17
|
+
CustomPointStruct = Struct.new(:x, :y)
|
18
|
+
raw_point = CustomPointStruct.new(51.5256, -0.0875)
|
19
|
+
|
20
|
+
point = described_class.new(raw_point)
|
21
|
+
|
22
|
+
expect(point.x).to eq(51.5256)
|
23
|
+
expect(point.y).to eq(-0.0875)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'handling raw points which are objects' do
|
28
|
+
it 'determines the :x, :y value' do
|
29
|
+
class MyCustomPoint
|
30
|
+
attr_reader :x, :y
|
31
|
+
|
32
|
+
def initialize(x, y)
|
33
|
+
@x = x
|
34
|
+
@y = y
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
raw_point = MyCustomPoint.new(51.5256, -0.0875)
|
39
|
+
|
40
|
+
point = described_class.new(raw_point)
|
41
|
+
|
42
|
+
expect(point.x).to eq(51.5256)
|
43
|
+
expect(point.y).to eq(-0.0875)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'missing x/y values' do
|
48
|
+
it 'raises an error if the points are missing keys' do
|
49
|
+
invalid_point = { Z: 51.5256, y: -0.0875 }
|
50
|
+
expect { described_class.new(invalid_point) }.to raise_error(ArgumentError, 'Points must have :x and :y values')
|
51
|
+
|
52
|
+
invalid_point = { x: 51.5256, Z: -0.0875 }
|
53
|
+
expect { described_class.new(invalid_point) }.to raise_error(ArgumentError, 'Points must have :x and :y values')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'raises an error if points don\'t respond to x / y' do
|
57
|
+
class UnconventialPoint
|
58
|
+
attr_reader :a, :b
|
59
|
+
|
60
|
+
def initialize(a, b)
|
61
|
+
@a = a
|
62
|
+
@b = b
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
invalid_point = UnconventialPoint.new(51.5256, -0.0875)
|
67
|
+
expect { described_class.new(invalid_point) }.to raise_error(ArgumentError, 'Points must have :x and :y values')
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/spec/simplify_rb_spec.rb
CHANGED
@@ -20,6 +20,22 @@ describe SimplifyRb::Simplifier do
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
describe 'extra properties on the data' do
|
24
|
+
it 'preserves the extra properties' do
|
25
|
+
richer_data = [
|
26
|
+
{ x: 51.5256, y: -0.0875, note: 'Foo bar' },
|
27
|
+
{ x: 51.7823, y: -0.0912, attr: 123 }
|
28
|
+
]
|
29
|
+
|
30
|
+
result = subject.process(richer_data, 5, true)
|
31
|
+
|
32
|
+
expect(result.length).to eq 2
|
33
|
+
|
34
|
+
expect(result.first[:note]).to eq 'Foo bar'
|
35
|
+
expect(result.last[:attr]).to eq 123
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
23
39
|
context 'only one point' do
|
24
40
|
it 'returns a list with one point' do
|
25
41
|
data = [{ x: 1, y: 2 }]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simplify_rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- odlp
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -69,15 +69,17 @@ files:
|
|
69
69
|
- Rakefile
|
70
70
|
- example/example.rb
|
71
71
|
- lib/simplify_rb.rb
|
72
|
-
- lib/simplify_rb/
|
72
|
+
- lib/simplify_rb/douglas_peucker_simplifier.rb
|
73
|
+
- lib/simplify_rb/point.rb
|
74
|
+
- lib/simplify_rb/radial_distance_simplifier.rb
|
73
75
|
- lib/simplify_rb/version.rb
|
74
76
|
- simplify_rb.gemspec
|
75
77
|
- spec/fixtures/all-points.yml
|
76
78
|
- spec/fixtures/result-fast.yml
|
77
79
|
- spec/fixtures/result-high-quality.yml
|
80
|
+
- spec/simplify_rb/point_spec.rb
|
78
81
|
- spec/simplify_rb_spec.rb
|
79
82
|
- spec/spec_helper.rb
|
80
|
-
- spec/symbolizer_spec.rb
|
81
83
|
homepage: https://github.com/odlp/simplify_rb
|
82
84
|
licenses:
|
83
85
|
- MIT
|
@@ -106,6 +108,6 @@ test_files:
|
|
106
108
|
- spec/fixtures/all-points.yml
|
107
109
|
- spec/fixtures/result-fast.yml
|
108
110
|
- spec/fixtures/result-high-quality.yml
|
111
|
+
- spec/simplify_rb/point_spec.rb
|
109
112
|
- spec/simplify_rb_spec.rb
|
110
113
|
- spec/spec_helper.rb
|
111
|
-
- spec/symbolizer_spec.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
module SimplifyRb
|
2
|
-
class Symbolizer
|
3
|
-
def keys_are_symbols?(keys)
|
4
|
-
keys.all? { |k| k.is_a? Symbol }
|
5
|
-
end
|
6
|
-
|
7
|
-
def symbolize_keys(collection)
|
8
|
-
collection.map do |item|
|
9
|
-
item.each_with_object({}) { |(k,v), memo| memo[k.to_sym] = v }
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
data/spec/symbolizer_spec.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'simplify_rb/symbolizer'
|
3
|
-
|
4
|
-
describe SimplifyRb::Symbolizer do
|
5
|
-
describe '#keys_are_symbols?' do
|
6
|
-
it 'returns false if any key is not a Symbol' do
|
7
|
-
expect(subject.keys_are_symbols?([:a, 'b', :c])).to equal(false)
|
8
|
-
end
|
9
|
-
|
10
|
-
it 'returns return true if all the keys are Symbols' do
|
11
|
-
expect(subject.keys_are_symbols?([:a, :b, :c])).to equal(true)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
describe '#symbolize_keys' do
|
16
|
-
it 'converts all of the collection\'s keys to symbols' do
|
17
|
-
collection = [{ 'a' => 1, 'b' => 2 }, { 'c' => 3 }]
|
18
|
-
symbolized_result = subject.symbolize_keys(collection)
|
19
|
-
expected_result = [{ a: 1, b: 2 }, { c: 3 }]
|
20
|
-
|
21
|
-
expect(symbolized_result).to eq(expected_result)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|