activerecord-spatial 0.0.1 → 0.1.0
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.
- data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb +24 -6
- data/lib/activerecord-spatial/associations.rb +5 -283
- data/lib/activerecord-spatial/associations/active_record.rb +146 -0
- data/lib/activerecord-spatial/associations/active_record_3.rb +123 -0
- data/lib/activerecord-spatial/associations/base.rb +182 -0
- data/lib/activerecord-spatial/spatial_columns.rb +1 -1
- data/lib/activerecord-spatial/spatial_function.rb +3 -2
- data/lib/activerecord-spatial/version.rb +1 -1
- data/test/associations_tests.rb +24 -4
- data/test/spatial_function_tests.rb +77 -0
- data/test/spatial_scopes_geographies_tests.rb +14 -14
- data/test/spatial_scopes_tests.rb +68 -68
- data/test/test_helper.rb +40 -31
- metadata +22 -11
- checksums.yaml +0 -15
data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb
CHANGED
@@ -5,12 +5,17 @@ module ActiveRecord
|
|
5
5
|
module ConnectionAdapters
|
6
6
|
class PostgreSQLColumn
|
7
7
|
def simplified_type_with_spatial_type(field_type)
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
case field_type
|
9
|
+
# This is a special internal type used by PostgreSQL. In this case,
|
10
|
+
# it is being used by the `geography_columns` view in PostGIS.
|
11
|
+
when 'name'
|
12
|
+
:string
|
13
|
+
when /^geometry(\(|$)/
|
14
|
+
:geometry
|
15
|
+
when /^geography(\(|$)/
|
16
|
+
:geography
|
17
|
+
else
|
18
|
+
simplified_type_without_spatial_type(field_type)
|
14
19
|
end
|
15
20
|
end
|
16
21
|
alias_method_chain :simplified_type, :spatial_type
|
@@ -24,6 +29,19 @@ module ActiveRecord
|
|
24
29
|
def geography_columns?
|
25
30
|
ActiveRecordSpatial::POSTGIS[:lib] >= '1.5'
|
26
31
|
end
|
32
|
+
|
33
|
+
if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID)
|
34
|
+
module OID
|
35
|
+
class Spatial < Type
|
36
|
+
def type_cast(value)
|
37
|
+
value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
register_type 'geometry', OID::Spatial.new
|
42
|
+
register_type 'geography', OID::Spatial.new
|
43
|
+
end
|
44
|
+
end
|
27
45
|
end
|
28
46
|
end
|
29
47
|
end
|
@@ -1,288 +1,10 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
module Associations #:nodoc:
|
4
|
-
class SpatialAssociation < HasManyAssociation
|
5
|
-
attr_reader :geom, :foreign_geom, :relationship, :scope_options
|
6
|
-
|
7
|
-
def initialize(*args)
|
8
|
-
super
|
9
|
-
|
10
|
-
@geom = self.options[:geom]
|
11
|
-
@foreign_geom = self.options[:foreign_geom]
|
12
|
-
@relationship = self.options[:relationship].to_s
|
13
|
-
@scope_options = (self.options[:scope_options] || {}).merge({
|
14
|
-
:column => @foreign_geom
|
15
|
-
})
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
class Builder::Spatial < Builder::HasMany #:nodoc:
|
20
|
-
self.macro = :has_many
|
21
|
-
|
22
|
-
self.valid_options += [
|
23
|
-
:geom, :foreign_geom, :relationship, :scope_options
|
24
|
-
]
|
25
|
-
|
26
|
-
self.valid_options -= [
|
27
|
-
:through, :source, :source_type, :dependent, :finder_sql, :counter_sql,
|
28
|
-
:inverse_of
|
29
|
-
]
|
30
|
-
|
31
|
-
private
|
32
|
-
def dependency_method_name
|
33
|
-
"spatially_#{self.relationship}_dependent_for_#{name}"
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
class Preloader #:nodoc:
|
38
|
-
class SpatialAssociation < HasMany #:nodoc:
|
39
|
-
SPATIAL_FIELD_ALIAS = '__spatial_ids__'
|
40
|
-
SPATIAL_JOIN_NAME = '__spatial_ids_join__'
|
41
|
-
SPATIAL_JOIN_QUOTED_NAME = %{"#{SPATIAL_JOIN_NAME}"}
|
42
|
-
|
43
|
-
def records_for(ids)
|
44
|
-
table_name = reflection.quoted_table_name
|
45
|
-
join_name = model.quoted_table_name
|
46
|
-
column = %{#{SPATIAL_JOIN_QUOTED_NAME}.#{model.quoted_primary_key}}
|
47
|
-
geom = {
|
48
|
-
:class => model,
|
49
|
-
:table_alias => SPATIAL_JOIN_NAME
|
50
|
-
}
|
51
|
-
|
52
|
-
if reflection.options[:geom].is_a?(Hash)
|
53
|
-
geom.merge!(reflection.options[:geom])
|
54
|
-
else
|
55
|
-
geom[:column] = reflection.options[:geom]
|
56
|
-
end
|
57
|
-
|
58
|
-
scoped.
|
59
|
-
select(%{array_to_string(array_agg(#{column}), ',') AS "#{SPATIAL_FIELD_ALIAS}"}).
|
60
|
-
joins(
|
61
|
-
"INNER JOIN #{join_name} AS #{SPATIAL_JOIN_QUOTED_NAME} ON (" <<
|
62
|
-
klass.send("st_#{reflection.options[:relationship]}",
|
63
|
-
geom,
|
64
|
-
(reflection.options[:scope_options] || {}).merge(
|
65
|
-
:column => reflection.options[:foreign_geom]
|
66
|
-
)
|
67
|
-
).where_values.join(' AND ') <<
|
68
|
-
")"
|
69
|
-
).
|
70
|
-
where(model.arel_table.alias(SPATIAL_JOIN_NAME)[model.primary_key].in(ids)).
|
71
|
-
group(table[klass.primary_key])
|
72
|
-
end
|
73
|
-
|
74
|
-
private
|
75
|
-
|
76
|
-
def associated_records_by_owner
|
77
|
-
owners_map = owners_by_key
|
78
|
-
owner_keys = owners_map.keys.compact
|
79
|
-
|
80
|
-
if klass.nil? || owner_keys.empty?
|
81
|
-
records = []
|
82
|
-
else
|
83
|
-
sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
|
84
|
-
records = sliced.map { |slice| records_for(slice) }.flatten
|
85
|
-
end
|
86
|
-
|
87
|
-
records_by_owner = Hash[owners.map { |owner| [owner, []] }]
|
88
|
-
|
89
|
-
records.each do |record|
|
90
|
-
record[SPATIAL_FIELD_ALIAS].split(',').each do |owner_key|
|
91
|
-
owners_map[owner_key].each do |owner|
|
92
|
-
records_by_owner[owner] << record
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
records_by_owner
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def preloader_for_with_spatial(reflection)
|
102
|
-
if reflection.options[:relationship]
|
103
|
-
SpatialAssociation
|
104
|
-
else
|
105
|
-
preloader_for_without_spatial(reflection)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
alias_method_chain :preloader_for, :spatial
|
109
|
-
end
|
110
|
-
|
111
|
-
class AssociationScope #:nodoc:
|
112
|
-
def add_constraints_with_spatial(scope)
|
113
|
-
return add_constraints_without_spatial(scope) if !self.association.is_a?(SpatialAssociation)
|
114
|
-
|
115
|
-
tables = construct_tables
|
116
|
-
|
117
|
-
chain.each_with_index do |reflection, i|
|
118
|
-
table, foreign_table = tables.shift, tables.first
|
2
|
+
require 'activerecord-spatial/associations/base'
|
119
3
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
if self.association.geom.is_a?(Hash)
|
126
|
-
geom_options.merge!(
|
127
|
-
:value => owner[self.association.geom[:name]]
|
128
|
-
)
|
129
|
-
geom_options.merge!(self.association.geom)
|
130
|
-
else
|
131
|
-
geom_options.merge!(
|
132
|
-
:value => owner[self.association.geom],
|
133
|
-
:name => self.association.geom
|
134
|
-
)
|
135
|
-
end
|
136
|
-
|
137
|
-
if reflection == chain.last
|
138
|
-
scope = scope.send("st_#{self.association.relationship}", geom_options, self.association.scope_options)
|
139
|
-
|
140
|
-
if reflection.type
|
141
|
-
scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
|
142
|
-
end
|
143
|
-
|
144
|
-
conditions.each do |condition|
|
145
|
-
scope = scope.where(interpolate(condition))
|
146
|
-
end
|
147
|
-
else
|
148
|
-
constraint = scope.where(
|
149
|
-
scope.send(
|
150
|
-
"st_#{self.association.relationship}",
|
151
|
-
owner[self.association.foreign_geom],
|
152
|
-
self.association.scope_options
|
153
|
-
).where_values
|
154
|
-
).join(' AND ')
|
155
|
-
|
156
|
-
if reflection.type
|
157
|
-
type = chain[i + 1].klass.base_class.name
|
158
|
-
constraint = table[reflection.type].eq(type).and(constraint)
|
159
|
-
end
|
160
|
-
|
161
|
-
scope = scope.joins(join(foreign_table, constraint))
|
162
|
-
|
163
|
-
unless conditions.empty?
|
164
|
-
scope = scope.where(sanitize(conditions, table))
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
scope
|
170
|
-
end
|
171
|
-
alias_method_chain :add_constraints, :spatial
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
module Reflection #:nodoc:
|
176
|
-
class AssociationReflection < MacroReflection #:nodoc:
|
177
|
-
def association_class_with_spatial
|
178
|
-
if self.options[:relationship]
|
179
|
-
Associations::SpatialAssociation
|
180
|
-
else
|
181
|
-
association_class_without_spatial
|
182
|
-
end
|
183
|
-
end
|
184
|
-
alias_method_chain :association_class, :spatial
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
# ActiveRecord Spatial associations allow for +has_many+-style associations
|
190
|
-
# using spatial relationships.
|
191
|
-
#
|
192
|
-
# == Example
|
193
|
-
#
|
194
|
-
# class Neighbourhood < ActiveRecord::Base
|
195
|
-
# has_many_spatially :cities,
|
196
|
-
# :relationship => :contains
|
197
|
-
# end
|
198
|
-
#
|
199
|
-
# class City < ActiveRecord::Base
|
200
|
-
# has_many_spatially :neighbourhoods,
|
201
|
-
# :relationship => :within
|
202
|
-
# end
|
203
|
-
#
|
204
|
-
# Neighbourhood.first.cities
|
205
|
-
# #=> All cities that the neighbourhood is within
|
206
|
-
#
|
207
|
-
# City.first.neighbourhoods
|
208
|
-
# #=> All neighbourhoods contained by the city
|
209
|
-
#
|
210
|
-
# City.includes(:neighbourhoods).first.neighbourhoods
|
211
|
-
# #=> Eager loading works too
|
212
|
-
#
|
213
|
-
# Spatial associations can be set up using any of the relationships found in
|
214
|
-
# ActiveRecordSpatial::SpatialScopes::RELATIONSHIPS.
|
215
|
-
#
|
216
|
-
# == Options
|
217
|
-
#
|
218
|
-
# Many of the options available with standard +has_many+ associations will work
|
219
|
-
# with the exceptions of +:through+, +:source+, +:source_type+, +:dependent+,
|
220
|
-
# +:finder_sql+, +:counter_sql+, and +:inverse_of+.
|
221
|
-
#
|
222
|
-
# Polymorphic relationships can be used via the +:as+ option as in standard
|
223
|
-
# +:has_many+ relationships. Note that the default field for the geometry
|
224
|
-
# in these cases is "#{association_name}_geom" and can be overridden using
|
225
|
-
# the +:foreign_geom+ option.
|
226
|
-
#
|
227
|
-
# * +:relationship+ - sets the spatial relationship for the association.
|
228
|
-
# Valid options can be found in ActiveRecordSpatial::SpatialScopes::RELATIONSHIPS.
|
229
|
-
# The default value is +:intersects+.
|
230
|
-
# * +:geom+ - sets the geometry field for the association in the calling model.
|
231
|
-
# The default value is +:the_geom+ as is often seen in PostGIS documentation.
|
232
|
-
# * +:foreign_geom+ - sets the geometry field for the association's foreign
|
233
|
-
# table. The default here is again +:the_geom+.
|
234
|
-
# * +:scope_options+ - these are options passed directly to the SpatialScopes
|
235
|
-
# module and as such the options are the same as are available there. The
|
236
|
-
# default value here is <tt>{ :invert => true }</tt>, as we want our
|
237
|
-
# spatial relationships to say "Foo spatially contains many Bars" and
|
238
|
-
# therefore the relationship in SQL becomes
|
239
|
-
# <tt>ST_contains("foos"."the_geom", "bars"."the_geom")</tt>.
|
240
|
-
#
|
241
|
-
# Note that you can modify the default geometry column name for all of
|
242
|
-
# ActiveRecordSpatial by setting it via ActiveRecordSpatia.default_column_name.
|
243
|
-
#
|
244
|
-
# == Caveats
|
245
|
-
#
|
246
|
-
# * You should consider spatial associations to be essentially readonly. Since
|
247
|
-
# we're not dealing with unique IDs here but rather 2D and 3D geometries,
|
248
|
-
# the relationships between rows don't really map well to the traditional
|
249
|
-
# foreign key-style ActiveRecord associations.
|
250
|
-
module ActiveRecordSpatial::Associations
|
251
|
-
extend ActiveSupport::Concern
|
252
|
-
|
253
|
-
DEFAULT_OPTIONS = {
|
254
|
-
:relationship => :intersects,
|
255
|
-
:geom => ActiveRecordSpatial.default_column_name,
|
256
|
-
:foreign_geom => ActiveRecordSpatial.default_column_name,
|
257
|
-
:scope_options => {
|
258
|
-
:invert => true
|
259
|
-
}
|
260
|
-
}.freeze
|
261
|
-
|
262
|
-
module ClassMethods #:nodoc:
|
263
|
-
def has_many_spatially(name, options = {}, &extension)
|
264
|
-
options = build_options(options)
|
265
|
-
|
266
|
-
if !ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.include?(options[:relationship].to_s)
|
267
|
-
raise ArgumentError.new(%{Invalid spatial relationship "#{options[:relationship]}", expected one of #{ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.inspect}})
|
268
|
-
end
|
269
|
-
|
270
|
-
ActiveRecord::Associations::Builder::Spatial.build(self, name, options, &extension)
|
271
|
-
end
|
272
|
-
|
273
|
-
def build_options(options)
|
274
|
-
if !options[:foreign_geom] && options[:as]
|
275
|
-
options[:foreign_geom] = "#{options[:as]}_geom"
|
276
|
-
end
|
277
|
-
|
278
|
-
if options[:geom].is_a?(Hash)
|
279
|
-
options[:geom][:name] ||= ActiveRecordSpatial.default_column_name
|
280
|
-
end
|
281
|
-
|
282
|
-
DEFAULT_OPTIONS.deep_merge(options)
|
283
|
-
end
|
284
|
-
private :build_options
|
285
|
-
end
|
4
|
+
if ActiveRecord::VERSION::MAJOR <= 3
|
5
|
+
require 'activerecord-spatial/associations/active_record_3'
|
6
|
+
else
|
7
|
+
require 'activerecord-spatial/associations/active_record'
|
286
8
|
end
|
287
9
|
|
288
10
|
module ActiveRecord
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Associations
|
5
|
+
class Builder::Spatial < Builder::HasMany #:nodoc:
|
6
|
+
def macro
|
7
|
+
SPATIAL_MACRO
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid_options
|
11
|
+
super + VALID_SPATIAL_OPTIONS - INVALID_SPATIAL_OPTIONS
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Preloader #:nodoc:
|
16
|
+
class SpatialAssociation < HasMany #:nodoc:
|
17
|
+
def records_for(ids)
|
18
|
+
table_name = reflection.quoted_table_name
|
19
|
+
join_name = model.quoted_table_name
|
20
|
+
column = %{#{SPATIAL_JOIN_QUOTED_NAME}.#{model.quoted_primary_key}}
|
21
|
+
geom = {
|
22
|
+
:class => model,
|
23
|
+
:table_alias => SPATIAL_JOIN_NAME
|
24
|
+
}
|
25
|
+
|
26
|
+
if reflection.options[:geom].is_a?(Hash)
|
27
|
+
geom.merge!(reflection.options[:geom])
|
28
|
+
else
|
29
|
+
geom[:column] = reflection.options[:geom]
|
30
|
+
end
|
31
|
+
|
32
|
+
scoped = scope.
|
33
|
+
select(%{array_to_string(array_agg(#{column}), ',') AS "#{SPATIAL_FIELD_ALIAS}"}).
|
34
|
+
joins(
|
35
|
+
"INNER JOIN #{join_name} AS #{SPATIAL_JOIN_QUOTED_NAME} ON (" <<
|
36
|
+
klass.send("st_#{reflection.options[:relationship]}",
|
37
|
+
geom,
|
38
|
+
(reflection.options[:scope_options] || {}).merge(
|
39
|
+
:column => reflection.options[:foreign_geom]
|
40
|
+
)
|
41
|
+
).where_values.join(' AND ') <<
|
42
|
+
")"
|
43
|
+
).
|
44
|
+
where(model.arel_table.alias(SPATIAL_JOIN_NAME)[model.primary_key].in(ids)).
|
45
|
+
group(table[klass.primary_key])
|
46
|
+
|
47
|
+
if reflection.options[:conditions]
|
48
|
+
scoped = scoped.where(reflection.options[:conditions])
|
49
|
+
end
|
50
|
+
|
51
|
+
scoped
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class AssociationScope #:nodoc:
|
57
|
+
def add_constraints_with_spatial(scope)
|
58
|
+
return add_constraints_without_spatial(scope) if !self.association.is_a?(SpatialAssociation)
|
59
|
+
|
60
|
+
tables = construct_tables
|
61
|
+
|
62
|
+
chain.each_with_index do |reflection, i|
|
63
|
+
table, foreign_table = tables.shift, tables.first
|
64
|
+
|
65
|
+
geom_options = {
|
66
|
+
:class => self.association.klass
|
67
|
+
}
|
68
|
+
|
69
|
+
if self.association.geom.is_a?(Hash)
|
70
|
+
geom_options.merge!(
|
71
|
+
:value => owner[self.association.geom[:name]]
|
72
|
+
)
|
73
|
+
geom_options.merge!(self.association.geom)
|
74
|
+
else
|
75
|
+
geom_options.merge!(
|
76
|
+
:value => owner[self.association.geom],
|
77
|
+
:name => self.association.geom
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
if reflection == chain.last
|
82
|
+
scope = scope.send("st_#{self.association.relationship}", geom_options, self.association.scope_options)
|
83
|
+
|
84
|
+
if reflection.type
|
85
|
+
scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
|
86
|
+
end
|
87
|
+
else
|
88
|
+
constraint = scope.where(
|
89
|
+
scope.send(
|
90
|
+
"st_#{self.association.relationship}",
|
91
|
+
owner[self.association.foreign_geom],
|
92
|
+
self.association.scope_options
|
93
|
+
).where_values
|
94
|
+
).join(' AND ')
|
95
|
+
|
96
|
+
if reflection.type
|
97
|
+
type = chain[i + 1].klass.base_class.name
|
98
|
+
constraint = table[reflection.type].eq(type).and(constraint)
|
99
|
+
end
|
100
|
+
|
101
|
+
scope = scope.joins(join(foreign_table, constraint))
|
102
|
+
end
|
103
|
+
|
104
|
+
if reflection.options[:conditions].present?
|
105
|
+
scope = scope.where(reflection.options[:conditions])
|
106
|
+
end
|
107
|
+
|
108
|
+
# Exclude the scope of the association itself, because that
|
109
|
+
# was already merged in the #scope method.
|
110
|
+
scope_chain[i].each do |scope_chain_item|
|
111
|
+
klass = i == 0 ? self.klass : reflection.klass
|
112
|
+
item = eval_scope(klass, scope_chain_item)
|
113
|
+
|
114
|
+
if scope_chain_item == self.reflection.scope
|
115
|
+
scope.merge! item.except(:where, :includes)
|
116
|
+
end
|
117
|
+
|
118
|
+
scope.includes! item.includes_values
|
119
|
+
scope.where_values += item.where_values
|
120
|
+
scope.order_values |= item.order_values
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
scope
|
125
|
+
end
|
126
|
+
alias_method_chain :add_constraints, :spatial
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
module ActiveRecordSpatial::Associations
|
133
|
+
module ClassMethods #:nodoc:
|
134
|
+
def has_many_spatially(name, *args, &extension)
|
135
|
+
options = build_options(args.extract_options!)
|
136
|
+
scope = args.first
|
137
|
+
|
138
|
+
if !ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.include?(options[:relationship].to_s)
|
139
|
+
raise ArgumentError.new(%{Invalid spatial relationship "#{options[:relationship]}", expected one of #{ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.inspect}})
|
140
|
+
end
|
141
|
+
|
142
|
+
ActiveRecord::Associations::Builder::Spatial.build(self, name, scope, options, &extension)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|