clumpy 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +42 -0
- data/Rakefile +33 -0
- data/clumpy.gemspec +25 -0
- data/lib/clumpy.rb +7 -0
- data/lib/clumpy/builder.rb +52 -0
- data/lib/clumpy/cluster.rb +40 -0
- data/lib/clumpy/version.rb +3 -0
- data/spec/builder_spec.rb +58 -0
- data/spec/cluster_spec.rb +34 -0
- data/spec/spec_helper.rb +15 -0
- metadata +132 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Clumpy
|
2
|
+
|
3
|
+
Little gem to cluster markers, points or anything that responds to `lat` and `lng`.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'clumpy'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install clumpy
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
It's dead easy:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
builder = Clumpy::Builder.new(points)
|
25
|
+
clusters = builder.cluster
|
26
|
+
```
|
27
|
+
|
28
|
+
Optionally you could add a `precision: :high` option to the builder initialization to move the cluster a little bit after all points were assigned.
|
29
|
+
|
30
|
+
## Contributing
|
31
|
+
|
32
|
+
1. Fork it
|
33
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
34
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
35
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
36
|
+
5. Create new Pull Request
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
WTFPL - What The Fuck You Want To Public License
|
41
|
+
|
42
|
+
Read more at [http://www.wtfpl.net](http://www.wtfpl.net)
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
task :default => :spec
|
6
|
+
|
7
|
+
desc 'Open an irb session preloaded with this library'
|
8
|
+
task :console do
|
9
|
+
sh 'irb -rubygems -I lib -r clumpy.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'Run a simple benchmarking'
|
13
|
+
task :benchmark do
|
14
|
+
require 'benchmark'
|
15
|
+
require 'ostruct'
|
16
|
+
require 'clumpy'
|
17
|
+
|
18
|
+
number = 20_000
|
19
|
+
points = []
|
20
|
+
max_lat_range = -85..85.05115
|
21
|
+
max_lng_range = -180..180
|
22
|
+
number.times { points << OpenStruct.new(lat: rand(max_lat_range), lng: rand(max_lng_range)) }
|
23
|
+
|
24
|
+
Benchmark.bm do |x|
|
25
|
+
x.report('low precision') do
|
26
|
+
clusters = Clumpy::Builder.new(points).cluster
|
27
|
+
end
|
28
|
+
|
29
|
+
x.report('high precision') do
|
30
|
+
clusters = Clumpy::Builder.new(points, precision: :high).cluster
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/clumpy.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'clumpy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "clumpy"
|
8
|
+
spec.version = Clumpy::VERSION
|
9
|
+
spec.authors = ["Johannes Opper"]
|
10
|
+
spec.email = ["xijo@gmx.de"]
|
11
|
+
spec.description = %q{Cluster points e.g. for a map}
|
12
|
+
spec.summary = %q{Cluster a bunch of points}
|
13
|
+
spec.homepage = "http://github.com/xijo/clumpy"
|
14
|
+
spec.license = "WTFPL"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_development_dependency 'simplecov'
|
25
|
+
end
|
data/lib/clumpy.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Clumpy
|
2
|
+
class Builder
|
3
|
+
MAX_LATITUDE_DISTANCE = 170.05115
|
4
|
+
MAX_LONGITUDE_DISTANCE = 360
|
5
|
+
DISTANCE_MODIFIER = 5
|
6
|
+
|
7
|
+
attr_accessor :points, :options, :clusters
|
8
|
+
|
9
|
+
def initialize(points, options = {})
|
10
|
+
@points = points
|
11
|
+
@options = options || {}
|
12
|
+
@clusters = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Clusters the given points
|
16
|
+
#
|
17
|
+
# == Returns:
|
18
|
+
# An array of cluster objects.
|
19
|
+
#
|
20
|
+
def cluster
|
21
|
+
points.each { |point| add_to_cluster(point) }
|
22
|
+
options[:precision] == :high and clusters.each(&:reposition)
|
23
|
+
clusters
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_to_cluster(point)
|
27
|
+
parent_cluster = find_parent_cluster(point)
|
28
|
+
|
29
|
+
if parent_cluster
|
30
|
+
parent_cluster.points << point
|
31
|
+
else
|
32
|
+
clusters << Cluster.new(point, cluster_distance)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_parent_cluster(point)
|
37
|
+
clusters.find { |c| c.contains?(point) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def cluster_distance
|
41
|
+
latitude_distance / DISTANCE_MODIFIER
|
42
|
+
end
|
43
|
+
|
44
|
+
def latitude_distance
|
45
|
+
if options[:nelat] && options[:swlat]
|
46
|
+
(options[:nelat] - options[:swlat]).abs
|
47
|
+
else
|
48
|
+
MAX_LATITUDE_DISTANCE
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Clumpy
|
2
|
+
class Cluster
|
3
|
+
attr_accessor :latitude, :longitude, :points
|
4
|
+
|
5
|
+
def initialize(point, distance)
|
6
|
+
@latitude, @longitude = point.lat, point.lng
|
7
|
+
@bb_latitudes = (latitude - distance)..(latitude + distance)
|
8
|
+
@bb_longitudes = (longitude - distance)..(longitude + distance)
|
9
|
+
@points = [point]
|
10
|
+
end
|
11
|
+
|
12
|
+
def contains?(point)
|
13
|
+
point.respond_to?(:lat) &&
|
14
|
+
point.respond_to?(:lng) &&
|
15
|
+
@bb_latitudes.include?(point.lat) &&
|
16
|
+
@bb_longitudes.include?(point.lng)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reposition
|
20
|
+
@latitude = points.inject(0) { |m, p| m + p.lat } / points.size
|
21
|
+
@longitude = points.inject(0) { |m, p| m + p.lng } / points.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def as_json(*)
|
25
|
+
{
|
26
|
+
swlat: @bb_latitudes.min,
|
27
|
+
swlng: @bb_longitudes.min,
|
28
|
+
nelat: @bb_latitudes.max,
|
29
|
+
nelng: @bb_longitudes.max,
|
30
|
+
lat: latitude,
|
31
|
+
lng: longitude,
|
32
|
+
points: points.length
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"Clumpy::Cluster(latitude: #{latitude}, longitude: #{longitude}, # points: #{points.length})"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Clumpy::Builder do
|
4
|
+
let(:point1) { OpenStruct.new(lat: 15, lng: 20) }
|
5
|
+
let(:point2) { OpenStruct.new(lat: 15.5, lng: 20.5) }
|
6
|
+
let(:point3) { OpenStruct.new(lat: 50, lng: 50) }
|
7
|
+
|
8
|
+
let(:points) { [point1, point2, point3] }
|
9
|
+
let(:builder) { Clumpy::Builder.new(points) }
|
10
|
+
|
11
|
+
it "takes points and options" do
|
12
|
+
builder = Clumpy::Builder.new(points, { foo: :bar })
|
13
|
+
builder.points.should eq points
|
14
|
+
builder.options.should eq({ foo: :bar })
|
15
|
+
end
|
16
|
+
|
17
|
+
context "#cluster" do
|
18
|
+
it "creates clusters from points" do
|
19
|
+
clusters = builder.cluster
|
20
|
+
clusters.size.should eq 2
|
21
|
+
clusters.first.points.should eq [point1, point2]
|
22
|
+
clusters.last.points.should eq [point3]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "#add_to_cluster" do
|
27
|
+
it "creates a new cluster if there is no matching" do
|
28
|
+
builder.clusters.should be_empty
|
29
|
+
builder.add_to_cluster(point1)
|
30
|
+
builder.clusters.size.should eq 1
|
31
|
+
end
|
32
|
+
|
33
|
+
it "appends markers to a existing cluster if they match" do
|
34
|
+
builder.clusters << Clumpy::Cluster.new(point1, builder.cluster_distance)
|
35
|
+
builder.clusters.size.should eq 1
|
36
|
+
builder.clusters.first.points.size.should eq 1
|
37
|
+
builder.add_to_cluster(point2)
|
38
|
+
builder.clusters.size.should eq 1
|
39
|
+
builder.clusters.first.points.size.should eq 2
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "#find_parent_cluster" do
|
44
|
+
it "returns the surrounding cluster if there is one" do
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "calculates the latitude distance" do
|
50
|
+
builder.latitude_distance.should eq Clumpy::Builder::MAX_LATITUDE_DISTANCE
|
51
|
+
builder.options = { nelat: -30, swlat: 40 }
|
52
|
+
builder.latitude_distance.should eq 70
|
53
|
+
builder.options = { nelat: -30, swlat: -35 }
|
54
|
+
builder.latitude_distance.should eq 5
|
55
|
+
builder.options = { nelat: 20, swlat: 35 }
|
56
|
+
builder.latitude_distance.should eq 15
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Clumpy::Cluster do
|
4
|
+
let(:point) { OpenStruct.new(lat: 15, lng: 20) }
|
5
|
+
let(:other_point) { OpenStruct.new(lat: 40, lng: 40) }
|
6
|
+
let(:cluster) { Clumpy::Cluster.new(point, 10) }
|
7
|
+
|
8
|
+
it "has a nicish to_s representation" do
|
9
|
+
cluster.to_s.should include '# points: 1'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "has a json represtation as well" do
|
13
|
+
cluster.as_json.should have_key :points
|
14
|
+
cluster.as_json.should have_key :lat
|
15
|
+
cluster.as_json.should have_key :lng
|
16
|
+
end
|
17
|
+
|
18
|
+
context "#contains" do
|
19
|
+
it "returns true if the given point is within the bounds" do
|
20
|
+
cluster.contains?(point).should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns false for strange values" do
|
24
|
+
cluster.contains?(nil).should be_false
|
25
|
+
cluster.contains?('').should be_false
|
26
|
+
cluster.contains?(42).should be_false
|
27
|
+
cluster.contains?([42, 42]).should be_false
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns false for a points that isn't contained" do
|
31
|
+
cluster.contains?(other_point).should be_false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
SimpleCov.adapters.define 'gem' do
|
4
|
+
add_filter '/spec/'
|
5
|
+
add_filter '/autotest/'
|
6
|
+
add_group 'Libraries', '/lib/'
|
7
|
+
end
|
8
|
+
SimpleCov.start 'gem'
|
9
|
+
|
10
|
+
require 'clumpy'
|
11
|
+
require 'ostruct'
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.color_enabled = true
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clumpy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Johannes Opper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: simplecov
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Cluster points e.g. for a map
|
79
|
+
email:
|
80
|
+
- xijo@gmx.de
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- Gemfile
|
87
|
+
- LICENSE.txt
|
88
|
+
- README.md
|
89
|
+
- Rakefile
|
90
|
+
- clumpy.gemspec
|
91
|
+
- lib/clumpy.rb
|
92
|
+
- lib/clumpy/builder.rb
|
93
|
+
- lib/clumpy/cluster.rb
|
94
|
+
- lib/clumpy/version.rb
|
95
|
+
- spec/builder_spec.rb
|
96
|
+
- spec/cluster_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
homepage: http://github.com/xijo/clumpy
|
99
|
+
licenses:
|
100
|
+
- WTFPL
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
hash: -2869748522569163282
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
hash: -2869748522569163282
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.8.25
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: Cluster a bunch of points
|
129
|
+
test_files:
|
130
|
+
- spec/builder_spec.rb
|
131
|
+
- spec/cluster_spec.rb
|
132
|
+
- spec/spec_helper.rb
|