tsuga 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/mezis/tsuga.png?branch=master)](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
|