ppe-postgis-adapter 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,423 @@
1
+ #
2
+ # PostGIS Adapter
3
+ #
4
+ #
5
+ # Code from
6
+ # http://georuby.rubyforge.org Spatial Adapter
7
+ #
8
+ require 'activerecord'
9
+ require 'active_record/connection_adapters/postgresql_adapter'
10
+ require 'geo_ruby'
11
+ require 'postgis_adapter/common_spatial_adapter'
12
+ require 'postgis_functions'
13
+ require 'postgis_functions/common'
14
+ require 'postgis_functions/class'
15
+ require 'postgis_functions/bbox'
16
+ require 'postgis_adapter/acts_as_geom'
17
+
18
+ include GeoRuby::SimpleFeatures
19
+ include SpatialAdapter
20
+
21
+ #tables to ignore in migration : relative to PostGIS management of geometric columns
22
+ ActiveRecord::SchemaDumper.ignore_tables << "spatial_ref_sys" << "geometry_columns"
23
+
24
+ #add a method to_yaml to the Geometry class which will transform a geometry in a form suitable to be used in a YAML file (such as in a fixture)
25
+ GeoRuby::SimpleFeatures::Geometry.class_eval do
26
+ def to_fixture_format
27
+ as_hex_ewkb
28
+ end
29
+ end
30
+
31
+ ActiveRecord::Base.class_eval do
32
+
33
+ #Vit Ondruch & Tilmann Singer 's patch
34
+ def self.get_conditions(attrs)
35
+ attrs.map do |attr, value|
36
+ attr = attr.to_s
37
+ column_name = connection.quote_column_name(attr)
38
+ if columns_hash[attr].is_a?(SpatialColumn)
39
+ if value.is_a?(Array)
40
+ attrs[attr.to_sym]= "BOX3D(" + value[0].join(" ") + "," + value[1].join(" ") + ")"
41
+ "#{table_name}.#{column_name} && SetSRID(?::box3d, #{value[2] || @@default_srid || DEFAULT_SRID} ) "
42
+ elsif value.is_a?(Envelope)
43
+ attrs[attr.to_sym]= "BOX3D(" + value.lower_corner.text_representation + "," + value.upper_corner.text_representation + ")"
44
+ "#{table_name}.#{column_name} && SetSRID(?::box3d, #{value.srid} ) "
45
+ else
46
+ "#{table_name}.#{column_name} && ? "
47
+ end
48
+ else
49
+ attribute_condition("#{table_name}.#{column_name}", value)
50
+ end
51
+ end.join(' AND ')
52
+ end
53
+
54
+ #For Rails >= 2
55
+ if method(:sanitize_sql_hash_for_conditions).arity == 1
56
+ # Before Rails 2.3.3, the method had only one argument
57
+ def self.sanitize_sql_hash_for_conditions(attrs)
58
+ conditions = get_conditions(attrs)
59
+ replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
60
+ end
61
+ elsif method(:sanitize_sql_hash_for_conditions).arity == -2
62
+ # After Rails 2.3.3, the method had only two args, the last one optional
63
+ def self.sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name)
64
+ attrs = expand_hash_conditions_for_aggregates(attrs)
65
+
66
+ conditions = attrs.map do |attr, value|
67
+ unless value.is_a?(Hash)
68
+ attr = attr.to_s
69
+
70
+ # Extract table name from qualified attribute names.
71
+ if attr.include?('.')
72
+ table_name, attr = attr.split('.', 2)
73
+ table_name = connection.quote_table_name(table_name)
74
+ end
75
+
76
+ if columns_hash[attr].is_a?(SpatialColumn)
77
+ if value.is_a?(Array)
78
+ attrs[attr.to_sym]= "BOX3D(" + value[0].join(" ") + "," + value[1].join(" ") + ")"
79
+ "#{table_name}.#{connection.quote_column_name(attr)} && SetSRID(?::box3d, #{value[2] || DEFAULT_SRID} ) "
80
+ elsif value.is_a?(Envelope)
81
+ attrs[attr.to_sym]= "BOX3D(" + value.lower_corner.text_representation + "," + value.upper_corner.text_representation + ")"
82
+ "#{table_name}.#{connection.quote_column_name(attr)} && SetSRID(?::box3d, #{value.srid} ) "
83
+ else
84
+ "#{table_name}.#{connection.quote_column_name(attr)} && ? "
85
+ end
86
+ else
87
+ attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value)
88
+ end
89
+ else
90
+ sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
91
+ end
92
+ end.join(' AND ')
93
+
94
+ replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
95
+ end
96
+ else
97
+ raise "Spatial Adapter will not work with this version of Rails"
98
+ end
99
+ end
100
+
101
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
102
+
103
+ include SpatialAdapter
104
+
105
+ alias :original_native_database_types :native_database_types
106
+ def native_database_types
107
+ original_native_database_types.update(geometry_data_types)
108
+ end
109
+
110
+ alias :original_quote :quote
111
+ #Redefines the quote method to add behaviour for when a Geometry is encountered
112
+ def quote(value, column = nil)
113
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
114
+ "'#{value.as_hex_ewkb}'"
115
+ else
116
+ original_quote(value,column)
117
+ end
118
+ end
119
+
120
+ alias :original_tables :tables
121
+ def tables(name = nil) #:nodoc:
122
+ original_tables(name) + views(name)
123
+ end
124
+
125
+ def views(name = nil) #:nodoc:
126
+ schemas = schema_search_path.split(/,/).map { |p| quote(p.strip) }.join(',')
127
+ query(<<-SQL, name).map { |row| row[0] }
128
+ SELECT viewname
129
+ FROM pg_views
130
+ WHERE schemaname IN (#{schemas})
131
+ SQL
132
+ end
133
+
134
+ def create_table(name, options = {})
135
+ table_definition = ActiveRecord::ConnectionAdapters::PostgreSQLTableDefinition.new(self)
136
+ table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
137
+
138
+ yield table_definition
139
+
140
+ if options[:force]
141
+ drop_table(name) rescue nil
142
+ end
143
+
144
+ create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
145
+ create_sql << "#{name} ("
146
+ create_sql << table_definition.to_sql
147
+ create_sql << ") #{options[:options]}"
148
+ execute create_sql
149
+
150
+ #added to create the geometric columns identified during the table definition
151
+ unless table_definition.geom_columns.nil?
152
+ table_definition.geom_columns.each do |geom_column|
153
+ execute geom_column.to_sql(name)
154
+ end
155
+ end
156
+ end
157
+
158
+ alias :original_remove_column :remove_column
159
+ def remove_column(table_name,column_name, options = {})
160
+ columns(table_name).each do |col|
161
+ if col.name == column_name.to_s
162
+ #check if the column is geometric
163
+ unless geometry_data_types[col.type].nil? or
164
+ (options[:remove_using_dropgeometrycolumn] == false)
165
+ execute "SELECT DropGeometryColumn('#{table_name}','#{column_name}')"
166
+ else
167
+ original_remove_column(table_name,column_name)
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ alias :original_add_column :add_column
174
+ def add_column(table_name, column_name, type, options = {})
175
+ unless geometry_data_types[type].nil? or (options[:create_using_addgeometrycolumn] == false)
176
+ geom_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumnDefinition.
177
+ new(self,column_name, type, nil,nil,options[:null],options[:srid] || -1 ,
178
+ options[:with_z] || false , options[:with_m] || false)
179
+
180
+ execute geom_column.to_sql(table_name)
181
+ else
182
+ original_add_column(table_name,column_name,type,options)
183
+ end
184
+ end
185
+
186
+ # Adds a GIST spatial index to a column. Its name will be
187
+ # <table_name>_<column_name>_spatial_index unless
188
+ # the key :name is present in the options hash, in which case its
189
+ # value is taken as the name of the index.
190
+ def add_index(table_name, column_name, options = {})
191
+ index_name = options[:name] || index_name(table_name,:column => Array(column_name))
192
+ if options[:spatial]
193
+ execute "CREATE INDEX #{index_name} ON #{table_name} USING GIST (#{Array(column_name).join(", ")} GIST_GEOMETRY_OPS)"
194
+ else
195
+ super
196
+ end
197
+ end
198
+
199
+ def indexes(table_name, name = nil) #:nodoc:
200
+ result = query(<<-SQL, name)
201
+ SELECT i.relname, d.indisunique, a.attname , am.amname
202
+ FROM pg_class t, pg_class i, pg_index d, pg_attribute a, pg_am am
203
+ WHERE i.relkind = 'i'
204
+ AND d.indexrelid = i.oid
205
+ AND d.indisprimary = 'f'
206
+ AND t.oid = d.indrelid
207
+ AND i.relam = am.oid
208
+ AND t.relname = '#{table_name}'
209
+ AND a.attrelid = t.oid
210
+ AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
211
+ OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
212
+ OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
213
+ OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
214
+ OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
215
+ ORDER BY i.relname
216
+ SQL
217
+
218
+ current_index = nil
219
+ indexes = []
220
+
221
+ result.each do |row|
222
+ if current_index != row[0]
223
+ #index type gist indicates a spatial index (probably not totally true but let's simplify!)
224
+ indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.
225
+ new(table_name, row[0], row[1] == "t", row[3] == "gist" ,[])
226
+
227
+ current_index = row[0]
228
+ end
229
+ indexes.last.columns << row[2]
230
+ end
231
+ indexes
232
+ end
233
+
234
+ def columns(table_name, name = nil) #:nodoc:
235
+ raw_geom_infos = column_spatial_info(table_name)
236
+
237
+ column_definitions(table_name).collect do |name, type, default, notnull|
238
+ if type =~ /geometry/i
239
+ raw_geom_info = raw_geom_infos[name]
240
+ if raw_geom_info.nil?
241
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_simplified(name, default, notnull == "f")
242
+ else
243
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.new(name, default,raw_geom_info.type, notnull == "f", raw_geom_info.srid, raw_geom_info.with_z, raw_geom_info.with_m)
244
+ end
245
+ else
246
+ ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(name, default, type, notnull == "f")
247
+ end
248
+ end
249
+ end
250
+
251
+ # # For version of Rails where exists disable_referential_integrity
252
+ # if self.instance_methods.include? "disable_referential_integrity"
253
+ # #Pete Deffendol's patch
254
+ # alias :original_disable_referential_integrity :disable_referential_integrity
255
+ # def disable_referential_integrity(&block) #:nodoc:
256
+ # ignore_tables = %w{ geometry_columns spatial_ref_sys }
257
+ # execute(tables.select { |name| !ignore_tables.include?(name) }.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
258
+ # yield
259
+ # ensure
260
+ # execute(tables.select { |name| !ignore_tables.include?(name)}.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
261
+ # end
262
+ # end
263
+
264
+ private
265
+
266
+ def column_spatial_info(table_name)
267
+ constr = query <<-end_sql
268
+ SELECT * FROM geometry_columns WHERE f_table_name = '#{table_name}'
269
+ end_sql
270
+
271
+ raw_geom_infos = {}
272
+ constr.each do |constr_def_a|
273
+ raw_geom_infos[constr_def_a[3]] ||= ActiveRecord::ConnectionAdapters::RawGeomInfo.new
274
+ raw_geom_infos[constr_def_a[3]].type = constr_def_a[6]
275
+ raw_geom_infos[constr_def_a[3]].dimension = constr_def_a[4].to_i
276
+ raw_geom_infos[constr_def_a[3]].srid = constr_def_a[5].to_i
277
+
278
+ if raw_geom_infos[constr_def_a[3]].type[-1] == ?M
279
+ raw_geom_infos[constr_def_a[3]].with_m = true
280
+ raw_geom_infos[constr_def_a[3]].type.chop!
281
+ else
282
+ raw_geom_infos[constr_def_a[3]].with_m = false
283
+ end
284
+ end
285
+
286
+ raw_geom_infos.each_value do |raw_geom_info|
287
+ #check the presence of z and m
288
+ raw_geom_info.convert!
289
+ end
290
+
291
+ raw_geom_infos
292
+ rescue => e
293
+ nil
294
+ end
295
+
296
+ end
297
+
298
+ module ActiveRecord
299
+ module ConnectionAdapters
300
+ class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc:
301
+ def convert!
302
+ self.type = "geometry" if self.type.nil? #if geometry the geometrytype constraint is not present : need to set the type here then
303
+
304
+ if dimension == 4
305
+ self.with_m = true
306
+ self.with_z = true
307
+ elsif dimension == 3
308
+ if with_m
309
+ self.with_z = false
310
+ self.with_m = true
311
+ else
312
+ self.with_z = true
313
+ self.with_m = false
314
+ end
315
+ else
316
+ self.with_z = false
317
+ self.with_m = false
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+
324
+
325
+ module ActiveRecord
326
+ module ConnectionAdapters
327
+ class PostgreSQLTableDefinition < TableDefinition
328
+ attr_reader :geom_columns
329
+
330
+ def column(name, type, options = {})
331
+ unless (@base.geometry_data_types[type.to_sym].nil? or
332
+ (options[:create_using_addgeometrycolumn] == false))
333
+
334
+ geom_column = PostgreSQLColumnDefinition.new(@base,name, type)
335
+ geom_column.null = options[:null]
336
+ geom_column.srid = options[:srid] || -1
337
+ geom_column.with_z = options[:with_z] || false
338
+ geom_column.with_m = options[:with_m] || false
339
+
340
+ @geom_columns = [] if @geom_columns.nil?
341
+ @geom_columns << geom_column
342
+ else
343
+ super(name,type,options)
344
+ end
345
+ end
346
+
347
+ SpatialAdapter.geometry_data_types.keys.each do |column_type|
348
+ class_eval <<-EOV
349
+ def #{column_type}(*args)
350
+ options = args.extract_options!
351
+ column_names = args
352
+
353
+ column_names.each { |name| column(name, '#{column_type}', options) }
354
+ end
355
+ EOV
356
+ end
357
+ end
358
+
359
+ class PostgreSQLColumnDefinition < ColumnDefinition
360
+ attr_accessor :srid, :with_z,:with_m
361
+ attr_reader :spatial
362
+
363
+ def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil,null=nil,srid=-1,with_z=false,with_m=false)
364
+ super(base, name, type, limit, default,null)
365
+ @spatial=true
366
+ @srid=srid
367
+ @with_z=with_z
368
+ @with_m=with_m
369
+ end
370
+
371
+ def to_sql(table_name)
372
+ if @spatial
373
+ type_sql = type_to_sql(type.to_sym)
374
+ type_sql += "M" if with_m and !with_z
375
+ if with_m and with_z
376
+ dimension = 4
377
+ elsif with_m or with_z
378
+ dimension = 3
379
+ else
380
+ dimension = 2
381
+ end
382
+
383
+ column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})"
384
+ column_sql += ";ALTER TABLE #{table_name} ALTER #{name} SET NOT NULL" if null == false
385
+ column_sql
386
+ else
387
+ super
388
+ end
389
+ end
390
+
391
+
392
+ private
393
+ def type_to_sql(name, limit=nil)
394
+ base.type_to_sql(name, limit) rescue name
395
+ end
396
+
397
+ end
398
+
399
+ end
400
+ end
401
+
402
+ #Would prefer creation of a PostgreSQLColumn type instead but I would
403
+ # need to reimplement methods where Column objects are instantiated so
404
+ # I leave it like this
405
+ module ActiveRecord
406
+ module ConnectionAdapters
407
+ class SpatialPostgreSQLColumn < Column
408
+
409
+ include SpatialColumn
410
+
411
+ #Transforms a string to a geometry. PostGIS returns a HewEWKB string.
412
+ def self.string_to_geometry(string)
413
+ return string unless string.is_a?(String)
414
+ GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(string) rescue nil
415
+ end
416
+
417
+ def self.create_simplified(name,default,null = true)
418
+ new(name,default,"geometry",null,nil,nil,nil)
419
+ end
420
+
421
+ end
422
+ end
423
+ end
@@ -0,0 +1,128 @@
1
+ ###
2
+ ##
3
+ #
4
+ # BBox
5
+ #
6
+ #
7
+ module PostgisFunctions
8
+
9
+ #
10
+ # These operators utilize indexes. They compare geometries by bounding boxes.
11
+ #
12
+ # You can use the literal forms or call directly using the 'bbox' method. eg.:
13
+ #
14
+ # @point.bbox(">>", @area)
15
+ # @point.bbox("|&>", @area)
16
+ #
17
+ #
18
+ # Cheatsheet:
19
+ #
20
+ # A &< B => A overlaps or is to the left of B
21
+ # A &> B => A overlaps or is to the right of B
22
+ # A << B => A is strictly to the left of B
23
+ # A >> B => A is strictly to the right of B
24
+ # A &<| B => A overlaps B or is below B
25
+ # A |&> B => A overlaps or is above B
26
+ # A <<| B => A strictly below B
27
+ # A |>> B => A strictly above B
28
+ # A = B => A bbox same as B bbox
29
+ # A @ B => A completely contained by B
30
+ # A ~ B => A completely contains B
31
+ # A && B => A and B bboxes interact
32
+ # A ~= B => A and B geometries are binary equal?
33
+ #
34
+ def bbox(operator, other)
35
+ postgis_calculate(:bbox, [self, other], operator)
36
+ end
37
+
38
+ #
39
+ # bbox literal method.
40
+ #
41
+ def completely_contained_by? other
42
+ bbox("@", other)
43
+ end
44
+
45
+ #
46
+ # bbox literal method.
47
+ #
48
+ def completely_contains? other
49
+ bbox("~", other)
50
+ end
51
+
52
+ #
53
+ # bbox literal method.
54
+ #
55
+ def overlaps_or_above? other
56
+ bbox("|&>", other)
57
+ end
58
+
59
+ #
60
+ # bbox literal method.
61
+ #
62
+ def overlaps_or_below? other
63
+ bbox("&<|", other)
64
+ end
65
+
66
+ #
67
+ # bbox literal method.
68
+ #
69
+ def overlaps_or_left_of? other
70
+ bbox("&<", other)
71
+ end
72
+
73
+ #
74
+ # bbox literal method.
75
+ #
76
+ def overlaps_or_right_of? other
77
+ bbox("&>", other)
78
+ end
79
+
80
+ #
81
+ # bbox literal method.
82
+ #
83
+ def strictly_above? other
84
+ bbox("|>>", other)
85
+ end
86
+
87
+ #
88
+ # bbox literal method.
89
+ #
90
+ def strictly_below? other
91
+ bbox("<<|", other)
92
+ end
93
+
94
+ #
95
+ # bbox literal method.
96
+ #
97
+ def strictly_left_of? other
98
+ bbox("<<", other)
99
+ end
100
+
101
+ #
102
+ # bbox literal method.
103
+ #
104
+ def strictly_right_of? other
105
+ bbox(">>", other)
106
+ end
107
+
108
+ #
109
+ # bbox literal method.
110
+ #
111
+ def interacts_with? other
112
+ bbox("&&", other)
113
+ end
114
+
115
+ #
116
+ # bbox literal method.
117
+ #
118
+ def binary_equal? other
119
+ bbox("~=", other)
120
+ end
121
+
122
+ #
123
+ # bbox literal method.
124
+ #
125
+ def same_as? other
126
+ bbox("=", other)
127
+ end
128
+ end
@@ -0,0 +1,63 @@
1
+ module PostgisFunctions
2
+
3
+ #
4
+ # Class Methods
5
+ #
6
+ module ClassMethods
7
+
8
+ #
9
+ # Returns the closest record
10
+ def closest_to(p, opts = {})
11
+ srid = opts.delete(:srid) || 4326
12
+ opts.merge!(:order => "ST_Distance(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))")
13
+ find(:first, opts)
14
+ end
15
+
16
+ #
17
+ # Order by distance
18
+ def close_to(p, opts = {})
19
+ srid = opts.delete(:srid) || 4326
20
+ opts.merge!(:order => "ST_Distance(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))")
21
+ find(:all, opts)
22
+ end
23
+
24
+ def by_length opts = {}
25
+ sort = opts.delete(:sort) || 'asc'
26
+ opts.merge!(:order => "ST_length(geom) #{sort}")
27
+ find(:all, opts)
28
+ end
29
+
30
+ def longest
31
+ find(:first, :order => "ST_length(geom) DESC")
32
+ end
33
+
34
+ def contains(p, srid=4326)
35
+ find(:all, :conditions => ["ST_Contains(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))"])
36
+ end
37
+
38
+ def contain(p, srid=4326)
39
+ find(:first, :conditions => ["ST_Contains(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))"])
40
+ end
41
+
42
+ def by_area sort='asc'
43
+ find(:all, :order => "ST_Area(geom) #{sort}" )
44
+ end
45
+
46
+ def by_perimeter sort='asc'
47
+ find(:all, :order => "ST_Perimeter(geom) #{sort}" )
48
+ end
49
+
50
+ def all_within(other, margin=1)
51
+ # find(:all, :conditions => "ST_DWithin(geom, ST_GeomFromEWKB(E'#{other.as_ewkt}'), #{margin})")
52
+ find(:all, :conditions => "ST_DWithin(geom, ST_GeomFromEWKT(E'#{other.as_hex_ewkb}'), #{margin})")
53
+ end
54
+
55
+ def by_boundaries sort='asc'
56
+ find(:all, :order => "ST_Boundary(geom) #{sort}" )
57
+ end
58
+
59
+ end
60
+
61
+
62
+
63
+ end