simplify_rb 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 25ddf7e1fd393b6bcfe5c5f13a07fe3d01de5077
4
- data.tar.gz: 2f46c665b134257da7bb0f0a205386d7e7c72a57
3
+ metadata.gz: 33cd91e84b17826cf512f3b17083e297b65c3268
4
+ data.tar.gz: 48d4337b44170364e78f3e206e5b19a90a84d567
5
5
  SHA512:
6
- metadata.gz: 87e8a58fa61d2afcc0d3b769e9f2713639b980a180efc7382430dbf38521a6edc09e2442751c5d6b11fc8dd38a867b49ca3c073db1915b8c697a87add825571f
7
- data.tar.gz: be9911eedb076f6fc15b4e6ff03587c785b654e6a51e668f2f8785bfa77a3612b1ae807039d2c0b19f0782f1223f0cba33ee763232d54f1580b2ae2d8d042f3f
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
@@ -1,5 +1,5 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
  task default: [:spec]
@@ -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
@@ -1,3 +1,3 @@
1
1
  module SimplifyRb
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/simplify_rb.rb CHANGED
@@ -1,107 +1,26 @@
1
1
  require 'simplify_rb/version'
2
- require 'simplify_rb/symbolizer'
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(points, tolerance = 1, highest_quality = false)
7
- raise ArgumentError.new('Points must be an array') unless points.is_a? Array
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 points if points.length <= 1
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
- # Optimisation step 1
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
- ((first + 1)...last).each do |i|
53
- sq_dist = get_sq_seg_dist(points[i], points[first], points[last])
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
- dx = point[:x] - x
102
- dy = point[:y] - y
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
@@ -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.2.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-07-29 00:00:00.000000000 Z
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/symbolizer.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
@@ -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