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.
- checksums.yaml +15 -0
- data/.gitignore +16 -0
- data/Gemfile +21 -0
- data/Guardfile +17 -0
- data/MIT-LICENSE +23 -0
- data/README.rdoc +169 -0
- data/Rakefile +28 -0
- data/activerecord-spatial.gemspec +26 -0
- data/lib/activerecord-spatial.rb +32 -0
- data/lib/activerecord-spatial/active_record.rb +14 -0
- data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb +36 -0
- data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/postgis.rb +24 -0
- data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/unknown_srid.rb +21 -0
- data/lib/activerecord-spatial/active_record/models/geography_column.rb +17 -0
- data/lib/activerecord-spatial/active_record/models/geometry_column.rb +17 -0
- data/lib/activerecord-spatial/active_record/models/spatial_column.rb +22 -0
- data/lib/activerecord-spatial/active_record/models/spatial_ref_sys.rb +20 -0
- data/lib/activerecord-spatial/associations.rb +292 -0
- data/lib/activerecord-spatial/spatial_columns.rb +345 -0
- data/lib/activerecord-spatial/spatial_function.rb +201 -0
- data/lib/activerecord-spatial/spatial_scope_constants.rb +114 -0
- data/lib/activerecord-spatial/spatial_scopes.rb +297 -0
- data/lib/activerecord-spatial/version.rb +5 -0
- data/lib/tasks/test.rake +45 -0
- data/test/accessors_geographies_tests.rb +149 -0
- data/test/accessors_geometries_tests.rb +151 -0
- data/test/adapter_tests.rb +44 -0
- data/test/associations_tests.rb +656 -0
- data/test/database.yml +17 -0
- data/test/fixtures/bars.yml +16 -0
- data/test/fixtures/blorts.yml +37 -0
- data/test/fixtures/foo3ds.yml +17 -0
- data/test/fixtures/foo_geographies.yml +16 -0
- data/test/fixtures/foos.yml +16 -0
- data/test/fixtures/zortables.yml +36 -0
- data/test/geography_column_tests.rb +40 -0
- data/test/geometry_column_tests.rb +40 -0
- data/test/models/bar.rb +17 -0
- data/test/models/blort.rb +12 -0
- data/test/models/foo.rb +17 -0
- data/test/models/foo3d.rb +17 -0
- data/test/models/foo_geography.rb +16 -0
- data/test/models/zortable.rb +17 -0
- data/test/spatial_scopes_geographies_tests.rb +106 -0
- data/test/spatial_scopes_tests.rb +444 -0
- data/test/test_helper.rb +272 -0
- 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
|