postgis_adapter 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
data/install.rb ADDED
File without changes
@@ -0,0 +1,388 @@
1
+ # #
2
+ # PostGIS Adapter
3
+ #
4
+ # Spatial Adapter PostGIS Adapter for ActiveRecord
5
+ #
6
+ #
7
+ #require 'active_record'
8
+ require 'geo_ruby'
9
+ require 'postgis_adapter/common_spatial_adapter'
10
+ require 'postgis_functions'
11
+ require 'postgis_functions/common'
12
+ require 'postgis_functions/class'
13
+ require 'postgis_functions/point'
14
+ require 'postgis_functions/linestring'
15
+ require 'postgis_functions/polygon'
16
+ require 'postgis_functions/bbox'
17
+ require 'postgis_adapter/acts_as_geom'
18
+
19
+ include GeoRuby::SimpleFeatures
20
+ include SpatialAdapter
21
+
22
+ module PostgisAdapter
23
+ VERSION = '0.1.8'
24
+ end
25
+
26
+ #tables to ignore in migration : relative to PostGIS management of geometric columns
27
+ ActiveRecord::SchemaDumper.ignore_tables << "spatial_ref_sys" << "geometry_columns"
28
+
29
+
30
+ #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)
31
+ GeoRuby::SimpleFeatures::Geometry.class_eval do
32
+ def to_fixture_format
33
+ as_hex_ewkb
34
+ end
35
+ end
36
+
37
+
38
+ ActiveRecord::Base.class_eval do
39
+ require 'active_record/version'
40
+
41
+ #Vit Ondruch & Tilmann Singer 's patch
42
+ def self.get_conditions(attrs)
43
+ attrs.map do |attr, value|
44
+ attr = attr.to_s
45
+ if columns_hash[attr].is_a?(SpatialColumn)
46
+ if value.is_a?(Array)
47
+ attrs[attr.to_sym]= "BOX3D(" + value[0].join(" ") + "," + value[1].join(" ") + ")"
48
+ "#{table_name}.#{connection.quote_column_name(attr)} && SetSRID(?::box3d, #{value[2] || DEFAULT_SRID} ) "
49
+ elsif value.is_a?(Envelope)
50
+ attrs[attr.to_sym]= "BOX3D(" + value.lower_corner.text_representation + "," + value.upper_corner.text_representation + ")"
51
+ "#{table_name}.#{connection.quote_column_name(attr)} && SetSRID(?::box3d, #{value.srid} ) "
52
+ else
53
+ "#{table_name}.#{connection.quote_column_name(attr)} && ? "
54
+ end
55
+ else
56
+ "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
57
+ end
58
+ end.join(' AND ')
59
+ end
60
+
61
+ #For Rails >= 2
62
+ def self.sanitize_sql_hash_for_conditions(attrs)
63
+ conditions = get_conditions(attrs)
64
+ replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
65
+ end
66
+
67
+ end
68
+
69
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
70
+
71
+ include SpatialAdapter
72
+
73
+ alias :original_native_database_types :native_database_types
74
+ def native_database_types
75
+ original_native_database_types.update(geometry_data_types)
76
+ end
77
+
78
+ alias :original_quote :quote
79
+ #Redefines the quote method to add behaviour for when a Geometry is encountered
80
+ def quote(value, column = nil)
81
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
82
+ "'#{value.as_hex_ewkb}'"
83
+ else
84
+ original_quote(value,column)
85
+ end
86
+ end
87
+
88
+ alias :original_tables :tables
89
+ def tables(name = nil) #:nodoc:
90
+ schemas = schema_search_path.split(/,/).map { |p| quote(p.strip) }.join(',')
91
+ original_tables(name) + query(<<-SQL, name).map { |row| row[0] }
92
+ SELECT viewname
93
+ FROM pg_views
94
+ WHERE schemaname IN (#{schemas})
95
+ SQL
96
+ end
97
+
98
+ def create_table(name, options = {})
99
+ table_definition = ActiveRecord::ConnectionAdapters::PostgreSQLTableDefinition.new(self)
100
+ table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
101
+
102
+ yield table_definition
103
+
104
+ if options[:force]
105
+ drop_table(name) rescue nil
106
+ end
107
+
108
+ create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
109
+ create_sql << "#{name} ("
110
+ create_sql << table_definition.to_sql
111
+ create_sql << ") #{options[:options]}"
112
+ execute create_sql
113
+
114
+ #added to create the geometric columns identified during the table definition
115
+ unless table_definition.geom_columns.nil?
116
+ table_definition.geom_columns.each do |geom_column|
117
+ execute geom_column.to_sql(name)
118
+ end
119
+ end
120
+ end
121
+
122
+ alias :original_remove_column :remove_column
123
+ def remove_column(table_name,column_name, options = {})
124
+ columns(table_name).each do |col|
125
+ if col.name == column_name.to_s
126
+ #check if the column is geometric
127
+ unless geometry_data_types[col.type].nil? or
128
+ (options[:remove_using_dropgeometrycolumn] == false)
129
+ execute "SELECT DropGeometryColumn('#{table_name}','#{column_name}')"
130
+ else
131
+ original_remove_column(table_name,column_name)
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ alias :original_add_column :add_column
138
+ def add_column(table_name, column_name, type, options = {})
139
+ unless geometry_data_types[type].nil? or (options[:create_using_addgeometrycolumn] == false)
140
+ geom_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumnDefinition.
141
+ new(self,column_name, type, nil,nil,options[:null],options[:srid] || -1 ,
142
+ options[:with_z] || false , options[:with_m] || false)
143
+
144
+ execute geom_column.to_sql(table_name)
145
+ else
146
+ original_add_column(table_name,column_name,type,options)
147
+ end
148
+ end
149
+
150
+ # Adds a GIST spatial index to a column. Its name will be
151
+ # <table_name>_<column_name>_spatial_index unless
152
+ # the key :name is present in the options hash, in which case its
153
+ # value is taken as the name of the index.
154
+ def add_index(table_name,column_name,options = {})
155
+ index_name = options[:name] || index_name(table_name,:column => Array(column_name))
156
+ if options[:spatial]
157
+ execute "CREATE INDEX #{index_name} ON #{table_name} USING GIST (#{Array(column_name).join(", ")} GIST_GEOMETRY_OPS)"
158
+ else
159
+ index_type = options[:unique] ? "UNIQUE" : ""
160
+ #all together
161
+ execute "CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
162
+ end
163
+ end
164
+
165
+ def indexes(table_name, name = nil) #:nodoc:
166
+ result = query(<<-SQL, name)
167
+ SELECT i.relname, d.indisunique, a.attname , am.amname
168
+ FROM pg_class t, pg_class i, pg_index d, pg_attribute a, pg_am am
169
+ WHERE i.relkind = 'i'
170
+ AND d.indexrelid = i.oid
171
+ AND d.indisprimary = 'f'
172
+ AND t.oid = d.indrelid
173
+ AND i.relam = am.oid
174
+ AND t.relname = '#{table_name}'
175
+ AND a.attrelid = t.oid
176
+ AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
177
+ OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
178
+ OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
179
+ OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
180
+ OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
181
+ ORDER BY i.relname
182
+ SQL
183
+
184
+ current_index = nil
185
+ indexes = []
186
+
187
+ result.each do |row|
188
+ if current_index != row[0]
189
+ #index type gist indicates a spatial index (probably not totally true but let's simplify!)
190
+ indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.
191
+ new(table_name, row[0], row[1] == "t", row[3] == "gist" ,[])
192
+
193
+ current_index = row[0]
194
+ end
195
+ indexes.last.columns << row[2]
196
+ end
197
+ indexes
198
+ end
199
+
200
+ def columns(table_name, name = nil) #:nodoc:
201
+ raw_geom_infos = column_spatial_info(table_name)
202
+
203
+ column_definitions(table_name).collect do |name, type, default, notnull|
204
+ if type =~ /geometry/i
205
+ raw_geom_info = raw_geom_infos[name]
206
+ if raw_geom_info.nil?
207
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_simplified(name,default,notnull == "f")
208
+ else
209
+ 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)
210
+ end
211
+ else
212
+ ActiveRecord::ConnectionAdapters::Column.new(name, ActiveRecord::ConnectionAdapters::PostgreSQLColumn.extract_value_from_default( default), type,notnull == "f")
213
+ end
214
+ end
215
+ end
216
+
217
+ # # For version of Rails where exists disable_referential_integrity
218
+ # if self.instance_methods.include? "disable_referential_integrity"
219
+ # #Pete Deffendol's patch
220
+ # alias :original_disable_referential_integrity :disable_referential_integrity
221
+ # def disable_referential_integrity(&block) #:nodoc:
222
+ # ignore_tables = %w{ geometry_columns spatial_ref_sys }
223
+ # execute(tables.select { |name| !ignore_tables.include?(name) }.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
224
+ # yield
225
+ # ensure
226
+ # execute(tables.select { |name| !ignore_tables.include?(name)}.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
227
+ # end
228
+ # end
229
+
230
+ private
231
+
232
+ def column_spatial_info(table_name)
233
+ constr = query <<-end_sql
234
+ SELECT * FROM geometry_columns WHERE f_table_name = '#{table_name}'
235
+ end_sql
236
+
237
+ raw_geom_infos = {}
238
+ constr.each do |constr_def_a|
239
+ raw_geom_infos[constr_def_a[3]] ||= ActiveRecord::ConnectionAdapters::RawGeomInfo.new
240
+ raw_geom_infos[constr_def_a[3]].type = constr_def_a[6]
241
+ raw_geom_infos[constr_def_a[3]].dimension = constr_def_a[4].to_i
242
+ raw_geom_infos[constr_def_a[3]].srid = constr_def_a[5].to_i
243
+
244
+ if raw_geom_infos[constr_def_a[3]].type[-1] == ?M
245
+ raw_geom_infos[constr_def_a[3]].with_m = true
246
+ raw_geom_infos[constr_def_a[3]].type.chop!
247
+ else
248
+ raw_geom_infos[constr_def_a[3]].with_m = false
249
+ end
250
+ end
251
+
252
+ raw_geom_infos.each_value do |raw_geom_info|
253
+ #check the presence of z and m
254
+ raw_geom_info.convert!
255
+ end
256
+
257
+ raw_geom_infos
258
+
259
+ end
260
+
261
+ end
262
+
263
+ module ActiveRecord
264
+ module ConnectionAdapters
265
+ class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc:
266
+ def convert!
267
+ self.type = "geometry" if self.type.nil? #if geometry the geometrytype constraint is not present : need to set the type here then
268
+
269
+ if dimension == 4
270
+ self.with_m = true
271
+ self.with_z = true
272
+ elsif dimension == 3
273
+ if with_m
274
+ self.with_z = false
275
+ self.with_m = true
276
+ else
277
+ self.with_z = true
278
+ self.with_m = false
279
+ end
280
+ else
281
+ self.with_z = false
282
+ self.with_m = false
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+
290
+ module ActiveRecord
291
+ module ConnectionAdapters
292
+ class PostgreSQLTableDefinition < TableDefinition
293
+ attr_reader :geom_columns
294
+
295
+ def column(name, type, options = {})
296
+ unless (@base.geometry_data_types[type.to_sym].nil? or
297
+ (options[:create_using_addgeometrycolumn] == false))
298
+
299
+ geom_column = PostgreSQLColumnDefinition.new(@base,name, type)
300
+ geom_column.null = options[:null]
301
+ geom_column.srid = options[:srid] || -1
302
+ geom_column.with_z = options[:with_z] || false
303
+ geom_column.with_m = options[:with_m] || false
304
+
305
+ @geom_columns = [] if @geom_columns.nil?
306
+ @geom_columns << geom_column
307
+ else
308
+ super(name,type,options)
309
+ end
310
+ end
311
+
312
+ SpatialAdapter.geometry_data_types.keys.each do |column_type|
313
+ class_eval <<-EOV
314
+ def #{column_type}(*args)
315
+ options = args.extract_options!
316
+ column_names = args
317
+
318
+ column_names.each { |name| column(name, '#{column_type}', options) }
319
+ end
320
+ EOV
321
+ end
322
+ end
323
+
324
+ class PostgreSQLColumnDefinition < ColumnDefinition
325
+ attr_accessor :srid, :with_z,:with_m
326
+ attr_reader :spatial
327
+
328
+ def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil,null=nil,srid=-1,with_z=false,with_m=false)
329
+ super(base, name, type, limit, default,null)
330
+ @spatial=true
331
+ @srid=srid
332
+ @with_z=with_z
333
+ @with_m=with_m
334
+ end
335
+
336
+ def to_sql(table_name)
337
+ if @spatial
338
+ type_sql = type_to_sql(type.to_sym)
339
+ type_sql += "M" if with_m and !with_z
340
+ if with_m and with_z
341
+ dimension = 4
342
+ elsif with_m or with_z
343
+ dimension = 3
344
+ else
345
+ dimension = 2
346
+ end
347
+
348
+ column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})"
349
+ column_sql += ";ALTER TABLE #{table_name} ALTER #{name} SET NOT NULL" if null == false
350
+ column_sql
351
+ else
352
+ super
353
+ end
354
+ end
355
+
356
+
357
+ private
358
+ def type_to_sql(name, limit=nil)
359
+ base.type_to_sql(name, limit) rescue name
360
+ end
361
+
362
+ end
363
+
364
+ end
365
+ end
366
+
367
+ #Would prefer creation of a PostgreSQLColumn type instead but I would
368
+ # need to reimplement methods where Column objects are instantiated so
369
+ # I leave it like this
370
+ module ActiveRecord
371
+ module ConnectionAdapters
372
+ class SpatialPostgreSQLColumn < Column
373
+
374
+ include SpatialColumn
375
+
376
+ #Transforms a string to a geometry. PostGIS returns a HewEWKB string.
377
+ def self.string_to_geometry(string)
378
+ return string unless string.is_a?(String)
379
+ GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(string) rescue nil
380
+ end
381
+
382
+ def self.create_simplified(name,default,null = true)
383
+ new(name,default,"geometry",null,nil,nil,nil)
384
+ end
385
+
386
+ end
387
+ end
388
+ end
@@ -0,0 +1,39 @@
1
+ # #
2
+ # PostGIS Adapter
3
+ #
4
+ #
5
+ # http://github.com/nofxx/postgis_adapter
6
+ #
7
+ module PostgisFunctions
8
+ def self.included(base)
9
+ base.send :extend, ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ # acts_as_geom :geom
15
+ def acts_as_geom(*columns)
16
+ cattr_accessor :postgis_geoms
17
+
18
+ geoms = columns.map do |g|
19
+ geom_type = get_geom_type(g)
20
+ case geom_type
21
+ when :point
22
+ send :include, PointFunctions
23
+ when :polygon
24
+ send :include, PolygonFunctions
25
+ when :line_string
26
+ send :include, LineStringFunctions
27
+ end
28
+ {g => geom_type}
29
+ end
30
+ self.postgis_geoms = {:geoms => geoms}#, :opts => options}
31
+ end
32
+
33
+ def get_geom_type(column)
34
+ self.columns.select { |c| c.name == column.to_s}.first.geometry_type
35
+ end
36
+ end
37
+ end
38
+
39
+ ActiveRecord::Base.send :include, PostgisFunctions
@@ -0,0 +1,179 @@
1
+ # #
2
+ # PostGIS Adapter
3
+ #
4
+ # Common Spatial Adapter for ActiveRecord
5
+ #
6
+ #
7
+ #Addition of a flag indicating if the index is spatial
8
+ ActiveRecord::ConnectionAdapters::IndexDefinition.class_eval do
9
+ attr_accessor :spatial
10
+
11
+ def initialize(table, name, unique, spatial,columns)
12
+ super(table,name,unique,columns)
13
+ @spatial = spatial
14
+ end
15
+
16
+ end
17
+
18
+ ActiveRecord::SchemaDumper.class_eval do
19
+ def table(table, stream)
20
+
21
+ columns = @connection.columns(table)
22
+ begin
23
+ tbl = StringIO.new
24
+
25
+ if @connection.respond_to?(:pk_and_sequence_for)
26
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
27
+ end
28
+ pk ||= 'id'
29
+
30
+ tbl.print " create_table #{table.inspect}"
31
+ if columns.detect { |c| c.name == pk }
32
+ if pk != 'id'
33
+ tbl.print %Q(, :primary_key => "#{pk}")
34
+ end
35
+ else
36
+ tbl.print ", :id => false"
37
+ end
38
+
39
+ if @connection.respond_to?(:options_for)
40
+ res = @connection.options_for(table)
41
+ tbl.print ", :options=>'#{res}'" if res
42
+ end
43
+
44
+ tbl.print ", :force => true"
45
+ tbl.puts " do |t|"
46
+
47
+ columns.each do |column|
48
+
49
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
50
+ next if column.name == pk
51
+ #need to use less_simplified_type here or have each specific geometry type be simplified to a specific simplified type in Column and each one treated separately in the Column methods
52
+ if column.is_a?(SpatialColumn)
53
+ tbl.print " t.column #{column.name.inspect}, #{column.geometry_type.inspect}"
54
+ tbl.print ", :srid => #{column.srid.inspect}" if column.srid != -1
55
+ tbl.print ", :with_z => #{column.with_z.inspect}" if column.with_z
56
+ tbl.print ", :with_m => #{column.with_m.inspect}" if column.with_m
57
+ else
58
+ tbl.print " t.column #{column.name.inspect}, #{column.type.inspect}"
59
+ end
60
+ tbl.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit]
61
+ tbl.print ", :default => #{column.default.inspect}" if !column.default.nil?
62
+ tbl.print ", :null => false" if !column.null
63
+ tbl.puts
64
+ end
65
+
66
+ tbl.puts " end"
67
+ tbl.puts
68
+ indexes(table, tbl)
69
+ tbl.rewind
70
+ stream.print tbl.read
71
+ rescue => e
72
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
73
+ stream.puts "# #{e.message} #{e.backtrace}"
74
+ stream.puts
75
+ end
76
+
77
+ stream end
78
+
79
+ def indexes(table, stream)
80
+ indexes = @connection.indexes(table)
81
+ indexes.each do |index|
82
+ stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}"
83
+ stream.print ", :unique => true" if index.unique
84
+ stream.print ", :spatial=> true " if index.spatial
85
+ stream.puts
86
+ end
87
+
88
+ stream.puts unless indexes.empty?
89
+ end
90
+ end
91
+
92
+
93
+
94
+
95
+ module SpatialAdapter
96
+ #Translation of geometric data types
97
+ def geometry_data_types
98
+ {
99
+ :point => { :name => "POINT" },
100
+ :line_string => { :name => "LINESTRING" },
101
+ :polygon => { :name => "POLYGON" },
102
+ :geometry_collection => { :name => "GEOMETRYCOLLECTION" },
103
+ :multi_point => { :name => "MULTIPOINT" },
104
+ :multi_line_string => { :name => "MULTILINESTRING" },
105
+ :multi_polygon => { :name => "MULTIPOLYGON" },
106
+ :geometry => { :name => "GEOMETRY"}
107
+ }
108
+ end
109
+
110
+ end
111
+
112
+
113
+ #using a mixin instead of subclassing Column since each adapter can have a specific subclass of Column
114
+ module SpatialColumn
115
+ attr_reader :geometry_type, :srid, :with_z, :with_m
116
+
117
+ def initialize(name, default, sql_type = nil, null = true,srid=-1,with_z=false,with_m=false)
118
+ super(name,default,sql_type,null)
119
+ @geometry_type = geometry_simplified_type(@sql_type)
120
+ @srid = srid
121
+ @with_z = with_z
122
+ @with_m = with_m
123
+ end
124
+
125
+
126
+ #Redefines type_cast to add support for geometries
127
+ def type_cast(value)
128
+ return nil if value.nil?
129
+ case type
130
+ when :geometry then self.class.string_to_geometry(value)
131
+ else super
132
+ end
133
+ end
134
+
135
+ #Redefines type_cast_code to add support for geometries.
136
+ #
137
+ #WARNING : Since ActiveRecord keeps only the string values directly returned from the database, it translates from these to the correct types everytime an attribute is read (using the code returned by this method), which is probably ok for simple types, but might be less than efficient for geometries. Also you cannot modify the geometry object returned directly or your change will not be saved.
138
+ def type_cast_code(var_name)
139
+ case type
140
+ when :geometry then "#{self.class.name}.string_to_geometry(#{var_name})"
141
+ else super
142
+ end
143
+ end
144
+
145
+
146
+ #Redefines klass to add support for geometries
147
+ def klass
148
+ case type
149
+ when :geometry then GeoRuby::SimpleFeatures::Geometry
150
+ else super
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ #Redefines the simplified_type method to add behaviour for when a column is of type geometry
157
+ def simplified_type(field_type)
158
+ case field_type
159
+ when /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i then :geometry
160
+ else super
161
+ end
162
+ end
163
+
164
+ #less simlpified geometric type to be use in migrations
165
+ def geometry_simplified_type(field_type)
166
+ case field_type
167
+ when /^point$/i then :point
168
+ when /^linestring$/i then :line_string
169
+ when /^polygon$/i then :polygon
170
+ when /^geometry$/i then :geometry
171
+ when /multipoint/i then :multi_point
172
+ when /multilinestring/i then :multi_line_string
173
+ when /multipolygon/i then :multi_polygon
174
+ when /geometrycollection/i then :geometry_collection
175
+ end
176
+ end
177
+
178
+
179
+ end