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 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