tsuga 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +12 -0
  6. data/Gemfile +16 -0
  7. data/Gemfile.lock +146 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +161 -0
  11. data/Rakefile +1 -0
  12. data/lib/tsuga.rb +11 -0
  13. data/lib/tsuga/adapter.rb +4 -0
  14. data/lib/tsuga/adapter/active_record/base.rb +61 -0
  15. data/lib/tsuga/adapter/active_record/cluster.rb +52 -0
  16. data/lib/tsuga/adapter/active_record/migration.rb +50 -0
  17. data/lib/tsuga/adapter/active_record/record.rb +15 -0
  18. data/lib/tsuga/adapter/active_record/test.rb +73 -0
  19. data/lib/tsuga/adapter/memory/base.rb +146 -0
  20. data/lib/tsuga/adapter/memory/cluster.rb +32 -0
  21. data/lib/tsuga/adapter/memory/test.rb +27 -0
  22. data/lib/tsuga/adapter/mongoid/base.rb +41 -0
  23. data/lib/tsuga/adapter/mongoid/cluster.rb +29 -0
  24. data/lib/tsuga/adapter/mongoid/record.rb +16 -0
  25. data/lib/tsuga/adapter/mongoid/test.rb +77 -0
  26. data/lib/tsuga/adapter/sequel/base.rb +57 -0
  27. data/lib/tsuga/adapter/sequel/cluster.rb +43 -0
  28. data/lib/tsuga/adapter/sequel/record.rb +15 -0
  29. data/lib/tsuga/adapter/sequel/test.rb +73 -0
  30. data/lib/tsuga/adapter/shared.rb +4 -0
  31. data/lib/tsuga/adapter/shared/cluster.rb +19 -0
  32. data/lib/tsuga/errors.rb +3 -0
  33. data/lib/tsuga/model/cluster.rb +147 -0
  34. data/lib/tsuga/model/point.rb +206 -0
  35. data/lib/tsuga/model/record.rb +20 -0
  36. data/lib/tsuga/model/tile.rb +136 -0
  37. data/lib/tsuga/service/aggregator.rb +175 -0
  38. data/lib/tsuga/service/clusterer.rb +260 -0
  39. data/lib/tsuga/service/labeler.rb +20 -0
  40. data/lib/tsuga/version.rb +3 -0
  41. data/script/benchmark-aggregator.rb +72 -0
  42. data/script/benchmark-clusterer.rb +102 -0
  43. data/spec/adapter/memory/base_spec.rb +174 -0
  44. data/spec/adapter/memory/cluster_spec.rb +39 -0
  45. data/spec/adapter/shared/cluster_spec.rb +56 -0
  46. data/spec/integration/active_record_spec.rb +10 -0
  47. data/spec/integration/memory_spec.rb +10 -0
  48. data/spec/integration/mongoid_spec.rb +10 -0
  49. data/spec/integration/sequel_spec.rb +10 -0
  50. data/spec/integration/shared.rb +50 -0
  51. data/spec/model/point_spec.rb +102 -0
  52. data/spec/model/tile_spec.rb +116 -0
  53. data/spec/service/aggregator_spec.rb +143 -0
  54. data/spec/service/clusterer_spec.rb +84 -0
  55. data/spec/spec_helper.rb +26 -0
  56. data/spec/support/mongoid.yml +17 -0
  57. data/tsuga.gemspec +29 -0
  58. metadata +226 -0
@@ -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
@@ -0,0 +1,5 @@
1
+ .bundle
2
+ coverage
3
+ tmp
4
+ .tags*
5
+ doc/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ #--format d
3
+ --format progress
@@ -0,0 +1 @@
1
+ 2.0.0-p247
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ script:
5
+ - bundle exec rspec spec
6
+ branches:
7
+ only:
8
+ - master
9
+
10
+ services:
11
+ - mongodb
12
+
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'
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'bundler/setup'
2
+
3
+ guard :rspec do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/tsuga/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { "spec" }
7
+ end
8
+
@@ -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.
@@ -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
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,11 @@
1
+ module Tsuga
2
+ MIN_DEPTH = 3
3
+ MAX_DEPTH = 19
4
+
5
+ def self.Point(*args)
6
+ require 'tsuga/model/point'
7
+ Tsuga::Model::Point.new(*args)
8
+ end
9
+ end
10
+
11
+ require "tsuga/version"
@@ -0,0 +1,4 @@
1
+ require 'tsuga'
2
+
3
+ module Tsuga::Adapter
4
+ end
@@ -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