tsuga 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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +146 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/Rakefile +1 -0
- data/lib/tsuga.rb +11 -0
- data/lib/tsuga/adapter.rb +4 -0
- data/lib/tsuga/adapter/active_record/base.rb +61 -0
- data/lib/tsuga/adapter/active_record/cluster.rb +52 -0
- data/lib/tsuga/adapter/active_record/migration.rb +50 -0
- data/lib/tsuga/adapter/active_record/record.rb +15 -0
- data/lib/tsuga/adapter/active_record/test.rb +73 -0
- data/lib/tsuga/adapter/memory/base.rb +146 -0
- data/lib/tsuga/adapter/memory/cluster.rb +32 -0
- data/lib/tsuga/adapter/memory/test.rb +27 -0
- data/lib/tsuga/adapter/mongoid/base.rb +41 -0
- data/lib/tsuga/adapter/mongoid/cluster.rb +29 -0
- data/lib/tsuga/adapter/mongoid/record.rb +16 -0
- data/lib/tsuga/adapter/mongoid/test.rb +77 -0
- data/lib/tsuga/adapter/sequel/base.rb +57 -0
- data/lib/tsuga/adapter/sequel/cluster.rb +43 -0
- data/lib/tsuga/adapter/sequel/record.rb +15 -0
- data/lib/tsuga/adapter/sequel/test.rb +73 -0
- data/lib/tsuga/adapter/shared.rb +4 -0
- data/lib/tsuga/adapter/shared/cluster.rb +19 -0
- data/lib/tsuga/errors.rb +3 -0
- data/lib/tsuga/model/cluster.rb +147 -0
- data/lib/tsuga/model/point.rb +206 -0
- data/lib/tsuga/model/record.rb +20 -0
- data/lib/tsuga/model/tile.rb +136 -0
- data/lib/tsuga/service/aggregator.rb +175 -0
- data/lib/tsuga/service/clusterer.rb +260 -0
- data/lib/tsuga/service/labeler.rb +20 -0
- data/lib/tsuga/version.rb +3 -0
- data/script/benchmark-aggregator.rb +72 -0
- data/script/benchmark-clusterer.rb +102 -0
- data/spec/adapter/memory/base_spec.rb +174 -0
- data/spec/adapter/memory/cluster_spec.rb +39 -0
- data/spec/adapter/shared/cluster_spec.rb +56 -0
- data/spec/integration/active_record_spec.rb +10 -0
- data/spec/integration/memory_spec.rb +10 -0
- data/spec/integration/mongoid_spec.rb +10 -0
- data/spec/integration/sequel_spec.rb +10 -0
- data/spec/integration/shared.rb +50 -0
- data/spec/model/point_spec.rb +102 -0
- data/spec/model/tile_spec.rb +116 -0
- data/spec/service/aggregator_spec.rb +143 -0
- data/spec/service/clusterer_spec.rb +84 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/mongoid.yml +17 -0
- data/tsuga.gemspec +29 -0
- metadata +226 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3b7231a847c915d67cec25a993f18e479eafaab9
|
4
|
+
data.tar.gz: c75775155ef056b707cf291bfff6bedf8c2ee176
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a7a8011265392a799313b381e7fd2b16efa724d626f4ec0eeb02268c5b33588adfe27d7cb8a947aeb26315aa7e2d42a348b5fd66beb24c18f7fe1c1881ab333b
|
7
|
+
data.tar.gz: f0a71c00014efc2e0b72019488ab472ddaa270e36666130902b3e1efffb1bd540acba79c535891cd0d9e61573da0832553b1daf1b9078cc90562920481ff6635
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p247
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source ENV.fetch('GEM_SOURCE', 'https://rubygems.org')
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in tsuga.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'sqlite3', :require => false
|
7
|
+
gem 'mysql2', :require => false
|
8
|
+
gem 'sequel', :require => false
|
9
|
+
gem 'mongoid', :require => false
|
10
|
+
gem 'activerecord', :require => false
|
11
|
+
gem 'perftools.rb', :require => false
|
12
|
+
|
13
|
+
gem 'travis', :require => false
|
14
|
+
gem 'websocket-native', :require => false
|
15
|
+
|
16
|
+
gem 'ruby-progressbar'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
tsuga (0.0.1)
|
5
|
+
ruby-progressbar
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://yarp.dev/
|
9
|
+
specs:
|
10
|
+
activemodel (3.2.15)
|
11
|
+
activesupport (= 3.2.15)
|
12
|
+
builder (~> 3.0.0)
|
13
|
+
activerecord (3.2.15)
|
14
|
+
activemodel (= 3.2.15)
|
15
|
+
activesupport (= 3.2.15)
|
16
|
+
arel (~> 3.0.2)
|
17
|
+
tzinfo (~> 0.3.29)
|
18
|
+
activesupport (3.2.15)
|
19
|
+
i18n (~> 0.6, >= 0.6.4)
|
20
|
+
multi_json (~> 1.0)
|
21
|
+
addressable (2.3.5)
|
22
|
+
arel (3.0.2)
|
23
|
+
backports (3.3.5)
|
24
|
+
builder (3.0.4)
|
25
|
+
celluloid (0.15.2)
|
26
|
+
timers (~> 1.1.0)
|
27
|
+
coderay (1.0.9)
|
28
|
+
diff-lcs (1.2.4)
|
29
|
+
ethon (0.6.1)
|
30
|
+
ffi (>= 1.3.0)
|
31
|
+
mime-types (~> 1.18)
|
32
|
+
faraday (0.8.8)
|
33
|
+
multipart-post (~> 1.2.0)
|
34
|
+
faraday_middleware (0.9.0)
|
35
|
+
faraday (>= 0.7.4, < 0.9)
|
36
|
+
ffi (1.9.0)
|
37
|
+
formatador (0.2.4)
|
38
|
+
gh (0.13.0)
|
39
|
+
addressable
|
40
|
+
backports
|
41
|
+
faraday (~> 0.8)
|
42
|
+
multi_json (~> 1.0)
|
43
|
+
net-http-persistent (>= 2.7)
|
44
|
+
net-http-pipeline
|
45
|
+
guard (2.1.1)
|
46
|
+
formatador (>= 0.2.4)
|
47
|
+
listen (~> 2.1)
|
48
|
+
lumberjack (~> 1.0)
|
49
|
+
pry (>= 0.9.12)
|
50
|
+
thor (>= 0.18.1)
|
51
|
+
guard-rspec (4.0.3)
|
52
|
+
guard (>= 2.1.1)
|
53
|
+
rspec (~> 2.14)
|
54
|
+
highline (1.6.20)
|
55
|
+
i18n (0.6.5)
|
56
|
+
launchy (2.3.0)
|
57
|
+
addressable (~> 2.3)
|
58
|
+
listen (2.1.1)
|
59
|
+
celluloid (>= 0.15.2)
|
60
|
+
rb-fsevent (>= 0.9.3)
|
61
|
+
rb-inotify (>= 0.9)
|
62
|
+
lumberjack (1.0.4)
|
63
|
+
method_source (0.8.2)
|
64
|
+
mime-types (1.25)
|
65
|
+
mongoid (3.1.5)
|
66
|
+
activemodel (~> 3.2)
|
67
|
+
moped (~> 1.4)
|
68
|
+
origin (~> 1.0)
|
69
|
+
tzinfo (~> 0.3.29)
|
70
|
+
moped (1.5.1)
|
71
|
+
multi_json (1.8.2)
|
72
|
+
multipart-post (1.2.0)
|
73
|
+
mysql2 (0.3.13)
|
74
|
+
net-http-persistent (2.9)
|
75
|
+
net-http-pipeline (1.0.1)
|
76
|
+
netrc (0.7.7)
|
77
|
+
origin (1.1.0)
|
78
|
+
perftools.rb (2.0.1)
|
79
|
+
pry (0.9.12.2)
|
80
|
+
coderay (~> 1.0.5)
|
81
|
+
method_source (~> 0.8)
|
82
|
+
slop (~> 3.4)
|
83
|
+
pry-nav (0.2.3)
|
84
|
+
pry (~> 0.9.10)
|
85
|
+
pusher-client (0.3.1)
|
86
|
+
ruby-hmac (~> 0.4.0)
|
87
|
+
websocket (~> 1.0.0)
|
88
|
+
rake (10.1.0)
|
89
|
+
rb-fsevent (0.9.3)
|
90
|
+
rb-inotify (0.9.2)
|
91
|
+
ffi (>= 0.5.0)
|
92
|
+
rspec (2.14.1)
|
93
|
+
rspec-core (~> 2.14.0)
|
94
|
+
rspec-expectations (~> 2.14.0)
|
95
|
+
rspec-mocks (~> 2.14.0)
|
96
|
+
rspec-core (2.14.6)
|
97
|
+
rspec-expectations (2.14.3)
|
98
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
99
|
+
rspec-mocks (2.14.4)
|
100
|
+
ruby-hmac (0.4.0)
|
101
|
+
ruby-progressbar (1.2.0)
|
102
|
+
sequel (4.3.0)
|
103
|
+
slop (3.4.6)
|
104
|
+
sqlite3 (1.3.8)
|
105
|
+
terminal-notifier (1.5.1)
|
106
|
+
thor (0.18.1)
|
107
|
+
timers (1.1.0)
|
108
|
+
travis (1.5.5)
|
109
|
+
backports
|
110
|
+
faraday (~> 0.8.7)
|
111
|
+
faraday_middleware (~> 0.9)
|
112
|
+
gh (~> 0.13)
|
113
|
+
highline (~> 1.6)
|
114
|
+
launchy (~> 2.1)
|
115
|
+
netrc (~> 0.7)
|
116
|
+
pry (~> 0.9)
|
117
|
+
pusher-client (~> 0.3, >= 0.3.1)
|
118
|
+
terminal-notifier (>= 1.4.2)
|
119
|
+
typhoeus (~> 0.6)
|
120
|
+
typhoeus (0.6.5)
|
121
|
+
ethon (~> 0.6.1)
|
122
|
+
tzinfo (0.3.38)
|
123
|
+
websocket (1.0.7)
|
124
|
+
websocket-native (1.0.0)
|
125
|
+
|
126
|
+
PLATFORMS
|
127
|
+
ruby
|
128
|
+
|
129
|
+
DEPENDENCIES
|
130
|
+
activerecord
|
131
|
+
bundler (~> 1.3)
|
132
|
+
guard
|
133
|
+
guard-rspec
|
134
|
+
mongoid
|
135
|
+
mysql2
|
136
|
+
perftools.rb
|
137
|
+
pry
|
138
|
+
pry-nav
|
139
|
+
rake
|
140
|
+
rspec
|
141
|
+
ruby-progressbar
|
142
|
+
sequel
|
143
|
+
sqlite3
|
144
|
+
travis
|
145
|
+
tsuga!
|
146
|
+
websocket-native
|
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Julien Letessier
|
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,161 @@
|
|
1
|
+
# Tsuga
|
2
|
+
|
3
|
+
[](https://travis-ci.org/mezis/tsuga)
|
4
|
+
|
5
|
+
<img width="320" style="margin-left:1em;margin-bottom:1em;clear:both;float:right;" src="http://cl.ly/image/251X2b1p1B00/b1.jpg"/>
|
6
|
+
<img width="320" style="margin-left:1em;margin-bottom:1em;clear:both;float:right;" src="http://cl.ly/image/1T1U421F1P2G/b2.jpg"/>
|
7
|
+
|
8
|
+
A **clustering engine** for geographical data (points of interest) that
|
9
|
+
produces a **tree** of clusters, with depths matching zoomlevels on typical
|
10
|
+
maps, and source points of interest as leaves.
|
11
|
+
|
12
|
+
Makes heavy use of [Geohash](http://en.wikipedia.org/wiki/Geohash)-like and
|
13
|
+
[Morton codes](http://en.wikipedia.org/wiki/Morton_number_%28number_theory%29).
|
14
|
+
|
15
|
+
Designed with Rails usage in mind, but usable without Rails or even without a database.
|
16
|
+
|
17
|
+
Go play with the [live demo](http://tsuga-demo.herokuapp.com/) from which
|
18
|
+
the screenshots on the right were taken. Be patient, it's a free Heroku app!
|
19
|
+
The [source](http://github.com/mezis/tsuga-demo) of the demo is an example
|
20
|
+
of how to use Tsuga.
|
21
|
+
|
22
|
+
### Why?
|
23
|
+
|
24
|
+
Yes, Google Maps [does this](https://code.google.com/p/google-maps-utility-library-v3/wiki/Libraries#Marker_Clusterer_Plus)... for small datasets.
|
25
|
+
|
26
|
+
- **Performance**. If you're handling thousands to millions of points of
|
27
|
+
interest, you cannot rely on client-side clustering anymore, if only because
|
28
|
+
sending the coordinates across would take minutes. <br/>
|
29
|
+
Tsuga gives you millisecond queries to find clusters to display, even when your
|
30
|
+
source dataset is huge.
|
31
|
+
|
32
|
+
- **Structure**. Client-side solution cluster the markers you have. If you
|
33
|
+
want to let users drill down into data, you need to preserve a tree
|
34
|
+
structure: zooming in on a cluster should show what's "inside". <br/>
|
35
|
+
Tsuga builds a walkable tree of clusters.
|
36
|
+
|
37
|
+
|
38
|
+
# Installation
|
39
|
+
|
40
|
+
Add the `tsuga` gem to your `Gemfile`:
|
41
|
+
|
42
|
+
gem 'tsuga'
|
43
|
+
|
44
|
+
|
45
|
+
# Usage
|
46
|
+
|
47
|
+
Four steps are typically involved:
|
48
|
+
|
49
|
+
1. Provide a source of points of interest to cluster
|
50
|
+
2. Provide storage for clusters
|
51
|
+
3. Run the clusterer
|
52
|
+
4. Lookup clusters or a particlar map viewport
|
53
|
+
|
54
|
+
|
55
|
+
## Providing source points
|
56
|
+
|
57
|
+
Tsuga need to know how to iterate over points of interests.
|
58
|
+
Any enumerable will do as long as
|
59
|
+
- it responds to `find_each` or `each`, and
|
60
|
+
- it yields records that respond to `id` with an integer, and to `lat` and `lng` with floating-point numbers.
|
61
|
+
|
62
|
+
Simply put, anything `ActiveModel`-ish should do.
|
63
|
+
|
64
|
+
|
65
|
+
## Providing cluster storage
|
66
|
+
|
67
|
+
|
68
|
+
Tsuga also need you to provide storage for the clusters. Currently supported
|
69
|
+
are [Mongoid][mongoid], [Sequel][sequel], and [ActiveRecord][active_record] (an in-memory backend is provided, but is
|
70
|
+
only useful for testing or extremely small datasets).
|
71
|
+
|
72
|
+
|
73
|
+
### ActiveRecord
|
74
|
+
|
75
|
+
Example with [ActiveRecord][active_record]. Create a migration:
|
76
|
+
|
77
|
+
require 'tsuga/adapter/active_record/cluster_migration'
|
78
|
+
|
79
|
+
class AddClusters < ActiveRecord::Migration
|
80
|
+
include Tsuga::Adapter::ActiveRecord::Migration
|
81
|
+
self.clusters_table_name = :clusters
|
82
|
+
end
|
83
|
+
|
84
|
+
And the matching `Cluster` model:
|
85
|
+
|
86
|
+
# app/models/cluster.rb
|
87
|
+
require 'tsuga/adapter/active_record/cluster_model'
|
88
|
+
|
89
|
+
class Cluster < ActiveRecord::Model
|
90
|
+
include Tsuga::Adapter::ActiveRecord::ClusterModel
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
### Mongoid
|
95
|
+
|
96
|
+
Example with [Mongoid][mongoid].
|
97
|
+
|
98
|
+
# app/models/cluster.rb
|
99
|
+
require 'tsuga/adapter/mongoid/cluster_model'
|
100
|
+
|
101
|
+
class Cluster
|
102
|
+
include Tsuga::Adapter::Mongoid::ClusterModel
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
### Sequel
|
107
|
+
|
108
|
+
Example with [Sequel][sequel].
|
109
|
+
|
110
|
+
# app/models/cluster.rb
|
111
|
+
require 'tsuga/adapter/sequel/cluster_model'
|
112
|
+
|
113
|
+
class Cluster < Sequel::Model(:clusters)
|
114
|
+
include Tsuga::Adapter::Sequel::ClusterModel
|
115
|
+
end
|
116
|
+
|
117
|
+
You will have to provide your own migration, respecting the schema in
|
118
|
+
`Tsuga::Adapter::ActiveRecord::Migration`.
|
119
|
+
|
120
|
+
|
121
|
+
## Running the clusterer service
|
122
|
+
|
123
|
+
The clustering engine is `Tsuga::Service::Clusterer`, and running a full
|
124
|
+
clustering is as simple as:
|
125
|
+
|
126
|
+
require 'tsuga'
|
127
|
+
Tsuga::Service::Clusterer.new(source: PointOfInterest, adapter: Cluster).run
|
128
|
+
|
129
|
+
This will delete all existing clusters, walk the points of interest, and
|
130
|
+
rebuild a tree of clusters.
|
131
|
+
|
132
|
+
|
133
|
+
## Finding clusters
|
134
|
+
|
135
|
+
Tsuga extended your cluster class with helper class methods:
|
136
|
+
|
137
|
+
nw = Tsuga::Point(lat: 45, lng: 1)
|
138
|
+
se = Tsuga::Point(lat: 44, lng: 2)
|
139
|
+
Cluster.in_viewport(nw, se)
|
140
|
+
|
141
|
+
will return an enumerable (scopish where possible), that responds to
|
142
|
+
`find_each`, `each`, and `count`, and contains clusters within the specified
|
143
|
+
viewport.
|
144
|
+
|
145
|
+
## Cluster API
|
146
|
+
|
147
|
+
Clusters have at least the following accessors:
|
148
|
+
|
149
|
+
| method | description |
|
150
|
+
|-------------|----------------------------------------|
|
151
|
+
| `lat` | latitude of the cluster's barycenter |
|
152
|
+
| `lng` | longitude of the cluster's barycenter |
|
153
|
+
| `weight` | total number of points (leaves) in subtree |
|
154
|
+
| `children` | enumerable of child clusters (or points of interest) |
|
155
|
+
| `depth` | the scale this cluster is relevant at, where 0 is the whole world |
|
156
|
+
|
157
|
+
|
158
|
+
[mongoid]: http://mongoid.org/
|
159
|
+
[sequel]: http://sequel.rubyforge.org/
|
160
|
+
[active_record]: http://guides.rubyonrails.org/
|
161
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/tsuga.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'tsuga/errors'
|
2
|
+
require 'tsuga/adapter'
|
3
|
+
require 'active_record'
|
4
|
+
require 'delegate'
|
5
|
+
|
6
|
+
module Tsuga::Adapter::ActiveRecord
|
7
|
+
module Base
|
8
|
+
def self.included(by)
|
9
|
+
by.extend DatasetMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
@_id ||= super
|
14
|
+
end
|
15
|
+
|
16
|
+
def persist!
|
17
|
+
save!
|
18
|
+
end
|
19
|
+
|
20
|
+
module DatasetMethods
|
21
|
+
def mass_create(new_records)
|
22
|
+
return if new_records.empty?
|
23
|
+
|
24
|
+
# Old SQLite versions (like on Travis) do not support bulk inserts
|
25
|
+
if connection.class.name !~ /sqlite/i || connection.send(:sqlite_version) >= '3.7.11'
|
26
|
+
_bulk_insert(new_records)
|
27
|
+
else
|
28
|
+
new_records.each(&:save!)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def mass_update(records)
|
33
|
+
transaction do
|
34
|
+
records.each(&:save!)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def collect_ids
|
39
|
+
pluck(:id)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def _bulk_insert(records)
|
45
|
+
attributes = records.map(&:attributes)
|
46
|
+
keys = attributes.first.keys - ['id']
|
47
|
+
column_names = keys.map { |k| connection.quote_column_name(k) }.join(', ')
|
48
|
+
sql = <<-SQL
|
49
|
+
INSERT INTO #{quoted_table_name} (#{column_names}) VALUES
|
50
|
+
SQL
|
51
|
+
value_template = (["?"] * keys.length).join(', ')
|
52
|
+
value_strings = attributes.map do |attrs|
|
53
|
+
values = keys.map { |k| attrs[k] }
|
54
|
+
sanitize_sql_array([value_template, *values])
|
55
|
+
end
|
56
|
+
full_sql = sql + value_strings.map { |str| "(#{str})"}.join(', ')
|
57
|
+
connection.insert_sql(full_sql)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|