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.
@@ -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
- if field_type =~ /^geometry(\(|$)/
9
- :geometry
10
- elsif field_type =~ /^geography(\(|$)/
11
- :geography
12
- else
13
- simplified_type_without_spatial_type(field_type)
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
- module ActiveRecord
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
- conditions = self.conditions[i]
121
- geom_options = {
122
- :class => self.association.klass
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
+