activerecord-spatial 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.
Files changed (47) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +21 -0
  4. data/Guardfile +17 -0
  5. data/MIT-LICENSE +23 -0
  6. data/README.rdoc +169 -0
  7. data/Rakefile +28 -0
  8. data/activerecord-spatial.gemspec +26 -0
  9. data/lib/activerecord-spatial.rb +32 -0
  10. data/lib/activerecord-spatial/active_record.rb +14 -0
  11. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb +36 -0
  12. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/postgis.rb +24 -0
  13. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/unknown_srid.rb +21 -0
  14. data/lib/activerecord-spatial/active_record/models/geography_column.rb +17 -0
  15. data/lib/activerecord-spatial/active_record/models/geometry_column.rb +17 -0
  16. data/lib/activerecord-spatial/active_record/models/spatial_column.rb +22 -0
  17. data/lib/activerecord-spatial/active_record/models/spatial_ref_sys.rb +20 -0
  18. data/lib/activerecord-spatial/associations.rb +292 -0
  19. data/lib/activerecord-spatial/spatial_columns.rb +345 -0
  20. data/lib/activerecord-spatial/spatial_function.rb +201 -0
  21. data/lib/activerecord-spatial/spatial_scope_constants.rb +114 -0
  22. data/lib/activerecord-spatial/spatial_scopes.rb +297 -0
  23. data/lib/activerecord-spatial/version.rb +5 -0
  24. data/lib/tasks/test.rake +45 -0
  25. data/test/accessors_geographies_tests.rb +149 -0
  26. data/test/accessors_geometries_tests.rb +151 -0
  27. data/test/adapter_tests.rb +44 -0
  28. data/test/associations_tests.rb +656 -0
  29. data/test/database.yml +17 -0
  30. data/test/fixtures/bars.yml +16 -0
  31. data/test/fixtures/blorts.yml +37 -0
  32. data/test/fixtures/foo3ds.yml +17 -0
  33. data/test/fixtures/foo_geographies.yml +16 -0
  34. data/test/fixtures/foos.yml +16 -0
  35. data/test/fixtures/zortables.yml +36 -0
  36. data/test/geography_column_tests.rb +40 -0
  37. data/test/geometry_column_tests.rb +40 -0
  38. data/test/models/bar.rb +17 -0
  39. data/test/models/blort.rb +12 -0
  40. data/test/models/foo.rb +17 -0
  41. data/test/models/foo3d.rb +17 -0
  42. data/test/models/foo_geography.rb +16 -0
  43. data/test/models/zortable.rb +17 -0
  44. data/test/spatial_scopes_geographies_tests.rb +106 -0
  45. data/test/spatial_scopes_tests.rb +444 -0
  46. data/test/test_helper.rb +272 -0
  47. metadata +138 -0
