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.
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,52 @@
1
+ require 'tsuga/model/cluster'
2
+ require 'tsuga/model/tile'
3
+ require 'tsuga/adapter/active_record/base'
4
+ require 'tsuga/adapter/shared/cluster'
5
+
6
+ module Tsuga::Adapter::ActiveRecord
7
+ module Cluster
8
+ def self.included(by)
9
+ by.send :include, Base
10
+ by.send :include, Tsuga::Model::Cluster
11
+ by.send :include, Tsuga::Adapter::Shared::Cluster
12
+ by.extend Scopes
13
+
14
+ by.class_eval do
15
+ belongs_to :parent, class_name: by.name
16
+ end
17
+ end
18
+
19
+ def children_ids
20
+ @_children_ids ||= begin
21
+ stored = super
22
+ stored ? stored.split(',').map(&:to_i) : []
23
+ end
24
+ end
25
+
26
+ def children_ids=(value)
27
+ changed = (@_children_ids != value)
28
+ @_children_ids = value
29
+ super(@_children_ids.join(',')) if changed
30
+ @_children_ids
31
+ end
32
+
33
+ module Scopes
34
+ def at_depth(depth)
35
+ where(depth: depth)
36
+ end
37
+
38
+ # FIXME: this also is redundant with the mongoid adapter implementation
39
+ def in_tile(*tiles)
40
+ depths = tiles.map(&:depth).uniq
41
+ raise ArgumentError, 'all tile must be at same depth' if depths.length > 1
42
+ where(tilecode: tiles.map(&:prefix))
43
+ end
44
+
45
+ def in_viewport(sw:nil, ne:nil, depth:nil)
46
+ tiles = Tsuga::Model::Tile.enclosing_viewport(point_sw: sw, point_ne: ne, depth: depth)
47
+ in_tile(*tiles)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,50 @@
1
+ require 'tsuga/adapter'
2
+ require 'active_record'
3
+
4
+ module Tsuga::Adapter::ActiveRecord
5
+ module Migration
6
+ def self.included(by)
7
+ by.extend(ClassMethods)
8
+ end
9
+
10
+ def up
11
+ create_table _clusters_table_name do |t|
12
+ t.string :tilecode, limit:32
13
+ t.integer :depth, limit:1
14
+ t.string :geohash, limit:32
15
+ t.float :lat
16
+ t.float :lng
17
+ t.integer :weight
18
+ t.integer :parent_id
19
+ t.string :children_type
20
+ t.text :children_ids
21
+ t.float :sum_lat, limit:53
22
+ t.float :sum_lng, limit:53
23
+ t.float :ssq_lat, limit:53
24
+ t.float :ssq_lng, limit:53
25
+ end
26
+
27
+ add_index _clusters_table_name, :tilecode, using: :hash
28
+ end
29
+
30
+ def down
31
+ drop_table _clusters_table_name
32
+ end
33
+
34
+ private
35
+
36
+ def _clusters_table_name
37
+ self.class.clusters_table_name
38
+ end
39
+
40
+ module ClassMethods
41
+ def clusters_table_name=(custom_name)
42
+ @clusters_table_name = custom_name
43
+ end
44
+
45
+ def clusters_table_name
46
+ @clusters_table_name ||= :clusters
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ require 'tsuga/model/record'
2
+ require 'tsuga/adapter/active_record/base'
3
+
4
+ module Tsuga::Adapter::ActiveRecord
5
+ module Record
6
+ def self.included(by)
7
+ by.send :include, Base
8
+ by.send :include, Tsuga::Model::Record
9
+ by.extend Scopes
10
+ end
11
+
12
+ module Scopes
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,73 @@
1
+ require 'tsuga/adapter/active_record/base'
2
+ require 'tsuga/adapter/active_record/cluster'
3
+ require 'tsuga/adapter/active_record/record'
4
+ require 'tsuga/adapter/active_record/migration'
5
+ require 'active_record'
6
+ require 'sqlite3'
7
+ require 'ostruct'
8
+ require 'forwardable'
9
+ require 'yaml'
10
+
11
+ module Tsuga::Adapter::ActiveRecord
12
+ module Test
13
+ class << self
14
+ extend Forwardable
15
+ delegate [:records, :clusters] => :models
16
+
17
+ def models
18
+ @_models ||= _build_test_models
19
+ end
20
+
21
+ private
22
+
23
+ # Makes sure a connection exists
24
+ def _db
25
+ @_db ||= begin
26
+ unless ActiveRecord::Base.connected?
27
+ ActiveRecord::Base.establish_connection(adapter:'sqlite3', database:':memory:')
28
+ end
29
+ ActiveRecord::Base.connection
30
+ end
31
+ end
32
+
33
+ def _prepare_tables
34
+ _db.drop_table(:test_records) if _db.table_exists?(:test_records)
35
+ _db.create_table(:test_records) do |t|
36
+ t.string :geohash, limit:32
37
+ t.float :lat
38
+ t.float :lng
39
+ end
40
+ _db.add_index :test_records, :geohash
41
+
42
+ _db.drop_table(:test_clusters) if _db.table_exists?(:test_clusters)
43
+ Migration.new.tap { |m| m.verbose = false ; m.up }
44
+ end
45
+
46
+ def _build_test_models
47
+ _prepare_tables
48
+
49
+ cluster_model = Class.new(ActiveRecord::Base) do
50
+ self.table_name = 'test_clusters'
51
+ include Tsuga::Adapter::ActiveRecord::Cluster
52
+
53
+ def run_callbacks(*args)
54
+ yield if block_given?
55
+ end
56
+ end
57
+
58
+ record_model = Class.new(ActiveRecord::Base) do
59
+ self.table_name = 'test_records'
60
+ include Tsuga::Adapter::ActiveRecord::Record
61
+ end
62
+
63
+ OpenStruct.new :clusters => cluster_model, :records => record_model
64
+ end
65
+
66
+ class Migration < ActiveRecord::Migration
67
+ include Tsuga::Adapter::ActiveRecord::Migration
68
+ self.clusters_table_name = :test_clusters
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,146 @@
1
+ require 'set'
2
+ require 'tsuga/errors'
3
+ require 'tsuga/adapter'
4
+
5
+ module Tsuga::Adapter::Memory
6
+ # A memory-backed activerecord pattern implementation.
7
+ # Makes my test fly like crazy.
8
+ module Base
9
+
10
+ def self.included(by)
11
+ by.send :attr_reader, :id
12
+ by.extend ClassMethods
13
+ end
14
+
15
+
16
+ def initialize(*args)
17
+ @new_record = false
18
+ super(*args)
19
+ end
20
+
21
+
22
+ def new_record?
23
+ @new_record
24
+ end
25
+
26
+ def persist!
27
+ @id ||= self.class.generate_id
28
+ @new_record = false
29
+ self.class._records[id] = self.clone
30
+ self
31
+ end
32
+
33
+ def destroy
34
+ self.class._records.delete(id)
35
+ @id = nil
36
+ self
37
+ end
38
+
39
+
40
+ module ClassMethods
41
+ def mass_create(records)
42
+ records.each(&:persist!)
43
+ end
44
+
45
+ def mass_update(records)
46
+ records.each(&:persist!)
47
+ end
48
+
49
+ # FIXME: not thread safe. not sure we care, either.
50
+ def generate_id
51
+ @_last_id ||= 0
52
+ @_last_id += 1
53
+ end
54
+
55
+ def find_by_id(id)
56
+ _records.fetch(id) { raise Tsuga::RecordNotFound }.clone
57
+ end
58
+
59
+ def scoped(*filters)
60
+ Scope.new(self, filters)
61
+ end
62
+
63
+ def delete_all
64
+ _records.replace Hash.new
65
+ end
66
+
67
+ def find_each
68
+ _records.dup.each_value { |r| yield r.clone }
69
+ end
70
+
71
+ def collect_ids
72
+ Set.new(_records.keys)
73
+ end
74
+
75
+ def _records
76
+ @_records ||= {}
77
+ end
78
+ end # ClassMethods
79
+
80
+
81
+ class Scope
82
+ attr_reader :_filters
83
+ attr_reader :_origin
84
+
85
+ def initialize(origin, filters)
86
+ @_origin = origin
87
+ @_filters = filters
88
+ end
89
+
90
+ def scoped(*filters)
91
+ Scope.new(_origin, _filters + filters)
92
+ end
93
+
94
+ def find_by_id(id)
95
+ _origin.find_by_id(id).tap do |record|
96
+ raise Tsuga::RecordNotFound unless _matches?(record)
97
+ end
98
+ end
99
+
100
+ def delete_all
101
+ _origin._records.each_pair do |id,record|
102
+ next unless _matches?(record)
103
+ _origin._records.delete(id)
104
+ end
105
+ end
106
+
107
+ def find_each
108
+ _origin._records.dup.each_value do |record|
109
+ next unless _matches?(record)
110
+ yield record.clone
111
+ end
112
+ end
113
+
114
+ def collect_ids
115
+ Set.new.tap do |result|
116
+ _origin._records.each_value do |record|
117
+ next unless _matches?(record)
118
+ result << record.id
119
+ end
120
+ end
121
+ end
122
+
123
+ def to_a
124
+ Array.new.tap do |ary|
125
+ find_each { |record| ary << record }
126
+ end
127
+ end
128
+
129
+ def method_missing(method, *args)
130
+ result = _origin.send(method, *args)
131
+ result = scoped(*(result._filters)) if result.kind_of?(Scope)
132
+ end
133
+
134
+ def respond_to?(method, include_private=false)
135
+ super || _origin.respond_to?(method, include_private)
136
+ end
137
+
138
+ private
139
+
140
+ def _matches?(record)
141
+ _filters.all? { |f| f.call(record) }
142
+ end
143
+ end # Scope
144
+
145
+ end
146
+ end
@@ -0,0 +1,32 @@
1
+ require 'tsuga/model/cluster'
2
+ require 'tsuga/adapter/memory/base'
3
+ require 'tsuga/adapter/shared/cluster'
4
+
5
+ module Tsuga::Adapter::Memory
6
+ module Cluster
7
+ module Fields
8
+ attr_accessor :geohash, :lat, :lng, :depth, :parent_id
9
+ attr_accessor :children_type, :children_ids
10
+ attr_accessor :sum_lat, :sum_lng, :ssq_lat, :ssq_lng, :weight
11
+ attr_accessor :tilecode
12
+ end
13
+
14
+ def self.included(by)
15
+ by.send :include, Fields
16
+ by.send :include, Base
17
+ by.send :include, Tsuga::Model::Cluster
18
+ by.send :include, Tsuga::Adapter::Shared::Cluster
19
+ by.extend ClassMethods
20
+ end
21
+
22
+ module ClassMethods
23
+ def at_depth(depth)
24
+ scoped(lambda { |r| r.depth == depth })
25
+ end
26
+
27
+ def in_tile(*tiles)
28
+ scoped(lambda { |r| tiles.any? { |t| (t.depth == r.depth) && t.contains?(r) } })
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ require 'tsuga/adapter/memory/base'
2
+ require 'tsuga/adapter/memory/cluster'
3
+ require 'ostruct'
4
+
5
+ module Tsuga::Adapter::Memory
6
+ module Test
7
+ class << self
8
+ def clusters
9
+ models.clusters
10
+ end
11
+
12
+ def models
13
+ @_models ||= _build_test_models
14
+ end
15
+
16
+
17
+ private
18
+
19
+ def _build_test_models
20
+
21
+ OpenStruct.new :clusters => Class.new {
22
+ include Tsuga::Adapter::Memory::Cluster
23
+ }, :records => Array
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ require 'tsuga/errors'
2
+ require 'tsuga/adapter'
3
+ require 'scanf'
4
+
5
+ module Tsuga::Adapter::Mongoid
6
+ module Base
7
+ def self.included(by)
8
+ by.extend ScopeMethods
9
+ end
10
+
11
+ def persist!
12
+ save!
13
+ end
14
+
15
+ def id
16
+ @_id ||= super
17
+ end
18
+
19
+ module ScopeMethods
20
+ def mass_create(new_records)
21
+ collection.insert(new_records.map(&:attributes))
22
+ end
23
+
24
+ def mass_update(records)
25
+ records.map(&:persist!)
26
+ end
27
+
28
+ def find_by_id(id)
29
+ find(id)
30
+ end
31
+
32
+ def collect_ids
33
+ pluck(:id)
34
+ end
35
+
36
+ def find_each(&block)
37
+ each(&block)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ require 'tsuga/model/tile'
2
+ require 'tsuga/model/cluster'
3
+ require 'tsuga/adapter/mongoid/base'
4
+ require 'tsuga/adapter/shared/cluster'
5
+ require 'mongoid'
6
+
7
+ module Tsuga::Adapter::Mongoid
8
+ module Cluster
9
+ def self.included(by)
10
+ by.send :include, Base
11
+ by.send :include, Tsuga::Model::Cluster
12
+ by.send :include, Tsuga::Adapter::Shared::Cluster
13
+ by.extend ScopeMethods
14
+ end
15
+
16
+ module ScopeMethods
17
+ def at_depth(depth)
18
+ where(:depth => depth)
19
+ end
20
+
21
+ def in_tile(*tiles)
22
+ # where(:geohash.gte => sw, :geohash.lte => ne)
23
+ depths = tiles.map(&:depth).uniq
24
+ raise ArgumentError, 'all tiles must be at same depth' if depths.length > 1
25
+ where(:tilecode.in => tiles.map(&:prefix))
26
+ end
27
+ end
28
+ end
29
+ end