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
@@ -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
|