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