@@ -0,0 +1,17 @@
1
+
2
+ module ActiveRecordSpatial
3
+ class GeographyColumn < ActiveRecord::Base
4
+ include SpatialColumn
5
+
6
+ self.table_name = 'geography_columns'
7
+
8
+ def spatial_type
9
+ :geography
10
+ end
11
+
12
+ def spatial_column
13
+ self.f_geography_column
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,17 @@
1
+
2
+ module ActiveRecordSpatial
3
+ class GeometryColumn < ActiveRecord::Base
4
+ include SpatialColumn
5
+
6
+ self.table_name = 'geometry_columns'
7
+
8
+ def spatial_type
9
+ :geometry
10
+ end
11
+
12
+ def spatial_column
13
+ self.f_geometry_column
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,22 @@
1
+
2
+ module ActiveRecordSpatial
3
+ module SpatialColumn #:nodoc:
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.primary_key = nil
8
+
9
+ # PostGIS inserts a "type" column into these tables/views that can
10
+ # really mess things up good.
11
+ self.inheritance_column = 'nonexistent_column_name_type'
12
+
13
+ belongs_to :spatial_ref_sys,
14
+ :foreign_key => :srid
15
+ end
16
+
17
+ def readonly?
18
+ true
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,20 @@
1
+
2
+ module ActiveRecordSpatial
3
+ class SpatialRefSys < ::ActiveRecord::Base
4
+ self.table_name = 'spatial_ref_sys'
5
+ self.primary_key = 'srid'
6
+
7
+ has_many :geometry_columns,
8
+ :foreign_key => :srid,
9
+ :inverse_of => :spatial_ref_sys
10
+
11
+ has_many :geography_columns,
12
+ :foreign_key => :srid,
13
+ :inverse_of => :spatial_ref_sys
14
+
15
+ def spatial_columns
16
+ self.geometry_columns + self.geography_columns
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,292 @@
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
119
+
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
286
+ end
287
+
288
+ module ActiveRecord
289
+ class Base #:nodoc:
290
+ include ActiveRecordSpatial::Associations
291
+ end
292
+ end
@@ -0,0 +1,345 @@
1
+
2
+ module ActiveRecordSpatial
3
+ # This little module helps us out with geometry columns. At least, in
4
+ # PostgreSQL it does.
5
+ #
6
+ # This module will add a method called spatial_columns to your model
7
+ # which will contain information that can be gleaned from the
8
+ # geometry_columns and geography_columns tables/views that PostGIS creates.
9
+ #
10
+ # You can also have the module automagically create some accessor
11
+ # methods for you to make your life easier. These accessor methods will
12
+ # override the ActiveRecord defaults and allow you to set geometry
13
+ # column values using Geos geometry objects directly or with
14
+ # PostGIS-style extended WKT and such. See
15
+ # create_spatial_column_accessors! for details.
16
+ module SpatialColumns
17
+ extend ActiveSupport::Concern
18
+
19
+ SPATIAL_COLUMN_OUTPUT_FORMATS = %w{ geos wkt wkb ewkt ewkb wkb_bin ewkb_bin }.freeze
20
+
21
+ class InvalidGeometry < ::ActiveRecord::ActiveRecordError
22
+ def initialize(geom)
23
+ super("Invalid geometry: #{geom}")
24
+ end
25
+ end
26
+
27
+ class SRIDNotFound < ::ActiveRecord::ActiveRecordError
28
+ def initialize(table_name, column)
29
+ super("Couldn't find SRID for #{table_name}.#{column}")
30
+ end
31
+ end
32
+
33
+ class CantConvertSRID < ::ActiveRecord::ActiveRecordError
34
+ def initialize(table_name, column, from_srid, to_srid)
35
+ super("Couldn't convert SRID for #{table_name}.#{column} from #{from_srid} to #{to_srid}")
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ protected
41
+ @geometry_columns = nil
42
+ @geography_columns = nil
43
+
44
+ public
45
+ # Build call to ActiveRecordSpatial::SpatialFunction.build! that helps
46
+ # you create spatial function calls.
47
+ def spatial_function(*args)
48
+ SpatialFunction.build!(self, *args)
49
+ end
50
+
51
+ # Stubs for documentation purposes:
52
+
53
+ # Returns an Array of available geometry columns in the
54
+ # table. These are PostgreSQLColumns with values set for
55
+ # the srid and coord_dimensions properties.
56
+ def geometry_columns; end
57
+
58
+ # Returns an Array of available geography columns in the
59
+ # table. These are PostgreSQLColumns with values set for
60
+ # the srid and coord_dimensions properties.
61
+ def geography_columns; end
62
+
63
+ # Force a reload of available geometry columns.
64
+ def geometry_columns!; end
65
+
66
+ # Force a reload of available geography columns.
67
+ def geography_columns!; end
68
+
69
+ # Grabs a geometry column based on name.
70
+ def geometry_column_by_name(name); end
71
+
72
+ # Grabs a geography column based on name.
73
+ def geography_column_by_name(name); end
74
+
75
+ # Returns both the geometry and geography columns for a table.
76
+ def spatial_columns
77
+ self.geometry_columns + self.geography_columns
78
+ end
79
+
80
+ # Reloads both the geometry and geography columns for a table.
81
+ def spatial_columns!
82
+ self.geometry_columns! + self.geography_columns!
83
+ end
84
+
85
+ # Grabs a spatial column based on name.
86
+ def spatial_column_by_name(name)
87
+ self.geometry_column_by_name(name) || self.geography_column_by_name(name)
88
+ end
89
+
90
+ %w{ geometry geography }.each do |m|
91
+ src, line = <<-EOF, __LINE__ + 1
92
+ undef :#{m}_columns
93
+ def #{m}_columns
94
+ if !defined?(@#{m}_columns) || @#{m}_columns.nil?
95
+ @#{m}_columns = ActiveRecordSpatial::#{m.capitalize}Column.where(
96
+ :f_table_name => self.table_name
97
+ ).all
98
+ @#{m}_columns.freeze
99
+ end
100
+ @#{m}_columns
101
+ end
102
+
103
+ undef :#{m}_columns!
104
+ def #{m}_columns!
105
+ @#{m}_columns = nil
106
+ #{m}_columns
107
+ end
108
+
109
+ undef :#{m}_column_by_name
110
+ def #{m}_column_by_name(name)
111
+ @#{m}_column_by_name ||= self.#{m}_columns.inject(HashWithIndifferentAccess.new) do |memo, obj|
112
+ memo[obj.spatial_column] = obj
113
+ memo
114
+ end
115
+ @#{m}_column_by_name[name]
116
+ end
117
+ EOF
118
+ self.class_eval(src, __FILE__, line)
119
+ end
120
+
121
+ # Quickly grab the SRID for a geometry column.
122
+ def srid_for(column_name)
123
+ column = self.spatial_column_by_name(column_name)
124
+ column.try(:srid) || ActiveRecordSpatial::UNKNOWN_SRID
125
+ end
126
+
127
+ # Quickly grab the number of dimensions for a geometry column.
128
+ def coord_dimension_for(column_name)
129
+ self.spatial_column_by_name(column_name).coord_dimension
130
+ end
131
+
132
+ protected
133
+ # Sets up nifty setters and getters for spatial columns.
134
+ # The methods created look like this:
135
+ #
136
+ # * spatial_column_name_geos
137
+ # * spatial_column_name_wkb
138
+ # * spatial_column_name_wkb_bin
139
+ # * spatial_column_name_wkt
140
+ # * spatial_column_name_ewkb
141
+ # * spatial_column_name_ewkb_bin
142
+ # * spatial_column_name_ewkt
143
+ # * spatial_column_name=(geom)
144
+ # * spatial_column_name(options = {})
145
+ #
146
+ # Where "spatial_column_name" is the name of the actual
147
+ # column.
148
+ #
149
+ # You can specify which spatial columns you want to apply
150
+ # these accessors using the :only and :except options.
151
+ def create_spatial_column_accessors!(options = nil)
152
+ create_these = []
153
+
154
+ if options.nil?
155
+ create_these.concat(self.spatial_columns)
156
+ else
157
+ if options[:geometry_columns]
158
+ create_these.concat(self.geometry_columns)
159
+ end
160
+
161
+ if options[:geography_columns]
162
+ create_these.concat(self.geography_columns)
163
+ end
164
+
165
+ if options[:except] && options[:only]
166
+ raise ArgumentError, "You can only specify either :except or :only (#{options.keys.inspect})"
167
+ elsif options[:except]
168
+ except = Array.wrap(options[:except]).collect(&:to_s)
169
+ create_these.reject! { |c| except.include?(c) }
170
+ elsif options[:only]
171
+ only = Array.wrap(options[:only]).collect(&:to_s)
172
+ create_these.select! { |c| only.include?(c) }
173
+ end
174
+ end
175
+
176
+ create_these.each do |k|
177
+ src, line = <<-EOF, __LINE__ + 1
178
+ def #{k.spatial_column}=(geom)
179
+ if !geom
180
+ self['#{k.spatial_column}'] = nil
181
+ else
182
+ column = self.class.spatial_column_by_name(#{k.spatial_column.inspect})
183
+
184
+ if geom =~ /^SRID=default;/i
185
+ geom = geom.sub(/default/i, column.srid.to_s)
186
+ end
187
+
188
+ geos = Geos.read(geom)
189
+
190
+ if column.spatial_type != :geography
191
+ geom_srid = if geos.srid == 0 || geos.srid == -1
192
+ ActiveRecordSpatial::UNKNOWN_SRIDS[column.spatial_type]
193
+ else
194
+ geos.srid
195
+ end
196
+
197
+ if column.srid != geom_srid
198
+ if column.srid == ActiveRecordSpatial::UNKNOWN_SRIDS[column.spatial_type] || geom_srid == ActiveRecordSpatial::UNKNOWN_SRIDS[column.spatial_type]
199
+ geos.srid = column.srid
200
+ else
201
+ raise CantConvertSRID.new(self.class.table_name, #{k.spatial_column.inspect}, geom_srid, column.srid)
202
+ end
203
+ end
204
+
205
+ self['#{k.spatial_column}'] = geos.to_ewkb
206
+ else
207
+ self['#{k.spatial_column}'] = geos.to_wkb
208
+ end
209
+ end
210
+
211
+ SPATIAL_COLUMN_OUTPUT_FORMATS.each do |f|
212
+ instance_variable_set("@#{k.spatial_column}_\#{f}", nil)
213
+ end
214
+ end
215
+
216
+ def #{k.spatial_column}_geos
217
+ @#{k.spatial_column}_geos ||= Geos.from_wkb(self['#{k.spatial_column}'])
218
+ end
219
+
220
+ def #{k.spatial_column}(options = {})
221
+ format = case options
222
+ when String, Symbol
223
+ options
224
+ when Hash
225
+ options = options.stringify_keys
226
+ options['format'] if options['format']
227
+ end
228
+
229
+ if format
230
+ if SPATIAL_COLUMN_OUTPUT_FORMATS.include?(format)
231
+ return self.send(:"#{k.spatial_column}_\#{format}")
232
+ else
233
+ raise ArgumentError, "Invalid option: \#{options[:format]}"
234
+ end
235
+ end
236
+
237
+ self['#{k.spatial_column}']
238
+ end
239
+ EOF
240
+ self.class_eval(src, __FILE__, line)
241
+
242
+ SPATIAL_COLUMN_OUTPUT_FORMATS.reject { |f| f == 'geos' }.each do |f|
243
+ src, line = <<-EOF, __LINE__ + 1
244
+ def #{k.spatial_column}_#{f}(*args)
245
+ @#{k.spatial_column}_#{f} ||= self.#{k.spatial_column}_geos.to_#{f}(*args) rescue nil
246
+ end
247
+ EOF
248
+ self.class_eval(src, __FILE__, line)
249
+ end
250
+ end
251
+ end
252
+
253
+ # Creates column accessors for geometry columns only.
254
+ def create_geometry_column_accessors!(options = {})
255
+ options = {
256
+ :geometry_columns => true
257
+ }.merge(options)
258
+
259
+ create_spatial_column_accessors!(options)
260
+ end
261
+
262
+ # Creates column accessors for geometry columns only.
263
+ def create_geography_column_accessors!(options = {})
264
+ options = {
265
+ :geography_columns => true
266
+ }.merge(options)
267
+
268
+ create_spatial_column_accessors!(options)
269
+ end
270
+
271
+ # Stubs for documentation purposes:
272
+
273
+ # Returns a Geos::Geometry object.
274
+ def __spatial_column_name_geos; end
275
+
276
+ # Returns a hex-encoded WKB String.
277
+ def __spatial_column_name_wkb; end
278
+
279
+ # Returns a WKB String in binary.
280
+ def __spatial_column_name_wkb_bin; end
281
+
282
+ # Returns a WKT String.
283
+ def __spatial_column_name_wkt; end
284
+
285
+ # Returns a hex-encoded EWKB String.
286
+ def __spatial_column_name_ewkb; end
287
+
288
+ # Returns an EWKB String in binary.
289
+ def __spatial_column_name_ewkb_bin; end
290
+
291
+ # Returns an EWKT String.
292
+ def __spatial_column_name_ewkt; end
293
+
294
+ # An enhanced setter that tries to deduce how you're
295
+ # setting the value. The setter can handle Geos::Geometry
296
+ # objects, WKT, EWKT and WKB and EWKB in both hex and
297
+ # binary.
298
+ #
299
+ # When dealing with SRIDs, you can have the SRID set
300
+ # automatically on WKT by setting the value as
301
+ # "SRID=default;GEOMETRY(...)", i.e.:
302
+ #
303
+ # spatial_column_name = "SRID=default;POINT(1.0 1.0)"
304
+ #
305
+ # The SRID will be filled in automatically if available.
306
+ # Note that we're only setting the SRID on the geometry,
307
+ # but we're not doing any sort of re-projection or anything
308
+ # of the sort. If you need to convert from one SRID to
309
+ # another, you're stuck for the moment, but we'll be adding
310
+ # support for reprojections/transoformations via proj4rb
311
+ # soon.
312
+ #
313
+ # For WKB, you're better off manipulating the WKB directly
314
+ # or using proper Geos geometry objects.
315
+ def __spatial_column_name=(geom); end
316
+
317
+ # An enhanced getter that accepts an options Hash or
318
+ # String/Symbol that can be used to determine the output
319
+ # format. In the options Hash, use :format, or set the
320
+ # format directly as a String or Symbol.
321
+ #
322
+ # This basically allows you to do the following, which
323
+ # are equivalent:
324
+ #
325
+ # spatial_column_name(:wkt)
326
+ # spatial_column_name(:format => :wkt)
327
+ # spatial_column_name_wkt
328
+ def __spatial_column_name(options = {}); end
329
+
330
+ undef __spatial_column_name_geos
331
+ undef __spatial_column_name_wkb
332
+ undef __spatial_column_name_wkb_bin
333
+ undef __spatial_column_name_wkt
334
+ undef __spatial_column_name_ewkb
335
+ undef __spatial_column_name_ewkb_bin
336
+ undef __spatial_column_name_ewkt
337
+
338
+ undef __spatial_column_name=
339
+ undef __spatial_column_name
340
+ end
341
+ end
342
+
343
+ # Alias for backwards compatibility.
344
+ GeometryColumns = SpatialColumns
345
+ end