activerecord-spatial 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+