activerecord-spatial 0.0.1

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